vk: render target images reused from a pool, not made and destroyed for each of OoT's shifting transient targets #1416
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1416
Loading…
Reference in a new issue
No description provided.
Delete branch "perf/target-image-pool"
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?
What
Render target images are reused from a pool instead of being made and destroyed for each of OoT's shifting transient targets.
OoT makes a frame's transient targets in a scratch heap, and their offsets shift with what came before them in the frame. So each shifted target is a texture with a new image. Every 60 presents, the dead-target sweep (#899) lets go of the dead ones, and the next present destroys them all at once. In Kokiri Forest that is 300-700 images per sweep, at about 0.07 ms each: a 20-65 ms stall on the replaying thread, about once a second while the camera moves.
In one 4.5-minute run, 10,500 images (17 GB) were made and destroyed this way. The sweep's choice is cheap (under 1 ms). All of the stall is the destruction.
Change (
gpu/vk/vk_texture.cpp,vk_texture.h)VkImageCreateInfo(pool_key_of). The dead list puts them there once the GPU is done with them, instead of destroying them.create_imageis asked. It is still put intoGENERALfromUNDEFINEDand cleared exactly as a new image is, so nothing it held shows.RX_VK_TARGET_POOL_MB(default 1024;0turns the pool off and every image is destroyed as before).pool_trimdestroys at most 16 images a present (64 while VRAM is short), so there is never a burst.vk: render target images: N reused from the pool, M made new, K destroyed from it; it holds X (Y MB).Measured
HOS_HEADLESS,HOS_AUDIO=0), a Kokiri Forest save, the same scripted 36 s walk with camera turns each run. The numbers are the in-play part only (presents 3300 and on, after the save's load).0828863with #1127, #1338 and local timing logs (not in this PR). The A/B is the same build withRX_VK_TARGET_POOL_MB=0, alternated.Not covered
pressure > allowed / 10 * 9) to be given back. I could not test that on a small card.Tests
tests/test_target_image_pool.py(source checks, 7 cases):0turns the pool off;create_imageand still goesUNDEFINED→GENERALand is cleared;tests/run_all.pyon every test that readsvk_texture.cpp, plustest_vulkan_clear_mip.pyandtest_parallel_init_order.pywith the Vulkan headers.RX_VK_ONLY=1 nx2/build.py --standalone).🤖 Generated with Claude Code
This landed in the v2.0.12 branch (merged there for testing; it reaches
mainwith that release), so closing. Thanks!Reopened: closing this was a mistake on my side. It was tested in the v2.0.12 branch, and it will be merged into main here, so it shows as merged. Sorry for the noise.
Thanks, this is a nice win and the reuse itself looks safe to me. I checked the hazards I'd worry about with a pool like this:
value <= completed, the same gate as destruction, andpool_trimonly destroys images that are already idle there. Fine.Texturestarts fresh, and the reused image still goes through theUNDEFINED -> GENERALbarrier and the clear (both are unconditional after thecreate_imagechain). Fine.make_viewis intexture_view's view key (use, type, format, aspect, level/layer range, swizzle), and depth/cube/3D come from the create info, so moving the view map along with the image is correct. The few view caches outside the texture (destroy_gen) only ever see the same view handle on the same image. Fine.pool_key_ofcovers every fieldcreate()sets onci.pNextis never set, and sparse plus the BC/ASTC retry are excluded before the key is used.The points that are still open:
create()givesCOLOR_ATTACHMENTto every non-BC, non-ASTC colour image, decoded ASTC (RGBA8) included, so the usage check doesn't filter anything. Uploaded textures, decoded ASTC arrays and textures the budget evicts all end up in the pool too. One effect is that budget eviction no longer frees VRAM for up to 120 presents or 1 GB. The description and the test name say "render targets only". #1436 fixes this withas_target(!for_sampling), which suggests that's the intent. Either pull that one line into this PR or reword the description.pressure(used - block_free) but not intotal_bytes. Once pressure goes past 90%, the budget is cut by the wholeoverin the same pass, so live textures get evicted for memory that the pool is only giving back 64 images per present. Takingpool_bytesout ofover(or skipping the cut while the pool can still cover it) would let the pool give way first. This matters most on the 6-8 GB cards you flagged as untested. There, the default cap of 1024 MB comes on top of a texture budget of 70% of what the driver allows.create_imageretry chain increate(), so this needs a rebase. When you rebase, keep!is_bcin the pool rule, because main now gives BC imagesCOLOR_ATTACHMENTtoo.Order with #1436: that PR is stacked on this one (its first commit is
c34f350), so this one should land first and #1436 after it. Point 1 is already handled there, and its 256 MB default softens point 2 but doesn't remove it.Nits:
pool_trimgoes through shapes in hash-map order, so it doesn't free the oldest images across the whole pool first. Also,st_pool_madeis counted beforecreate_imagesucceeds.No CI checks ran on this branch. tests/run_all.py picks up the new test on its own, so it doesn't need to be registered.
Every non-BC/ASTC colour image gets
COLOR_ATTACHMENTincreate()(around L1730), decoded ASTC included, so this check passes for all of them, not just targets. Uploaded and evicted textures end up pooled too. #1436'sas_target &&is the fix.pressureincludespool_bytes, but the budget cut just below takes the wholeoverout of live textures in this same pass, while the pool drains at only 64 images per present. I'd subtractpool_bytesfromoverhere (over > pool_bytes ? over - pool_bytes : 0) so the pool gives way first.