Repository navigation
fix(vk): let go of render targets drawn over from another start - #899
Merged
Merged
Conversation
OoT makes a frame's transient render targets (768 x 432 RGBA16F, 1536 x 864, 640 x 360) in a scratch heap whose offsets shift with what came before them in the frame. Turning the camera in Hyrule Field made new ones almost every frame, ~1.2 MB from the last and overlapping it, each one a new image. drawn_over() only knew a target as dead when another one was drawn at the same start, so these were kept until the budget or the 3600-present rule took them: up to ~300 dead targets (1.2-1.9 GB) on an 8 GB RX 7700S, the VRAM at the driver's limit, the driver paging, and the GPU's frame going from ~16 to ~35 ms (12-18 fps, hitches of 0.3-0.6 s every few seconds). A render target of one level and layer is now dead too when another render target, starting elsewhere and not inside it, drew over an eighth of it or more after its last use (gpu/vk/target_overlap.h). Those go after 16 frames unused, and under the budget the sweep for drawn-over targets runs every 60 presents instead of every 600. RX_VK_DEAD_TARGETS=0: as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assisted-by: Claude Opus 5.5
A render target whose storage another one has drawn over since holds nothing the game will sample again, and the eviction pass already checks the GPU is done with it (last_value). Waiting 16 frames kept ~500 MB of them in Hyrule Castle Town's market at ~20 fps (59 768 x 432 RGBA16F, 135 384 x 384, 41 1536 x 864 at a time), with the VRAM at the driver's limit on an 8 GB card. RX_VK_DEAD_MIN_UNUSED=<frames> sets it (3 by default). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 9, 2026
27thLiz
pushed a commit
to 27thLiz/Hookshot
that referenced
this pull request
Nov 10, 2026
…or each of OoT's shifting transient targets OoT makes a frame's transient targets in a scratch heap whose offsets shift with what came before them in the frame, so each shifted one is a texture with a new image, and every 60 presents the dead-target sweep (NotRexed#899) let go of 300-700 of them at once, destroyed in the next present: 20-65 ms on the replaying thread, about once a second while the camera moves (RTX 5070, Kokiri Forest: 10,500 images made and destroyed in 4.5 minutes, 17 GB). A render target's image whose texture is gone now waits in a pool by its create info, with its views, and a new texture of that create info takes it. It is still put into GENERAL from UNDEFINED and cleared as a new image is, so nothing it held shows. Uncompressed colour and depth targets only; bounded by RX_VK_TARGET_POOL_MB (default 1024, 0: off, as before), given back while VRAM is near what the driver allows, and an image unused in it for 120 presents is destroyed, at most 16 a present. tests/test_target_image_pool.py keeps the rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeusward1-a11y
pushed a commit
to zeusward1-a11y/Hookshot
that referenced
this pull request
Nov 10, 2026
…or each of OoT's shifting transient targets OoT makes a frame's transient targets in a scratch heap whose offsets shift with what came before them in the frame, so each shifted one is a texture with a new image, and every 60 presents the dead-target sweep (NotRexed#899) let go of 300-700 of them at once, destroyed in the next present: 20-65 ms on the replaying thread, about once a second while the camera moves (RTX 5070, Kokiri Forest: 10,500 images made and destroyed in 4.5 minutes, 17 GB). A render target's image whose texture is gone now waits in a pool by its create info, with its views, and a new texture of that create info takes it. It is still put into GENERAL from UNDEFINED and cleared as a new image is, so nothing it held shows. Uncompressed colour and depth targets only; bounded by RX_VK_TARGET_POOL_MB (default 1024, 0: off, as before), given back while VRAM is near what the driver allows, and an image unused in it for 120 presents is destroyed, at most 16 a present. tests/test_target_image_pool.py keeps the rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NotRexed
added a commit
that referenced
this pull request
Nov 10, 2026
…or each of OoT's shifting transient targets (#1416) OoT makes a frame's transient targets in a scratch heap whose offsets shift with what came before them in the frame, so each shifted one is a texture with a new image, and every 60 presents the dead-target sweep (#899) let go of 300-700 of them at once, destroyed in the next present: 20-65 ms on the replaying thread, about once a second while the camera moves (RTX 5070, Kokiri Forest: 10,500 images made and destroyed in 4.5 minutes, 17 GB). A render target's image whose texture is gone now waits in a pool by its create info, with its views, and a new texture of that create info takes it. It is still put into GENERAL from UNDEFINED and cleared as a new image is, so nothing it held shows. Uncompressed colour and depth targets only; bounded by RX_VK_TARGET_POOL_MB (default 1024, 0: off, as before), given back while VRAM is near what the driver allows, and an image unused in it for 120 presents is destroyed, at most 16 a present. tests/test_target_image_pool.py keeps the rules. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: NotRexed <therealnothingfn@gmail.com>
NotRexed
added a commit
that referenced
this pull request
Nov 10, 2026
…wept every 10 presents (follow-up to #1416) (#1436) * vk: render target images reused from a pool, not made and destroyed for each of OoT's shifting transient targets OoT makes a frame's transient targets in a scratch heap whose offsets shift with what came before them in the frame, so each shifted one is a texture with a new image, and every 60 presents the dead-target sweep (#899) let go of 300-700 of them at once, destroyed in the next present: 20-65 ms on the replaying thread, about once a second while the camera moves (RTX 5070, Kokiri Forest: 10,500 images made and destroyed in 4.5 minutes, 17 GB). A render target's image whose texture is gone now waits in a pool by its create info, with its views, and a new texture of that create info takes it. It is still put into GENERAL from UNDEFINED and cleared as a new image is, so nothing it held shows. Uncompressed colour and depth targets only; bounded by RX_VK_TARGET_POOL_MB (default 1024, 0: off, as before), given back while VRAM is near what the driver allows, and an image unused in it for 120 presents is destroyed, at most 16 a present. tests/test_target_image_pool.py keeps the rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * vk: render targets' images in VMA blocks of their own; dead targets swept every 10 presents; the target pool bound 256 MB Follow-up to #1416. OoT's transient targets, made and let go all the time, shared the image pool's 128 MB blocks with the textures that stay: each one gone left a hole beside a texture that kept its block. In Kokiri Forest the pool's 47-54 blocks (6-6.9 GB) held 1.9-3.6 GB of images, VRAM sat at 93-98% of what the driver allows, and an image made then took 30-70 ms (RTX 5070). - Images texture_get makes for a target (not for sampling) come from blocks of their own (RX_VK_TARGET_BLOCKS=0: as before), and only those go to the target pool. - The dead targets are swept every 10 presents (RX_VK_DEAD_EVERY, 60 before): the pool got 300-700 at once and the new targets of the next second missed most of them. The choice is cheap (1930 textures ordered in 0.3 ms). - The target pool's bound comes down to 256 MB. - Every 600 presents the log says what the image blocks hold (vma_log_image_blocks). tests/test_target_image_pool.py checks the new rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: NotRexed <therealnothingfn@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
OoT makes a frame's transient render targets (768 x 432 RGBA16F, 1536 x 864, 640 x 360) in a scratch heap whose offsets
shift with what came before them in the frame. When the camera turns, what is drawn before them changes, so almost every
frame they land ~1.2 MB away from the previous ones, overlapping them, and the renderer makes a new image for each.
drawn_over()only treats a render target as dead when another one is drawn at the same start address. These startelsewhere, so they stayed until the texture budget or the 3600-present rule took them.
Now a render target of one level and one layer is also dead when another render target that starts elsewhere, and is not
inside it, has drawn over an eighth of it or more after its last use. The rule is in
gpu/vk/target_overlap.h. Suchtargets go after 3 frames unused (
RX_VK_DEAD_MIN_UNUSED; 16 in the first version of this PR). Under the budget, the sweep for drawn-over targets runs every 60 presents instead ofevery 600.
RX_VK_DEAD_TARGETS=0restores the old behaviour.Why
On an 8 GB card the dead targets filled the VRAM. The driver then paged it out to system memory, and the GPU time per frame
doubled while it stayed that way.
How it was tested
RX 7700S Laptop (8 GB), Ryzen 7 7735HS, Adrenalin 26.10.1, Windows 11, v2.0.5, 1600x900, preset Custom, no frame
generation. Same route each time: out of Lon Lon Ranch into Hyrule Field, turning the camera a lot.
Turning the switch off (
RX_VK_DEAD_TARGETS=0) in the same build brought the 1.7-2 GB of rendered textures and the low fpsback.
Game-free check:
tests/test_target_overlap.pycompilestests/test_target_overlap.cppagainst the rule: shiftedoverlaps, views inside the target, the same start, overlaps under an eighth, and order of use. It also checks that the
eviction pass still uses the rule. It is listed in
tests/run_all.pywith--cxx g++.python -I -B tests/run_all.py: everything passes here exceptnx2/test_nup_paths.pyandtests/test_fps_overlay_order.py, which fail the same way on a cleanmainon this machine.Update: 3 frames instead of 16
With 16 frames, Hyrule Castle Town's market still held ~500 MB of dead targets at ~20 fps (59 768 x 432 RGBA16F,
135 384 x 384 and 41 1536 x 864 at a time), with the VRAM at the driver's limit. Their storage has been drawn over by
another target, so nothing the game samples is lost, and the pass still skips anything the GPU has not finished
(
last_value). Played on the same RX 7700S for a long session (field, market, ranch) with 3: no missing or staletextures.
RX_VK_DEAD_MIN_UNUSED=<frames>sets it; the test checks the switch and the GPU check are still there.🤖 Generated with Claude Code