Repository navigation
Uncompressed textures are taken from their storage unless the GPU wrote that storage, not their pool: Ocarina of Time's ground no longer opens in holes after walking a distance - #1604
Conversation
…te that storage, not their pool: Ocarina of Time's ground no longer opens in holes after walking a distance tex_cpu_content refused every uncompressed 2D texture of a pool once anything in that pool had been a render target, a clear's or a copy's destination, or a shader image. Ocarina of Time keeps its terrain tiles' height (260x260 R32F) and blend / hole maps (260x260 RGBA8) in the pool its tiles' normal maps are drawn into, and the CPU fills them before nvnTextureInitialize with no flush: after the first normal map was drawn, every tile streamed in became a GPU texture of zeros. The host now keeps the address ranges the GPU writes (mtl_tex_gpu_written, gws_span), dropped with their pool; each keeps the dirty-page generation of its last GPU write and has its whole pages protected, so storage the CPU rewrites afterwards is taken again. Reference entry, game note, symptom index and t_cpu_storage cases.
|
Thanks for this, and for the careful route test. #1390 (open, from our fork) fixes the same symptom the same way: GPU-written storage kept as address ranges instead of per pool, so a pool holding both kinds no longer leaves the CPU-written textures at zero. Worth a look before both land, since they touch the same code ( One idea in yours that #1390 lacks: protecting the pages of a range, so a later CPU write to reused storage makes the storage the content again. #1390 decides that case with |
|
Thanks for pointing to #1390. I missed it when I searched for related PRs before opening this one, and it has the same core idea, earlier. I measured the reuse case on Ocarina of Time. A diagnostic counter counted every uncompressed texture taken from its storage over storage the GPU had written, where the CPU then wrote again: that is, every texture a range rule without a CPU-rewrite check would refuse. Same route as before: load the cave save, walk out a long way, walk back in. About 9,900 frames.
So tile storage does move between GPU-drawn and CPU-filled textures in this game. A range has to stop counting once the CPU writes it again, which is what the page protection in this PR does. Happy to rebase this onto #1390 if that is easier to review: its range table, with this PR's page-protection and re-stamp on top. Or the other way round. Your call with the maintainers. |
|
Thank you for the numbers: 10 terrain maps in 20 reuses over a walk is exactly the case to get right. Yes please, rebasing your page protection and re-stamp onto #1390's range table would be welcome; one range table is easier to review than two. I'll check the reuse case against #1390 on our side too. |
…te that storage, not their pool: Ocarina of Time's terrain height and blend maps no longer come out as zeros, the ground opening in holes after walking (PR #1604) tex_cpu_content refused every uncompressed 2D texture of a pool once anything in that pool had been a render target, a clear's or a copy's destination, or a shader image. Ocarina of Time keeps its terrain tiles' height (260x260 R32F) and blend / hole maps (260x260 RGBA8) in the pool its tiles' normal maps are drawn into, and the CPU fills them before nvnTextureInitialize with no flush: after the first normal map was drawn, every tile streamed in became a GPU texture of zeros. The host now keeps the address ranges the GPU writes (mtl_tex_gpu_written, gws_span), dropped with their pool. Each keeps the dirty-page generation of its last GPU write and has its whole pages protected, so storage the CPU rewrites afterwards is taken again, and a texture taken there becomes a GPU texture at the next GPU write (cpu_unc_drop). Ranges do not merge: a GPU write over part of one cuts it and the rest keeps its own stamp. Storage the CPU cannot write (CPU_NO_ACCESS, virtual) gets no range, and a range keeps its dirty-page handle, so the check at each render target, clear and copy scans no pool list. Ocarina of Time (M4 Pro, Kokiri autosave and a walk): the blend and height maps are taken from their storage (none on main), 433 CPU-written textures alive against 270, faults 523 against 538, frame and GPU time unchanged. Minecraft Dungeons II (menu to first control): the same picture and frame time; mprotect 44.7 ms over the run against 5.4 ms. t_cpu_storage 44 checks, 0 failed. Co-authored-by: Felipe Milioni <20670722+fmilioni@users.noreply.github.com>
|
Landed as e2b76d4, with you as co-author. Thank you for the diagnosis, the route test and the reuse-case numbers: the page protection that lets storage go back to the CPU is what made this the right fix. Changed on top of your branch:
Checked on an M4 Pro:
|
…ir storage by GPU-written range, PR seedds#1604; a revoked GPU stops the game, PR seedds#1332) into switch2 The fork's own GPU-written range tracking (DM_NVN_GPUW_RANGE) gives way to upstream's gws_* spans, which do the same for Ocarina's terrain; the NDVE scale marks its target through mtl_tex_gpu_written. front_gpu_lost uses the fork's front_stop_alert beside front_gpu_memory_stop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
…il cached view, on the test stack seedds#1604 uncompressed textures are taken from their storage unless the GPU wrote that storage, by address range, not their pool: the terrain no longer opens holes after walking a distance (streamed tiles came out zero once any texture in their pool had been a render target; the 260x260 tera_tile special case goes with it). Hand-merged with the stack's own ownership code: serial GPU writes (seedds#1558) and the BC render-target shadows (seedds#1590) stay; the image bind records the range through mtl_tex_gpu_written beside them; mtl_pool_gpu_written is gone with its last caller. seedds#1607 a texture ID with no texture drops its cached view: nil == nil kept a released texture valid on the pool tables' fast path, and the fast paths gave the shaders its resource ID after the host had released it (GPU page faults). The nil guard keeps the alias-view parent match. Tested in game on an M3 Max: a walk between areas holds the ground whole, no page faults, no regression of the earlier fixes (upscaler lines, load-time holes, stutters). Front-ends: host change only, chain unchanged (2027aa29b1b7).
…whose pages were write-protected are no longer lost (issue #1591) A zero-copy Metal buffer made while any of its pages is read-only drops every GPU write through it, silently. Chunks of large pools, a lengthened chunk, the whole-pool buffer of raw global memory and a small pool sharing a page with a protected neighbour were all made that way. pool_buf makes them under nvn_dirty_hold: protected pages become writable and count as written (packaged textures are read again), and GPU-written storage the CPU had not written gets its stamp and protection back (PR #1604's spans). Open: a CPU write into such storage while the buffer is being made is not seen. Co-authored-by: daviddiblasi <92635269+daviddiblasi@users.noreply.github.com>
…merges clang-tidy found one more core.NullDereference in nvn_mtl.m (tex_cpu_content's first-use log line read t->pool->flags without a pool, seedds#1604), and one more nullability.NullablePassedToNonnull each in t_copy_alias.m (a kernel function looked up by name, seedds#1556) and t_pso_archive.m (the archive path from a boxed C string, seedds#1307). The log line prints 0 for a texture without a pool; the tests stop when the lookup fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK6QbhFrzr2Vop9t34k
…age again: GPU-written spans keep the dirty-page stamp of their last GPU write and protect their whole pages; Ocarina of Time's terrain tiles reused over drawn normal maps no longer open holes in the ground after leaving the Deku Tree (from upstream e2b76d4, PR seedds#1604, on this tree's ranges)
…te that storage, not their pool: Ocarina of Time's terrain height and blend maps no longer come out as zeros, the ground opening in holes after walking (PR seedds#1604) tex_cpu_content refused every uncompressed 2D texture of a pool once anything in that pool had been a render target, a clear's or a copy's destination, or a shader image. Ocarina of Time keeps its terrain tiles' height (260x260 R32F) and blend / hole maps (260x260 RGBA8) in the pool its tiles' normal maps are drawn into, and the CPU fills them before nvnTextureInitialize with no flush: after the first normal map was drawn, every tile streamed in became a GPU texture of zeros. The host now keeps the address ranges the GPU writes (mtl_tex_gpu_written, gws_span), dropped with their pool. Each keeps the dirty-page generation of its last GPU write and has its whole pages protected, so storage the CPU rewrites afterwards is taken again, and a texture taken there becomes a GPU texture at the next GPU write (cpu_unc_drop). Ranges do not merge: a GPU write over part of one cuts it and the rest keeps its own stamp. Storage the CPU cannot write (CPU_NO_ACCESS, virtual) gets no range, and a range keeps its dirty-page handle, so the check at each render target, clear and copy scans no pool list. Ocarina of Time (M4 Pro, Kokiri autosave and a walk): the blend and height maps are taken from their storage (none on main), 433 CPU-written textures alive against 270, faults 523 against 538, frame and GPU time unchanged. Minecraft Dungeons II (menu to first control): the same picture and frame time; mprotect 44.7 ms over the run against 5.4 ms. t_cpu_storage 44 checks, 0 failed. Co-authored-by: Felipe Milioni <20670722+fmilioni@users.noreply.github.com>
…whose pages were write-protected are no longer lost (issue seedds#1591) A zero-copy Metal buffer made while any of its pages is read-only drops every GPU write through it, silently. Chunks of large pools, a lengthened chunk, the whole-pool buffer of raw global memory and a small pool sharing a page with a protected neighbour were all made that way. pool_buf makes them under nvn_dirty_hold: protected pages become writable and count as written (packaged textures are read again), and GPU-written storage the CPU had not written gets its stamp and protection back (PR seedds#1604's spans). Open: a CPU write into such storage while the buffer is being made is not seen. Co-authored-by: daviddiblasi <92635269+daviddiblasi@users.noreply.github.com>
What
In Ocarina of Time (
0400486024DEC000), the ground opens in holes after walking a distance: loading a save in a cave, walking out and on, the terrain starts missing (black holes, water and sky showing through), and walking back into the cave shows it broken too. Saving and loading makes everything right again, until the next areas stream in.Cause
tex_cpu_contenttakes an uncompressed 2D texture from its storage at first use only in a pool the GPU has not written (pool_t.gpu_written): once anything in a pool had been a render target, a clear's or a copy's destination, or a shader image, no uncompressed 2D texture of that pool was taken from its storage again.Ocarina of Time keeps its terrain tiles' height maps (260x260 R32F) and blend / hole maps (260x260 RGBA8, one and two layers) in the same
CPU_CACHED | GPU_CACHEDpool (flags 0x24) as the tiles' normal maps (260x260 RG8), which the GPU draws. The CPU fills the height and blend maps beforenvnTextureInitialize, with no flush, so neithertex_dirty_upload(it stamps at registration) nor the flushed-range fill sees the write. After the first normal map was drawn, every tile streamed in became a GPU texture of zeros. A reload hid it because the area's tiles were read again before the first normal map of the session was drawn.A diagnostic line at GPU texture creation (storage holding data, made as a GPU texture) showed exactly those textures:
260x260 format 0xaand260x260x1/x2 format 0x25, pool flags 0x24,gpu_written 1, not flushed, about 25 seconds into the walk.Fix
The decision is made per address range instead of per pool (
nvn_mtl.m):mtl_tex_gpu_written,gws_span), dropped with their pool. It replacesmtl_pool_gpu_writtenat the same seven sites, andpool_t.gpu_writtengoes.tex_cpu_contentrefuses an uncompressed 2D texture only when the GPU wrote its own storage.cpu_unc_drop, now triggered by those ranges instead of a pool's first write). Only whole pages count, since a range's first and last pages are shared with whatever lies beside it.Related pull requests
nvn_ft_polldropped becausemtl_tex_gpu_ownedtook render-target usage for GPU ownership. Here the CPU writes before the first use, a path A registered texture the CPU rewrites after its first use is kept as the GPU's only once the GPU has written it (Ocarina of Time's flat blue terrain holes) #1558 does not reach. The two do not overlap.Tests
t_cpu_storage: 40 checks, 0 failed. New cases: a texture beside a GPU-written texture in the same pool is taken from its storage; one over storage the GPU wrote and the CPU then rewrote is taken, and becomes a GPU texture at the next GPU write there.dm/gfx/tests: every test of the run list passes exceptt_nvnandt_gfx_suspend(they do not link onmain, fixed by The graphics run list passes on main again: t_nvn and t_gfx_suspend link, t_fuse_check checks its kept target by serial #1578) andt_fuse_check(2 of 10 checks fail onmainas well).lint_c.py tidy(LLVM 23.1.4) onnvn_mtl.m,hle_nvn.c,t_cpu_storage.m: no new finding; the 3 file/check pairs above the baseline innvn_mtl.mare the same onmain.test_docs.py: pass.regress.py
Games: Super Mario Bros. Wonder, the only local build with a route that runs the NVN texture paths. Breath of the Wild has a route but its local build is on another chain. Ocarina of Time has no route; it was checked by hand above. Wonder never reaches the changed path (neither run logs a texture taken from storage), so its run shows no collateral change, not the fix. Unchecked: Mario Kart World, the game the previous per-pool rule was found on (its terrain patches lie in a pool the GPU does not write, which the range rule treats the same way), and the other profiles with
tex_dirty_upload.regress.py check (full): PASS
Baseline
88c7e0388c7e03, candidate731096b731096b3615a; Apple M5 Max, macOS 27.0.1, Xcode 27.2; 743.7 s010015100B514000Super Mario Bros Wonder
010015100B514000: PASS