refactor: replace hardcoded desktop counts with FWM_DESKTOPS macro - #13
refactor: replace hardcoded desktop counts with FWM_DESKTOPS macro#13YoussefZidan-1 wants to merge 1 commit into
Conversation
iluaii
left a comment
There was a problem hiding this comment.
The direction is right — FWM_DESKTOPS is already the convention in expo_*, config.c, server_output.c, server_drag.c, server_pointer.c, ipc.c and parts of physics.c, so the remaining literals are a genuine inconsistency and I do want them gone. I also want to note you did not touch server_actions.c:330 (action[10], a string index) or the tok[10]/key[10] in toml.c — a blind sed would have broken both, so thank you for reading rather than replacing.
But as it stands the patch does not achieve what the summary claims, and for the exact scenario it names it makes things worse. Three things.
1. The backing arrays are still [10] — this one is blocking
src/server.h:503 BspNode *bsp_roots[10];
src/server.h:504 int desktop_mode[10];
src/ui/tray.h:35 int desktop_window_counts[10];
src/ui/tray.h:73 int sig_counts[10];Every loop bound this PR changes indexes one of exactly those arrays:
| changed line | indexes | declared |
|---|---|---|
view.c:635 |
bsp_roots[i], desktop_mode[i] |
[10] |
server_actions.c:525,545,564 |
desktop_mode[d] |
[10] |
tray.c:253 |
desktop_window_counts[i] |
[10] |
Set FWM_DESKTOPS to 12 — the example from your summary — and the loops now run to 12 while the arrays stay at 10. Today the literal 10 in the loop and the [10] in the declaration agree with each other, so changing the macro is merely ignored. After this patch it is an out-of-bounds write. The patch turns "the macro does nothing" into "the macro corrupts memory", which is the opposite of the stated goal.
physics.h:139 (PhysicsProfile desktops[FWM_DESKTOPS]) and config.h:133 already do this correctly — the array declarations need to come first, then the loops that walk them.
2. The sweep is about a third done
Still hardcoded after this PR, roughly twenty sites across twelve files:
server_tiling.c:138 ipc.c:270 group.c:180 foreign.c:31
session.c:178,269 server.c:340,349,423,426 server_config.c:409
server_tick.c:232 server_output.c:1247 server_seat.c:261
config.c:960,1071 server_actions.c:679,748 tray.c:69
A partial sweep is in some ways worse than none: the touched files start to look audited, and whoever comes next will believe changing the macro is safe. If we do this, let's finish it.
3. Please keep the whitespace out of the diff
About twelve of the hunks in view.c are trailing-whitespace removal unrelated to desktops. The real change in that file is a single line (635). It reads as 29/29 when it is closer to 1/1, which makes it much harder to review. Happy to take the whitespace cleanup as its own PR if it bothers you — it bothers me too.
Minor
tray.c does not include defines.h; the macro only arrives transitively through ../theme.h -> config.h -> defines.h. It builds, but an explicit include would be honest about the dependency.
The order matters here: item 1 first, then item 2. With the array declarations converted and the sweep finished this is a clear improvement and I will merge it.
d70b94c to
29bd8ab
Compare
|
Hi @iluaii, Thanks for the detailed code review! You made a crucial point about the backing array declarations—updating the loop bounds without scaling the array definitions was definitely a memory corruption hazard. I've updated the PR to address all items:
Branch rebased against |
iluaii
left a comment
There was a problem hiding this comment.
This is a real improvement over the first version, and the two blocking items are properly done. The array declarations came first this time — src/server.h:503-504 and src/ui/tray.h:36,74 are all [FWM_DESKTOPS] now, so the loops and the memory they walk finally agree. The sweep is complete: I grepped the whole of src/ and there is not one desktop literal left. The view.c diff is down to the single line it always was, and defines.h is included explicitly. Every substitution is right, including the awkward ones — d = 9 → d = FWM_DESKTOPS - 1 and active_pos > 9.0 → > FWM_DESKTOPS - 1.0.
So I built it at three values of the macro. 10 and 12 are clean: compiles, and all 10 ctest targets pass. At 5 it compiles and two tests fail — and the tests are what is wrong, not the code.
The sweep stopped at tests/
tests/test_physics.c:77
CHECK_INT(b->desktop_id, 9);At FWM_DESKTOPS = 5 the body comes out at desktop 4, which is the correct answer — the far end of a five-desktop strip. The test asserts the old boundary, so the code is right and the test says it is wrong.
tests/test_config.c:425,443
The fixture writes desktops = [7], then reads it back:
const PhysicsProfileConfig *water = &cfg.physics.profiles[cfg.physics.desktop_profile[7]];desktop_profile is [FWM_DESKTOPS] (src/config.h:133) — you converted that one yourself. At 5 desktops, desktops = [7] is out of range so the loader rejects it (2 errors where the test wants 0), and the read at index 7 goes past the end of the array into whatever field follows it in FwmConfig. ASan does not catch it, since intra-object overflow is not something it can see for a C struct, so it just silently returns a neighbouring field and the profile lookup lands on the wrong entry.
This matters more than the count of failing assertions. The whole claim of this PR is "the macro can now be changed", and tests/ is where that claim is supposed to be checked. As it stands, someone who sets FWM_DESKTOPS = 5 and runs the suite is told the change broke the compositor, when what broke is the assertion. Same argument as last time, one directory over: a partial sweep leaves things looking audited that are not.
Both fixes are small — FWM_DESKTOPS - 1 in the physics check, and a fixture that picks its desktop indices from the macro instead of naming 3, 4 and 7.
Minor
src/config.c now has both spellings in the same function, a line apart:
if (desk.u.i < 0 || desk.u.i >= FWM_DESKTOPS)
if (desk.u.i < 0 || desk.u.i > FWM_DESKTOPS - 1)Same bound, two ways of saying it. Pick >= FWM_DESKTOPS.
Fix the tests and this goes in. The src/ half is genuinely done, and thank you for converting the declarations first — that ordering was the whole point.
29bd8ab to
d1a4ada
Compare
|
Hi @iluaii, Thanks for testing the unit test suite at different I've updated the PR:
|
iluaii
left a comment
There was a problem hiding this comment.
Closer, and the src/ side is still good — the style fix at config.c:1071 is in and the sweep there holds up. But the suite still does not pass at an arbitrary desktop count, and one of the values it fails at is 12, the example your own PR description opens with.
I built the branch and ran the whole suite at six values of the macro:
FWM_DESKTOPS = 10 10/10 pass
FWM_DESKTOPS = 12 9/10 physics: ring, leftward
FWM_DESKTOPS = 5 10/10 pass
FWM_DESKTOPS = 4 10/10 pass
FWM_DESKTOPS = 3 9/10 config: profiles claim desktops
FWM_DESKTOPS = 2 9/10 config: profiles claim desktops
Three separate things behind that.
1. tests/test_physics.c:29 — the sweep missed one
#define SPAN (10 * SW)This is the width of the world the ring tests measure against, and it is still a literal 10. At FWM_DESKTOPS = 12 the world is twelve screens wide while SPAN says ten, so CHECK(b->x > SPAN - 2 * SW) on line 76 is comparing against a threshold from a different world — it passes by being too low, not by being right.
2. tests/test_physics.c:77 — FWM_DESKTOPS - 1 is not what that test measures
FWM_DESKTOPS = 12 -> b->desktop_id: got 9, want 11
The body is thrown at -4000 px/s and run for 60 steps; where it ends up is decided by that speed and by friction, not by how many desktops exist. It wraps past the left edge, comes out at the right, and keeps going until it has spent its throw. At 10 desktops that happens to land it on the last one, which is why the hardcoded 9 passed. Substituting FWM_DESKTOPS - 1 promotes that coincidence to a rule, and at 12 the rule is wrong — the body is two desktops short of the end and the test now claims the compositor is broken.
What the test actually wants to say is "it came out at the far end and is still flying", which lines 76 and 78 already say. Fixing SPAN to be FWM_DESKTOPS * SW makes line 76 mean that again at any count; line 77 should then either go, or assert something derived from the same arithmetic rather than from the desktop count.
3. tests/test_config.c:427 — the fixture breaks at small counts
Indices FWM_DESKTOPS - 3, - 2, - 1 work down to 4 and then come apart:
- At 3 they are 0, 1, 2. Line 438 asserts
desktop_profile[0] == -1, "unclaimed desktops stay on the world's values" — but desktop 0 is now claimed by the fixture, so the test contradicts itself. - At 2 the first index is -1. The config gets
desktops = [-1], the loader rejects it, and line 439 readscfg.physics.desktop_profile[-1]— an out-of-bounds read in front of the array.
Two ways out, and I do not mind which:
- Declare a floor.
_Static_assert(FWM_DESKTOPS >= 4, "...")indefines.hsays out loud what the code assumes, and then the fixture is correct by construction. Given the tray draws a marker per desktop and the config binds them to digits, a floor is defensible. - Or make the fixture pick indices that are valid at any count ≥ 2 and keep an unclaimed one for line 438 to check.
On the shape of this
Three rounds in, each round has fixed what I pointed at and introduced the next instance of the same thing: a literal replaced without checking what it meant. 9 in a physics assertion was never the last desktop, it was where a throw ran out; 10 in SPAN is a world width. The macro is only meaningfully "changeable" once every 10 and 9 has been read for which of those it is.
The good news is the remaining set is small and now fully known — SPAN, line 77, and the fixture floor. Fix those and the suite passes everywhere it should, and I will merge.
d1a4ada to
c2a8555
Compare
|
Hi @iluaii, Thanks for running the test suite across multiple macro values! You were right about I've updated the PR to finalize
|
iluaii
left a comment
There was a problem hiding this comment.
Everything I asked for is here and correct: SPAN is FWM_DESKTOPS * SW, line 77 is gone and lines 76/78 carry the test on their own, the fixture leaves desktop 0 unclaimed, and _Static_assert(FWM_DESKTOPS >= 4, ...) states the floor out loud — a build at 3 is now refused rather than mysteriously failing.
And fixing SPAN has earned its keep, because it turned up something neither of us was looking at. Ran the suite again:
FWM_DESKTOPS = 10 10/10
FWM_DESKTOPS = 12 9/10 physics: ring, leftward + ring, rightward
FWM_DESKTOPS = 8 9/10
FWM_DESKTOPS = 5 9/10
FWM_DESKTOPS = 4 9/10
FWM_DESKTOPS = 3 build refused by the static_assert, as intended
This time the tests are right and src/ is wrong.
The world is ten screens wide no matter what the macro says
src/physics.c:605 double W = 10.0 * screen_w; // full virtual-desktop span
src/physics.c:1172 double W = 10.0 * screen_width, H = (double)screen_height;
src/server_tick.c:921 double max_x = 10.0 * server->screen_width - b->width;
Line 605 builds the walls; 1172 is the wrap transport, the thing that decides where a window thrown off one end reappears; 921 is the escape net. All three say ten screens, and none of them has ever asked FWM_DESKTOPS.
So at FWM_DESKTOPS = 12, desktops 10 and 11 sit outside the right-hand wall — there is no reachable space at those coordinates, and a window can never arrive there. At 4, six screens of world belong to no desktop at all, and anything that flies into them is clamped to desktop_id = 3. Your patch made the indices agree with each other; the geography they index into stayed at ten.
That is why the ring tests fail now and passed before: with SPAN hardcoded to 10 * SW they were measuring the same wrong world the code was, and the two errors cancelled. Correcting one of them made the disagreement visible. This is a good outcome for the change, not an argument against it.
I checked what it would take
Substituting FWM_DESKTOPS at those three sites, plus tests/test_physics_speed.c:31 (#define WORLD (10.0 * SW) — the same literal, one more copy):
FWM_DESKTOPS = 12 10/10
FWM_DESKTOPS = 8 10/10
FWM_DESKTOPS = 10 10/10
FWM_DESKTOPS = 5 9/10 test_physics_speed.c:320
FWM_DESKTOPS = 4 9/10 test_physics_speed.c:320
The last one is CHECK(b->x < WORLD / 2.0) — "and did not reappear at the far end". Half the world is a fine proxy for "the far end" when the world is ten screens; in a four-screen world it stops meaning that. Rephrasing it is a judgement call about what the test intends, not a substitution.
What I would like to do with this PR
Not hold it hostage to that. What you have is correct and worth having on its own: every desktop index and every array that indexes into one now derive from a single constant, and the out-of-bounds hazard I flagged in the first round is gone. It merges cleanly and the suite is green at the shipped value.
What it is not is "FWM_DESKTOPS is now configurable" — that needs the world to be as wide as the macro says, which is a change to the physical model rather than a refactor, and I would rather that arrived as its own PR with its own reasoning about what a four-desktop world should feel like.
So: could you retitle to something like "derive every desktop index from FWM_DESKTOPS" and rewrite the description to claim that and only that — indices and arrays agree, floor asserted, tests no longer hardcode the count — with a note that the world width is still fixed at ten screens and is tracked separately? I will open the issue for the width and credit this PR for turning it up.
Then I will merge it.
Three rounds of me sending this back and each round found something real. Thanks for staying with it.
|
Opened #15 for the world width, with the three sites, the failure modes at 12 and 4, and the measurements above. Credited this PR for turning it up. |
Summary
In
src/defines.h, the number of virtual desktops is defined via macro as#define FWM_DESKTOPS 10. However, across several core files (server_desktop.c,server_actions.c,physics.c,view.c, andtray.c), the literal numbers10and9were hardcoded in boundary checks and loop bounds.If
FWM_DESKTOPSis ever modified for custom builds (e.g., set to 5 or 12), these hardcoded bounds cause out-of-bounds array access and compositor segmentation faults.The Fix
Replaced all instances of hardcoded
10and9desktop boundary checks and loop bounds withFWM_DESKTOPSand(FWM_DESKTOPS - 1).Testing
./dev.sh.