Skip to content

hle_fs: support mount-list Commit for one host save transaction - #25

Closed
BeeeeeMo wants to merge 163 commits into
seedds:mainfrom
BeeeeeMo:fix/mount-list-commit
Closed

BeeeeeMo wants to merge 163 commits into
seedds:mainfrom
BeeeeeMo:fix/mount-list-commit

Conversation

@BeeeeeMo

@BeeeeeMo BeeeeeMo commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The host takes over nn::fs::Commit(const char*), but not Commit(const char* const*, int). A game using the mount-list overload enters native filesystem IPC instead of committing the host transaction. With no native filesystem/service-manager session, the SDK aborts and leaves the writes in the uncommitted work directory.

Observed on Animal Crossing: New Horizons (base v0, SDK 9), using the native Mac host on Apple M2.

Change and limits

Both overloads use the existing clone-and-atomic-swap transaction engine. The list overload validates every mount before committing once when all names refer to the same host backing transaction. Duplicate names and existing aliases do not cause repeated commits. Write-failure rollback, commit errors and callbacks are preserved.

Lists spanning independent backing transactions are explicitly refused before either is published. This does not implement Horizon's full multi-save atomic transaction support. The existing single-name behavior is unchanged.

Verification

Isolated branch based on 06d9375, using the checked-in Super Mario Bros. Wonder profile for build headers:

  • The new regression compiles against unchanged upstream hle_fs.c and exits 1 because the exact SDK symbol is missing. With the fix, 30 checks pass through the registered big-stack trampoline.
  • Tests cover persisted bytes and remount, poisoned-write rollback, retry, duplicates/aliases, invalid lists, snapshot-clone failure, and refusal of partial multi-save commits. Fixtures are synthetic; no game files are needed.
  • Existing t_fs: 68 checks pass. Full Mac host builds; arm64 iOS source syntax/type check passes. No iOS runtime test or full host-suite claim.
  • git diff --check passes. The bundled native-write scan passes its self-test, but its repository scan fails on both base and patch because it references the absent dm_vault_ui.c; that unrelated scanner was not changed.

The earlier local game test (c628df1 plus local bring-up hooks) loaded a recovered save, saved back to the title screen, and loaded it again in a fresh process. That reproduction also used local bring-up hooks and a separate clock-difference fix; those are not included here. This PR contains no game profile, save recovery tool, clock/audio change, or claim of full game support.

make -C dm/host GAME=010015100B514000 game_profile
make -C dm/host/tests GAME=010015100B514000 build/t_fs_commit_list build/t_fs
dm/host/tests/build/t_fs_commit_list
dm/host/tests/build/t_fs
make -C dm/host GAME=010015100B514000 dmhost

seedds added 30 commits October 12, 2026 22:04
Generic host, gfx and tools taken from d2r-mobile (MIT), with the D2R
gameplay, diagnostics and Blizzard/Granny/movie layers removed.

- hle_hid.c: generic standard-controller layer (all Npad styles)
- hle_sdk16.c: newer-SDK oe/time/pl/vsync/diag hooks
- hle_offline.c: offline stubs for prepo/ldn/socket/ssl/nifm/nsd/friends/ngc/err
- hle_fs.c, hle_account.c: SDK 16 overloads
- loader.c: unreferenced forced hooks only logged (DM_FORCE_STRICT=1 aborts)
- NVN: API 55.15, 13 newer names, ~45 state-only functions, tex_t packed
  to 0x80 (Mario allocates NVNtexture as 0x80; 0x88 corrupted the heap)

Frontier: null read at main+0x64763c inside the frame loop.
- hle_svc.c: guest AS 6 GB with a 1.25 GB alias region (newer SDK's
  AddressRegionManager always takes ~1 GB of it as its node area),
  MapPhysicalMemory rounded to 16 KB host pages, GetInfo(UserExceptionContext)
  backed by a real buffer, program id now SMBW (DM_PROGRAM_ID overrides)
- hle_audren.c (new): silent AudioRenderer, event fired every 5 ms
- hle_fs.c: SetPriorityOnCurrentThread / IsEnabledAccessLog (fsp-srv)
- hle_sdk16.c: settings language comparison; diag aborts report the call
  site and backtrace
- NVN: nvnProgramSetShadersExt (0x18-byte stage entries)

The game now runs ~40 threads; the main thread waits on job events while
render commands (CallCommands, BindTexture, BindVertexBuffer, DrawElements,
WriteTexels) are still unimplemented.
- BindTexture (combined handles go straight into the texture slot; the
  replay already splits texture / sampler ids), BindImage,
  BindTexturesDeferred, BindVertexBuffer, CallCommands
- DrawElements / BaseVertex / ArraysInstanced; viewport, scissors and
  depth ranges in single / multi forms
- ReportValue via the event-write record; new OP_CLEAR_BUF record with
  in-order replay (fillBuffer, or a pattern buffer for non-uniform values)
- TextureWriteTexels: deswizzle level, patch region, swizzle back, after
  lifting write protection
- Command memory: when the game's callback cannot supply enough, continue
  in a host segment instead of aborting
- Remaining state the replay lacks (conditional render, depth clamp,
  rasterizer discard, ...) is logged once and drawing continues

The game now runs its frame without errors from these; the main thread
waits in the job system's fork-join (executors keep polling).
- patch_nso.py: fix exclusive-window exit order. The fall-through stub
  must come right after the copied window; it was emitted last, so a
  successful stxr in 'ldxr; cmp; b.ne out; stxr; cbnz' fell into the
  branch-out stub (main 0x81c40: job-system enqueue retried forever,
  tail filled to capacity with no task stored). Bug inherited from
  d2r-mobile, latent there (no branch-out windows).
- patch_nso.py: rewrite 'mrs xN, ctr_el0' (trapped on macOS) to a
  constant with 64-byte data cache lines.
- hle_thread.c: SetThreadCoreMask moves the ideal core into the new mask
  (Horizon behavior), so pinned threads report the right core.

Frames 2 and 3 now record, submit and present (~1,900 NVN calls).
Next: Mario's shaders (bfsha) are not in the translation map yet.
Shaders
- shader_split.py (new): scan RomFS and main for every BNSH and agl BAHS
  container (bfsha, particle effects, layouts, agl/gsys packs, main):
  12,811 shaders, map + compute facts from the control section
- key = sha256(control + code): many shaders share a control section
- NVN 55 binding layout: bound textures at 0x20 + 8*slot, images 0x120
- bindless textures read from uniform buffers (tex_ubo / samp_ubo)
- GL depth mode: translator remaps z' = z/2 + w/2 in vertex shaders
- compute code areas carry a 0x30 header (magic 0x12345679)

Replay
- 8-bit index buffers widened to 16-bit
- texture swizzle through Metal swizzled views
- non-packaged textures shared by storage (fixes a ~13 MB/frame leak
  from transient render targets rebuilt every frame)
- view_t packed to 16 bytes (SDK 16 NVNtextureView; fixes a stack smash)
- TBDR optimizations off by default until they know bindless readers

Host
- alias region 2.875 GB for the 1.75 GB VirtualAddressMemory reservation
- ideal core -2 resolves to the process default core
- shared font served with a BFTTF "decoded" header
- crash reports scan the stack and dump the popped frame

Docs: docs/agents (guides for porting other titles), AGENTS.md
- DM_KEYS scripted controller input ("seconds:button[+button][~hold]")
- NVN virtual memory pools aliased onto their physical pool when every
  MapVirtual request shares one base (Mario's 128 MB pool); previously
  MapVirtual failed and the game treated the pool as unusable
- DM_PROBE ported from d2r-mobile (host/probe.c): brk probes with
  register / backtrace output, plus DM_PROBE_X2 object dumps; used
  because lldb launches now wait on a macOS authorization prompt
- diagnostics: DM_NVN_VLOG (pools, MapVirtual), DM_NVN_CBLOG (command
  memory grants and callbacks), DM_NVN_ALLOCPROF
- docs: new failure patterns and the probe recipe

Open: starting a new game crashes while loading the opening course
(model resource heap overflow for NpcCommonerA.bfres).
…PU-written text

- patch_nso.py: Mario main cut gives every resource heap +16 KB; Model/NpcCommonerA.bfres
  came up 0x70 bytes short of its size-table budget while loading Course900
- dmsh_build.py: compute instructions start at code area +0x100, not +0x30; the misaligned
  decode turned 12 particle / post-effect compute shaders into endless loops (GPU hang)
- Linear textures in CPU-visible pools are built on the pool buffer, so the CPU-rasterized
  glyph cache reaches the GPU (menu and dialogue text)
- Host command-memory segments grow on demand; RGB10A2SN vertex format; instanced DrawArrays
- DM_PROBE: thread IDs, register-relative dumps, hit filters, ExpHeap walker
- dm/tools/ghidra/Decompile.java; agent guides and README updated
…y colour ramps

- dmsh_build.py: vertex shaders whose inputs are only used as integer bit casts are
  re-translated with uint inputs (529 shaders); bone indices were converted to floats and
  every character was stretched off screen
- Host: Metal vertex formats follow the shader input type (signedness swap, 32-bit float
  reinterpretation), mismatches reported
- LINEAR_RENDER_TARGET textures in CPU-visible pools are zero-copy too (sky colour ramps)
- Pass dump lists all vertex attributes; docs and README updated
- dm/ios: lean app shell (main.m boot/env/log, front_ios.m front.h + 16:9 Metal view,
  DmGamepad.m touch gamepad in Nintendo positions + GameController), xcodegen project,
  tools/build_ipa.sh (embedded game code, iOS shader libraries, RomFS in the bundle)
- Guest address space back to 5 GB (heap 1.625 GB; the game asks for 1.47 GB)
- README / agent guide: iOS build steps and address-space sizing
…nt, ordered copies

- Audio: run the sdk's own audio renderer on the CPU in manual mode (hle_audren.c),
  feed its DeviceSink output to the AudioOut sink; catch-up render on back-to-back
  updates fixes the ApplyLightLimiterEffect crash at the title screen
- Button icons: make_ext_font.py generates a stand-in Nintendo extension font
  (A/B/X/Y, L/R/ZL/ZR/SL/SR, +/-, D-pad, stick arrows); served as shared font type 5;
  the iOS bundle now carries both shared fonts
- Fountain panels: Metal 4 does not order copies inside one encoder; add barriers
  when ranges overlap, so the GPU wave simulation no longer reads a stale uniform
- Zero-fill new colour textures; scissored zero clears; uniform-update snapshots
- Docs: graphics ordering, audio, diagnostics, intermittent-glitch method
Ranges are rounded to 16 KB host pages but NVN pools are only 4 KB
aligned, so adjacent pools share their first / last page and one
address can fall in several registered ranges. Every lookup took the
first range containing the address:

- textures whose pool's first page is shared with a smaller preceding
  pool were judged untracked and re-uploaded on every use (two ASTC 6x5
  textures in the opening course: 12 re-uploads, about 6 MB, per frame);
- a fault or host pre-write on a shared page released only one range,
  leaving the other marked protected;
- destroying a pool could unregister its neighbour (same rounded base).

Tracked / protect / since now use the range that holds the whole span;
faults and host pre-writes release every range covering the page; pool
removal matches the exact pool start. Opening course: 3600 re-uploads
per 300 frames -> 0; DM_NVN_DIRTYCHECK=1 over 3900 frames: 0 misses.
t_dirty_slots covers the shared-page case (3 checks fail on the old code).
A sideloaded app cannot be given environment variables from outside.
main.m now reads KEY=VALUE lines from Documents/env.txt and from
dm/env.txt in the bundle (Documents wins, real environment wins over
both) and logs each applied line as "boot: 环境 ...".
build_ipa.sh copies DM_ENV_FILE=<file> into the bundle as dm/env.txt.
dmsh_chain.py --update-lock on a fresh Mac (Xcode 27.0, metal 32023.921
unchanged): the translator DLLs built here and Homebrew's spirv-cross
have different fingerprints. Chain b1a4876b9511; dmsh_build gives the
same result as documented (12809 / 12811 compile, the same 2 agl compute
shaders make SPIRV-Cross throw) and c1_full.tsv lists the same 11 shaders.
Another machine with other tool builds needs --update-lock + --header.
Opening course on an iPhone 18 Pro Max (A20 Pro GPU): black diagonal
band, black-edged white glow at the horizon, grey striped blocks,
alternating frame by frame. The Mac (M4 Pro) never shows it, not even
with the iOS metallibs, display pacing, -Os, DM_3D_SCALE=0.75 or the
no-BC path.

On-device colour-target stats (DM_DBG_TEXSTAT, diagnostic commit that
follows): even frames' main 1920x1080 RG11B10 target has 54 NaN pixels
(Mac: 52); odd frames 727k NaN, and the 960x540 / 1100x280 / 275x70
targets downsampled from it carry it on. The NaN is already in the main
target before the first downsample (pass 47 of 80), so it comes from
the scene passes, not the post-effect chain.

The translated shaders reproduce Maxwell's NaN rules with
select(x, 0, isnan(x)) (8337 / 12809 shaders). Default fast math
records air.compile.fast_math_enable, which lets each GPU's backend
compiler assume no NaN / Inf; relaxed keeps the reassociation /
contraction but preserves NaN / Inf. Hypothesis only: chain
1b46c9a2e920 (MSL byte-identical to b1a4876b9511; c1_full.tsv lists
the same 11 shaders) still has to be run on the iPhone. If the NaN
persists, the next step is finding the producing draw in passes 12-45.
Temporary instrumentation for the A20 Pro NaN investigation; every line
is tagged [DEBUG-flk1] so it can be removed with one grep.

- DM_DBG_TEXSTAT=N with DM_NVN_TEXDUMP=<frame>:<path>: for N consecutive
  frames, log one line per colour target instead of writing PNGs:
  per-channel mean, NaN / Inf counts, largest finite value, black share.
- Inside the DM_NVN_PASSDUMP window: each submission's commit->completion
  time and the counter values written back (timestamps / samples passed).
- DM_NVN_SHOT accepts up to 4096 frames (was 16).

Device capture used so far (unsigned IPA, DM_ENV_FILE for build_ipa.sh):
  DM_NVN_TEXDUMP=3600:td_%d.png
  DM_DBG_TEXSTAT=2
  DM_NVN_PASSDUMP=3600-3601
  DM_NVN_SHOT=3600,3601,3602,3603:shot_%d.png
  DM_NVN_SHOT_OUT=960x540
Fixes the flicker / black band in the opening course and the crash after
the first scene on the iPhone (A20 Pro), confirmed on the device.

- nvn_obj.h: cmd_t leaves byte +0x10 of NVNcommandBuffer to the game.
  Mario's inlined NVN code writes 1 there (main+0x4da5c, main+0xb8dec).
  It used to hit the low byte of `end`: the game's 512-byte material
  setup blocks shrank and overflowed into host segments,
  GetCommandMemoryUsed (cur - begin) returned 0, the game marked the
  setups empty and skipped calling them, and draws ran with the previous
  vertex buffers (out-of-range vertices, NaN).
- hle_nvn.c: GetCommandMemoryUsed counts bytes across host segments plus
  16 for the jump. Host segments are released only when the game
  re-grants memory overlapping the recording's guest bytes (the game
  keeps calling recorded setup blocks for many frames); the 8-frame
  recycling is gone.
- nvn_mtl.m: failed pipelines are cached and reported once with their
  vertex / fragment keys instead of being rebuilt on every draw.
- front_ios.m / main.m: the audio session asks for a 5 ms I/O buffer at
  boot and on return to the foreground. The default (~21 ms, 1024
  frames per callback) outran the 4 x 5 ms the renderer keeps queued:
  every callback underran (crackling). Device: 240-frame callbacks,
  0 underruns.

Still contains [DEBUG-flk1] instrumentation (tagged, mostly behind
DM_DBG_* switches; audio statistics every 2000 callbacks in
hle_audio.c). To be removed with one grep.
Metal builds a pipeline only when every pixel shader input is a vertex
shader output; Maxwell gives an unwritten attribute a default. With
shaders translated one at a time, 331 of v0's 9,043 vertex / pixel
pairs failed to link and later levels lost those draws (about 11,500
failed pipeline builds in one device session).

- shader_split.py writes shader_pairs.tsv (pairs of every BNSH
  variation) and takes comma-separated RomFS / main lists for several
  game versions, writing their union (keys are content hashes; v0 rows
  keep their package indices).
- dmsh_build.py vs_pad(): vertex shaders get the outputs their partners
  read but they don't write, float4 (0, 0, 0, 1); vertex shaders with
  partners are finished last and their partner list is part of the
  cache rule. shader_pairs.tsv is part of the chain version.
- Tables now cover v0 and update 1.2.1: 16,268 shaders (1.2.1
  recompiled 3,451 SecredUber pixel shaders and added 6 effect
  shaders), 14,603 pairs. Chain 5f1ec82dfcf0: 16,266 compile (the 2
  known SPIRV-Cross failures); an offline link test of all pairs gives
  14,603 / 0 (v0 before the padding: 8,712 / 331). c1_full.tsv
  regenerated (same 11 keys).

The v0 IPA with this padding played 11 minutes on the iPhone with no
pipeline failures.
…calls

- dm/tools/nx_extract (C#, LibHac 0.19): hactool stops with "Invalid
  BKTR layout!" on the update's patch NCA. nx_extract reads the NCAs
  straight from both NSPs, imports their common tickets, merges the
  update's RomFS over the base game, prints the display version and
  module build IDs, and writes files identical to a base RomFS as APFS
  clones (the merged 1.2.1 RomFS adds about 114 MB).
- dm/tools/nso_fnmap.py: carries offsets between builds by matching
  eh_frame functions with address bits masked; globals through their
  adrp references.
- patch_nso.py: resource heap +16 KB cut for 1.2.1 main ff773e90...
  (main+0x1cf9b0, mapped from v0 0x1e62fc; the function is identical
  after masking). fixups.tsv now records the module build ID; the
  loader keeps it in module_t.build_id.
- hle_oe.c: the 3D render scale inherited from d2r-mobile writes and
  calls fixed main addresses (0x31e0848, 0x8582f0). In Mario v0
  0x8582f0 is the middle of a static initializer; it only did nothing
  because the pointer read 0. Now enabled only on D2R's build.
- hle_misc.c: owned add-on content defaults to none except on D2R's
  build (DM_AOC_OWNED overrides).
- New imports in 1.2.1 answered as a Switch 1 without amiibo:
  nn::nfp (one NFC device on the connected controller, detection never
  finds a tag; nfp::Initialize at boot used to abort in nn::sm),
  pl::IsRunningOnOunce (false), ec::ShowShopApplicationInformation-
  ForUpgrade (no-op), account::PopOpenUsers (none: the game falls back
  to TryOpenPreselectedUser as in v0). oe::GetDisplayVersion by build
  (1.0.0 / 1.2.1).

Mac: 1.2.1 and v0 both run the scripted opening for 120 s with only
the known SetRenderEnableConditional red line. iPhone: 1.2.1 played
8.5 minutes with no crash.
- README: status for v0 / 1.2.1 on the iPhone, update 1.2.1 build
  steps, diagnostic env files, finishing the IPA by hand when Xcode
  uses a custom build location, app data folders after a reinstall,
  prewarm lists (pso_user.tsv / pso_manifest.tsv).
- AGENTS.md: per-build knowledge keyed by build ID, shader_pairs.tsv on
  the chain, unsigned IPAs only.
- docs/agents: update extraction and offset porting (01), the
  command-buffer byte, command memory, iOS audio, update imports and
  build-gated D2R code (03), vertex output padding and iOS math mode
  (04), on-device diagnostics and the offline link check (05).
- dm/gfx README / nvn_metal_mapping (en + zh-CN): pairs table, several
  game versions, prewarm lists, DM_PSO_USER, command buffer notes;
  corrected shader key, build script and 8-bit index lines.
- dm/host/README(.zh-CN).md removed: an index of d2r-mobile's D2R
  features; most files it listed no longer exist.
- DmGamepad.m: layout after a PS remote-play pad. ZL above L and ZR
  above R (teardrops) in the top corners; -/+ in the bottom corners
  with the D-pad (four arrow pads) and the A/B/X/Y diamond just above
  them; left and right sticks next to those along the bottom edge.
  Everything hugs the edges to leave the picture room. Layout
  constants are at the top of the file.
- Sticks are fixed rings (tilt from the ring centre, 0.12 R dead zone)
  instead of the floating left stick; the new right stick merges with
  a physical pad's right stick the same way as the left one.
- One CAShapeLayer per control instead of a full-screen drawRect: a
  press swaps the fill, a stick move only moves its knob. Colours
  carry low alpha (fill 0.14, outline 0.38, labels 0.6) rather than a
  view alpha, which would add an off-screen pass.
- front_ios.m header and the README app-shell line follow.
- dm/ios/tools/build_ipa_from_nsp.py (Spyder or python3): picks the
  base game and update 1.2.1 by title ID (DLC is skipped), extracts
  them with nx_extract, checks the main / sdk build IDs, rewrites the
  modules, splits the shaders and requires the tables to match the
  repo's byte for byte, builds the iOS shader libraries, then runs
  build_ipa.sh. It uses only the repo, the NSP folder and prod.keys:
  intermediates go to <NSP folder>/smbw_build/ (full step logs in
  logs/), and the extracted game files are deleted after success.
  Tested end to end: first run 33 min, re-run 10 min.
- dm/gfx/tools/dmsh_xlate_bin: the translator build that matches
  dmsh_chain.lock. A fresh dotnet publish gives different dmsh_xlate
  and Ryujinx dll fingerprints, which dmsh_build refuses.
- build_ipa.sh: when Xcode's Locations setting is a custom location,
  take the app path from the build log instead of stopping at
  "no built app".
- README: the script; the custom-location note replaces the by-hand
  finish.
- nx_extract --manifest: write ExeFS and romfs_manifest.tsv (path,
  size, first 32 bytes of every file) instead of the RomFS, so a large
  title can be surveyed without writing its game files.
- nx_extract prints the build ID of every NSO module, the display
  version and the game name, and saves the raw control.nacp next to
  the output for naming the app.
- Game name: newer SDKs compress the NACP title area (u16 size + raw
  deflate, inflating to 32 languages x 0x300; Mario 1.2.1 does this).
  LibHac 0.19 does not decode it, so nx_extract does.
- nso_syms.py: '-' for the reference game compares one game's imports
  with the host hooks only.
- dm/games/<id>/game.tsv holds what is specific to one game: title and
  program ID, known builds (module build IDs, display versions),
  package and shader versions, code modules, RomFS marker, zip store
  list, memory split, NVN driver constants, shader split groups, app
  name / bundle ID / IPA name. Mario's profile is smbw, the default.
- The shader tables, c1_full.tsv, pso_manifest.tsv, bf_want.tsv and
  dmsh_chain.h move from dm/gfx/nvn to dm/games/smbw.
- dm/games/game.py loads profiles (DM_GAME, by title ID), answers
  shell queries and generates the host's game_profile.h.
- Shader tools (dmsh_chain, dmsh_build, dmsh_facts, shader_split,
  tbdr_budget) take tables and constants from the profile. dmsh_build
  stops if the profile's texture / sampler bases differ from the
  translator's built-in ones. dmsh_chain hashes the profile's nvn_* /
  shader_* keys; the chain version is now 02ed42db694f. All 16,266
  translated shaders are byte-identical to 5f1ec82dfcf0's, and
  c1_full.tsv differs only in its header.
- Host: GAME ?= smbw in the Makefile; memory split, display versions,
  NVN bases, default modules, window title and the shader map path
  come from the profile. D2R-only code (Settings.json edits, the
  main+0x3509a0 read) runs only on D2R's build, which also stops a
  stray Settings.json in Mario's iOS save.
- iOS: build_ipa.sh, gen_build.py, project.yml and main.m take the
  app name, bundle ID, category, modules, RomFS marker and tables from
  the profile (GAME=<id>, workspace ~/<id>-work).
- dm/games/README(.zh-CN).md: the keys and how to add a game.
- The game comes from the NSPs' title ID and its profile in
  dm/games/; the versions to extract, the accepted build IDs, the
  modules to rewrite and the tables to compare all come from the
  profile. TITLE / BUILDS are gone from the script. Work folder:
  <NSP folder>/<id>_build/.
- A title with no profile is surveyed instead:
  <NSP folder>/<title ID>_survey/survey.md lists the NSPs, modules and
  build IDs, SDK version and components, Unity version, engine / API
  markers, RomFS folders, magics and extensions, insn_census counts,
  imports vs host hooks, and notes. Fire Emblem Engage was surveyed
  this way.
- APP_NAME setting; empty uses the game name from the base NSP's
  NACP (compressed title areas decoded), then the profile's app_name.
- PACKAGE and OUT_IPA default to the profile's package and ipa.
- Tested on Mario end to end from fresh extraction: the IPA is named
  "Super Mario Bros. Wonder" and carries the current chain's tables.
- README: presented as Switch games running natively on Apple
  platforms, with Mario as the one supported game. A games table
  (Mario playable, Fire Emblem Engage surveyed), component status,
  game profiles and how to add a game, the NSP script, generic
  step-by-step build commands (profile, workspace and version as
  variables), and a Mario section for the per-game notes (1.2.1).
- AGENTS.md: profiles, DM_GAME / GAME, workspaces, build-ID gating,
  the chain rule per profile.
- docs/agents: one repository with one profile per title; survey
  mode in 02-survey, including the compressed NACP title area.
- dm/gfx README (+ zh-CN): table paths in the profile.
- hle_nvn.c: drop the command memory callback / grant / caller
  logging, the overflow details, the +0x10 write log, the one-off
  NVN table dump, and the DM_DBG_GRANTS, DM_DBG_CALLSITES,
  DM_DBG_CMDMEM_SHRINK and DM_DBG_OLD_USED switches.
- nvn_mtl.m: drop the per-draw NaN probe (DM_DBG_NANPROBE), the
  colour target statistics (DM_DBG_TEXSTAT) and the PASSDUMP
  completion timing.
- Kept as regular diagnostics: the audio callback line every 2000
  callbacks (frames per callback, queued blocks, underruns) and
  DM_DBG_AU_FRAMES (docs/agents/03-bringup.md, crackling on iOS);
  DM_NVN_SHOT up to 4096 frames (the 05-debugging burst recipe).
- Mac 1.2.1 scripted run reaches the opening course with the same
  single red line as before.
- attach(): a render-target view that changes the format is attached
  as a Metal view of that format (kept until the submission completes),
  so pipeline formats follow it. Mario's last pass writes the RGBA8
  swapchain texture through an RGBA8_SRGB view; with the base texture
  attached, linear values were stored unencoded and the whole picture
  was darker: on the Mac and the iPhone every channel equalled the
  Switch value decoded from sRGB once more (0.55 -> 0.26). After the
  fix the Mac title frame matches a Switch screenshot (mean RGB
  0.556 / 0.621 / 0.633 vs 0.557 / 0.621 / 0.633).
- present_raw_src(): an sRGB-typed swapchain texture is sampled through
  a non-sRGB view, so the stored bytes reach the screen as on the
  Switch's scan-out (no change for Mario, whose swapchain is RGBA8).
  t_present_crop gets an sRGB case (34/34; fails without the fix).
- docs/agents/04-graphics.md and the mapping docs: the rule and how it
  was measured.
- patch_nso.py: three more x18 forms from IL2CPP code: an offset jump
  table (targets read at rewrite time), blr x18 (original return
  address kept for C++ unwinding) and an sp write from x18 (alloca).
  Mario's rewrite output stays byte-identical.
- loader.c: symbols resolve globally in load order, also for a
  module's references to its own exports (local / non-default
  visibility excepted). Engage's main overrides operator new / delete;
  sdk bound its own copies and freed Unity's blocks into the SDK heap.
- hle_svc.c: QueryMemory reports each module's Horizon layout (code,
  read-only, data, patch area) and other host memory as free, which
  the SDK's C++ unwinder (nn::ro::detail::GetExceptionInfo) needs.
- New takeovers: nn::fs::IsMounted, nn::ro (hle_ro.c), prepo
  Initialize, SDK 12 audio device names and OpenDefaultAudioOut, an
  SDK 12 hid block (single GetNpadState, modes, touch, gestures,
  six-axis details, colors, applets), movie::Decoder with no impl
  (hle_movie.c) and failing nvdrv opens (hle_nvdrv.c), and NVN getters
  / no-ops Engage calls. nvn.h: TextureGetMemoryPool returns a pointer.
- Makefile: a game stamp, so changing GAME rebuilds the host.
- docs/agents/03-bringup.md: catalogue entries for all of the above.
- Mario 1.2.1 scripted run still reaches the opening course with the
  same single red line.
- dm/games/fe_engage: identity and the 2.0.0 build IDs (main,
  subsdk0, sdk), modules in Horizon load order, RomFS marker, empty
  shader tables and their chain header. Memory: an 8 GB reservation
  with a 5.5 GB alias region for its virtual address memory (Mac only
  for now: iOS allows at most 6 GB without the extended-addressing
  entitlement); NVN constants are Mario's until measured.
- On the Mac the host boots Engage through Unity's startup and runs to
  the opening movie with nothing on screen yet: its shaders are in
  UnityFS bundles that shader_split.py cannot read, and movies are not
  decoded.
- README: the games table row.
- New dm/gfx/tools/unityfs.py: UnityFS bundles (LZ4 / LZMA blocks),
  serialized files (formats 19..22), objects read by their type trees
  (Unity's common string table comes from the game's own main), and a
  Shader object's per-platform compressed subprograms. The player's
  own files have no type trees and borrow the bundles' Shader tree of
  the same type hash.
- shader_split.py reads Unity Shader objects in a second pass: each
  Switch subprogram is one NVN program (0x60-byte header of per-stage
  offset / control size / code size, then control + code per stage);
  a program's vertex / pixel stages form a pair. Engage: 9,269
  shaders, 4,634 pairs. Mario's tables and package are byte-identical.
- Engage profile: shader groups engine / assets, measured NVN layout
  (texture handles 0x168 + 8 r, 128 slots; separate samplers 0x568),
  regenerated tables, chain a295a8035b7b. 9,268 of 9,269 compile (one
  geometry shader; Metal has none).
- dmsh_build.py: the translator's built-in bases only matter with
  per-slot facts, so the guard checks that; c2 reads count the sampler
  table and 16-byte rows overlapping a table as fillable.
- game.py: DM_GAME_NVN_SAMP_BASE. NSP script survey note updated.
- Mario chain is now 271056163ea2 (Mac and iOS libraries rebuilt,
  16,266 of 16,268 as before; c1_full regenerated).
seedds and others added 23 commits October 20, 2026 09:57
- The idle timer is off while the app is active: only touches reset it, so with a controller or in a cutscene the screen dimmed and locked.
- Open file limit raised from 256 to 4096 at host start; Documents/romfs and temp are kept out of device backups.
- Audio: observers for audio session interruptions (Siri, alarms, calls) and media services resets (dm_audio_reset recreates the AudioUnits); becoming active ends an interruption whose "ended" never came.
- A watchdog switches a started output whose device has not called back for 200 ms to the silent sink and retries the AudioUnit after 1, 2, 4, 8 s; a start that fails falls back the same way instead of aborting. t_audio scenario 3 stops a playing AudioUnit behind the host's back.
- GetCurrentProcessorNumber: a thread with no ideal core reports the lowest core of its mask, not core 0.
- 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.
…ad spun a core on iOS)

- MessageSuperviseThread (main+0x6bc8fc4) waits on oe::GetNotificationMessageEvent with WaitAny, which does not clear it, then pops until the queue is empty. The host only ever signalled the event, so after the focus change at iOS launch it stayed set and the thread looped: 995 ms/s, 5-6 W on an iPhone.
- hle_oe.c mq_ev_sync: the event is set while a notification is queued and cleared when the last one is popped (as the am service does), decided under the queue lock; hle_thread.c hle_clear_system_event.
- DM_OE_SCRIPT also starts at the game's first GetNotificationMessageEvent, for games that pop only once the event fires. Mac with focus=2@5,focus=1@6: before 995 ms/s, CPU 109%, 6.97 W; after the thread is idle, 0.05 W. Hades still pops its notifications.
- t_oe: stubs for the system event calls (it no longer linked) and 6 checks of the event state; 53/53.
- TOTK moves from "being ported" into the games table (60 fps on the iPhone, 30 on the Switch); DM_FPS_TARGET=30 in env.txt keeps 30 for battery.
- Monster Hunter Rise: runs on an iPhone at 30 fps from character creation into the opening.
- AGENTS.md: thirteen playable games.
…ost)

- 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.
…n whole vsyncs decided by the host

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).
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.
…nd draws with no target (from PR seedds#11)

Tears of the Kingdom's three remaining red lines, with the fixes PR seedds#11 found:
- A BC4 texture written through an RGBA16UI view (BC4 encoded on the GPU; TOTK a 32x32x32 3D texture one depth
  plane per pass, BotW a 32x32 one) could not be attached, so its draws were lost. Such a pass now draws into an
  uncompressed shadow of blocks x blocks; the blocks are copied in through a buffer before the pass and back after it
  (nvn_mtl.m bcrt_attach / bcrt_flush). Shadows stay resident and are reused, and they are kept out of the
  load-skip / store-elision bookkeeping.
- An agl fragment shader that only stores to a buffer draws into a 1x1 viewport with no target set: the pass is now
  attachment-less at the viewport size (bare_dims) instead of skipped. The opening cave gets the ambient light it
  lacked.
- nvnDeviceRegisterFastClearDepth returns success; nvnCommandBufferSetAlphaRef is logged once.

Neither game has a PC version, so the translator declared every fragment output float and Metal refused the
encoders' integer targets. The profiles' shader_facts_manual.tsv declare them (TOTK 0544282c162305db and the
Ultrahand mask e3fb0bb58a691e10, BotW 8e2cc2671cae6020), which changes only those two chains (743f7a102a72,
f5259a6c6fb6; Mac and iOS libraries and c1_full.tsv rebuilt). A pipeline red line now prints the row to add when a
float output meets an integer attachment.

Mac: the 3D BC4 texture was all zeros before and holds encoded blocks in all 32 planes after (raw dump, frame 1500);
TOTK's three red lines are gone; BotW (two runs) and Mario Wonder show no new red lines.

Co-authored-by: digitalhivehub <141404180+digitalhivehub@users.noreply.github.com>
…scale line only on request

With the frame step at 2 vsyncs every frame stays on screen 33.3 ms, and the on-screen summary counted all of
them as dropped against the 60 fps period ("300 frames, dropped 300" in an iPhone BotW log). Each present's hold now
goes into a ring by present number, and the interval read 8 presents later is judged against its own presents'
holds: dropped = on screen longer than 1.5 x max(target period, hold x 16.7 ms). The [slowframe] diagnostic uses
the same limit. Checked with a scratch test built like t_nvn: step 2 at 33.3 ms 0 dropped, step changes 1 -> 3 -> 2
0, five doubled frames at step 1 5, discarded presents still counted.

The D2R 3D render-scale line ("main build ... has no known addresses") printed for every other game, where it read
like a failure (BotW and TOTK scale through DM_RENDER_SCALE); it now prints only when DM_3D_SCALE is set.
seedds#14)

nn::time::StandardUserSystemClock::GetSystemClockContext reads the time service's shared memory, which the host never
opens, so it returned 0x74 and wrote nothing. Unity's DateTime.UtcNow (Fire Emblem Engage main+0x4ad40c; Ace Attorney
Investigations 1&2 per PR seedds#14) then never started its clock and read 0001-01-01. The hook answers with the Horizon
layout: offset = user time minus the steady time point, the steady time point and source ID from the host steady
clock (as the clock snapshot fills it), DM_FIXED_TIME honoured. Unity only compares the context to notice the clock
being set, so it stays the same until the host clock moves against the guest ticks by more than a second.

Engage on the Mac: Unity's clock now starts and DateTime.UtcNow reads the right UTC time, monotonic; without the hook
it read 0 ticks. Wonder boot unchanged.

Co-authored-by: armyja <546323405@qq.com>
…teger-as-float attribute

Two red lines from an iPhone log of the PR seedds#11 build:
- agl's BC4 block encoder has a twin for a 2D source (93aa5e22a8202409, agl_common BAHS@0x44918; 8e2cc2671cae6020 reads
  a 3D one). Now that the BC4 target is attached, its pipeline was refused (float4 output, RGBA16Uint attachment) and its
  draws were lost. The profile's shader_facts_manual.tsv declares its output uint: chain d3a07e79011d (Mac and iOS
  libraries and c1_full.tsv rebuilt). The Mac route does not reach it; a prewarm row for the pair fails on the old chain
  and builds on the new one.
- NVN's integer-read-as-float vertex formats (*_UI2F / *_I2F, 0x51-0x68) had no Metal format, so the attribute read 0.
  They now go through the replay's CPU conversion (vconv_i2f in nvn_mtl.m): the values as floats for a float input,
  their float32 bits for an integer one. BotW's grass (uking_grass variants 3 and 7) reads attribute 2 as RG8_UI2F;
  with it the grass parts around Link's feet again (Mac A/B from the same outdoor save). No other game's Mac logs show
  these formats; Mario Wonder runs as before.
… the vertices each draw reads

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.
…deadline

Tears of the Kingdom on an iPhone ran 60 fps for 217 s, then stepped to hold 2 after three late frames
and stayed at 23-30 fps: the CPU fell from 3.4 to 1.5 GHz and from 65% to 5% on performance cores
(thermal 0, GPU time unchanged). The acquire thread's time-constraint declaration followed the hold
(33.3 ms) and every change reset its computation to 2 ms, so the performance controller parked the
game on the efficiency cores, where every trial back to 60 failed.

- hle_nvn.c: rt_period_ns declares the cap's period while the frame step holds longer for lateness
  (a 30 cap and a step the game fixes itself are declared as they are); a period change keeps the
  computation declared so far instead of 2 ms.
- front_ios.m: Low Power Mode at start and on each change, beside the thermal state.

Mac, 20 ms present lag: TOTK and BotW step to hold 2 and keep period 16.667 ms at a steady 33.4 ms;
without lag both stay at 60, game time 1.000 of real time. t_fstep 20/20, t_nvn only its known failure.
…haders, the C library's file checks and clock

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 seedds#15 (this main's string table is intact) and seedds#16
(nninitStartup sizes no heap with virtual address memory).

Much of the bring-up was found first by armyja (GitHub issues seedds#17, seedds#18, seedds#19, pull requests seedds#15, seedds#16).

Co-authored-by: armyja <546323405@qq.com>
…table (on an iPhone)

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".
…9's nn::ro::LoadModule

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 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".
…Opus C interface, CSRNG, alloc_big_align

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 Unicorn Overlord profile plays on an iPhone (A20 Pro: 60 fps, sound, a 2.88 GB footprint).
README lists it (~4 GB) with a screenshot from the Mac (the opening scene in the castle garden); AGENTS.md counts
seventeen games and notes the engine it shares with 13 Sentinels and their alloc_big_align key.
…ode, the host's allocator for Unreal

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.
The native mount-list overload opened filesystem IPC even though the host owns save writes, aborting before those writes were committed. Share the existing transaction engine and validate the whole list before publishing one backing tree. Reject independent transactions rather than exposing a partial commit.

Add synthetic regression coverage through the registered big-stack ABI, including rollback, remount, clone failure and unsupported multi-save requests. The regression fails on the unchanged base when the overload is missing.
@seedds

seedds commented Oct 21, 2026

Copy link
Copy Markdown
Owner

I am sorry that I accidentally closed the PR. I will look into it.

seedds added a commit that referenced this pull request Oct 21, 2026
nn::fs::Commit(const char* const*, int) had no takeover, so the SDK's own
code opened fsp-srv and aborted with the save writes left uncommitted
(Animal Crossing: New Horizons). Both overloads now share one transaction
engine. A list commits once when every name is the same host save; cache
and temporary storage are skipped as the single-name Commit does, and a
list of two separate saves is refused without committing either.

t_fs_commit_list covers rollback, remount, aliases, clone failure, cache
storage beside the save and the refused multi-save list.

Co-authored-by: BeeeeeMo <14192706+BeeeeeMo@users.noreply.github.com>
@seedds

seedds commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Thanks for this fix. It's now on main as fa6ce1a, and you're credited as co-author.

This branch is built on the repository's history from before the rewrite and shares no commits with current main, so it couldn't be merged. I ported it by hand instead:

  • commit_mount_locked and the list takeover are kept as you wrote them. The D2R pre-commit and post-commit hooks are left out because main no longer has them.
  • Big-stack trampoline: the test uses main's hle_bigstack.c instead of the assembly stub in t_fs_stub.c. It still calls through the registered swforce entry.
  • Cache and temporary storage: a writable mount without a host transaction is now skipped in the list instead of refused, matching the single-name Commit. On hardware, cache storage is journaled and games commit it, so Commit({"save", "cache"}, 2) would otherwise fail and abort. Unknown names, read-only mounts and lists spanning two separate saves are still refused.
  • Tests: t_fs_commit_list has 34 checks, including save plus cache storage and cache storage alone. The full host test suite passes and the Mac host builds.

Closing, since the change is on main.

seedds added a commit that referenced this pull request Oct 21, 2026
nn::fs::Commit(const char* const*, int) had no takeover, so the SDK's own
code opened fsp-srv and aborted with the save writes left uncommitted
(Animal Crossing: New Horizons). Both overloads now share one transaction
engine. A list commits once when every name is the same host save; cache
and temporary storage are skipped as the single-name Commit does, and a
list of two separate saves is refused without committing either.

t_fs_commit_list covers rollback, remount, aliases, clone failure, cache
storage beside the save and the refused multi-save list.

Co-authored-by: BeeeeeMo <14192706+BeeeeeMo@users.noreply.github.com>
@BeeeeeMo
BeeeeeMo deleted the fix/mount-list-commit branch October 21, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants