Files
nxnative-easy/dm/games/0100F2C0115B6000/README.md
T
58f74014f4 Seed TotK prewarm from the recorded M2 iPad route (#261)
Add 1,537 shader, 2,512 render-pipeline and 43 compute-pipeline rows from
the update 1.4.3 capture, preserving first-use order. Document the
author-reported route and results, and limit the coverage claim to
that route.

This profile-only capture is separate from the shared CPU and loader
optimizations. Validation checks the row counts, uniqueness and exact
data/order preservation; no new cold-start or gameplay run here.

Co-authored-by: draco-cong <71372970+draco-cong@users.noreply.github.com>
Co-authored-by: laobeibei <laobeibei@users.noreply.github.com>
2026-10-26 15:28:59 +08:00

19 KiB
Raw Blame History

The Legend of Zelda: Tears of the Kingdom (0100F2C0115B6000)

Update 1.4.3 (v720896) over the base game, SDK 20.5.22. Nintendo EPD's engine (sead / agl / gsys, NintendoWare G3d2 / Ui2d / Vfx2, Havok) on NVN. Modules main, subsdk0 (the movie library) and sdk; subsdk0 and sdk are the same builds as Breath of the Wild 1.9.0's (01007EF00011E000). Plays on the iPhone; 60 fps (30 on the Switch) through the host's frame step.

What it needs

Setting Where Why
Frame step, fps_target 60 patches.py FRAME_STEP, game.tsv 60 fps at normal speed; see Frame rate
Node area cut 1 GB → 62 MB dm/tools/sdk_patches.py, sdk build 574284b4… the resource heaps ran out of alias region (issue #70); see Memory
5 GB guest layout: mem_reserve 0x140000000, mem_alias 0x98000000, mem_stack 0x10000000, app_pool 0xCD500000 game.tsv 256 MB in front + 2.375 GB alias + 256 MB stack + 2.125 GB heap region. 5 GB is the total proven on a phone (one reservation of at most 6 GB without the extended virtual addressing entitlement)
render_cond on game.tsv the 3D scene was black: the game skips clears with conditional rendering
tex_flush_upload on game.tsv streamed textures are written by the CPU into pool memory in GPU layout and published with nvnMemoryPoolFlushMappedRange
tex_dirty_upload on game.tsv textures filled from the CPU with no flush at all (the magenta cave, the unlit stone blocks)
Integer output rows shader_facts_manual.tsv no PC version, so the translator has no output types: agl's BC4 encoder and the Ultrahand mask (e3fb0bb58a691e10, R8Uint, from PR #11; the Mac route does not reach it)
pad_layout full game.tsv the 3D gamepad layout: the right stick turns the camera

Frame rate

  • Current: the game picks a present interval each frame (main+0x2db174: 2 = 30 fps, 3 under load) and steps its game time by 60 / fps (int fps at timing +0x50, read at main+0x2daa54 and main+0x3e562c). Stubs in patches.py (sites from masagrator's FPSLocker config for this build) replace that logic with the host's frame step in whole vsyncs, as in Breath of the Wild: int fps = 60 / step, and the float fps at timing +0x24 the same. Prerendered movies hold 30. DM_FPS_TARGET=30 saves power, 0 gives the game's own step back (every stub then runs the original instructions).
  • Current: before the host has decided a step (the first frames), the stub steps by the measured frame time, capped at the target. Game time that ran ahead after a short frame is paid back by the next frames (the idea from OJZen/totk-mobile); frames longer than 1/15 s are not caught up.
  • Measured on the Mac (M4 Pro): the opening cave holds 60 at 1.000 of real time; with 12 ms added to every frame it drops to 30 after three late frames and stays at 1.000.
  • Render scale: DM_RENDER_SCALE=0.5–1.0 multiplies the size of the 3D views (1600x896 docked) and fixes it there; the game's own dynamic resolution runs only at 1.0. The HUD keeps its resolution. The stub multiplies in the view size function (main+0xd5c24), unlike Breath of the Wild's, which sets the scale at the dynamic-resolution block.
  • Resolved: the yield back-off used to start after 200 µs, which caught the main thread waiting for its workers on the frame's critical path: frames ran past 33 ms and the game dropped itself to 20 fps (9 times in 300 s on the Mac; on the iPhone the threads also stayed on efficiency cores). From 2 ms on it never backs off (development.md, Power).
  • The spin-wait rewrite finds 843 loops in main; 701 of them are one inlined atomic decrement (a compare-and-swap retried after another thread changed the count), which yields only on a second retry in a row.

Memory

  • Current (issue #147): confirmed transient upload-memory bug on the Mac; iOS validation pending. With update 1.4.3, the corrected issue #70 SDK module, shader chain dec5ee141a9f and 30 fps, walking from the Great Sky Island save produced 10.70 GiB peak footprint after about 474 s; a shorter repeat reached 12.81 GiB after about 150 s. Live staging peaked at 4.48 / 5.82 GiB and the open batch at 4.41 / 5.75 GiB, then memory fell back near 4 GiB. The shorter burst logged 3,084.2 MB of uploads in one frame. A temporary 50 ms sampler and the kernel footprint-peak ledger caught peaks missed by the periodic reports and the 8 GiB sampled watchdog. With bounded batches and upload-completion retirement, the 220 s shortened route with deferred encoding enabled peaked at 4.24 GiB footprint and 216 MiB live staging, without GPU errors. A 520 s replay of the original input timings peaked at 4.23 GiB / 200 MiB, but landed differently and reached Game Over earlier, so it is not an equivalent traversal comparison. These local results do not establish the cause of the reporter's full-device reboot or rule out longer-term cache growth; the requested save/route, full logs and Jetsam/panic reports are still needed for that comparison.
  • The NPDM declares 24 MB of system resource, so IsVirtualAddressMemoryEnabled is true and the game's memory lives in the alias region. nninitStartup also calls SetHeapSize(0x86000000) (2.1 GB of the pool as its heap; a 2 GB heap region aborts there with 0xd401). What it can map is bounded by the pool less the heap (1.1 GB).
  • Resolved (issue #70): the resource heaps are address regions in the alias region (2.375 GB here, 64 GB on the Switch). With the SDK's ~1 GB node area at its start, the alias region was used up to +2.25 GB at the first descent from the Great Sky Island with 1.00 GB mapped. The game then took a memory shortage: its frame check (main+0x1fd32ec) logs MSF, then R and Rf (reason bits of one resource heap's object), and plays the memory-shortage Blood Moon, which puts Link back at the start. With the node area cut to 62 MB (as Fire Emblem Engage's, Hollow Knight: Silksong's and other builds have it) the high-water mark is +1.75 GB, and no shortage came in 3.7 minutes on the island (it came within a minute in 7 of 8 runs without it).

Rendering

Game-specific findings. The general host changes they led to are listed under Found on this game.

  • Resolved: the 3D scene was black. The game wraps nearly every ClearColor / ClearDepthStencil in SetRenderEnableConditional(RENDER_IF_NOT_EQUAL, rec) … SetRenderEnable(true), each clear followed by ReportCounter(TIMESTAMP_TOP, rec). The CPU sets rec to 1 when a target needs a clear, and rec + 16 is never written. A timestamp report puts 0 in the counter field, so after one clear the two counters are equal and later conditional clears of that target are skipped. The host ignored the condition, so every clear ran: the first tile-lighting round, the half-resolution scene depth the light volumes test against, and the light accumulation were all cleared before the composite read them. Fixed by render_cond. With it, the game's scissored colour clears (50×50 tiles of a 500×400 target, about five a frame) are all skipped.
  • Resolved: the cave was magenta. The terrain samples two 121-layer ASTC 8×8 arrays (42 MB each), created on zeroed memory and filled from the CPU with no nvnMemoryPoolFlushMappedRange or FlushTexels (its cache maintenance runs natively). They stayed GPU-only with content 0, and Metal decodes an all-zero ASTC block as (1, 0, 1). Fixed by tex_dirty_upload.
  • Resolved: the stone blocks and steps of the opening cave got no torch light (Mac and iPhone). Their fragment shader 54e8c08998050e79 takes its roughness from a 5120×5120 BC4 atlas (roughness = the square root of its red, packed into the upper six bits of blue in colour target 3, which the tile-light shaders read). The game fills 1024×1024 BC4 material textures on the CPU with no flush, reads them back with nvnTextureReadTexels on its own threads before any draw uses them, and copies the blocks into the atlas, which it fills after first binding it. ReadTexels returned the never-uploaded host texture (zeros). Found by running MacNativeGames' build on the same save (their atlas held data). Traced with DM_NVN_TEXDUMP (roughness bits in colour target 3), the shader's MSL, DM_NVN_TEXRAW on the atlas and DM_NVN_TEXINFO, then lldb reading the atlas in their process.
  • Resolved: draws into a 32×32×32 BC4 3D texture were lost. agl's BC4 block encoder (0544282c162305db, the same shader Mario Wonder ships) encodes it one depth plane per pass through an RGBA16UI view. It draws through the compressed-target shadow (Breath of the Wild's), with its output declared uint in shader_facts_manual.tsv. A raw dump at frame 1500 of the new-game route: all 32 planes hold encoded blocks.
  • Resolved: a draw with no render target. An agl fragment shader that only stores to a storage buffer draws one triangle into a 1×1 viewport with no target (it samples a 64×64 cube map; the game reads the buffer back). With the attachment-less pass, the cave's rock and stone blocks get ambient light they lacked, and Zelda's bloom halo in the descent cutscene is gone.
  • How the tile lighting was traced: the G-buffer class byte selects per-class half-resolution passes through a depth buffer of class values (gl_FragDepth = mix(0.5, 1, nibble-swapped class / 255), then full-screen or per-tile quads with depth EQUAL), so DM_NVN_DEPTHOVR (depth test forced to ALWAYS) with HILITE tells "rejected by depth" from "never rasterized".
  • Current: Ultrahand and Recall highlight validation pending. The Depth16 load-clear correction, ported from MacNativeGames, addresses its reported tile grid. The depth conversion mismatch is reproduced in synthetic Mac GPU tests; this game's highlights and the iPhone have not been rechecked here.
  • Resolved in the host: packaged block dimensions and linear compressed storage sizing, from the driver probes linked under Found on this game. CPU layout and CPU / GPU packaged 3D deswizzle tests cover the correction; no new gameplay validation of these cases or iPhone run here.
  • The game issues a depth clear scissored to 8×8 of a 64×64 target outside a pass, which the host still clears whole (depth-stencil clear with a scissor or partial stencil mask, cleared whole).
  • About one draw per 8-bit index widening, up to a dozen widenings a frame on the title and load screens, no pass split (log line 8-bit index GPU expansion).
  • Its 14,766 vertex shaders have no transform feedback.
  • Attachment loads and stores per frame on the Mac went from 1,100 to 700 MB once an inline uniform update inside a render pass stopped ending the pass (development.md, Power).

Command memory

  • Resolved (issue #61): CRASH sig=10 in mtl_exec after minutes of play, the fault address equal to x19 and shaped like 0x1000000003. The game gives each model's prerecorded lists fixed slots of the size a recording of that list measured (GetCommandMemoryUsed), slot k at base + size × k, and passes AddCommandMemory max(size, 512) (main+0xbb120); its memory callback is empty. A JUMP the host wrote after a 496-byte list reached 8 bytes into the next slot. The host now never writes past what GetCommandMemoryUsed reported (OP_RESERVE, Prerecorded lists in slots of their measured size).

Services

Files and shaders

  • Current: prewarm capture reported in PR #261. The seed in pso_manifest.tsv contains 1,537 shader, 2,512 render-pipeline and 43 compute-pipeline rows, recorded on an M2 iPad Pro with update 1.4.3 on 2026-10-25 over about 33 minutes: boot, title, a save load and overworld exploration. The author reports no pipeline compilations in the second session's long frames with this list. This covers the recorded route, not the whole game; no new gameplay or cold-install validation here. The capture is separate from the shared deswizzle and symbol-index optimizations.
  • Most RomFS files are zstd with dictionaries (*.zs); the dictionaries are Pack/ZsDic.pack.zs, which is also the profile's romfs_marker.
  • Shaders: Shader/*.bfsha.zs (BNSH; material.bfsha holds most) and the layout shaders in Shader/ApplicationPackage.Nin_NX_NVN.release.sarc.zs.
  • Resolved in the host: duplicate pipeline keys from unused colour state. MacNativeGames commit f3e2f66, reported in PR #196, found 8,479 of 24,146 recorded render pipelines (35%) differed only in unused state. The general normalization and prewarm deduplication are ported; this is the source project's measurement, with no new gameplay or prewarm-count measurement on this game here.

Mac routes

  • The issue #70 save: DM_KEYS="20:A*6/4,f2480:LUp~5" (title, load, jump, steer into the pond).
  • The issue #147 short streaming route uses a fresh copy of the issue #70 save and its corrected SDK module: DM_FPS_TARGET=30 DM_KEYS="20:A*6/4,f2480:LUp~9,f4000:LUp~20,f4700:LRight~5,f5000:LUp~20", run for at least 220 s. This is a fall/respawn stress route, ending at Game Over in both short comparison runs; it is not a long surviving traversal test. The movement timings are repeatable, but the upload burst varies with streaming and CPU/GPU timing, so the deterministic staging regression complements it.
  • The in-cave autosave (run/save): keys f2500:A,f2800:A,f3100:A,f3600:A, shot at frame 4500. DM_DETCLOCK cannot be used: the game stops at frame 1 waiting for guest time.

Found on this game

General host changes first needed here; each applies to every game:

  • Resolved in the host: frame-step recovery after a 30 fps movie cap: The frame step after a 30 fps cap ends. Modelled with the movie flag; no new gameplay recovery observation is claimed.

  • Packaged texture block dimensions and linear storage rows: Packaged texture block dimensions and linear storage rows.

  • Resolved in the host: dirty-page writes missed during upload, with both unprotect paths covered by regression tests; the glyph-cache symptom remains unconfirmed here: Dirty-page writes missed during upload.

  • Depth16 load-clears match fragment writes: Depth16 load-clears match fragment writes.

  • Pipeline keys omit absent-target blend and mask state, and old prewarm rows are normalized and deduplicated: Pipeline keys omit unused colour state.

  • Bounded upload staging and retirement on upload completion: Upload staging grows within one rendering submission.

  • Resolved: registry lock priority inversion (g_rmu): Registry lock priority inheritance. Ported from MacNativeGames; checked here through the Metal registry view tests, without a new gameplay contention measurement.

  • Conditional rendering evaluated at replay (OP_RENDER_COND, profile key render_cond) and CPU-written textures with no flush (tex_dirty_upload, ft_add): Conditional rendering evaluated at replay, CPU writes with no flush, caught by page protection.

  • Mid-pass, partial-mask and partial-scissor clears drawn as a triangle (clear_color_tri / clear_ds_tri), 8-bit indices widened on the GPU when written in the same submission (wide_run), ReadTexels from guest memory on any thread with nvn_ft_poll marking textures written after first use, attachment-less passes (bare_dims): same section.

  • Rewriter: out-of-line atomic retry blocks (retry_block / site_retry_window, 8 sites) and jump tables relative to an adr (jump_adr_offsets, main+0x393bb0): Out-of-line retry blocks of atomic updates, Offset tables relative to an adr.

  • Resolved: black screen at boot since 9a56ecf (issue #247), handoffs that waited for the whole core: Same-core wake handoffs. Each wake's handoff waited for every runnable higher-priority thread pinned to the caller's core. BootupBackgroundWorkThread (priority 58) then waited 200–290 ms at a time for ARes Loading 1, Presentation Thread and BaseProcExecutor, threads its wake had not made runnable. Checked on the Mac with update 1.4.3 and no shader libraries for chain 8393d85a4eae (draws skipped), the issue #70 route for 180 s. On 82ab25b the scene stayed empty (84 functions 771 calls a frame, no texture uploaded), and handoffs waited 250,008 ms in total. With handoffs limited to the threads the wake made runnable, they waited 1,221 ms, and the run matched 9feac2f (before the change) frame by frame: 11 first uploads in both, 14,500–20,500 calls a frame. Gameplay with shaders has not been rechecked.

  • Slotted command lists (OP_RESERVE, t_nvn "slotted lists"): Prerecorded lists in slots of their measured size.

Open

  • Texture layout: NVN's automatic block height now truncates for every game (pick_tile(ah * 3 / 16)). That changes layouts at 6–10, 17–21, 38–42 and 81–85 block rows, which matters for this game's flushed textures. Not re-checked here.
  • game.tsv still describes the game as "In bring-up", and some memory values as "Breath of the Wild's", not measured for this game.