Repository navigation
perf(vk): a readback waits for the part that writes its range back, and no game thread waits behind a present for nothing - #2602
Conversation
…nd no game thread waits behind a present for nothing Hyrule Field lost about 15 fps between v2.0.11-588 (ba86aee) and main (RTX 3090, 2560x1440, uncapped: 79 -> 64-71 fps, CPU 39 -> 64-73 ms a frame). One of the game's job threads was blocked 5.5 ms a frame in a CPU read of GPU results (0.1 ms before), and the game's other job threads polled with 20 us sleeps meanwhile (1.9 cores in sched_yield against 0.3). The read is of the culling results the first part of the scene's submission writes; that submission then stops at an event wait and is finished later, on the presenting thread. Three things held the read up: - since NotRexed#2419 a submission's value maps to its last part's recording, so the read waited for the GPU's whole submission. Each part writes back the pages the GPU wrote in it, at its own end: the page table now keeps the part with the value, and the read waits for the newest part of its pages. A page written in place (no mirror) keeps the wait for its whole submission, whose later parts read and write guest memory itself (issue 997). RX_VK_READBACK_PART=0: the last part, as before. - since NotRexed#2125 the read took sync_order_mu whenever a queue was stopped, behind the presenting thread's whole replay and its present. A queue can be stopped at a met wait with nobody to let it on only after a CPU signal; the read looks at the stopped queues when the CPU has signalled since the last look (2-9 of 3000 readbacks, all of them before). RX_VK_READBACK_SETTLE_ALWAYS=1: whenever anything is stopped, as before. - a wait whose recording was already done, and the host's copy bookkeeping (rx_nvn_replayed, under its own lock), took dev.present_mu to flush held submits, which exist only with batched submits. A present holds that mutex 4 ms a frame. The flushes look at an atomic first; a wait returns at once when the recordings' timeline is already there. Hyrule Field, same session, fix / base / main: 78.6 / 81.3 / 63.1 and 78.7 / 73.9 / 65.9 fps; CPU 46.7 / 38.3 / 71.0 and 45.1 / 40.0 / 69.5 ms a frame. Readbacks that waited: ~1350 of 3000 at 0.14 ms (main 172 at 5.5 ms, base ~1300 at 0.13 ms). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Testing this on Linux with an RTX 4090 (NVIDIA 615.78.08, i9-13900K, Arch); I'll post the numbers for this exact commit as each run is read. Two things from your "Not yet verified" list are what I can give: Kokiri Forest timing, and the flash check with What I have already, measured tonight before this pull request existed, and it agrees with your "one item": in a forest scene the whole loss of main is the readback wait, and nothing else in the range costs anything there. Kokiri Forest, a save with camera turns, 2048x1152, the game's own upscaler, uncapped (
Coming for |
|
One thing from reading the code while the timing runs build, in part 2 (the look only after a CPU signal). I think there is a window in which a readback can go through without waiting for work that The order today
The window is between 2 and 3. A readback on another thread calls Shown without a game. A fixture that cuts The fixture (it imports the helpers of your A way to close it, if you agree it is one: store |
|
A second one, in part 3 (the flush with nothing held), and only on the batched path, which your description lists as checked by a source test only. Same file and commit. What the flag is meant to guarantee (your comment at 397): a thread that finds a recording in the maps and waits for it never skips the flush its wait needs. The flag is raised before the maps are written (399) and again after the append (435). In between, a flush of older submits clears it (249). The order that breaks it
After that the readback waits until something else sends the batch: the producer's own Shown without a game. The real The fixture: A way to close it: count the producers that have raised the flag and not appended yet (up at 399, down after 435 under the mutex), and let the quick test return only when the flag is down and that count is zero; or have the flush leave the flag up while that count is not zero. |
…s look at the stopped queues covers events in guest memory Review of the readback change: - The part wait asked only whether the read's own pages were written in place. A later part of the same submission may read other guest memory in place: a pool bound with no mirror (refused, wanted, or bound outside a recording's uploads), a mirror's dirty pages that no capture holds, a mirror's first copy. Let go after the first part, the game could write what those parts still read (issue 997's kind of frame, on a card short of VRAM). vk_memory.cpp now notes the newest part with such a read, and a readback whose part is older waits for its submission's last part, as before this branch. The log's readbacks line counts them. - The look at the stopped queues was skipped unless nvnEventSignal had been called. An event neither the stream nor nvnEventSignal ever set has its value read from guest memory, where a plain store meets a wait with no signal. While a queue is stopped at such an event the readback looks every time, as before this branch. - memory_writeback_value's page loop skips chunks never made, as max_in does. - The readbacks line counts a readback as waited whenever the replay was behind, as main does, so the counts compare. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
@mooki000 thanks for testing. Heads up: I pushed 9daa071 on top of 798d625. It fixes two review findings: the readback's part wait now gives way to the whole-submission wait while any part reads guest memory in place (direct-bound pools, live mirror syncs), and the look at stopped queues also covers events in guest memory. On a GPU with enough VRAM for mirrors it shouldn't change timing (no in-place reads in our Field/Kokiri logs). Your 798d625 numbers still count for perf; the flash check would be most useful on 9daa071. |
|
@hckrman101 thanks, the flash check and the timing will be on One note so the two comments above do not get lost under the new head: both still apply to
|
|
@mooki000 thanks, both are real as far as I can see from the code, and both are good catches.
Both get tests of their own that fail on |
|
Thanks. In case it helps for the two tests: the settle fixture adjusted to It models an event the CPU signalled ( Timing and the flash check on |
|
A third one, on
Shown without a game. The real page table, readback choice, recording bookkeeping, marker and The fixture: By reading only, not run: A way to close it: call |
|
The refresh copy from my last comment is now run as well, no longer only read: the same fixture with g++ 16.2 and clang++ 23.1 on |
…d submit is found only once it is held, and more in-place reads noted mooki000's two findings on the pull request, and the second review's: - A look at the stopped queues said it was done at the look's end, before the burst it ran in had finished its recording (burst_end: the write-backs noted, the recording submitted). A readback in between found the counts equal, took no look, and read the page table without that recording's pages: the hole NotRexed#2125 closed. The count is now said at the burst's end, after the recording is finished (found by mooki000). - A held submit's recording was in value_rec / part_rec before the submit was in the list of held ones. A thread that found it there and waited for it had nothing to flush, and waited for a signal still held until something else sent the batch. The recording is now put in the maps under dev.present_mu, after the submit is in the list and pend_held is up; without batched submits as before. The early raise of pend_held is gone with its window (found by mooki000; main has the same window with the mutex). - In-place reads noted for the readback's part wait: the pool handed out whole (gpu_buffer: a large texture unswizzled on the GPU, a texel buffer's view), and a binding made outside a recording's uploads now counts for the next part. The counters are read and written in order (no relaxed loads). - Not noted, with the reason at note_in_place_read: memory_rb_refresh's copies of the readback pages into their mirrors (in every part's uploads, they are how a CPU write to such a page reaches the mirror; noted, no read would wait for less than its submission), and the shaders' own pointers into a pool with no mirror. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
@mooki000 thanks again. Both of these were on
|
|
Status: ready for review at Measured on Review:
Still running: the framelog flash check on Not testable here: Windows, a no-mirror iGPU. If something turns up after merging: |
|
Flash check on |
|
Checked
So the two races and the On the refresh copy: your argument makes sense to me from the code, and I would rather answer your question with counts than with a guess. I am taking them from this PC's runs (how often a readback range's page also holds data a later part reads through a refresh, forest and open field) and will post them with the timing and the flash check on |
|
First timing here, on an RTX 4090 (NVIDIA 615.78.08, i9-13900K, Arch Linux). This hold ran on Kokiri Forest, uncapped (a save with camera turns, 2048x1152, the game's own upscaler, headless, 30 s after the load, base and fix in turn inside one exclusive hold, the same counting patch in both):
The fix: 3.20 ms a frame less (15.65 to 12.45), 16.4 fps more, 21.1 ms less CPU a frame, and the long frames gone (399 and 375 over 20 ms against 20 and 17). GPU time is the same in all four (5.91 to 5.99 ms a frame). For scale, in other holds tonight the build from before #2125 and #2419 (49f75dc) ran this scene at 78.4 to 78.6 fps; the comparison with it in one hold comes on What main costs under a 60 cap, since most people play capped: the same scene with DLAA and the 60 cap, 49f75dc against main
So even capped, main is 4.2 fps lower here, with about 5 times the frames over 20 ms. The same arm with the fix follows. Reading Still to come from here: forest and open field on |
|
Counts for the refresh question (whether leaving How it was counted:
All 610 are one shape: a 192-byte readback with a read-only 16-byte binding that starts 464 bytes before it and a read-only 64-byte binding that starts 192 bytes after it, both outside every readback range of that submission. In each case the recording of the part that binds them had completed when the readback's wait returned, so the game could not have changed those bytes under it. So in these two scenes the choice costs nothing: the sharing is rare, read-only, and never still in flight. This is an answer in numbers for one card and two scenes, not a proof:
The counter is a small local patch on top of |
|
Batched-submit path on hardware: RX 580 (RADV, GCN), with batching forced on (
|
|
The same readback wait on Windows, with an 8 GB NVIDIA card and a 6-core CPU My logs show the wait this PR removes, on a build that contains
How I counted: from the log's |
|
@mooki000 thank you, that's a lot of useful data.
@luiz-august0 thanks, this is the Windows data we were missing. About one wait a frame at 9-12 ms, roughly one frame's GPU time, is the same cause-1 picture on a 4060 / 5600X. Right: batched submits are RADV only, so cause 3 doesn't apply to you. If you can run this PR's build (the Windows CI artifact of |
… them (NotRexed#2602) Session 6 found the synced build's lost quarter of its frame rate in the game's readbacks of GPU results: upstream NotRexed#2419's wait for a submission's last part and NotRexed#2125's settle under sync_order_mu. A fix written here (kept as backup/readback-by-part-mine) took Kokiri Forest from 39 to 53.5 fps on the i7-11700 and RTX 3080; upstream merged the same fix, more complete, as NotRexed#2602, which this fork takes with its next sync instead. Area: docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The capped arm with the fix, as asked: Kokiri Forest at a player's settings (DLAA, the 60 cap, 2048x1152), headless, RTX 4090 (NVIDIA 615.78.08), base and fix in turn in one exclusive hold, all four runs counting. This hold ran on the pull request's head
With the fix the scene holds 59.3 and 59.2 of 60 (before: 54.2 and 53.9), frames over 20 ms go from 468 and 484 to 64 and 71, over 25 ms from 39 and 47 to 10 and 7, and the game uses about 25 ms less CPU a frame. For scale, the build from before #2125 and #2419 in another hold tonight (so not a paired comparison): 58.4 and 58.9 fps, 85 and 85 frames over 20 ms. Uncapped, the fix against the build from before the two changes, in one hold each (own upscaler), and the field against the base:
Against the old build the ranges overlap in both scenes, so nothing is left over on this PC: the fix gives back all of it. What remains of the readback wait is 0.6 to 0.7 ms a frame in the forest and about 0.3 ms in the field (0.02 ms on the old build). Still coming from here, on merged main: this capped hold again, and the flash check with a window and the pixel frame log (a run with the readback wait switched off first, to show the check sees a flash at all). |
Two upstream commits, no conflict: the readback and present waits (the scene used for every run here went from 41-44 to 59 fps on an RTX 5060 Ti), and the nightly's tag moving forward during merge bursts, with this fork's wait for a release just made kept in scripts/release_upload.sh. Run in the game: DLSS with DLSS Frame Generation and the open neural rendering network at 50% (600 of 600 sets). Tests: 462 passed; upstream's new test_nightly_two_runs_at_once_end_at_the_newer_commit fails about one run in six here and two in eight in upstream's own tree (its two runs race). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Results from an 8 GB laptop card on Linux. This PR fixes the main thing that held my game back: one job thread blocked in readbacks about a third of the time. At the same scene size, fps went up 30-50%. Setup: RTX 3070 Laptop 8 GB, NVIDIA 620.70.06, CachyOS (Linux). DLSS (built with the SDK), output 1920x1080,
The commits in between are #2598, #2347 and #2556. On this card DLSS-G is not available, so none of those three should change performance. The two sessions are normal play, not an A/B run in one spot, and they took different routes. So I compare them by draw count rather than by place. Readbacks
More readbacks wait now, but each wait is about 100 times shorter. Before this PR the line came out about every 5.4 s. So a game thread was blocked for about 1.1 s of every 5.4, the same picture as in your Hyrule Field trace. GPU time did not change, so the gain is all on the CPU side. Fps at the same scene size
(Median of the On an 8 GB card the gain is larger than the 3090 numbers in the description. Before this PR my waits were 10-20 ms each, not 5.5 ms. The gain shrinks as scenes get bigger, where the replay itself (#995) takes over. I saw no visual problems: no flashes, no missing shadows or geometry. There was one 930 ms hitch, when VRAM ran out entering a large area ( |
Windows test: Hyrule Field, stock and with our backend switchesSetup
Result (Hyrule Field, 1080p, same save and route)
This is the same direction as your RTX 3090 result: +11.2 fps stock (+23%), 5.5 ms -> 0.02 ms, and the long frames go away (min 42.7 -> 56.3). Windows was on the "not yet verified" list; this covers it for one NVIDIA machine at 1080p. Kakariko Village (same build and final config, ~2510 draws): 47.4 fps avg (40.4-50.0, last 48.2), 1227-1245 readbacks of 3000 at 0.15-0.16 ms, 0 fallback. The town is CPU-bound; the readback wait is no longer what limits it. The one interaction we found:
|
| arm | fps avg (min-max) | readbacks that waited | ms each | last-part fallback |
|---|---|---|---|---|
SYNC_LIVE=1 fix |
51.39 (49.4-52.7) | 482-1068 | 0.55-5.43 | 309-1022 |
our full config fix (SYNC_LIVE + 4 GB + pct 90 + NO_CATCHUP + WB_MAX) |
53.25 (51.1-54.9) | 838 | 0.83 | 820 |
| our full config, main waits | 53.86 (52.6-55.2) | 753 | 0.91 | n/a |
The log's own counter shows why: with SYNC_LIVE=1 the mirror syncs read 80,954 pages "from guest memory as it was at the replay" per 600 submits (0 with it off), so a later part reads guest memory in place and the part wait gives way to the whole-submission wait for ~90% of the readbacks. That is exactly the guard you added in 9daa0718, not a wrong read; the switch and the part wait just cannot both apply. We will drop SYNC_LIVE in our config and keep the part wait.
RX_VK_VRAM_MB=4096 + RX_VK_COMPACT_PCT=90 alone and RX_VK_READBACK_NO_CATCHUP=1 alone do not trigger the fallback (both at 59.8-60.0 fps, 0 fallback), so the 4 GB texture budget is not the problem.
Checks
tests/test_readback_part.py,tests/test_flush_nothing_held.py,tests/test_readback_settle.py,tests/test_recorded_wait_flush.py: pass on Windows at this head.0 looked at the stopped queues firstin every fix line matches your field count;SETTLE_ALWAYS=1restores the look (172 of 3000 in the stock-waits arm), as documented.- No framelog/false-flash pass on Windows yet; that is the one check we have not run here. No screenshots were uploaded.
Limits
- One NVIDIA machine, 1080p, one save and route; no AMD/Intel, no iGPU without mirrors.
- The stock-waits arm is the same build with the two switches, not a separate main build; the third part (flush with nothing held) stays on in both arms and has no switch.
- The byte sizes above are this build's; the PR's own base and lift may differ.
|
One more data point for the absolute level, same save and route, 1080p: my current playable build ( |
|
An integrated GPU without VRAM mirrors, which the description listed as untested: Intel UHD Graphics 770 (RPL-S), Mesa 26.2.5 (ANV), Linux 7.2.15-1-omarchy, i9-13900K. Merged main The part wait does not act on this card, so there is nothing for it to cost.
The card is GPU-bound in this scene (85 ms of GPU time in a 95 ms frame in the first, long run), and the game spends 75 ms of each frame waiting in readbacks: here that is the wait for the GPU's frame itself, the whole-submission wait your follow-up would fall back to. What does differ between the two arms is how the same wait is cut up, and by the code that can only be the settle switch, since no readback here has a part: with the defaults 604 and 593 of 3000 readbacks wait 30.7 and 31.2 ms each (51 and 57 of them after a look at the stopped queues); with both switches 236 and 224 wait 83.7 and 87.2 ms each, every one after that look. About 2.6 times as many waits, each about 2.8 times shorter, for about the same time a frame (85.4 and 83.6 ms against 88.5 and 87.7 ms): the shape you describe on the RX 580, on a card where the part wait cannot be its cause. Whether it costs frames here I cannot say: other work used 13 to 30 cores beside all four runs of the pair, so none of them counts as a timing. Not a correct picture on this device, with or without this change: on today's main the UHD 770 draws this area in false colours (dispatches are left out while their compute pipelines compile; reported separately). "The same scene" above means the same draws a frame. No device lost, no i915 line in the kernel log, in any run. |
|
Windows run for @hckrman101's request: main against the last nightly before this PR, on an RTX 3080. This complements DFN-master's RTX 3080 run above: there, one build with the switches that restore the old waits, at a 60 Hz cap; here, two separate builds (main and the nightly itself), uncapped, at the same teleport spot. The nightly matches DFN-master's "stock waits" arm (172 of 3000, ~5.6 ms each), and without the cap main's gain shows in full. Setup. Windows 11, RTX 3080 10 GB (driver 621.14), i9-12900K, 64 GB. Both built from source with llvm-mingw, no DLSS: main
So on this card about +31 % fps (median frame 16.35 -> 12.2 ms), in line with the 3090's 63-66 -> 77-79. On main, more readbacks wait (about half, against 6 %), but each one for 0.04-0.10 ms instead of ~5.7 ms. "Looked at the stopped queues first" and "waited for their submission's last part" are 0 in every line of both main runs. Log lines, the first five of each measured window (55 and 55 lines on main, 42 and 43 on the nightly): Not tested: DLSS, frame generation, Reflex, a frame rate cap, other areas, moving through the field. |
|
Windows numbers for this one, after the merge: it recovered the loss from #2125/#2419 on an RTX 5090. (#2700 has RTX 3080 data on the same question.) Setup. i7-13700K (24 threads), 98 GB, Windows 11, RTX 5090 (driver 621.14), 165 Hz. Every build was made from source with the Hookshot Builder's toolchain and v2.1.0's profile and hot list. Kokiri Forest, the same autosave loaded by a scripted input each run, 2048x1152. Frame generation off and
The readback wait is the sum of "waited x ms each" from the With the player's own settings (frame generation Auto) the game's limiter holds every build at 58-60 fps, so this is the uncapped view. No run had a stall, a "timed out" line or a device loss, and screenshots of main and 49f75dc in the same view match. What's left after this PR
|
#2602 (#2700) The tested configurations had no Windows RTX 30 run of the readback change (#2602), and the workflow asks for a row for each measured run. This adds two: main and the last nightly before #2602, on an RTX 3080, uncapped, in the same Hyrule Field spot. **What changed:** two rows at the top of `docs/tested-configurations.md`: main `87991bc8` at 78.4 / 79.3 fps (readbacks about 1,550 of 3,000 waiting 0.04-0.07 ms), the nightly `55b976c0` at 59.7 / 60.8 fps (172 of 3,000 waiting 5.6-5.8 ms). The full setup, table and log lines are on #2602: #2602 (comment) **Tests:** `python -I -B tests/run_all.py` on Windows with llvm-mingw: 453 passed, 4 failed (`test_fpx_sticky`, `test_input_background`, `test_lift_cache`, `nx2/test_nup_paths`). The same four fail on this PC without this change; no check reads this file. Docs only, nothing on screen. See #2602 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: d4nkn3ss <claude@dank.industries> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
On top of this PR I built the stack I actually play (zstd program cache + BC7 big textures + the 4 GB texture budget) and ran the same save and route:
One cross-PR note from the Kakariko run: the BC7 path's GPU unswizzle reads the source through No device loss on this stack; the earlier crash in my runs was stale pipeline play-cache files from older builds ( |
Upstream NotRexed/Hookshot a1eb460: on Linux OoT's idle job workers burned a processor each spinning in a yield loop; sleep_ns never counted the processors there, so every sleep under a millisecond yielded. With NotRexed#2602, five ModuleSystemWorker threads and sead::MainThread yielded 3.3-3.6 ms a frame each in Hyrule Field while one waited on a GPU readback, about 9 ms of CPU a frame in all. Ported by hand into host/hos_thread.c: when HOS_SHORT_SLEEP_TIMER is not set, the Linux build makes the game's own sleeps (SleepThread) wait on the timer (mode 1); the host's own short sleeps (the frame limiter, the audio renderer) still yield, so frame pacing is unchanged. HOS_SHORT_SLEEP_TIMER=0 keeps all yielding and =1 puts all on the timer, as before. Windows is unchanged. Test: tests/test_short_sleep_timer.py now builds its fixture twice, once for the Windows code and once with _WIN32 undefined for the Linux code. The Linux cases check mode 1 for every processor count when the switch is unset (processor groups included) and that 0 and 1 still override it; two broken variants prove the checks fail without the new default. No in-game run was made: the Linux A/B (Hyrule Field and Kokiri, uncapped and at a 60 cap, and a Steam Deck) is still to do. Assisted-by: opencode (DeepSeek V4.1 Flash 1017)
|
Thanks @dankman42 for the uncapped two-build RTX 3080 comparison, @Brett-D for including v2.1.0 and separating the remaining readback/CPU cost on the RTX 5090, and @DFN-master for the RTX 3080 switch comparisons and follow-up with your play stack. The Linux three-column comparison is on #2595, and I'm measuring Brett-D's remaining cost on Linux now. I'm also checking the BC7 interaction you reported, without drawing a conclusion yet. |
… them (NotRexed#2602) Session 6 found the synced build's lost quarter of its frame rate in the game's readbacks of GPU results: upstream NotRexed#2419's wait for a submission's last part and NotRexed#2125's settle under sync_order_mu. A fix written here (kept as backup/readback-by-part-mine) took Kokiri Forest from 39 to 53.5 fps on the i7-11700 and RTX 3080; upstream merged the same fix, more complete, as NotRexed#2602, which this fork takes with its next sync instead. Area: docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d here (NotRexed#2622) Five runs on the synced build with NotRexed#2602: Kokiri Forest 51.8 fps (51.2-52.0), 1% low 40.8, frame 19.3 ms (session 5: 36.9 fps). The shader preparation works again with a driver cache of the benchmarks' own. Upstream draft NotRexed#2622's presenter thread changed nothing here: 51.1 and 50.9 fps on, 51.4 and 50.2 off. Area: docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hyrule Field runs about 15 fps slower on
mainthan on v2.0.11-588 (ba86aee), and uses almost twice the CPU. One of the game's job threads sits blocked for 5.5 ms of every frame in a CPU read of GPU results, and the other job threads poll while they wait for it. This PR removes that wait without undoing what #2125 and #2419 fixed.What regressed
RTX 3090, Ryzen 9 7900X, Linux, 2560x1440, uncapped, standing in Hyrule Field from the same save:
ba86aee)fce53453)GPU time and the replay's own time are the same on both.
Cause
An all-thread sample of both builds (
HOS_SAMPLE_ALL) shows the extra CPU is one item:sched_yieldin the host's short-sleep loop, 0.28 cores on the old build and 1.92 on main. It is the game's six job threads (ModuleSystemWorker1-5andsead::MainThread), which poll with 20 us sleeps while one of them is blocked. On main a job thread is inside a readback at 38.7 % of all instants, with 3.9 threads polling meanwhile.The blocked read is of the results that the first part of the scene's submission writes (two buffers, alternating by frame). That submission then stops at an event wait and is finished later, on the presenting thread. Three things held the read up:
sync_order_muwhenever any queue was stopped. fix(vk): with per-queue fences, a CPU read-back waits for the GPU work that writes it again #2125 added a look at the stopped queues before the read. The presenting thread holds that mutex for the whole replay of its submission and for its present, so the read waited for both (24 % of all instants, 3.9 ms a frame) for a look that let nothing on.dev.present_muto flush held submits. Held submits exist only with batched submits (RADV). A present holds that mutex about 4 ms a frame. A readback whose recording was already done waited there, and so did the host's copy bookkeeping (rx_nvn_replayed, called under its own lock), which queued the other job threads behind it. The old build has this one too.The fix
All of it is on by default; the two switches only turn parts off, for A/B.
RX_VK_READBACK_PART=0restores the last part. The read still waits for its submission's last part, as on main, in two cases:nvnEventSignal) nobody has looked at since, or while a queue is stopped at an event that neither the stream nornvnEventSignalever set, whose value is read from guest memory. A look counts as taken only once the burst it ran in has finished its recording, so its write-backs are in the page table. In the field 0-9 of 3000 readbacks look; on main every readback that found a queue stopped did.RX_VK_READBACK_SETTLE_ALWAYS=1restores the look whenever anything is stopped.dev.present_mu. An atomic says whether any submit is held. With batched submits a recording is now put where waiters find it only after its submit is in the held list, under the mutex, which also closes a window main has. A wait returns at once when the recordings' timeline has already reached it. No switch.The trade-off against main
Two reads of guest memory at the GPU's time are deliberately not counted for the part wait. Both behave as they did in v2.0.14 and before, that is before #2419:
memory_rb_refresh, in every part's uploads). These copies are how a CPU write to a page it reads back reaches the mirror at all, and a later part that writes such a page again is waited for through its own part. Counting them would put an in-place read in every part and cancel the change. What they carry along is any other data that shares a 4 KB page with a readback range. mooki000 counted it on an RTX 4090 (see their comment in this PR). In Kokiri Forest 0 of 20,371 readbacks shared a page this way. In Hyrule Field 610 of 45,916 did, all with read-only bindings, and in every case the reading part's recording was already complete when the readback's wait returned.Review
Three commits:
798d625a: the three parts above.9daa0718: first review. The part wait gives way to in-place reads in later parts; the look also covers events whose value is in guest memory.4ba31740: mooki000's two findings on this PR, both real, and the second review.Measured
Hyrule Field, 30 s each, alternating builds in one session. Rounds 1-3 are
798d625a's renderer (built before its rebase onto6f7ab2a7) on main's game binary (fce53453); the last two rows are the later commits, built as committed.9daa07184ba31740Round 3's fix run had 367,000 pages swapped in on the PC during its 30 s (the others 20,000-135,000), so I count it as disturbed. The PC was short of memory for the whole session, which is also why the old build swings between 66 and 81.
Time blocked in readbacks, per 3000: about 220 ms with the fix (1500-1540 waits of 0.14-0.16 ms), about 230 ms on the old build, about 1100 ms on main. The counts themselves are not comparable between builds: main counts a readback as waited whenever the replay was behind.
Kokiri Forest,
798d625a, two rounds: fix 65.3 / 58.2 fps at 68-69 ms of CPU a frame, main 40.5 (a hitched run) / 45.9 at 113-117 ms, the old build 50.8 / 56.0 at 84-88 ms. The number of draws there varies from run to run (955 to 1255) on every build, so these are rougher than the field's.The CPU is still 6-8 ms a frame above the old build in the field. The read now makes one real wait a frame, for the first part on the GPU (1-2 ms), and about three job threads poll during it; on the old build those threads sat blocked on mutexes instead. That is the yield loop for sleeps under a millisecond.
HOS_SHORT_SLEEP_TIMER=1on main brings the CPU to 37.5 ms a frame at the same fps, so it is a separate follow-up in the host, not part of this PR.SadTransGirl measured the cost of #2419 independently with an
RX_VK_WAIT_FIRST_PARTswitch (branch) on an RTX 4070 SUPER, DLAA 1440p: about 4-7 fps (Town to Hyrule Field 55.9-56.2 before #2419, 47.4-47.5 on main, 52.7-54.2 with the switch).Tested
python -I -B tests/run_all.pyon Linux at4ba31740: 447 passed, 3 failed, 25 skipped. The three are not this change:test_bake_portabilityandtest_builder_log_read_only(nozstandardortkinteron this PC) andtest_env_on(HOS_FPS_LOGinhost/oot_high_fps.c, on main).tests/test_readback_part.pyruns the page table, the part bookkeeping, the readback's choice of wait, the look at the stopped queues and the burst's end, cut from the source, and counts every copy out of a guest pool.tests/test_flush_nothing_held.pychecks the order in which a held submit is listed, flagged and made findable. Both fail on the commit before the fix they cover.tests/test_readback_settle.py,tests/test_recorded_wait_flush.py, and five fixtures that compile the changed functions.RX_VK_FRAMELOGwithscripts/framelog.py, 30 s each,798d625aagainst the same build with both switches off:4ba31740, fix only: Hyrule Field 0 events in 2558 frames (brightness 37.6); Kokiri Forest 0 events in 1692 frames. That Kokiri run was the 964-draw variant of the scene, darker at 79.1, and I have no switches-off run of that variant to compare its brightness with.Not yet verified
4ba31740in the game beyond one field pair and one framelog pass (above): no Kokiri timing, no run of the old build beside it.4ba31740, so a check of798d625adoes not cover it; it is covered by a source test only.