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.
- dmsh_build.py vs_init: every float4 vertex output is set to (0, 0, 0, 1) before the translated body, as unwritten components read on the hardware. The translator's MSL started from all zeros; Link's Awakening's surface shaders divide a world position by its w, which the vertex shader never writes, so every sun-shadow compare value was infinite and the whole outdoor scene was lit as if in shadow (dim, no shadows from fences, house or trees). All seven chain versions change.
- Mac libraries and c1_full.tsv rebuilt for Link's Awakening, Engage, 13 Sentinels and Mario Wonder (Breath of the Wild, Super Mario 3D World and Metroid Dread follow).
- DM_NVN_TEXRAW dumps the depth plane of a Depth32Float_Stencil8 texture (shadow maps).
- The IPA's version (CFBundleShortVersionString / CFBundleVersion) is the packed game's display version: the update's when an update is packed, else the base game's (nacp.py --version, DM_APP_VERSION in build_ipa.sh, project.yml; the NSP script prints it).
- README: Link's Awakening screenshot outdoors in Mabe Village.
- NVN's depth mode is runtime state: Mario, Metroid Dread, Super Mario 3D
World and Breath of the Wild keep near = -w (GL); Link's Awakening,
13 Sentinels and Engage set near = 0. The translator rewrote z for GL
in every vertex shader, so near = 0 games had all depths in 0.5..1 and
depth-to-distance effects were wrong: Link's Awakening's depth of field
blurred the whole screen outdoors.
- The translator no longer rewrites z; dmsh_build.py (vs_depth) writes two
entry points into every vertex library, main0 with the GL rewrite (the
same arithmetic as before) and main0_z without; the host picks one from
the mode the game sets (mtl_set_depth_zero, shader_load). No per-game
setting; a red line if the mode changes after vertex shaders loaded.
Metal function constants were measured at about 2.4 ms of
specialization per vertex shader, so two entry points instead.
- New translator dll (Ryujinx dlls unchanged), lock and chain versions for
every profile; Mac libraries and c1_full.tsv rebuilt for Link's
Awakening, Engage, 13 Sentinels and Mario Wonder (Breath of the Wild,
Super Mario 3D World and Metroid Dread follow).
- Checked on the Mac: Link's Awakening outdoors (from a save in Mabe
Village) has a sharp middle band again; Mario Wonder's title renders
the same as before (differences only in animation).
- README gallery: Engage, 13 Sentinels and Link's Awakening recaptured
with this build.
- Link's Awakening 1.1.0 main: the per-world PhysX completion task destroys
itself in its release (vtable back to the base, mTm = 0). At 30 fps the
game steps physics twice per frame; a worker descheduled between the run's
unlock and the release let the main thread rebuild the task first, and
the late destructor wiped the new one, so the next completion crashed in
removeReference (main+0x10ccf28). A per-build cut makes that release a
ret. Unfixed, every Mac run with the CPU loaded crashed; fixed, runs with
138-257 switches to 30 fps and walks into and out of the house are clean.
- Crash reports print what each register points at ([dm] *xN), so an
object's vtable names its class.
- RED_ONCE's flag array was one slot short.
- README gallery: Super Mario Bros. Wonder, Super Mario 3D World, Breath of
the Wild and Metroid Dread recaptured with the current build; no "(Mac)"
captions.
- docs: the crash in the bring-up catalogue; debugging recipe for racy
crashes (CPU load, trace gates, a ring printed at the crash).
- 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.
- Sound: SDK 8 numbers the DSP commands differently and its DeviceSink is
0x10, not 0x13, so every rendered frame played silence (only the movies
had sound). capture() now takes the one 0x148-byte command with 1-6
inputs as the sink when the expected ID is missing.
- Lighting: the light-culling compute shader reads its workgroup count
from NVN's driver segment c0 (+0x200). dmsh_build's drv_reads scan only
matches fragment-shader names, so compute and vertex shaders got an
all-zero c0: every tile row wrote row 0, and the doorway step flickered
with white 64-pixel tiles. A declared driver-segment binding now counts
as read, and compute c0 carries the dispatch size (0x200) and the
uniform buffer table (0x210 + 16 r). BotW and SM3DW render as before.
- Foliage: the shadow / depth pre-pass pipeline 147c829e79fb322f failed
(uint4 inputs fed 16-bit normalized formats), so with depth test EQUAL
the palm tree and flowers never drew. New profile file
shader_facts_manual.tsv (merged by shader_split.py) declares those
inputs float; Link's Awakening's chain is now ccb18ba743bf.
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