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:
1 parent
4d8ac53681
commit
53642a6f9c
7 files changed
+74
-8
No files matched your search
@@ -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,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,2 +1,2 @@
|
||||
// 生成文件(dm/gfx/tools/dmsh_chain.py --header):本源码树期望的 B 着色器库翻译链版本(缺省选项);宿主只加载 <库目录>/b-<V>/
|
||||
#define DMSH_CHAIN "1b3a96e6a7d3"
|
||||
#define DMSH_CHAIN "3620e9b5e1bd"
|
||||
+24
-3
@@ -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); }
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user