WIP: perf(vk): images the renderer lets go of are destroyed on a thread of their own #792
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
risk
high
risk
low
risk
medium
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!792
Loading…
Reference in a new issue
No description provided.
Delete branch "dyst/Longshot:perf/image-reaper"
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: medium · Tested on: Windows 11 (game-free checks) · Needs: Windows, NVIDIA (any RTX), turning the camera in the Market and Hyrule Field; any GPU for the compaction
Why
The game stutters while the camera turns. Part of that is the renderer freeing GPU images on its own thread: when it lets go of textures and render targets,
texture_framedestroys each image, its views, buffer and memory right there, and the render-target pool's trim destroys up to 16 more a present. On NVIDIA's Windows driver avmaDestroyImageis a kernel call of a millisecond and more, and a turn of the camera lets go of many in one present.Project ReLink, a fork of Longshot, measured 2.4 ms of each frame over 25 ms spent there, against 0.1 ms of a normal frame. Moving the frees to a thread of their own cut its frames over 25 ms and its 99th percentile (31689010, 5224e821).
What changed
The images the renderer lets go of are destroyed on a reaper thread. The textures' compaction rule is kept: nothing is freed while a pass is open.
vk:
destroy_image_later(gpu/vk/vk_vma.cpp) queues an image with its views, buffer and memory objects for a reaper thread, started on first use.vma_init). The images handed over are ones nothing can use again: their last recorded use has finished.vk: the compaction rule.
compact_in_passunderreap_mu.image_compact_passtakes that lock beforevmaBeginDefragmentationPass, andimage_compact_pass_endholds it throughvmaEndDefragmentationPass.reaping) under it, so the render thread'sdestroy_image_laternever waits for the queue;image_compact_passwaits for that one free (reap_done_cv) beforevmaBeginDefragmentationPass(5333dd63: the first version freed under the lock and kept it while the queue had more, so a hand-over waited for the whole queue).texture_framestill hands nothing over whilecompact.in_pass, as before.vk:
texture_frame's dead images andpool_destroy(the render-target pool's trim) use it.switch:
RX_VK_REAPER=0destroys them at once on the render thread, as before. The log saysvk: images are destroyed on a thread of their ownwhen it's on.docs: the switch's row in
docs/development.md.tests:
tests/test_image_reaper.py(new) checks:tests/test_target_image_pool.pyfollows the new calls and still checks pooling before freeing.Measurements
Not measured here. What to measure, on Windows with an NVIDIA card: frames over 25 ms and the 99th percentile while turning the camera in the Market and on arriving in Hyrule Field, on main and on this PR, at least two interleaved runs each, with
tex_frame_msfrom the frame trace. ReLink's numbers, for reference:Flags and settings
One new switch, on by default.
RX_VK_REAPER0: on the render thread0only to compare or if a driver misbehavesTesting
Tested:
Rebased onto
main@7bacf846(this PR @5333dd63): the 63 checks that read this diff (listed below), not the whole suite, throughtests/run_all.py --jobs 6on Windows 11 with the Builder's llvm-mingw (v2.0.14 tools): 42 passed, 9 failed, 12 skipped for want of the Vulkan headers. Of the 9,test_check_pr_description,test_dev_packs,test_drawn_page_mapandtest_keep_renderedfailed with no output under load and pass when run again;test_crash_dialog,test_oot_state_tap,test_vk_pgoandtest_walk_keyalso fail on main @1310ecc6(the full suite there: 500 passed, 17 failed, 79 skipped), andtest_vk_occlusion_querieson main @7bacf846. The 12 skipped, again withRX_CPMset to the Builder's CPM cache: 11 pass, andnx2/test_bc_storage_viewsfails as on main @7bacf846. After the fix below (5333dd63),vk_vma.cppandvk_texture.cppcompile (-fsyntax-only) with the renderer's flags fromnx2/build.pyand the Builder's CPM headers.The 63 checks
nx2/test_bc_storage_views.pynx2/test_texture_init_barrier.pytests/test_3d_view_create_flags.pytests/test_amd_subgroup_profiles.pytests/test_astc_bc_policy.pytests/test_bc_raw_targets.pytests/test_bc_upload_retry.pytests/test_blocks_at_start.pytests/test_check_pr_description.pytests/test_commit_limit.pytests/test_commit_room.pytests/test_compressed_over_rt.pytests/test_crash_dialog.pytests/test_dev_packs.pytests/test_direct_buffer_writes.pytests/test_drawn_page_map.pytests/test_drawn_writeback.pytests/test_dump_readback.pytests/test_evict_reason.pytests/test_frame_trace_waits.pytests/test_fsr_uniform_ownership.pytests/test_hold_gpu.pytests/test_image_reaper.pytests/test_image_view_retry.pytests/test_inline_write_order.pytests/test_intel_compiler_gate.pytests/test_intel_compiles_auto.pytests/test_jobs_failure.pytests/test_jobs_wait_runs_queued.pytests/test_keep_rendered.pytests/test_linux_pressure.pytests/test_log_levels.pytests/test_low_vram_texture_mips.pytests/test_mem_trim.pytests/test_mesh_fallback.pytests/test_met_wait_capture.pytests/test_native_astc_texture_count.pytests/test_oot_state_tap.pytests/test_parallel_init_order.pytests/test_parallel_rb_refresh.pytests/test_pipeline_state_portable.pytests/test_prepare_stall.pytests/test_prof_clock.pytests/test_prof_sampling.pytests/test_quick_pipelines.pytests/test_sparse_mirror.pytests/test_split_before_draw.pytests/test_staging_arena_fallback.pytests/test_target_image_pool.pytests/test_target_overlap.pytests/test_texel_buffer_ownership.pytests/test_texture_budget.pytests/test_texture_layout_sizes.pytests/test_texture_squeeze.pytests/test_texture_staging_publication.pytests/test_third_party_objects.pytests/test_threaded_rendering.pytests/test_vk_occlusion_queries.pytests/test_vk_pgo.pytests/test_vram_compact.pytests/test_vulkan_clear_mip.pytests/test_walk_key.pytests/test_wb_shadow_remake.pytests/run_all.py image_reaper target_image_pool import_buffer compact texture_budget vram sparseon Windows: 6 passed, 2 skipped (no Vulkan headers for those two).vk_vma.cppandvk_texture.cppcompile with the build's flags (llvm-mingw).Not tested:
Must be tested:
vk: VRAM compactedlines and no device loss (by hand).Risks
What could break:
Turning it off:
RX_VK_REAPER=0, or revert the commit.Compatibility: nothing changes for a player beyond where the frees happen; caches and saves are untouched.
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.tests/that reads changed source updated in this PR, not weakened.🤖 Generated with Claude Code
@Beanstalk test Windows with an NVIDIA RTX GPU at head
8d361dc63f. This PC is Windows with an RTX 4070 and the patched Beanstalk agent dev-9585e4a (isolated_gameplay support).Compare main and this PR with identical settings, frame generation off and no frame-rate cap, using locally created disposable new-game saves. In the Market, measure 20 seconds standing and 20 seconds while turning the camera at full right-stick speed; also measure Hyrule Field arrivals. Use at least two interleaved runs per side, report median frame time, p99, counts of frames over 25 ms if the farm supports them, and VRAM. Record crashes, device loss and invalid-renderer errors. Include comparison screenshots where useful.
Also exercise the default compaction for 30 minutes on the PR, checking for vk: VRAM compacted lines and no device loss. Explicitly state if the farm cannot represent concurrent camera motion and measurement, the long-run duration, the tex_frame_ms trace, or any requested metric; measurements taken after the camera stops must not be described as camera-turn measurements. A short smoke test or skipped scenario is not full performance/lifetime coverage.
Please keep all game files, generated builds, caches and saves local; only normal farm reports, scrubbed logs and requested game screenshots may be uploaded.
Beanstalk test farm
Commit
5333dd6· tier untrusted · approved by nobody yet · dashboardA member of
BigBeanscan review this commit and comment@Beanstalkto test it.@Beanstalk cancel
@futondestroyer cancelled 1 queued job.
8d361dc63f5333dd636fRebased onto main @
7bacf846. Fixed in5333dd63: the reaper freed each image under reap_mu and, with more queued, never let go of it between frees, so a destroy_image_later on the render thread waited for the whole queue (and a beginning pass for all of it, not the one in flight). Each free now runs outside the lock, marked in flight; image_compact_pass waits for that one before vmaBeginDefragmentationPass, and compact_in_pass, set under the same lock, still keeps every free out of a pass. tests/test_image_reaper.py checks the order. The 63 checks that read this diff: no failure main doesn't have (4 under load passed when run again). Still WIP: Measurements needs its main/this PR table (the Test Farm can't run the camera turn on Windows).View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.