diff --git a/dm/games/0100C1F0051B6000/README.md b/dm/games/0100C1F0051B6000/README.md index fbda785b3d..df751e593e 100644 --- a/dm/games/0100C1F0051B6000/README.md +++ b/dm/games/0100C1F0051B6000/README.md @@ -28,6 +28,7 @@ No rendering key: the title logo and menu panels that needed `tex_flush_upload` ## Rendering +- **Resolved: background rectangles flickering in levels 1-3, 2-1, 2-2 and the world 1 boss** (GitHub issue #676): [Sampling the depth attachment of the same pass](../../../docs/reference/rendering.md#sampling-the-depth-attachment-of-the-same-pass). - **Resolved: the title logo and the menus' panels magenta** (no red line). The 912×512 ASTC 5×5 logo sits in a `CPU_UNCACHED | GPU_CACHED` pool (flags 0x22) whose storage holds its texels at first use (`DM_NVN_TEXINFO=912x512`: 242,374 of 376,832 bytes non-zero) and is never flushed. The profile keys `tex_flush_upload` and `tex_dirty_upload` brought the button icons but not the logo. A texture in a CPU-uncached pool now starts with its storage: [Storage of a CPU-uncached pool at first use](../../../docs/reference/nvn-objects.md#storage-of-a-cpu-uncached-pool-at-first-use). - **Resolved: DK wrapped in a brown box while he moves** (sliding, swimming, running; fine standing still). `DM_NVN_HILITE` showed it is his fur shells: an instanced draw (`648304c43bbe3463` / `5b5ffba62bca9e0a`, three instances on the body's index buffer, after the body `659ce20d37af3304` and the blended fur `2d24f1883b9f626f` / `3637414591888cf6`). The shell vertex shader pulls each shell toward a lagged position, `c3` rows 13–15 applied to vertex attribute 6, and that stream is a buffer the frame's vertex-only draw (`6979dd3cdbd62373` / `7dcdaed991c74bfe`, 3,400 vertices, `fs=-`) fills by transform feedback; `DM_NVN_KEYDUMP=5b5ffba62bca9e0a` showed it held zeros and `2.3e24`. Its feedback buffer captures two outputs (xyz each, stride 24), a form the build did not decode, and the game draws the capture with rasterization on, which the host did not capture. With both general fixes DK's fur follows him: [Transform feedback captured by a vertex entry point](../../../docs/reference/rendering.md#transform-feedback-captured-by-a-vertex-entry-point). diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index f3b343e05e..e38d5fe5a8 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -9450,6 +9450,13 @@ static int fb_mode(void) { if (g_fb_mode != 1) MLOG("same-pass feedback sampling snapshot: %s (DM_NVN_FB_SNAP)", g_fb_mode ? "a fresh one for every draw sampling an attachment of its pass" : "off"); } return g_fb_mode; } +// A draw sampling its own pass's depth attachment reads a copy (below, in fb_prepare): on by default only for Donkey Kong Country: Tropical Freeze (#676), whose reads in place give +// wrong tiles under Apple's lossless compression; elsewhere the read stays in place and in its pass. DM_NVN_FB_DEPTH=1 or 0 turns the copy on or off for any game +static int g_fb_depth = -1; +static int fb_depth_on(void) { + if (g_fb_depth < 0) { const char *e = getenv("DM_NVN_FB_DEPTH"); g_fb_depth = e && *e ? strcmp(e, "0") != 0 : DM_GAME_PROGRAM_ID == 0x0100C1F0051B6000ull; } + return g_fb_depth; +} static inline void fb_pass_sync(void) { uint64_t p = g_stat_pass + !g_enc; if (p != g_fb_pass) { g_fb_pass = p; g_fb_wmask = g_fb_rmask = g_fb_dread = 0; } } static inline int fb_shape(const fb_cache_t *e, MTLPixelFormat pf, NSUInteger w, NSUInteger h) { return e->snap && e->snap.pixelFormat == pf && e->snap.width == w && e->snap.height == h; } static inline int fb_live(const fb_cache_t *e) { return e->ser && e->bar == g_fb_bar && e->sub == g_sub_serial; } // a later draw may still reuse it @@ -9656,7 +9663,28 @@ static void fb_prepare(NvProg *pr) { MLOG("same-pass feedback sampling: snapshot #%llu, %llu draws reused one, %llu read the tile (%llu waited for their pipeline), %llu passes ended before a write to an attachment read directly (frame %llu pass %llu, shader %s slot %d samples attachment %d ser=%llu %lux%lu format %lu)", (unsigned long long)g_fb_count, (unsigned long long)g_fb_reuse, (unsigned long long)g_fb_tile_draws, (unsigned long long)g_fb_tile_wait, (unsigned long long)g_fb_split, (unsigned long long)atomic_load(&g_cur_frame), g_stat_pass + !g_enc, fs->key, b->reg, k, (unsigned long long)ser, (unsigned long)w, (unsigned long)h, (unsigned long)base.pixelFormat); PD("N %llu same-pass feedback sampling: attachment %d ser=%llu level %d layer %d copied into a snapshot; this draw samples the snapshot", g_stat_pass + !g_enc, k, (unsigned long long)ser, lv, sl); - } } + } + // The depth buffer of the draw's own pass, sampled (Donkey Kong Country: Tropical Freeze: 976bc74475e9443b, dbd64ad1611ed4c9 on its 1920x1080 Depth32Float): with Apple's lossless compression the read gives wrong + // values in whole tiles, background rectangles flickering from frame to frame (#676), even when the pass does not write depth. Without compression the read is right, at about 10% of the frame rate on that game. So it reads a copy + if (R.rt.depth && R.rt.depth->serial == ser && g_fb_n < 4 && fb_depth_on()) { + uint64_t aser; int lv, sl; rt_sub(&R.rt, 8, &aser, &lv, &sl); + os_unfair_lock_lock(&g_rmu); void *tv = NULL; tex_fast_rid(tid, &tv); id t = (__bridge id)tv; os_unfair_lock_unlock(&g_rmu); + if (!t || t.textureType != MTLTextureType2D || t.sampleCount != 1 || t.mipmapLevelCount != 1 || lv || sl || b->dtt != MTLTextureType2D) continue; + id dbase = t.parentTexture ? t.parentTexture : t; // Breath of the Wild samples it through a swizzled view: the same view of the copy + if (dbase.parentTexture || dbase.mipmapLevelCount != 1 || dbase.textureType != MTLTextureType2D || (t.parentTexture && (t.parentRelativeLevel || t.parentRelativeSlice))) continue; + int dup = 0; for (int q = 0; q < g_fb_n; q++) dup |= g_fb_src[q] == t; if (dup) continue; + int fresh; fb_cache_t *e = fb_snap(ser, 0, 0, t.pixelFormat, t.width, t.height, 0, &fresh); if (!e) continue; + if (fresh) { if (g_enc) rp_end(); id be = blit(); enc_b_copy_texture(be, dbase, 0, 0, MTLOriginMake(0, 0, 0), MTLSizeMake(t.width, t.height, 1), e->snap, 0, 0, MTLOriginMake(0, 0, 0)); } + if (!g_enc) fb_pass_sync(); + MTLTextureSwizzleChannels sw = t.swizzle; + int same = sw.red == MTLTextureSwizzleRed && sw.green == MTLTextureSwizzleGreen && sw.blue == MTLTextureSwizzleBlue && sw.alpha == MTLTextureSwizzleAlpha; + id use = same ? e->snap : [e->snap newTextureViewWithPixelFormat:t.pixelFormat textureType:MTLTextureType2D levels:NSMakeRange(0, 1) slices:NSMakeRange(0, 1) swizzle:sw]; + if (!use) continue; + if (g_fb_use[g_fb_n] && g_fb_use[g_fb_n] != use) defer_obj_unref(g_fb_use[g_fb_n]); + g_fb_src[g_fb_n] = t; g_fb_use[g_fb_n] = use; g_fb_rid[g_fb_n] = use.gpuResourceID._impl; g_fb_n++; + static uint64_t nd; if (fresh && (++nd == 1 || nd == 1000 || nd % 100000 == 0)) MLOG("same-pass depth sampling: snapshot #%llu (frame %llu, shader %s slot %d, ser=%llu %lux%lu format %lu)", (unsigned long long)nd, (unsigned long long)atomic_load(&g_cur_frame), fs->key, b->reg, (unsigned long long)ser, (unsigned long)t.width, (unsigned long)t.height, (unsigned long)t.pixelFormat); + } + } g_fb_wmask |= wm; if (g_fb_n) { g_fb_seq = g_draw_seq; args_dirty_all(); g_fb_rebind = 1; } if (ntile) { g_fb_tseq = g_draw_seq; g_fb_tile_draws++; diff --git a/dm/gfx/tests/t_feedback.m b/dm/gfx/tests/t_feedback.m index 1abb3fd488..a4c6a3e3b0 100644 --- a/dm/gfx/tests/t_feedback.m +++ b/dm/gfx/tests/t_feedback.m @@ -202,6 +202,7 @@ static void reuse_case(NvProg *pr) { CHECK(g_fb_n == 1 && !g_enc && g_fb_count == copies + 2, "a game barrier makes the next writing draw take a fresh snapshot"); rp_end(); // The depth attachment read, then written in the same pass (only serials matter to the rule: the attachment is named after the pass opens) + g_fb_depth = 0; // the default outside Donkey Kong Country: Tropical Freeze, whatever GAME the test is built for { tex_t output, depth; target(&output, 99791); target(&depth, 99792); mtl_reg_tex(651, &depth, NULL); // NOLINT(clang-analyzer-osx.cocoa.RetainCount): g_by_serial owns both textures (serial_put's CFBridgingRetain) until mtl_tex_release below, not a leak R.rt.color[0] = &output; R.rt.depth = NULL; S.tex[1][0] = 651; S.chmask.mask = 0xffffffffu; S.ds.dt = 1; S.ds.dw = 0; clear(MTLClearColorMake(0, 0, 0, 1)); R.rt.depth = &depth; @@ -216,6 +217,15 @@ static void reuse_case(NvProg *pr) { fb_prepare(pr); CHECK(g_enc != nil, "a draw writing depth with no read of it in its pass stays"); R.rt.depth = NULL; S.ds.dt = S.ds.dw = 0; rp_end(); mtl_tex_release(&output); mtl_tex_release(&depth); } + // DM_NVN_FB_DEPTH=1 (Donkey Kong Country: Tropical Freeze's default): the read takes a copy of the depth attachment, outside the pass + g_fb_depth = 1; + { tex_t output, depth; target(&output, 99793); target(&depth, 99794); mtl_reg_tex(652, &depth, NULL); // NOLINT(clang-analyzer-osx.cocoa.RetainCount): g_by_serial owns both textures until mtl_tex_release below + R.rt.color[0] = &output; R.rt.depth = NULL; S.tex[1][0] = 652; S.chmask.mask = 0xffffffffu; S.ds.dt = 1; S.ds.dw = 0; + clear(MTLClearColorMake(0, 0, 0, 1)); R.rt.depth = &depth; + fb_prepare(pr); + CHECK(g_fb_n == 1 && g_fb_src[0] != nil && g_fb_use[0] != g_fb_src[0], "with the copy on, a draw that reads the depth attachment samples a copy"); + R.rt.depth = NULL; S.tex[1][0] = 0; S.ds.dt = 0; rp_end(); mtl_tex_release(&output); mtl_tex_release(&depth); } + g_fb_depth = -1; CHECK(mtl_wait(mtl_sub_commit(), 5000000000ull), "GPU submission completed"); CHECK(!mtl_last_commit_error(), "GPU submission has no error"); printf("feedback reuse: %s\n", before == failed ? "PASS" : "FAIL"); diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 32b812c193..178eabe1af 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -541,6 +541,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - `texture clear case not implemented … mask 0x8` while entering a rendered scene. → [Texture clears with partial channel masks or regions](../reference/rendering.md#texture-clears-with-partial-channel-masks-or-regions) - red lines for unimplemented mid-pass, partial-mask and partial-scissor clears (the clears were dropped); `depth-stencil clear with a scissor or partial stencil mask, cleared whole` (logged once). → [Clears inside a pass, with a partial mask or a partial scissor](../reference/rendering.md#clears-inside-a-pass-with-a-partial-mask-or-a-partial-scissor) - a tile grid in Ultrahand and Recall highlights; an `EQUAL` depth test rejects fragments writing the same depth as a cleared Depth16 target. → [Depth16 load-clears match fragment writes](../reference/rendering.md#depth16-load-clears-match-fragment-writes) +- large rectangles or small squares flicker over the background from frame to frame, with no red line. The picture is right with lossless compression off on that one depth buffer, at a cost in frame rate (Donkey Kong Country: Tropical Freeze: about 50 fps instead of 60 at a boss). → [Sampling the depth attachment of the same pass](../reference/rendering.md#sampling-the-depth-attachment-of-the-same-pass) - solid magenta blocks, exactly (255, 0, 255), hard-edged and aligned to the 16-pixel grid (16×16, sometimes two stacked): Super Mario 3D World and Super Mario Odyssey showed them only on the iPhone; Captain Toad and Origami King on a Mac, Apple M6, Captain Toad in about half the frames of the opening course, moving with the falling leaves. On an RG11B10F target (no alpha) the blocks are pure black or neon green, (0, 252, 0), and flicker, sometimes only part of a tile: Super Mario Bros. Wonder on an Apple M6 and an M1, most visible on the speech bubbles of dialogue; or non-finite pixels that bloom spreads into green blobs (NieR:Automata on the Mac). A rendered cutscene alternates brightness every game frame, with blocky bands of light and shadow switching on and off: each lighting-zone bit flips every frame (Xenoblade Chronicles 3, a read-only image load). → [Same-pass feedback read through a snapshot](../reference/rendering.md#same-pass-feedback-read-through-a-snapshot) - a scene full of deferred decals runs far below its target where the same scene with `DM_NVN_FB_SNAP=0` holds it, and the log's `same-pass feedback sampling: snapshot #…` count climbs by hundreds a frame. Tears of the Kingdom's first shrine on an iPad Pro 11 (M5): GPU p50 58.8 ms, about 15 fps, about 284 snapshots a frame (seedds/nxnative#737); Breath of the Wild on the Great Plateau, camera toward the Temple of Time, on an iPad Pro 13 (M4): 60 → about 42 fps, about 45 one-draw passes more a frame on the 1920×1080 G-buffer. Splatoon Raiders in inked stages on a MacBook Air (M2): GPU p50 22–26 ms against 11–14 ms in the hub, a one-draw pass and a whole-map snapshot of its 6400×6400 ink map (80 MB) for each ink stamp (seedds/nxnative#1063). The `[nvn] same-pass feedback up to frame …` line (every 300 frames) gives the snapshots, reuses and tile reads per frame. → [Same-pass feedback read from the tile](../reference/rendering.md#same-pass-feedback-read-from-the-tile) - the `[nvn] residency set up to frame …` line (every 300 frames) shows hundreds to thousands of additions from `fb_snap`'s line in `nvn_mtl.m`, gigabytes of them, and as many removals, with the residency commits' time and the submit thread's kernel time up. Tears of the Kingdom on an iPhone 17 (1.4.2, seedds/nxnative#931): 1,500-1,600 additions of 2.4 MB textures every 300 frames, residency commits 250-600 ms every 300 frames against about 25 in an older build, kernel time on the submit thread 1-2 ms a frame against 0.2. → [Snapshot textures taken by format and size](../reference/rendering.md#snapshot-textures-taken-by-format-and-size) diff --git a/docs/env_switches.tsv b/docs/env_switches.tsv index b9534c8a81..7d579e10b9 100644 --- a/docs/env_switches.tsv +++ b/docs/env_switches.tsv @@ -119,6 +119,7 @@ DM_NVN_DUAL_WANT path `=`: appends the key of each pixel shader a pipeline DM_NVN_ENC_LABEL diagnostic `=1` labels every render encoder `p x d` for Instruments traces DM_NVN_ENC_THREAD player `1` runs the replay's Metal encoding on a thread of its own while the submit thread goes on translating; on by default on iOS (`0` turns it off), off on macOS DM_NVN_FAST_WAKE switch `0` makes the game's `SyncWait`s wake on the commit feedback handler instead of the GPU's own signal of the fence (on by default) +DM_NVN_FB_DEPTH switch `1` makes a draw that samples the depth attachment of its own pass read a copy (the default only in Donkey Kong Country: Tropical Freeze), `0` the attachment in place DM_NVN_FB_SNAP switch Draws that sample a colour attachment of their own render pass: unset, those that write it (or follow a write in the pass) read a snapshot, reused until the game's next barrier; `0` none; `all` a fresh snapshot for every draw that samples one (slow, diagnostic, no tile reads) DM_NVN_FB_TILE switch `0` makes such draws that sample the attachment at their own pixel (a projective sample at the vertex shader's screen varying) read a snapshot too, instead of the tile DM_NVN_FLUSH_FILL switch `0` starts a new GPU texture in a CPU-writable pool as zeros; by default one whose storage the game flushed before its first use is filled from that storage once diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index 973930c440..e06f347c97 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -451,6 +451,14 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E - **Fix:** recorded as a float clear; both replay paths convert the value to the attachment's integer type. - **Found on:** [Paper Mario: The Thousand-Year Door](../../dm/games/0100ECD018EBE000/README.md) (an integer target cleared once Mario stands on Rogueport's docks, about frame 12,300 of a new file). +### Sampling the depth attachment of the same pass + +- **Symptom:** large rectangles or small squares flicker over the background from frame to frame, with no red line. The picture is right with lossless compression off on that one depth buffer, at a cost in frame rate (Donkey Kong Country: Tropical Freeze: about 50 fps instead of 60 at a boss). +- **Cause:** a draw samples the depth buffer attached to its own pass. `fb_prepare` covered colour attachments only. With Apple's lossless compression the read gave wrong values in whole tiles, whether or not the pass wrote depth. Keeping the store of unwritten depth (`DM_NVN_UNWRITTEN_DC=0`) did not help. +- **Fix:** `fb_prepare` gives such a draw a copy of the depth buffer, from the same snapshot cache as colour attachments, for a single-level, single-layer 2D view. It is on by default only in Donkey Kong Country: Tropical Freeze: the copy ends the pass, and elsewhere a draw that only reads depth stays in its pass and reads it in place (`t_feedback` checks both). `DM_NVN_FB_DEPTH=1` or `0` turns it on or off for any game. +- **Check:** `make -C dm/gfx/tests GAME= check_t_feedback` still passes; it does not cover depth. Played in Donkey Kong Country: Tropical Freeze. +- **Found on:** [Donkey Kong Country: Tropical Freeze](../../dm/games/0100C1F0051B6000/README.md) (`976bc74475e9443b`, `dbd64ad1611ed4c9`, 1920×1080 Depth32Float, [#676](https://github.com/seedds/nxnative/issues/676)). + ### Same-pass feedback read through a snapshot - **Symptom:** solid magenta blocks, exactly (255, 0, 255), hard-edged and aligned to the 16-pixel grid (16×16, sometimes two stacked): Super Mario 3D World and Super Mario Odyssey showed them only on the iPhone; Captain Toad and Origami King on a Mac, Apple M6, Captain Toad in about half the frames of the opening course, moving with the falling leaves. On an RG11B10F target (no alpha) the blocks are pure black or neon green, (0, 252, 0), and flicker, sometimes only part of a tile: Super Mario Bros. Wonder on an Apple M6 and an M1, most visible on the speech bubbles of dialogue; or non-finite pixels that bloom spreads into green blobs (NieR:Automata on the Mac). A rendered cutscene alternates brightness every game frame, with blocky bands of light and shadow switching on and off: each lighting-zone bit flips every frame (Xenoblade Chronicles 3, a read-only image load).