perf(vk): Arc cards read the occlusion counts from a GPU copy: Castle Town's market from 34.8 to 45.8 fps on a B580 #761
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/tooling
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
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/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!761
Loading…
Reference in a new issue
No description provided.
Delete branch "nyannoying/Longshot:perf/vk-occ-copy"
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?
Type: perf · Risk: low · Tested on: Windows 11, Arc B580 · Needs: Linux, any GPU (a build and a short run with
RX_VK_OCC_COPY=1)Why
In busy places like Castle Town's market the game waits on the CPU, not the GPU. On an Arc B580 about a quarter of
the render thread's time went into reading back the occlusion counts: the numbers the game uses to leave out objects
hidden behind others (the GPU's real counts, #122). Intel's Windows driver takes ~3.4 µs for each count it is asked
for, and the replay asked for about 2,000 a frame: 7.4 ms of a 28.8 ms frame. With this PR the GPU writes the counts
into ordinary memory itself, and reading them costs next to nothing: the market runs at 45.8 fps instead of 34.8 on
the B580.
See #122.
What changed
At the end of each recording the GPU copies the results of the occlusion queries that recording ran into a buffer in
host memory (
vkCmdCopyQueryPoolResults), and the harvest reads them there as plain memory instead of asking thedriver for each one. A count is taken only once its availability word and its value are both written; whatever a copy
didn't have is read from the query pool once its submission is done, as before. On by default with Intel's Windows
driver on a discrete GPU (Arc), where it was measured;
RX_VK_OCC_COPY=0or1turns it off or on anywhere.gpu/vk/vk_replay.cpp:occ_copy_wanted()(the default and the switch); the host bufferocc_host, made inocc_queries_on()(host-cached and coherent, else the pool is read);occ_copy_part(), called after therecording's end barrier in
maybe_split()andfinish_recording(), copies the queriesocc_take()handed out inthat recording (
occ_part_lo/occ_part_hi, forgotten by everysubmit_recording()) and makes them the host's witha transfer → host barrier;
occ_collect()reads the copy, and the pool (vkGetQueryPoolResults) only for what thecopy didn't have, once the submission is done;
occ_reset()clears the host words with the queries; theRX_VK_OCC_LOGline counts the copies and the pool reads after a copy.gpu/vk/vk_recorder.h,gpu/vk/vk_work.h:vkCmdCopyQueryPoolResultsgoes through the recorder like the othercommands, and counts as work for
full_barrier.gpu/vk/vk_vma.cpp,gpu/vk/vk_internal.h:host_coherent().tests/test_vk_occlusion_queries.py: the ring's harvest from the copies (no pool read when the copy has the count;the pool read for a query not done at the copy and for a recording that ended without one; a count without its
availability word, or an availability word without its count, not taken; a never-drawn query stays visible; two
copies across the ring's end; the host words cleared at the reset),
occ_copy_wanted()for each kind of GPU and theswitch, and the wiring (the copy after the end barrier and before the submit, the range forgotten at every submit).
tests/recorder_sync_test.cpp,tests/draw_stream_visibility_test.cpp,tests/fsr_image_retirement_test.cpp: thechecks that cut
submit_recording,finish_recordingand the recorder's calls out of the source know the new names;the recorder's check records the copy through the recorder like the other packed calls.
docs/development.md: the copy andRX_VK_OCC_COPY, with the numbers;docs/tested/: the 6 runs.Measurements
Verdict: faster on the Arc B580 in the market: 34.8 → 45.8 fps average (+32%), 1% low 22.7 → 25.3 fps, median
frame time 27.59 → 20.41 ms; the counts' reads went from 7.43 to 0.11 ms a frame, for 2.0 ms more GPU time a frame
(10.2 → 12.2 ms). The picture is the same.
Baseline:
RX_VK_OCC_COPY=0on the same build, which reads the query pool exactly asmaindoes (none of the new coderuns). Both columns are one build: 690d8446, this PC's game folder (
mainat2b1d38c6with my open #443, #446, #459 and #500merged), with this PR's diff applied:
main's renderer (MKWVK_API_VERSION23 since #729) doesn't load in thatfolder's host (22), and a new folder for
mainmeans preparing the 38,809 shaders again. The shader stamp is the sameat
2b1d38c6, 690d8446 and9e6f231d, and neither the four PRs normainsince2b1d38c6touches the occlusion code.Measured before the rebase onto
a00c485c, which adds only an agents' skill (#742): the numbers hold.Hardware: Windows 11 Pro Insider 10.0.29704, Intel Arc B580 12 GB (driver 32.0.101.9154), Core i5-13600K, 64 GB
Scene: Castle Town's market as child Link by day, standing at the entrance (the control bridge's
teleport Market_Child); 1280x720 window, render 75% with Lanczos, frame generation off, V-Sync Auto, the console's 60 fps cap(never reached)
Method: 3 interleaved pairs, each from a fresh start (on, off, off, on, on, off): 45 s after the teleport, then 45 s
measured. fps and frame times from
RX_VK_FRAME_LOG(every present of the 45 s; the 1% low is the rate of the slowest1% of frames); the render thread's time from
HOS_PROFILE=5; the counts' reads, copies and zeros fromRX_VK_OCC_LOG=1; GPU time from the log'sGPU timeline (timestamps around each recording). Each cell is the medianof the arm's 3 runs, the runs in brackets.
How to reproduce:
python -I scripts/longshot_ctl.py launch <game folder> --env RX_VK_OCC_LOG=1 --env RX_VK_FRAME_LOG=frames.csv [--env RX_VK_OCC_COPY=0], thenlongshot_ctl.py teleport Market_Child, 45 s, and 45 smeasured.
RX_VK_OCC_COPY=0(the pool read, as onmain): this PR @c0da900ec0da900eThe render thread's submits take 4 ms more a frame with the copies (2.8 → 6.8 ms) while its other jobs drop from 23.0
to 11.9 ms: most likely the wait for the recorder thread, which each pool read began with (
vkGetQueryPoolResultsdrains it), now comes at the submit instead. Where the 2 ms of GPU time go, from a diagnostic build (690d8446 with this
change and my other work in progress, one 30 s run each, not part of this PR): the copies without the host barrier
12.2 ms, the barrier without the copies 11.2 ms, the copies without the availability words 12.7 ms, both 12.2 ms,
neither 10.2 ms. It is the copies themselves, ~1.4 µs a query on the GPU; the barrier adds nothing next to them.
Flags and settings
One new switch,
RX_VK_OCC_COPY, on by default where it was measured.RX_VK_OCC_COPYRX_VK_PARALLEL) and without coherent host memoryTesting
Tested:
python -I -B tests/run_all.pywith the toolchain's Vulkan headers (RX_CPM), on this PR andon
main9e6f231don the same PC (before the rebase ontoa00c485c, which changes no code): 540 passed, 18 failed, 35 skipped on both, the same 18 failing the same way onmain(e.g.test_env_ononRX_VK_PRESENT_AFTER_SIGNAL,test_tested_configurations' CLI pipe on Windows lineends); the 35 skipped run on Linux or need spirv-val or glslang.
tests/test_vk_occlusion_queries.pypasses all fourparts, and fails as it should with any of three deliberate bugs put in: the host words not cleared at the reset, an
availability word taken without its count, the range not forgotten at a submit.
(above), no device lost, the same picture each way: nothing missing, nothing extra. Link flashes red in two of the
copies' end-of-run screenshots: that is the game's low-health warning (he is on one heart), and nine shots 0.4 s
apart caught it each way (2 of 9 with the pool read, 1 of 9 with the copies). The log's
copied into host memory at each recording's endline with the default, and 0 counts read from the pool after their copy in every run.Not tested: Linux (the copies are off there by default; the code uses no platform API), Arc A-series cards,
Intel iGPUs, AMD and NVIDIA with
RX_VK_OCC_COPY=1. The parallel replay keeps the pool read.Must be tested:
@Beanstalk linux): build this PR, then play Castle Town's market for a minute withRX_VK_OCC_COPY=1 RX_VK_OCC_LOG=1, expecting the build to pass, the log'socclusion queries online to end withcopied into host memory at each recording's end (RX_VK_OCC_COPY), itsocclusion queries:lines to show copieswith few or no queries read from the pool after their copy, and the picture the same as with
RX_VK_OCC_COPY=0.Risks
What could break: a count read wrong would hide an object that is in view. The GPU writes a count and its
availability word in either order, so a count is taken only when both are there (the test covers both orders), and an
unknown count stays "visible" as before. The copies cost GPU time: 2 ms a frame in the market on the B580 at 75% of
720p, which a GPU-bound Arc (an A380, or 4K) would pay in frames. The counts land ~2.2 recordings after their report
instead of ~0.6, still well inside the frame or two the game waits (the share read 0 is the same: 53.5% with the
copies, 53.9% without).
Turning it off:
RX_VK_OCC_COPY=0.Compatibility: Nothing changes: no option, cache or save format, and the renderer's interface is the same
(
MKWVK_API_VERSIONunchanged).Issues
See #122
Checklist
Everyone
type(scope): description) of the status line's type.main.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md).tests/that reads changed source updated in this PR, not weakened.Farm's Linux run under Must be tested builds it.)
🤖 Generated with Claude Code
a00c485cMeasurements' columns renamed to the 'Before, variant: this PR @ hash' form the description check asks for; both are one build (690d8446 with this PR's diff), as the Baseline line says.
@Beanstalk linux, any GPU: build this PR, then play Castle Town's market (child, by day) for a minute with RX_VK_OCC_COPY=1 RX_VK_OCC_LOG=1. Expected: the build passes; the log's 'occlusion queries on' line ends with 'copied into host memory at each recording's end (RX_VK_OCC_COPY)'; its 'occlusion queries:' lines show copies with few or no queries read from the pool after their copy; the picture is the same as with RX_VK_OCC_COPY=0 (nothing missing, nothing extra). Please also say the fps both ways if you have them: the copies are off by default there and this tells whether they would pay on that GPU.
@nyannoying only members of
BigBeanscan start the farm. One of them can approve this request by reacting 👍 to it.WIP: perf(vk): Arc cards read the occlusion counts from a GPU copy: Castle Town's market from 34.8 to 45.8 fps on a B580to perf(vk): Arc cards read the occlusion counts from a GPU copy: Castle Town's market from 34.8 to 45.8 fps on a B580dukenheimer referenced this pull request2026-11-15 15:19:00 +00:00
dukenheimer referenced this pull request2026-11-15 15:21:15 +00:00