From 2ef0a17e15d83e6896efd6e59581cab9a74e922d Mon Sep 17 00:00:00 2001 From: Aleksandr Svidchenkov Date: Sun, 25 Oct 2026 20:18:40 +0400 Subject: [PATCH] Pipeline keys: the write mask and blend bit of colour targets a normal pass lacks leave the key too, and prewarm rows are normalized when read (general, from MacNativeGames) pso_key_norm (66abbc3) drops the blend factors of targets that do not blend. It now also clears, outside a merged pass, the write mask and blend-enable bit of a colour target the pass has no format for: pso_desc reads them only for targets with a format, and they keep whatever earlier draws left. A merged pass keeps them, as its blend / mask index logical targets through lmap. MacNativeGames f3e2f66 found 35% of a Tears of the Kingdom prewarm list's render pipelines (8,479 of 24,146) differing only in such state, each compiled again. pso_key_norm moves to nvn_pso_skey.h with the merged flag, and pso_skey_parse normalizes, so a prewarm list recorded before gives one key per pipeline; the prewarm takes each parsed key once (the line check alone let such rows through as different lines). t_pso_skey: keys round-trip normalized, two keys differing only in unused blend / mask state parse equal, a merged pass keeps the mask and blend bits. t_pso_archive gives every target a format, so its normalization checks still test the blend rule alone. --- dm/gfx/nvn/nvn_mtl.m | 13 +++++-------- dm/gfx/nvn/nvn_pso_skey.h | 14 ++++++++++++++ dm/gfx/tests/t_pso_archive.m | 4 ++-- dm/gfx/tests/t_pso_skey.m | 18 ++++++++++++++++-- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index 70c1056..76dec77 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -4564,12 +4564,6 @@ static id pso_build(MTLRenderPipelineDescriptor *d, NSEr if (ps) g_pb_spec++; return ps; } -// What no pipeline depends on leaves the key: the blend state of targets whose blending is off (pso_desc reads blend[l] only where blend_en has bit l). Breath of the Wild recorded -// 4,829 keys for 1,982 different pipelines, 2,845 of them told apart by such blend state alone. The depth / stencil formats stay: a Metal 4 base names none (pbase_key leaves them -// out, so keys that differ only there cost one specialization each), but a pipeline built through the MTLDevice must name the pass's -static void pso_key_norm(pso_key *k) { - for (int i = 0; i < 8; i++) if (!(k->blend_en >> i & 1)) memset(&k->blend[i], 0, sizeof k->blend[i]); -} // Pipeline key: current state S + pass formats (normal pass; a merged pass additionally carries lmap / specialized function, filled in by pso_get) static void pso_key_fill(pso_key *k, NvShader *vs, NvShader *fs, const uint32_t cfmt[8], uint32_t dfmt, uint32_t sfmt, uint32_t samples, int lmap) { memset(k, 0, sizeof *k); @@ -4579,7 +4573,7 @@ static void pso_key_fill(pso_key *k, NvShader *vs, NvShader *fs, const uint32_t for (int i = 0; i < 8; i++) if (k->cfmt[i] || lmap) k->blend[i] = S.blend[i]; const nvn_refl *rf = &vs->rf; for (int v = 0; v < rf->nv; v++) { int a = rf->v[v].nvn; if (a < 16 && a < S.nattr) { k->attr[a] = S.attr[a]; int st = S.attr[a].stream; if (st < 16) k->stream[st] = S.stream[st]; } } - pso_key_norm(k); + pso_key_norm(k, lmap); } // ---- Parallel precompile: pipelines met for the first time used to be compiled synchronously one by one on the submit thread; 9 misses in a row in one frame = 284 ms (heavy scene on a real device). // After satisfying the first pipeline miss in a submission, scan ahead once along the remaining commands (replaying state only, no drawing) and kick a bounded number of normal-pass pipelines to the background; @@ -4701,6 +4695,7 @@ static void warm_worker(NSArray *paths) { @autoreleasepool { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSMutableSet *seen = [NSMutableSet set]; + NSMutableSet *seen_k = [NSMutableSet set]; // rows recorded before pso_key_norm that are one pipeline now for (NSString *path in paths) { NSString *txt = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; if (!txt) { MLOG("[RED] cannot read prewarm list: %s", path.UTF8String); continue; } @@ -4718,6 +4713,8 @@ static void warm_worker(NSArray *paths) { warm_enqueue(item); g_warm_shaders++; } else if ([c[0] isEqualToString:@"R"] && c.count >= 2) { pso_skey sk; if (pso_skey_parse(c[1].UTF8String, &sk)) continue; + NSData *kd = [NSData dataWithBytes:&sk length:sizeof sk]; if ([seen_k containsObject:kd]) continue; + [seen_k addObject:kd]; NSString *vkey = warm_key(sk.vs, sizeof sk.vs), *fkey = warm_key(sk.fs, sizeof sk.fs); NvShader *vs = vkey.length ? warm_shader(dict, vkey, sk.vs_stage) : nil; NvShader *fs = fkey.length ? warm_shader(dict, fkey, sk.fs_stage) : nil; @@ -4727,7 +4724,7 @@ static void warm_worker(NSArray *paths) { if (sk.fz_desc.kind == 1 && fs) { int fx[32]; for (int i = 0; i < 32; i++) fx[i] = sk.fz_desc.fx[i]; fsfn = bf_fn(fs, fx, sk.fz_desc.fbmask, sk.k.lmap); } if (!fsfn) { g_warm_skip_fz++; continue; } } - pso_key k = sk.k; k.vs = (__bridge void *)vs; k.fs = (__bridge void *)fs; k.fsfn = (__bridge void *)fsfn; pso_key_norm(&k); // rows written before the key was normalized + pso_key k = sk.k; k.vs = (__bridge void *)vs; k.fs = (__bridge void *)fs; k.fsfn = (__bridge void *)fsfn; // pso_skey_parse normalized rows written before the key was NSError *e = nil; id ps = pso_build(pso_desc(&k, vs, fs, fsfn, sk.merged), &e); if (!ps && !vs->layer_out && [e.localizedDescription containsString:@"render_target_array_index"]) { // a vertex shader that writes the layer (see pso_get_) vs->layer_out = 1; e = nil; ps = pso_build(pso_desc(&k, vs, fs, fsfn, sk.merged), &e); } diff --git a/dm/gfx/nvn/nvn_pso_skey.h b/dm/gfx/nvn/nvn_pso_skey.h index 0b280f9..51c4e7c 100644 --- a/dm/gfx/nvn/nvn_pso_skey.h +++ b/dm/gfx/nvn/nvn_pso_skey.h @@ -16,6 +16,19 @@ typedef struct { typedef struct { char vs[24], fs[24]; uint8_t vs_stage, fs_stage, merged, fz; pso_fz_desc fz_desc; pso_key k; } pso_skey; +// What no pipeline depends on leaves the key, so one pipeline has one key: a target's blend factors when it does not blend (pso_desc reads blend[l] only where blend_en has +// bit l) and, outside a merged pass (whose blend / mask index logical targets through lmap), the write mask and blend bit of a colour target the pass does not have. They +// keep whatever earlier draws left. Breath of the Wild recorded 4,829 keys for 1,982 different pipelines, 2,845 of them told apart by such blend state alone; MacNativeGames +// f3e2f66 found 35% of a Tears of the Kingdom prewarm list's render pipelines (8,479 of 24,146) differing only in it, each compiled again. The depth / stencil formats stay: +// a Metal 4 base names none (pbase_key leaves them out, so keys that differ only there cost one specialization each), but a pipeline built through the MTLDevice must name +// the pass's. pso_key_fill makes keys so, and pso_skey_parse reads rows recorded before so +static inline void pso_key_norm(pso_key *k, int merged) { + for (int i = 0; i < 8; i++) { + if (!merged && !k->cfmt[i]) { k->blend_en &= ~(1u << i); k->chmask &= ~(0xfu << (4 * i)); } + if (!(k->blend_en >> i & 1)) memset(&k->blend[i], 0, sizeof k->blend[i]); + } +} + static inline void pso_skey_from(const pso_key *k, NvShader *vs, NvShader *fs, int merged, const pso_fz_desc *fz, pso_skey *out) { memset(out, 0, sizeof *out); memcpy(&out->k, k, sizeof out->k); @@ -44,5 +57,6 @@ static inline int pso_skey_parse(const char *hex, pso_skey *out) { if (a < 0 || b < 0) return -1; p[i / 2] = (uint8_t)(a << 4 | b); } + pso_key_norm(&out->k, out->merged); return 0; } diff --git a/dm/gfx/tests/t_pso_archive.m b/dm/gfx/tests/t_pso_archive.m index fa8e409..e6162f4 100644 --- a/dm/gfx/tests/t_pso_archive.m +++ b/dm/gfx/tests/t_pso_archive.m @@ -206,8 +206,8 @@ int main(void) { @autoreleasepool { concurrent_bases(1); // key normalization pso_key k; memset(&k, 0, sizeof k); k.blend_en = 0x5; k.dfmt = MTLPixelFormatDepth32Float; k.sfmt = MTLPixelFormatStencil8; - for (int i = 0; i < 8; i++) { k.blend[i].src_rgb = 5; k.blend[i].dst_rgb = 6; } - pso_key_norm(&k); + for (int i = 0; i < 8; i++) { k.cfmt[i] = MTLPixelFormatRGBA8Unorm; k.blend[i].src_rgb = 5; k.blend[i].dst_rgb = 6; } // every target present: only the blend rule acts + pso_key_norm(&k, 0); EXPECT("blend of an enabled target kept", 5, k.blend[0].src_rgb); EXPECT("blend of an enabled target kept (2)", 6, k.blend[2].dst_rgb); EXPECT("blend of a disabled target dropped", 0, k.blend[1].src_rgb); diff --git a/dm/gfx/tests/t_pso_skey.m b/dm/gfx/tests/t_pso_skey.m index 1ccd357..1d15591 100644 --- a/dm/gfx/tests/t_pso_skey.m +++ b/dm/gfx/tests/t_pso_skey.m @@ -15,7 +15,7 @@ static void fill(pso_key *k, unsigned seed) { int main(void) { for (unsigned seed = 1; seed <= 3; seed++) { - pso_key k; fill(&k, seed); + pso_key k; fill(&k, seed); pso_key_norm(&k, seed & 1); // keys are made normalized (pso_key_fill), and a parse normalizes NvShader *vs = [NvShader new], *fs = [NvShader new]; vs->stage = (int)seed; fs->stage = (int)(seed + 1); snprintf(vs->key, sizeof vs->key, "vs-%u", seed); snprintf(fs->key, sizeof fs->key, "fs-%u", seed); @@ -27,6 +27,20 @@ int main(void) { hex[0] = hex[0] == '0' ? '1' : '0'; assert(pso_skey_parse(hex, &b) == 0); assert(memcmp(&a, &b, sizeof a) != 0); assert(pso_skey_parse(hex + 1, &b) == -1); hex[0] = 'g'; assert(pso_skey_parse(hex, &b) == -1); } - puts("PSO_SKEY OK 3 cases round-trip; one changed character differs, missing digits / illegal characters rejected"); + // pso_key_norm: blend factors of a target that does not blend, and the mask / blend bit of a target a normal pass lacks, are not part of the key; a merged pass keeps + // the mask and blend bits (they index logical targets through lmap) + { + NvShader *vs = [NvShader new]; snprintf(vs->key, sizeof vs->key, "vs-n"); + pso_key k1; memset(&k1, 0, sizeof k1); k1.cfmt[0] = 80; k1.cfmt[1] = 80; k1.blend_en = 1; k1.chmask = 0xff; + k1.blend[0].src_rgb = 1; k1.blend[0].dst_rgb = 2; + pso_key k2 = k1; k2.blend[1].src_rgb = 7; k2.blend[3].dst_a = 5; k2.blend_en |= 1u << 5; k2.chmask |= 0xfu << 20; + pso_skey a, b; char ha[sizeof a * 2 + 1], hb[sizeof b * 2 + 1]; + pso_skey_from(&k1, vs, nil, 0, NULL, &a); pso_skey_from(&k2, vs, nil, 0, NULL, &b); assert(memcmp(&a, &b, sizeof a) != 0); + pso_skey_hex(&a, ha); pso_skey_hex(&b, hb); assert(pso_skey_parse(ha, &a) == 0 && pso_skey_parse(hb, &b) == 0); + assert(memcmp(&a, &b, sizeof a) == 0 && a.k.blend[0].src_rgb == 1 && a.k.blend[0].dst_rgb == 2 && a.k.chmask == 0xff && a.k.blend_en == 1); + pso_skey_from(&k2, vs, nil, 1, NULL, &b); pso_skey_hex(&b, hb); assert(pso_skey_parse(hb, &b) == 0); + assert(b.k.chmask == (0xffu | 0xfu << 20) && b.k.blend_en == (1u | 1u << 5) && b.k.blend[1].src_rgb == 0 && b.k.blend[3].dst_a == 0); + } + puts("PSO_SKEY OK 3 cases round-trip; one changed character differs, missing digits / illegal characters rejected; unused blend and mask state left out of the key"); return 0; }