diff --git a/NX2_GPU_THREAD_SPLIT.md b/NX2_GPU_THREAD_SPLIT.md
new file mode 100644
index 0000000000..75fd0e4105
--- /dev/null
+++ b/NX2_GPU_THREAD_SPLIT.md
@@ -0,0 +1,63 @@
+# Splitting the GPU thread
+
+Title 1 (Mario Kart World) is bound by one host thread, the GPU thread: ~100% busy while the
+host GPU is 20-40% busy and the game's own CPU thread mostly polls the counter. Per second of
+3D scenes (profiled, 2026-10-29): command processing and register writes ~335 ms, macros
+~200 ms (1.3 million calls), draw setup ~345 ms (55,000 draws), compute ~70 ms. Speeding up the
+pieces (NX2_NOTES rows 123-139) took frames from ~22-38 ms to ~18-26 ms; a locked 60 fps on the
+heavy courses needs work on more than one core.
+
+## Split point: method calls
+
+- **Front thread** (the current GPU thread): takes the CPU's command queue, parses GPFIFO
+ entries and command lists (DmaPusher), runs macros (compiled, macro_turing_jit.h), and keeps
+ what macros read: a mirror of each engine's registers, its macro code and data RAM.
+- **Back thread**: executes the resulting stream of method writes in order: Maxwell3D /
+ KeplerCompute / Fermi2D / DMA engines' CallMethod and everything behind them (register
+ state, dirty flags, constant buffer and inline uploads, draws, dispatches, queries, fences,
+ the rasterizer and its caches). Only the back thread touches the rasterizer.
+- The stream: an SPSC ring of {engine, method, argument} plus copied data for multi-word
+ methods (command lists are often guest memory read in place, which the guest reuses).
+ CPU-side commands (flush/invalidate requests, RunInOrder actions, presentation) are
+ forwarded into the same ring so they keep their order.
+
+## What the front needs without waiting
+
+- Register values macros read (MME_READ): the front mirrors every register write it sends.
+ Back-side changes to registers it must reproduce: constant buffer offset (+4 per
+ const_buffer.buffer word), shadow_scratch[0] = 1 (falcon[4]), shadow RAM control (Track /
+ Replay change the effective value), macro upload pointers (the front handles load_mme
+ itself). Compute: native_registers (0xad is changed temporarily by the inline QMD path).
+- Macro code uploads and binds (load_mme.*, compute 0x45-0x48): handled by the front.
+
+## Where the front waits for the back (sync points)
+
+- Macro DMA reads (MME_DMA_READ / _FIFOED, ~450 a second): read guest memory as of their
+ position, so the back must have executed everything before them.
+- Semaphore acquires that are not satisfied yet (~10,500 a second, cross-channel waits on QMD
+ releases): the release is executed by the back, so the front waits until the semaphore
+ holds (or parks the channel and runs another one, as the pending-channel code does now).
+- Syncpoint waits, WaitForIdle-style methods, and anything else that reads guest memory or
+ host state on the front.
+- Expected cost: each wait drains the back only up to the dependency, then the back idles
+ until the front sends the next method (a few microseconds), so even ~10,000 waits a second
+ leave most of the overlap. Ceiling: front ~35-40% of the work, back ~60-65% -> ~1.4-1.6x.
+
+## Stages
+
+1. Instrument: count sync points per second and the front/back share precisely (zones).
+2. Ring + back thread executing method writes; the front still does everything else.
+ Correctness first: an EDEN_NX2_SPLIT=0 switch runs both on one thread.
+3. Move macros to the front with the register mirrors; verify mirrors against the back's
+ registers in a debug mode (compare on every MME_READ).
+4. Sync points; crash and screenshot tests across courses; then title 4.
+
+## Status (2026-10-29)
+
+Stages 1-3 are in (babd395, `gpu_split.h`), off by default: `EDEN_NX2_SPLIT=1` runs engine methods
+on the back, `EDEN_NX2_SPLIT=2` also runs the 3D and compute macros on the front against register
+copies. Measured in title 1 (NX2_NOTES rows 142-144): ~44,000 draws a second on one thread, ~47,400
+with stage 2, 50,000-66,500 with stage 3. The front waits ~4,000-5,000 times a second (semaphore
+acquires, MME DMA reads); the back still spends ~200 ms a second outside its zones (ring dispatch,
+method sink). Clean A/B (NX2_NOTES row 145): 1.46x throughput, frames 17% shorter; on by
+default since. The back is ~92% busy, ~53% of it draw setup: the next step is there, not the ring.
diff --git a/NX2_NOTES.md b/NX2_NOTES.md
new file mode 100644
index 0000000000..a50b037b85
--- /dev/null
+++ b/NX2_NOTES.md
@@ -0,0 +1,264 @@
+# 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 of `eden-nx2/`, branch `nx2`). The original
+ working folder path is ~180 chars, and `cl.exe` can'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 "
\exefs\main"`
+- Debug helpers:
+ - `EDEN_NX2_SVC_TRACE=1` logs every SVC with arguments and results.
+ - `EDEN_NX2_IPC_TRACE=1` logs every dispatched CMIF command and non-zero result words.
+ - `EDEN_NX2_AUTO_STUB=1` answers every unknown command with an empty success (the
+ `use_auto_stub` setting gets reset by eden-cli's config rewrite). Used for all runs below.
+ - `eden-cli --filter ""` sets the log filter for one run (e.g. `*:Info Loader:Debug`
+ to get module load addresses).
+ - `tools/nx2/nso_syms.py 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 the `loaded module X @ addr` lines this turns frames into SDK names.
+ - `tools/nx2/a64_coverage.py ...` lists encodings in an NSO's .text that
+ dynarmic's decoder table doesn't match.
+ - `tools/nx2/npdm_info.py ` dumps the META/ACI0 header, services and kernel caps.
+ - `tools/nx2/nso_dis.py ` 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 |
+|---|---|---|---|---|---|---|---|
+| Mario Kart World `exefs` | 0400C3F00006E000 | 114,767 | 37,560 | 24,718 | 6,640 | 2,470 | CASP 6, XPAC 7, PAC 2, STG 1 |
+| Pokopia `exefs` and `updates/exefs` | 0400A3A01EE4C000 | 12 (rtld only) | - | - | - | - | main/sdk/subsdk0 don't decompress (broken dump) |
+| Splatoon Raiders `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(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** |
+| 68 | crash ~5 s after the save that follows character customization (garbage pointers in two threads) | nx2:gpu 213 (unmap) and 102 (destroy GPU memory) were no-ops; the game reuses the freed memory as heap and stale GPU write-backs overwrote it | unmap from every address space, drop freed semaphore regions (bed817c); not yet confirmed past customization: frame-locked replays desync across loading screens |
+| 69 | hatched edges on UI portraits, buttons, squid cursor | UI textures are ASTC; the GPU ASTC decoder ignored the Switch 2 GOB swizzle | apply it in astc_decoder.comp (7fa2b46) |
+| 70 | freeze ~15 s after the post-customization save; channels 2/4 wait on 0x2115c98000 (equal 0x5293, now 0x5310) | a 3D queue ticks a counter two queues acquire value by value; Eden wrote several ticks between their re-checks | releases latch the waiters they meet (8e37f58) |
+| 71 | audio broken, game at 90-160 fps | vi2 presents were never paced | 60 Hz vblank pacing |
+| 72 | stuck loading the next scene ("too many files open" for route_wallClimb.pack.zs, save.dat) | VFS open-file cache cap equal to the CRT limit (8192) | cap 512 below _getmaxstdio() |
+| 73 | loading screen hangs, host GPU TDR | macro parameters split across GP entries were parked; method data continues across entries | continuous split methods (e8d060b) |
+| 74 | rejected ATOMG/STL/LDL with small immediate offsets | lowering only took a zero offset | s20 ATOM offset, 24-bit STL/LDL offset (6302be8) |
+| 75 | same crash 3 runs (415 s, 429 s, 9960 s): read 0x29 at pc 0x85096d10 (recursive mutex lock), lr 0x82ada310 | worker job runs main+0x2aba2b0 on object C = [A+0x18] whose manager [C+0x720] is null; C's destructor (main+0x2ab8e20, vtable main+0x4b660f8) nulls it before A's teardown (main+0x2b37b50) clears A+0x18; teardown and per-frame update are both jobs (via 0x8365c634); NFC Finalize comes 14-22 s before each crash; LSE atomics (exclusive pairs) and DMB (mfence) checked fine | open: catch all threads at the abort with the gdbstub (`nx2work/dbg_crash_threads.rsp`) |
+| 76 | debugger run, crash at 406 s in the same scene load: writes through a null result at pc 0x82b471fc (main+0x2b271fc) | the scene's frame heap "PaintSceneProcHeap.26" (fixed 90 MiB + 0xfc8 parent, head 0x12c9525d18, tail 0x12c9800208) had 2.98 MB left for a 0x302088 allocation (A init main+0x2b37de0 -> main+0x2b27020); about 160 KB short, so some allocation of that scene is bigger than on the console (only 11.7 MB of the heap is nx2:gpu memory); the main thread was waiting in svc 0x34 for job workers | open: log that heap's allocations to find the oversized one |
+| 77 | why the scene heap overflowed (run 6) | the heap size is a constant (mov w1, #0x5a00000 at main+0x2b32070); its fixed contents are 71.3 MiB (main+0x2ab9218) + 1.05 MiB + 1.25 MiB + 3 MiB; in runs 3, 4, 5 and the save test the scene's two 0x514000 GPU pools come from the root heap (next to the 0x2ee000 pool), in run 6 from the scene heap (current heap of the loading thread), 10 MiB it has no room for; which heap a job allocates from depends on the thread running it. The game's npdm asks for cores 0..5 (mask 0x3f, reported as is by GetInfo CoreMask) and Eden folds 4, 5 onto 0, 1, so two cores run two workers each | candidate: report or emulate the real core layout; ruled out: memory layout (12 GB), controllers (one), GPU pool alignment |
+| 78 | six emulated cores (4fbfd5b), run 7 with the gdbstub: same scene-heap overflow at 292 s, the 0x514000 pools again in the scene heap | both overflow runs (6, 7) had the gdbstub on (memory accesses checked, slower JIT); the three runs without it (3, 4, 5) put those pools in the root heap and crashed later on the torn-down object instead, so the debugger itself shifts this timing | open: a six-core run without the gdbstub |
+| 79 | run 8 (six cores, no gdbstub): original crash at 338.6 s, 11 s after the paint scene set up (327 s, pools in the root heap) | C's init (main+0x2ab9120) creates "ColPaintBuild.%u" (main+0x147d3c0 -> main+0x34f3850) sized by [C+0x738], 100 MiB by default (ctor main+0x2ab874c); when that returns null, init returns before setting [C+0x720], and the next state request (main+0x2aba230) crashes the update. So every crash at this load is a memory shortage: 100 MiB (no gdbstub) or 3 MiB in the scene heap (gdbstub). Root heap is 0x19506f000 at 0x1145e10020 | open: why memory is short (reported total memory for Switch 2, frees delayed behind the GPU) |
+| 80 | the memory shortage behind rows 75-79 | title 4 is told 9.55 GiB (GetInfo 21) and plans all of it: SetHeapSize 3.125 GiB plus a 6.33 GiB VAMM-backed sead root heap in the alias region; Eden backed the six 300 MB gpu 214 regions from the application pool, 1.8 GB the kernel still reported as free | 214 backing from the non-secure system pool, +8 GiB DRAM on the 12 GB layout, device memory tracker 35 physical bits (5cff620); run 9 passed the crash spot (NFC Finalize 272.8 s, ran to 388 s), no allocation failures |
+| 81 | title 1 hangs before its first frame (~17 s): main logic stops, two cores spin, only audio and hid keep running, almost no GPU submits | it asks gpu 214 for ~20 GiB (two 0x16ad80000 regions, these two still fail with the new pool); gets further than before (save data, friend list) | open: thread snapshot at the hang; 214 regions this large must be sparse on the console |
+| 82 | title 1 hang (row 81) | `monitor handles` (a4051e4) showed the main thread waiting forever on the nx2:vi2 32 event (loop at main+0x25ec5e4 until [obj+0x88] >= 2); the stub never signalled it | 31 and 32 events signalled at 60 Hz (6196a11); title 1 now presents (241 frames by 95 s) |
+| 83 | title 1 after the hang: ~3 fps with "fence not reached in 4 s" stalls | unimplemented vi2 104/105 (every frame) and gpu 215, gpu 214 still short for the two 5.7 GiB regions, one Nx2Map "out of SMMU space", rejected shaders (formatted surface store, IPA mode, LEA carry) | open |
+| 84 | title 1 GPU stalls (row 83): channels wait on semaphores 0x21f6093140/3040 until the 4 s timeout | NVN2 queue syncs run through a ring of QMDs (dependency 2 each: the previous entry's launch and macro 0xda, which patches release slot 52 and schedules/decrements via PCAS 3/7); macro 0xda reads the ring base from MME data RAM word 0x27, which each channel sets (0x21098d39, 0x213e8ff8, 0x213ea168) but Eden shared one data RAM for all channels, so every channel drove the same ring | per-channel MME data RAM, one QMD table for all channels (6c20e37); no stalls, title 1 reaches its title screen (world map with logo) |
+| 85 | title 1 address space | 214 = CreateSparseRegion, 215 = DestroySparseRegion (SDK symbols); SMMU space (16 GiB) ran out | 64 GiB device space (71b38ab), 215 handled (cc0c400) |
+| 86 | title 1 after its title screen: black frames | tens of thousands of rejected shaders: IPA interpolation modes, integer carry/third source, uniform comparison, formatted surface load/store, uniform constant load, unsupported opcodes | open |
+| 87 | title 1 loading crash: Vulkan out of memory | an 8192x8192x68-layer virtual texture (~23 GB) | images over 2^28 texels are created sparse with no memory (`Unbacked`: no uploads/downloads) (96a9b81); the game reaches its real title screen ("Press L + R"), ground black (that texture has no data) |
+| 88 | title 1 shader coverage | checked against nvdisasm (`nx2work/nvd.py` disassembles one instruction at a time, since a single illegal word stops nvdisasm); calls: RET may restore the link through IMAD.MOV with code between, library functions may EXIT, `@Pg EXIT Pc`; PLOP3/UPLOP3 any table, UP2UR, ULOP3 predicate output; UIMAD.WIDE, uniform USHF/FMNMX/I2F/F2I, LDS `[Ra.Xn+URb]`, ATOMG/RED/STG forms, PRMT, FSWZADD, REDUX.MAX | 1351c73, 52cda01, 49396b7: 1699 of 1712 dumped programs lower; left: 12 task/mesh programs (ISBERD/ISBEWR, row 89) and one SURED |
+| 89 | task/mesh shaders | title 1 draws some geometry with NV-style task + mesh programs: the task program culls a batch and writes its payload with `ISBEWR.O.ATTR [Rn+0x10]` and the mesh count with `ISBEWR [0x4]`; mesh programs read it back with `ISBERD.ATTR.SKEW`. The most-drawn rejected program (0x114c459500) is a task program. Eden has no mesh pipeline | open: needs a task/mesh pipeline (VK_EXT_mesh_shader) or a compute + indirect-draw emulation |
+| 90 | each rejected program was lowered and logged again on every draw (1.7M log lines in 13 min) | no negative cache | the shader cache keeps the first exception per address until that memory is written; logs deduplicated (bcb4ff0) |
+| 91 | a compute program with a jump table: "Failed to track indirect branch" | Eden's tracker wants IMNMX imm -> SHL imm -> LDC -> BRX; IMAD.SHL was lowered to an XMAD multiply | IMAD by a power of two lowers to SHL (49396b7+) |
+| 92 | with rows 88's compute forms, title 1 stays on its loading map (file reads stop ~46 s in) | two compute programs failing in Eden's compiler: a jump table whose targets lie past the first EXIT, and a 128-bit store of RZ from unaligned scratch registers | programs with BRX end at the compiler's closing branch-to-self; R248-R251 for 128-bit zero stores (3df6a81) |
+| 93 | title 1 colour cast (magenta/cyan, red/green, changing per course) | the sky/atmosphere 32^3 volume shader starts with an unguarded R2UR; R2UR/MATCH/HFMA2.RELU saved P6 with the uniform-guard mask `1 << (6 - 7)` (undefined), which shifted into the opcode bits | always save/restore P6 (2ed0c48). Found with `EDEN_NX2_DUMP` (flush GPU ranges at a present to files) and `nx2work/vol3d.py` / `volslices.py` (untile Switch 2 3D textures) |
+| 94 | near terrain black | 8192x8192x68 ASTC 8x8 virtual textures: 1.5 GB in guest memory, 23 GB each as Eden's RGBA8 decode | huge GPU-decoded ASTC arrays keep levels from the first that fits 512 MiB (here 3) on the host (60c4791) |
+| 95 | wind/foliage shards | SM70+ MUFU.SIN/COS take turns (sin(2pi x)); Eden's Maxwell MUFU takes radians | scale by 2pi (245e6ad); jungle canopy not re-checked yet |
+| 96 | title 1 aborts after ~1.5-4 min at the title or when a race starts (nn::diag::Abort from main+0x19e5df0) | the movie player (main+0x19e5860) lays out its decoder frames with nn::codec::CalculateDecoderImagePoolBuffer, which sends nx2:gsv 991; auto-stubbed, the reply kept the request words, so the frame size read as 0x167027f'00000001 and the budget check at main+0x19e5b58 failed. (Not the "video-frame-count" < 9 check: the game sets that from a two-slot registry, at most 2.) | gsv 991 answers a block-linear NV12 size (640x360 -> 0x64000) |
+| 97 | pop-in, objects flicker in and out, low-quality meshes | high-detail LODs drawn through task/mesh programs (bound in stage slot 1, header type VS, ISBERD/ISBEWR), which are rejected and skipped | open: task/mesh support |
+| 98 | ~12 fps at the title | host GPU ~20% busy, emulated cores ~0.9 host cores, GPU thread 45-70% busy, much of it waiting on the host GPU: semaphore acquires wait for host fences under GPU accuracy High; ASTC uploads end in Finish() (a Flush() there crashed at boot); a 1 ms poll sleep while channels are suspended | gpu_accuracy=Low: ~16 fps; SDL main loop capped at 240 Hz (88f04ad). Open: ASTC upload without a full stall (and per-layer re-upload of the trimmed virtual textures), native MME interpreter cost, draw path |
+| 99 | VRAM over the card (8.4-9.8 GB used on an 8 GB RTX 4070 Laptop), texture cache always collecting | ~700 ASTC 8x8 textures decoded to RGBA8 (16x their size, ~4.5 GB) | astc_recompression=Bc3 + accelerate_astc=CpuAsynchronous in the config; huge ASTC arrays stay on the GPU decoder as trimmed RGBA8 (ed0a3c9): 4.6-5.6 GB, ~10 -> 16 fps |
+| 100 | 8 ImageTranscode threads at ~44% each (3.5 cores) | Eden's default pool is half the hardware threads | an eighth, at least 2 (`EDEN_NX2_TRANSCODE_THREADS` overrides): ~19 fps on the attract loop |
+| 101 | Medal's capture hook inside Eden (`medal_hook64!OBS_Negotiate` in GPU-thread stacks) | Medal registers a per-user Vulkan implicit layer that loads into every Vulkan app | launches set `DISABLE_VULKAN_MEDAL_OBS_CAPTURE=1` (the layer's own off switch) |
+| 102 | host crash (log stops, segfault) at 113 s in one run; under cdb "Free Heap block modified after it was freed" at 243 s, twice | Eden's TickAsyncDecode erased a finished decode while still holding its mutex (unlocked freed memory); only reached with accelerate_astc=CpuAsynchronous (the BC3 setup) | release the lock first, cancel decodes of deleted images (8766a72); clean past 330 s. Also made nx2 service callbacks (vsync timer, syncpt actions, capture/dump) independent of their sessions' lifetime (190747a) |
+| 103 | (title 1) texture churn: the collector evicted ~1,250 decoded ASTC textures per 600 frames and they were decoded again (lag and pop-in turning the camera) | device memory sat above the collector's critical line (5.4 GB of a 7.0 GB budget): ASTC 8x8 is 4x larger as BC3, and a bloom chain at unmapped GPU 0xf0000-0x15b600 was re-created on every use (each lookup got a fresh stand-in CPU address), ~5,400 images per 600 frames on the map | unmapped GPU addresses get a stand-in address fixed by the GPU address, so images are found again; ASTC whose blocks carry no alpha (exact endpoint-mode scan, ~70% of them) goes to BC1 (`EDEN_NX2_ASTC_BC1=0` turns it off). Attract loop: 4.8-5.1 GB, no costly evictions, ~17 -> 22-23 fps |
+| 104 | garbage textures (title logo, road patches) and a host crash 90-120 s in that skipped the crash handler (fail-fast) | an image re-decoded while a decode was in flight has two; the first to finish cleared IsDecoding, so deleting the image did not cancel the second, which was then uploaded into the next image to take the slot (another texture's data; a staging overrun when that image was smaller) | every pending decode of a deleted image is cancelled; a decode larger than its image is dropped and logged. Frames clean; crash: verifying with long runs |
+| 105 | (title 1) movie abort moved on: `nn::codec::InitializeVp9Decoder` failed with 2001-0114 (InvalidHandle) | gsv 901/902/1001/1002 (`nn::codec::video::detail::ipc::Initialize`) were auto-stubbed; 902/1002 must return sessions (handle word 3, result word 4) whose TIPC 1 (CreateVideoDecoderService) returns another | sessions returned (2970100); the movie plays with no picture. Open: decode VP9 (vdecsvc 200/101/3/311 are auto-stubbed) |
+| 106 | GPU thread ~50% in the Turing macro interpreter | every instruction re-decoded on every run, register file copied per instruction | decode once per PC, operands read up front (a97d26d): GPU thread ~68% -> ~45-50% busy |
+| 107 | streamed textures shown as garbage/black for seconds; 2 transcode threads pegged | ASTC was decoded and re-compressed to BC on the CPU, ~40 textures/s on the attract loop | accelerate_astc=Gpu (config value 1) + BC recompression: GPU ASTC decode into a temporary RGBA8 image and `bc_encoder.comp` (69cc66b); textures ready the frame they are used |
+| 108 | frame time is CPU work + GPU-thread work in series | nx2:vi2 229 blocks the game until Eden's GPU thread has processed the frame (WaitForFrame), and RequestComposite waits for the GPU thread to drain its queue too; the present timeline log shows 8-56 ms of each 34-105 ms frame blocked | `EDEN_NX2_FRAMES_AHEAD=1` (wait for the previous frame) deadlocks at the first scene change: the main thread waits on syncpoint 1 (present fences) for a value no queued work reaches. Open |
+| 109 | ~1,400 SVCs per frame | the job system's WaitForAddress (0x34) / SignalToAddress (0x35) pairs and yields, all through Eden's global scheduler lock (3 of 6 cores seen blocked on it) | idle-core spinning (`EDEN_NX2_IDLE_SPIN_US`) gave no gain. Open |
+| 110 | (title 1) GPU thread saturated, 3D attract scenes ~22 fps | profiled with the new GPU zone timers (`EDEN_NX2_GPU_PROFILE`) and the in-process sampler (`EDEN_NX2_SAMPLE`): per second, ~355 ms command parsing outside macros, ~210 ms in 745k macro calls, ~325 ms draw setup (33k draws), ~89 ms compute (4.4k dispatches) | see 111-115; now ~27-33 ms per frame in the same scenes |
+| 111 | the guest clock (CNTPCT) cost ~10% of the main core | AMD CPUs have no CPUID leaf 0x15, so the TSC frequency stayed 0 and every read went through QueryPerformanceCounter | measure the TSC when the leaf is missing (948a391) |
+| 112 | C++ exceptions on the GPU thread (~1-2%) | a rejected native shader (mesh/task programs) was rethrown twice every time it was bound | return false for remembered rejections (948a391) |
+| 113 | debug aids on the hot path (~4% of the GPU thread) | semaphore watch per command word, FNV hash of every pushbuffer entry, first-write trace, two debug logs per macro call | flag checks unless their variable is set (948a391) |
+| 114 | texture churn: 1,400-2,600 textures deleted and recreated per 600 frames, one purge of 4,300 | ~38% of device memory was empty space in 256 MiB VMA blocks; usage came from the driver, so evictions did not lower it | 64 MiB blocks and usage minus VMA block slack (7b0ff0b): 0-224 per 600 frames, 6.24 of 6.97 GB used |
+| 115 | main core: ~12% slow-path reads of GPU-tracked pages, GPU thread and CPU fighting over the caches' mutex | `use_reactive_flushing` (on by default on desktop) protects tracked pages against reads too | off in the test config: the slow reads are gone, no visual differences seen in the attract loop. Kernel lock spins before sleeping (5c99031): ~20-25% less time blocked |
+| 116 | macro interpreter is ~11-13% of the GPU thread | operand fetch is not the cost: a slot-based rewrite matched the old interpreter on 200,000 recorded calls (`EDEN_NX2_MME_RECORD`, `C:\dev\nx2work\mme_test`) but was no faster | open: needs a JIT or pre-built handlers |
+| 117 | macro calls: ~1,000,000 a second, 7 instructions each, 96% exact repeats | the per-call interpreter cost was the time, not the instructions | memo of calls that depend only on their parameters and the data RAM words they read (1cd4a3c, b75fdf8, c28ea9a for compute macros): 97% replayed, verified on 200,000 recorded calls |
+| 118 | crashes ~26 s in (fencing thread walking a corrupt operation list; query cache Barriers; Vulkan worker SubmitExecution) | `async_presentation=true` (config): 4 crashes in 8 runs with it, 0 in 6 without; a race between the present thread and the Vulkan scheduler | keep it off (test build 3b). The fence manager's SyncOperation also appended to a list SignalFence moved from another thread: own lock now |
+| 119 | (tester, RTX 4070 SUPER) 50 MB log; textures evicted with 3 GB VRAM unused | nx2:vi2 104/105 called ~75 times a second through the logging auto stub; collector thresholds from Eden's ~7 GB device memory cap | quiet stubs (649fb56); thresholds from the driver budget (49225dd) |
+| 120 | GPU thread asleep ~8% on the texture/buffer cache mutexes | holders: the fencing thread (it took both caches for every fence, one per compute dispatch, and holds the buffer cache while it copies GPU-written buffers back) and CPU write traps; std::recursive_mutex slept at once | spinning recursive mutex, per-cache fence flush flags, separate locks (f10767f): asleep 3.7%. Buffer downloads at every fence stay: the native MME DMA reads of indirect arguments depend on them (no synchronous flush logged with them on) |
+| 121 | first-run freezes from shader translation (one 28,102-instruction compute shader: 3.4 s on the GPU thread) | BreadthFirstSearch's visited list was a linear scan (quadratic), and GlobalMemoryToStorageBufferPass searched again for every access | hash set past 64, per-start memo (ba173b6): 0.85 s; runtime builds now ~2-4 ms each. Translating on workers was not done: environments read live GPU state (texture types, constant buffer values) while translating |
+| 122 | final clean run (attract loop): 21-25 ms per frame in most 3D stretches, 26-38 ms in compute-heavy courses (industrial, highway), host GPU ~38% busy | the GPU thread is still the limit: draw setup ~25%, command parsing and register writes ~15%, compute dispatches/inline uploads (heavy courses) | open: per-draw binding cache, uniform buffer copies (staging slices are recycled by tick, so reuse needs care), buffer download interval sets |
+| 123 | (title 1) 5,500-7,000 fences a second, one per compute release, each made in full (both cache locks twice, a fence object, a fencing-thread wake-up) even with nothing to wait for: SignalFence 7.6% of the GPU thread | with fast GPU fences the release callback runs at once; ~20% of the fences had no downloads, queries or operations. OverlapRangeSet::Subtract restarted its search after every erase | empty fences skipped (one in 64 still made), one lock per cache per fence, single-pass Subtract (9905916): SignalFence 3.9% |
+| 124 | GPU thread ~9% spinning on, sleeping on and waking for the cache locks | the fencing thread took the buffer cache once per fence to write 250-330 MB/s of downloaded buffer data back (13,000-20,000 ranges a second) | signaled fences released together (31cd7b2, `EDEN_NX2_FENCE_BATCH=0` off): spinning 7.0% -> 5.5%; fence cost per call unchanged within noise (longer holds). Open: the downloads themselves |
+| 125 | crash every few minutes: BindHostGraphicsUniformBuffer -> ReadBlockUnsafe, access violation at 0x600 (2 of 4 runs) | DeviceMemoryManager's translation cache was shared by all threads; Map on a service thread (nx2:gpu 211 job mappings, logged at both crashes) cleared it between the GPU thread's null check and its copy | per-thread cache invalidated by a generation counter (1a497fa): 0 crashes in 6 runs |
+| 126 | macro parameters appended to a std::vector per call (~2% of the GPU thread) | ~1,000,000 calls a second, nearly all whole in one method run | stack copy (3caf8ea, `EDEN_NX2_MACRO_COPY=1` off). Passing the command list words in place is unsafe: they are often guest memory read in place, and the memo stores the parameters after the call |
+| 127 | main core: ~23% of its samples read CNTPCT (~17 million reads a second) | the game polls the counter while it waits for the GPU; a spinning thread slows its SMT sibling (8-core/16-thread Ryzen) | PAUSE between counter reads less than ~1.7 us apart (8abeb0b, `EDEN_NX2_CNTPCT_PAUSE=0` off); effect not separable from run-to-run noise |
+| 128 | Vulkan worker woken with a system call every 8 draws (~2% of the GPU thread) | DispatchWork notified even when the worker was busy | every 32 draws, notify only a waiting worker (d9d7e94) |
+| 129 | macro memo arena (16 MiB) cleared every ~2 s | records stored again with another shape left their old copies | compaction (9a37142); live records are ~12 MiB, so most compactions still end in a clear; hits stay ~97% (46% on the last call's slot) |
+| 130 | measurements | a stuck `nvidia-smi -l` loop had spun one core for 12 h (every run since the evening of 10-28 had one core less); the attract loop shows different courses each run (frame means vary +-15%); stopped test loops kept running and overlapped | compare per-call costs (`C:\dev\nx2work\zones.py` on `EDEN_NX2_GPU_PROFILE` logs); `ab_env.sh` and `hangcheck.sh` refuse to start while eden-cli runs; `stopall.ps1` stops every loop |
+| 131 | (title 1) 250-330 MB/s of buffer data copied back at fences (13,000-20,000 ranges a second) | Eden downloads every GPU-written range at the next fence. Measured: the native MME's DMA reads never touched those pages (`EDEN_NX2_GPU_PROFILE` counter), and with reactive flushing the guest CPU read GPU-written memory at most 64 times in 200 s | off for Switch 2 titles (d530bc4, `EDEN_NX2_BUFFER_DOWNLOADS=0/1` forces): fences ~1.1 us (were ~5), draw setup -7%, cache-lock spinning 1.8% |
+| 132 | inline uploads 10.5% of the GPU thread | IsRegionGpuModified visited every GPU-written range in the query (they stay fragmented without downloads); std::hash of integers and pointers is FNV-1a on MSVC in every Common::unordered_map | RangeSet::Intersects (one lookup) and a pass-through hash for integer/pointer keys (0fe581f): 7.5% |
+| 133 | some courses (desert/canyon, dense city, highway) run 30-45 ms with presents blocked 9-15 ms on the previous frame's fence (also before today: t14) | not the host GPU (nvidia-smi: 20-40% busy, ~22 W) but the GPU thread: ~100% busy there, its command processing 370-420 ms/s instead of 300-350 (more commands per frame). The 8 GB laptop GPU is also near its budget (6.6-6.9 of 7.1 GB): the texture collector evicts hundreds of decoded ASTC images per 600 frames, the buffer collector GPU-written buffers every frame | `EDEN_NX2_TC_MIN_AGE=120` (0f0fbb9) made no difference over three runs; buffer evictions batched into one GPU wait (db4c539), still ~2.7% on those courses (open: asynchronous eviction) |
+| 134 | each inline write invalidated the buffer, texture, shader and query caches on its own | thousands of small writes between draws | guest memory written at once, pending query results dropped at once, GPU-written buffer data uploaded in place; the other caches get merged ranges at the next FlushCaching (c06ac80, `EDEN_NX2_INLINE_DEFER=0` off): inline writes 4.4% |
+| 135 | ~6 million 3D register writes a second, ~60% writing the value the register already holds (mostly macro memo replays) | each went through shadow RAM, dirty flags (re-checked at the next draw) and the method switch | state registers (not in execution_mask) written with their value are dropped after shadow RAM processing (f265911, `EDEN_NX2_SKIP_SAME=0` off): macro calls 169 ns instead of 185, draw setup 5.80 us instead of 5.95. For non-executable registers ProcessMethodCall is skipped as well (it acts only on executable ones) |
+| 136 | native compute: ~20% of the GPU thread under KeplerCompute::ProcessNativeMethod (dispatches 8%, inline QMD/uploads 4%) | its compute macros replay ~64 QMD register writes per dispatch, each through the macro/QMD/upload/semaphore checks | plain native registers stored directly from CallMethod (f7a82b2): its own time 2.2% -> 1.6% |
+| 137 | 2D blits (Fermi2D) 3.6% on heavy courses | mip chains 64x64..4x1 copied near the GPU-only regions; neither image is GPU-written, so the software blitter (un)swizzles them on the CPU | open (expected for small copies; many of them) |
+| 138 | `async_presentation=true` crashed 4 of 8 runs ~26 s in (row 118) | retested after the fence-operation race (row 118) and translation cache race (row 125) were fixed: 0 crashes in 4 runs, screenshots correct | on in the dev config (`async_presentation\default=false`, `async_presentation=true`); every later run is a crash test for it |
+| 139 | buffer collector: up to 64 buffers evicted every frame, GPU-written ones downloaded with a GPU wait (~5% of the GPU thread on 8 GB) | it compared the whole device's use (textures, ~6.8 GB) with thresholds from the device size (6.14 GiB reported): aggressive every frame, while the buffers were ~200 MiB | Switch 2 titles collect on the buffers' own size (1 GiB, aggressive 2 GiB) and keep GPU-written buffers outside aggressive mode (c4eaa4b): 0%, presents no longer blocked on those courses (22-26 ms in t23) |
+| 140 | macros ~20% of the GPU thread; the memo's lookups (~76 ns a call in the game, 48 ns in a warm harness) missed the CPU caches | memo records and its map fall out of cache between calls; 4-way last-call slots raised slot hits 48% -> 63% but saved nothing | x86-64 compiler for the Turing MME (a95cedd, `macro_turing_jit.h`, `EDEN_NX2_MME_JIT=0` off): identical to the interpreter on 200,000 recorded calls (`C:\dev\nx2work\mme_test\jit_test.cpp`); in the game 148 ns a call instead of 174, 0 failures, 0 crashes in 3 runs |
+| 141 | draw setup ~6.4 us a draw (55,000 a second) | spread over ~12 steps: uniform buffer copies (2.4%, the fast path streams them every draw and bypasses write tracking), texture views (8.5% incl. ~3.6% streaming new course textures), handle reads, vertex/storage buffer lookups | no single cache-able step; open: small per-step trims. The way past it is a second thread (NX2_GPU_THREAD_SPLIT.md) |
+| 142 | GPU thread ~100% busy, host GPU 20-40%: one thread parses, runs macros, sets state and records draws | the work is sequential but only a few points need a result from what came before (semaphore acquires, MME DMA reads) | GPU thread split (babd395, `gpu_split.h`, `EDEN_NX2_SPLIT=1`): engine methods run in order on a second thread fed by a lock-free ring; releases, syncpoints, wait-for-idle, flushes and channel binds are deferred into the ring, only acquires wait (~2,400 drains a second). ~47,400 draws a second against ~44,000 (3 A/B rounds), same per-call costs |
+| 143 | crashes with the split: CommandChunk::Record (null chunk), vkCmdEndRenderPass on the Vulkan worker | two threads recording into the scheduler: (a) the DMA pusher's split-method debug log read guest memory with the cache-flushing ReadBlock on the front; (b) with async presentation, Swapchain::AcquireNextImage -> Scheduler::Wait flushed the main scheduler from the present thread whenever its tick was not submitted yet (pre-existing race, the split made it frequent) | (a) ReadBlockUnsafe; (b) e4523ab: the present thread waits up to 20 ms for the GPU thread to submit, never flushes. Found with `EDEN_NX2_SPLIT_CHECK=1` (stack of any other thread using the scheduler while the back runs) |
+| 144 | macros on the split's front (`EDEN_NX2_SPLIT=2`) need the registers they read before the back has them | the front keeps copies: every write it sends plus what the back changes on its own (constant buffer offset, shadow_scratch[0], macro bind pointer, shadow RAM). `EDEN_NX2_SPLIT_VERIFY=1` found three differences: writes still in the back's method sink at the handover; a multi-word run that changes the shadow RAM mode mid-run; and shadow_state.shadow_ram_control being the shadow copy of register 0x49 itself (a Replay write reads the mode back) | all three modelled: 0 differences in 40,193 checks (~165 million macro calls). Front: macros 128 ns a call (180 on one thread, cache-hot), drops ~55% of 3D state writes as same-value. Back: 50,000-66,500 draws a second against 41,500-47,000 without the split; frames 20.5 / 25.5 ms on two courses where the unsplit runs measured 25.6-40.7 ms. Default stays off until a clean A/B |
+| 145 | is the split worth having on by default | clean A/B, nothing else running, 3 rounds each (profiler on in both): one thread 41,100 / 48,600 / 37,000 draws a second, frames 30.5 / 35.0 / 27.1 ms; `EDEN_NX2_SPLIT=2` 59,200 / 62,400 / 62,700 draws a second, frames 28.6 / 23.3 / 25.1 ms. Throughput 1.46x, frames 17% shorter (the back is now ~92% busy, ~53% of it draw setup); 0 crashes, 0 register differences in 57,089 checks after the front's fast paths (no guarded statics, inline ring writes) | on by default for Switch 2 titles (`EDEN_NX2_SPLIT=0` one thread, `=1` macros on the back). Default run without the profiler: 16.9 ms frames (~59 fps) on a light course |
+| 146 | title 4 aborts with InvalidHandle (0xE401) at its first movie, ~30 min into a new game | newer SDKs (`nn::codec::video::detail::InitializeServerContext`) open the decoder with gsv 906(u8, u64 library version 0x3'00030000) instead of 902; auto-stubbed, no handle came back | 906 hands out the 902 session (d8d11ec) |
+| 147 | title 1: Grand Prix cup-opening screen stays black, decoder registered but never started; then an abort with 2111-3604 (0x1c286f) when it did start | `nn::syncpt::sdk::CancelSyncPointEvent` sends 202 and always waits on the slot's event; we signalled it only for pending requests. Then the movie player reused slot 0, which NVN's fence waits had left signalled, read it as a decoded frame and found none | 202 always signals, 201 clears the slot's event first (b09629f) |
+| 148 | title 1/4 movies have no picture | the video decoder service (nn::codec::video, gsv 902/906/1002 -> vdec -> vdecsvc) completed frames without decoding | FFmpeg decodes on a thread per decoder service (`video_core/host1x/nx2_video_decoder`): 101 names the codec ("video/vp9") in an A buffer, 109 submits frames (A buffer, pts us). 311/314 reply NativeSyncHandles {~syncpoint, 0, value} on a syncpoint the service allocates, incremented per decoded frame, frames filling images in register/release order. gsv 911 creates the pool's images, 961 returns the CpuUserImageDescriptor that `ReadDecoderOutputImage*` copy out with (pitch-linear NV12/RGBA/BGRA, kinds 10/8/9). Title 1's Mushroom Cup opening (2560x1440 VP9, 1423 frames) decodes in real time and the game goes on to the race. The picture still stayed on the first frame: see 149. `EDEN_NX2_VDEC_DUMP=` saves decoded frames |
+| 149 | title 1: the cup-opening movie's picture stays on its first frame although every frame decodes and reaches GPU memory | not the texture cache: the movie is drawn into a 2560x1440 render target whose NVN2 texture has GPU address 0 (also the 640x360 and 512x512 passes), so the draws go nowhere. NVN2's texture init (sdk+0x5ca240) places a texture in a compressible pool (flags 0x080000a1) only after `NvRmGpuMappingModify` switched the pool's mapping (0x815f0d0000, a gpu 214 region) to the texture's kind; NvRm fails that call with 2 unless the characteristics flags have NVGPU_GPU_FLAGS_SUPPORT_MAPPING_MODIFY (bit 34; ours were 0x4400155), and the texture keeps address 0 | report bit 34; the modify then runs through nn::gpu::sdk's VirtualMachine as gpu sub 212 (NormalModifyMemoryKind4K: words 3-6 range, 7 wanted kind, 8 fallback, 10 job slot; reply word 3 the kind applied), answered with the wanted kind and the slot signalled. The movie plays on screen |
+
+Run settings used (in `build/bin/user/config/sdl2-config.ini`): `memory_layout_mode=4`
+(12 GB). Debug knobs: `EDEN_NX2_TC_EXPECTED_MIB` / `EDEN_NX2_TC_CRITICAL_MIB` (collector thresholds),
+`EDEN_NX2_ASTC_FLUSH=1` (GPU ASTC decode without the full wait), `EDEN_NX2_CRASH_DUMP=` (prints the
+faulting stack as module+offset, then a minidump), `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_GPU_PROFILE` (GPU-thread zone timers every 10 s), `EDEN_NX2_SAMPLE=` with
+`_START`/`_SECONDS`/`_OUT` (sampling profiler; `C:\dev\nx2work\folded.py` reads its .folded stacks),
+`EDEN_NX2_MME_RECORD=` (macro calls for `C:\dev\nx2work\mme_test`),
+`EDEN_NX2_FENCE_BATCH=0`, `EDEN_NX2_MACRO_COPY=1`, `EDEN_NX2_CNTPCT_PAUSE=0` (turn rows 124, 126, 127 off),
+`EDEN_NX2_MME_MEMO_STATS` (memo counters),
+`EDEN_NX2_BUFFER_DOWNLOADS=0/1`, `EDEN_NX2_INLINE_DEFER=0`, `EDEN_NX2_TC_MIN_AGE=` (rows 131, 134, 133),
+`EDEN_NX2_SKIP_SAME=0` (row 135),
+`EDEN_NX2_MME_JIT=0` (row 140),
+`EDEN_NX2_SPLIT=0/1` (default 2), `EDEN_NX2_SPLIT_VERIFY=1`, `EDEN_NX2_SPLIT_CHECK=1`, `EDEN_NX2_SPLIT_SKIP=0` (rows 142-145),
+`EDEN_NX2_FRAME_CAPTURE=` (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 ` 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`): boots, shows the world-map
+loading screen and reaches its title screen ("Press L + R to start") with sky, buildings, props,
+kart and logo drawn. Wrong: magenta/cyan colour cast, black ground (unbacked virtual texture, row
+87), task/mesh geometry missing (row 89), ~10-17 fps. Launch with frame capture:
+`EDEN_NX2_FRAME_CAPTURE=C:/dev/frames_t1x EDEN_NX2_AUTO_STUB=1 EDEN_NX2_SHADER_DUMP=C:/dev/shaders_title1 ./eden-cli.exe -g "C:/switch/first switch game/exefs/main"`.
+Offline shader check: `nx2work/lowertest/lowertest.exe C:/dev/shaders_title1 [-all [op]]`.
+
+**Title 2**: main/sdk/subsdk0 fail LZ4 and SHA-256 checks; can't be run from these files.
+
+## Prioritized next steps
+
+1. **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.
+2. **Title 1 / title 3** retests with the current tree; dump their shaders and widen the lowering.
+3. 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.
+4. **Six cores** instead of folding 4/5 onto 0/1; rare SIMD encodings still undecoded.
+5. 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.
diff --git a/README.md b/README.md
index 7d3fe5572e..c0d8b65d8a 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ For information on provided development tooling, see the [Tools directory](./too
## Download
-You can download the latest releases from [our release page](https://git.eden-emu.dev/eden-emu/eden/releases).
+You can download the latest releases from [our release page](https://git.eden-emu.dev/Takoroka/eden-nx2/releases).
Save us some bandwidth! We have [mirrors available](./docs/user/ThirdParty.md#mirrors) as well.
diff --git a/src/audio_core/renderer/behavior/info_updater.cpp b/src/audio_core/renderer/behavior/info_updater.cpp
index a88eae91ad..ca3eaece94 100644
--- a/src/audio_core/renderer/behavior/info_updater.cpp
+++ b/src/audio_core/renderer/behavior/info_updater.cpp
@@ -588,7 +588,11 @@ Result InfoUpdater::UpdateBehaviorInfo(BehaviorInfo& behaviour_) {
return Service::Audio::ResultInvalidUpdateInfo;
}
- if (in_params->revision != behaviour_.GetUserRevision()) {
+ // Compare revision numbers: Switch 2 opens the renderer with 'REV?' but tags its updates
+ // 'REv?' (lowercase v) for the same revision.
+ if (GetRevisionNum(in_params->revision) != GetRevisionNum(behaviour_.GetUserRevision())) {
+ LOG_ERROR(Service_Audio, "BehaviorInfo revision {:08X} does not match the renderer's {:08X}",
+ in_params->revision, behaviour_.GetUserRevision());
return Service::Audio::ResultInvalidUpdateInfo;
}
diff --git a/src/audio_core/sink/sink_stream.cpp b/src/audio_core/sink/sink_stream.cpp
index c70020e9da..9dfd3bf747 100644
--- a/src/audio_core/sink/sink_stream.cpp
+++ b/src/audio_core/sink/sink_stream.cpp
@@ -4,8 +4,10 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include
#include
#include
+#include
#include
#include
#include
@@ -213,6 +215,19 @@ void SinkStream::ProcessAudioOutAndRender(std::span output_buffer, std::siz
std::memcpy(last_frame.data(), &output_buffer[(frames_written - 1) * frame_size], frame_size_bytes);
+ // EDEN_NX2_AUDIO_LEVEL: log the output peak about every two seconds (is a game audible?).
+ if (static const bool meter = std::getenv("EDEN_NX2_AUDIO_LEVEL") != nullptr; meter) {
+ for (size_t i = 0; i < num_frames * frame_size; ++i) {
+ level_peak = std::max(level_peak, std::abs(static_cast(output_buffer[i])));
+ }
+ level_frames += num_frames;
+ if (level_frames >= TargetSampleRate * 2) {
+ LOG_INFO(Audio, "Audio output peak over {} frames: {} of 32767", level_frames, level_peak);
+ level_frames = 0;
+ level_peak = 0;
+ }
+ }
+
{
std::scoped_lock lk{sample_count_lock};
last_sample_count_update_time = system.CoreTiming().GetGlobalTimeNs();
diff --git a/src/audio_core/sink/sink_stream.h b/src/audio_core/sink/sink_stream.h
index acb7537dda..280b7747ec 100644
--- a/src/audio_core/sink/sink_stream.h
+++ b/src/audio_core/sink/sink_stream.h
@@ -245,6 +245,9 @@ private:
SinkBuffer playing_buffer{};
/// The last played (or received) frame of audio, used when the callback underruns
std::array last_frame{};
+ /// EDEN_NX2_AUDIO_LEVEL meter state.
+ s32 level_peak{};
+ u64 level_frames{};
/// Number of buffers waiting to be played
std::atomic queued_buffers{};
/// The ring size for audio out buffers (usually 4, rarely 2 or 8)
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 5d1b30a549..d3e7b03cd2 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -96,6 +96,7 @@ add_library(
${CMAKE_CURRENT_BINARY_DIR}/scm_rev.cpp
scm_rev.h
scope_exit.h
+ spin_recursive_mutex.h
scratch_buffer.h
settings.cpp
settings.h
@@ -151,7 +152,7 @@ add_library(
set_source_files_properties(zbic_compression.cpp PROPERTIES
INCLUDE_DIRECTORIES "${ZBIC_INCLUDE_DIR}"
- COMPILE_OPTIONS "$<$:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>")
+ COMPILE_OPTIONS "$<$:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$:/wd4505;/wd5245>")
if(WIN32)
target_sources(common PRIVATE windows/timer_resolution.cpp
diff --git a/src/common/container/unordered_map.h b/src/common/container/unordered_map.h
index 7d9e49fd80..c4d4a72663 100644
--- a/src/common/container/unordered_map.h
+++ b/src/common/container/unordered_map.h
@@ -9,7 +9,7 @@
namespace Common {
-template , class Pred = std::equal_to,
+template , class Pred = std::equal_to,
class Allocator = std::allocator>>
using unordered_map = boost::unordered::unordered_flat_map;
diff --git a/src/common/container/unordered_set.h b/src/common/container/unordered_set.h
index 54d71406a1..690c33286f 100644
--- a/src/common/container/unordered_set.h
+++ b/src/common/container/unordered_set.h
@@ -9,7 +9,7 @@
namespace Common {
-template , class Pred = std::equal_to,
+template , class Pred = std::equal_to,
class Allocator = std::allocator>
using unordered_set = boost::unordered::unordered_flat_set;
diff --git a/src/common/container_hash.h b/src/common/container_hash.h
index d7477f7e4f..f06f69ad8b 100644
--- a/src/common/container_hash.h
+++ b/src/common/container_hash.h
@@ -19,6 +19,28 @@
namespace Common {
+/// The default hash of Common::unordered_map/set. std::hash of an integer or a pointer is a
+/// byte-wise FNV-1a on MSVC, and boost's flat containers mix a hash that does not say it
+/// avalanches anyway: pass those keys through (the GPU thread's page maps looked keys up
+/// tens of thousands of times a second with it). Other keys keep std::hash.
+template
+struct FastHash : std::hash {};
+
+template
+ requires std::is_integral_v
+struct FastHash {
+ std::size_t operator()(Key key) const noexcept {
+ return static_cast(key);
+ }
+};
+
+template
+struct FastHash {
+ std::size_t operator()(T* pointer) const noexcept {
+ return reinterpret_cast(pointer);
+ }
+};
+
namespace detail {
template
diff --git a/src/common/cpu_features.cpp b/src/common/cpu_features.cpp
index 8ccb6847f7..52fd003c44 100644
--- a/src/common/cpu_features.cpp
+++ b/src/common/cpu_features.cpp
@@ -225,6 +225,13 @@ const CPUCaps g_cpu_caps = [] {
}
}
+ // AMD CPUs have no leaf 0x15 (Zen 4 stops at 0x10): measure their invariant TSC too, or every
+ // guest clock read (CNTPCT, which games read thousands of times a frame) goes through the
+ // host's much slower QueryPerformanceCounter.
+ if (caps.invariant_tsc && caps.tsc_frequency == 0) {
+ caps.tsc_frequency = X64::EstimateRDTSCFrequency();
+ }
+
if (max_std_fn >= 0x16) {
__cpuid(cpu_id, 0x16);
caps.base_frequency = cpu_id[0];
diff --git a/src/common/range_sets.h b/src/common/range_sets.h
index f8fcee4837..e5cb53b058 100644
--- a/src/common/range_sets.h
+++ b/src/common/range_sets.h
@@ -26,6 +26,9 @@ public:
void Clear();
bool Empty() const;
+ /// Whether any range overlaps [base_address, base_address + size): one lookup.
+ bool Intersects(AddressType base_address, size_t size) const;
+
template
void ForEach(Func&& func) const;
diff --git a/src/common/range_sets.inc b/src/common/range_sets.inc
index 3edd8c8a43..aeda7bc351 100644
--- a/src/common/range_sets.inc
+++ b/src/common/range_sets.inc
@@ -116,28 +116,25 @@ struct OverlapRangeSet::OverlapRangeSetImpl {
}
AddressType end_address = base_address + static_cast(size);
IntervalType interval{base_address, end_address};
- bool any_removals = false;
m_split_ranges_set += std::make_pair(interval, -amount);
- do {
- any_removals = false;
- auto it = m_split_ranges_set.lower_bound(interval);
- if (it == m_split_ranges_set.end()) {
- return;
+ // One pass, erasing the segments that dropped to zero or below in address order (the
+ // search restarted after every erase: quadratic in the segments of the range, and the
+ // GPU fencing thread does this for each downloaded buffer range while holding the buffer
+ // cache's lock). Erasing a node leaves the other iterators valid.
+ auto it = m_split_ranges_set.lower_bound(interval);
+ const auto end_it = m_split_ranges_set.upper_bound(interval);
+ while (it != end_it) {
+ if (it->second > 0) {
+ ++it;
+ continue;
}
- auto end_it = m_split_ranges_set.upper_bound(interval);
- for (; it != end_it; it++) {
- if (it->second <= 0) {
- if constexpr (has_on_delete) {
- if (it->second == 0) {
- on_delete(it->first.lower(), it->first.upper());
- }
- }
- any_removals = true;
- m_split_ranges_set.erase(it);
- break;
+ if constexpr (has_on_delete) {
+ if (it->second == 0) {
+ on_delete(it->first.lower(), it->first.upper());
}
}
- } while (any_removals);
+ m_split_ranges_set.erase(it++);
+ }
}
template
@@ -223,6 +220,18 @@ bool RangeSet::Empty() const {
return m_impl->m_ranges_set.empty();
}
+template
+bool RangeSet::Intersects(AddressType base_address, size_t size) const {
+ const auto& set = m_impl->m_ranges_set;
+ if (set.empty() || size == 0) {
+ return false;
+ }
+ const AddressType end_address = base_address + static_cast(size);
+ // The first range not entirely below the query; it overlaps when it starts before its end.
+ const auto it = set.lower_bound(typename RangeSetImpl::IntervalType{base_address, end_address});
+ return it != set.end() && it->lower() < end_address;
+}
+
template
template
void RangeSet::ForEach(Func&& func) const {
diff --git a/src/common/sparse_large_vector.cpp b/src/common/sparse_large_vector.cpp
index eee8ff6d18..b96340999d 100644
--- a/src/common/sparse_large_vector.cpp
+++ b/src/common/sparse_large_vector.cpp
@@ -26,7 +26,7 @@ static std::mutex vector_regions_mutex {};
// Workaround for handling non-commited memory accessed by Dynarmic; usually result of an error
static LONG WINAPI FakePageFaultHandler(PEXCEPTION_POINTERS info) {
DWORD code = info->ExceptionRecord->ExceptionCode;
- u64 exception_addr = reinterpret_cast(info->ExceptionRecord->ExceptionInformation[1]);
+ u64 exception_addr = static_cast(info->ExceptionRecord->ExceptionInformation[1]);
if (code != EXCEPTION_ACCESS_VIOLATION || info->ExceptionRecord->ExceptionInformation[0] == 1) {
// Not our problem
diff --git a/src/common/spin_recursive_mutex.h b/src/common/spin_recursive_mutex.h
new file mode 100644
index 0000000000..291dd4e41e
--- /dev/null
+++ b/src/common/spin_recursive_mutex.h
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+
+#if defined(_M_X64) || defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)
+#include
+#endif
+
+namespace Common {
+
+/// A recursive mutex that spins briefly before sleeping. The GPU caches' locks are taken tens of
+/// thousands of times a second by the GPU thread, the fencing thread and CPU write traps, and
+/// held for microseconds: std::recursive_mutex put a waiting thread to sleep at once, and its
+/// wake-up took longer than the critical section (title 1's GPU thread spent ~8% asleep there).
+class SpinRecursiveMutex {
+public:
+ void lock() {
+ const std::thread::id self = std::this_thread::get_id();
+ if (owner.load(std::memory_order_relaxed) == self) {
+ ++depth;
+ return;
+ }
+ std::uint32_t expected = 0;
+ if (!state.compare_exchange_strong(expected, 1, std::memory_order_acquire,
+ std::memory_order_relaxed)) {
+ LockContended();
+ }
+ owner.store(self, std::memory_order_relaxed);
+ depth = 1;
+ }
+
+ bool try_lock() {
+ const std::thread::id self = std::this_thread::get_id();
+ if (owner.load(std::memory_order_relaxed) == self) {
+ ++depth;
+ return true;
+ }
+ std::uint32_t expected = 0;
+ if (!state.compare_exchange_strong(expected, 1, std::memory_order_acquire,
+ std::memory_order_relaxed)) {
+ return false;
+ }
+ owner.store(self, std::memory_order_relaxed);
+ depth = 1;
+ return true;
+ }
+
+ void unlock() {
+ if (--depth != 0) {
+ return;
+ }
+ owner.store(std::thread::id{}, std::memory_order_relaxed);
+ if (state.exchange(0, std::memory_order_release) == 2) {
+ state.notify_one();
+ }
+ }
+
+private:
+ // About 10 microseconds of spinning before sleeping.
+ static constexpr int SpinCount = 1024;
+
+ static void Pause() {
+#if defined(_M_X64) || defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)
+ _mm_pause();
+#elif defined(__aarch64__)
+ asm volatile("yield");
+#endif
+ }
+
+ void LockContended() {
+ for (int spin = 0; spin < SpinCount; ++spin) {
+ Pause();
+ if (state.load(std::memory_order_relaxed) == 0) {
+ std::uint32_t expected = 0;
+ if (state.compare_exchange_weak(expected, 1, std::memory_order_acquire,
+ std::memory_order_relaxed)) {
+ return;
+ }
+ }
+ }
+ // 2: held with (possible) sleepers; taken this way it stays marked, so its unlock wakes
+ // the next sleeper.
+ while (state.exchange(2, std::memory_order_acquire) != 0) {
+ state.wait(2, std::memory_order_relaxed);
+ }
+ }
+
+ std::atomic state{0}; ///< 0 free, 1 held, 2 held with sleepers
+ std::atomic owner{};
+ std::uint32_t depth{}; ///< Recursion depth, touched only by the owner
+};
+
+} // namespace Common
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 1b578f67ba..56fa653738 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -457,6 +457,7 @@ add_library(core STATIC
hle/service/am/service/application_functions.cpp
hle/service/am/service/application_functions.h
hle/service/am/service/application_proxy.cpp
+ hle/service/am/service/graphics_scale_controller.h
hle/service/am/service/application_proxy.h
hle/service/am/service/application_proxy_service.cpp
hle/service/am/service/application_proxy_service.h
@@ -526,6 +527,7 @@ add_library(core STATIC
hle/service/audio/audio_out.h
hle/service/audio/audio_renderer_manager.cpp
hle/service/audio/audio_renderer_manager.h
+ hle/service/audio/nx2_audio_user_client.h
hle/service/audio/audio_renderer.cpp
hle/service/audio/audio_renderer.h
hle/service/audio/audio.cpp
@@ -865,6 +867,7 @@ add_library(core STATIC
hle/service/nvdrv/devices/nvhost_vic.h
hle/service/nvdrv/devices/nvmap.cpp
hle/service/nvdrv/devices/nvmap.h
+ hle/service/nvdrv/devices/nx2_stub_device.h
hle/service/nvdrv/nvdata.h
hle/service/nvdrv/nvdrv.cpp
hle/service/nvdrv/nvdrv.h
@@ -1033,6 +1036,8 @@ add_library(core STATIC
hle/service/server_manager.h
hle/service/service.cpp
hle/service/service.h
+ hle/service/nx2_stub_interface.h
+ hle/service/nx2_gsv.h
hle/service/services.cpp
hle/service/services.h
hle/service/set/factory_settings_server.cpp
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.cpp b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
index 894857a86d..a2c0124f33 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
@@ -4,6 +4,11 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include
+#include
+#if defined(_M_X64) || defined(__x86_64__)
+#include
+#endif
#include "common/settings.h"
#include "core/arm/dynarmic/arm_dynarmic.h"
#include "core/arm/dynarmic/arm_dynarmic_64.h"
@@ -24,13 +29,24 @@ DynarmicCallbacks64::DynarmicCallbacks64(ArmDynarmic64& parent, Kernel::KProcess
u64 DynarmicCallbacks64::MemoryRead(u64 vaddr, size_t size) {
CheckMemoryAccess(vaddr, size, Kernel::DebugWatchpointType::Read);
+ u64 value{};
switch (size) {
- case sizeof(u64): return m_memory.Read64(vaddr);
- case sizeof(u32): return m_memory.Read32(vaddr);
- case sizeof(u16): return m_memory.Read16(vaddr);
- case sizeof(u8): return m_memory.Read8(vaddr);
+ case sizeof(u64): value = m_memory.Read64(vaddr); break;
+ case sizeof(u32): value = m_memory.Read32(vaddr); break;
+ case sizeof(u16): value = m_memory.Read16(vaddr); break;
+ case sizeof(u8): value = m_memory.Read8(vaddr); break;
default: UNREACHABLE();
}
+ // With memory aborts ignored the guest keeps running on zeros: say where it read from.
+ if (auto& unmapped = Core::Memory::UnmappedReadFlag(); unmapped != 0) {
+ unmapped = 0;
+ static std::atomic logged{};
+ if (logged.fetch_add(1) < 32) {
+ LOG_ERROR(Core_ARM, "Unmapped read {:#x} ({} bytes) at pc {:#x} lr {:#x} sp {:#x}", vaddr, size,
+ m_parent.m_jit->GetPC(), m_parent.m_jit->GetRegister(30), m_parent.m_jit->GetSP());
+ }
+ }
+ return value;
}
Dynarmic::A64::Vector DynarmicCallbacks64::MemoryRead128(u64 vaddr) {
CheckMemoryAccess(vaddr, 16, Kernel::DebugWatchpointType::Read);
@@ -154,7 +170,26 @@ u64 DynarmicCallbacks64::GetTicksRemaining() {
}
u64 DynarmicCallbacks64::GetCNTPCT() {
- return m_parent.m_system.CoreTiming().GetClockTicks();
+ const u64 ticks = m_parent.m_system.CoreTiming().GetClockTicks();
+ // Guest code that waits by polling the counter (title 1's main thread reads it ~17 million
+ // times a second while waiting for the GPU) keeps a host core busy at full speed, and the
+ // GPU thread is often on the other half of that SMT core. Two reads within ~1.7 us of each
+ // other are such a loop: pause, which hands the core's resources to the sibling thread for a
+ // moment. The guest sees the same counter. EDEN_NX2_CNTPCT_PAUSE=0 turns it off.
+ static const bool pause_spins = [] {
+ const char* value = std::getenv("EDEN_NX2_CNTPCT_PAUSE");
+ return value == nullptr || value[0] != '0';
+ }();
+ if (pause_spins && ticks - m_last_cntpct < 32) {
+#if defined(_M_X64) || defined(__x86_64__)
+ _mm_pause();
+ _mm_pause();
+#elif defined(__aarch64__)
+ asm volatile("yield");
+#endif
+ }
+ m_last_cntpct = ticks;
+ return ticks;
}
bool DynarmicCallbacks64::CheckMemoryAccess(u64 addr, u64 size, Kernel::DebugWatchpointType type) {
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.h b/src/core/arm/dynarmic/arm_dynarmic_64.h
index 5ea5dde8a6..acd1fe773f 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.h
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.h
@@ -65,6 +65,7 @@ public:
Core::Memory::Memory& m_memory;
u64 m_tpidrro_el0{};
u64 m_tpidr_el0{};
+ u64 m_last_cntpct{}; ///< The previous counter read (GetCNTPCT spin detection)
Kernel::KProcess* m_process{};
const bool m_debugger_enabled{};
const bool m_check_memory_access{};
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 06543b0833..f3d224b1ce 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -24,6 +24,7 @@
#include "core/device_memory.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/patch_manager.h"
+#include "core/file_sys/program_metadata.h"
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/romfs_factory.h"
#include "core/file_sys/savedata_factory.h"
@@ -68,6 +69,8 @@
namespace Core {
+std::atomic g_nx2_present_count{};
+
FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
const std::string& path) {
// To account for split 00+01+etc files.
@@ -107,6 +110,19 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
return vfs->OpenFile(path, FileSys::OpenMode::Read);
}
+namespace {
+
+// Switch 2 titles boot as a loose ExeFS, with main.npdm next to main.
+bool IsSwitch2Program(const FileSys::VirtualFile& file) {
+ const auto dir = file != nullptr ? file->GetContainingDirectory() : nullptr;
+ const auto npdm = dir != nullptr ? dir->GetFile("main.npdm") : nullptr;
+ FileSys::ProgramMetadata metadata;
+ return npdm != nullptr && metadata.Load(npdm) == Loader::ResultStatus::Success &&
+ metadata.IsSwitch2();
+}
+
+} // namespace
+
struct System::Impl {
explicit Impl(System& system)
: kernel{system}, fs_controller{system}, hid_core{kernel}, cpu_manager{system},
@@ -297,6 +313,16 @@ struct System::Impl {
fs_controller.InitTempStorage();
}
+ // Switch 2 titles size their heap from the total memory and need the 12 GiB layout. Set it
+ // as a per-game value, so RestoreGlobalState brings back the configured layout afterwards.
+ if (virtual_filesystem != nullptr &&
+ Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_12Gb &&
+ IsSwitch2Program(GetGameFileFromPath(virtual_filesystem, filepath))) {
+ LOG_INFO(Core, "Switch 2 program: using the 12 GiB memory layout");
+ Settings::values.memory_layout_mode.SetGlobal(false);
+ Settings::values.memory_layout_mode.SetValue(Settings::MemoryLayout::Memory_12Gb);
+ }
+
InitializeKernel(system);
if (params.applet_type == Service::AM::AppletType::Application) {
diff --git a/src/core/core.h b/src/core/core.h
index d88ed64151..0276679d76 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -6,6 +6,7 @@
#pragma once
+#include
#include
#include
#include
@@ -115,6 +116,9 @@ class RenderdocAPI;
namespace Core {
+/// Frames the guest has presented (Switch 2 vi2 present calls); input replay is locked to it.
+extern std::atomic g_nx2_present_count;
+
class CpuManager;
class Debugger;
class DeviceMemory;
diff --git a/src/core/debugger/gdbstub.cpp b/src/core/debugger/gdbstub.cpp
index 44ddf11c39..71af2acee3 100644
--- a/src/core/debugger/gdbstub.cpp
+++ b/src/core/debugger/gdbstub.cpp
@@ -25,7 +25,10 @@
#include "core/debugger/gdbstub_arch.h"
#include "core/hle/kernel/k_page_table.h"
#include "core/hle/kernel/k_process.h"
+#include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_readable_event.h"
#include "core/hle/kernel/k_thread.h"
+#include "core/hle/service/kernel_helpers.h"
#include "core/loader/loader.h"
#include "core/memory.h"
@@ -731,8 +734,26 @@ void GDBStub::HandleRcmd(const std::vector& command) {
break;
cur_addr = next_address;
}
+ } else if (command_str == "handles" || command_str == "get handles") {
+ // Every handle of the process: its object type and, for service events, their name.
+ reply = "Handles:\n";
+ auto& handle_table = debug_process->GetHandleTable();
+ for (size_t i = 0; i < handle_table.GetTableSize(); ++i) {
+ Kernel::Handle handle{};
+ auto object = handle_table.GetObjectByIndex(system.Kernel(), std::addressof(handle), i);
+ if (object.IsNull()) {
+ continue;
+ }
+ std::string name;
+ if (auto* readable = object->DynamicCast(); readable != nullptr) {
+ name = Service::KernelHelpers::ServiceContext::GetEventName(readable->GetParent());
+ } else if (auto* event = object->DynamicCast(); event != nullptr) {
+ name = Service::KernelHelpers::ServiceContext::GetEventName(event);
+ }
+ reply += fmt::format(" {:#010x} {} {}\n", handle, object->GetTypeObj().GetName(), name);
+ }
} else {
- reply += "Commands: fastmem, info, mappings\n";
+ reply += "Commands: fastmem, info, mappings, handles\n";
}
std::span reply_span{reinterpret_cast(&reply.front()), reply.size()};
diff --git a/src/core/device_memory_manager.h b/src/core/device_memory_manager.h
index b4b3b46088..46cda14e78 100644
--- a/src/core/device_memory_manager.h
+++ b/src/core/device_memory_manager.h
@@ -132,6 +132,27 @@ private:
u8* host_ptr{};
};
+ /// A thread's last four page translations of one manager. Several threads read through
+ /// them (the GPU thread, the fencing thread, service threads): one cache shared by all was
+ /// cleared by Map on a service thread between the GPU thread's null check of an entry and
+ /// its use, and the GPU thread copied from address 0 (title 1 crashed every few minutes).
+ struct TranslationCache {
+ const void* owner{};
+ u64 generation{};
+ std::array slots{};
+ u32 cursor{};
+ };
+ static inline thread_local TranslationCache t_cache{};
+
+ /// This thread's translations, emptied when Map or Unmap ran since they were made.
+ TranslationCache& Translations() {
+ const u64 current = translation_generation.load(std::memory_order_acquire);
+ if (t_cache.owner != this || t_cache.generation != current) {
+ t_cache = TranslationCache{.owner = this, .generation = current};
+ }
+ return t_cache;
+ }
+
// Internal helper that performs the update assuming the caller already holds the necessary lock.
void UpdatePagesCachedCountNoLock(DAddr addr, size_t size, s32 delta);
@@ -139,7 +160,7 @@ private:
static constexpr size_t device_virtual_bits = Traits::device_virtual_bits;
static constexpr size_t device_as_size = 1ULL << device_virtual_bits;
static constexpr size_t physical_min_bits = 32;
- static constexpr size_t physical_max_bits = 33;
+ static constexpr size_t physical_max_bits = 35; // covers the 12 GiB layout plus nx2 graphics server memory
static constexpr size_t page_bits = 12;
static constexpr size_t page_size = 1ULL << page_bits;
static constexpr size_t page_mask = page_size - 1ULL;
@@ -204,8 +225,7 @@ private:
tracked_entries.GetUnchecked(page_index).cpu_backing_address = address | (asid.id << asid_start_bit);
}
- std::array t_slot{};
- u32 cache_cursor = 0;
+ std::atomic translation_generation{1}; ///< Bumped by Map and Unmap
using CounterType = u8;
using CounterAtomicType = std::atomic_uint8_t;
static constexpr size_t subentries = 8 / sizeof(CounterType);
diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc
index 97c2706abb..d70444ef8c 100644
--- a/src/core/device_memory_manager.inc
+++ b/src/core/device_memory_manager.inc
@@ -234,7 +234,7 @@ void DeviceMemoryManager::Map(DAddr address, VAddr virtual_address, size
}
impl->multi_dev_address.Register(new_dev, start_id);
}
- t_slot = {};
+ translation_generation.fetch_add(1, std::memory_order_release);
if (track) {
TrackContinuityImpl(address, virtual_address, size, asid);
}
@@ -268,7 +268,7 @@ void DeviceMemoryManager::Unmap(DAddr address, size_t size) {
base_dev = new_start | MULTI_FLAG;
}
}
- t_slot = {};
+ translation_generation.fetch_add(1, std::memory_order_release);
}
template
void DeviceMemoryManager::TrackContinuityImpl(DAddr address, VAddr virtual_address,
@@ -404,9 +404,10 @@ void DeviceMemoryManager::ReadBlock(DAddr address, void* dest_pointer, s
const std::size_t page_offset = address & Memory::YUZU_PAGEMASK;
if (size <= Memory::YUZU_PAGESIZE - page_offset) {
const DAddr guest_page = address & ~static_cast(Memory::YUZU_PAGEMASK);
- for (size_t i = 0; i < 4; ++i) {
- if (t_slot[i].guest_page == guest_page && t_slot[i].host_ptr != nullptr) {
- std::memcpy(dest_pointer, t_slot[i].host_ptr + page_offset, size);
+ TranslationCache& cache = Translations();
+ for (const TranslationEntry& entry : cache.slots) {
+ if (entry.guest_page == guest_page && entry.host_ptr != nullptr) {
+ std::memcpy(dest_pointer, entry.host_ptr + page_offset, size);
return;
}
}
@@ -415,8 +416,8 @@ void DeviceMemoryManager::ReadBlock(DAddr address, void* dest_pointer, s
const auto phys_addr = tracked_entries[page_index].compressed_physical_ptr;
if (phys_addr != 0) {
auto* const mem_ptr = GetPointerFromRaw((PAddr(phys_addr - 1) << Memory::YUZU_PAGEBITS));
- t_slot[cache_cursor % t_slot.size()] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
- cache_cursor = (cache_cursor + 1) & 3U;
+ cache.slots[cache.cursor] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
+ cache.cursor = (cache.cursor + 1) & 3U;
std::memcpy(dest_pointer, mem_ptr + page_offset, size);
return;
}
@@ -462,9 +463,10 @@ void DeviceMemoryManager::ReadBlockUnsafe(DAddr address, void* dest_poin
const std::size_t page_offset = address & Memory::YUZU_PAGEMASK;
if (size <= Memory::YUZU_PAGESIZE - page_offset) {
const DAddr guest_page = address & ~static_cast(Memory::YUZU_PAGEMASK);
- for (size_t i = 0; i < 4; ++i) {
- if (t_slot[i].guest_page == guest_page && t_slot[i].host_ptr != nullptr) {
- std::memcpy(dest_pointer, t_slot[i].host_ptr + page_offset, size);
+ TranslationCache& cache = Translations();
+ for (const TranslationEntry& entry : cache.slots) {
+ if (entry.guest_page == guest_page && entry.host_ptr != nullptr) {
+ std::memcpy(dest_pointer, entry.host_ptr + page_offset, size);
return;
}
}
@@ -473,8 +475,8 @@ void DeviceMemoryManager::ReadBlockUnsafe(DAddr address, void* dest_poin
const auto phys_addr = tracked_entries[page_index].compressed_physical_ptr;
if (phys_addr != 0) {
auto* const mem_ptr = GetPointerFromRaw((PAddr(phys_addr - 1) << Memory::YUZU_PAGEBITS));
- t_slot[cache_cursor % t_slot.size()] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
- cache_cursor = (cache_cursor + 1) & 3U;
+ cache.slots[cache.cursor] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
+ cache.cursor = (cache.cursor + 1) & 3U;
std::memcpy(dest_pointer, mem_ptr + page_offset, size);
return;
}
diff --git a/src/core/file_sys/program_metadata.cpp b/src/core/file_sys/program_metadata.cpp
index 63ca9d3fe3..464e496ea3 100644
--- a/src/core/file_sys/program_metadata.cpp
+++ b/src/core/file_sys/program_metadata.cpp
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include
+#include
#include
#include "common/logging.h"
@@ -33,13 +34,30 @@ Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
return Loader::ResultStatus::ErrorBadACIDHeader;
}
+ // Switch 2 ACID blocks have a 0x300-byte signature/key area instead of 0x200, so the
+ // header starts 0x100 bytes later. Offsets inside the header are unchanged.
+ if (std::memcmp(acid_header.magic.data(), "ACID", 4) != 0) {
+ constexpr std::size_t acid_sig_size_v1 = 0x200;
+ constexpr std::size_t acid_sig_size_v2 = 0x300;
+ constexpr std::size_t acid_body_size = sizeof(AcidHeader) - acid_sig_size_v1;
+ auto* const body = reinterpret_cast(&acid_header) + acid_sig_size_v1;
+ if (file->ReadBytes(body, acid_body_size, npdm_header.acid_offset + acid_sig_size_v2) !=
+ acid_body_size ||
+ std::memcmp(acid_header.magic.data(), "ACID", 4) != 0) {
+ return Loader::ResultStatus::ErrorBadACIDHeader;
+ }
+ LOG_INFO(Loader, "NPDM uses the Switch 2 ACID layout");
+ is_switch2 = true;
+ }
+
if (sizeof(AciHeader) != file->ReadObject(&aci_header, npdm_header.aci_offset)) {
return Loader::ResultStatus::ErrorBadACIHeader;
}
// Load acid_file_access per-component instead of the entire struct, since this struct does not
// reflect the layout of the real data.
- std::size_t current_offset = acid_header.fac_offset;
+ // FAC/FAH offsets are relative to the start of the ACID/ACI block, like the KAC offsets.
+ std::size_t current_offset = npdm_header.acid_offset + acid_header.fac_offset;
if (sizeof(FileAccessControl::version) != file->ReadBytes(&acid_file_access.version,
sizeof(FileAccessControl::version),
current_offset)) {
@@ -57,7 +75,7 @@ Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) {
}
// Load aci_file_access per-component instead of the entire struct, same as acid_file_access
- current_offset = aci_header.fah_offset;
+ current_offset = npdm_header.aci_offset + aci_header.fah_offset;
if (sizeof(FileAccessHeader::version) != file->ReadBytes(&aci_file_access.version,
sizeof(FileAccessHeader::version),
current_offset)) {
diff --git a/src/core/file_sys/program_metadata.h b/src/core/file_sys/program_metadata.h
index 115e6d6cd1..5ce32af1a4 100644
--- a/src/core/file_sys/program_metadata.h
+++ b/src/core/file_sys/program_metadata.h
@@ -72,6 +72,10 @@ public:
KernelCapabilityDescriptors capabilities);
bool Is64BitProgram() const;
+ /// True when the NPDM uses the Switch 2 ACID layout.
+ bool IsSwitch2() const {
+ return is_switch2;
+ }
ProgramAddressSpaceType GetAddressSpaceType() const;
u8 GetMainThreadPriority() const;
u8 GetMainThreadCore() const;
@@ -179,6 +183,7 @@ private:
Header npdm_header{};
AciHeader aci_header{};
AcidHeader acid_header{};
+ bool is_switch2{};
FileAccessControl acid_file_access{};
FileAccessHeader aci_file_access{};
diff --git a/src/core/file_sys/vfs/vfs_real.cpp b/src/core/file_sys/vfs/vfs_real.cpp
index 14e0caad3c..fd36752fee 100644
--- a/src/core/file_sys/vfs/vfs_real.cpp
+++ b/src/core/file_sys/vfs/vfs_real.cpp
@@ -19,6 +19,10 @@
// For FileTimeStampRaw
#include
+#ifdef _WIN32
+#include // _getmaxstdio
+#endif
+
#ifdef _MSC_VER
#define stat _stat64
#endif
@@ -33,7 +37,17 @@ namespace FS = Common::FS;
namespace {
-constexpr size_t MaxOpenFiles = 8192;
+// Files the cache keeps open. On Windows it has to stay below the C runtime's open-file limit with
+// room to spare: at the limit every other open fails (saves, shader caches, and the cache's own
+// next file, which it only evicts for once it reaches its cap).
+size_t MaxOpenFiles() {
+#ifdef _WIN32
+ static const size_t limit = static_cast((std::max)(_getmaxstdio() - 512, 64));
+ return (std::min)(size_t{8192}, limit);
+#else
+ return 8192;
+#endif
+}
bool IsWithinRoot(std::string_view root, std::string_view full_path) {
if (root.empty())
@@ -251,7 +265,7 @@ void RealVfsFilesystem::DropReference(std::unique_ptr&& reference
}
void RealVfsFilesystem::EvictSingleReferenceLocked() {
- if (num_open_files < MaxOpenFiles || open_references.empty()) {
+ if (num_open_files < MaxOpenFiles() || open_references.empty()) {
return;
}
diff --git a/src/core/hardware_properties.h b/src/core/hardware_properties.h
index 3f870becbf..aeea5f858f 100644
--- a/src/core/hardware_properties.h
+++ b/src/core/hardware_properties.h
@@ -15,11 +15,14 @@ namespace Hardware {
constexpr u64 BASE_CLOCK_RATE = 1'020'000'000; // Default CPU Frequency = 1020 MHz
constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
-constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores - sync with dynarmic exclusive_monitor.h
+// Switch 1 applications get cores 0..3. Switch 2 applications ask for 0..5 (title 4's npdm), and
+// its job system starts a worker per core, so emulate six: with four, cores 0 and 1 each ran two
+// of its workers. Switch 1 titles leave cores 4 and 5 idle.
+constexpr u32 NUM_CPU_CORES = 6; // Number of CPU Cores - sync with dynarmic exclusive_monitor.h
// Virtual to Physical core map.
constexpr std::array()> VirtualToPhysicalCoreMap{
- 0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,
};
diff --git a/src/core/hle/kernel/board/nintendo/nx/k_system_control.cpp b/src/core/hle/kernel/board/nintendo/nx/k_system_control.cpp
index 3343d1d282..ba4737e25f 100644
--- a/src/core/hle/kernel/board/nintendo/nx/k_system_control.cpp
+++ b/src/core/hle/kernel/board/nintendo/nx/k_system_control.cpp
@@ -94,7 +94,7 @@ size_t KSystemControl::Init::GetIntendedMemorySize() {
case Smc::MemorySize_10GB:
return 10_GiB;
case Smc::MemorySize_12GB:
- return 12_GiB;
+ return 12_GiB + KSystemControl::Nx2GraphicsServerMemorySize;
}
}
diff --git a/src/core/hle/kernel/board/nintendo/nx/k_system_control.h b/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
index 41a25ba1c8..cc5572bde8 100644
--- a/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
+++ b/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
@@ -21,6 +21,10 @@ public:
// This can be overridden as needed.
static constexpr size_t SecureAppletMemorySize = 4 * 1024 * 1024; // 4_MB
+ // Extra DRAM on the 12 GB layout for memory the Switch 2 graphics server keeps itself (nx2:gpu
+ // 214 GPU-only regions). It sits in the non-secure system pool, outside the application pool.
+ static constexpr size_t Nx2GraphicsServerMemorySize = 8ULL * 1024 * 1024 * 1024; // 8_GiB
+
public:
class Init {
public:
diff --git a/src/core/hle/kernel/global_scheduler_context.cpp b/src/core/hle/kernel/global_scheduler_context.cpp
index 6d1cd030e0..98d14c8549 100644
--- a/src/core/hle/kernel/global_scheduler_context.cpp
+++ b/src/core/hle/kernel/global_scheduler_context.cpp
@@ -40,12 +40,15 @@ void GlobalSchedulerContext::RemoveThread(KThread* thread) noexcept {
/// @note This operation happens every 10ms.
void GlobalSchedulerContext::PreemptThreads(KernelCore& kernel) noexcept {
// The priority levels at which the global scheduler preempts threads every 10 ms. They are
- // ordered from Core 0 to Core 3.
+ // ordered from Core 0 to Core 5; core 3 is shared with the system, 4 and 5 are application
+ // cores like 0..2.
static constexpr std::array per_core{
59,
59,
59,
63,
+ 59,
+ 59,
};
ASSERT(KScheduler::IsSchedulerLockedByCurrentThread(kernel));
for (u32 core_id = 0; core_id < per_core.size(); core_id++)
diff --git a/src/core/hle/kernel/k_memory_block.h b/src/core/hle/kernel/k_memory_block.h
index f6543bdb3a..2abca31d87 100644
--- a/src/core/hle/kernel/k_memory_block.h
+++ b/src/core/hle/kernel/k_memory_block.h
@@ -195,7 +195,9 @@ enum class KMemoryAttribute : u8 {
Uncached = static_cast(Svc::MemoryAttribute::Uncached),
PermissionLocked = static_cast(Svc::MemoryAttribute::PermissionLocked),
- SetMask = Uncached | PermissionLocked,
+ GpuAccessible = static_cast(Svc::MemoryAttribute::GpuAccessible),
+
+ SetMask = Uncached | PermissionLocked | GpuAccessible,
};
DECLARE_ENUM_FLAG_OPERATORS(KMemoryAttribute);
diff --git a/src/core/hle/kernel/k_page_table_base.cpp b/src/core/hle/kernel/k_page_table_base.cpp
index 2a0a7d49d5..fd5d2c2dce 100644
--- a/src/core/hle/kernel/k_page_table_base.cpp
+++ b/src/core/hle/kernel/k_page_table_base.cpp
@@ -2102,7 +2102,8 @@ Result KPageTableBase::SetMemoryAttribute(KProcessAddress addr, size_t size, KMe
constexpr KMemoryAttribute AttributeTestMask =
~(KMemoryAttribute::SetMask | KMemoryAttribute::DeviceShared);
const KMemoryState state_test_mask =
- (True(mask & KMemoryAttribute::Uncached) ? KMemoryState::FlagCanChangeAttribute
+ (True(mask & (KMemoryAttribute::Uncached | KMemoryAttribute::GpuAccessible))
+ ? KMemoryState::FlagCanChangeAttribute
: KMemoryState::None) |
(True(mask & KMemoryAttribute::PermissionLocked) ? KMemoryState::FlagCanPermissionLock
: KMemoryState::None);
diff --git a/src/core/hle/kernel/k_process.cpp b/src/core/hle/kernel/k_process.cpp
index 27d1b1bfc4..c3a3b73559 100644
--- a/src/core/hle/kernel/k_process.cpp
+++ b/src/core/hle/kernel/k_process.cpp
@@ -1261,6 +1261,14 @@ Result KProcess::LoadFromMetadata(KernelCore& kernel, const FileSys::ProgramMeta
// Assign remaining properties.
m_ideal_core_id = metadata.GetMainThreadCore();
+ // Switch 2 applications get more memory than a 64-bit heap region holds (nninitStartup sizes
+ // its heap as total memory minus ~6.3 GiB), so count the alias region as well, bounded by the
+ // pool through the resource limit.
+ if (metadata.IsSwitch2()) {
+ m_is_switch2 = true;
+ m_max_process_memory = m_page_table.GetHeapRegionSize() + m_page_table.GetAliasRegionSize();
+ }
+
// Set up emulation context.
this->InitializeInterfaces(kernel);
diff --git a/src/core/hle/kernel/k_process.h b/src/core/hle/kernel/k_process.h
index b5693751c3..d20711c222 100644
--- a/src/core/hle/kernel/k_process.h
+++ b/src/core/hle/kernel/k_process.h
@@ -135,6 +135,7 @@ private:
bool m_is_signaled : 1 = false;
bool m_is_initialized : 1 = false;
bool m_is_application : 1 = false;
+ bool m_is_switch2 : 1 = false;
bool m_is_default_application_system_resource : 1 = false;
bool m_is_suspended : 1 = false;
bool m_is_immortal : 1 = false;
@@ -248,6 +249,10 @@ public:
return m_is_application;
}
+ bool IsSwitch2() const {
+ return m_is_switch2;
+ }
+
bool IsDefaultApplicationSystemResource() const {
return m_is_default_application_system_resource;
}
diff --git a/src/core/hle/kernel/k_spin_lock.cpp b/src/core/hle/kernel/k_spin_lock.cpp
index 8525320374..107d6f1aa3 100644
--- a/src/core/hle/kernel/k_spin_lock.cpp
+++ b/src/core/hle/kernel/k_spin_lock.cpp
@@ -3,18 +3,60 @@
#include "core/hle/kernel/k_spin_lock.h"
+#if defined(_M_X64) || defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)
+#include
+#endif
+
namespace Kernel {
+namespace {
+
+// Spins of about 30-140 cycles each before a contended Lock sleeps (a few microseconds).
+constexpr int SpinCount = 128;
+
+void Pause() {
+#if defined(_M_X64) || defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)
+ _mm_pause();
+#elif defined(__aarch64__)
+ asm volatile("yield");
+#endif
+}
+
+} // Anonymous namespace
+
void KSpinLock::Lock() {
- m_lock.lock();
+ u32 expected = 0;
+ if (m_state.compare_exchange_strong(expected, 1, std::memory_order_acquire,
+ std::memory_order_relaxed)) {
+ return;
+ }
+ for (int spin = 0; spin < SpinCount; ++spin) {
+ Pause();
+ if (m_state.load(std::memory_order_relaxed) == 0) {
+ expected = 0;
+ if (m_state.compare_exchange_weak(expected, 1, std::memory_order_acquire,
+ std::memory_order_relaxed)) {
+ return;
+ }
+ }
+ }
+ // Mark the lock contended and sleep until it is released; taken this way it stays marked,
+ // so its Unlock wakes the next sleeper.
+ while (m_state.exchange(2, std::memory_order_acquire) != 0) {
+ m_state.wait(2, std::memory_order_relaxed);
+ }
}
void KSpinLock::Unlock() {
- m_lock.unlock();
+ if (m_state.exchange(0, std::memory_order_release) == 2) {
+ m_state.notify_one();
+ }
}
bool KSpinLock::TryLock() {
- return m_lock.try_lock();
+ u32 expected = 0;
+ return m_state.compare_exchange_strong(expected, 1, std::memory_order_acquire,
+ std::memory_order_relaxed);
}
} // namespace Kernel
diff --git a/src/core/hle/kernel/k_spin_lock.h b/src/core/hle/kernel/k_spin_lock.h
index 094a1e6be0..f7bc8b4d05 100644
--- a/src/core/hle/kernel/k_spin_lock.h
+++ b/src/core/hle/kernel/k_spin_lock.h
@@ -3,9 +3,11 @@
#pragma once
-#include
+#include
+#include // Kernel headers that use std::mutex get it through this one
#include "common/common_funcs.h"
+#include "common/common_types.h"
#include "core/hle/kernel/k_scoped_lock.h"
namespace Kernel {
@@ -22,7 +24,11 @@ public:
bool TryLock();
private:
- std::mutex m_lock;
+ // 0 free, 1 held, 2 held with (possible) sleepers. The scheduler lock is taken by every
+ // core thousands of times a frame and held for microseconds: a contended Lock spins briefly
+ // before sleeping, instead of putting the host thread to sleep at once and paying its
+ // wake-up latency (std::mutex did).
+ std::atomic m_state{0};
};
// TODO(bunnei): Alias for now, in case we want to implement these accurately in the future.
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index 4f1d849209..b437dd4b4e 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -171,7 +171,7 @@ struct KernelCore::Impl {
schedulers[core_id].reset();
}
- // Next host thead ID to use, 0-3 IDs represent core threads, >3 represent others
+ // Next host thead ID to use, IDs below NUM_CPU_CORES represent core threads, the rest others
next_host_thread_id = Core::Hardware::NUM_CPU_CORES;
// Close kernel objects that were not freed on shutdown
diff --git a/src/core/hle/kernel/physical_core.cpp b/src/core/hle/kernel/physical_core.cpp
index c5236c64e3..c3ae7629aa 100644
--- a/src/core/hle/kernel/physical_core.cpp
+++ b/src/core/hle/kernel/physical_core.cpp
@@ -4,6 +4,9 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include
+#include
+#include
#include "common/scope_exit.h"
#include "common/settings.h"
#include "core/core.h"
@@ -222,8 +225,22 @@ void PhysicalCore::LogBacktrace(KernelCore& kernel) {
}
void PhysicalCore::Idle() {
+ // Guest job systems hand work between cores many times per frame; blocking at once adds the
+ // host's thread wake-up latency to every handoff. EDEN_NX2_IDLE_SPIN_US=: poll for that
+ // many microseconds before blocking.
+ static const auto spin = std::chrono::microseconds{[] {
+ const char* text = std::getenv("EDEN_NX2_IDLE_SPIN_US");
+ return text ? std::strtoll(text, nullptr, 10) : 0LL;
+ }()};
+ if (spin.count() > 0) {
+ const auto deadline = std::chrono::steady_clock::now() + spin;
+ while (!m_is_interrupted.load(std::memory_order_acquire) &&
+ std::chrono::steady_clock::now() < deadline) {
+ std::this_thread::yield();
+ }
+ }
std::unique_lock lk{m_guard};
- m_on_interrupt.wait(lk, [this] { return m_is_interrupted; });
+ m_on_interrupt.wait(lk, [this] { return m_is_interrupted.load(); });
}
bool PhysicalCore::IsInterrupted() const {
diff --git a/src/core/hle/kernel/physical_core.h b/src/core/hle/kernel/physical_core.h
index b92201204a..20371d6717 100644
--- a/src/core/hle/kernel/physical_core.h
+++ b/src/core/hle/kernel/physical_core.h
@@ -6,6 +6,7 @@
#pragma once
+#include
#include
#include
#include
@@ -71,7 +72,7 @@ private:
std::condition_variable m_on_interrupt;
Core::ArmInterface* m_arm_interface{};
KThread* m_current_thread{};
- bool m_is_interrupted{};
+ std::atomic m_is_interrupted{};
bool m_is_single_core{};
};
diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp
index 3542d5a3b6..53d964ecbc 100644
--- a/src/core/hle/kernel/svc.cpp
+++ b/src/core/hle/kernel/svc.cpp
@@ -6,6 +6,8 @@
// This file is automatically generated using svc_generator.py.
// DO NOT MODIFY IT MANUALLY
+#include
+#include
#include
#include "core/arm/arm_interface.h"
@@ -2505,11 +2507,60 @@ void Call(Core::System& system, u32 imm) {
LOG_TRACE(Kernel_SVC, "{} [0]={:#x} [1]={:#x} [2]={:#x} [3]={:#x} [4]={:#x} [5]={:#x} [6]={:#x}",
imm, GetArg64(args, 0), GetArg64(args, 1), GetArg64(args, 2),
GetArg64(args, 3), GetArg64(args, 4), GetArg64(args, 5), GetArg64(args, 6));
- if (process.Is64Bit()) {
+ // Set EDEN_NX2_SVC_TRACE=1 to log every SVC with its arguments and results, or
+ // EDEN_NX2_SVC_TRACE=: for presents first..last only (a frame-time profile).
+ static const std::pair trace_range = [] {
+ const char* text = std::getenv("EDEN_NX2_SVC_TRACE");
+ if (text == nullptr) return std::pair{1, 0};
+ const char* colon = std::strchr(text, ':');
+ if (colon == nullptr) return std::pair{0, ~u64{}};
+ return std::pair{std::strtoull(text, nullptr, 10), std::strtoull(colon + 1, nullptr, 10)};
+ }();
+ const u64 presents = Core::g_nx2_present_count.load(std::memory_order_relaxed);
+ const bool svc_trace = presents >= trace_range.first && presents <= trace_range.second;
+ if (svc_trace) {
+ LOG_INFO(Kernel_SVC, "svc {:#04x} in t{} [0]={:#x} [1]={:#x} [2]={:#x} [3]={:#x}", imm,
+ GetCurrentThread(kernel).GetThreadId(),
+ GetArg64(args, 0), GetArg64(args, 1), GetArg64(args, 2), GetArg64(args, 3));
+ }
+ if (imm == 0x80) {
+ // Switch 2 only: svcSetGpuMemoryAttribute(address, size, mask, attr). nn::gpu::sdk and
+ // nn::gs native memory set the new "GPU accessible" attribute (0x20) before registering
+ // memory with the GPU and clear it on destruction; NVN can see it through QueryMemory.
+ // Same checks as svcSetMemoryAttribute, restricted to that bit (as in changes.patch).
+ const u64 address = GetArg64(args, 0);
+ const u64 size = GetArg64(args, 1);
+ const u32 mask = static_cast(GetArg64(args, 2));
+ const u32 attr = static_cast(GetArg64(args, 3));
+ constexpr u32 Supported = static_cast(Svc::MemoryAttribute::GpuAccessible);
+ auto& page_table = process.GetPageTable();
+ Result result = ResultSuccess;
+ if (!Common::IsAligned(address, PageSize)) {
+ result = ResultInvalidAddress;
+ } else if (size == 0 || !Common::IsAligned(size, PageSize)) {
+ result = ResultInvalidSize;
+ } else if ((attr & ~mask) != 0 || ((mask | attr) & ~Supported) != 0) {
+ result = ResultInvalidCombination;
+ } else if (address + size <= address || !page_table.Contains(address, size)) {
+ result = ResultInvalidCurrentMemory;
+ } else {
+ result = page_table.SetMemoryAttribute(address, size,
+ static_cast(mask),
+ static_cast(attr));
+ }
+ LOG_DEBUG(Kernel_SVC, "Switch 2 svc 0x80 addr={:#x} size={:#x} mask={:#x} attr={:#x} -> {:#x}",
+ address, size, mask, attr, result.raw);
+ args[0] = result.raw;
+ } else if (process.Is64Bit()) {
Call64(system, imm, args);
} else {
Call32(system, imm, args);
}
+ if (svc_trace) {
+ LOG_INFO(Kernel_SVC, "svc {:#04x} out t{} [0]={:#x} [1]={:#x} [2]={:#x}", imm,
+ GetCurrentThread(kernel).GetThreadId(), GetArg64(args, 0), GetArg64(args, 1),
+ GetArg64(args, 2));
+ }
//kernel.ExitSVCProfile();
kernel.CurrentPhysicalCore().LoadSvcArguments(process, args);
}
diff --git a/src/core/hle/kernel/svc/svc_info.cpp b/src/core/hle/kernel/svc/svc_info.cpp
index 863bb58fa8..bbd7d32445 100644
--- a/src/core/hle/kernel/svc/svc_info.cpp
+++ b/src/core/hle/kernel/svc/svc_info.cpp
@@ -4,6 +4,8 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include
+
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/kernel/k_memory_manager.h"
@@ -40,6 +42,11 @@ Result GetInfo(Core::System& system, u64* result, InfoType info_id_type, Handle
case InfoType::UsedNonSystemMemorySize:
case InfoType::IsApplication:
case InfoType::FreeThreadCount:
+ case InfoType::Nx2RegionAddress:
+ case InfoType::Nx2RegionSize:
+ case InfoType::Nx2VammSize:
+ case InfoType::Nx2RemoteMemorySize:
+ case InfoType::Nx2RemoteMemoryUsed:
case InfoType::AliasRegionExtraSize: {
R_UNLESS(info_sub_id == 0, ResultInvalidEnumValue);
@@ -66,6 +73,35 @@ Result GetInfo(Core::System& system, u64* result, InfoType info_id_type, Handle
case InfoType::HeapRegionSize:
*result = process->GetPageTable().GetHeapRegionSize();
R_SUCCEED();
+ case InfoType::Nx2RegionAddress:
+ case InfoType::Nx2RegionSize: {
+ // Looks like the region Switch 2 VAMM reserves address space in (a title asks for a
+ // ~7.5 GiB reservation). VAMM backs it with MapPhysicalMemory, which only works in
+ // the alias region here, so hand out the upper half of the alias region.
+ auto& page_table = process->GetPageTable();
+ const u64 half = page_table.GetAliasRegionSize() / 2;
+ *result = info_id_type == InfoType::Nx2RegionAddress
+ ? GetInteger(page_table.GetAliasRegionStart()) + half
+ : half;
+ LOG_WARNING(Kernel_SVC, "(STUBBED) Switch 2 InfoType {}, returning {:#x}",
+ static_cast(info_id_type), *result);
+ R_SUCCEED();
+ }
+ case InfoType::Nx2RemoteMemorySize:
+ // Unknown real value; nothing backs it yet.
+ *result = 0;
+ LOG_WARNING(Kernel_SVC, "(STUBBED) Switch 2 InfoType 31 (remote memory size), returning 0");
+ R_SUCCEED();
+ case InfoType::Nx2RemoteMemoryUsed:
+ *result = 0;
+ LOG_WARNING(Kernel_SVC, "(STUBBED) Switch 2 InfoType 32 (remote memory used), returning 0");
+ R_SUCCEED();
+ case InfoType::Nx2VammSize:
+ // A page granule: the SDK checks module addresses/sizes against (value - 1) and
+ // VAMM maps one granule up front. 4 KiB matches what Switch 1 kernels use.
+ *result = Core::Memory::YUZU_PAGESIZE;
+ LOG_WARNING(Kernel_SVC, "(STUBBED) Switch 2 InfoType 33, returning {:#x}", *result);
+ R_SUCCEED();
case InfoType::AslrRegionAddress:
*result = GetInteger(process->GetPageTable().GetAliasCodeRegionStart());
R_SUCCEED();
@@ -310,7 +346,16 @@ Result GetSystemInfo(Core::System& system, uint64_t* out, SystemInfoType info_ty
Result GetInfo64(Core::System& system, uint64_t* out, InfoType info_type, Handle handle,
uint64_t info_subtype) {
- R_RETURN(GetInfo(system, out, info_type, handle, info_subtype));
+ const Result result = GetInfo(system, out, info_type, handle, info_subtype);
+ // EDEN_NX2_MEM_TRACE: log the memory layout and size queries of a title (diagnostics).
+ static const bool trace = std::getenv("EDEN_NX2_MEM_TRACE") != nullptr;
+ const u32 type = static_cast(info_type);
+ if (static u32 logs{}; trace && logs < 200 && (type <= 7 || (type >= 12 && type <= 33))) {
+ ++logs;
+ LOG_INFO(Kernel_SVC, "mem trace: GetInfo type {} sub {:#x} -> {:#x} = {:#x}", type,
+ info_subtype, result.raw, *out);
+ }
+ R_RETURN(result);
}
Result GetSystemInfo64(Core::System& system, uint64_t* out, SystemInfoType info_type, Handle handle,
diff --git a/src/core/hle/kernel/svc/svc_physical_memory.cpp b/src/core/hle/kernel/svc/svc_physical_memory.cpp
index 48915b2b1e..4fde85fdaa 100644
--- a/src/core/hle/kernel/svc/svc_physical_memory.cpp
+++ b/src/core/hle/kernel/svc/svc_physical_memory.cpp
@@ -4,6 +4,8 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include
+
#include "core/core.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/svc.h"
@@ -142,16 +144,47 @@ Result SetUnsafeLimit(Core::System& system, uint64_t limit) {
R_THROW(ResultNotImplemented);
}
+// EDEN_NX2_MEM_TRACE: log the heap and physical memory requests of a title (diagnostics).
+static bool MemTrace() {
+ static const bool trace = std::getenv("EDEN_NX2_MEM_TRACE") != nullptr;
+ return trace;
+}
+
Result SetHeapSize64(Core::System& system, uint64_t* out_address, uint64_t size) {
- R_RETURN(SetHeapSize(system, out_address, size));
+ const Result result = SetHeapSize(system, out_address, size);
+ if (MemTrace()) {
+ LOG_INFO(Kernel_SVC, "mem trace: SetHeapSize {:#x} -> {:#x} at {:#x}", size, result.raw,
+ *out_address);
+ }
+ R_RETURN(result);
}
Result MapPhysicalMemory64(Core::System& system, uint64_t address, uint64_t size) {
- R_RETURN(MapPhysicalMemory(system, address, size));
+ const Result result = MapPhysicalMemory(system, address, size);
+ static u64 total{};
+ if (MemTrace()) {
+ if (result.IsSuccess()) {
+ total += size;
+ }
+ static u32 logs{};
+ if (result.IsError() || logs++ < 400) {
+ LOG_INFO(Kernel_SVC, "mem trace: MapPhysicalMemory {:#x}+{:#x} -> {:#x} (mapped total {:#x})",
+ address, size, result.raw, total);
+ }
+ }
+ R_RETURN(result);
}
Result UnmapPhysicalMemory64(Core::System& system, uint64_t address, uint64_t size) {
- R_RETURN(UnmapPhysicalMemory(system, address, size));
+ const Result result = UnmapPhysicalMemory(system, address, size);
+ if (MemTrace()) {
+ static u32 logs{};
+ if (result.IsError() || logs++ < 400) {
+ LOG_INFO(Kernel_SVC, "mem trace: UnmapPhysicalMemory {:#x}+{:#x} -> {:#x}", address,
+ size, result.raw);
+ }
+ }
+ R_RETURN(result);
}
Result MapPhysicalMemoryUnsafe64(Core::System& system, uint64_t address, uint64_t size) {
diff --git a/src/core/hle/kernel/svc/svc_thread.cpp b/src/core/hle/kernel/svc/svc_thread.cpp
index dbca13dd26..7f6b959256 100644
--- a/src/core/hle/kernel/svc/svc_thread.cpp
+++ b/src/core/hle/kernel/svc/svc_thread.cpp
@@ -20,6 +20,18 @@ constexpr bool IsValidVirtualCoreId(int32_t core_id) {
return (0 <= core_id && core_id < static_cast(Core::Hardware::NUM_CPU_CORES));
}
+// Switch 2 hardware has eight cores. Six are emulated (the most an application is given), so
+// fold any id past them back onto the low cores, both in core ids and in affinity masks.
+constexpr int32_t FoldExtraCoreId(int32_t core_id) {
+ constexpr auto num_cores = static_cast(Core::Hardware::NUM_CPU_CORES);
+ return core_id >= num_cores && core_id < 2 * num_cores ? core_id - num_cores : core_id;
+}
+
+constexpr u64 FoldExtraCoreMask(u64 mask) {
+ constexpr u64 low = (1ULL << Core::Hardware::NUM_CPU_CORES) - 1;
+ return (mask & low) | ((mask >> Core::Hardware::NUM_CPU_CORES) & low);
+}
+
} // Anonymous namespace
/// Creates a new thread
@@ -36,6 +48,7 @@ Result CreateThread(Core::System& system, Handle* out_handle, u64 entry_point, u
if (core_id == IdealCoreUseProcessValue) {
core_id = process.GetIdealCoreId();
}
+ core_id = FoldExtraCoreId(core_id);
// Validate arguments.
R_UNLESS(IsValidVirtualCoreId(core_id), ResultInvalidCoreId);
@@ -261,6 +274,10 @@ Result SetThreadCoreMask(Core::System& system, Handle thread_handle, s32 core_id
R_UNLESS((affinity_mask | process_core_mask) == process_core_mask, ResultInvalidCoreId);
R_UNLESS(affinity_mask != 0, ResultInvalidCombination);
+ // Validated against the real mask above; fold to the emulated cores from here on.
+ affinity_mask = FoldExtraCoreMask(affinity_mask);
+ core_id = FoldExtraCoreId(core_id);
+
// Validate the core id.
if (IsValidVirtualCoreId(core_id)) {
R_UNLESS(((1ULL << core_id) & affinity_mask) != 0, ResultInvalidCombination);
diff --git a/src/core/hle/kernel/svc_types.h b/src/core/hle/kernel/svc_types.h
index b0f4796da4..0bf2aaaa2d 100644
--- a/src/core/hle/kernel/svc_types.h
+++ b/src/core/hle/kernel/svc_types.h
@@ -50,6 +50,7 @@ enum class MemoryAttribute : u32 {
DeviceShared = (1 << 2),
Uncached = (1 << 3),
PermissionLocked = (1 << 4),
+ GpuAccessible = (1 << 5), // Switch 2 (svc 0x80)
};
DECLARE_ENUM_FLAG_OPERATORS(MemoryAttribute);
@@ -157,6 +158,17 @@ enum class InfoType : u32 {
IsSvcPermitted = 26,
IoRegionHint = 27,
AliasRegionExtraSize = 28,
+ // Switch 2 kernels: address/size of a region the SDK queries between the alias and
+ // ASLR regions during nnosInitialize; likely the VAMM address-space region.
+ Nx2RegionAddress = 29,
+ Nx2RegionSize = 30,
+ // Switch 2 kernels: nn::os::GetRemoteMemoryUsage returns {id 31, id 32}; used by
+ // nn::gsv/nn::nvsrv BootWithRemoteMemory (memory set aside for the graphics stack).
+ Nx2RemoteMemorySize = 31,
+ Nx2RemoteMemoryUsed = 32,
+ // Switch 2 kernels: a page granule. The SDK aligns module ranges and VAMM's first
+ // MapPhysicalMemory to it.
+ Nx2VammSize = 33,
TransferMemoryHint = 34,
diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp
index 90d292c4a2..d4f7cd6041 100644
--- a/src/core/hle/service/am/service/application_functions.cpp
+++ b/src/core/hle/service/am/service/application_functions.cpp
@@ -118,7 +118,8 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_
{220, nullptr, "Unknown220"}, // [20.0.0+]
{300, nullptr, "CreateMovieWriter"}, // [19.0.0+]
{310, nullptr, "Unknown310"}, // [20.0.0+]
- {320, nullptr, "Unknown320"}, // [20.0.0+]
+ {320, D<&IApplicationFunctions::Unknown320>, "Unknown320"}, // [20.0.0+]
+ {321, D<&IApplicationFunctions::Unknown321>, "Unknown321"}, // Switch 2
{330, D<&IApplicationFunctions::Unknown330>, "Unknown330"}, // [20.0.0+]
{500, nullptr, "StartContinuousRecordingFlushForDebug"},
{1000, nullptr, "CreateMovieMaker"},
@@ -352,10 +353,11 @@ Result IApplicationFunctions::GetPseudoDeviceId(Out out_pseudo_dev
// This should be hashed with the device specific hash
// for now this will do
const auto res = FileSys::PatchManager::GetMetadataFromBaseOrUpdate(system, m_applet->program_id);
- R_UNLESS(res.first != nullptr, ResultUnknown);
std::array hash;
unsigned int hash_len = 0;
- auto const seed = res.first->raw.seed_for_pseudo_device_id;
+ // Loose ExeFS boots have no control.nacp; seed from the program id instead of failing.
+ auto const seed = res.first != nullptr ? res.first->raw.seed_for_pseudo_device_id
+ : m_applet->program_id;
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
auto const algorithm = EVP_sha1();
EVP_DigestInit_ex(ctx, algorithm, nullptr);
@@ -544,6 +546,18 @@ Result IApplicationFunctions::GetUnknownEvent210(
R_SUCCEED();
}
+Result IApplicationFunctions::Unknown320() {
+ // Called by Switch 2 titles during nn::oe::Initialize; arguments unknown.
+ LOG_WARNING(Service_AM, "(STUBBED) called");
+ R_SUCCEED();
+}
+
+Result IApplicationFunctions::Unknown321() {
+ // Called by Switch 2 titles right after 320; arguments unknown.
+ LOG_WARNING(Service_AM, "(STUBBED) called");
+ R_SUCCEED();
+}
+
Result IApplicationFunctions::Unknown330(Out out) {
LOG_DEBUG(Service_AM, "called");
*out = 0;
diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h
index b97ed7e25e..72bf8998b1 100644
--- a/src/core/hle/service/am/service/application_functions.h
+++ b/src/core/hle/service/am/service/application_functions.h
@@ -82,6 +82,8 @@ private:
Result GetHealthWarningDisappearedSystemEvent(OutCopyHandle out_event);
Result CreateApplicationAndRequestToStart(u64 application_id);
Result GetUnknownEvent210(OutCopyHandle out_event);
+ Result Unknown320();
+ Result Unknown321();
Result Unknown330(Out out);
Result PrepareForJit();
diff --git a/src/core/hle/service/am/service/application_proxy.cpp b/src/core/hle/service/am/service/application_proxy.cpp
index df438e1e17..430f3c6888 100644
--- a/src/core/hle/service/am/service/application_proxy.cpp
+++ b/src/core/hle/service/am/service/application_proxy.cpp
@@ -11,11 +11,13 @@
#include "core/hle/service/am/service/common_state_getter.h"
#include "core/hle/service/am/service/debug_functions.h"
#include "core/hle/service/am/service/display_controller.h"
+#include "core/hle/service/am/service/graphics_scale_controller.h"
#include "core/hle/service/am/service/library_applet_creator.h"
#include "core/hle/service/am/service/process_winding_controller.h"
#include "core/hle/service/am/service/self_controller.h"
#include "core/hle/service/am/service/window_controller.h"
#include "core/hle/service/cmif_serialization.h"
+#include "core/hle/service/nx2_stub_interface.h"
namespace Service::AM {
@@ -33,6 +35,13 @@ IApplicationProxy::IApplicationProxy(Core::System& system_, std::shared_ptr, "GetProcessWindingController"},
{11, D<&IApplicationProxy::GetLibraryAppletCreator>, "GetLibraryAppletCreator"},
{20, D<&IApplicationProxy::GetApplicationFunctions>, "GetApplicationFunctions"},
+ // Switch 2-only getters; see nx2_stub_interface.h.
+ {40, &IApplicationProxy::GetNx2Interface, "Nx2Getter40"},
+ {50, &IApplicationProxy::GetNx2Interface, "Nx2Getter50"},
+ {60, D<&IApplicationProxy::GetGraphicsScaleController>, "GetGraphicsScaleController (Switch 2)"},
+ {70, &IApplicationProxy::GetNx2Interface, "Nx2Getter70"},
+ {80, &IApplicationProxy::GetNx2Interface, "Nx2Getter80"},
+ {90, &IApplicationProxy::GetNx2Interface, "Nx2Getter90"},
{1000, D<&IApplicationProxy::GetDebugFunctions>, "GetDebugFunctions"},
};
// clang-format on
@@ -42,6 +51,17 @@ IApplicationProxy::IApplicationProxy(Core::System& system_, std::shared_ptr> out_controller) {
+ LOG_DEBUG(Service_AM, "called");
+ *out_controller = std::make_shared(system);
+ R_SUCCEED();
+}
+
+void IApplicationProxy::GetNx2Interface(HLERequestContext& ctx) {
+ ReturnNx2StubInterface(system, ctx, "IApplicationProxy");
+}
+
Result IApplicationProxy::GetAudioController(
Out> out_audio_controller) {
LOG_DEBUG(Service_AM, "called");
diff --git a/src/core/hle/service/am/service/application_proxy.h b/src/core/hle/service/am/service/application_proxy.h
index b56f597605..0a706be4fd 100644
--- a/src/core/hle/service/am/service/application_proxy.h
+++ b/src/core/hle/service/am/service/application_proxy.h
@@ -17,6 +17,7 @@ class IApplicationFunctions;
class ICommonStateGetter;
class IDebugFunctions;
class IDisplayController;
+class IGraphicsScaleController;
class ILibraryAppletCreator;
class IProcessWindingController;
class ISelfController;
@@ -30,6 +31,8 @@ public:
~IApplicationProxy();
private:
+ void GetNx2Interface(HLERequestContext& ctx);
+ Result GetGraphicsScaleController(Out> out_controller);
Result GetAudioController(Out> out_audio_controller);
Result GetDisplayController(Out> out_display_controller);
Result GetProcessWindingController(
diff --git a/src/core/hle/service/am/service/application_proxy_service.cpp b/src/core/hle/service/am/service/application_proxy_service.cpp
index 7727951d8c..67306aadc0 100644
--- a/src/core/hle/service/am/service/application_proxy_service.cpp
+++ b/src/core/hle/service/am/service/application_proxy_service.cpp
@@ -19,6 +19,8 @@ IApplicationProxyService::IApplicationProxyService(Core::System& system_,
: ServiceFramework{system_, "appletOE"}, m_window_system{window_system} {
static const FunctionInfo functions[] = {
{0, D<&IApplicationProxyService::OpenApplicationProxy>, "OpenApplicationProxy"},
+ // Switch 2 SDKs open the proxy with command 10, same arguments.
+ {10, D<&IApplicationProxyService::OpenApplicationProxy>, "OpenApplicationProxy (Switch 2)"},
};
RegisterHandlers(functions);
}
diff --git a/src/core/hle/service/am/service/graphics_scale_controller.h b/src/core/hle/service/am/service/graphics_scale_controller.h
new file mode 100644
index 0000000000..07e38c6ac8
--- /dev/null
+++ b/src/core/hle/service/am/service/graphics_scale_controller.h
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "core/hle/service/cmif_types.h"
+#include "core/hle/service/nx2_stub_interface.h"
+#include "core/hle/service/service.h"
+
+namespace Service::AM {
+
+// Switch 2 interface returned by IApplicationProxy command 60 (name is a guess). nn::oe's
+// GetCurrentGraphicsScale reads command 10 and accepts 10, 20 or 30 (scale enum 0, 1, 2).
+// Commands without a handler are auto-stubbed because the name carries the nx2: prefix.
+class IGraphicsScaleController final : public ServiceFramework {
+public:
+ explicit IGraphicsScaleController(Core::System& system_)
+ : ServiceFramework{system_, "nx2:IGraphicsScaleController"} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {10, D<&IGraphicsScaleController::GetCurrentGraphicsScale>, "GetCurrentGraphicsScale"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ }
+
+private:
+ Result GetCurrentGraphicsScale(Out out_scale) {
+ LOG_WARNING(Service_AM, "(STUBBED) called, returning 10");
+ *out_scale = 10;
+ R_SUCCEED();
+ }
+};
+
+} // namespace Service::AM
diff --git a/src/core/hle/service/am/service/self_controller.cpp b/src/core/hle/service/am/service/self_controller.cpp
index 3a654b9daa..a8078e6048 100644
--- a/src/core/hle/service/am/service/self_controller.cpp
+++ b/src/core/hle/service/am/service/self_controller.cpp
@@ -12,6 +12,7 @@
#include "core/hle/service/caps/caps_su.h"
#include "core/hle/service/cmif_serialization.h"
#include "core/hle/service/nvnflinger/nvnflinger.h"
+#include "core/hle/service/nx2_gsv.h"
#include "core/hle/service/sm/sm.h"
#include "core/hle/service/vi/vi_results.h"
@@ -73,6 +74,7 @@ ISelfController::ISelfController(Core::System& system_, std::shared_ptr