Opening course playable: integer vertex inputs for skinned models, sky colour ramps

- dmsh_build.py: vertex shaders whose inputs are only used as integer bit casts are
  re-translated with uint inputs (529 shaders); bone indices were converted to floats and
  every character was stretched off screen
- Host: Metal vertex formats follow the shader input type (signedness swap, 32-bit float
  reinterpretation), mismatches reported
- LINEAR_RENDER_TARGET textures in CPU-visible pools are zero-copy too (sky colour ramps)
- Pass dump lists all vertex attributes; docs and README updated
This commit is contained in:
seedds committed 2026-10-13 08:56:30 +08:00
1 parent 4d8ac53681
commit 53642a6f9c
7 files changed
+74 -8

No files matched your search

+3 -1
View File
@@ -12,7 +12,7 @@ Same approach as d2r-mobile: the game's own aarch64 code runs natively. A rewrit
| Mac host | **Plays through the menus (player count, character select) into the opening course**: the course, effects, HUD, menu and dialogue text render in Chinese or English. Title screen at 60 fps, memory flat (about 900 MB physical footprint on the title screen) |
| Shaders | All 12,811 shaders the game ships (bfsha archives, particle effects, engine `agl` packages, UI layouts, `main`) are extracted by `shader_split.py` and translated offline; 12,809 compile. 2 `agl` post-effect compute shaders make SPIRV-Cross throw |
| Input | Scripted input `DM_KEYS="12:A,14:A,20:A,26:A,32:A"` (seconds:button) reaches the opening course; `DM_WINDOW=1` for a window with keyboard input |
| Current frontier | In the opening course the sky is black and no character models (player, NPCs) are visible; controller-button icons in text show as `?` (the host's stand-in system font has no Nintendo-extension glyphs) |
| Current frontier | **The opening course is playable** (run, jump, collect coins; characters, NPCs, sky, text all render). Open: an intermittent water / refraction effect over the fountain draws as tall blue panels (sometimes with a wrong texture); controller-button icons in text show as `?` (the host's stand-in system font has no Nintendo-extension glyphs); no audio |
| Known gaps | Partial / mid-pass clears; sparse virtual-pool mappings; conditional render; `DrawElementsIndirect`; TBDR optimizations are off by default (they don't understand bindless textures yet); resource heaps get +16 KB slack because one model's setup came up 0x70 bytes short of the size table (cause unknown) |
| Audio | Silent AudioRenderer (`hle_audren.c`); real mixing comes later |
| iOS bundle | Not started. Plan: reuse `dm/ios` from d2r-mobile, bundle the RomFS (3.5 GB) inside the app, standard-controller virtual gamepad |
@@ -44,9 +44,11 @@ Same approach as d2r-mobile: the game's own aarch64 code runs natively. A rewrit
- Linear textures in CPU-visible pools are created directly on the pool buffer (zero copy). Mario's glyph cache (2048×8192 R8) is filled by the CPU; as a private GPU texture all menu and dialogue text was blank.
- Host command-memory segments (lent when a command buffer outgrows the memory the game gave it) grow on demand instead of a fixed 64 × 1 MB.
- `RGB10A2SN` vertex format; instanced `DrawArrays`.
- Vertex formats follow the shader input type (integer inputs get integer formats of the matching sign).
- Shader chain:
- `dm/gfx/tools/shader_split.py` (new) scans RomFS and `main` for BNSH and BAHS (agl `sharcb`) containers and writes the package directory, the shader map and the answer table (compute thread-group size from control +0x748, shared memory from +0x754).
- The translator uses GL depth (`-1..1`) and no depth clip control, so vertex shaders remap z' = z/2 + w/2: Mario sets `nvnDeviceSetDepthMode` to near = -w, and without the remap its full-screen copy was clipped away.
- Vertex shaders whose inputs are only used as integers are re-translated with `uint` inputs (`int_inputs`): skinned models read bone indices as raw integer bits, and the float default made every character invisible.
- Compute code areas (magic 0x12345679) start their instructions at +0x100 (0x30 header + zero padding); `dmsh_build.py` strips all of it. Stripping only 0x30 misaligned the translator's 32-byte instruction groups: 12 particle / post-effect compute shaders became endless loops and hung the GPU at the first gameplay scene.
- Host:
- Guest address space repartitioned: alias region 2.875 GB, heap 2.625 GB. Mario reserves 1.75 GB of `VirtualAddressMemory` (HeapDef) that it maps into the alias region on demand; with 1.25 GB, large layout archives got no address and were skipped.
+1 -1
View File
@@ -1,4 +1,4 @@
# chain b-1b3a96e6a7d3 2026-10-13T08:05:33+08:00
# chain b-3620e9b5e1bd 2026-10-13T08:39:12+08:00
35cbc686dd8461b8
4872bec9a5567f8b
54a618792b2d5f0f
1 # chain b-1b3a96e6a7d3 2026-10-13T08:05:33+08:00 # chain b-3620e9b5e1bd 2026-10-13T08:39:12+08:00
2 35cbc686dd8461b8
3 4872bec9a5567f8b
4 54a618792b2d5f0f
+1 -1
View File
@@ -1,2 +1,2 @@
// 生成文件(dm/gfx/tools/dmsh_chain.py --header):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-<V>/
#define DMSH_CHAIN "1b3a96e6a7d3"
#define DMSH_CHAIN "3620e9b5e1bd"
+24 -3
View File
@@ -1199,7 +1199,8 @@ static id<MTLTexture> pkg_upload_(const tex_t *t, uint64_t *stamp) {
// Linear textures (LINEAR) in a CPU-visible pool: the game writes the pool memory directly from the CPU (SMBW's glyph cache: 2048×8192 R8, IMAGE|LINEAR, pool CPU_UNCACHED|GPU_CACHED,
// the font engine rasterizes glyphs into it on the CPU). A GPU-private texture never sees those writes → all menu / dialog text is blank. On unified memory, create a linear texture directly on the pool buffer (zero copy): the GPU reads the current content at execution time, same as NVN.
static id<MTLTexture> tex_linear(const tex_t *t) {
if (!(t->d.flags & NVN_TEXTURE_FLAGS_LINEAR) || !t->mem || !t->pool || (t->pool->flags & NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS)) return nil;
// LINEAR_RENDER_TARGET 也是线性排布(SMBW 的天空色带:4 张 64×1 RGBA16F,CPU 每帧写)| LINEAR_RENDER_TARGET is a linear layout too (SMBW's sky colour ramps: four 64×1 RGBA16F, written by the CPU every frame)
if (!(t->d.flags & (NVN_TEXTURE_FLAGS_LINEAR | NVN_TEXTURE_FLAGS_LINEAR_RENDER_TARGET)) || !t->mem || !t->pool || (t->pool->flags & NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS)) return nil;
if (t->d.target != NVN_TEXTURE_TARGET_2D || t->d.levels > 1 || t->d.samples > 1) return nil;
uint32_t pf = 0; uint8_t swz[4]; int bw, bh, bpb = nvn_fmt_bpb(t->d.format, &bw, &bh);
if (!nvn_fmt_to_mtl(t->d.format, &pf, swz) || !pf || !bpb || bw != 1 || bh != 1) return nil;
@@ -2384,6 +2385,25 @@ static void state_reset(void) {
S.st_vmask[0] = S.st_vmask[1] = S.st_wmask[0] = S.st_wmask[1] = 0xff; S.dr[1] = 1;
}
// 顶点格式按着色器输入的分量类型校正:Metal 要求整数输入配同符号的整数格式、浮点输入配浮点 / 归一化格式。
// 整数输入(翻译时按「只按整数用」推断,dmsh_build.py int_inputs)配整数格式:符号不同则换同宽的另一符号(位模式不变);配 32 位浮点格式则按同宽整数读(位模式不变)。
// 其余不匹配(整数输入配归一化 / 半精度格式,浮点输入配整数格式)位模式保不住,报红一次照原格式。
// Correct the vertex format by the shader input's component type: Metal requires integer inputs to get integer formats of the same signedness, and float inputs float / normalized formats.
// Integer input (inferred at translation time as "only used as an integer", dmsh_build.py int_inputs) with an integer format: on a signedness mismatch switch to the same width with the other sign (bit pattern unchanged); with a 32-bit float format read it as an integer of the same width (bit pattern unchanged).
// Other mismatches (integer input with a normalized / half format, float input with an integer format) cannot keep the bit pattern: report once and keep the format.
static MTLVertexFormat vfmt_for_ct(MTLVertexFormat f, int ct, int nvn_fmt) {
static const MTLVertexFormat U[] = { MTLVertexFormatUChar, MTLVertexFormatUChar2, MTLVertexFormatUChar3, MTLVertexFormatUChar4, MTLVertexFormatUShort, MTLVertexFormatUShort2, MTLVertexFormatUShort3, MTLVertexFormatUShort4, MTLVertexFormatUInt, MTLVertexFormatUInt2, MTLVertexFormatUInt3, MTLVertexFormatUInt4 };
static const MTLVertexFormat I[] = { MTLVertexFormatChar, MTLVertexFormatChar2, MTLVertexFormatChar3, MTLVertexFormatChar4, MTLVertexFormatShort, MTLVertexFormatShort2, MTLVertexFormatShort3, MTLVertexFormatShort4, MTLVertexFormatInt, MTLVertexFormatInt2, MTLVertexFormatInt3, MTLVertexFormatInt4 };
static const MTLVertexFormat F[] = { MTLVertexFormatFloat, MTLVertexFormatFloat2, MTLVertexFormatFloat3, MTLVertexFormatFloat4 };
int iu = -1, ii = -1, iff = -1;
for (int i = 0; i < 12; i++) { if (U[i] == f) iu = i; if (I[i] == f) ii = i; }
for (int i = 0; i < 4; i++) if (F[i] == f) iff = i;
if (ct == NVN_CT_UINT) { if (iu >= 0) return f; if (ii >= 0) return U[ii]; if (iff >= 0) return U[8 + iff]; }
else if (ct == NVN_CT_INT) { if (ii >= 0) return f; if (iu >= 0) return I[iu]; if (iff >= 0) return I[8 + iff]; }
else { if (iu < 0 && ii < 0) return f; }
RED_ONCE(66, "顶点属性格式 %#x 与着色器输入类型 %s 对不上,位模式保不住(照原格式)", nvn_fmt, ct == NVN_CT_UINT ? "uint" : ct == NVN_CT_INT ? "int" : "float");
return f;
}
static MTLVertexFormat vfmt(int f) {
switch (f) {
case 0x01: return MTLVertexFormatUCharNormalized; case 0x02: return MTLVertexFormatCharNormalized; case 0x03: return MTLVertexFormatUChar; case 0x04: return MTLVertexFormatChar;
@@ -2468,6 +2488,7 @@ static MTLRenderPipelineDescriptor *pso_desc(const pso_key *k, NvShader *vs, NvS
continue;
}
int st = k->attr[a].stream;
vf = vfmt_for_ct(vf, rf->v[v].ct, k->attr[a].fmt);
vd.attributes[loc].format = vf; vd.attributes[loc].offset = k->attr[a].offset; vd.attributes[loc].bufferIndex = 16 + st;
vd.layouts[16 + st].stride = k->stream[st].stride ? k->stream[st].stride : 4;
vd.layouts[16 + st].stepFunction = k->stream[st].stride == 0 ? MTLVertexStepFunctionConstant : k->stream[st].divisor ? MTLVertexStepFunctionPerInstance : MTLVertexStepFunctionPerVertex;
@@ -4548,8 +4569,8 @@ frz_plain:;
if (hn < 12 && u->size <= 608 && v->rf.b[i].reg == 3) { hn++; char hx[1300]; for (uint64_t q = 0; q < u->size; q++) snprintf(hx + 2 * q, 3, "%02x", base[mo + q]); PD("x %llu r%d %s", g_stat_pass, v->rf.b[i].reg, hx); } }
o += snprintf(ub + o, sizeof ub - o, " r%d=%016llx/%llu/%016llx", v->rf.b[i].reg, (unsigned long long)h, (unsigned long long)u->size, (unsigned long long)h64); }
PD("u %llu%s", g_stat_pass, ub); }
if (all) { char vb[512]; size_t o = 0; o += snprintf(vb + o, sizeof vb - o, "nattr=%d", S.nattr);
for (int i = 0; i < S.nattr && i < 6; i++) { int st = S.attr[i].stream;
if (all) { char vb[1600]; size_t o = 0; o += snprintf(vb + o, sizeof vb - o, "nattr=%d", S.nattr);
for (int i = 0; i < S.nattr && i < 16 && o < sizeof vb - 100; i++) { int st = S.attr[i].stream; if (!S.attr[i].fmt) continue; // 只打有格式的属性 | only attributes with a format
o += snprintf(vb + o, sizeof vb - o, " a%d{fmt=%#x off=%d s=%d stride=%u div=%u vb=%#llx/%llu}", i, S.attr[i].fmt, S.attr[i].offset, st,
st < 16 ? S.stream[st].stride : 0, st < 16 ? S.stream[st].divisor : 0, st < 16 ? (unsigned long long)S.vbuf[st].address : 0ull, st < 16 ? (unsigned long long)S.vbuf[st].size : 0ull); }
PD("v %llu %s", g_stat_pass, vb); }
+41 -1
View File
@@ -411,13 +411,53 @@ RULES = [] # 按着色器名挂的 MSL 改写规则(函数, 适用判定)
CODE_FP = hashlib.sha256(source_without_rule_bodies().encode() + b''.join(b'\0' + open(m.__file__, 'rb').read() for m in (dmsh_chain,) + ((ios_metal,) if IOSOUT else ()))).hexdigest()
TOOL_FP = tool_fingerprint().encode()
_CAST = ('as_type<int>(', 'as_type<uint>(')
def int_inputs(src):
"""顶点着色器里只按整数用的输入(float 声明下每次使用都是 as_type<int/uint> 位转换,或原样拷走)→ {位置: 'uint'}。
Maxwell 读顶点属性寄存器不做类型转换:整数格式(骨骼索引 R8UI / RG8UI / RGBA8UI 等)的位模式原样进寄存器,着色器再按整数用。
本作没有 PC 版反射可配对,翻译器的属性类型查询缺省全答 float:Metal 把整数格式转成浮点值送进来,位转换后成了乱值——骨骼索引全错,蒙皮模型(角色)的顶点被拉成长条或飞出画面。
Inputs that a vertex shader only uses as integers (under a float declaration every use is an as_type<int/uint> bit cast, or a plain copy) → {location: 'uint'}.
Maxwell does no type conversion when reading vertex attribute registers: the bit pattern of integer formats (bone indices R8UI / RG8UI / RGBA8UI etc.) goes into the register as is and the shader uses it as an integer.
This game has no PC version to pair reflection with, so the translator's attribute type queries all defaulted to float: Metal converts the integer format to a float value, and after the bit cast it is garbage - every bone index is wrong and skinned models (the characters) are stretched into streaks or thrown off screen."""
ins = {m.group(2): int(m.group(3)) for m in re.finditer(r'(\w+)\s+(\w+)\s*\[\[attribute\((\d+)\)\]\]', src)}
i = src.find('vertex main0_out main0(')
if i < 0: return {}
body = src[i:]; out = {}
for name, loc_ in ins.items():
uses = list(re.finditer(r'in\.%s\b' % re.escape(name), body))
nint = 0; bad = False
for u in uses:
ls = body.rfind('\n', 0, u.start()) + 1; le = body.find('\n', u.end())
m = re.match(r'\s*float\s+(_\w+)\s*=\s*in\.%s\.[xyzw];$' % re.escape(name), body[ls:le])
if not m:
if body[:u.start()].endswith(_CAST): nint += 1; continue
bad = True; break
v = m.group(1)
for o in list(re.finditer(r'\b%s\b' % re.escape(v), body))[1:]:
if body[:o.start()].endswith(_CAST): nint += 1; continue
l2s = body.rfind('\n', 0, o.start()) + 1; l2e = body.find('\n', o.end())
if re.match(r'\s*[\w.\[\]]+\s*=\s*%s;$' % re.escape(v), body[l2s:l2e]): continue # 原样拷走:两种声明下位模式都不变 | plain copy: the bit pattern is the same under either declaration
bad = True; break
if bad: break
if not bad and nint: out[str(loc_)] = 'uint'
return out
def finish(key, w, j, c1):
name, k = loc[key]
res = json.load(open(os.path.join(w, key + '.res.json')))
msl = os.path.join(w, key + '.metal'); air = msl[:-6] + '.air'; 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()
if j['stage'] == 'vs': # 第二遍:只按整数用的输入改报 uint 重新翻译(见 int_inputs) | second pass: re-translate with inputs that are only used as integers reported as uint (see int_inputs)
extra = {l: t for l, t in int_inputs(src).items() if l not in j['attr']}
if extra:
j['attr'].update(extra); jp = os.path.join(w, f'job1_{key}.json'); json.dump([j], open(jp, 'w'))
rx = subprocess.run([XLATE, jp, w], capture_output=True, text=True)
if rx.returncode or not os.path.exists(os.path.join(w, key + '.spv')): return '翻译失败', ('整数输入重翻:' + rx.stderr.strip())[:160], None
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()
res = json.load(open(os.path.join(w, key + '.res.json')))
try:
subprocess.run(['xcrun', '-sdk', 'macosx', 'metal', '-c', msl, '-o', air], check=True)
tmp_lib = lib + '.tmp-' + str(os.getpid())
+2
View File
@@ -64,6 +64,8 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **Frames stop, no crash, the main thread sits in `nvnSyncWait(sync, -1)`, and the watchdog's wait-state line shows the main fence one short (`已提交 N / 已完成 N-1`)** → a GPU command buffer never completes. Check GPU load from outside: `ioreg -r -d 1 -c IOAccelerator | grep 'Device Utilization'` at 100 % means a shader is looping forever. Find the shaders first used in the last frames (`DM_NVN_PASSDUMP`, compare the `K` keys) and look at their `.metal` for an empty `do {} while (true);` with no `return` (see [04](04-graphics.md) section 1: the compute program start).
- **Menus and dialogue boxes draw but contain no text; images and pictogram digits are fine** → the game rasterizes glyphs on the CPU into a *linear* texture in a CPU-visible pool (Mario: 2048×8192 R8, flags IMAGE|LINEAR). A private Metal texture never sees those writes. Linear textures in CPU-visible pools are now created on the pool buffer (`tex_linear` in `nvn_mtl.m`). `DM_NVN_TEXCPU=1` lists non-packaged textures in CPU-visible pools.
- **Characters (skinned models) invisible or stretched into streaks while the level renders** → integer vertex inputs (bone indices) translated as float; see [04](04-graphics.md) section 6. To find the draws: log model file ranges with `DM_PROBE=main:480f0` (x1 = file data, x2 = size), match the vertex/index buffer addresses in `DM_NVN_PASSDUMP_ALL` against them, then `DM_NVN_HILITE` their fragment shader.
- **Sky black** → CPU-written linear colour ramps (see the linear-texture entry above).
- **Controller-button icons in text show as `?`** → those glyphs come from the system shared font's Nintendo-extension set; the host serves a desktop font for every shared-font type. Open.
### NVN object sizes (memory corruption)
+2 -1
View File
@@ -52,7 +52,8 @@ Consequence: any optimization that infers "which textures a draw reads" from bou
- **Compute code areas have a header** in Mario (`0x12345679`), not in D2R; `dmsh_build.py` strips it.
- **Virtual memory pools** (flag 0x200): aliased onto the physical pool named by `MapVirtual` when all requests share one base (Mario's case); otherwise unimplemented.
- **SPIRV-Cross failures**: 2 agl post-effect compute shaders still produce SPIR-V that SPIRV-Cross can't parse (`nullptr` exception). Before the compute start-offset fix it was 7: misdecoded code produced the bad SPIR-V.
- **CPU-written linear textures**: a texture with `NVN_TEXTURE_FLAGS_LINEAR` in a CPU-visible pool is created on the pool's `MTLBuffer` (`tex_linear`), so CPU writes (Mario's glyph cache) are visible to the GPU. Block-linear CPU-written textures still need the packaged-texture re-upload path.
- **CPU-written linear textures**: a texture with `NVN_TEXTURE_FLAGS_LINEAR` or `LINEAR_RENDER_TARGET` in a CPU-visible pool (Mario's glyph cache; its four 64×1 RGBA16F sky colour ramps, without which the sky is black) is created on the pool's `MTLBuffer` (`tex_linear`), so CPU writes (Mario's glyph cache) are visible to the GPU. Block-linear CPU-written textures still need the packaged-texture re-upload path.
- **Integer vertex inputs**: without PC reflection the translator declares every vertex input `float4`. Skinned models read their bone index (`R8UI` / `RG8UI` / `RGBA8UI`) as raw integer bits (`as_type<int>(in.m_N.x) * 48`); Metal converted the integers to floats, every bone index was garbage, and all characters were stretched into streaks. `dmsh_build.py` (`int_inputs`) now re-translates a vertex shader with `uint` inputs when every use of an input is an integer bit cast (529 of 4,854 in Mario), and the host makes the Metal vertex format agree with the declared type (`vfmt_for_ct`; signedness swaps and 32-bit float→int reinterpretation keep the bits). A remaining mismatch is reported as `顶点属性格式 … 与着色器输入类型 … 对不上`.
- **Vertex formats**: Mario uses `RGB10A2SN` (0x69) → `MTLVertexFormatInt1010102Normalized`. Unmapped formats fall back to a 4-component read and print `顶点属性格式 … 未映射` once.
- **Instanced array draws** (`DrawArraysInstanced`) carry instance count and base instance in the `OP_DRAW` record.