About 1 run in 3 crashed about 3 s after Start (operator new at main+0x155a79c
returned null, written through at main+0x155a7a4). Each model file gets a heap
of its resource size table entry + 0x1008; the file goes in 4 KB aligned and
the model's setup (main+0x155a2ac) takes the rest as a frame heap. Part of the
setup is GPU memory for the model's prerecorded commands, which the game sizes
by recording each mesh's draw and each shape's state for each render pass into
a scratch command buffer and summing nvnCommandBufferGetCommandMemoryUsed. So
our record sizes count against the table: draw records had grown to carry the
indirect and multi-draw fields (0x38 -> 0x50 per recorded draw on 10-18 and
10-20), and every recording began with an 8-byte BEGIN record. Of the 497
models of the world load, Obj_TreeConiferous, Obj_TreeBroadleaf_B and
FldObj_HyliaBridge_A-02 needed one 4 KB page more than their entries allow,
which fit only when the file's alignment pad was small (about 15% of loads each
failed); the others had about 0xd00 to spare for any pad.
- Plain draws leave out the indirect / multi-draw fields (hle_nvn.c rec_draw);
the replay reads draw records through draw_rec / draw_idx_rec (nvn_obj.h).
- A recording starts with its first command, as in NVN: no BEGIN record, the
handle is the first record, an empty recording takes only its END.
- 0x30 per recorded draw now; all 510 models of the world load fit whatever
the pad (probe check in docs/agents/03-bringup.md). World-load A/B on the Mac:
main 2 crashes of 8 runs, this 0 of 8. Super Mario Bros. Wonder (opening
course), Age of Calamity (first battle), Monster Hunter Rise and Three Houses
boots behave as on main. t_nvn 74/74 (6 new checks on recording sizes).
- DM_PROBE heap walk: layouts of other sead versions (DM_PROBE_HEAP), frame
heaps (DM_PROBE_HEAPFRAME), a heap-name filter (DM_PROBE_HEAPNAME), block
contents (DM_PROBE_HEAPWORDS), the raw heap object (DM_PROBE_HEAPRAW); each
hit prints as one block.
Mario's +16 KB resource-heap cut (a model 0x70 bytes short) may have the same
cause; not checked, so the cut stays.
Outdoors in Breath of the Wild (the user's Great Plateau save, Mac M4 Pro) a frame had about 386
render passes and loaded and stored about 900 MB of attachments, which an iPhone at 60 fps can
hardly move (about 55 GB/s). 220 of those passes ended only because the game updates the first
64 bytes of a 64 KB vertex uniform block before each draw group (nvnCommandBufferUpdateUniformBuffer
inside the pass) while its shaders read up to 544 bytes of it. A deferred update that covers only
part of what a draw reads used to end the pass.
- Partly covered blocks (nvn_mtl.m ubp_get): the shadow starts from memory and takes every inline
update queued in the submission, newest last, incrementally per range. It is refused, and the
pass ends as before, when something else may have written the range by the time the pass runs:
a copy, fill or feedback write queued in the submission, a storage buffer a shader may write or
a texture->buffer copy over it (sbw, by 4 KB page), or a GPU write or counter report of an
earlier submission still running (gw). A counter report of the same submission does not count:
the host writes reports on the CPU after the whole submission completes (Fire Emblem Engage
writes its reports into a uniform block's page). DM_NVN_UB_PARTIAL=0 turns it off;
DM_NVN_UBP_CHECK=1 lands the updates, waits for the GPU and compares every shadow with memory.
- cb_extent.tsv (new tool dm/gfx/tools/cb_extent.py, c1_guard.py's interval arithmetic over the
translated MSL): per shader, how far into each uniform slot it can read, and the storage slots
it never writes. Like c1_full.tsv, the host loads it only for its chain version; build_ipa.sh
copies it, the NSP script writes it for provisional profiles, and the chain procedure
regenerates it. Without it bindings are copied whole (up to 64 KB). Ten profiles have one.
- Route B's reflection never says whether a storage buffer is written, so every one read as
read-only and its writes went unnoticed: a storage buffer now counts as written unless
SPIRV-Cross typed it const (nvn_refl.m sbuf_rw) or cb_extent.tsv proves the slot unwritten.
- Lazy clears (lc_*): a set of targets that gets only clears before the game sets other targets
no longer opens a pass just to store them; the next pass that attaches the same texture level
and layer through the same view clears it as its load action, and the rest land as before
before anything else could see them. Breath of the Wild's four clear-only passes before the
G-buffer fold into its first pass. Off in TBDR mode; DM_NVN_LAZY_CLEAR=0 turns it off.
- The residency set line counts shadows from memory, refusals by reason, and lazy clears.
On the Mac, old (DM_NVN_UB_PARTIAL=0 DM_NVN_LAZY_CLEAR=0) and new side by side on each game's
route; attachment loads + stores per frame:
- Fire Emblem Engage battles: about 1,850 -> 360 MB, 88 -> 38 passes.
- Tears of the Kingdom: about 1,100 -> 700 MB, 124 -> 116 passes.
- Breath of the Wild outdoors: 386 -> about 158 passes, GPU p50 about 9.3 -> 6.5 ms, submit
5.7 -> 4.2-4.9 ms; where the old host fell to 30 or 20 fps under load, the new held 60.
- Link's Awakening, 13 Sentinels: about 15% less. Super Mario 3D World: 5% less (lazy clears).
- Odyssey, Three Houses: 2-8% less. Mario Wonder, Metroid Dread: unchanged.
No new red lines in any of them. The shadow check found every shadow equal to the GPU's memory
(BotW 346,000, Engage 284,000, TOTK 86,000), and t_nvn passes 68/68. Screenshots differ only by
timing. Tried and dropped: DM_NVN_TBDR=1 (it turns the deferred updates off), DM_NVN_BARRIER=game
(no gain), and same-target continuation outside TBDR mode (BotW's decals end the pass anyway).
What still splits BotW's big passes: the GPU particles' compute dispatches (about 190 MB a frame)
and decals that sample the G-buffer. Mac shader libraries were built for the current chain of
TOTK, Engage, Wonder, Dread, Odyssey, Link's Awakening, Three Houses and 13 Sentinels (c1_full.tsv
headers already matched). The IPA ZeldaBreathOfTheWild-2026-10-21-passes.ipa (an earlier state of
these changes plus the uncommitted frame-step probes) awaits an iPhone test.
The game steps a fixed amount per update, from three 30 fps values, and its updates are not held
by the presents:
- A KIDS task (main+0x166d980) waits until 1 / fps has passed and steps the game clock by a
frame-rate object's 1/30 s (the measured path runs only without the object).
- The battle steps 1 / the game's frame-rate block (main+0x269e150: {30, interval 2, 30, 30.0}),
read by the scene update's caller (main+0x8c3610) and its jobs; found with a sample profile.
- The event scripts step by the KTGL device's frame rate, 59.94 / the interval in its config.
The 60 fps cuts (d975e2e) changed only the task's wait and the interval, and were checked only on
cutscenes. DM_PROBE showed the game clock at 2.00 x real time, and battles ran twice as fast on an
iPhone. Fixing the clock alone was not enough: Link still covered twice the ground in a 3 s stick
hold, and the enemy waves came early.
AoC 1.3.0 main joins the rewriter's frame step (patch_nso.py FRAME_STEP), with three stubs. The
updates run at the cap: the task's fps and the frame-rate block become the cap; the object's step
is scaled by its fps x vsyncs per update / 60; the device frame rate becomes 59.94 / vsyncs; and
nnMain's device config gets interval 1 above a 30 fps cap. They replace the two CUTS entries.
Every step stays per update, as on the Switch. The host paces the presents: 60 fps, or 30 when 60
does not hold. Profile fps_target 60; DM_FPS_TARGET=30 runs at 30, and 0 is the Switch original.
On the Mac:
- Game time follows real time at 60 and 30.
- After the same stick hold at the same wall time, Link stops on the same spot at 60 fps as in
the 30 fps original.
- The battle loads at about 133 s in every run.
- The forced 30 matches the original.
Two variants were tried and dropped. Stepping by the measured time still ran battles at twice the
speed. A task wait that follows the step locked the hold at 3 vsyncs.
AoC's shader libraries were rebuilt for chain a9687d01ebfa (Mac and iOS); c1_full.tsv is
regenerated with the same list. The IPA HyruleWarriorsAgeOfCalamity-2026-10-21-speed.ipa, built
from 89b608d plus these changes, awaits an iPhone test.
The app shows the frames put on screen each second in the top row (front_ios.m, from mtl_present_count, so NVN and Vulkan games alike).
build_ipa.sh's DM_SHOW_FPS=1 sets DMShowFPS in Info.plist; DM_SHOW_FPS=0 / 1 in Documents/env.txt overrides it without a rebuild.
- Asked for 60 Hz, the system fired the display link on every third refresh of a 165 Hz screen (55 Hz), so games paced by it ran at 55 fps. On screens whose rate is not a multiple of the link's, link_range (nvn_mtl.m) now runs the link at the screen's rate and counts as ticks only the refreshes nearest a steady 1/60 s clock; DM_LINK_VTICK=0 keeps the system's choice. front_screen_max_fps (front.h, front_mac.m) gives the window's screen's rate.
- A tick lies up to half a refresh off its slot, so there the hold of frames over 25 ms tolerates one refresh more. Without it, 30 fps frames kept a third tick: 22.5 fps at 165 Hz and 24 at 144 in a simulation of the callback. This part is not in the PR.
- The frame step (nvn_fstep.h shown_slack, hle_nvn.c fstep_tick) lets a frame on time show one refresh longer than its hold on such screens: a cheap 50 ms frame shows 54.5 ms at 165 Hz, and counted as late it kept the hold at 20 fps for good (Breath of the Wild, Tears of the Kingdom). Three new t_fstep checks.
- Not taken from the PR: the responsive pacing mode, its uniform-buffer shadows (redone separately) and the Breath of the Wild resource-size-table overlay. docs/development.md records what the PR found about the world-load crash (the Obj_TreeConiferous resource heap) and the overlay runs; docs/agents/04-graphics.md describes the ticks.
- Not yet run on a 144 or 165 Hz screen here (this Mac's is 120 Hz, where nothing changes); iOS screens are not affected.
Co-authored-by: Yuecheng Liao <128268702+lyccccy@users.noreply.github.com>
No host change was needed. The profile reports 84 MB of used memory (mem_used): the rewritten
modules take 80 MB, and with the default 64 MB SetHeapSize fails and the game aborts at its
first allocation. 3,118 shaders, every binding decoded; prewarm list recorded on the Mac
(boot, title, storybook, Elder Kettle's house, Inkwell Isle One, Forest Follies).
README, AGENTS.md and the game notes list it as playable on an iPhone.
A touch that lands in the right third of the screen on no control holds the
right stick, centred where it lands; past the stick radius the centre is
dragged along, so turning back responds at once. Controls in that third
(A/B/X/Y, ZR, R, R3) keep their touches; in Super Mario Galaxy the pointer
keeps the other unclaimed touches. Only the left stick is drawn now, so a
right-stick swipe redraws nothing.
The NVN driver's constant segments have the same layout in every game, so the
nvn_* profile keys are gone. Combined textures are at c2 0x20 + 8 r (a slot past
31 shares the image table; Hades II binds 34), images at 0x120, separate textures
at 0x168 (now always the host's slots 128 + n, as Monster Hunter Rise had them),
samplers at 0x568, and the storage buffer tables at c0 0x110 / 0x310 / 0x510.
dmsh_build.py decodes with that layout, the replay fills both texture tables,
and the translator numbers slots the same way (rebuilt; its SPIR-V and
reflection are byte-identical on 1,491 sample shaders, lock updated).
The shader_* switches and graphics are gone too; the build reads them from the
files (profile_guess.py shader_sources):
- Vulkan when main imports Vulkan and not NVN; its capture when the RomFS has
no SPIR-V.
- A GLSLC capture when main holds GLSL sources and imports glslcCompile.
- An Oodle decode for Unreal paks, and the tool-mode decoder for Trinity packs.
- GLSLC outputs from every main.
- A cover pairing for each single-stage vertex shader (GLSLC, Unreal).
build_ipa.sh takes GFXAPI from the NSP script.
Also removed: the last D2R pieces (the embedded-shader path in dmsh_build.py,
dmsh_facts.py --freeze / --check, maxwell_refs.py, ref/hle_hid_d2r.c).
Chain: every profile's dmsh_chain.h is regenerated. The c1_full lists are
carried over: the change touches no c1 read, and the rebuilt Octopath
Traveler 0, Hades II, Unicorn Overlord and Rhythm Heaven Groove gave the same
lists, identical tables and the same reflection apart from separate texture
slots + 128.
Still to do:
- Run the chain procedure for the other NVN profiles.
- Engage's and Three Houses' shader tables need a new split: Engage gains
Unity's 11 GLSLC shaders in main, and Three Houses' 17 BNSH vertex shaders
lose their cover pairs.
- Ace Attorney's new tables are in, but its libraries are not rebuilt.
- Profile 0100F43008C44000 (2.0.2, SDK 19.3.11): 5 GB layout with two cuts (StandardAllocator 12 → 4 GB, node area), mem_map_limit (the pool enforced as on Horizon), shader_oodle.
- Shaders: Trinity packs (arc/data.trpfs, FlatBuffers, Oodle Mermaid) decoded with the game's own OodleLZ_Decompress in the Mac host; 28,603 shaders.
- SDK 19: GetAudioOutBufferCount, crypto random bytes, ToPosixTime with a rule, NVN state getters, CommandBufferFenceSync, the 64-bit WaitForAddress type.
- Graphics: transform feedback (vertex capture entry point, rasterizer discard), layered passes (the colour grading LUT), point size dropped beside a layer output,
function constants specialized, a stencil-only view for STENCIL-mode texture views (striped lighting), sampler min / max reduction (Metal 26),
the draw's base instance at c0 + 0x644 (occlusion boxes; characters' legs folded back from the hips), the zero cube for a 2D texture in a cube slot.
- Shader chain changed (dmsh_build.py): every profile's dmsh_chain.h rewritten; the other profiles' c1_full lists carried over (that change touches no c1 read).
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).
On an iPhone it plays at 30 fps through character creation, name entry and the story opening, with no audio underruns and three saves (a footprint of about 2.35 GB after 3.4 minutes). It joins the table with a screenshot of the castle in the snow; AGENTS.md lists it with the nineteen games and a sentence on its Unreal Engine 5 port; the game notes give the iPhone status.
While swkbd::ShowKeyboard was up the host froze the guest clock (d2r-mobile's choice, so that monsters did not attack
while the player typed). Unreal calls it off its game thread, so the game kept running, and every thread that waits
on the clock spun while its audio mixing starved: on an iPhone the sound stuttered and the input box lagged (one
4.7 s frame, 5,035 wakeups a second while the box was up instead of about 1,800, no audio underruns). On the Switch
the applet blocks only the caller while the clock runs, which the host now does too. The test switch
DM_TEST_SWKBD_HOLD_MS (with DM_KBD) holds the keyboard up on the Mac: frozen, 8.8 fps and 7,936 wakeups a second in
that window; running, 30 fps and 1,788. The inline keyboard keeps the freeze: its game (Monster Hunter Rise) does
not block on hardware, and the host's input box blocks it.
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.
Unicorn Overlord 1.05 (SDK 16.2.4, Vanillaware's engine as in 13 Sentinels) plays from boot through the title screen
into the opening story scene. Three boots, three fixes:
- the hardware multi-stream Opus decoder's C interface (CRI's Opus streams; the size query takes the SDK 12+ option
argument) goes to the sdk's software OpusMultiStreamDecoder, as the single-stream one does (hle_codec.c);
- nn::crypto::GenerateCryptographicallyRandomBytes, which the sdk's own getentropy calls under std::random_device,
fills from the host's entropy stream instead of opening csrng (hle_os.c);
- 13 Sentinels' 64 KB alignment of large StandardAllocator blocks becomes the profile key alloc_big_align. The engine
cuts ring heaps out of a memory pool at 64 KB GPU alignment; with 4 KB aligned pool storage one kept base 0, and the
game wrote to 0x268 right after the title screen. 13 Sentinels sets the key (its profile lists only the 1.10 build the
old check named), so its behaviour is unchanged.
The profile (dm/games/010069401ADB8000) holds 1,725 shaders from 2,188 GLSLC outputs in main, Mario's binding layout
with every binding decoded, translation chain b-239c170b7616.
The IPA built from the Hades II profile plays on an iPhone. README lists it (~4 GB) with a screenshot from the Mac
(Erebus, the first room of a run); AGENTS.md counts sixteen games and notes its texture slots up to 34 and the staff's
Omega aim lines that look like a rendering bug; the game notes drop "not tested on an iPhone".
New profile 0100A00019DE0000 (base game v0, display version 0.0.126424, SDK 19.3.9), from the NSP script's provisional
profile. Unlike Hades it renders with NVN: 88 BNSH shaders in Shaders/NVN/bin; the ExeFS holds only main and sdk.
- Shaders bind textures at fixed, sparse slots (0, 1, 3, …, 31 and 34): SceneFade.frag reads c2 0x130 = 0x20 + 8 x 34,
past Mario's 32 slots, so dmsh_build left that binding undecoded. nvn_tex_slots 35; the game has no image bindings.
- hle_ro.c: SDK 19's LoadModule takes one more bool (…mib), which the older hook did not catch; it and ro::Finalize
are hooked. The Epic Online Services NRO stays offline as in Hades (RegisterModuleInfo fails).
- Memory: no system resource; the 3.10 GB heap fits the 4 GB layout; main's 82 MB bss gives mem_used 108 MB.
- nvn_mtl.m: DM_NVN_HILITE set its keys and library up lazily on whichever thread built a pipeline first; with several
keys two threads raced and one used a library the other had replaced (objc_msgSend crash in pso_desc). Set up once.
On the Mac: title, a new save, the opening and Erebus with combat at 60 fps, resident memory flat at about 3.4 GB,
one red line (the NRR). The two long lines through Melinoë while the staff's Omega attack charges are the game's own
aim lines (WeaponStaffSwing5: AimLineCountOverride 2, AimLineAngleOffsetOverride 180), traced with vertex dumps and
probes; docs/development.md says so. IPA HadesII.ipa (4.0 GB) built; awaits an iPhone test.
The IPA built from b1cd440 plays on an iPhone. README lists it (~7 GB) with a screenshot from the Mac
(Edgeworth's office, the first investigation); AGENTS.md counts fifteen games and says where its Unity 2022 shader and
file-API handling live; the game notes and the profile header drop "not tested on an iPhone".
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>
60 fps (30 on the Switch), two host-only cuts on 1.3.0 main (patch_nso.py):
- nnMain sets KTGL's device up with present interval 2 (main+0x8d94b0), and the device's frame rate, 59.94 / interval,
is what its event scripts step by (main+0x7802c4). The config interval becomes 1, the engine's own 60 fps mode.
- A KIDS task waits until 1 / fps has passed since the last frame (fps 30 from engine data) and then steps the game by
the time that really passed. Its wait becomes 1/60 s (main+0x166db80).
On the Mac the opening scenes at frame 2N match the original's frame N (same subtitle, same poses) and hold 60.
Battles ran at 25.8 fps at a 30 or a 60 fps target: the replay converted the soldiers' Half2 / Short2 attributes (fed
to integer shader inputs) on the CPU over whole bound mesh pools, 4.4 million vertices per submission at about twelve
submissions a frame, 24 of the 32 ms the replay took. In nvn_mtl.m:
- Profile key vconv_range (Age of Calamity on): only the vertices each draw reads (first / count, or the indices'
smallest and largest, kept while the index pages stay unwritten and dropped once a GPU write is queued on them),
bound as many vertices before the conversion as the range starts at. 219,000 vertices per submission;
DM_NVN_VCONV_CHECK=1 found no stale index range in 983,000 reuses. Keeping the conversions across frames did not
pay: the soldiers' attributes change every frame.
- vconv_fill: loops of their own for the 8 / 16-bit integer and the half formats (byte-identical to vconv_elem for
every format in an offline test; affects every game that converts).
- The visibility buffer ring is as deep as the command allocator pool (8, was 4): the submit thread waited for the
previous frame's GPU work.
First battle on the Mac (M4 Pro): 58.6 fps, frame p50 16.9 ms, replay CPU 10 ms, GPU 11 ms. On an iPhone the GPU will
likely decide in battle; the game steps by measured time, so it keeps normal speed below 60.
IPA HyruleWarriorsAgeOfCalamity-2026-10-20-60fps.ipa built from HEAD + these changes; awaits an iPhone test.
A stub sets the scale where the game's dynamic resolution applies its own (main+0x1266880), so the scene buffers, the
shader's resolution ratio and the LOD check all follow it; a second stub takes that apply block every frame when the
factor is not 1.0 (the game only runs it after recomputing its scale). 1.0, the default, keeps the game's dynamic
resolution. Mac: at 0.5 every scene pass halves while the shadow maps, the 1920x1080 output and the HUD keep their
size; handheld works too (960x540 at 0.75). The Mac's GPU time does not change (about 650 passes a frame, fixed cost
first); the iPhone test decides whether it helps there.
The host decides each frame's step (1 to 3 vsyncs) from its own measurements (nvn_fstep.h, hle_nvn.c fstep_tick),
keeps the frame on screen that long and pays back real time lost to late frames one vsync per frame: even motion,
game time = real time, and 30 fps (not 40) when 60 does not hold. Tears of the Kingdom's frame-step stub writes
60 / step; Breath of the Wild gets a stub at the framework's setWaitVsyncInterval, so its VFR and every reader of the
framework's interval take the host's step, and its VFR override is published so the host holds it.
DM_FRAME_STEP=legacy (profile key frame_step) keeps each game's earlier method for the iPhone A/B test.
Mac: both games at 1.000 of real time at 60, and at 30 with 12 ms added to every frame. The docs record a
pre-existing intermittent Breath of the Wild crash at the world load (legacy too).
- hle_fpslock.c: on every present the host writes the profile's fps_target into RE Engine's frame limit at [main+0x1295b3f0]+0x434 (FPSLocker's address for build c9a3dd77; the game sets 30 after boot). Its frame loop (main+0x6b106c8) waits until [+0x430]/[+0x434] sixtieths of a second have passed and steps by the time that really passed.
- The game also asks for present interval 2: the profile's present_interval display ignores it. Both are needed; either alone stays at 30.
- Profile: fps_target 60, present_interval display; DM_FPS_TARGET=30 gives back 30. Mac: frame time p50 16.67 ms.
- patch_nso.py FRAME_STEP_BUILD (main 277178b7, 1.4.3): stubs step the game by the measured frame time instead of its own interval (2 = 30 fps), capped at the target (30 while a prerendered movie plays), with game time that ran ahead paid back by the next frames. Also the two readers of the fps, the D-pad menu fade and the swapchain argument. Sites from masagrator's FPSLocker config for this build; the pay-back from OJZen/totk-mobile; stubs written and assembled here.
- State page after the key page (fixups key FPS_STATE_OFF): the loader writes the target (profile key fps_target, DM_FPS_TARGET; 0 = every stub runs the original instructions) and the render scale (DM_RENDER_SCALE 0.5-1.0; any factor but 1 fixes the size and drops the game's dynamic resolution).
- hle_nvn.c paces presents by the cap instead of the game's interval and logs game time against real time every 1,800 presents.
- TOTK profile: fps_target 60. Mac (M4 Pro): 60 presents a second in the opening cave, game time 1.000 of real time; at 30 and 0 as before.
- dm/ios/tools/pack_ipa.py replaces the Payload/ copy and Info-ZIP: it packs the app, the shader
libraries, the add-on content and the RomFS straight from their folders, reading and deflating on
all cores (zip64, modes, symlinks, the profile's zip_store as before). Metroid Dread 156 s -> 12 s,
Breath of the Wild 171 s -> 57 s (the T7 writes at about 310 MB/s).
- The shader libraries no longer go through Xcode (Gen/msl left the project): no 1.5 GB detour
through the home disk.
- NSP script: the rewrite, the shader split and the iOS / Vulkan shaders are reused when a sha256
of their inputs matches the one stored beside their output; the modules are rewritten in
parallel; J = the core count; the extraction is kept by default (--no-keep-extracted).
A Metroid Dread repeat build: about 8 min -> 19 s.
- Profile 0100B04011742000 (update 16.0.2, RE Engine): KPKA PAK shaders (shader_split.py kpka_jobs), XCI base game in nx_extract and the NSP script, BIG_TEXT patch area in patch_nso.py.
- Sparse virtual pool: a 4 KB page table (vpt_*) that the replay reads textures through, dirty tracking per mapped page, texture views from the first mapped level.
- MultiDraw*IndirectCount with a CPU wait for the count; pending GPU writes indexed by page; bindless handles read from queued inline updates.
- sbuf_ubo binding kind, integer vertex formats into float inputs, clears on textures that are not render targets.
- Inline keyboard text, state and finished callback; ngc::ProfanityFilter host vtable.
- Ryujinx BindlessElimination follows phi webs (218 MHR pixel shaders); dmsh_xlate_bin and dmsh_chain.lock updated; dmsh_chain.h and c1_full.tsv rebuilt for every Maxwell profile.
- README: Monster Hunter Rise under being ported, gallery image, XCI base games, Ryujinx local fixes.
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.
The route from a new game into the prologue battle counted presented frames at 30 fps. At 60 fps its
frame numbers double, and the repeating confirm step becomes Left, A, Left, A: A, Left, A loops forever
once it starts on an open dialog (A answers No, Left changes the selection, A reopens it), which
happened at character creation because A presses during the opening movie pause it and shift the
scenes after it. The new route reaches the battle in about 7 minutes. The DM_KEYS example in
05-debugging uses the same cycle.
The render thread forced present interval 2 while either of two flags was set: one for the full-screen
movie image (the opening movie), one from the scene object (the character creation screen after it, and
similar scenes). The engine's own 60 fps modes dropped to 30 there to keep 1080p; with the per-mode
tables at 1 it would also run the render thread's own steps at half speed. A cut at main+0x50fb7c
ignores both. Mac, new game: 59.9 fps through the movie and character creation; the movie lasts
210.6 s in both the original and the 60 fps build.
KTGL already updates the game 60 times a second on its main thread (a fixed 1/60 s step); the render
thread presents the latest update with the render mode's interval, 2 in the docked and handheld modes.
Per-build cuts on 1.2.0 main (patch_nso.py) set the per-mode interval tables to 1, as in the engine's
own unused 60 fps modes, so every update is shown at the same game speed. Two more cuts make the waits
cheap on the host: the frame limiter's yield-and-poll loop sleeps the time left (it spun 2 ms a frame
and ran at 59.5 updates a second), and the render thread's wait for the next update gets one frame
instead of 16 µs (a millisecond count passed to a microsecond wait; about 50,000 wakeups a second).
Mac, prologue battle: 59.9 fps, 55% of a core, 1.8 W of CPU energy (the original at 30 fps: 44%,
1.7 W). Not yet tested on an iPhone.
- 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.
The back-off (each yield sleeps an eighth of the run so far, 10-250 µs) started after 200 µs of consecutive yields. That also caught TOTK's main thread waiting for its ModuleSystem workers, about 350 yields a frame in runs of a few hundred µs on the frame's critical path: a sleep that overran the workers' finish pushed frames past 33 ms, and the game then drops itself to present interval 3 (20 fps) for a while. Mac, two runs side by side: 9 drops in 300 s and frame p95 41.7 ms with the back-off, 0 drops and 34.4 ms without, at the same CPU time. On the iPhone frame p95 was 87-91 ms and the threads stayed on efficiency cores at 1.5 GHz.
Waits under 2 ms are handoffs between working threads; limiters and idle polls run far longer. With the 2 ms start TOTK's main thread and busiest worker never back off (0 drops, 30.00 fps, as with the back-off off), and Three Houses' 23 ms limiter wait still does: CPU 78% of a core without the back-off, 19% with it, 14% with the old start, same frame times. DM_YIELD_BACKOFF=<cap µs>[:<start µs>] sets both (default 250:2000).
- hle_nvn.c power_line: the 驻留集 line ends with attachment bytes loaded and stored per frame, passes that load and store everything, and the stores DM_NVN_UNWRITTEN_DC saved (mtl_bw, so far only in the per-frame TSV, which phone runs do not send). A new 线程 line gives guest-thread CPU in ms per second by priority band (<= 44, 45-59, >= 60) and the three busiest threads (dm_thread_cpu in hle_thread.c).
- hle_thread.c DM_QOS_MAP=1 (DM_QOS_EDGES=44,59): priority <= 44 user-interactive, 45-59 user-initiated, above utility, at thread start and on SetThreadPriority (applied by the thread itself at its next SVC). A thread waiting in ArbitrateLock lends its class to the owner until the hand-off (pthread_override_qos_class_start_np). Host helper threads then set their own class instead of inheriting the creating guest thread's: AudioOut notify, silent sink and vsync user-interactive, movie decode user-initiated.
Measured on the Mac: BotW runs 527 ms/s of guest CPU at priority <= 44 and 65 at 45-59, Mario Wonder 138 and 4.5, Three Houses all at <= 44, so the map has little to move; it stays off for a phone A/B. The same runs settle the TBDR question: DM_NVN_TBDR=1 cuts BotW's stores 13% and Wonder's 42% but raises GPU p50 (8.8 → 10.4 ms, 2.8 → 3.5 ms) and CPU energy (+22%, +73%).
General host changes so the games stop doing work the Switch never did; frames and frame rate unchanged
(Mac A/B with the same routes, 11 games, no crash, red lines as before; dm/gfx/tests results as HEAD).
- Yield back-off: a run of SleepThread(0) with no other SVC sleeps an eighth of the wait after 200 us
(10-250 us; DM_YIELD_BACKOFF). Three Houses' frame limiter yielded 228k times a frame on the iPhone;
Mac CPU energy 3.5 W -> 0.5 W. BotW's PlacementMgr / RadarMgr spun the same way.
- SVC exit takes the global lock only while a pause is pending; a signalled event wakes only its
waiters; the audio renderer and upload / QueueFinish waits block instead of polling.
- Upload staging: each upload slot keeps its block (BotW allocated, made resident and freed a 32 MB
block per re-upload, ~3 GB/s); a re-uploaded texture alternates with a kept spare (movie frames,
Link's Awakening title: submit 1.64 -> 0.37 ms); the residency set skips repeated adds / removals;
copy parameters from the frame arena; host pre-writes unprotect page runs with one mprotect.
- Replay: per-thread memory-pool address cache, per-encoder state filter, GPU-only texture IDs stay
cached across guest writes, kept dirty-range handle per packaged texture. Instructions per draw:
TOTK -40%, Dread -32%, Odyssey -19%, 13 Sentinels -20%; replay kernel time BotW 1.3 -> 0.1 ms.
- Inline uniform updates inside a render pass are deferred to the pass end; later draws read a
shadow made of the deferred updates when they cover the bound range, else the pass ends first
(DM_NVN_UB_DEFER=0). Dread: 272 -> 26 passes a frame, GPU 8.3 -> 2.8 ms, CPU energy 1.2 -> 0.4 W.
- Whole-level format-changing copies do not load the destination.
- Measurement: [nvn] 功耗 (CPU energy W and mJ/frame, CPU, P-core share, wakeups, thermal state, top
yielders) and [nvn] 驻留集 (residency churn by source line, pass barriers, cuts, pass splits) every
300 frames; iOS thermal state and brightness. Opt-in switches for A/B: DM_RT=0, DM_NVN_BARRIER=game.
Docs: development.md Power section, 04 section 7 switches, 05 power lines, dm/gfx/README fixes.
Hades, Hyrule Warriors: Age of Calamity and Fire Emblem: Three Houses move into the Games table and
the screenshot gallery (Mac captures); Tears of the Kingdom is listed as being ported. The NSP script
now handles a graphics vulkan profile: vk_shaders.py translates the SPIR-V shaders for the Mac and iOS
in place of the NVN shader split and chain build (the change from an uncommitted worktree, ported to
the current script; Hades packed with it: 74 shaders, 3.6 GB IPA). AGENTS.md and the development
notes follow.
Fire Emblem: Three Houses' KTGL loader calls nvnQueueFinish after each resource upload, and mtl_finish
flushed the present queue first: with a window that waits for the display link to take every queued
present, one or two display frames per call. QueueFinish now waits for the submitted GPU work only, as
on the Switch; AcquireTexture still waits for a texture's previous present, and mtl_drain keeps its
flush. Windowed Mac run: first load 6.1 s, frame pacing unchanged.
nvnTextureCompare and nvnTextureGetSwizzle share tex_swz. The BotW docs
keep the capture investigation, now told from the cause (#6). Odyssey's
layouts go through the same compare-and-replace code (main+0x982320):
with the fix its alpha_from_red list converts no texture on the Mac
smoke route to frame 20,100 (main's host did from frame 4129) and the
HUD is pixel-identical; the list stays until an iPhone run.
- Ryujinx (vendored, Instructions.cs GenerateTextureSample): texel offsets read from a
register are built with OpCompositeConstruct + Offset, and a four-offset gather becomes
four one-offset gathers. Upstream wrote an OpConstantComposite, invalid SPIR-V that
SPIRV-Cross refused (2 shaders each in Wonder, Link's Awakening, TOTK, Age of Calamity).
- dmsh_xlate: a shader whose SPIR-V declares a 64-bit float is translated again with
Ryujinx's DoubleToFloat pass (30 TOTK main-material vertex shaders); TOTK's
visibility-counter vertex shader gets MacNativeGames' subgroup lowering (MIT).
Every other shader's SPIR-V is byte-identical.
- New dm/gfx/tools/dmsh_mtlcache.py: dmsh_build.py compiles each MSL through a cache keyed
by the source, the compile flags and the compiler (<cache>/cas/metallib/). Before a build
it is filled from the earlier b-*/ folders beside the output and IOSOUT, and from
DMSH_PREFILL, once a fresh compile of a few of a folder's shaders reproduces their
libraries byte for byte. A chain change that leaves a shader's MSL unchanged no longer
recompiles it.
- dmsh_build.py clones files with the clonefile call instead of one cp per file: each fork
of the build process cost about 6 ms and serialized the finishing phase.
- TOTK's rebuild after the translator fix: 49 min -> 9 min, libraries byte-identical to
the previous build apart from the 39 fixed shaders. All 12 chain versions are new;
Dread, 13 Sentinels, Three Houses, TOTK and Wonder are rebuilt (Mac + iOS + c1_full.tsv),
the other six follow.
Tears of the Kingdom (two profile keys, off for every other game):
- render_cond: NVN conditional rendering is evaluated at replay from the
timestamp reports' counter 0; before, every conditional clear ran and the
lighting passes drew nothing.
- tex_dirty_upload: textures the game fills from the CPU without an NVN flush
(its 121-layer ASTC terrain arrays) are found through write-protected pages
and uploaded; before, they stayed empty (magenta in ASTC).
Breath of the Wild, issue #9 (iPhone 16 Pro Max killed for memory in the first
shrine; an 8 GB iPhone gives the app 3,375 MB):
- Finalized textures' serials no longer stay in the serial table for good:
live serials are marked at nvnTextureInitialize, dead ones resolve through a
512-entry table (about 10 a frame in BotW before, 276k after 25k frames).
- The memory-pool tracking table grows past 4,096 entries (BotW reached 4,006).
- Most of the growth was the defer_drain leak fixed in 0fc0ce4: the shrine now
stays at 2.8-2.9 GB resident on the Mac, about 500 MB under the limit.
- Falls back to 30 fps at the right speed when the GPU can't hold 60: two cuts
make the frame-interval controller always use its GPU-time path, and the
profile key gpu_timestamps segment gives each timestamp report its
segment's GPU start / end (the default, submission, is unchanged). Under
load, interval 2 held at 0.93-0.98 of real time (0.78-0.95 before). The
check that it still holds 60 on a quiet machine is pending.
NVN: clears inside a pass, with a partial channel mask or a partial scissor
are drawn as a triangle in the open pass instead of dropped, and 8-bit indices
written by the GPU earlier in the submission are widened on the GPU (TOTK red
lines 6 / 7 / 48). Adapted from MacNativeGames (MIT, dm/third_party/macnativegames).
NVN vertex fetch: 8 / 16-bit integer attributes fed to an input of the other
sign are widened by their own sign (CPU conversion, red 96 once); before, the
signedness swap widened them the other way and Three Houses' terrain heights
below 0 left the screen (no ground under close-up cameras). The pass dump's d
line shows polygon offset and depth clamp.
NSP script: a GLSLC capture stage for profiles with shader_glslc_capture
(Three Houses' 9 run-time shaders), so only the NSPs and prod.keys are needed.
AGENTS.md: only the coordinating session runs the multi-game smoke test.
Each part was checked on its own game on the Mac. The multi-game smoke test
runs after this push (Wonder and Engage already passed on the same changes
minus the vertex-fetch fix). The translator fix and its chain rebuild are not
in this commit.
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.
scripts/build_ipa_from_nsp.py shows [k/n] stage · step. Where the work can be counted, the line has a bar, done/total, percent, elapsed time and ETA: bytes extracted, modules rewritten, shaders (cache check, translation, compile, read from the files dmsh_build.py writes), files compiled by Xcode, and bytes copied and zipped into the IPA. Elsewhere it shows a spinner. In a terminal or Spyder the line is redrawn in place; piped output gets a plain line every BEAT (30) seconds. PROGRESS / --progress auto|bar|lines|off picks the mode. Tool output now goes only to <id>_build/logs/; a failed step prints its log's last 40 lines and the log's path. No chain-hashed file and no build_ipa.sh change.
- Odyssey moves from "being ported" into the Games table (base game +
update 1.4.1, ~6 GB IPA) and the gallery (Cap Kingdom fog, captured on
the Mac with the current build). It played at 60 fps on an iPhone; the
fog fix (ca7216c) is in the latest IPA.
- Touch controls: Odyssey starts in 3D like 3D World and Breath of the Wild.
- Hades is the one game still being ported.
- AGENTS.md: eight games play on the iPhone; development notes drop
Odyssey's "being ported".
On the Switch the ground fog opens around Mario as he walks through it. The effect is a small fluid
simulation: 256x256 and 128x128 RG16F velocity fields, a blur and a blended composite. Its only input
is a point-sprite splat per mover, drawn with agl's alPointSpriteShader. The vertex shader writes
gl_PointSize = min(size, c0[0x610]), and the host left that offset of the NVN driver segment at 0.
Every splat had size 0, so the fields stayed exactly zero and the fog never moved (reported on an
iPhone; the Mac did the same).
- nvn_mtl.m: the graphics stages' c0 driver segment gets the largest point size (2047, Maxwell's
limit) at 0x610. It is not the SetPointSize value: the game sets that to 1, and one-pixel splats
came out with zero disc weight. The splats now ask for 3-18 px, the fields hold velocities up to
+/-44, and the fog clears around Mario on the Mac.
- docs: 04-graphics.md gets the offset in the driver-segment table and a write-up, with a note that
dmsh_build.py's drv_reads scan only sees fragment-shader reads. development.md gets a Fog note under
Odyssey.
Checked on the Mac: the smoke test of the other seven games shows only their usual red lines.
- README.md: Super Mario Odyssey and Hades both play on the Mac. Their iPhone builds are not tested
yet, and the NSP script cannot pack Hades yet (it doesn't translate Vulkan shaders).
- AGENTS.md: Odyssey plays on the Mac (it no longer just reaches its frame loop).
- docs/development.md: Hades game notes. They cover the Vulkan layer, the SPIR-V shader table from
vk_shaders.py, the notification-event hook, EOS staying offline, Mac input to Tartarus, and how to
pack it by hand.
- Movies: H.264 decoded with VideoToolbox (dm/host/hle_movie_avc.c). This SDK's MP4 demuxer passes the parameter sets as the "avcc" record; pictures leave in presentation order through a 4-deep queue (41 of Odyssey's 44 movies have B-frames), and an end of stream drains the queue. The hint videos in Odyssey's tutorial boxes now play (they were black). VP9 unchanged (Engage: 2,816 packets in, 2,816 frames out).
- Controller input: GetNpadStates fills every requested history entry with the current state under consecutive sampling numbers. Odyssey reads all 16 entries whatever count comes back and polls every slot each frame; with only the newest written, neither stick did anything in play while buttons and menus worked (its first hint, "Look Around", waited forever). DM_KEYS takes RLeft / RUp / RRight / RDown.
- Odyssey's HUD counters were solid white cards: the profile lists its 26 layout shaders (main's BNSH at 0x132c000) under alpha_from_red, as for Breath of the Wild.
- -fpreserve-invariance on every Metal compile (dmsh_build.py MAC_METAL, ios_metal.py): Odyssey draws a depth pre-pass with alRenderDepthOnly, then the G-buffer with other vertex shaders and depth test less-or-equal; Metal gave the identical position math slightly different depths and those pixels kept stale content (Cap Kingdom's ground in 32x32 blocks on every fourth frame). Checked by recompiling the frame's 78 vertex shaders with the flag: four consecutive frames identical, no blocks. All eight chain versions change; Mac libraries and c1_full.tsv are being rebuilt and follow.
- DM_NVN_TEXSNAP works again (its entries lacked the layer field the dump loop reads) and names its files <path>_s<pass index>_<serial>_<level>.png.
- Smoke runs of the seven playable games on the Mac after these changes: only their known red lines.