
- Title ID
- 010055D009F78000
- Tested build
- 1.2.0
- Nintendo SDK
- 6.5.0
- Engine
- Koei Tecmo's KTGL
- Graphics API
- NVN
Build this game
Keep [010055D009F78000] in your file names, put the base game, update and DLC in one folder and run the script. Follow the build guide, then the sideloading guide. Needs an iPhone 15 Pro or newer, or an M1 iPad or later.
Technical notes
The project's own record of bringing this game up: what its profile needs and why, what was found and what is still open.
Update 1.2.0 (v327680) over the base game, SDK 6.5.0. Koei Tecmo's older KTGL engine on NVN (the game is co-developed with Intelligent Systems). Modules main, subsdk0 (GLSLC, NVN's runtime shader compiler), subsdk1 (the movie library, NintendoSDK_movie) and sdk. Plays on the iPhone; 60 fps (30 on the Switch) through rewriter cuts on 1.2.0 main.
What it needs
| Setting | Where | Why |
|---|---|---|
| Present interval table → 1 for every render mode (int and float tables) | patches.py CUTS, main build 89048449… |
60 fps at the same game speed: KTGL already updates the game 60 times a second; see Frame rate |
Render thread's forced interval 2 ignored (main+0x50fb7c) |
patches.py |
the opening movie and scenes such as character creation run at 60 too |
Frame limiter sleeps instead of yielding (main+0x5be4c0–0x5be4d0) |
patches.py |
its yield loop spun on the host: 59.5 updates a second and 1.5 W of CPU on the Mac |
Render thread waits a frame instead of 16 µs (main+0x50f8e8–0x50f8f4) |
patches.py |
its wait polled about 50,000 times a second at 60 fps |
GLSLC capture (/glslc_capture/=runtime in shader_groups) |
game.tsv, NSP script |
9 built-in shaders exist only as compiles of the game's own GLSLC at boot; see Files and shaders |
4 GB layout: mem_reserve 0x100000000, mem_alias 0x10000000, mem_stack 0x10000000, app_pool 0xCD500000 |
game.tsv |
the NPDM declares no system resource (META +0x14 = 0): the game uses SetHeapSize. Link's Awakening's partition, not measured for this game |
pad_layout full |
game.tsv |
the 3D gamepad layout: the camera is on the right stick in the monastery and in battle |
Frame rate
- KTGL runs the game on the main thread at 60 updates a second whatever it shows: each update steps by 1/fps with fps = 60.0 (
main+0x1a38f08, set atmain+0x50e114; dt atmain+0x510010), and the frame end (main+0x50f680) waits out 60/fps ticks of 1/59.94 s in a yield loop (main+0x5be430). - The render thread (
main+0x50f8c0) draws the latest update and presents it with the interval of the render mode (frame +0xbb84, set atmain+0x50f3d0): 0 = 1080p docked, 2 = 720p handheld, 3 / 5 their two-view variants (interval 2); 1 and 4 are 720p / 1080p modes at interval 1 that nothing selects. Measured on the Mac before the cuts: 59.2 updates and 29.8 presents a second. - Current: the per-mode interval table (int at
main+0xd13de0,{2,1,2,2,1,2}, read bymain+0x4b0420for the present and by the 6-tick fade counter inmain+0x4b7530; float atmain+0xd13e40, the per-frame scroll step inmain+0x4c2b30) becomes 1 for every mode, so every update is shown, the game runs at the same speed and the render thread's own steps stay at the same speed. Resolutions stay per mode. - Current: the render thread also forced interval 2 while either of two flags was set:
main+0x21c9a03, a full-screen image drawn before the frame (main+0x4b4908: the opening movie), and frame +0xc55f / +0xc560 (from the object at app +0x40: the character creation screen after the movie, and so on). In the engine's own 60 fps modes this traded 60 fps for resolution in those scenes (main+0x50f7d0drops 720p mode 1 to 1080p mode 0). A cut atmain+0x50fb7c(orr w8,w9,w8→mov w8,wzr) ignores both, so those scenes run at 60 too; the movie still lasts 210.6 s (6,318 frames at 30 fps), as in the original. - Current: the frame limiter (
main+0x5be430: yield, read the tick, until the target has passed; x8 = time passed and x23 = target, in 100 ns) yielded and polled the clock, which on the host spun its first 2 ms on a performance core and then overslept by up to 250 µs: frames took 16.8 ms (59.5 fps, a repeated display frame every 2 s) and CPU energy went from 0.16 W at 30 fps to 1.5 W on the Mac (228,000 yields a frame on an iPhone). The yield becomesnn::os::SleepThreadof 96% of the time left (PLTmain+0xafc300), and the loop goes back to the limiter's own tick read atmain+0x5be44c; the frames take 16.68 ms. - Current: the render thread waits for the next update with KTGL's event wait (
main+0xaa90e0, timeout in µs: × 1000 →nn::os::TimedWaitEvent) and passed 1/fps × 1000 = 16, one frame in ms, so it polled every 16 µs (42,000–53,000 wakeups a second on the Mac at 60 fps, against 1,200 at 30 fps, where it waited in the present instead). The timeout becomes 1000/fps × 1000 = 16,666 µs. The other four callers wait without a timeout. - Current: both pacing cuts stay per-build, and neither stands in for a host fault (checked 2026-10-26, host at
7753b7b, headless boot to the title on the Mac, 100 s per run, each cut undone in the rewrittenmain). Without the limiter cut the general yield back-off (thr_yield) holds 59.7–59.8 fps (frame p50 16.73 ms against 16.68) at 0.9–1.2 W of CPU energy against 0.14–0.42 W: about 9,700 plain yields a frame before the back-off starts at 2 ms. Without the timeout cut: 53,000–55,000 wakeups a second against 1,450, and 0.7–1.0 W. The 16 µs is the game's own: the render thread passes (1/fps) × 1000 = 16 tomain+0xaa90e0, which multiplies it by 1000 into theTimeSpan(nanoseconds) ofnn::os::TimedWaitEvent, so a frame in milliseconds goes where microseconds are taken. Only a wait that knows its deadline can sleep until it, and only the game's code holds it: of the 206 modules on hand, the limiter (main+0x5be4b4) is the only loop whose only calls areYieldThreadandGetSystemTick, so a rewriter pass for such loops would match this one site. - Prologue battle on the Mac with the cuts: 59.9 fps, 55% of a core and 1.8 W of CPU energy (the original at 30 fps: 44% and 1.7 W).
- Before the limiter cut, the host's yield back-off alone took its CPU time on the Mac from 78% to 19% of a core, same frame times (development.md, Power).
Memory
- About 3.5 GB resident and 2.8 GB of Metal allocations on the Mac; the game itself makes a 1.5 GB GPU-only memory pool.
Rendering
Game-specific findings. The general host changes they led to are listed under Found on this game.
- Resolved: frames stopped after about 15 s; a game thread spun in its command-memory callback. KTGL gives its main command buffer one 16 MB chunk of a ring and, in its callback (
main+0x9ca960), takes chunk start +GetCommandMemoryUsedas the position the GPU has reached; the ring allocator (main+0x9cc120) waited for fences that had all signalled. NVN counts used bytes from the lastAddCommandMemory; the host counted fromBeginRecording, so the position stayed at one frame's size and the ring never freed. - Resolved:
FATAL submit re-enteredin the first frames. The game initializes two NVN queues and submits on both from two threads; submissions are serialized now. - Resolved: menus showed plain bars or only the cursor; the screen was black. The scene binds a 12,544-entry texture pool and a 3,856-entry sampler pool, the 2D UI's command buffer binds 4,096-entry ones, and both fill IDs from 256. With one ID table the scene's default 8×8 white texture overwrote the UI's textures: the title menu's labels drew as plain bars, other menus were empty except for the cursor. The pools are now kept apart. The scene registers each draw's textures into fresh IDs of a ring (8,729 and up a few seconds in), which used to read as empty slots and left the screen black.
- Resolved: black silhouettes, then flat tan characters. The character shaders read Half2 UVs through integer-declared inputs: Metal refused the pipeline (
Vertex attribute m_36(5) of type uint4 cannot be read using MTLAttributeFormatHalf2), so the characters' shading draws were dropped, and reading the raw bytes as integers gave denormals (UVs collapsed onto one texel). They are now converted to float32 bits. - Current: depth clamp is replayed (the game turns
SetDepthClampon; it used to be ignored). - Resolved: the ground under close-up battle cameras was flat steel blue. The terrain's vertex shader (
56778feb1cfdee7e) reads its grid position from anRGBA16Iattribute: x and z the grid cell, y the height, which spans the whole int16 range; so does the side pass that draws the river surface into targets of its own (db725c85ca060403). The input was declareduint4, and the host swappedShort4forUShort4, which widens with zeros: every negative height read as 32,768–65,535, and those vertices went far up out of view. Where the ground was missing nothing wrote the depth (reversed-Z, cleared to 0), and the lighting takes depth 0 for sky and paints the fog colour. The G-buffer colour targets are never cleared, so they kept an old image there: cobblestones of a mesh under the terrain, which from the map camera showed through the gaps like a road.- How it was traced, since two steps misled: a
HILITEcolour over the gap was stale too (the never-cleared albedo), and the end-of-frame depth showed 0 there. Recomputing every draw's clip positions fromCBDUMPdata showed that no draw of the shader reached the gap. Then the side pass's layer, which showed only scattered tiles of the river, led to the vertex format. Ruled out on the way: depth clamp (Metal's clip and clamp modes gave the same depth), the pass without a depth attachment (142595 is a screen-space decal pass: it samples the depth and writes albedo and normals), polygon offset (none), dropped draws.
- How it was traced, since two steps misled: a
- Light and decal lists: 321 shaders read them through the instruction Maxwell also uses for 1D textures, and they used to read 0. The decal pass (
417a523053457352, slots 16 and 18) and the lighting (094a2d11253f126c,f54830a75e07e14a, slots 18–20) read them from texture buffers declaredtexture2d(texture type mismatch: shader declares 2, texture is 9). Since row wrap they read the real lists: slot 18 holds 1,052,640 elements, 65 rows. On the Mac prologue route the torches and burning houses now light the battle map and the close-up ground (they lit nothing before), and the blood decals take the scene's light instead of showing as dark blue patches. Its Half2 / RGBA16I vertex attributes go through shadow vertex inputs instead of the CPU conversion. - Resolved: the first loading screen took 78 s on the iPhone (5 s on the Mac). KTGL's loader uploads each resource with a copy on the queue followed by
nvnQueueFinish, hundreds of times per loading screen (effect banks, face models), and that call waited for the display to take every queued frame: one to two display frames per call. The windowless Mac runs present at once, so they never showed it;DM_WINDOW=1does (one step took 20 s instead of 1.7 s).nvnQueueFinishnow waits only for the GPU. Windowed Mac run after the fix: first load 6.1 s, frame pacing unchanged. Found on the device withDocuments/env.txtholdingDM_WATCH=50(thread table at 50 s: only the yield-polling main thread used CPU) andDM_FSLOG=<absolute path>(the reads came in bursts, with stalls of 4 / 24 / 13 s right after the effect banks).
Audio
- Resolved: no sound; red
audren: command 1 in the command list is malformed (magic 0 size 0). SDK 6's DSP command header is +0x04 size (s16), +0x06 command ID (s16), +0x10 enabled, where SDK 8–20 have +0x04 enabled, +0x05 ID, +0x06 size (CommandListProcessor::Process,sdk+0xaaa00); its DeviceSink is ID 0x10.
Services
- Resolved: the first loading screen never ended;
DM_FS=1showedsave:/system"not found" and nofs mount save. In SDK 6account::OpenPreselectedUserreturns a Result, and the game skips its save mount when it is not 0 (main+0x53b7e0). The host's hook returned void and left the handle pointer in w0; it returns 0 now. - DLC: the Mac needs
DM_AOC_DIR=<NSP folder>/010055D009F78000_build/aoc, or a "DLC deleted" dialog blocks loading a save.
Files and shaders
- RomFS: the LINKDATA archive (
DATA0.binis an index of 32-byte entries{u64 offset, u64 length, u64 stored length, u64 compressed}intoDATA1.bin, 6.9 GB), loose overrides inpatch1–patch4(listed bypatchN/INFO0.bin, 0x120-byte entries ending in arom:/patchN/…path), WebM movies innx/movieand KTSL sound banks (*.ktsl2stbin). Compressed entries and*.gzfiles are Koei Tecmo's chunked zlib (kt_gz; 01). - Shaders: 1,364.
shader_split.py(kt_shaders) reads the SPKG packages (G2S containers, as in Age of Calamity) inDATA1.bin(132 entries) andpatch*/nx/shader, a_SPK4100post-effect package without G2S (DATA1.binentry 549), map and model archives carrying packages of their own, and the BNSH and GLSLC outputs inmain.DATA1.binis split into 64 scan jobs by entry range (linkdata_jobs). - Its GLSLC (output version 0x1d.2) writes 0x848 / 0x850-byte control sections and compute code with no header, which the reader used to drop: 24 shaders before, 1,355 after. A few outputs of version 0x1d.0 keep something else at control +0x20 (odd values such as 0x136b), so a control length must be a multiple of 4;
glslc_sectionlater took 8 more of them (GLSLC outputs read through their section headers). - Shaders compiled at run time: KTGL compiles 9 built-in GLSL shaders at boot with the game's own GLSLC (
glslcCompilefrommain+0x9c89f8; the sources are strings inmain): 2D primitives, text, the loading quill. No game file holds their output, so they are captured on the Mac: the game runs withDM_GLSLC_CAPTURE=<version dir>/glslc_capture(hle_glslc.c), andshader_split.pyreads that folder. The outputs are identical on every run. The NSP script does this by itself in itsGLSLC capturestage, so the NSPs and prod.keys are enough: it builds the Mac host into<id>_build/tools/and boots the game until the 9 files are in<id>_build/extract/v327680/glslc_capture. Without the folder the split tables would differ from the profile, and the script would stop at its table check. By hand, run the game for a few seconds with that variable (step 5 of the build by hand).
Mac routes
- A fresh save needs
DM_KBD=<name>for the name entry, and its confirm dialogs default to No. - From a new game into the prologue battle (frame-based at 60 fps, about 7 minutes):
DM_KEYS="f720:A*185/90,f2400:Plus*35/438,f17400:Left~0.3*120/500,f17480:A*120/500,f17560:Left~0.3*120/500,f17640:A*120/500,f18020:Plus*100/562"; the game offers a save right before the battle. The repeating Left, A, Left, A reaches Yes whether a confirm dialog is open or not (A, Left, A loops once it starts on an open dialog: A answers No, Left changes the selection, A reopens it). A presses during the opening movie pause it, so the scenes after it start a few seconds later from run to run. - To a first fight, from a save right before the prologue battle, with
DM_AOC_DIRset:DM_KEYSwith A every 90 frames fromf720tof2610, thenf2700:B,f2800:B,f2900:A,f3000:Up,f3060:Up,f3120:Up,f3180:Up,f3240:A,f3340:A,f3440:A,f3540:A,f3640:A,f3740:Acloses the movement tutorial, moves Byleth up to the thief and attacks: the combat close-up runs at frames 3,400–3,700.
Found on this game
General host changes first needed here; each applies to every game:
- Resolved in the sanitizer audit: Unaligned command headers during layout probing, reproduced with the SDK 6 command-list fixture.
- Submissions serialized across queues (
g_sub_mu) andGetCommandMemoryUsedcounted from the lastAddCommandMemory: Submissions from two queues on two threads, WhatGetCommandMemoryUsedcounts. - SDK 6:
account::OpenPreselectedUserreturning a Result, SDK 6's DSP command header (captureinhle_audren.c) and run-time GLSLC capture (DM_GLSLC_CAPTURE,hle_glslc.c): SDK 6'saccount::OpenPreselectedUserreturns a Result, SDK 6: the DSP command header layout, Built-in shaders compiled with the game's own GLSLC; the NSP script's capture stage: development.md. - Overlapping texture / sampler pool IDs kept apart (
bind_handles, pool base incmd_t), integer vertex inputs fed a float-type format (vconv_bind) and other-signedness integer formats (vconv_int, since replaced by shadow vertex inputs), depth clamp (OP_DEPTH_CLAMP),nvnQueueFinishwaits only for the GPU (mtl_finish): Texture and sampler pools with overlapping IDs, CPU conversion of vertex attributes the input type cannot take, Depth clamp,nvnQueueFinishwaits for the GPU, not the display. - Row wrap and shadow vertex inputs: Buffer textures read by 1D integer fetches (row wrap), Shadow vertex inputs.
- Lighting that alternated between lit and unlit frames on an iPhone (one of the light list's two copies sits at a pool offset the phone's linear-texture alignment rejects, read as zeros;
tbuf_rows_copy): Buffer texture rows at a pool offset the device will not view. - GLSLC 0x848 / 0x850 controls and headerless compute code (
glslc_shaders) and the SPKG / LINKDATA reader (kt_shaders): SPKG packages in KTGL LINKDATA. - Yield back-off and the yield-progress rule (
thr_yield; this game's limiter is a poll that keeps its registers): development.md, Power.
Open
- To confirm: 64-pixel rectangles in the albedo where decals land; skipping the decal pass removed them. They did not appear on the Mac prologue route before or after row wrap (no 64-pixel structure in the close-up frames), so the scene where they were seen still has to be checked. The player-facing README lists "small square patches on the ground where decals land" as a known issue.
- Deferred binds are not remapped to the pool base (red line).
game.tsvstill describes the game as "In bring-up" and the memory layout as Link's Awakening's, not measured; the development notes call SDK 6.5.0 "the oldest so far", though DARK SOULS: REMASTERED (SDK 4.5.0) and Mario Kart 8 Deluxe (SDK 4.4.0) are older.- The capture boot's time: the development notes give "about 10 s in all" for the NSP script's stage and "about a second after launch" for the 9 files.
These notes are republished from the nxnative repository under its MIT license (license and attribution). The original, which may be newer, is the game's notes on GitHub. This site hosts no game files.