diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 00000000..5bd20ae4 --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,1472 @@ +# NX2navi audit + +This file holds the audit's state across sessions: the project context, the subsystem map, the threads and what they +share, the path of one frame, the allocators, pools and caches, the leads for Phase 3, the findings ledger and what +has been reviewed. + +- Phase 1 (map, no code changes): written 2026-11-10 on branch `audit`, cut from `test-all-fixes` at `ed36773d`. +- Phases 2 to 5: not started. + +Citations name our code by file and function; a `~line` after them is a hint taken at `ed36773d` and will drift. + +Rules this file follows: + +- A **finding** is labelled "confirmed" (reproduced, or shown by tool output) or "suspected" (from reading code only). +- A **lead** is something seen while mapping that Phase 3 should look at. Every lead is suspected, and none is a + finding yet. +- Where the docs and the code disagree, the map follows the code. Section 6 lists every disagreement. + +--- + +## 1. Project context + +**What it is.** A static recompilation of *Ocarina of Time*, the Switch 2 release (ARM64, game version 1.1.1), into a +native Windows x86-64 executable with a Vulkan renderer. The launcher builds the game on the player's PC from the +player's own dump. Design: `docs/architecture.md`. Build and checks: `docs/building.md`. Launcher: `docs/launcher.md`. +Maxwell behaviour the Vulkan path must match: `docs/maxwell.md`. + +The runtime also carries code for other builds of the game, gated on `rt_game_version` (`host.h`): 110, 121 and 143. +`tools/make_game.py` writes `rt_game_version = 111`. The audit targets 1.1.1. Some files (`mouse_direct.c`, +`menu_cursor.c`, parts of `ov_zstd.c`) describe themselves as 1.4.3 features; which of their paths run on 1.1.1 is +not determined yet. + +### Repository layout + +| Folder | What it holds | +|---|---| +| `recomp/` | The recompiler: an ARM64-to-C lifter and generator in Python. `gen.py` is the generator, `lifter.py` the decoder, `simd_c.py` the NEON/FP lowering, `flags.py` and `regs.py` the locals for flags and registers, `jumptables.py` the jump tables. `layout.py` alone decides module bases. `patches.py` and `patches-v111.txt` hold the game-code patch sets. | +| `runtime/` | The host runtime in C and C++: 125 `.c` files, 15 `.cpp`, 70 headers. | +| `launcher/`, `installer/`, `tools/` | The build kit. `tools/make_game.py` is the build a player runs. | +| `tests/` | The checks. | +| `docs/` | The design. | + +The recompiled output is not in the repository: it is generated under ignored folders. + +### Stack + +- Our own static recompiler. It is written in Python and outputs C with GNU extensions. +- The runtime is C11 (plus C++ for the menu, DLSS frame generation and AMD FidelityFX), built with MSYS2 UCRT64 Clang. +- A Vulkan 1.3 renderer with `VK_EXT_mesh_shader`, `VK_EXT_external_memory_host` and the graphics pipeline library + where the driver has it. Device memory goes through Vulkan Memory Allocator. +- Shaders are translated to SPIR-V by a separate translator process (`nx2navi_xlate.exe`) into an on-disk cache, + prepared at install time. +- SDL3 for gamepads and audio output, Win32 for the window, keyboard and mouse. libopus for voice, libvpx and dav1d for + movies, libzstd, and Dear ImGui for the menu. + +### Targets + +- PC only. There is no fixed resolution or frame rate. +- The frame limit (`NX2NAVI_FPS_LIMIT`, `cfg.c` default 60, live; 30 to 240, `display` or `unlimited`) counts frames + the game draws. Frame generation multiplies on top of it. +- Reference PC: RTX 5090. The user plays at a 120 limit with DLSS frame generation x2 and HDR. +- Minimum: 16 GB RAM and a Vulkan 1.3 GPU with mesh shaders. + +### Memory budget + +- **Guest application pool.** The pool is `rt_app_pool_bytes`, which `tools/make_game.py` generates as 9809 MiB. + - `memory.c` `mem_init` (~860) actually sets the limit to `NX2NAVI_APP_POOL` when that is set. Otherwise it uses + `rt_app_pool_bytes + res_heap_extra_bytes()`, and the extra comes from the resolution settings (`resolution.c`). + So the pool can be larger than 9809 MiB. + - The limit is enforced in `mem_set_heap_size` (~899) and `alias_map` (~1080). The heap is also capped at + `HEAP_CAP` (12 GiB). +- **VRAM.** VRAM follows the driver's heap budget (`VK_EXT_memory_budget`, `vk_memory.c` `vram_query`). + `NX2NAVI_VRAM_BUDGET_MB` caps it (`vk_memory.c` `budget_cap`, a test switch). +- **CPU and GPU memory are separate.** The game's guest memory pools are imported into Vulkan as host memory. On a + discrete GPU they are mirrored into device-local buffers (`vk_memory.c`). + +### Game logic rate + +- The game ships with whole-vsync 30 fps steps. +- Patch set `ostep` makes it step by the clock, up to 240 game frames a second. The step is written each present by + `frame_rate.c` `frame_step_present`, then `ocarina_step`, then `ocarina_step_write`. +- Several known bugs come from game code that assumes the original rate. + +### Commands for the later phases + +Phase 1 ran nothing. + +- **Dev builds** run from `C:\Users\PC\Documents\deepseek-harness\default-workspace`: + - `C:\msys64\ucrt64\bin` first on PATH, with system Python 3.12; + - a copy of `work\build_rt_allfix.py` pointed at this worktree. +- **Scripted game runs** use a runner script with an input script and a save folder; a Kakariko Village save exists. + Useful settings: `NX2NAVI_TRACE=perf` and `NX2NAVI_PROFILE`. +- **Launcher tests:** `python -m unittest tests.launcher_test`. +- **Runtime self-checks:** `nx2navi.exe --selftest`, `--probe-kernel`, `--probe-vkexec`, `--probe-vkmem`, + `--probe-cfg`, `--probe-prewarm`. +- **Unit checks that build one file with the rest stubbed:** the `tests\*_check.py` files listed in + `docs/building.md`, "Checks". Examples: `logq_check.py`, `sync_wake_check.py`, `save_share_check.py`, + `save_zip_check.py`, `frame_step_check.py`, `wb_lazy_check.py`, `target_frames_check.py`. + +### Known problems at the start of the audit + +These are what the user or players see. None is a finding until it is reproduced or traced. + +**Stutter and speed** +- Stutter and frame-time spikes at uncapped frame rates in the user's own play. Not reproduced in scripted runs. +- Big towns run slower than the field in play. Kakariko Village measures 135-138 fps in scripted runs on the reference + PC; the user reports heavy slowdowns in play. + +**Rendering** +- White, untextured ground after long play under VRAM pressure. A fix is in; it is unconfirmed. +- A one-frame dark shadow at the Castle Town gate. Switch `depth_alias` addresses it and is off by default. +- Foliage flicker on Linux with an AMD card. Open. + +**Crashes and device loss** +- A device loss on an RX 6600, and a crash while fighting on AMD cards. Lazy write-back is now off by default on AMD's + Windows driver (`wb_lazy_choice.h`, `vk_memory.c` `vk_mem_lazy_decide`). Untested: there is no AMD card here. +- Crashes with third-party overlay layers injected into the process. + +**Logic tied to the frame rate** +- The rupee pick-up popup stays above Link above 60 fps. Patch set `opickup` addresses it; it is proven statically, + not seen in game. An item jingle glitches the same way. +- The ocarina song recorder rounds wrongly at high frame rates: four sites, unfixed. + +**Other** +- The shader cache pack grows with every translator change. +- The replay on its own thread (`NX2NAVI_REPLAY_ASYNC=1`) has a known fault and stays off. +- `tests.installer_test` cannot be imported on this PC. + +### Parts of the audit prompt that do not apply + +The prompt was written for a port to the Switch 2. This project goes the other way: a Switch 2 game recompiled for +the PC. + +- **"On device", "Switch 2 build", "docked", "handheld".** Do not apply. There is no device build and no docked or + handheld mode. The only targets are Windows (and an in-progress Linux build, `docs/building.md`), at whatever + resolution and frame limit the player picks. No substitute target is invented here. +- **"Display list buffers".** The game does not build N64-style display lists. It records NVN command buffers, which + the runtime replays into Vulkan. The prompt's display list is read as the NVN command buffer throughout. +- **"Recompiler config".** The configuration is `recomp/patches-v111.txt` plus the generator's options. Our host code + is `runtime/`. +- **"Emulated RAM".** Guest memory: the game's address space at fixed host addresses below 2^39 (`memory.c`). +- **"Target frame rate".** The player's frame limit, not a fixed 30 or 60. +- **"Stays inside its memory budget".** Two budgets: the guest pool above (CPU side) and the driver's VRAM budget. + Process commit charge matters too: `vk_texture.c` evicts on commit pressure, `commit_evict`. +- **"Test route ... a boss ... save, quit, reload".** Applies as written, on PC. The Phase 2 baseline also needs the + route at the user's own settings (a 120 limit, DLSS frame generation x2, HDR), because the stutter reports come from + those. +- **"Original-game crashes handled more gracefully".** Applies. On the PC, a guest fault goes to the crash handler: + `platform/win32.c` `crash_veh`, then `main.c` `crash_handler`. + +### Sanitizer builds (Phase 2): what the code says + +Nothing here was tried. + +**AddressSanitizer and UndefinedBehaviorSanitizer on Windows** + +- *Fixed reservations.* The runtime reserves guest memory at fixed addresses below 2^39 before anything else + (`memory.c` `mem_init`): + - the module window; + - the 256 MiB kernel region; + - a 14 GiB low region (stacks and heap); + - a 64 GiB alias region. + + It re-executes itself when a range is taken (`NX2NAVI_REEXEC`, `REEXEC_LIMIT` 20). An ASan shadow mapping placed in + that range would make every start re-execute and then fail. Whether the Windows ASan runtime's shadow placement + collides is not determined. +- *Faults are part of normal operation.* The runtime registers a vectored exception handler first + (`platform/win32.c` `plat_mem_fault_hook`, `AddVectoredExceptionHandler(1)`) and takes access violations on purpose: + - write-watch freeze and thaw (`memory.c` `thaw_fault`); + - lazy write-back pages (`memory.c` `lazy_fault`, which runs a GPU copy inside the handler); + - probe pages. + + ASan on Win64 commits its shadow memory on demand from an exception handler of its own. The order of the two + handlers, and whether ours tolerates the faults ASan takes, would need a test. +- *Stack tricks.* + - Guest threads reserve 256 MiB host stacks (`kernel.c` `HOST_STACK`). + - Guest code runs on fibers (`kernel.c` `kern_fiber_initialize`, `plat_fiber_*`). + - Frames are left by non-unwinding jumps (`plat_jmp_go`, used for ExitThread). + - Asynchronous preemption suspends a thread and rewrites its instruction pointer to `kern_ap_stub` + (`kernel.c` `ap_try`). + + ASan's fake stacks and its stack bookkeeping would need fiber annotations and probably + `detect_stack_use_after_return=0`. +- *Generated code.* Instrumenting the generated C would put a shadow check on every guest load and store. That is + slow, but possible in principle. Instrumenting only `runtime/` is the smaller first step. +- *Toolchain.* Whether MSYS2 UCRT64's clang ships the ASan and UBSan runtimes for the `x86_64-w64-windows-gnu` + target was not checked. + +**ThreadSanitizer** + +- It does not exist for Windows targets. +- The Linux build (`runtime/platform/linux.c`) is the only place it could run. Its memory layout would have to accept + the fixed guest reservations below 2^39 and the userfaultfd write watch; not determined. +- It would also see the guest's own lock-free per-core data, which the kernel keeps safe by its virtual-core rule + (section 3.3), as races. Expect noise there. + +--- + +## 2. Subsystem map + +There are 22 subsystems. Files are under `runtime/` unless the path says otherwise. + +| # | Subsystem | Files | What it owns | +|---|---|---|---| +| 1 | Recompiler (build time) | `recomp/*.py`, `recomp/patches-v111.txt` | Lifting ARM64 to C; module bases (`layout.py`); jump tables; patch sets (`patches.py`). Output is generated C under ignored folders (never edited by hand). | +| 2 | Process entry and boot | `main.c`, `boot.c`, `loader.c`, `hostfn.c`, `dispatch.c`, `patches.c`, `guest_setjmp.c`, `fma.c`, `cpu.h`, `simd.h`, `simd_fp.h` | Command line and modes; loading the lifted modules at their bases, linking host overrides (`HOST_OVERRIDE`/`HOST_OBSERVER` tables, `hostfn.c`); the indirect-branch table (`rt_dispatch`); runtime switching of patch sets (`rt_patch_on`, `rt_patch_switch`); guest setjmp/longjmp; the boot sequence and shader/pipeline prewarm wait (`boot.c` `rt_boot`, `boot_wait`). | +| 3 | Kernel and scheduling | `kernel.c`, `kernel.h`, `kernel_ap_stub.h`, `svc.c` | Guest threads (one host thread each), six virtual cores and their hand-off, preemption (`core_kick`, async preemption), waits and events, the handle table, fibers, system events, SVC entry. | +| 4 | Guest memory | `memory.c`, `platform/win32.c` (reserve, commit, write watch, fault hooks) | The fixed guest address space; heap, stacks, alias region and app-pool limit; the kernel-region allocator; write watch; freeze/thaw of idle regions; lazy write-back of GPU writes (armed pages fetched on fault). | +| 5 | Services (in-process Horizon) | `ipc.c`, `ov_system.c`, `net_offline.c`, `nfp.c`, `amiibo.c`, `swkbd.c`, `swkbd_win.c`, `swkbd_sdl.c`, `ov_media.c` (Opus) | `sm:` and the service table; oe/applet/am/account/time and others answered on the host; offline sockets; amiibo; the software keyboard window. | +| 6 | Files: RomFS, decompression, mods | `romfs.c`, `romfs_path.h`, `restbl.c`, `ov_zstd.c`, `mods.c`, `cheats.c` | The read-only game RomFS from the player's image with overlays (`romfs-patches*`, mods); the resource size table; host zstd decode with dictionaries; mod scan and order; cheats from the mods' cheat files. | +| 7 | Save data | `fs_host.c`, `save_share.c`, `save_zip.c`, `sha256.c` | The game's save mount on the host file system, the save transaction (work copy, commit, swap, backups), recovery at start, import and export zips. | +| 8 | Settings and logging | `cfg.c`, `cfg.h`, `logq.c`, `main.c` (log sink, crash handler), `perf.c` (switches `rt_knob`, `rt_ab`) | Every setting with its default (`nx2navi.ini`, `NX2NAVI_*`), live changes and saving; the log queue and its writer thread; log rotation; the crash report. | +| 9 | Frame pacing and game timing | `frame_rate.c`, `nvn_api_obj.c` (`present_period`, `pace`, `nvnWindowAcquireTexture`), `rtss_limit.c`, `resolution.c`, `widescreen.c`, `dynres.c` | The frame limit and present slots; the game's frame step (`ostep`); the world clock; the RivaTuner limit hand-off; render resolution, aspect and dynamic resolution. | +| 10 | NVN front end | `nvn_api_core.c`, `nvn_api_cmd.c`, `nvn_api_obj.c`, `nvn_ext.c`, `nvn_procs.c`, `nvn_procs.inc`, `nvn_tex.c`, `tex_deswizzle.c`, `ov_gfx.c`, `nvn.h`, `nvn_api.h`, `nvn_obj.h`, `nvn_hmap.h`, `nvn_pool_index.h` | The NVN API the game calls: devices, queues, memory pools, textures and samplers, command buffer recording into op records, deferred binds, recording copies and pins, submit and present entry points, texture layout and deswizzle. | +| 11 | Replay and submission | `vk_backend.c`, `vk_exec.c`, `vk_exec_draw.c`, `vk_exec_xfer.c`, `vk_exec.h`, `vk_rec.c`, `vk_rec.h`, `vk_sync.c`, `vk_usnap.c`, `vk_target_frames.h` | Replaying NVN op records into Vulkan command buffers, draws, binds, the bindless pools, copies, events and counters, event order across queues; the recorder thread's packet ring; timelines, submit batching, the completion thread; the uniform snapshot; frame hold. | +| 12 | GPU memory | `vk_memory.c`, `vk_vma.cpp`, `vk_headroom.c`, `vk_gpa.c`, `vk_gpa.h`, `vk_vaddr.c`, `wb_lazy_choice.h` | Import of guest pools, VRAM mirrors and their eviction, shadow storage, upload batches and staging, deferred destruction, write-back of GPU writes, the lazy write-back's GPU side, the device address table, the VRAM budget, the headroom spare. | +| 13 | Textures | `vk_texture.c`, `vk_astc.c`, `tex_astc.c`, `tex_astc_mt.c`, `vk_formats.c`, `tex.h` | The texture image cache and its eviction (release, staleness, idle, disuse, VRAM and commit pressure), samplers, texture detail, uploads and ASTC decode (GPU, CPU fallback). | +| 14 | Shaders | `gfx_shader.c`, `gfx_shader_guess.c`, `gfx.h`, `tools/xlate/` (translator source), `tools/vendor/` (pinned translator) | Translation through child processes, the memory cache, the disk cache and pack, the carry of the previous translator's translations, prewarm and preparation, disk-read budget. | +| 15 | Pipelines | `vk_pipeline.c`, `vk_warm_hint.h`, `vk_caps.c` | Graphics and compute pipelines, stage libraries and fast links, background workers, the boot prewarm and prebuild, driver cache warm checks, the pipeline cache file and autosave, records (`pipeline-keys.bin`, `compute-used.bin`), device capability decisions. | +| 16 | Device and present | `vk_device.c`, `gfx_present.c`, `gfx_window_win.c`, `gfx_window_sdl.c`, `gfx_wsys.h`, `vk_hdr.c`, `vk_fsr.c`, `vk_color.c`, `vk_tonemap.c`, `vk_contact.c`, `gfx_null.c` | Instance and device, the queue lock, the swapchain, the present blit and FSR 1, HDR, colour adjustment, the window thread, the vsync thread, the scene upscaler stand-in, contact shadows. | +| 17 | Upscaling and frame generation | `vk_dlaa.c`, `vk_dlaa.h`, `vk_ffx.cpp`, `vk_fg.cpp`, `vk_dlssg.c`, `dlssg/nx2_sl.cpp`, `vk_mvprev.c`, `vk_mvsurvey.c`, `vk_mvcheck.c` | DLSS/DLAA through NGX, FSR 3.1 upscaling and frame generation, DLSS frame generation through `nx2navi_dlssg.dll`, motion vectors. | +| 18 | Audio | `audren.c`, `audren.h`, `audio_mixer.c`, `audio_effects.c`, `audio_reverb.c`, `audio_capture.c`, `ov_audio_input.c`, `menu_sound.c` | The game's audio renderer (`audren:u`) on a host render thread, the mixer and its SDL output ring, effects, the microphone, menu sounds. | +| 19 | Movies | `ov_movie.c`, `ov_movie2.c`, `movie.h` | Host VP9 (libvpx) and AV1 (dav1d) decoders standing in for the SDK's, with their threads and frame queues. | +| 20 | Input | `input.c`, `input_win.c`, `input_sdl.c`, `input_map.c`, `input_ds3.c`, `input_ds3_win.c`, `ov_input.c`, `mouse_direct.c`, `motion_fusion.c`, `camera_speed.c`, `menu_cursor.c` | Gamepads (SDL3, XInput, DualShock 3), keyboard and mouse, bindings, the state the game's HID calls read, rumble, motion, mouse camera, camera speed rewrite, menu pointer. | +| 21 | In-game menu and overlay | `ui/*.cpp`, `ui_text.c`, `menu_assets.c`, `folder_pick_win.cpp`, `folder_pick_sdl.c`, `perfmon.c`, `perfmon.h`, `sysinfo.c` | The Dear ImGui menu drawn on the present path, its art and fonts from the game's files, the performance overlay and its sampler, save export/import from the menu. | +| 22 | Diagnostics | `perf.c`, `prof_win.c`, `prof.h`, `flight.c`, `vk_dump.c`, `vk_crumbs.c`, `rdoc.c`, `discord_rpc.c`, `*_probe.c` | Frame timing and counters, the sampling profiler, the flight recorder, frame dumps, GPU breadcrumbs, the external capture hook, Discord presence (not diagnostics but a side service), the runtime probes. | +| — | Platform | `platform/platform.h`, `platform/win32.c`, `platform/linux.c`, `platform/common.c`, `platform/fp_x86.h` | Threads, locks, events, atomics, fibers, virtual memory, write watch, fault and crash hooks, files (atomic writes), child processes, CPU reservation. | +| — | Build kit and launcher | `tools/make_game.py`, `tools/prepare_shaders.py`, `tools/*`, `launcher/*`, `installer/*` | The player's build, shader preparation, updates, save import/export from outside the game. Not part of the running game; mapped only by what the docs say. | + +--- + +## 3. Threads + +### 3.1 Primitives + +From `platform/platform.h` and `platform/win32.c`. + +- **`PlatMutex`.** A Windows SRW lock, taken exclusive or shared. Not recursive. +- **`PlatRMutex`.** A recursive mutex: an owner and a depth over a `PlatMutex` (`platform/common.c`). +- **`PlatCond`, `PlatEvent`, `PlatOnce`.** Condition variables, events, and one-time init. +- **`plat_thread_new` / `_ex`.** Creates a joinable thread: suspended `CreateThread`, named, then resumed. + - The default stack reservation is 16 MiB (`PLAT_STACK_DEFAULT`). + - `plat_thread_spawn` does the same and then detaches. Detaching closes the handle and leaves the small + `PlatThread` struct (commented as intended). +- **Per-thread handles.** These `__thread` handles are never closed when a thread exits: + - the high-resolution waitable timer behind `plat_wait_ns`; + - the `plat_file_pread` and `plat_file_pwrite` events. + + See lead L-26. +- **`NVN_BLOCKING` and `kern_core_away`.** A guest thread that makes a long host call gives its virtual core away + first, then takes it back after the call. This covers submit, present, fence waits, shader translation, `nn::fs` + calls and pool setup. + +### 3.2 Thread table + +About 55 kinds of thread. "Detached" means started with `plat_thread_spawn` and never joined. Priorities are the +platform's seven levels; "normal" when nothing sets one. + +#### Process, kernel and system + +| Thread (name) | Entry | Started by, when | How many | Ends | +|---|---|---|---|---| +| Process main thread | `main.c` `main` → `boot.c` `rt_boot` | OS | 1 | Blocks in `plat_thread_join` on guest main; `rt_fatal` if guest main returns | +| "guest main" | `kernel.c` `guest_main` → `kern_main_thread` → `boot.c` `guest_main` (runs `nn::init::Start`) | `kernel.c` `kern_run_on_guest_main` (~1469) from `rt_boot` after the prewarm wait | 1, 256 MiB stack reserved | Joined by the main thread | +| "guest" | `kernel.c` `thread_main` (~1383): waits for `StartThread`, takes a core, `rt_call(entry)` | SVC `CreateThread` (0x08, `kern_svc_inner` ~1562) | One per guest thread; detached; 256 MiB reserved stack each | `ExitThread` (0x0A) → `thread_finish`; a thread created and never started waits forever (lead L-12) | +| (guest fibers) | `kernel.c` `fiber_main` | `kern_fiber_initialize` (~2206) | Up to `FIBER_MAX` 1024 | Not threads: they run on whichever guest thread switches to them | +| "prewarm" | `boot.c` `prewarm_main` (~348): `vk_pipeline_prewarm`, `gfx_shader_prepare_recorded`, `gfx_shader_carry_start` | `rt_boot` (~407) before the modules load | 1 | Joined, or detached by `boot_wait` after `NX2NAVI_PREWARM_WAIT_MS` or 60 s without progress; then runs on during play | +| "log" | `logq.c` `log_main` (~114) | `main.c` `log_open` → `logq_async` (~125), game runs, unless `NX2NAVI_LOG_ASYNC=0` | 1, detached | Never ends; `logq_sync` turns the queue off | +| "save commit" | `fs_host.c` `commit_main` → `commit_run` → `save_commit_locked` | `fs_host.c` `save_commit` (~601) on each `nn::fs::Commit` with a dirty save | One per Commit, detached; inline when the spawn fails | After the commit | +| PGO flush | `platform/win32.c` `pgo_flush_thread` | Constructor, only with `NX2NAVI_PGO_GEN` (instrumented builds) | 1 | Never | +| Open folder | `platform/win32.c` `open_folder_main` | `plat_open_folder` (menu) | Per call | After `ShellExecuteW` | +| Exception handlers | `platform/win32.c` `fault_veh` (first), `crash_veh` (last), `crash_filter` | Registered by `plat_mem_fault_hook` (from `memory.c` `mem_init`) and `plat_crash_hook` (from `main.c`) | — | Run on the faulting thread; `crash_veh` → `main.c` `crash_handler` → `plat_exit(71)` | +| "reap" (Linux only) | `platform/linux.c` `reap_main` | Child process start | Per child | When the child exits | + +#### Graphics: replay, submission, present + +| Thread | Entry | Started by, when | How many, priority | Ends | +|---|---|---|---|---| +| Game submit thread(s) | `nvn_api_core.c` `nvnQueueSubmitCommands` → `vk_backend.c` `submit_sync` → `vk_sync.c` `submit` → `vk_exec.c` `vk_exec_record` | The game | Guest threads | — | +| "The recording thread" | Whichever guest thread first spends 0.5 s in `vk_exec_record` | `vk_exec.c` `rec_claim` (~1643) | Raised to highest priority unless `NX2NAVI_RECORDER_PRIO=0`. Gets a reserved core (`plat_cpu_reserve`) with `NX2NAVI_RECORDER_CORE=1`, or `auto` with 12 or more usable CPUs | — | +| Game present thread | `nvn_api_obj.c` `nvnQueuePresentTexture` → `vk_backend.c` `vk_be_present` | The game | Guest thread | — | +| "vk present" | `vk_backend.c` `present_main` (~75) | `present_image` (~252) at the first present | 1, detached, normal | Never | +| "vk replay" | `vk_backend.c` `replay_main` (~406) | `replay_enqueue` (~472), only with `NX2NAVI_REPLAY_ASYNC=1` (`cfg.c` default 0) | 1, detached; becomes the recording thread | Never | +| "vk recorder" | `vk_rec.c` `rec_main` (~61) | `rec_begin` (~179) at the first submission part | 1. On unless AMD (`NX2NAVI_VK_RECORDER`, `rt_knob` default "0" on AMD, else "1"). Takes the priority of whichever thread called `rec_begin` first | Never (handle kept, never joined) | +| "vk completion" | `vk_sync.c` `completion_main` (~382) | `vk_sync_start` (~471) | 1, normal | Joined in `vk_sync_shutdown` | +| "vsync" | `gfx_present.c` `vsync_main` (~175) | `vsync_ensure` (~210) | 1, detached, highest | Never | +| "fg present" | `gfx_present.c` `ng_thread` (~1098) | `ng_ready` (~1180), only when `vk_ngxfg_wanted()`; the comment at ~925 says 0 on Windows | 1, detached | Never | +| "vk headroom" | `vk_headroom.c` `headroom_main` (~157) | `vk_headroom_start` from `vk_device.c` `vk_init`; off with `NX2NAVI_VRAM_HEADROOM=0` | 1, detached, below normal | After `vk_headroom_stop` | +| "mem census" | `vk_memory.c` `census_main` (~3138) | `heap_log`, only with `NX2NAVI_MEM_CENSUS=1` | At most one at a time | After one walk | + +#### Pipelines + +| Thread | Entry | Started by, when | How many, priority | Ends | +|---|---|---|---|---| +| "pipe worker" | `vk_pipeline.c` `pipe_worker` (~3533) | `workers_start` (~3623), first use | `plat_cpu_usable()/4` clamped 2..`MAX_WORKERS` (4). Below normal for prebuild jobs, normal otherwise | Never | +| "pipe lib worker" | `vk_pipeline.c` `lib_worker` (~3500) | `workers_start` (~3638), with GPL and `NX2NAVI_PIPE_WAIT` not `draw` | Pipe-worker count or 2 | Never | +| "pipe prewarm" | `vk_pipeline.c` `warm_builder` (~4015) | `prewarm_run` (~4186) on the prewarm thread | min(cpus, 64) | Joined; ends at `PREWARM_PIPE_MS` (20 s) unless `NX2NAVI_PREWARM_FULL=1` | +| "pipe list" | `warm_builder` (~5942) | `seed_pipelines`, preparation modes only | min(cpus, 64) | Joined | +| "big compute" | `vk_pipeline.c` `bg_main` (~5140) | `bg_start` (~5252) by the last `lb_worker`; `NX2NAVI_LIB_PREBUILD_BIG_BG` (default 1) | 1, below normal; raised to normal when a dispatch waits for it | Joined in `lb_stop` | +| "lib prebuild" | `vk_pipeline.c` `lb_worker` (~5418) | `lb_start` (~5707) from the boot prewarm; `NX2NAVI_LIB_PREBUILD` | `NX2NAVI_LIB_PREBUILD_THREADS` or 2 (max 16). Idle priority, normal around `P.lock` and shader loads | When the list is done | +| "lib warm check" | `vk_pipeline.c` `lw_main` (~5572) | `lw_start` (~5649); `NX2NAVI_LIB_WARM` (default 1) | 1, below normal | Joined in `lw_stop` | +| "lib warm" | `vk_pipeline.c` `lw_worker` (~5532) | `lw_main` when the driver cache is cold | cpus/4 clamped 2..6, idle | Joined in `lw_stop` | +| "lib warm" | `vk_pipeline.c` `lb_warm_worker` (~5733) | `lb_warm_prepared` (~5794), preparation modes only | min(cpus, 64) | Joined | +| "pipe autosave" | `vk_pipeline.c` `autosave_main` (~6390) | `vk_pipeline_autosave` (~6402) from the present thread, once | 1, detached, below normal, 500 ms loop | Never; not stopped by `vk_pipeline_shutdown` (lead L-15) | + +#### Shaders + +| Thread | Entry | Started by, when | How many, priority | Ends | +|---|---|---|---|---| +| "xlate" | `gfx_shader.c` `xlate_worker` (~2070) | `start_workers` (~2127), first async job | `NX2NAVI_XLATE_PROCS` (default 2, 1..32) | Never | +| "shload" | `gfx_shader.c` `load_worker` (~2093) | Same | `LOADERS` = 3 | Never | +| "use preload" | `gfx_shader.c` `use_preload` (~1304) | `use_preload_start` (~1335) | 1, below normal | After the scan | +| "shader ids" | `gfx_shader.c` `ids_write_main` (~506) | `ids_note` (~515), once | 1 | After one write | +| "shader prewarm" | `gfx_shader.c` `warm_worker` (~2314) | `gfx_shader_prewarm` (~2399) from `prewarm_run` | cpus, 1..64. Translator children raised to `NX2NAVI_PREWARM_XLATE` or min(cpus/2, 16) meanwhile | Joined | +| "shader prepare" | `gfx_shader.c` `prep_worker` (~2732) | `prepare_files` (~2787), preparation modes | 1..64 | Joined | +| "shader carry" | `gfx_shader.c` `carry_main` (~3194) | `gfx_shader_carry_start` (~3305) from `prewarm_main`; `NX2NAVI_SHADER_CARRY` (default 1) | 1, lowest; own translator child at idle priority | After one pass | +| Translator processes | `nx2navi_xlate.exe --serve` | `gfx_shader.c` `child_start` (~533) via `plat_child_start` | Pool `S.s[MAX_SVC=32]`, limit `NX2NAVI_XLATE_PROCS`, raised during prewarm | Kept for the life of the process | +| "astc" | `tex_astc_mt.c` `worker` (~58) | `pool_init` via `plat_once` at the first large CPU ASTC decode | Up to 7 | Never | + +#### Audio and movies + +| Thread | Entry | Started by, when | How many, priority | Ends | +|---|---|---|---|---| +| "audren" | `audren.c` `render_thread` (~517) | `audren_manager_handler` cmd 0 (~682) on the game's IPC thread | One per open renderer, highest | Joined in `renderer_close` | +| SDL audio output | `audio_mixer.c` `sdl_pull` (~320) | SDL's own device thread, `SDL_OpenAudioDeviceStream` in `stream_open` (~344) | 1 per stream | When the stream is destroyed | +| SDL audio capture | `audio_capture.c` `received` (~26) | SDL's own thread, `mic_open` (~98) | 1 per mic | `mic_close` | +| "movie" | `ov_movie.c` `worker` (~238) | `ov_ctor` (~454) on a game thread | One per VP9 decoder (`MAX_MOVIES` 16), plus libvpx's own threads (`VPX_THREADS` 4) | Joined in `destroy` | +| "movie decoder" | `ov_movie2.c` `decoder_thread` (~483) | `ov_initialize` (~601) | One per decoder, plus dav1d's own threads (8) | Joined in `stop_thread` | +| "movie convert" | `ov_movie2.c` `convert_thread` (~443) | `ov_initialize` (~603) | One per decoder | Joined in `stop_thread` | +| "movie convert" (stripes) | `ov_movie2.c` `stripe_run` (~269) | `run_stripes` (~287), per converted picture | 5 per call, created and joined each frame (lead L-34) | Joined | + +#### Input, window and menu + +| Thread | Entry | Started by, when | How many | Ends | +|---|---|---|---|---| +| "pads" | `input.c` `up_main` (~416) | `sdl_start` (~458) at the first poll | 1, detached | Never (unless SDL init fails) | +| "motion" | `input.c` `motion_main` (~1159) | `input_motion_start` (~1186) from `ov_input.c` `ov_six_start` | 1, detached, 250 Hz loop | Never, also after the sensor stops | +| "ds3-scan" | `input_ds3_win.c` `scan_thread` (~122) | `ds3_read` (~276) with `NX2NAVI_DS3` | 1, detached, 256 KiB stack | Never | +| "window" | `gfx_window_win.c` `window_main` (~643): the Win32 message loop | `wsys_open` (~729) | 1, detached | When `GetMessage` returns <= 0 | +| "folder dialog" | `folder_pick_win.cpp` `pick_main` (~51) | `pick` (~90) from the menu | Per pick | After the dialog | +| "save export" | `ui/menu.cpp` `share_export_main` (~1116) | `rows_saves` (~1176) | One at a time (`g_share_busy`) | After the export | +| "menu-art" | `ui/menu_art.cpp` `decode_all` (~48) | `menu_art_start` (~66), once | 1 | After decoding | +| "camera speed" | `camera_speed.c` `search_main` (~262) | `cam_speed_tick` (~304) on the present path | One at a time (`searching`) | After one search | + +#### Monitoring, profiling and other + +| Thread | Entry | Started by, when | Ends | +|---|---|---|---| +| "discord" | `discord_rpc.c` `discord_main` (~193) | `discord_init` from `main.c` | Never ("there is no shutdown path") | +| "rtss" | `rtss_limit.c` `worker` (~245) | `start` (~319) at the first `rtss_limit_want` | Never | +| "perfmon" | `perfmon.c` `sampler` (~360) | `perfmon_mode(2)` (~404) from the menu's overlay | When the full overlay is turned off | +| Profiler sampler | `prof_win.c` `sampler_main` (~242), time-critical, suspends every other thread each 1 ms | `prof_sampler_start` (~386), only with `NX2NAVI_PROFILE` | When `prof_sampler_write` stops it (waits 2 s) | +| "flight dump" | `flight.c` `dump_main` (~231), below normal | `start_dump` (~311), with `NX2NAVI_FLIGHT` | After one dump | + +#### Threads inside libraries + +Not enumerated: + +- the GPU driver; +- NVIDIA Streamline and NGX; +- AMD FidelityFX (its frame-generation presenter; `vk_device.c` makes two more graphics-family queues for it, + `vk_fg_queues`); +- SDL's internal threads; +- libvpx and dav1d workers. + +### 3.3 Shared state and what protects it + +Each line names a piece of shared state, the threads that touch it, and the rule. "Protection not found" marks state +touched from more than one thread where no lock or atomic rule was found. That is a lead for Phase 3 pass 2, not a +finding. + +#### Kernel (`kernel.c`) + +- **`G` (`PlatMutex`, ~114).** Header comment: "every wait hangs on one global lock G with one condition variable per + thread". It guards: + - the handle table `g_h[MAX_H]`, `g_all`, `g_wait`, `g_core_owner[]`, `g_core_wait`; + - every `RtThread` wait and core field, event `signaled` and `refs`; + - `g_next_id`. + + Threads: every guest thread at SVCs and waits, plus the waiter that suspends a holder (`ap_try`, `dg_sample`). +- **Lock-free reads, documented.** + - `g_core_wait` in `rt_spin_gate` and the SleepThread yield ("a racy look"). + - `t->preempt` at checkpoints. + - `host_depth`. + - TLR+0x100 and TLR+0x102 ("any thread may read them"). +- **Guest per-core data.** Kept without locks by the game itself, made safe by the virtual-core rule. A guest thread + runs guest code only while it holds a core, and gives it up only where Horizon would switch. + `NX2NAVI_FREE_CORES=1` turns the gate off. +- **`g_se_lock`** (rwlock, ~2062): the `g_se[512]` system-event table. +- **`g_fiber_lock`** (rwlock, ~2133): `g_fibers[]`. The `Fiber` fields written by `kern_fiber_initialize` outside the + lock: protection not found (guest threads re-initialising and switching to a fiber). +- **Lazy one-time init outside `G`:** protection not found (guest main and the first guest threads). + - `cores_enabled` (~341), with `diag_init` and `ap_init`. + - `g_spin_on`, and `lim` in `kern_svc`. + - `mode` in `wake_sync_waiters`. + +#### Guest memory (`memory.c`) + +- **`lock`** (rwlock, ~50): heap size, alias bookkeeping, `stack_maps`, `module_perm`, `map_gen`. + - `mem_read_hold` keeps it shared over long reads. + - `stack_move` (~997) reads `heap_size` before taking `lock`: protection not found (guest threads mapping against + `SetHeapSize`). +- **`frz_lock`** (~75). It guards: + - the freeze/thaw times; + - the lazy write-back segments `lzs`; + - the hot list and probe hits; + - every `lz_*` counter. + + Taken by any faulting thread inside the exception handler (`thaw_fault`, `lazy_fault`, `probe_fault`). The comment + calls it a leaf lock, but the back end's `lz_fetch` (`vk_memory.c` `lazy_fetch`: a GPU copy and a wait of up to + 10 s) and `lz_clean` run under it (section 6, D-05). +- **Atomic flags read without the lock:** `frz[]`, `prb[]`, `lzr[]`, `nfrz`, `nprb`, `frz_count`, and `lz_n` in + `mem_lazy_note` (documented). +- **`klock`** (~1284): the kernel-region allocator's free lists and bump pointer. +- **Write watch.** `plat_mem_written` (`GetWriteWatch` with reset) is called from `vk_memory.c` (~748, ~2606, ~2882) + under `vk_memory.c`'s `gen_lock` on the replay side. `memory.c` itself takes no lock for it. +- **Lock orders:** `lock` → `frz_lock` (commit, decommit, protect); `klock` → `frz_lock`. + +#### Save data (`fs_host.c`) + +- **`tx_lock`** (~220): "taken before lock, never inside it". It guards: + - the save transaction (`ready_root`, `ready_seq`, the working copy, commit and swap steps); + - the backup rotation; + - `fs_save_export`. + + The commit thread takes `tx_lock` alone. +- **`lock`** (rwlock, ~48): slot assignment and release in `mounts[]`, `files[]`, `dirs[]`. +- **`pend_lock` and `pend_cv`** (~562): `commit_pending`, `exiting`. `save_writers`, `save_dirty`, `save_changed_ns` + and `storage_ready` are atomics. +- **Protection not found:** + - the `files[].h` handle used after `file_of` (~1068) drops the shared lock while `close_file` may close it (guest + threads); + - `dirs[]` contents read in `read_directory` (~1307) while `close_directory` frees them; + - `remove_failed` and `remove_error` (commit thread and guest threads). + +#### Logging and settings + +- **`logq.c`.** `Q.q` guards the buffer; `Q.io` guards writes and the output, and is taken first. + - `logq_sync` try-locks each for up to 500 ms, then writes without them (documented). + - The log file is appended with whole writes (`plat_append_write`). +- **`cfg.c`.** `set_lock` (~28) covers `cfg_set`, `cfg_reset` and `cfg_save_to`. Readers take no lock: "an entry is + complete before n_entries counts it, and value pointers are swapped atomically". + - `cfg_memo` is per thread. + - `getenv`-based reads against `cfg_setenv` → `_putenv_s` while the game runs (`gfx_present.c` ~90): protection + not found. + +#### Files, services and the rest of the CPU side + +**RomFS, decompression, IPC** +- `romfs.c`: + - `lock` (rwlock, ~83) guards the handle tables. + - Protection not found: `mount_names` read by `romfs_owns` while `romfs_mount` writes it; a handle used after + `handle_source` unlocks while `romfs_close` closes it. +- `ov_zstd.c`: + - `dict_lock` guards the dictionaries ("never freed: another thread may be decoding with one"). + - `stream_lock` covers slot claim and drop only; a slot's stream fields belong to the thread that holds it. + - A "spent" slot can be handed out while the old thread still reads it: protection not found (lead L-11). +- `ipc.c`: + - `g_applet_lock` guards the applet message ring. + - Protection not found: `IpcSession` domain fields changed during dispatch by any thread using the session; the + session pointer is used without a reference while `CloseHandle` may free it. + +**System services** +- `ov_system.c`: + - `lock` and `message_cv` guard the message queue. + - `account_lock` guards account state. + - `fonts.lock` guards the shared fonts. +- `net_offline.c`: `lock`, then `G` through `kern_event_clear`. +- `amiibo.c`: `g_amiibo_lock`. +- `swkbd.c`: `one` serialises keyboards. +- `loader.c`: `plt_lock`. +- `guest_setjmp.c`: `jt_lock`. + +**Written once at start, then read-only** +- `dispatch.c`: the dispatch table is sealed read-only. +- `hostfn.c`: the registry is fixed at seal ("guest threads start after that"). + +**Patches, mods, cheats** +- `patches.c`: `rt_patch_on[]` bytes are written atomically by `rt_patch_switch`. +- `mods.c`: `mods[]` is changed by `mods_move` and `mods_set_enabled` (menu) while romfs, cheats and boot read it. + Protection not found. + +#### NVN front end + +- **`nvn_api_cmd.c`** + - `copy_lock` (~417): "guards the two maps, the counters and every prefix", and `zombies[]`. Order: `copy_lock`, + then `pin_lock`. + - `pin_lock` (~257): pins and the pin queue. + - `pool_lock` (~86): the recording-buffer pool. + - `cb_lock` (~41). + - `nvn_submit_prepare_async` writes `prefix->deferred` after unlocking (~1294): protection not found. +- **`nvn_api_core.c`** + - `pool_lock` (~75): "guards gva and index". + - `submit_lock` (~638): only with the asynchronous replay. + - With the default synchronous replay there is no front-end submit lock. `q->submits++` and + `nvn_submits_in_frame++` (~634, ~676) are plain increments, and `nvnQueuePresentTexture` resets + `nvn_submits_in_frame` (`nvn_api_obj.c` ~868). Protection not found if the game submits and presents from + different threads. +- **`nvn_api_cmd.c` `nvn_submit_prepare` → `resolve_deferred`** rewrites op records in place, unlocked. Another + thread may be replaying the same handle under `sub_lock`. Protection not found (sync mode). +- **`nvn_api_obj.c`** + - `live_lock` guards the live-texture copies. + - `img_fence[]` and `last_present_fence` are written at present and read at acquire. Protection not found if those + come from different threads. + +#### Replay, submission and the queue + +- **`vk_sync.c` `sub_lock`** (~31): "Submissions: one at a time, so timeline values are signalled in the order they + are handed out". + - It is held across all of `submit()`: the whole `vk_exec_record` replay, `vk_upload_flush`, the prologue and + `queue_submit`. + - Through that it guards vk_exec's `X` state, the recorder's producer side (`vk_rec.h`: every producer holds it), + and vk_memory's per-submission lists ("submit lock"). + - Taken by: game submit threads (sync), the replay thread (async), the present thread (its empty submission) and + memory probes. +- **`st_lock`, `cv_work`, `cv_done`** (~39): `issued`, `done`, `reserved`, `stop` and the completion queue. Shared + with the completion thread, which runs callbacks and `vk_mem_collect` outside the lock. +- **`start_lock`** (~26): device start-up and the completion thread handle. +- **The queue lock `g_ql`** (`gfx_present.c` ~42, recursive). + - `vk.h` ~213: "Every submission to ctx->gfx_queue (and xfer_queue when shared) takes this lock". + - `vk_queue_lock` also flushes held submissions; the `_raw` forms do not. + - Submit batching `H` is guarded by it (`vk_sync.c` ~58). + - Callers of `vkQueueSubmit2`: + - `vk_sync.c` `hold_flush_locked`: from `queue_submit`, `hold_release` (fence waiters; the completion thread only + try-locks), and `vk_queue_lock`; + - `gfx_present.c` `present_image` (~1542); + - `vk_memory.c` `vk_upload_flush` (~3750) and `lazy_fetch` (~2582); + - `vk_fg.cpp` `submit_game`. + - Callers of `vkQueuePresentKHR`: `gfx_present.c` `flip_locked` (~313) and `ng_present` (~991), through Streamline + in `vk_dlssg_present`. + - `vkDeviceWaitIdle` without `g_ql`: `vk_pipeline.c` `vk_pipeline_shutdown` (~6412) and `vk_device.c` `destroy_all` + (~548); see D-11. +- **`vk_backend.c`** + - `pl` and `pcv` (~41): the present job (`job`, `pending`, `busy`). Taken by the present thread and every + `vk_present_drain` caller. + - `rl` and `rcv` (~279): the asynchronous replay ring (`RQ_CAP` 1024). + - `PH` (present held, ~338) and `FH` (frame hold, ~145): protection not found. Touched by the game present thread + and the submit thread(s) in sync mode, and `FH` by the recording thread (`vk_frame_hold_left`). +- **`vk_exec.c`** + - `bcs_lock` (~172): the BC stand-ins ("the replay adds and reads; destroy_tex (any thread) removes"). + - `ET.lock` (~1061): the event-order trace. + - Called outside `sub_lock` from `vk_backend.c` (~220-232, ~360, ~549): `vk_exec_held()`, `vk_exec_held_ready()`, + `vk_exec_frame_log`, `vk_exec_pending_draws_take`, `vk_exec_gate_wait`. Protection not found when the presenting + and the recording thread differ. +- **`vk_exec_draw.c`** + - `spare_lock` (~134): spare descriptor pools and chunks; the replay takes them, the completion thread returns them. + - `pool_lock_` (~1951): the bindless pool layout's lazy creation. + - The texture lock is held throughout the bindless pool sync. +- **`vk_exec_xfer.c`** `cpu_ev_lock` (~458): the CPU event signals. Guest threads (`nvnEventSignal`) against the + replay. +- **`vk_usnap.c`** + - `free_lock`: the free arenas. + - `gather_lock`: the collect buffers, shared by the submitting thread and the replay thread. + - `tq_lock`: the taken-snapshot queue. + - The lazy `U.mode` init: protection not found (benign). +- **`vk_rec.c`.** A lock-free single-producer ring. + - `head` and `tail` are atomics; `wake_ev` is an auto-reset event. + - The producer is any thread holding `sub_lock`. + - `OWNED(cb)` reads `rec_live` and `rec_cb` without atomics from other threads that record into other command + buffers: protection not found (benign by value, per the mapping pass). + +#### GPU memory (`vk_memory.c`) + +**Documented order:** `pool_lock` → `shadow_lock` → `gen_lock`. + +| Lock | Guards | Taken by, and order | +|---|---|---| +| `pool_lock` (~105) | Pool index, physical list, page tables, `mlist` (mirrored pools) | Shared on lookups, exclusive on init, finalize, map and mirror changes | +| `gen_lock` (~729) | Page-generation tables, `snap_*`, `mgen`/`sgen` | The replay, guest threads (`vk_guest_written`), the completion thread (`vk_gpu_written`), textures (`vk_mem_gen_lock`) | +| `shadow_lock` (~1035) | Shadow storage | After `pool_lock`, before `gen_lock` | +| `flush_lock` (~1609) | Flush ranges per pool | Order `pool_lock` → `flush_lock` | +| `dirty_lock` (~983) | Perf census | — | +| `mirror_stat_lock` (~1622) | Mirror summary | — | +| `retire_lock` (~1982) | `retiring[]` | Finalizing guest thread vs the completion thread | +| `snap_pool_lock` (~2221) | Write-back snapshot chunks | — | +| `lz_init_lock` (~2553) | One-time lazy write-back setup | — | +| `cmd_lock` (~3460, recursive) | The upload batch, deferred destruction `dead[]`, the staging cache and arena | Held from `vk_upload_begin` to `vk_upload_end`; order `cmd_lock` → `g_ql` | + +Protection not found: +- `PoolRec.mstate`: set to `MS_ON` after `pool_lock` is released in `mirror_create` (~1949), read by `vk_pool_fini` + (guest) and `vk_gpu_written_back` (completion thread). See L-07. +- `PoolRec.st_*`: "the log reads and clears them without a lock" (acknowledged). + +#### Textures (`vk_texture.c`) + +- **`tex_lock`** (~119, recursive). It guards: + - the texture cache (`by_key`, `by_addr`); + - the texture and sampler slots, the sampler cache; + - the released lists; + - the eviction state. + + Taken by the replay, guest threads (register, release, ReadTexels/WriteTexels, `guest_read`, `range_gone` from pool + finalize) and the present path (`vk_texture_get`). + - Orders: `tex_lock` → `cmd_lock` (`init_image(e, vk_upload_begin())`); `tex_lock` → `gen_lock`; + `tex_lock` → `pool_lock` (shared). +- **`owned_lock`** (~165): GPU-written mirrored textures. "Changed under tex_lock and owned_lock; read under either". + +#### Pipelines and shaders + +**`vk_pipeline.c`** + +- **`P.lock`** (~71). It guards: + - the pipeline, layout and library hash tables; + - the job queues; + - the prebuild bookkeeping and statistics. + + `work_cv`, `done_cv`, `lib_cv` and `A.idle` go with it. Builds run with `P.lock` released. +- **`cache_rw`** (~520). Creates against `P.cache` hold it shared; `cache_save` takes it exclusive, by trylock only. +- **Other locks:** + + | Lock | Guards | + |---|---| + | `save_lock` | One cache save at a time; order `P.lock` → `save_lock` at shutdown | + | `cu_io` | `compute-used.bin` | + | `PB.lock` | Pipeline binaries | + | `K.io` | `pipeline-keys.bin` | + | `VO.lock`, `FB.lock`, `CU.lock`, `LB.stat` | Small tables and statistics | +- **Not protected:** `BG.building` is written by "big compute" without `P.lock` and read under it (benign per the + mapping pass). + +**`gfx_shader.c`** + +| Lock | Guards | +|---|---| +| `cache_lock` (~135) | The memory cache | +| `S.lock` | Translator children | +| `F.lock` | "one translation per id at a time" | +| `ids_lock` | `identities.txt` | +| `failed_lock` | Failed keys | +| `X.lock` with `work`/`done`/`lwork` | Job queues | +| `load_lock` | The disk-read budget | +| `RC.lock`, `rec_io` | Record files; order `rec_io` → `RC.lock` | + +- `use_files` is filled by "use preload" and published by an atomic flag. +- `gfx_shader_drop_memory` (probe only) frees it while it may still be filling: protection not found. +- The code drops threads to idle or lowest priority, and raises them to normal around `cache_lock`. The comment says + why: "use_store takes the cache lock the render thread reads under". + +#### Present, window and menu + +- **`gfx_present.c` `g_pl`** (~125, recursive): the swapchain and present state. Order `g_pl` → `g_ql`. + - Held across the 5 s fence wait, the 1 s acquire and `create_swapchain`. + - Taken by the present thread, the boot thread (`gfx_present_boot`) and the window thread (`gfx_window_resize`). +- **`g_start_lock`** (recursive): window and vsync start. Order `g_start_lock` → `g_pl`. +- **`g_vs_lock`**: vsync events; exclusive in `gfx_vsync_bind`, shared in the vsync thread. +- **`g_mv_lock`**: the movie frame (decoder threads write, the present thread reads). +- **`ng.m` and `ng.cv`**: the NGX presenter hand-off. `ng.interval`, `ng.tried` and `ng.n`: protection not found + (non-Windows only). +- **Menu (`ui/menu.cpp`).** + - "ImGui runs on the present path only, under gfx_present.c's present lock; the event functions only queue." + - `open_lock` → `ev_lock`, and `open_lock` → `cur_lock` (`input_win.c`) through `input_menu`. + - `g_share_lock`; `g_pick_lock` (`amiibo_page.cpp`). +- **`menu_assets.c`** `dicts` and `ndicts`: lazily loaded without a lock from the "menu-art" thread and the present + thread. Protection not found. +- **`gfx_window_win.c`.** The window thread owns its state. Settings arrive by `PostMessage`; `wsys_resize` and + `wsys_probe_*` call into it synchronously. + +#### Audio, movies and input + +**Audio** + +- **`audren.h` `AudioRenderer.lock`.** It guards voices, mixes, effects, sinks, state and mixer render state, between + the game's IPC thread (`request_update`) and the render thread. + - Unlocked: `GetState`, `time_limit`, and `g_game_prio`. Protection not found (benign ints). +- **`audio_mixer.c`.** The output ring is single-producer single-consumer and lock-free ("the SDL audio thread drains + that ring without taking any lock"): `wr`/`rd` atomics. + - The stream fields are changed only after the stream is destroyed. + - `change_events[16]`: the slot index is reserved before the slot is stored. Protection not found for the slot + contents. + - `host_volume` and `sink_dump` statics: protection not found if two renderers are open. +- **`menu_sound.c`.** Per-voice atomics. `started` is plain while `menu_sound_play` is called from the window thread, + the polling thread and the present path: protection not found. +- **`audio_capture.c`.** Relies on the SDL stream lock. + +**Movies** + +- **`ov_movie.c`.** `g_lock` → `Movie.mu`. + - `find()` releases `g_lock` before the movie is used while `destroy` may free it: protection not found (L-08). +- **`ov_movie2.c`.** `lock` ("queues, frames, flags") and `codec` (dav1d use). + - `ov_decode` reads `v->dav1d` without either while `finalize` closes it: protection not found. + - Static timing totals: protection not found (benign). + +**Input** + +- **`input.c`.** + - `lock` guards the pads, state, script, binds and DualShock 3 state. + - `up_lock` with `up_go` and `up_done` guards the update hand-off to the pad thread. + - `sdl_lock` is held around SDL gamepad calls; others only try-lock it. + - Order: `lock` → `up_lock` → `sdl_lock` (trylock). + - `mouse_sens_get` statics: protection not found. +- **`ov_input.c`.** `lock`; `vib_lock` → `input.c` `lock`. The `rumble_cfg` statics written outside `vib_lock`: + protection not found. +- **`input_win.c`.** Mouse deltas are interlocked; `cur_lock` serialises cursor updates. + +**Frame rate and resolution** + +- **`frame_rate.c`.** + - The step words on the patch page are atomics: "one 64-bit and one 32-bit store per present; the guest reads the + floats on its own threads". + - `limit_hz`, `limit_seen`, `limit_read` and `fps60_set` in `frame_limit_live` are reached from three threads: the + presenting guest thread, the menu's present path (`frame_step_limit_hz`), and the mouse camera thread + (`frame_step_game_hz`). Protection not found. +- **`resolution.c`.** `renderer` (written by `res_fn_hit` on any guest thread) and `R.sw`/`R.sh` (DLAA vs the menu): + protection not found. +- **`camera_speed.c`.** `applied`, written by the search thread and read by the poll thread: protection not found. + +### 3.4 Lock order (observed) + +These orders were observed in the code; only a few are documented in comments. + +- `sub_lock` → `g_ql`, `cmd_lock`, `st_lock`, `tex_lock`, `pool_lock`, `gen_lock`. +- `tex_lock` → `cmd_lock`, `gen_lock`, `pool_lock`. +- `cmd_lock` → `g_ql`. +- `g_start_lock` → `g_pl` → `g_ql`. +- `rl` → `st_lock`. +- Async `submit_lock` → `copy_lock`, `gather_lock`, `tq_lock`, `rl`. +- `start_lock` → `st_lock`, `sub_lock`, astc `g_lock`. +- `P.lock` → `save_lock`; `rec_io` → `RC.lock`. +- `memory.c` `lock` → `frz_lock`; `klock` → `frz_lock`. +- `fs_host.c` `tx_lock` → `lock`. +- `ov_movie.c` `g_lock` → `Movie.mu`; `ov_media.c` registry → decoder. +- `input.c` `lock` → `up_lock` → `sdl_lock` (trylock). +- `ov_input.c` `vib_lock` → `input.c` `lock`. +- `menu.cpp` `open_lock` → `ev_lock`, `cur_lock`. +- Kernel `G` is innermost against the other modules. + +No inversion was found in what was read. Not checked: +- `frz_lock` against the GPU-side locks `lazy_fetch` takes (`g_ql`); +- any lock held by a thread that faults into `fault_veh` (L-05). + +--- + +## 4. The path of one frame + +This is the default configuration: replay on the submitting thread, recorder thread on (not on AMD), a discrete GPU +with VRAM mirrors, no frame generation. Steps are in order, function by function. + +### 4.1 The game's CPU frame (guest threads) + +1. **Acquire.** `nvn_api_obj.c` `nvnWindowAcquireTexture` (~813). + 1. Gives the core away (`NVN_BLOCKING`). + 2. Picks the next image index. + 3. `sleep_until(o->avail[idx])`: the image's previous present slot plus one period. This is back-edge pacing at + the frame limit, using `plat_wait_ns` and then `plat_yield`. + 4. Waits for the GPU to finish that image's newest present (`fence_wait`, `NX2NAVI_ACQUIRE_WAIT` default 1; 2 + waits for the newest present of any image). + 5. `frame_start`. +2. **Game logic.** The game's update runs on its own threads, stepped by the value `frame_rate.c` wrote at the last + present (section 4.4). Audio, movie and input services answer on the threads that call them. +3. **Recording.** The game records command buffers through `nvn_api_cmd.c`: the `nvnCommandBuffer*` entry points. + - Each call appends an op record (`op_hdr` plus payload) to recording memory. + - `CallCommands` is inlined at record time (`inline_recording`). + - Deferred binds are noted, to be resolved at submit. + - Nothing reaches Vulkan yet. +4. **Submit.** `nvn_api_core.c` `nvnQueueSubmitCommands` (~627). + 1. `NVN_BLOCKING`; `q->submits++`. + 2. `nvn_submit_prepare(h, n)` resolves deferred binds in place. + 3. `gfx_backend()->submit` → `vk_backend.c` `vk_be_submit` → `submit_sync` (~383). + +### 4.2 Replay into Vulkan (on the submitting thread) + +5. **`submit_sync`.** + 1. If a present is waiting for held work, the held work goes first (`held_resume`, `ph_flush`). + 2. `vk_present_drain` waits until the present thread has taken and submitted its job. + 3. `vk_exec_gate_wait` handles a submission that begins with a wait for a CPU signal. + 4. `vk_texture_new_submit` bumps the submit epoch. Every 8th epoch it also runs the texture VRAM pressure check + `sweep_pressure` and the evictions. + 5. `vk_sync_submit` → `vk_sync.c` `submit` (~573). +6. **`submit`.** + 1. Takes `sub_lock`. + 2. `fence = issued + 1`. + 3. `take_slot` → `claim_slot` reuses or allocates a command buffer. + 4. `vkBeginCommandBuffer`, `ts_begin` (GPU timestamps with `NX2NAVI_TRACE=perf`). + 5. `rec_begin` hands the part to the recorder ring. + 6. `vk_exec_record`. +7. **`vk_exec_record`** (`vk_exec.c` ~1666). + 1. `nvn_pins_sweep`. + 2. `vk_exec_event_sync(1)`: a CPU signal after this point counts as late. + 3. `vk_mem_mirror_begin`: the pages the CPU wrote (write watch) and the ranges the game flushed are queued as + copies into the VRAM mirrors. + 4. `vk_usnap_begin` copies every uniform range the submission binds from CPU-written pools. + 5. `evo_resume` lets held submissions whose signal came go on. + 6. For each command handle, `walk()`. +8. **`walk`** (~1532) dispatches each op: + + | Ops | Handler | + |---|---| + | `OP_SET_RT` | `set_rt` | + | Clears | `clear_color`, `clear_ds` | + | Viewport, scissor, stencil, bias, blend state | Stored in `ex`, marked dirty | + | Draws, dispatches, binds | `vk_exec_draw.c` `vk_exec_op_draw` | + | Copies, event signals, counters, conditional rendering | `vk_exec_xfer.c` `vk_exec_op_xfer` | + | `OP_WAIT_EVENT` | `vk_exec_wait_event`. An unmet wait ends the pass, and the rest of the submission is held (`cont_push`) until the signal is replayed | + | `OP_BARRIER` | `barrier_to_why` | + | (long submissions) | `maybe_split` sends a part early: `vk_sync_split` → `split_part` → `queue_submit` | +9. **A draw** (`vk_exec_draw.c` `op_draw`, `op_draw_idx`, mesh and dispatch variants) → `prepare_draw_ex` (~3856). + 1. **Per-command-buffer resources and the pass.** + 1. `cb_res` provides transient chunks and descriptor pools for this command buffer. + 2. `vk_exec_pass_begin` begins dynamic rendering. Its targets are resolved through `resolve_rt` and the texture + cache. + 2. **Shaders.** + 1. `shader_for` → `gfx_shader.c` `gfx_shader_get_async` searches, in order: + 1. the memory cache; + 2. the disk cache, read within `load_budget`: 1/8 of wall time, 2 ms at once; + 3. the "shload" or "xlate" workers. + 2. A draw whose shader is not ready: + - reads the rest of the submission ahead (`vk_exec_ahead`), so later pipelines' stage libraries are queued; + - is skipped when its targets are per-frame (`vk_exec_targets_recent`); + - otherwise waits from the frame's wait pool (`frame_wait`, `pipe_frame_wait_ms`). + 3. **Pipeline.** + 1. `vk_pipeline.c` `vk_pipeline_graphics_async` looks up the pipeline hash. A miss is fast-linked from stage + libraries and queued for an optimised build on the pipe workers. + 2. Waits come from `link_budget`, `frame_wait` (full-screen draws), or `last_wait` when the frame hold has no + allowance left. + 3. A pending pipeline skips the draw (`draw_skipped`); the count feeds the frame hold. + 4. **Bind and record.** + 1. `bind_pipeline`. + 2. Dynamic state: `vk_pipeline_set_dynamic_delta`. + 3. `record_fixed`: viewport, scissor, depth bias, stencil. + 4. `bind_resources` (~3189), under the texture lock: + - `pool_sync` → `pool_refill`: the bindless texture and sampler descriptor sets, at most `pool_create_budget` + new entries per refresh and `pool_create_ms` per frame; + - uniform and storage buffers resolved through `vk_mem_lookup` and the uniform snapshot; + - push descriptors. + 5. `feedback_barrier`, when the draw reads a target of its own pass. + 6. `bind_vertex_buffers`. + 7. `vk_exec_oq_draw` (occlusion queries). + 8. `vkCmdDraw*`. + + Every `vkCmd*` goes through the `rc_*` wrappers (`vk_rec.h`), which copy the arguments into the ring. The + "vk recorder" thread makes the real calls in order. +10. **End of the recording** (`vk_exec_record` ~1736). + 1. `vk_exec_pass_end`. + 2. `vk_exec_barrier`, `vk_exec_pool_marks`. + 3. If there are write-backs (`NX2NAVI_WB_SPLIT`), `vk_sync_split_exec` ends the part that signals the executed + timeline `exec_tl`. + 4. `vk_mem_mirror_end` records the copies of what the GPU wrote into mirrors back to guest memory. With lazy + write-back, it arms those pages instead. + 5. `vk_usnap_end`. + 6. Per-command-buffer views are handed to the completion queue. +11. **Back in `submit`.** + 1. `rec_end_cb`: the recorder ends the command buffer. + 2. `vk_upload_flush(0)`: the texture and staging uploads this submission asked for are submitted first. + 3. `prologue`: + - `vk_mem_shadow_sync` (shadow mode); + - `vk_mem_gpa_sync`; + - `vk_mem_lazy_part_end`; + - the mirror copies (`vk_mem_mirror_prologue`) in a command buffer of their own. + 4. `queue_submit` (~115) takes `g_ql` raw and holds the submission. Up to `NX2NAVI_SUBMIT_BATCH` (default 4) are + held, for at most `HOLD_NS` (0.5 ms), then `hold_flush_locked` → `vkQueueSubmit2`. + 5. `vk_mem_note_submit`. + 6. `issued = fence`, broadcast to the completion thread. + 7. Releases `sub_lock`. +12. **Wait after the frame's first submission.** Back in `nvnQueueSubmitCommands`, the first submission of each frame + waits until the GPU has executed that submission's own passes. The wait is `exec_wait` on `exec_tl`, governed by + `NX2NAVI_SUBMIT_FINISH`, default mask 1. The comment explains why: the game rewrites data those passes read + without a fence. + +### 4.3 GPU completion ("vk completion") + +13. **`completion_main`** (`vk_sync.c` ~382). + 1. Waits on the timeline semaphore. It uses 100 ms steps, or `HOLD_NS` while submissions are held, flushing aged + held submissions with a trylock. + 2. Runs the `vk_completion_add` callbacks in FIFO order up to the reached value: + - `cb_done`: chunks and descriptor pools go back to the spares; + - GPU-write stamps (`note_gpu_write`); + - `arena_done` for the uniform snapshot arenas; + - write-back snapshot chunks; + - counter reports (`write_counter`); + - `mirror_retire`, `gpa_retire`. + 3. Sets `done` and wakes fence waiters. + 4. Calls `vk_mem_collect`: deferred destruction and retired upload batches. + +### 4.4 Present + +14. **The game presents.** `nvn_api_obj.c` `nvnQueuePresentTexture` (~858). + 1. `movie_tick` hands over movie frames that are due. + 2. `res_present`, `wide_present`. + 3. `NVN_BLOCKING`; `nvn_submits_in_frame = 0`. + 4. `present_period` → `frame_rate.c` `frame_step_cap_ns` gives the limit's period. While RivaTuner holds the + limit it returns 0. + 5. `pace()` keeps the phase: early waits for the slot; late by less than a period presents now; later restarts. + 6. `gfx_backend()->present` → `vk_backend.c` `vk_be_present(t, crop, slot)`. + 7. `o->avail[idx] = slot + period`; the image's fence. + 8. `frame_step_present(slot, interval)`: `ocarina_step` writes the game's next step (patch set `ostep`) to the + patch page or the game's time manager, and the world clock advances. + 9. `rt_frame_presented`. +15. **`vk_be_present`** (sync). + 1. Frame counter; `vk_dlssg_submitted`; `perf_present`. + 2. With `NX2NAVI_PRESENT_HELD` (default on in sync mode), a present that finds held work waits for it, up to + `PH_MAX` (8) submissions. + 3. Otherwise `present_image` (~215): + 1. Frame-level work: `vk_exec_frame_log`, `dynres_frame`, `vk_dlaa_frame`, `vk_sampler_aniso_live`, + `vk_mem_census`. + 2. `vk_texture_get(t)` gets the presented texture's image, possibly recording an upload. + 3. `frame_hold` checks for skipped draws, a resize or an area load. If it holds the frame, the previous + picture stays and this present is dropped (`NX2NAVI_FRAME_HOLD_MS`, 120 ms allowance). + 4. `vk_hdr_present_source`. + 5. `vk_upload_flush(0)`. + 6. The `PresentJob` goes to the "vk present" thread. At most one job waits, so the caller blocks while one is + pending. +16. **"vk present" `present_main`** (~75). + 1. `vk_thumbs_frame` (flight recorder), `vk_dlssg_present_frame`. + 2. `vk_present_blit` → `gfx_present.c` `present_image(defer=1)` (~1323), under `g_pl`. + 3. `vk_sync_submit(NULL, 0)`: an empty submission so deferred destruction waits for the blit. It may also replay + held work. + 4. `wait_until(pace_smooth(...))`: back-edge pacing to the slot. `NX2NAVI_PACE_SMOOTH` keeps a floor of 7/8 of + the recent interval. + 5. `vk_present_flip` → `flip_locked` → `vkQueuePresentKHR` under `g_ql` (or `vk_dlssg_present` / + `vk_fg_present`). + 6. `perfmon_shown`, `vk_pipeline_autosave`. +17. **What `present_image` (gfx_present.c) does,** under `g_pl`: + 1. `flip_locked` for the previous blit; the swapchain is remade if dirty. + 2. `vkWaitForFences` on the slot (5 s); `vkAcquireNextImageKHR` (1 s), or the frame-generation acquire. + 3. `window_settings_live`, `cam_speed_tick`. + 4. `menu_overlay_frame`: the ImGui layout, the font upload on the first frame. + 5. Record: + 1. bars; + 2. `put_picture`: blit, or FSR 1 (EASU, RCAS), colour, HDR encode; + 3. `hudless_record`; + 4. `movie_overlay`; + 5. `menu_overlay_draw`; + 6. capture and thumbnail; + 7. `vk_fg_frame`, `vk_dlssg_frame`, `ng_record`. + 6. `vkQueueSubmit2` under `vk_queue_lock`: waits on acquire, signals render-done, fences the slot. + 7. The flip is deferred to `vk_present_flip`. + +### 4.5 With `NX2NAVI_REPLAY_ASYNC=1` (off by default) + +- **Submit.** `nvnQueueSubmitCommands` takes the front-end `submit_lock` and runs `nvn_submit_prepare_async`. That + clones recordings still read by queued submissions. +- **Queueing.** `vk_be_submit` copies the handles, takes the uniform snapshot now (`vk_usnap_take`), and + `replay_enqueue`s. That reserves the timeline value under `rl`, and waits while the 1024-item ring is full. +- **Replay thread.** "vk replay" runs steps 5 to 11 for each item, and `present_image` for each present item. +- **Present.** The game's present waits until the previous present was handled ("frame bound"), without giving up + its core (L-43). +- **What changes:** + - event order across queues is off; + - the present-held logic is off; + - `exec_wait` becomes a full fence wait. + +### 4.6 Where a frame can stall + +Collected from the path; these are leads, not findings. + +**Waits in the path** +- The acquire's pacing sleep and GPU fence wait (step 1). +- `vk_present_drain` at every submit (step 5). +- The wait after the first submission of every frame (step 12). +- Shader and pipeline waits from the frame's pools (step 9). +- The texture pressure sweep every 8th submit, under `tex_lock` (step 5). +- The present thread's 5 s fence and 1 s acquire waits under `g_pl` (step 17). + +**Contention** +- `g_ql` held across `vkQueuePresentKHR` (step 16), which `queue_submit` waits behind. +- `sub_lock` held across a whole replay, which the present thread's empty submission waits behind. + +**Other sources** +- Lazy write-back faults on any guest thread, which wait for GPU work under `frz_lock`. +- Pool finalize, which waits for the GPU (`vkQueueWaitIdle`) on a guest thread. + +--- + +## 5. Allocators, pools and caches + +"Limit" names the constant, setting or variable that bounds it, or "none found". "Frees" says when its memory goes +back. + +### 5.1 Guest memory and kernel (CPU side) + +| # | What it holds | Owner | Limit | Frees | +|---|---|---|---|---| +| 1 | Guest address space reservations (`memory.c` `mem_init` ~818): module window, kernel region (`RT_KERNEL_SIZE` 256 MiB), 14 GiB low region (stacks 256 MiB, heap), 64 GiB alias region, host-function window | `memory.c` | Fixed sizes | Never released; pages decommitted on heap shrink, `UnmapPhysicalMemory`, stack unmap | +| 2 | Guest heap and alias mappings (the application pool) | `memory.c` `mem_set_heap_size`, `alias_map` | `NX2NAVI_APP_POOL`, else `rt_app_pool_bytes` (9809 MiB) + `res_heap_extra_bytes()`; heap `HEAP_CAP` 12 GiB | Committed on demand, decommitted when shrunk or unmapped | +| 3 | Kernel-region allocator (`mem_kernel_alloc`, power-of-two classes, `KCLASSES` 29, `KCOMMIT` 64 KiB steps): thread TLR and TLS, shared fonts (up to 64 MiB each), the exception page | `memory.c`, `klock` | The kernel region; "kernel region exhausted" is fatal | Free lists; never decommitted | +| 4 | Moved stacks `stack_maps[4096]`, `alias_pages` (16 MiB calloc), `module_perm[]` | `memory.c` | Fixed | Commit/decommit on map/unmap; tables never freed | +| 5 | Lazy write-back segments `lzs` | `memory.c`, `frz_lock` | None found (realloc doubling from 256) | When touched, thawed or released before a mirror eviction; the array never shrinks | +| 6 | Lazy write-back hot list `lz_hots` | `memory.c` | `LZ_HOTS` 1024 | Replacement | +| 7 | Guest thread host stacks | `kernel.c` | 256 MiB reserved each, committed on demand | At thread exit | +| 8 | `RtThread` + `Cpu` | `kernel.c` | — | When the thread has exited and its handle is closed | +| 9 | Handle table `g_h` | `kernel.c` | `MAX_H` 4096; fatal when full | Slots freed at `CloseHandle` | +| 10 | Fibers | `kernel.c` | `FIBER_MAX` 1024; 256 MiB reserve each | At `FinalizeFiber` | +| 11 | System-event table `g_se` | `kernel.c` | 512; fatal "table full" | Entries never removed (L-10) | +| 12 | Events | `kernel.c` | — | When `refs` reach 0 | +| 13 | Critical-section trace buffer | `kernel.c` | `CRIT_N` × 32 B, only with `NX2NAVI_CRIT` | Never | +| 14 | Dispatch table | `dispatch.c` | `TABLE_SPAN_LIMIT` 1 GiB reserved; host hash `HOST_CAP` 8192 | Never (sealed) | +| 15 | Host-function registry | `hostfn.c` | `MAX_HOSTFN` 4096 | Never | +| 16 | Guest `jmp_buf` table `jt` | `guest_setjmp.c` | `JMP_N` 1024; fatal beyond | Never removed (L-10) | +| 17 | Loader tables (hash, functions, PLT slots) | `loader.c` | — | Never | + +### 5.2 Files, services, settings, logs (CPU side) + +| # | What it holds | Owner | Limit | Frees | +|---|---|---|---|---| +| 18 | Log queue `Q.buf`, `Q.out` | `logq.c` | `LOGQ_CAP` 1 MiB each; a larger line is written by its own thread | Never | +| 19 | Log file | `main.c` | `LOG_CAP` 64 MiB, `LOG_KEEP` 5 rotations | Rotated at start | +| 20 | Early log buffer | `main.c` | 32 KiB static | — | +| 21 | Settings entries and strings | `cfg.c` | `CFG_MAX_KEYS` 512 | Old strings kept on every change "for readers" (L-24) | +| 22 | Per-thread settings memo `cfg_memo` | `cfg.c` | 256 per thread | Thread exit | +| 23 | RomFS index, overlays, open counters, `handles[65536]` | `romfs.c` | Fixed | Never | +| 24 | RomFS whole-image mapping | `romfs.c` | Only with `NX2NAVI_ROMFS_MMAP` | Never | +| 25 | zstd dictionaries | `ov_zstd.c` | `NDICT` 32, each ≤ `MAX_DICT` 64 MiB; beyond, the guest decoder runs | Never ("another thread may be decoding with one") | +| 26 | zstd stream contexts | `ov_zstd.c` | `NSTREAM` 16 | Kept with the slot | +| 27 | Per-thread `ZSTD_DCtx` in `host_decode` | `ov_zstd.c` | One per thread | Never | +| 28 | Save file tables `mounts`, `files`, `dirs` | `fs_host.c` | 16, 1024, 256; fatal when full | Slots at close | +| 29 | Directory listings | `fs_host.c` | — | At `CloseDirectory` | +| 30 | Copy buffers (commit, import) | `fs_host.c` | `COPY_CHUNK` 1 MiB, 2 MiB | Per call | +| 31 | Save backups on disk | `fs_host.c` `rotate_backup` | `SAVE_BACKUPS` 3 | Oldest removed | +| 32 | Save zip read and write buffers | `save_share.c`, `save_zip.c` | `MAX_ZIP` 512 MiB, members 256 MiB, 4000 files | After staging | +| 33 | Mods table | `mods.c` | `MAX_MODS` 512 | Never | +| 34 | Cheats | `cheats.c` | None found (realloc) | Never | +| 35 | IPC sessions and domain objects | `ipc.c` | — | Sessions at refs 0; domain sub-objects not freed (L-25) | +| 36 | Offline socket events | `net_offline.c` | `NEFD` 64; spare events 64 | Spare events never closed | +| 37 | System message queue | `ov_system.c` | 32; dropped silently when full | — | +| 38 | Applet message queue | `ipc.c` | 32; dropped with a log line | — | +| 39 | Detached `PlatThread` structs, per-thread timer and file events | `platform/win32.c` | One set per thread | Never (L-26) | + +### 5.3 Audio, movies, input, menu, diagnostics + +| # | What it holds | Owner | Limit | Frees | +|---|---|---|---|---| +| 40 | Audio renderer tables (voices, pools, mixes, sinks, effects) | `audren.c` `renderer_new` | Voices ≤ 1024, mixes ≤ 256, sinks ≤ 256, effects ≤ 4096; game's work buffer 0x10000 | At renderer close | +| 41 | Mixer output ring | `audio_mixer.c` | `RING_FRAMES` 16384 × 6 channels (about 340 ms); prefill `NX2NAVI_AUDIO_PREFILL_MS` 0..500, default 20 | At mixer close | +| 42 | Mixer work buffers, splitter ids (reallocated every update), limiter delay | `audio_mixer.c` | Limiter ≤ 6 × 192000 floats | At close | +| 43 | Effect delay lines, 3D reverb (made lazily on the render thread) | `audio_effects.c`, `audio_reverb.c` | Delay ≤ 20000 ms at ≤ 192 kHz | At close | +| 44 | Guest AUX/capture rings, circular sinks | `audio_mixer.c` | ≤ 16M samples (fatal above); sink ≤ 256 MiB | At close | +| 45 | Audio change events | `audio_mixer.c` | 16 slots; events past 16 never signalled or closed (L-25) | Never | +| 46 | Menu sounds | `menu_sound.c` | 7 sounds ≤ 10 s each; 8 voices | Never | +| 47 | Microphone ring | `audio_capture.c` | 2 s | `mic_close` | +| 48 | VP9 decoder buffers: `IN_BUFS` 4 × `IN_CAP` 2 MiB (guest-visible), `OUT_BUFS` 8 NV12 frames, libvpx context | `ov_movie.c` | `MAX_MOVIES` 16 decoders | `release_codec`, `destroy` | +| 49 | AV1 decoder queues: `QMAX` 8 pictures, `INMAX` 8 samples, `CQMAX` 2, 16 registered frames; per-picture Y16/U16/V16 temporaries | `ov_movie2.c` | Fixed counts | `clear_queues`, `finalize`; `Video` struct at `ov_dtor` | +| 50 | Opus decoders | `ov_media.c` | None found (one per guest object) | At Finalize | +| 51 | Input script events | `input.c` | Script file size | Never | +| 52 | Motion ring | `motion_fusion.c` | `MOTION_RING` 64 | — | +| 53 | Menu art pixels (14 arts) | `ui/menu_art.cpp` | A few MB | Never ("the pixels stay"); GPU images by `menu_art_release` | +| 54 | Menu asset dictionaries | `menu_assets.c` | 8; per asset ≤ `MAX_RAW` 512 MiB | Never | +| 55 | Menu event queue | `ui/menu.cpp` | `EV_MAX` 256 | — | +| 56 | Perf overlay history and histograms | `perfmon.c`, `perf.c` | `PERF_HISTORY` 120; fixed bins | Static | +| 57 | Profiler tables | `prof_win.c` | About 15 MB static, `SLOW_DUMPS` 64 | Static | +| 58 | Flight recorder ring and thumbnails | `flight.c`, `gfx_present.c` | `NX2NAVI_FLIGHT_FRAMES` (600, 64..7200) | Never; dump jobs freed by the dump thread | +| 59 | Camera speed copies and temporaries | `camera_speed.c` | `copies[16]`; temporaries ≤ 64 MiB | Per search | + +### 5.4 GPU memory (`vk_memory.c`, `vk_headroom.c`, `vk_gpa.c`) + +**How the VRAM budget works.** + +- *Computation.* `vram_query` sums `heapUsage` and `heapBudget` over the device-local heaps (`VK_EXT_memory_budget`; + the driver's figure counts every process). `NX2NAVI_VRAM_BUDGET_MB` caps it (`budget_cap`). + `vk_mem_vram_live` counts free space inside VMA's blocks as room. +- *Mirrors.* `mirror_create` evicts idle mirrors (`mirror_evict`) only when that frees enough. It may then exceed the + budget while mirrors total no more than half of it (`MIRROR_SHARE` 2), asking textures for the room + (`vk_texture_want_room`). Otherwise it refuses the pool, which stays imported (read over PCIe). +- *Textures.* `vk_texture.c` `sweep_pressure` runs every 8 submits. Its limit is `NX2NAVI_TEX_VRAM_PRESSURE` percent + of the budget, default 80. +- *Headroom.* The headroom thread keeps one spare block per memory type while usage plus two blocks stays at or under + 85 % of VMA's budget. It does not apply the cap (D-04). + +| # | What it holds | Owner | Limit | Frees | +|---|---|---|---|---| +| 60 | Pool records: imported host pools (`import_pool`), physical pools (dedicated device-local, `alloc_physical`, no budget check), virtual pools (page tables) | Guest threads via the back end | None found | Pool finalize; mirrored ones by `mirror_retire` on the completion thread | +| 61 | VRAM mirrors of imported pools | Replay | The budget rules above; `NX2NAVI_MEMORY` auto/import/mirror/verify; `NX2NAVI_MIRROR_EVICT` (default 1) | Pool finalize, or `mirror_evict`: idle ≥ `MIRROR_IDLE` 120 submissions, ≤ `MAXV` 64 candidates, oldest first. Deferred to fence +2 | +| 62 | Mirror retry state | Replay | `RETRY_SOON` 16, `RETRY_SUBS` 600 submissions; a pool refused for `MR_ALLOC` is never retried (L-23) | — | +| 63 | Per-submission copy lists (`pend`, `wbl`, `mregions`) | Replay | Grow only | Reset each submission | +| 64 | Flush ranges per pool | `flush_lock` | `FLUSH_RANGES` 32 | — | +| 65 | Page-generation tables (`gen_chunk`, `cgen_chunk`, 256 MiB chunks of uint32 on first stamp) | `gen_lock` | `RT_ADDRESS_LIMIT` | Shutdown | +| 66 | Shadow storage per imported pool, and base copies (one 4 KiB malloc per GPU-written page) | Shadow mode (`NX2NAVI_SHADOW`; Linux default) | None found | Pool finalize (L-24) | +| 67 | Write-back merge snapshot chunks | Replay / completion | ≥ `SNAP_CHUNK` 1 MiB; pool ≤ `SNAP_POOL_N` 128 and budget/64 clamped 64-256 MiB; `NX2NAVI_WB_MERGE_MAX` 32 MiB | At fence; the excess destroyed | +| 68 | Lazy write-back command buffer and fence | `lz_init_lock` | One | Never freed explicitly | +| 69 | Upload batches (command buffers and fences: inflight and spare) | `cmd_lock` | Grow only | Reused; shutdown | +| 70 | Staging cache | `cmd_lock` | `STG_CACHE` 24 buffers, `STG_CACHE_BYTES` 192 MiB | At batch retire | +| 71 | Staging VMA pool | — | `STG_BLOCK` 64 MiB blocks, `STG_MIN_BLOCKS` 2; requests ≤ 32 MiB | — | +| 72 | Staging arena | `cmd_lock` | `ARENA_CHUNK` 8 MiB, ≤ 2 MiB per request, `ARENA_KEEP` 8; `NX2NAVI_STG_ARENA` | At fence; shutdown | +| 73 | Deferred destruction list `dead[]` (images, views, buffers, host frees) | `cmd_lock` | None found | `vk_mem_collect` on the completion thread once the stamped value is reached | +| 74 | Headroom spare (half a VMA block per device-local type; block `NX2NAVI_VMA_BLOCK_MB` 64, 16..256) | "vk headroom" | 85 % of VMA's budget; `NX2NAVI_VRAM_HEADROOM=0` off | Replaced, dropped when there is no room or commit is low, at stop | +| 75 | Device address table (L1 65536 entries, `BLOCKS` 1024 × 16 MiB L2, `RECS` 65536, about 17 MiB) | `gpa_lock` | Fixed; when exhausted, logs and shaders read zeros | Free lists at fence +1 | +| 76 | VMA allocator (`preferredLargeHeapBlockSize` = `vk_vma_block_size`) | `vk_device.c` | — | Shutdown | + +### 5.5 Textures (`vk_texture.c`, `vk_astc.c`) + +**Texture cache eviction.** + +- *Release overflow.* `sweep_released`: `RELEASED_MAX` 2048 entries, and `NX2NAVI_TEX_RELEASED_MB` (default RAM/32 + clamped 512-2048 MiB, raised by budget/4 up to 8192). +- *Stale.* `drop_stale`: `STALE_FRAMES` 120 presents, `GPU_STALE_FRAMES` 1800. +- *Overwritten.* `drop_overwritten`: `OVERWRITTEN_KEEP` 600. +- *VRAM pressure.* `PRESSURE_STEP` 32 per check. +- *Idle.* `evict_idle`: idle for `IDLE_ROUNDS` 240 pool walks and `STALE_FRAMES`; `EVICT_STEP` 64 per check. +- *Disuse.* `evict_disused`: unsampled for `DISUSE_SECONDS` 120; retired, then destroyed after the pool sets turn over. +- *Commit pressure.* `commit_evict`: `COMMIT_EVICT_STEP` 256 MiB, `COMMIT_LOW_KEEP` 256 MiB; `NX2NAVI_TEX_PROC_COMMIT_MB` + (minimum 24576). +- *Destruction.* Deferred (`vk_defer_image`, `vk_defer_free`). + +| # | What it holds | Owner | Limit | Frees | +|---|---|---|---|---| +| 77 | Texture image cache (`by_key`, `by_addr`). Memory comes from: pool memory; a VMA sub-allocation ≤ `SUBALLOC_MAX` 32 MiB; a reused image (`NX2NAVI_TEX_REUSE`); its own allocation (dedicated ≥ `DEDICATED_MIN` 32 MiB) | `tex_lock` | No count cap; the evictions above | The evictions above | +| 78 | Disuse holds | `tex_lock` | `HOLDS_MAX` 65536; cleared when full | — | +| 79 | Image memory requirements cache `img_reqs` | `tex_lock` | None found | Shutdown | +| 80 | Reusable released images | `tex_lock` | ≤ 128 searched per create | With the release sweep | +| 81 | Texture and sampler slots | `tex_lock` | Texture slots ≤ 1<<20 (fatal beyond); sampler slots ≤ 4096 | Shutdown | +| 82 | Sampler cache | `tex_lock` | None found; keyed by parameters, DLSS LOD bias and anisotropy; `vkCreateSampler` failure is fatal (L-19) | Shutdown | +| 83 | Failed-create hold ring | `tex_lock` | 32 entries, 2 s | — | +| 84 | ASTC decode scratch | `vk_astc.c` `g_lock` | ≥ `SCRATCH_MIN` 32 MiB, `CHUNK_OUT` 16 MiB pieces; new per upload batch unless `NX2NAVI_ASTC_SCRATCH_KEEP=1` | Deferred | +| 85 | CPU ASTC worker pool | `tex_astc_mt.c` | 7 threads | Never | + +### 5.6 Replay, submission, present (`vk_exec*.c`, `vk_usnap.c`, `vk_rec.c`, `vk_sync.c`, `gfx_present.c`) + +| # | What it holds | Owner | Limit | Frees | +|---|---|---|---|---| +| 86 | Per-command-buffer resources (transient host-coherent chunks, descriptor pools, written ranges) | Replay; `spare_lock` | `CHUNK_BYTES` 4 MiB; spares `SPARE_POOLS` 64, `SPARE_CHUNKS` 16; allocation failure is fatal | `cb_done` on the completion thread | +| 87 | Image view cache (`view_of`, `views_by_image`) | Replay | No count limit | With the image (`vk_exec_image_gone`) | +| 88 | Bindless texture and sampler pool sets | Replay | `VK_POOL_SETS` 4 (fewer when the device has no room), `VK_POOL_TEX` 32768 × 16 kinds, `VK_POOL_SAMP` 4096; change log 1<<20 | Shutdown | +| 89 | Mesh indirect ring and count buffer | Replay | `MESH_RING` 4096 × 12 B, `MESH_COUNTS` 4096; wraps | Never destroyed (allocation handle discarded, L-30) | +| 90 | Fixed draw tables (stand-ins, shader table 2048×4, and others) | Replay | Fixed | Shutdown | +| 91 | BC stand-ins | `bcs_lock` | `BC_STANDINS` 64 | — | +| 92 | Held submissions (deep copies of recordings for event order) | Replay | `CONT_MAX` 36, `EVO_QUEUES` 16 | When replayed | +| 93 | Completion callbacks per GPU write, report, event | Replay | — | When run | +| 94 | Pending counter reports | Replay | `PENDING` 1<<16 | — | +| 95 | Occlusion query ring | Replay | `OQ_N` 4096; when full, draws go uncounted | — | +| 96 | Uniform snapshot arenas (with a host copy under DLSS) | `free_lock` | ≥ `ARENA_MIN` 4 MiB each; `MAX_RANGE` 64 KiB per binding; free list never trimmed | `arena_done` at fence; destroyed only at shutdown | +| 97 | Uniform snapshot GPU-write list `U.wr` | Replay | Pruned only while snapshots are on (L-29) | `wr_prune` | +| 98 | Recorder packet ring | `vk_rec.c` | `RING_WORDS` 1<<21 (16 MiB static); a packet > 1/4 ring is fatal; a full ring spins | Static | +| 99 | Command buffer slots | `sub_lock` | Grow only | Reused after their fence | +| 100 | Completion queue | `st_lock` | Grows ×2 | Entries run and drop | +| 101 | GPU timestamp pairs | `vk_sync.c` | `TS_PAIRS` 1024 | — | +| 102 | Async replay ring | `rl` | `RQ_CAP` 1024 with back-pressure | — | +| 103 | Recording-buffer pool | `nvn_api_cmd.c` | `POOL_SLOTS` 16, `POOL_MAX_BYTES` 4 MiB | Reused | +| 104 | Texture pins | `nvn_api_cmd.c` | `NVN_PIN_AGE` submissions without references | Swept | +| 105 | Recording copies and zombies | `copy_lock` | None found | Zombies once their submissions are replayed | +| 106 | Live-texture copy table | `live_lock` | One per storage address | — | +| 107 | GVA window | `nvn_api_core.c` | `GVA_MAX` 1024 ranges; full → `gva_alloc` returns 0 | — | +| 108 | Swapchain images and views, present command buffers and fences, capture buffer, thumbnails | `g_pl` | `MAX_IMAGES` 16 | On swapchain remake, release | +| 109 | HDR, FSR 1, tonemap targets and source views | `vk_hdr.c`, `vk_fsr.c`, `vk_tonemap.c` | `SLOTS` 8 source views | Remade on a size change after `vkDeviceWaitIdle` | +| 110 | DLSS features and work images, motion-vector blocks | `vk_dlaa.c` | `DLSS_SIZES` 5; a size unused for 3600 evaluations is freed; `MVM_SLOTS` 16 | — | +| 111 | Frame dump read-back buffers | `vk_dump.c` | `PNG_CAP` 2 GiB, `INFLIGHT_CAP` 3 GiB; debug only | After writing | +| 112 | GPU breadcrumbs | `vk_crumbs.c` | `GP_SLOTS` 16384 | — | +| 113 | Address bind reports | `vk_vaddr.c` | None found while `NX2NAVI_VA_REPORT` is on | Never | + +### 5.7 Pipelines and shaders (memory and disk) + +| # | What it holds | Owner | Limit | Frees | +|---|---|---|---|---| +| 114 | Pipelines and their fast-link variants, hash `pipes[4096]` chained | `P.lock` | None found; never evicted | Only at `vk_pipeline_shutdown`: `vk_program_release` is a weak no-op (D-03, L-24) | +| 115 | Pipeline layouts, set layouts, update templates | `P.lock` | None found | Shutdown | +| 116 | Stage libraries `libs[1024]` chained; failed builds cached as null | `P.lock` | None found | Shutdown (`libs_destroy`) | +| 117 | Library and pipeline job queues | `P.lock` | None found | When built; dropped at shutdown | +| 118 | `VkPipelineCache` in memory and `pipeline-cache.bin` | `cache_rw`, `save_lock` | File > `NX2NAVI_PIPELINE_CACHE_MAX_MB` (512) starts empty; in memory: the driver's, none found | Saved by "pipe autosave" at most every 20 s and at exit (waits ≤ 5 s) | +| 119 | Pipeline binaries `pipeline-binaries.bin` | `PB.lock` | `NX2NAVI_PIPE_BINARIES_MAX_MB` 1024; `NX2NAVI_PIPE_BINARIES` default 0 | — | +| 120 | `pipeline-keys.bin` and its `seen` set | `K.io` | None found (append-only) | Never | +| 121 | `compute-used.bin` | `cu_io` | `NX2NAVI_COMPUTE_KEEP` 512 | — | +| 122 | Large-compute builder's own pipeline cache | "big compute" | `NX2NAVI_BIG_BUILD_FREE_MB` 8192, `NX2NAVI_BIG_BUILD_MAX_MB` 4096 | Merged, then destroyed | +| 123 | Shader memory cache `buckets[4096]` (SPIR-V and reflection) | `cache_lock` | None found; "for the life of the process" | Only by the probe's `gfx_shader_drop_memory` | +| 124 | Use records `use_buckets`, `use_files` | `cache_lock` | `USE_FILE_BUCKETS` 65536 buckets; entries none found | Never | +| 125 | Failed shader keys | `failed_lock` | `FAILED_N` 4096; when full, failures are no longer remembered | — | +| 126 | Shader disk cache: `shader-cache/` loose files, `shader-cache.pack`, `in/*.in`, records | `gfx_shader.c`, `tools/make_game.py` | None found on size; translators listed ≤ `IDS_MAX` 8; packed once 1,000 loose translations exist (`PACK_LOOSE_MIN`) | The carry pass prunes old translators' entries; the pack is replaced by a pruned copy at the next start | +| 127 | Translation job and load queues `X.head`, `X.lhead` | `X.lock` | None found | When done | +| 128 | Translator child processes | `S.lock` | `NX2NAVI_XLATE_PROCS` (2), `MAX_SVC` 32; raised during prewarm | Kept for the process | +| 129 | GPU driver caches (per exe; NVIDIA's per-process disk cache on Linux) | The driver | Driver-defined | Driver-defined | + +That is 129 allocators, pools and caches. + +--- + +## 6. Docs and comments against the code + +The code is what the map follows. "Verified" means the item was checked directly at `ed36773d`. The others come from +the mapping passes' reading, cited, and not re-checked line by line. + +| # | Where | What it says | What the code does | Verified | +|---|---|---|---|---| +| D-01 | `docs/architecture.md` "Threads and cores"; `kernel.c` comment ~322 | Three application cores, three virtual cores | `kernel.c` `NCORE` = 6. Comment ~30: Switch 2 titles get cores 0-5. `NX2NAVI_CORES` can lower it | yes | +| D-02 | `docs/architecture.md` "Graphics" | A recorder thread makes the `vkCmd*` calls | Only where `NX2NAVI_VK_RECORDER` is on: the default is off on AMD (`vk_rec.c` ~170, `rt_knob("VK_RECORDER", amd ? "0" : "1")`) | yes | +| D-03 | `vk.h` ~687, `vk_backend.c` ~581 | `vk_pipeline.c` implements `vk_program_release` and drops a released program's pipelines | No strong definition exists; the weak no-op in `vk_backend.c` always runs | yes | +| D-04 | `vk_memory.c` ~656 | `NX2NAVI_VRAM_BUDGET_MB` caps "every decision here and in vk_texture.c" | `vk_headroom.c` `room` reads `vmaGetHeapBudgets` itself, without the cap | yes | +| D-05 | `memory.c` ~68 | "frz_lock is a leaf lock: nothing writes guest memory under it" | `lazy_drop_locked` and `lazy_piece_locked` call the back end's `lz_fetch` under it (a GPU copy into guest memory and a wait of up to 10 s); `mem_lazy_arm` calls `lz_clean` under it | yes | +| D-06 | `vk_memory.c` ~1744 vs the log line in `evict_enabled` ~1766 | The comment: "Mirrors never go to make room for textures". The log line: idle mirrors make room "for textures under VRAM pressure" | `mirror_evict` is called only from `mirror_create`. The comment is right; the log line is wrong | partly (comment read) | +| D-07 | `svc.c` ~45 `unimplemented[]` | 0x36 `SynchronizePreemptionState` is listed as never reached | `kernel.c` implements 0x36. The table is reached only after the kernel declines, so this has no effect | yes (listing) | +| D-08 | `audren.h` ~176 | "Every call but open/close/set_running runs with r->lock held" | `audio_mixer_pace_ns` (which can destroy and reopen SDL streams) and `audio_mixer_out_stats` run without the lock | no | +| D-09 | `ov_movie2.c` header ~16; `make_picture` ~311, ~333 | One host thread per decoder; make_picture runs on the decoder thread; "a frame the PC has no memory for is dropped" | A decoder thread, a convert thread and 5 stripe threads per picture; make_picture runs on the convert thread; the 8-bit fast path calls `rt_fatal` when its temporaries fail to allocate | no | +| D-10 | `input.c` ~7 | Every SDL gamepad call is made under `lock` | The pad thread makes them under `sdl_lock` only (the comment at ~401 describes the newer scheme) | no | +| D-11 | `vk_sync.c` ~54, `vk_fsr.c` ~105 | Every idle wait takes the queue lock | `vk_pipeline.c` `vk_pipeline_shutdown` (~6412) and `vk_device.c` `destroy_all` (~548) call `vkDeviceWaitIdle` without `g_ql` | no | +| D-12 | `vk_pipeline.c` `bg_main` comment ~5143 | `bg_queue` runs on the replay thread | Its callers are in `lb_compute`, which runs on `lb_worker` threads | no | +| D-13 | `vk_rec.c` ~32 | `rec_prio` is the submitting thread's priority | It is captured once at the first `rec_begin`, from whichever thread holds `sub_lock` then, and never follows `rec_claim`'s raise | no | +| D-14 | `nvn_api_cmd.c` ~417 | `copy_lock` guards every prefix | `nvn_submit_prepare_async` writes `prefix->deferred` after unlocking (~1294) | no | +| D-15 | `gfx_shader.c` ~3 | More translator processes for the boot prewarm only | If `boot_wait` detaches the prewarm, the raised limit (up to 16) stays during play until the prewarm returns | no | +| D-16 | `ui/menu_art.cpp` ~4; `gfx_window_sdl.c` ~6; `menu_sound.c` ~10 | The window thread uploads the art; the window thread makes every SDL video call; the menu thread is the only producer of sounds | The present path uploads the art; several SDL video calls run on the caller's thread; sounds are started from the window, polling and present threads | no | +| D-17 | `ov_input.c` ~2, `flight.c` ~10, `audio_mixer.c` ~786 | `input_poll` in `input_win.c`; `perf_present` in `prof_win.c`; `audio_renderer.c` | They are in `input.c`, `perf.c` and `audren.c` | no | +| D-18 | `vk_fg.cpp` ~51 | The game's queue is shared with the recorder | The recorder never submits (`vk_rec.h`: "submitting and the command pool stay on the submitting thread") | no | +| D-19 | `platform/win32.c` ~473 | The timer handle is "one handle per thread, kept until exit" | Kept until process exit: never closed when a thread exits | no | +| D-20 | Task context and docs: app pool 9809 MiB | — | `memory.c` `mem_init` adds `res_heap_extra_bytes()` (resolution settings), and `NX2NAVI_APP_POOL` overrides it | yes | + +--- + +## 7. Leads for Phase 3 + +Every lead is suspected, from reading code only, and none is a finding. Each line gives the place and the +suspicion. + +### Crashes, hangs, data loss (pass 1, 2, 8) + +- **L-01** `fs_host.c` `save_commit`, `commit_run`. A second Commit while the first runs: the first thread clears + `commit_pending` while the second waits for `tx_lock`, so `save_wait_commit` may return early and a write may land + during the second copy. +- **L-02** `fs_host.c` `file_of`, `read_directory`; `romfs.c` `handle_source`. A handle or listing is used after the + lock is dropped while another guest thread may close it. +- **L-03** `fs_host.c`. Crash and `rt_fatal` paths do not call `fs_save_flush`. Recovery is left to `save_recover` at + the next start; pass 8 should interrupt each commit step and check it. +- **L-04** `memory.c` `lazy_drop_locked`, `vk_memory.c` `lazy_fetch`. A GPU copy and a wait (10 s, then fatal) inside + the exception handler under `frz_lock` serialise every other fault, arm and thaw. It is a candidate for the AMD + crash and device-loss reports, and for hitches where lazy write-back is on. +- **L-05** `platform/win32.c` `fault_veh` → `memory.c` handlers. A fault on a thread that already holds `frz_lock` + (or `lock`, in `stack_move`'s copy) would re-take a non-recursive lock. +- **L-06** `vk_memory.c` `vk_gpu_written_back`. A `PoolRec` from `vk_mem_lookup` is used after `pool_lock` is dropped, + on the completion thread, while `vk_pool_fini` may destroy it. +- **L-07** `vk_memory.c` `mirror_create`. `mstate = MS_ON` is set after `pool_lock` is released; a concurrent finalize + may skip `mirror_forget` and leave the record in `mlist`. +- **L-08** `ov_movie.c` `find`. The movie is used after `g_lock` is released while `destroy` may free it. + `ov_movie2.c` `ov_decode` reads `v->dav1d` unlocked against `finalize`. +- **L-09** `ipc.c` `ipc_svc` 0x21/0x22. The session is used without a reference while `CloseHandle` may free it. + Domain fields are unlocked. +- **L-10** Fixed tables that are never emptied, each fatal when full, for multi-hour sessions: + - `kernel.c` `g_se[512]`: system events, bound by `ov_system.c` `ov_report_event` and `net_offline.c`; + - `guest_setjmp.c` `jt[1024]`; + - `fs_host.c` `files[1024]`. +- **L-11** Concurrency in the kernel and zstd: + - `kernel.c` `kern_fiber_finalize`, `kern_fiber_initialize`: fiber fields outside `g_fiber_lock`; + - `kernel.c` `kern_bind_system_event`: check-then-create; + - `ov_zstd.c` `stream_take`: a spent slot reused while still read. +- **L-12** `kernel.c` `thread_main`. A thread created but never started (or closed first) waits forever, keeping its + stack reservation. +- **L-13** `camera_speed.c` `search_main`. It rewrites every byte-pattern match of the camera speed array across the + guest heap and alias region, while the game runs. +- **L-14** `menu_assets.c` `dicts_load`. Lazy dictionary load without a lock, from the "menu-art" thread and the + present thread. +- **L-15** `vk_pipeline.c` `autosave_main`. It is never stopped and outlives `vk_pipeline_shutdown`; `vkDeviceWaitIdle` + at shutdown runs without `g_ql` (D-11). +- **L-16** Sync-mode races: + - `vk_backend.c` `PH`, `FH`, `vk_exec_held` and `nvn_submits_in_frame`; + - `nvn_api_core.c` `q->submits`; + - `nvn_api_cmd.c` `resolve_deferred` in place. + + All of these apply if the game submits and presents from different threads. +- **L-17** `frame_rate.c` `frame_limit_live`. It is reached from three threads without a lock, and can call + `rt_patch_switch` concurrently. +- **L-18** `cfg.c` `raw_get`. `getenv` races with `cfg_setenv` → `_putenv_s` while the game runs + (`gfx_present.c` ~90). +- **L-19** Fatal on allocation failure: staging, transient chunks, samplers (`vk_texture.c` `sampler_for`), the AV1 + picture temporaries. A texture create failure triggers no eviction, only a 2 s hold. +- **L-20** `audio_mixer.c` and `audren.c`. `rt_fatal` on game-supplied parameters during RequestUpdate, with the + renderer lock held. + +### Memory and VRAM (pass 3) + +- **L-21** `vk_texture.c` `upload` (~2411). A failed GPU ASTC decode leaves the texture marked uploaded and + uninitialised until its storage changes. This is one candidate for the white ground after long play. +- **L-22** `vk_texture.c` `evict_disused`, `retire_sweep`, `create_tex`. Retired or failed textures show stand-ins; + re-creation is limited per frame (`WANT_PER_FRAME` 32). Another candidate for white or untextured ground. +- **L-23** `vk_memory.c` `mirror_create`. A pool refused for `MR_ALLOC` is never retried and is read over PCIe for + life. A candidate for towns running slower in long play than in fresh scripted runs. +- **L-24** Growth without a limit for the life of the process: + - pipelines, libraries, layouts and the shader memory cache (`vk_program_release` is a no-op); + - the sampler cache; + - shadow-storage base copies (`vk_memory.c` `sbase_make`, one 4 KiB copy per GPU-written page, kept until the pool + is finalized; shadow mode only); + - `pipeline-keys.bin`; + - `cfg_set`'s old strings. +- **L-25** Leaked kernel objects per call: + - `audio_mixer.c` change events (beyond 16); + - `ipc.c` domain sub-objects; + - `net_offline.c` spare events. +- **L-26** `platform/win32.c`. Per-thread timer and file-event handles are never closed. One "save commit" thread per + Commit and one "camera speed" thread per search leak a few handles each. +- **L-27** `vk_headroom.c` `room`. It ignores `NX2NAVI_VRAM_BUDGET_MB`, so a capped test run is not the small card it + claims to be. +- **L-28** `memory.c` `lzs`. Lazy write-back segments grow without a cap, and every fault and arm scans them linearly. +- **L-29** `vk_usnap.c`. `U.wr` grows without pruning when `NX2NAVI_UBO_SNAPSHOT=0`. The arena free list is never + trimmed. +- **L-30** `vk_exec_draw.c` `mesh_ring_entry` (~5048). The mesh ring's allocation handle is discarded, so it is never + destroyed (shutdown only). + +### Stutter (pass 4) and performance (pass 7) + +- **L-31** `nvn_api_core.c` `nvnQueueSubmitCommands` and `nvn_api_obj.c` `nvnWindowAcquireTexture`. Every frame waits + for the GPU twice (after its first submission, and at acquire for the image's last present). At 120 fps and above, + a slow GPU frame becomes a CPU stall. This is the first suspect for the uncapped-rate spikes. +- **L-32** `vk_sync.c` `submit`. `sub_lock` is held over a whole replay; the present thread's empty submission waits + behind it, and held work may then replay on the normal-priority present thread. +- **L-33** `gfx_present.c` `flip_locked`, `create_swapchain`, `movie_overlay`, `hudless_record`. + `vkQueuePresentKHR` and `vkDeviceWaitIdle` run under `g_ql`, which `queue_submit` waits for. `vk_memory.c` + `vk_upload_flush(1)` waits for fences under `cmd_lock`. +- **L-34** Thread churn and polling: + - `ov_movie2.c` `run_stripes` creates and joins 5 threads per frame; + - `input.c` `motion_main` takes `lock` 250 times a second forever; + - `vk_rec.c` `wait_space` spins without yielding; + - `vk_pipeline.c` `vk_pipeline_compute` polls every 1 ms without a limit. +- **L-35** `vk_texture.c` `sweep_pressure`, `evict_idle`, `evict_disused`. Each scans the whole cache under `tex_lock` + on the submitting thread every 8 submits. +- **L-36** `vk_memory.c` `vk_pool_fini`. `vk_upload_flush(1)` plus `vkQueueWaitIdle` on a guest thread per finalized + pool: a hitch at area changes. +- **L-37** `boot.c` `boot_wait` → detach. A prewarm still running in play keeps up to 16 translator processes and + cpus-many worker threads busy. +- **L-38** The present path does slow work under the present lock: `ui/menu.cpp` `menu_overlay_frame` (`cfg_save`, + `vkQueueWaitIdle` for the font, art uploads); `audio_mixer.c` reverb allocation and device enumeration on the + highest-priority render thread. +- **L-39** `vk_rec.c` `rec_begin`. The recorder thread keeps the first caller's priority, not the recording thread's + highest, and may sit outside the reserved core. +- **L-40** `vk_sync.c` completion queue. It is FIFO, not ordered by fence, so one late callback holds back the frees + behind it. +- **L-41** `prof_win.c` sampler. Time-critical, suspending every thread each 1 ms. Phase 2 must not compare profiled + runs with unprofiled ones. +- **L-43** `vk_backend.c` `vk_be_present` (asynchronous replay only). The "frame bound" wait (~561-567) blocks the + game's present thread without giving its virtual core away, unlike `replay_drain` and `replay_enqueue`. + +### Logic that assumes the original rate (pass 8) + +- **L-42** Assumptions in the host code: + - `input.c` `shake_stick` uses `SHAKE_SIDE_NS`, two frames at 30 fps; + - `dynres.c` defaults its target to 16.7 ms whatever the limit; + - `resolution.c` `res_present` gives up after 1800 presents ("half a minute at 60 fps"); + - `frame_rate.c` `movie_cap_hz` forces 60 during movies; + - `mouse_direct.c` `camera_input` scales by the 30 Hz ratio. + + Already known in game code: the pick-up popup (`opickup`), the item jingle, and the ocarina recorder's four + rounding sites. + +--- + +## 8. Findings ledger + +Empty. Phase 3 fills it. + +| Field | Meaning | +|---|---| +| ID | `F--`, e.g. `F-1-03` | +| Severity | S1 crash, data loss or out-of-memory; S2 hitch, unbounded memory growth or major visual bug; S3 minor visual or UI bug; S4 optimization | +| Subsystem | A row of section 2 | +| Location | `file` `function` (~line) | +| What happens | — | +| How to trigger | — | +| Status | confirmed (reproduced, or shown by tool output, with the evidence) or suspected (code reading, with the reasoning) | +| Proposed fix | — | +| Risk of the fix | Including whether it changes gameplay timing or behaviour, the save format, or crosses subsystems (those need the user's approval first) | +| Lead | The L-number it came from, if any | +| Fix commit / verification | Phase 4: the commit, the repro, and what showed the fix works | + +| ID | Sev | Subsystem | Location | What happens | Trigger | Status | Fix | Risk | Lead | Commit / verified | +|---|---|---|---|---|---|---|---|---|---|---| + +--- + +## 9. What was reviewed + +Phase 1 read code to map it. It reviewed nothing for bugs. + +The map was made in this session by one auditor with four read-only mapping passes run in parallel: + +1. kernel, memory, save, settings and system services; +2. audio, input, movies, menu and diagnostics; +3. graphics threads and locks; +4. graphics allocators and caches. + +The auditor read the frame path directly and checked the items marked "verified" in section 6. The rest of the +mapping passes' statements are cited to file and function, not re-checked line by line. Phase 3 should treat them as +leads. + +### Read fully + +**By the auditor** +- `docs/architecture.md`, `docs/building.md`, `docs/maxwell.md`, `docs/launcher.md`, `AGENTS.md`. +- `platform/platform.h`. +- Read fully in the path's functions: + - `vk_backend.c` 40-450 and 537-636; + - `vk_sync.c` 50-190 and 370-649; + - `vk_exec.c` `walk`, `rec_claim`, `vk_exec_record` (1532-1771); + - `vk_exec_draw.c` `prepare_draw_ex` (3856-4073); + - `gfx_present.c` 300-354 and 1300-1610; + - `nvn_api_core.c` 600-697; + - `nvn_api_obj.c` 755-880. + +**By the mapping passes** +- `kernel.c`, `kernel.h`, `svc.c`, `dispatch.c`, `memory.c`, `fs_host.c`, `save_share.c`, `logq.c`, `boot.c`, + `main.c`, `hostfn.c`, `guest_setjmp.c`, `patches.c`, `platform/common.c`, `platform/win32.c` 1-1717. +- `audren.c`, `audren.h`, `audio_mixer.c`, `audio_capture.c`, `ov_audio_input.c`, `ov_movie.c`, `ov_movie2.c`, + `ov_media.c`, `movie.h`, `menu_sound.c`, `frame_rate.c`. +- `input.c`, `ov_input.c`, `input_win.c`, `input_sdl.c`, `input_ds3.c`, `input_ds3_win.c`, `motion_fusion.c`, + `mouse_direct.c`, `camera_speed.c`. +- `discord_rpc.c`, `rtss_limit.c`, `perfmon.c`, `perf.c`, `prof_win.c`, `flight.c`, `tex_astc_mt.c`, `dynres.c`, + `menu_cursor.c`. +- `gfx_window_win.c`, `gfx_window_sdl.c`, `folder_pick_win.cpp`, `ui/menu_art.cpp`, `ui/menu_win.cpp`. +- `vk_backend.c`, `vk_rec.c`, `vk_rec.h`, `vk_sync.c`, `vk_headroom.c`, `vk_vma.cpp`. +- `vk_usnap.c` (all but ~141-224). + +### Read in sections, with searches for threads, locks, allocations and caches + +**Large graphics files** +- `vk_memory.c`, `vk_texture.c`, `vk_pipeline.c`, `gfx_shader.c`. +- `vk_exec.c`, `vk_exec_draw.c`, `vk_exec_xfer.c`. +- `gfx_present.c` (1-490, 660-1676). + +**Other graphics and front-end files** +- `vk_device.c`, `vk_astc.c`, `vk_gpa.c`, `vk_vaddr.c`, `vk_dump.c`, `vk_dlaa.c`, `vk_fsr.c`, `vk_dlssg.c`, + `vk_fg.cpp`, `dlssg/nx2_sl.cpp`. +- `nvn_api_cmd.c`, `nvn_api_obj.c`, `nvn_api_core.c`, `nvn_procs.c`, `ov_gfx.c`. + +**CPU-side files** +- `ipc.c`, `romfs.c`, `cfg.c`, `ov_zstd.c` (not `decode()`), `ov_system.c`, `net_offline.c`, `amiibo.c`, `nfp.c`, + `swkbd.c`, `swkbd_win.c`, `cheats.c`, `mods.c`, `loader.c`. +- `ui/menu.cpp`, `menu_assets.c`, `resolution.c`, `ui_text.c`, `audio_effects.c`, `ui/amiibo_page.cpp`. + +### Searched only + +- `restbl.c`, `save_zip.c`, `input_map.c`, `audio_reverb.c`, `sysinfo.c`. +- `ui/perf_panel.cpp`, `ui/cheats_tab.cpp`. +- `vk_ffx.cpp`, `vk_hdr.c`, `vk_tonemap.c`, `vk_crumbs.c`. +- `nvn_tex.c`, `tex_astc.c`, `tex_deswizzle.c`. +- `platform/win32.c` 1717-2166 (load counters, child processes, IPC, restart). +- The header comment only, for the subsystem map: `vk_caps.c`, `vk_color.c`, `vk_contact.c`, `vk_mvcheck.c`, + `vk_mvprev.c`, `vk_mvsurvey.c`, `rdoc.c`, `nvn_ext.c`, `gfx_shader_guess.c`, `gfx_null.c`, `widescreen.c`. + +### Not opened + +**Runtime** +- `platform/linux.c` beyond the thread-creation search, and `platform/fp_x86.h`. +- `vk_formats.c` beyond its header, `fma.c`, `sha256.c`, `folder_pick_sdl.c`, `swkbd_sdl.c`, `input_sdl.h` and the + other headers not named above. +- `ui/binds_page.cpp`, `ui/option_rows.cpp`, `ui/menu_skin.cpp`, `ui/menu_sdl.cpp`. +- Every `*_probe.c` (probe modes only). +- `runtime/third_party/`, `runtime/shaders/`. + +**Outside the runtime** +- `recomp/` (the generator itself). +- The generated C (by the audit's rule). +- `launcher/`, `installer/`, `tools/`, beyond `tools/make_game.py` (searched for the app pool) and `tools/build_game.py` + (searched). +- `tests/`. +- `tools/xlate/` (the translator lowering and its patches). + +### Phase 3 coverage + +| Pass | Status | +|---|---| +| 1 Crashes and memory safety | not started | +| 2 Threading and races | not started | +| 3 Memory and VRAM budget | not started | +| 4 Shader stutter and hitching | not started | +| 5 Rendering correctness | not started | +| 6 UI | not started | +| 7 Performance | not started | +| 8 Edge cases | not started | + +--- + +## 10. Not determined in Phase 1 + +**Threads and synchronisation** +- Whether the game submits and presents from the same guest thread. Several sync-mode leads (L-16) depend on it. The + log line "vk: thread N is the recording thread" and a thread trace in Phase 2 would answer it. +- The threads inside the GPU driver, Streamline/NGX, FidelityFX, SDL, libvpx and dav1d, and what they touch of ours. +- What `plat_cpu_reserve` leaves for the recorder and pipeline threads on the reference PC. +- Which caller thread runs `tex_astc_mt.c`'s first large decode, `dynres_override`, `cheats_frame`, and + `rt_fatal_note_add`. + +**Version and platform** +- Which of the 1.4.3-specific files (`mouse_direct.c`, `menu_cursor.c`, `ov_zstd.c` patch set `zstd`) are active on + 1.1.1. +- Whether the Windows ASan runtime's shadow memory fits beside the fixed guest reservations, and whether TSan on Linux + accepts them (section 1). + +**Memory** +- Real sizes in play: how large the shader memory cache, the pipeline tables, the sampler cache, the lazy write-back + segment list and the deferred-destruction list get in a multi-hour session. Phase 2's memory logging per scene + should record them. +- What the front end does when `vk_pool_init` fails for a physical pool.