21 KiB
Eden on Switch 2 titles (branch nx2)
Local experiment: get Eden (Switch 1 emulator) to load a Switch 2 title's loose ExeFS and push boot as far as it goes. Nothing here is upstreamed or pushed. Graphics (Ampere / NVN2) is out of scope for this pass.
Setup
- Source + build live in
C:\dev\eden(a worktree ofeden-nx2/, branchnx2). The original working folder path is ~180 chars, andcl.execan't open its PDB from a build folder there (C1041), so the build moved to a short path. - Toolchain: VS 2026 (MSVC 14.51) via
vcvars64.bat, Ninja from VS, CMake 4.4. - Configure:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INTERMEDIATE_DIR_STRATEGY=SHORT -DENABLE_QT=OFF -DYUZU_CMD=ON -DENABLE_WEB_SERVICE=OFF -DYUZU_ROOM=OFF -DYUZU_TESTS=OFF -DENABLE_WERROR=OFF -DENABLE_UPDATE_CHECKER=OFF - Build:
cmake --build build --target yuzu-cmd->build/bin/eden-cli.exe. - Run (portable mode,
build/bin/user/holds config/logs; no keys or firmware installed):eden-cli.exe -g "<title>\exefs\main" - Debug helpers:
EDEN_NX2_SVC_TRACE=1logs every SVC with arguments and results.EDEN_NX2_IPC_TRACE=1logs every dispatched CMIF command and non-zero result words.EDEN_NX2_AUTO_STUB=1answers every unknown command with an empty success (theuse_auto_stubsetting gets reset by eden-cli's config rewrite). Used for all runs below.eden-cli --filter "<log filter>"sets the log filter for one run (e.g.*:Info Loader:Debugto get module load addresses).tools/nx2/nso_syms.py <nso> addr|got|find ...maps module offsets to exported symbols, GOT slots to imports, or searches symbol names. Eden's backtrace shows "unknown" modules for some titles; with theloaded module X @ addrlines this turns frames into SDK names.tools/nx2/a64_coverage.py <a64.inc> <nso>...lists encodings in an NSO's .text that dynarmic's decoder table doesn't match.tools/nx2/npdm_info.py <main.npdm>dumps the META/ACI0 header, services and kernel caps.tools/nx2/nso_dis.py <llvm bin> <nso> <text_off> <count> <workdir>disassembles part of an NSO's .text (clang + llvm-objdump from VS 2022's LLVM).- Under cdb, stop only on second-chance AVs (
sxd -c2 "..." av): dynarmic fastmem uses first-chance access violations.
Titles
All titles share the same main.npdm layout: 64-bit, 39-bit address space, ThreadInfo cores
0..5 (core mask 0x3f), priorities 28..59, KernelVersion cap 9.1, SVCs 0x01-0x36 plus 0x80,
Switch 2 ACID layout.
All four are Switch 2 builds: none is a plain Switch 1 sanity check. Instruction counts that
stock dynarmic did not decode (a64_coverage.py against the unpatched a64.inc):
| Title (ExeFS) | Program id | RETAA/B | LDAPR | LSE atomics | CAS | SWP | other |
|---|---|---|---|---|---|---|---|
first exefs |
0400C3F00006E000 | 114,767 | 37,560 | 24,718 | 6,640 | 2,470 | CASP 6, XPAC 7, PAC 2, STG 1 |
second exefs and updates/exefs |
0400A3A01EE4C000 | 12 (rtld only) | - | - | - | - | main/sdk/subsdk0 don't decompress |
third exefs (update updates/65536 ~same) |
04005B902517C000 | 134,287 | 37,052 | 160,741 | 19,987 | 1,623 | XPAC 3, PAC 2, STG 1 |
fourth exefs (update updates/65536 ~same) |
0400380020056000 | 134,911 | 100,520 | 32,110 | 8,667 | 3,733 | PACGA 58, PAC 7, CASP 6, XPAC 3, STG 1 |
With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in sdk undecoded
(e.g. 5f119f9c, 4ea05c42, 0f413044, FP16/v8.2 forms).
Blockers hit, in order
| # | Where it stopped | Cause | Fix (commit) |
|---|---|---|---|
| 1 | CMake try-compile | C1041, PDB path ~290 chars | build in C:\dev\eden |
| 2 | Compile | MSVC 14.51: reinterpret_cast<u64>(ULONG_PTR), C4505 on amalgamated zstd.c, C4800 int->bool |
Fix build with MSVC 14.51 |
| 3 | Decoder | dynarmic had no LSE atomics, CAS/CASP, LDAPR, RETAA/RETAB, BRA*/BLRA*, LDRA*, PAC*/AUT*/XPAC*, PACGA, MTE ops. Title 1 has ~115k RETAB, ~37k LDAPR, ~25k LSE, ~6.6k CAS, ~2.5k SWP | dynarmic: decode ARMv8.1-8.5 instructions (atomics are exclusive load/op/store with retry; PAC is identity; MTE tags are 0) |
| 4 | Loader | main identified as a bare NSO, so main.npdm/rtld/sdk were skipped, then a crash writing into an uninitialised process |
loader: load a loose ExeFS when given its main |
| 5 | NPDM parse (ErrorBadFileAccessControl) | Switch 2 ACID has a 0x300-byte signature/key area (Switch 1: 0x200); FAC/FAH offsets were also read without the block base | npdm: parse the Switch 2 ACID layout |
| 6 | eden-cli crash on start/exit | *appstate never set, SDL3 callbacks returned SUCCESS (quit), InputSubsystem was a local of SDL_AppInit, mouse timer before mouse driver |
eden-cli: fix SDL3 main-callback lifetime bugs |
| 7 | nnosInitialize abort |
svcGetInfo 29/30 unknown (region between alias and ASLR) |
kernel: stub ids 29/30 (empty region after alias) |
| 8 | VammManager::InitializeIfEnabled abort |
svcGetInfo 33 unknown; it's a page granule (module ranges are checked against value-1; VAMM maps one granule first) |
return 0x1000 |
| 9 | nn::am::InitializeApplicationInterfaces |
appletOE cmd 10 (Switch 2 OpenApplicationProxy) |
alias to cmd 0 |
| 10 | same | IApplicationProxy cmds 60 and 40: new getters |
return a generic nx2: stub interface (nx2_stub_interface.h); calls on it are auto-stubbed and logged |
| 11 | nn::oe::SetupGpuErrorHandler -> CreateThread |
thread on virtual core 5; Eden emulates 4 cores | fold virtual cores 4/5 onto 0/1 in CreateThread/SetThreadCoreMask |
| 12 | (title 3) nn::oe init |
IApplicationFunctions 320 has no handler |
empty-success stub |
| 13 | (titles 3/4) nn::fs::QueryMountRomCacheSize -> GetFileSystemProxyServiceObject abort 2002-1002 |
fsp-srv SetCurrentProcess returned ResultTargetNotFound: the loose-ExeFS loader never registered the process with the fs controller, and never set its RomFS |
loader: give loose ExeFS boots a RomFS (sibling romfs/) and an fs registration |
| 14 | (title 4) IApplicationFunctions 321 |
unknown | empty-success stub |
| 15 | (title 4) nn::oe::GetCurrentGraphicsScale UnexpectedDefault |
IApplicationProxy 60 is a graphics-scale interface; its cmd 10 must return 10/20/30 |
IGraphicsScaleController returning 10 |
| 16 | (title 4) nn::gs::sdk::BootGraphicsSystem -> nn::os::GetRemoteMemoryUsage abort |
svcGetInfo 31/32 (remote memory for gsv/nvsrv) |
return 0 for both (logged) |
| 17 | (title 4) nn::gsv::BootWithRemoteMemory abort |
it sends TIPC on a session handle from ISelfController 2000, which had no handler |
ISelfController 2000 returns a raw session to INx2GraphicsSystemSession (nx2_gsv.h), which accepts every command |
| 18 | (title 3) nn::oe::GetPseudoDeviceId abort |
needs control.nacp, which loose ExeFS lacks | seed from program id |
| 19 | (title 4) nn::syncpt::sdk::InitializeEx |
gsv 202 must return the syncpt session; gsv 204 takes two transfer memories and must leave a header (version 1, count, offsets) in the read-only one and return two events; syncpt 1 returns a u32 < 0x400 | INx2SyncptSession, gsv 202/204 |
| 20 | (title 4) nn::gpu::sdk::BootWithRemoteMemory |
gsv 497 returns the GPU session; gpu 11 (id range), 12(id) event, 21(id) start + wait on that event, 22 another session | INx2GpuSession |
| 21 | (title 4) nn::vi2::sdk::Initialize / nn::vi::CreateLayer |
gsv 305 returns the vi2 session; vi2 1/3/5 return small u32s (5 = timestamp recorder count, 7 needed by LayerHistory), 11/31/32 return handles | INx2Vi2Session |
| 22 | all TIPC replies with data | Eden bug: ResponseBuilder::Push(Result) writes the 2-word CMIF result, so TIPC data lands one word late and past the copied size |
TipcReplyWords |
| 23 | (title 4) NVN -> NvRm device open | nvsrv is the nvdrv INvDrvServices protocol; serve it with Eden's nvdrv, initialised without Initialize |
nvdrv-backed nx2:nvsrv |
| 24 | NvRm chip check | Switch 2 NvRm asks for a 0xD0-byte characteristics block and accepts chips 0x17E/0x12B/0x12E, but NVN needs the Ampere one; s16 interface versions at 0x70..0x80 must be >= 6 etc. | report chip 0x17E, Ampere classes, versions 0x40; unknown ctrl-gpu ioctls zero-stubbed for such callers |
| 25 | NvRm address space | AllocAsEx(flags 5, no big pages, [0x8000000, 0xC000000000), split 0); NVN then reserves 4 KiB VA up to 512 GiB; Eden's split/limit handling made the reported regions unusable | small pages up to the end, absolute big-page limit |
| 26 | (title 4) NvRm channel setup | nvhost-gpu ioctls 0x1F/0x20, /dev/nvhost-tsg-gpu, GetEngineInfo, object class 0 |
logged echo for unknown ioctls once an nx2 driver is seen; GR + async-copy engines; Ampere/class-0 objects on Maxwell slots (70f1651, 82dbd88) |
| 27 | GPU memory | svc 0x80 (make memory GPU-accessible), gpu sub-session 101 (register range) and 211 (job) | stubs (608f0d6) |
| 28 | syncpt::sdk::InitializeEx -> InterProcessEvent::Signal abort E401 |
gsv 204's first handle is attached writable (client -> server request), the second readable; the shared page has u64 values (RO+0xc), armed bits (RW+0x18, RO+0x1c) and summary words | gsv 204 returns a writable event, lays out the page, a 500 us poller mirrors host1x values and acknowledges armed waits (88367ea) |
| 29 | NvOsEventPoolWait E401 |
syncpt 22(slot) must return an event; 201(slot, ~syncpoint, value, mode) / 202(slot) request/cancel a signal | slot events via host1x guest actions (88367ea) |
| 30 | GPU never executed submissions | Switch 2 never maps through nvhost-as-gpu: memory registered with gpu sub 101 is used at GPU VA == CPU VA | Nx2GpuMemoryRegistry identity-maps every range into each GMMU; SMMU physical range 33 -> 34 bits for 10/12 GB layouts (88367ea) |
| 31 | Puller | binds Ampere classes C797/C7C0/C7B5 and class 0; NVN2 uploads Turing+ (MME64) macros, which Eden's Fermi macro JIT runs as garbage until it crashes | Ampere classes run on the Maxwell engines; macros skipped while Tegra::g_nx2_ampere (88367ea) |
| 32 | NVN device init waits on syncpoint 1 >= 1 | submits use increment_value (bit 8) and signal completion only with semaphore releases |
Eden appends the syncpoint increments for such submits (88367ea) |
| 33 | wsi::ounce::NativeWindow::Acquire on a null window |
gsv 1510 is gsv::shim::CalculateRequirements; size 0 made window init fail |
block-linear size/alignment, 4 bytes/pixel (5fb37b5) |
| 34 | NvOsEventPoolWait E401 (second channel) |
nvhost-gpu QueryEvent 7 | per-channel event (5fb37b5) |
| 35 | CPU spins in NVN for [0x2106028010] >= 3 |
queues release progress to page P+0x1000 and wait (GPU acquire + CPU poll) on the same offset in page P | puller mirrors releases into the first two-page gpu region (5fb37b5; guess, works) |
| 36 | font header check on a wild pointer | pl:u 2010 GetBuiltInFontData (bool at offset 0, then {flags, u32 offset, u32 size}) and 2000 (font shared memory + size) |
plain TTF copies of Eden's shared fonts after the BFTTF data (5fb37b5) |
| 37 | null "SoundResource" heap (300 MB) | nninitStartup sizes the heap as total - used - 0x195000000; Eden capped total at the 8 GiB heap region, leaving 1.55 GiB |
Switch 2 processes count the alias region in max memory (5fb37b5) |
| 38 | game waits for service aud:u |
Switch 2 audio entry (nn::audio::CreateUserClientSessionByHipc); cmd 2 returns an IAudioRendererManager |
INx2AudioUserClient backed by Eden's audren (2e6c39c) |
| 39 | every audio RequestUpdate fails |
updates tagged REv? (lowercase v), renderer opened with REV? |
compare revision numbers (2e6c39c) |
| 40 | main thread parked in ExecuteVirtualMachineMapBufferEx |
gpu sub 211's job slot is request word 17, not word 2 | signal word 17's event (2e6c39c) |
| 41 | "too many files open" reading RomFS | eden-cli never raised the CRT stdio limit | _setmaxstdio(8192) (2e6c39c) |
| 42 | GPU memory model | gpu sub 101 = CreateGpuAccessibleMemory (reply word 3 = GpuMemoryHandle); 211 = NormalMapGpuMemory4K (words 3-4 first VA, 5-6 last, 7 handle, 8-9 offset, 17 slot), also into sparse regions at 0x81_xxxx_xxxx; 102/213 (NormalUnmapGpuMemory4K)/214 (CreateSparseRegion) complete through slot events (words 3/8/11) |
real handles, Nx2GpuMemoryRegistry::MapAt with SMMU range reuse, slot signals |
| 43 | (title 1) nn::syncpt::sdk::Initialize abort |
newer SDK opens syncpt with TIPC 0(u64 token) and aborts unless it is echoed | handshake echo |
| 44 | (title 1) NVN closes the GPU device after GetVARegions | newer SDK asks gpu 201/202(id) for the user/gsv partitions (two u64s each, stored as min/max) instead of using 203 | 201 = [0x8000000, 0xC000000000), 202 empty |
| 45 | NVN command buffer with no command memory (~12 s, titles 1 and 4) | NvRmGpuTSGSubcontextCreate failed (0x11): characteristics reported one VEID |
flags bit 8/22 (TSG, TSG_SUBCONTEXTS) and 64 subcontexts at characteristics +0xb4 (12125bc) |
| 46 | NVN asserts in GetSyncpointShimRoAddress |
Switch 2 reads syncpoints through a read-only GPU page (flag bit 26, nvhost-as ioctl 0xB) | shim at the top of the VA range; GPU reads of it return the syncpoint value (12125bc, from changes.patch) |
| 47 | window/image creation | gsv 111/112/114/423/429/430/435/1510 native memory and images; svc 0x80 is SetGpuMemoryAttribute | native object registry + layout calc (from changes.patch), real svc 0x80 (12125bc) |
| 48 | presents go nowhere | vi2 229 (word 5 image id, ~word 22/word 24 fence) | composite the block-linear image with native GOB tiling (f63ff90, from changes.patch) |
| 49 | crash in DmaPusher::CallMethod (null engine) |
NVN uses subchannel 2 (inline-to-memory) without binding a class | bind KeplerMemory there by default; drop methods on unbound subchannels (9f30683) |
| 50 | GPU parses garbage, then stalls (~14-19 s) | NVN2 gates each command buffer with a SEMAPHORED acquire the CPU releases after writing it; Eden read past unmet acquires and ran stale commands (EDEN_NX2_PB_CHECK confirmed entries changed after submit) |
suspend the channel on an unmet acquire and resume it from the same word (9f30683, approach from changes.patch) |
| 51 | desync after a called command buffer | NVN2 ends a GPFIFO entry inside a method's data, calls another buffer, and continues at the next address | set the unfinished method aside and resume it when the stream continues (9f30683) |
| 52 | semaphore words next to releases get clobbered | 4-byte releases (SEMAPHORED bit 24) were written as 16 bytes | 4-byte releases write 4 bytes (9f30683) |
| 53 | no draws: 3D macros skipped | NVN2 drives 3D/compute state and draws through Turing MME64 macros; compute dispatches through QMD 3.0 | changes.patch's MME interpreter (one macro context shared by the process's channels, also on the compute class), native compute and QMD scheduler (d7826ce) |
| 54 | draws hit Eden's Maxwell decoder ("Invalid insn") | shaders are SM86 SASS | changes.patch's Ampere-to-Maxwell lowering, widened until all 32 of title 4's programs lower; EDEN_NX2_SHADER_DUMP + C:\dev\nx2work\lowertest test it offline (d7826ce) |
| 55 | GPU desyncs again after ~15-30 s | the game ran a full ring of command memory ahead of Eden's GPU thread: the vi2 229 reply reports all buffers free, so nothing paced it | vi2 229 waits (bounded) for the frame's GPU fence (325cac7) |
| 56 | macro arguments lost | NVN interleaves several command buffers, so more than one split method can be pending | several parked methods keyed by resume address, dropped at the end of the submission (325cac7) |
| 57 | frames all black although draws run | gpu 214 creates GPU-only memory (six 300 MB regions at 0x81_0000_0000) that the graphics server backs itself; the render targets live there and were unmapped | back each region with application-pool pages mapped into the alias code region (325cac7). Title 4 now shows its loading screen and a menu screen |
| 58 | (title 4) hang after confirming difficulty: macro 0x142 "parameter FIFO underflow" | NVN2's conditional-render macro reads two query counters with MME_DMA_READ_FIFOED (3D methods 0x154/0x155 address, 0x158 count) and loads them after a load barrier | MME DMA reads into the load FIFO / data RAM (8cbe854) |
| 59 | equal-value gate semaphore missed (~24 s) | the CPU side ran ~96 command buffers ahead when the 250 ms present wait timed out during shader compiles | present waits up to 4 s for the frame fence (8cbe854) |
| 60 | 3D queue waits forever on a word the compute queue should release | NVN2 patches a third QMD release slot at word 52 for queue-to-queue syncs | fire release slots 24, 28 and 52 (8cbe854) |
| 61 | 3D queue waits for N compute completions, 4 missing | compute jobs whose shader Eden rejected never completed, so their +1 releases never fired | rejected QMDs still complete and release (a84ec80); IMAD -UR addend, LDSM lowered |
| 62 | queue waits for ≥ a value that keeps moving backwards | QMD release slot 0 (flag bit 19) is a max release into a word a 3D macro also advances | bit-19 QMD releases write max(old, payload) (a84ec80) |
| 63 | in-game draws refused (~20k per run), black screen behind the dialogue | strong LDG (bit 77), 2D TLD4, RED MAX, AST of RZ unsupported | lowered (a84ec80); 210/214 post-menu shaders lower |
| 64 | intro scene black (DLSS-style pass) | title 4 renders at 2560x1440 and upscales to 3840x2160 with a tensor-core (HMMA/MOVM/LDSM) compute network; emulating it takes the host driver minutes to compile and would cost seconds per frame | bilinear blit from its input (cbuf0+0x90) to its output (cbuf0+0xa0) when the program is rejected (22333bd); full emulation opt-in (EDEN_NX2_TENSOR) |
| 65 | fullscreen blit passes read constant bank 24 | console-provided blit parameter block, never bound by the game | bank 24 -> 17, synthesized from the render area by BlitConstantsPass in vertex and fragment shaders (22333bd, d75e57d) |
| 66 | synthesized blit sizes were 0 | the game never programs surface_clip, which feeds Eden's render area | fall back to colour target 0's size (d75e57d) |
| 67 | tonemap output black | the 3D colour-grading LUT is drawn once per scene by a pixel shader using CALL.REL.NOINC; capture treated it as a real call, never found the end and rejected the shader | NOINC calls count as forward branches in capture (4d58a74). Intro cutscene renders correctly |
Run settings used (in build/bin/user/config/sdl2-config.ini): memory_layout_mode=4
(12 GB). Debug knobs: EDEN_NX2_AUTO_STUB, EDEN_NX2_IPC_TRACE, EDEN_NX2_SVC_TRACE,
EDEN_NX2_PULLER_TRACE, EDEN_NX2_METHOD_TRACE, EDEN_NX2_HIDE_WINDOW,
EDEN_NX2_FRAME_CAPTURE=<dir> (every 120th presented frame as TGA), EDEN_NX2_PB_CHECK
(pushbuffer entries changed between submit and execution), EDEN_NX2_QUERY_TRACE (every GPU
report/semaphore write). C:\dev\crashrun.sh runs eden-cli under cdb and prints the crash stack;
C:\dev\stackdump.sh <main> <seconds> dumps host thread stacks of a running test.
Where it stops now
All runs: EDEN_NX2_AUTO_STUB=1, 12 GB memory layout, loose ExeFS with the sibling romfs/.
Title 4 (fourth switch game\exefs, 0400380020056000) plays its intro cutscene correctly
rendered: boot, loading screen, menu (difficulty select, keyboard input), confirm, then the intro
(plane cockpit, newspaper/map) lit, colour-graded and subtitled, matching console footage. The
DLSS-style upscale is a bilinear stand-in. In a played session the game went on to a later loading
screen that did not finish; open there: a compute shader with a 64-bit add (UIADD3 carry-out to
UP0), TMML (0xb69) in two graphics shaders, "reserved attribute 23" (a[0x5c]) reads. Scripted runs
(C:\dev\run.sh) move any title-4 save to C:\dev\save_backups first so they start a new game.
Scripted input: EDEN_NX2_KEY_SCRIPT="50:4,50.5:-4,55:79,55.4:-79,57:4,57.5:-4".
Title 3: last checked before the graphics work; should now track title 4.
Title 1 (first switch game\exefs, 0400C3F00006E000, Drive.nss): reached its main loop and
the empty-command-buffer stop that blocker 45 fixed for both titles; not retested since the
macro/shader/present work.
Title 2: main/sdk/subsdk0 fail LZ4 and SHA-256 checks; can't be run from these files.
Prioritized next steps
- Title 4 past the intro: lower the 64-bit add with carry (UIADD3/IADD3 carry-out and .X carry-in), TMML, and attribute 0x5c; then whatever the next area needs (dump + lowertest). Squid/paint-splat hatch on the menu. A faster tensor-core emulation (shared-memory HMMA) would replace the bilinear upscale stand-in.
- Title 1 / title 3 retests with the current tree; dump their shaders and widen the lowering.
- Replace guesses with real protocol knowledge: syncpt page layout, the semaphore mirror page, gsv 214 (GPU-only memory) and vi2 present buffer availability (real buffer release instead of the fence wait), gsv 1510 formats, svcGetInfo 29-33.
- Six cores instead of folding 4/5 onto 0/1; rare SIMD encodings still undecoded.
- Upstream-worthy pieces independent of Switch 2: MSVC 14.51 build fixes, eden-cli SDL3
lifetime fixes and
_setmaxstdio, ExeFS loader fixes, FAC/FAH offset fix, GetPseudoDeviceId without NACP.