diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..67f94016 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,36 @@ + + +## Problem + + + +## Change + + + +## Where it reproduces + + + +## Proof + + + +## Checks run + + + +- [ ] `python tools/check_content.py --staged` +- [ ] `totk.exe --selftest` + +## Measurements + + + +## Not run + + diff --git a/AGENTS.md b/AGENTS.md index ef92d714..2f0b5611 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,15 @@ This repository is public. Game data lives outside it and every derived file sta - Graphics bugs: [docs/maxwell.md](docs/maxwell.md) lists the Maxwell behaviour the Vulkan path must match and the TotK bugs other ports fixed. - Comments say what the code does and why, in words that stand on their own. Cite our code by file plus function (a line number may follow as a hint) and upstream code by project, file and function: `refs/` moves, so its line numbers rot. Commit hashes belong to the attribution of copied code. +## Running and debugging + +- Running the game, screenshots, frame dumps, timings: [docs/agents/running.md](docs/agents/running.md). +- Investigating a bug, glitch or crash: search [docs/agents/symptoms.md](docs/agents/symptoms.md) by the player's words first, then follow [docs/agents/debugging.md](docs/agents/debugging.md). Read the code before running the game. + +## Pull requests + +Preparing a pull request: target `dev`; the checks by changed area, the proof and the performance runs it carries are in [CONTRIBUTING.md](CONTRIBUTING.md), "Pull requests". Reviewing one: [docs/pr-review-kit/](docs/pr-review-kit/). + ## Writing Code, comments, docs and commit messages are in English. Commit messages are plain sentences without type prefixes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b404f3e..49784216 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,14 +29,93 @@ Use the issue form. Include the port version and game update from the title line ## Pull requests -- One topic per pull request. -- Describe the problem, then what changed. -- Say how you tested it: which version, which scene, how long. -- A change to the lifter (`recomp/`) or the CPU headers passes the differential tests in [docs/building.md](docs/building.md), "Checks". -- Commit messages are plain sentences, without `feat:` or other prefixes. -- AI-assisted code is welcome. You are responsible for having read and run it. +### Branch + +Open pull requests against `dev`. Branch from the current `dev` and rebase on it before you open the pull request. `main` receives `dev` at a release; a pull request against `main` is retargeted to `dev` before review. + +### Before you start + +- Search the open pull requests and issues on the topic. When another pull request already covers it, improve that one or say in yours how they differ. +- Fixes are reviewed before features, in order of how many players the bug hits. A feature with a design in [docs/milestones/](docs/milestones/) (mods, cheats, the menu) follows that design. For a feature without one, open an issue first. +- Find out where the bug reproduces: OS, GPU vendor and generation, driver, game update, settings. Put that in the description. + +### What the change must keep + +- **The default for everyone.** A fix for one setup (a GPU vendor, a memory size, a display) is gated on that setup. Players without the problem see no change. +- **A switch.** New behaviour on the frame path or in rendering sits behind a `totk.ini` setting or a `TOTK_*` variable, so a player can turn it off and a reviewer can compare. A new setting is described in [totk.ini.example](totk.ini.example) and [docs/settings.md](docs/settings.md). +- **The fix, not a disabled feature.** A crash that goes away with a feature off points at the bug in that feature. Turning the feature off for a vendor is not a fix. +- **The frame time on weak hardware.** Game threads and the render thread do no file I/O, no GPU allocations, no per-draw state copies and no global atomic counters. Memory kept per frame or per object is freed. The full list with measured costs: [docs/pr-review-kit/02-performance.md](docs/pr-review-kit/02-performance.md), "What the hot path must not do". +- **Other GPUs.** New Vulkan code queries each feature, extension, format and limit it uses and keeps a working path without it. Nothing is hardcoded from what one GPU reports. +- **Text in the catalogs.** Text a player sees gets an id in `runtime/ui_text.h` and an entry in `runtime/ui_text.c` for every menu language (English, Russian, Brazilian Portuguese, Simplified Chinese); installer text goes into `installer/text/*.json`. +- **The generator owns generated C.** Change `recomp/`, never `generated/`. + +### Checks by what you changed + +Run every row that matches your diff and list the results in the description. Commands and details: [docs/building.md](docs/building.md), "Checks". + +| You changed | Run | +|---|---| +| anything | `python tools/check_content.py --staged`; `build\\totk.exe --selftest` | +| `recomp/`, `runtime/cpu.h`, `runtime/simd.h` | the differential tests against Unicorn: `tests\diff\lift.py`, `simd.py`, `atomic.py`, `func.py`, `tests\flags_check.py`, `tests\excl_mt.py`; the same with a `--cpu v2` build | +| `recomp/jumptables.py` | `tests\jumptables_check.py`: 0 disagreements on 1.2.1 and 1.4.3 | +| `runtime/` | `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex`, `--probe-shader`, plus the probe for the file you touched (list at the top of `runtime/main.c`) | +| settings | `totk.exe --probe-cfg`; `python tests\settings_doc_check.py` | +| button prompts | `.venv-py314\Scripts\python tests\romfs_prompts_test.py` | +| mods, ExeFS patches | `.venv\Scripts\python tests\exefs_patches_test.py`; `totk.exe --probe-mods` | +| `installer/` | `python tests\installer_test.py`; `python tests\installer_gui_test.py` | +| shader translator | `tools\xlate_smoke.py`; `tools\shader_census.py --version --translate` with no new failure groups | +| rendering | a run in a scene where the change acts, with `TOTK_VK_VALIDATION=1`: no new validation messages; screenshots before and after | +| frame path, timing, scheduling | the measurements below | + +Checks that need game files run on your own copy of the game. Results go into the pull request as text: pass or fail, counts, log lines. Never attach logs or dumps that contain game data. + +### Proof + +- **A bug fix** shows the bug before the change and gone after it, on the same repro: a screenshot pair, a log line, a probe or test that fails before and passes after. A scripted repro (`TOTK_INPUT_RECORD`, [docs/controls.md](docs/controls.md), "Scripted input") is the strongest. If the bug cannot be reproduced on your hardware, say so and point at the code that causes it. +- **A crash fix** names the crash: the log's crash report, `vk: pass marks` lines for a lost device. +- **Parts you could not run** (Linux-only code, HDR, vendor-only paths) are listed as unrun and gated away from everyone else. + +### Performance measurements + +A change on the frame path, in timing or in scheduling carries these numbers, or it waits until someone measures it. + +1. **Ceiling first.** Before the work, estimate the share of the frame the changed code holds on the critical path (`TOTK_CRIT` + `tools/crit_path.py`). A ceiling of a few percent is not worth a hot-path change. +2. **Build 1.4.3** with prepared shaders in that build folder (`tools/prepare_shaders.py`, [docs/building.md](docs/building.md) step 6). Caches copied from another build folder do not count. +3. **A heavy scene** where the change acts: a fight with a large enemy and a crowd, or a town, with `TOTK_FPS_LIMIT=360`. +4. **Whole runs, switch off and on, at least two each**, the same save and the same input script, timed by game seconds: + + ```powershell + .venv\Scripts\python tools\run_game.py build\v143 --script --seconds 60 --quiet --env TOTK_TRACE=perf,sched --env TOTK_FPS_LIMIT=360 --env =0 + ``` + + `--quiet` waits until no other game or build runs. Close everything heavy on the machine. Before reading the numbers, check that each run reached the world and showed the same scene. +5. **Report per run**: average fps, 1% low or worst frame, the number of `perf: slow frame` lines, VRAM; `tools/perf_summary.py` condenses the log. Add CPU, GPU, driver, RAM, resolution and the settings you changed. + +A switch toggled inside one run on a moving scene gives false results: compare whole runs. Zero new slow frames is a hard bar; an fps gain does not buy back new hitches. Background and the reasons behind each rule: [docs/pr-review-kit/02-performance.md](docs/pr-review-kit/02-performance.md). + +### Description and commits + +Fill in the pull request template: problem, change, where it reproduces, checks, measurements, what stayed unrun. One topic per pull request. + +Commit messages are plain English sentences, without `feat:`, `fix:` or other prefixes. A commit that works on an issue ends its subject with the issue numbers: `Render the handheld scene at the asked size on 1.4.3 (#998, #536)`. + +### Review and merge + +The maintainers review every pull request against [docs/pr-review-kit/](docs/pr-review-kit/): read it to see what gets a pull request rejected, which bug classes are already fixed and which invariants a change must keep. A good pull request that misses the project's conventions is fixed up while merging rather than sent back. Part of a pull request can be taken; the reason for what was left out or rejected is posted on the pull request. + +Accepted work merges into `dev` and reaches players through test builds. `main` moves only at a release. + +## Coding agents + +AI-assisted code is welcome. You are responsible for having read and run it, and the pull request says which checks ran on your machine. + +Point your agent at [AGENTS.md](AGENTS.md) (Claude Code reaches it through `CLAUDE.md`), this file and [docs/pr-review-kit/](docs/pr-review-kit/) before it writes code. [docs/agents/](docs/agents/) holds what it needs to run the game unattended, measure, and debug: the method, the bug classes of a recompiled Switch game, and a symptom catalog. The rules in them bind the agent as they bind you: + +- Key values stay out of prompts, logs, commands and files. The agent passes the keys file path to the tools. +- Results in the pull request come from runs that happened. A check the agent did not run is listed as not run. +- The agent writes the description in its own words about this diff. A review kit checklist pasted in with every box ticked tells the reviewer nothing. ## Code and docs - Code, comments, commit messages and documentation are in English. -- Generated code is never edited by hand: change the generator. +- Comments say what the code does and why. Cite our code by file and function, upstream code by project, file and function. diff --git a/docs/agents/debugging.md b/docs/agents/debugging.md new file mode 100644 index 00000000..a1327a83 --- /dev/null +++ b/docs/agents/debugging.md @@ -0,0 +1,117 @@ +# Debugging the port: method for agents + +A game run costs minutes and a full rebuild costs several more, while reading code costs seconds. Order the work so the +cheap steps decide as much as they can before the game starts. Each step ends on its stated condition. + +## Steps + +1. **Evidence.** Hold one artifact that shows the symptom: the player's screenshot, log or input recording, a + flight-recorder dump, a frame dump ([running.md](running.md), "Observing"). Done when you can point at the wrong + pixel, line or frame. +2. **Where it reproduces.** OS, GPU vendor and generation, driver, game update, settings, mods, the save. Done when you + know whether your hardware can show the bug at all. NVIDIA bugs do not reproduce on AMD; RDNA1/2 bugs do not + reproduce on RDNA3/4 (another driver branch); a Ryzen 7000 iGPU is RDNA2 and runs that branch. When your hardware + cannot show it, say so and work from the code and the reporter's logs; runs that cannot decide anything are skipped. +3. **Known already?** Search [symptoms.md](symptoms.md), [pr-review-kit/04-knowledge.md](../pr-review-kit/04-knowledge.md) + and [maxwell.md](../maxwell.md) sections 6-7 with the words the player used (black, magenta, flicker, slow motion, + missing, stale, streaks, stuck) and the object (sky, grass, shadow, fog, HUD, weapon). Then the upstream notes: + `refs/nxnative/docs`, `refs/MacNativeGames/docs` and the SavageWindRecomp issue tracker (`tools/fetch_refs.sh` + fetches `refs/`). Done when every hit is either ruled out or is your first hypothesis. A hit on a class we already + fixed: confirm that fix still holds before deriving anything new. +4. **Read the code.** The game's function in Ghidra ([building.md](../building.md), "Reverse engineering") or in + `generated//`, and our runtime path in `runtime/`. Done when the code either rules the hypothesis out or names + the exact condition a run must show. A run confirms what the code could not rule out. +5. **The cheapest falsifying check.** An existing switch (a `totk.ini` setting or a `TOTK_*` variable), a dump, a trace + line, a probe. When it refutes the hypothesis, stop: write down what was checked and why it fell, and start again + at step 3 with the next hypothesis. No build, no benchmark for a refuted idea. +6. **Repro red, then green.** The same repro shows the bug before the fix and its absence after. A fix without a + reproduced failure is a guess. +7. **Fix the cause.** A crash that goes away with a feature off points at the bug in that feature; fix the feature. + A fix that one setup needs is gated on that setup, and everyone else keeps today's behaviour. + +## Traps + +- **One-off failures.** A failure that happens once and not again: rerun before you debug it as a code bug. A crash or + a forced shutdown leaves zero-filled cache files, and memory errors on the host flip bits in files and objects. +- **Every build fails the same way at once.** Suspect the inputs and caches (game files, the OS file cache, shader and + pipeline caches), not the code. Bisecting code is pointless when the old builds fail too. +- **Compile errors in generated C** on a line the generator could not have written are a flipped bit. Regenerate and + compare hashes; `recomp/` is fine. +- **A crash that looks like a race.** Try the save first: some saves crash the same way on a real Switch. +- **Theories about locks and threads.** Read the game's own protocol in Ghidra before running anything. Ten runs on + lock theories once went where one read of the game's double-buffer code would have narrowed it. +- **Numbers from a fast machine.** A change neutral on a fast desktop can matter on a weak laptop or the Steam Deck, + in either direction. Say which hardware your numbers come from. + +## Bug classes of a recompiled Switch game + +The port runs the game's own code on another OS and another GPU. Most bugs come from places where the host differs +from the console. + +### Variable frame step and epsilon-gated timers + +The console steps the game by whole 30 Hz units, so a countdown started at a whole value reaches exactly 0. With the +real-time step (`realtime = 1`, the default) the step is a measured ratio, and the last step can leave a remainder of +about 1e-8. Many of the game's updaters step a timer only while it is above `FLT_EPSILON`; a remainder in +(0, `FLT_EPSILON`] is never stepped again. A consumer that tests the same field with `> 0` then sees it active +forever, until a load reinitialises the object. Issue #18 (weapons locked after a Fuse) was this. + +Symptom words: stuck until a reload, a teleport or a shrine; locked after an action; works sometimes; only above +30 fps. To check a report: find the field the stuck behaviour reads, then compare the updater's threshold with the +consumer's test. A change to the frame step, the pacer or the fps limit is checked for this class. + +### Thread scheduling: Horizon is not a desktop OS + +- Horizon runs a woken better-priority thread at once on its core, before the thread that woke it goes on. A host + runs both in parallel, so state can be read one step early or freed while another thread still uses it. +- Threads created with ideal core -2 start pinned to one core on Horizon. The game keys per-core data by + `GetCurrentProcessorNumber` without locks and expects that pinning. +- The game has hundreds of spin loops without a system call. On the console a spinning thread holds one core of three; + on the host it burns a host core and can keep an equal-priority waiter out. +- ARMv8 `stlr` followed by `ldar` is ordered (RCsc). C11 release/acquire lets x86 reorder that store and load. A + two-thread flag handshake that loses a wake points here; lifter changes to barriers and atomics pass + `tests\diff\atomic.py` and `tests\excl_mt.py`. +- A scheduling change is judged by how long a ready, better thread waits for a core it may use, plus the slow-frame + count. A check that made one bug rarer once added 50-75 ms hitches. + +### Maxwell behaviour versus Vulkan defaults + +The port must do what the Switch GPU does: last-vertex provoking, (0,0,0,1) for unwritten vertex outputs, colour +targets outside the shader's output map keep their texels, the G-buffer pass samples its own targets. The full list +with sources: [maxwell.md](../maxwell.md), section 7. + +### Memory shared by the CPU and the GPU + +- The game fills textures and buffers with the CPU and samples them on the GPU without the flush a desktop API needs. + Missing uploads show as magenta, brown or stale textures. +- The game reads GPU-rendered textures with the CPU (grass maps, cut and burn masks). Missing write-backs show as + effects that never happen: cut grass stays up, fire does not spread. +- Memory only the GPU writes looks idle to write-watch tracking: a store from a shader leaves no fault and no dirty bit. +- Anything kept per texture, per frame or per object without a free grows host memory until a commit failure after + hours. Watch private memory over a few idle minutes. + +### Device lost and GPU hangs + +- The `vk: pass marks` lines name the pass in flight. Validation names the misuse when there is one. +- Submits and presents share one queue under one lock. A present from its own thread on a second queue gave device + lost on NVIDIA before Turing in another port. +- Caches from another driver or GPU (pipeline caches, translated shaders) gave black windows after driver updates; + every cache is validated on load and dropped when vendor, device or `pipelineCacheUUID` change. +- A first-time shader stall over 2 s on a slow GPU trips the Windows GPU timeout (display event 4101). Large one-shot + submissions (a boot-time decode of every texture) do the same. +- Point lists from vertex shaders that write no point size are undefined without `VK_KHR_maintenance5`. + +### Windows and Wine host costs + +What the hot path must not do, with measured costs: [pr-review-kit/02-performance.md](../pr-review-kit/02-performance.md), +"What the hot path must not do". For debugging, also: + +- `SuspendThread` returns at once, but `GetThreadContext` waits until the thread leaves the kernel. A sampler or a + debugger stalls for as long as the stopped thread's file open or driver call runs. +- A thread stopped between its `sub rsp` and its stack probe has its stack pointer inside the guard page. Check + `PAGE_GUARD` with `VirtualQuery` before reading another thread's stack. +- Paths go through the W APIs with UTF-8 to UTF-16 conversion. An A API call breaks on a path with non-Latin letters, + such as a Windows user name in Cyrillic or Chinese. +- Wine emulates `GetWriteWatch` with `mprotect` and one fault per first write. Anything relying on cheap OS memory + tricks (write-watch, protection changes, fault handlers) costs more under Proton. +- `win32u.dll` frames in a host stack are kernel graphics calls: memory allocation or submission. diff --git a/docs/agents/running.md b/docs/agents/running.md new file mode 100644 index 00000000..926a09b4 --- /dev/null +++ b/docs/agents/running.md @@ -0,0 +1,90 @@ +# Running and observing the game as an agent + +How an agent runs a build into gameplay without a person at the controls, captures what the game shows, and reads the +results without flooding its context. Setting names and file formats live in [settings.md](../settings.md) and +[controls.md](../controls.md); this page says which to use when. How to judge a performance change: +[pr-review-kit/02-performance.md](../pr-review-kit/02-performance.md). + +## Before the first run + +- **A build dir per change.** Build into its own folder (`tools/build_game.py --out build/-`) so a + comparison has two exes side by side. After every build check the exe's modification time: a failed link leaves the + old `totk.exe` in place and you test old code. +- **Prepared shaders in that folder.** `tools/prepare_shaders.py build/ --version ` ([building.md](../building.md) + step 6). Without it the game compiles shaders during play, and every timing and every "a draw is missing" shot of the + first minutes says nothing about your change. Shader and pipeline caches are not copied between build folders. +- **The same `totk.ini` on both sides** of any comparison. A folder without one runs at the defaults; check the log's + `resolution:` line of each run before comparing them. +- **Saves.** `tools/run_game.py` copies the profile `build/profile`, swaps in a save from `build/save-backup//` + and deletes the copy afterwards, so the real profile is never written. To make a backup, copy the user folder from + `build/profile/save/` into `build/save-backup//`. Pick a save that starts where the change acts. + +## Running + +```powershell +.venv\Scripts\python tools\run_game.py build\ --save --seconds 30 [--script ] [--shot out.png] [--env KEY=VALUE] [--quiet] [--tag ] +``` + +- `--seconds` is game time since the world loaded (the log line `frame step: world clock starts` marks the start), at + any frame rate. Measure and script by game time; a frame count at another fps is another scene. +- `--script` replays input ([controls.md](../controls.md), "Scripted input"). A new script starts with the presses of + `tools/input/load_save.txt`, which load the first save from the title. `TOTK_INPUT_RECORD` turns a person's play into + a script; ask the user to record the repro rather than guessing the button sequence. +- `--shot` saves the last frame and builds pipelines synchronously: use it for pictures, never for timing. +- `--quiet` waits until no other `totk.exe` or `ninja.exe` runs. Every timing you report comes from a `--quiet` run. +- The log goes to `\logs\.log`; the tool prints `: rc=`. + +Run one game at a time: two instances share the GPU's memory and the CPU, and both runs' numbers are void. + +Running `totk.exe` yourself: set `TOTK_ASSUME_FOCUS=1` (an unfocused window otherwise pauses the game), +`TOTK_INPUT=none` (the user's keyboard and pad stay out) and `TOTK_BACKGROUND_WINDOW=1`. Stop only the process you +started, by PID: `taskkill /IM totk.exe` also closes the user's game. + +**Check that the run reached the world** before trusting it: the world clock line is in the log and the shot shows the +scene you expected. Title menus on a slow boot eat presses, and runs of one script drift apart after 10-15 game seconds +(enemy AI, physics, weather). Two runs are comparable only when shots at the same game second show the same scene. + +## Observing + +| You need | Use | +|---|---| +| pictures over a time span | `TOTK_SHOTS=w-:` (world seconds) and `TOTK_SHOTS_DIR`; 480x270 by default | +| the frames around a hitch or a one-frame glitch | `TOTK_FLIGHT=1`: keeps the last 600 frames with timings and thumbnails, dumps on F8, on a frame over `TOTK_FLIGHT_SPIKE_MS` or at `TOTK_FLIGHT_AT=`; `python tools/flight_view.py ` lists the worst frames and writes a contact sheet | +| every render pass of one frame | `TOTK_DUMP=` and `TOTK_DUMP_DIR`: a PNG per pass target plus `passes.txt` (draws, bound textures with swizzles, storage buffers, copies, clears) and `images.txt`; `TOTK_DUMP_BUF` adds a guest buffer | +| Vulkan misuse | `TOTK_VK_VALIDATION=1` (needs the Vulkan SDK), run into the world; compare the message kinds with a run without your change. The layer caps repeats, so counts are not totals | +| which pass the GPU was in at a device lost | the `vk: pass marks` lines; on by default on AMD, `TOTK_PASS_CRUMBS=1` elsewhere | +| a crash | the crash report at the end of the log: exception, guest and host addresses, registers | +| a run on a large card that acts like a small one | `TOTK_VRAM_LIMIT_MIB=` | + +Frame dumps: a draw's argument lines come before its pass header. Pass numbers shift between runs, so find a pass by +its draw count or its targets, not by its number. RenderDoc cannot capture this renderer ([controls.md](../controls.md), +"RenderDoc capture"). + +## Measuring + +| What | How | +|---|---| +| fps, worst frame, render-thread waits, GPU busy time, VRAM, `bound: gpu\|cpu\|pace` | `--env TOTK_TRACE=perf,sched`, then `python tools\perf_summary.py ` | +| slow frames | count the `perf: slow frame` lines; each names what the render thread was blocked on (pipeline, translation, texture, fence, pacing) | +| the frame's critical path, by thread and state | `TOTK_CRIT=w-` and `TOTK_CRIT_OUT=` with `sched` tracing, then `python tools\crit_path.py ` | +| where CPU time goes | `TOTK_PROFILE=` and `TOTK_PROFILE_FRAMES=w-`, then `python tools\prof_report.py `; `TOTK_PROFILE_RENDER=1` samples the render thread with call chains | +| a switch on a static scene | a setting set to `ab` with `TOTK_AB=` alternates the two sides in blocks of game time; `python tools\ab_summary.py --skip `. On a moving scene compare whole runs instead | + +A single run proves little: runs of one script on one build spread by a third in their 1% lows. Compare medians of at +least two whole runs per side, on a quiet machine. + +## Probes + +Each probe is a mode of `totk.exe`, run from the build folder; the list with what each covers is at the top of +`runtime/main.c`. `--selftest` needs no game files. `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex` and +`--probe-shader` cover most runtime changes and run in seconds: run them before any game run. + +## Reading output without filling the context + +- Game logs run to megabytes. `grep` the lines you need, `tail` the end, pipe `perf_summary.py` through `grep SUMMARY`. +- Generated C runs to hundreds of thousands of lines per file. `grep -n` the function, then print its line range. +- Look at screenshots downscaled: paste several into one 1280x720 grid and read the grid once. Write down what it shows + instead of opening it again. +- Send long builds and runs to the background with their output in a file; read the last lines when they end. +- Keep a notes file in the build folder from the first finding: what is ruled out, the leading hypothesis, the next + step, temporary debug edits with file and line. A fresh session continues from it. diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md new file mode 100644 index 00000000..55a2aca0 --- /dev/null +++ b/docs/agents/symptoms.md @@ -0,0 +1,71 @@ +# Symptoms and their usual causes + +What a player reports, the causes this port and other Switch ports (nxnative, MacNativeGames, SavageWindRecomp, +LAS Recomp) found behind it, and the first check. Search by the player's words and by the object. Which of these are +already fixed here, and where: [pr-review-kit/04-knowledge.md](../pr-review-kit/04-knowledge.md), "Fixed bug classes". +GPU semantics behind the graphics rows: [maxwell.md](../maxwell.md). The method around this table: +[debugging.md](debugging.md). + +## Graphics + +| Symptom | Usual causes | First check | +|---|---|---| +| magenta, brown or stale textures; caves or sky wrong | the CPU filled the texture and the GPU never got the upload (no flush on the console) | frame dump: the texture's upload count and mirror state in `passes.txt` | +| effects that never happen: cut grass stays up, fire does not spread | the CPU reads a GPU-rendered texture that was never written back | which texture the game reads after its fence; write-back of that target | +| black scene, black dialog background or loading map | deferred binds read at record time; a 3D target rejected; a texture compare ignoring the channel mapping | frame dump: the first pass whose target goes black | +| lime silhouettes, rectangles in the sky | a target outside the shader's output map overwritten instead of kept | dump: the target's previous contents versus after the pass | +| dark or magenta 16x16 blocks, missing characters | a pass samples its own render targets (G-buffer feedback) | feedback-loop layout and barriers on that pass | +| tile rectangles, wrong data in tiles | storage buffers bound in the wrong slot order | `passes.txt`: storage buffers with their c0 slots | +| streaks on cliffs, characters stretched into streaks | primitive restart ignored; integer vertex inputs against a float format | validation run; the draw's topology and vertex formats in the dump | +| garbage geometry on title or load screens | 8-bit indices widened on the CPU before a copy in the same submission wrote them | whether a copy to the index buffer precedes the draw in one submission | +| black tiles, masks not cleared | clears with a partial channel or stencil mask skipped or done in full | `passes.txt` clears and their masks | +| garbage in feedback effects at first use | new GPU-only images start undefined; the console starts them at zero | clear on creation and compare | +| empty clouds or fog for the whole session | a one-shot draw skipped while its pipeline built, never drawn again | `perf: slow frame` lines naming pipeline waits; the draw's first frame | +| fog that never parts, flicker in tile rows | driver constants (c0: dispatch size, point size, base instance) not filled | the c0 contents the translator expects for that stage | +| no sun shadows, wrong lighting | unwritten vertex outputs read as zero instead of (0,0,0,1) | the shader's output map | +| one flat ambient colour | a 1-layer 2D array sampled as 2D | the view type the shader expects | +| upside-down text | lower-left window origin | origin mode in the log | +| flicker or a texture swapped with a similar one | the newest texture picked at shared storage instead of the one with the matching view | texture lookup by id in the dump | + +## Timing and frame pacing + +| Symptom | Usual causes | First check | +|---|---|---| +| stuck until a reload or teleport, locked after an action | a timer remainder below `FLT_EPSILON` with the real-time step ([debugging.md](debugging.md)) | `realtime = 0` in `totk.ini`; if the bug is gone, find the timer field | +| slow motion in GPU-heavy scenes, dynamic resolution never drops | the engine's GPU time comes from report timestamps; wrong timestamps make the frame look free | what the engine reads as GPU time and what the report writes | +| frame counter wanders 50-60 | two 60 Hz clocks (vsync event and present acquire) drifting apart | whether both follow one display tick | +| a 30 Hz thread runs at 27 Hz, slow cutscenes | host sleep overshoot: Windows waitable timers wake 0.3-0.5 ms late | `TOTK_TRACE=sched` sleep asked versus took | +| 20 fps after a scheduler change | a yield back-off that starts too early | yield handling against the console's | +| climbing or movement wrong only above some fps | game code that assumes a fixed step count per frame | compare at 30, 60 and the failing rate | + +## Threads and crashes + +| Symptom | Usual causes | First check | +|---|---|---| +| state read one step early, one frame of inconsistent data | a woken better-priority thread does not run before the waker goes on | sched trace around the handoff | +| per-core data corrupted, rare crash in a container walk | threads the console pins to one core run on any host core | the thread's core mask from its creator; a run on one core | +| rare hang or lost wake | address-arbiter signal values or wake order differ from Horizon | the arbiter path against mesosphere | +| a flag handshake loses a wake | `stlr`/`ldar` ordering lost in x86 release/acquire | the guest code's barrier pattern | +| a host core at 100% while the game waits | game spin loops without system calls | profile: which guest function spins | +| crash on world load | sizes the game reads from the driver (command memory used) differ from the console's | the sizes the game asks for against the console's | +| crash with one save only | the save itself; some crash on a real Switch | the same save on another build and, if possible, on hardware | + +## Device lost, hangs, black window + +| Symptom | Usual causes | First check | +|---|---|---| +| device lost a few frames in, GPU writes to address 0 (older NVIDIA) | present from another thread or queue | every submit and present on one queue under one lock | +| device lost or garbage on one AMD generation | views the driver branch mishandles (BC block views of 3D textures) | `vk: pass marks`, validation; the formats in the pass | +| device lost on one scene, every run, one vendor | a shader or pass that driver cannot run | pass marks name the pass; skip shaders by hash to bisect | +| GPU timeout after 1-2 min, single frames over 2 s | first-time shader stalls or a huge one-shot submission on a slow GPU | prepared shaders; submission sizes at boot | +| GPU hang at boot with pipeline libraries | point lists without a point size | the shader writes point size or the device clamps it | +| black window after a driver update | caches from another driver | the log line `vk_pipeline: ... written by another device or driver`; delete the caches next to `totk.exe` | + +## Memory + +| Symptom | Usual causes | First check | +|---|---|---| +| host memory grows in play until a commit failure after hours | state kept per texture init or per frame and never freed | private memory over a few idle minutes | +| multi-GB peaks while travelling | upload staging grows inside one submission | staging bytes per submission | +| out of memory after minutes | dead textures re-entering tables | live images and allocator bytes over time | +| stutter as VRAM fills | new memory blocks allocated on the render thread (8-23 ms each) | profile the render thread over the hitch: `vkAllocateMemory` under the allocator's lock |