Compare revision shader builds and pipeline interfaces across local games,
and run available smoke routes with crash, hang, timing and image checks.
Add the developer loop and share the measuring lock with bench.py.
Keep all local games in static checks and honor --no-smoke. Include shader
bytes and fallback pairs in cache keys, recheck interfaces on reuse, and
reuse smoke baselines only for matching inputs and run settings. Detect
failed prebuilds, missing or intermittently bad screenshots, and Vulkan
build failures even when no MSL was emitted. Keep game-derived files and
writable profile copies inside each game's NSP work folder.
Validation: tool tests and the lint ratchet pass. Full shader rebuilds and
gameplay comparisons remain unverified; the 26 discovered Mac builds have
no smoke routes in this checkout.
Co-authored-by: v0lumehi <aljic.zijad@gmail.com>
D2R (Diablo II Resurrected) has no profile and its code could not run here: the
host-call svcs its cuts emitted only died, the touch-run-v4 rule set needed a
missing dm/PRODUCT, and the host files its hooks called were never in this
repository. Removed with it:
- rewriter: D2R cuts, observation gates and parameter slots, the touch-run-v4
rule set and private gates, the boot list, the cut scope column and the
KRES / KASPECT key slots (fixups.tsv keeps the four thread slots; no
params.tsv any more); dm/tools/product.sh
- host: D2R save edits, add-on content, render-scale controller, time()
takeover and Terror Zone clock, crash decoding at D2R offsets, host-call
registry and stubs, i18n, UI-screen, shell-app loader, zero-stripped
archives, boot profiling hooks with no implementation; D2R headers, tests
and scripts; dm/ios/Sources/dm_touch_controls; dm/third_party/libdeflate
- graphics: runtime extrapolation, world freeze, iPad UI shift, shop drag
image, brightness LUT, visual ruler, bloom / FXAA shader hashes, DM_GFXUSE,
the eco-fps setting, the loading-screen gate; shader_map.py,
nometal_run.py, nvn_slots.py, nvn_slots.tsv and the PC-texture samples
The legacy frame step (frame_step legacy, DM_FRAME_STEP) is retired: the
Breath of the Wild and Tears of the Kingdom stubs lose their mode test.
Breath of the Wild's interval-controller cuts stay: the framework keeps the
controller's interval while the game's VFR override holds.
patch_nso.py output is byte-identical for all 15 games on this machine except
the two frame-step stubs; all 18 profiles build; the host tests pass (their
Makefile now builds the general tests only).
A 26-minute run from a fresh save goes through character creation and the story opening into free play with no red
line; the game saves four times and a later boot continues from the saves. Not yet run on an iPhone.
- Dynamic texture slots (profile key tex_dyn_slots, on for this game only): 61 vertex shaders (particles) pick one of
several c2 table slots at run time, which the translator cannot trace to one slot (Ryujinx's 32-entry fallback
arrays, <stage>_<t|tb|s>_cb0_40000000). The reflection reads them as NVN_RB_TEXD / NVN_RB_SAMPD arrays, the replay
writes slot indices into that shader's c2 tables and binds slot k at element k. They used not to load; the castle
scene's snow is back. Host only: no chain version changes.
- Image atomics: SPIRV-Cross's MSL 2.3 emulation binds a device atomic pointer (<image>_atomic) beside the image, which
the build reads as a second image of the slot, so the replay put a texture resource ID where an address belongs.
NVN_RB_IMGA binds the storage of a buffer or linear image there (a buffer texture is a view of its pool); a
block-linear image's atomics go to the dummy buffer (logged). 2,084 shaders in this game, at least ten run.
- Buffer textures at an offset below Metal's reported 16-byte alignment are created when the offset is a multiple of
the element size (measured on an M6: views at offsets 4, 8 and 12 read the right elements); they were dummies.
- The first occlusion-query reset inside a pass without a visibility buffer splits the pass, so it is counted.
- A buffer texture in a slot a shader reads as another type (a cube) reads 0 instead of undefined data.
- Service calls: nn::fs::GetSaveDataSizeMax (SIGSEGV at the first save, about 13 minutes in), GetCacheStorageSize,
GetFreeSpaceSize, account::PushOpenUsers, web::ShowOfflineHtmlPage, oe::ExecuteProgram, found by listing main's
unhooked imports.
- The engine is Unreal Engine 5 (its paged global distance field shaders and LowLevelTasks), not 4.26 / 4.27; the 16
compute shaders with 3D image atomics are that global distance field, which the mobile renderer does not dispatch.
Tears of the Kingdom boots the same with the new host (no shader libraries there, so the new bindings are not
exercised); the dm/gfx unit tests give the same results as before the change.
Octopath Traveler 0 1.0.5 (Unreal Engine 4.26 / 4.27 mobile renderer, SDK 19.3.13) plays from boot through character
creation into the story on the Mac (15 minutes with the iPhone memory layout, no crash); not yet run on an iPhone.
- Shaders: the 10,324 are in the two shader code archives of the game's pak, each one Oodle-compressed. uepak.py reads
the version 11 pak (AES-256 index; the key is the window of main that decrypts it, never printed or written; AES
through CommonCrypto) and the archives; the Mac host decodes them with the game's own OodleLZ_Decompress
(DM_OODLE_JOB, hle_oodle.c, build-gated) and shader_split.py reads the decoded LSLG records (profile key
shader_ue_decode; the NSP script's decode_ue_shaders stage). Bindings measured: Engage's separate texture layout.
- Memory: Unreal's nninitStartup reserves a 62 GB address region and its FMallocBinned3 37 GB of it. A cut skips the
allocator's constructor and the host puts its own FMalloc vtable on a malloc zone in the object (hle_uemalloc.c);
the region is capped at 2 GB with the game's own -SwitchVirtualAddressSpaceSize (new profile key game_args, also
for the iOS app) and SDK 19's node area is cut to 62 MB as Engage's, for a 3.75 GB guest block.
- Host: game arguments are set before nninitStartup, as on hardware; an undefined weak import nothing defines binds
to 0, as rtld binds it (Tears of the Kingdom boots the same with the old and new hosts: its weak glslc* imports
now resolve to 0); nvnCommandBufferFenceSync (OP_FENCE_SYNC), the texture / sampler pool getters,
vi::GetLatestFrameNumber / ListFrameHistory; DM_ALIAS_WINDOWS for sizing fixed-window allocators.
- Rewriter: a byte jump table whose add sits 17 instructions before its br (jump_bytes_far, tried last).
- build_ipa.sh quotes FONT_PY (a python path with spaces failed the font step).
Open: 61 vertex shaders pick a texture slot at run time (dynamic handle), 16 compute shaders with 3D image atomics
do not build. The profile (dm/games/01005270232F2000) holds the shader tables, translation chain b-ba76ba138e78.
New profile 010005501E68C000 (update 1.0.0.2, Unity 2022.3.17f1 IL2CPP, SDK 17.5.3; the base game an XCI):
- Rewriter: two IL2CPP thunks end in an indirect tail call, ldr x18, [x0, #0x18]; ...; br x18 (main+0x1d271bc,
+0x204ebec), which asserted as an unknown jump table. When neither table form is found, the last write of x18 is a
plain load and the br ends the function, it becomes x16 = guest x18; br x16 (tail_call / site_br_call). Tried only
where the rewriter used to stop, so modules it accepted before come out byte-identical.
- sdk cut: the AddressRegionManager node area 1 GB -> 62 MB on this build (sdk+0x139468 / +0x1394fc), as for Engage;
StandardAllocator::Initialize aborted in nninitStartup without it.
- Shaders (unityfs.py, shader_split.py): Unity 2022 keeps each program's parameters as blob entries of their own
(m_ParameterBlobIndices; the programs are m_PlayerSubPrograms[tier][].m_BlobIndex), and subprogram version
202012090 (from 2021.2) has no local keyword list. The player's files borrow the unitybuiltinshaders bundle's Shader
type tree. 4,951 shaders, 4,950 build (one geometry shader); Engage's layout of driver segments decodes every binding.
UnityPy's type trees for 2020.3.18 and 2021.3 have neither field, so Engage reads every entry as before.
- hle_fs.c: nnmusl (open / stat / opendir) checks every path with nn::fs::IsPathMounted, GetPathLengthMax,
GetUtf16PathLengthMax, GetEntryNameLengthMax and GetUtf16EntryNameLengthMax, which the SDK answered from its empty
mount table: File.Exists failed with no nn::fs call, Addressables never found settings.json and the game crashed on
a null asset (main+0x31100ec). Answered from the host's mount table with Horizon's 0x300-byte limits.
- hle_time.c: clock_gettime(CLOCK_REALTIME) reads the source nntimeStartup selects (sdk+0x8e414),
nn::time::GetStandardUserSystemClockCurrentTimespec, which asked the time service; system_clock::now() then called
std::terminate after the first dialog. It and the adjustable clock's getter are the host clock now.
- Memory: Engage's 5 GB block with a 1 GB application pool. StandardAllocator commits 0.19 GB; with a 2.5 GB pool
its reservation and Unity's own pools reached the end of the 4.375 GB alias region after 140 s (probes on
VammManager::AllocateAddressRegion / FreeAddressRegion). 25 minutes now peak at +2.81 GB.
On the Mac: first-boot settings, title, Investigations 1's Episode 1 into the first investigation, 60 fps, FMOD sound,
25 minutes with no red line, 1.55 GB resident and 1.25 GB of Metal allocations, flat.
IPA AceAttorneyInvestigations.ipa (7.0 GB) built from HEAD + these changes; awaits an iPhone test.
Also: UNIMPL stubs log once (the seen counter was a byte and wrapped every 256 calls); build_ipa.sh quotes FONT_PY
(a Python path with spaces broke the font step); profile_guess.py marks IL2CPP by global-metadata.dat (Unity 2022's
main has no il2cpp_ names). Not needed in the end: pull requests #15 (this main's string table is intact) and #16
(nninitStartup sizes no heap with virtual address memory).
Much of the bring-up was found first by armyja (GitHub issues #17, #18, #19, pull requests #15, #16).
Co-authored-by: armyja <546323405@qq.com>
dm/games/profile_guess.py decides the profile keys a game's files show, each written with its reason:
graphics vulkan and the modules to load (a subsdk only when main takes something from it the host does
not supply), shader_vk_capture / shader_main_glslc / shader_separable / shader_glslc_capture, the memory
partition (giant bss, no heap, virtual address memory), the SDK 8 / 10 font and NVN API, Hagi's handheld
pointer. --check compares the guess with every profile in dm/games: all 13 agree on these keys.
After the rewrite the script sets mem_used from the rewritten modules (Age of Calamity's 72 MB), writes
the empty shader tables and chain header a provisional Vulkan game lacked, and boots a game with virtual
address memory that sets a heap until its heap request to size the heap region (about 7 s; Tears of the
Kingdom gets its 0x98000000 alias region). A minute's boot could not tell which games need render_cond,
tex_flush_upload or core_preempt, so they stay with the developer.
--provisional builds a game that has a profile from a guessed one (DM_GAMES_FIRST in game.py and the host
Makefile, whose profile header is now rewritten only when it changes); --no-heap-check skips the check.
A capture run whose host stops early now says so. hle_svc.c logs a heap request the pool check refuses.
- Log messages, script output, test labels, Makefile errors and comments are English. Red log lines start with "[RED] " (was 【红】).
- Shader map stage column: vertex / pixel / compute / geometry / hull / domain (generators, dmsh_build / dmsh_facts and nvn_mtl.m stage_of follow).
- The NSP script's regexes match the new shader_split.py / dmsh_build.py summary lines; patch_nso cut scopes are all / host-only.
- Host UI text table (dm_i18n_tab.h) is English only; dm_lang_zh() removed.
- Text that matches game data (t_swkbd.c, t_textbox.c) is written as \x escapes.
- Shader chain: the chain files lose their bilingual comments, so every profile has a new chain version. dmsh_xlate.dll rebuilt from the translated Program.cs (SPIR-V byte-identical on 4,500 shaders of Wonder, TOTK and Age of Calamity), lock and dmsh_chain.h headers regenerated. c1_full.tsv restamped for Dread, 13 Sentinels and Wonder; the other profiles follow when their rebuild finishes.
- AGENTS.md and docs/agents: the language rule now covers logs; quoted log lines updated.
The Wii game on NERD's Hagi layer (PowerPC code recompiled into main), rendering
with Vulkan through Hades' layer. Title screen, file creation and the storybook
prologue into the Star Festival at about 58 fps on the Mac; the IPA awaits an
iPhone test.
- Vulkan layer (hle_vk.m): push constants, descriptor arrays, dual-source
blending variants, copies inside a render pass (cut and resume with deferred
store actions), linear images over their memory, timestamp and occlusion
queries, pipeline caches, vkCmdDraw, image copies and read-backs, USCALED /
SSCALED and three-component vertex formats, viewport and scissor kept across
passes, the INHERIT swapchain transform, the vi layer crop at present.
- vk_shaders.py: ArrayStride on struct members, push constants, array counts,
`_d` dual-source variants, extra SPIR-V input dirs; non-vert/frag stages
skipped with a red line.
- Run-time SPIR-V capture: DM_VK_CAPTURE in the layer, profile key
shader_vk_capture and vk_capture.tsv (3,089 keys), and the NSP script's
Vulkan shader capture stage.
- Handheld pointer: profile key op_mode, touch hooks fill touches only in
handheld mode, InitializeTouchScreen(int,int), DM_KEYS taps, iOS gamepad
forwards untaken touches to the game.
- SleepThread under 20 us takes the yield back-off (Hagi's 100 ns poll kept a
core at 100%).
- nx_extract / NSP script: --allow-damaged for NSPs whose files fail the NCA
hash check.
Hyrule Warriors: Age of Calamity (01002B00111A2000, 1.3.0) reaches its first
battle on the Mac:
- shader_split.py reads Koei Tecmo's RDB / IDRK archives (G2S `_S2G` / `_DOK`).
- Profile key mem_used (DM_GAME_MEM_USED) for svc GetInfo UsedMem; the default
stays 0x4000000 for every other game.
- patch_nso.py: jump tables indexed by x30 / x29 (capstone names them lr / fp);
a per-build cut for AoC's KTSL sound heaps.
- NVN: the compute program is bound apart from the graphics stages (cprog);
nvnTexturePoolRegisterImage; DrawArraysIndirect; reflection c1 on the heap.
- oe copyright-frame calls and the hardware multi-stream Opus decoder answered.
Fire Emblem: Three Houses (010055D009F78000, 1.2.0, SDK 6.5, KTGL) plays its
prologue on the Mac:
- shader_split.py reads LINKDATA / SPKG with KT chunked zlib and keeps GLSLC
outputs with 0x848 / 0x850 controls and headerless compute; 9 run-time GLSLC
compiles are captured once with DM_GLSLC_CAPTURE (hle_glslc.c, profile key
shader_glslc_capture).
- NVN: submissions serialized across the two queues; GetCommandMemoryUsed counts
from AddCommandMemory; per-pool texture / sampler ID ranges; depth clamp
replayed; integer vertex inputs fed half / unorm / snorm / packed formats get
float32 bits converted on the CPU (also fixes AoC's flat soldiers).
- SDK 6: OpenPreselectedUser returns a Result; the audio renderer's DSP command
header layout. DM_KEYS steps run in time order.
NVN indirect paths: quads and triangle fans from DrawArraysIndirect, and
DrawElementsIndirect, go through a GPU compute conversion into indexed indirect
draws; DispatchComputeIndirect is implemented (AoC's battle effects; red line
88 gone).
Metroid Dread GPU leak: defer_drain() runs in its own autorelease pool. Run on
the guest submit thread, whose implicit pool never drains, it kept every
retired object (and its upload staging block) alive; under CPU load Dread
climbed to the 24 GB watchdog. The committed tree had the same leak; it is also
behind Breath of the Wild's memory growth in issue #9.
NVN pass dump diagnostics (TOTK): TEXSNAP copies the attachments after the pass
on the Metal 4 stream; DM_NVN_DEPTHOVR=<fs key prefixes> forces depth ALWAYS;
D lines show the depth range and logic op; recorded clears are logged.
Docs: game notes, bring-up and graphics catalogue entries, the README's list of
games being ported. .gitignore: __pycache__/.
Checked on the Mac with the 9-game smoke test (Wonder, Engage, 13 Sentinels,
3D World, Breath of the Wild, Dread, Link's Awakening, Odyssey, Hades): known
red lines only, screenshots match the earlier runs, Dread flat at 1769 MB to
frame 24300. iPhone builds of the new games are untested.
- README.md: the Chinese half is removed, and so is its link at the top.
- Requirement: an iPhone 15 Pro or newer (A17 Pro or later), or an M1 or later iPad, because the games'
textures are BC-compressed.
- Hades is listed as being ported, next to Super Mario Odyssey.
- The third-party list names the Vulkan headers instead of bcdec.
- The Super Mario 3D World magenta-squares known issue is gone: fixed and confirmed on an iPhone
(feedback_snap).
- AGENTS.md and docs/agents/README.md: everything, the README included, is in English. AGENTS.md also
notes that Hades plays on the Mac through the Vulkan layer.
- README: a gallery with one picture per game (docs/images): iPhone shots
with the virtual gamepad for Mario Wonder, 3D World, BotW and Link's
Awakening; Mac shots (captioned) for Engage, Metroid Dread and 13
Sentinels. Link's Awakening is now listed as in testing on an iPhone.
- scripts/build_ipa_from_nsp.py: a game with no profile is surveyed, then
gets a provisional profile guessed from the survey in
<NSP folder>/<id>_build/profile/<id>/ (outside the repo, reused on the
next run) and a best-effort IPA; --survey-only keeps the old behaviour.
game.py searches DM_GAMES_DIR after dm/games; the host Makefile reaches
such a profile through a symlink; build_ipa.sh finds it through game.py.
zip_store also stores files whose headers say they are compressed.
- copy_t2t_draw: depth-stencil copies now carry the stencil ([[stencil]]
export from an X32_Stencil8 view). Link's Awakening's occlusion pass
tests stencil bit 1 of its quarter-size depth copy, whose stencil was
the new texture's undefined content.
- Link's Awakening: four outdoor shaders with the foliage shader's input
loop failed on the iPhone (Half2 / normalized formats); listed in
shader_facts_manual.tsv. Chain c4995bb3161c.
The prompt mask is a layout capture (panes tagged CaptureOn / A8): an R8
render target mapped (R, 0, 0, 1) holding the blurred alpha in red. Besides
the black shadow (e6a88e12), each prompt bar draws a cream glow (aa209f57)
whose alpha is min(cloud alpha, capture alpha); with only the shadow shader
listed, the glow filled each key's rectangle with the Kumo64 cloud.
alpha_from_red now lists every pixel-shader variant of the layout library
(main's BNSH at 0x1d83000) and tex_alpha_red only converts render targets.
Game work folders now live in <NSP folder>/<id>-work instead of the home
folder: AGENTS.md and the docs say so, and build_ipa.sh requires W and
writes the IPA next to the work folder by default.
BotW: the button-prompt shadow samples an R8 blur whose swizzle (R,0,0,1)
made alpha 1, so every prompt got a solid box. New profile key
alpha_from_red takes alpha from red for the named shader.
Link's Awakening:
- NVN lower-left window origin (negative viewport height, winding flip,
flipped present and raw screenshots)
- texture copies with format change, scaling, mirroring and depth
(copy_t2t_draw); SetCopyImageStride; nvnWindowGetPresentInterval
- oe::PopNotificationMessage, vi SetLayerCrop / DestroyLayer /
CloseDisplay / Finalize, audren P_SIZE 0x40 for SDK 8, hid style-set
update event signalled once on bind
- Vorbis movie audio through stb_vorbis (dm/third_party/stb_vorbis)
- patch_nso jump tables: other index registers, mov copies, masked copies
- Magenta 16x16 blocks in Super Mario 3D World 1-1 (iPhone only): the composite shader 66a71a3081b91b9a
renders into the RGB10A2 image while sampling it in the same pass. That is undefined in Metal: the
M4 Pro reads the old content, the A20 Pro sometimes returns whole 16x16 blocks as (1,0,1). Found with
the new device self-check DM_NVN_MAGSCAN (blocks first appear on that attachment, and still appear
with the suspected particle dropped). fb_prepare ends the pass, copies the attachment level into a
snapshot and binds the snapshot for that draw, only for shaders in the new profile key feedback_snap
(doing it for every shader cost BotW 16.6 -> 36.6 ms). DM_NVN_FB_SNAP=0 / all / <keys> overrides.
- The game language now defaults to English (nn::oe::GetDesiredLanguage returns en-US; DM_LANG=zh-Hans
in Documents/env.txt for Chinese).
- Code comments, docstrings and docs are English only; the zh-CN doc twins are removed. README.md has the
English text followed by the same text in Chinese. Files the shader chain version hashes keep their
bilingual comments (editing them changes every game's chain version).
- Profile 01007EF00011E000 (SDK 20.5.22, NVN 55, main + subsdk0 + sdk): 19,592 shaders, all translated (chain bd3979ea9b3d).
- Loader applies DT_RELR relative relocations (SDK 20; main has 223,994); nvn_slots_got.py and DM_LOADHASH read them too.
- 60 fps: five rewriter cuts lower the GameFrameworkNx dynamic-interval floor from 2 to 1; ksys::VFR scales the
logic time step by the interval (ratio 0.5 at 60 fps, falls back to 30 with ratio 1 when frames run long).
- Black 3D scene: nvnCommandBufferBindTexturesDeferred reads the handles at execution (bh_fresh); the engine
records once and fills the handles later, so the lighting composite sampled texture 0.
- A texture attached at two colour slots is attached once (TBDR tile stores raced, albedo kept scattered tiles).
- Cube textures are 6-layer 2D arrays so tex_fit can bind cube-array views (1,512 shaders sample cube arrays).
- NVN functions other than command recording run on the per-thread host stack (Metal compiled on a small guest stack).
- Address-to-pool lookups use the last hit plus a sorted index (1,760 pools): submit time 21 ms -> 7 ms per frame.
- SDK 20: ARUID hook, audio renderer layout (Span GenerateCommand, new offsets, DeviceSink ID 0x14), nfp events,
VR mode, seven-six-axis sensor, steady clock, launch parameter, stereo vision permission.
- Compressed render targets (BC4 via an RGBA16UI view) are skipped with a red line instead of a Metal abort.
- DM_KEYS takes frame numbers (f2500:A); TEXDUMP saves 2D-array layers; PASSDUMP prints skipped passes.
- NSP script skips empty NSP files and keeps one copy per DLC.
- Profile 010028600EBDA000 (SDK 10.4.0, NVN API 53): 11,080 shaders from Yaz0 SARCs (BNSH, BAHS v9, effects, main).
- Shader key hashes min(0x880, control +0x20) bytes (0x850 / 0x848 controls); other games' tables unchanged.
- Profile keys device_api (NVN API version the game checks) and shared_font (ttf / bfttf at the shared-font address).
- File system hooks run on a per-thread host stack (HLE_FORCE_BIG, hle_bigstack.c): the save thread has 8 KB.
- Washed-out scenes: a one-layer 2D array sampled as texture2d read one value on Metal; tex_fit binds 2D / array / cube views.
- Unwritten colour outputs get a zero write mask; NVN 1D textures are one-row 2D.
- Hooks: InlineKeyboard, settings LanguageCode, ListAllUsers, oe perf / album, nifm SDK 10, prepo session, vi indirect image size;
nvnQueueAcquireTexture, SamplerCompare, debug groups, TextureGetSwizzle / GetStorageClass.
- NSP script builds nx_extract in a temp folder (an apostrophe in the path broke MSBuild silently); DM_KEYS left stick.
- New profile 01003FC01670C000: sdk and subsdk0 are byte-identical to Metroid Dread's; data is six CRI CPKs; the 751 shaders are GLSLC outputs compiled into main (shader_split.py: shader_main_glslc), built as single-stage programs and paired per draw (shader_separable: every vertex shader is paired with a cover of the pixel shader inputs, so vs_pad lets any pair link)
- Host hooks for SDK 11 calls that reach a service: hid::GetMouseState, fs::CleanDirectoryRecursively, fs::EnableGlobalFileDataCache, oe::SetUserInactivityDetectionTimeExtendedUnsafe, time::ToPosixTime, audio::GetAudioOutName
- StandardAllocator::Allocate(size): on this build only, blocks of 64 KB or more are 64 KB aligned; the engine cuts three ring heaps out of a GPU pool at 64 KB alignment relative to its GPU address, and with the host's 4 KB-aligned address the third heap kept base 0 (null write when the prologue enters its first battle)
- NVN: triangle fans and indexed quads, vertex streams 14 and 15 on Metal slots 14 / 15 (Metal assertion buffer index 31), TextureBuilderSetPackagedTextureData copied into the storage at TextureInitialize, InsertDebugMarker as a no-op
- Mario 1.2.1 sets present interval 2 for single frames after a slow one and advances its time step by the interval; frames were shown for one tick and the game jumped ahead on the iPhone. The display link now keeps a frame for its whole period, and the 30 fps target waits until interval 2 has held for a second (Engage, which asks for 1 with its 60 fps cut, is unchanged). Test switch DM_TEST_PRESENT_LAG
- Survey notes: extra modules are supported, GLSLC outputs in main are counted
- Regression: Mario 1.2.1, Engage and Metroid Dread runs show only their known red lines; t_nvn 63/63; t_present_crop / t_mip_extent build again (t_mip_extent's one failure is the same at a9c56fd)
New profile 010093801237C000 (MercurySteam engine, SDK 11.4.3). A new game
runs through the title, the story recap and the opening cutscene into ZDR,
with movies and music. An unsigned IPA builds from the NSP folder; it has
not been tested on a device yet.
Rewriter and loader
- main has a 1.8 GB bss (the engine's static heap): when an appended patch
area would start beyond 112 MB, it goes 32 MB before the module base
(negative PATCH_OFF, IMAGE_SIZE / FILE_IMAGE_LEN). Mac loader, iOS
embedding (no bss in the executable) and QueryMemory handle both layouts;
Mario's and Engage's output is byte-identical.
- Observation gates may sit right after a bl as well as at function entries.
- Used memory counts that bss, so SetHeapSize asks for 1.38 GB, not the pool.
Host
- Hooks: oe::FinishStartupLogo, fs::CommitSaveData / DeleteDirectory (SDK 11
names), hid::detail::SendVibrationValues (vibration graph flush).
- Guest paths resolve "." and ".." (Atk stream paths use ../../).
- Movies: the SDK's own demuxer runs natively; the decoder supports the
native-texture mode (RGBA into the client's NVNtexture, sync signalled),
the D1 destructor, copied client events, MediaCodec-like Flush / Stop, and
an asynchronous input signal after Start.
- Same-core preemption approximation (profile key core_preempt, Dread only)
plus gates that wait for the movie player's request ring: the game frees
objects its higher-priority threads still use, relying on them preempting
the main thread. 12 of 12 runs clean, before about half crashed.
- SleepThread is precise (macOS timer slack made 33 ms sleeps 36.4 ms): the
game's 30 Hz sound thread drives cutscene frames, now 33.3 ms. Mario's
frame-time p95 19.7 -> 18.4 ms.
- DM_KEYS takes <key>*<count>/<interval>.
Graphics
- BindProgram binds per stage: Dread's depth prepass unbinds only the
fragment stage, which used to drop every prepass draw and leave scenes
black. Composite programs give a vertex-only pipeline.
- WriteTexels writes regions in place (the map writes one texel per call);
t_nvn checks 15 regions byte for byte.
- Pass dumps print Z lines for dropped draws.
- shader_split.py reads MSHD containers (1,010 shaders, 659 pairs).
Regression: Mario reaches frame 6900 with only the known red line; Engage
plays its opening movie and battle cutscene.
- The repository is now nxnative (first called smbw-mobile): a general NSP → native app converter, not a Mario port. README title and history lines updated; each game keeps its own app name and bundle ID (Mario's org.smbwmobile.app stays, so installed apps keep their saves)
- No default game: game.load() without DM_GAME, make -C dm/host / dm/gfx/tests without GAME, and build_ipa.sh without GAME stop and list the profiles (title ID and app name). The host Makefile still lets clean and a bare make (libvpx only) through; host unit tests only need GAME when they use the profile
- The NSP script is unchanged in use: it picks the profile from the NSPs' title ID
- Docs (README, AGENTS.md, profile and gfx READMEs, docs/agents) describe the required GAME / DM_GAME. dmsh_chain.py / dmsh_build.py comments still mention the old default: they are part of the chain hash
- dm/games/smbw → dm/games/010015100B514000 (Mario, still the default everywhere), dm/games/fe_engage → dm/games/0100A6301214E000; GAME= / DM_GAME= take the title ID, and game.py lists the profiles (ID and app name) when given an unknown name
- Defaults updated in game.py, the host / host tests / gfx tests Makefiles and build_ipa.sh; workspaces follow as ~/<title ID>-work and NSP builds as <NSP folder>/<title ID>_build/
- Both chain versions are unchanged (Mario 271056163ea2, Engage a295a8035b7b: the hash covers table contents and keys, not the profile name). dmsh_chain.py and dmsh_build.py keep "default smbw" in their comments on purpose, since their source is part of the chain hash and editing them would force a full shader rebuild
- dmsh_chain.h headers regenerated (comment only); README, AGENTS.md, profile READMEs, gfx READMEs and docs/agents use the IDs
- Mario regression t82 clean; Engage boots and mounts its DLC under the new name; t_fs 68/68, t_fs_case 51/51, t_oe 47/47, t_nvn 61/61; iOS builds
- Acknowledgements first: d2r-mobile, Ryujinx, Atmosphere, yuzu, LibHac / hactool, SPIRV-Cross, deko3d and the vendored libraries
- Pack an IPA: requirements, NSP folder layout, terminal examples for Mario and Fire Emblem Engage (version, output, app name, keep-extracted, diagnostic build), Spyder use, what the script does
- Games table lists the two games that pack today and what each NSP folder needs
- Install and play, how it works, porting another game, building by hand, prewarm lists, game notes, tools, license
- AGENTS.md mentions Engage; docs/agents links d2r-mobile on GitHub
- Scripts the user runs now live in scripts/ at the repository root;
build_ipa_from_nsp.py moves there from dm/ios/tools (history kept).
AGENTS.md records the convention.
- It still runs in Spyder (F5, settings block, prompts for an empty
NSP folder or prod.keys). From a terminal it now takes options that
override the settings: <NSP folder>, -k prod.keys, -p version,
-o out.ipa, --app-name, --keep-extracted, --env-file (-h for help).
Without keyboard input a missing value is an error instead of a hung
prompt. Exit codes 0 / 1 / 130 in a terminal; inside IPython (Spyder)
it does not call sys.exit, so the console stays clean.
- The repository root is found by walking up to dm/tools/nx_extract
rather than a fixed depth.
- Checked: -h from another directory; error paths (missing folder, no
terminal input, empty folder, unregistered version) exit 1 with a
clear message; a CLI run on the Engage NSP folder identifies the game,
skips DLC, picks base + update, passes the tool check and stops at
the free-space check; IPython %run under the Spyder interpreter
(prompts, -h, an error) keeps the console alive.
- README, dm/games READMEs and docs/agents point at the new path.
- README: presented as Switch games running natively on Apple
platforms, with Mario as the one supported game. A games table
(Mario playable, Fire Emblem Engage surveyed), component status,
game profiles and how to add a game, the NSP script, generic
step-by-step build commands (profile, workspace and version as
variables), and a Mario section for the per-game notes (1.2.1).
- AGENTS.md: profiles, DM_GAME / GAME, workspaces, build-ID gating,
the chain rule per profile.
- docs/agents: one repository with one profile per title; survey
mode in 02-survey, including the compressed NACP title area.
- dm/gfx README (+ zh-CN): table paths in the profile.
- README: status for v0 / 1.2.1 on the iPhone, update 1.2.1 build
steps, diagnostic env files, finishing the IPA by hand when Xcode
uses a custom build location, app data folders after a reinstall,
prewarm lists (pso_user.tsv / pso_manifest.tsv).
- AGENTS.md: per-build knowledge keyed by build ID, shader_pairs.tsv on
the chain, unsigned IPAs only.
- docs/agents: update extraction and offset porting (01), the
command-buffer byte, command memory, iOS audio, update imports and
build-gated D2R code (03), vertex output padding and iOS math mode
(04), on-device diagnostics and the offline link check (05).
- dm/gfx README / nvn_metal_mapping (en + zh-CN): pairs table, several
game versions, prewarm lists, DM_PSO_USER, command buffer notes;
corrected shader key, build script and 8-bit index lines.
- dm/host/README(.zh-CN).md removed: an index of d2r-mobile's D2R
features; most files it listed no longer exist.
Shaders
- shader_split.py (new): scan RomFS and main for every BNSH and agl BAHS
container (bfsha, particle effects, layouts, agl/gsys packs, main):
12,811 shaders, map + compute facts from the control section
- key = sha256(control + code): many shaders share a control section
- NVN 55 binding layout: bound textures at 0x20 + 8*slot, images 0x120
- bindless textures read from uniform buffers (tex_ubo / samp_ubo)
- GL depth mode: translator remaps z' = z/2 + w/2 in vertex shaders
- compute code areas carry a 0x30 header (magic 0x12345679)
Replay
- 8-bit index buffers widened to 16-bit
- texture swizzle through Metal swizzled views
- non-packaged textures shared by storage (fixes a ~13 MB/frame leak
from transient render targets rebuilt every frame)
- view_t packed to 16 bytes (SDK 16 NVNtextureView; fixes a stack smash)
- TBDR optimizations off by default until they know bindless readers
Host
- alias region 2.875 GB for the 1.75 GB VirtualAddressMemory reservation
- ideal core -2 resolves to the process default core
- shared font served with a BFTTF "decoded" header
- crash reports scan the stack and dump the popped frame
Docs: docs/agents (guides for porting other titles), AGENTS.md