Repository navigation
nvn: preserve texture upgrades, BC render targets, and retired descriptor bounds - #1731
ksolenk38ze wants to merge 1 commit into
Conversation
5e56c98 to
a43d23e
Compare
|
One data point for the fps drops you describe, from Ocarina of Time (0400486024DEC000 v131072) on an iPad Pro (M5) and an M4 Pro Mac, in case it meets the same scenes: in Kokiri Forest's main colour pass (Docked 1536x864, RG11B10 + D32F, ~800 draws) the 11 indirect task/mesh grass draws cost 5.4-5.7 ms on the M5 and 4.7-4.9 ms on the M4 Pro, while the other 760 draws of the pass take 3.1-3.5 ms. The cost is in the vertex (mesh) phase, nearly the same on both GPUs and independent of the render size: the heavy grass mesh bodies (4,700-8,900 lines, a 2,100-3,973-word threadgroup stage array zeroed at every launch) run about 20x slower per threadgroup than the light variants the prepass uses at the same task counts (700-1,300 task threadgroups a frame, at most 32 mesh threadgroups each; no out-of-range counts). Encoding them as direct draws costs the same, so it is not the indirect path. Have you seen this pass in your profiles, and do the descriptor and texture-upgrade fixes here touch anything these draws read? Happy to run this PR on the iPad against our numbers if useful. |
|
Thanks @aksmith-git! The severe frame-time drop (60 fps down to 10-12 fps) addressed in this PR is separate from the 4.7-5.7 ms GPU mesh-stage cost in Kokiri Forest: In Ocarina of Time, the guest texture registry grows to |
…ptor bounds Retain explicit pixel-format view upgrades across subsequent non-view resolves and record the parent texture's usage mask in the alias map so later descriptor resolves do not downgrade or recreate the shared texture. Allow block-compressed textures with CPU backing to serve as color render targets via an uncompressed RGBA8 backing texture, and suppress render-target range clears on descriptors backed by CPU-written memory. When resolving bindless texture IDs in pool walks and direct draws, preserve retired descriptor views across submissions when the registry exceeds 2048 entries, keep finalized NvPkg records in the ghost table so shared weak references stay live while descriptors remain registered, and clamp feedback readback regions and cleared array slices to the target texture's dimensions.
a43d23e to
90b8a99
Compare
… retired descriptor bounds, depth-read pass split; merged with the memory guard's pinned objects and alias_let_go, NvPkg.used, the pool heaps) Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR): - seedds#1731 by ksolenk38ze (open): nvn: preserve texture upgrades, BC render targets, and retired descriptor bounds
…he dynamic-resolution climb, frag_z_snap, mul_extended; main's rh_texture and frag_z_snap replace seedds#1453's pool heaps and seedds#1742's frag_z; our seedds#1731 hints, compaction, cond_conservative and fixed resolution kept) Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR): - seedds#1453 by PromethOps (closed): A GPU-only pool's render targets share one placement heap: Ocarina of Time's 3 GB of render targets fit in 0.7 GB - seedds#1731 by ksolenk38ze (open): nvn: preserve texture upgrades, BC render targets, and retired descriptor bounds - seedds#1742 by cocoman93 (open): position.z one ULP down against a surface's own stored depth: fixes Ocarina of Time mist specks
…ing entry, and entries nothing looks up are swept (Ocarina of Time on a 24 GB Mac: residency set at its peak 8.0 GB against 11.5, footprint 12.3 GB against 14.9, no stall at an area change) Ocarina of Time lays a frame's render targets out with a linear allocator in one GPU-only pool. When one of them changes size the ones after it move, and each move is a new entry in the sharing table (pool, offset, description) with a Metal texture of its own. Walking into another area of Castle Town the textures in the residency set went from 2.3 GB to 5.4-6.5 GB, about 4.8 GB of it in render-target formats (1,744 Depth32Float, 624 RGBA16Float), while only 796 targets (1.5 GB) were live. On a 24 GB M5 the system then ran out of free pages (60-120 MB), compressed 5 GB in 10 s and Metal's submission queue sat in IOGPUCommandQueueSubmitCommandBuffers: frames of 62-92 ms for tens of seconds. Two things kept those textures: - the ring of finalized serials (g_ghost), 32,768 entries since PR seedds#1731: a texture evicted from the sharing table stays resident while a finalized serial still resolves to it, and at the 400 textures a second this game finalizes the ring takes about 80 s to turn. An entry that leaves the sharing table now takes its ghost serials with it (ghost_purge; DM_NVN_GHOST_TAKEOVER=0 for the old behaviour). Shrinking the ring instead does not work: under about 10,000 entries the serials the registered descriptors still read are evicted and entered again every frame (59 to 155 million entries in 100 s, submit CPU from 7 to 16-23 ms), and over that nothing is freed sooner; - nothing let an entry go by its age. A takeover skips a compressible entry for 1800 submissions past its last lookup (PR seedds#1731's hold) and is not tried again, it never evicts a heap-backed entry in this tree, and an entry whose storage nothing takes over left only with the memory guard, which reads kern.memorystatus_level: that still said 17-34 % available with 100 MB of free pages. A sweep every 120 frames now lets go of the entries nothing holds and no lookup found for 600 submissions (alias_sweep; DM_NVN_ALIAS_SWEEP, DM_NVN_ALIAS_SWEEP_IDLE). The hold itself stays, and DM_NVN_ALIAS_KEEP_MB only names its bound (half of Metal's working set, as before; 0 turns it off). With it off the scripted walk used less memory still (four runs: residency set 6.5-7.1 GB, footprint 11.4-11.9 GB), but an entry then lasts 16 submissions past its last lookup, and in the app at full screen, where the game's dynamic resolution moves and its layouts alternate, the host made the same render targets again every frame: 16,819 textures (30 GB of allocations) in 300 frames, a submit CPU of 17.7 ms against 7.9, frames of 34 ms for 5 s. In the app (full screen, MetalFX for the game's upscaler, native output, 150 s of the walk, one run each, so indicative): with the hold off the host made 28,781 and 47,407 textures, against 14,902 before this commit and 15,010 with the hold on, and its worst 300-frame window was 34.0 ms; with the hold on 20.4 ms; before this commit 67.7 ms. The footprint at its peak in those runs: 13.8 GB before, 12.1 and 13.0 GB with the hold off, 11.2 GB with it on in a run without Game Mode (GPU-bound at about 53 fps), and 13.6 GB at the tip of this branch with Game Mode on and 60 fps held (GPU allocation 9.3 GB against 10.6). In the app the stall is what goes away; the memory gain is not established there. Measured from the shell on the scripted walk through Castle Town (the host's own report of the residency set every 10 s, the system's memory every 2 s), handheld mode, dynamic resolution on, from the same save, one run each on 10 Nov at the tip of this branch (this commit and the two after it, which do not touch memory) and at the commit before this one: tip before residency set at its peak 8.0 GB 11.5 GB textures in it 4.2 GB 5.2 GB heaps in it 1.1 GB 4.2 GB footprint 12.3 GB 14.9 GB wired, system-wide 10.8 GB 13.6 GB 300-frame windows > 17.5 ms 0 of 54 10 of 32 (seven of them 42-69 ms) textures made in the walk 18,410 32,071 1,200 consecutive frames around the area change, captured one by one with the hold off (the variant that evicts more), show no dark frame and no one-frame flicker, as before the change. Not covered: other areas than Castle Town, interiors, the pause menu held open, long sessions. The system still reaches its free-page floor at the area change (65 MB): this takes 3 GB and the stalls away, not all of the pressure. DM_NVN_GHOST_N=<entries> (the ring's size, for measuring) stays as a diagnostic. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7
… of ending (Ocarina of Time after 28 minutes of play: 23,100 textures made in 300 frames, frames of 93 ms and a footprint of 17 GB when the GPU's allocation passed half of Metal's working set) PR seedds#1731's hold applies while the GPU's allocation is under a bound, half of Metal's working set (9,093 MB on a 24 GB Mac). Over it a compressible entry was let go 16 submissions after its last lookup. That is a cliff: in the app at full screen the game alternates between the layouts of its dynamic resolution, and with the hold gone every takeover evicted the render targets of the layout the game was about to use again. A player's session at 60 fps for 28 minutes crossed the bound on entering an area (7,855 to 10,308 MB), and in the next 900 frames the host made 9,889, then 15,793, then 23,100 textures per 300 frames (59 GB of allocations, 77 textures a frame), the submit CPU went from 5 to 41 ms, the GPU from 13 to 36 ms, frames to 93 ms, and the footprint from 13.2 to 17.0 GB with the releases waiting on the GPU. The storm keeps the allocation over the bound, so it does not end by itself: the player restarted the game. Over the bound the hold is now ALIAS_KEEP_SHORT submissions. This commit set it to 240; the review two commits later found that this game makes 28-37 submissions a frame, so 240 is 7-8 frames, no longer than the dynamic resolution holds a level, and raised it to 600, the idle sweep's own wait. DM_NVN_ALIAS_KEEP_MB=0 still turns the hold off entirely. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7
…tion gives back its oldest half On a 16 GB M5 (Hyrule Field, about 15 minutes in) the guard's system side ended the game while its evictions found almost nothing idle: GPU allocated 6.5 GB, footprint 12.2 GB, and the ring of finalized serials (32,768 entries since PR seedds#1731, about 80 s of what Ocarina of Time lets go) held 31,987. Now guard_evict also calls ghost_trim: the oldest half of the ring goes at each eviction, down to DM_NVN_GHOST_TRIM_MIN entries (default 2048, about 5 s; 0 = off), and the stop level goes straight to that floor before it re-checks. Textures go behind their submissions' fences (obj_drop), as the ring's own eviction does; a trimmed serial resolves anew, so a retired ID still read is made again from its storage (a render target's content would read cleared). The memory line every 300 frames is followed by what the ring alone holds (entries, textures held by no live serial, MB), so the next log shows whether this is the lever. t_ghost_trim: 10 checks, and 2 with the trim off. Not run in game yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aves the table takes its finalized serials with it, and an idle sweep lets go of entries nothing looked up for 600 submissions Ocarina of Time lays a frame's render targets out with a linear allocator in one GPU-only pool. When one of them changes size the ones after it move, and each move is a new entry in the sharing table (pool, offset, description) with a Metal texture of its own. Walking into another area of Castle Town on a 24 GB M5, the textures in the residency set went from 2.3 GB to 5.4-6.5 GB, about 4.8 GB of them render targets the game had already let go (1,744 MB Depth32Float, 624 MB RGBA16Float, while 796 live targets took 1.5 GB). The Mac ran out of free pages (60-120 MB), compressed 5 GB in 10 s, and frames took 62-92 ms for tens of seconds. Three things kept those textures: - the ghost ring. A finalized serial that a replayed record resolved keeps its texture until the ring turns. With 512 entries the ring is too small for this game: under about 10,000 entries the serials its registered descriptors still read are evicted and entered again every frame (59 to 155 million puts in 100 s, submit CPU from 7 to 16-23 ms; the bursts in issue seedds#2213 look alike). PR seedds#1731 makes it 32,768, and then at the 400 textures a second this game finalizes it takes about 80 s to turn, holding what the game let go for that long. The ring is 32,768 here, and an entry that leaves the sharing table now takes its ghost serials with it (ghost_purge; DM_NVN_GHOST_TAKEOVER=0 for the old behaviour, DM_NVN_GHOST_N to measure other sizes); - nothing let an entry go by its age. Only a takeover over the same storage or the pool's Finalize removed one. Every 120 frames a sweep now lets go of the entries nothing holds and no lookup found for 600 submissions (alias_sweep, with PR seedds#1589's idle eviction; DM_NVN_ALIAS_SWEEP, DM_NVN_ALIAS_SWEEP_IDLE); - a takeover spares an entry looked up in the last 16 submissions. For compressible entries that was too short: at full screen, while the game's dynamic resolution moves, its alternating layouts were made again every frame (16,819 textures, 30 GB of allocations, in 300 frames; submit CPU 17.7 ms against 7.9; frames of 34 ms). A compressible entry now outlives a takeover for 1,800 submissions while the GPU's allocation is under half of Metal's working set (PR seedds#1731's hold; DM_NVN_ALIAS_KEEP_MB sets the bound, 0 turns it off), and for 600 over it. Ending the hold at the bound instead was a cliff: a session that crossed it on entering an area (7,855 to 10,308 MB) made 23,100 textures in 300 frames, frames of 93 ms and a footprint of 17 GB. Measured on an M5 with 24 GB, on a branch that also carries PR seedds#1731 and PR seedds#1589, from the shell on a scripted walk through Castle Town from one save (handheld, dynamic resolution on; the host's residency report every 10 s, the system's memory every 2 s), one run each: after before residency set at its peak 8.0 GB 11.5 GB footprint 12.3 GB 14.9 GB wired, system-wide 10.8 GB 13.6 GB 300-frame windows > 17.5 ms 0 of 54 10 of 32 (seven of them 42-69 ms) textures made in the walk 18,410 32,071 In the app at full screen, 150 s of the same walk: worst 300-frame window 19-20 ms against 67.7 ms. With 8 GB of the Mac held wired, so the game and the rest live in 16 GB, the walk completed with two windows over 17.5 ms. 1,200 consecutive frames around the area change, captured one by one, show no dark frame and no one-frame flicker. One behaviour changes for a texture ID that names a finalized texture: when a takeover lets that texture go, the ID's serial goes with it and the ID resolves anew in the takeover's submission (patched, no table built), where before it kept the old texture until the ring let the serial go and then resolved anew the same way. t_pool_notes's takeover case now expects that (it fails with DM_NVN_GHOST_TAKEOVER=0). Checked: t_alias_takeover under Metal validation, with three new cases: a finalized serial a replayed record resolved goes with the texture a takeover lets go (fails with DM_NVN_GHOST_TAKEOVER=0), the sweep lets go of an entry nothing resolves to at the first sweep after 600 submissions and keeps the live ones (fails with DM_NVN_ALIAS_SWEEP=0), and over the hold's bound a compressible entry outlives a takeover past 16 submissions and goes past 600. The whole graphics suite (make -C dm/gfx/tests run, M5, macOS 27.2) passes, except t_pool_retire once on its 1 s wait while the Mac was building something else; alone it passed three times out of three. test_docs passes; the Mac host builds. Not run: the game on main, whose shaders do not convert there since 0662f9d (issue seedds#2272); other areas than Castle Town, interiors, sessions longer than 34 minutes; regress.py on other games. The system still reaches its free-page floor at the area change (65 MB): this takes the stalls and about 3 GB away, not all of the pressure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7
|
A heads-up for the rebase, about item 3 and the ghost table. Since 034e91a (issue #2213, PR #2389), The two together make a loop in Ocarina of Time. A retired ID's view that stays in a pool table is never looked up again, so its ghost entry is never marked as used. All of them (about 13,000 in Kokiri Forest) go idle 16 frames after the walk that resolved them, in the same frame. The commits then evict them, each eviction writes a note, the notes overflow what a patch takes ( I measured it on my branch, which carries this PR on top of I sent a fix as #2585. The owner counts, per serial, the registered IDs that name it, and I'm reworking it along that review: the count kept under the lock |
What and why
In The Legend of Zelda: Ocarina of Time (
0400486024DEC000), four texture and descriptor lifecycle interactions caused missing wall/tunnel fixtures, black environment reflections, or severe frame-time drops (60 fps down to 10-12 fps):tex_upgrade_pfv) or additional usage (tex_upgrade), subsequent non-view resolves or alias lookups could see the child view's narrow usage mask or overwrite the upgraded texture with a fresh base allocation. Recording the parent texture's usage ing_alias_usage(g_upg_hint) and retainingMTLTextureUsagePixelFormatViewandNvImageHeapacross upgrades keeps the shared texture intact across dynamic-resolution steps.RGB_DXT10x42,RGTC1_UNORM0x49) failed Metal validation (MTLPixelFormatBC*is not color-renderable). Allocating an uncompressed shadow target (bcrt_attach) and BC-encoding back into the guest texture onrp_end(bc_encode_bc1,bc_encode_bc4), while skipping destructive range clears on CPU-backed descriptors, preserves sky/water reflection probes and pre-initialized texture data.g_ntreg > 2048, ~33,000 IDs in Ocarina of Time), dropping cached views on all ~5,766 retired descriptors at the end of every submission (treg_finish_submission) forced thousands ofMTLTextureViewrecreations and residency mutations every frame (dropping frame rate to ~12 fps), whereas skipping retired descriptors inpool_entryleft wall and tunnel textures bound to the dummy texture when the game finalized its temporaryNVNtexturestruct after registration. KeepingNvPkgrecords ing_ghost(NGHOST = 32768) onmtl_tex_releaseso__weakreferences ing_pkg_shareremain alive while descriptors are registered, resolving already-made retired descriptors inpool_entry, and keeping retired descriptor views cached across submissions wheng_ntreg > 2048preserves both 60 fps and all environment fixtures.R.rt.depthafter an earlier draw in the same pass wrote depth (g_fb_dwrite), and clampingmirror_readbackregions andclear_tex_trislice counts to the target texture's dimensions, prevents TBDR depth-feedback flickering and out-of-bounds blits.Related
Checked
make -C dm/gfx/tests GAME=0400486024DEC000 t_pool_notes t_upgrade_alias t_image_alias t_alias_takeover t_srgb_alias t_rt_lifetime t_pool_chunks t_feedback t_lookahead_bindless(all pass with 0 failures),python3 dm/tools/docs_symptoms.py0400486024DEC000, v131072, SDK 22.2.8), tested across Kokiri Forest, Lost Woods, Hyrule Field, Zora's River, Zora's Domain, and Forest Temple at 60 fps with all tunnel/cave walls and reflections visible.Docs
docs/reference/<subsystem>.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)