fix(vk): give the game's occlusion reports the GPU's counts, so it culls what is hidden #197
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
5 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!197
Loading…
Reference in a new issue
No description provided.
Delete branch "HumanBean/Longshot:fix/occlusion-queries"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Ocarina of Time hides objects that are behind something by itself: its z-prepass draws each candidate's bounds between a samples-passed reset and a report, and the next frame it leaves out every object whose report read 0. On Vulkan every report reads 4096 (since https://github.com/NotRexed/Hookshot/pull/487), so nothing was ever left out: the Market draws ~2,930 draws a frame where ~2,460 are needed. This gives the game the GPU's real counts, on by default.
It builds on dyst's #23 (jordanella's https://github.com/NotRexed/Hookshot/pull/2871 carried over), which is the same idea, off by default and not yet measured on Longshot, and fixes the two things that kept it from being a clear win: the counts often came too late for the game to use, and queries around the scene's real draws cost GPU time.
What changed
gpu/vk/vk_replay.cpp), as in #23: each draw from a samples-passed reset (NOP_RESET_COUNTER) to its report (NOP_REPORT_COUNTER) can get a Vulkan occlusion query from a 65,536-entry ring (occ_take,occ_room, numbered inNvnStateso a parallel replay's dry pass and workers agree); the report is still written as 4096 at replay, and the count is written over it later unless the slot was reported into again. A run that overflows (256 draws), or whose draw never ran, stays 4096.occ_collect()reads every waiting report's queries with onevkGetQueryPoolResults(no wait; two calls where the ring wraps) as each recording begins, writes the reports from the oldest on whose queries have all ended, and resets them with onevkResetQueryPool. #23 made one results call and one reset call per report (~1,300-1,600 reports a frame). Waiting for a whole submission's timeline value instead wrote the counts 3-13 recordings late (about two frames), mostly after the game had read the slot: the Market then drew as many draws as main. Now they land 1-4 recordings after the report, at ~5 µs a read.occ_take: the channel mask writes no colour): the game's bounding boxes. A run with a draw that writes colour stays "visible". A query's end waits for its draw to finish; around the scene's real draws that cost ~0.9-1.9 ms of GPU time a frame in the Market for a few more objects left out (see Measurements).RX_VK_OCC_QUERIES=2counts them too.hostQueryReset(core in Vulkan 1.2);RX_VK_OCC_QUERIES=0turns it off, and so does settingRX_VK_OCC_FIXED=<n>(that value, as before).RX_VK_OCC_LOG=1also logs the reads' cost and the counts' sizes;RX_VK_OCC_DRY=1(a test) makes and reads the queries but leaves the reports at 4096.gpu/vk/vk_draw.cppdraw(),gpu/vk/vk_draw.hDrawArgs): the query begins once the pass is open and ends as the draw returns (OccGuard), beforeeach_guardcan end the pass. Precise where the device hasocclusionQueryPrecise.gpu/vk/vk_main.cpp,gpu/vk/vk_internal.h):occlusion_precise,host_query_reset.tests/test_vk_occlusion_queries.pyruns the ring's code against a fake query pool and guest memory: counts written once their queries end and not before; one results call and one reset call for a batch of 41 reports (two across the ring's end); in order; visible when a draw never ran, a run overflowed, a run had a colour draw, or the slot was reported again; the dry pass numbering as the workers do; the wiring in the source.tests/fsr_uniform_ownership_test.cppstubsocc_collect.docs/development.md("Switches for testing and debugging"); a row indocs/tested-configurations.md.Flags and settings
RX_VK_OCC_QUERIES0: off (every report 4096, as before);2: runs with colour draws counted too1)RX_VK_OCC_FIXED=<n>RX_VK_OCC_LOGRX_VK_OCC_DRYMeasurements
main:
839e85ff(2026-11-13)this PR: fix/occlusion-queries @
3b651bb8(measured as 3f67aad0: the same code;3b651bb8only adds the tested-configurations row)Hardware: RTX 3090 24 GB (NVIDIA 620.70.06), Ryzen 9 7900X, 30 GB, Linux (CachyOS, kernel 7.3.3)
Scene: the pristine Kokiri save after the opening event: Kokiri Forest standing 40 s, then the camera turning at full speed 30 s (right stick held); then Castle Town Market (debug teleport, 30 s to settle), standing 40 s and turning 30 s. 2048x1152, default options, shaders prepared, frame rate cap 240.
Method: one game per run in a private headless sway with scripted input, under the PC's timing lock (own CPU cores), alternated M P P M M P M P (4 runs each). Draws and GPU time: the medians of the renderer's 120-present lines (
N presents, X fps; a frame: N draws,GPU time N ms a frame) inside each phase; frame times fromRX_VK_FRAME_LOG. Each cell: the median of the 4 runs' values. The PC swapped in every run (100k-280k pages in a phase in the worst, the last P run): its frame times are in the per-run list below.How to reproduce: a save in Kokiri Forest and the Market; stand 40 s, then hold the right stick 30 s; compare
RX_VK_OCC_QUERIES=0against unset withRX_VK_OCC_LOG=1.839e85ff3b651bb8Per run (frame median, ms; main / this PR): Kokiri standing 16.92 15.57 16.59 16.49 / 15.15 15.53 16.36 16.94; Market standing 16.41 16.43 16.88 16.66 / 15.04 14.83 15.63 17.16; Market turning 12.82 12.86 13.08 11.77 / 12.79 12.54 12.86 14.72. The PR's last run is the one that swapped hard (Market standing 282k pages swapped in, against 151k for main's last run); without it the PR's medians are Kokiri standing 15.53, Market standing 15.04, Market turning 12.79 ms.
What it took to get here (same PC, same scenes;
runs/named in the comments):RX_VK_OCC_QUERIES=2here)RX_VK_OCC_QUERIES=2 RX_VK_OCC_DRY=1)Where the turning time goes (
HOS_PROFILE=5 HOS_SAMPLE_ALL+HOS_CHAIN, Market turning, one sampled run each,scripts/chain.py): the frame's critical path is the replay thread running, 11.79 ms a frame on main and 10.29 ms with this PR (16.46 against 14.67 ms a frame on the path, the first ~260-280 frames of the turn). Fewer draws to record is the saving; the reads cost ~5 µs each, ~23 a frame (RX_VK_OCC_LOG). The camera-turning regression seen with https://github.com/NotRexed/Hookshot/pull/2871 on an RTX 3080 (shaders not prepared) did not show here: with every run counted the GPU time while turning was 5.80 -> 6.15 ms (4 runs), the cost of queries around real draws, which this PR leaves out.Pass timing (
RX_VK_PASS_TIMING=1, Market standing): the passes took 4.83 ms a frame on main and 5.74 ms with every run queried but no counts used; the bounding-box pass itself only 0.091 -> 0.102 ms. The extra was in the scene's real programs (RX_VK_PASS_TIMING=2: a few programs 10-60 µs slower each), so this PR leaves the runs with colour draws uncounted. Kokiri showed no query cost either way.AMD: RX 580 (RADV, no mesh shaders: the compute fallback)
Hardware: RX 580 (Polaris 10), Mesa RADV, i7-9700K (8 threads), 15 GB, Linux (CachyOS).
Method: this PR @
3b651bb8, built on that PC from a player's build (Compile shaders Most). The same build withRX_VK_OCC_QUERIES=0(off, every report 4096 as on main) against the default, alternated A B B A A B B A, 4 runs a side, the median of each run's median. The route is the same as above.Baseline: this PR @
3b651bb8,RX_VK_OCC_QUERIES=0. It was chosen for one build on a slow PC; a round against main itself is still running there.3b651bb8,RX_VK_OCC_QUERIES=03b651bb8That PC is GPU-bound, so the culled draws save GPU time directly; on the RTX 3090 the gain is on the CPU side. In the Market 46-68% of reports read 0, and counts arrive 1.5-3 recordings after the report. 0 reports were left visible because a draw didn't run. No hang and no device loss.
Testing
Tested: the runs above (RTX 3090, NVIDIA 620.70.06, Linux),
python -I -B tests/run_all.pyon Linux: 517 passed, 0 failed, 20 skipped (Windows-only and missing-header skips, the same on main). The new test fails on main (the code it cuts out isn't there). In the game: screenshots at the same spot (Market entrance, Kokiri) show the same objects on both sides; 0 reports left "visible" because a draw did not run in play; no device lost or validation line. The flash detector (RX_VK_FLASH=1, every present compared with its neighbours) over the same route (Kokiri and the Market, standing and turning), one session each: 0 one-frame flashes on main in ~11,800 presents, 0 with this PR in ~12,200. Non-precise queries on this card return the same counts as precise ones (the same share of zeros and the same sizes), so the counts are the GPU's real ones.Not tested: AMD's flash and validation runs (in progress on the RX 580), AMD on Windows, Intel, the Steam Deck and Windows; dungeons and interiors; water, windows and the sun held behind cover (the sun's haze may now follow its real count: https://github.com/NotRexed/Hookshot/pull/2871 found 0 flashes and no change in Hyrule Field with every run counted); long play.
Needs other testers:
RX_VK_OCC_QUERIES=0against unset, expecting fewer draws, no objects missing and no flicker (T-KEFP)Screenshots
Nothing changes on screen by design: hidden objects are left out. Market entrance, standing, main @
839e85ffand this PR @3b651bb8: the same buildings, banners, guard and fountain (not attached: made from the game; available on request).Risk and rollback
An object could be left out that should be drawn if a count were wrong or landed in the wrong slot; the code only ever writes a count it read from that report's own queries, after checking the slot's timestamp, and anything unknown stays 4096. Queries could cost GPU time on a driver where they are dearer than on NVIDIA.
RX_VK_OCC_QUERIES=0turns it all off (the reports read 4096 as before); reverting the commit is enough.Reviewer notes
Start with
occ_collect()andocc_take()ingpu/vk/vk_replay.cpp. This supersedes #23 if it is taken (it contains #23's change); #36 (live switches, withRX_VK_OCC_QUERIESas the first) would need its switch's default updated. Left for later: the colour runs could be counted where queries are cheap, if another driver shows it; and the Lichii approach of reading counts on the GPU (vkCmdCopyQueryPoolResults) was not needed, since the batched host read costs ~5 µs.Issues
See #23. See https://github.com/NotRexed/Hookshot/pull/2871, https://github.com/NotRexed/Hookshot/pull/487.
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.docs/tested-configurations.mdfor each measured run.mainand passing here.tests/that reads changed source updated in this PR, not weakened.longshot_vk.dllbuilt with llvm-mingw 20261104 from Linux (RX_TARGET=windows RX_VK_ONLY=1), not run on Windows. No paths or platform code changed.)WIP: fix(vk): give the game's occlusion reports the GPU's counts, so it culls what is hiddento fix(vk): give the game's occlusion reports the GPU's counts, so it culls what is hiddenYes: #197 can continue from here, carrying #23. It keeps the counts and fixes what #23 got wrong (the turning cost came from counting every draw), so #197 is the one to review; I've marked #23 superseded. My #36 (live switches, RX_VK_OCC_QUERIES its first) was stacked on #23 and will follow #197. I'm running T-DG3Z now on the RTX 3080 (Windows, NVIDIA 621.14) that measured the +25% GPU time while turning on https://github.com/NotRexed/Hookshot/pull/2871, the same scenes, and will post the numbers here.
AMD results, on the lab's RX 580 (RADV, no mesh shaders, i7-9700K). The same build of
3b651bb8with RX_VK_OCC_QUERIES=0 against the default, 4 alternated runs a side:That PC is GPU-bound, so the gain is GPU time; on the RTX 3090 it is CPU time. No hangs, no device loss, and 0 reports left visible wrongly. Flash and validation runs on that PC are in progress. The description's Measurements has the table. (T-KEFP: Linux RADV covered; AMD on Windows still open.)
Test request T-DG3Z: pass. Turning the camera doesn't raise GPU time with the queries on; it goes down, as standing does.
Hardware: RTX 3080 10 GB, NVIDIA 621.14, i9-12900K, 64 GB RAM, Windows 11. Built
3b651bb8(this PR's head), standalone with llvm-mingw; window 1280x720,HOS_FPS_MAX=240, the save's settings (no DLSS), scripted input only.Method:
RX_VK_OCC_QUERIES=0, unset,=0, unset.HOS_FPS_LOG, per phase: the median ofvk: GPU time ... ms a frame(12-15 lines a phase), ofvi: ... fps over the last 5.0 s(6 a phase), and of the draws invk: N presents ...; a frame: D draws.vk: occlusion queries on (RX_VK_OCC_QUERIES)when on.Not looked at: the picture (no screenshots compared), shaders prepared by the Builder's preparation (warmed by a run instead), DLSS.
T-DG3Z, a second Windows NVIDIA run: it agrees with d4nkn3ss's, with what theirs left out. The shaders here were prepared by the Builder's own preparation (
make_game.py), the options are the defaults, and the CPU is slower (a Ryzen 9 3900X, CPU-bound at about 26-32 fps). With #197 the GPU time goes down while the camera turns, in Kokiri Forest and the Market, againstRX_VK_OCC_QUERIES=0, and so do the frame times. This is the RTX 3080 that measured +25% GPU time turning on https://github.com/NotRexed/Hookshot/pull/2871.Hardware: RTX 3080 10 GB (NVIDIA 621.14), Ryzen 9 3900X, 64 GB, Windows 11
Build: this PR @
3b651bb8,make_game.pywith shaders prepared (Compile shaders: Most, the default), default options (no options file),HOS_FPS_MAX=240, headless, mutedMethod: #197's phases, by the control bridge: Continue to Kokiri Forest, standing 40 s, turning at full speed 30 s (right stick held); the debug teleport to the Market, 30 s to settle, standing 40 s, turning 30 s. Runs alternated O U U O O U O U (O =
RX_VK_OCC_QUERIES=0, U = unset,RX_VK_OCC_LOG=1on both), each under the PC's bench lock with nothing else building. Draws and GPU time: the medians of the renderer's 120-present lines inside each phase (phases cut by the status's frame count at their ends); frame times fromRX_VK_FRAME_LOG. Each cell: the median of the runs' values. Two batches: in the first, the save wasn't put back between runs, so after the first run Continue started in the Market; its Market phases count, its "Kokiri" ones don't. Kokiri: 5 runs off, 4 on; the Market: 8 and 8.RX_VK_OCC_QUERIES=0Per run (frame median ms; GPU ms):
On this PC the game is CPU-bound at about 26-32 fps in these scenes (the frame is well above the GPU time), so the frame times say less than the GPU time does; both moved the same way. One run with the queries on had a 20.4 ms GPU stretch while turning in the Market (its frame median 43.25 ms, within the others'); the medians leave it out. No objects missing or flicker was checked by eye: these runs were headless.
Steam Deck OLED (RADV VANGOGH): the run T-5BXS and T-KEFP asked for
This merged before I could claim the requests, so here it is for the record: good news on the Deck.
Setup: Steam Deck OLED, AMD Custom APU 0932, RADV VANGOGH (Mesa 26.2.0-devel), SteamOS 3.9.5, kernel 7.2.7-valve2, 16 GB. Longshot built from main
000f19fb; the renderer built alone from this PR's commit3b651bb8applied on000f19fb(no conflict). One binary,RX_VK_OCC_QUERIES=0against unset, alternated,RX_VK_OCC_LOG=1on both. Headless, handheld profile, Minimum preset, the game's own upscaler. Each run starts the game, loads a save by script, waits 3 log windows, then measures exactly 720 presents; draws, replay and GPU time are the log's.What T-5BXS asked (Steam Deck: no hang, no frame-time loss): it holds. 18 runs, no hang, no
FATALordevice lostline, noring ... timeoutin the kernel log; the frame is never slower with the queries, standing or turning.What T-KEFP asked (AMD on RADV: fewer draws, nothing missing, no flicker): it holds for what a headless run can see. Fewer draws: the table. The flash detector (
RX_VK_FLASH=1,RX_VK_SHOT_EVERY=1) with the camera turning in Kokiri finds 0 flashes in 1,680 presents off and 0 in 1,800 on. Not checked: the picture by eye (nobody looked at these runs), so "nothing missing" is still open on RADV, and the Market (I have no save there; the heavy view is another save).Two notes from the Deck:
RX_VK_OCC_LOG: 28-29% of the reports read 0 in Kokiri, 48% on the heavy view, 13-15% in Hyrule Field; written 1.1 to 1.9 recordings after the report; a read costs 15 to 32 µs here, against the ~5 µs you measured.AMD lab, round 2 and correctness (RX 580, RADV, the compute mesh fallback). Both sides built on that PC from a player's build.
839e85ffagainst #197, alternated M B B M M B, 3 runs a side. Market standing: 2,931 -> 2,468 draws, GPU 30.1 -> 26.6 ms, frame median 42.4 -> 38.5 ms. Kokiri standing: GPU 33.8 -> 33.1 ms. Turning medians equal; the Market's longest turning frame 342 -> 297 ms. Main matches the RX_VK_OCC_QUERIES=0 side of the first round.Until the fix lands, RX_VK_OCC_QUERIES=0 turns culling off if anything goes missing.
Follow-up with the code-review fixes: #260 (one query numbering across queues, no count from a draw that didn't run, host resets only after the submission is done, held reports written visible). On an RTX 3090 it takes validation from both query VUIDs on every frame to 0, with the same culling.
Steam Deck (one Deck, 6 runs on, 2 off): no measurable gain; p95 and hitches worse in both interleaved on-runs.
Setup:
0877184, which has this PR, plus the Steam Deck profile (#225, head0d372bf). Plain main runs out of memory on the Deck.Interleaved pair,
RX_VK_OCC_QUERIES=0against the default (on):Four earlier runs of the same build with the queries on (my first "off" pair was void, see below): 14.0, 15.5, 13.6, 13.6 real fps.
hookshot_env.txt, which Longshot doesn't read, so both were really "on" runs (counted above as on). The rerun exports it before the launch. The off-runs' logs have novk: occlusion queries online, and the on-runs do.Not covered: other scenes (the Market, where the PR measured its gain), standing still, and more runs.