diff --git a/dm/games/0100000000010000/dmsh_chain.h b/dm/games/0100000000010000/dmsh_chain.h index 558296e..49427c2 100644 --- a/dm/games/0100000000010000/dmsh_chain.h +++ b/dm/games/0100000000010000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 0100000000010000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "123e7650b527" +#define DMSH_CHAIN "d4c4453854a2" diff --git a/dm/games/010015100B514000/c1_full.tsv b/dm/games/010015100B514000/c1_full.tsv index 6e63738..c953b40 100644 --- a/dm/games/010015100B514000/c1_full.tsv +++ b/dm/games/010015100B514000/c1_full.tsv @@ -1,4 +1,4 @@ -# chain b-8c8ebaa0059a 2026-10-18T10:29:59+08:00 +# chain b-4c64982e23bf 2026-10-18T23:48:25+08:00 35cbc686dd8461b8 4872bec9a5567f8b 54a618792b2d5f0f diff --git a/dm/games/010015100B514000/dmsh_chain.h b/dm/games/010015100B514000/dmsh_chain.h index 1128330..e7a0e70 100644 --- a/dm/games/010015100B514000/dmsh_chain.h +++ b/dm/games/010015100B514000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 010015100B514000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "8c8ebaa0059a" +#define DMSH_CHAIN "4c64982e23bf" diff --git a/dm/games/010028600EBDA000/dmsh_chain.h b/dm/games/010028600EBDA000/dmsh_chain.h index 7399665..1a31e72 100644 --- a/dm/games/010028600EBDA000/dmsh_chain.h +++ b/dm/games/010028600EBDA000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 010028600EBDA000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "fe21bb337dab" +#define DMSH_CHAIN "fa0edc540e9a" diff --git a/dm/games/01002B00111A2000/dmsh_chain.h b/dm/games/01002B00111A2000/dmsh_chain.h index 1285467..896bdec 100644 --- a/dm/games/01002B00111A2000/dmsh_chain.h +++ b/dm/games/01002B00111A2000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 01002B00111A2000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "6322afcaa206" +#define DMSH_CHAIN "5f57cfae0ff6" diff --git a/dm/games/01003FC01670C000/c1_full.tsv b/dm/games/01003FC01670C000/c1_full.tsv index aa928c3..809772d 100644 --- a/dm/games/01003FC01670C000/c1_full.tsv +++ b/dm/games/01003FC01670C000/c1_full.tsv @@ -1 +1 @@ -# chain b-1c784ff32922 2026-10-18T09:51:27+08:00 +# chain b-9e8a8bed7481 2026-10-18T23:25:23+08:00 diff --git a/dm/games/01003FC01670C000/dmsh_chain.h b/dm/games/01003FC01670C000/dmsh_chain.h index 3d27578..4eb551a 100644 --- a/dm/games/01003FC01670C000/dmsh_chain.h +++ b/dm/games/01003FC01670C000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 01003FC01670C000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "1c784ff32922" +#define DMSH_CHAIN "9e8a8bed7481" diff --git a/dm/games/0100535012974000/dmsh_chain.h b/dm/games/0100535012974000/dmsh_chain.h index d5dea3c..9d5fb1e 100644 --- a/dm/games/0100535012974000/dmsh_chain.h +++ b/dm/games/0100535012974000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 0100535012974000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "3aa49d130ba8" +#define DMSH_CHAIN "907d5c3232ae" diff --git a/dm/games/010055D009F78000/c1_full.tsv b/dm/games/010055D009F78000/c1_full.tsv index 1a07db2..c73ab80 100644 --- a/dm/games/010055D009F78000/c1_full.tsv +++ b/dm/games/010055D009F78000/c1_full.tsv @@ -1,3 +1,3 @@ -# chain b-d2524031d033 2026-10-18T15:24:03+08:00 +# chain b-020352bb86fa 2026-10-18T23:25:58+08:00 a936529cf487e4e0 bf81dc122c781790 diff --git a/dm/games/010055D009F78000/dmsh_chain.h b/dm/games/010055D009F78000/dmsh_chain.h index 7ec5a17..89c7ce6 100644 --- a/dm/games/010055D009F78000/dmsh_chain.h +++ b/dm/games/010055D009F78000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 010055D009F78000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "d2524031d033" +#define DMSH_CHAIN "020352bb86fa" diff --git a/dm/games/01006BB00C6F0000/dmsh_chain.h b/dm/games/01006BB00C6F0000/dmsh_chain.h index 6777cc4..ea12923 100644 --- a/dm/games/01006BB00C6F0000/dmsh_chain.h +++ b/dm/games/01006BB00C6F0000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 01006BB00C6F0000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "7363976f0e53" +#define DMSH_CHAIN "d84b01f127bd" diff --git a/dm/games/01007EF00011E000/dmsh_chain.h b/dm/games/01007EF00011E000/dmsh_chain.h index e865a38..46df90e 100644 --- a/dm/games/01007EF00011E000/dmsh_chain.h +++ b/dm/games/01007EF00011E000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 01007EF00011E000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "428aef474fa8" +#define DMSH_CHAIN "c2bffc1b7bae" diff --git a/dm/games/010093801237C000/c1_full.tsv b/dm/games/010093801237C000/c1_full.tsv index 88b1bb0..18080c8 100644 --- a/dm/games/010093801237C000/c1_full.tsv +++ b/dm/games/010093801237C000/c1_full.tsv @@ -1,3 +1,3 @@ -# chain b-1865e500180e 2026-10-18T11:16:10+08:00 +# chain b-c1dfcd9e019a 2026-10-18T23:25:13+08:00 57eb9f0bae677756 f826f2b59cfa39a8 diff --git a/dm/games/010093801237C000/dmsh_chain.h b/dm/games/010093801237C000/dmsh_chain.h index c4a28b6..d1fb061 100644 --- a/dm/games/010093801237C000/dmsh_chain.h +++ b/dm/games/010093801237C000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 010093801237C000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "1865e500180e" +#define DMSH_CHAIN "c1dfcd9e019a" diff --git a/dm/games/0100A6301214E000/dmsh_chain.h b/dm/games/0100A6301214E000/dmsh_chain.h index dc56293..ede2500 100644 --- a/dm/games/0100A6301214E000/dmsh_chain.h +++ b/dm/games/0100A6301214E000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 0100A6301214E000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "32031379f5b9" +#define DMSH_CHAIN "26bf029c37b4" diff --git a/dm/games/0100F2C0115B6000/c1_full.tsv b/dm/games/0100F2C0115B6000/c1_full.tsv index 5b8ddad..d345f8f 100644 --- a/dm/games/0100F2C0115B6000/c1_full.tsv +++ b/dm/games/0100F2C0115B6000/c1_full.tsv @@ -1,4 +1,4 @@ -# chain b-29bb51ae3ce7 2026-10-18T11:41:59+08:00 +# chain b-e1b1691d9514 2026-10-18T23:35:28+08:00 35cbc686dd8461b8 4872bec9a5567f8b 759c14c6d029f531 diff --git a/dm/games/0100F2C0115B6000/dmsh_chain.h b/dm/games/0100F2C0115B6000/dmsh_chain.h index 7b418db..5d2922d 100644 --- a/dm/games/0100F2C0115B6000/dmsh_chain.h +++ b/dm/games/0100F2C0115B6000/dmsh_chain.h @@ -1,2 +1,2 @@ // 生成文件(dm/gfx/tools/dmsh_chain.py --header,游戏 0100F2C0115B6000):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-/ -#define DMSH_CHAIN "29bb51ae3ce7" +#define DMSH_CHAIN "e1b1691d9514" diff --git a/dm/gfx/tools/dmsh_build.py b/dm/gfx/tools/dmsh_build.py index 2abe593..b04fd74 100644 --- a/dm/gfx/tools/dmsh_build.py +++ b/dm/gfx/tools/dmsh_build.py @@ -2,7 +2,7 @@ # B 路线离线出包:Switch 原版着色器 → SPIR-V(dmsh_xlate,Ryujinx 翻译器)→ spirv-cross MSL → metallib + 回放反射 JSON。 # 运行时不翻译(iOS 无 JIT):产物进着色器缓存目录,文件名 = Switch 控制段键(dm_ctl_sha16,与 shader_map.tsv 第 4 列同)+「_b」。 # 查询作答来源:游戏档案里的作答表 dm/games/<名>/shader_facts.tsv(dmsh_facts.py)——贴图类型、顶点属性 / 片元输出分量类型、计算线程组大小与共享内存;出包不需要 PC 版文件。 -# 用法:dmsh_build.py <输出缓存目录> [键…](不给键 = 全部);产物实际写进 <输出缓存目录>/b-<链版本>/(IOSOUT 同),链版本见 dmsh_chain.py 环境:DM_GAME=游戏档案(缺省 smbw,dm/games/),XLATE=翻译工具路径(缺省仓内 dmsh_xlate_bin),DMSH_CAS=内容寻址缓存目录(缺省为输出缓存目录/cas),J=并发 +# 用法:dmsh_build.py <输出缓存目录> [键…](不给键 = 全部);产物实际写进 <输出缓存目录>/b-<链版本>/(IOSOUT 同),链版本见 dmsh_chain.py 环境:DM_GAME=游戏档案(缺省 smbw,dm/games/),XLATE=翻译工具路径(缺省仓内 dmsh_xlate_bin),DMSH_CAS=内容寻址缓存目录(缺省为输出缓存目录/cas),DMSH_PREFILL=Mac 库根目录[=iOS 库根目录](以 : 分隔,其下 b-*/ 先收进按 MSL 的编译缓存,同级的 b-*/ 总是收),J=并发 # 程序本体内嵌的图形着色器(映射表 main.rodata@偏移 行)从 MAIN(Switch main NSO,缺省 3.3.94012 Update/ExeFS/main)解出;解 NSO 要 lz4,用 dm 的 venv python 跑 # 产物:<键>_b.metallib、<键>_b.json(回放反射,含驱动段 c0 / c2 的运行时读取偏移 drv_reads)、<键>_b.metal(MSL) # 环境 IOSOUT=目录:同一份 MSL 经 dm/ios/tools/ios_metal.compile 同时出 iphoneos metallib + 反射(chain_metal 记 iOS 编译口径) @@ -10,7 +10,7 @@ # Route B offline build: original Switch shaders → SPIR-V (dmsh_xlate, the Ryujinx translator) → spirv-cross MSL → metallib + replay reflection JSON. # No translation at runtime (iOS has no JIT): build output goes into the shader cache directory, file name = Switch control segment key (dm_ctl_sha16, same as column 4 of shader_map.tsv) + "_b". # Source of query answers: the answer table in the game profile, dm/games//shader_facts.tsv (dmsh_facts.py) - texture types, vertex attribute / fragment output component types, compute thread group size and shared memory; the build needs no PC version files. -# Usage: dmsh_build.py [key...] (no keys = all); build output is actually written to /b-/ (same for IOSOUT), for the chain version see dmsh_chain.py Environment: DM_GAME=game profile (default smbw, dm/games/), XLATE=path to the translation tool (default the in-repo dmsh_xlate_bin), DMSH_CAS=content-addressed cache dir (default /cas), J=concurrency +# Usage: dmsh_build.py [key...] (no keys = all); build output is actually written to /b-/ (same for IOSOUT), for the chain version see dmsh_chain.py Environment: DM_GAME=game profile (default smbw, dm/games/), XLATE=path to the translation tool (default the in-repo dmsh_xlate_bin), DMSH_CAS=content-addressed cache dir (default /cas), DMSH_PREFILL=Mac library root[=iOS library root] (separated by :, their b-*/ go into the MSL-keyed compile cache first; sibling b-*/ always do), J=concurrency # Graphics shaders embedded in the executable itself (mapping table rows main.rodata@offset) are extracted from MAIN (Switch main NSO, default 3.3.94012 Update/ExeFS/main); extracting the NSO needs lz4, run with dm's venv python # Build output: _b.metallib, _b.json (replay reflection, including drv_reads, the runtime read offsets into driver segments c0 / c2), _b.metal (MSL) # Environment IOSOUT=dir: the same MSL also goes through dm/ios/tools/ios_metal.compile to produce an iphoneos metallib + reflection (chain_metal records the iOS compile convention) @@ -19,7 +19,7 @@ import ast, inspect, os, re, sys, json, struct, hashlib, shutil, subprocess, tem DM_ROOT = os.environ.get('DM_ROOT') or os.path.expanduser('~/dm-work') # 工作区根目录(游戏文件、产物、运行现场都在它下面) | Workspace root (game files, build outputs and run artifacts are all under it) HOME = os.path.expanduser('~') dm_dir, out = sys.argv[1:3]; want = set(sys.argv[3:]); cache_root = out -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); import dmsh_chain, dmsh_fetch, dmsh_facts # 翻译链版本:先核工具与锁文件,产物进 b-/ | translation chain version: verify the tools against the lock file first; build output goes into b-/ +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); import dmsh_chain, dmsh_fetch, dmsh_facts, dmsh_mtlcache # 翻译链版本:先核工具与锁文件,产物进 b-/ | translation chain version: verify the tools against the lock file first; build output goes into b-/ _bad = dmsh_chain.check_tools() if _bad: sys.exit('\n'.join('【红】翻译链工具与锁文件不符:' + b for b in _bad) + '\n(换了工具就 dmsh_chain.py --update-lock 并入仓;只是没重编翻译器就重编)') CHAIN = dmsh_chain.version(); out = os.path.join(out, 'b-' + CHAIN) @@ -46,6 +46,9 @@ PRE_STATS = collections.Counter() # Super Mario Odyssey draws a depth pre-pass with alRenderDepthOnly, then the G-buffer with another vertex shader and depth test less-or-equal: a hair off and the test failed, leaving those pixels with old content (Cap Kingdom's ground in 32x32 blocks) MAC_METAL = ['xcrun', '-sdk', 'macosx', 'metal', '-c', '-fpreserve-invariance'] MAC_RECORD = ' '.join(MAC_METAL) + '; xcrun -sdk macosx metallib' +def mac_compile(msl, air, lib): # 同 ios_metal.compile 的接口 | same interface as ios_metal.compile + subprocess.run([*MAC_METAL, msl, '-o', air], check=True) + subprocess.run(['xcrun', '-sdk', 'macosx', 'metallib', air, '-o', lib], check=True) # 贴图句柄表(贴图指令立即数句柄所在的驱动段):马力欧(NVN 55)是 0x20 + 8 r、32 槽——全库 4888 个着色器的立即数句柄恰是 0x20..0xF0、步长 8(D2R 的旧驱动是 0x168)。 # 本作只用合并句柄(nvnCommandBufferBindTexture),没有分离采样器;图像句柄表仍在 0x120 + 8 r(8 槽,紧接贴图表之后;agl 的 10 个计算着色器实测 0x120 / 0x128 / 0x130,与 D2R 同址)。 # Texture handle table (the driver segment holding texture instructions' immediate handles): Mario (NVN 55) uses 0x20 + 8 r, 32 slots - the immediate handles of all 4888 shaders are exactly 0x20..0xF0 in steps of 8 (D2R's older driver used 0x168). @@ -64,13 +67,9 @@ 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 clone(src, dst): +def clone(src, dst): # APFS 写时复制,否则普通复制;用 clonefile 调用而不起 cp:本进程内存大,fork 一次约 6 ms | APFS copy-on-write, else a plain copy; the clonefile call, not a cp: a fork of this large process costs about 6 ms os.makedirs(os.path.dirname(dst), exist_ok=True) - tmp = dst + '.tmp-' + str(os.getpid()) + '-' + str(threading.get_ident()) - r = subprocess.run(['cp', '-c', src, tmp], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=60) - if r.returncode: - shutil.copyfile(src, tmp) - os.replace(tmp, dst) + dmsh_mtlcache.clone(src, dst) def atomic_json(path, value): os.makedirs(os.path.dirname(path), exist_ok=True) @@ -216,9 +215,8 @@ def fetch_finish(key, src, refl): open(fmsl, 'w').write(fv) fair = fmsl[:-6] + '.air'; flib = os.path.join(out, key + '_bf.metallib') try: - subprocess.run([*MAC_METAL, fmsl, '-o', fair], check=True) tmp_flib = flib + '.tmp-' + str(os.getpid()) - subprocess.run(['xcrun', '-sdk', 'macosx', 'metallib', fair, '-o', tmp_flib], check=True) + mac_compile(fmsl, fair, tmp_flib) os.replace(tmp_flib, flib) ilib = None if IOSOUT: @@ -517,7 +515,7 @@ def vs_depth(src): def finish(key, w, j, c1): name, k = loc[key] - msl = os.path.join(w, key + '.metal'); air = msl[:-6] + '.air'; lib = os.path.join(out, key + '_b.metallib') + msl = os.path.join(w, key + '.metal'); lib = os.path.join(out, key + '_b.metallib') r = subprocess.run(['spirv-cross', *XC, os.path.join(w, key + '.spv'), '--output', msl], capture_output=True, text=True) if r.returncode: return 'MSL 失败', r.stderr.strip()[:160], None src = open(msl).read() @@ -540,19 +538,12 @@ def finish(key, w, j, c1): src, full = vs_depth(src) open(msl, 'w').write(full) res = json.load(open(os.path.join(w, key + '.res.json'))) - try: - subprocess.run([*MAC_METAL, msl, '-o', air], check=True) - tmp_lib = lib + '.tmp-' + str(os.getpid()) - subprocess.run(['xcrun', '-sdk', 'macosx', 'metallib', air, '-o', tmp_lib], check=True) - os.replace(tmp_lib, lib) + try: MTL.compile('mac', msl, lib) # MSL 与以前编过的一样就直接取那次的库(dmsh_mtlcache.py) | MSL identical to one compiled before takes that library (dmsh_mtlcache.py) except subprocess.CalledProcessError as e: return 'metal 失败', str(e)[:160], None clone(msl, os.path.join(out, key + '_b.metal')) if IOSOUT: # iOS:同一份 MSL 走 iOS 的唯一编译口径(dm/ios/tools/ios_metal.py)出 iphoneos metallib | iOS: the same MSL goes through iOS's single compile convention (dm/ios/tools/ios_metal.py) to produce an iphoneos metallib - ilib = os.path.join(IOSOUT, key + '_b.metallib') - try: - ios_metal.compile(msl, msl[:-6] + '.ios.air', ilib + '.tmp') - os.replace(ilib + '.tmp', ilib) + try: MTL.compile('ios', msl, os.path.join(IOSOUT, key + '_b.metallib')) except subprocess.CalledProcessError as e: return 'iOS metal 失败', str(e)[:160], None entry = re.search(r'(vertex|fragment|kernel)\s+\S+\s+(\w+)\s*\(', src) dr = drv_reads(src) @@ -570,6 +561,13 @@ keys = sorted(want) if want else sorted(loc) NJ = int(os.environ.get('J', '8')) os.makedirs(out, exist_ok=True) w = tempfile.mkdtemp(prefix='dmsh_', dir=os.environ.get('WORK')) +MTL = dmsh_mtlcache.Cache(os.path.join(CAS, 'metallib'), {'mac': ('macosx', MAC_RECORD, mac_compile), **({'ios': ('iphoneos', ios_metal.RECORD, ios_metal.compile)} if IOSOUT else {})}, w) +# 先收下早先的链版本留在盘上的库:输出目录同级的 b-*/(iOS 取同名目录)与 DMSH_PREFILL 列的根目录 | first take the libraries earlier chain versions left on disk: the b-*/ beside the output (iOS: the folder of the same name) and the roots DMSH_PREFILL lists +for _spec in [cache_root + ('=' + os.path.dirname(IOSOUT) if IOSOUT else '')] + [x for x in os.environ.get('DMSH_PREFILL', '').split(os.pathsep) if x]: + _mac, _, _ios = _spec.partition('=') + for _b in sorted(os.listdir(_mac)) if os.path.isdir(_mac) else []: + if _b.startswith('b-') and os.path.realpath(os.path.join(_mac, _b)) != os.path.realpath(out): + for _ln in MTL.fill(os.path.join(_mac, _b), os.path.join(_ios, _b) if _ios else None): print(_ln, flush=True) packs, jobs, c1s = {}, {}, {} digests, rule_fps, cache_reasons, cache_hits = {}, {}, {}, set() for key in keys: @@ -619,6 +617,7 @@ print(f'前置缓存:命中 {PRE_STATS["hit"]},重出 {PRE_STATS["miss"]}') for reason in ('新键', '规则变', '代码指纹变化:全部重出'): if PRE_STATS[reason]: print(f' {reason} {PRE_STATS[reason]}') print(f'变体缓存:生成 {BF_STATS["generated"]},命中 {BF_STATS["hit"]},重出 {BF_STATS["miss"]},编译 {BF_STATS["compile"]}') +print(MTL.summary()) for lvl in [k for k in st if k != 'ok']: for e, n in collections.Counter(r[2] for r in res if r[1] == lvl).most_common(10): print(f' [{lvl}] {n:5d} {e}') ok = [r[3] for r in res if r[1] == 'ok'] diff --git a/dm/gfx/tools/dmsh_chain.lock b/dm/gfx/tools/dmsh_chain.lock index b39ba13..8e25e7f 100644 --- a/dm/gfx/tools/dmsh_chain.lock +++ b/dm/gfx/tools/dmsh_chain.lock @@ -1,8 +1,8 @@ # 翻译链工具指纹(dmsh_chain.py --update-lock 生成;换工具后重生成并随提交入仓) -dmsh_xlate.dll bde0f0e23d095ae5 -Ryujinx.Graphics.Shader.dll 950b659a54a52636 -Ryujinx.Common.dll 74f785bc3a81386e -xlate_source 41339b01f7cde45f +dmsh_xlate.dll e85245171dbb3310 +Ryujinx.Graphics.Shader.dll 011c50cb1c08e44f +Ryujinx.Common.dll 5222da62ffb43db1 +xlate_source e5e4cf3ed43f05a2 spirv-cross d9dc197f061b42a5 metal_macosx Apple metal version 32023.921 (metalfe-32023.921.6) metal_iphoneos Apple metal version 32023.921 (metalfe-32023.921.6) diff --git a/dm/gfx/tools/dmsh_mtlcache.py b/dm/gfx/tools/dmsh_mtlcache.py new file mode 100644 index 0000000..6ca4323 --- /dev/null +++ b/dm/gfx/tools/dmsh_mtlcache.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# Compiled-shader cache keyed by the Metal source (dmsh_build.py): a library is compiled once per (MSL text, compile convention, compiler), so a chain change +# that leaves a shader's MSL as it was (a translator fix, a new rule for other shaders) costs that shader no Metal compile. The Metal compiler is deterministic: +# the same MSL with the same flags gives a byte-identical metallib (checked for macOS and iOS), which is what makes reuse safe. +# Layout: //<2 hex>/.metallib, digest = sha256 over FORMAT, the target's compile convention (the record written into json chain_metal), +# the compiler's identity (metal / metallib --version without the install path, the SDK version and build) and the MSL bytes. +# Filling from earlier builds: a Mac b-/ folder holds _b.metal (the exact source that was compiled) beside _b.metallib, and the iOS b-/ folder +# of the same chain holds the iOS libraries of the same sources. A folder is taken only when its json chain_metal is the current convention and a fresh compile of +# a few of its shaders (two per stage) reproduces their libraries byte for byte; each folder is read once (filled.json remembers it by its number of libraries). +# Not part of the chain version (dmsh_chain.py): the cache never changes an output, it only skips recompiling one. Change FORMAT when what it stores changes. +import os, re, json, shutil, ctypes, hashlib, threading, subprocess, collections + +FORMAT = b'dmsh_mtlcache 1' +_clonefile = getattr(ctypes.CDLL(None, use_errno=True), 'clonefile', None) # APFS copy-on-write copy (no data written); elsewhere a plain copy + + +def clone(src, dst): + tmp = f'{dst}.tmp-{os.getpid()}-{threading.get_ident()}' + if _clonefile is None or _clonefile(os.fsencode(src), os.fsencode(tmp), 0) != 0: shutil.copyfile(src, tmp) + os.replace(tmp, dst) + + +def compiler_id(sdk): + def run(*a): return subprocess.run(['xcrun', '-sdk', sdk, *a], capture_output=True, text=True).stdout + tools = ''.join(run(t, '--version') for t in ('metal', 'metallib')) + return re.sub(r'(?m)^InstalledDir:.*\n?', '', tools) + run('--show-sdk-version') + run('--show-sdk-build-version') # the install path changes with each toolchain mount + + +class Cache: + def __init__(self, root, targets, work): + """targets: {name: (sdk, record, compile)}, compile(msl, air, lib) as ios_metal.compile; work: a folder for temporary .air files""" + self.root, self.targets, self.work = root, targets, work + self.ident = {t: hashlib.sha256(FORMAT + b'\0' + rec.encode() + b'\0' + compiler_id(sdk).encode()).digest() for t, (sdk, rec, _) in targets.items()} + self.stats, self.lock = collections.Counter(), threading.Lock() + + def path(self, target, src): + d = hashlib.sha256(self.ident[target] + b'\0' + src).hexdigest() + return os.path.join(self.root, target, d[:2], d + '.metallib') + + def count(self, what, n=1): + with self.lock: self.stats[what] += n + + def _compile(self, target, msl, lib): + air = os.path.join(self.work, f'mtlcache-{target}-{os.getpid()}-{threading.get_ident()}.air') + try: self.targets[target][2](msl, air, lib) + finally: + if os.path.exists(air): os.unlink(air) + + def compile(self, target, msl, lib): + """msl → lib for target: a clone of the library that the same source compiled to before, or a compile (then kept). Raises CalledProcessError as the compile does""" + p = self.path(target, open(msl, 'rb').read()) + if os.path.exists(p): + clone(p, lib); self.count((target, 'reused')); return + tmp = f'{lib}.tmp-{os.getpid()}-{threading.get_ident()}' + self._compile(target, msl, tmp) + os.replace(tmp, lib) + os.makedirs(os.path.dirname(p), exist_ok=True); clone(lib, p); self.count((target, 'compiled')) + + def _check(self, target, d, stems, mac_dir): + """None if folder d was built with the current convention and compiler, else why not""" + rec, picks = self.targets[target][1], collections.defaultdict(list) + for stem in stems[::max(1, len(stems) // 24)]: + try: j = json.load(open(os.path.join(d, stem + '.json'))) + except (OSError, ValueError): return f'no readable {stem}.json' + if j.get('chain_metal') != rec: return f'built with "{j.get("chain_metal")}"' + if len(picks[j.get('stage')]) < 2: picks[j.get('stage')].append(stem) + for stem in (s for v in picks.values() for s in v): + tmp = os.path.join(self.work, f'mtlcache-check-{target}.metallib') + try: self._compile(target, os.path.join(mac_dir, stem + '.metal'), tmp) + except subprocess.CalledProcessError: return f'{stem}.metal does not compile now' + same = open(tmp, 'rb').read() == open(os.path.join(d, stem + '.metallib'), 'rb').read(); os.unlink(tmp) + if not same: return f'a fresh compile of {stem}.metal differs from its library' + return None + + def fill(self, mac_dir, ios_dir=None): + """Takes the libraries of an earlier build: mac_dir = its Mac b-/ folder, ios_dir = the iOS folder of the same chain (or None). Returns the lines to print""" + stamp_file = os.path.join(self.root, 'filled.json') + try: stamps = json.load(open(stamp_file)) + except (OSError, ValueError): stamps = {} + names = set(os.listdir(mac_dir)); stems = sorted(n[:-6] for n in names if n.endswith('_b.metal')) + todo, notes = {}, [] + for target, d in (('mac', mac_dir), ('ios', ios_dir)): + if target not in self.targets or not d or not os.path.isdir(d): continue + libs = names if d == mac_dir else set(os.listdir(d)) + have = [s for s in stems if s + '.metallib' in libs]; key = os.path.realpath(d) + if not have or stamps.get(key) == len(have): continue + why = self._check(target, d, have, mac_dir) + if why: notes.append(f'Metal cache: not filled from {d}: {why}') + else: todo[target] = (d, set(have)) + stamps[key] = len(have) + added = collections.Counter() + for stem in stems: + ts = [t for t, (d, have) in todo.items() if stem in have] + if not ts: continue + src = open(os.path.join(mac_dir, stem + '.metal'), 'rb').read() + for t in ts: + p = self.path(t, src) + if os.path.exists(p): continue + os.makedirs(os.path.dirname(p), exist_ok=True); clone(os.path.join(todo[t][0], stem + '.metallib'), p); added[t] += 1 + for t, (d, have) in todo.items(): + self.count((t, 'filled'), added[t]); notes.append(f'Metal cache: {added[t]} {t} libraries added from {d} ({len(have)} there)') + os.makedirs(self.root, exist_ok=True) + tmp = f'{stamp_file}.tmp-{os.getpid()}'; json.dump(stamps, open(tmp, 'w'), indent=1); os.replace(tmp, stamp_file) + return notes + + def summary(self): + s = self.stats + return 'Metal cache: ' + '; '.join(f'{t} {s[t, "reused"]} reused, {s[t, "compiled"]} compiled' + (f', {s[t, "filled"]} filled from earlier builds' if s[t, 'filled'] else '') + for t in self.targets) diff --git a/dm/gfx/tools/dmsh_xlate/Program.cs b/dm/gfx/tools/dmsh_xlate/Program.cs index bcc3ba3..bc4fa02 100644 --- a/dm/gfx/tools/dmsh_xlate/Program.cs +++ b/dm/gfx/tools/dmsh_xlate/Program.cs @@ -101,6 +101,21 @@ static class SwshXlate // 作业 reduced = true:放宽精度——翻译器只给强制精确的运算打 NoContraction(缺省全部打:保持 Maxwell 分开舍入的逐位结果,代价是 MSL 里大量禁优化的 spvFMul 等) // Job reduced = true: relaxed precision - the translator marks NoContraction only on operations that must be exact (the default marks all: keeps the bit-exact results of Maxwell's separate rounding, at the cost of many optimization-disabled spvFMul etc. in the MSL) public bool QueryHostReducedPrecision() => _job["reduced"] is JsonNode n && (bool)n; + // Job softfloat64 = true: the host has no FP64, so Ryujinx's DoubleToFloat pass lowers double operations to float (Metal has no double type). Set only on the retry of a + // shader whose first translation declared a 64-bit float (see Main): answering "no" for every shader would also drop the Float64 capability from all the others + public bool QueryHostSupportsShaderFloat64() => !(_job["softfloat64"] is JsonNode n && (bool)n); + } + + // True when the SPIR-V declares a 64-bit float type (OpTypeFloat 64): SPIRV-Cross then writes double, which the Metal compiler rejects + static bool UsesFloat64(byte[] spirv) + { + for (int i = 20; i + 12 <= spirv.Length; ) + { + uint word = BitConverter.ToUInt32(spirv, i); + if ((word & 0xffff) == 22 && (word >> 16) == 3 && BitConverter.ToUInt32(spirv, i + 8) == 64) return true; + i += Math.Max(1, (int)(word >> 16)) * 4; + } + return false; } static int Main(string[] args) @@ -126,7 +141,13 @@ static class SwshXlate var flags = (job["debug"] is JsonNode dbg && !(bool)dbg ? 0 : TranslationFlags.DebugMode) | ((string)job["stage"] == "cs" ? TranslationFlags.Compute : 0); var ctx = Translator.CreateContext(0, acc, new TranslationOptions(TargetLanguage.Spirv, TargetApi.Vulkan, flags)); ShaderProgram p = ctx.Translate(); - File.WriteAllBytes(Path.Combine(outDir, key + ".spv"), p.BinaryCode); + if (UsesFloat64(p.BinaryCode)) // Tears of the Kingdom 1.4.3: 30 shaders used double and failed in the Metal compiler + { + job["softfloat64"] = true; acc = new Accessor(code, job); + ctx = Translator.CreateContext(0, acc, new TranslationOptions(TargetLanguage.Spirv, TargetApi.Vulkan, flags)); + p = ctx.Translate(); acc.Notes.Add("FP64 lowered to FP32 (DoubleToFloat)"); + } + File.WriteAllBytes(Path.Combine(outDir, key + ".spv"), VertexCounterLowering.Apply(code, (string)job["stage"], p.BinaryCode, acc.Notes)); var res = new JsonObject { ["key"] = key, ["stage"] = (string)job["stage"], ["notes"] = new JsonArray(acc.Notes.ConvertAll(n => (JsonNode)n).ToArray()), @@ -146,3 +167,48 @@ static class SwshXlate return bad == 0 ? 0 : 1; } } + +// Tears of the Kingdom 1.4.3 visibility counter (one vertex shader, exact code SHA-256 below): each invocation that passes its test joins a subgroup ballot, and the +// highest active lane adds popcount(ballot) to s1[0]. SPIRV-Cross cannot give a Metal vertex function the subgroup builtins ("Subgroup builtins are not available in +// this type of function"), so the shader never built and its draws were skipped. Lowered to singleton groups: lane id 0 and ballot(true) = (1, 0, 0, 0), so every passing +// invocation adds one; the atomic's result is unused, so the final sum (mod 2^32) is the same. Not a general subgroup fallback: it runs only on these analysed bytes. +// Adapted from MacNativeGames (games/totk/tools/totk_xlate/VertexCounterLowering.cs, https://github.com/ly05010419/MacNativeGames), MIT License: dm/third_party/macnativegames/LICENSE. +static class VertexCounterLowering +{ + const string InstanceCodeHash = "F5F4D23149565ACD852F6B7091F64CC8CB3AA019CCB33BA3F88CED62C6D1FF7B"; + + internal static byte[] Apply(byte[] code, string stage, byte[] spirv, List notes) + { + if (stage != "vs" || Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(code)) != InstanceCodeHash) return spirv; + uint[] w = new uint[spirv.Length / 4]; Buffer.BlockCopy(spirv, 0, w, 0, spirv.Length); + uint lane = 0, uintType = 0, vectorType = 0, trueId = 0; int ballots = 0, loads = 0; + for (int i = 5; i < w.Length; i += (int)(w[i] >> 16)) + { + uint op = w[i] & 65535; + if (op == 71 && w[i + 2] == 11 && w[i + 3] == 41) lane = w[i + 1]; // OpDecorate BuiltIn SubgroupLocalInvocationId + if (op == 21 && w[i + 2] == 32 && w[i + 3] == 0) uintType = w[i + 1]; // OpTypeInt 32 unsigned + if (op == 41) trueId = w[i + 2]; // OpConstantTrue + if (op == 339) { vectorType = w[i + 1]; ballots++; if (w[i + 4] != trueId) throw new InvalidOperationException("visibility counter: ballot predicate changed"); } // OpGroupNonUniformBallot + } + if (lane == 0 || uintType == 0 || vectorType == 0 || ballots != 1) throw new InvalidOperationException("visibility counter: SPIR-V shape changed"); + uint zero = w[3]++, one = w[3]++, mask = w[3]++; // new ids past the bound + var result = new List(w.Length + 16); result.AddRange(w.AsSpan(0, 5).ToArray()); + bool constants = false; + for (int i = 5; i < w.Length; i += (int)(w[i] >> 16)) + { + uint op = w[i] & 65535; + if (op == 54 && !constants) // before the first OpFunction: uint 0, uint 1, uvec4(1, 0, 0, 0) + { + result.AddRange(new uint[] { (4u << 16) | 43, uintType, zero, 0, (4u << 16) | 43, uintType, one, 1, (7u << 16) | 44, vectorType, mask, one, zero, zero, zero }); + constants = true; + } + if (op == 61 && w[i + 3] == lane) { result.AddRange(new uint[] { (4u << 16) | 83, w[i + 1], w[i + 2], zero }); loads++; } // OpLoad lane id -> OpCopyObject 0 + else if (op == 339) result.AddRange(new uint[] { (4u << 16) | 83, w[i + 1], w[i + 2], mask }); // ballot(true) -> OpCopyObject (1, 0, 0, 0) + else result.AddRange(w.AsSpan(i, (int)(w[i] >> 16)).ToArray()); + } + if (loads != 1 || !constants) throw new InvalidOperationException("visibility counter: lane id use changed"); + notes.Add("visibility counter lowered to singleton groups (one atomic add per passing vertex)"); + byte[] output = new byte[result.Count * 4]; Buffer.BlockCopy(result.ToArray(), 0, output, 0, output.Length); + return output; + } +} diff --git a/dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Common.dll b/dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Common.dll index 3ed6070..7daa2b2 100644 Binary files a/dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Common.dll and b/dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Common.dll differ diff --git a/dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Graphics.Shader.dll b/dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Graphics.Shader.dll index d69218b..9fc0180 100644 Binary files a/dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Graphics.Shader.dll and b/dm/gfx/tools/dmsh_xlate_bin/Ryujinx.Graphics.Shader.dll differ diff --git a/dm/gfx/tools/dmsh_xlate_bin/Spv.Generator.dll b/dm/gfx/tools/dmsh_xlate_bin/Spv.Generator.dll index c471773..4b81741 100644 Binary files a/dm/gfx/tools/dmsh_xlate_bin/Spv.Generator.dll and b/dm/gfx/tools/dmsh_xlate_bin/Spv.Generator.dll differ diff --git a/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate b/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate index 63652e9..2241f67 100755 Binary files a/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate and b/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate differ diff --git a/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate.dll b/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate.dll index 1587f9d..fde2abc 100644 Binary files a/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate.dll and b/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate.dll differ diff --git a/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate.pdb b/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate.pdb index 97203a9..a17633c 100644 Binary files a/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate.pdb and b/dm/gfx/tools/dmsh_xlate_bin/dmsh_xlate.pdb differ diff --git a/dm/third_party/README.md b/dm/third_party/README.md index 44e7dee..1ba38b6 100644 --- a/dm/third_party/README.md +++ b/dm/third_party/README.md @@ -8,11 +8,11 @@ This is code written by others, shipped with the repository so there is nothing | `libdeflate/` | Decompression library | Upstream commit `92e6a0d`, unmodified | Host: zlib decoding of the game's resource blocks (`dm/host/hle_tact.c`). Only the five decompression-related source files are built; they are listed in `dm/host/Makefile` | `libdeflate/COPYING` (MIT) | | `libvpx/` | The VP9 decoder from libvpx: the 71 source files a VP9-decoder-only build compiles, their headers, and the generated configuration in `config/` | v1.17.0 (upstream commit `6df3ec3`), unmodified | Host: decodes the games' VP9 movies (`dm/host/hle_movie.c`). `dm/host/Makefile` builds it into `gen/vpx/libvpx.a`; the iOS build compiles the same files | `libvpx/LICENSE` (BSD 3-Clause), with the patent grant in `libvpx/PATENTS` | | `stb_vorbis/` | Ogg Vorbis decoder, a single file `stb_vorbis.c` | 1.22 (upstream commit `1ee679c`), unmodified | Host: decodes the Vorbis sound tracks of the games' WebM movies (`dm/host/hle_movie.c`, which includes the file with its push-data API only) | `stb_vorbis/LICENSE` (MIT or public domain, your choice; the same text ends `stb_vorbis.c`) | -| `ryujinx/` | The shader translation library from Ryujinx, three projects: `Ryujinx.Graphics.Shader`, `Ryujinx.Common`, `Spv.Generator` | Ryujinx 1.3.2, unmodified | Referenced by the shader translation tool `dm/gfx/tools/dmsh_xlate`, which translates the original Switch shaders to SPIR-V | `ryujinx/LICENSE.txt` (MIT); `ryujinx/src/Spv.Generator/LICENSE` is that subproject's own license text | +| `ryujinx/` | The shader translation library from Ryujinx, three projects: `Ryujinx.Graphics.Shader`, `Ryujinx.Common`, `Spv.Generator` | Ryujinx 1.3.2, with the local changes listed in `ryujinx/README.md` | Referenced by the shader translation tool `dm/gfx/tools/dmsh_xlate`, which translates the original Switch shaders to SPIR-V | `ryujinx/LICENSE.txt` (MIT); `ryujinx/src/Spv.Generator/LICENSE` is that subproject's own license text | | `vulkan-headers/` | The Vulkan C headers: `vulkan_core.h`, `vk_platform.h`, `vulkan_vi.h` and the `vk_video/` headers they include | Vulkan-Headers 1.4.363, unmodified | Host: the Vulkan layer on Metal (`dm/gfx/vk/hle_vk.m`) that runs Hades | `vulkan-headers/LICENSE.md` (Apache-2.0 or MIT, your choice) | A few more notes: - `flip/tool/` also carries three single-header libraries used by the FLIP command-line tool: `stb_image.h` (2.23), `stb_image_write.h` (1.13) and `tinyexr.h`. Their license notices are in the files themselves. - `libdeflate/VERSION.md`, `libvpx/README.md`, `stb_vorbis/README.md`, `ryujinx/README.md` and `vulkan-headers/README.md` record where each came from and what it is used for. -- Three pieces of third-party code live outside this directory: `dm/gfx/nvn/include/nvn.h` (MIT, author named in the file header), the layout size algorithm in `dm/gfx/nvn/nvn_tex.c`, ported from deko3d (zlib license), and in `dm/gfx/nvn/nvn_mtl.m` the GPU widening of 8-bit indices (`wide_*`) and the in-pass clear triangles (`clear_color_tri` / `clear_ds_tri`), adapted from MacNativeGames' `widen_u8_gpu` and `clear_quad` / `clear_ds_quad` (MIT, license text in `macnativegames/LICENSE`). +- Four pieces of third-party code live outside this directory: `dm/gfx/nvn/include/nvn.h` (MIT, author named in the file header), the layout size algorithm in `dm/gfx/nvn/nvn_tex.c`, ported from deko3d (zlib license), and two adapted from MacNativeGames (MIT, license text in `macnativegames/LICENSE`): in `dm/gfx/nvn/nvn_mtl.m` the GPU widening of 8-bit indices (`wide_*`) and the in-pass clear triangles (`clear_color_tri` / `clear_ds_tri`), from its `widen_u8_gpu` and `clear_quad` / `clear_ds_quad`, and the Tears of the Kingdom visibility-counter lowering in `dm/gfx/tools/dmsh_xlate/Program.cs` (`VertexCounterLowering`). diff --git a/dm/third_party/ryujinx/README.md b/dm/third_party/ryujinx/README.md index f85a8fa..c208e76 100644 --- a/dm/third_party/ryujinx/README.md +++ b/dm/third_party/ryujinx/README.md @@ -1,5 +1,10 @@ # Ryujinx shader translator (vendored subset) -Three projects from Ryujinx 1.3.2, unmodified: `Ryujinx.Graphics.Shader`, `Ryujinx.Common` and `Spv.Generator`. +Three projects from Ryujinx 1.3.2: `Ryujinx.Graphics.Shader`, `Ryujinx.Common` and `Spv.Generator`, with these local changes: + +- `Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs`, `GenerateTextureSample`: + - Texel offsets that are not constants (Maxwell AOFFI / TLD4.PTP offsets read from a register) are built with `OpCompositeConstruct` and passed as `Offset`. A four-offset gather becomes four one-offset gathers, each giving component w. Upstream wrote them as an `OpConstantComposite` with `ConstOffset(s)`, which is invalid SPIR-V; SPIRV-Cross threw `nullptr` and the shader never built (2026-10-18: 8 shaders in Wonder, Link's Awakening, Tears of the Kingdom and Age of Calamity). Marked `nxnative:` in the code. + - Texture coordinates that are NaN are replaced by 0 (the `nfZero` select). This came with the first import. + The shader translation tool in `dm/gfx/tools/dmsh_xlate` references them to translate the original Switch shaders to SPIR-V. License: MIT, see `LICENSE.txt`. Upstream: . diff --git a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs index 79383ec..8ad6ff7 100644 --- a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs +++ b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs @@ -1308,6 +1308,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv lod = Src(coordType); } + // nxnative: texel offsets read from a register (Maxwell AOFFI / PTP from a non-immediate) are not constants. Upstream put them + // into an OpConstantComposite and passed them as ConstOffset / ConstOffsets: invalid SPIR-V, on which SPIRV-Cross throws "nullptr" + // (the shader never builds). Constant offsets keep the old form byte for byte; runtime ones use Offset (see the gather below) + bool offsetsConst = true; + SpvInstruction AssembleOffsetVector(int count) { if (count > 1) @@ -1321,11 +1326,25 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv SpvInstruction vectorType = context.TypeVector(context.TypeS32(), count); + if (Array.Exists(elems, e => e.Opcode != Op.OpConstant)) + { + offsetsConst = false; + + return context.CompositeConstruct(vectorType, elems); + } + return context.ConstantComposite(vectorType, elems); } else { - return Src(AggregateType.S32); + SpvInstruction elem = Src(AggregateType.S32); + + if (elem.Opcode != Op.OpConstant) + { + offsetsConst = false; + } + + return elem; } } @@ -1398,10 +1417,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv if (hasOffset) { - operandsMask |= ImageOperandsMask.ConstOffset; + operandsMask |= offsetsConst ? ImageOperandsMask.ConstOffset : ImageOperandsMask.Offset; operandsList.Add(offsets[0]); } - else if (hasOffsets) + else if (hasOffsets && offsetsConst) { operandsMask |= ImageOperandsMask.ConstOffsets; SpvInstruction arrayv2 = context.TypeArray(context.TypeVector(context.TypeS32(), 2), context.Constant(context.TypeU32(), 4)); @@ -1431,6 +1450,25 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { result = context.ImageFetch(resultType, image, pCoords, operandsMask, operands); } + else if (isGather && hasOffsets && !offsetsConst) + { + // nxnative: ConstOffsets must be constant. Each texel of a four-offset gather is texel (i0, j0) of a gather at that one offset, + // which is component w of its result (gather order: i0j1, i1j1, i1j0, i0j0) + SpvInstruction[] texels = new SpvInstruction[4]; + + for (int index = 0; index < 4; index++) + { + SpvInstruction[] gatherOperands = [.. operands, offsets[index]]; + ImageOperandsMask gatherMask = operandsMask | ImageOperandsMask.Offset; + SpvInstruction texel4 = isShadow + ? context.ImageDrefGather(resultType, image, pCoords, dRef, gatherMask, gatherOperands) + : context.ImageGather(resultType, image, pCoords, compIdx, gatherMask, gatherOperands); + + texels[index] = context.CompositeExtract(context.TypeFP32(), texel4, 3); + } + + result = context.CompositeConstruct(resultType, texels); + } else if (isGather) { if (isShadow) diff --git a/docs/development.md b/docs/development.md index 599c40d..5d3fde1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -101,6 +101,7 @@ GAME=$G W=$W dm/ios/tools/build_ipa.sh [out.ipa] ``` Notes on these steps: +- `dmsh_build.py` compiles each shader's Metal source through a cache keyed by that source, the compile flags and the compiler (`dm/gfx/tools/dmsh_mtlcache.py`, in `/cas/metallib/`). A chain change that leaves a shader's MSL unchanged, such as a translator fix, costs that shader no Metal compile. Before building, the cache takes the libraries of the earlier `b-*/` folders beside its output (and beside `IOSOUT`), and of `DMSH_PREFILL=[=]` (several separated by `:`). It takes a folder only when its recorded flags are the current ones and a fresh compile of a few of its shaders reproduces their libraries byte for byte. The summary line `Metal cache:` shows reused, compiled and filled counts per target. - `build_ipa.sh` takes the rewritten modules from `$W/build/patched_` and the RomFS from `$W/data//RomFS`; `PATCHED` and `ROMFS` override them. - It also bundles a standard font (macOS's Arial Unicode, `STD_FONT=` overrides it) and the generated `nintendo_ext.ttf`. - The icon is the `icon.jpg` that `nx_extract` saves next to `RomFS/`; `DM_ICON=` points at another image.