Repository navigation
Ocarina of Time: fix the "plaid" terrain / missing texture tiles (GPU-drawn textures now reach the storage the game copies) - #2077
Conversation
…aches that storage: Ocarina of Time's terrain no longer turns to crossing stripes ("plaid") after dungeon entries and exits
On the hardware a render pass, a texture clear or a copy into a texture writes
the texture's storage. The host wrote it only for textures it uploads from
their storage (Tears of the Kingdom's terrain normal maps, which a flush makes
CPU-written). Ocarina of Time has the same terrain defragmentation but never
flushes its normal tiles (260x260 RG8, 9 levels, a CPU_CACHED | GPU_CACHED
pool): they stay GPU textures, their drawing lived in the host texture alone,
and the defragmentation's memcpy moved zeros. The texture re-created at the new
address was taken from them: a zero normal.
tex_storage_gpu admits an uncompressed 2D texture that tex_storage_ok admits.
Render passes, clears and copies mark the level, slices and region they write,
by storage range (gsb_mark); the marks are read back once at the end of the
submission (gsb_flush) through mirror_readback. A level written within the two
frames before waits until two frames pass without a write, so a target drawn
every frame costs two readbacks, not one a frame. The bytes land only in
storage that is still that texture's and the GPU's (mirror_rb_own); the span
and the registered texture's entry are stamped again, so the host's own write
is not taken for the CPU's. A texture clear of a texture uploaded from its
storage is read back as its render passes are. DM_NVN_GPU_STORAGE=0 turns the
new readbacks off.
Reference entry: docs/reference/nvn-objects.md, "GPU textures in storage the
CPU can read". Refs seedds#1865.
…aches that storage: Ocarina of Time's terrain no longer turns to crossing stripes ("plaid") after dungeon entries and exits (PR seedds#2077)
seedds#2030, seedds#2077, seedds#2022, seedds#2075, seedds#2085), last merged at 04811a6, and the Castle Town result on a MacBook Air M4 Castle Town on a 16 GB MacBook Air M4 at 30 fps and full resolution: GPU p50 26-27 ms a frame and 6.9 GB of GPU memory allocated with seedds#2085, against 16.2 GB and about 9 fps before it. Tests: dm/tools/tests/test_docs.py passes.
…e "plaid" terrain / missing texture tiles (GPU-drawn textures now reach the storage the game copies) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
This is a strong lead for the persistent plaid terrain seen in our OOT playthrough. Our earlier terrain regression began with CPU-filled content; it did not establish the GPU-generated normal-map → backing storage → CPU relocation direction you traced here. Thank you for documenting that distinction and the exact-branch gameplay limit. Two additional synthetic cases would help make this safe to take generally. These are source-review concerns, not claims of failures reproduced in our gameplay:
For our integration we are holding this change separately until those visibility/ownership cases are covered. We are not proposing a full-size stand-in, blanket texture flag change or lower rendering resolution as a substitute. |
Conflicts resolved by hand: - seedds#2085, seedds#2078 and seedds#2065 came back from upstream as reworked commits (6f50de0, 12a44e0, 5e1c1ae): our three merges were reverted first, so the merge brings upstream's version instead of two copies. - GPU copies into a texture (copy_b2t, mirror_rb_land, nvn_obj.h): upstream's seedds#1310 rework (mspan_t, pkg_copy_absorb, for the readback case) and our seedds#2077 (mirror_rb_own, gsb_mark, for a texture the GPU owns) cover different cases and both stay: own = !rb is taken before the synchronous mirror, so the two paths exclude each other. mirror_rb_own passes no span. - loader.c: upstream's render-scale parse with the stub's largest scale (Hades II) and our graphics-scale tier block. t_readback.m: upstream's additive tests added to ours. pso_offline.py: its header comment. Docs and tables keep both sides; DM_RENDER_SCALE rows, render_base and the settings table take upstream's Hades II range plus our text. - dmsh_chain.h regenerated; .github/workflows/tests.yml follows upstream (a test list). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… written into that storage on the GPU, in the same submission: Ocarina of Time's terrain no longer turns to crossing stripes ("plaid") after dungeon entries and exits (PR #2077)
On the hardware a render pass, a texture clear or a copy into a texture writes its storage, and a
game may read it there. The host wrote that storage only for textures it uploads from their
storage (a CPU readback when the submission completes). Ocarina of Time's terrain normal tiles
(260x260 RG8, 9 levels, a CPU_CACHED | GPU_CACHED pool, never flushed) stay GPU textures, so their
drawing lived in the host texture alone; the terrain defragmentation moves them with memcpy and
re-creates them, and the texture made at the new address was taken from zeros. Shown by Roan de
Graaf with a tag in each drawn tile's storage, found at the copies' addresses.
gsb_store runs after each render pass, texture clear and buffer or texture copy into such a
texture (tex_storage_gpu: uncompressed 2D, tex_storage_ok, not CPU-written, not one the host
uploads from its storage, a host format of the storage's texel size): the region written is
copied from the host texture into the frame arena and swizzled into the pool's buffer by the new
swz kernel (dm/gfx/msl/deswz.metal, the inverse of deswz), only the region's texel bytes. It is in
the same command buffer, so it lands before any fence after the write, and page protection does
not see it: the storage stays the GPU's and the texture a GPU texture. The PR's first version read
the region back on the CPU and held a level written in two nearby frames until two quiet frames
passed; a copy within that time got the older content, and the late landing needed guards
against reused storage. Written on the GPU, every write is in the storage when its fence signals
and none lands later. A texture clear of a texture uploaded from its storage is read back as its
render passes are. The power line gains "[nvn] GPU texture storage". DM_NVN_GPU_STORAGE=0 turns
the stores off.
Checked (M4 Pro): make -C dm/gfx/tests GAME=0400486024DEC000 run exits 0; t_cpu_storage 136
checks (a write the frame after another is in the storage at once and a memcpy right after it
gets it; nothing lands over storage the game reused, however many frames pass; a partial region;
every level of a 260x260 RG8 9-level tile, every texel) and t_readback 204 cases, 9 and 5 of them
failing with DM_NVN_GPU_STORAGE=0. Minecraft Dungeons II, title to first control, three on/off
pairs: about 470 regions (116-125 MB) stored while loading, about 280 (47 MB) entering the level,
none in play; 60 fps in play either way, the same picture. Ocarina of Time not run on this design
(its build's libraries are of an older chain); regress.py check not run (no smoke route on the
games built here). Refs #1865.
Co-authored-by: Roan de Graaf <36153028+roandegraaf@users.noreply.github.com>
|
Landed reworked in 6c3aeb3, with you as co-author. Thank you: the tag in the storage made the cause certain, and the diagnosis, the What changed is how the drawing reaches the storage. Instead of a CPU readback when the submission completes, the GPU writes it: after each render pass, texture clear or copy into such a texture,
akratch's two cases are tests in Checked on an M4 Pro: Not run here: Ocarina of Time on this design (the build here has libraries of an older chain). A play session with dungeon entries and exits on current main, and a count of the Closing this PR, as the change is on main. |
seedds#1901 seedds#1459 seedds#1928 seedds#1942 seedds#1940 seedds#1952 seedds#1611 seedds#2065 seedds#2083 seedds#2008 seedds#2079 seedds#2022 seedds#2075 seedds#2077 seedds#2020 seedds#2021 onto main 04811a6 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7 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#1459 by PromethOps (open): DM_UPSCALER=metalfx takes agl's agl_ndsr_vr.pck networks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's - seedds#1611 by Esteban-V (open): An argument block whose only change is a uniform buffer is patched instead of encoded again - seedds#1774 by uncraftablexd (closed): Compute threads that exit before a barrier keep arriving at it, and every shader loop has a cap (GPU hang defences), chain change - seedds#1824 by cocoman93 (open): Clamp texel fetches past a sampled 2D texture's edge: no hard edge in Ocarina of Time's mist - seedds#1901 by FifteenPen (open): The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms - seedds#1928 by FeReuss (open): Ocarina of Time: enc_thread on, the replay encodes on a thread of its own - seedds#1929 by FeReuss (open): Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC) - seedds#1930 by FeReuss (open): A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop - seedds#1940 by uncraftablexd (open): A prewarm list's lines after its #@after_first_frame line are built after the first frame, in the background, instead of in the prepare step (issue seedds#1747) - seedds#1942 by uncraftablexd (open): Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed - seedds#1952 by AdyaGMD (open): Compute moves ahead of scene passes past occlusion queries and conditional rendering; clears wait across dispatches - seedds#2008 by bquilty25 (open): nvn: reactive memory watchdog, 16-way view cache, and texture upload batching - seedds#2020 by padsiwono (open): Ocarina of Time v131072: Output Native draws the full output size instead of the game's 80 % frame (`DM_OUTPUT`) - seedds#2021 by padsiwono (open): Ocarina of Time v131072 renders at the launcher's Resolution (`DM_RENDER_SIZE`), docked - seedds#2022 by sleepyeyedjoe (closed): Ocarina of Time: large models no longer go black for one frame twice, 4–5 frames apart, when the game streams new textures into them (a texture ID a shader has read keeps its read mark when it gets another texture) - seedds#2030 by humanperzeus (open): A NaN colour at alpha 0 is returned as 0, as NVIDIA's blender takes a zero factor: Ocarina of Time's white clouds over the ferns are gone - seedds#2035 by Know-It-All-Brother (open): The shader build runs as worker processes, and no shader waits for a batch (Ocarina of Time's cold Mac map: 3,002 s to 1,298 s) - seedds#2065 by Raymon82777 (closed): A vertex library with layer entry points of its own gets bases for its main0 pairs: Ocarina of Time's 230 ms camera-turn hitch in Hyrule Field goes to 39 ms - seedds#2075 by sleepyeyedjoe (closed): Ocarina of Time: a patch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make holds a stand-in of its own colours until its first read) - seedds#2077 by roandegraaf (closed): Ocarina of Time: fix the "plaid" terrain / missing texture tiles (GPU-drawn textures now reach the storage the game copies) - seedds#2079 by uncraftablexd (open): Render pipelines that share a vertex function are taken from the archive or compiled one at a time: the AGX driver crashes in createVertexProgramVariant's completion block - seedds#2083 by FifteenPen (closed): Ocarina of Time's prewarm list gets 41 more task / mesh pipelines from later play
and seedds#2085 in upstream's versions, the host at -O2, Ocarina of Time's camera-turn hitch in Hyrule Field (230 → 39 ms), 41 more task / mesh pipelines and 4,959 render / compute pipelines in its prewarm list, GPU writes into uploaded textures kept on the GPU, compact NVN recordings; chain 8f2d8a09c3f9 PR seedds#2030 (blend_zero) stays the fork's own: upstream moved the shader build's MSL rewrites into dmsh_rewrite.py (80fc2ec), so blend_zero and BLEND_ZERO move there, dmsh_build.py finish calls dmsh_rewrite.blend_zero, and test_dmsh_blend_zero.py imports it. dmsh_chain.h regenerated (8f2d8a09c3f9). Tests: test_dmsh_*.py 26/26 OK (test_dmsh_pairs with DM_GAME=0400486024DEC000); t_pool_chunks 151/0, t_cpu_storage 135/0, t_readback 218/0, t_pool_seen_replace 44/0, t_pool_standin 31/0, t_pool_notes 81/0, t_mesh_first_use 53/0.
What and why
Symptom. In The Legend of Zelda: Ocarina of Time (
0400486024DEC000, v131072) on the Mac, after several area changes with dungeon entries and exits the ground turns into crossing stripes ("plaid"), tile by tile, and stays that way until the game is restarted. Loading a save alone does not bring it. This is one of the wrong-terrain kinds reported in #1865 (refs #1865; it does not claim the others).Cause. On the hardware a render pass, a texture clear or a copy into a texture writes the texture's storage, and a game may read it there. The host wrote that storage only for textures it uploads from their storage (Render passes into textures uploaded from their storage: Tears of the Kingdom's terrain normal maps, which a flush makes CPU-written).
Ocarina of Time has the same terrain defragmentation (
SeqExecuteDefragmentationForTexture:memcpyto a new address in the pool, then the texture is re-created there) but never flushes its normal tiles (260x260 RG8, 9 levels, aCPU_CACHED | GPU_CACHEDpool). They stay GPU textures, so their drawing lived in the host texture alone and thememcpymoved zeros. The texture made at the new address was then taken from those zeros: a zero normal.How it was shown, not guessed: a 32-byte tag written into each drawn normal tile's storage was later found at a different address 172 times in one play session (the earliest 12 frames after the tile was drawn, half after about 1,290). In the frame that showed the stripes, 42 of the 57 registered normal tiles had been made from storage holding a copied tag and otherwise zeros, 16 of the 18 bound to the terrain draws among them.
Fix (every game, no profile key). What the GPU writes into a GPU texture whose storage could be its content is read back into that storage, through the existing
mirror_readbackpath:tex_storage_gputex_storage_okadmits (no depth, display, video-decode,COMPRESSIBLE,IMAGE,LINEAR, multisample or buffer texture; a pool the CPU can access, not virtual)gsb_markgsb_flushmirror_rb_owntex_storage_dstis unchanged, and the new kind never uses the CPU mirror of a copy,g_mrborg_abs. One addition to the existing path: a texture clear of a texture uploaded from its storage is now read back as its render passes are.DM_NVN_GPU_STORAGE=0turns the new readbacks off (to measure).Reference entry with the open points:
docs/reference/nvn-objects.md, "GPU textures in storage the CPU can read". Game note line under "Found on this game".Checked
Tests (on this branch, base
04811a63, M4 Pro):make -C dm/gfx/tests GAME=0400486024DEC000 runexits 0. New cases int_cpu_storage(138 checks) andt_readback(gpu_storage, 175 / 176 cases); withDM_NVN_GPU_STORAGE=0, 10 and 5 of them fail.dm/tools/tests/test_docs.pyandtest_env_switches.pypass.Games run: Ocarina of Time
0400486024DEC000v131072, Mac.Base of the gameplay runs, please read: they were made on a local tree at chain
3510ea767778(an oldermainwith PRs Ocarina of Time takes the launcher's Render scale: the 3D views at that fraction of the game's own size, the dynamic resolution held at its top level (Kokiri Forest docked at 0.75: GPU p50 15.77 → 14.31 ms, 60 fps held) #1903, Drag x Drive (Switch 2, 040041601D83A000, v196608, SDK 20.5.6) plays on the Mac with a pair of Joy-Con 2s at the game's own 30 fps: its profile, note and table row (it needs PR #1609 to boot, and PR #1921, the first commit of this branch) #1944, The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms #1901, A draw that writes depth and samples its own depth attachment reads a copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone #1917, The controller applet's stand-in runs on the big host stack: Drag x Drive opened it from a game thread with too little stack for its log lines (SIGBUS in fprintf under focus_sync) #1943 and nn::hid::EnableUnintendedHomeButtonInputProtection is answered by the host and logged: Drag x Drive no longer crashes at sdk+0x63ce10 as player 1's Joy-Con 2 joins #1921 merged locally), with my own variant of the sliver fix that A GPU-written span's sliver inside a texture does not make it the GPU's: Ocarina of Time's terrain tiles no longer vanish or show another tile's surface (#1566) #1920 has since landed onmain. For this PR the change was ported onto currentmain(chain39eb2f781d07) and the landing guard adapted tomain'sgws_cpu_since. On this exact branch only the unit tests ran: I have no shader library for the new chain, so the game was not replayed on it.Devices and OS: MacBook Pro M4 Pro, macOS 27.2. No iPhone or iPad run.
regress.py:impactnames Breath of the Wild, Tears of the Kingdom and Pokémon Scarlet through the shared NVN entry points (render targets, clears, copies).check --gameswas not run: Ocarina of Time is the only game built on this Mac, and it has nosmoke.tsv. What the change can reach elsewhere, from the notes:COMPRESSIBLEorIMAGEgets one readback at its first write and one when it stops being written. The Great Ace Attorney Chronicles (1920x1080 targets in aCPU_CACHEDpool) is the one I would count first;A run on those by someone who has them would be welcome; the switch above gives the before/after.
Measurements: Ocarina frame times unchanged in the routes above (the readbacks happen only in the frames tiles are baked).
Docs
docs/reference/nvn-objects.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)