Skip to content

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

Closed
Romen-Adama-Dev wants to merge 1 commit into
seedds:mainfrom
Romen-Adama-Dev:fix/tex-poll-gpu-written
Closed

Romen-Adama-Dev wants to merge 1 commit into
seedds:mainfrom
Romen-Adama-Dev:fix/tex-poll-gpu-written

Conversation

@Romen-Adama-Dev

@Romen-Adama-Dev Romen-Adama-Dev commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

What

With tex_dirty_upload, a registered texture that the CPU writes after its first use is uploaded from guest memory by nvn_ft_poll, unless its host texture "is the GPU's" (mtl_tex_gpu_owned), so that a CPU write to pages it shares does not replace what the GPU drew. That test was the texture's usage: render-target or shader-writable. But NVN has no flag for a render target, so tex_desc gives every uncompressed texture made without a CPU fill render-target use from the start, and the test held for every such texture whether or not the GPU had ever written it. A texture bound before the CPU filled it, then filled, was never uploaded; only block-compressed textures (no render-target use) came through, which is why the poll worked for Tears of the Kingdom's BC4 atlas and ASTC arrays.

Ocarina of Time's terrain tiles (260×260, one CPU_CACHED | GPU_CACHED pool: R32F height, RGBA8 and 2-layer RGBA8 blend and hole maps, block-compressed detail, RG8 normals with 9 levels) are bound at creation and written from the CPU as tile slots are reused while the player moves. Every uncompressed one was dropped, and after about an hour the ground opened flat blue holes with grass and rocks floating over them (Kokiri Forest, the village floor, the Deku Tree), the same look the profile's keys had fixed for tiles written before their first use.

Fix

  • nvn_mtl.m: mtl_tex_storage_gpu_owned: whether the GPU wrote the texture's storage and the CPU has not rewritten it since, through the storage spans tex_cpu_content keeps since e2b76d4 (gws_owned, stamped when a GPU write is recorded).
  • nvn_ft_poll asks it instead of the host texture's usage: a texture the GPU never wrote, or wrote before the CPU's write, is uploaded; one the GPU wrote after the CPU's write stays the GPU's. mtl_tex_gpu_owned (usage) stays for tex_storage_dst, so packaged textures keep their render-pass readbacks.
  • The poll logs the first four uploads and the first four textures kept.
  • t_cpu_storage: storage the GPU never wrote is the CPU's; the GPU's once it writes it; the CPU's again once it rewrites it.
  • Reference entry Textures made for GPU use kept from their CPU rewrites, the game note, the symptom index.

Measured

Mac (M3 Pro, 18 GB), loading a Kokiri Forest save and turning the camera, two runs:

before after
CPU-rewritten textures the poll uploaded 0 (every one dropped) 263 in 6,600 frames; 127 in 10,600 frames
kept as the GPU's all 4 (RG8 normal tiles the GPU had drawn)

The 4 kept are right: the diagnostic showed the CPU zeroing their level 0 (every byte 0 at the poll) before the GPU draws every level. The uploaded ones are the tiles' height, blend and hole maps and a few small lookup textures (4×1 RGBA8, 32×32 RGBA8, 4×4×6 R32F, 64³ BC4) that had the same fate. Frame time unchanged on this walk (p50 16.9–18.9 ms); the uploads happen as tiles stream in.

Not yet eye-checked at the exact spots of the reported holes (they appeared after an hour of play on the previous host); the mechanism and the textures match the profile note's description of the symptom, and the fixed host is now in the play build for that check.

Tests

  • t_cpu_storage (45 → 48 checks), t_cpu_tex (68), t_clear_tex (51), t_readback (153): pass.
  • dm/tools/tests/test_docs.py: pass.
  • regress.py check --games cannot run on this Mac: Ocarina of Time is the only local game. Reach: every profile with tex_dirty_upload (Tears of the Kingdom, FINAL FANTASY TACTICS, The Great Ace Attorney Chronicles, Kirby Air Riders, Ocarina of Time), whose block-compressed cases are unchanged; uncompressed textures in those games that were bound before the CPU filled them now upload too.

@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation game: 0400486024DEC000 A game labels Nov 6, 2026
@dderiso1

dderiso1 commented Nov 6, 2026

Copy link
Copy Markdown

Independent corroboration of the CPU-upload ownership problem on Ocarina of Time v131072, macOS 27.0, Apple M5 Ultra. In Kokiri Forest we captured two sampled 260×260 R32Float height maps whose Metal backing contained 67,600 zeros while the corresponding guest storage held nonzero heights around 0.134–0.175.

Our downstream fix tracks actual GPU writes and lets unrelated textures sharing a GPU-written pool retain CPU upload ownership. Afterward the two captured streamed height maps matched the deswizzled guest storage byte-for-byte. This tested a separate implementation; direct validation of #1558 and #1447 remains pending.

Two regression cases proved useful beyond usage-flag classification:

  • Start with a CPU-filled texture, write an unrelated resource in the same pool, then make another CPU fill and verify the texture receives it. Both tex_cpu_content and the later cpu_unc_drop transition need the per-texture rule.
  • Create another texture object over an already GPU-written Metal backing, then make a CPU write. The alias must preserve the backing's GPU ownership so the poll cannot overwrite the GPU result through that second serial. We also preserve that state across texture-view or format upgrades.

Those cases may be useful alongside this PR's coverage of a texture bound before its first CPU fill. Our 106-check storage suite includes real GPU readback for these transitions.

cfryerdev pushed a commit to cfryerdev/nxnative that referenced this pull request Nov 7, 2026
…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).
…the GPU's only when the GPU wrote its storage after the CPU: every uncompressed GPU-only texture is made with render-target use, so the poll dropped every such rewrite, and Ocarina of Time's terrain tiles (height, blend and hole maps rewritten as tile slots are reused) opened flat blue holes in the ground after an hour

nvn_ft_poll asks mtl_tex_storage_gpu_owned, the storage spans tex_cpu_content keeps (gws_owned: the GPU wrote some of the storage and the CPU has not rewritten it since), instead of the host texture's usage. Loading a Kokiri Forest save: 127-263 textures uploaded in the first 3-4 minutes where none was, 4 GPU-drawn normal tiles kept (the CPU only zeroes their level 0 before the GPU draws them). t_cpu_storage checks the three cases; reference entry, game note and symptom index.
@Romen-Adama-Dev
Romen-Adama-Dev force-pushed the fix/tex-poll-gpu-written branch from 022558e to 0ed3c57 Compare November 7, 2026 04:49
seedds added a commit that referenced this pull request Nov 7, 2026
…age by the first-use rule: with tex_dirty_upload the poll asked the host texture's usage, which every uncompressed GPU-only texture has as a render target, so Ocarina of Time's terrain tile maps kept their old content (flat blue holes in the ground) (PR #1558)

nvn_ft_poll asks mtl_tex_cpu_rewritten, which applies tex_cpu_content's rule without its non-zero check: the same flags and formats (tex_storage_ok, split out of tex_cpu_content), and an uncompressed 2D texture not while the GPU has written its storage and the CPU has not rewritten it since (gws_owned). An uncompressed texture it uploads joins g_cpu_unc (cpu_unc_take), so cpu_unc_drop makes it a GPU texture again when the GPU draws into it. A block-compressed texture is uploaded whatever the GPU wrote: GPU copies into a registered one land in its storage too, so a GPU-written span over it does not make the storage stale. A texture left to the GPU because the GPU wrote after the CPU stays registered with a new stamp (its pages protected before the question), so the CPU's next rewrite is still seen; textures the rule leaves out keep the usage test.

Ocarina A/B (M4 Pro, Kokiri autosave and a walk onto the village floor, 160 s, both hosts on chain d866f9700d78 for the build's libraries): about 250 textures uploaded by the poll against none, 17 of them made GPU textures again when drawn into, picture whole at 60 fps, 36 long frames against 57. t_cpu_storage 56 checks, t_cpu_tex 73 (the poll itself, with a tex_dirty_upload profile), t_clear_tex 51, t_readback 154, test_docs: pass. Tears of the Kingdom not run (no build on hand).

Co-authored-by: Romen Adama Caetano Ramírez <56840314+Romen-Adama-Dev@users.noreply.github.com>
@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Landed as b547f1e, with you as co-author: thank you for the diagnosis. The usage test could never answer the question, and your log lines and reference entry are kept.

The commit reworks the poll in three places:

  • Block-compressed textures upload as before. A GPU copy into a registered BC texture is also written into its storage (tex_storage_dst). So a GPU-written span over one doesn't make its storage stale, and asking the span would have kept a CPU rewrite followed by a copy from being uploaded. The poll now applies tex_cpu_content's own rule (mtl_tex_cpu_rewritten, sharing tex_storage_ok): the span question only for uncompressed 2D textures, and the usage test for the textures that rule leaves out (depth, display, video decode, compressible, multisample).
  • An uncompressed texture the poll uploads joins g_cpu_unc, so cpu_unc_drop makes it a GPU texture again when the GPU draws into it. Otherwise it stayed CPU-written for good, and a later CPU write nearby would re-upload stale storage over the drawing.
  • A texture left to the GPU stays registered with a new stamp, so the CPU's next rewrite is still asked about. Once the decision depends on time rather than on usage, leaving the list for good would miss it.

On an M4 Pro (Kokiri autosave and a walk onto the village floor, 160 s): about 250 textures were uploaded by the poll against none before, and 17 of them became GPU textures again when drawn into. The picture was whole at 60 fps. t_cpu_storage has the span cases, and t_cpu_tex now drives the poll itself with a tex_dirty_upload profile.

@dderiso1, thanks for the independent check. Both of your cases hold, because the spans are by address: an unrelated GPU write in the same pool doesn't take a texture away from the CPU, and a second texture object over GPU-written storage is the GPU's too. The second one is now a test.

Still open: an eye check at the exact spots where the holes appeared after an hour of play.

@seedds seedds closed this Nov 7, 2026
Aznracer pushed a commit to Aznracer/Mighty-Green-Kokiri-Kid that referenced this pull request Nov 8, 2026
…age by the first-use rule, and Ocarina of Time turns on tex_flush_upload / tex_dirty_upload: its terrain tile maps kept their old content (flat blue holes in the ground) (from upstream b547f1e, PR seedds#1558, on this tree's GPU-written ranges)
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…age by the first-use rule: with tex_dirty_upload the poll asked the host texture's usage, which every uncompressed GPU-only texture has as a render target, so Ocarina of Time's terrain tile maps kept their old content (flat blue holes in the ground) (PR seedds#1558)

nvn_ft_poll asks mtl_tex_cpu_rewritten, which applies tex_cpu_content's rule without its non-zero check: the same flags and formats (tex_storage_ok, split out of tex_cpu_content), and an uncompressed 2D texture not while the GPU has written its storage and the CPU has not rewritten it since (gws_owned). An uncompressed texture it uploads joins g_cpu_unc (cpu_unc_take), so cpu_unc_drop makes it a GPU texture again when the GPU draws into it. A block-compressed texture is uploaded whatever the GPU wrote: GPU copies into a registered one land in its storage too, so a GPU-written span over it does not make the storage stale. A texture left to the GPU because the GPU wrote after the CPU stays registered with a new stamp (its pages protected before the question), so the CPU's next rewrite is still seen; textures the rule leaves out keep the usage test.

Ocarina A/B (M4 Pro, Kokiri autosave and a walk onto the village floor, 160 s, both hosts on chain d866f9700d78 for the build's libraries): about 250 textures uploaded by the poll against none, 17 of them made GPU textures again when drawn into, picture whole at 60 fps, 36 long frames against 57. t_cpu_storage 56 checks, t_cpu_tex 73 (the poll itself, with a tex_dirty_upload profile), t_clear_tex 51, t_readback 154, test_docs: pass. Tears of the Kingdom not run (no build on hand).

Co-authored-by: Romen Adama Caetano Ramírez <56840314+Romen-Adama-Dev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants