Files
nxnative-easy/dm/gfx/tools/shader_map.py
T
seedds c5955d42a6 English only: no Chinese left in code, scripts, logs, tables or docs
- Log messages, script output, test labels, Makefile errors and comments are English. Red log lines start with "[RED] " (was 【红】).
- Shader map stage column: vertex / pixel / compute / geometry / hull / domain (generators, dmsh_build / dmsh_facts and nvn_mtl.m stage_of follow).
- The NSP script's regexes match the new shader_split.py / dmsh_build.py summary lines; patch_nso cut scopes are all / host-only.
- Host UI text table (dm_i18n_tab.h) is English only; dm_lang_zh() removed.
- Text that matches game data (t_swkbd.c, t_textbox.c) is written as \x escapes.
- Shader chain: the chain files lose their bilingual comments, so every profile has a new chain version. dmsh_xlate.dll rebuilt from the translated Program.cs (SPIR-V byte-identical on 4,500 shaders of Wonder, TOTK and Age of Calamity), lock and dmsh_chain.h headers regenerated. c1_full.tsv restamped for Dread, 13 Sentinels and Wonder; the other profiles follow when their rebuild finishes.
- AGENTS.md and docs/agents: the language rule now covers logs; quoted log lines updated.
2026-10-19 15:34:57 +08:00

145 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Mapping table: Switch shader → PC shader (DXBC) → PC-chain MSL (metallib).
# Basis: data/hd/shaders on both sides holds 162 SHAD packages with the same names, one BTCD record per variant, and record counts are equal package by package (10847 in total);
# apart from sizes, record headers differ only in the platform number (PC 3 / Switch 5), and there is no permutation key ⇒ map one-to-one by index within the package.
# Evidence of order alignment: compute shader thread group sizes are equal record by record, 22/22; for the 22 packages with ≥16 records, code size is rank-correlated, 0.81–1.00 when aligned and lower in every case when shifted by one record.
# Switch record: after the header = GPU code segment (starts with 0x12345678), followed by = control segment (starts with 0x98761234, length in the record header).
# At runtime nvnProgramSetShaders receives the control segment pointer ⇒ key = sha256(control segment)[:16].
# PC record: contains one DXBC; PC-chain cache file name = sha256(DXBC)[:16].metallib (platform/shell/mtl12/shaders/build_cache.py).
# Binding mapping (header comment of maxwell_refs.py): NVN slot number = D3D register number; verified record by record against what the Switch machine code actually references that "used by Switch ⊆ declared by PC".
# On a key conflict (same Switch binary, different PC DXBC) pick one: the one with the smallest difference from the output count / cb / t / s actually used by Switch;
# on a tie take the simplest (fewer outputs, fewer resources), on a further tie take the smallest DXBC hash (deterministic). The choice and the reason are written to <output>.conflicts.tsv.
# Shaders embedded in the executable itself (not in packages): the Switch main raw image has 10 control segments; the first 6 are graphics shaders and map one-to-one, in the same order, to the 6 DXBC embedded in PC D2R.exe
# (each side has a pair of identical duplicates at the same position); the last 4 are compute shaders with no PC counterpart, handled separately.
# Usage: shader_map.py <PC package dir> <Switch package dir> <PC metallib cache dir> <output tsv> [<main raw image> <PC D2R.exe>]
import hashlib, json, os, re, struct, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from maxwell_refs import bindings, vs_inputs, sph_stage
pc_dir, dm_dir, cache_dir, out = sys.argv[1:5]
def records(d):
r = [m.start() for m in re.finditer(b'BTCDBTCD', d)]
return [d[x:(r[k + 1] if k + 1 < len(r) else len(d))] for k, x in enumerate(r)]
def pc_dxbc(rec):
i = rec.find(b'DXBC')
return rec[i:i + struct.unpack_from('<I', rec, i + 24)[0]]
def dx_outputs(dx):
j = dx.find(b'OSGN')
if j < 0: return -1
n = struct.unpack_from('<I', dx, j + 8)[0]
return sum(1 for q in range(n) if struct.unpack_from('<I', dx, j + 16 + q * 24 + 8)[0] == 0)
def dm_parts(rec):
i = rec.find(bytes.fromhex('34127698')) # control segment magic (little-endian 0x98761234)
csz = struct.unpack_from('<I', rec, 0x28)[0] # record header: platform number 5, control segment length...
return rec[i:i + csz], i
STAGE = {'vs': 'vertex', 'ps': 'pixel', 'cs': 'compute', 'hs': 'hull', 'ds': 'domain'}
cache = set(os.listdir(cache_dir))
rows, keys, cand, dxs, locs = [], {}, {}, {}, {}
for name in sorted(os.listdir(dm_dir)):
A = records(open(os.path.join(pc_dir, name), 'rb').read())
B = records(open(os.path.join(dm_dir, name), 'rb').read())
if len(A) != len(B): sys.exit(f'[RED] {name}: record counts differ, PC {len(A)} / Switch {len(B)}')
for k, (a, b) in enumerate(zip(A, B)):
dx = pc_dxbc(a); ctl, _ = dm_parts(b)
if len(ctl) < 0x100: sys.exit(f'[RED] {name}#{k}: bad control section')
swk = hashlib.sha256(ctl).hexdigest()[:16]; pck = hashlib.sha256(dx).hexdigest()[:16]
ml = pck + '.metallib' in cache
dm_out = struct.unpack_from('<I', ctl, 0x734)[0] if name.startswith('ps_') else -1
pc_out = dx_outputs(dx) if name.startswith('ps_') else -1
refl = json.load(open(os.path.join(cache_dir, pck + '.json')))['bindings'] if ml else []
pcb = {x['reg'] for x in refl if x['type'] == 'cbuffer'}
pt = {x['reg'] for x in refl if x['type'].startswith('texture') or x['type'].startswith('buffer')}
ps_ = {x['reg'] for x in refl if x['type'] == 'sampler'}
_, ci = dm_parts(b); sb = bindings(b[0x38:ci], name[:2])
pbuf = {x['reg'] for x in refl if x['type'] in ('buffer_raw', 'buffer_structured', 'atomic_alias')}
ok = int(sb['ubo'] <= pcb) | int(sb['tex'] <= pt) << 1 | int(sb['samp'] <= ps_) << 2 | int(sb['ssbo'] == pbuf) << 3
fmt = lambda x: ','.join(map(str, sorted(x))) or '-'
rows.append((name, k, STAGE.get(name[:2], name[:2]), swk, pck, int(ml), pc_out, dm_out,
fmt(sb['ubo']), fmt(sb['tex']), fmt(sb['samp']), fmt(sb['ssbo']), ok))
keys.setdefault(swk, set()).add(pck)
cand.setdefault(swk, {})[pck] = (pc_out, dm_out, pcb, pt, ps_, sb)
dxs[pck] = dx; locs.setdefault(swk, []).append(f'{name}#{k}')
if len(sys.argv) > 6:
mi = open(sys.argv[5], 'rb').read(); pe = open(sys.argv[6], 'rb').read()
dm_ctl = [m.start() for m in re.finditer(bytes.fromhex('34127698'), mi)]
pc_dx = []
for m in re.finditer(b'DXBC', pe):
o = m.start(); sz = struct.unpack_from('<I', pe, o + 24)[0] if o + 32 <= len(pe) else 0
if 64 < sz < 1 << 20 and o + sz <= len(pe) and struct.unpack_from('<I', pe, o + 20)[0] == 1: pc_dx.append(pe[o:o + sz])
gfx = [o for o in dm_ctl if mi.rfind(bytes.fromhex('78563412'), o - 0x1000, o) > 0]
# Pair by features (not by order: disassembly showed the embedded order differs between the two sides): stage is taken from SPH; set difference = symmetric difference of cb/t/s and (vertex) input slots, take the unique minimum
def pc_feat(dx):
h = hashlib.sha256(dx).hexdigest()[:16]
j = json.load(open(os.path.join(cache_dir, h + '.json')))
bs = j['bindings']
return h, j['stage'], ({x['reg'] for x in bs if x['type'] == 'cbuffer'}, {x['reg'] for x in bs if x['type'].startswith('texture')},
{x['reg'] for x in bs if x['type'] == 'sampler'}, {x['location'] for x in j['vertex_inputs']})
pcs = [pc_feat(dx) for dx in pc_dx]
for k, o in enumerate(gfx):
g = mi.rfind(bytes.fromhex('78563412'), o - 0x1000, o)
code = mi[g:o]; stg = sph_stage(code); sb = bindings(code, stg)
dm = (sb['ubo'], sb['tex'], sb['samp'], vs_inputs(code) if stg == 'vs' else set())
cands = sorted((sum(len(a ^ b) for a, b in zip(dm, f)), h) for h, st, f in pcs if st == stg)
if not cands or (len(cands) > 1 and cands[0][0] == cands[1][0] and cands[0][1] != cands[1][1]):
sys.exit(f'[RED] embedded shader @{o:#x} ({stg}): no unique PC counterpart: {cands}')
pck = cands[0][1]; swk = hashlib.sha256(mi[o:o + 0x880]).hexdigest()[:16]
ml = pck + '.metallib' in cache
refl = json.load(open(os.path.join(cache_dir, pck + '.json')))['bindings'] if ml else []
pbuf = {x['reg'] for x in refl if x['type'] in ('buffer_raw', 'buffer_structured', 'atomic_alias')}
f = pcs[[h for h, _, _ in pcs].index(pck)][2]
ok = int(dm[0] <= f[0]) | int(dm[1] <= f[1]) << 1 | int(dm[2] <= f[2]) << 2 | int(sb['ssbo'] == pbuf) << 3
fmt = lambda x: ','.join(map(str, sorted(x))) or '-'
rows.append(('main.rodata@%x' % o, k, STAGE.get(stg, stg), swk, pck, int(ml), -1, -1, fmt(sb['ubo']), fmt(sb['tex']), fmt(sb['samp']), fmt(sb['ssbo']), ok))
keys.setdefault(swk, set()).add(pck)
print(f' embedded @{o:#x} {stg} uses cb{fmt(dm[0])} t{fmt(dm[1])} s{fmt(dm[2])} inputs {fmt(dm[3])} → PC {pck} (difference {cands[0][0]}, runner-up {cands[1][0] if len(cands) > 1 else "-"})')
print(f'embedded graphics shaders {len(gfx)} (with metallib {sum(1 for r in rows[-len(gfx):] if r[5])}), embedded compute shaders not mapped {len(dm_ctl) - len(gfx)}')
amb = {k: v for k, v in keys.items() if len(v) > 1}
with open(out, 'w') as f:
f.write('# kind: evidence Switch shader variants → PC DXBC → PC-chain metallib (dm/gfx/tools/shader_map.py)\n')
f.write('# key conflicts (one Switch control section, several PC DXBC): %d\n' % len(amb))
f.write('package\tindex\tstage\tdm_ctl_sha16\tpc_dxbc_sha16\tmetallib\tpc_outputs\tdm_outputs\tdm_ubo\tdm_tex\tdm_samp\tdm_ssbo\tsubset_ok(cb|t<<1|s<<2|ssbo_equal<<3)\n')
for r in rows: f.write('\t'.join(map(str, r)) + '\n')
# Key conflict resolution
def dx_chunks(d):
out = {}
for i in range(struct.unpack_from('<I', d, 28)[0]):
o = struct.unpack_from('<I', d, 32 + 4 * i)[0]
out[d[o:o + 4]] = d[o + 8:o + 8 + struct.unpack_from('<I', d, o + 4)[0]]
return out
def klass(pcs):
ch = [dx_chunks(dxs[p]) for p in pcs]
code = [c.get(b'SHEX', c.get(b'SHDR')) for c in ch]
if len(set(code)) == 1: return 'same instruction block, only the input signature differs (picked by the vertex shader outputs when the pipeline is built)'
if len({len(x) for x in code}) == 1: return 'instruction blocks of equal length, input signatures differ (probably renumbered input registers, also picked by pairing)'
return 'different instruction blocks (real code differences Switch compiled into one binary; needs the package and index located at run time, or a visual check)'
def score(c):
po, so, pcb, pt, ps_, sb = c
diff = (po != so and po >= 0) + len(pcb ^ sb['ubo']) + len(pt ^ sb['tex']) + len(ps_ ^ sb['samp'])
return (diff, max(po, 0) + len(pcb) + len(pt) + len(ps_))
with open(out + '.conflicts.tsv', 'w') as f:
f.write('# kind: evidence PC candidate rulings for Switch control section key conflicts (smallest difference → simplest → smallest hash)\n')
f.write('dm_ctl_sha16\tchosen_pc\tclass\tcandidates(pc:diff:size)\trecords\n')
for k in sorted(amb):
cs = sorted(cand[k].items(), key=lambda kv: (score(kv[1]), kv[0]))
f.write('%s\t%s\t%s\t%s\t%s\n' % (k, cs[0][0], klass([p for p, _ in cs]), ' '.join('%s:%d:%d' % (p, *score(c)) for p, c in cs), ' '.join(locs[k])))
n = len(rows); ml = sum(r[5] for r in rows)
okc = [sum(1 for r in rows if r[12] >> b & 1) for b in range(4)]
print(f'binding subset holds: cb {okc[0]}/{len(rows)}, t {okc[1]}/{len(rows)}, s {okc[2]}/{len(rows)}; storage buffers equal {okc[3]}/{len(rows)}')
uniq_dm = len(keys); uniq_pc = len({r[4] for r in rows}); uniq_pc_ml = len({r[4] for r in rows if r[5]})
outd = [r for r in rows if r[6] >= 0 and r[6] != r[7]]
print(f'records {n}; distinct Switch control sections {uniq_dm}; distinct PC DXBC {uniq_pc}; key conflicts {len(amb)}')
print(f'records with a PC metallib {ml}/{n}; distinct PC DXBC with a metallib {uniq_pc_ml}/{uniq_pc}')
print(f'pixel shaders whose output counts differ (PC has more targets, Switch writes fewer) {len(outd)}')
by = {}
for r in rows: by.setdefault(r[2], [0, 0]); by[r[2]][0] += 1; by[r[2]][1] += r[5]
for s, (t, m) in by.items(): print(f' {s}: records {t}, with metallib {m}')