fix(vk): let render targets go as newer ones together cover them, and reuse their images at once #2766
Open
github-sync
wants to merge 1 commit from
github/pr/2766 into main
pull from: github/pr/2766
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
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/Hookshot!2766
Loading…
Reference in a new issue
No description provided.
Delete branch "github/pr/2766"
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
The renderer kept render-target images that newer render targets had already drawn over, because each newer target was judged alone: two or more partial overlaps that together cover an eighth of an old target were ignored. In #2180's guard room the target images then grew to 3-4 GB, VRAM reached the driver's allowance and the main and presentation threads stopped. On our A/B save, v2.1.0 let go 4,727 dead targets per run.
Fixes #2180 - v2.1.0, RX 9070 XT, 4K HDR + frame generation: render targets remade ~3.6 times a frame, VRAM reaches the allowed limit, then the main and presentation threads stop (freeze on breaking a jar)
Issue: #2180
What changed
target_overlap.h:DrawnOveradds up the shares of several newer render targets that drew over an old one (the candidates come in start order) instead of judging each one alone against the eighth. A view inside an old target is still not a new target (5bfa6aa, PR #899).vk_texture.cpptexture_frame: the dead-target rule uses that sum.vk_texture.cppdestroy_later: a render target's image goes to the pool as it is evicted, not at the next present. Between the two the next frame is recorded, and its target of the same create info made a new image while the old one was destroyed unused later.tests/test_target_overlap.cpp(the union and its edge cases),tests/test_target_image_reuse.{cpp,py}(an evicted image is pooled at once and taken by the next frame, with stubs, plus source checks),tests/test_target_image_pool.pyupdated for the new eviction path.Testing
Game-free:
python -I -B tests/run_all.pyin the worktree, full suite.tests/test_input_background.py, a declared known environment failure on this PC (no MSVC SDK).tests/test_precompile_compute_guard.pyandtests/test_recorder_sync.pypassed in this run.test_target_image_reuse,test_target_image_pool,test_target_overlappass.In game: NVIDIA GeForce RTX 5070 12 GB, driver 32.0.16.2114 (Vulkan 1.4), Windows 11 build 26200, i5-14600KF. Hookshot v2.1.0, Vulkan back end, windowed 3840x2160, render resolution 100%, frame generation on, Kokiri Forest clearing save, no synthetic VRAM allowance (real VRAM ~6352 MB used of 11175 MB).
Union rule, 2 runs per build:
Pool change added, 2 runs per build:
Path evidence: dead targets let go 4,727 / 51,467 MB (control, union-rule session) against 7,807 / 85,078 MB with the union rule (+65%). With the pool change, images made new 2,441 -> 1,339 (the control's 1,244), pool reuse 4,722 -> 6,802 (+44%), and the union rule's gains are kept. Speed is neutral within the run-to-run spread (79.87 -> 79.34 -> 79.16 avg FPS). The issue's 16 GB AMD repro was not reproduced on this 12 GB card; the state line and pool counters are the evidence.
Relation to #2484
#2484 (draft) sweeps the dead-target pile more often and drains the pool faster. This PR changes which targets are dead at all (the union accounting) and when an evicted target's image is reused. They touch different hunks of
texture_frameandpool_trim, and can land in either order.AI assistance
Assisted-by: oc-impl-1 (opencode/deepseek-v4.1-flash), oc-upstream-1 (opencode/deepseek-v4.1-flash-free)
Tracking: #993 (where #2180 was consolidated).
I merged this into current main and ran test_target_overlap, test_target_image_pool and test_target_image_reuse (against Khronos Vulkan-Headers), plus the texture, target, evict and pool checks from run_all. All pass. CI hasn't run on this head yet.
DrawnOver (gpu/vk/target_overlap.h) is correct: by_start is sorted, so the clipped starts never go backwards and merging span by span gives the union with no byte counted twice. The #899 view exclusion is kept.
On pooling at eviction: the only caller of destroy_later, texture_frame's evict loop, skips textures with last_value > completed, and the flush waits first, so a pooled image is never in flight. Skipping compact.in_pass is fine because targets have their own pool.
What's left is reach. The union rule marks more targets dead for everyone (+65% in your numbers), and a false positive recreates a live target empty, which would show on screen. The RTX 5070 run covered Kokiri only, while #2180 is an RX 9070 XT report. A run past a scene change (Castle Town, or #2180's guard room) would settle it. #2484 touches the same parts of texture_frame and pool_trim.
Thanks for this, and for the A/B tables. I agree with @Joey-Magnus's review: the union is computed right, and pooling at eviction is safe because the evict loop skips anything with
last_value > completed. I checked that too:destroy_laterhas no other caller, and pooled target images are never made in the compacted textures' pool, so skipping thecompact.in_passgate is fine. A few more points.What the sum actually adds. A newer target fully inside the old one is still excluded, so every one that counts either straddles its start, straddles its end, or covers it whole. All the ones over the start clip to
lo, so their union is just the longest of them, and the same goes for the end. So the only new case is a target with one newer target over its head and another over its tail, each under an eighth, together an eighth or more. That's what your edge test checks, and it's a sound reading of the console's memory: if both ends really were drawn over, the game can't sample it there either. Please say in the description (and the header comment) that this is the shape it catches; "several partial overlaps add up" reads broader than it is.Which targets are exposed. Targets used every frame can't be hit: TAA history, shadow maps, the exposure meter, the presented image. Sampling or drawing one updates
last_frame, the rule needs a newer write after that, and the pass only looks at targets unused for 3+ frames. What's exposed is a target that sits unused for a while and is sampled again later, like the pause-screen capture. The finalized textures the game samples again are exposed too, because the sum also feedskeep_finalized(return !drawn_over(tex) && !drawn_over_shifted(tex);). A finalized texture with both edges drawn over now loses its keep and goes without a write-back. That's right only whileguest_sizematches the size the game gave that storage: any rounding there now counts at both ends. I'm fine with the rule, but this is why the run across scene loads matters.Evidence. Our rule for memory and performance changes is before/after on the same scene with the hardware named, and for a fix, the bug shown gone in the game. The fps tables show it's speed-neutral on the 5070, thanks. But this is a memory fix and there's no resident memory in it: "let go" and "reused / made / destroyed" are running totals, and the one VRAM figure (~6352 MB) is a single number. Could you add, for control and PR on the same route:
textures retired (... left, N MB)line, at the start and after a few minutes;#2180 wasn't reproduced (a 12 GB card against the 16 GB RX 9070 XT at 4K HDR with frame generation). That's acceptable if the numbers show the target images no longer climbing in the guard room.
Other PRs.
pool_trimandtexture_frame), but both add a test at the same place intests/test_target_image_pool.py, so whichever lands second needs a small rebase. They also interact: this PR pools images a present sooner and lets more targets go, which raises the pool's bytes and lowersst_pool_made, the two things #2484's drain and pile trigger read. I'd land this one first and have #2484 re-measured on top.vk_texture.cppchange marks a finalized pool's textures released; the render-target trim its description mentions isn't in its diff. Neither makes the other redundant, and they can land in either order.CI: only the title check has run; the game-free and AppImage workflows are waiting for approval. Joey ran the three focused tests merged with main and they pass. No conflicts with main.
Nits inline. Once the memory numbers and the scene-load run are in, I'm happy to approve.
With views inside excluded, every candidate over the start clips to
a == lo, so this merge just keeps the longest of them, and likewise at the end. The sum only ever differs from the old rule for head + tail. Worth one line in the struct's comment, so nobody later reads it as "any number of pieces add up" and loosens the view exclusion to match.Nit:
sparse_chunksis always empty here (theifchecks it), so this line does nothing. The attachments andtotal_byteslines below repeat the tail too.if (pooled) pool_put(d); else dead.push_back(std::move(d));with the shared tail after would keep one copy.Nit: this exact line is also in the
by_holdloop above the eviction loop, so the check still passes if the eviction loop's own guard went. Cutting the loop that callsdestroy_laterand checking inside it would pin the guard that keeps a pooled image out of flight.