Repository navigation
Vulkan: cheaper write-back bookkeeping (report words, write-back pages) - #1036
Conversation
|
Thanks for the careful review. Three commits address it: cd70a0a, bca9997 and 6f66a61; details inline. I re-measured on the same setup (Ryzen 9 3900X, Kokiri Forest, standing still, two runs): 35.8 / 36.0 fps, against 35.7 / 35.6 for the previous version and 34.7 / 35.1 for main. |
…list without moving all of it each time cpu_words (the words the replay writes on the CPU, cut out of mirror write-backs) took every write-back's new words with an inplace_merge over the whole sorted vector, up to 65536 words, and a unique over it again: at each write-back, so at every recording split (~20 a frame). Words already kept are now left out first, and the new ones go into a second, small sorted set that goes into the big one only once it is an eighth of its size (at least 4096); the cut walks both in order. The same words, the same 65536 limit. ~2.4-4% of the replay thread on a Ryzen 9 3900X in Kokiri (HOS_SAMPLE). Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…map entry a page wb_pages (each guest page a mirror write-back covers, with the timeline value a CPU read or a capture waits for) took one unordered_map insert for every page of every write-back, at every recording split: ~1.9% of the replay thread on a Ryzen 9 3900X in Kokiri (HOS_SAMPLE). Now a map of 2 MB regions with a page array each, the last region kept for the next page: one lookup for 512 pages. The same values (0: none; 1: written back long ago, as the pruning sets them), the same pruning at 200000 pages. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… runs against both The fixture takes sorted_merge too and declares cpu_words_recent; check() can start with words kept from an earlier merge (in the big set) beside the new ones, so the cut walks both sets: interleaved, a new word already kept, all of them kept. With the cut's walk over the recent set taken out, the fixture fails. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the approval. I took all five nits:
|
…erations, and no pruning As the review of NotRexed#1036 suggests: the table follows the page generations' layout in this file (gen_chunks: a vector of kChunkPages chunks over the guest's pages, each made when one of its pages is first written back), in place of a hash map of 2 MB regions and its last-region cache. No hash, no constants of its own; write-backs are of imported pools only, so few chunks are ever made (the GPU windows' 12 GB are 48 of them at most). The pruning is gone. Once the table held 200000 pages (it never shrinks) it ran at almost every write-back, a walk over the whole table under wb_pages_mu. It never changed an answer: it set values the GPU was past to 1, and both readers (memory_writeback_value's callers and the capture's wait) only compare a value with the GPU's progress, so a value it is past reads the same as 1: written back, nothing to wait for. Ryzen 9 3900X, Kokiri Forest, standing still, two runs: 35.8 / 36.0 fps (the hash-of-regions version 35.7 / 35.6, main 34.7 / 35.1). Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e new The earlier comment described the single sorted vector this replaced; it is folded into the new one (as the review of NotRexed#1036 points out), keeping its history. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the 65536 clear As the review of NotRexed#1036 asks: after every cut the two sets must be sorted, without repeats, with no word in both, nothing left noted, and together exactly the words given (a new word already kept used to pass with the de-duplication removed: the cut hid it). check_promotion crosses both thresholds (4096 and an eighth of the big set); check_clear goes through memory_cpu_owned past 65536 words with the recent set left recent, so both must be forgotten. Each fails with its rule taken out of vk_memory.cpp: no de-duplication, no promotion, only the big set cleared. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…range reads skip chunks never made As the approving review of NotRexed#1036 suggests: the comment described the table by the map and pruning it replaced, and spoke of two readers where there are three (memory_writeback_value's "ever written back" and its value against the GPU's progress, and the capture's wait); it now says what the table holds and why values are never lowered. memory_writeback_value reads its range through WbPages::max_in, which steps over a chunk never made whole instead of looking at each of its pages. tests/test_wb_pages.py builds WbPages from the source with small chunks: values per page, ranges over chunks never made, both edges of a chunk, the table's end, the "ever" flag. A range read that stops short of a chunk's or the range's last page fails it (by its timeout, as it then never moves on). Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cpu_words_merge always leaves cpu_words_new empty, so the cpu_words_new clear right after it never ran (on main either), as the review of NotRexed#1036 points out. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent set, and ignores RX_VK_WB_REPORTS As the review of NotRexed#1036 suggests: check_repeats merges a batch with a word in it twice, then a word that is in the recent set already; with either the unique or the recent set's binary_search taken out of cpu_words_merge the fixture fails. The fixture runs without RX_VK_WB_REPORTS from the caller's environment, which would turn memory_cpu_owned off and fail check_clear. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main took narrower versions of the same two changes meanwhile: report words already kept left out of the merge (4ca115a) and the write-back page map hashed by the page number (da44bd4). This branch's versions are kept, as they include the first and replace the map the second sped up: the recent set holds main's filter, and the chunks need no hash (Common::FastHash's include goes with the map). The capture's wait takes main's late list (ab68f35) with the chunks' lookup. tests/test_report_words.py keeps both tests: main's randomized merge checks both sets together now, over 800 rounds of 16384 addresses, so the recent set is promoted on the way. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Main is merged in. Meanwhile main took narrower versions of both changes: 4ca115a leaves report words already kept out of the merge, and da44bd4 hashes the write-back page map by page number. I kept this branch's versions, since the recent set already includes 4ca115a's filter and the chunks need no hash at all. The capture's wait takes main's new late-page list (ab68f35) with the chunks' lookup. Re-measured on main 8b2e1d3 (v2.0.8-rc.1), Ryzen 9 3900X, Kokiri Forest, two runs each in two rounds: 38.6 / 37.8 fps standing against main's 36.8 / 36.3 (+4.5%, replay 21.3 against 22.5 ms a frame), and 35.5 / 34.1 panning against 33.9 / 34.9 (+1.2%). |
main's write-back diff (55036ef) has memory_cpu_owned tell the shadows its pages (shadow_written, kPageShift): the report-word test's fixture, which builds memory_cpu_owned for check_clear, stubs both. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two pieces of bookkeeping in
vk_memory.cppthat run at every write-back, and so at every recording split (~20 a frame in Kokiri Forest), cost ~4-6% of the replay thread in the sampling profiler (HOS_SAMPLE=Presentation). Both now keep exactly the same contents and answers, at less cost:cpu_words(the words the replay writes on the CPU, cut out of mirror write-backs). Each merge appended every new word to the sorted vector, raninplace_mergeover all of it (up to 65536 words), then de-duplicated the whole thing again. Report words arrive at new addresses all the time, so this ran in full at most write-backs. Now words already kept are skipped first, and new ones go into a second, small sorted set. That set is merged into the big one only once it reaches an eighth of the big one's size (at least 4096 words), and the write-back cut walks both sets in order. Same words, same 65536 limit.wb_pages(each guest page a write-back covers, with the timeline value a CPU read or a capture waits for) was anunordered_mapwith one insert per page of every write-back. It's now a map of 2 MB regions, each with a 512-entry page array, and the last region is cached for the next page: one lookup per 512 pages instead of a hash insert per page. Same values (0: none; 1: written back long ago, which the pruning sets), same pruning at 200000 pages.Measured on a Ryzen 9 3900X with an RTX 3080 on Windows 11. Kokiri Forest, standing still after loading a save, headless, input timed in game frames (
HOS_INPUT_FRAME_CLOCK=1), live numbers over presents 3120-5520:How much this saves depends on the scene: on how much GPU-written memory goes back each frame, and how many counter reports the game makes. I expect more where GPU-driven effects and occlusion queries are heavier, and little in simple interiors.
The numbers were measured on v2.0.5 (232eecb). The branch is rebased onto main 3b4347d; the rebase did not change the patches (
git range-diff), andvk_memory.cpphas no upstream changes since.python -I -B tests/run_all.py: 104 passed, 3 failed, 13 skipped on Windows. The three failures (test_changelog.py,test_fps_overlay_order.py,test_precompile_compute_guard.py) fail the same way on main 3b4347d on this machine. This PR also updatestests/test_report_words.py, which compilescpu_words_mergeandcut_cpu_wordsout of the source into a fixture: it now takessorted_mergeand the second set, and adds cases where kept and new words are cut together. With the cut's walk over the recent set removed, the fixture fails.🤖 Generated with Claude Code