WIP: perf(host): the write marks' collection steps over 256 clear marks at a time #63

Draft
HumanBean wants to merge 2 commits from HumanBean/Longshot:perf/collect-wide into main
Owner

Why

Every submit collects the pages the game wrote since the last one, on the thread that replays the frame. The walk over the write marks was most of that cost, and almost every mark it looks at is clear. This takes about 0.12-0.24 ms a frame of CPU time off the replay thread in Kokiri Forest; no frame-rate gain is claimed.

What changed

  • host: collect_in (host/rx_backend.c) walks one mark byte per 4 KB page over every range the GPU can see. It stepped 32 marks at a time while none was set. It now tests 256 marks at once first (32 words; the compiler makes four AVX2 loads of them), and falls back to the 32 and 8 mark steps where something is marked.
  • The pages found, their order and the marks cleared are the same.
  • HOS_COLLECT_WIDE=0 turns the wide step off (for A/B).
  • tests: new tests/test_collect_wide.py compiles collect_in from the source and compares it with a plain walk over random marks and ranges (aligned or not, short, empty, past the guest's end, past the array's end), with the switch on and off, and checks that the wide step is really taken.

Flags and settings

Flag New or changed What it does Default now Goal: on by default? Helps on Doesn't help on Untested on Untested, but should help on
HOS_COLLECT_WIDE New 0 turns the 256-mark step off, leaving the 32 and 8 mark steps as before (for A/B) On (the wide step is taken) Yes: the pages found, their order and the marks cleared are the same RTX 3090, Linux, Kokiri Forest: 0.02-0.04 ms a submit less in "writes collected" Frame rate in that scene: the frame is bounded by the GPU there Windows, AMD, Intel, a CPU without AVX2, other areas than Kokiri Forest -

The switch is not yet in the switch reference of docs/development.md.

Measurements

Baseline: this PR @ 20e0b2d9, HOS_COLLECT_WIDE=0
main was not the comparison: the switch turns the change off inside the same build, so the two sides differ in nothing else.

main: 15d4ae8c (2026-11-12)
this PR: perf/collect-wide @ 20e0b2d9
Hardware: RTX 3090, NVIDIA 620.70.06, Linux (CPU and RAM not recorded in this description)
Scene: Kokiri Forest, the pristine save, standing; 2048x1152 window, default options, 6 submits a frame
Method: four runs in the order A B B A (A is HOS_COLLECT_WIDE=0, B the default); the renderer's own line, vk: a submit's memory work (ms): ... writes collected, 600 submits a line, 8 lines a run
How to reproduce: start the game into Kokiri Forest on the pristine save with default options, stand still, and read the vk: a submit's memory work (ms) lines of the log, once with HOS_COLLECT_WIDE=0 and once without it

The timed build was on an older base than the final commit. The game that was timed was built with this change on main 15d4ae8c. The change then moved onto a newer main without conflict as 20e0b2d9, and the branch head dc3ae7ba merges the forge's main into it. No game was built or timed on the final tree; the new test compiles the function from the final source.

this PR @ 20e0b2d9, HOS_COLLECT_WIDE=0 this PR @ 20e0b2d9, default (wide step on)
writes collected, ms a submit, first run of each 0.06 (one line 0.08) 0.03 (one line 0.04)
writes collected, ms a submit, second run of each 0.07 (two lines 0.06) 0.04
fps of the two runs (says nothing either way: see below) 56.6 and 62.25 68.4 and 59.5

That is 0.02-0.04 ms a submit, about 0.12-0.24 ms a frame of CPU time on the replay thread.

No frame-rate gain is claimed. In this scene the frame is bounded by the GPU's time and the present's wait for it, not by this thread's CPU time, so the fps of the runs does not move outside its noise (the four runs, in the order run: 56.6, 68.4, 59.5 and 62.25 fps, on a PC that was swapping throughout; they say nothing either way).

Not measured: the 1% low, the median and p99 frame time, and a run of unchanged main beside the switch. The log lines themselves are not pasted here, only the figures read from them.

Testing

Tested:

  • tests/test_collect_wide.py (new), on Linux: the function compiled from the source against a plain walk, with the switch on and off.
  • python -I -B tests/run_all.py: 501 passed, 2 failed, 26 skipped on this PC (Linux, 4 at once), the same on the tree merged with the forge's main. The two that fail need tools this PC lacks (zstandard, libtk) and fail on unchanged main here too.
  • In the game: the four timed runs above, in Kokiri Forest on an RTX 3090 (NVIDIA 620.70.06, Linux), with the build made on the older main.

Not tested:

  • The game that was timed was built with this change on a slightly older main; the change moved onto the current base without conflict and its test compiles the function from the final source, but no full game build and play session was made on the final tree.
  • A CPU without AVX2: the test is the same loop of 32 words, only slower than four vector loads.
  • Other areas than Kokiri Forest.

Needs other testers:

  • Windows: the game starts and plays as before with the change (it is in shared host code; the fixture is plain C), expecting no difference on screen and a lower "writes collected" figure than with HOS_COLLECT_WIDE=0 (request to open)
  • AMD and Intel GPUs: the same check, expecting the same (request to open)

Screenshots

Nothing on screen.

Risk and rollback

The walk decides which written pages reach the GPU: a mistake in it would show as stale or missing data on screen. The test compares it with a plain walk over random marks and ranges. HOS_COLLECT_WIDE=0 turns the wide step off; reverting the commit is enough to remove it.

Reviewer notes

Read collect_in in host/rx_backend.c first: the 256-mark test is in front of the 32 and 8 mark steps, which are unchanged.

Issues

None.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main. (Not rebased: the forge's main was merged into the branch, and main has moved on since that merge.)
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). (Filled in; the test requests are still to open.)
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (Two interleaved runs each of the switch off and on, on the same PC, scene and settings; but the timed build was on an older main than the final commit, unchanged main was not run beside it, and no median, 1% low or frame time was taken.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags table filled in; README / docs/development.md switch reference updated. (The table is filled in; the switch reference is not updated yet.)
  • Screenshots for a change that shows on screen. (n/a)
  • A row in docs/tested-configurations.md for each measured run. (Not added.)
  • MKWVK_API_VERSION bumped (gpu/vk/vk_api.h's MkwVkHost or MkwVkRenderer changed). (n/a)
  • The SPIR-V headers regenerated with glslang (a shader changed). (n/a)
  • A game-free test added for the fix, failing on main and passing here. (n/a: not a fix. A game-free test of the new walk is added.)
  • A check under tests/ that reads changed source updated in this PR, not weakened. (n/a)
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Built on Linux only; Windows not built.)
  • Docs updated. (Not yet: see the switch reference above.)
## Why Every submit collects the pages the game wrote since the last one, on the thread that replays the frame. The walk over the write marks was most of that cost, and almost every mark it looks at is clear. This takes about 0.12-0.24 ms a frame of CPU time off the replay thread in Kokiri Forest; no frame-rate gain is claimed. ## What changed - host: `collect_in` (`host/rx_backend.c`) walks one mark byte per 4 KB page over every range the GPU can see. It stepped 32 marks at a time while none was set. It now tests 256 marks at once first (32 words; the compiler makes four AVX2 loads of them), and falls back to the 32 and 8 mark steps where something is marked. - The pages found, their order and the marks cleared are the same. - `HOS_COLLECT_WIDE=0` turns the wide step off (for A/B). - tests: new `tests/test_collect_wide.py` compiles `collect_in` from the source and compares it with a plain walk over random marks and ranges (aligned or not, short, empty, past the guest's end, past the array's end), with the switch on and off, and checks that the wide step is really taken. ## Flags and settings | Flag | New or changed | What it does | Default now | Goal: on by default? | Helps on | Doesn't help on | Untested on | Untested, but should help on | |---|---|---|---|---|---|---|---|---| | `HOS_COLLECT_WIDE` | New | `0` turns the 256-mark step off, leaving the 32 and 8 mark steps as before (for A/B) | On (the wide step is taken) | Yes: the pages found, their order and the marks cleared are the same | RTX 3090, Linux, Kokiri Forest: 0.02-0.04 ms a submit less in "writes collected" | Frame rate in that scene: the frame is bounded by the GPU there | Windows, AMD, Intel, a CPU without AVX2, other areas than Kokiri Forest | - | The switch is not yet in the switch reference of `docs/development.md`. ## Measurements Baseline: this PR @ 20e0b2d9, HOS_COLLECT_WIDE=0 main was not the comparison: the switch turns the change off inside the same build, so the two sides differ in nothing else. main: 15d4ae8c (2026-11-12) this PR: perf/collect-wide @ 20e0b2d9 Hardware: RTX 3090, NVIDIA 620.70.06, Linux (CPU and RAM not recorded in this description) Scene: Kokiri Forest, the pristine save, standing; 2048x1152 window, default options, 6 submits a frame Method: four runs in the order A B B A (A is `HOS_COLLECT_WIDE=0`, B the default); the renderer's own line, `vk: a submit's memory work (ms): ... writes collected`, 600 submits a line, 8 lines a run How to reproduce: start the game into Kokiri Forest on the pristine save with default options, stand still, and read the `vk: a submit's memory work (ms)` lines of the log, once with `HOS_COLLECT_WIDE=0` and once without it **The timed build was on an older base than the final commit.** The game that was timed was built with this change on main 15d4ae8c. The change then moved onto a newer main without conflict as 20e0b2d9, and the branch head dc3ae7ba merges the forge's main into it. No game was built or timed on the final tree; the new test compiles the function from the final source. | | this PR @ 20e0b2d9, HOS_COLLECT_WIDE=0 | this PR @ 20e0b2d9, default (wide step on) | |---|---|---| | writes collected, ms a submit, first run of each | 0.06 (one line 0.08) | 0.03 (one line 0.04) | | writes collected, ms a submit, second run of each | 0.07 (two lines 0.06) | 0.04 | | fps of the two runs (says nothing either way: see below) | 56.6 and 62.25 | 68.4 and 59.5 | That is 0.02-0.04 ms a submit, about 0.12-0.24 ms a frame of CPU time on the replay thread. **No frame-rate gain is claimed.** In this scene the frame is bounded by the GPU's time and the present's wait for it, not by this thread's CPU time, so the fps of the runs does not move outside its noise (the four runs, in the order run: 56.6, 68.4, 59.5 and 62.25 fps, on a PC that was swapping throughout; they say nothing either way). Not measured: the 1% low, the median and p99 frame time, and a run of unchanged main beside the switch. The log lines themselves are not pasted here, only the figures read from them. ## Testing **Tested:** - `tests/test_collect_wide.py` (new), on Linux: the function compiled from the source against a plain walk, with the switch on and off. - `python -I -B tests/run_all.py`: 501 passed, 2 failed, 26 skipped on this PC (Linux, 4 at once), the same on the tree merged with the forge's main. The two that fail need tools this PC lacks (`zstandard`, `libtk`) and fail on unchanged main here too. - In the game: the four timed runs above, in Kokiri Forest on an RTX 3090 (NVIDIA 620.70.06, Linux), with the build made on the older main. **Not tested:** - The game that was timed was built with this change on a slightly older main; the change moved onto the current base without conflict and its test compiles the function from the final source, but no full game build and play session was made on the final tree. - A CPU without AVX2: the test is the same loop of 32 words, only slower than four vector loads. - Other areas than Kokiri Forest. **Needs other testers:** - Windows: the game starts and plays as before with the change (it is in shared host code; the fixture is plain C), expecting no difference on screen and a lower "writes collected" figure than with `HOS_COLLECT_WIDE=0` (request to open) - AMD and Intel GPUs: the same check, expecting the same (request to open) ## Screenshots Nothing on screen. ## Risk and rollback The walk decides which written pages reach the GPU: a mistake in it would show as stale or missing data on screen. The test compares it with a plain walk over random marks and ranges. `HOS_COLLECT_WIDE=0` turns the wide step off; reverting the commit is enough to remove it. ## Reviewer notes Read `collect_in` in `host/rx_backend.c` first: the 256-mark test is in front of the 32 and 8 mark steps, which are unchanged. ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [ ] Rebased on current `main`. (Not rebased: the forge's main was merged into the branch, and main has moved on since that merge.) - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [ ] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). (Filled in; the test requests are still to open.) - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (Two interleaved runs each of the switch off and on, on the same PC, scene and settings; but the timed build was on an older main than the final commit, unchanged main was not run beside it, and no median, 1% low or frame time was taken.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [ ] Flags table filled in; README / docs/development.md switch reference updated. (The table is filled in; the switch reference is not updated yet.) - [ ] Screenshots for a change that shows on screen. (n/a) - [ ] A row in `docs/tested-configurations.md` for each measured run. (Not added.) - [ ] `MKWVK_API_VERSION` bumped (`gpu/vk/vk_api.h`'s `MkwVkHost` or `MkwVkRenderer` changed). (n/a) - [ ] The SPIR-V headers regenerated with glslang (a shader changed). (n/a) - [ ] A game-free test added for the fix, failing on `main` and passing here. (n/a: not a fix. A game-free test of the new walk is added.) - [ ] A check under `tests/` that reads changed source updated in this PR, not weakened. (n/a) - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Built on Linux only; Windows not built.) - [ ] Docs updated. (Not yet: see the switch reference above.)
Every submit collects the pages the game wrote since the last one: a walk over one mark byte per 4 KB page of
every range the GPU can see, 32 marks a step while none is set. Almost all of them are clear at any submit, so
the walk itself was the cost: 0.06 ms a submit, six submits a frame, on the thread that replays the frame.

collect_in now tests 256 marks at once (32 words, which the compiler turns into four AVX2 loads) before it falls
back to the 32 and 8 mark steps. The pages found, their order and the marks cleared are the same.
HOS_COLLECT_WIDE=0 turns the wide step off.

Kokiri Forest, RTX 3090, Linux: "writes collected" 0.06 -> 0.03 ms a submit in the log's memory work line.

tests/test_collect_wide.py compiles collect_in from the source and compares it with a plain walk over random
marks and ranges, with the switch on and off.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Merge Longshot main into the write marks' collection change
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 2s
dc3ae7ba30
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
Owner

The description is now in the template's sections. No code changed and no new numbers: the same facts as before, with the commits the measurements were taken on named, and the checklist ticked only where true. Three For everyone boxes are not true yet (main has moved since the branch merged it; the test requests are not open; the timed build was on an older main, with no median), so this goes back to work in progress.

The description is now in the template's sections. No code changed and no new numbers: the same facts as before, with the commits the measurements were taken on named, and the checklist ticked only where true. Three For everyone boxes are not true yet (main has moved since the branch merged it; the test requests are not open; the timed build was on an older main, with no median), so this goes back to work in progress.
HumanBean changed title from perf(host): the write marks' collection steps over 256 clear marks at a time to WIP: perf(host): the write marks' collection steps over 256 clear marks at a time 2026-11-13 02:36:50 +00:00
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 2s
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'perf/collect-wide:HumanBean-perf/collect-wide'
git switch 'HumanBean-perf/collect-wide'
Sign in to join this conversation.
No description provided.