Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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});
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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]); }
Expand Down Expand Up @@ -12168,14 +12182,15 @@ 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;
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_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)) {
Expand All @@ -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; }
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions dm/gfx/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions dm/gfx/tests/t_clear_chmask.m
Original file line number Diff line number Diff line change
@@ -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<MTLCommandQueue> 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<MTLTexture> base = [g_dev newTextureWithDescriptor:tex_desc(&t, 0)];
CHECK(base != nil, "synthetic target allocates");
if (!base) continue;
id<MTLCommandBuffer> 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<MTLBuffer> result = [g_dev newBufferWithLength:W * W * 4 options:MTLResourceStorageModeShared];
cb = [queue commandBuffer]; id<MTLBlitCommandEncoder> 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;
} }
1 change: 1 addition & 0 deletions docs/agents/symptoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading