fix(vk): give the game's occlusion reports the GPU's counts, so it culls what is hidden #197

Merged
HumanBean merged 1 commit from HumanBean/Longshot:fix/occlusion-queries into main 2026-11-13 15:32:01 +00:00
Owner

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

  • replay (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 in NvnState so 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.
  • replay, new here:
    • occ_collect() reads every waiting report's queries with one vkGetQueryPoolResults (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 one vkResetQueryPool. #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.
    • Only depth-only draws are counted (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=2 counts them too.
    • On by default where the device has hostQueryReset (core in Vulkan 1.2); RX_VK_OCC_QUERIES=0 turns it off, and so does setting RX_VK_OCC_FIXED=<n> (that value, as before).
    • RX_VK_OCC_LOG=1 also 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.
  • draw (gpu/vk/vk_draw.cpp draw(), gpu/vk/vk_draw.h DrawArgs): the query begins once the pass is open and ends as the draw returns (OccGuard), before each_guard can end the pass. Precise where the device has occlusionQueryPrecise.
  • device (gpu/vk/vk_main.cpp, gpu/vk/vk_internal.h): occlusion_precise, host_query_reset.
  • tests: tests/test_vk_occlusion_queries.py runs 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.cpp stubs occ_collect.
  • docs: the switches in docs/development.md ("Switches for testing and debugging"); a row in docs/tested-configurations.md.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
RX_VK_OCC_QUERIES The game's samples-passed reports get the GPU's counts for its depth-only draws, so it leaves out hidden objects. 0: off (every report 4096, as before); 2: runs with colour draws counted too on (1) on by default RTX 3090 Linux: Market standing (-16% draws, -1.2 ms frame median), Kokiri standing (-0.6 ms); RX 580 RADV: Market standing (-11% GPU time, +12% fps) camera turning: no change measured AMD on Windows, Intel, the Steam Deck, Windows
RX_VK_OCC_FIXED=<n> Every report reads n (existing); setting it now also turns the queries off unset (4096 when the queries are off) unchanged - - -
RX_VK_OCC_LOG Every ~5,000 reports: how many read 0, how late, the reads' cost, the counts' sizes off off (a developer switch) - - -
RX_VK_OCC_DRY A test: the queries made and read, the reports left at 4096 (times the queries alone) off off - - -

Measurements

main: 839e85ff (2026-11-13)
this PR: fix/occlusion-queries @ 3b651bb8 (measured as 3f67aad0: the same code; 3b651bb8 only 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 from RX_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=0 against unset with RX_VK_OCC_LOG=1.

main @ 839e85ff this PR @ 3b651bb8
Kokiri standing: draws a frame 2,862 2,632 (-8%)
Kokiri standing: GPU time a frame 10.10 ms 10.10 ms
Kokiri standing: frame time median / p99 / max 16.54 / 19.95 / 23.4 ms 15.95 / 18.60 / 24.8 ms
Kokiri turning: draws 1,928 1,767 (-8%)
Kokiri turning: GPU time 9.00 ms 9.25 ms
Kokiri turning: frame time median / p99 / max 14.43 / 19.00 / 22.8 ms 14.35 / 18.35 / 25.4 ms
Market standing: draws 2,926 2,464 (-16%)
Market standing: GPU time 8.60 ms 8.07 ms
Market standing: frame time median / p99 / max 16.55 / 27.45 / 33.0 ms 15.34 / 26.10 / 36.8 ms
Market turning: draws (varies with where the turn ends) 1,008 1,104
Market turning: GPU time 5.38 ms 5.42 ms
Market turning: frame time median / p99 / max 12.84 / 29.15 / 71.0 ms 12.82 / 28.25 / 46.3 ms

Per 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):

Market standing GPU time Market standing draws
main 8.20 ms (4 runs) 2,935
every report run counted, as #23 does (RX_VK_OCC_QUERIES=2 here) 8.75 ms (4 runs) 2,354
queries made, counts not used (RX_VK_OCC_QUERIES=2 RX_VK_OCC_DRY=1) 10.12 ms (2 runs) 2,908
depth-only runs only (this PR), a quieter hour 7.30 ms (2 runs; 7.55 for every run counted, alternated) 2,446

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.

main, Market standing:      vk: 120 presents, ... a frame: 2938 draws ...   vk: GPU time 8.7 ms a frame
this PR, Market standing:   vk: 120 presents, ... a frame: 2461 draws ...   vk: GPU time 7.8 ms a frame
this PR, RX_VK_OCC_LOG=1:   vk: occlusion queries: 5061 reports written (2566 of them 0, 50.7%), 0 left visible (a draw did not run), 0 reported again first; written 1.1 recordings after the report; 111 waiting; 86 reads (67 wrote), 4.5 us each

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 with RX_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.

this PR @ 3b651bb8, RX_VK_OCC_QUERIES=0 this PR @ 3b651bb8
Kokiri standing: draws / GPU / frame median 2,861 / 33.9 ms / 44.4 ms 2,617 / 33.1 ms / 44.2 ms
Kokiri turning: draws / GPU / frame median 2,060 / 30.0 ms / 43.1 ms 1,862 / 29.05 ms / 42.6 ms
Market standing: draws / GPU / frame median / fps 2,929 / 30.1 ms / 42.5 ms / 22.4 2,486 / 26.7 ms / 38.8 ms / 25.0
Market turning: GPU / frame median / p99 16.6 ms / 24.4 ms / 210 ms 15.85 ms / 24.8 ms / 176 ms

That 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.py on 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:

  • AMD (RADV and Windows): Kokiri Forest and the Market, standing and turning, RX_VK_OCC_QUERIES=0 against unset, expecting fewer draws, no objects missing and no flicker (T-KEFP)
  • Steam Deck: the same, expecting no hang and no frame-time loss (T-5BXS)
  • Windows NVIDIA: the same, expecting the GPU time and frame time not to go up while the camera turns (https://github.com/NotRexed/Hookshot/pull/2871's RTX 3080 saw +25% GPU time turning with every run counted and shaders not prepared) (T-DG3Z)

Screenshots

Nothing changes on screen by design: hidden objects are left out. Market entrance, standing, main @ 839e85ff and 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=0 turns it all off (the reports read 4096 as before); reverting the commit is enough.

Reviewer notes

Start with occ_collect() and occ_take() in gpu/vk/vk_replay.cpp. This supersedes #23 if it is taken (it contains #23's change); #36 (live switches, with RX_VK_OCC_QUERIES as 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

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • 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).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • 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 noted, and the switch reference updated (docs/development.md, the README).
  • A row in docs/tested-configurations.md for each measured run.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (The renderer: built and run on Linux; longshot_vk.dll built with llvm-mingw 20261104 from Linux (RX_TARGET=windows RX_VK_ONLY=1), not run on Windows. No paths or platform code changed.)
## 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 - replay (`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 in `NvnState` so 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. - replay, new here: - `occ_collect()` reads **every** waiting report's queries with one `vkGetQueryPoolResults` (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 one `vkResetQueryPool`. #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. - Only depth-only draws are counted (`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=2` counts them too. - On by default where the device has `hostQueryReset` (core in Vulkan 1.2); `RX_VK_OCC_QUERIES=0` turns it off, and so does setting `RX_VK_OCC_FIXED=<n>` (that value, as before). - `RX_VK_OCC_LOG=1` also 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. - draw (`gpu/vk/vk_draw.cpp` `draw()`, `gpu/vk/vk_draw.h` `DrawArgs`): the query begins once the pass is open and ends as the draw returns (`OccGuard`), before `each_guard` can end the pass. Precise where the device has `occlusionQueryPrecise`. - device (`gpu/vk/vk_main.cpp`, `gpu/vk/vk_internal.h`): `occlusion_precise`, `host_query_reset`. - tests: `tests/test_vk_occlusion_queries.py` runs 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.cpp` stubs `occ_collect`. - docs: the switches in `docs/development.md` ("Switches for testing and debugging"); a row in `docs/tested-configurations.md`. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `RX_VK_OCC_QUERIES` | The game's samples-passed reports get the GPU's counts for its depth-only draws, so it leaves out hidden objects. `0`: off (every report 4096, as before); `2`: runs with colour draws counted too | on (`1`) | on by default | RTX 3090 Linux: Market standing (-16% draws, -1.2 ms frame median), Kokiri standing (-0.6 ms); RX 580 RADV: Market standing (-11% GPU time, +12% fps) | camera turning: no change measured | AMD on Windows, Intel, the Steam Deck, Windows | | `RX_VK_OCC_FIXED=<n>` | Every report reads n (existing); setting it now also turns the queries off | unset (4096 when the queries are off) | unchanged | - | - | - | | `RX_VK_OCC_LOG` | Every ~5,000 reports: how many read 0, how late, the reads' cost, the counts' sizes | off | off (a developer switch) | - | - | - | | `RX_VK_OCC_DRY` | A test: the queries made and read, the reports left at 4096 (times the queries alone) | off | off | - | - | - | ## Measurements main: 839e85ff (2026-11-13) this PR: fix/occlusion-queries @ 3b651bb8 (measured as 3f67aad0: the same code; 3b651bb8 only 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 from `RX_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=0` against unset with `RX_VK_OCC_LOG=1`. | | main @ 839e85ff | this PR @ 3b651bb8 | |---|---|---| | Kokiri standing: draws a frame | 2,862 | 2,632 (-8%) | | Kokiri standing: GPU time a frame | 10.10 ms | 10.10 ms | | Kokiri standing: frame time median / p99 / max | 16.54 / 19.95 / 23.4 ms | 15.95 / 18.60 / 24.8 ms | | Kokiri turning: draws | 1,928 | 1,767 (-8%) | | Kokiri turning: GPU time | 9.00 ms | 9.25 ms | | Kokiri turning: frame time median / p99 / max | 14.43 / 19.00 / 22.8 ms | 14.35 / 18.35 / 25.4 ms | | Market standing: draws | 2,926 | 2,464 (-16%) | | Market standing: GPU time | 8.60 ms | 8.07 ms | | Market standing: frame time median / p99 / max | 16.55 / 27.45 / 33.0 ms | 15.34 / 26.10 / 36.8 ms | | Market turning: draws (varies with where the turn ends) | 1,008 | 1,104 | | Market turning: GPU time | 5.38 ms | 5.42 ms | | Market turning: frame time median / p99 / max | 12.84 / 29.15 / 71.0 ms | 12.82 / 28.25 / 46.3 ms | Per 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): | | Market standing GPU time | Market standing draws | |---|---|---| | main | 8.20 ms (4 runs) | 2,935 | | every report run counted, as #23 does (`RX_VK_OCC_QUERIES=2` here) | 8.75 ms (4 runs) | 2,354 | | queries made, counts not used (`RX_VK_OCC_QUERIES=2 RX_VK_OCC_DRY=1`) | 10.12 ms (2 runs) | 2,908 | | depth-only runs only (this PR), a quieter hour | 7.30 ms (2 runs; 7.55 for every run counted, alternated) | 2,446 | 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. ``` main, Market standing: vk: 120 presents, ... a frame: 2938 draws ... vk: GPU time 8.7 ms a frame this PR, Market standing: vk: 120 presents, ... a frame: 2461 draws ... vk: GPU time 7.8 ms a frame this PR, RX_VK_OCC_LOG=1: vk: occlusion queries: 5061 reports written (2566 of them 0, 50.7%), 0 left visible (a draw did not run), 0 reported again first; written 1.1 recordings after the report; 111 waiting; 86 reads (67 wrote), 4.5 us each ``` ### 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 with `RX_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. | | this PR @ 3b651bb8, `RX_VK_OCC_QUERIES=0` | this PR @ 3b651bb8 | |---|---|---| | Kokiri standing: draws / GPU / frame median | 2,861 / 33.9 ms / 44.4 ms | 2,617 / 33.1 ms / 44.2 ms | | Kokiri turning: draws / GPU / frame median | 2,060 / 30.0 ms / 43.1 ms | 1,862 / 29.05 ms / 42.6 ms | | Market standing: draws / GPU / frame median / fps | 2,929 / 30.1 ms / 42.5 ms / 22.4 | 2,486 / 26.7 ms / 38.8 ms / 25.0 | | Market turning: GPU / frame median / p99 | 16.6 ms / 24.4 ms / 210 ms | 15.85 ms / 24.8 ms / 176 ms | That 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.py` on 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:** - AMD (RADV and Windows): Kokiri Forest and the Market, standing and turning, `RX_VK_OCC_QUERIES=0` against unset, expecting fewer draws, no objects missing and no flicker (T-KEFP) - Steam Deck: the same, expecting no hang and no frame-time loss (T-5BXS) - Windows NVIDIA: the same, expecting the GPU time and frame time not to go up while the camera turns (https://github.com/NotRexed/Hookshot/pull/2871's RTX 3080 saw +25% GPU time turning with every run counted and shaders not prepared) (T-DG3Z) ## Screenshots Nothing changes on screen by design: hidden objects are left out. Market entrance, standing, main @ 839e85ff and 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=0` turns it all off (the reports read 4096 as before); reverting the commit is enough. ## Reviewer notes Start with `occ_collect()` and `occ_take()` in `gpu/vk/vk_replay.cpp`. This supersedes #23 if it is taken (it contains #23's change); #36 (live switches, with `RX_VK_OCC_QUERIES` as 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 - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [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. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [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. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] A row in `docs/tested-configurations.md` for each measured run. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (The renderer: built and run on Linux; `longshot_vk.dll` built with llvm-mingw 20261104 from Linux (`RX_TARGET=windows RX_VK_ONLY=1`), not run on Windows. No paths or platform code changed.)
fix(vk): give the game's occlusion reports the GPU's counts, so it culls what is hidden
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 10s
CI / Workflow lint (actionlint) (pull_request) Successful in 13s
Third-party parts / As their notes say (pull_request) Successful in 29s
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 3s
3b651bb805
Every samples-passed report read 4096, so Ocarina of Time never left out an
object hidden behind something. Each depth-only draw from a samples-passed
reset to its report now gets a Vulkan occlusion query. As each recording
begins, the replay thread reads every waiting report's queries with one
vkGetQueryPoolResults (no wait), writes the sum over the reports whose queries
have all ended, and resets them with one vkResetQueryPool. A count not known
yet stays 4096 (visible); so does a run with a draw that writes colour, where a
query's end held the GPU up for little gain. On by default;
RX_VK_OCC_QUERIES=0 or RX_VK_OCC_FIXED=<n> turn it off, =2 counts every run.

Adapted from jordanella's Hookshot draft
(https://github.com/NotRexed/Hookshot/pull/2871): the per-report reads and
resets batched, the counts written as soon as they are ready, and the
colour runs left out.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
HumanBean changed title from WIP: fix(vk): give the game's occlusion reports the GPU's counts, so it culls what is hidden to fix(vk): give the game's occlusion reports the GPU's counts, so it culls what is hidden 2026-11-13 13:15:00 +00:00
Member

Yes: #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.

Yes: #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.
Author
Owner

AMD results, on the lab's RX 580 (RADV, no mesh shaders, i7-9700K). The same build of 3b651bb8 with RX_VK_OCC_QUERIES=0 against the default, 4 alternated runs a side:

  • Market standing: 2,929 -> 2,486 draws, GPU 30.1 -> 26.7 ms, frame median 42.5 -> 38.8 ms, 22.4 -> 25.0 fps.
  • Kokiri standing: GPU 33.9 -> 33.1 ms.
  • Market turning: GPU 16.6 -> 15.85 ms, p99 210 -> 176 ms.

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.)

AMD results, on the lab's RX 580 (RADV, no mesh shaders, i7-9700K). The same build of 3b651bb8 with RX_VK_OCC_QUERIES=0 against the default, 4 alternated runs a side: - Market standing: 2,929 -> 2,486 draws, GPU 30.1 -> 26.7 ms, frame median 42.5 -> 38.8 ms, 22.4 -> 25.0 fps. - Kokiri standing: GPU 33.9 -> 33.1 ms. - Market turning: GPU 16.6 -> 15.85 ms, p99 210 -> 176 ms. 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.)
HumanBean deleted branch fix/occlusion-queries 2026-11-13 15:32:02 +00:00
Contributor

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:

  • Pairs: one warm-up run for shaders, then four runs interleaved: RX_VK_OCC_QUERIES=0, unset, =0, unset.
  • Each run: Kokiri Forest, then the Market, by the debug teleport. 25 s to settle, 30 s standing, 30 s turning the camera at full speed (right stick held at 1, 0).
  • Numbers: from HOS_FPS_LOG, per phase: the median of vk: GPU time ... ms a frame (12-15 lines a phase), of vi: ... fps over the last 5.0 s (6 a phase), and of the draws in vk: N presents ...; a frame: D draws.
Phase GPU ms a frame, queries off (run 1, run 2) queries on (run 1, run 2) draws a frame, off → on (means)
Kokiri, standing 6.60, 6.60 6.50, 6.50 937 → 824
Kokiri, turning 5.90, 6.10 5.55, 5.40 798 → 673
Market, standing 4.20, 4.10 3.95, 3.85 936 → 839
Market, turning 4.20, 4.40 4.00, 3.95 934 → 818
  • GPU time: lower with the queries on in every phase and both pairs; while turning, -0.5 ms in Kokiri (about -9%) and -0.3 ms at the Market. No rise anywhere, unlike the +25% #2871 saw counting every run.
  • Draws: 10-16% fewer.
  • Frame time: unchanged within noise. This PC is CPU-bound here at 50-56 fps uncapped (frame time 17.6-20.5 ms off, 17.6-20.4 ms on, with run-to-run spread larger than any difference), so the GPU saving doesn't show as fps.
  • Errors: none in any run (no device-lost or die lines). Every run logs 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.

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: - **Pairs:** one warm-up run for shaders, then four runs interleaved: `RX_VK_OCC_QUERIES=0`, unset, `=0`, unset. - **Each run:** Kokiri Forest, then the Market, by the debug teleport. 25 s to settle, 30 s standing, 30 s turning the camera at full speed (right stick held at 1, 0). - **Numbers:** from `HOS_FPS_LOG`, per phase: the median of `vk: GPU time ... ms a frame` (12-15 lines a phase), of `vi: ... fps over the last 5.0 s` (6 a phase), and of the draws in `vk: N presents ...; a frame: D draws`. | Phase | GPU ms a frame, queries off (run 1, run 2) | queries on (run 1, run 2) | draws a frame, off → on (means) | |---|---|---|---| | Kokiri, standing | 6.60, 6.60 | 6.50, 6.50 | 937 → 824 | | **Kokiri, turning** | 5.90, 6.10 | **5.55, 5.40** | 798 → 673 | | Market, standing | 4.20, 4.10 | 3.95, 3.85 | 936 → 839 | | **Market, turning** | 4.20, 4.40 | **4.00, 3.95** | 934 → 818 | - **GPU time:** lower with the queries on in every phase and both pairs; while turning, -0.5 ms in Kokiri (about -9%) and -0.3 ms at the Market. No rise anywhere, unlike the +25% #2871 saw counting every run. - **Draws:** 10-16% fewer. - **Frame time:** unchanged within noise. This PC is CPU-bound here at 50-56 fps uncapped (frame time 17.6-20.5 ms off, 17.6-20.4 ms on, with run-to-run spread larger than any difference), so the GPU saving doesn't show as fps. - **Errors:** none in any run (no device-lost or die lines). Every run logs `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.
Member

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, against RX_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.py with shaders prepared (Compile shaders: Most, the default), default options (no options file), HOS_FPS_MAX=240, headless, muted
Method: #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=1 on 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 from RX_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=0 #197 (unset)
Kokiri standing: draws a frame 1,566 1,342 (-14%)
Kokiri standing: GPU time a frame 10.25 ms 9.90 ms
Kokiri standing: frame time median / p99 / max 33.01 / 55.63 / 178.5 ms 31.70 / 56.16 / 91.2 ms
Kokiri turning: draws a frame 2,466 2,114 (-14%)
Kokiri turning: GPU time a frame 12.20 ms 11.50 ms
Kokiri turning: frame time median / p99 / max 38.47 / 61.84 / 130.1 ms 36.08 / 57.44 / 134.4 ms
Market standing: draws a frame 2,648 2,330 (-12%)
Market standing: GPU time a frame 10.38 ms 9.30 ms
Market standing: frame time median / p99 / max 39.64 / 64.45 / 89.6 ms 35.41 / 54.56 / 81.3 ms
Market turning: draws a frame 1,574 1,328 (-16%)
Market turning: GPU time a frame 9.28 ms 8.50 ms
Market turning: frame time median / p99 / max 36.85 / 111.37 / 264.9 ms 35.62 / 63.98 / 117.9 ms

Per run (frame median ms; GPU ms):

kokiri turning   off: 30.06 39.39 38.47 36.82 38.88; GPU 10.65 13.15 11.90 12.20 12.40
kokiri turning   on:  36.19 41.55 35.97 33.59;       GPU 11.60 11.70 11.40 11.20
market turning   off: 35.81 32.67 34.10 35.34 44.96 42.03 37.89 42.31; GPU 8.65 8.60 8.60 8.30 14.50 10.75 9.90 10.40
market turning   on:  31.28 31.33 35.49 35.76 37.81 43.25 37.52 33.71; GPU 7.75 8.80 7.50 8.20 9.60 20.40 8.90 8.15
market standing  off: 38.49 36.83 37.72 39.36 43.78 40.42 40.27 39.92; GPU 9.90 10.60 10.40 10.35 10.70 10.05 10.80 10.00
market standing  on:  34.23 33.21 35.42 35.40 39.98 35.65 35.52 35.09; GPU 9.30 9.00 9.70 9.60 9.20 9.30 9.40 9.30

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.

**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, against `RX_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.py` with shaders prepared (Compile shaders: Most, the default), default options (no options file), `HOS_FPS_MAX=240`, headless, muted Method: #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=1` on 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 from `RX_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=0` | #197 (unset) | |---|---|---| | Kokiri standing: draws a frame | 1,566 | 1,342 (-14%) | | Kokiri standing: GPU time a frame | 10.25 ms | 9.90 ms | | Kokiri standing: frame time median / p99 / max | 33.01 / 55.63 / 178.5 ms | 31.70 / 56.16 / 91.2 ms | | Kokiri turning: draws a frame | 2,466 | 2,114 (-14%) | | **Kokiri turning: GPU time a frame** | **12.20 ms** | **11.50 ms** | | Kokiri turning: frame time median / p99 / max | 38.47 / 61.84 / 130.1 ms | 36.08 / 57.44 / 134.4 ms | | Market standing: draws a frame | 2,648 | 2,330 (-12%) | | Market standing: GPU time a frame | 10.38 ms | 9.30 ms | | Market standing: frame time median / p99 / max | 39.64 / 64.45 / 89.6 ms | 35.41 / 54.56 / 81.3 ms | | Market turning: draws a frame | 1,574 | 1,328 (-16%) | | **Market turning: GPU time a frame** | **9.28 ms** | **8.50 ms** | | Market turning: frame time median / p99 / max | 36.85 / 111.37 / 264.9 ms | 35.62 / 63.98 / 117.9 ms | Per run (frame median ms; GPU ms): ``` kokiri turning off: 30.06 39.39 38.47 36.82 38.88; GPU 10.65 13.15 11.90 12.20 12.40 kokiri turning on: 36.19 41.55 35.97 33.59; GPU 11.60 11.70 11.40 11.20 market turning off: 35.81 32.67 34.10 35.34 44.96 42.03 37.89 42.31; GPU 8.65 8.60 8.60 8.30 14.50 10.75 9.90 10.40 market turning on: 31.28 31.33 35.49 35.76 37.81 43.25 37.52 33.71; GPU 7.75 8.80 7.50 8.20 9.60 20.40 8.90 8.15 market standing off: 38.49 36.83 37.72 39.36 43.78 40.42 40.27 39.92; GPU 9.90 10.60 10.40 10.35 10.70 10.05 10.80 10.00 market standing on: 34.23 33.21 35.42 35.40 39.98 35.65 35.52 35.09; GPU 9.30 9.00 9.70 9.60 9.20 9.30 9.40 9.30 ``` 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.
Contributor

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 commit 3b651bb8 applied on 000f19fb (no conflict). One binary, RX_VK_OCC_QUERIES=0 against unset, alternated, RX_VK_OCC_LOG=1 on 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.

off → on draws a frame replay GPU time fps
Kokiri Forest, standing (2 runs each) 2,811 → 2,579 (-8%) 35.9 → 32.6 ms 21.6 → 20.5 ms 21.65 → 22.66
a heavy view, standing (2 runs each) 2,495 → 2,153 (-14%) 32.0 → 25.3 ms 19.5 → 18.5 ms 24.08 → 24.66
Hyrule Field, standing (2 runs each) 1,632 → 1,583 (-3%) 18.5 → 18.6 ms 24.5 → 24.5 ms 23.59 → 23.54
Kokiri Forest, camera turning (1 run each) 2,296 → 2,041 (-11%) 31.5 → 28.9 ms 21.2 → 20.7 ms 21.97 → 21.84
the heavy view, camera turning (1 run each) 1,241 → 1,076 (-13%) 21.0 → 19.0 ms 17.2 → 15.9 ms 26.43 → 27.34

What T-5BXS asked (Steam Deck: no hang, no frame-time loss): it holds. 18 runs, no hang, no FATAL or device lost line, no ring ... timeout in 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.
  • On main the frame rate gains little next to the replay, because this frame waits on read-backs here. Where the replay is the limit it shows: with a local patch that reads the two hot 0x3980-byte ranges without their wait, and #198, on both sides (one binary again, queries off against on): the heavy view 27.43 / 27.38 / 27.20 → 30.02 / 29.87 / 30.05 fps, Kokiri 25.23 / 25.02 → 26.32 / 26.14, Hyrule Field 25.81 / 25.94 → 26.18 / 26.33, the heavy view with the camera turning 30.67 → 32.86.
off, Kokiri standing:  720 presents: 21.86 fps, 2830 draws, replay 35.58 ms, GPU 21.63 ms
on,  Kokiri standing:  720 presents: 22.89 fps, 2525 draws, replay 32.28 ms, GPU 20.30 ms
on,  Kokiri standing:  vk: occlusion queries: 5350 reports written (1501 of them 0, 28.1%), 0 left visible (a draw did not run), 0 reported again first; written 1.5 recordings after the report; 112 waiting; 77 reads (55 wrote), 31.6 us each
on,  heavy view:       vk: occlusion queries: 5084 reports written (2450 of them 0, 48.2%), 0 left visible (a draw did not run), 0 reported again first; written 1.2 recordings after the report; 99 waiting; 83 reads (62 wrote), 27.7 us each
**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 commit 3b651bb8 applied on 000f19fb (no conflict). One binary, `RX_VK_OCC_QUERIES=0` against unset, alternated, `RX_VK_OCC_LOG=1` on 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. | off → on | draws a frame | replay | GPU time | fps | |---|---|---|---|---| | Kokiri Forest, standing (2 runs each) | 2,811 → 2,579 (-8%) | 35.9 → 32.6 ms | 21.6 → 20.5 ms | 21.65 → 22.66 | | a heavy view, standing (2 runs each) | 2,495 → 2,153 (-14%) | 32.0 → 25.3 ms | 19.5 → 18.5 ms | 24.08 → 24.66 | | Hyrule Field, standing (2 runs each) | 1,632 → 1,583 (-3%) | 18.5 → 18.6 ms | 24.5 → 24.5 ms | 23.59 → 23.54 | | Kokiri Forest, camera turning (1 run each) | 2,296 → 2,041 (-11%) | 31.5 → 28.9 ms | 21.2 → 20.7 ms | 21.97 → 21.84 | | the heavy view, camera turning (1 run each) | 1,241 → 1,076 (-13%) | 21.0 → 19.0 ms | 17.2 → 15.9 ms | 26.43 → 27.34 | **What T-5BXS asked (Steam Deck: no hang, no frame-time loss): it holds.** 18 runs, no hang, no `FATAL` or `device lost` line, no `ring ... timeout` in 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. - On main the frame rate gains little next to the replay, because this frame waits on read-backs here. Where the replay is the limit it shows: with a local patch that reads the two hot 0x3980-byte ranges without their wait, and https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/198, on both sides (one binary again, queries off against on): the heavy view 27.43 / 27.38 / 27.20 → 30.02 / 29.87 / 30.05 fps, Kokiri 25.23 / 25.02 → 26.32 / 26.14, Hyrule Field 25.81 / 25.94 → 26.18 / 26.33, the heavy view with the camera turning 30.67 → 32.86. ``` off, Kokiri standing: 720 presents: 21.86 fps, 2830 draws, replay 35.58 ms, GPU 21.63 ms on, Kokiri standing: 720 presents: 22.89 fps, 2525 draws, replay 32.28 ms, GPU 20.30 ms on, Kokiri standing: vk: occlusion queries: 5350 reports written (1501 of them 0, 28.1%), 0 left visible (a draw did not run), 0 reported again first; written 1.5 recordings after the report; 112 waiting; 77 reads (55 wrote), 31.6 us each on, heavy view: vk: occlusion queries: 5084 reports written (2450 of them 0, 48.2%), 0 left visible (a draw did not run), 0 reported again first; written 1.2 recordings after the report; 99 waiting; 83 reads (62 wrote), 27.7 us each ```
Author
Owner

AMD lab, round 2 and correctness (RX 580, RADV, the compute mesh fallback). Both sides built on that PC from a player's build.

  • Main 839e85ff against #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.
  • Flash detector: 0 flashes on either side. The standing screenshots differ between sides no more than between runs of one side, and no objects are missing. No hang. The same 11 failed programs as on main.
  • Vulkan validation does not pass. With culling off: 0 errors. With #197: about 25k VUID-vkResetQueryPool-firstQuery-02741 (a host reset of a query still in use by a command buffer) and 2.64M VUID-vkCmdBeginQuery-None-00807 (query not reset). That fits this PR's code review: queries numbered per game queue collide, and a host reset can come before the submission that used the query has finished. On the RTX 3090, main shows 8,000 query VUIDs, and the follow-up fix shows 0 (being finished now; it also checks the in-use reset).
    Until the fix lands, RX_VK_OCC_QUERIES=0 turns culling off if anything goes missing.
AMD lab, round 2 and correctness (RX 580, RADV, the compute mesh fallback). Both sides built on that PC from a player's build. - Main 839e85ff against #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. - Flash detector: 0 flashes on either side. The standing screenshots differ between sides no more than between runs of one side, and no objects are missing. No hang. The same 11 failed programs as on main. - **Vulkan validation does not pass.** With culling off: 0 errors. With #197: about 25k VUID-vkResetQueryPool-firstQuery-02741 (a host reset of a query still in use by a command buffer) and 2.64M VUID-vkCmdBeginQuery-None-00807 (query not reset). That fits this PR's code review: queries numbered per game queue collide, and a host reset can come before the submission that used the query has finished. On the RTX 3090, main shows 8,000 query VUIDs, and the follow-up fix shows 0 (being finished now; it also checks the in-use reset). Until the fix lands, RX_VK_OCC_QUERIES=0 turns culling off if anything goes missing.
Author
Owner

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.

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.
First-time contributor

Steam Deck (one Deck, 6 runs on, 2 off): no measurable gain; p95 and hitches worse in both interleaved on-runs.

Setup:

  • Hardware and OS: Steam Deck OLED, SteamOS 3.9.5 (kernel 7.2.7-valve2), RADV Mesa 26.2.0-devel.
  • Build: main 0877184, which has this PR, plus the Steam Deck profile (#225, head 0d372bf). Plain main runs out of memory on the Deck.
  • Settings: the user's options (Auto (this PC)), stock clocks, Gaming Mode 1280x720.
  • Route: the same moving Kokiri Forest route each time, warm caches, memory guard on.
  • Measured: real (game) fps, shown fps and frame times over the measured window.

Interleaved pair, RX_VK_OCC_QUERIES=0 against the default (on):

run occlusion queries real fps shown fps p95 hitches over 100 ms draws a frame
1 off 15.0 27.8 90 ms 91 1611
2 on 13.2 24.5 101 ms 103 1547
3 off 14.2 26.5 79 ms 62 1584
4 on 15.4 28.2 96 ms 100 1154

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.

  • Real fps: median 13.8 with the queries on (6 runs) against 14.6 off (2 runs). That's inside this Deck's run-to-run spread of about ±1 fps, so no measurable difference either way.
  • Draws: the queries cut them clearly in one run (1154). The frame on the Deck is bound by the readback and GPU waits, not by the replay's draw count, so fewer draws don't show up as fps.
  • p95 and hitches: worse in both interleaved on-runs (96-101 ms and 100-103 hitches, against 79-90 ms and 62-91 off). The PR's note that a query's end waits for its draw may cost more on the Deck's GPU. With 2 runs each that's a lead, not a result.
  • Void first pair: my first "off" pair did not reach the game. The harness wrote the variable into 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 no vk: occlusion queries on line, and the on-runs do.

Not covered: other scenes (the Market, where the PR measured its gain), standing still, and more runs.

**Steam Deck (one Deck, 6 runs on, 2 off): no measurable gain; p95 and hitches worse in both interleaved on-runs.** **Setup:** - Hardware and OS: Steam Deck OLED, SteamOS 3.9.5 (kernel 7.2.7-valve2), RADV Mesa 26.2.0-devel. - Build: main 0877184, which has this PR, plus the Steam Deck profile (#225, head 0d372bf). Plain main runs out of memory on the Deck. - Settings: the user's options (Auto (this PC)), stock clocks, Gaming Mode 1280x720. - Route: the same moving Kokiri Forest route each time, warm caches, memory guard on. - Measured: real (game) fps, shown fps and frame times over the measured window. Interleaved pair, `RX_VK_OCC_QUERIES=0` against the default (on): | run | occlusion queries | real fps | shown fps | p95 | hitches over 100 ms | draws a frame | |---|---|---|---|---|---|---| | 1 | off | 15.0 | 27.8 | 90 ms | 91 | 1611 | | 2 | on | 13.2 | 24.5 | 101 ms | 103 | 1547 | | 3 | off | 14.2 | 26.5 | 79 ms | 62 | 1584 | | 4 | on | 15.4 | 28.2 | 96 ms | 100 | 1154 | 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. - **Real fps:** median 13.8 with the queries on (6 runs) against 14.6 off (2 runs). That's inside this Deck's run-to-run spread of about ±1 fps, so no measurable difference either way. - **Draws:** the queries cut them clearly in one run (1154). The frame on the Deck is bound by the readback and GPU waits, not by the replay's draw count, so fewer draws don't show up as fps. - **p95 and hitches:** worse in both interleaved on-runs (96-101 ms and 100-103 hitches, against 79-90 ms and 62-91 off). The PR's note that a query's end waits for its draw may cost more on the Deck's GPU. With 2 runs each that's a lead, not a result. - **Void first pair:** my first "off" pair did not reach the game. The harness wrote the variable into `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 no `vk: occlusion queries on` line, and the on-runs do. **Not covered:** other scenes (the Market, where the PR measured its gain), standing still, and more runs.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
5 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!197
No description provided.