Merge upstream main at 40e6bae while preserving nxnative Easy features
No files matched your search
@@ -70,6 +70,7 @@ What earlier titles needed beyond sections 1–5 is filed by mechanism: textures
|
||||
| `DM_NVN_HILITE=fskey,…` | Replace those fragment shaders with solid colours: tells "geometry is wrong" from "shading is wrong" |
|
||||
| `DM_NVN_DEPTHOVR=fskey,…` | Those fragment shaders' draws compare depth with `ALWAYS`: with `HILITE`, tells "the depth test rejects every fragment" from "nothing rasterizes". With `PASSDUMP`, the `D` line shows the depth range (`dr=`) and logic op (`lo=`), and `c` lines each recorded clear (slot, serial, mask, scissor; depth / stencil values) |
|
||||
| `DM_NVN_SHDUMP=dir` | Control + code of programs missing from the shader map |
|
||||
| `DM_NVN_VSDUMP=<file>` / `DM_NVN_VSDUMP_KEY=<vertex key>` | One native post-vertex capture: a u32 header (first vertex, count, stride in words, base instance), then the 144-word geometry records. Refuses shaders with writes. With key `*`, saves position-only files by draw/key; `DM_NVN_VSDUMP_FROM` selects the first frame, `DM_NVN_VSDUMP_MAX` limits captures (default 256). These diagnostics drain GPU work |
|
||||
| `DM_NVN_ALLOCPROF=1` | Metal allocation growth per replayed op type and per function, command allocator sizes, new textures |
|
||||
| `DM_NVN_CBDUMP_KEY=vskey` (with `CBDUMP` and `PASSDUMP_ALL`) | Only that vertex shader's draws: uniform buffers plus vertex streams and index buffer (`F<frame>_p<pass>_vb<stream>_<addr>.bin`, `_ib<type>_<addr>.bin`) |
|
||||
| `DM_NVN_TEXINFO=WxH` | Storage and description (pool, offset, flags, packaged or not, storage size and how many of its bytes in guest memory are non-zero, i.e. written by the CPU) of every texture of that size on first use, each pool registration of one (`TEXINFO register`: builder channel mapping, view and view mapping), and each one as it is initialized (`TEXINFO init`, render targets included, the first 200: serial, pool and offset, target, format, flags, storage size) |
|
||||
|
||||
@@ -52,6 +52,9 @@ Per-probe suffixes, in this order:
|
||||
The `!xN` heap walk takes the layout of the game's sead version from `DM_PROBE_HEAP=<name>,<start>,<size>,<free list>,<used list>` (hex offsets in the heap object; default Mario's `0,28,30,b0,c8`, Breath of the Wild's `28,38,40,e0,f8`; find them in the heap's constructor and `tryAlloc`). `DM_PROBE_HEAPNAME=<text>` reports only hits whose heap name contains the text (resource heaps are named after their file), `DM_PROBE_HEAPWORDS=1` adds the first two words of each used block (a vtable names the object), `DM_PROBE_HEAPFRAME=<vtable offset in main>` prints heaps of that class as frame heaps (head, tail, free), and `DM_PROBE_HEAPRAW=1` dumps the heap object's first 0x130 bytes, to find another class's fields.
|
||||
|
||||
`DM_PROBE_ALL=1` adds x9–x18; `DM_PROBE_X2=<bytes>` dumps the memory x2 points to.
|
||||
`DM_PROBE_FRAMES=3000-3001` restricts observations to those presented frames
|
||||
(a single frame is also accepted); `DM_PROBE_LIMIT=20` caps printed hits per
|
||||
probe. These controls bound a late-game observation without logging startup.
|
||||
|
||||
**Who writes a word: `DM_PROBE_WATCH=<address>[+<length>]`** (hex guest address, 8 bytes by default; the bytes must lie in one 16 KB host page). The host write-protects that page as soon as it is writable and keeps it protected: the fault handler carries out every store that lands on the page through a second, writable mapping of it (STR / STUR / STP of general and SIMD registers in their immediate, pre- and post-index forms, register-offset STR, DC ZVA), so the stores of a busy page all pass through it. A store that reaches the watched bytes prints `WATCH … store of N bytes at …: old → new`, the thread, pc, lr and a backtrace. Other instructions (atomics, exclusives) get a `not emulated` line once per pc and run on an unprotected page, protected again a millisecond later. A guest object sits at the same address in every run (the guest heap is laid out the same), so take the address from a probe's register dump first. STEINS;GATE RE:BOOT's render size setting (engine object + 0x6000): the static search for stores with that offset, conditional probes on 25 candidate stores and a wrapper around the host's `memcpy` / `memset` (the guest's go straight to the host's libc, `hle_table.c`, where guest probes cannot see them) all found nothing; a watch that unprotected the page for every fault missed the write among the bursts of stores to the engine object's page; this one found it on the first run (`main+0x389e90`, a setter its start-up code calls with 0).
|
||||
|
||||
@@ -93,7 +96,7 @@ Patterns that worked: print every call of one function with its arguments; stop
|
||||
|
||||
## Scripted input and thread scheduling
|
||||
|
||||
- `DM_KEYS="<seconds>:<button>[+button][~hold],…"` presses buttons from host start (`hle_hid.c`). `<seconds>:<button>*<count>/<interval>` repeats a press, e.g. `42:A*170/2` presses A every 2 s from 42 s on, 170 times; at most 1,024 steps after expansion. Steps run in time order, so repeated groups interleave (`f17400:Left~0.3*120/500,f17480:A*120/500,f17560:Left~0.3*120/500,f17640:A*120/500` = Left, A, Left, A every 500 frames: Fire Emblem: Three Houses' confirm dialogs default to No, and this cycle confirms Yes whether a dialog is open or not when it starts; A, Left, A loops forever once it starts on an open dialog); a list that mixes seconds and frame numbers keeps its listed order. A long run through a game's story is then a single switch, such as Metroid Dread's title, new game and story recap. `LLeft` / `LUp` / `LRight` / `LDown` push the left stick fully (Super Mario 3D World moves on its map by stick only); `Shake` shakes the controller for the hold time, at least one whole shake (its six-axis sensor); combos such as `L+R~0.5` press buttons together. Scripted presses are player 1's (`dm_pad_set`), as is the keyboard. An `f` before the time counts presented frames instead of seconds (`f2500:A`, repeat intervals in frames too): with another heavy job running, seconds drift away from the game's progress and a route stops lining up; frame numbers do not. Breath of the Wild from its title menu into the autosave: `DM_KEYS=f2500:A,f2800:A,f3100:A,f3400:A` (control at frame ~3900).
|
||||
- `DM_KEYS="<seconds>:<button>[+button][~hold],…"` presses buttons from host start (`hle_hid.c`). `<seconds>:<button>*<count>/<interval>` repeats a press, e.g. `42:A*170/2` presses A every 2 s from 42 s on, 170 times; at most 1,024 steps after expansion. Steps run in time order, so repeated groups interleave (`f17400:Left~0.3*120/500,f17480:A*120/500,f17560:Left~0.3*120/500,f17640:A*120/500` = Left, A, Left, A every 500 frames: Fire Emblem: Three Houses' confirm dialogs default to No, and this cycle confirms Yes whether a dialog is open or not when it starts; A, Left, A loops forever once it starts on an open dialog); a list that mixes seconds and frame numbers keeps its listed order. A long run through a game's story is then a single switch, such as Metroid Dread's title, new game and story recap. `LLeft` / `LUp` / `LRight` / `LDown` push the left stick fully (Super Mario 3D World moves on its map by stick only); `Shake` shakes the controller for the hold time, at least one whole shake (its six-axis sensor); combos such as `L+R~0.5` press buttons together. Scripted presses are player 1's, as is the keyboard, on a path of their own that is ORed with the keyboard and player 1's controller (`script_set`): a connected controller (a DualShock 4 publishes its sticks all the time) no longer overwrites them. An `f` before the time counts presented frames instead of seconds (`f2500:A`, repeat intervals in frames too): with another heavy job running, seconds drift away from the game's progress and a route stops lining up; frame numbers do not. Breath of the Wild from its title menu into the autosave: `DM_KEYS=f2500:A,f2800:A,f3100:A,f3400:A` (control at frame ~3900).
|
||||
- `DM_CTL=<file>` (Mac) drives a running game step by step instead: the host polls the file every 100 ms and acts on each line appended to it (`hle_hid.c` `ctl_main`). A line is a press as in `DM_KEYS` (`A`, `LDown~0.2`, `L+R~0.5`), or `shot <path>` (the next presented frame as PNG), `passdump <n>` (the next n frames as `DM_NVN_PASSDUMP`), `texdump <path with %d>` (the next frame's colour targets as `DM_NVN_TEXDUMP`), `texraw <serial> <path>` (level 0 of the host texture behind a serial, raw, with a `width height format row` header), `texlist <NVN target>` (registered textures of that target, `2` = 3D, with storage address, size and block-linear layout), `memdump <address> <bytes> <path>` (guest memory to a file: a buffer texture's storage from its `B` line holds what the GPU wrote), `peek <address> <count>` (floats to the log), `cvarf` / `cvari <main offset> <value>` (an Unreal console variable's game- and render-thread copies, for a `TAutoConsoleVariable` found as for `hle_fpslock.c`), or `env KEY=VALUE` (for switches read at use). Look, press, look again: menus and loads whose timing moves between runs need no blind timed script. A shell helper that appends a line and waits for a shot to be written keeps the loop short (DRAGON QUEST VII Reimagined's work folder: `run/ctl.sh` with `wait S` / `snap NAME`, and route scripts on top of it into the opening cutscene).
|
||||
- Frame numbers are a stable way to aim dumps (`DM_NVN_PASSDUMP=<frame>`, `DM_NVN_SHOT=…`) at the same scene in another run: the per-frame NVN call counts (`[nvn] frame N: … calls`) matched between runs of the same script. Times in seconds drift with load.
|
||||
- **A route through a 3D level** (Captain Toad: Treasure Tracker's opening course, where Toad has to find his way up a diorama to the star): screenshots alone took a dozen runs and still lost Toad behind the scenery, and turning the camera mid-route made the same input land differently each run. What worked: read the player's position every frame with a probe and plan from the stage's own data. The guest heap is laid out the same in every run, so the player actor is at the same address. `DM_PROBE=main:1fb2e0@*x0+48+8` (`PlayerActor::control`, the actor's pose at +0x48, the position 8 bytes into it, as `al::getTrans` reads it) prints x, y (height), z as three floats per frame. The stage map (`StageData/<stage>.szs`, a BYML) lists every actor with its `Translate`, including the goal and the rail the guide character walks. Stick directions are camera-relative: on the default camera two directions together (`LUp+LRight`) move along one world axis. Hold against walls where possible, since a wall stops the player at the same place in every run; the route then reproduced in 5 of 5 runs.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# 06 — The developer loop: which tool when, and before a pull request
|
||||
|
||||
One page for people and agents: the tools this repository has for building, running, debugging, measuring and checking a change, when to use which, and what a pull request needs. It links to where each tool is described instead of repeating it. Game files, keys, builds and runs stay outside the repository ([AGENTS.md](../../AGENTS.md)).
|
||||
|
||||
## 1. Build and run on the Mac
|
||||
|
||||
| Step | Tool | Details |
|
||||
|---|---|---|
|
||||
| Pack a game for the Mac | `scripts/build_ipa_from_nsp.py <NSP folder> -k <prod.keys> --platform mac` | Extraction, rewriter, shader split and chain, the Mac host; prints the command that runs the game. [A Mac-only build](../development.md#a-mac-only-build-for-development) |
|
||||
| A double-clickable app | the same with `--platform mac-app` (adds the offline pipeline archive) | [A Mac app](../development.md#a-mac-app) |
|
||||
| An iPhone build | the same without `--platform` (an unsigned IPA the user sideloads) | [The packing script in detail](../development.md#the-packing-script-in-detail) |
|
||||
| Rebuild only the host | `make -C dm/host GAME=<id>` | [Build by hand (Mac)](../development.md#build-by-hand-mac) |
|
||||
| Rebuild only the shaders | `DM_GAME=<id> dm/gfx/tools/dmsh_build.py` (after a chain change: `dmsh_chain.py --header` first) | the chain procedure in [AGENTS.md](../../AGENTS.md); [04](04-graphics.md) |
|
||||
| Run with scripted input | `DM_KEYS` (button presses at fixed seconds or frames), `DM_CTL` (presses, screenshots and dumps while it runs) | [05, Scripted input](05-debugging.md#scripted-input-and-thread-scheduling) |
|
||||
|
||||
Start Mac runs with the Metal performance HUD (`DM_METAL_HUD=1`). A new title starts from the [checklist](README.md#checklist-for-a-new-title) and [03](03-bringup.md).
|
||||
|
||||
## 2. When something is wrong
|
||||
|
||||
1. Search the [symptom index](symptoms.md) for the `[RED]` line, abort text or what you see; it leads to the reference entry and the games it was found on.
|
||||
2. Crashes: the host's `CRASH sig=…` report, the macOS crash report, lldb scripts, Ghidra / objdump on the rewritten modules: [05](05-debugging.md#reading-a-crash-report).
|
||||
3. Hangs and threads: `DM_WATCH=<s>` / `DM_WATCH_STALL=<s>` dump every thread ([03, Reading the log](03-bringup.md#reading-the-log)); `DM_THR=1`, `DM_SVC=1`, `DM_FS=1` log thread SVCs, memory SVCs and file opens; `sample` / `xctrace` name the guest threads during a hitch ([05](05-debugging.md#scripted-input-and-thread-scheduling)); `DM_HLECOUNT=<file>` counts every HLE function and SVC the game called.
|
||||
4. Guest state without a debugger: `DM_PROBE` and its watch and heap variants ([05](05-debugging.md#probes-without-a-debugger-dm_probe)).
|
||||
5. Wrong picture: `DM_NVN_SHOT` screenshots, `DM_NVN_PASSDUMP` (every pass, its attachments, bindings and draw state), `DM_NVN_TEXDUMP` / `DM_NVN_TEXSNAP` (render targets as PNG), `DM_NVN_CBDUMP` (uniform buffers, vertex streams), `DM_NVN_HILITE` / `DM_NVN_DEPTHOVR` / `DM_NVN_SKIP_FS` (isolate a draw), `DM_NVN_KEYDUMP` and `DM_NVN_MAGSCAN` on the phone: [04, Diagnostic switches](04-graphics.md#7-diagnostic-switches-host) and [05, Intermittent rendering glitches](05-debugging.md#intermittent-rendering-glitches).
|
||||
6. Slow frames: the `[longframe]` lines and the hitch dump (`DM_HITCH_DUMP`), pipeline compile waits ([05](05-debugging.md#pipeline-compile-waits)), GPU time per pass (`DM_NVN_GPUPROF`, `DM_AB` A/B phases, `DM_GPU_CAPTURE` for Xcode; read with `dm/tools/gpuprof.py`): [05, GPU time on the device](05-debugging.md#gpu-time-on-the-device). Attachment traffic: `dm/gfx/tools/tbdr_*.py` over a pass dump.
|
||||
7. Memory: the host's memory line, `footprint`, `vmmap`, `DM_NVN_ALLOCPROF=1`: [05, Memory growth](05-debugging.md#memory-growth).
|
||||
8. Shaders: `dm/gfx/tools/shader_regressions.py` (shaders that built in the previous chain and fail now), `DM_NVN_SHDUMP` (programs missing from the map), the offline link check ([05](05-debugging.md#offline-pipeline-link-check)).
|
||||
9. On the iPhone: switches in `Documents/env.txt`, results in `Documents/log.txt` ([05, On-device diagnostics](05-debugging.md#on-device-diagnostics-ios)).
|
||||
10. Game updates and fixed addresses: `dm/tools/nso_fnmap.py`, `nso_syms.py`, `insn_census.py`, `sdis.py`, `nso2elf.py` ([Tools](../development.md#tools-dmtools)).
|
||||
|
||||
## 3. Measuring
|
||||
|
||||
`scripts/bench.py <id>_build --route <name>` runs a route of the profile's `bench.tsv` from a fixed save and gives frame-time percentiles, GPU time and screenshots; variants (`--variant`) compare two hosts, shader libraries or sets of switches in one invocation, which is the only comparison it supports ([A benchmark on the Mac](../development.md#a-benchmark-on-the-mac)). Power and energy per frame: [Power](../development.md#power). Pass `--lock <file>` when other agents or people measure on the same Mac: the lock is held only while a game runs, and heavy builds should wait for it.
|
||||
|
||||
## 4. Checking a change on every game: `scripts/regress.py`
|
||||
|
||||
One chain and one host serve every profile, and each contributor owns only some games (GitHub issue #272). `scripts/regress.py` compares a baseline revision (by default the merge base with `main`) with a candidate (by default this checkout, uncommitted files included) on every game the Mac has a `--platform mac` build of. Its shader count uses the rule of `shader_regressions.py`, which the NSP script applies to a game you rebuild; the harness adds the other games, the MSL, the pipeline interface and a run. Full description: [The regression harness](../development.md#the-regression-harness).
|
||||
|
||||
| Command | What runs | Cost |
|
||||
|---|---|---|
|
||||
| `regress.py chain` | Static: each revision's shader build, compared shader by shader (built or failed, MSL / reflection / variant hashes), and the vertex / pixel interface of the pipeline archive's bases (issue #283) | nothing when the chain did not change; a changed chain translates every shader again (14 min for Breath of the Wild's 19,592 on an M2 Max) |
|
||||
| `regress.py check --quick` | The above plus a short boot per game: the route's `quick` seconds, no pipeline prebuild | about a minute per game and run |
|
||||
| `regress.py check` | The whole route with the pipeline prebuild first (`DM_PSO_PREBUILD=tool`) under a watchdog | Breath of the Wild about 5 min, Tears of the Kingdom about 7 |
|
||||
| `regress.py baseline --rev main --runs 2` | Records a baseline (a missing one is recorded by `check` itself); two runs give the noise | as `check`, per run |
|
||||
|
||||
The routes are `dm/games/<id>/smoke.tsv`; a game without one takes part in the chain checks only. Add one when you own a game: a fixed save, the presses that load it, screenshots only while the player stands (where a walk ends moves from run to run), and the frame it must reach.
|
||||
|
||||
Reading the report (`report.md` beside `report.json` in `<root>/reports/`): one row per game, then the details. `PASS` and `CHANGED` (MSL or numbers moved, nothing worse) exit 0; `NEW-FAIL` (a shader, the interface check, a crash, a hang or the frame that worked in the baseline fails) and `REGRESSION` (new `[RED]` lines, a screenshot, frame time, memory or prepare time beyond the tolerances) exit 1; `ERROR` (the harness could not check a game) exits 2. Failures the baseline has too are listed as known and do not fail the check. Changed MSL is expected after a chain change: say in the pull request why it changed. A screenshot difference may be a fix: look at the run's screenshots in `<NSP folder>/<id>-work/regress/runs/` before calling it either. The report holds only numbers, hashes, shader keys and normalized `[RED]` texts, so it can be pasted as it is.
|
||||
|
||||
## 5. Which games a change can reach: usage manifests and `regress.py impact`
|
||||
|
||||
`regress.py` checks only the games on your Mac. For the others, each profile can carry a usage manifest, `dm/games/<id>/usage.tsv`, written by someone who owns the game with `python3 scripts/regress.py usage --games <id> --write`: the features of its translated shaders (stages, binding kinds, image and buffer atomics, transform feedback, layer and integer outputs, translator notes, texture types), and from one run of its smoke route the NVN functions, HLE functions and SVCs it called, its present path and its profile keys. It holds names and counts only.
|
||||
|
||||
`python3 scripts/regress.py impact <base>..<head>` (or one commit) maps a change to those names: the functions around its changed lines, followed through the host's callers to the NVN entry points and HLE functions they serve, and the rules of `dm/tools/impact_map.tsv` (a file, or functions in it, to manifest rows: `dmsh_atomic.py` to the image atomic features, the prepare-step functions of `nvn_mtl.m` to the games that run it). It lists the games that use what the change touches, those reached only through a broad rule (every game of the API), and the profiles without a manifest as unknown. It reads only git and the committed manifests, so it needs neither game files nor a harness root: the report is printed, and written as `impact.md` and `impact.json` to `--out` or, when there is one, `<root>/reports/`. Run it before a change to the chain or host, and run `regress.py check` on the games it names that you have; ask their owners for the others. A changed file no rule maps is listed: add a rule for it.
|
||||
|
||||
## 6. Continuous integration
|
||||
|
||||
`.github/workflows/tests.yml` runs on every pull request: the host's unit tests and ThreadSanitizer targets, the CPU-only graphics tests, the Python tool tests (`dm/tools/tests`, `dm/ios/tests`, `dm/gfx/tests`) including the translator's synthetic-shader tests, Mac host builds for an NVN, a Vulkan and an OpenGL profile, and the iOS source compile and smoke project ([Continuous integration](../development.md#continuous-integration)). GitHub's Macs have no Metal 4 and no game files, so these stay local: `make -C dm/gfx/tests GAME=<id> run` (the Metal graphics tests), the translator tests with `DMSH_TEST_METAL=1` / `DMSH_TEST_GPU=1`, `profile_guess.py --check`, and everything `regress.py` does.
|
||||
|
||||
## 7. Before you open a pull request
|
||||
|
||||
By what the change touches (the largest that applies):
|
||||
|
||||
- **Docs only:** `python3 -m unittest dm/tools/tests/test_docs.py`; after a change to `docs/reference`, `python3 dm/tools/docs_symptoms.py`.
|
||||
- **One game's profile or note** (`dm/games/<id>/`): the Python tool tests; a full shader build of that game after a change to its shader inputs; the game on the Mac (or the iPhone) along its route, with `scripts/bench.py` numbers when the change is about speed; the finding in the game's note ([AGENTS.md](../../AGENTS.md)).
|
||||
- **The host, the chain or the tools** (`dm/host`, `dm/gfx`, `dm/tools`, `dm/gfx/tools`, `dm/ios`, `scripts`): everything above, `make -C dm/gfx/tests GAME=<id> run`, `dmsh_chain.py --header` after a chain change, `python3 scripts/regress.py impact main..HEAD` to see which games it reaches, then `python3 scripts/regress.py check` over every local game (`--quick` while iterating, the full run before the pull request), and its `report.md` pasted into the pull request with a word on every `CHANGED`, `NEW-FAIL` or `REGRESSION`. Name the games the report covers: the others are unchecked.
|
||||
@@ -11,6 +11,7 @@ Everything here was learned on Diablo II: Resurrected (SDK ~12, NVN 53, the orig
|
||||
| [03-bringup.md](03-bringup.md) | Getting the host to boot the game: the run loop and how to read the log; the failures met so far are in the [subsystem references](../reference/README.md), found through the [symptom index](symptoms.md) |
|
||||
| [04-graphics.md](04-graphics.md) | NVN on Metal for a new title: finding every shader container, shader keys, binding layouts, bindless textures, depth mode, the diagnostic switches; what other titles needed is in the [rendering](../reference/rendering.md), [shaders](../reference/shaders.md) and [NVN objects](../reference/nvn-objects.md) references |
|
||||
| [05-debugging.md](05-debugging.md) | Recipes: crash reports, lldb scripts, Ghidra headless decompilation, objdump, memory growth, render-pass and texture dumps, on-device diagnostics, offline pipeline link checks |
|
||||
| [06-dev-loop.md](06-dev-loop.md) | The developer loop: which tool to build, run, debug, measure and check with, CI, and what a pull request needs by what it changes |
|
||||
| [symptoms.md](symptoms.md) | One line per symptom met so far (an assertion, a `[RED]` line, an abort or crash site, a visible result) → the reference entry that explains it |
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -15,6 +15,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- `jump table <addr>` assertion in `patch_nso.py`, in a Unity / IL2CPP `main`. → [Offset jump tables through x18 read at rewrite time](../reference/rewriter.md#offset-jump-tables-through-x18-read-at-rewrite-time)
|
||||
- `jump table <addr>` assertion in `patch_nso.py`, in a `subsdk`. → [Offset jump tables with the bounds check on another register](../reference/rewriter.md#offset-jump-tables-with-the-bounds-check-on-another-register)
|
||||
- `jump table <addr>` assertion where the index is a copy made before the check (`mov w0,w18`, then 41 instructions holding two other jump tables on `x0`, then `cmp w18,#4; b.hi; ldrsw x18,[x11,x0,lsl #2]; add x18,x18,x11; br x18`). → [Index copied before the bounds check](../reference/rewriter.md#index-copied-before-the-bounds-check)
|
||||
- `jump table <addr>` assertion in `patch_nso.py` on an offset table whose index is checked with a 64-bit compare (`cmp x22, #0x10; …; b.hi; adrp x18 + add x18; mov x0, x18; ldrsw x18, [x0, x22, lsl #2]; add x18, x18, x0; br x18`). → [64-bit bounds check of the index](../reference/rewriter.md#64-bit-bounds-check-of-the-index)
|
||||
- `jump table <addr>` assertion in a Unity / IL2CPP `main` whose dispatch checks a fresh mask of another register than the one it indexes with. → [Index masked in the block that branches to the dispatch](../reference/rewriter.md#index-masked-in-the-block-that-branches-to-the-dispatch)
|
||||
- `jump table <addr>` assertion on an offset table whose base was set far back (`cmp w18,#5; b.hi; ldrsw x18,[x24,x18,lsl #2]; add x18,x18,x24; br x18`). → [Offset table base set far back in the function](../reference/rewriter.md#offset-table-base-set-far-back-in-the-function)
|
||||
- `jump table <addr>` assertion on an offset table whose base was set far back and moved through other registers (`ldrb w18,[x14]; sub w18,w18,#1; cmp w18,#6; b.hi; ldrsw x18,[x7,x18,lsl #2]; add x18,x18,x7; br x18`). → [Far table base passed through other registers](../reference/rewriter.md#far-table-base-passed-through-other-registers)
|
||||
@@ -45,14 +46,18 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
## Memory and address space
|
||||
|
||||
- large resources silently skipped, later a null deref where the resource is used; also visible as `SetHeapSize` asking for much less than the pool. → [Partitioning the guest block between alias region, stacks and heap](../reference/memory.md#partitioning-the-guest-block-between-alias-region-stacks-and-heap)
|
||||
- `SetHeapSize OOM: req 0x600000 > cap 0`, followed by an SDK abort in `nninitStartup` on the first provisional boot. → [Provisional Unreal layout leaves no heap](../reference/memory.md#provisional-unreal-layout-leaves-no-heap)
|
||||
- `nnMain` returns right after `nv::InitializeGraphics`, with no error (Hyrule Warriors: Age of Calamity); `[mem] SetHeapSize … + modules … > app pool` (Monster Hunter Rise), `[mem] SetHeapSize … + modules 0x5c00000 > app pool` (Catherine: Full Body); `SetHeapSize` asks for almost the whole application pool and fails at boot (Metroid Dread: 0xc9400000, larger than the heap region); the memory pool for the game's 96 MB GPU heap has no storage (`MemoryPoolInitialize` red line), then a null read at `main+0x2eb954` (Catherine: Full Body); heap allocations fail late in a busy scene. → [Heap sized from the application pool less used memory](../reference/memory.md#heap-sized-from-the-application-pool-less-used-memory)
|
||||
- `SetHeapSize OOM: req 0x120000000 > cap 0xd0000000`, then `Break#1 reason=0` in `nninitStartup`, with a backtrace through `nn::os::SetMemoryHeapSize` (`sdk+0x967a4`). → [A heap request beyond the heap region returns ResultOutOfMemory](../reference/memory.md#a-heap-request-beyond-the-heap-region-returns-resultoutofmemory)
|
||||
- `StandardAllocator::Initialize` aborts in `nninitStartup` (Fire Emblem Engage); `nn::diag` abort in `StandardAllocator::Initialize(void *, size_t, bool)` from `nninitStartup` (`sdk+0x2f98e4` ← `main+0x5b69e0`, Big Brain Academy: Brain vs. Brain). → [Virtual address memory follows the NPDM's system resource](../reference/memory.md#virtual-address-memory-follows-the-npdms-system-resource)
|
||||
- the first `AllocateAddressRegion` fails with 0x1803; `StandardAllocator::Initialize` aborts in `nninitStartup` (an `nn::diag` abort; the NSP script's heap check reports "the host stopped before nnMain"); the alias region fills in minutes; regions a game reserves later fail (Size-aligned regions exhaust the alias address space, The movie player's own StandardAllocator). → [The SDK's address-region node area](../reference/memory.md#the-sdks-address-region-node-area)
|
||||
- a null-based write minutes into play, at the pool size minus 0x20 (Engage: a write to `NULL + 0x1fffe0`, `addr=0x1fffe0` at `main+0x9a4664`); NULL index pointers written on several job threads at once (`main+0xdb0ad4`, `0xdaea58`, `0xdadb5c`), with 32-bit mesh indices turning up inside texture objects. → [Size-aligned regions exhaust the alias address space](../reference/memory.md#size-aligned-regions-exhaust-the-alias-address-space)
|
||||
- a cut engine region still does not fit; two large regions lose up to 1 GB each to alignment, depending on where `mmap` put the guest block (Absolum); the region comes 0.69 GB past the 1 GB node area, at the next 1 GB boundary (Paper Mario: The Thousand-Year Door). → [1 GB alignment of regions of 1 GB and more](../reference/memory.md#1-gb-alignment-of-regions-of-1-gb-and-more)
|
||||
- a write at NULL + 0x3fffe0 (a 4 MB pool − 0x20) at `main+0x48b770`, about six minutes into the prologue, with Unity's `ALLOC_CACHEOBJECTS_THREAD` allocator in x19; the `[mem]` high-water mark at +4.26 GB of the 4.375 GB alias region while 1.26 GB is mapped (Sea of Stars). → [Size-class alignment doubles Unity's regions](../reference/memory.md#size-class-alignment-doubles-unitys-regions)
|
||||
- nothing aborts at startup as with Unity 2022.3, but the provisional profile gave a 1 GB block and a 256 MB alias region while Unity maps 1.5 GB in the first room. → [Unity 6 takes its memory with no StandardAllocator and no heap](../reference/memory.md#unity-6-takes-its-memory-with-no-standardallocator-and-no-heap)
|
||||
- `nn::diag` abort from `StandardAllocator::Initialize` long after `nninitStartup`, right after the first Vulkan query pool (`sdk+0x12dad0` ← `main+0x62c52c`). → [The movie player's own StandardAllocator](../reference/memory.md#the-movie-players-own-standardallocator)
|
||||
- `StandardAllocator::Initialize` aborts in `nninitStartup` (Pokémon Legends: Z-A: `main+0x1b2e690`); `nn::diag` abort at `sdk+0x11ff34` (`StandardAllocator::Initialize`) in `nninitStartup`, SDK 15.3.2, called from `main+0x31370c` (Paper Mario: The Thousand-Year Door). → [StandardAllocators over 12 to 62 GB of address space](../reference/memory.md#standardallocators-over-12-to-62-gb-of-address-space)
|
||||
- a write to `0x20000ffff` in static initialization (`main+0x164990`), after `AllocateAddressRegion` returns `0x1803`; reducing the request to 3 GB still returns `0x1803` with the SDK's original large-region alignment. With only the outer ranges reduced, a later free crashes at `main+0x1668dc` after reading compressed file data as a slab-owner pointer. → [Separate virtual ranges in Snowdrop's page allocator](../reference/memory.md#separate-virtual-ranges-in-snowdrops-page-allocator)
|
||||
- `nn::diag` abort at `sdk+0x12dad0` in `nninitStartup`, called from `main+0x756310`. → [.NET NativeAOT malloc and the GC region range](../reference/memory.md#net-nativeaot-malloc-and-the-gc-region-range)
|
||||
- the game aborts in its static initialisers, before `nnMain`, when its own 62 GB address region fails (`main+0x37c9bd0`, `AllocateAddressRegion(&p, 0xf80000000)` at `main+0x37c9a5c`). → [A 62 GB page allocator behind `nu::VirtualAllocHook`](../reference/memory.md#a-62-gb-page-allocator-behind-nuvirtualallochook)
|
||||
- the game calls `exit(1)` in `nninitStartup` (Octopath Traveler 0: `main+0x12e74`); `nn::diag::Abort` from `main+0x352fbec` (Unreal's fatal error) after the first commit, `AllocateMemoryPages(0, 2 MB)` while the RHI starts (`main+0x353ff8c`, result 0xe03) (Pikmin 4); a crash in `NumberLineAllocator::Compaction` at boot (DRAGON QUEST XI S, region option 4224). → [Unreal's engine address region and its options](../reference/memory.md#unreals-engine-address-region-and-its-options)
|
||||
@@ -80,6 +85,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- the main thread waits forever on an event right after `nnMain` starts, with only three or four threads created; `DM_THR=1` shows no `CreateThread` for the thread it waits on. → [Free thread count (`svcGetInfo` type 24)](../reference/threads.md#free-thread-count-svcgetinfo-type-24)
|
||||
- in garbage-collected titles (Unity / IL2CPP), live objects freed, with their finalizers releasing native resources. → [Pausing threads and reading their context for garbage collectors](../reference/threads.md#pausing-threads-and-reading-their-context-for-garbage-collectors)
|
||||
- crashes around movie changes in the game's own player threads, about one run in two: `SignalEvent` → `MultiWaitImpl::NotifyAndWakeupThread` on a null MultiWait at address 0x20; `WaitAny` walking freed holders; a worker reading a freed object at `main+0x225338`. The other way round, a handoff that waits too widely: a black screen at boot that never loads a texture (`packaged textures: first uploads 0`), or a load that never ends behind an animated loading screen, with the thread dump's `same-core handoffs N, total T ms` larger than the run itself. → [Same-core wake handoffs](../reference/threads.md#same-core-wake-handoffs)
|
||||
- the game freezes when a movie starts playing beside the game (a guide movie in a corner of the screen): the last frame stays, the process idles at a few percent of a core, and the thread dump shows a lower-priority job thread `running/not waiting` with its backtrace in the movie player (`ReleaseOutputBufferIndex`), the main thread waiting for it, and the movie's input thread `running/not waiting` too. A `sample` of the process shows the job thread in `md_pump` → `md_event` → `dm_yield_to_core_peers`, and the input thread in `h_movie_SendIn` → `md_pump` waiting for a pthread mutex. → [A host lock held across a same-core handoff](../reference/threads.md#a-host-lock-held-across-a-same-core-handoff)
|
||||
- several threads crash at once and the dumps interleave, then the process hangs instead of exiting. → [Simultaneous crashes of several threads](../reference/threads.md#simultaneous-crashes-of-several-threads)
|
||||
- none seen; found while investigating Fire Emblem Engage's demo crash, which it did not change (that was the address-space shortage, Size-aligned regions exhaust the alias address space). → [`SignalToAddress` type 2 with count ≤ 0](../reference/threads.md#signaltoaddress-type-2-with-count--0)
|
||||
- a 64-bit wait could return at once or block when it should not (not met in a game). → [`WaitForAddress` type 3: 64-bit wait if equal](../reference/threads.md#waitforaddress-type-3-64-bit-wait-if-equal)
|
||||
@@ -90,6 +96,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- a host helper thread aborts when signalling a destroyed `SystemEvent`; repeated `vi::GetDisplayVsyncEvent` calls double the event's tick rate. Rapid replacement can abort with `too many vsync events`, or a retired vsync worker can signal another service's event at reused storage. → [Service-event producers follow the binding lifetime](../reference/threads.md#service-event-producers-follow-the-binding-lifetime)
|
||||
- the main thread waits forever on a job's done event at boot while every worker sleeps, two of them on one event (`main+0x1426c` waiting on `Job::m_doneEvent`); 4 of 6 Mac runs. → [One auto-clear event shared by a pool's workers](../reference/threads.md#one-auto-clear-event-shared-by-a-pools-workers)
|
||||
- abort in `os::WaitSystemEvent` under `oe::PopNotificationMessage` (The Legend of Zelda: Link's Awakening); one guest thread at about 1000 ms/s on the iPhone only, the phone hot, CPU energy 5–7 W (Monster Hunter Rise's `MessageSuperviseThread`, priority 32, in the `[nvn] threads` lines). → [The oe notification event and its queue](../reference/threads.md#the-oe-notification-event-and-its-queue)
|
||||
- Paper Mario: The Thousand-Year Door 1.0.1 stays black before its first frame. The main thread stops at `main+0x32796c`, after starting `SYS_Notification`; the priority-28 notifier is in `oe::PopNotificationMessage`, while the priority-44 main thread waits for its same-core wake handoff. → [Native HLE waits release the guest core](../reference/threads.md#native-hle-waits-release-the-guest-core)
|
||||
|
||||
## Services and SDK calls
|
||||
|
||||
@@ -107,6 +114,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- `fs … not found` lines with `DM_FS=1`. → [File-not-found noise from optional paths](../reference/services.md#file-not-found-noise-from-optional-paths)
|
||||
- a missing `fs::DeleteDirectory`; undefined `nn::fs::` / `nn::account::` imports with no hook, found from the import list (Listing a game's unhooked imports before they are met) before they were met. → [Directory, file data cache and save-thumbnail calls](../reference/services.md#directory-file-data-cache-and-save-thumbnail-calls)
|
||||
- the SDK's own code opened fsp-srv and aborted, leaving the save writes uncommitted. → [`fs::Commit` with a list of mounts](../reference/services.md#fscommit-with-a-list-of-mounts)
|
||||
- a failed write in one independent save can make another save's next `Commit` fail and discard its successful changes; the damaged save may then commit its truncated working file. → [Save-write failures stay with their backing transaction](../reference/services.md#save-write-failures-stay-with-their-backing-transaction)
|
||||
- SIGSEGV at address 0 in `nn::fs::GetSaveDataSizeMax`, about 13 minutes into a new game. → [Save data size queries](../reference/services.md#save-data-size-queries)
|
||||
- each aborted in `sm::Initialize` or read a null applet proxy, one per boot. → [Cache storage and other titles' save data](../reference/services.md#cache-storage-and-other-titles-save-data)
|
||||
- the island lives in device save data, which the host did not mount. → [Device save data on the account save's directory](../reference/services.md#device-save-data-on-the-account-saves-directory)
|
||||
@@ -121,6 +129,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- SDK 20 calls that reach a service; `nfp`'s `AttachActivateEvent` / `AttachDeactivateEvent` crashed in the SDK at `sdk+0x3f2a78`. → [SDK 20 six-axis sensor and amiibo (`nfp`) calls](../reference/services.md#sdk-20-six-axis-sensor-and-amiibo-nfp-calls)
|
||||
- abort in `nn::sm::Initialize` under `nn::hid::VibrationNode::Update`. → [Vibration values flushed by the SDK's vibration graph](../reference/services.md#vibration-values-flushed-by-the-sdks-vibration-graph)
|
||||
- `DM_RUMBLE_LOG=1` shows the game's startup handle request answered with none, then every vibration value sent to handle 0 and reaching no output for the whole session, although a controller with haptics (a DualSense on the Mac) is connected. A game that rumbles would stay silent. → [Vibration handles before a controller is reported](../reference/services.md#vibration-handles-before-a-controller-is-reported)
|
||||
- with a controller connected (a DualShock 4 here), `DM_KEYS` and `DM_CTL` presses are logged (`DM_KEYS: … press …`) but the game does not react to them, or only now and then; without the controller the same script works. → [Scripted presses on a path of their own](../reference/services.md#scripted-presses-on-a-path-of-their-own)
|
||||
- taps in handheld mode do nothing, though the game reads the touch screen (the title's PRESS ANY BUTTON, the menus' cards). → [Touch gestures recognized from the touch points](../reference/services.md#touch-gestures-recognized-from-the-touch-points)
|
||||
- the first boot aborted in `hid::GetMouseState` (the SDK's mouse resource was never initialized). → [Mouse, keyboard and key code maps](../reference/services.md#mouse-keyboard-and-key-code-maps)
|
||||
- the first boot crashed in `hid::detail::SetPalmaBoostMode`, a null read at `sdk+0x238eb0` (the hid service never opened). → [Poké Ball Plus (Palma)](../reference/services.md#poké-ball-plus-palma)
|
||||
@@ -135,7 +144,8 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- SIGSEGV in `vi::SetLayerCrop`. → [vi layer and display calls without vi service objects](../reference/services.md#vi-layer-and-display-calls-without-vi-service-objects)
|
||||
- SIGSEGV in `android::Surface::getQueuedFrameNumber` under `nn::vi::GetLatestFrameNumber`. → [Layer frame numbers](../reference/services.md#layer-frame-numbers)
|
||||
- SIGSEGV in `vi::GetDefaultDisplayPanelIdentifier` (`main+0xa9a4`, before the window). → [Default display panel identifier](../reference/services.md#default-display-panel-identifier)
|
||||
- the first boot crashed in `oe::DisableAlbumImageTakenNotification`: a null read at `sdk+0x338070`, the first call in `nnMain` (`main+0x6b188c`); other `oe` calls that reach am, found in the imports before they were met. → [oe calls answered without an applet proxy](../reference/services.md#oe-calls-answered-without-an-applet-proxy)
|
||||
- the first boot crashed in `oe::DisableAlbumImageTakenNotification`: a null read at `sdk+0x338070`, the first call in `nnMain` (`main+0x6b188c`); enabling those notifications also gives a null read at `sdk+0x33b0f0` (Xenoblade Chronicles 3); other `oe` calls that reach am, found in the imports before they were met. → [oe calls answered without an applet proxy](../reference/services.md#oe-calls-answered-without-an-applet-proxy)
|
||||
- a null read at `sdk+0x36ec60` in `oe::EnableAlbumImageTakenNotification`, called from `nnMain` (`main+0x286268`). → [oe calls answered without an applet proxy](../reference/services.md#oe-calls-answered-without-an-applet-proxy)
|
||||
- abort in `nn::sm::Initialize` under `nn::oe::FinishStartupLogo`, right after the first frames. → [The startup logo](../reference/services.md#the-startup-logo)
|
||||
- null read in `oe::InitializeCopyrightFrameBuffer`, right after the window setup. → [Copyright frame buffer overlay](../reference/services.md#copyright-frame-buffer-overlay)
|
||||
- SDK 20 calls that reach a service, each one boot; SDK 20's `OpenAudioRenderer` aborts on an ARUID of 0. → [Applet resource user ID, launch parameters and VR mode](../reference/services.md#applet-resource-user-id-launch-parameters-and-vr-mode)
|
||||
@@ -197,10 +207,13 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- abort with Result 0x499 in `audio::GetAudioRendererWorkBufferSize`. → [SDK 8's longer `AudioRendererParameter`](../reference/audio.md#sdk-8s-longer-audiorendererparameter)
|
||||
- the game is silent; only the game's updates render (catch-up frames that nothing plays). The log has `audren: OpenAudioRenderer (no event)` and no `render thread started`. → [A renderer opened without an event](../reference/audio.md#a-renderer-opened-without-an-event)
|
||||
- `[RED] audren: a second renderer` right after the first open, with the same work buffer and "original device 1 mode 1". → [The SDK's open with an event calls the open without one (`t_in_open`)](../reference/audio.md#the-sdks-open-with-an-event-calls-the-open-without-one-t_in_open)
|
||||
- the frame loop stops for good as a level loads while audio keeps rendering. The main thread waits in `os::WaitSystemEvent` under `CAudioMemoryPool::Shutdown`, and a sound thread of higher priority waits on the same condition variable. → [The renderer event as the service gives it: a kernel event](../reference/audio.md#the-renderer-event-as-the-service-gives-it-a-kernel-event)
|
||||
- none of its own; command generation races the game's `Update` on SDKs whose lock is not at SDK 16's offset. → [The renderer's update lock (`update_lock`)](../reference/audio.md#the-renderers-update-lock-update_lock)
|
||||
- `FATAL audren: missing SDK symbol _ZN2nn2os12SdkMutexType4LockEv` right after the renderer's render thread starts. → [No `SdkMutexType` in SDK 3](../reference/audio.md#no-sdkmutextype-in-sdk-3)
|
||||
- `CommandListProcessor::Process` crashes now and then (SIGSEGV under `render_main`, once at 60 s and once at 134 s in the shrine); every `RequestUpdate` renders a catch-up frame. → [The render thread's route through the public `ExecuteAudioRendererRendering` (SDK 20)](../reference/audio.md#the-render-threads-route-through-the-public-executeaudiorendererrendering-sdk-20)
|
||||
- SIGSEGV at address 0x18 in `UpsamplerManager::Allocate` on the render thread. → [A renderer below 48 kHz with no upsampler on the CPU](../reference/audio.md#a-renderer-below-48-khz-with-no-upsampler-on-the-cpu)
|
||||
- the main thread spins at start-up in a resource wait (a `YieldThread` loop, no crash), right after `OpenAudioRenderer`. → [A sound engine's heap too small for its renderer and players](../reference/audio.md#a-sound-engines-heap-too-small-for-its-renderer-and-players)
|
||||
- no sound after boot; `audren: no DeviceSink (ID 0x13) in the command list` and a malformed-command error persist even when later command lists are valid. → [Incomplete startup lists must not select the command layout](../reference/audio.md#incomplete-startup-lists-must-not-select-the-command-layout)
|
||||
- the CPU processor drops the final mix. → [The final mix is taken from the DeviceSink's mix buffers](../reference/audio.md#the-final-mix-is-taken-from-the-devicesinks-mix-buffers)
|
||||
- no sound: `[RED] audren: no DeviceSink (ID 0x10) in the command list, 0 commands of 0x148 bytes`. → [SDK 4: DeviceSink is command 0xf of 0x130 bytes](../reference/audio.md#sdk-4-devicesink-is-command-0xf-of-0x130-bytes)
|
||||
- no sound; red `audren: command 1 in the command list is malformed (magic 0 size 0)`. → [SDK 6: the DSP command header layout](../reference/audio.md#sdk-6-the-dsp-command-header-layout)
|
||||
@@ -208,6 +221,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- no sound; red `audren: command 0 in the command list is malformed (first word 0x181901 size 0)`. → [SDK 21 DSP command header without a magic](../reference/audio.md#sdk-21-dsp-command-header-without-a-magic)
|
||||
- FMOD waits for buffer release after stopping an AudioOut, hanging the game during a scene transition. → [Stopping an AudioOut releases its pending buffers](../reference/audio.md#stopping-an-audioout-releases-its-pending-buffers)
|
||||
- crackling on iOS only. → [RemoteIO's default I/O buffer duration](../reference/audio.md#remoteios-default-io-buffer-duration)
|
||||
- clicks and dropouts on the Mac, heard as clipping; the log's `audio #N device callbacks` lines show hundreds of underruns per 2000 callbacks. Closing an output can also log `[RED] audio device buffer: ... (status 560947818)` (`!obj`). → [The Mac's default I/O buffer](../reference/audio.md#the-macs-default-io-buffer)
|
||||
- sound stays silent after Siri, an alarm, a call or another app's audio; a game whose audio thread waits for its buffers back (Wwise's poll-only output, FMOD) then waits forever. → [Interruptions that stop RemoteIO callbacks](../reference/audio.md#interruptions-that-stop-remoteio-callbacks)
|
||||
- combined numbers mislead: one fully fed output plus one fed by nobody look like "every other callback underruns, 2.0 blocks queued". → [Several outputs at once (`audio #N`)](../reference/audio.md#several-outputs-at-once-audio-n)
|
||||
- a null read at `sdk+0x1f2314` as the first movie starts (the title menu's background, right after the first press), in `nn::audio::SetAudioOutVolume`, called from `main+0x1c4fbc`. → [AudioOut volume kept per output](../reference/audio.md#audioout-volume-kept-per-output)
|
||||
@@ -231,12 +245,14 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- the app is active and the game keeps running, but the picture freezes while display-link callbacks stop; the log repeatedly drops queued frames. → [Present thread rescues a silent display link](../reference/frame-rate.md#present-thread-rescues-a-silent-display-link)
|
||||
- with the display link driving presents, a presented frame comes out black when three presents are on the GPU at once, or two after a dropped frame; on an M6 the same reuse also logs `[RED] display command buffer error` with `kIOGPUCommandBufferCallbackErrorPageFault`. → [A present command allocator is reused only after its own present completes](../reference/frame-rate.md#a-present-command-allocator-is-reused-only-after-its-own-present-completes)
|
||||
- "sometimes the speed increases when jumping" (Mario 1.2.1 on the iPhone): in `display` mode interval-2 frames were shown for 16.7 ms and the game visibly jumped ahead. "When pressing B, Link's move pace is speeding up" (Breath of the Wild on the iPhone); `old frames dropped by merging`. → [Frames of a dynamic present interval held on screen and at the acquire](../reference/frame-rate.md#frames-of-a-dynamic-present-interval-held-on-screen-and-at-the-acquire)
|
||||
- with `present_interval display`, `DM_PRESENT_HZ=120` and `DM_PRESENT_LINK=2` (a 120 Hz display link) a game stays at 60 fps although its frame takes 8 ms; the display link logs about two ticks per present. With a requested 90 Hz, truncating the tick conversion instead allows 120 fps (reproduced through the real acquire with simulated link ticks). → [One frame per tick of a 120 Hz display link](../reference/frame-rate.md#one-frame-per-tick-of-a-120-hz-display-link)
|
||||
- frames miss the screen while the GPU stays at its lowest clock (444 MHz on an M2 Max); more work (a higher render scale) only makes the frames slower. → [Present snapshots: the game's swapchain texture free at once](../reference/frame-rate.md#present-snapshots-the-games-swapchain-texture-free-at-once)
|
||||
- a steady game whose frame-rate counter wanders between 50 and 60 on the iPhone only; the `display link up to tick` lines show about as many empty ticks as frames dropped by merging: frames arrive two in one tick and none in the next. → [Vulkan acquire and vsync event follow the display link](../reference/frame-rate.md#vulkan-acquire-and-vsync-event-follow-the-display-link)
|
||||
- asked for 60 Hz, the system fires the display link on every third refresh of a 165 Hz screen, 55 Hz, so games paced by it ran at 55 fps (27.5 at 30). → [Virtual 60 Hz ticks on screens that are not a multiple of 60](../reference/frame-rate.md#virtual-60-hz-ticks-on-screens-that-are-not-a-multiple-of-60)
|
||||
- virtual ticks keep the average, but a frame still stays up two or three refreshes in turn (13.9 / 20.8 ms at 144 Hz), and Apple's HUD showed the frame interval jumping between them. → [Fullscreen switches the screen to a rate the frame rate divides](../reference/frame-rate.md#fullscreen-switches-the-screen-to-a-rate-the-frame-rate-divides)
|
||||
- autorelease work takes a measurable share of replay time, with temporary arena-buffer ownership traffic a few times per draw. → [Arena buffer ownership on allocation](../reference/frame-rate.md#arena-buffer-ownership-on-allocation)
|
||||
- the submit thread waits on the texture / sampler registry lock while a lower-priority registration thread is preempted holding it. → [Registry lock priority inheritance](../reference/frame-rate.md#registry-lock-priority-inheritance)
|
||||
- in a replay profile, `_platform_memcmp` is most of the time spent in serial lookups (`tex_mtl` through `g_by_serial`). → [Hash-table lookups of 8-byte keys](../reference/frame-rate.md#hash-table-lookups-of-8-byte-keys)
|
||||
- the GPU sits idle each frame while the host translates the submission (a game that does not submit frame N+1 until frame N's GPU work is done). → [Submissions handed to the GPU in segments](../reference/frame-rate.md#submissions-handed-to-the-gpu-in-segments)
|
||||
- the submit thread's CPU samples dominated by `mtl_addr` / `mtl_gpu_addr` (vertex buffers in `draw`, uniform buffers in `encode_args`). → [Address to memory pool lookups by binary search](../reference/frame-rate.md#address-to-memory-pool-lookups-by-binary-search)
|
||||
- stalls of 30–60 ms about once a second, at 30 fps and at 60 (99th percentile 63–127 ms); the main thread waits 20–34 ms for the render thread once a second. → [Time-constraint computation declared below the thread's peaks](../reference/frame-rate.md#time-constraint-computation-declared-below-the-threads-peaks)
|
||||
@@ -250,6 +266,8 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- `[RED] memory pool … created at retry N`. → [`newBufferWithBytesNoCopy` retried under memory pressure](../reference/nvn-objects.md#newbufferwithbytesnocopy-retried-under-memory-pressure)
|
||||
- a buffer is created with a null pool right after a scene change. → [Virtual memory pools mapped onto one physical pool](../reference/nvn-objects.md#virtual-memory-pools-mapped-onto-one-physical-pool)
|
||||
- `[RED] MapVirtual: non-contiguous mapping (true sparse) not implemented`. → [Sparse virtual pools built from separate physical pools](../reference/nvn-objects.md#sparse-virtual-pools-built-from-separate-physical-pools)
|
||||
- `[RED] MapVirtual: non-contiguous mapping (true sparse) not implemented`, on small virtual pools; streamed textures black or missing (OCTOPATH TRAVELER's village ground and plants). → [Virtual pools of one texture each, mapped before and after the texture](../reference/nvn-objects.md#virtual-pools-of-one-texture-each-mapped-before-and-after-the-texture)
|
||||
- `[RED] NVN unimplemented function called: nvnTextureBuilderGetSparseTileLayout`, then an out-of-memory abort in Unreal's allocator (`Failed to allocate 1879113728 bytes from Standard Allocator`, on `DM_DBG=1`). → [The sparse tile layout of a texture](../reference/nvn-objects.md#the-sparse-tile-layout-of-a-texture)
|
||||
- `CRASH sig=10` in `memmove` from `pkg_upload` → `nvn_deswizzle`, at an address 32 GB into a sparse virtual pool; textures with unaliased tails came out magenta. → [Textures read through a 4 KB page table of a sparse pool](../reference/nvn-objects.md#textures-read-through-a-4-kb-page-table-of-a-sparse-pool)
|
||||
- GPU memory grows with every scene change (Engage's idle title demo reached 5.4 GB of GPU memory and 3.6 GB resident in 850 s). → [Finalized pools kept alive by residency and the sharing table](../reference/nvn-objects.md#finalized-pools-kept-alive-by-residency-and-the-sharing-table)
|
||||
- the memory line's `freed but alive` count only grows, while no host table holds those buffers; "GPU allocated" is far above resident memory (Ghost Trick: Phantom Detective, 125 to 146 buffers and 6.6 to 8.7 GB of GPU allocations against 2.2 GB resident). → [Finalized pool buffers kept by a guest thread's autorelease pool](../reference/nvn-objects.md#finalized-pool-buffers-kept-by-a-guest-threads-autorelease-pool)
|
||||
@@ -271,6 +289,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- after a few minutes of play the main thread spends whole frames in `WriteTexels` (Metroid Dread's frames stopped at about 175 s). → [`WriteTexels` regions written in place by block-linear address](../reference/nvn-objects.md#writetexels-regions-written-in-place-by-block-linear-address)
|
||||
- black screen with every geometry right (`DM_NVN_HILITE` shows each sprite and glyph in place); the uniform buffer right (a `CBDUMP`), so every texture read 0. → [A pool's storage at creation is its textures' content](../reference/nvn-objects.md#a-pools-storage-at-creation-is-its-textures-content)
|
||||
- a texture the game filled with `memcpy` and flushed reads as zeros (BotW's sky brownish grey, the distant haze brown). → [First-use fill of storage the game flushed](../reference/nvn-objects.md#first-use-fill-of-storage-the-game-flushed)
|
||||
- magenta (ASTC's error colour) where UI images belong: a title logo and menu panels, while the 3D scene and the text draw; `DM_NVN_ZBLOCK=1` reports nothing (the textures are not uploaded on the CPU at all); `DM_NVN_TEXINFO=WxH` shows storage holding data at first use (`non-zero …`) in a pool with flags 0x22 (`CPU_UNCACHED | GPU_CACHED`). → [Storage of a CPU-uncached pool at first use](../reference/nvn-objects.md#storage-of-a-cpu-uncached-pool-at-first-use)
|
||||
- a movie plays black (Age of Calamity: its intro movie under the "+ Skip" prompt). → [Mapped-range flushes taken as uploads (`tex_flush_upload`)](../reference/nvn-objects.md#mapped-range-flushes-taken-as-uploads-tex_flush_upload)
|
||||
- magenta surfaces (Metal decodes an all-zero ASTC block as the error colour (1, 0, 1)); a zero BC block decodes black, which looks like missing light; black screen while the UI (or a dialog box) draws in the right places under `HILITE` or in the pass dump; `packaged textures: first uploads 0`. → [CPU writes with no flush, caught by page protection (`tex_dirty_upload`)](../reference/nvn-objects.md#cpu-writes-with-no-flush-caught-by-page-protection-tex_dirty_upload)
|
||||
- a texture the game builds from CPU-filled textures read back with `nvnTextureReadTexels` comes out zero. → [`ReadTexels` from guest memory and textures written after first use](../reference/nvn-objects.md#readtexels-from-guest-memory-and-textures-written-after-first-use)
|
||||
@@ -288,6 +307,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- an abort in device setup when `nvnTexturePoolGetSize` / `nvnSamplerPoolGetSize` returned 0. → [Pool size getters, texture address and command memory size](../reference/nvn-objects.md#pool-size-getters-texture-address-and-command-memory-size)
|
||||
- red stub lines for NVN calls. → [NVN calls that were red stubs](../reference/nvn-objects.md#nvn-calls-that-were-red-stubs)
|
||||
- `nvnWindowGetPresentInterval`, `nvnCommandBufferSetCopyImageStride` missing. → [SDK 8 window and copy-stride entry points](../reference/nvn-objects.md#sdk-8-window-and-copy-stride-entry-points)
|
||||
- `[RED] texture bound to an image slot lacks the shader-write usage`, and compute image writes go to a dummy texture. → [Image bindings supply shader-write intent](../reference/nvn-objects.md#image-bindings-supply-shader-write-intent)
|
||||
|
||||
## Rendering
|
||||
|
||||
@@ -304,11 +324,16 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- `texture type mismatch … declares slot 10 as Metal type 5, texture … is type 3` (undefined reads); the later `[RED] texture type mismatch: shader declares a cube, texture is 2D` could also report an unused conditional input as an error. → [A 2D texture in a cube slot binds the zero cube](../reference/rendering.md#a-2d-texture-in-a-cube-slot-binds-the-zero-cube)
|
||||
- translucent UI (frosted text bands, button guides, dialog headers) yellow and white. → [Integer views for images a shader declares uint or int](../reference/rendering.md#integer-views-for-images-a-shader-declares-uint-or-int)
|
||||
- `texture type mismatch: shader declares 2, texture is 9`; the slot read 0. → [Buffer textures read by 1D integer fetches (row wrap)](../reference/rendering.md#buffer-textures-read-by-1d-integer-fetches-row-wrap)
|
||||
- particle effects drawn at the wrong scale or place, or flickering, where the particle data is a buffer texture at a pool offset that is not a multiple of 16; log line `buffer texture at pool offset …, below Metal's 16-byte alignment`. → [Buffer textures at offsets below 16 bytes](../reference/rendering.md#buffer-textures-at-offsets-below-16-bytes)
|
||||
- torn digits and a background in blue steps (Rhythm Heaven Groove's latency calibration). → [Automatic block height truncates](../reference/rendering.md#automatic-block-height-truncates)
|
||||
- magenta (the ASTC error colour) on streamed textures in a sparse pool; `DM_NVN_ZBLOCK` finds such textures (all-zero blocks). → [Sparse textures laid out in 64 KB tiles](../reference/rendering.md#sparse-textures-laid-out-in-64-kb-tiles)
|
||||
- a CPU-filled texture shuffled into a patchwork of rectangles (OCTOPATH TRAVELER's world map on the traveler selection). → [Sparse textures take the block height of their 64 KB tile](../reference/rendering.md#sparse-textures-take-the-block-height-of-their-64-kb-tile)
|
||||
- streamed textures read 0 (magenta on Monster Hunter Rise's title screen's clothes). → [Sparse textures bound from their first mapped level](../reference/rendering.md#sparse-textures-bound-from-their-first-mapped-level)
|
||||
- the process ends at once (the window vanishes), with `-[MTLTextureDescriptorInternal validateWithDevice:]` failing an assertion such as `requests 9 mipmap levels, but the dimensions (1, 1, 1) can only support 1`, or `sampleCount must be > 1 for multisample textures`. → [Texture fields Metal refuses](../reference/rendering.md#texture-fields-metal-refuses)
|
||||
- ≈13 MB/frame leak. → [Transient render targets shared by storage](../reference/rendering.md#transient-render-targets-shared-by-storage)
|
||||
- a model has correct geometry, base colour and normals but appears as a black silhouette; deferred lighting's material-ID depth texture stays zero. → [IMAGE usage does not split texture storage](../reference/rendering.md#image-usage-does-not-split-texture-storage)
|
||||
- GPU memory grows with every image shown; the memory line's `shared textures` count keeps rising. → [Shared-storage entries taken over by a new texture](../reference/rendering.md#shared-storage-entries-taken-over-by-a-new-texture)
|
||||
- GPU memory grows as resolution changes or rooms load, although almost every retained texture still has a live NVN descriptor; reclaiming unused sharing-table entries cannot recover it. → [Live resolution variants share physical image storage](../reference/rendering.md#live-resolution-variants-share-physical-image-storage)
|
||||
- the whole picture came out darker and more saturated on the Mac and the iPhone: comparing with a Switch screenshot, every channel at every level equalled the Switch value decoded from sRGB once more (0.55 → 0.26, 0.75 → 0.52). → [Render-target views that change the format](../reference/rendering.md#render-target-views-that-change-the-format)
|
||||
- a clear of, or a format-converting copy into, a texture made without render-target use is declined. → [Render-target use added on demand](../reference/rendering.md#render-target-use-added-on-demand)
|
||||
- `render target is a compressed-format texture …`; the pass's draws did nothing (the attachment was left out), or the pipeline-descriptor validation aborts the process. → [Compressed textures as render targets through an uncompressed shadow](../reference/rendering.md#compressed-textures-as-render-targets-through-an-uncompressed-shadow)
|
||||
@@ -337,13 +362,16 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- `Vertex attribute m_36(5) of type uint4 cannot be read using MTLAttributeFormatHalf2` (draws dropped: black silhouettes); UVs collapse onto one texel (flat tan characters); vertices far out of view (negative 16-bit values widened with zeros). → [CPU conversion of vertex attributes the input type cannot take](../reference/rendering.md#cpu-conversion-of-vertex-attributes-the-input-type-cannot-take)
|
||||
- `vertex attribute format … not mapped` (printed once). → [Vertex formats Metal lacks](../reference/rendering.md#vertex-formats-metal-lacks)
|
||||
- the assertion `buffer index (31) must be < 31`. → [Vertex streams 14 and 15 at Metal slots 14 and 15](../reference/rendering.md#vertex-streams-14-and-15-at-metal-slots-14-and-15)
|
||||
- Scaleform fills come out as solid white or light grey sheets over the whole screen, hard-edged triangles and bands, and the menu text is missing (a language list or a title screen that is all white behind its button prompt). → [Vertex streams at addresses that are not 4-byte aligned](../reference/rendering.md#vertex-streams-at-addresses-that-are-not-4-byte-aligned)
|
||||
- scenes and characters black, only foliage, light shafts and the HUD drawn; pass dumps full of `Z … draw skipped: no program bound`. → [Programs bound per stage](../reference/rendering.md#programs-bound-per-stage)
|
||||
- `[RED] failed to create render pipeline (vertex 790573a52b9c5053_b fragment f39fb42850f50ecd_b): Fragment input(s) user(locn0),user(locn1) … not written by vertex shader`, once on entering the first investigation; `texture type mismatch: shader declares a cube, texture is 2D` (a pixel shader run with another pair's bindings). → [Composite program taken again after a replay state restore](../reference/rendering.md#composite-program-taken-again-after-a-replay-state-restore)
|
||||
- `no program bound before the draw`, `program has no vertex shader` (a draw after a dispatch is dropped); `the dispatched program has no compute shader` (a dispatch after a graphics bind). → [Compute programs bound apart from graphics programs](../reference/rendering.md#compute-programs-bound-apart-from-graphics-programs)
|
||||
- a red line: the 8-bit index buffer had GPU-side writes in the submission (indices read stale). → [8-bit indices widened on the GPU after a write in the same submission](../reference/rendering.md#8-bit-indices-widened-on-the-gpu-after-a-write-in-the-same-submission)
|
||||
- indirect draws whose record the GPU writes earlier in the frame cannot be read on the CPU at replay; GPU-driven effects drawn as indirect quads, which Metal has no primitive for. → [Indirect draws and dispatches read by the GPU](../reference/rendering.md#indirect-draws-and-dispatches-read-by-the-gpu)
|
||||
- an empty G-buffer pass (the title screen showed only the UI); later, the submitting thread waiting for the GPU before each such draw (36–54 fps in Kamura Village on an iPhone). → [Indirect-count multi-draws](../reference/rendering.md#indirect-count-multi-draws)
|
||||
- characters collapsed into one thin spike (vertex streams read from feedback buffers that were never written); a pass dump shows a pass of draws with a vertex shader and no fragment shader (`fs=-`) before the pass that reads them. → [Transform feedback captured by a vertex entry point](../reference/rendering.md#transform-feedback-captured-by-a-vertex-entry-point)
|
||||
- the HUD and sky render but the battle's characters and terrain are absent; the replay logs `draw skipped: rasterizer discard` between model draws. → [Rasterizer discard preserves vertex storage writes](../reference/rendering.md#rasterizer-discard-preserves-vertex-storage-writes)
|
||||
- exploding meshes and displaced model parts after vertex-only work; native post-vertex captures can contain valid positions for some meshes while other palettes are incorrect. → [Primitive barriers order vertex storage writes](../reference/rendering.md#primitive-barriers-order-vertex-storage-writes)
|
||||
- characters collapsed into one thin spike (vertex streams read from feedback buffers that were never written); a character covered by a solid box while it moves (its fur shells pulled toward lagged positions that were never written); a pass dump shows draws with a vertex shader and no fragment shader (`fs=-`) before the draws that read them, and `Y tf begin … written 0/0/0/0` with no `T … tf capture` line. → [Transform feedback captured by a vertex entry point](../reference/rendering.md#transform-feedback-captured-by-a-vertex-entry-point)
|
||||
- `SIGSEGV` in `-[AGXG18GFamilyComputeContext_mtlnext performEndEncoding]` (from `rp_begin_ex_`, or in its `dealloc` on the encoding thread); Metal validation (`MTL_DEBUG_LAYER=1 MTL_DEBUG_LAYER_ERROR_MODE=nslog`) names it: "Command buffer already has an open command encoder". → [Helper encoders end the open copy encoder first](../reference/rendering.md#helper-encoders-end-the-open-copy-encoder-first)
|
||||
- the whole picture upside down (Absolum's title logo and "Press Any Button" mirrored top to bottom); a red-reporting stub for `nvnCommandBufferSetViewportSwizzles`. → [Viewport swizzles applied as a flipped viewport](../reference/rendering.md#viewport-swizzles-applied-as-a-flipped-viewport)
|
||||
- tall blue / textured panels that flickered frame to frame: the fountain's vertex shader pushed the water surface to the top of the screen. → [Metal 4 does not order operations inside one encoder](../reference/rendering.md#metal-4-does-not-order-operations-inside-one-encoder)
|
||||
@@ -368,6 +396,7 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- an assert (a stripped trap, `main+0x4d26e0`) when the surface does not list `preTransform` INHERIT. → [Swapchain pre-transform and layer crop](../reference/rendering.md#swapchain-pre-transform-and-layer-crop)
|
||||
- GX points and lines drawn as one-pixel points and lines; a red line for the sampler LOD bias. → [Not translated in the Vulkan layer: geometry shaders and LOD bias](../reference/rendering.md#not-translated-in-the-vulkan-layer-geometry-shaders-and-lod-bias)
|
||||
- `UNIMPL eglGetDisplay`, then `Break` right after `nv::InitializeGraphics` and `vi::OpenDefaultDisplay` (`main+0x5ed9d0`). → [Games that render with desktop OpenGL through EGL](../reference/rendering.md#games-that-render-with-desktop-opengl-through-egl)
|
||||
- `UNIMPL glCreateShader` returns 0, followed by `UNIMPL glShaderSource`, `glCompileShader`, `glGetShaderiv` and `glGetShaderInfoLog`; the game crashes in `sdk` with a null shader handle even though EGL initialized. → [Direct GL imports use the same implementations as the loader](../reference/rendering.md#direct-gl-imports-use-the-same-implementations-as-the-loader)
|
||||
- the NSP script's provisional profile said Vulkan, and the capture boot crashed in the GL layer. → [Telling a GL game from an NVN or Vulkan one](../reference/rendering.md#telling-a-gl-game-from-an-nvn-or-vulkan-one)
|
||||
- the NSP script took the game for an OpenGL one (`main imports EGL and names glShaderSource`), the GL shader capture found no shader, and the capture boot stopped with `[nvn-mtl] FATAL cannot read the shader map` from `h_nvnQueueSubmitCommands` called in an NRO (`nro8+0xc10d0`). → [NVN in NROs under an SDL main that names GL](../reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl)
|
||||
- a jump to address 0 right after `glGetFloatv(0xb12)` (`lr=main+0x2d58c4`). → [Which GL functions a game calls](../reference/rendering.md#which-gl-functions-a-game-calls)
|
||||
@@ -376,24 +405,29 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
## Shaders
|
||||
|
||||
- `shader_split.py` finds 0 shaders in a RomFS of zstd files named `.zst`. → [Zstd files recognised by their magic](../reference/shaders.md#zstd-files-recognised-by-their-magic)
|
||||
- the game asks for shaders the map does not have (`[RED] shader ... is not in the shader map`, hundreds of lines), and the split finds far fewer shaders than the game uses (121 against 27,288 on Kirby and the Forgotten Land). → [HAL's `.cmp` containers: a size-prefixed frame](../reference/shaders.md#hals-cmp-containers-a-size-prefixed-frame)
|
||||
- a provisional split finds only a fraction of the shaders (3,126 of Pokémon Sword's 41,516). → [BNSH entries of GFPAK archives](../reference/shaders.md#bnsh-entries-of-gfpak-archives)
|
||||
- the split found no shader in the game (0 shaders). → [GLSLC outputs holding several stages](../reference/shaders.md#glslc-outputs-holding-several-stages)
|
||||
- an NVN game's split finds none of its shipped shaders, and the ones it compiles at run time were not captured: the game's renderer is in NROs that call `glslcCompile`, and its whole RomFS is one zip. → [GLSLC outputs in a zip archive, and those NROs compile at run time](../reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time)
|
||||
- 24 shaders before the control-length fix, 1,355 after. → [SPKG packages in KTGL LINKDATA](../reference/shaders.md#spkg-packages-in-ktgl-linkdata)
|
||||
- the split finds no shaders in a RomFS made of `.sdfdata`, `.sdftoc` and `.sdfver` files. → [Shader records in Snowdrop SDF data slices](../reference/shaders.md#shader-records-in-snowdrop-sdf-data-slices)
|
||||
- `[RED] ... PKZ version 0x10000: only 0x20000 is known` stops the shader split. → [PKZ version 1 archives](../reference/shaders.md#pkz-version-1-archives)
|
||||
- the split finds zero shaders and refuses the shader build, although the CPK's DAT archives contain the objects passed to NVN at boot. → [Size-delimited PlatinumGames shader objects](../reference/shaders.md#size-delimited-platinumgames-shader-objects)
|
||||
- archive shaders are absent from the map; the two boot shaders in `main` are reported as raw controls with no identified code area. → [Monolith ARH1, XBC1 and HCPS/SLCT shader containers](../reference/shaders.md#monolith-arh1-xbc1-and-hcpsslct-shader-containers)
|
||||
- the split finds no shaders, although `main` contains NVN control sections; it reports that no data record or code-address pair identifies their code areas. → [Needle shader containers in main and GFD archives](../reference/shaders.md#needle-shader-containers-in-main-and-gfd-archives)
|
||||
- shaders the game draws are missing from the map (`DM_NVN_SHDUMP`), in no RomFS file and no module, stored or LZ4, and not compiled at run time. → [Shaders in a zlib stream in main (YEBIS .tfxo)](../reference/shaders.md#shaders-in-a-zlib-stream-in-main-yebis-tfxo)
|
||||
- two `[RED] shader … is not in the shader map` lines and `program has no vertex shader` as the opening movie starts; every movie drew white. → [Loose control sections and code areas in main (Bink 2)](../reference/shaders.md#loose-control-sections-and-code-areas-in-main-bink-2)
|
||||
- the provisional split finds only `main`'s 294 GLSLC outputs; the 3D scene and the title logo are magenta, with `[RED] shader … is not in the shader map` lines. → [SNVN program records in Retro Studios' RFRM forms](../reference/shaders.md#snvn-program-records-in-retro-studios-rfrm-forms)
|
||||
- the split stopped with `no bundle has a Shader type tree of the same hash to borrow`. → [Unity Shader layouts written out for players without bundles](../reference/shaders.md#unity-shader-layouts-written-out-for-players-without-bundles)
|
||||
- the 3D battle scene was black behind a correct UI, with `[RED] shader 854a8afc9d07f88d is not in the shader map` and `the dispatched program has no compute shader`. → [Unity ComputeShader objects](../reference/shaders.md#unity-computeshader-objects)
|
||||
- black, jagged holes in the ground of a map (whole tiles of the terrain mesh missing while others draw), with about 25 distinct `[RED] shader … is not in the shader map` keys and `program has no vertex shader`, on a Unity game whose `Shader` objects all split cleanly. → [GLSLC outputs in a Unity TextAsset](../reference/shaders.md#glslc-outputs-in-a-unity-textasset)
|
||||
- `the RomFS has Unreal paks, but none … holds a shader code archive`, then `the shader split found no shaders in main or the RomFS`. → [Unreal Engine 4.27 shaders inline in cooked packages (LZ4)](../reference/shaders.md#unreal-engine-427-shaders-inline-in-cooked-packages-lz4)
|
||||
- shaders reported missing at random, and their dumped control sections are garbage (Unity). → [Program keys computed inside `nvnProgramSetShaders`](../reference/shaders.md#program-keys-computed-inside-nvnprogramsetshaders)
|
||||
- 12 compute shaders (particles, post effects) translated into an endless loop that hung the GPU at the first gameplay scene; SPIRV-Cross failed on 7 shaders instead of 2 (SPIRV-Cross parse failures). → [Exact program start for the decoder](../reference/shaders.md#exact-program-start-for-the-decoder)
|
||||
- GLSLC outputs fail the reader's check and are dropped (44 of DARK SOULS: REMASTERED's `main`'s 72 outputs and most of its archives' pixel shaders). → [GLSLC outputs read through their section headers](../reference/shaders.md#glslc-outputs-read-through-their-section-headers)
|
||||
- `shader_split.py` finds no stage for vertex shaders and stops at the first captured output (`no GLSLC output`). Translated, every post pass and the window draw one texel's colour (a black screen with sound); every varying at (0, 0, 0, 1). → [VertexA and VertexB parts merged into one vertex shader](../reference/shaders.md#vertexa-and-vertexb-parts-merged-into-one-vertex-shader)
|
||||
- SPIRV-Cross can't parse the SPIR-V (`nullptr` exception). → [SPIRV-Cross parse failures](../reference/shaders.md#spirv-cross-parse-failures)
|
||||
- `Fragment input(s) user(locnN) mismatching vertex shader output type(s) or not written by vertex shader` and the draw is skipped; the host reports `[RED] failed to create render pipeline (vertex … fragment …)`. → [Pixel shader inputs the vertex shader does not write](../reference/shaders.md#pixel-shader-inputs-the-vertex-shader-does-not-write)
|
||||
- `Fragment input(s) user(locnN) mismatching vertex shader output type(s) or not written by vertex shader` and the draw is skipped; the host reports `[RED] failed to create render pipeline (vertex … fragment …)`. Offline packing fails with `pso_offline: metal-tt failed` for the same reason. → [Pixel shader inputs the vertex shader does not write](../reference/shaders.md#pixel-shader-inputs-the-vertex-shader-does-not-write)
|
||||
- outdoors dim and nothing casts a sun shadow (every shadow-map lookup fails). → [Unwritten components of a written output start as (0, 0, 0, 1)](../reference/shaders.md#unwritten-components-of-a-written-output-start-as-0-0-0-1)
|
||||
- the ground kept stale 32×32 blocks where the G-buffer draw failed the depth test: blocks of a stale image where a depth pre-pass and a later pass with other vertex shaders meet (Odyssey: 32×32 blocks on every fourth frame). → [Position invariance across shaders](../reference/shaders.md#position-invariance-across-shaders)
|
||||
- the half of Mario facing the camera's left was white (GitHub issue 72, on an iPhone). → [min, max and clamp with a NaN operand](../reference/shaders.md#min-max-and-clamp-with-a-nan-operand)
|
||||
@@ -424,18 +458,21 @@ One line per symptom in the [subsystem references](../reference/): what you see
|
||||
- bindless handles read from a uniform buffer name the wrong texture when the game writes them earlier in the same submission. → [Handles in uniform buffers written earlier in the same submission](../reference/shaders.md#handles-in-uniform-buffers-written-earlier-in-the-same-submission)
|
||||
- a storage buffer reported with `cb_slot` 3, which had no NVN slot. → [Storage buffer addresses in the game's own uniform buffer](../reference/shaders.md#storage-buffer-addresses-in-the-games-own-uniform-buffer)
|
||||
- `no resolved slot number`, the shader does not load; the translation has a 32-entry texture-pool array (`fp_t2d_cb0_40000000`, handle type Direct). → [Bindless handles through a loop phi](../reference/shaders.md#bindless-handles-through-a-loop-phi)
|
||||
- none in the log: texture reads come back 0. Particle meshes collapse to a point (their scale reads 0), so effects are missing or show as dark patches. The translator's own message (`Failed to find handle source for bindless access of type "textureBuffer"`) used to be dropped, because `dmsh_xlate`'s accessor had no `Log`; its messages now go into the shader's notes, marked `translator: `, and the build summary counts them apart from unanswered queries. → [Bindless reads whose handle phi also carries other values](../reference/shaders.md#bindless-reads-whose-handle-phi-also-carries-other-values)
|
||||
- the shaders' bindless arrays have no slot and used not to load; the translator falls back to a 32-entry array such as `array<texturecube<float>, 32> fp_tcube_cb0_40000000`. → [Texture slots picked at run time](../reference/shaders.md#texture-slots-picked-at-run-time)
|
||||
- Ryujinx's fallback arrays (`fp_t2d_cb0_40000000`) in pixel shaders whose handle comes from the game's own uniform buffers; the shaders do not load and their draws are skipped. → [A texture chosen at run time between two uniform-buffer handles](../reference/shaders.md#a-texture-chosen-at-run-time-between-two-uniform-buffer-handles)
|
||||
- the host stops at the shader's first load: `unexpected argument buffer layout: encodedLength=640 maxAid=48`. → [An array of handles in a uniform buffer](../reference/shaders.md#an-array-of-handles-in-a-uniform-buffer)
|
||||
- `shaders with a binding whose slot was not decoded 588`; shadow cascades read through `array<texture2d_array<float>, 32> fp_t2das_cb2_0`. → [An array of handles from the driver table c2](../reference/shaders.md#an-array-of-handles-from-the-driver-table-c2)
|
||||
- characters and props glowing white, about 10⁴ too bright, in DRAGON QUEST VII Reimagined's opening cutscene only, while walls and gameplay look right. → [A texture slot that shares its c2 word with an image slot](../reference/shaders.md#a-texture-slot-that-shares-its-c2-word-with-an-image-slot)
|
||||
- `[RED] shader … reflection conversion failed: 66 bindings, over the limit 64`, then `failed to load`, and the draws with it are dropped (a black map). → [More than 64 bindings in one shader](../reference/shaders.md#more-than-64-bindings-in-one-shader)
|
||||
- the replay put a texture resource ID where the shader wanted an address; with the old 64 KB dummy buffer, pointers in the game's objects and in the host's program objects got their upper 4 bytes replaced and the game or the replay crashed, or `kIOGPUCommandBufferCallbackErrorPageFault`, after which macOS ignores the process's command buffers (GitHub issue #101). → [Image atomics](../reference/shaders.md#image-atomics)
|
||||
- Metal shader validation reports invalid image-atomic stores even with an image-sized scratch buffer; without validation, GPU page faults can freeze rendering. → [Bounded MSL 2.3 atomic emulation](../reference/shaders.md#bounded-msl-23-atomic-emulation)
|
||||
- the replay put a texture resource ID where the shader wanted an address; with the old 64 KB dummy buffer, pointers in the game's objects and in the host's program objects got their upper 4 bytes replaced and the game or the replay crashed, or `kIOGPUCommandBufferCallbackErrorPageFault`, after which macOS ignores the process's command buffers (GitHub issue #101). Later, `[RED] image atomics on a block-linear texture … not followed, the atomics go to a scratch buffer`, and compute shaders with atomics on a 3D image did not build (`array subscript is not an integer`). → [Image atomics](../reference/shaders.md#image-atomics)
|
||||
- `image atomics: unsupported or overflowing IMG extent` at the first dispatch, despite a valid four-byte counter buffer. → [Buffer image reflection keeps its texture type](../reference/shaders.md#buffer-image-reflection-keeps-its-texture-type)
|
||||
- Metal shader validation reports invalid image-atomic stores even with an image-sized scratch buffer; without validation, GPU page faults can freeze rendering. → [Bounds of image atomics](../reference/shaders.md#bounds-of-image-atomics)
|
||||
- shaders missing from the map that no game file holds. → [Built-in shaders compiled with the game's own GLSLC](../reference/shaders.md#built-in-shaders-compiled-with-the-games-own-glslc)
|
||||
- a null read at `main+0x3316dc` in `nnMain`, right after the window setup (the program object's interface pointers at +0xd0 / +0xd8 both null). → [GLSLC GPU code version against the driver's range](../reference/shaders.md#glslc-gpu-code-version-against-the-drivers-range)
|
||||
- a self-contained Mac app (`--platform mac-app`) logs `pipeline prebuild: no .../Resources/dm/shader_pairs.tsv, nothing to build` although every shader library is there; it draws no hybrid stand-ins, and each pipeline seen for the first time compiles on the submit thread (frames of 100–550 ms). → [Shader-pair tables in a Mac app](../reference/shaders.md#shader-pair-tables-in-a-mac-app)
|
||||
- a truncated `.mtl4a` reaches `MTL4ArchiveImpl::loadFromURL`; the bundle reports coverage with `0 of 0 probed bases found`; rebuilt shader libraries reuse an archive of their old binaries; or packing fails with `pso_offline: no base to build` when every eligible pair has function constants. → [Offline pipeline archive reuse and fallback](../reference/shaders.md#offline-pipeline-archive-reuse-and-fallback)
|
||||
- the user cache folder (`getconf DARWIN_USER_CACHE_DIR`, then `com.apple.gpuarchiver/`) fills with `gpuarchiver-*` folders of 0.3–5 MB each, one per pipeline archive flush, and they stay after the game exits: 456 folders, 2.5 GB, on one Mac after two days of NVN game runs. On iOS they stay in the app's own cache folder. → [Metal's build folders left by pipeline archive flushes](../reference/shaders.md#metals-build-folders-left-by-pipeline-archive-flushes)
|
||||
- identical render pipelines compile repeatedly under different keys, lengthening startup prewarm and adding pipeline misses during play. → [Pipeline keys omit unused colour state](../reference/shaders.md#pipeline-keys-omit-unused-colour-state)
|
||||
- a vertex shader whose pairs changed was restored unpadded from `cas/`. → [Shader cache key covering the pairs](../reference/shaders.md#shader-cache-key-covering-the-pairs)
|
||||
- `fragmentFunction main0 cannot be used to build a pipeline state` (a Metal validation assertion). → [Function constants specialized at load](../reference/shaders.md#function-constants-specialized-at-load)
|
||||
|
||||
@@ -71,6 +71,26 @@ Generated projects and Xcode build products stay in that folder; the host profil
|
||||
header is generated under `dm/host/gen/` as usual. CI uploads failure logs.
|
||||
GPU regression tests still run locally with `make -C dm/gfx/tests GAME=<id> run`.
|
||||
|
||||
`.github/workflows/lint-python.yml` lints `scripts/` and the Python tools under
|
||||
`dm/` with ruff (rules for bugs, never reformatting) and type-checks them with
|
||||
pyright in basic mode (`pyproject.toml`). It is a ratchet: today's findings are
|
||||
in `dm/tools/lint_python_baseline.tsv`, so it fails only on a new finding, or on
|
||||
a fixed one still listed there. Astral's ty, a faster type checker, can replace
|
||||
pyright once it is stable. With the pinned tools in a new venv:
|
||||
|
||||
```sh
|
||||
python3 -m venv /path/to/lint-venv && /path/to/lint-venv/bin/pip install -r .github/requirements/lint.txt
|
||||
/path/to/lint-venv/bin/python3 dm/tools/lint_python.py # the check CI runs
|
||||
/path/to/lint-venv/bin/python3 dm/tools/lint_python.py --update # after fixing findings: shrink the baseline
|
||||
```
|
||||
|
||||
The baseline never gains entries: fix a new finding, or silence it on its line
|
||||
(`# noqa: <rule>`, `# pyright: ignore[<rule>]`) with a reason. Only a raised tool
|
||||
version in `.github/requirements/lint.txt` rewrites it (`--rebaseline`). On pull
|
||||
requests, only the checker whose pinned release version increased may gain
|
||||
baseline entries or counts. Comments, dependency updates and checker downgrades
|
||||
do not allow growth.
|
||||
|
||||
## The packing script in detail
|
||||
|
||||
`scripts/build_ipa_from_nsp.py` (`-h` lists every option):
|
||||
@@ -125,7 +145,7 @@ What the script does:
|
||||
A Vulkan game without SPIR-V files in its RomFS (Super Mario Galaxy) gets the same kind of run: the game boots in the Mac host with `DM_VK_CAPTURE` pointing at `<id>_build/extract/<version>/vk_capture` until every SPIR-V key in the profile's `vk_capture.tsv` is there (Galaxy: 3,089, about 7 s after launch), and gives up after 120 s.
|
||||
So does an OpenGL game with no GLSL source in its RomFS or `main` (Balatro: LÖVE puts its shaders together at run time, in Lua): `DM_GL_CAPTURE` at `<id>_build/extract/<version>/gl_capture` until every file in the profile's `gl_capture.tsv` is there (Balatro: 35, made before its first frame). Without the profile's tables (a provisional profile) a capture stops once nothing has been added for 20 s.
|
||||
The Vulkan and GL captures also run on a Mac whose GPU has no Metal 4, such as a virtual Mac (GitHub's `macos-26` runner). Those two layers draw on classic Metal queues of their own, so there the NVN layer starts only its classic part and drops every present, which a headless capture does not need (`nvn_mtl.m` `mtl_device_classic`; `DM_TEST_NO_METAL4=1` does the same on any Mac). The GLSLC capture boots an NVN game, which needs Metal 4.
|
||||
5. It splits out the shaders and checks them against the profile's tables (a Vulkan profile: it translates the RomFS's SPIR-V and the capture with `vk_shaders.py` instead; an OpenGL one: the GLSL sources and the capture with `gl_shaders.py`).
|
||||
5. It splits out the shaders from the game and every extracted DLC RomFS and checks them against the profile's tables (a Vulkan profile: it translates the RomFS's SPIR-V and the capture with `vk_shaders.py` instead; an OpenGL one: the GLSL sources and the capture with `gl_shaders.py`). Optional DLC that changes only the NVN shader union gets new tables in an external profile after the non-DLC shaders and facts have matched; registered module builds and fixes stay in use.
|
||||
6. It builds the iOS shader libraries, with the Mac's own Metal compiler and spirv-cross when they differ from those in `dmsh_chain.lock` (`dm/gfx/tools/own_compilers.py`; one line in the output names them). If a registered profile's `dmsh_chain.h` does not hold the version the source tree gives (a chain change left it behind), the script writes it again before any host is built, so it shows as changed in the checkout (`dmsh_build.py` would stop otherwise). Then the script makes the profile's `c1_full.tsv` and `cb_extent.tsv` from this build's Mac outputs when they are missing or name another chain. These two tables are not committed and git ignores them; `build_ipa.sh` stops without a current `c1_full.tsv`. Each line in the output names the file.
|
||||
It then compares the build with the game's previous one, the newest other `b-*/` in `<id>_build/shader/cache/` (`dm/gfx/tools/shader_regressions.py`, which reads the two `_b_build.json` reports). The translation chain is one for every game, so a translator change is tested only on the games its author owns; shaders that translated before and fail now are listed in `logs/shader_regressions.log` and stop the build ([GitHub issue #272](https://github.com/seedds/nxnative/issues/272)). Report them; `--allow-shader-regressions` (`ALLOW_SHADER_REGRESSIONS = True` in Spyder) packs the app without them.
|
||||
7. It calls `build_ipa.sh`. Xcode builds the app in the game's own build folder (`<id>_build/ipa/build/ios/pkg/dd`: `build_ipa.sh` sets `SYMROOT` and `OBJROOT`, which a custom build location in Xcode's settings does not override), so a repeat build compiles only what changed and two games built at once never write the same app. Then `dm/ios/tools/pack_ipa.py` packs it with the shader libraries, the add-on content and the RomFS straight from where they are. It reads and deflates files on all cores and writes at about the T7's write speed (300 MB/s): Breath of the Wild's 16 GB IPA takes 57 s, Info-ZIP's copy and zip took 2 min 50 s. A repeat pack reuses the previous IPA through `<ipa>.idx`, which records each packed file's size, times and inode: the unchanged leading entries stay in place in an APFS clone of the old IPA, and after the first changed entry, an unchanged file is copied compressed from it (both only when the source folder is on APFS; elsewhere every file is deflated again). `build_ipa.sh` passes the RomFS, the add-on content, the pipeline archive and the shader libraries before the app, so after a host fix only the app is written: Breath of the Wild repacks in 12-23 s instead of 105-163 s.
|
||||
@@ -185,7 +205,30 @@ Reuse works as for the IPA. Each shader output has its own key, named after the
|
||||
|
||||
`--variant 'label:KEY=VALUE,…'` adds a variant: environment switches, or another host (`host=`), other Mac shader libraries (`msl=` with `shmap=`), other rewritten modules (`patched=`) or another pipeline archive (`pso=`). Variants run in turn, `--runs` times each (2 by default), and the results are medians with their range. `--fps` and `--scale` set the frame rate target and the render scale for every variant. Each run's log, `frames.tsv` and two screenshots, taken at the same moments of the route, are in the output folder (`<id>_build/bench/<date-time>/`), with `results.md` and `results.json`. Load on the Mac moves the numbers, so compare the variants of one run of the script rather than separate runs.
|
||||
|
||||
Relative output and variant paths are resolved from the terminal's working directory before the host starts in its run folder. Each metric's median and range use the runs that supplied it; a missing optional GPU or game-speed reading leaves a dash when no run supplied one. A failed launch releases `--lock` so another measurement can run.
|
||||
Relative output and variant paths are resolved from the terminal's working directory before the host starts in its run folder. Each metric's median and range use the runs that supplied it; a missing optional GPU or game-speed reading leaves a dash when no run supplied one. A failed launch releases `--lock` so another measurement can run. A lock whose process is gone, with no game running, is stale and removed.
|
||||
|
||||
### The regression harness
|
||||
|
||||
One translation chain and one host serve every profile, but a contributor can test only the games they own, and CI has no game files and no Metal 4 (GitHub issue #272). `scripts/regress.py` compares two revisions on every game this Mac has a `--platform mac` build of, and writes a report that holds only numbers, hashes, shader keys, normalized `[RED]` texts and timings, so it can go into a pull request. It builds on the shader regression check the NSP script runs after each shader build ([packing step 6](#the-packing-script-in-detail)): that one compares one game's new build with its previous one when you rebuild it; the harness compares two revisions on every local game without touching their builds, and adds the MSL hashes, the pipeline interface and a run of the game.
|
||||
|
||||
```sh
|
||||
python3 scripts/regress.py check --quick # the merge base with main against this checkout (uncommitted files included): chain checks and a 60 s boot per game
|
||||
python3 scripts/regress.py check # the same with the whole route and the pipeline prebuild
|
||||
python3 scripts/regress.py chain # the static checks only, no game runs
|
||||
python3 scripts/regress.py baseline --rev main --runs 2 # record a baseline (check records a missing one itself)
|
||||
python3 scripts/regress.py list # the games found, their routes and stored baselines
|
||||
```
|
||||
|
||||
`--games <id>,…` selects games, `--baseline` / `--candidate` take any revision (`worktree` is this checkout), `--lock` (or `DM_MEASURE_LOCK`) is the measuring lock `bench.py --lock` shares, held only while a game runs, and `REGRESS_SKIP=<id>,…` leaves out a game someone else is working on. Source trees and public reports go under `--root` (`REGRESS_ROOT`, by default `/Volumes/NX/harness`): each revision is checked out once into `trees/`, and its chain, host and rewriter come from there. Game-derived files (shader libraries, rewritten modules, profile copies, saves, screenshots and baselines) stay in the game's NSP folder at `<id>-work/regress/`. Shader tools write generated tables only into those profile copies. The games are the NSP folders under `--games-dir` (`REGRESS_GAMES`). The exit status is 0 without a regression, 1 with one, and 2 when the harness could not check a game.
|
||||
|
||||
The default selection keeps every local build for the chain checks; games with a `smoke.tsv` also run its route. A game without a route does not build a host or run. `check --no-smoke` and `baseline --no-smoke` perform only the chain and interface checks. The chain cache includes the shader package's contents and the split's fallback `pso_pairs.tsv`; Vulkan builds include captured SPIR-V from the packed version's `vk_capture` folder. Interface checks run again with each revision's archive planner and profile even when the libraries are reused. Stored smoke baselines are reused only while the route, save bytes, build artifacts, compilers, machine and run settings match; older records are recorded again. Each candidate run must supply its scheduled screenshots, and a prebuild that crashes or times out fails the run. With repeated runs, every candidate screenshot is compared with its closest baseline screenshot.
|
||||
|
||||
Per game it checks:
|
||||
- **Chain** (no GPU): the shaders built by each revision's `dmsh_build.py` (a Vulkan game: `vk_shaders.py`), shader by shader: built or failed (newly failing by the rule of `shader_regressions.py`, which it imports; a new shader that fails is listed apart), and the SHA-256 of the MSL, of the reflection without its chain fields and of the variant libraries. A build is kept per chain version, profile inputs and compilers (`<NSP folder>/<id>-work/regress/chain/`), so a revision that did not change the chain reuses it, and the build takes the game's own shader cache (`DMSH_CAS`), so unchanged MSL is not compiled again. A changed chain still translates every shader: Breath of the Wild's 19,592 took 14 minutes on an M2 Max.
|
||||
- **Interface** (no GPU): every pipeline base `pso_offline.py` would put into the archive, from `shader_pairs.tsv` and `pso_pairs.tsv`, whose pixel shader reads a `[[user(locnN)]]` input the vertex shader does not write or writes with another type. `metal-tt` refuses such a pipeline and with it the whole archive (GitHub issue #283).
|
||||
- **Smoke** (Mac host, window, no audio): the route `dm/games/<id>/smoke.tsv` from a copy of its save, under a watchdog: the frame it must reach, a crash, a hang (no frame for `hang` seconds), the prepare step's time, `[RED]` lines the baseline did not print (numbers, addresses and paths normalized), frame times from `DM_NVN_TSV` (a Vulkan or GL game: presents counted from the log), the peak memory footprint, and perceptual hashes of screenshots at fixed times. The full mode first runs the pipeline prebuild (`DM_PSO_PREBUILD=tool`) under a watchdog and starts the game on its archive; `--quick` skips it (`DM_PSO_PREBUILD=0`).
|
||||
|
||||
A route is `key <TAB> value` lines: `save` (a folder in the NSP folder, `run` for `<id>_build/run/save`, `new` for an empty one), `keys` (`DM_KEYS`), `secs` and `quick` (the run lengths), `reach` (`<frame>@<seconds>`), `window` (start and length of the frame-time window), `shots` (seconds), `hang`. A game is `PASS`, `CHANGED` (MSL or numbers differ, nothing worse), `REGRESSION` (new `[RED]` lines, a screenshot, frame time, memory or prepare time beyond the tolerances at the top of the script), `NEW-FAIL` (a shader, the interface check, a crash, a hang or the frame that worked in the baseline fails) or `ERROR`. Failures the baseline has too are listed as known and do not fail the check. `report.json` (its schema is in the script's header) and `report.md` go to `<root>/reports/<date-time>-<command>/`; the screenshots, logs and frame rows stay in `<NSP folder>/<id>-work/regress/runs/`.
|
||||
|
||||
### GPU measurement
|
||||
|
||||
@@ -256,7 +299,7 @@ The unsigned IPA build still uses the repository's `dm/ios/DmIOS.xcodeproj`, ski
|
||||
|
||||
Each game has a profile in `dm/games/<id>/`:
|
||||
- `game.tsv` holds the title ID, known builds, code modules, RomFS marker, memory split, app name and bundle ID;
|
||||
- the shader tables: `shader_map.tsv`, `shader_facts.tsv`, `shader_pairs.tsv`, `pso_pairs.tsv` (pairs only the pipeline prebuild reads), `pso_manifest.tsv`, and `c1_full.tsv` / `cb_extent.tsv`, which each checkout's full shader build makes (not committed);
|
||||
- the shader tables: `shader_map.tsv`, `shader_facts.tsv`, `shader_pairs.tsv`, `pso_pairs.tsv` (pairs used by vertex output padding and the pipeline prebuild), `pso_manifest.tsv`, and `c1_full.tsv` / `cb_extent.tsv`, which each checkout's full shader build makes (not committed);
|
||||
- `patches.py` (when there are any): the rewriter's patches of the game's own builds.
|
||||
|
||||
The shader translation chain has one version for every game, in `dm/gfx/nvn/dmsh_chain.h`.
|
||||
@@ -375,7 +418,7 @@ flowchart TD
|
||||
|
||||
Every 300 frames the log has a `pipelines up to frame` line. It gives the submit thread's pipeline, library and prediction time; archive lookups that found the base or none; specializations and base compiles on all threads; draws with a hybrid pipeline; background builds; and the draws that waited, by why no stand-in served them. `DM_PSO_WAITLOG=1` logs every wait over 20 ms with its shader pair. `DM_PSO_PULL_ALL=1` (diagnostic) draws every draw that has a pulling pipeline with it. In steady state the GPU time is the same with and without the hybrid: in Breath of the Wild, mean GPU p50 over frames 2,400–5,100 was 8.98 ms with it, 8.98 ms on the Metal 4 path without it, and 9.35 ms through the `MTLDevice`.
|
||||
|
||||
The **prebuild** puts the pulling base of every known shader pair into the archive (`pso_prebuild`), so a cold start's draws have stand-ins. The pairs come from the profile's `shader_pairs.tsv` and `pso_pairs.tsv`. `shader_pairs.tsv` pairs the stages of one BNSH variation (and the other readers' programs). It held none of the pairs from agl's BAHS shader archives (`.sharcb`, the agl / gsys / terrain resource packs), and in Breath of the Wild 109 of the 543 pairs drawn on entering Zora's Domain came from those. `shader_split.py` now reads the binary and program arrays of a BAHS file (`bahs_variations`) and pairs the n-th vertex binary of a program with its n-th fragment binary. Those pairs go to `pso_pairs.tsv`, which only the prebuild reads. `vs_pad` keeps reading `shader_pairs.tsv`, so the shader libraries, the inputs digest and the other profiles' tables are unchanged. For Breath of the Wild that is 3,012 more pairs, and all 543 pairs drawn are now known. A profile made before the table existed still packs: the NSP script then keeps the table beside the shader package and hands it to the prebuild (`DM_PSO_PAIRS`). Super Mario Odyssey, Mario Bros. Wonder, Mario Kart 8 Deluxe, 3D World, Animal Crossing, Captain Toad and Tears of the Kingdom have BAHS files too; their tables are made at their next pack.
|
||||
The **prebuild** puts the pulling base of every known shader pair into the archive (`pso_prebuild`), so a cold start's draws have stand-ins. The pairs come from the profile's `shader_pairs.tsv` and `pso_pairs.tsv`. `shader_pairs.tsv` pairs the stages of one BNSH variation (and the other readers' programs). It held none of the pairs from agl's BAHS shader archives (`.sharcb`, the agl / gsys / terrain resource packs), and in Breath of the Wild 109 of the 543 pairs drawn on entering Zora's Domain came from those. `shader_split.py` now reads the binary and program arrays of a BAHS file (`bahs_variations`) and pairs the n-th vertex binary of a program with its n-th fragment binary. Those pairs go to `pso_pairs.tsv`. The shader build reads both tables to pad every vertex output its pixel partners need ([reference](reference/shaders.md#pixel-shader-inputs-the-vertex-shader-does-not-write)); the pairs also control build order and cache dependencies. The profile's `pso_pairs.tsv` is part of its inputs digest. For Breath of the Wild that is 3,012 more pairs, and all 543 pairs drawn are now known. A profile made before the table existed still packs: the NSP script then keeps the table beside the shader package, where the shader build reads it, and hands it to the prebuild (`DM_PSO_PAIRS`). Super Mario Odyssey, Mario Bros. Wonder, Mario Kart 8 Deluxe, 3D World, Animal Crossing, Captain Toad and Tears of the Kingdom have BAHS files too; their tables are made at their next pack.
|
||||
|
||||
With the prepare step (a window, the iPhone) the prebuild runs right after it when the bases are not there yet, before the game's first frame, behind the same progress screen ("Preparing pipelines"), on every core, flushing every 1,000 bases; a first start waits for it once. Built in the background while playing instead, they took an iPhone 17 Pro Max's GPU from about 13 to 15–18 ms a frame in Breath of the Wild, through the compiler's load on the shared power budget. Without a prepare step it still runs in the background after the prewarm, on a utility queue on half the performance cores (`DM_PSO_PREBUILD=0` turns it off either way). On the Mac it also runs as a tool, `DM_PSO_PREBUILD=tool`: build, write, exit. The NSP script runs the tool in a `--platform mac` build, into `<id>_build/run/pso_archive` (log `pso_prebuild.log`): this is how the Mac gets its archive. It runs once per chain version, depth mode and pair list (`pso_archive/prebuilt.tsv`: the pair lists' size and SHA-256). `DM_PSO_PREBUILD_DEPTH=zero` serves a game that sets the near=0 depth mode (the NSP script sets it from the profile key `depth_mode`); `DM_PSO_PREBUILD_THREADS=<n>` sets the workers. Its files hold bases without a vertex layout only, and are named `pNNNNN.mtl4a`; a base with a vertex layout is never looked up in them, and compaction leaves them alone. Breath of the Wild on an M2 Max: 15,767 bases, 910 MB in 16 files, 187 s with 7 workers for the first 12,755 and 19 s for the 3,012 more. Opening the files took 2.2 s on the first start (0.25 s from the file cache). The host's footprint is about 850 MB larger with them open (2,067 against 1,215 MiB at the title), which is one reason the Metal 4 path stays off on iOS unless a game's profile was tested with it (`pso_mtl4`). On iOS the standard is the archive made on the Mac and shipped in the app (next section); the device's own prebuild is the fallback, for an app without a usable archive.
|
||||
|
||||
@@ -432,6 +475,7 @@ Each game's notes are in its profile folder, `dm/games/<id>/README.md`: what the
|
||||
| `nx_extract/` | NSP → ExeFS / RomFS / icon with LibHac, merging an update over the base game (C#, `dotnet build`). `--manifest` writes ExeFS and a RomFS file list only, for surveys |
|
||||
| `nso_fnmap.py` | Carries offsets from one build to another (function matching with addresses masked; globals via their references) |
|
||||
| `nso2elf.py` | Converts NSO to ELF for Ghidra / REA |
|
||||
| `env_switches.py` | Writes [docs/env_switches.tsv](env_switches.tsv): every `DM_*` environment switch the code reads, with its class and a one-line meaning. The classes: player (a launcher setting, `dm/games/settings.tsv`), path (names a file or folder), switch (changes what the host or a tool does), diagnostic (logs, dumps, probes, measurements), test (read only by tests). It keeps the class and meaning of each row and adds a row for a new switch, whose meaning its author writes; `test_env_switches.py` fails until they do, and when a row names a switch nothing reads. It also lists the names the docs quote that nothing reads |
|
||||
| `gpuprof.py` | Sums the host's `[gpuprof]` lines (`DM_NVN_GPUPROF`) by pass shape or size class, lists one frame's encoders in GPU order, and gives the medians of the `[ab]` phases (`DM_AB`) |
|
||||
| `insn_census.py` | Counts the instruction sites the rewriter must handle |
|
||||
| `nso_code.py` | Function ranges from `.eh_frame` and the data blocks in a text segment, which the rewriter and the census leave out |
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
name class meaning
|
||||
DM_AB diagnostic `=<frames>` splits the run into phases of that many frames and alternates every switch set to `ab` (on in the odd phases); logs an `[ab]` line per phase with GPU and frame time and attachment traffic
|
||||
DM_ALIAS_WINDOWS diagnostic `=<MB>` logs every 30 s how much of each window of that size in the alias region is mapped (windows aligned to their size in absolute addresses)
|
||||
DM_AOC_DIR path Folder of add-on content: one numbered subfolder per DLC index holding that DLC's RomFS, reported by `aoc` and mounted read-only; the app, the Mac launcher and the NSP script set it when the build has DLC
|
||||
DM_AOC_OWNED switch `0` reports no add-on content even when `DM_AOC_DIR` has some, for runs without the DLC
|
||||
DM_APP_VERSION switch Overrides the IPA's version in `build_ipa.sh`; by default it is the packed game's display version from its control data (the update's when one is packed)
|
||||
DM_AUDIO switch `1` plays audio through Core Audio, `0` uses the silent sink; real output is the default on iOS, the silent sink on the Mac (the Mac launcher sets `1`)
|
||||
DM_AUDIO_DUMP diagnostic `=<file>` writes every PCM block the game appends as is (16-bit, 48 kHz, channel count as in the OpenAudioOut log), to judge whether a sound played
|
||||
DM_AUDIO_MUTE switch `=1` runs the real Core Audio output path (callback pacing, background take-over) with the output silenced, for lifecycle tests on the Mac without noise
|
||||
DM_AUDREN_TRACE diagnostic When set, the CPU audio renderer logs every update with the number of frames rendered since the previous one
|
||||
DM_BOOTPROF diagnostic When set, logs boot timing: the loader's breakdown (mapping, relocation, probes), Metal library loading, pipeline creation, texture uploads and the first present
|
||||
DM_BUNDLE_ID switch Overrides the iOS app's bundle ID in `build_ipa.sh` (default `org.nxnative.<title ID in lowercase>`)
|
||||
DM_COPY_GAME_RESOURCES switch `NO` skips the Xcode build phase that copies the game's resources into the app (`stage_resources.sh`, on by default); `build_ipa.sh` sets it for its unsigned build, which packs them itself
|
||||
DM_CTL diagnostic `=<file>` (Mac): the host polls the file every 100 ms and runs each appended line: button presses, `shot`, `passdump`, `texdump`, `env KEY=VALUE`, `peek`, `memdump`, `drag` and more
|
||||
DM_DBG diagnostic When set, prints the guest's `svcOutputDebugString` text to stderr as `[guest]` lines
|
||||
DM_DBG_AU_FRAMES diagnostic `=<frames>` asks the Mac's Core Audio output for that many frames per callback instead of about 5 ms (1024 reproduces the iOS default that caused crackling)
|
||||
DM_DETCLOCK switch `=<step ns>`: from the first present on, the guest clock advances a fixed step per present instead of following the wall clock, and the random source becomes a fixed-seed stream (replay and test runs)
|
||||
DM_DETCLOCK_PACE switch `=1` with `DM_DETCLOCK`: each frame takes at least one clock step of wall time, so background loading keeps up with the fixed clock (used by the screenshot gate)
|
||||
DM_DISPLAY_NAME switch Overrides the iOS app's name in `build_ipa.sh` (default the game name in the base NSP's control data)
|
||||
DM_ENTITLEMENTS path `=<plist>` gives a prepared Xcode project another entitlements file instead of the Increased Memory Limit one; empty means no entitlements
|
||||
DM_ENV_FILE path `=<file>` makes `build_ipa.sh` bake its `KEY=VALUE` lines into the app as `dm/env.txt`, applied at boot; `Documents/env.txt` overrides them
|
||||
DM_ENV_FILE_KEYS switch Set by the iOS app at launch to the space-separated names an env file set; the launcher keeps those values and shows them greyed out
|
||||
DM_FD_LOG diagnostic `=<seconds>` logs, every that many seconds, the number and kinds of file descriptors the process has open
|
||||
DM_FIXED_TIME switch `=<seconds>`: the user clock always returns that time and the host time zone is pinned to UTC, for deterministic comparison
|
||||
DM_FONT_DIR path Folder of the shared-font stand-ins (`standard.ttf`, `nintendo_ext.ttf` with the button icons); the app, the Mac launcher and the NSP script set it
|
||||
DM_FORCE_STRICT diagnostic `=1` makes the loader fail on forced-hook entries no module references, instead of only noting them (for checking a mistyped list)
|
||||
DM_FPS switch `30` or `60` pins the frame rate the display link and frame pacing aim for; otherwise it follows the game's present interval (60, or 30 when the game asks for 30)
|
||||
DM_FPS_HOLD player `on` / `off` overrides the profile's `fps_hold`: whether late frames may raise the hold above the frame cap's vsyncs (on by default)
|
||||
DM_FPS_TARGET player Frame-rate cap: `30` or `60`, or `0` for the game's own limit; overrides the profile's `fps_target`
|
||||
DM_FS diagnostic When set, logs file opens, paths not found or not mounted, and a read summary every 64 MiB or 1000 reads
|
||||
DM_FSLOG diagnostic `=<file>` writes one TSV line per ReadFile: seconds since boot, guest path, offset, length and guest buffer address
|
||||
DM_FSLOG_HASH diagnostic With `DM_FSLOG`, adds a column with the FNV-1a 64 hash of the bytes read
|
||||
DM_FULLSCREEN switch When set, the Mac window starts in fullscreen
|
||||
DM_GAME switch Selects the game profile (a title ID under `dm/games` or `DM_GAMES_DIR`) for the Python tools and the rewriter; there is no default
|
||||
DM_GAMES_DIR path Profile folders outside the repository, separated by `:`, searched after `dm/games`; the NSP script points it at a provisional or derived profile
|
||||
DM_GAMES_FIRST switch `=1` searches `DM_GAMES_DIR` before `dm/games`, so a provisional or derived profile wins over the game's own (the NSP script's `--provisional`)
|
||||
DM_GAME_ENV switch `KEY=VALUE` pairs, separated by spaces, that the game's own `getenv` returns, in place of the profile's `game_env` (`hle_os.c`); any other name goes to the SDK's empty environment
|
||||
DM_GFX_FIXTURE_OUT test `=<dir>`: graphics tests write their raw result textures and buffers there
|
||||
DM_GLSLC_CAPTURE path `=<dir>` saves every GLSLC output the game compiles at run time; `shader_split.py` reads it (default `<version dir>/glslc_capture`)
|
||||
DM_GL_CAPTURE path `=<dir>`: the GL layer writes every shader source the game compiles (`<key>.vert` / `<key>.frag`) for `gl_shaders.py`; the NSP script uses it to capture run-time shaders
|
||||
DM_GL_LOG diagnostic `=1` makes the GL layer log object creation
|
||||
DM_GL_STUBS diagnostic `=1` gives every GL function the layer lacks a stub that reports its first call and returns 0, instead of a null pointer (bring-up)
|
||||
DM_GPU_CAPTURE diagnostic `=<frame>[:<frames>]` or `gpu:<ms>[:<after frame>]` writes a Metal GPU trace of those frames for Xcode's Metal debugger; can be set while running through `DM_CTL`
|
||||
DM_GPU_CAPTURE_DIR path Folder for `DM_GPU_CAPTURE`'s traces (default `gpu_captures/` in the working directory)
|
||||
DM_GPU_CAPTURE_KEEP diagnostic Before each GPU capture, prunes the capture folder's `dm_*.gputrace` entries so that the newest `KEEP - 1` stay (default 2)
|
||||
DM_GUEST_SPAWN_TEST diagnostic `=1` runs a self-test before `nnMain`: a host-started guest thread does SDK malloc / free and locks an `nn::os` mutex, and the result is logged; `=2` also keeps a resident thread
|
||||
DM_HITCH_DUMP diagnostic `=<ms>[:<s>]`: when no frame has been presented for that long (from s seconds on), logs every guest thread once per stall: wait, last SVC, pc, lr, frames and registers
|
||||
DM_HLECOUNT diagnostic `=<file>` (`%d` = process ID) writes at exit a TSV of how many times the guest called each HLE function, zero counts included
|
||||
DM_ICON path `=<image>` gives `build_ipa.sh` another app icon (default the `icon.jpg` beside the RomFS)
|
||||
DM_IMPORTS diagnostic When set, logs each import the loader binds to a host function and each forced takeover
|
||||
DM_KBD switch `=<text>`: the system keyboard returns that text as OK without showing an input box; unset with no window, it returns cancel
|
||||
DM_KBD_PLAYER switch `own` makes the Mac keyboard player 1 on its own, with controllers from player 2 on (by default the first controller shares player 1 with the keyboard)
|
||||
DM_KEYS switch Scripted input: `<seconds or f<frame>>:<key>[+key][~hold][*count/interval],…` presses buttons, sticks and `Shake` at those times
|
||||
DM_LANG player Game language as a language code (default `en-US`; the iOS app sets the one the IPA was packed with)
|
||||
DM_LAUNCHER switch `0` skips the iOS settings screen and starts the game at once
|
||||
DM_LINK_VTICK switch `0` keeps the system's display-link rate on 144 or 165 Hz Mac screens; by default the link counts only the refreshes nearest a steady 1/60 s clock
|
||||
DM_LOADHASH diagnostic `=1` logs an address-independent SHA-256 per module after loading
|
||||
DM_LONGFRAME diagnostic `0` turns off the long-frame log (on by default), a line for a frame over 20 ms or with GPU time over 16.7 ms that is clearly above recent frames
|
||||
DM_MEASURE_LOCK path `=<file>`: `scripts/regress.py`'s default `--lock`, the measuring lock `scripts/bench.py --lock` shares: held only while a game runs, and the harness's shader builds pause while another process holds it
|
||||
DM_MEMSTAT diagnostic `=<tsv>` writes one line per second of memory accounting: footprint, Metal allocation and resident and charged memory by region class (guest, code, malloc, gpu, …)
|
||||
DM_MEMSTAT_DUMP diagnostic `=<seconds>`: with `DM_MEMSTAT`, at that second writes every VM region of 1 MiB or more to `<tsv>.regions`
|
||||
DM_MEM_CAP_GB switch `=<GB>`: the memory watchdog exits when resident memory exceeds that many GB (default 24 on the Mac, none on iOS; `0` turns it off)
|
||||
DM_METAL_CAPTURE diagnostic `=1` makes `build_ipa.sh` produce a build that can write GPU frame captures (`MetalCaptureEnabled` in `Info.plist`), for `DM_GPU_CAPTURE`
|
||||
DM_METAL_HUD player `1` shows Apple's Metal performance HUD over the game (frame rate, frame and GPU time, memory)
|
||||
DM_MOVIE_TRACE diagnostic When set, the movie decoder logs every call with its thread and decoder (by default the first 12 calls, then every 2^k-th)
|
||||
DM_MSL_DIR path Parent folder of the shader libraries: NVN loads from `b-<chain version>/` beneath it, Vulkan from `vk/`, GL from `gl/`; the app and the Mac launcher set it
|
||||
DM_NICK switch The account nickname the game sees (default `Player`)
|
||||
DM_NRO switch `0` turns off loading of the NRO modules the game's NRR files register (on by default): every `nn::ro::LoadModule` then fails
|
||||
DM_NVN_ALLOCPROF diagnostic `=1` charges Metal allocation growth to each replayed op type and function, and logs command allocator sizes and each new texture (serial, size, format, storage)
|
||||
DM_NVN_API switch `=<major>.<minor>` overrides the NVN API version `nvnDeviceGetInteger` reports (by default decided by the SDK version or the profile's `device_api`: 55.15 for SDK 11 and later, 53.313 before)
|
||||
DM_NVN_BARRIER switch How draw passes are ordered: `full` (default) waits for all earlier GPU work, `game` only for the previous pass's fragment stage unless the game set a barrier, `ab` alternates with the `DM_AB` phases
|
||||
DM_NVN_BF_WANT path `=<file>`: appends the key of each fragment shader whose passes could not be merged for lack of a readable-tile version to that file, once per key
|
||||
DM_NVN_BLIT_ORDER diagnostic `0` drops the barriers between dependent operations in the copy encoder, which reproduces the fountain's vertical stripes in Mario Wonder
|
||||
DM_NVN_CBDUMP diagnostic `=<dir>`: saves each draw's whole uniform blocks as `F<frame>_p<pass>_st<stage>_cb<slot>_<address>.bin`, and the source bytes of buffer-to-3D-texture copies, with `DM_NVN_PASSDUMP`
|
||||
DM_NVN_CBDUMP_KEY diagnostic `=<shader key>`: limits `DM_NVN_CBDUMP` to draws using that shader; for a vertex shader key also saves the draw's vertex streams and index buffer
|
||||
DM_NVN_CBLOG diagnostic `=1` logs the command memory the game adds to command buffers (`AddCommandMemory`: the first 60, then every 500th) and the out-of-memory callbacks
|
||||
DM_NVN_CLEARLOG diagnostic `=1` logs the first 40 colour clears drawn as a triangle (inside a pass, with a partial channel mask or a partial scissor)
|
||||
DM_NVN_CMDSTAT diagnostic `=1` logs every 300 frames the command recording usage: bytes and records per frame, command memory segment sizes, out-of-memory callbacks, host segments, count and bytes per command type
|
||||
DM_NVN_CS_HOIST switch `0` makes a compute dispatch between the draws of a pass end it (on by default: the dispatch runs ahead of the pass when that changes nothing the draws before it read)
|
||||
DM_NVN_CUT switch `=<draws>`: the replay commits its command buffer every that many draws, between passes (default 64; `0` one command buffer per submission, except the cut after a buffer-to-texture copy)
|
||||
DM_NVN_DEFER switch `0` turns off the deferred pass merging of the TBDR optimizations (which run only with `DM_NVN_TBDR=1`)
|
||||
DM_NVN_DEFER_AB diagnostic `=1` keeps the deferred pass merging (with `DM_NVN_TBDR=1`) off until the second screenshot has been written, then turns it on, to compare the pictures with and without it
|
||||
DM_NVN_DEPTHOVR diagnostic `=<fragment shader key prefix>,…`: those draws compare depth with `ALWAYS`; with `DM_NVN_HILITE`, tells "the depth test rejects every fragment" from "nothing rasterizes"
|
||||
DM_NVN_DIRTYCHECK diagnostic `=1` fully hashes pool content on every use to find changes the dirty-page tracking missed
|
||||
DM_NVN_DISPATCH_OWN switch `=1` gives every compute dispatch a compute encoder of its own with full barriers, as before (by default dispatches share the copy encoder); `DM_NVN_PASSGPU` does the same
|
||||
DM_NVN_ENC_LABEL diagnostic `=1` labels every render encoder `p<pass in frame> <width>x<height> <first 8 digits of the first draw's fragment key> d<draws>` for Instruments traces
|
||||
DM_NVN_ENC_THREAD player `1` runs the replay's Metal encoding on a thread of its own while the submit thread goes on translating; on by default on iOS (`0` turns it off), off on macOS
|
||||
DM_NVN_FAST_WAKE switch `0` makes the game's `SyncWait`s wake on the commit feedback handler instead of the GPU's own signal of the fence (on by default)
|
||||
DM_NVN_FB_SNAP switch Which fragment shaders sampling a colour attachment of their own pass read a snapshot of it: `0` none, `all` every shader (slow), `<keys,…>` that list instead of the profile's `feedback_snap`
|
||||
DM_NVN_FLUSH_FILL switch `0` starts a new GPU texture in a CPU-writable pool as zeros; by default one whose storage the game flushed before its first use is filled from that storage once
|
||||
DM_NVN_FRAMES diagnostic `=<N>` prints the NVN call table frame by frame for the first N frames (default 3)
|
||||
DM_NVN_FUSE_CHECK diagnostic `=1` checks pass merging in the same frame: renders the merged passes again apart and logs the comparison of the attachments every time
|
||||
DM_NVN_GPUPROF diagnostic `=<f>`, `<f1>-<f2>` or `every:<n>`: logs the GPU start and duration of every encoder of those frames (`[gpuprof]` lines from Metal 4 counter heap timestamps)
|
||||
DM_NVN_GPU_TS switch `segment` or `submission`: what timestamp reports record (GPU start / end of the segment holding each report, or one time for the whole submission), instead of the profile's `gpu_timestamps`
|
||||
DM_NVN_HILITE diagnostic `=<fragment shader key>,…`: those draws use a solid colour instead (magenta, green, cyan, yellow, blue, white in order), to tell wrong geometry from wrong shading
|
||||
DM_NVN_IXCLOG diagnostic `=<n>` logs the first n indirect draw and dispatch conversions (record to arguments) once the GPU has run them
|
||||
DM_NVN_KEYDUMP diagnostic `=<fs key>[:n[:every]]`: for up to n draws of that fragment shader (default 6), logs the pass dump binding lines plus vertex formats, vertex streams and uniform values; meant for devices
|
||||
DM_NVN_LAZY_CLEAR switch `0` makes clears with no draw after them open a pass of their own, as before; by default they wait to become the load action of the next pass on that target
|
||||
DM_NVN_LOADPROBE diagnostic `=1`, `2` or `3`: attachments whose load is skipped are cleared to a sentinel colour (`3` NaN; `1` NaN for float formats) and the sentinel pixels left at pass end are counted (should be 0)
|
||||
DM_NVN_LOG diagnostic `=1` logs the creation of NVN objects
|
||||
DM_NVN_MAGSCAN diagnostic `=<fs key>[:every[:phase]]`: device self-check that reads each pass's colour attachments every `every` frames (default 30) and logs `[mag]` lines for solid magenta 16×16 blocks or non-finite pixels
|
||||
DM_NVN_PASSDUMP diagnostic `=<f>`, `<f1>-<f2>` or `every:<n>`: logs every render pass of those frames (attachments, clears, shader keys, bindings, draw state, presents) for the `tbdr_*.py` tools
|
||||
DM_NVN_PASSDUMP_ALL diagnostic `=1` makes `DM_NVN_PASSDUMP` record every draw and binding change, not only the first per pass and stage, including copies and inline uniform updates
|
||||
DM_NVN_PASSGPU diagnostic `=<file>` writes the GPU time of each render and compute pass from stage-boundary timestamp counters (off under Metal 4); also gives each dispatch its own encoder
|
||||
DM_NVN_PASSGPU_SERIAL diagnostic `=1` makes each encoder on the main command buffer wait until all earlier work has finished, so `DM_NVN_PASSGPU` times read each pass's exclusive time (totals read high)
|
||||
DM_NVN_PREDICT switch `0` turns off the parallel precompile look-ahead: a pipeline miss compiles only the pipeline it needs
|
||||
DM_NVN_PROF diagnostic `=1` wraps the implemented NVN functions in a timing wrapper and collects the time each takes during recording
|
||||
DM_NVN_PROGTRACE diagnostic `=<vertex keys,...>` logs matching ProgramSetShaders calls and their guest backtraces
|
||||
DM_NVN_REGLUM diagnostic `=x0,y0,x1,y1:<file>` (normalized coordinates): on every present records the present number, time, mean luminance and a grayscale thumbnail of that region, to check animation speed against frame rate
|
||||
DM_NVN_RT_RANGE switch `=1` (experimental, off by default; with `DM_NVN_TBDR=1`) keeps the pass open when the game sets the same colour and depth target again with a view that differs only in its level count
|
||||
DM_NVN_RT_RANGE_CHECK diagnostic With `DM_NVN_RT_RANGE=1` and `DM_NVN_PASSDUMP`, renders a candidate pass both ways in the same frame and logs and writes the comparison (`CHECK_RESULTS.tsv`, `[RED]` on a difference)
|
||||
DM_NVN_SHDUMP diagnostic `=<dir>` saves programs missing from the shader map as `<key>.ctl` (control section) and `<key>.code`, to find the files that hold them
|
||||
DM_NVN_SHMAP path The shader map (`shader_map.tsv`); `c1_full.tsv` and `pso_manifest.tsv` are taken from the same folder. Default: the game profile's folder on the Mac; the iOS app sets it
|
||||
DM_NVN_SHOT diagnostic `=<frame>[,<frame>…]:<path>` saves the swapchain texture as an image at those frames (`%d` in the path = frame number)
|
||||
DM_NVN_SHOT_OUT diagnostic `=<W>x<H>` makes `DM_NVN_SHOT` capture the picture as the present pass upscales it to that size, the crop included, instead of the raw swapchain texture
|
||||
DM_NVN_SKIP_FS diagnostic `=<fragment key>[,…]` (up to 16) drops all draws of those fragment shaders, to measure their GPU cost or bisect a wrong draw; the picture is wrong
|
||||
DM_NVN_SKIP_SMALL diagnostic `=<width>` drops all draws whose target width is at most that, to measure the cost of tiny render passes; the picture is wrong
|
||||
DM_NVN_SLICE_MERGE switch `0` keeps passes into other layers of one array or cube texture level apart (on by default: they run as one layered pass)
|
||||
DM_NVN_SNAPRAW diagnostic `=1` makes `DM_NVN_TEXDUMP` also save each target's raw bytes, with a text header line, since converting an HDR target to an 8-bit image truncates
|
||||
DM_NVN_T2T_FROM diagnostic `=<frame>`: `DM_NVN_T2T_TRACE` records copies only from that frame on (default 0)
|
||||
DM_NVN_T2T_TRACE diagnostic `=<W>x<H>:<file>`: after each copy into an RGBA16F texture of that size, copies the result into a trace buffer on the GPU timeline; 240 entries are written to the file
|
||||
DM_NVN_TBDR switch `=1` turns on the replay's TBDR pass optimizations (store / load elision, pass merging, same-target continuation); off by default since bindless textures defeat their read inference
|
||||
DM_NVN_TEXCPU diagnostic `=1` logs non-packaged (non-swizzled) textures in CPU-visible pools
|
||||
DM_NVN_TEXDUMP diagnostic `=<frame>:<path>`, `size=<W>x<H>:<path>` or `layers=<n>:<path>`: saves every colour render target of that frame as PNG (`%d` = serial × 100 + layer × 10 + level)
|
||||
DM_NVN_TEXINFO diagnostic `=<W>x<H>` logs storage and description of every texture of that size on first use, registration and initialization (pool, flags, packaged, non-zero bytes, swizzles)
|
||||
DM_NVN_TEXRAW diagnostic `=<serial>,…` or `info` (the textures `DM_NVN_TEXINFO` picked out): saves those textures' raw bytes at the `DM_NVN_TEXDUMP` frame, with a text header line
|
||||
DM_NVN_TEXSNAP diagnostic `=<in-frame pass index>,…`: with `DM_NVN_TEXDUMP`, copies those passes' colour attachments as they end and saves them with the end-of-frame dump
|
||||
DM_NVN_TREG_ID diagnostic `=<n>` logs every registration of texture ID n: what it holds from then on, and the frame being recorded
|
||||
DM_NVN_TSV diagnostic `=<path>` writes the per-frame NVN call table and timing rows (frame, metric, value) to a TSV, read by `scripts/bench.py`
|
||||
DM_NVN_TSV_CALLS diagnostic `0` makes `DM_NVN_TSV` write only the timing rows, without the call counts (about two thirds of the file)
|
||||
DM_NVN_UB diagnostic `="name=item,…;…"`: alternating measurement in one run; odd phases of `DM_NVN_UB_N` frames (default 180) take one category in turn (`@set:<global>=<value>` or dropped shader keys) and log present times
|
||||
DM_NVN_UBP_CHECK diagnostic `=1` checks every uniform shadow made from memory: the pass ends, the updates land, the replay waits for the GPU and compares (a difference is a red line; slow)
|
||||
DM_NVN_UB_COPY_THREAD switch `0` copies uniform shadows of 512 bytes or more on the submit thread, as before (by default a worker copies them)
|
||||
DM_NVN_UB_DEFER switch `0` ends the render pass at every inline uniform update, as before; by default an update inside an open pass is deferred to the pass end and later draws read a shadow (off in TBDR mode)
|
||||
DM_NVN_UB_LAZY switch `=<n>`: the most deferred inline uniform updates kept across a render pass end (default 160); they land before the next copy, a conflicting draw or dispatch, or the submission end; `0` lands them at every pass end
|
||||
DM_NVN_UB_N diagnostic `=<frames>`: the length of each phase of the same-process alternating measurement `DM_NVN_UB` (default 180, minimum 60)
|
||||
DM_NVN_UB_PARTIAL switch `0`: a uniform block the deferred updates cover only in part ends the pass, as before; by default its shadow starts from memory and takes the submission's queued updates (on by default)
|
||||
DM_NVN_UDC_AB diagnostic `=1` starts with the DontCare store for unwritten attachments off and turns it on after the second screenshot, for an on/off picture comparison in one run
|
||||
DM_NVN_UNWRITTEN_DC switch `0` turns off storing attachments a render pass did not write as DontCare (on by default; part of the TBDR store lookahead), for equivalence comparisons
|
||||
DM_NVN_VBUF_CACHE switch `0` turns off the cache of resolved vertex stream addresses and vertex argument table writes across draws (on by default), for A/B runs
|
||||
DM_NVN_VCONV_CHECK diagnostic `=1` scans the indices again at every reuse of a cached vertex conversion range (`vconv_range`) and reports differences
|
||||
DM_NVN_VCONV_CPU switch `=1` converts integer vertex inputs fed formats of another class on the CPU, as before, instead of through the shader's shadow inputs (`<key>_bv`)
|
||||
DM_NVN_VCONV_RANGE switch `0` / `1` overrides the profile's `vconv_range`: CPU vertex conversion only over the vertices each draw reads instead of the whole bound range
|
||||
DM_NVN_VLOG diagnostic Set: logs each NVN memory pool as it is created (address, memory, size, flags) with a backtrace
|
||||
DM_NVN_VSDUMP path Output file for post-vertex captures; used with DM_NVN_VSDUMP_KEY and the frame/count bounds
|
||||
DM_NVN_VSDUMP_FROM diagnostic First presented frame eligible for a post-vertex capture (default 0)
|
||||
DM_NVN_VSDUMP_KEY diagnostic Vertex key to capture, or `*` for all eligible vertex shaders; storage-writing shaders are excluded
|
||||
DM_NVN_VSDUMP_MAX diagnostic Maximum post-vertex captures (default 1, or 256 for the wildcard key)
|
||||
DM_NVN_WAITS diagnostic `=1` turns on the wait ledger: every place a host or game thread waits on the CPU for the GPU or the encode thread, counted per frame
|
||||
DM_NVN_XV switch `0` turns off the cross-submission attachment validity ledger, which skips write-backs of colour attachments cleared before their next use (on by default, only with `DM_NVN_TBDR` on)
|
||||
DM_NVN_ZBLOCK diagnostic `=1` logs (`[zb]`) the all-zero blocks per level of each ASTC texture uploaded on the CPU (magenta when drawn); `=<folder>` also writes the raw storage of the first one per format and size
|
||||
DM_NVN_ZSKIP switch `0` turns off the zero-source skip, which drops a draw whose fragment shader is proven to write 0 over targets already 0 (on by default, only with `DM_NVN_TBDR` on)
|
||||
DM_OE_SCRIPT diagnostic Scripted applet state changes at given seconds, `key=value@seconds,…` with keys `focus`, `opmode` and `bg` (`bg=1` / `bg=0` run the whole background round trip), e.g. `bg=1@20,bg=0@25`
|
||||
DM_OODLE_HOST path The Mac host binary that `shader_split.py` runs in its Oodle tool mode to decode Trinity packs (`ONEPACK` files); set by the NSP script
|
||||
DM_OODLE_JOB path `=<job file>`: the host runs the game's own Oodle decoder on every record of the job file right before `nnMain`, writes `<job file>.out` and exits (Unreal shader archives)
|
||||
DM_OODLE_MODULES path The rewritten modules the Mac host's Oodle tool mode loads when `shader_split.py` decodes Trinity packs; set by the NSP script
|
||||
DM_OODLE_SERVE switch `=1`: the host answers decode requests on stdin / stdout with the game's own Oodle (or LZ5) decoder from the guest main thread before the SDK starts, until stdin ends
|
||||
DM_OPMODE player The operation mode the game sees: `0` handheld, `1` console (default: the profile's `op_mode`, console unless set); read once at startup
|
||||
DM_PAD_MERGE switch `=1` on the Mac merges every controller into player 1, as before, instead of giving each controller a player of its own
|
||||
DM_PATCH_FRONT switch `=<bytes>`: the size of the patch area right before the module base for a long text (`BIG_TEXT`); set only by the rewriter's own rerun with the size it needs
|
||||
DM_PREEMPT switch `0` / `1` overrides the profile's `core_preempt`: also check already-runnable higher-priority peers on the caller's core after every thread SVC
|
||||
DM_PRELOAD diagnostic Set: on the Mac, runs the host's early background loading (`dm_host_preload_start`) as the iOS app does, to exercise that path
|
||||
DM_PRESENT_FIFO switch `0` makes the display link take the newest queued frame each tick, as before; by default it shows frames in order, at most one tick behind
|
||||
DM_PRESENT_HZ switch `=<N>`: the frequency of presentation pacing (default 60); `0` means no frame limit and is for performance profiling only
|
||||
DM_PRESENT_INTERVAL switch `display` presents one frame per display-link tick whatever interval the game sets; `game` follows the game's present interval (default: the profile's `present_interval`)
|
||||
DM_PRESENT_LATENCY switch `=<n>` (above 0): the display link's preferred frame latency (default 1)
|
||||
DM_PRESENT_LINK switch Present pacing: `0` acquires the drawable itself, `1` (the default) presents on a 60 Hz display link, `2` uses a 120 Hz display link with a frame every other tick
|
||||
DM_PRESENT_SNAP player `=<n>`: present snapshots, a ring of n host textures (2 to 4; `1` = 2) the presents are copied to so the game's swapchain texture is free at once; `0` off (default: the profile's `present_snap`)
|
||||
DM_PRESENT_UPSCALE player `metalfx` scales a picture smaller than its rectangle on the screen through MetalFX's spatial scaler; `off` (the default) scales it bilinearly
|
||||
DM_PROBE diagnostic `=module:offset,…` (Mac only): puts a `brk` at each guest address and on every hit prints the thread, registers, strings they point to and a backtrace, then runs the original instruction
|
||||
DM_PROBE_ALL diagnostic Set: probe hits (`DM_PROBE`) also print x9–x18
|
||||
DM_PROBE_DUMP diagnostic `=module:offset:length[:file]`: on the first hit of any probe, writes that range of guest memory to a file (default `run/<platform>/probe_dump.bin`)
|
||||
DM_PROBE_FRAMES diagnostic Presented frame or inclusive `start-end` range in which guest probes print observations
|
||||
DM_PROBE_HEAP diagnostic `=<name>,<start>,<size>,<free list>,<used list>`: hex offsets of the sead heap layout the probes' `!xN` heap walk uses (default Mario's `0,28,30,b0,c8`)
|
||||
DM_PROBE_HEAPFRAME diagnostic `=<vtable offset in main>` (hex): the probes' heap walk treats heaps of that class as frame heaps (head +0xe0, tail +0xe8)
|
||||
DM_PROBE_HEAPNAME diagnostic `=<text>`: the probes' `!xN` heap walk reports only hits on heaps whose name contains the text
|
||||
DM_PROBE_HEAPRAW diagnostic Set: the probes' heap walk also prints the heap object's first 0x130 bytes
|
||||
DM_PROBE_HEAPWORDS diagnostic Set: the probes' heap walk also prints each used block's first two words
|
||||
DM_PROBE_LIMIT diagnostic Maximum printed hits per guest probe; 0 or absent leaves the count unlimited
|
||||
DM_PROBE_POLL diagnostic `=<address>[+<length>]` (hex, 4 bytes by default, up to 8): a host thread spins on the bytes and on each change snapshots every thread's pc, lr and frames, for writes the watch misses
|
||||
DM_PROBE_WATCH diagnostic `=<address>[+<length>]` (hex, 8 bytes by default): write-protects that host page and reports who writes these bytes, without a debugger
|
||||
DM_PROBE_X2 diagnostic `=<bytes>` (hex): probe hits also dump that many bytes of the memory x2 points to
|
||||
DM_PROGRAM_ID switch `=<hex>`: the program ID the host reports from `svcGetInfo(ProgramId)` (default: the profile's `program_id`)
|
||||
DM_PSO_ARCHIVE path The folder of the per-game Metal 4 pipeline archive of pipeline bases (default `pso_archive/` beside `pso_user.tsv`); `off` keeps no archive
|
||||
DM_PSO_BUNDLE path The pipeline archive made offline (`<GPU architecture>.mtl4a`, `prebuilt.tsv`), read only; the iOS app and Mac launcher set it to their `pso/` folder; `off` ignores it
|
||||
DM_PSO_GPUS switch `build_ipa.sh`: the GPU architectures, comma-separated, to build the offline pipeline archive for (`none` builds none); unset, `g18p` for a profile with `pso_mtl4 on`, else none
|
||||
DM_PSO_HYBRID switch `0`: a draw whose pipeline is not built yet waits for it instead of drawing with its shader pair's pulling pipeline meanwhile (on by default with the Metal 4 path)
|
||||
DM_PSO_IOS_MIN switch `build_ipa.sh`: the minimum iOS version of the offline pipeline archives (default the SDK's major version); an older system cannot open them and builds the bases itself
|
||||
DM_PSO_MANIFEST path The prewarm manifest to use instead of the profile's `pso_manifest.tsv`; `off` turns the prewarm off
|
||||
DM_PSO_MTL4 player `0` builds render pipelines through the `MTLDevice` with no bases and no archive; `1` uses MTL4Compiler with the archive, hybrid pipelines and prebuild (default on on the Mac, the profile's `pso_mtl4` on iOS)
|
||||
DM_PSO_PAIRS path The prebuild's second shader pair table (default: `pso_pairs.tsv` beside the shader map)
|
||||
DM_PSO_PREBUILD player `0` turns off the prebuild of every known shader pair's pulling base into the pipeline archive (on by default); `tool` runs the prebuild alone on the Mac and exits
|
||||
DM_PSO_PREBUILD_DEPTH switch `zero`: the prebuild tool (`DM_PSO_PREBUILD=tool`) builds bases for the near = 0 depth mode; the NSP script sets it from the profile's `depth_mode`
|
||||
DM_PSO_PREBUILD_THREADS switch `=<n>`: the workers of the prebuild tool (`DM_PSO_PREBUILD=tool`; default: the performance cores less one)
|
||||
DM_PSO_PREPARE switch `0` runs the prewarm in the background while the game starts, as without a window; `1` makes the first submission wait for the prewarm lists also without a window
|
||||
DM_PSO_PREPARE_THREADS switch `=<n>`: the workers of the prepare step that builds the prewarm lists before the first frame (default: the device's active cores)
|
||||
DM_PSO_PULL_ALL diagnostic `=1`: every draw that has a pulling pipeline draws with it, never with the pipeline for its vertex layout
|
||||
DM_PSO_RECORD path `=<file>`: appends the shaders and pipelines used in the run to that file, for re-recording the prewarm manifest
|
||||
DM_PSO_USER path The per-device list of shaders and pipelines built on the spot (default: `pso_user.tsv` in the parent of `DM_SAVE`)
|
||||
DM_PSO_WAITLOG diagnostic `=1` logs each draw that waited more than 20 ms for its pipeline, with its shader pair and why no stand-in served it
|
||||
DM_QOS_EDGES switch `=e1,e2`: the Switch priority edges of `DM_QOS_MAP` (default `44,59`)
|
||||
DM_QOS_MAP switch `=1` maps guest thread priority to QoS class: ≤ 44 user-interactive, 45–59 user-initiated, the rest utility (off by default: every guest thread runs user-interactive)
|
||||
DM_RENDER_SCALE player `0.5` to `1.0`: fixes the game's 3D views at that fraction of their size and turns its dynamic resolution off, for builds with a render scale stub; `1.0` (the default) keeps it
|
||||
DM_RESOURCE_AOC path The add-on content folder the iOS build copies into the app's `aoc/` (empty for none); written by `gen_resources.py`, read by `stage_resources.sh`
|
||||
DM_RESOURCE_CHAIN_H path Path of `dm/gfx/nvn/dmsh_chain.h`, written to `Gen/resources.env` by `build_ipa.sh`; the Xcode copy phase (`stage_resources.sh`) stops when the prepared shaders are not of its chain version
|
||||
DM_RESOURCE_MARKER path A file under the RomFS, written to `Gen/resources.env` by `build_ipa.sh`, that the Xcode copy phase (`stage_resources.sh`) checks for before copying the game files
|
||||
DM_RESOURCE_MSLDIR path Name of the shader library folder (`b-<chain>`), written to `Gen/resources.env` by `build_ipa.sh`; the Xcode copy phase copies the shaders to `msl/<name>` in the app
|
||||
DM_RESOURCE_PSO path Folder of the offline Metal pipeline archive (empty for none), written to `Gen/resources.env` by `build_ipa.sh`; the Xcode copy phase copies it to `<App>/pso`
|
||||
DM_RESOURCE_ROMFS path RomFS folder, written to `Gen/resources.env` by `build_ipa.sh`; the Xcode copy phase (`stage_resources.sh`) copies it into the app's `romfs`
|
||||
DM_RESOURCE_SHADERS path iOS shader library folder, written to `Gen/resources.env` by `build_ipa.sh`; the Xcode copy phase (`stage_resources.sh`) copies it into the app's `msl/b-<chain>`
|
||||
DM_RES_CHECK diagnostic `=1` prints the first 40 textures or buffers a shader reaches through its argument block that are not in the residency set (textures attributed to their parent allocation)
|
||||
DM_ROMFS path The game's RomFS folder, mounted read-only as `rom` (default `$DM_ROOT/romfs`); the Mac launcher, the iOS app and the NSP script set it
|
||||
DM_ROMFS_OVERLAY path `=<dir>[:<dir>…]`: RomFS reads look in these folders first, in order, and fall back to `DM_ROMFS` (for a modified copy of some files)
|
||||
DM_ROOT path Workspace root (default `~/dm-work`): the host's and tools' default paths (RomFS, temporary storage, shader libraries under `build/cache/metal_shader_cache`, probe dumps) are under it
|
||||
DM_RT diagnostic `0` leaves the thread that acquires the window on its QoS only, with no Mach time-constraint policy declaring the frame deadline (on by default; for power A/B)
|
||||
DM_RT_COMP switch `=<ms>` fixes the computation the present thread's time-constraint policy declares; default `auto` re-declares it every 120 frames from that thread's per-frame CPU time (p90 and peak)
|
||||
DM_RUMBLE player `0` turns game rumble off (on by default); launcher setting
|
||||
DM_RUMBLE_GAIN player `=0..3` scales the rumble strength (default 1, `0` is silent); launcher setting
|
||||
DM_RUMBLE_LOG diagnostic `=1` logs the game's vibration handle requests and, once a second, the values it sent, how many had no routed output and the peak amplitude
|
||||
DM_SAVE path This run's save folder (must be set); the device save is `<DM_SAVE>_device`, and `pso_user.tsv` goes in its parent folder; the Mac launcher, the iOS app and the NSP script set it
|
||||
DM_SCHEME_NAME switch Set by `build_ipa.sh` itself for xcodegen's `project.yml`: the Xcode scheme name, `DmIOS`, or `<display name> (<title ID>)` for a project prepared with `--prepare-xcode`
|
||||
DM_SCREEN_MATCH switch `0` keeps the screen's refresh rate in fullscreen; by default a Mac screen whose rate is not a multiple of the game's frame rate switches to one that is (144 → 120 Hz)
|
||||
DM_SF_TRACE diagnostic `=1` logs every request on the game's own HIPC client sessions that the host answers (`hle_sf.c`)
|
||||
DM_SHAKE_G switch `=<G>`: on iOS, the acceleration beyond gravity above which a flick of the phone shakes the controller's six-axis sensor (default 1.5)
|
||||
DM_SHARED_FONT path Font file served for every shared-font type except the Nintendo extension (default `$DM_FONT_DIR/standard.ttf`, then macOS's Arial Unicode)
|
||||
DM_SHARED_FONT_EXT path Font file served for shared-font type 5, the Nintendo extension with the button icons (default `$DM_FONT_DIR/nintendo_ext.ttf`, made by `make_ext_font.py`)
|
||||
DM_SHOW_FPS player `1` shows the frame-rate counter on the iPhone (frames put on screen per second, top left); at IPA build it sets `DMShowFPS` in `Info.plist`, which `Documents/env.txt` overrides
|
||||
DM_SLOWFRAME diagnostic `=1` writes the per-frame timing breakdown to the log only for dropped frames (kept in a ring of the last 16 frames, judged 8 frames late)
|
||||
DM_SPIN switch `0` makes the rewriter's spin gate return at once, so guest poll loops spin with no sleep; the gate stays (on by default)
|
||||
DM_SPIN_LIST diagnostic `=1` makes `patch_nso.py` print every guest poll loop it found and sends through a spin gate
|
||||
DM_SPIN_POLL switch `=<µs>`: how long a spin-gate wait polls before it sleeps with the yield back-off (default 2000; `0` yields on every pass)
|
||||
DM_SVC diagnostic `=1` logs memory SVCs (`SetHeapSize`, `MapMemory`, `Map`/`UnmapPhysicalMemory`) with their addresses and sizes
|
||||
DM_SYMCHECK diagnostic `=1` checks every hashed module symbol lookup against a linear search and logs a red line for each mismatch
|
||||
DM_TEAM_ID switch Apple team ID for the per-game Xcode project, used when `--team-id` / `TEAM_ID` is not given; `build_ipa.sh` uses the placeholder `0000000000` (unsigned) when unset
|
||||
DM_TEMP path Temporary storage folder (default `$DM_ROOT/run/platform/temp`); stand-in system data goes in `<DM_TEMP>_sysdata`; the Mac launcher, the iOS app and the NSP script set it
|
||||
DM_TEST_FS_FAIL diagnostic `=write:<n>[:<errno>]` makes the nth save write fail with that errno (default ENOSPC), which refuses the save transaction's commit
|
||||
DM_TEST_FS_FAILAT diagnostic `=<point>[:<count>][,…]` makes steps of the save transaction (`mountclone`, `reclone`, `guestname`, `renameback`) fail with ENOSPC
|
||||
DM_TEST_GEOMETRY test Path of the native `t_geometry` executable; `test_dmsh_geometry.py` runs its native geometry emitter checks only when it is set
|
||||
DM_TEST_KEYRACE diagnostic `=<n>` creates up to n (at most 512) pthread keys on a thread of its own while the host starts up
|
||||
DM_TEST_KILL_AT diagnostic `=write:<n>` exits the process (`_exit(9)`) at the nth save write, imitating a kill mid-write
|
||||
DM_TEST_LEAK_GPU diagnostic `=1` allocates and keeps a 1 MB shared Metal buffer every 100 ticks: a GPU memory leak on demand
|
||||
DM_TEST_LINK_STALL diagnostic `=<start s>,<duration s>`: counting from the first present, pauses the display link at that point for that long, to check the present fallback
|
||||
DM_TEST_NO_METAL4 diagnostic `=1` treats the GPU as one without Metal 4, as on a virtual Mac: the NVN layer starts only its classic part and drops every present
|
||||
DM_TEST_PRESENT_LAG diagnostic `=<every n>,<ms>[,<in a row>][;…]` sleeps that long before every nth present (and the ones after it in the run): a slow frame on demand
|
||||
DM_TEST_SWKBD_HOLD_MS diagnostic `=<ms>` (with `DM_KBD`) keeps the system keyboard up that long before `DM_KBD`'s text is returned, as a player typing would
|
||||
DM_TEST_SWKBD_LAG_MS diagnostic `=<ms>` sleeps that long between the keyboard handing back its text and the game's thread getting the result, to check that auto-accept does not act early
|
||||
DM_TEST_TRANSFORM_FEEDBACK test Path to the native t_transform_feedback executable used by the Python feedback tests
|
||||
DM_TEST_UP_LEAK diagnostic `=1` keeps the old upload staging block instead of retiring it, as before the fix, to measure the leak
|
||||
DM_TEST_UP_LOST diagnostic `=<batch index>` treats that batch of uploads as dropped, to exercise the re-upload path
|
||||
DM_THERMAL_CAP switch `0` turns off the thermal cap, which holds the frame step at 30 fps while iOS reports a serious or critical thermal state (on by default)
|
||||
DM_THR diagnostic `=1` logs each SVC (except `SleepThread`) with its first four arguments, and each `CreateThread` with entry, stack, priority and core
|
||||
DM_THRCPU diagnostic `=<tsv path>[,<seconds>]` writes each thread's CPU time (100% = one core) and the total every interval (default 10 s); on iOS a relative path is under `Documents`
|
||||
DM_TOUCH_HAPTICS player `0` turns off the light haptic tap when a touch control is pressed (on by default); launcher setting
|
||||
DM_TOUCH_LOG diagnostic `=1` logs each touch-screen read with touch points (sample, finger, position, attributes) and each gesture entry, up to 500 lines
|
||||
DM_UDC_SELFTEST diagnostic `=1` runs a start-up check of DontCare load and store on private depth-stencil and RGBA8 textures (clear, Load + DontCare, read back) and logs one result line
|
||||
DM_UE_SHADERS path Folder of the Unreal shader code archives decoded by `uepak.py`, for `shader_split.py` (default `ue_shaders` beside the RomFS); needed when the RomFS has Unreal paks
|
||||
DM_VK_CAPTURE path `=<dir>` (Mac) writes every SPIR-V module the game creates to `<dir>/<key>.spv`; the NSP script's Vulkan shader capture stage sets it
|
||||
DM_VK_LOG diagnostic `=1` logs Vulkan object creation in the Vulkan-on-Metal layer
|
||||
DM_VSYNC_HZ switch `=<Hz>`: rate at which a host thread fires the vi vsync event when the display link does not drive it (default 60)
|
||||
DM_WATCH diagnostic `=<s>` dumps all threads after that many seconds (SIGALRM) and turns on the stall watchdog; off unless set
|
||||
DM_WATCH_SAMPLE diagnostic `=<path>` (Mac): when the stall watchdog fires, samples the process for 1 s with `/usr/bin/sample` and writes the thread stacks to that path
|
||||
DM_WATCH_STALL diagnostic `=<s>`: when the frame loop makes no progress for that long (default 15 with `DM_WATCH`), logs a red line, dumps the threads and exits
|
||||
DM_WINDOW switch When set (to any value, `0` included) opens a Mac window with keyboard and controller input; unset runs headless with the guest on the current thread; the Mac launcher sets it
|
||||
DM_XCODE_DERIVED_DATA path Xcode derived data folder for `build_ipa.sh` (default `$W/build/ios/xcode-dd`); the NSP script points it into the game's build folder
|
||||
DM_XCODE_PROJECT_DIR path Folder of the per-game Xcode project that `build_ipa.sh --prepare-xcode` makes (default `$W/xcode`); the NSP script sets it
|
||||
DM_XV_FILE path File where the attachment validity ledger keeps the keys it disabled after a violation, read at the next launch
|
||||
DM_YIELD_BACKOFF switch `0` turns off the yield back-off; `=<cap µs>[:<start µs>]` sets its sleep cap and the run of yields after which it starts (default 250:2000)
|
||||
|
Can't render this file because it contains an unexpected character in line 83 and column 134.
|
@@ -1,83 +0,0 @@
|
||||
# Packed image-atomic binding admission
|
||||
|
||||
MSL 2.3 represents an image atomic with two arguments: the IMG texture supplies
|
||||
its dimensions, while IMGA is an atomic-word buffer pointer. Capacity and layout
|
||||
must agree **before** either is used by a draw or dispatch. This host check is
|
||||
separate from the shader translator's per-coordinate bounds checks: neither
|
||||
replaces the other. Packed addressing assumes the translator's reviewed default
|
||||
linear alignment of four bytes per 32-bit word.
|
||||
|
||||
`encode_args_` now resolves each IMGA's unique, same-register IMG once under the
|
||||
existing registry lock, then uses that exact texture for both arguments. It does
|
||||
not change argument-cache reuse, queue submission, resource residency, or deferred
|
||||
resource lifetime.
|
||||
|
||||
## Admitted paths
|
||||
|
||||
- Real images must resolve to a scalar R32 integer texture of the shader's
|
||||
texture type: buffer, 2D, or 2D array, without multisampling.
|
||||
- Direct guest-pool writes require an actual non-view buffer texture or base 2D
|
||||
texture, the matching guest dimensions/format size, one mip, the same Metal
|
||||
backing buffer and base offset, and enough bytes in both guest storage and the
|
||||
buffer. For 2D the actual buffer row pitch must equal width times four. A buffer
|
||||
texture's unused next row may be padded.
|
||||
- The existing **discard scratch** path for non-linear images is retained, not
|
||||
made coherent with the image. Its previous source-sized request, 1 MiB rounding,
|
||||
reuse and deferred replacement policy remain for admitted images. Checked
|
||||
arithmetic and actual IMG dimensions ensure the existing request covers the
|
||||
matched image; no request is enlarged to conceal a mismatch.
|
||||
- An unbound IMG may retain the existing dummy pointer only if its actual dummy
|
||||
extent fits and neither side resolves to a real image.
|
||||
|
||||
## Fail-closed limits
|
||||
|
||||
Direct padded-row textures, array images, original 1D descriptors (even when
|
||||
normalized to a one-row Metal 2D texture), mip/layer/format views, private fallback
|
||||
textures without the expected buffer, unsupported formats/shapes, failed scratch
|
||||
allocation, and capacity/arithmetic failures stop with a diagnostic before the
|
||||
argument buffer is bound and before the offending draw/dispatch. This is a
|
||||
conservative trial compatibility restriction; a safe but unproved direct view is
|
||||
also rejected. Real linear writes are **never redirected to discard scratch**.
|
||||
The translator's existing 1D/1D-array normalization is preserved, but that alone
|
||||
does not prove direct native storage. Non-linear images still follow the
|
||||
separate matched-extent discard-scratch admission above. Buffer shader routes
|
||||
remain scalar and are not rewritten by the 2D MSL helper; direct buffer host
|
||||
admission still requires the actual backing buffer and offset checks.
|
||||
|
||||
The decisive diagnostic suffixes are:
|
||||
|
||||
- `ambiguous IMG partner` / `missing IMG partner`
|
||||
- `unsupported or overflowing IMG extent`
|
||||
- `real image is not a resolved R32 integer IMG`
|
||||
- `unsupported direct IMG/IMGA layout or capacity`
|
||||
- `scratch allocation or capacity failed; refusing short dummy`
|
||||
- `unmatched unbound IMG/IMGA pair`
|
||||
|
||||
Each is prefixed by `image atomics:` and includes shader/stage/slot context.
|
||||
These are fatal errors, not warning suppression or silent skipped draws. The
|
||||
former scratch-allocation failure branch could return a short 64 KiB dummy while
|
||||
IMG retained its original extent; it can no longer do so. This source-level
|
||||
failure path has not been observed in the supplied validation run, which had a
|
||||
real scratch buffer. This patch does not establish that failure path, direct-pool
|
||||
layout mismatch, or residency caused an earlier GPU fault.
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
make -C dm/gfx/tests GAME=0100F43008C44000 imga-layout
|
||||
ASAN_OPTIONS=detect_leaks=0 make -C dm/gfx/tests GAME=0100F43008C44000 imga-layout-sanitize
|
||||
python3 dm/gfx/tests/test_imga_binding_source.py
|
||||
```
|
||||
|
||||
The portable fixture executes production checked arithmetic/admission helpers:
|
||||
3 MiB versus 64 KiB capacity, valid packed layouts, overflow, zero dimensions,
|
||||
short storage, wrong buffer/offset, view rejection, padding and direct arrays.
|
||||
The source checks establish bounded wiring invariants, not Metal execution.
|
||||
The fixture does not execute Metal binding. Native Objective-C compilation,
|
||||
actual Metal texture-property behavior and game compatibility require separate
|
||||
Apple validation. No allocator, texture readout,
|
||||
shader algorithm, or game eligibility changes are included.
|
||||
|
||||
The buffer metadata comes from Apple's read-only [buffer offset](https://developer.apple.com/documentation/metal/mtltexture/bufferoffset?language=objc)
|
||||
and [row pitch](https://developer.apple.com/documentation/metal/mtltexture/bufferbytesperrow)
|
||||
properties; row/offset are queried only when a source buffer is present.
|
||||
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 288 KiB |
@@ -36,6 +36,13 @@ For a replaced DSP renderer the size query keeps the service's work-buffer contr
|
||||
- **Fix:** a thread-local flag (`t_in_open`) hands the inner call straight to the SDK with the host's CPU / manual copy of the parameter, and only the outer call starts the thread (`hle_audren.c`).
|
||||
- **Found on:** [ROMANCE OF THE THREE KINGDOMS 8 REMAKE](../../dm/games/0100C2A01A730000/README.md) (SDK 17.5.3).
|
||||
|
||||
### The renderer event as the service gives it: a kernel event
|
||||
|
||||
- **Symptom:** the frame loop stops for good as a level loads while audio keeps rendering. The main thread waits in `os::WaitSystemEvent` under `CAudioMemoryPool::Shutdown`, and a sound thread of higher priority waits on the same condition variable.
|
||||
- **Cause:** with the DSP service (device 0, what the game asks for) `OpenAudioRenderer` attaches the service's renderer event, a kernel readable event (`AttachReadableHandleToSystemEvent`, auto-clear). With CPU rendering and manual execution, which the host switches to ([Running the SDK's own renderer on the CPU](#running-the-sdks-own-renderer-on-the-cpu)), it creates a local one instead (`CreateSystemEvent(event, AutoClear, false)`, SDK 4: `sdk+0x190b6c`). Signalling a local auto-clear event wakes one waiter, the one of highest priority. A kernel event wakes every waiter, and each one tries `ResetSignal`. Donkey Kong Country: Tropical Freeze waits on the event from two threads: its sound thread (priority 29, `main+0x971e8`) every frame, and the main thread while it detaches an audio memory pool (`main+0x6fa8c`: `RequestDetachMemoryPool`, then wait and update until `IsMemoryPoolAttached` is false). The sound thread took every wake, and the main thread never woke again.
|
||||
- **Fix:** when the game opened the renderer for the service, the host replaces the SDK's local event with an inter-process auto-clear one (`DestroySystemEvent`, then `hle_bind_system_event(event, 1)` in `ar_open`). The SDK's renderer signals through `os::SignalSystemEvent`, which handles either kind. A game that opens a CPU renderer itself keeps the SDK's local event.
|
||||
- **Found on:** [Donkey Kong Country: Tropical Freeze](../../dm/games/0100C1F0051B6000/README.md) (SDK 4.4.0): the first level's load after the intro cinematic.
|
||||
|
||||
### The renderer's update lock (`update_lock`)
|
||||
|
||||
- **Symptom:** none of its own; command generation races the game's `Update` on SDKs whose lock is not at SDK 16's offset.
|
||||
@@ -44,6 +51,13 @@ For a replaced DSP renderer the size query keeps the service's work-buffer contr
|
||||
- **Found on:** [Mario Kart 8 Deluxe](../../dm/games/0100152000022000/README.md) (4.4), the first game it was plainly wrong for.
|
||||
- **Check:** checked with sound on [Big Brain Academy: Brain vs. Brain](../../dm/games/0100620012D6E000/README.md) (11.4, +0x1d8), [DRAGON QUEST XI S](../../dm/games/01006C300E9F0000/README.md) (7.3, `MutexType` +0x1d8) and [Super Mario Odyssey](../../dm/games/0100000000010000/README.md) (7.3, which is silent for its first minute with either lock).
|
||||
|
||||
### No `SdkMutexType` in SDK 3
|
||||
|
||||
- **Symptom:** `FATAL audren: missing SDK symbol _ZN2nn2os12SdkMutexType4LockEv` right after the renderer's render thread starts.
|
||||
- **Cause:** the render path looked up `SdkMutexType::Lock` / `Unlock` before reading which lock the SDK's `Update` takes ([The renderer's update lock](#the-renderers-update-lock-update_lock)); SDK 3.5.1's `sdk` has no `SdkMutexType` and takes `nn::os::LockMutex` at +0x1f8.
|
||||
- **Fix:** `render_once` looks up the `SdkMutexType` functions only when `update_lock` does not find `LockMutex` (`hle_audren.c`).
|
||||
- **Found on:** [OCTOPATH TRAVELER](../../dm/games/010057D006492000/README.md) (SDK 3.5.1), the first SDK 3 game through the audio renderer (Bayonetta 2's 3.6.0 plays through `AudioOut`).
|
||||
|
||||
### The render thread's route through the public `ExecuteAudioRendererRendering` (SDK 20)
|
||||
|
||||
- **Symptom:** `CommandListProcessor::Process` crashes now and then (SIGSEGV under `render_main`, once at 60 s and once at 134 s in the shrine); every `RequestUpdate` renders a catch-up frame.
|
||||
@@ -70,6 +84,13 @@ For a replaced DSP renderer the size query keeps the service's work-buffer contr
|
||||
|
||||
## Command lists by SDK
|
||||
|
||||
### Incomplete startup lists must not select the command layout
|
||||
|
||||
- **Symptom:** no sound after boot; `audren: no DeviceSink (ID 0x13) in the command list` and a malformed-command error persist even when later command lists are valid.
|
||||
- **Cause:** the host's render thread can generate commands before the game's first `RequestUpdate`. Mario Kart 8 Deluxe's first list ended in a zeroed third command, so none of the supported header layouts could walk it. `capture` nevertheless latched the default layout and consumed the one-time sink scan. Later SDK 6 headers were read as SDK 8–20 headers, and SDK 4's DeviceSink was never found. Retrying only header detection still leaves the sink scan consumed.
|
||||
- **Fix:** select the header only when the whole list walks in a supported layout. Until then, return a silent frame before the sink scan; the first valid list can select both the header and its sink. `t_audren_layout` reproduces the failure and checks stereo PCM for SDK 4, 6, 16 and 21, with valid-first, empty and incomplete startup lists and SDK 21's short first list.
|
||||
- **Found on:** [Mario Kart 8 Deluxe](../../dm/games/0100152000022000/README.md) (SDK 4.4.0; [PR #331](https://github.com/seedds/nxnative/pull/331)).
|
||||
|
||||
### The final mix is taken from the DeviceSink's mix buffers
|
||||
|
||||
- **Symptom:** the CPU processor drops the final mix.
|
||||
@@ -88,7 +109,7 @@ For a replaced DSP renderer the size query keeps the service's work-buffer contr
|
||||
|
||||
- **Symptom:** no sound; red `audren: command 1 in the command list is malformed (magic 0 size 0)`.
|
||||
- **Cause:** SDK 6's DSP command header is +0x04 size (s16), +0x06 command ID (s16), +0x10 enabled, where SDK 8–20 have +0x04 enabled, +0x05 ID, +0x06 size (`CommandListProcessor::Process`, `sdk+0xaaa00`); its DeviceSink is ID 0x10.
|
||||
- **Fix:** `capture` in `hle_audren.c` takes the layout whose walk finds the 0xcafebabe magic on every command of the first full list.
|
||||
- **Fix:** `capture` in `hle_audren.c` takes the layout whose walk finds the 0xcafebabe magic on every command of the first valid list; [incomplete startup lists](#incomplete-startup-lists-must-not-select-the-command-layout) leave detection open.
|
||||
- **Found on:** [Fire Emblem: Three Houses](../../dm/games/010055D009F78000/README.md) (SDK 6.5.0).
|
||||
|
||||
### SDK 8: command IDs numbered differently
|
||||
@@ -129,6 +150,15 @@ For a replaced DSP renderer the size query keeps the service's work-buffer contr
|
||||
- **Fix:** `AVAudioSession` `setPreferredIOBufferDuration:0.005` (and 48 kHz) before the game opens audio and again on return to the foreground (`front_ios_audio_session` in `front_ios.m`); the device then calls back with 240 frames and nothing underruns.
|
||||
- **Check:** when checking a new device, read the host's `audio #N device callbacks` line, logged every 2000 callbacks: frames per callback, queued blocks, underruns.
|
||||
|
||||
### The Mac's default I/O buffer
|
||||
|
||||
- **Symptom:** clicks and dropouts on the Mac, heard as clipping; the log's `audio #N device callbacks` lines show hundreds of underruns per 2000 callbacks. Closing an output can also log `[RED] audio device buffer: ... (status 560947818)` (`!obj`).
|
||||
- **Cause:** the Mac's default output asks for 512 device frames per callback (10.7 ms at 48 kHz; 557 frames of the 48 kHz stream when the device runs at 44.1 kHz), more than a game that keeps only a few small blocks queued has ready every time. Unicorn Overlord on a MacBook Pro's speakers: 250–290 underruns in every 2000 callbacks at 48 kHz (0.1–0.2 s of zeros), about 1,000 at 44.1 kHz. Whether a game underruns depends on how much it queues: 13 Sentinels (2.8 blocks queued, like Unicorn Overlord's 3.0, but larger ones) had none at 512 frames.
|
||||
- **Fix:** like iOS's session buffer, the Mac sets its output unit's device buffer to about 5 ms of device frames (`kAudioDevicePropertyBufferFrameSize`, 240 at 48 kHz, 221 at 44.1 kHz), within the device's supported range (`au_buffer` in `hle_audio.c`). It reapplies this when the default output changes and when the same device's output sample rate changes; `CurrentDevice` alone does not notify a rate change. An unchanged buffer is not written again, since a write can itself notify the format listener. Both listeners are removed before uninitializing or disposing the unit, including a failed open: disposal otherwise calls the listener after the device has gone (`!obj`). The log reads back the actual frame count. `DM_DBG_AU_FRAMES=N` asks N frames instead.
|
||||
- **Found on:** [Unicorn Overlord](../../dm/games/010069401ADB8000/README.md) (Asian release 1.05 on the Mac).
|
||||
- **Check:** [PR #262](https://github.com/seedds/nxnative/pull/262) reports Unicorn Overlord at 240 frames: 0 underruns in 10,000 callbacks; 13 Sentinels stays at 0. A silent Core Audio probe on a Mac mini reproduced the disposal error and the stale buffer after a rate change, then verified the fixes: default output switched between USB at 48 kHz and speakers at 44.1 kHz with callbacks continuing, and the same speakers at 44.1 / 96 / 48 kHz used 221 / 480 / 240 frames. The original default output and device rates were restored. `make -C dm/host/tests GAME=<id> audio-device` checks setup, device and rate notifications, scope filtering, buffer limits, synchronous notifications, the diagnostic override, absent devices, close and failed-open listener cleanup without audio hardware.
|
||||
- **Open:** the listed HDMI output was not retained as the system default during the probe, so switching to it remains unvalidated. The probe verifies silent host playback; gameplay listening after these listener changes has not been repeated.
|
||||
|
||||
### Interruptions that stop RemoteIO callbacks
|
||||
|
||||
- **Symptom:** sound stays silent after Siri, an alarm, a call or another app's audio; a game whose audio thread waits for its buffers back (Wwise's poll-only output, FMOD) then waits forever.
|
||||
|
||||
@@ -8,7 +8,7 @@ Frame pacing: the host's frame step, engine frame limiters, present intervals an
|
||||
|
||||
- **Symptom:** a 30 fps game to run at 60; after the floor was lowered, slow motion on a GPU-bound device (GitHub issue #9).
|
||||
- **Cause:** find where the present interval comes from and how the logic time step follows it before patching anything. BotW's `sead::GameFrameworkNx` main loop (`main+0x112660c`) runs a dynamic interval: from the last frame's time it computes how many vertical syncs were needed, clamps that to [2, 3] (the maximum from the interval controller built at `main+0x1126c04`), steps up after three overrunning frames and down one level every six frames, and sets the result through `setWaitVsyncInterval` (vtable +0x130, `main+0x11269b8` → `nvnWindowSetPresentInterval`). The game logic follows on its own: `ksys::VFR` is initialized with a constant base interval of 2 (`main+0x127e740`), and every frame `VFR::updateInterval` (`main+0x151d3d0`) takes the framework's current interval, giving a frame ratio of interval / 2, so the engine's variable frame rate scales its time step by the present interval and the speed is right as long as the interval matches the frames. Then check what the controller measures: BotW's measured only the main thread's CPU time (about 3 ms a frame), so a GPU-bound device stayed at interval 1 and played in slow motion.
|
||||
- **Fix:** now the host's frame step in whole vsyncs: a stub at the framework's `setWaitVsyncInterval` hands the game the host's step, and frames stay on screen exactly that long. The method before it, which `DM_FRAME_STEP=legacy` still runs, cuts the game's frame-interval logic: five rewriter cuts lower the floor from 2 to 1 (its `patches.py`, build `cd57b23f…`): the candidate floor (two places) and the hysteresis targets. Two more cuts make the controller always take its GPU-time path, and the host's timestamp reports had to carry real GPU times for that path to see anything (profile key `gpu_timestamps`, [GPU timestamps per submission segment](#gpu-timestamps-per-submission-segment)).
|
||||
- **Fix:** now the host's frame step in whole vsyncs: a stub at the framework's `setWaitVsyncInterval` hands the game the host's step, and frames stay on screen exactly that long. The method before it cuts the game's frame-interval logic, and those cuts stay, for the interval the framework keeps while the game's VFR override holds (`DM_FRAME_STEP=legacy`, which ran this method on its own, was removed in 79d82f7b): five rewriter cuts lower the floor from 2 to 1 (its `patches.py`, build `cd57b23f…`): the candidate floor (two places) and the hysteresis targets. Two more cuts make the controller always take its GPU-time path, and the host's timestamp reports had to carry real GPU times for that path to see anything (profile key `gpu_timestamps`, [GPU timestamps per submission segment](#gpu-timestamps-per-submission-segment)).
|
||||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md) (1.9.0; its note has the interval controller and the legacy cuts).
|
||||
- **Check:** probes confirmed the legacy result: the framework presents at interval 1, VFR reports interval 1 with ratio 0.5, and when a frame runs long the game steps back to 2 with ratio 1, so the game speed does not change.
|
||||
- **Note:** the zeldaret/botw decompilation (`VFR.cpp`) gave the semantics; 1.9.0's addresses were found by the constants (`60 / interval`, the `fmov s11, #2.0` floor).
|
||||
@@ -102,6 +102,14 @@ Frame pacing: the host's frame step, engine frame limiters, present intervals an
|
||||
- **Check:** `DM_TEST_PRESENT_LAG=90,30` (sleep 30 ms before every 90th present) makes Mario do this on the Mac; run it with `DM_WINDOW=1` to get the display link. `DM_NVN_TSV` writes each frame's interval as `@itv`.
|
||||
- **Note:** the windowless Mac host was never affected by the acquire: there the acquire sleeps until the interval's slot.
|
||||
|
||||
### One frame per tick of a 120 Hz display link
|
||||
|
||||
- **Symptom:** with `present_interval display`, `DM_PRESENT_HZ=120` and `DM_PRESENT_LINK=2` (a 120 Hz display link) a game stays at 60 fps although its frame takes 8 ms; the display link logs about two ticks per present. With a requested 90 Hz, truncating the tick conversion instead allows 120 fps (reproduced through the real acquire with simulated link ticks).
|
||||
- **Cause:** the acquire waits for the present interval's vsyncs on the display link (`mtl_link_wait_tick`), counted in 60 Hz vsyncs, so two ticks of a 120 Hz link per frame; display mode's interval is 1. Dividing the link rate by the requested rate once per acquire loses fractional ticks (120 / 90 becomes 1).
|
||||
- **Fix:** in display mode above 60 Hz the acquire waits as many link ticks as one tick of the present rate takes (`link_wait` in `hle_nvn.c`, `mtl_link_wait_ticks`): one at 120 Hz. Fractional ticks carry across acquires: at 90 Hz, three frames wait four link ticks (1, 1, 2); a rate or pacing-mode change starts a new phase. Rates at or above the link's rate wait one tick.
|
||||
- **Found on:** Batman: Arkham Knight (`0100ACD0163D0000`; its profile and note are in a PR of their own): 119.8 fps in the opening cutscene on an M2 Max.
|
||||
- **Check:** `t_link_ticks` (also run in CPU-only CI): with a controlled clock and one tick delivered per timed wait, a 60 Hz vsync waits two ticks of a 120 Hz link and one tick of a 60 Hz one, and a wait in the link's own ticks takes one tick per frame. The real acquire checks display pacing at 60, 75, 90, 120 and 240 Hz, game intervals and frame-step caps, including the 60 and 30 Hz link modes. Exact tick counts, already elapsed ticks, spurious wakes and silent-link timeouts are checked without depending on OS scheduling: a sleeping ticker could miss the acquire's wall-clock timeout on CI. These checks do not measure a game's frame rate on a screen.
|
||||
|
||||
### Present snapshots: the game's swapchain texture free at once
|
||||
|
||||
- **Symptom:** frames miss the screen while the GPU stays at its lowest clock (444 MHz on an M2 Max); more work (a higher render scale) only makes the frames slower.
|
||||
@@ -157,6 +165,14 @@ Frame pacing: the host's frame step, engine frame limiters, present intervals an
|
||||
- **Found on:** [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md), measured upstream in MacNativeGames commit `5f5de5e` and ported in [PR #195](https://github.com/seedds/nxnative/pull/195): in a shrine, game threads at priority 46 delayed the submit thread at priority 97 by about 1.8 ms a frame. Waiting fell from 155 to about 0 ms/s there, with unchanged frame time. Those game measurements have not been repeated here.
|
||||
- **Check:** `t_stencil_view`, `t_view_levels` and `t_view_swizzle` exercise production registry lookups and cached views with the new lock on the Mac.
|
||||
|
||||
### Hash-table lookups of 8-byte keys
|
||||
|
||||
- **Symptom:** in a replay profile, `_platform_memcmp` is most of the time spent in serial lookups (`tex_mtl` through `g_by_serial`).
|
||||
- **Cause:** `nvn_hm_get` compared each probed key with `memcmp` and the table's run-time key length, a libc call per probe. The serial, live-serial, ghost, view-cache and compute-pipeline tables have 8-byte keys and are looked up several times per draw.
|
||||
- **Fix:** for 8-byte keys `nvn_hm_get` loads the key as one word and compares words. The hash, probe order and answers are the same; puts and deletes are unchanged.
|
||||
- **Found on:** MacNativeGames commit `7431eb7` (38 of 55 samples of the serial lookup were `memcmp` in a Tears of the Kingdom profile). Here, a lookup in a 3,000-entry table of 8-byte keys takes 1.5 ns instead of 5.9 ns on an M4 Pro (`clang -O2`, 60 million lookups); no game frame has been measured.
|
||||
- **Check:** `t_hmap` checks 8- and 20-byte keys against a naive model through random puts, overwrites, deletes and clears.
|
||||
|
||||
### Submissions handed to the GPU in segments
|
||||
|
||||
- **Symptom:** the GPU sits idle each frame while the host translates the submission (a game that does not submit frame N+1 until frame N's GPU work is done).
|
||||
@@ -165,6 +181,7 @@ Frame pacing: the host's frame step, engine frame limiters, present intervals an
|
||||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md): a stack sample in the shrine had the main thread in `nvnSyncWait` (the fence of the frame just submitted) for 59% of the time and the render thread in `nvnQueueSubmitCommands` for 40%; the translation was about 3,600 draws and 347 passes, 7 ms, so per frame the GPU ran 13–14 ms and then sat idle 7.7 ms (`DM_NVN_TSV` rows `@gpu_beg_ms` / `@gpu_end_ms` against `@call_ms`). GPU idle per frame went from 7.7 to 3.8 ms; most of the rest is the game's own work between its fence wait and the next submit (about 2.5 ms). The other five games' frame times were unchanged; Metroid Dread's p95 dropped from 19.6 to 16.9 ms.
|
||||
- **Note:** after this, BotW's docked frame p50 was 16.7 ms (was 20.8 ms before segmenting). Host translation is 8 ms per frame (22,000 instructions per draw), spread over Metal's encoder creation and draw calls, argument encoding, Objective-C retain / release and pool lookups. The per-pass GPU timer (`DM_NVN_PASSGPU`) is switched off under Metal 4 (`pg_render` returns at once), so GPU time cannot yet be attributed to passes. Render-target traffic per frame is in `DM_NVN_TSV`'s `@rt_load_mb` / `@rt_store_mb`.
|
||||
- **Note:** with the encode thread (`DM_NVN_ENC_THREAD`) a segment's commit record is handed to it at once (`rec_publish` in `sub_cut`), not when its block of records fills. Breath of the Wild on an M2 Max (encode thread and snapshots, `DM_FPS_TARGET=120`): the frame's wait on its newest fence 12.5 ms with `DM_NVN_CUT=0`, 5.6 ms with 64, 5.35 ms with 16.
|
||||
- **Note:** MacNativeGames moved its cut to 30 draws for the first segment and 40 after (`2fa27d7`; uncapped Tears of the Kingdom in a shrine, 10.77 → 10.60 ms a frame). At this repository's 60 fps cap the size changes nothing where the GPU finishes inside the frame: Breath of the Wild outdoors on an M4 Pro (2026-10-26, the outdoor seed of the [BotW Mac routes](../../dm/games/01007EF00011E000/README.md#mac-routes), frames 3,001–5,700, cut 64) waited on its fence 0.02 ms a frame on average (`@sync_ms`), with GPU span 10.8 ms. The default stays 64; a GPU-bound device (an iPhone) is where a smaller cut could show, through `DM_NVN_CUT`.
|
||||
|
||||
### Address to memory pool lookups by binary search
|
||||
|
||||
|
||||
@@ -15,6 +15,13 @@ The guest address space, heaps, pools, virtual address memory and the allocators
|
||||
|
||||
## Heap and application pool
|
||||
|
||||
### Provisional Unreal layout leaves no heap
|
||||
|
||||
- **Symptom:** `SetHeapSize OOM: req 0x600000 > cap 0`, followed by an SDK abort in `nninitStartup` on the first provisional boot.
|
||||
- **Cause:** the provisional Unreal profile reserved `0x1028000000` bytes but assigned `0x1008000000` to the alias region. The 256 MB module region and 256 MB stacks consumed the remainder, leaving no heap.
|
||||
- **Fix:** `profile_guess.py` gives the alias region exactly 64 GB (`0x1000000000`), leaving 128 MB for the initial heap before the heap check measures the game. `test_profile_guess.py` checks the partition through the real profile generator.
|
||||
- **Found on:** [SHIN MEGAMI TENSEI V: Vengeance](../../dm/games/010069C01AB82000/README.md) 1.0.3, whose C library asks for 6 MB.
|
||||
|
||||
### Heap sized from the application pool less used memory
|
||||
|
||||
- **Symptom:** `nnMain` returns right after `nv::InitializeGraphics`, with no error (Hyrule Warriors: Age of Calamity); `[mem] SetHeapSize … + modules … > app pool` (Monster Hunter Rise), `[mem] SetHeapSize … + modules 0x5c00000 > app pool` (Catherine: Full Body); `SetHeapSize` asks for almost the whole application pool and fails at boot (Metroid Dread: 0xc9400000, larger than the heap region); the memory pool for the game's 96 MB GPU heap has no storage (`MemoryPoolInitialize` red line), then a null read at `main+0x2eb954` (Catherine: Full Body); heap allocations fail late in a busy scene.
|
||||
@@ -23,6 +30,13 @@ The guest address space, heaps, pools, virtual address memory and the allocators
|
||||
- **Found on:** [Hyrule Warriors: Age of Calamity](../../dm/games/01002B00111A2000/README.md); [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md); [Metroid Dread](../../dm/games/010093801237C000/README.md); [Catherine: Full Body](../../dm/games/0100BF00112C0000/README.md) (SDK 8.2.1; one boot). The source names no game for the late failures in a busy scene.
|
||||
- **Check:** probe `DM_SVC=1`'s `svc 0x1` request and the `AllocateMemoryBlock` result.
|
||||
|
||||
### A heap request beyond the heap region returns ResultOutOfMemory
|
||||
|
||||
- **Symptom:** `SetHeapSize OOM: req 0x120000000 > cap 0xd0000000`, then `Break#1 reason=0` in `nninitStartup`, with a backtrace through `nn::os::SetMemoryHeapSize` (`sdk+0x967a4`).
|
||||
- **Cause:** a game may look for the largest heap it can have by asking for more than there is. Donkey Kong Country: Tropical Freeze's `nninitStartup` (`main+0x1e84c`) binary-searches `SetMemoryHeapSize` between 3 and 6 GB (6 GB is the heap region of a 39-bit address space) and narrows on every failure. SDK 4's `SetMemoryHeapSize` (`sdk+0x9672c`) turns the kernel's ResultOutOfMemory (description 104), 103 and ResultLimitReached (132) into `os::ResultOutOfMemory` (0x1003) and aborts on any other result. For a size larger than the heap region the host returned 0xD401 (InvalidCurrentMemory, 106), so the SDK aborted. Horizon's `KPageTable::SetHeapSize` returns ResultOutOfMemory there.
|
||||
- **Fix:** a request larger than the heap region returns 0xD001 (`hle_svc.c`, svc 0x1). The search then settles on 3.09 GB, the pool less the rewritten modules (the same LimitReached check as before).
|
||||
- **Found on:** [Donkey Kong Country: Tropical Freeze](../../dm/games/0100C1F0051B6000/README.md) (SDK 4.4.0).
|
||||
|
||||
### `nninitStartup`'s heap only without virtual address memory
|
||||
|
||||
- Not needed in the end for [Ace Attorney Investigations Collection](../../dm/games/010005501E68C000/README.md) (1.0.0.2, SDK 17.5.3): the process heap of pull request #16. `nninitStartup` calls `SetMemoryHeapSize` / `AllocateMemoryBlock` only without virtual address memory, and nothing else in `main` calls them ([Virtual address memory follows the NPDM's system resource](#virtual-address-memory-follows-the-npdms-system-resource)).
|
||||
@@ -56,10 +70,12 @@ The guest address space, heaps, pools, virtual address memory and the allocators
|
||||
- **Fix:** shrink the node area with a rewriter cut for that SDK build (`dm/tools/sdk_patches.py`): the two `mov w8/w9, #0x3fe00000` in the AddressRegionManager constructor → 62 MB.
|
||||
- **Finding the constructor on a new `sdk` build:** search its text for the two `mov w8 / w9, #0x3fe00000` words (`08fca752`, `09fca752`); `sdis.py --fn` names the function `AddressRegionManagerC2Emm` from its dynamic symbols. SDK 7 / 8 builds load the node area with `orr w8 / w9, wzr, #0x3fe00000` (`e8230b32`, `e9230b32`), so the `mov` search finds nothing there.
|
||||
- **Found on** (the builds that take the cut):
|
||||
- [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md) (SDK 12.3.9): constructor `sdk+0x346420`, cuts `+0x346430` / `+0x3464cc`, the same offsets as Engage's SDK 12.3.7, beside the large-region alignment cut. The game's own allocator needs a separate compact layout.
|
||||
- [Pokémon Brilliant Diamond](../../dm/games/0100000011D90000/README.md) (Unity 2019.4.27f1, SDK 11.4.3): constructor `sdk+0x34e0b8`, node-area limit `+0x34e0c8` and occupied length `+0x34e164`. The original node area prevents StandardAllocator's pool-minus-used reservation from fitting at startup.
|
||||
- [Fire Emblem Engage](../../dm/games/0100A6301214E000/README.md): the two `mov w8/w9, #0x3fe00000` in the constructor → 62 MB; without it the alias region ran out of address space minutes into play ([Size-aligned regions exhaust the alias address space](#size-aligned-regions-exhaust-the-alias-address-space)).
|
||||
- [Ace Attorney Investigations Collection](../../dm/games/010005501E68C000/README.md) (Unity 2022.3, SDK 17.5.3): `StandardAllocator::Initialize` aborts in `nninitStartup` on SDK 17, then the alias region fills in minutes; the same node area as Engage's SDK; the constructor is at `sdk+0x139458` on that build and takes the same cut.
|
||||
- [Brain Memory Training Academy](../../dm/games/0100F5D01DA3A000/README.md) (Unity 2022.3.5f1, SDK 16.2.4): `nn::diag` abort at `sdk+0x126490`, `StandardAllocator::Initialize`, called from `nninitStartup` at `main+0x73adcc`. Its `nninitStartup` is Ace Attorney's: `Initialize(nullptr, pool − used)` with virtual address memory and no `SetMemoryHeapSize`. The `sdk` build is Unicorn Overlord's, whose NPDM declares no system resource, so it never needed the cut; the constructor (`AddressRegionManagerC2`, `sdk+0x134768`) takes it at `+0x134778` / `+0x13480c`.
|
||||
- [Sea of Stars](../../dm/games/01008C0016544000/README.md) (Unity 2021.3.25f1, SDK 16.2.0, the `sdk` build of Super Mario Bros. Wonder 1.0.0): `nn::diag` abort at `sdk+0x126440`, `StandardAllocator::Initialize`, called from `nninitStartup` at `main+0x8b6710`; the heap check reports "the host stopped before nnMain". Brain Memory Training Academy's constructor at `sdk+0x134718` takes the cut at `+0x134728` / `+0x1347bc`, beside the [region alignment cut](#size-class-alignment-doubles-unitys-regions).
|
||||
- [Patrick's Parabox](../../dm/games/0100E44016C22000/README.md) (Unity 2020.3.30f1, SDK 13.3.7): `StandardAllocator::Initialize` at `sdk+0x324dec`, called from `nninitStartup` at `main+0x1cd080`; the same `nninitStartup` and the same cut, on this build's constructor (`sdk+0x37c3a0`: `+0x37c3b0` / `+0x37c44c`).
|
||||
- [Advance Wars 1+2: Re-Boot Camp](../../dm/games/0100300012F2A000/README.md) (Unity 2020.2.1f3, SDK 10.4.1): `StandardAllocator::Initialize` at `sdk+0x2edf90`, called from `nninitStartup` at `main+0x365310`; the heap check reports "the host stopped before nnMain"; the same `nninitStartup`. The `sdk` build is The Great Ace Attorney Chronicles', which has no virtual address memory; its constructor (`sdk+0x340e90`) has the `orr` form (`e8230b32` at `+0x340ea0`, `e9230b32` at `+0x340f3c`).
|
||||
- [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) (SDK 10.4.8): the build's AddressRegionManager is the 10.4.1 build's at the same offsets and takes the same three cuts (node area and alignment).
|
||||
@@ -96,8 +112,16 @@ The guest address space, heaps, pools, virtual address memory and the allocators
|
||||
- [Super Smash Bros. Ultimate](../../dm/games/01006A800016E000/README.md) (SDK 8.2.1): alignment at `sdk+0x336d5c`, cut as DRAGON QUEST XI S's SDK 7.3.2 build.
|
||||
- [Paper Mario: The Thousand-Year Door](../../dm/games/0100ECD018EBE000/README.md) (SDK 15.3.2, Celeste's `sdk` build): `AllocateAddressRegion`'s alignment at `sdk+0x12e2e0`, as Absolum's did.
|
||||
- [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) (SDK 10.4.8): the 10.4.1 build's offsets.
|
||||
- [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md) (SDK 12.3.9): large-region alignment at `sdk+0x34662c`, 1 GB to 32 MB; its compact 4 GB allocator reservation then fits after the 62 MB node area.
|
||||
- **Note:** FINAL FANTASY RESONANCE's SDK 20.5.17 build aligns regions of 2 MB and more to 2 MB only (`VammManager::AllocateAddressRegion`, `sdk+0x145e00`), so it needs no alignment cut.
|
||||
|
||||
### Size-class alignment doubles Unity's regions
|
||||
|
||||
- **Symptom:** a write at NULL + 0x3fffe0 (a 4 MB pool − 0x20) at `main+0x48b770`, about six minutes into the prologue, with Unity's `ALLOC_CACHEOBJECTS_THREAD` allocator in x19; the `[mem]` high-water mark at +4.26 GB of the 4.375 GB alias region while 1.26 GB is mapped (Sea of Stars).
|
||||
- **Cause:** before SDK 20, `VammManager::AllocateAddressRegion` aligns a region by its size (64 KB under 2 MB, 2 MB under 4 MB, 4 MB under 32 MB, 32 MB under 1 GB, else 1 GB) and starts the next one at that alignment past a 4 KB guard. Unity 2021.3's virtual allocator (`main+0x48deb0` → `main+0x12e12c0`) asks for N MB + 252 KB and aligns its blocks to 256 KB itself, so every region spills just past a size class: a 16.25 MB region takes a 20 MB slot, a 4.25 MB one an 8 MB slot, a 32.5 MB one 64 MB. Measured with probes on the success path (`sdk+0x134868`: x21 size, x22 address) and on `FreeAddressRegion` (`sdk+0x1350cc`: x1 address): 94 regions of 16.25 MB, 35 of 4.25 MB and 15 of 2.25 MB among 167 live regions holding 2.59 GB, with 0.86 GB of gaps between them, ending at +4.22 GB. On a Switch the alias region is 64 GB.
|
||||
- **Fix:** a cut on that `sdk` build gives the 4 MB, 32 MB and 1 GB classes SDK 20's 2 MB (`sdk_patches.py`). The same route then ends its live regions (2.59 GB) at +3.24 GB with 0.58 GB of gaps, and the high-water mark stays at +3.0 GB over 20 minutes.
|
||||
- **Found on:** [Sea of Stars](../../dm/games/01008C0016544000/README.md) (SDK 16.2.0: `sdk+0x134914`, `+0x13491c`, `+0x134920`), beside the node-area cut and a 1.5 GB pool.
|
||||
|
||||
### Unity 6 takes its memory with no StandardAllocator and no heap
|
||||
|
||||
- **Symptom:** nothing aborts at startup as with Unity 2022.3, but the provisional profile gave a 1 GB block and a 256 MB alias region while Unity maps 1.5 GB in the first room.
|
||||
@@ -134,6 +158,13 @@ The guest address space, heaps, pools, virtual address memory and the allocators
|
||||
- **Found on:** [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md) (2.0.2, SDK 19.3.11; one boot); [Paper Mario: The Thousand-Year Door](../../dm/games/0100ECD018EBE000/README.md): the allocator maps 1.5 GB at boot and 2.0 GB by the arrival in Rogueport. The `sdk` build is [Celeste](../../dm/games/01002B30028F6000/README.md)'s. Absolum's malloc StandardAllocator (40 GB, cut to 2.5 GB) is in [.NET NativeAOT malloc and the GC region range](#net-nativeaot-malloc-and-the-gc-region-range).
|
||||
- **Note:** on Pokémon Legends: Z-A, `QueryMemoryInfo` reporting more than 4 GB would take the game's big-memory path (heaps of allocatable − 4.27 GB instead of − 2.76 GB).
|
||||
|
||||
### Separate virtual ranges in Snowdrop's page allocator
|
||||
|
||||
- **Symptom:** a write to `0x20000ffff` in static initialization (`main+0x164990`), after `AllocateAddressRegion` returns `0x1803`; reducing the request to 3 GB still returns `0x1803` with the SDK's original large-region alignment. With only the outer ranges reduced, a later free crashes at `main+0x1668dc` after reading compressed file data as a slab-owner pointer.
|
||||
- **Cause:** the engine reserves three adjacent virtual ranges before committing pages in them. Its enclosing reservation exceeds the iPhone's available guest address space, and the engine does not check the failed reserve before deriving the page addresses. A smaller reservation also needs the SDK's node area and large-region alignment adapted: a separate metadata reservation comes first, and the 1 GB alignment wastes the remaining contiguous space. The large-allocation child has an independent maximum page count; reducing only the outer range lets it allocate across the boundary used by `free` to distinguish allocation classes.
|
||||
- **Fix:** the build-gated game cuts reduce the range sizes, their combined reservation and the large allocator's maximum page count together. The fixed metadata reserve remains large enough, and page allocation, commits and frees retain their behavior. The SDK build gets the existing [node-area cut](#the-sdks-address-region-node-area) and [32 MB alignment](#1-gb-alignment-of-regions-of-1-gb-and-more). Per-range occupancy and the original growth fallbacks remain **open** until sustained gameplay validates the smaller ranges; see the game note for the layout, cut sites and current validation.
|
||||
- **Found on:** [Mario + Rabbids Sparks of Hope](../../dm/games/0100317013770000/README.md) (1.6.2225577, SDK 13.3.5).
|
||||
|
||||
### .NET NativeAOT malloc and the GC region range
|
||||
|
||||
- **Symptom:** `nn::diag` abort at `sdk+0x12dad0` in `nninitStartup`, called from `main+0x756310`.
|
||||
@@ -161,6 +192,8 @@ The guest address space, heaps, pools, virtual address memory and the allocators
|
||||
- [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) (Unreal Engine 4.26.2, 1.1.1, SDK 10.4.8): the region option is the trap. This `nninitStartup` reserves the region only when `-SwitchVirtualAddressSpaceSize=` is absent (`ldr x8,[x22,#0x130]; cbnz x8` past the default 62 GB and its `AllocateAddressRegion`, `main+0x353f564`), so with the argument the base stayed 0 and the first commit went into Unreal's fatal error. The default is cut to 4064 MB instead (two `orr #0xf80000000` → `mov #0xfe000000`, its `patches.py`) and the profile has no `game_args`. With Engage's 5 GB block, from the boot into the Olimar prologue: up to +1.5 GB of the alias region with 1.0 GB mapped, an Unreal heap of 1 GB.
|
||||
- [DRAGON QUEST XI S](../../dm/games/01006C300E9F0000/README.md) (Unreal Engine 4.18, SDK 7.3.2) reads the same option and the others from `argv[1]` on (`main+0x177b5a4`; `main+0x177c440` parses each one: `atoi`, MB up to 4095, bytes above). Its region is 16 GB by default. A value of 4224 was taken as bytes: a 4 KB region, and the first 2 MB request crashed in `NumberLineAllocator::Compaction` at boot. With the node area cut and 32 MB alignment for large regions on that SDK build (`sdk_patches.py`), a 4064 MB region starts 64 MB into Engage's 4.375 GB alias region. On the Mac the mapped total stays at 1.5 GB from the title to the first field (Cobblestone Tor), used up to +1.75 GB of the alias region; the process holds about 2.1 GB, 1.1 GB of it Metal allocations.
|
||||
|
||||
- **Note:** without virtual address memory there is no engine region: [OCTOPATH TRAVELER](../../dm/games/010057D006492000/README.md) (Unreal Engine 4.18, SDK 3.5.1, no system resource in its NPDM) has `nninitStartup` set a heap of pool − used (3.08 GB) and the engine runs on it, so it takes the 4 GB heap layout. The provisional profile used to give every Unreal game the 64 GB alias layout, which left this one no heap region (`SetHeapSize OOM: req 0xc5400000 > cap 0` and a `Break` in `nninitStartup`); `profile_guess.py` now gives it only with virtual address memory.
|
||||
|
||||
### Unreal's FMallocBinned3 replaced by the host allocator
|
||||
|
||||
- **Symptom:** a null read in the out-of-memory report at `main+0x1005824` when the engine region is smaller than the allocator's pools (Octopath Traveler 0).
|
||||
@@ -172,6 +205,7 @@ The guest address space, heaps, pools, virtual address memory and the allocators
|
||||
- [FINAL FANTASY RESONANCE](../../dm/games/0100606026F12000/README.md) (Unreal Engine 5.6.1, SDK 20.5.17): `nninitStartup` constructs its FMallocBinned3 in place in the bss, as Octopath Traveler 0's (`main+0x77c6210`; the call at `main+0x137f0` is cut, and its constructor would reserve 38 GB). UE 5.5 and later add `MallocZeroed` / `TryMallocZeroed` after `Free`, two TLS cache calls (`MarkTLSCachesAsUsed` / `Unused`) and two cached-memory queries, so the FMalloc vtable has 32 slots (layout `UE5_6` in `hle_uemalloc.c`: Malloc 6 … Free 10, QuantizeSize 13, GetAllocationSize 14). The base vtable shows it: its three pure slots are 0, and its slots 11 and 12 are one function that calls slot 6, then `memset`.
|
||||
- [Pikmin 4](../../dm/games/0100B7C00933A000/README.md) (Unreal Engine 4.26.2, 1.1.1, SDK 10.4.8): the allocator is made as in 4.27, with the same 21-slot FMalloc vtable (`GCreateMalloc` `main+0x353f770` mallocs 0x1c38 bytes and calls the constructor `main+0x35ba610`, the one store of the vtable `main+0x8da4758` outside its two destructors; found from the FMallocBinned3 error strings through the relocated slots). The cut and the gate are as DRAGON QUEST VII Reimagined's, the gate on the `ldp x29, x30` after the cut call (the `ret` case of [Observation gates where x9–x17 are dead](rewriter.md#observation-gates-where-x9x17-are-dead)).
|
||||
- [OCTOPATH TRAVELER II](../../dm/games/0100A3501946E000/README.md) (Unreal Engine 4.27, 1.1.1, SDK 12.3.2): as DRAGON QUEST VII Reimagined's (`GCreateMalloc` `main+0xbecbf0`, the constructor `main+0xc4d778`, the vtable `main+0x64710b0`), with 20 slots: no `OnMallocInitialized`, so the 21st word the host copies is the next vtable's offset to top, which nothing calls.
|
||||
- [SHIN MEGAMI TENSEI V: Vengeance](../../dm/games/010069C01AB82000/README.md) (1.0.3, SDK 15.3.2): the same UE4 layout and constructor replacement; its note records the measured call sites and vtable.
|
||||
- **Note:** Unreal Engine 4.18 (DRAGON QUEST XI S) needs no allocator cut: the SDK's `NumberLineAllocator` hands its region out in 64 KB units (`main+0x177c4d0`), and the engine maps only what it uses.
|
||||
|
||||
### Thread stacks from the host's Unreal heap
|
||||
|
||||
@@ -65,6 +65,21 @@ The game may also write into an NVN object directly, without an NVN call, throug
|
||||
- **Fix:** such a pool gets an address range of its own at `MemoryPoolInitialize` (reserved, inaccessible: 64 GB on the Mac, at most 1 GB on iOS), and each mapping request `vm_remap`s the physical pages over its part of the range, so virtual addresses read and write the same memory, as the GPU's page table does. Each run of mapped pages is registered with the replay like a pool of its own and rebuilt when a request joins or splits runs (`vpool_*` in `hle_nvn.c`). Mario's 128 MB pool, mapped whole onto one physical pool, keeps the alias. The log reports the mapped high-water mark per 128 MB.
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md).
|
||||
|
||||
### Virtual pools of one texture each, mapped before and after the texture
|
||||
|
||||
- **Symptom:** `[RED] MapVirtual: non-contiguous mapping (true sparse) not implemented`, on small virtual pools; streamed textures black or missing (OCTOPATH TRAVELER's village ground and plants).
|
||||
- **Cause:** Unreal Engine 4.18's NVN RHI gives each streamed texture a virtual pool of its own (0x20000 to 0x560000 bytes, flags 0x2a1), creates the texture in it before mapping anything, maps the mip tail from a 4 MB physical pool after writing its texels there on the CPU, and later maps the higher mips from other pages of that pool. Only pools larger than the application pool had [an address range of their own](#sparse-virtual-pools-built-from-separate-physical-pools); the others were aliased onto `physical − offset` at their first mapping, so the texture had no storage (its pool had none when it was created) and every later mapping at another base was refused.
|
||||
- **Fix:** every virtual pool gets its reserved range at `MemoryPoolInitialize` (`vpool_reserve`; the sparse records are now one per pool, reached through `pool_t.vp`, without the old limit of 4). Its first mapping request decides: one that maps the whole pool from offset 0 keeps the alias (the range is given back; Mario's 128 MB pool), anything else makes the pool sparse, with the page table and the uploads through it ([next entry](#textures-read-through-a-4-kb-page-table-of-a-sparse-pool)).
|
||||
- **Found on:** [OCTOPATH TRAVELER](../../dm/games/010057D006492000/README.md): 221 pools by the first village; the same route with the old alias left the ground, plants and props black.
|
||||
- **Not checked:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md) (no game files on hand), whose pool should take the alias branch as before.
|
||||
|
||||
### The sparse tile layout of a texture
|
||||
|
||||
- **Symptom:** `[RED] NVN unimplemented function called: nvnTextureBuilderGetSparseTileLayout`, then an out-of-memory abort in Unreal's allocator (`Failed to allocate 1879113728 bytes from Standard Allocator`, on `DM_DBG=1`).
|
||||
- **Cause:** the call fills `NVNtextureSparseTileLayout` {numTiledLevels, tile width, height and depth in texels, 16 reserved bytes}; Unreal Engine 4.18 sizes a sparse texture's mip tail as storage(all levels) − storage(numTiledLevels levels) and allocates it from its heap. Unimplemented, the count was stack garbage and the whole 1.75 GB texture counted as tail.
|
||||
- **Fix:** `nvnTextureBuilderGetSparseTileLayout` and `nvnTextureGetSparseTileLayout` answer from the host's [sparse layout](rendering.md#sparse-textures-laid-out-in-64-kb-tiles) (`nvn_tex_sparse_tiles`: the levels at least one tile wide, high and deep).
|
||||
- **Found on:** [OCTOPATH TRAVELER](../../dm/games/010057D006492000/README.md), at boot (`main+0x1f1a688`).
|
||||
|
||||
### Textures read through a 4 KB page table of a sparse pool
|
||||
|
||||
- **Symptom:** `CRASH sig=10` in `memmove` from `pkg_upload` → `nvn_deswizzle`, at an address 32 GB into a sparse virtual pool; textures with unaliased tails came out magenta.
|
||||
@@ -170,6 +185,8 @@ Games read this count as a position in their own command memory and size their h
|
||||
- **Note:** the commit feedback handler's other blocks (diagnostics such as `DM_NVN_FUSE_CHECK`) still run when the handler does, after a fast-woken wait has returned.
|
||||
- **Resolved in review of #158:** claiming a record is not completion. The original early path skipped records a handler or another waiter owned and advanced `g_done` before their writes finished; a delayed callback also retained a recyclable slot pointer. `post_claim` now holds `g_post_mu` through `post_run`, and both contenders call `post_complete` with their fence to apply earlier records in order. A slot is reused under that lock only after its writes finish; an old fence cannot claim its new generation. `g_done` and `g_fence` are atomic.
|
||||
- **Check:** `make -C dm/gfx/tests GAME=010015100B514000 check_t_perf_order` covers a paused completion owner, a competing waiter, recycled-slot generations, event order and exactly-once writes, with direct/deferred encoding and fast wake disabled. `t_perf_order --cpu` and `perf-order-tsan` run the completion cases without a GPU in CI. The original harness reproduced premature completion; the corrected CPU cases pass ThreadSanitizer. Current gameplay coverage and timing limits are recorded in [BotW](../../dm/games/01007EF00011E000/README.md#final-synchronization-and-pass-validation-2026-10-26-current) and [Metroid Dread](../../dm/games/010093801237C000/README.md#final-gameplay-validation-2026-10-26-current); [13 Sentinels](../../dm/games/01003FC01670C000/README.md#rendering) has additional menu/report traffic coverage.
|
||||
- **Finite timeout budget:** when the event wait times out, the feedback wait uses only the remaining requested time. Previously an expired 20 ms event wait could be followed by another full 20 ms feedback wait. Elapsed time is taken from the monotonic wait clock independently of wait diagnostics. Zero/sub-millisecond polls, successful CPU completion, and the existing per-stage one-hour cap for `UINT64_MAX` are unchanged. This bounds the time assigned to the two waits, not scheduling delays or the CPU completion lock.
|
||||
- **Budget regression:** the `--cpu` path also uses a synthetic event, clock and condition-wait deadline to exercise the actual `mtl_wait_at` function without a Metal device. Run with `DM_NVN_FAST_WAKE=0/1` and `DM_NVN_WAITS=0/1`; no gameplay or FPS improvement is inferred from these timeout cases.
|
||||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md): it reads 5 timestamp reports a frame right after the wait (its interval controller and dynamic resolution), which is why the waiter makes them itself. Wake after the GPU end 0.29 → 0.08 ms on an M2 Max, 0.147 ms on an iPhone 17 Pro Max.
|
||||
|
||||
### `nvnQueueFinish` waits for the GPU, not the display
|
||||
@@ -252,6 +269,13 @@ Games read this count as a position in their own command memory and size their h
|
||||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md), GitHub issue #52 (Mac and iPhone). The sky is precomputed atmospheric scattering: each frame a pass (`08fe25061d583637`, `uking_pass_shader.sharcb`) turns a 256×32×16 RGBA16F 3D table of in-scattered light into a 256×256 sky map, which the sky draw (`8110c74078548172`) and the materials' aerial perspective sample. The table and two 2D ones (transmittance 256×64, irradiance 64×64) come from `System/KSys/sky.skybin` in `Pack/Bootup_Graphics.pack` (1,212,416 bytes, the three textures' storage back to back). The game makes a `CPU_CACHED | GPU_CACHED` pool for each texture, creates the textures, copies the file's data in with `memcpy` and flushes (`main+0x1257d50`, once, at the sky's setup; the flush goes through `main+0xebf4c4`, which flushes only CPU-cached pools). With zeros the sky had no scattered light: only the clouds and the sun's colour were left. In the Great Plateau save the fill covers that one texture; the flushes (about 15 a frame) merge into about 530 ranges, and frame times did not change. DRAGON QUEST XI S (title, free play, menus; 300 s) filled none.
|
||||
- **Note:** `DM_NVN_TEXRAW=212,214` (all zero) and `DM_NVN_TEXINFO=256x32` (`storage 1048576 bytes non-zero 781567` at first use, `swizzled=0`) showed it.
|
||||
|
||||
### Storage of a CPU-uncached pool at first use
|
||||
|
||||
- **Symptom:** magenta (ASTC's error colour) where UI images belong: a title logo and menu panels, while the 3D scene and the text draw; `DM_NVN_ZBLOCK=1` reports nothing (the textures are not uploaded on the CPU at all); `DM_NVN_TEXINFO=WxH` shows storage holding data at first use (`non-zero …`) in a pool with flags 0x22 (`CPU_UNCACHED | GPU_CACHED`).
|
||||
- **Cause:** in a `CPU_UNCACHED` pool the CPU's writes reach memory with no flush, and the GPU reads that memory: what the storage holds is the texture's content. The host took a CPU-visible pool's storage only when the game had flushed it ([previous entry](#first-use-fill-of-storage-the-game-flushed)), and a game that writes an uncached pool never needs to. With `tex_flush_upload` and `tex_dirty_upload` the logo stayed magenta too, so its texels were apparently in the storage before the texture was registered, where no later write marks it.
|
||||
- **Fix:** `tex_init_fill` (`nvn_mtl.m`) also fills a new texture in a `CPU_UNCACHED` pool from its storage once, at first use, when the storage holds data; the same exclusions as for `CPU_NO_ACCESS` pools apply (packaged layouts, `COMPRESSIBLE`, multisample, depth / stencil, sparse). The log says `initial content: … a CPU_UNCACHED pool's`. A texture the CPU writes again after its first use still needs the profile's keys.
|
||||
- **Found on:** [Donkey Kong Country: Tropical Freeze](../../dm/games/0100C1F0051B6000/README.md): the 912×512 ASTC 5×5 title logo (242,374 of 376,832 storage bytes non-zero at first use, no packaged layout) and the menus' ASTC panels; with the fill and neither profile key, the title and every menu draw.
|
||||
|
||||
### Mapped-range flushes taken as uploads (`tex_flush_upload`)
|
||||
|
||||
- **Symptom:** a movie plays black (Age of Calamity: its intro movie under the "+ Skip" prompt).
|
||||
@@ -400,6 +424,13 @@ Games read this count as a position in their own command memory and size their h
|
||||
- `nvnTexturePoolRegisterImage` writes the same table as `RegisterTexture` (image handles are pool indices).
|
||||
- **Found on:** [Hyrule Warriors: Age of Calamity](../../dm/games/01002B00111A2000/README.md).
|
||||
|
||||
### Image bindings supply shader-write intent
|
||||
|
||||
- **Symptom:** `[RED] texture bound to an image slot lacks the shader-write usage`, and compute image writes go to a dummy texture.
|
||||
- **Cause:** Metal usage came only from the texture builder's `IMAGE` flag. A render texture may instead have only `COMPRESSIBLE`, then its descriptor be used in a shader image slot. Snowdrop writes these descriptors with `RegisterTexture`, not `RegisterImage`. Its depth-conversion compute shader `cf5ba107e29b51f4` gathers from a depth texture and unconditionally writes four pixels per invocation to a 256×256 R16F texture whose flags are `0x8`. Its Metal texture had read / render-target usage (`0x5`), so the image binding was rejected.
|
||||
- **Fix:** before a draw or dispatch uses a writable image, `image_prepare` resolves it with image intent and ensures its backing allows shader writes. `RegisterImage` also keeps this intent in its host snapshot; neither path changes the guest texture. A texture already rendered into is upgraded through `tex_upgrade`: end its render pass first, copy every mip / slice, replace all shared serials and invalidate cached sampled views. Load-only image slots accept read-only storage without upgrading, and ordinary render targets retain their original usage. `t_image_usage.m` exercises both registration entry points, a late registration while a render pass is open, the clear and other mip surviving the upgrade, and a GPU image write read through a sampled alias.
|
||||
- **Found on:** [Mario + Rabbids Sparks of Hope](../../dm/games/0100317013770000/README.md) (SDK 13.3.5, Snowdrop).
|
||||
|
||||
### Plural texture binds, builder-registered samplers, strided texel writes, device item 0x67
|
||||
|
||||
- `nvnCommandBufferBindTextures` (the plural of `BindTexture`), `nvnSamplerPoolRegisterSamplerBuilder` (a sampler registered from its builder), `nvnTextureWriteTexelsStrided` (`WriteTexels` with row and image strides) and device item 0x67, which the game reads before every program it loads and uses as the alignment of its copies of the shader (answered 256, not measured).
|
||||
|
||||
@@ -33,7 +33,7 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
|
||||
- **Cause:** agl's layouts rebuild a capture texture on the same storage with a new channel mapping, and replace the registered descriptor only if `nvnTextureCompare` reports the two textures different. The host compared the description and the storage but not the mapping, so every rebuild was dropped.
|
||||
- **Fix:** `nvnTextureCompare` compares the mapping too, an unset mapping counting as (R, G, B, A). `t_nvn` covers the comparison. From GitHub PR #6.
|
||||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md): panes tagged `CaptureOn` (about 140 layouts, e.g. `PaGuideKeySmoke_00`; user data `A8`, `CaptureScale` 0.5, `MultiFilter`) are drawn into a target, blurred at half size and copied into a capture texture that the layout's materials sample in place of the placeholder `FBLayout_00`. The game creates it with its per-format default mapping (one call site, `main+0xecb0bc`: R8 → (R, 0, 0, 1), RG8 → (R, G, 0, 1), and so on), then rebuilds it with the capture's own mapping: `main+0xf597dc` picks (1, 1, 1, R) for an `A8` capture (the material colour kept, the mask as alpha) and alpha 1 when the pane says `CaptureOutputAlpha` 255 (read at `main+0xf58e98`; an RGB10A2 capture then reads (R, G, B, 1)); `main+0xecacf0` stores the mapping and rebuilds the texture (`main+0xecabe8`), and `main+0xec33f0` replaces the descriptor only on a difference. The captures kept (R, 0, 0, 1), whose alpha is always 1: hard-edged dark boxes behind every prompt (the shadow pane `e6a88e12dbae9743`: black, alpha = capture alpha × vertex alpha 180/255) and rectangles filled with cream cloud (the glow `aa209f57b133e862`: alpha = min(cloud alpha, capture alpha)). Checked on the Mac by the PR's author (M4 Pro, unchanged shader libraries): green stamina, the purple noise meter and the game's own blurred System menu background. [Super Mario Odyssey](../../dm/games/0100000000010000/README.md): its layouts rebuild their captures through the same agl code (`main+0x982320` replaces the registered descriptor only if `nvnTextureCompare` reports a difference), so the fix covers them too.
|
||||
- **Note:** before the cause was known, a profile key, `alpha_from_red`, listed the layout library's pixel shaders and bound such render targets through a view mapped (R, 0, 0, R). In BotW (all 26 pixel-shader variants, `main`'s BNSH at 0x1d83000) that fixed the alpha but zeroed green and blue, so tinted panes such as the stamina wheel came out black, and the System menu background (a capture with alpha forced to 1) stayed black. In Odyssey (all 26 layout pixel shaders, `main`'s BNSH at 0x132c000) it had turned the coin counters' solid white cards into the right shapes. With the comparison fixed it found no (R, 0, 0, 1) target on either game (the mapping a capture is rebuilt with replaces it, on the Mac and the iPhone alike), so the key and its host code (`tex_alpha_red`, `DM_NVN_ALPHA_RED`) were removed; Odyssey's note has the A/B runs.
|
||||
- **Note:** before the cause was known, a profile key, `alpha_from_red`, listed the layout library's pixel shaders and bound such render targets through a view mapped (R, 0, 0, R). In BotW (all 26 pixel-shader variants, `main`'s BNSH at 0x1d83000) that fixed the alpha but zeroed green and blue, so tinted panes such as the stamina wheel came out black, and the System menu background (a capture with alpha forced to 1) stayed black. In Odyssey (all 26 layout pixel shaders, `main`'s BNSH at 0x132c000) it had turned the coin counters' solid white cards into the right shapes. With the comparison fixed it found no (R, 0, 0, 1) target on either game (the mapping a capture is rebuilt with replaces it, on the Mac and the iPhone alike), so the key and its host code (`tex_alpha_red`, `DM_NVN_ALPHA_RED`) were removed (c76f6798); Odyssey's note has the A/B runs.
|
||||
|
||||
### Texture views cached by texture and view parameters
|
||||
|
||||
@@ -104,9 +104,10 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
|
||||
|
||||
### Buffer textures at offsets below 16 bytes
|
||||
|
||||
- **Cause:** Metal reports a 16-byte alignment for texture buffer views, and the host used to leave a view at a smaller offset as a dummy.
|
||||
- **Fix:** on an Apple GPU a view at any multiple of the element size reads the right elements (measured on an M6 with R32Float views at offsets 4, 8 and 12), so the host now creates it there; below the element size it stays a dummy.
|
||||
- **Found on:** [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md): one at pool offset `0x168`, read by its particle shaders ([Texture slots picked at run time](shaders.md#texture-slots-picked-at-run-time), `shaders.md`).
|
||||
- **Symptom:** particle effects drawn at the wrong scale or place, or flickering, where the particle data is a buffer texture at a pool offset that is not a multiple of 16; log line `buffer texture at pool offset …, below Metal's 16-byte alignment`.
|
||||
- **Cause:** Metal requires 16-byte alignment for texture buffer views (`minimumTextureBufferAlignmentForPixelFormat`), and an Apple GPU addresses a view created at a smaller offset from the 16-byte boundary below it: on an M4 Pro, R32Float views at offsets 4, 8 and 12 read element 0 where elements 1, 2 and 3 are expected, for every format and width tried, shared and private buffers, bound directly or in an argument buffer (`t_tbuf_align`). The host used to leave such a view as a dummy, then created it at the offset on the strength of a measurement on an M6 that the M4 Pro contradicts. NVN places buffer textures at any element-aligned offset, and no Metal view can start there.
|
||||
- **Fix:** the view is still created at the offset, and a shader reads it through a copy of its bytes at an aligned offset of a shared staging block (`tbuf_shadow` in `nvn_mtl.m`), made at its first binding in each submission and cached for the rest of it; aligned views are untouched. This is a workaround for the platform limit, with gaps that are **open**: the copy holds what the CPU wrote before the submission was encoded, so a GPU write to the buffer in the same submission, or in one still running, is not seen; a view in a pool the CPU cannot access is bound as is (`[RED]` once); and shader writes through an image view at such an offset land at the boundary below (`[RED]` once at creation). A shader-side element offset (the view at the boundary below, the shader adding the skew) would close all three, at the cost of a chain change and a per-binding value in the argument buffer.
|
||||
- **Found on:** [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md): one at pool offset `0x168`, read by its particle shaders ([Texture slots picked at run time](shaders.md#texture-slots-picked-at-run-time), `shaders.md`). [FINAL FANTASY RESONANCE](../../dm/games/0100606026F12000/README.md): particle data at offsets 4, 8 and 12 past the boundary (village fires, smoke).
|
||||
|
||||
### Automatic block height truncates
|
||||
|
||||
@@ -124,6 +125,14 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
|
||||
- **Fix:** `nvn_tex.c` `level_walk`: a level at least one tile wide, high and deep is laid out with its width padded to whole tiles, the levels from the first smaller one on form the mip tail (packed as usual), and the storage is whole tiles, 64 KB aligned.
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md): laid out unpadded, the 1024×1024 ASTC 6×6 clothes of Hinoa and Minoto (43 GOBs wide instead of 48, level 1 at 0x81000 instead of 0x90000) read 2,183 all-zero blocks in level 0 and 3,043 in level 1, which ASTC decodes as its error colour: their trousers, sleeves and gloves were magenta, worst in Kamura Village.
|
||||
|
||||
### Sparse textures take the block height of their 64 KB tile
|
||||
|
||||
- **Symptom:** a CPU-filled texture shuffled into a patchwork of rectangles (OCTOPATH TRAVELER's world map on the traveler selection).
|
||||
- **Cause:** a sparse 2D texture without a packaged layout is laid out with the block height of its 64 KB tile, whose shape in blocks is the standard one: 256×256, 256×128, 128×128, 128×64 and 64×64 blocks for 1, 2, 4, 8 and 16 bytes per block, i.e. 32, 16, 16, 8 and 8 GOBs. The host used the [automatic block height](#automatic-block-height-truncates): 16 GOBs for the 4096×2048 BC1 map (512 block rows), whose tiles then landed in the wrong places.
|
||||
- **Fix:** `nvn_tex.c` `sparse_tile_h` gives such a texture its tile's block height; a packaged layout still wins.
|
||||
- **Found on:** [OCTOPATH TRAVELER](../../dm/games/010057D006492000/README.md) (Unreal Engine 4.18 flags its streamed and CPU-loaded textures sparse).
|
||||
- **Check:** the map's guest bytes deswizzled in Python at 1 to 32 GOBs read right only at 8; a 512×512 RGBA8 sprite sheet only at 16; [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md)'s packaged ASTC 6×6 textures (16 bytes per block) carry 8 in their layout. The 1- and 2-byte heights follow the same tile shapes and were not seen.
|
||||
|
||||
### Sparse textures bound from their first mapped level
|
||||
|
||||
- **Symptom:** streamed textures read 0 (magenta on Monster Hunter Rise's title screen's clothes).
|
||||
@@ -137,6 +146,14 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
|
||||
- **Fix:** reduction samplers filter mips linearly (`samp_by_id`): at the integer LODs reductions are used with, that is the same level.
|
||||
- **Found on:** [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md): its depth pyramid is built with a min-reduction sampler (`SamplerBuilderSetReductionFilter 1`; `151607c7a92386cc` samples the level above at an explicit LOD).
|
||||
|
||||
### Texture fields Metal refuses
|
||||
|
||||
- **Symptom:** the process ends at once (the window vanishes), with `-[MTLTextureDescriptorInternal validateWithDevice:]` failing an assertion such as `requests 9 mipmap levels, but the dimensions (1, 1, 1) can only support 1`, or `sampleCount must be > 1 for multisample textures`.
|
||||
- **Cause:** NVN takes texture fields Metal refuses, and Metal aborts instead of returning nil: more mip levels than the size holds (on NVN the extra levels are 1×1), an array or 3D texture of no layers, more than 2048 layers (a 1D array's layer count is its height, which the size check let through), and sample counts the GPU lacks (an M4 Pro has 1, 2 and 4; NVN also has 8 and 16). A blit at a level the Metal texture lacks is not checked either.
|
||||
- **Fix:** `tex_desc` makes such textures with the levels their size holds and one layer for none. Textures of more than 2048 layers, and multisample textures of one sample or of a sample count the GPU lacks, are not made, as textures past Metal's size limits are (fewer samples would leave shaders reading sample indices past the texture). A red line names each case once. Copies and `ReadTexels` at a level the host texture lacks are skipped (`tex_level_ok`). Retail data is within every bound except, possibly, 8- or 16-sample targets.
|
||||
- **Found on:** reported by MacNativeGames (`f864eb0`): a mod's 1×1 texture in Tears of the Kingdom declaring 9 levels. No game here is known to reach it; the sample counts (an M4 Pro has 1, 2 and 4, and a multisample texture of one sample aborts) were checked on that Mac while porting the fix.
|
||||
- **Check:** `t_view_levels` makes each case with Metal's validation layer on: a 1×1 texture of 9 levels, a 3D texture whose depth holds the levels, an array of no layers, a 1D array of 4,096 layers, an 8-sample and a 1-sample multisample texture, and a copy at a missing level.
|
||||
|
||||
## Render targets, passes and clears
|
||||
|
||||
### Transient render targets shared by storage
|
||||
@@ -146,6 +163,14 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
|
||||
- **Fix:** host textures are shared by `(pool, offset, description)`.
|
||||
- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md).
|
||||
|
||||
### IMAGE usage does not split texture storage
|
||||
|
||||
- **Symptom:** a model has correct geometry, base colour and normals but appears as a black silhouette; deferred lighting's material-ID depth texture stays zero.
|
||||
- **Cause:** render and stencil-sampling descriptors name the same pool, offset and layout, differing only by `NVN_TEXTURE_FLAGS_IMAGE`. Including that usage flag in the storage key made two independent Metal textures; sampling read the empty one's stencil.
|
||||
- **Fix:** the storage key excludes `IMAGE`. Sampling shares the original texture. A writable image binding prepares any needed `ShaderWrite` usage before opening its encoder; the upgrade copies the pixels and replaces every name of the old texture. Upgrading while encoding arguments would end the active encoder underneath its caller.
|
||||
- **Check:** `t_stencil_view` writes both planes, reads through an IMAGE stencil descriptor and copies to separate storage under Metal validation; `t_upgrade_alias` checks image-write preparation and every live/finalized alias.
|
||||
- **Found on:** [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md).
|
||||
|
||||
### Shared-storage entries taken over by a new texture
|
||||
|
||||
- **Symptom:** GPU memory grows with every image shown; the memory line's `shared textures` count keeps rising.
|
||||
@@ -154,6 +179,15 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
|
||||
- **Found on:** [STEINS;GATE RE:BOOT](../../dm/games/01008BC024FDE000/README.md) (ASTC 4x4 images up to 4096 wide, RGBA8 render targets, R8 masks): from a quick save, 297 entries and 1.29 GB of GPU memory at frame 6,600; 468 entries and 1.58 GB 24,000 frames into a new game. With the fix: 97 entries and 0.93 GB at frame 6,600; 15 minutes of story from the quick save (53,700 frames) stayed at 1.08–1.15 GB of GPU memory and 1.02–1.13 GB resident, and a new game from boot stays at 0.97–1.03 GB from its first story scene to frame 27,300 (with the 16-submission guard).
|
||||
- **Check:** Captain Toad: Treasure Tracker's opening course, rendered with agl's transient targets, keeps its 26 entries and takes none over, with the same memory and frame times as before.
|
||||
|
||||
### Live resolution variants share physical image storage
|
||||
|
||||
- **Symptom:** GPU memory grows as resolution changes or rooms load, although almost every retained texture still has a live NVN descriptor; reclaiming unused sharing-table entries cannot recover it.
|
||||
- **Cause:** an NVN texture object describes storage in a pool. Several live descriptions at the same pool offset can describe the same physical memory at different resolutions, but `(pool, offset, description)` sharing allocated a separate private Metal texture for every size. The live-object guard in `alias_take_over` correctly kept those descriptors, and consequently kept all their independent allocations.
|
||||
- **Fix:** `image_texture` in `nvn_mtl.m` gives a declared resolution family an exact-sized placement heap. Eligibility is GPU-only, mapped, non-sparse storage, exactly `COMPRESSIBLE|IMAGE`, single-level, single-sample 2D images. Sharing stays within one NVN pool, offset and format. Live descriptions are recorded at initialization so the first used resolution can allocate the largest declared requirement; a single-description address retains its ordinary allocation. No power-of-two capacity or extra pixel-format-view usage is imposed. A fresh heap is zeroed through its largest description; creating a later alias never queues a zero fill over another alias's content. Metal 4 residency names the heap, and encoder boundaries include resource-alias visibility while these heaps exist. Each texture holds its heap owner; normal sharing-table eviction and deferred texture release retire the allocation after its last texture/view goes, without a pool-lifetime heap cache.
|
||||
- **Limits:** this handles initialized resolution families, not arbitrary overlapping offsets, cross-format inheritance, CPU-visible or sparse storage. If a larger description is declared later and cannot fit an existing heap, it gets a new exact allocation; old heaps follow their existing textures' lifetimes. Inherited texels are not translated between incompatible texture layouts. No game-specific profile switch is used.
|
||||
- **Check:** `make -C dm/gfx/tests GAME=0100DCA0064A6000 check_t_image_alias` runs under Metal validation with direct and deferred encoding: 128 live descriptions retain their individual dimensions while sharing one allocation; small-first fresh storage, literal pixels across resolution changes, descriptor creation after a writer, indirect Metal 4 argument-buffer access, native integer-image atomics, depth/stencil clears and stencil views, pool/offset/format separation, excluded CPU-visible/sparse images and repeated destruction are checked. Integer-image matching restores the `ShaderAtomic` usage that Metal omits from its queried usage flags.
|
||||
- **Found on:** [Luigi's Mansion 3](../../dm/games/0100DCA0064A6000/README.md#texture-memory-2026-10-27), investigated from [#257](https://github.com/seedds/nxnative/issues/257) and [#100](https://github.com/seedds/nxnative/issues/100). The game's note records the live-allocation census and Mac comparison; other games and long iOS sessions remain unvalidated.
|
||||
|
||||
### Render-target views that change the format
|
||||
|
||||
- **Symptom:** the whole picture came out darker and more saturated on the Mac and the iPhone: comparing with a Switch screenshot, every channel at every level equalled the Switch value decoded from sRGB once more (0.55 → 0.26, 0.75 → 0.52).
|
||||
@@ -411,6 +445,13 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
|
||||
- **Fix:** translated shaders use only `buffer(0)`, so streams 14 and 15 now go to slots 14 and 15 (`VB_SLOT`).
|
||||
- **Found on:** [13 Sentinels: Aegis Rim](../../dm/games/01003FC01670C000/README.md) (a minute and a half into the prologue).
|
||||
|
||||
### Vertex streams at addresses that are not 4-byte aligned
|
||||
|
||||
- **Symptom:** Scaleform fills come out as solid white or light grey sheets over the whole screen, hard-edged triangles and bands, and the menu text is missing (a language list or a title screen that is all white behind its button prompt).
|
||||
- **Cause:** Metal reads a vertex buffer only from a 4-byte-aligned address: bound at an address 2 mod 4, every vertex was read 2 bytes early. NVN takes any address, and Scaleform's vertex streams (16-byte vertices: two RGBA8 attributes and an RG32F position) sit at addresses such as `…6d6a`. The positions became garbage spanning the screen, and the shapes, white with alpha 0.2–0.7 by their colour transforms, overlapped many times.
|
||||
- **Fix:** `bind_vbufs` copies a stream whose address is not 4-byte aligned into the submission's arena (`vbuf_unaligned`; one copy per address and size until guest writes or pool mappings change, up to 64 MB) and binds the copy. CPU copies require a host-readable, non-sparse range without pending GPU writers in this or an earlier submission; otherwise `vbuf_prepare` encodes an ordered GPU copy before the render encoder, including feedback and geometry captures. Submissions and pool mappings invalidate copies; buffer and shader writes invalidate the copies they can overlap, so unrelated uniform updates keep their vertex copies. The log's `vertex bindings` line counts the copies; `t_vbuf` checks byte-aligned readbacks, reuse, queued writes, writes between draws and pool replacement. The index buffers at addresses 2 mod 4 (16-bit indices) needed nothing.
|
||||
- **Found on:** Batman: Arkham Knight (`0100ACD0163D0000`; its profile and note are in a PR of their own) (Scaleform GFx; 2–13 copies of 3–18 MB a frame in the menus and at the title, which the draws' vertex ranges could narrow). With the copy the language menu and the title screen draw right.
|
||||
|
||||
### Programs bound per stage
|
||||
|
||||
- **Symptom:** scenes and characters black, only foliage, light shafts and the HUD drawn; pass dumps full of `Z … draw skipped: no program bound`.
|
||||
@@ -474,15 +515,35 @@ The replay on Metal (`dm/gfx/nvn/nvn_mtl.m`) and the Vulkan and OpenGL layers. E
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md).
|
||||
- **Note:** the replay used to commit its work and wait for the GPU before the first such draw after each GPU write, then read the count on the CPU (`mdi_wait`, now only for quads, fans and 8-bit indices). In Kamura Village that wait held the submitting thread 14 of its 20 ms a frame on an iPhone (36–54 fps). On the Mac the same scene's submit took 20 ms of wall time for 6.1 ms of work, and now 5.1 ms, all of it work.
|
||||
|
||||
### Rasterizer discard preserves vertex storage writes
|
||||
|
||||
- **Symptom:** the HUD and sky render but the battle's characters and terrain are absent; the replay logs `draw skipped: rasterizer discard` between model draws.
|
||||
- **Cause:** the replay returned from every discarded draw without active transform feedback. Rasterizer discard only suppresses rasterization: vertex storage writes and atomics still execute. Metal additionally requires a void-returning vertex function when rasterization is disabled.
|
||||
- **Fix:** the offline builder adds `main0_discard` to vertex shaders with writable reflected bindings. It retains the original body and early returns and drops only the stage outputs. Replay uses that entry in a separate pipeline with no fragment stage and rasterization disabled, with the ordinary array/indexed/instanced draw machinery and GPU-write tracking. Switching discard marks the pipeline dirty; discarded draws cannot prove an attachment fully overwritten. Pure vertex shaders with neither storage writes nor active feedback can still be skipped. Transform feedback retains its capture entry.
|
||||
- **Check:** `t_rast_discard` passes 19 checks under Metal validation: array/indexed storage writes despite an empty scissor and both faces culled, two instances, unchanged clear colours, normal rendering between two discard phases, and an ordered storage-write consumer. `test_discard.py` compiles a generated entry with side effects before two different returns for Mac and iOS.
|
||||
- **Found on:** [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md#files-and-shaders); its note tracks the gameplay verification.
|
||||
|
||||
### Primitive barriers order vertex storage writes
|
||||
|
||||
- **Symptom:** exploding meshes and displaced model parts after vertex-only work; native post-vertex captures can contain valid positions for some meshes while other palettes are incorrect.
|
||||
- **Cause:** `ORDER_PRIMITIVES` was omitted from the pass-splitting condition after shader storage writes. A primitive barrier orders earlier vertex work before later vertex work, including its storage writes. Metal does not supply that ordering between draws in one render encoder. Xenoblade Chronicles 3 uses barrier `0x21` after vertex shaders update its bone matrices; fragment-only barrier handling left dependent vertex work unordered.
|
||||
- **Fix:** a primitive barrier after shader writes ends the render pass, as fragment barriers already do. The next encoder's full consumer barrier supplies the required visibility.
|
||||
- **Check:** `t_rast_discard` passes 19 GPU checks under Metal validation, including a 4,096-vertex storage producer followed by a primitive barrier and a vertex consumer in the same submission.
|
||||
- **Found on:** [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md#files-and-shaders); `boot24barrier` removes the exploding meshes.
|
||||
|
||||
### Transform feedback captured by a vertex entry point
|
||||
|
||||
- **Symptom:** characters collapsed into one thin spike (vertex streams read from feedback buffers that were never written); a pass dump shows a pass of draws with a vertex shader and no fragment shader (`fs=-`) before the pass that reads them.
|
||||
- **Symptom:** characters collapsed into one thin spike (vertex streams read from feedback buffers that were never written); a character covered by a solid box while it moves (its fur shells pulled toward lagged positions that were never written); a pass dump shows draws with a vertex shader and no fragment shader (`fs=-`) before the draws that read them, and `Y tf begin … written 0/0/0/0` with no `T … tf capture` line.
|
||||
- **Cause:** a game that skins on the GPU with transform feedback: a vertex-only program runs under `SetRasterizerDiscard` between `BeginTransformFeedback` / `EndTransformFeedback` and writes into feedback buffers that later draws read as vertex streams. The host ignored all of it, so the streams kept whatever was there. Metal has no feedback stage.
|
||||
- **Fix:**
|
||||
- the layout comes from the program's control section, where the NVN driver would read it (`tf_layout` in `dmsh_build.py`): +0x6dc u16 strides, +0x6e8 per-buffer enables, +0x6ec buffer count, and per buffer b the captured output (0x20 + n) at +0xdc + 0x80 b and its component count at +0x4dc + 0x80 b. Any form other than one output per buffer from x on gets a red line and no capture entry point, so one shader cannot stop a game's build;
|
||||
- such a vertex shader gets a third entry point, `main0_tf` (`vs_tf`): the same body, returning nothing, storing the captured components as their bits into `device uint *` buffers at Metal vertex buffer slots 8–11 (`TF_SLOT`), indexed by vertex ID; the reflection lists `entry_tf` and `tf_strides`;
|
||||
- the binds, begin / end / pause / resume and rasterizer discard are records (`OP_TF_BIND`, `OP_TF_STATE`, `OP_RAST_DISCARD`). At replay a draw under rasterizer discard runs `tf_draw` in `nvn_mtl.m`: with feedback active and a capture entry point, an encoder of its own with no attachments, a pipeline with rasterization off, the buffers bound at their current offset less first × stride, the offsets advanced and the written ranges queued as GPU writes (`pend_add` kind 4); without feedback the draw rasterizes nothing, as on the GPU (it used to draw, depth writes included). Only plain array draws are captured; indexed, indirect or instanced ones give a red line. `DM_NVN_PASSDUMP` prints the records as `Y tf …` and each capture as a `T … tf capture` line.
|
||||
- the layout comes from the program's control section (`tf_layout` in `dmsh_build.py`): +0x6dc u16 strides, +0x6e8 per-buffer output counts, +0x6ec buffer count, and per buffer b the output locations (0x20 + n) at +0xdc + 0x80 b and matching **byte** component counts at +0x4dc + 0x80 b. Multiple outputs are interleaved in that order. Unknown forms get a red line and no capture entry;
|
||||
- `main0_tf` (`vs_tf`) retains the body and stores the output bits into Metal vertex buffers 8–11, with the GPU-prepared capture record at buffer 14. The reflection lists `entry_tf` and `tf_strides`. Geometry programs give their output reader the same entry; it gathers the packed point/line/triangle indices while writing linear feedback records;
|
||||
- `nvn_feedback.inc` keeps offsets and counts on the GPU. Begin clears them; End/Pause save them in the game's opaque 32-byte control buffer; Resume restores them. A compute helper bounds each capture by all active streams and complete primitives. `DrawTransformFeedback` uses the saved count as indirect array-draw arguments. Captures, counter copies and consuming draws follow encoder ordering, and captured ranges enter GPU-write tracking. Plain vertex capture supports array point/line/triangle lists with or without rasterizer discard, including instances; indexed/indirect vertex capture remains open. Geometry capture uses the existing general assembly and compute path. `DM_NVN_PASSDUMP` prints the records as `Y tf …` and array captures as `T … tf capture`.
|
||||
- **Check:** `test_transform_feedback.py` compiles generated vertex/geometry entries for Mac and iOS; `t_transform_feedback` checks interleaving, saved offsets, resume, bounded capture, GPU-produced geometry counts, complete primitives and a following draw that consumes the saved count in the same submission.
|
||||
- **Found on:** [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md): the character preview. 28 such shaders (e.g. `681cc42e48c0bbc0` in the `special/imgui.trsha` pack; skin indices in attribute 12, weights in 13) write positions (location 0 xyz, 12 bytes a vertex) and packed normal and tangent bits (locations 1 and 2, x only, 4 bytes) into three feedback buffers; the character draws read them as vertex streams 0–2 (`RGB32F`, `RGB10A2SN`, `RGB10A2SN`). Every buffer in this game holds one output from x on (strides 12/4/4, 12/12/12, 12/4, 12). The game draws these while its frame target is bound. The pass dump showed a pass of 19 draws with `fs=-` before the character pass. Tears of the Kingdom's 14,766 and Ace Attorney's 2,475 vertex shaders have no feedback at all.
|
||||
- [Donkey Kong Country: Tropical Freeze](../../dm/games/0100C1F0051B6000/README.md): DK's fur. While DK moved, his fur shells (an instanced draw, `648304c43bbe3463` / `5b5ffba62bca9e0a`, three shells on the body's index buffer) covered him as one solid box. Each frame a vertex-only draw (`6979dd3cdbd62373` / `7dcdaed991c74bfe`, 3,400 vertices) captures the fur's lagged positions into one buffer, two outputs per vertex (xyz each, stride 24; output count 2, component bytes `03 03`), and the shells read them as vertex attribute 6. Two things kept the buffer at zero (`DM_NVN_KEYDUMP=5b5ffba62bca9e0a`: stream 6 held zeros and `2.3e24`): the old one-output rule rejected the layout (`[RED] … transform feedback buffer 0 has a layout of unknown form`), and the game draws the capture with rasterization on (no fragment shader, colour mask 0, no `SetRasterizerDiscard`), which the host did not capture. Four of its 11,644 vertex shaders have this form; over Z-A's and this game's 23,277 vertex shaders together, the old and new readings give identical layouts and capture code for all 28 shaders the old rule took.
|
||||
|
||||
- **Found on:** [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md#files-and-shaders): effect vertex and geometry programs use interleaved outputs, including eleven outputs in a 172-byte stream, and draw from saved feedback counts.
|
||||
|
||||
### Helper encoders end the open copy encoder first
|
||||
|
||||
@@ -700,6 +761,13 @@ Digimon Story Cyber Sleuth: Complete Edition (Media.Vision's MVGL engine, SDK 6.
|
||||
- **Fix:** the host's GL layer (`dm/gfx/gl/hle_gl.m`) answers the EGL imports and `eglGetProcAddress` (the rest of this section).
|
||||
- **Found on:** [Digimon Story Cyber Sleuth: Complete Edition](../../dm/games/010014E00DB56000/README.md) (SDK 6.4.0, Media.Vision's MVGL engine; one boot).
|
||||
|
||||
### Direct GL imports use the same implementations as the loader
|
||||
|
||||
- **Symptom:** `UNIMPL glCreateShader` returns 0, followed by `UNIMPL glShaderSource`, `glCompileShader`, `glGetShaderiv` and `glGetShaderInfoLog`; the game crashes in `sdk` with a null shader handle even though EGL initialized.
|
||||
- **Cause:** the GL implementations were registered only in `__DATA,swgl`, which `eglGetProcAddress` searched. Normal imports search `__DATA,swhle`; a game that imports GL directly never consulted the GL table and got named unimplemented stubs instead.
|
||||
- **Fix:** `GL` and `GLB` register in `swhle`, and `eglGetProcAddress` searches that same table. Both paths retain `GLB`'s big-stack entry; unsupported loader names still return null by default (`DM_GL_STUBS=1` enables diagnostic stubs). `make -C dm/gfx/tests GAME=010014E00DB56000 gl-imports` exercises the real import resolver and GL registrations, shader creation and source queries, wrapper addresses and missing names in both diagnostic modes, without game files.
|
||||
- **Found on:** Darkest Dungeon Complete (`01008F1008DA6000`, update v327680, SDK 3.6.0, provisional profile), reported in [issue #228](https://github.com/seedds/nxnative/issues/228). The import failure is reproduced by the regression test; gameplay and run-time shader capture for this title remain unverified.
|
||||
|
||||
### Telling a GL game from an NVN or Vulkan one
|
||||
|
||||
- **Symptom:** the NSP script's provisional profile said Vulkan, and the capture boot crashed in the GL layer.
|
||||
|
||||
@@ -46,6 +46,13 @@ To enroll a cut for any game's own module:
|
||||
- **Found on:** [DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition](../../dm/games/01006C300E9F0000/README.md) `main+0x2dd8cd0`.
|
||||
- **Note:** before fixing one site, a census of all of them saves a run per site: a copy of `patch_nso.py` whose assert prints and continues found this one alone among the 25,508 `x18` sites. That `main` has no unwind entries for its first 70 MB, so the rewriter disassembles them as one function and takes 6 minutes (see [Long functions analysed in pieces](#long-functions-analysed-in-pieces)).
|
||||
|
||||
### 64-bit bounds check of the index
|
||||
|
||||
- **Symptom:** `jump table <addr>` assertion in `patch_nso.py` on an offset table whose index is checked with a 64-bit compare (`cmp x22, #0x10; …; b.hi; adrp x18 + add x18; mov x0, x18; ldrsw x18, [x0, x22, lsl #2]; add x18, x18, x0; br x18`).
|
||||
- **Cause:** `jump_offsets` took only a 32-bit `cmp wI, #N` as the bounds check. A 64-bit compare bounds the index as well (the load uses the whole `xI`); here the table base also reaches the load through a copy, so only the far-base pass reads it.
|
||||
- **Fix:** in its last pass (`far_base`), `jump_offsets` accepts `cmp xI, #N` on the index too. The earlier passes are unchanged, so modules that rewrote before come out the same.
|
||||
- **Found on:** [OCTOPATH TRAVELER](../../dm/games/010057D006492000/README.md) `main+0x1676f58` (1.0.5), the only such site in that `main`.
|
||||
|
||||
### Index masked in the block that branches to the dispatch
|
||||
|
||||
- **Symptom:** `jump table <addr>` assertion in a Unity / IL2CPP `main` whose dispatch checks a fresh mask of another register than the one it indexes with.
|
||||
|
||||
@@ -14,8 +14,8 @@ SDK calls and system services the host answers (`dm/host/hle_*.c`). Each entry:
|
||||
|
||||
### SDK 3.6.0 answered as SDK 4
|
||||
|
||||
- The oldest SDK so far booted to its frame loop with the provisional profile as generated and no host change: the services, the NVN API version (53, as for every SDK up to 10), the audio output and the heap all answered as for SDK 4.
|
||||
- **Found on:** [Bayonetta 2](../../dm/games/01007960049A0000/README.md) (PlatinumGames' engine in its Wii U form): what it needed was graphics ([Clears of attachments larger than the render pass](rendering.md#clears-of-attachments-larger-than-the-render-pass), [`Downsample` of a single-sample texture](rendering.md#downsample-of-a-single-sample-texture)) and a reader for its CRI CPK archives ([BNSH files in PlatinumGames DAT archives inside CRI CPK archives](shaders.md#bnsh-files-in-platinumgames-dat-archives-inside-cri-cpk-archives)); [Bayonetta](../../dm/games/010076F0049A2000/README.md), on the same `sdk` and `subsdk0` builds, then needed no change at all (its profile is Bayonetta 2's with its own build row and tables, and the route from the boot to the first fight logged no red line).
|
||||
- SDK 3.6.0 booted to its frame loop with the provisional profile as generated and no host change: the services, the NVN API version (53, as for every SDK up to 10), the audio output and the heap all answered as for SDK 4.
|
||||
- **Found on:** [Bayonetta 2](../../dm/games/01007960049A0000/README.md) (PlatinumGames' engine in its Wii U form): what it needed was graphics ([Clears of attachments larger than the render pass](rendering.md#clears-of-attachments-larger-than-the-render-pass), [`Downsample` of a single-sample texture](rendering.md#downsample-of-a-single-sample-texture)) and a reader for its CRI CPK archives ([BNSH files in PlatinumGames DAT archives inside CRI CPK archives](shaders.md#bnsh-files-in-platinumgames-dat-archives-inside-cri-cpk-archives)); [Bayonetta](../../dm/games/010076F0049A2000/README.md), on the same `sdk` and `subsdk0` builds, then needed no change at all (its profile is Bayonetta 2's with its own build row and tables, and the route from the boot to the first fight logged no red line). [OCTOPATH TRAVELER](../../dm/games/010057D006492000/README.md)'s SDK 3.5.1 answered as SDK 4 as well (NVN 53.313); its audio renderer needed the update lock found without `SdkMutexType` ([No `SdkMutexType` in SDK 3](audio.md#no-sdkmutextype-in-sdk-3)).
|
||||
|
||||
### Listing a game's unhooked imports before they are met
|
||||
|
||||
@@ -138,6 +138,14 @@ Most service calls can be found before they crash, before the first boot or the
|
||||
- **Found on:** [Animal Crossing: New Horizons](../../dm/games/01006F8002326000/README.md) (PR #25).
|
||||
- **Note:** SDK 11's `fs::CommitSaveData(mount)` is the older name of `fs::Commit` ([Older SDK names for the same calls](#older-sdk-names-for-the-same-calls)); `fs::Commit` with a `CommitOption` is under [Device save data on the account save's directory](#device-save-data-on-the-account-saves-directory).
|
||||
|
||||
### Save-write failures stay with their backing transaction
|
||||
|
||||
- **Symptom:** a failed write in one independent save can make another save's next `Commit` fail and discard its successful changes; the damaged save may then commit its truncated working file.
|
||||
- **Cause:** the host used one process-wide failure marker, consumed by whichever save committed first.
|
||||
- **Fix:** each canonical backing save owns its failure state. Open file handles retain that transaction identity, aliases share it, and each commit waits for in-flight writes or size changes to its backing save before consuming or restoring its failure marker (`hle_fs.c`). Concurrent first writes initialize the shared failure-injection settings with `pthread_once`.
|
||||
- **Check:** `t_fs_commit_list` injects write failures after truncation in either of two independent saves, verifies that the other save commits its changes, and checks failure propagation through aliases, rename failure handling, and a failed recovery clone. It blocks writes and size changes through an alias while another save commits, then verifies that both Commit ABIs wait for the affected I/O and refuse its failure. `make -C dm/host/tests GAME=<title ID> fs-tsan` also checks concurrent first writes and failed file I/O concurrent with mounting another alias.
|
||||
- **Found on:** the [two-save host regression](../../dm/host/tests/t_fs_commit_list.c). [Mario Kart 8 Deluxe](../../dm/games/0100152000022000/README.md) uses independent device and account saves.
|
||||
|
||||
### Save data size queries
|
||||
|
||||
- **Symptom:** SIGSEGV at address 0 in `nn::fs::GetSaveDataSizeMax`, about 13 minutes into a new game.
|
||||
@@ -242,6 +250,14 @@ Most service calls can be found before they crash, before the first boot or the
|
||||
- **Fix (resolved):** the handles no longer wait for a routed output: with rumble on and an output sink installed (`dm_haptics_enabled`), a connected npad style gets its two handles; `vib_send` checks for an output at each send (`hle_hid.c`). No sink, rumble off or a disconnected npad still get none, and `IsVibrationPermitted` still follows the routed outputs. `DM_RUMBLE_LOG=1` logs the handle requests and, once a second, the values sent, those with no routed output and the peak amplitude. Each input value is counted separately, including mixed batches; a mutex protects the diagnostic totals across game threads. `t_hid` covers cached startup handles delivering a nonzero value after routing, mixed-batch diagnostics and concurrent sends; `t_hid_tsan` checks those sends for data races.
|
||||
- **Found on:** [Unicorn Overlord](../../dm/games/010069401ADB8000/README.md) (the Asian release, `010054B01AD92000`), in the log only: the game has no rumble of its own (it sends amplitude 0 throughout), so nothing could be felt before or after. Not yet checked on a game that rumbles.
|
||||
|
||||
### Scripted presses on a path of their own
|
||||
|
||||
- **Symptom:** with a controller connected (a DualShock 4 here), `DM_KEYS` and `DM_CTL` presses are logged (`DM_KEYS: … press …`) but the game does not react to them, or only now and then; without the controller the same script works.
|
||||
- **Cause:** scripted presses went through player 1's controller path (`dm_pad_set`). A connected controller publishes its whole state there on every change, replacing scripted holds and sticks; the press latch preserves only the first read. Script release also cleared the physical controller's state.
|
||||
- **Fix (resolved):** `DM_KEYS` and `DM_CTL` set buttons, a press latch and sticks of their own (`script_set` in `hle_hid.c`), ORed with the keyboard / touch path and player 1's controller when the game reads player 1; for the sticks, the one deflected more wins. Shake includes the scripted path too.
|
||||
- **Check:** `dm/host/tests/t_hid.c` (`t_script`) runs the scripted publisher with a controller update during the hold: buttons survive repeated reads after the latch is consumed, the scripted stick wins over drift, shake continues, and release preserves physical input. Short presses are latched once, all four axes reach the handheld npad, and player 2 stays separate.
|
||||
- **Found on:** Batman: Arkham Knight (`0100ACD0163D0000`), reported in [PR #326](https://github.com/seedds/nxnative/pull/326) while driving the opening route with `DM_KEYS` and a DualShock 4. Reproduced independently in the host HID test without game files; its game profile is not in this checkout.
|
||||
|
||||
### Touch gestures recognized from the touch points
|
||||
|
||||
- **Symptom:** taps in handheld mode do nothing, though the game reads the touch screen (the title's PRESS ANY BUTTON, the menus' cards).
|
||||
@@ -347,10 +363,14 @@ Most service calls can be found before they crash, before the first boot or the
|
||||
|
||||
### oe calls answered without an applet proxy
|
||||
|
||||
- **Symptom:** the first boot crashed in `oe::DisableAlbumImageTakenNotification`: a null read at `sdk+0x338070`, the first call in `nnMain` (`main+0x6b188c`); other `oe` calls that reach am, found in the imports before they were met.
|
||||
- **Symptom:** the first boot crashed in `oe::DisableAlbumImageTakenNotification`: a null read at `sdk+0x338070`, the first call in `nnMain` (`main+0x6b188c`); enabling those notifications also gives a null read at `sdk+0x33b0f0` (Xenoblade Chronicles 3); other `oe` calls that reach am, found in the imports before they were met.
|
||||
- **Symptom:** a null read at `sdk+0x36ec60` in `oe::EnableAlbumImageTakenNotification`, called from `nnMain` (`main+0x286268`).
|
||||
- **Fix:** `EnableAlbumImageTakenNotification` uses the same host implementation as the disable call: no system-album notifications are produced. Found on [Mario + Rabbids Sparks of Hope](../../dm/games/0100317013770000/README.md) (SDK 13.3.5); the next boot reaches the NVN frame loop.
|
||||
- **Cause:** the SDK asks am through the applet proxy the host never made.
|
||||
- **Album notification enable:** `oe::EnableAlbumImageTakenNotification` follows the existing disable takeover. The host produces no Horizon album captures, so enabling notifications produces no messages. Found on [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md) (SDK 12.3.9, `main+0x127fac4`).
|
||||
- **Inactivity extension:** `oe::GetUserInactivityDetectionTimeExtensionMode` also reads the absent SelfController (`sdk+0x3391f0`, after Xenoblade Chronicles 3's opening movie). The host retains the requested mode: 0 initially or when either setter receives false, 1 for `SetUserInactivityDetectionTimeExtended(true)`, 2 for its `Unsafe` form. The boolean query is mode != 0, matching SDK 12.3.9's implementation. This state does not make the host turn the screen off. `t_oe` checks both setters and both queries.
|
||||
- **Fix:** hooked (`hle_oe.c`): `oe::DisableAlbumImageTakenNotification`; `oe::IsInStoreDemoModeEnabled` (no); `oe::GetDefaultDisplayResolution` and its change event; `oe::SendServerMaintenanceOverlayNotification`; `oe::SetUserInactivityDetectionTimeExtendedUnsafe`; `oe::GetPerformanceConfiguration`; `oe::SetAlbumImageOrientation`; `oe::Begin` / `EndAutoSleepDisabledSection` and `IsAutoSleepDisabled` (counted); `oe::ExecuteProgram` (logged red: the host runs one program). The ones with a symptom of their own follow: [The startup logo](#the-startup-logo), [Copyright frame buffer overlay](#copyright-frame-buffer-overlay), [Applet resource user ID, launch parameters and VR mode](#applet-resource-user-id-launch-parameters-and-vr-mode), [CPU overclock calls without an apm session](#cpu-overclock-calls-without-an-apm-session), [Focus handling mode decides whether focus changes are notified](#focus-handling-mode-decides-whether-focus-changes-are-notified).
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md) (SDK 11: `GetDefaultDisplayResolution`, `SendServerMaintenanceOverlayNotification`); [13 Sentinels: Aegis Rim](../../dm/games/01003FC01670C000/README.md) (SDK 11: `SetUserInactivityDetectionTimeExtendedUnsafe`); [Super Mario 3D World + Bowser's Fury](../../dm/games/010028600EBDA000/README.md) (SDK 10: `GetPerformanceConfiguration`, `SetAlbumImageOrientation`); [DRAGON QUEST XI S](../../dm/games/01006C300E9F0000/README.md) (SDK 7.3.2: the auto-sleep calls); [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md) (`oe::ExecuteProgram`); [The Great Ace Attorney Chronicles](../../dm/games/010036E00FB20000/README.md) (SDK 10.4.1, Capcom's MT Framework: `DisableAlbumImageTakenNotification`, then `IsInStoreDemoModeEnabled` from the import list). After all of its fixes The Great Ace Attorney Chronicles runs its frame loop at 30 fps; the black screen it showed was its textures, never uploaded (CPU-written textures, the profile's `tex_flush_upload` and `tex_dirty_upload`: [Mapped-range flushes taken as uploads (`tex_flush_upload`)](nvn-objects.md#mapped-range-flushes-taken-as-uploads-tex_flush_upload), [CPU writes with no flush, caught by page protection (`tex_dirty_upload`)](nvn-objects.md#cpu-writes-with-no-flush-caught-by-page-protection-tex_dirty_upload)).
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md) (SDK 11: `GetDefaultDisplayResolution`, `SendServerMaintenanceOverlayNotification`); [13 Sentinels: Aegis Rim](../../dm/games/01003FC01670C000/README.md) (SDK 11: `SetUserInactivityDetectionTimeExtendedUnsafe`); [Super Mario 3D World + Bowser's Fury](../../dm/games/010028600EBDA000/README.md) (SDK 10: `GetPerformanceConfiguration`, `SetAlbumImageOrientation`); [DRAGON QUEST XI S](../../dm/games/01006C300E9F0000/README.md) (SDK 7.3.2: the auto-sleep calls); [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md) (`oe::ExecuteProgram`); [The Great Ace Attorney Chronicles](../../dm/games/010036E00FB20000/README.md) (SDK 10.4.1, Capcom's MT Framework: `DisableAlbumImageTakenNotification`, then `IsInStoreDemoModeEnabled` from the import list); [Donkey Kong Country: Tropical Freeze](../../dm/games/0100C1F0051B6000/README.md) (SDK 4.4.0: `GetUserInactivityDetectionTimeExtensionMode`, a null read at `sdk+0x15b73c` when the first level's cinematic camera starts, `CScreenDimmingUtil::DisableScreenDimming`). After all of its fixes The Great Ace Attorney Chronicles runs its frame loop at 30 fps; the black screen it showed was its textures, never uploaded (CPU-written textures, the profile's `tex_flush_upload` and `tex_dirty_upload`: [Mapped-range flushes taken as uploads (`tex_flush_upload`)](nvn-objects.md#mapped-range-flushes-taken-as-uploads-tex_flush_upload), [CPU writes with no flush, caught by page protection (`tex_dirty_upload`)](nvn-objects.md#cpu-writes-with-no-flush-caught-by-page-protection-tex_dirty_upload)).
|
||||
|
||||
### The startup logo
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
- **Fix:** it now goes by the zstd magic (`28 b5 2f fd`) whatever the name.
|
||||
- **Found on:** [Paper Mario: The Thousand-Year Door](../../dm/games/0100ECD018EBE000/README.md) (8,726 of the RomFS's 9,959 files, the shader archives among them). Of the other 21 games' RomFS on hand only Super Mario Galaxy's `GDDR3.zst` (a Vulkan game, no NVN split) has such a file.
|
||||
|
||||
### HAL's `.cmp` containers: a size-prefixed frame
|
||||
|
||||
- **Symptom:** the game asks for shaders the map does not have (`[RED] shader ... is not in the shader map`, hundreds of lines), and the split finds far fewer shaders than the game uses (121 against 27,288 on Kirby and the Forgotten Land).
|
||||
- **Cause:** HAL keeps its assets in `.cmp` containers: a 4-byte little-endian uncompressed size, then a frame (zstd, or LZ4 in the `ubin/` map data). The split decompressed a zstd file by its magic at offset 0, so a frame that starts at offset 4 was read raw, and the shaders inside the BFRES archives of those containers were never seen.
|
||||
- **Fix:** the split opens a size-prefixed frame as well (`unsized`: zstd, with a dictionary as for a plain zstd file, or LZ4) when the file's bytes 4 to 8 are a frame magic and bytes 0 to 4 are not the zstd magic. A frame whose header records a size other than the prefix is not such a container, and the file is read as it is: Tears of the Kingdom's 1,202 `Cave/…/C.crbin.<hash>/*.quad` files are a 4-byte hash (the one in their folder name) and a zstd frame, the only files with a frame at +4 in the RomFS of 39 other games on hand. A frame that records no size must decompress to exactly the prefix's size; otherwise (it does not decompress, it holds more or less, or it names a dictionary `Pack/ZsDic.pack.zs` does not have) the split prints a `[RED]` line and reads the file as it is.
|
||||
- **Found on:** [Kirby and the Forgotten Land](../../dm/games/01004D300C5AE000/README.md): 7,201 `.cmp` files, 5,121 of them zstd frames with 19,364 BNSH inside (11.2 GB decompressed) and 2,080 LZ4 frames, all of the latter in `ubin/` (0.3 GB, and no BNSH); the same layout is in 430 `.cmpbin` files (76 zstd in `lyt/`, 354 LZ4 in `snd/`). Every zstd frame records its size, the same as its prefix; no LZ4 frame does, and each decompresses to its prefix's size. The split now finds 27,288 shaders in the base game (18,476 in `gfx/`, 8,313 in `ptcl/`, 378 in `lyt/`, 67 in the loose `shader` files, 54 in `main`), against 121 before, and 27,302 with update 1.1.0 (v65536: 14 more in `lyt/`).
|
||||
|
||||
### BNSH in Bezel Engine archives
|
||||
|
||||
- **How it is stored:** the shaders are BNSH in the `.bfsha` entries of the `.bea` archives (zstd entries, [01](../agents/01-extract.md)), most of them the engine's own in `System.nx.bea` (`_BezelSystemResources/<system>/shader/*.bfsha`: PBR shadow maps 846, post effects 770, …), the rest in the layouts' `__ArchiveShader.bnsh` and the per-archive shader packs (`.bnbshpk`), plus one BNSH in `main`.
|
||||
@@ -109,6 +116,14 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
- **Reader:** `kpka_jobs` maps the PAKs, takes the newest copy of each path, peeks at 4 decompressed bytes and inflates only SDF entries (`sdf_shaders`).
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md): 63,111 shaders (30,233 vertex, 21,475 pixel, 11,271 compute, 53 geometry, 33 hull, 46 domain) and 102,345 pairs, in 5 s with 12 jobs.
|
||||
|
||||
### Shader records in Snowdrop SDF data slices
|
||||
|
||||
- **Symptom:** the split finds no shaders in a RomFS made of `.sdfdata`, `.sdftoc` and `.sdfver` files.
|
||||
- **Cause:** Snowdrop stores the shaders in individually compressed records inside its `BERG` data slices. These SDF archives are a different format from RE Engine's shader definitions above. A shader record has a 20-byte header `{u32 0xabcd00c6, flags, 0, raw length, stored length}`, then one zstd frame; reflection data precedes the NVN sections inside it.
|
||||
- **Fix:** `shader_split.py` recognizes `BERG` by magic and reads the length-delimited compressed records (`snowdrop_shaders`). Every control section is followed immediately by its code, with lengths from control +0x20 and +0x704; they are byte-packed, so a control need not be aligned. A single record can hold more than one shader. Each section must pass the control / code / stage checks. Programs hold one stage, and the vertex shaders take the [single-stage cover](#single-stage-programs-paired-with-a-cover). Scanning for the record header avoids mistaking a coincidental zstd magic inside compressed data for another asset.
|
||||
- **DLC:** additional shader records ship in the DLC's data slices. The NSP script includes each extracted DLC RomFS as an extra shader root, without an ExeFS module, and fingerprints its index and extraction timestamps for split reuse. Vulkan and OpenGL builds include the DLC roots too. When installing or removing optional DLC changes an NVN shader union, the script uses an external profile with new shader tables only after every non-DLC shader row and fact matches the registered tables; package indices may change. The registered module builds, settings and build-gated cuts are preserved. A cached proof of this comparison permits repeat builds to reuse the split.
|
||||
- **Found on:** [Mario + Rabbids Sparks of Hope](../../dm/games/0100317013770000/README.md) (Snowdrop, SDF version 22).
|
||||
|
||||
### SPK shader package in an MT Framework ARC
|
||||
|
||||
- **How it is stored:** the engine's one shader package, `sc\NX_64\root` (`SPK\0`, type hash `0x02358e1a`) in `nativeNXx64/sa/NX_64/root.arc` ([01](../agents/01-extract.md)). After the package's tables every shader is a code area followed directly by its control section, back to back to the end of the file: 230 vertex shaders, then 230 pixel shaders.
|
||||
@@ -142,6 +157,15 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
- **Found on:** [Bayonetta 2](../../dm/games/01007960049A0000/README.md): 1,856 distinct keys (1,547 pixel, 309 vertex) in `shader.dat`, `shader2.dat` and `shadereff.dat` of `data000.cpk` (the split about 2.5 minutes); [Bayonetta](../../dm/games/010076F0049A2000/README.md): 458 distinct keys (236 pixel, 222 vertex), the members of `shader.dat` in `data00.cpk` (about 1.7 minutes). All build for both.
|
||||
- **Not checked:** 13 Sentinels and Catherine: Full Body's CPKs (no files on the drives).
|
||||
|
||||
### Monolith ARH1, XBC1 and HCPS/SLCT shader containers
|
||||
|
||||
- **Symptom:** archive shaders are absent from the map; the two boot shaders in `main` are reported as raw controls with no identified code area.
|
||||
- **Cause:** Monolith uses ARH1/ARD archives and XBC1 streams. HCPS shader containers live inside models, map streams and standalone shader files; a standalone SLCT in `main` omits the HCPS header.
|
||||
- **Fix:** `shader_split.py` reconstructs paths from the ARH1 XOR trie, reads bounded archive entries in parallel, and decodes stored/zlib/zstd XBC1 streams recursively. The ARH stored size excludes an XBC1 wrapper's 48 bytes. Decoded members are searched for both HCPS and complete GLSLC outputs (effect asset packs); compressed literals are visited only after decoding. HCPS's SLCT/NVSD descriptors locate each variant's control and code ranges; a vertex bundle can include geometry, so stages are split by each control's own lengths. Text-bearing controls advance at eight-byte alignment. Standalone SLCT sections use offsets relative to their own start. Source-only HCPS containers with empty code/control ranges hold no NVN binaries. Pairs stay within their program variant. Synthetic fixtures cover wrapper bounds, embedded geometry, variants, standalone offsets and compressed streams containing literal container magics.
|
||||
- **Nested effect compression:** Firehawk WIPAC resources can wrap their `BlShaderArchive` GLSLC programs in `cmp\0zlib`: an eight-byte signature, little-endian stored/raw u32 lengths, then zlib. This layer lives inside the decoded XBC1 entry. The recursive reader checks both lengths and the end of the stream; it excludes these compressed ranges from raw container scans too. The four initially missing geometry/pixel keys and two additional vertex/pixel keys from Xenoblade Chronicles 3's opening match `/effect/fld/ma01a.wipac` after this decoding. Synthetic tests cover nested literal streams and incorrect sizes.
|
||||
- **Found on:** [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md#files-and-shaders), whose note records the census and runtime key checks.
|
||||
- **Format references:** [ARH1 layout](https://github.com/roccodev/ard-tools/blob/master/docs/arh1.md), [xc3_lib shader structures](https://github.com/ScanMountGoat/xc3_lib/blob/main/xc3_lib/src/spch.rs). The reader's lengths and keys were checked against the supplied game files.
|
||||
|
||||
### Needle shader containers in main and GFD archives
|
||||
|
||||
- **Symptom:** the split finds no shaders, although `main` contains NVN control sections; it reports that no data record or code-address pair identifies their code areas.
|
||||
@@ -164,6 +188,14 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
- **Fix:** the reader (`raw_shaders`) takes them. Both neighbours can pass the header and length checks, so it takes only a code area the code builds the address of, the nearest one to the control section's.
|
||||
- **Found on:** [Advance Wars 1+2: Re-Boot Camp](../../dm/games/0100300012F2A000/README.md) (Unity 2020.2 with Bink 2's Unity plugin). Of 25 `main`s on hand no other has such control sections.
|
||||
|
||||
### SNVN program records in Retro Studios' RFRM forms
|
||||
|
||||
- **Symptom:** the provisional split finds only `main`'s 294 GLSLC outputs; the 3D scene and the title logo are magenta, with `[RED] shader … is not in the shader map` lines.
|
||||
- **How it is stored:** Retro's files are RFRM forms (`RFRM`, big-endian u64 data length, u64, then the form type and two u32 versions; chunks inside are a fourcc, u64 length, two u32 versions, then the data). `MaterialArchive.arc` is one `MTRL` form holding an `SDX ` chunk (the same materials' D3D bytecode, 3,468 DXBC programs) and an `SNVN` chunk (the NVN ones). The `.pak` files are `PACK` forms whose `ADIR` chunk lists the resources (u32 count, then 36-byte entries: fourcc type, 16-byte GUID, big-endian u64 offset and length); each `MTRL` resource is an RFRM form whose data is one zlib stream holding another `MTRL` form with an `SNVN` chunk. An SNVN program record is a big-endian u32 code length, u32 control length, a byte, u32 stream length, then a zlib stream that inflates to the program's code areas back to back followed by their control sections in the same order (0x840 bytes each on SDK 4). The chunk's tables differ between files, so the reader finds the records by their own lengths: a stream whose header ends where it says and that inflates to exactly code + control bytes.
|
||||
- **Startup archive in `main`:** the first programs the game builds (the `CGraphicsUtilCache` quads that draw the title logo) come from a startup material archive compressed into `main` (`NMaterialStartup::get_startup_archive_compressed_buffer`, 5,870 bytes; `NMaterialArchive::ImportStartupArchive` inflates it at boot). Its `CBufferedCompression` header is a type byte (1: LZSS on 8-bit units, `CLZSSDecompressor::DecompressBuffer8`) and three zero bytes; the data inflates to an `MTRL` form like `MaterialArchive.arc`'s. Its programs share code areas with `MaterialArchive.arc`'s but differ in control +0x7d0 (a per-program value), so their keys are their own.
|
||||
- **Reader:** `rfrm_shaders`, `snvn_records`, `retro_main_forms` and `retro_lzss8` in `shader_split.py`; each record is one program, so its stages pair with each other.
|
||||
- **Found on:** [Donkey Kong Country: Tropical Freeze](../../dm/games/0100C1F0051B6000/README.md) (SDK 4.4.0): 24,098 shaders (11,644 vertex, 11,734 pixel, 720 geometry, which run as compute kernels): 3,460 first seen in `MaterialArchive.arc` (its 3,468 stages), the rest in the 795 `MTRL` resources of the packs, 8 in the startup archive and 294 GLSLC outputs in `main`; about 8 s.
|
||||
|
||||
### MGFX effects in MonoGame XNB files
|
||||
|
||||
- **How it is stored:** the 13 XNB files in `Content/Effects` (XNB platform `S`, uncompressed, read by `EffectReader`) each hold one MGFX effect, and `main` holds SpriteBatch's built-in one; no other file and no GLSLC output. A compressed XNB (MonoGame's LZX flag 0x80 or LZ4 flag 0x40, as in Stardew Valley) is decompressed first (`xnb_decompress`).
|
||||
@@ -197,6 +229,7 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
- [Brain Memory Training Academy](../../dm/games/0100F5D01DA3A000/README.md) (Unity 2022.3.5f1: only `Data/*.assets`, `level*` and the two engine files in `Data/Resources`, none with type trees). The `Shader` type hash there (`ea424a5f…`) is the one in Ace Attorney Investigations Collection's bundles (2022.3.17f1): the same layout, about 30 structs. Checked against Ace Attorney's own tree: the same 2,324 nodes with the same alignment flags, and its 40 bundle `Shader` objects decode identically with either. 496 stage shaders: 439 engine built-ins, 46 of the game's, 11 of the player's in `main`, and the built-in VR blit's geometry shader, which does not build.
|
||||
- [Patrick's Parabox](../../dm/games/0100E44016C22000/README.md) (Unity 2020.3.30f1, built-in shaders only, no bundles) stopped there with hash `a30e3dcf…`, which one 18 KB shader bundle of Fire Emblem Engage (2020.3.18f1) carries; its tree is now in the table too (`SHADER_2020_3`: per subprogram global and local keyword indices, every subprogram in `m_SubPrograms`, no parameter blobs, keyword names or stage counts), equal to Engage's node for node (2,179 nodes, the alignment flags included), and each of the game's 48 `Shader` objects reads to its exact end.
|
||||
- [Raging Loop](../../dm/games/010020400E1C2000/README.md) (Unity 2018.3.10f1, no bundles: 48 `Shader` objects in `Data/*.assets` and `Data/Resources`) stopped there with hash `8db76007…`, and no game of that Unity version with a bundle was at hand, so `SHADER_2018_3` is written from AssetRipper's TypeTreeDumps (`StructsDump/release/2018.3.10f1.dump`), checked node for node against the dump (1,325 nodes, the alignment flags included). It is a layout of its own, older than the common structs: no conservative raster state, `BufferBinding` without `m_ArraySize`, `ConstantBuffer` without `m_IsPartialCB`, the parameters in each subprogram and no `m_CommonParameters`, one segment per platform (the pre-2019.3 form Cuphead's trees gave `shader_programs`). Each of the 48 objects reads to its exact end: 450 subprograms of version 201802150 (older than the local keyword list, which starts at 201806140).
|
||||
- [Donkey Kong Country Returns HD](../../dm/games/01009D901BC56000/README.md) (Unity 6000.0.28f1, no bundles): hash `2ec7223d…` matches BALL x PIT's Unity 6000.0.62f1 bundles. `SHADER_6000_0` derives from the 2022.3 layout by removing each pass's `m_EditorDataHash` and `m_Platforms` arrays and appending the shader's `m_AssetGUID`. The generated tree equals the bundle tree in types, field order and alignment flags; the game note records the object census and build results.
|
||||
- **Note:** a Unity version whose `Shader` hash is not in the table still stops with the same message; read its tree from any bundle of that Unity version and add it. To take one file out of an NSP without extracting the RomFS, filter `nx_extract`'s `Copy` by path.
|
||||
|
||||
### Unity 6 Shader objects
|
||||
@@ -214,6 +247,13 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
- **Older layouts:** Unity 2019.4 stores each kernel's code directly, without `uniqueVariants` or `variantMap`. `unity_compute` accepts that form too, and `COMPUTE_2019_4` supplies the stripped-file layout. Found on [Pokémon Brilliant Diamond](../../dm/games/0100000011D90000/README.md) (2019.4.27f1); its missing runtime shader is the built-in GPU skinning kernel. The layout matches the release dump's 116 nodes and alignment flags, and both built-in compute objects decode to their exact ends.
|
||||
- **Note:** a file whose `ComputeShader` has no tree still goes to the second pass; if it has none there either, the split prints a `[RED]` line and leaves its kernels out instead of stopping, so profiles whose compute shaders were never read keep their tables: [Cuphead](../../dm/games/0100A5C00D162000/README.md) (2) and [Fire Emblem Engage](../../dm/games/0100A6301214E000/README.md) (4) have `ComputeShader` objects only in files without type trees, of hashes no layout covers. So does [Raging Loop](../../dm/games/010020400E1C2000/README.md) (Unity 2018.3: the two built-ins in `unity default resources`, whose 2018 layout lists kernels with no unique variants), which dispatches no compute.
|
||||
|
||||
### GLSLC outputs in a Unity TextAsset
|
||||
|
||||
- **Symptom:** black, jagged holes in the ground of a map (whole tiles of the terrain mesh missing while others draw), with about 25 distinct `[RED] shader … is not in the shader map` keys and `program has no vertex shader`, on a Unity game whose `Shader` objects all split cleanly.
|
||||
- **Cause:** the game draws its maps and effects with programs of its own, not Unity's: `DM_NVN_SHDUMP` saved 24 of them, and none was in any `Shader` subprogram of the 1,606 `Shader` objects in the RomFS (all of them decoded); their code areas were in `Data/resources.assets`, uncompressed, inside the `TextAsset` (class 49) `shader.switch.vfxsb` (11 MB, first word 0x20160817): 1,160 whole GLSLC outputs of a vertex and a pixel stage each. Their control sections carry GLSLC's full header (control +0x20 = 0xa10 and more), unlike the 0x850-byte sections Unity 2018 stores in its subprograms. The split read only `Shader` and `ComputeShader` objects, and the fallback for loose GLSLC outputs skips Unity files.
|
||||
- **Fix:** `unity_text_glslc` in `shader_split.py` reads the `TextAsset`s of a serialized file that holds the GLSLC magic (`unityfs.text_assets`: name and bytes, the two leading fields every Unity version shares, so no type tree is needed) through `glslc_shaders`, which keeps only outputs that pass its checks. A file without the magic is not decoded, so other games' tables do not change. This one TextAsset added 1,207 distinct shaders (the game has 31,635 in all), with 1,156 vertex / pixel pairs; they covered all 57 keys the runs had reported missing.
|
||||
- **Found on:** FINAL FANTASY CRYSTAL CHRONICLES Remastered Edition (0100CE4010AAC000, Unity 2018.4.2f1; provisional profile, no note yet): the intro's overworld shot.
|
||||
|
||||
### Unreal Engine 5 shader code archives (Oodle)
|
||||
|
||||
- **How it is stored:** `ShaderArchive-Global-SF_GLSL_SWITCH_MOBILE-SP_GLSL_SWITCH_MOBILE.ushaderbytecode` (1,219 shaders) and `ShaderArchive-Octopath_Traveler0_Chunk0-…` (9,377), both stored in the pak ([01](../agents/01-extract.md)). Version 2 archives of shader map hashes, shader hashes, shader map entries, shader entries `{u64 offset, u32 size, u32 raw size, u8 frequency}` (17 bytes packed; frequency 0 vertex, 3 pixel, 5 compute), preload entries and shader indices, then the code.
|
||||
@@ -221,7 +261,7 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
- **Record:** a decoded shader is an `LSLG` record: `LSLG`, a stage tag (`SV`, `SP`, `SC`), a header with the source name (`/Engine/Private/…usf:MainPS`), then `u32` code length, the code area, `u32` control length (0x880), the control section and a 24- or 29-byte tail, all at unaligned offsets (`uepak.lslg`).
|
||||
- **Check:** the host keys came out exactly: 37 of 37 shaders a first run reported missing were in the two archives.
|
||||
- **Found on:** [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md): 10,324 unique shaders (1,457 vertex, 6,346 pixel, 2,521 compute); vertex and pixel shaders combine per draw, so every vertex shader is paired with the single-stage cover. Bindings: separate textures and samplers, as Engage's (`0x168` / `0x568`); 61 vertex shaders pick a texture slot at run time (profile key `tex_dyn_slots`, [Texture slots picked at run time](#texture-slots-picked-at-run-time)), 2,084 shaders do atomics on buffer images ([Image atomics](#image-atomics)).
|
||||
- **Note:** 16 compute shaders do atomics on a 3D `r32ui` image, which SPIRV-Cross's MSL 2.3 image-atomic emulation indexes with an `int3` (`array subscript is not an integer`): all are Unreal's paged global distance field (`GlobalDistanceFieldCompositeObjects.usf:CompositeObjectsIntoPagesCS`), which the mobile renderer does not dispatch (none in any run), so they stay unbuilt.
|
||||
- **Note:** 16 compute shaders do atomics on a 3D `r32ui` image, all Unreal's paged global distance field (`GlobalDistanceFieldCompositeObjects.usf:CompositeObjectsIntoPagesCS`), which the mobile renderer does not dispatch (none in any run). They build since image atomics are native ([Image atomics](#image-atomics)).
|
||||
|
||||
### Unreal Engine 4.27 shader code archives (LZ4)
|
||||
|
||||
@@ -309,11 +349,11 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
|
||||
### Pixel shader inputs the vertex shader does not write
|
||||
|
||||
- **Symptom:** `Fragment input(s) user(locnN) mismatching vertex shader output type(s) or not written by vertex shader` and the draw is skipped; the host reports `[RED] failed to create render pipeline (vertex … fragment …)`.
|
||||
- **Cause:** every pixel shader input must be a vertex shader output. Maxwell attributes are untyped slots: a pixel shader that reads a slot the vertex shader never wrote gets a default, but Metal refuses to build the pipeline. Translating one shader at a time cannot know what the partner reads.
|
||||
- **Fix:** `shader_split.py` writes the pairs of each BNSH variation (`shader_pairs.tsv`) and `dmsh_build.py` gives each vertex shader the outputs its partners read but it doesn't write, as `float4` (0, 0, 0, 1) (`vs_pad`; vertex shaders with partners are finished after the pixel shaders, and the partner list is part of the shader's cache rule, [Shader cache key covering the pairs](#shader-cache-key-covering-the-pairs)). The host caches failed pipelines and reports each once with its vertex / fragment keys; before that, every draw retried the failing compile.
|
||||
- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md): 331 of 9,043 pairs failed; the opening course used none of them, later levels did (about 11,500 failed builds in one 11-minute session).
|
||||
- **Check:** check all pairs offline after a build (recipe in [05](../agents/05-debugging.md)).
|
||||
- **Symptom:** `Fragment input(s) user(locnN) mismatching vertex shader output type(s) or not written by vertex shader` and the draw is skipped; the host reports `[RED] failed to create render pipeline (vertex … fragment …)`. Offline packing fails with `pso_offline: metal-tt failed` for the same reason.
|
||||
- **Cause:** every pixel shader input must be a vertex shader output. Maxwell attributes are untyped slots: a pixel shader that reads a slot the vertex shader never wrote gets a default, but Metal refuses to build the pipeline. Translating one shader at a time cannot know what the partner reads. The offline archive exposed a second omission: prebuild reads both `shader_pairs.tsv` and BAHS program variations in `pso_pairs.tsv`, but padding read only the first table.
|
||||
- **Fix:** `dmsh_build.py` reads both pair tables and gives each vertex shader the outputs its partners read but it doesn't write, as `float4` (0, 0, 0, 1) (`vs_pad`). A profile without `pso_pairs.tsv` takes the split's copy beside the package, as the archive builder does. The union controls pixel-before-vertex build order and shader cache dependencies ([Shader cache key covering the pairs](#shader-cache-key-covering-the-pairs)); the profile input stamp includes its `pso_pairs.tsv`, and the NSP script's shader reuse key includes the generated fallback. Padding precedes the depth, shadow-input, pulling and geometry-capture variants. Rebuild shaders and the host for the new chain. The host caches failed pipelines and reports each once with its vertex / fragment keys; before that, every draw retried the failing compile.
|
||||
- **Found on:** [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md): 331 of 9,043 pairs failed; the opening course used none of them, later levels did (about 11,500 failed builds in one 11-minute session). The missing BAHS table was reproduced on [Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md#files-and-shaders) ([#283](https://github.com/seedds/nxnative/issues/283)) and [Super Mario Odyssey](../../dm/games/0100000000010000/README.md#files-and-shaders) ([#276](https://github.com/seedds/nxnative/issues/276)); their notes keep the pair counts and validation.
|
||||
- **Check:** `dm/gfx/tests/test_dmsh_pairs.py` covers both table locations, union/deduplication, defaults and cache dependencies; Metal compiles the generated sources for Mac and iOS and builds an archive with regular and pulling vertex entries in both depth modes. `test_nsp_mac.py` checks fallback invalidation and progress ordering; `test_nsp_incremental.py` regenerates a missing fallback. Check all pairs offline after a game build (recipe in [05](../agents/05-debugging.md)).
|
||||
|
||||
### Single-stage programs paired with a cover
|
||||
|
||||
@@ -392,8 +432,8 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
|
||||
- **Symptom:** `SPIRV-Cross threw an exception: Subgroup builtins are not available in this type of function`.
|
||||
- **Cause:** Metal has no SIMD-group functions in vertex functions, and SPIRV-Cross's own subgroup emulation covers only `Elect`. RE Engine scalarizes uniform loads: ballot the active lanes, shuffle the first lane's value to all and take a fast path when all agree.
|
||||
- **Fix:** with a one-lane subgroup that is exact, so `dmsh_build.py` rewrites the vertex shaders' SPIR-V that way before SPIRV-Cross (`spv_single_lane`: the ballot is the lane's own bit, shuffles and broadcasts return its own value, all / any are the predicate, all-equal and elect are true, lane ID 0, size 1).
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md) (about 4 % of its vertex shaders).
|
||||
- **Fix:** with a one-lane subgroup that is exact, so `dmsh_build.py` rewrites the vertex shaders' SPIR-V that way before SPIRV-Cross (`spv_single_lane`: the ballot is the lane's own bit, shuffles and broadcasts return its own value, all / any are the predicate, all-equal and elect are true, lane ID 0, size 1). A mask component read through `OpAccessChain` / `OpInBoundsAccessChain` becomes an extraction from the constant mask, preserving the index.
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md) (about 4 % of its vertex shaders); [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md) (vertex `4b98320a0f512efe`, a prefix ballot that reads `SubgroupLtMask` through a component pointer).
|
||||
- **Note:** the rewrite covers vertex shaders only: two of Bayonetta 3's domain shaders still fail this way ([Tessellation stages are not drawn](#tessellation-stages-are-not-drawn)).
|
||||
|
||||
### Geometry shaders that only pick the layer
|
||||
@@ -569,6 +609,13 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
- **Fix:** the vendored translator now follows the phis that feed a phi, and treats two `constant buffer | constant` handles as the same value ([third_party/ryujinx/README.md](../../dm/third_party/ryujinx/README.md)). Those accesses become ordinary type-3 handles.
|
||||
- **Found on:** [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md) (218 pixel shaders).
|
||||
|
||||
### Bindless reads whose handle phi also carries other values
|
||||
|
||||
- **Symptom:** none in the log: texture reads come back 0. Particle meshes collapse to a point (their scale reads 0), so effects are missing or show as dark patches. The translator's own message (`Failed to find handle source for bindless access of type "textureBuffer"`) used to be dropped, because `dmsh_xlate`'s accessor had no `Log`; its messages now go into the shader's notes, marked `translator: `, and the build summary counts them apart from unanswered queries.
|
||||
- **Cause:** the handle reaches the read through a phi. Some inputs are c2 handle words, the others are not handles: undefined, or a value the compiler kept in the same register on a path where the predicated read does not run (float arithmetic, a texture result, the absolute value of a uniform word). Some phis merge two or more different c2 handles too (a float and a half-precision buffer). `TryConvertBindless` cannot trace one handle, and `GenerateBindlessAccess` refuses a phi with such an input, so bindless elimination deleted the read and wrote 0 to its results.
|
||||
- **Fix:** only for a phi `GenerateBindlessAccess` refuses (`TryTableHandle` in the vendored `BindlessElimination.cs`, marked `nxnative:`), the read takes the c2 handles among the phi's inputs (a word, or a texture word ORed with a sampler word or a constant sampler). With one handle, it takes the phi's place. With several, one read is made per handle and the result is the read whose handle equals the phi's value at run time: the host fills c2 with the bound handles, or with slot indices for `tex_dyn_slots`. A candidate that other reads of the shader use as a texture of another shape is left out, so those reads are converted in a second sweep once every other read has registered its texture (`ResourceManager.GetTextureTypes`). An image store or atomic with several candidates is left alone, since one per candidate would write each of them. A phi made only of handle words keeps `GenerateBindlessAccess` (the dynamic arrays of [Texture slots picked at run time](#texture-slots-picked-at-run-time)), so no other shader changes: in FINAL FANTASY RESONANCE, the SPIR-V of the 69,449 other shaders is byte for byte what it was. A chain change.
|
||||
- **Found on:** [FINAL FANTASY RESONANCE](../../dm/games/0100606026F12000/README.md): 58,122 reads removed (57,861 of them texture-buffer reads, mostly in Unreal's Niagara particle vertex factories), of which 58,096 in 3,197 shaders are now resolved: 21,380 with one handle, the rest choosing among 2 to 5 at run time. 26 reads in 14 shaders, whose handle comes from no c2 word, are still removed. Reported in GitHub issue #168.
|
||||
|
||||
### Texture slots picked at run time
|
||||
|
||||
- **Symptom:** the shaders' bindless arrays have no slot and used not to load; the translator falls back to a 32-entry array such as `array<texturecube<float>, 32> fp_tcube_cb0_40000000`.
|
||||
@@ -615,65 +662,26 @@ Finding, keying and translating shaders (`dm/gfx/tools/shader_split.py`, `dmsh_b
|
||||
|
||||
### Image atomics
|
||||
|
||||
- **Symptom:** the replay put a texture resource ID where the shader wanted an address; with the old 64 KB dummy buffer, pointers in the game's objects and in the host's program objects got their upper 4 bytes replaced and the game or the replay crashed, or `kIOGPUCommandBufferCallbackErrorPageFault`, after which macOS ignores the process's command buffers (GitHub issue #101).
|
||||
- **Cause:** SPIRV-Cross's MSL 2.3 emulation declares the image twice: the texture (`texture_buffer<uint> cp_i_cb2_4C_r32ui`) and a `device atomic_uint* cp_i_cb2_4C_r32ui_atomic` pointer to the same memory, which the build read as a second image of the same slot.
|
||||
- **Fix:** the pointer is now its own kind (`NVN_RB_IMGA`), bound to the storage of the image in that slot: a buffer texture is a view of its memory pool, so the atomics and the texture reads see the same words. A block-linear image is a deswizzled Metal copy whose pool memory has another layout, so its atomics go to a scratch buffer (logged red) until a game needs them followed; a 3D image does not compile at all ([Unreal Engine 5 shader code archives (Oodle)](#unreal-engine-5-shader-code-archives-oodle)). The scratch buffer is at least as large as the image (`atom_scratch`): SPIRV-Cross indexes the atomics by the image bound in the same slot (`spvImage2DAtomicCoord`: x + y × its width).
|
||||
- **Found on:** [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md): 2,084 such shaders (Unreal's GPU particles); three ran in the first 100 seconds. [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md): in the 64 KB dummy buffer used before, its atomic max into a 480x270 r32ui 2D array (fragment shader `2bd5037003980716`, the scene after the story opening's first cutscene) ran about 1.4 MB past its end, writing into zero-copy pools over the game's heap and into unmapped addresses (GitHub issue #101).
|
||||
- **Note:** Metal shader validation (`MTL_SHADER_VALIDATION=1`) stops out-of-bounds accesses, so under it the scene played.
|
||||
- **Symptom:** the replay put a texture resource ID where the shader wanted an address; with the old 64 KB dummy buffer, pointers in the game's objects and in the host's program objects got their upper 4 bytes replaced and the game or the replay crashed, or `kIOGPUCommandBufferCallbackErrorPageFault`, after which macOS ignores the process's command buffers (GitHub issue #101). Later, `[RED] image atomics on a block-linear texture … not followed, the atomics go to a scratch buffer`, and compute shaders with atomics on a 3D image did not build (`array subscript is not an integer`).
|
||||
- **Cause:** below MSL 3.1, SPIRV-Cross emulates an image atomic with a raw device pointer beside the texture (`texture_buffer<uint> cp_i_cb2_4C_r32ui` and `device atomic_uint* cp_i_cb2_4C_r32ui_atomic`), indexed by the bound image's width (`spvImage2DAtomicCoord`), so the host had to bind memory in that packed layout. A buffer image's pool memory has it. A block-linear image's Metal texture is a deswizzled copy whose pool memory has another layout, so its atomics went to a scratch buffer that nothing read, and a pointer shorter than the image (or an invalid coordinate) wrote over other GPU memory. For a 3D image SPIRV-Cross indexes the pointer with the `int3` coordinate itself.
|
||||
- **Fix:** image atomics are Metal's native texture atomics (MSL 3.1: `texture3d<uint, access::read_write>::atomic_fetch_max`). `dmsh_atomic.py` has SPIRV-Cross write shaders with image atomics (an `OpImageTexelPointer`) at MSL 3.1, and every other shader at MSL 2.3 as before, so their MSL is unchanged and their libraries come from the compile cache; a shader whose MSL still has the pointer form fails the build. The atomics act on the texture every other access of the image uses (a buffer image's view of its pool, a linear image's pool memory, a block-linear image's Metal texture), for buffer, 2D, 2D-array and 3D images. Metal runs texture atomics only on an R32Uint / R32Sint texture made with `MTLTextureUsageShaderAtomic`: NVN images of those formats take that usage when they are made (`nvn_mtl.m` `img_usage`; a view keeps working through its base), the build marks the slots a shader uses atomically (`"atomic"` in the reflection, `nvn_rbind.atom`), and the host binds an atomic-capable write sink, logged red, in place of any other texture there (`dummy_img_atomic`). The translator's coordinate guard stays ([Bounds of image atomics](#bounds-of-image-atomics)).
|
||||
- **Found on:** [Octopath Traveler 0](../../dm/games/01005270232F2000/README.md): 2,084 shaders with atomics on buffer images (Unreal's GPU particles; three ran in the first 100 seconds) and 16 on a 3D image. [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md): an atomic max into a 480x270 r32ui block-linear 2D array (fragment shader `2bd5037003980716`, the scene after the story opening's first cutscene), which ran about 1.4 MB past the 64 KB dummy buffer into zero-copy pools over the game's heap and into unmapped addresses (GitHub issue #101), then went to a scratch buffer. [FINAL FANTASY RESONANCE](../../dm/games/0100606026F12000/README.md) (8 shaders) and [Ghost Trick](../../dm/games/010029B018432000/README.md) (5, one of them a compare-and-swap): atomics on 3D images.
|
||||
- **Check:** `dm/tools/tests/test_image_atomic_bounds.py` with the rebuilt translator, `DMSH_TEST_METAL=1 DMSH_TEST_GPU=1`: synthetic Maxwell atomics on buffer, 2D, 2D-array and 3D images, compare-and-swaps among them, run on native atomic textures of a Mac GPU under the Metal validation layer (`t_atomic_return.m`). `t_refl.m` checks the atomic flag of a reflected image.
|
||||
|
||||
### Bounded MSL 2.3 atomic emulation
|
||||
### Buffer image reflection keeps its texture type
|
||||
|
||||
- **Symptom:** `image atomics: unsupported or overflowing IMG extent` at the first dispatch, despite a valid four-byte counter buffer.
|
||||
- **Cause:** `nvn_refl.m` classified `texture_buffer<uint>` as the default 2D texture. The atomic admission check of the pointer emulation then rejected the actual Metal buffer texture against that 2D descriptor.
|
||||
- **Fix:** the shared reflection type decoder recognizes `texture_buffer` as `MTLTextureTypeTextureBuffer`, so a buffer image keeps its buffer-image shape. `t_refl.m` checks the reflection converter with a buffer image.
|
||||
- **Found on:** [SHIN MEGAMI TENSEI V: Vengeance](../../dm/games/010069C01AB82000/README.md), compute shader `2d2093c1f1d3a7e0`, image slot 2 (one R32Uint texel).
|
||||
|
||||
### Bounds of image atomics
|
||||
|
||||
- **Symptom:** Metal shader validation reports invalid image-atomic stores even with an image-sized scratch buffer; without validation, GPU page faults can freeze rendering.
|
||||
- **Cause:** the translator lost the surface instruction's `IGN` bounds mode before lowering an image atomic to a raw buffer pointer. Scratch capacity cannot protect against invalid coordinates; the upstream 2D coordinate helper also omits array layers.
|
||||
- **Fix:** preserve the supported bounds mode, guard every coordinate before pointer construction, use layer-aware 64-bit addressing, and validate the matching image and buffer's layout and capacity before binding. The supported contract and tests are described below.
|
||||
- **Found on:** [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md#rendering), with the development-build report and remaining gameplay validation recorded there.
|
||||
|
||||
Sizing the backing buffer does not make an out-of-range image coordinate safe.
|
||||
The offline translator now preserves the Maxwell surface instruction's bounds
|
||||
mode and explicitly discarded result. Its Metal-only lowering accepts `IGN`
|
||||
reductions and non-CAS atomics writing `RZ`; returning, predicate-returning,
|
||||
`TRAP`, and unknown modes fail translation rather than acquiring invented
|
||||
out-of-bounds semantics. This path supports non-indexed 32-bit buffer, 2D and
|
||||
2D-array images. Other translator callers do not enable it. Source 1D and
|
||||
1D-array instructions retain the existing one-row normalization to 2D `(x, 0)`
|
||||
and 2D-array `(x, 0, layer)`; buffer coordinates stay scalar, and buffer MSL is
|
||||
not rewritten by the 2D helper. Source 64-bit, floating-point and half2 atomics
|
||||
are rejected before descriptor-format resolution can erase their original size.
|
||||
Descriptor-sized 32-bit atomics also reject non-R32 integer descriptors before
|
||||
the legacy format fallback, both for bound images and constant-buffer-resolved
|
||||
bindless handles. An unresolved bindless atomic fails translation instead of
|
||||
being removed by bindless elimination. These checks apply only to the Metal
|
||||
atomic path; other translator backends retain their existing behavior.
|
||||
Indexed, 3D, multisample and other unsupported effective image shapes fail
|
||||
closed on this opt-in path; this is not a native-image-atomic implementation.
|
||||
|
||||
The SPIR-V queries the bound image's dimensions and tests every original
|
||||
coordinate (including the layer) before forming the texel pointer or executing
|
||||
the atomic. Negative coordinates fail an unsigned comparison. Coordinates are
|
||||
not clamped, flattened for the bounds check, or changed in SPIR-V.
|
||||
`dmsh_atomic.py` recognizes the exact standalone SPIRV-Cross MSL 2.3 coordinate
|
||||
helper and replaces only that definition with typed, layer-aware helpers using
|
||||
64-bit address arithmetic; shader statements and call sites are untouched.
|
||||
An unrecognized helper or default alignment fails the build. This is a packed
|
||||
emulation layout, not proof that an arbitrary native pool's row pitch, layer
|
||||
stride or texture-view offset matches it. The host checks the matched IMG/IMGA
|
||||
[layout and capacity contract](../image-atomic-binding-contract.md) before
|
||||
binding; block-linear scratch still discards results and does not implement
|
||||
image read/write coherence.
|
||||
|
||||
Changing this path invalidates the chain and shader cache. Rebuild the translator
|
||||
and record its actual fingerprints before preparing shaders again; a host-only
|
||||
rebuild is insufficient. The synthetic regression is
|
||||
`dm/tools/tests/test_image_atomic_bounds.py`: portable helper/algebra checks plus
|
||||
an opt-in rebuilt-translator → SPIR-V validation → MSL 2.3 check. Set
|
||||
`DMSH_TEST_XLATE` to the rebuilt translator, with `spirv-val` and `spirv-cross` on
|
||||
`PATH`; `DMSH_TEST_METAL=1` also compiles and links with Apple's Metal compiler.
|
||||
Set `DMSH_TEST_METAL_SDKS=macosx,iphoneos` to check both SDKs.
|
||||
The dimension fixture covers both instruction families for source 1D, buffer,
|
||||
1D array, 2D, 2D array and rejected 3D; accepted forms pass SPIR-V validation,
|
||||
Cross/helper lowering and the optional Metal checks. Shader acceptance does not
|
||||
imply admission of every native host layout. None of these tests executes a
|
||||
shader on a GPU.
|
||||
- **Cause:** the translator lost the surface instruction's `IGN` bounds mode before lowering an image atomic, so a coordinate outside the image reached memory. Neither a pointer nor a native texture atomic outside its image is defined on Metal.
|
||||
- **Fix:** the translator (`TranslationFlags.MetalImageAtomics`, Metal only; other backends keep their behaviour) keeps the bounds mode and whether the result is used, queries the bound image's size and tests every original coordinate, the layer included, before the atomic. Negative coordinates fail an unsigned comparison; coordinates are not clamped or flattened. Invalid coordinates leave memory unchanged and return zero ([ARB_shader_image_load_store, issue 30](https://registry.khronos.org/OpenGL/extensions/ARB/ARB_shader_image_load_store.txt)): a returning `IGN` atomic joins the old texel from the valid branch and zero from the other with an SPIR-V phi. A compare-and-swap (`SUATOM.CAS`: compare with Rb, swap in Rb+1) is Metal's texture compare-exchange behind the same guard; SPIRV-Cross loops `atomic_compare_exchange_weak` until it succeeds or the texel differs. Predicate-returning, `TRAP` and unknown modes fail translation, as do 64-bit, floating-point and half2 sources (checked before the descriptor format can erase their size), descriptors that are not R32 integers, bindless handles that do not resolve statically, and indexed or multisample images. Source 1D and 1D-array surfaces keep the translator's one-row normalization to 2D.
|
||||
- **Found on:** [Pokémon Legends: Z-A](../../dm/games/0100F43008C44000/README.md#rendering) ([PR #178](https://github.com/seedds/nxnative/pull/178)); [SHIN MEGAMI TENSEI V: Vengeance](../../dm/games/010069C01AB82000/README.md), whose two buffer-image counter shaders dispatched at boot consume their returned values.
|
||||
- **Check:** `dm/tools/tests/test_image_atomic_bounds.py`. Portable tests check the MSL version choice and the rejection of the pointer form. With `DMSH_TEST_XLATE` set to a rebuilt translator (and `spirv-val`, `spirv-cross` on `PATH`) the synthetic shaders are translated, the guard's dataflow is checked in the SPIR-V (the size query and comparison dominate the atomic, the phi carries zero on the invalid edge), and the MSL is produced; `DMSH_TEST_METAL=1` compiles and links it (`DMSH_TEST_METAL_SDKS=macosx,iphoneos` for both SDKs) and `DMSH_TEST_GPU=1` runs the returning fixtures (above). Changing this path changes the chain version: rebuild the translator and update the lock before building shaders.
|
||||
|
||||
## Shaders compiled at run time
|
||||
|
||||
@@ -713,6 +721,14 @@ shader on a GPU.
|
||||
- **Found on:** [The Legend of Zelda: Breath of the Wild](../../dm/games/01007EF00011E000/README.md#found-on-this-game), during review of its offline archive path with synthetic libraries and pair lists; no new gameplay or iPhone run.
|
||||
- **Check:** `dm/tools/tests/test_pso_offline.py` changes vertex and fragment library bytes without changing entries or stamps, and exercises the all-constant plan. `t_pso_bundle` rejects bare headers, truncated archives, malformed load commands and out-of-range universal slices, accepts real offline and serialized archives, and rejects zero-probe coverage.
|
||||
|
||||
### Metal's build folders left by pipeline archive flushes
|
||||
|
||||
- **Symptom:** the user cache folder (`getconf DARWIN_USER_CACHE_DIR`, then `com.apple.gpuarchiver/`) fills with `gpuarchiver-*` folders of 0.3–5 MB each, one per pipeline archive flush, and they stay after the game exits: 456 folders, 2.5 GB, on one Mac after two days of NVN game runs. On iOS they stay in the app's own cache folder.
|
||||
- **Cause:** the Metal 4 serializer (`MTL4PipelineDataSetSerializer`) makes a build folder for the captures between two flushes: at the first compile it captures, or, for bases taken again from lookup archives (compaction), during the flush. No flush, release or exit removes the folder. Nothing reads it once its flush has written the archive file: a probe that removed each folder after its flush took every pipeline from the files again. The process keeps no file open in it, and on the Mac the folder is shared by all of the user's programs.
|
||||
- **Fix:** `nvn_mtl.m` (`ga_capture`, `ga_flushed`, `ga_exit`): the first capture after a flush lists the folder before and after its compile and takes the one folder that appeared as its own; the flush removes it once its file is written, together with the one folder the flush made when it held recaptures; exit removes the folder of captures no flush wrote. When more than one folder appeared in that window (another program's capture), none is removed. On iOS the start removes the folders earlier runs left. Folders left before this fix stay; they are safe to delete while no game or Metal tool runs.
|
||||
- **Found on:** reported by MacNativeGames (`1eff989`, `110e6c6`, Metal 3's `MTLBinaryArchive`, 25 GB overnight on Tears of the Kingdom); reproduced here with the Metal 4 serializer by a probe and on this repository's Mac, no single game identified.
|
||||
- **Check:** `t_pso_archive` checks that the first capture finds the folder and the flush removes it; a run of the test leaves no new folder (it left two before the fix, one from compaction and one from captures no flush wrote).
|
||||
|
||||
### Pipeline keys omit unused colour state
|
||||
|
||||
- **Symptom:** identical render pipelines compile repeatedly under different keys, lengthening startup prewarm and adding pipeline misses during play.
|
||||
|
||||
@@ -45,10 +45,27 @@ Guest threads, their synchronization and how the host schedules them. Each entry
|
||||
- **Symptom:** crashes around movie changes in the game's own player threads, about one run in two: `SignalEvent` → `MultiWaitImpl::NotifyAndWakeupThread` on a null MultiWait at address 0x20; `WaitAny` walking freed holders; a worker reading a freed object at `main+0x225338`. The other way round, a handoff that waits too widely: a black screen at boot that never loads a texture (`packaged textures: first uploads 0`), or a load that never ends behind an animated loading screen, with the thread dump's `same-core handoffs N, total T ms` larger than the run itself.
|
||||
- **Cause:** Horizon schedules the highest-priority runnable thread on a core ([Mesosphere's scheduler](https://github.com/Atmosphere-NX/Atmosphere/blob/master/libraries/libmesosphere/source/kern_k_scheduler.cpp)). A lower-priority thread that wakes a higher-priority thread pinned to its core cannot continue until that work blocks. The host's pthreads run in parallel. The earlier approximation also confused host dispatch state with guest readiness: `wake()` cleared the guest wait before the pthread resumed, but `thread_info` could still report that pthread waiting. The caller then continued too soon. Its 5 ms timeout opened the same hole under a longer host delay; the recorded `yield_pending` flag was unused unless the profile enabled checks after every thread SVC. The first unbounded handoff then waited for every runnable higher-priority thread pinned to the caller's core, not only the ones its wake made runnable. The host runs a lower-priority thread beside such threads at all other times, and a core's busy loaders, job workers and pollers are seldom all blocked at the same moment, so a wake could hold its caller for minutes.
|
||||
- **Fix:** every game's thread SVC honours `yield_pending`. Before returning, it waits for the threads its wakes made runnable that are higher priority and pinned to the caller's core, and for those such a thread makes runnable in turn while the handoff lasts (Dread's dispatcher wakes the worker), until each blocks, sleeps, yields to any thread (`SleepThread(-2)`, which takes it off its core so that lower-priority threads run there), exits, is suspended, or enters explicit host GPU work (`svc_thread` / `dm_yield_to_core_peers` in `hle_thread.c`). A wake records the thread in the waker's current handoff and in those the waker belongs to (`note_runnable`, at most four); blocking ends that (`hand_leave`), and a handoff ends when its caller's thread SVC returns (`hand_epoch`). Guest runnable state is authoritative, including the wake-to-pthread-resume interval; a host dispatch snapshot cannot release the handoff, and there is no timeout that lets unfinished work through. A higher-priority peer the wake did not make runnable does not hold the caller, as at every other point on the host. Host-created helpers and threads that can run on another core are excluded.
|
||||
- **Check:** `make -C dm/host/tests GAME=010093801237C000 preempt` delays a woken pthread's dispatch by 1 ms and 20 ms through real event, mutex and condition-variable SVCs. The worker's payload and exit must precede the signaller's return. The original implementation failed both delays; changing readiness alone fixed 1 ms but still failed 20 ms. Equal-priority, other-core, migratable, host-helper, suspended and host-GPU-work peers do not block the caller. Its handoff-scope cases add a busy higher-priority peer that nobody woke: the woken worker must still finish first and the signaller must return (the whole-core wait never returns), a worker woken by the woken dispatcher must finish before the signaller returns (fails without the recorded chain), and a woken thread that yields to any thread releases the signaller (never returns without that rule).
|
||||
- **Check:** `make -C dm/host/tests GAME=010093801237C000 preempt` delays a woken pthread's dispatch by 1 ms and 20 ms through real event, mutex and condition-variable SVCs. The worker's payload and exit must precede the signaller's return. The original implementation failed both delays; changing readiness alone fixed 1 ms but still failed 20 ms. Equal-priority, other-core, migratable, host-helper, suspended and host-GPU-work peers do not block the caller. Its handoff-scope cases add a busy higher-priority peer that nobody woke: the woken worker must still finish first and the signaller must return (the whole-core wait never returns), a worker woken by the woken dispatcher must finish before the signaller returns (fails without the recorded chain), and a woken thread that yields to any thread releases the signaller (never returns without that rule). The native-mutex case wakes a higher-priority pinned peer while holding the mutex it needs; the signal must return at that native block, and the peer finishes after unlock (hangs without the wait annotation).
|
||||
- **Found on:** [Metroid Dread](../../dm/games/010093801237C000/README.md), whose per-build movie request-ring gates and `core_preempt on` setting are now removed; the whole-core wait on [The Legend of Zelda: Tears of the Kingdom](../../dm/games/0100F2C0115B6000/README.md) ([issue #247](https://github.com/seedds/nxnative/issues/247)) and Donkey Kong Country: Tropical Freeze ([issue #259](https://github.com/seedds/nxnative/issues/259), section 6; no profile here).
|
||||
- **Native helper joins:** `dm_host_thread_join` also releases the guest core
|
||||
during a native helper's lifetime, preserving an enclosing host-work scope.
|
||||
[Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md) exposed an
|
||||
audio-unit setup join in `AKEventManager` during a later stall. That sample
|
||||
establishes the missing scheduler annotation, but does not establish an
|
||||
unbounded Apple audio-unit construction deadlock. The preemption regression
|
||||
gates a native helper, verifies that a same-core signaller returns while the
|
||||
peer joins it, then checks completion, the join result and nested host work.
|
||||
- **Scope:** these are cooperative thread-SVC handoffs, not a complete asynchronous guest scheduler. The optional `core_preempt` profile key (`DM_PREEMPT=0` / `1` overrides it) additionally checks already-runnable peers after every thread SVC; it defaults off. Historically that broader policy moved [Super Mario Bros. Wonder](../../dm/games/010015100B514000/README.md)'s first frame from 0.6 s to 2.9 s by waiting for busy decompression threads. Wake-triggered handoffs apply with either setting.
|
||||
|
||||
### A host lock held across a same-core handoff
|
||||
|
||||
- **Symptom:** the game freezes when a movie starts playing beside the game (a guide movie in a corner of the screen): the last frame stays, the process idles at a few percent of a core, and the thread dump shows a lower-priority job thread `running/not waiting` with its backtrace in the movie player (`ReleaseOutputBufferIndex`), the main thread waiting for it, and the movie's input thread `running/not waiting` too. A `sample` of the process shows the job thread in `md_pump` → `md_event` → `dm_yield_to_core_peers`, and the input thread in `h_movie_SendIn` → `md_pump` waiting for a pthread mutex.
|
||||
- **Cause:** the host's movie decoder holds its decode lock (`dmu`) across guest code: the "input available" and "output ready" events are signalled with the SDK's `SignalEvent` from inside `md_pump`. The signal woke the movie's higher-priority threads pinned to the signaller's core, so the signaller's [same-core wake handoff](#same-core-wake-handoffs) waited for them to block before its SVC returned. One of them, the input thread, had called `SendInputBufferForDecode` and was waiting for `dmu` in host code. A thread waiting for a host lock still counted as runnable, so the handoff never ended and the lock was never released. On Horizon that lock is guest-visible (the SDK's decoder holds an `nn::os` mutex there), and a thread waiting for it is blocked.
|
||||
- **Fix:** `dm_host_lock` (`hle_thread.c`) takes a host lock that HLE code holds while it runs guest code. A guest thread that has to wait for it is in a [native HLE wait](#native-hle-waits-release-the-guest-core) (`dm_thread_native_wait`) while it waits: blocked for `thr_runnable`, and out of the handoff that made it runnable, so the handoff does not wait for it. The movie decoder takes `dmu` and the deferred-event lock `g_dfmu` with it (`hle_movie.c`). Releasing `dmu` before signalling instead would let a signal land after `Stop` has returned, on events the game is tearing down (the reason `md_defer_body` signals under its lock).
|
||||
- **Check:** `make -C dm/host/tests GAME=010093801237C000 preempt`, handoff scope "woken thread waiting for the signaller's host lock": the signaller holds a host lock across its signal and the woken higher-priority worker waits for it with `dm_host_lock`. The signaller must return before the worker finishes; without the native wait around the lock the test never returns.
|
||||
- **Found on:** [Kirby and the Forgotten Land](../../dm/games/01004D300C5AE000/README.md): the first guide movie ("A Jump", 480×270 VP9) froze the game on the first walk from the beach, every time. The job worker (priority 46) and the movie threads (priority 44) are all pinned to core 0.
|
||||
- **Found on:** [Xenoblade Chronicles 3](../../dm/games/010074F013262000/README.md), whose 140-second opening VP9 movie stopped at its first output release. A live stack sample identified the input thread in `md_pump` waiting on `dmu`, and the output thread holding it inside `SignalEvent` → `dm_yield_to_core_peers`.
|
||||
|
||||
### Simultaneous crashes of several threads
|
||||
|
||||
- **Symptom:** several threads crash at once and the dumps interleave, then the process hangs instead of exiting.
|
||||
@@ -133,3 +150,11 @@ The yield back-off (`DM_YIELD_BACKOFF`) and the rewriter's spin waits (`patch_ns
|
||||
- **Fix:** the host now implements the blocking pop on its notification queue (a condition variable, then the `TryPop` path), and the event follows the queue as the am service's does: set while a notification is queued, cleared when the last one is popped (`hle_oe.c` `mq_ev_sync`, `hle_clear_system_event`).
|
||||
- **Found on:** [The Legend of Zelda: Link's Awakening](../../dm/games/01006BB00C6F0000/README.md) (SDK 8.2.0, the blocking pop); [Monster Hunter Rise](../../dm/games/0100B04011742000/README.md) (the event).
|
||||
- **Check:** Mac repro `DM_OE_SCRIPT=focus=2@5,focus=1@6` (the script also starts at the game's first `GetNotificationMessageEvent`, since such a game pops only once the event fires): before 995 ms/s, CPU 109%, 6.97 W; after, the thread is idle and CPU energy is 0.05 W.
|
||||
|
||||
### Native HLE waits release the guest core
|
||||
|
||||
- **Symptom:** Paper Mario: The Thousand-Year Door 1.0.1 stays black before its first frame. The main thread stops at `main+0x32796c`, after starting `SYS_Notification`; the priority-28 notifier is in `oe::PopNotificationMessage`, while the priority-44 main thread waits for its same-core wake handoff.
|
||||
- **Cause:** the notification HLE blocks in a host `pthread_cond_wait`, outside the guest kernel's wait bookkeeping. The notifier remains classified as runnable and retains its inherited handoff, so its lower-priority waker cannot continue even though the notifier has no work.
|
||||
- **Fix:** `dm_thread_native_wait` marks the blocked native-wait interval under the scheduler lock and drops the inherited handoff. `PopNotificationMessage` enters that state while holding the queue mutex, waits, and restores runnable state after reacquiring the mutex. Ordinary native execution is still runnable.
|
||||
- **Found on:** [Paper Mario: The Thousand-Year Door](../../dm/games/0100ECD018EBE000/README.md#startup).
|
||||
- **Check:** the original PR #291 reproducer on upstream `1402ded2` blocks until its watchdog exits with code 99. `make -C dm/host/tests GAME=0100ECD018EBE000 notify-scheduler` now uses real `CreateThread`, `StartThread`, wait-for-exit and close SVCs; only the assembly transition to a guest stack is replaced by a C test entry. The original notification HLE strands `StartThread` until the watchdog exits with code 99. The fixed HLE passes 100 runs in each scheduling mode, including a wake without a queued message, runnable-state restoration and delivery of focus notification 15. The full host test suite (`make -C dm/host/tests GAME=0100ECD018EBE000 run`) also passes with CoreAudio access. The game's note records its separate Mac startup comparison and iPhone report.
|
||||