Metal builds a pipeline only when every pixel shader input is a vertex shader output; Maxwell gives an unwritten attribute a default. With shaders translated one at a time, 331 of v0's 9,043 vertex / pixel pairs failed to link and later levels lost those draws (about 11,500 failed pipeline builds in one device session). - shader_split.py writes shader_pairs.tsv (pairs of every BNSH variation) and takes comma-separated RomFS / main lists for several game versions, writing their union (keys are content hashes; v0 rows keep their package indices). - dmsh_build.py vs_pad(): vertex shaders get the outputs their partners read but they don't write, float4 (0, 0, 0, 1); vertex shaders with partners are finished last and their partner list is part of the cache rule. shader_pairs.tsv is part of the chain version. - Tables now cover v0 and update 1.2.1: 16,268 shaders (1.2.1 recompiled 3,451 SecredUber pixel shaders and added 6 effect shaders), 14,603 pairs. Chain 5f1ec82dfcf0: 16,266 compile (the 2 known SPIRV-Cross failures); an offline link test of all pairs gives 14,603 / 0 (v0 before the padding: 8,712 / 331). c1_full.tsv regenerated (same 11 keys). The v0 IPA with this padding played 11 minutes on the iPhone with no pipeline failures.
smbw-mobile
Work in progress: an iOS native-execution port of Super Mario Bros. Wonder (Switch, base game v0), built from the generic parts of d2r-mobile (MIT). Bring your own game files; nothing from the game is in this repository.
Same approach as d2r-mobile: the game's own aarch64 code runs natively. A rewriter changes only the instructions that cannot run under Apple's kernel (x18, thread pointers, svc, timers). The host replaces Switch system services, and the NVN graphics API is reimplemented on Metal.
Status (2026-10-13)
| Step | State |
|---|---|
Rewriter (dm/tools/patch_nso.py, DM_RULESET=native) |
Runs unchanged on Mario main / sdk |
| 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 | The opening course is playable with sound (run, jump, collect coins; characters, NPCs, sky, text, button icons all render; music and effects play). Only the opening course has been played |
| 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 | The game's own SDK renderer runs in-process in its CPU / manual-execution mode (hle_audren.c); the host renders one 5 ms frame per buffer the output hands back and sends the device-sink mix (5.1 folded to stereo) through Core Audio (DM_AUDIO=1 on Mac, default on iOS). About 0.1 ms of CPU per frame on an M-series Mac |
| iOS bundle | dm/ios/tools/build_ipa.sh builds one unsigned IPA (iPhone + iPad): game code embedded in the executable, iOS shader libraries, RomFS (3.5 GB) inside the app, standard-controller virtual gamepad and physical controllers. Compiles and links; not yet run on a device (this Mac has no signing identity, and the Simulator SDK has no Metal 4) |
What changed relative to d2r-mobile
- Removed: all D2R gameplay / UI modules (
dm_*.c), D2R diagnostics (diag.c), Blizzard TACT/CASC, Granny, movie, auto-targeting, and the archive validator.host_stubs.cholds the leftover hook points as no-ops. hle_hid.c: rewritten as a generic standard-controller layer covering all Npad styles plus six-axis, vibration, mouse and keyboard stubs.hle_sdk16.c(new): newer-SDKoe/time/ shared font (pl) / vsync event /diaghooks.hle_offline.c(new): offline stubs for play reports, local wireless, sockets (C++ and C APIs), SSL, nifm, nsd, friends, ngc and err.hle_fs.c,hle_account.c: newer-SDK overloads (MountRom(name),ReadFilevariants,GetDirectoryEntryCount, user enumeration, ...).loader.c: forced hooks that a game never references are now only logged (DM_FORCE_STRICT=1restores the abort).patch_nso.py, two fixes:- Exclusive-window bug, inherited from d2r-mobile. When an ldxr/stxr window contained a branch out (
ldxr; cmp; b.ne out; stxr; cbnz), a successful stxr fell into the branch-out exit. Every enqueue in Mario's job system retried forever and the job system hung. The fall-through exit is now emitted first. D2R's windows had no branch out, so D2R never hit it. mrs xN, ctr_el0is rewritten to a constant (macOS traps it; SDK 16'sFlushDataCachereads it).
- Exclusive-window bug, inherited from d2r-mobile. When an ldxr/stxr window contained a branch out (
hle_thread.c: SetThreadCoreMask moves the ideal core into the new mask, as Horizon does, soGetCurrentProcessorNumberis right for pinned threads.- NVN:
- API version reported as 55.15 (
DM_NVN_APIoverrides it). - 13 newer names were added to the table.
tex_tis packed to 0x80 bytes: Mario allocatesNVNtextureas 0x80, and the old 0x88 record overran it and corrupted the heap.view_tis packed to 16 bytes: SDK 16'sNVNtextureViewis 16 bytes and Mario keeps it in a 16-byte stack slot; the old 28-byte record overwrote the saved frame pointer and return address.- Shader key = sha256(control section + code area): Mario has many shaders with an identical control section and different code.
- Bindless textures: 38 % of Mario's shaders read texture handles out of their own uniform buffers. New binding kinds
tex_ubo/samp_ubo(NVN_RB_TEXU/NVN_RB_SAMPU) read the handle at replay time. - Bound texture handles live at
0x20 + 8 × slotin NVN 55 (D2R's driver:0x168); images stay at0x120 + 8 × slot. - 8-bit index buffers are widened to 16-bit at replay (Metal has no 8-bit indices); Mario's full-screen passes and its final copy to the window use them.
- Texture swizzle (
TextureBuilderSetSwizzle/TextureViewSetSwizzle) is applied through Metal texture views (the font atlas is(1, 1, 1, R)). - Non-packaged textures are shared by storage (pool + offset + description): Mario rebuilds its transient render targets on new objects every frame and never finalizes them, which leaked about 13 MB of GPU memory per frame.
- TBDR optimizations are off by default (
DM_NVN_TBDR=1turns them on) until they recognize bindless readers. - 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.
RGB10A2SNvertex format; instancedDrawArrays.- Vertex formats follow the shader input type (integer inputs get integer formats of the matching sign).
- Copies, fills and inline uniform updates batched in one Metal 4 encoder get a barrier when they touch a range an earlier one wrote (Metal 4 does not order them). Mario writes the same uniform slot twice in a row; the reversed order made the fountain's GPU wave simulation diverge and draw tall panels over the screen.
- Scissored zero clears outside a pass are copied from a zero buffer into the scissor rectangle (they were skipped).
- New non-packaged colour textures are zero-filled (Metal leaves them undefined; Switch memory pools start as zero), and inline uniform updates are copied out of command memory when encoded.
- API version reported as 55.15 (
- Shader chain:
dm/gfx/tools/shader_split.py(new) scans RomFS andmainfor BNSH and BAHS (aglsharcb) 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 setsnvnDeviceSetDepthModeto 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
uintinputs (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.pystrips 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. - Threads created with ideal core -2 run on the process default core (0);
GetCurrentProcessorNumbernever returns a negative core. Mario's font engine indexes per-core state with it. - Shared fonts are served with a BFTTF "already decoded" header so
ScalableFontEngineHelper::Decodeleaves them alone. Type 5 (Nintendo extension) gets a generated icon font (dm/tools/make_ext_font.py: A/B/X/Y, L/R/ZL/ZR/SL/SR, +/−, D-pad, stick arrows at the original private-use code points); the other types share a desktop font.DM_FONT_DIRpoints atstandard.ttf/nintendo_ext.ttf(the iOS app ships both). - Crash reports scan the stack for return addresses and dump the popped frame.
DM_PROBE(host/probe.c): register-relative dumps (@xN+off), hit filters (?[xN+off]=val), asead::ExpHeapwalker (!xN), thread IDs,DM_PROBE_ALL.
- Guest address space repartitioned: alias region 2.875 GB, heap 2.625 GB. Mario reserves 1.75 GB of
- Audio (
hle_audren.c): newer SDKs link the whole audio renderer into the game (nn::audio::servercommand generation andnn::audio::dsp::CommandListProcessor, the CPU version of the DSP).OpenAudioRendereris switched to rendering device = CPU and execution mode = manual and then runs in-process; a host-started guest thread calls the game'sExecuteAudioRendererRenderingfor every buffer the output returns, the takeover ofAudioRendererImpl::ExecuteAudioRendererRenderingreads the DeviceSink inputs out of the mix buffers (DeviceSink is a no-op in the CPU processor), and an update hook renders a catch-up frame when the game sends two updates without a frame in between (otherwise newly added effects are never initialized and the light limiter dereferences null). patch_nso.pycode cuts for Mariomain(build IDcd6e42ae…): resource heaps (main+0x1e62fc) get +16 KB.dm/tools/ghidra/Decompile.java: headless decompile by module offset;@off(re)creates functions reached only through vtables and undoes Ghidra's wrong "non-returning" mark onnn::osimports.
Build and run on Mac
Requires Xcode, the .NET SDK, spirv-cross and the game files extracted to ~/smbw-work/data/v0/{ExeFS,RomFS}.
# 1. rewrite the code modules
DM_RULESET=native python3 dm/tools/patch_nso.py ~/smbw-work/data/v0/ExeFS/{main,sdk} ~/smbw-work/build/patched_v0 {main,sdk}
# 2. shaders: extract, then translate (writes <cache>/b-<chain version>/)
<venv python with lz4> dm/gfx/tools/shader_split.py ~/smbw-work/data/v0/RomFS ~/smbw-work/data/v0/ExeFS/main ~/smbw-work/shader/pkg dm/gfx/nvn/shader_map.tsv dm/gfx/nvn/shader_facts.tsv
dotnet publish dm/gfx/tools/dmsh_xlate -c Release -o ~/smbw-work/build/gfx/dmsh_xlate
XLATE=~/smbw-work/build/gfx/dmsh_xlate/dmsh_xlate python3 dm/gfx/tools/dmsh_chain.py --header
XLATE=... python3 dm/gfx/tools/dmsh_build.py ~/smbw-work/shader/pkg ~/smbw-work/build/cache/metal_shader_cache
python3 dm/gfx/tools/c1_guard.py ~/smbw-work/build/cache/metal_shader_cache/b-<V> dm/gfx/nvn/c1_full.tsv
# 3. host
make -C dm/host dmhost
# 4. button-icon font for the Nintendo-extension shared font (needs fontTools: pip install fonttools)
python3 dm/tools/make_ext_font.py ~/smbw-work/build/font/nintendo_ext.ttf
~/smbw-work/run/boot.sh <logname> [seconds] # sets DM_MSL_DIR to the shader cache and DM_FONT_DIR to ~/smbw-work/build/font
Agent-facing guides for analysing and porting other titles are in docs/agents.
Build the iOS app (unsigned IPA)
Needs Xcode 27, xcodegen, fontTools (FONT_PY= a python that has it, if python3 does not), and the Mac build steps above (the rewritten modules in ~/smbw-work/build/patched_v0). The app also bundles dm/font/standard.ttf (copied from macOS's Arial Unicode, STD_FONT= to override) and the generated nintendo_ext.ttf.
# iOS shader libraries for the current chain version (writes ~/smbw-work/build/ios/dmsh_b/b-<V>/; about 15 min)
IOSOUT=~/smbw-work/build/ios/dmsh_b XLATE=... python3 dm/gfx/tools/dmsh_build.py ~/smbw-work/shader/pkg ~/smbw-work/build/cache/metal_shader_cache
# the IPA (default ~/Desktop/SMBWMobile.ipa)
dm/ios/tools/build_ipa.sh [out.ipa]
Sign and install it with your own tool (AltStore, Sideloadly, ...), which replaces the placeholder bundle ID org.smbwmobile.app. The app shell is dm/ios/Sources:
main.m: environment, log, boot thread.front_ios.m: thefront.hfunctions and a centred 16:9 Metal view.DmGamepad.m: touch gamepad (left stick anywhere in the lower left, D-pad, A/B/X/Y in Nintendo positions, ZL/L, ZR/R, −/+). Hidden while a physical controller is connected; physical buttons map by position.
The host log goes to the app's Documents (log.txt, previous run log.prev.txt), visible in the Files app. Game files are also accepted at Documents/romfs.
Tools (dm/tools)
| Tool | What it does |
|---|---|
nso2elf.py |
Converts NSO to ELF for Ghidra / REA |
insn_census.py |
Counts the instruction sites the rewriter must handle |
nso_syms.py |
Compares the SDK import surface of two games |
nvn_slots_got.py |
Builds the NVN fetch table and call sites (GOT-indirect loaders) |