From 2152b8937b9451d72a834066c404cf217924ef57 Mon Sep 17 00:00:00 2001 From: FifteenPen <354289956+FifteenPen@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:57:31 -0500 Subject: [PATCH] A colour clear under a channel mask that lets only some of its target's channels through leaves the others: Nintendo Switch Sports Resort's outfits were black in its menus (its shadow target's R and A cleared under a mask of G and B) --- dm/gfx/nvn/nvn_mtl.m | 34 +++++++++++++---- dm/gfx/tests/Makefile | 3 ++ dm/gfx/tests/t_clear_chmask.m | 71 +++++++++++++++++++++++++++++++++++ docs/agents/symptoms.md | 1 + docs/reference/rendering.md | 9 +++++ 5 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 dm/gfx/tests/t_clear_chmask.m diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index ee0f03a316..1bb516eae0 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -11650,6 +11650,18 @@ static int pass_ds_need(void) { static void seq_push(uint32_t **v, uint32_t *n, uint32_t *cap, uint32_t x) { if (*n == *cap) { *cap = *cap ? *cap * 2 : 1024; *v = realloc_or_die(*v, *cap * sizeof **v, "the store lookahead index"); } (*v)[(*n)++] = x; } static void sdx_ev_add(sdx_ev e) { if (sdx_bucket(e.ser, 0) < 0) return; if (g_sdx_n == g_sdx_cap) { g_sdx_cap = g_sdx_cap ? g_sdx_cap * 2 : 4096; g_sdx = realloc_or_die(g_sdx, g_sdx_cap * sizeof *g_sdx, "the store lookahead index"); g_sdx_tmp = realloc_or_die(g_sdx_tmp, g_sdx_cap * sizeof *g_sdx, "the store lookahead index"); } g_sdx[g_sdx_n++] = e; } static void sdx_lvsl(const view_t *v, int hv, uint16_t *lv, uint16_t *sl) { *lv = (uint16_t)(hv && v->set_levels ? v->base_level : 0); *sl = (uint16_t)(hv && v->set_layers ? v->min_layer : 0); } +// The channels a colour clear writes. Under a channel mask state that lets only some of its target's channels through, the clear leaves the others, as a draw does: Nintendo Switch +// Sports Resort blurs G and B of its screen-space shadow target back and forth under a mask of G and B, clearing the target (own mask 0xf) before each pass, and its material shaders +// read R and A of it afterwards. Cleared whole, those were 0, and outfits in the menus were lit by nothing (black). With every channel of the target off the clear is whole, as it +// was: the same game clears its main colour target right after a shadow pass drawn with the mask at 0, and held back there the target was never cleared (its Select User background +// came out black with outlines). What a console does under a mask of 0 is not known; this is the narrowest rule both cases bear out. The scans ahead of the replay follow the state +// (scan_chmask) from where they start, so a clear that leaves channels is not taken for one that replaces the target's content +static inline int clear_mask(const p_clear_color *c, uint32_t chmask) { + int m = c->mask & 0xf, st = c->index >= 0 && c->index < 8 ? (int)(chmask >> (4 * c->index) & 0xf) : 0xf; + return st ? m & st : m; +} +static inline void scan_chmask(const void *a, uint32_t *cm) { const p_bind_state *b = a; if (b->kind == 2) memcpy(cm, b->bytes, sizeof *cm); } +static uint32_t g_pp_cm; // the channel mask state as sdx_walk meets the records static void sdx_walk(const uint8_t *p, int depth) { if (depth > 64) { g_sdx_ok = 0; return; } for (uint32_t guard = 0; guard < 4000000 && p && g_sdx_ok; guard++) { @@ -11664,8 +11676,9 @@ static void sdx_walk(const uint8_t *p, int depth) { for (int k = 0; k < r->n && k < 8; k++) if (r->color[k]) sdx_bucket(r->color[k]->serial, 1); if (r->depth) sdx_bucket(r->depth->serial, 1); break; } + case OP_BIND_STATE: scan_chmask(a, &g_pp_cm); break; case OP_CLEAR_COLOR: { const p_clear_color *c = a; const p_set_rt *r = g_pp_rt; - if (!r || (c->mask & 0xf) != 0xf || c->index < 0 || c->index >= r->n || c->index >= 8 || !r->color[c->index]) break; + if (!r || clear_mask(c, g_pp_cm) != 0xf || c->index < 0 || c->index >= r->n || c->index >= 8 || !r->color[c->index]) break; uint16_t lv, sl; sdx_lvsl(&r->cview[c->index], r->has_cview[c->index], &lv, &sl); int last = -1; // When the same subresource is attached at several indices, the last one counts for (int k = 0; k < r->n && k < 8; k++) { uint16_t l2, s2; if (!r->color[k] || r->color[k]->serial != r->color[c->index]->serial) continue; sdx_lvsl(&r->cview[k], r->has_cview[k], &l2, &s2); if (l2 == lv && s2 == sl) last = k; } if (last == c->index) sdx_ev_add((sdx_ev){seq, g_pp_rtseq, r->color[c->index]->serial, p, lv, sl, SDX_CLEAR, 0, 0, 0}); @@ -11714,7 +11727,7 @@ static void mtl_sub_index_(const uint64_t *h, int n) { // Start of submit (hle memset(g_sdx_hk, 0, sizeof g_sdx_hk); g_sdx_nb = 0; for (int i = 0; i < g_xv_n; i++) sdx_bucket(g_xv[i].ser, 1); // Textures in the ledger enter the candidate set: otherwise, when one is only sampled and not used as an attachment in this submit, its read events are not indexed and the fallback cannot see them if (R.has_rt) { for (int k = 0; k < R.rt.n && k < 8; k++) if (R.rt.color[k]) sdx_bucket(R.rt.color[k]->serial, 1); if (R.rt.depth) sdx_bucket(R.rt.depth->serial, 1); } // The targets at the start of the submit (the first draw may reopen the render pass without going through SetRenderTargets) - g_pp_prog = S.prog; g_pp_cprog = S.cprog; g_pp_rt = NULL; g_pp_rtseq = SDX_NONE; + g_pp_prog = S.prog; g_pp_cprog = S.cprog; g_pp_rt = NULL; g_pp_rtseq = SDX_NONE; g_pp_cm = S.chmask.mask; for (int st = 0; st < 6; st++) { for (int k = 0; k < NTEX; k++) g_pp_slot[st][k] = S.tex[st][k] ? h_ser(S.tex[st][k]) : 0; for (int k = 0; k < 32; k++) g_pp_slot[st][NTEX + k] = S.img[st][k] ? h_ser(S.img[st][k]) : 0; } for (int i = 0; i < n && g_sdx_ok; i++) sdx_walk((const uint8_t *)h[i], 0); @@ -11773,7 +11786,7 @@ static void hoist_try(void) { if (g_hoist_n || !g_exec_next) return; typedef struct { const uint8_t *rt_op; const p_set_rt *rt; uint8_t clear_c[8]; MTLClearColor cc[8]; uint8_t clear_d, clear_s; double cd; uint32_t cs; p_scissor sc; int has_sc; const uint8_t *ops[8]; int nops; } grp; - grp G[4]; int ng = 0, in_grp = 0; p_scissor sc = R.sc; int has_sc = R.has_sc; + grp G[4]; int ng = 0, in_grp = 0; p_scissor sc = R.sc; int has_sc = R.has_sc; uint32_t cm = S.chmask.mask; // The binding set needed for "the cleared texture is not on any slot": the table is looked up only when a hoistable clear group is really found and we are back to this render pass's targets (the vast majority of render passes never get that far). // Handles bound within the scan range are noted first and converted to texture serials together with the current slots at lookup time; the scan only reads commands and the current slots S do not change meanwhile, so the result is the same as checking at the start uint64_t bh[512]; int nbh = 0; @@ -11786,12 +11799,13 @@ static void hoist_try(void) { case OP_MULTI_DRAW: return; case OP_DRAW: case OP_DRAW_IDX: if (in_grp) return; break; case OP_SCISSOR: sc = *(const p_scissor *)a; has_sc = 1; break; + case OP_BIND_STATE: scan_chmask(a, &cm); break; case OP_BIND_TEX: case OP_BIND_TEX1: case OP_BIND_IMG: { p_bind_handles t1; const p_bind_handles *b = bind_rec(h.op, a, &t1); bh_fresh(b); for (int k = 0; k < b->n && k < 32 && nbh < 512; k++) if (b->h[k]) bh[nbh++] = b->h[k]; break; } case OP_CLEAR_COLOR: case OP_CLEAR_DS: { if (!in_grp || G[ng - 1].nops >= 8) return; grp *g = &G[ng - 1]; if (h.op == OP_CLEAR_COLOR) { const p_clear_color *c = a; - if (c->index < 0 || c->index >= g->rt->n || !g->rt->color[c->index] || c->mask != 0xf) return; + if (c->index < 0 || c->index >= g->rt->n || !g->rt->color[c->index] || clear_mask(c, cm) != 0xf) return; const tex_t *t = g->rt->color[c->index]; if (has_sc && !(sc.x <= 0 && sc.y <= 0 && sc.x + sc.w >= (int)t->d.width && sc.y + sc.h >= (int)t->d.height)) return; g->clear_c[c->index] = 1; g->cc[c->index] = MTLClearColorMake(c->rgba[0], c->rgba[1], c->rgba[2], c->rgba[3]); } @@ -12168,7 +12182,7 @@ static void fuse_try(void) { if (g_fz.active || !g_exec_next || g_hoist_n + 12 > HOIST_MAX || !R.rt.n) return; if (!fz_prefilter()) return; // One: from the earlier pass to the next SetRenderTargets, only draws and state are allowed - const uint8_t *p = NULL, *r2op = NULL, *reuse[8]; int nreuse = 0; const p_set_rt *r2 = NULL; p_scissor sc = R.sc; int has_sc = R.has_sc; + const uint8_t *p = NULL, *r2op = NULL, *reuse[8]; int nreuse = 0; const p_set_rt *r2 = NULL; p_scissor sc = R.sc; int has_sc = R.has_sc; uint32_t cm = S.chmask.mask; fz_cursor cur = {.p = g_exec_next}; op_hdr h; const void *a; for (int guard = 0; guard < 200000 && !r2; guard++) { if (!fz_next(&cur, &p, &h, &a)) return; @@ -12176,6 +12190,7 @@ static void fuse_try(void) { case OP_COPY_T2T: case OP_DRAW_TEX: case OP_COPY_B2T: case OP_COPY_T2B: case OP_CLEAR_TEX: case OP_DLSS: case OP_DISPATCH: case OP_RENDER_COND: case OP_CLEAR_COLOR: case OP_CLEAR_DS: case OP_UPDATE_UB: case OP_COPY_B2B: case OP_CLEAR_BUF: return; case OP_SCISSOR: sc = *(const p_scissor *)a; has_sc = 1; break; + case OP_BIND_STATE: scan_chmask(a, &cm); break; case OP_SET_RT: { int pend = R.clear_d || R.clear_s; for (int i = 0; i < 8; i++) pend |= R.clear_c[i]; if (rt_same(&R.rt, a)) { @@ -12196,7 +12211,8 @@ static void fuse_try(void) { switch (h.op) { case OP_COPY_T2T: case OP_DRAW_TEX: case OP_COPY_B2T: case OP_COPY_T2B: case OP_CLEAR_TEX: case OP_DLSS: case OP_DISPATCH: case OP_SET_RT: case OP_UPDATE_UB: case OP_COPY_B2B: case OP_CLEAR_BUF: case OP_RENDER_COND: return; case OP_SCISSOR: sc = *(const p_scissor *)a; has_sc = 1; break; - case OP_CLEAR_COLOR: { const p_clear_color *c = a; if (ncl >= 8 || c->index < 0 || c->index >= r2->n || !r2->color[c->index] || c->mask != 0xf) return; + case OP_BIND_STATE: scan_chmask(a, &cm); break; + case OP_CLEAR_COLOR: { const p_clear_color *c = a; if (ncl >= 8 || c->index < 0 || c->index >= r2->n || !r2->color[c->index] || clear_mask(c, cm) != 0xf) return; const tex_t *t = r2->color[c->index]; if (has_sc && !(sc.x <= 0 && sc.y <= 0 && sc.x + sc.w >= (int)t->d.width && sc.y + sc.h >= (int)t->d.height)) return; clr[c->index] = 1; cc[c->index] = MTLClearColorMake(c->rgba[0], c->rgba[1], c->rgba[2], c->rgba[3]); cl[ncl++] = p; break; } @@ -15014,8 +15030,12 @@ void mtl_exec(const uint8_t *p) { sub_cut(); R.rt = *r; R.has_rt = 1; break; } case OP_CLEAR_COLOR: { - const p_clear_color *c = a; + const p_clear_color *c = a; p_clear_color masked; if (g_rc_skip) { PD("c %llu color clear skipped by conditional rendering slot%d", g_stat_pass, c->index); break; } + if (clear_mask(c, S.chmask.mask) != (c->mask & 0xf)) { // the channel mask state holds some of its channels back (clear_mask) + masked = *c; masked.mask = clear_mask(c, S.chmask.mask); c = &masked; + if (!c->mask) { PD("c %llu color clear writes no channel (its own mask and the channel mask share none) slot%d", g_stat_pass, c->index); break; } + } // A scissored clear to 0 outside a pass with all channels (SMBW clears pyramid levels as sub-rectangles of 480×272 and other targets): copy from the zero buffer into the scissor rectangle in the replay stream if (!g_enc && c->mask == 0xf && !c->rgba[0] && !c->rgba[1] && !c->rgba[2] && !c->rgba[3] && R.has_sc && c->index >= 0 && c->index < R.rt.n && R.rt.color[c->index] && !scissor_full(R.rt.color[c->index], &R.rt.cview[c->index], R.rt.has_cview[c->index]) && clear_rect_zero(c->index)) break; diff --git a/dm/gfx/tests/Makefile b/dm/gfx/tests/Makefile index b4630c7baa..f7165d5d3b 100644 --- a/dm/gfx/tests/Makefile +++ b/dm/gfx/tests/Makefile @@ -445,6 +445,9 @@ check_t_clear_ds: RUN_ENV = MTL_DEBUG_LAYER=1 t_clear_layers: t_clear_layers.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_clear_layers.m $(MTL_T_SRC) $(FW) check_t_clear_layers: RUN_ENV = MTL_DEBUG_LAYER=1 +t_clear_chmask: t_clear_chmask.m $(MTL_T_DEP) + clang $(CFLAGS) -fobjc-arc -o $@ t_clear_chmask.m $(MTL_T_SRC) $(FW) +check_t_clear_chmask: RUN_ENV = MTL_DEBUG_LAYER=1 t_clear_tex: t_clear_tex.m $(MTL_T_DEP) clang $(CFLAGS) -fobjc-arc -o $@ t_clear_tex.m $(MTL_T_SRC) $(FW) .PHONY: check_t_clear_tex diff --git a/dm/gfx/tests/t_clear_chmask.m b/dm/gfx/tests/t_clear_chmask.m new file mode 100644 index 0000000000..9c93c8c347 --- /dev/null +++ b/dm/gfx/tests/t_clear_chmask.m @@ -0,0 +1,71 @@ +// A colour clear writes only the channels the bound channel mask state lets through, as a draw does: cleared under a mask of G and B, a target keeps its R and A +// (Nintendo Switch Sports Resort's screen-space shadow target; docs/reference/rendering.md, A colour clear under a partial channel mask leaves the other channels). The mask comes from the state the replay holds or from a state +// bind earlier in the same records. With every channel of the target off the clear is whole (what a console does there is not known). +#include "../nvn/nvn_mtl.m" +void *front_metal_layer(void) { return NULL; } +module_t *module_of(const void *p) { (void)p; return NULL; } +module_t *g_mod; int g_nmod; +void svc_thread_dump(void) {} +uint64_t now_ns(void) { return bp_now(); } +static int checks, failed; +#define CHECK(c, label) do { checks++; if (!(c)) { failed++; fprintf(stderr, "FAIL: %s\n", label); } } while (0) + +enum { W = 4, OLD_C = 0x11223344 }; // RGBA8 bytes R 0x44, G 0x33, B 0x22, A 0x11; the clear is 0.5 (0x80) in every channel + +int main(void) { @autoreleasepool { + mtl_init(); + id queue = [g_dev newCommandQueue]; + // state: the channel mask the replay holds when the records start; bind: a channel mask bound by a record before the clear (-1: none); clear: the clear's own mask + struct { uint32_t state; int64_t bind; int clear; uint32_t want; const char *label; } cases[] = { + {0xffffffffu, -1, 0xf, 0x80808080, "every channel let through: the clear replaces all four"}, + {0xfffffff6u, -1, 0xf, 0x11808044, "a channel mask of G and B: the clear keeps R and A"}, + {0xffffffffu, 0xfffffff6, 0xf, 0x11808044, "the same mask bound by a record before the clear"}, + {0xfffffff6u, -1, 0x3, 0x11228044, "the clear's own mask R and G under a state of G and B: only G"}, + {0xfffffff0u, -1, 0xf, 0x80808080, "every channel of the target off: the clear is whole, as without the state"}, + {0xfffffff6u, -1, 0x9, OLD_C, "the clear's own mask R and A under a state of G and B: nothing to write"}, + {0xffffff6fu, -1, 0xf, 0x80808080, "another target's mask does not hold this one back"}, + }; + for (unsigned i = 0; i < sizeof cases / sizeof *cases; i++) { + tex_t t = {.magic = MAGIC('T','E','X',0), .serial = 99701 + i}; + t.d = (nvn_texdesc){.target = NVN_TEXTURE_TARGET_2D, .format = NVN_FORMAT_RGBA8, .width = W, .height = W, .depth = 1, .levels = 1, .samples = 1}; + id base = [g_dev newTextureWithDescriptor:tex_desc(&t, 0)]; + CHECK(base != nil, "synthetic target allocates"); + if (!base) continue; + id cb = [queue commandBuffer]; + MTLRenderPassDescriptor *init = [MTLRenderPassDescriptor renderPassDescriptor]; + init.colorAttachments[0].texture = base; init.colorAttachments[0].loadAction = MTLLoadActionClear; init.colorAttachments[0].storeAction = MTLStoreActionStore; + init.colorAttachments[0].clearColor = MTLClearColorMake(0x44 / 255., 0x33 / 255., 0x22 / 255., 0x11 / 255.); + [[cb renderCommandEncoderWithDescriptor:init] endEncoding]; + [cb commit]; [cb waitUntilCompleted]; + CHECK(cb.status == MTLCommandBufferStatusCompleted, "the old content reaches the GPU"); + res_add(base); mtl_tex_init(&t); serial_put(t.serial, base); + state_reset(); memset(&R, 0, sizeof R); mtl_sub_begin(); + S.chmask.mask = cases[i].state; + R.has_rt = 1; R.rt.n = 1; R.rt.color[0] = &t; + uint8_t ops[256] = {0}; size_t at = 0; + if (cases[i].bind >= 0) { + op_hdr h = {OP_BIND_STATE, (uint32_t)(sizeof(op_hdr) + offsetof(p_bind_state, bytes) + sizeof(chmask_st))}; memcpy(ops + at, &h, sizeof h); + p_bind_state b = {.kind = 2}; chmask_st m = {(uint32_t)cases[i].bind}; memcpy(b.bytes, &m, sizeof m); memcpy(ops + at + sizeof h, &b, offsetof(p_bind_state, bytes) + sizeof m); + at += h.size; + } + op_hdr h = {OP_CLEAR_COLOR, sizeof(op_hdr) + sizeof(p_clear_color)}; memcpy(ops + at, &h, sizeof h); + p_clear_color c = {.index = 0, .mask = cases[i].clear, .rgba = {.5f, .5f, .5f, .5f}}; memcpy(ops + at + sizeof h, &c, sizeof c); at += h.size; + op_hdr end = {OP_END, sizeof(op_hdr)}; memcpy(ops + at, &end, sizeof end); + mtl_exec(ops); + mtl_sub_commit(); mtl_drain(); + CHECK(!mtl_last_commit_error(), "the clear completes without a GPU error"); + id result = [g_dev newBufferWithLength:W * W * 4 options:MTLResourceStorageModeShared]; + cb = [queue commandBuffer]; id b = [cb blitCommandEncoder]; + [b copyFromTexture:base sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0, 0, 0) sourceSize:MTLSizeMake(W, W, 1) toBuffer:result destinationOffset:0 + destinationBytesPerRow:W * 4 destinationBytesPerImage:W * W * 4]; + [b endEncoding]; [cb commit]; [cb waitUntilCompleted]; + CHECK(cb.status == MTLCommandBufferStatusCompleted, "the target is read back"); + const uint32_t *px = result.contents; int valid = 1; + for (int k = 0; k < W * W; k++) if (px[k] != cases[i].want) { valid = 0; fprintf(stderr, "case %u texel %d: %#x, want %#x\n", i, k, px[k], cases[i].want); break; } + CHECK(valid, cases[i].label); + printf("case %u: %s %s\n", i, cases[i].label, valid ? "PASS" : "FAIL"); + mtl_tex_release(&t); + } + printf("clears under a channel mask: %d checks, %d failed\n", checks, failed); + return failed != 0; +} } diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 3a52815bc9..c43587fba8 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -713,6 +713,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - `texture clear case not implemented … mask 0x1`. → [A channel mask that covers every channel is a whole clear](../reference/rendering.md#a-channel-mask-that-covers-every-channel-is-a-whole-clear) - `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 character's outfit is black in a menu and right in play (Nintendo Switch Sports Resort's Customize screen: skin, face and hair lit, clothes black); its lights, textures and environment map are all there. → [A colour clear under a partial channel mask leaves the other channels](../reference/rendering.md#a-colour-clear-under-a-partial-channel-mask-leaves-the-other-channels) - a pass loads a whole depth/stencil target every frame only to clear it: Donkey Kong Bananza clears a 2880x1620 Depth32F-S8 target with stencil mask `0x1`, before any pass is open, and loads 22.8 MB a frame for it. → [A depth clear over the whole target with a partial stencil mask](../reference/rendering.md#a-depth-clear-over-the-whole-target-with-a-partial-stencil-mask) - 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) - 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) diff --git a/docs/reference/rendering.md b/docs/reference/rendering.md index 8703dc610f..a39d0722dd 100644 --- a/docs/reference/rendering.md +++ b/docs/reference/rendering.md @@ -536,6 +536,15 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E - **Fix:** such clears are drawn as one triangle over the pass in the open render encoder (`clear_color_tri` / `clear_ds_tri` in `nvn_mtl.m`, adapted from MacNativeGames, MIT). The pipeline has the pass's formats, the clear's mask as the write mask of its slot and no writes to the other slots. Depth is `ALWAYS` (and written for a depth clear), stencil `REPLACE` through the clear's write mask, the scissor is clamped as for draws, and visibility counting is off for the triangle. The value comes from the frame arena through the triangle's own argument tables, and the draws' tables are put back after it. A clear outside a pass opens the pass as a draw would. Clears a load action can express keep it. Two cases are still cleared whole, logged once: a depth / stencil clear with a scissor outside a pass, and a partial stencil write mask. - **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md): it issues the first whole-cleared case (a depth clear scissored to 8×8 of a 64×64 target), and so does Wonder ([Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md)). With `render_cond` ([Conditional rendering evaluated at replay](#conditional-rendering-evaluated-at-replay)) TOTK's scissored colour clears are all skipped; without it they are 50×50 tiles of a 500×400 target cleared to opaque black, about five a frame. [Metroid Dread](../../dm/games/010093801237C000/README.md) logged the red line once while loading on its smoke route. +### A colour clear under a partial channel mask leaves the other channels + +- **Symptom:** a character's outfit is black in a menu and right in play (Nintendo Switch Sports Resort's Customize screen: skin, face and hair lit, clothes black); its lights, textures and environment map are all there. +- **Cause:** the game's menu scene blurs its screen-space shadow target back and forth between two 2048x1152 targets. It binds a channel mask of G and B, then for each pass clears the target (own mask 0xf, to 0) and draws the blur. Its material shader then reads G, A and R of the target (a view swizzle of G, A, B, R, of which it takes x, y and w) and multiplies its lighting by them. A pass before the blurs writes all four channels; the host cleared by the clear's own mask alone, so R and A were 0 from the first blur on. In play the target is written once, with no blur, so nothing was lost there. +- **Fix:** under a channel mask state that lets only some of the target's channels through, a colour clear writes its own mask's channels among those (`clear_mask` in `nvn_mtl.m`), drawn as a triangle with that mask ([Clears inside a pass, with a partial mask or a partial scissor](#clears-inside-a-pass-with-a-partial-mask-or-a-partial-scissor)). The scans that plan passes ahead of the replay (`sdx_walk`, `hoist_try`, `fuse_try`) follow the state's binds, so such a clear is not taken for one that replaces the target's content. +- **Open:** with every channel of the target off the clear stays whole, as before. The same game clears its main colour target right after a shadow pass drawn with the mask at 0; held back there, its Select User background came out black with outlines. Whether a console clears under a mask of 0 is not known: this is the narrowest rule both cases bear out. +- **Found on:** Nintendo Switch Sports Resort (`04008E001F098000`, v65536; no profile on main). Measured in the outfit's draw with a debug copy of its pixel shader: A and R of the target read 0 in the menu and about 1 in a golf round; with the fix the outfit is lit in the menu. +- **Check:** `make -C dm/gfx/tests GAME= check_t_clear_chmask` (GPU): a clear under a mask of G and B keeps R and A, from the state or from a bind in the same records; the clear's own mask and the state's combine; a mask of nothing leaves the clear whole; another target's mask does not count. + ### A depth clear over the whole target with a partial stencil mask - **Symptom:** a pass loads a whole depth/stencil target every frame only to clear it: Donkey Kong Bananza clears a 2880x1620 Depth32F-S8 target with stencil mask `0x1`, before any pass is open, and loads 22.8 MB a frame for it.