mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 09:51:02 +00:00
Compare commits
No files matched your search
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Target branch: dev. Rules: CONTRIBUTING.md, "Pull requests". Write each section in your own words about this diff;
|
||||
delete the rows that do not apply. Never attach game files, keys, extracted data or logs that contain them.
|
||||
-->
|
||||
|
||||
## Problem
|
||||
|
||||
<!-- What the player sees or what is wrong in the code. Issue link if there is one. -->
|
||||
|
||||
## Change
|
||||
|
||||
<!-- What the diff does and why this way. Name the switch (totk.ini setting or TOTK_* variable) that turns it off. -->
|
||||
|
||||
## Where it reproduces
|
||||
|
||||
<!-- OS, CPU, GPU and driver, game update (1.2.1 / 1.4.3), settings. Hardware it cannot appear on, if known. -->
|
||||
|
||||
## Proof
|
||||
|
||||
<!-- Before and after on the same repro: screenshots, log lines, a test that failed and now passes. -->
|
||||
|
||||
## Checks run
|
||||
|
||||
<!-- The rows of CONTRIBUTING.md "Checks by what you changed" that match the diff, each with its result. -->
|
||||
|
||||
- [ ] `python tools/check_content.py --staged`
|
||||
- [ ] `totk.exe --selftest`
|
||||
|
||||
## Measurements
|
||||
|
||||
<!-- Frame path, timing or scheduling only. Per run, switch off and on, at least two each: average fps, 1% low or
|
||||
worst frame, `perf: slow frame` count. Scene, save, seconds, CPU, GPU, driver, resolution. -->
|
||||
|
||||
## Not run
|
||||
|
||||
<!-- Paths you could not run (Linux, HDR, a vendor you do not have) and how they are gated. -->
|
||||
@@ -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.
|
||||
+86
-7
@@ -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\<ver>\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 <ver> --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 <input.txt> --seconds 60 --quiet --env TOTK_TRACE=perf,sched --env TOTK_FPS_LIMIT=360 --env <SWITCH>=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.
|
||||
@@ -26,6 +26,7 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
- 16:10 (Steam Deck, handhelds), 3:2 (Surface, Framework), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
|
||||
- Anisotropic texture filtering up to 16x, for sharp ground into the distance
|
||||
- In-game settings menu (F1) and performance overlay (F10)
|
||||
- Play time across runs on the Game tab of the settings menu, kept in `playtime.txt` next to the saves
|
||||
- Discord Rich Presence ("Playing TotK Recomp" with the play time), off by default, own application id supported
|
||||
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
|
||||
- Keyboard, mouse and gamepad with remapping, rumble and gyro
|
||||
@@ -34,7 +35,8 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
- Shaders prepared at install instead of during play
|
||||
- Mods made for emulators: unpack a mod into the `mods` folder next to `totk.exe`; switch and order them in the menu. RomFS
|
||||
mods, TKMM's merged output and ExeFS code patches (`.ips`, `.pchtxt`) work; the game rebuilds itself for code patches
|
||||
([docs/mods.md](docs/mods.md))
|
||||
([docs/mods.md](docs/mods.md)). HD texture packs work: the port grows the texture heap a pack asks for by itself. Tested
|
||||
with the HDPacks.com 4K Texture Pack v1.0 on 1.4.3 and 1.2.1; other packs come with no guarantee
|
||||
- Atmosphère cheats: put a cheat file into a mod and switch its cheats in the menu ([docs/cheats.md](docs/cheats.md))
|
||||
|
||||
## Planned
|
||||
|
||||
@@ -27,7 +27,7 @@ Code copied or adapted from another project is listed here with its source, the
|
||||
| deko3d block-linear image layout, as ported in MacNativeGames' `nvn_tex.c` (Apple parts dropped, layout math unchanged; licence text in `games/totk/third_party/d2r_gfx/LICENSE.deko3d`, reproduced in the file header) | https://github.com/devkitPro/deko3d (`source/dk_image.cpp`), via MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/nvn/nvn_tex.c`, `nvn_tex.h`), Copyright (C) 2018-2020 fincs | zlib | `runtime/nvn_tex.c`, `runtime/nvn_tex.h` |
|
||||
| MacNativeGames texture auto-layout goldens (40 original-SDK size and block-height cases) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/tests/texture_auto_layout/probe.c`), Copyright (c) 2026 jhaizhou | MIT | `runtime/nvn_tex_probe.c`; `runtime/nvn_probe.c` (four of the cases, through the procs) |
|
||||
| MacNativeGames block-linear deswizzler (address function; copy loop split out as `tex_bl_copy`) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/tex/nvn_deswizzle.c`), Copyright (c) 2026 jhaizhou | MIT | `runtime/tex_deswizzle.c`, `runtime/tex.h` |
|
||||
| SDL 3.4.18, dynamic link, `SDL3.dll` copied next to `totk.exe`. Developer builds use the MSYS2 build (`mingw-w64-ucrt-x86_64-sdl3`, which imports `libiconv-2.dll`, LGPL-2.1, unmodified, copied beside it); the installer release ships the upstream release `SDL3-3.4.18-win32-x64.zip`, which imports no libiconv | https://libsdl.org, Copyright (c) 1997-2026 Sam Lantinga | zlib | `runtime/input_win.c` (gamepad input, rumble, gyro and accelerometer) |
|
||||
| SDL 3.4.20, dynamic link, `SDL3.dll` copied next to `totk.exe`. Developer builds use the MSYS2 build (`mingw-w64-ucrt-x86_64-sdl3`, which imports `libiconv-2.dll`, LGPL-2.1, unmodified, copied beside it); the installer release ships the upstream release `SDL3-3.4.20-win32-x64.zip`, which imports no libiconv | https://libsdl.org, Copyright (c) 1997-2026 Sam Lantinga | zlib | `runtime/input_win.c` (gamepad input, rumble, gyro and accelerometer) |
|
||||
| libopus (MSYS2 `mingw-w64-ucrt-x86_64-opus`), dynamic link, `libopus-0.dll` copied next to `totk.exe` | https://opus-codec.org, Copyright (c) 2001-2023 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon | BSD-3-Clause | `runtime/ov_media.c` (nn::codec hardware Opus decoder) |
|
||||
| Dear ImGui 1.91.9 (core and Vulkan back end, unmodified), as vendored by LAS Recomp | https://github.com/ocornut/imgui, via https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f` (`third_party/imgui/`), Copyright (c) 2014-2025 Omar Cornut | MIT | `runtime/third_party/imgui/` (compiled into `totk.exe` for the in-game menu, `runtime/ui/`) |
|
||||
| SavageWindRecomp settings menu: two-pane option rows (row look, left / right stepping, hold speed-up, description pane), the menu style and tab bar | https://github.com/Revivedx/SavageWindRecomp @ `575ad88e02f9a89a9a885d90039d48af33c982f4` (`runtime/ui/option_rows.mm`, `runtime/ui/menu.mm`), Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors, Copyright (c) 2026 Peiikai | MIT | `runtime/ui/option_rows.cpp`, `runtime/ui/option_rows.h`, `runtime/ui/menu.cpp` |
|
||||
@@ -73,7 +73,7 @@ These tools run on the developer's or player's machine during the build. They ar
|
||||
| CPython 3.14.9 embeddable package, with `tkinter` and Tcl/Tk 9.0.4 taken from the python.org 3.14.9 installer's component MSI (unpacked, never run); includes `vcruntime140*.dll` (Microsoft, redistributable with Python), OpenSSL, libffi, SQLite, zlib, libtommath | https://www.python.org/downloads/release/python-3149/ | PSF-2.0 (Tcl/Tk: Tcl/Tk licence, BSD-style) | `python/` |
|
||||
| numpy 2.5.4 (with its bundled OpenBLAS), lz4 4.4.5, cryptography 50.0.2 (with its bundled OpenSSL), fontTools 4.68.0, cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | BSD-3-Clause; lz4 BSD-3-Clause; cryptography Apache-2.0 OR BSD-3-Clause; fontTools MIT; cffi MIT; pycparser BSD-3-Clause | `python/Lib/site-packages/` |
|
||||
| llvm-mingw 20261020 (LLVM/Clang/LLD 23, mingw-w64, libc++, libunwind, compiler-rt), x86_64 subset | https://github.com/mstorsjo/llvm-mingw/releases/tag/20261020 | Apache-2.0 with LLVM exception; mingw-w64 ZPL-2.1/BSD/public domain; libc++ and libunwind Apache-2.0 with LLVM exception and MIT/UIUC; winpthreads MIT | `llvm/` |
|
||||
| SDL 3.4.18 upstream Windows release (`SDL3.dll`) and mingw development package (headers, `libSDL3.dll.a`) | https://github.com/libsdl-org/SDL/releases/tag/release-3.4.18 | zlib | `bin/SDL3.dll`, `runtime/deps/` |
|
||||
| SDL 3.4.20 upstream Windows release (`SDL3.dll`) and mingw development package (headers, `libSDL3.dll.a`) | https://github.com/libsdl-org/SDL/releases/tag/release-3.4.20 | zlib | `bin/SDL3.dll`, `runtime/deps/` |
|
||||
| libopus 1.6.1 (MSYS2 `mingw-w64-ucrt-x86_64-opus` 1.6.1-2; `libopus-0.dll` imports only KERNEL32 and the UCRT api-ms-win-crt DLLs), headers and import library | https://repo.msys2.org/mingw/ucrt64/ | BSD-3-Clause | `bin/libopus-0.dll`, `runtime/deps/` |
|
||||
| libvpx 1.17.0 (MSYS2 `mingw-w64-ucrt-x86_64-libvpx` 1.17.0-1), headers and static library compiled into `totk.exe`; `libvpx-LICENSE.txt` and `libvpx-PATENTS.txt` come from the v1.17.0 source tag | https://github.com/webmproject/libvpx | BSD-3-Clause with the WebM patent grant | `runtime/deps/`, `licenses/` |
|
||||
| Zstandard 1.5.7 (MSYS2 `mingw-w64-ucrt-x86_64-zstd` 1.5.7-2), headers and static library compiled into `totk.exe`; `zstd-LICENSE.txt` from the package | https://github.com/facebook/zstd | BSD-3-Clause | `runtime/deps/`, `licenses/` |
|
||||
|
||||
@@ -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/<ver>/`, 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.
|
||||
@@ -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/<ver>-<name>`) 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/<dir> --version <ver>` ([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/<name>/`
|
||||
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/<name>/`. Pick a save that starts where the change acts.
|
||||
|
||||
## Running
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\run_game.py build\<dir> --save <name> --seconds 30 [--script <input.txt>] [--shot out.png] [--env KEY=VALUE] [--quiet] [--tag <name>]
|
||||
```
|
||||
|
||||
- `--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 `<dir>\logs\<tag>.log`; the tool prints `<log>: rc=<code>`.
|
||||
|
||||
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<lo>-<hi>:<step>` (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=<frame>`; `python tools/flight_view.py <dump dir>` lists the worst frames and writes a contact sheet |
|
||||
| every render pass of one frame | `TOTK_DUMP=<frame>` 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=<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 <log> <lo> <hi>` |
|
||||
| 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<lo>-<hi>` and `TOTK_CRIT_OUT=<absolute path>` with `sched` tracing, then `python tools\crit_path.py <file>` |
|
||||
| where CPU time goes | `TOTK_PROFILE=<path>` and `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, then `python tools\prof_report.py <profile> <totk.map>`; `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=<seconds>` alternates the two sides in blocks of game time; `python tools\ab_summary.py <log> --skip <seconds>`. 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.
|
||||
@@ -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 |
|
||||
@@ -85,11 +85,17 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
- Vulkan 1.3 back end in place of MacNativeGames' Metal one. At `QueueSubmitCommands` the recorded NVN commands are replayed into Vulkan command buffers (`vk_exec*.c`).
|
||||
- Memory pools. Apple's unified memory let MacNativeGames use the game's pools zero-copy. The back end imports the pools as host memory (`VK_EXT_external_memory_host`). On a discrete GPU, pools bound as buffers are mirrored in device-local memory and the pages the CPU wrote (write watch, above) are uploaded before a replay; idle pool regions are made read-only so the write-watch queries skip them (`vk_memory.c`). GPU-rendered textures are written back when the CPU reads them: on a pool invalidate, on a CPU copy, and for small targets once the CPU waits for a fence (`vk_texture.c`).
|
||||
- Replay thread. `TOTK_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is off by default. Where the game waits for the GPU it overlaps the replay with the game threads (a GTX 1650 in the Depths, issue #88), a gain measured only there; while it was the default, a GTX 1660 Ti at the 60 fps cap with the GPU at 26 % reported uneven motion (#659). It adds up to a frame of latency; `replay_async = 1` turns it on.
|
||||
- Chunked replay. A large submission is recorded in chunks by several threads (`TOTK_REPLAY_WORKERS`, 0 by default: the sequential replay; 4 is a good value from 8 logical processors) and the chunks go to the queue in order (`vk_exec.c`). A sequential scan keeps a copy of the queue state where each chunk starts and lists what a chunk would otherwise learn from the chunks before it: inline uniform updates, conditions, contact shadow marches, uniform drops. It also makes render targets live in replay order and notes the op, since a chunk can sample a storage before the chunk that renders it got there or after the scan went past a later target there: a chunk's texture slots resolve as of its own op, and the chunks after a texture with no entry yet wait for the chunk that makes it. DLSS's decisions (the G-buffer, the light target, the end of the scene, the scaling pass) are the scan's too, and its copy of the lit scene and its evaluation are records of the chunk that holds the op; a chunk's draws are jittered while they come before the scan's end of the scene. The evaluation waits until the chunks before it are recorded, as it reads what their draws noted (the camera block, the jitter, the per-object motion list, which chunks add to under one lock), and the chunks after it wait for it, as they sample its output. `TOTK_REPLAY_STATE_CHECK=1` compares the state after the last chunk with the scan's.
|
||||
- Frame ahead. `TOTK_SYNC_AHEAD=1` (`sync_ahead`, off by default) makes the one `nvnSyncWait` per frame that waits for the frame presented last wait for the frame before it, so the game builds the next frame while the GPU still runs the previous one (`nvn_api_core.c`, `nvnSyncWait`). The game then writes memory that in-flight work still reads. This is safe only with shadow storage (`vk_memory.c`, the CPU-visible pools are host-visible copies of guest memory, on by default on Linux and Wine, `TOTK_SHADOW=1` on native Windows; without it the switch is ignored): a CPU write reaches the GPU only through the shadow copy made before each submit (`vk_mem_shadow_sync`), and a copy that would overwrite bytes an unfinished submission reads is queued as a GPU copy in the prologue command buffer of the part submitted next (`vk_mem_shadow_prologue`, `TOTK_SHADOW_DEFER=1`, the default with the feature) or, with `=0`, waited for on the replay thread (`shadow_reader_wait`). Only that one wait lags (identified by its call site, known for 1.2.1 and 1.4.3); results the game reads back from the GPU after it can be a frame old.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. It is off by default on AMD GPUs, where the driver loses the device or hangs the GPU at startup with it (`TOTK_VK_RECORDER=1` forces it on, `=0` makes every call on the submitting thread).
|
||||
- Uniform snapshot. The game rewrites per-object uniform blocks on other threads soon after it submits the frame
|
||||
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
|
||||
binds from CPU-written pools is copied, and the draws bind the copy (`vk_usnap.c`, after MacNativeGames'
|
||||
`TOTK_UBO_SNAPSHOT`); without it, late draws read another object's matrix.
|
||||
- Uniform updates. `nvnCommandBufferUpdateUniformBuffer` between the draws of a pass is replayed as an overlay: the
|
||||
bytes go to scratch memory, later bindings of the range bind them, and the copies into the game's buffers are
|
||||
recorded behind one barrier when the pass ends (`vk_exec_draw.c` `op_update_ub`, `TOTK_UB_OVERLAY`). A copy at the
|
||||
update itself would end the pass with a full barrier, which the terrain paid before most of its draws.
|
||||
- Driver data. The NVN driver keeps a per-stage constant bank (c0) that shaders read for things like point size, dispatch size and base instance. The back end fills it the way the driver does.
|
||||
- Shaders: the Ryujinx translator 1.3.2 with MacNativeGames' patches and ours (`tools/xlate/patches/`), built with .NET 10 NativeAOT as `totk_xlate.exe` and run as a separate process. Its SPIR-V goes to Vulkan directly. `tools/prepare_shaders.py` and `totk.exe --prepare-shaders` translate and compile the variants of MacNativeGames' shared list ahead of play (29,587 for 1.4.3); the rest are translated on first use and cached on disk.
|
||||
- Pipelines: `VK_EXT_graphics_pipeline_library` where the driver has it. Shader stages are compiled ahead into libraries; a draw that meets a new state links them at once (fast link), and an optimised pipeline replaces the linked one when a background thread finishes it. Pipeline keys seen in play are saved and built at boot. A draw whose pipeline is not ready is skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise, so one-shot passes are never lost; a full-screen draw is skipped as well rather than waiting (`TOTK_PIPE_SKIP`); the stage libraries a waiting draw needs are built on idle workers in parallel (`TOTK_PIPE_WAIT`). Without the extension, a missing pipeline is built on a worker and the draw waits or is skipped by the same rule.
|
||||
|
||||
+17
-25
@@ -118,6 +118,13 @@ of them.
|
||||
button prompts"), which `build_game.py` copies next to `totk.exe` like `romfs-patches`. `--prompts pc` writes
|
||||
`romfs-patches-pc`, the keyboard and mouse prompts ([controls.md](controls.md), "PC button prompts").
|
||||
|
||||
A build from source has no layouts for other shapes than 16:9. At an `output_res` such as 5120x1440 the game then
|
||||
draws the largest 16:9 size inside it (2560x1440) and the present adds bars, instead of a wide camera over a
|
||||
stretched HUD (`runtime/resolution.c`, `shape_fallback`). For the full shape make `romfs-patches-<w>x<h>` once per
|
||||
size: `make_romfs_patches.py --version <ver> --aspect <w>x<h> --out build\<ver>\romfs-patches-<w>x<h>` (the sizes
|
||||
the installer makes are `ASPECT_SIZES` in `installer/steps.py`); `build_game.py` copies every `romfs-patches*`
|
||||
folder next to `totk.exe`.
|
||||
|
||||
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
|
||||
for the shared shader list:
|
||||
|
||||
@@ -153,30 +160,9 @@ folder. The folder holds code and data derived from your game: for your own mach
|
||||
|
||||
### Profile-guided build
|
||||
|
||||
`build_game.py --pgo <file>.profdata` compiles the generated code at -O2 with an execution profile, and the runtime
|
||||
with it too: hot paths laid out straight, hot calls inlined, code that never ran compiled for size. On 1.4.3 the fight
|
||||
bench ran 12-15% faster per frame and a fight outside the training about 6%. The installer builds with
|
||||
`data/pgo-<ver>.profdata` of the release when the update has one, else at -O1.
|
||||
|
||||
A profile only fits the generated C it was trained on: functions are matched by name and control-flow hash, so a
|
||||
function whose C changes loses its counts without a warning. Retrain after a lifter or generator change, from a clean
|
||||
`recomp/`. The instrumented build needs llvm-mingw, as MSYS2's clang has no profile runtime:
|
||||
|
||||
```powershell
|
||||
$llvm = "toolchain\llvm-mingw-<date>\llvm-mingw-<date>-ucrt-x86_64"
|
||||
.venv\Scripts\python tools\build_game.py --version v143 --out build\v143-pgogen --toolchain $llvm --pgo gen
|
||||
```
|
||||
|
||||
Play `build\v143-pgogen\totk.exe` through varied scenes (fights, towns, riding, building, the Depths, menus) for 20-30
|
||||
minutes; it is slower than a normal build. It writes `build\v143-pgogen\pgo\*.profraw` every minute and at exit.
|
||||
Scripted runs from saves (`tools/run_game.py`) add to the same file. Then:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\pgo.py merge build\v143-pgogen --version v143
|
||||
```
|
||||
|
||||
writes `build\pgo\pgo-v143.profdata` and its record (`.json`: the `recomp/` tree it fits). `make_release.py` ships every
|
||||
profile of `build\pgo` in `data\` and warns when a record names another `recomp/` tree than the one released.
|
||||
`build_game.py --pgo <file>.profdata` compiles the generated code and the runtime at -O2 with an execution
|
||||
profile, 6-12% shorter frames. Which build uses which profile, when to retrain, where and how to train and how to
|
||||
measure a new profile: [pgo.md](pgo.md).
|
||||
|
||||
## Linux (in progress)
|
||||
|
||||
@@ -230,7 +216,13 @@ Before a commit:
|
||||
|
||||
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
|
||||
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
|
||||
files); a change to the settings passes `totk.exe --probe-cfg`.
|
||||
files); a change to the settings passes `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`, which fails
|
||||
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md). A change to the mods' ExeFS patches
|
||||
(`tools/exefs_patches.py`, `runtime/mods.c`, the `folded.txt` of `recomp/gen.py`) passes
|
||||
`.venv\Scripts\python tests\exefs_patches_test.py`, which makes its own modules and runs `gen.py` on one with a jump
|
||||
table (no game files), and `totk.exe --probe-mods`. A change to the cheat VM or the cheat opcode tables
|
||||
(`runtime/cheats.c`, `tools/cheats.py`) passes `.venv\Scripts\python tests\cheats_vm_check.py`, which runs both on
|
||||
cheats of its own with clang from PATH (`--cc` names another).
|
||||
|
||||
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
|
||||
change passes these, all run with `.venv`:
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ The game's code is compiled ahead of time, so a cheat cannot write new instructi
|
||||
|---|---|---|
|
||||
| code | Opcode 0 writes into main's code | Built into the game at setup as a patch set (`recomp/patches.py`); switches at once. |
|
||||
| key | Code writes inside a button condition (`8` mask, writes, `21` else, the original words, `20`) | A patch set that is on while the cheat is on and the buttons are held. |
|
||||
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A, every presented frame while on. |
|
||||
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A and C0 to C3, every presented frame while on. |
|
||||
| master | `{name}` with code writes | A patch set that is always on. |
|
||||
| restore | Writes back only the game's original instructions | Not needed: switching a cheat off restores them. Greyed out. |
|
||||
|
||||
@@ -66,7 +66,7 @@ A cheat whose code writes call or jump into a code cave the cheat writes itself
|
||||
|
||||
Not supported, and shown greyed out with the reason:
|
||||
|
||||
- the 0xC and 0xF opcode extensions;
|
||||
- opcode C4 (extended button condition) and the 0xF extensions (FF0 pause, FF1 resume, FFF debug log);
|
||||
- opcode 5 with a register offset;
|
||||
- code writes mixed with other opcodes, except the button pattern above;
|
||||
- code writes that change the CPU flags differently from the game's instruction, or code caves with branches or pc-relative instructions.
|
||||
|
||||
+76
-22
@@ -14,7 +14,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|
||||
|---|---|---|---|
|
||||
| Left stick | left stick | W A S D | move |
|
||||
| Right stick | right stick | mouse; arrow keys as a second source | camera |
|
||||
| Y | west | left mouse button | attack |
|
||||
| Y | west | left mouse button, T | attack |
|
||||
| ZL | left trigger (over 50%) | right mouse button, Z | shield, lock on |
|
||||
| ZR | right trigger (over 50%) | F, X | bow |
|
||||
| X | north | Space | jump |
|
||||
@@ -29,7 +29,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|
||||
| Minus | Back | M | map |
|
||||
| Right stick shaken left and right | shake the right stick | G | detach a fused part from what Ultrahand holds |
|
||||
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. T duplicates Y for the game's menus, where the left mouse button is A while the pointer is shown (sort the inventory with T). The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
|
||||
## Xbox button prompts
|
||||
|
||||
@@ -46,7 +46,7 @@ Esc is unbound in the game; it opens the settings menu and releases the mouse cu
|
||||
| Plus, Minus | Menu, View | Menu, View |
|
||||
| D-pad | cross | cross |
|
||||
|
||||
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters are right but the diamond keeps the Switch positions: A shows on its right, while the button labelled A is at the bottom. The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
|
||||
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters stay and the diamond is laid out as an Xbox pad: A at the bottom, B on the right, X on the left and Y at the top, the button the prompt names lit in its place, its bar reaching left from X and right from the others (`tools/make_romfs_patches.py`, `bflyt_xbox_diamond`: the panes of `blyt/DoCommand_00.bflyt` take their partner's place, A's of B and X's of Y). The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
|
||||
|
||||
The prompts are part of the game's fonts, textures and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the prompt the game draws for it, which then names the wrong button. Use the Xbox prompts with the default bindings.
|
||||
|
||||
@@ -55,7 +55,9 @@ The game reads its UI archive and fonts once at boot, so the prompts change at t
|
||||
- every prompt but the button guide in play is text in the button font, `scft/External.bfotf` in each `Font/Font*.Nin_NX_NVN.bfarc.zs`: glyphs U+E040 to U+E043 (A B X Y), U+E044 to U+E047 (L R ZL ZR), U+E04B and U+E04C (Plus, Minus), U+E049 and U+E060 to U+E065 (the d-pad). fontTools writes the font again, so the archives grow and the folder also carries the resource size table (`System/Resource/ResourceSizeTable.Product.<n>...rsizetable.zs`) with their entries raised by as much; without that the game stops at start.
|
||||
- the button guide in play draws L and ZL from the images `Nt_KeyTexL_00` and `Nt_KeyTexZL_00` of the Common archive's `timg/__Combined.bntx`, painted with the same icons. The action prompt draws its button from `Nt_KeyTexA/B/X/Y_00`, whose image data is swapped for `nintendo`, and the diamond's letters are text the game takes from `LayoutMsg/DoCommand_00.msbt` of each language's `Mals` archive (and the layout's own text panes behind it), swapped the same way.
|
||||
|
||||
The Switch prompts stay, with a note under *Button prompts* and a `RomFS overlay: ...` line in the log, when the folder is missing (an install made by an older setup, or one where the setup could not prepare it and showed a warning; running the setup again on the install folder writes it), when it fails its check, with a game resolution of another shape than 16:9 (its own UI layout wins; 16:10 and 21:9), and when the `romfs_mods` folder replaces one of its files: `romfs-patches` then loads in its place, where two overlays replacing one file would stop the game. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts xbox` (and `--prompts xbox-label`) writes `build\<ver>\romfs-patches-xbox` (`-label`); it needs fontTools from `tools/requirements-py314.txt`. `--xbox-prompts` is the older name of `--prompts xbox`.
|
||||
The Switch prompts stay, with a note under *Button prompts* and a `RomFS overlay: ...` line in the log, when the folder is missing (an install made by an older setup, or one where the setup could not prepare it and showed a warning; running the setup again on the install folder writes it), when it fails its check, with a game resolution of another shape than 16:9 that has no folder of its own (below), and when the `romfs_mods` folder replaces one of its files: `romfs-patches` then loads in its place, where two overlays replacing one file would stop the game. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts xbox` (and `--prompts xbox-label`) writes `build\<ver>\romfs-patches-xbox` (`-label`); it needs fontTools from `tools/requirements-py314.txt`. `--xbox-prompts` is the older name of `--prompts xbox`.
|
||||
|
||||
At a game resolution of another shape than 16:9 the game loads `romfs-patches-xbox-<w>x<h>` (or `romfs-patches-xbox-label-<w>x<h>`) in place of `romfs-patches-<w>x<h>`: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts xbox --aspect <w>x<h> --reuse <install>\romfs-patches-xbox --out <install>\romfs-patches-xbox-<w>x<h>`. The setup writes both for each of the fifteen sizes it lays the UI out for, as for the PC prompts below, so on a display of one of those shapes the game resolution follows the display with these prompts too. At another size, without that folder, the Switch prompts stay (*off at this game resolution*).
|
||||
|
||||
## PC button prompts
|
||||
|
||||
@@ -70,10 +72,10 @@ The Switch prompts stay, with a note under *Button prompts* and a `RomFS overlay
|
||||
| Left stick, its press | W A S D (or the keys of the directions meant), Ctrl |
|
||||
| Right stick, its press | the mouse, with an arrow for each direction meant, C |
|
||||
|
||||
- The glyphs keep the width of the game's own, so the right-aligned guides do not run into their text. A key is a rounded square with its label cut out in the game's own text fonts; a mouse button is a mouse with that button filled and the others hollow.
|
||||
- The glyphs keep the width of the game's own, so the right-aligned guides do not run into their text. A key is a rounded square with its label cut out in the game's own text fonts (Shift: an arrow over its name); a mouse button is a mouse with that button filled and the others hollow.
|
||||
- The button guide in play draws every button from images of the Common archive (`Nt_KeyTexA/B/X/Y/L/ZL_00`, `Nt_KeyTexLSt_00` and `_01`, the stick tilted and pressed): all of them are painted with the same icons. The letters of the action prompt's diamond are one character of the Normal font each: E, an arrow for Shift, a bar for Space, L and R for the mouse buttons.
|
||||
- Like the Xbox prompts they follow the defaults and not the bindings. `--binds <totk.ini>` draws the first key of each `bind_` line of that file instead, for a folder made by hand.
|
||||
- At a game resolution of another shape than 16:9 the game loads `romfs-patches-pc-<w>x<h>` in place of `romfs-patches-<w>x<h>` when it is there: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts pc --aspect <w>x<h> --out <install>\romfs-patches-pc-<w>x<h>`. The setup does not write those, so without one the Switch prompts stay (*off at a game resolution not 16:9*).
|
||||
- At a game resolution of another shape than 16:9 the game loads `romfs-patches-pc-<w>x<h>` in place of `romfs-patches-<w>x<h>`: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts pc --aspect <w>x<h> --reuse <install>\romfs-patches-pc --out <install>\romfs-patches-pc-<w>x<h>`. The setup writes one for each of the fifteen sizes it lays the UI out for, so on a display of one of those shapes the game resolution follows the display with these prompts too. At another size, without that folder, the Switch prompts stay (*off at this game resolution*).
|
||||
|
||||
## Window keys
|
||||
|
||||
@@ -96,7 +98,7 @@ The tab writes `totk.ini` lines, one per Switch button or stick direction: `bind
|
||||
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
|
||||
- `none` leaves the button unbound.
|
||||
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above, less any key a line binds: with `bind_X = key:t`, T presses X only, not also Y. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
|
||||
|
||||
A pad that reports its face buttons swapped (A/B and X/Y, as some GuliKit models do) is fixed with:
|
||||
|
||||
@@ -109,31 +111,83 @@ bind_Y = pad:north, key:mouse_l
|
||||
|
||||
## Mouse camera
|
||||
|
||||
`mouse_mode = direct` (the default) turns the camera by the raw mouse motion on game 1.4.3, through hooks in the game's camera code (`runtime/mouse_direct.c`, the `mouse_direct` sites of `recomp/patches-v143.txt`). None of them changes a step of the gamepad alone. The free-orbit camera receives raw-count
|
||||
angular increments at its final spherical-angle output: 0.08 degrees per count on both axes at sensitivity 1, with
|
||||
no time multiplier, curve or carry into later frames. The controller's persistent pitch/yaw move with the output.
|
||||
Mouse input is removed from its stick path first, so the camera's later vertical signed-square curve and velocity
|
||||
filters cannot process it a second time. Other camera modes receive input after the radial dead zone, unit-circle
|
||||
clamp and stick response exponent; Ultrahand has a separate hook after its manipulation input's power curves.
|
||||
The game's camera inversion and gamepad response remain in effect. `mouse_sens` scales raw counts; its scale
|
||||
differs from the stick-emulation mode. Start at 1.
|
||||
`mouse_mode = direct` (the default) turns the camera by the raw mouse motion on games 1.4.3 and 1.2.1, through hooks in the game's camera code (`runtime/mouse_direct.c`, the `mouse_direct` sites of `recomp/patches-v143.txt` and `recomp/patches-v121.txt`). None of them changes a step of the gamepad alone. The free-orbit camera receives raw-count
|
||||
angular increments at its final spherical-angle output: 0.08 degrees per count on both axes at sensitivity 1 (about
|
||||
14.3 cm a turn at 800 DPI), with no time multiplier, curve or carry into later frames. The controller's persistent pitch/yaw move with
|
||||
the output. Mouse input is removed from its stick path first, so the camera's later vertical signed-square curve and
|
||||
velocity filters cannot process it a second time.
|
||||
The aiming camera (bow, throwing), the Purah Pad camera, the scope, the ability camera (Ultrahand, Fuse, Ascend,
|
||||
Recall, Autobuild), the horse camera, lock-on, the bow drawn while locked on, climbing (walls and objects), the
|
||||
waterfall's turn, shield surfing, the wakeboard and the dialogue cameras take the mouse the same way, at the same
|
||||
degrees per count: each turns by
|
||||
the stick times its own speeds and the frame time and adds that to a base angle, so the load of the stick it turns by
|
||||
gets the gamepad's part only and the load of that base angle gets the mouse's degrees (`camera_load`, two sites per
|
||||
axis per camera). Their speeds, time step, slow motion (bullet time) and, for the Purah Pad camera and the abilities,
|
||||
the eased curve their pitch moves along do not touch the mouse; their own limits, collision and blends apply to the
|
||||
result. Past the pitch limit of the scope or the ability camera the stick moves the camera's distance; the mouse's
|
||||
motion past the limit keeps doing that through the stick. Recall's cursor is the ability camera's offset mode: the
|
||||
camera keeps a direction, the stick moves the cursor across the screen (ReverseRecorderCursorSpeed, within
|
||||
ReverseRecorderCursorRange) and the camera follows it by up to ReverseRecorderCameraRange (10 degrees across, 7.8 up).
|
||||
The mouse moves the cursor so that its aim, the camera's follow plus the cursor's angle from the screen's centre (the
|
||||
offset taken as the screen's half width or height at the camera's 50 degree view and aspect ratio), turns as many
|
||||
degrees a count as the camera does elsewhere. The follow range is read from the game each step (until it has been read
|
||||
once, the mouse is stick input there). The stick loads that only test for input keep the mouse in the stick. A step with mouse motion that no
|
||||
direct hook took is logged (`mouse: step N with mouse motion took the stick path`, the first 30, then one in 600, with
|
||||
the player's state), so the cameras still on stick input show in `logs/totk.log`.
|
||||
Lock-on keeps pointing the camera at the target; the stick, and so the mouse, adds an offset around that direction,
|
||||
which the game limits and lets return: the mouse's degrees are the step's turn the game adds to the offset and judges
|
||||
the return by.
|
||||
Shield surfing and the wakeboard turn their pitch by the square of the stick's y and move a second camera parameter
|
||||
with it: their stick y keeps the mouse (that parameter and the camera's state follow it as before), the pitch gets
|
||||
the degrees, and the pitch turn the game made from y is cut to the gamepad's share (p |p| / t |t|). Their stick x, and
|
||||
the dialogue camera's (which is also its test for input), give a tiny value in the mouse's direction when the
|
||||
gamepad's axis is at rest, so the camera still sees input without a stick turn.
|
||||
Two inputs stay stick input: the waterfall's vertical axis moves the camera's height, not an angle, and the horse's
|
||||
lock-on without a target (HorseLockOnEmpty) turns the stick's position into an offset that returns when the stick
|
||||
does, which a mouse has no position for.
|
||||
Those receive input after
|
||||
the radial dead zone, unit-circle clamp and stick response exponent: x is 0.0343 stick units a count over the frame
|
||||
ratio (the stick turns yaw 70 x / 30 degrees a step) and y 70 / 51 times that, linear like their pitch (51 y / 30
|
||||
degrees a step), so both axes turn by the same amount for the same motion. `stick_sens` (which scales the game's
|
||||
stick turn rates) is divided out. While the player uses an ability and the ability camera's hooks do not take the
|
||||
mouse, its part is scaled by 1 / 0.8 and 70 / (1.5 x 51), the ability camera's own stick scales.
|
||||
The player's verbs are read every 50 ms from the player actor (the player root's GOT slot main+0x3969918, the
|
||||
component array +0x228, its AS component +0x158 and the verbs of its five slots). Ultrahand has a separate hook after
|
||||
its manipulation input's power curves; it gets 0.01 stick units a count over the frame ratio, at most three full tilts
|
||||
a step.
|
||||
`mouse_aim_scale` (0.1 to 3, default 1) multiplies the mouse's motion while the bow is drawn (the verb UseBow or
|
||||
DrawnBow with ZR held). The game's camera inversion and gamepad response remain in effect. `mouse_sens` scales raw
|
||||
counts, with the same meaning as in 0.3.10 (the free camera turns 0.08 degrees a count at 1, and now every other camera
|
||||
does too); its scale differs from the stick-emulation mode.
|
||||
The Controls tab switches between *Direct* and *Stick emulation* (`mouse_mode = stick`).
|
||||
The orbit camera keeps its desired pitch at the final angular output, including stationary frames, so a later game
|
||||
filter cannot recenter it. The persistent filter state accounts for the camera's current vertical offset instead of
|
||||
accumulating corrections. A gamepad vertical input or the game's lock-on/override releases that mouse target.
|
||||
While L is held, mouse counts move the ability wheel's virtual cursor, which stays where it was when the mouse stops.
|
||||
accumulating corrections. A gamepad vertical input releases that mouse target to the game's own. The game's
|
||||
lock-on, recenter and override (aiming) drive the camera themselves: the mouse stops holding without restoring its
|
||||
earlier target, and when they end (or after the camera went 100 ms without a step) it holds the pitch the camera has
|
||||
then, so the camera does not spring back to a pitch from before the aim or to the game's default after a shot.
|
||||
While the ability wheel is open (L held and its screen, ShortCutMainItem_00 or ShortCut_00, has the focus), mouse
|
||||
counts move its virtual cursor, which stays where it was when the mouse stops. L or the D-pad held with no wheel or
|
||||
strip on screen (L soothes the horse, turns menu tabs) leaves the mouse on the camera; with the menu cursor off
|
||||
(`menu_cursor = 0`) the screens are not read and the buttons alone decide.
|
||||
The horizontal quick lists (D-pad up/left/right: materials, shields and weapons) use separate 32 ms right-stick steps,
|
||||
with at least 32 ms of neutral between them. At sensitivity 1, 32 horizontal counts request a step; a large flick
|
||||
cannot queue continued scrolling after the mouse stops. Vertical movement is ignored in these lists. Switching or
|
||||
releasing a selector resets its cursor/step state without sending the last movement to the camera. Selector input is
|
||||
not consumed by the direct camera or Ultrahand hooks.
|
||||
The mouse never stands in for a stick shake: to detach a fused part, press G (`bind_SHAKE`). A press swings the right
|
||||
stick left and right for 0.8 s, holding the key keeps it swinging. The arrow keys or the gamepad's right stick shaken by
|
||||
hand work too. A mouse gesture for it would also fire while a held object is turned back and forth.
|
||||
To detach a fused part from what Ultrahand holds, shake the mouse left and right, or press G (`bind_SHAKE`). A press
|
||||
swings the right stick left and right for 0.8 s, holding the key keeps it swinging. With `mouse_mode = direct` a shake
|
||||
of the mouse while Ultrahand holds an object swings the right stick with it: once the swings (10 degrees of camera turn
|
||||
or more each, 125 counts at sensitivity 1) change direction twice within 0.35 s, the stick goes full tilt the
|
||||
way the mouse moves, each side held at least two frames so the game reads it, and goes neutral as soon as the mouse
|
||||
rests. Only your own swings reach the stick, so nothing keeps swinging after you stop; shake until the part comes off.
|
||||
The camera hooks ignore that swing (the mouse turns the camera in degrees already). Turning a held object back and
|
||||
forth at an ordinary pace is slower and does not count. The arrow keys or the gamepad's right stick shaken by hand
|
||||
work too.
|
||||
A step of the gamepad alone, with no mouse motion and no pitch the mouse holds, reaches the camera exactly as without
|
||||
the hooks.
|
||||
The mapping below is still used when the camera-input hook is unavailable or the game disables camera input.
|
||||
Games 1.1.0 and 1.2.1 have no hooks and keep stick emulation whatever `mouse_mode` says.
|
||||
Game 1.1.0 has no hooks and keeps stick emulation whatever `mouse_mode` says. On 1.2.1 `mouse_aim_scale` has no effect (the bow-in-hand state is not read there).
|
||||
|
||||
In stick-emulation mode, mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * TOTK_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
|
||||
|
||||
@@ -145,7 +199,7 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
|
||||
|
||||
## Mouse in the game's menus
|
||||
|
||||
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild, the Purah Pad's camera and the map stay gameplay: the mouse keeps the camera there. When the menu closes the cursor is captured again.
|
||||
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The middle and side mouse buttons keep their bindings (bound to L and R they turn the tabs), and T presses Y (sort). The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
|
||||
|
||||
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`TOTK_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `TOTK_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
|
||||
|
||||
|
||||
+23
-9
@@ -38,7 +38,10 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
|
||||
|
||||
The player runs a newer installer on the same folder. It reads `totk-install.json`, keeps `romfs/`, `exefs/`,
|
||||
`shared-fonts/`, `profile/` and `totk.ini` after checking their hashes, rebuilds `totk.exe` and re-prepares the
|
||||
shaders. A newer release that changes nothing game-derived skips the rebuild.
|
||||
shaders. A newer release that changes nothing game-derived skips the rebuild. The window starts with the last install folder
|
||||
and the base, update, keys and firmware paths it ran with (paths only, never the keys file's contents), kept in
|
||||
`%LOCALAPPDATA%\TotK Recomp\setup.json` (`$XDG_STATE_HOME/totk-recomp/setup.json` elsewhere). Entries that no longer exist
|
||||
stay empty. A reinstall or a switch to another game version starts filled in.
|
||||
|
||||
Given an update file other than the recorded one (1.1.0, then 1.2.1, then 1.4.3), the installer extracts and builds again from it
|
||||
and keeps `profile/`. Saves carry over both ways between 1.2.1 and 1.4.3: 1.4.3 converts a 1.2.1 save on load, and 1.2.1
|
||||
@@ -57,7 +60,9 @@ loads the saves 1.4.3 wrote.
|
||||
4. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
|
||||
to the installer. The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets first
|
||||
(`tools/cheats.py`, `tools/exefs_patches.py`, status in `build-cache/cheats.status` and `exefs.status`); a file the
|
||||
tools refuse costs those cheats or patches, not the install.
|
||||
tools refuse costs those cheats or patches, not the install. The patch files' writes into main's data go to
|
||||
`build-cache/exefs.data`, which the game stores into main at start; after the lift they are checked against the
|
||||
bytes the lifted code read as constants (`<gen>/main/folded.txt`, its jump tables).
|
||||
5. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
|
||||
hashes and the game version's patch page) and link
|
||||
them with the prebuilt runtime library for the host's level (`libtotkrt.a` or `libtotkrt-v2.a`) into `totk.exe`
|
||||
@@ -69,8 +74,11 @@ loads the saves 1.4.3 wrote.
|
||||
writes the translations it makes in play as loose files; running the tool again with the game closed folds those in.
|
||||
The patches are written four times: `romfs-patches`, `romfs-patches-xbox` and `romfs-patches-xbox-label`
|
||||
(`--prompts`: Xbox button prompts for `prompts = xbox`, the first for `pad_layout = nintendo`, the second for `xbox`
|
||||
and `auto`), and `romfs-patches-pc` (keyboard and mouse prompts for `prompts = pc`). The three prompt folders are
|
||||
optional: when one cannot be made, the setup warns and finishes.
|
||||
and `auto`), and `romfs-patches-pc` (keyboard and mouse prompts for `prompts = pc`). Each of the three prompt
|
||||
folders is then laid out once more for each game resolution of another shape, `romfs-patches-<style>-<w>x<h>`
|
||||
(`--prompts <style> --aspect`). Those differ from `romfs-patches-<style>` in the UI archive only, and `--reuse`
|
||||
hard-links the other files from it, so the forty-five folders take seconds and a few MiB each. The prompt folders are optional: when one cannot be made, the
|
||||
setup warns and finishes.
|
||||
When the graphics driver crashes while building the shaders, `prepare_shaders.py` runs that part again without the
|
||||
variants it was building ([building.md](building.md), step 6), and the setup finishes with a warning that names how
|
||||
many were left out.
|
||||
@@ -129,9 +137,12 @@ python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR
|
||||
English as the game language, because Chinese game text needs the Switch shared fonts, which the setup extracts only
|
||||
from a firmware folder. A folder with
|
||||
`totk-install.json` needs only the folder. `--selftest [--shots DIR]` builds every screen in every language and
|
||||
closes itself.
|
||||
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the self-closing window and one end-to-end run
|
||||
of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its docstring).
|
||||
closes itself. The window is not stretched on a display whose scale is the system DPI: `installer/dpi.py` makes the
|
||||
process system DPI aware before Tk starts, Tk then draws the fonts at the display's scale and `dpi.px` sizes the
|
||||
paddings and wrap widths with it; the launcher does the same for its error box.
|
||||
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the display scale, the self-closing window and
|
||||
one end-to-end run of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its
|
||||
docstring).
|
||||
- `tests/installer_test.py`: bookkeeping with fake steps. `tests/installer_e2e.py`: a dev-mode run with the files of
|
||||
`config.local.toml`.
|
||||
|
||||
@@ -149,7 +160,7 @@ runtime/ libtotkrt.a, libtotkrt-v2.a and the headers the generated
|
||||
include
|
||||
bin/ totk_xlate.exe, hactool.exe, SDL3.dll, libopus-0.dll, ninja.exe
|
||||
data/ shader_list.tsv (MacNativeGames, MIT); pgo-<ver>.profdata + .json, the execution profile an
|
||||
update's code and the runtime libraries compile with (building.md, "Profile-guided build")
|
||||
update's code and the runtime libraries compile with (pgo.md)
|
||||
THIRD_PARTY_NOTICES.md
|
||||
totk.ini.example, VERSION the default ini; the release string (the repository's VERSION plus the commit)
|
||||
```
|
||||
@@ -212,7 +223,8 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
|
||||
|
||||
## Constraints
|
||||
|
||||
- The installer steps contain no Win32 calls: Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The installer steps contain no Win32 calls; only the window's DPI awareness (`installer/dpi.py`) is Windows-only, and
|
||||
it does nothing elsewhere. Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The keys file path is passed to the tools; key values never appear in the UI, logs or files.
|
||||
- The window is a front end over the CLI (`python -m installer --base ... --update ... --keys ... --lang ... --dest
|
||||
...`), which prints one JSON progress event per line. Tests and CI use the CLI.
|
||||
@@ -236,3 +248,5 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
|
||||
## Later
|
||||
|
||||
- Re-warm the driver cache on start after a move, GPU or driver change.
|
||||
- Per-monitor DPI in the setup window: Tk 9 has none, so a display whose scale differs from the primary one still gets
|
||||
the window stretched.
|
||||
+23
-1
@@ -48,7 +48,10 @@ Two mods that edit the same game file do not combine: the higher one wins the wh
|
||||
(`.tkcl` files) go the same way.
|
||||
|
||||
The port merges the resource size table on its own: at every start with mods on, it combines the tables the mods ship
|
||||
and raises the entries for files a mod made larger or added. A separate RESTBL mod is not needed.
|
||||
and raises the entries for files a mod made larger or added, the files inside a mod's packs (`Pack\**\*.pack.zs`)
|
||||
included. A separate RESTBL mod is not needed. The table tells the game how much memory each file needs: a file that
|
||||
outgrew its entry crashes the game when it loads, which can be long after the start, when the game first needs it. The
|
||||
log line `RomFS: resource size table: ...` says how many entries were raised and names the first files inside packs.
|
||||
|
||||
## Code patches (ExeFS)
|
||||
|
||||
@@ -68,6 +71,10 @@ The rebuild needs the setup folder the game was installed from. When that folder
|
||||
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
|
||||
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
|
||||
|
||||
A patch file may also change the game's data (constants and tables in its `.rodata` and `.data`). Those bytes are
|
||||
stored into the loaded game at start, before it runs. When two mods write the same bytes, the higher one wins,
|
||||
as with files.
|
||||
|
||||
## Cheats
|
||||
|
||||
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
|
||||
@@ -84,6 +91,21 @@ The port keys a mod by its folder name. A renamed folder counts as a new mod:
|
||||
|
||||
Rename mod folders before you set them up in the menu, not after.
|
||||
|
||||
## HD texture packs
|
||||
|
||||
Texture packs made for the emulators replace the game's textures with larger ones, so the heap the world's textures
|
||||
stream into must grow. Such a pack grows it in its own `System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs`, sized for
|
||||
the emulators' 8 GB memory option. The port reads that file at start: the texture heap and the game's memory pool grow
|
||||
by the pack's amount, and the game gets the port's own heap table with that growth (the log says
|
||||
`resolution: mod <name> ships a HeapDef with TextureHeap ...`). Nothing needs to be set. For a pack without such a file
|
||||
that still loops the panic Blood Moon, set `tex_heap_extra_mb` under `[mods]` in `totk.ini` (0 to 4096 MiB).
|
||||
|
||||
Tested: **HDPacks.com Tears of the Kingdom 4K Texture Pack v1.0** (`4kTextures`, 10,407 textures, 6.5 GB) on game 1.4.3
|
||||
and 1.2.1, RTX 4080 SUPER: it loads into the game from a save and runs at the same frame rate, with about 3.3 GB more of
|
||||
textures in memory (4.5 GB of game memory pools against 1.2 GB without it). Other packs are not tested and come with no
|
||||
guarantee. Such a pack needs a lot of video memory; on a GPU with less than 8 GB, `astc_bc = 1` under `[performance]`
|
||||
keeps its textures at a quarter of the size.
|
||||
|
||||
## What does not work
|
||||
|
||||
| On the Mods tab | Why | What to do |
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# Profile-guided build
|
||||
|
||||
A profile-guided build compiles the generated code and the runtime at -O2 with an execution profile: a record of
|
||||
which functions and branches ran while someone played. Hot paths are laid out straight, hot calls are inlined, and code
|
||||
that never ran is compiled for size. On 1.4.3 a trained fight ran 12% faster per frame and an untrained fight about
|
||||
6%; on 1.2.1 the trained fight ran 11% faster.
|
||||
|
||||
Each game version has its own profile, `pgo-v121.profdata` and `pgo-v143.profdata`, trained on that version's code.
|
||||
|
||||
## How a build uses a profile
|
||||
|
||||
- `build_game.py --pgo <file>.profdata` compiles with that profile. Every compile depends on the file, so a new
|
||||
profile rebuilds everything.
|
||||
- `tools/pgo.py merge` writes the profile to `build\pgo\pgo-<ver>.profdata`, next to its record `pgo-<ver>.json`. The
|
||||
record holds the git tree of `recomp/` it was trained on and the digests of the runtime headers the generated C
|
||||
includes (`cpu.h`, `simd.h`, `simd_fp.h`).
|
||||
- `make_release.py` ships every profile in `build\pgo` in the release's `data\` folder. It warns when a record names
|
||||
another `recomp/` tree than the one released, or a header digest differs. The runtime libraries of the release
|
||||
compile with the newest version's profile.
|
||||
- The installer builds a player's game with `data\pgo-<ver>.profdata` when the release has one for that version, and
|
||||
at -O1 without it.
|
||||
|
||||
A profile only fits the code it was trained on. clang matches each function by name and control-flow hash, and a
|
||||
function whose code changed loses its counts without a warning (`build_game.py` passes `-Wno-backend-plugin`, which
|
||||
hides the "hash mismatch" messages). The inline helpers of `cpu.h`, `simd.h` and `simd_fp.h` become part of every
|
||||
lifted function, so a change to them invalidates the profile as surely as a generator change: one change to the
|
||||
floating-point helpers left a profile with a fifth of its weight and 2% of its gain.
|
||||
|
||||
## When to retrain
|
||||
|
||||
- After a change to the lifter or the generator (`recomp/`) that changes the generated C. Patch table changes
|
||||
(`recomp/patches-<ver>.txt`) touch only the patched functions and can wait for the next retrain.
|
||||
- After a change to `runtime/cpu.h`, `runtime/simd.h` or `runtime/simd_fp.h`.
|
||||
- Before a release whose `make_release.py` run warns about a profile.
|
||||
|
||||
Train both versions from the same commit, from a clean `recomp/`.
|
||||
|
||||
## Train
|
||||
|
||||
### 1. Instrumented build
|
||||
|
||||
MSYS2's clang has no profile runtime, so the instrumented build uses llvm-mingw (unpack it under `toolchain\`):
|
||||
|
||||
```powershell
|
||||
$llvm = "toolchain\llvm-mingw-<date>\llvm-mingw-<date>-ucrt-x86_64"
|
||||
.venv\Scripts\python tools\build_game.py --version v143 --out build\v143-pgogen --toolchain $llvm --pgo gen
|
||||
```
|
||||
|
||||
Regenerate `generated\<ver>` first if it is older than the commit you train on. If `totk.exe` stops at start with
|
||||
`0xc000007b`, copy `libiconv-2.dll` from `C:\msys64\ucrt64\bin` next to it.
|
||||
|
||||
`totk.exe` writes `build\v143-pgogen\pgo\default_<id>.profraw` every minute and at exit. Each run of the same exe adds
|
||||
to that file.
|
||||
|
||||
### 2. Shaders
|
||||
|
||||
Copy your `totk.ini` into the build folder and prepare the shaders (`tools\prepare_shaders.py build\v143-pgogen
|
||||
--version v143`). Shader preparation runs `totk.exe` too, so move `pgo\*.profraw` out of the build folder afterwards:
|
||||
its counts describe shader building, not play.
|
||||
|
||||
Keep the default settings, with the GPU command recorder on (`vk_recorder = 1`, the default).
|
||||
|
||||
### 3. Training scenes
|
||||
|
||||
The instrumented build runs slower than a normal one. Count each scene's time by the game clock, not by the wall
|
||||
clock. Spend 90 to 120 seconds on each of the first four scenes, where most frames are spent, and about 60 seconds on
|
||||
each of the others, which cover code the first four do not reach.
|
||||
|
||||
| Place | What to do | Time |
|
||||
| --- | --- | --- |
|
||||
| Lookout Landing | Stand in the middle of the town and turn the camera slowly all the way round, then walk through the crowd. | 120 s |
|
||||
| Kakariko Village | The same: a slow camera turn, then walk between the houses. | 100 s |
|
||||
| A fight with a Battle Talus | Fight it with weapons and the bow. | 110 s |
|
||||
| The Depths, near a Lightroot | Turn the camera over the lit area, throw a Brightbloom Seed. | 90 s |
|
||||
| The Depths, away from any light | Walk and turn the camera in the dark. | 60 s |
|
||||
| The edge of a sky island | Run off the edge, skydive, open the paraglider. | 60 s |
|
||||
| Outdoors in the rain | Walk and look around while it rains. | 60 s |
|
||||
| A cave with Brightbloom plants | Walk in and fight the Moblins and Bokoblins inside. | 60 s |
|
||||
| Inside a shrine | Walk through the first rooms. | 60 s |
|
||||
| A Moblin camp | Fight several Moblins at once. | 60 s |
|
||||
| Next to Yunobo | Use his ability: roll him into something. | 60 s |
|
||||
| Hyrule Field, by the standing stone pillars | Run across the field, also backwards past the pillars. | 60 s |
|
||||
|
||||
Keep two scenes out of the training, so that you can measure on scenes the profile has not seen: Hyrule Field in tall
|
||||
grass, and a fight with Moblins and Zonai constructs at once.
|
||||
|
||||
With a save at a scene, `tools\run_game.py` plays it without you: `--save <backup> --script <input file> --seconds
|
||||
<game time>` ([agents/running.md](agents/running.md)). Without a save, play the scenes by hand.
|
||||
|
||||
### 4. Free play
|
||||
|
||||
Then play for 15 to 25 minutes and touch every kind of thing the game does, so that no common path is left without
|
||||
counts:
|
||||
|
||||
- Run, sprint, climb, swim and glide; ride a horse; jump from a tower and skydive.
|
||||
- Fight with one-handed and two-handed weapons, a spear and the bow; shield parry, flurry rush, fused arrows.
|
||||
- Use every ability: Ultrahand (build a vehicle with a fan and wheels and drive it), Fuse, Ascend, Recall, Autobuild.
|
||||
- Call the sages and use their abilities: Tulin, Riju, Sidon, Yunobo, Mineru.
|
||||
- Open the inventory, the map and the camera; cook at a pot; talk to people in a town.
|
||||
- Travel quickly to a few places, so that areas load; enter and leave a shrine and the Depths.
|
||||
- Play in daytime and at night, in clear weather and in rain or snow.
|
||||
|
||||
### 5. Merge
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\pgo.py merge build\v143-pgogen --version v143
|
||||
```
|
||||
|
||||
writes `build\pgo\pgo-v143.profdata` and its record. Merge right after the training, from the checkout and commit the
|
||||
instrumented build was made in: the record reads the `recomp/` tree from git and the header digests from the build's
|
||||
Ninja deps log.
|
||||
|
||||
Repeat steps 1 to 5 with `v121` for 1.2.1.
|
||||
|
||||
## Measure
|
||||
|
||||
Build the same code three ways in three folders with llvm-mingw, as the installer does: with the new profile, with
|
||||
the old one and with none. Prepare the shaders in each folder. Then run the held-out scenes and one trained scene
|
||||
in the order new, old, none, none, old, new, and compare the mean frame time over the same stretch of game time. One
|
||||
run of a fight can vary by up to 15% from the next, so a profile counts as better only when it wins both pairs.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Rules for taking a PR
|
||||
|
||||
## 1. What to review first
|
||||
|
||||
1. Fixes before features. A PR that fixes a bug, a glitch or a crash goes first; feature PRs wait.
|
||||
2. Order fixes by reach: how many players the bug hits and how badly. Windows and default settings first, rare
|
||||
hardware and unusual setups later.
|
||||
3. Scale the depth of the checks to the reach. A crash many players hit and anything on the frame path get the full
|
||||
checks. A rare case (a CPU without AVX, an unusual setup) gets the minimal fix, one targeted check and a short
|
||||
note. A crash fix on rare hardware is still worth taking: cut the checks around it, not the fix.
|
||||
4. Before testing, find out where the bug reproduces: OS, GPU vendor and generation, game version, settings. If the
|
||||
symptom cannot appear on the hardware at hand (an NVIDIA paging bug on an AMD machine), say so and do not run
|
||||
tests that cannot decide it.
|
||||
5. Check a feature PR against the designs in `docs/milestones/`. A feature with a design (mods, cheats) goes in as
|
||||
part of that design.
|
||||
6. Several PRs on one topic: compare them before taking either. Another PR may already do it right.
|
||||
|
||||
## 2. How to read the diff
|
||||
|
||||
- The author's claim is a hypothesis. Read the whole diff: races, wrong semantics, edge cases, leaks, lock order.
|
||||
- Check the behaviour against evidence: how yuzu, Ryujinx, LAS Recomp, SavageWindRecomp, nxnative and MacNativeGames
|
||||
do the same thing in `refs/` (`tools/fetch_refs.sh` recreates it); the Vulkan spec; hardware docs
|
||||
([docs/maxwell.md](../../docs/maxwell.md)); upstream issues.
|
||||
- Weigh two things for every change: its cost on the hot path, and how it fails (device lost, hang, stale data, weak
|
||||
or non-AVX CPUs, other GPU vendors, Wine/Proton).
|
||||
- Read the code a hypothesis depends on before running the game: the game's functions in Ghidra or
|
||||
`generated/<ver>`, our path in `runtime/`. A run confirms what the code could not rule out. It is one run, not a
|
||||
series of variants.
|
||||
- Order the work so the cheapest check that could refute the PR's idea runs first. Refuted: stop, write down why,
|
||||
no build, no benches.
|
||||
|
||||
## 3. What a PR needs
|
||||
|
||||
- **Proof.** The bug reproduced before the fix, the same repro after, both results side by side: a screenshot, a log
|
||||
line, a probe, a differential test. A fix without a reproduced failure is not proof. Where a repro is impossible
|
||||
on the hardware at hand, say so; the fix is then taken only on strong static evidence in the code. Where a repro is
|
||||
risky (device lost or a TDR can hang the machine), write the recipe and let the owner decide.
|
||||
- **Frame-path changes** get whole runs of the heavy scene with the switch off and on, at least two each
|
||||
([02-performance.md](02-performance.md)). Zero new slow frames is a hard bar.
|
||||
- **A switch.** A change on the hot path or in rendering sits behind an env or `totk.ini` switch, so a player can turn
|
||||
it off and we can compare.
|
||||
- **Nothing changes for players outside the feature.** The DLSS PRs touched shared code; they went in once every
|
||||
path that runs without DLSS was gated on DLSS being on.
|
||||
- **A fix for one setup keeps the default for everyone.** The PRs that raised the low memory block from 6 to 10 GiB for
|
||||
all players were rejected; the right shape is 6 GiB plus the extra where it is needed.
|
||||
- **Parts we cannot run** (Linux-only code, an HDR display, RTX-only paths) are gated so they cannot touch anyone
|
||||
else, or wait for a report from someone with that hardware. Note them as unrun.
|
||||
- **Project tests** from [03-correctness.md](03-correctness.md): lifter differential tests, probes, `--selftest`,
|
||||
`tools/check_content.py --staged`.
|
||||
- **Text the player sees** goes into the catalog: `runtime/ui_text.h` ids and `runtime/ui_text.c` entries in every
|
||||
language the menu has (English, Russian, Brazilian Portuguese, Simplified Chinese), and `installer/text/*.json` for
|
||||
the installer. No hardcoded sentences, no second string table.
|
||||
- **A new setting** is described in `totk.ini.example` and [docs/settings.md](../../docs/settings.md).
|
||||
- **Licences.** MIT, BSD, ISC, zlib and Apache-2.0 code may be copied, with its header and a row in
|
||||
`THIRD_PARTY_NOTICES.md`. yuzu, suyu and other GPL code is read for behaviour only; we write our own.
|
||||
|
||||
## 4. Reasons to reject or rework
|
||||
|
||||
- No test or repro for a change that is not trivial.
|
||||
- Slow: a state copy per draw, a global atomic counter on the hot path, synchronous file I/O or a GPU allocation on a
|
||||
game or render thread, new slow frames.
|
||||
- Changes behaviour for everyone to serve one setup, or is on by default without a run in a scene where it acts.
|
||||
- "Fixes" a crash by turning a feature off for a vendor. A crash that goes away with a feature off points at the bug;
|
||||
the feature gets fixed.
|
||||
- Breaks another feature (a preview that loses anti-aliasing).
|
||||
- Fake features: labelled FSR4 or frame generation, and not that.
|
||||
- Wrong measurements: percentiles over window means, counters over the wrong set.
|
||||
- Generated C edited by hand instead of the generator (`recomp/`).
|
||||
- A disallowed licence or pasted GPL code.
|
||||
|
||||
## 5. Finishing a good PR
|
||||
|
||||
A good PR that misses the project's scheme (hardcoded text, conflicts, layout, comment style) is brought to it while
|
||||
merging, not sent back to the author for another round. When only part of a PR is good, take that part and note what
|
||||
was left out and why.
|
||||
|
||||
A bug found outside the PR under review is not fixed on the spot: search the open PRs on that topic, report the bug
|
||||
with them, and let the owner decide.
|
||||
|
||||
## 6. Merging, branches, commits
|
||||
|
||||
- Each PR or wave of PRs gets its own branch `task/<tag>` from a fresh `dev` and its own build dir. It merges into
|
||||
`dev`.
|
||||
- Once the review is done, the symptom is gone on one paired shot and the probes pass, merge into `dev` and hand the
|
||||
build to players: they test in game faster and wider than our routes. The wider checks (other routes, validation,
|
||||
fps cost) follow as a separate diff. Speed never skips the review of the diff itself.
|
||||
- `main` only with the owner's explicit yes. Push only when told to.
|
||||
- A commit message is a plain English sentence, no `feat:`/`fix:` prefixes.
|
||||
- The version lives in the repo-root `VERSION` file; the owner decides when it moves.
|
||||
- After a merge, record which PRs went in, what was reworked, what was rejected and why. The reason for a rejection
|
||||
goes into the PR as a plain English comment.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Performance: no lost frames on weak hardware
|
||||
|
||||
## Who we tune for
|
||||
|
||||
- The goal is many more fps on modest hardware, and Steam Deck later (Zen 2 4c/8t APU, shared LPDDR5, the exe runs
|
||||
through Proton/Wine).
|
||||
- The developer's machine (Ryzen 9 7900X + RX 9070) is fast and its numbers do not carry over. A laptop with an
|
||||
i7-9750H and a GTX 1050 Ti is a sample of a weak machine, not the target to fit constants to.
|
||||
- What weak hardware measured:
|
||||
- GTX 1050 Ti (4 GiB, 214 MiB BAR, no ReBAR): the GPU read imported host pools over PCIe, 110-120 ms a frame
|
||||
against 4.3 ms on the RX 9070. The VRAM mirror brought it to 10-11 ms; that machine is now CPU-bound at 25-45 fps,
|
||||
replay 25-30 ms a frame.
|
||||
- The same write-watch scan costs about 3x the CPU time there.
|
||||
- NVIDIA and desktop AMD GPUs have no ASTC: textures decode in a compute shader. CPU decodes before that made
|
||||
0.4-0.7 s load frames on the 1050 Ti.
|
||||
- Wine emulates `GetWriteWatch` with `mprotect` and a fault per first write. Anything relying on cheap OS memory
|
||||
tricks (write-watch, `VirtualProtect` freezing, fault handlers) is checked against Wine's implementation.
|
||||
- The port uses about half of even a weak CPU, so the frame's critical path is the limit, not throughput. TotK keys
|
||||
per-core data by `GetCurrentProcessorNumber` without locks, so the game's threads share three virtual cores on any
|
||||
host. In a fight the main thread and five job workers keep 2.6 of the 3 busy: guest code speed sets the fps.
|
||||
- Perf work builds and measures 1.4.3. A 1.2.1 build is for a change about to merge with a large gain.
|
||||
|
||||
## Gain ceiling first
|
||||
|
||||
Before writing or taking a perf change, compute its ceiling: the share of the code it removes times the share of the
|
||||
frame that code holds on the critical path, on weak hardware. A ceiling of a few percent or less is not worth it.
|
||||
|
||||
- Use the critical path (`TOTK_CRIT` + `tools/crit_path.py`, heavy frames only), not one thread's profile. On the weak
|
||||
laptop the submit thread is 55% replay, yet the critical path of heavy frames is 67% guest code, 15%
|
||||
`nvnSyncWait`, 13% core waits and wake latency.
|
||||
- Changes that can move the frame by 10% or more are usually design changes. Polishing a hot function from a flat
|
||||
profile almost always gives zero.
|
||||
|
||||
## A neutral safe change stays
|
||||
|
||||
An optimization that measures neutral on the fast machine stays on by default when:
|
||||
|
||||
- upstream (`refs/`, other ports) reports a gain for it, especially on weak CPUs or GPUs;
|
||||
- the static read leaves no hazard, and the probe or differential test passes;
|
||||
- whole heavy-scene runs off/on x2 show no fps loss and no new slow frames;
|
||||
- a switch turns it off.
|
||||
|
||||
"No gain here" is not a reason to drop a safe change with upstream evidence. A regression or a hazard nobody ruled
|
||||
out is.
|
||||
|
||||
## How to measure
|
||||
|
||||
- **By game time, never by frame counts.** Run length, analysis windows, A/B blocks and input scripts go by the world
|
||||
clock (`--seconds`, `TOTK_CRIT=w<lo>-<hi>`, `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, `TOTK_SHOTS=w<lo>-<hi>:<step>`). The
|
||||
same frame count at another fps is another scene.
|
||||
- **Check a scripted run reached the world.** Title menus on a slow boot eat button presses; confirm the run is in the
|
||||
world before trusting its numbers.
|
||||
- **Whole runs off and on, two each at least.** An in-run A/B on a moving scene lies: the A and B blocks land on the
|
||||
same game seconds every run, so a content difference repeats. One in-run A/B showed "-11% replay"; whole runs gave
|
||||
5.84/5.73 against 5.76/5.73 ms, nothing. In-run A/B only on a static scene (standing still).
|
||||
- **Spread.** One script on near-identical builds gave 1% lows from 31.5 to 50 fps. Compare medians of three runs,
|
||||
never one run against one run.
|
||||
- **A quiet machine.** `tools/run_game.py --quiet` takes the machine-wide lock; no other `totk.exe`, build or test runs
|
||||
meanwhile. One game at a time.
|
||||
- **A warm shader cache, prepared properly.** Shader and pipeline caches are not copied between build dirs:
|
||||
`totk.exe --prepare-shaders` or `--warm-shaders` in that dir. The AMD driver cache is keyed to the exe path and
|
||||
limited in size: preparing another build dir can evict this one's libraries (40-220 ms per new effect in play).
|
||||
- **A heavy scene.** A fight with a large enemy and a crowd, fps limit at the maximum (`TOTK_FPS_LIMIT=360`). Compare
|
||||
one shot with the scene's reference before trusting numbers.
|
||||
- **No base runs by habit.** Run the candidate alone and judge it against the targets: max fps, zero slow frames
|
||||
(`perf: slow frame`), no black or flat frames, no new validation errors. Glitches are found by looking at shots and
|
||||
frame dumps, not by diffing metrics. Run a base build only when a specific question needs a side-by-side.
|
||||
|
||||
| What | How |
|
||||
|---|---|
|
||||
| fps, worst frame, render-thread buckets, GPU busy, VRAM | `--env TOTK_TRACE=perf,sched`, then `python tools/perf_summary.py <log> <lo> <hi>` |
|
||||
| the frame's critical path | `TOTK_CRIT=w<lo>-<hi>` + `TOTK_CRIT_OUT=<path>`, then `python tools/crit_path.py <file>` |
|
||||
| sampling profiler | `TOTK_PROFILE=<path>` + `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, then `tools/prof_report.py <profile> <totk.map>` |
|
||||
| A/B on a static scene | `TOTK_AB=<seconds>`, then `tools/ab_summary.py --skip <seconds>` |
|
||||
| VRAM pressure on a large card | `TOTK_VRAM_LIMIT_MIB=<MiB>` caps the budget the port sees |
|
||||
|
||||
## What the hot path must not do (Windows host)
|
||||
|
||||
- **Files.** The game threads (main, job workers) and the render thread never open, read, write or flush a file, and
|
||||
never wait for a thread that does. Opening a 20-byte file in a 139k-file folder: 0.1 ms idle, 11-40 ms in play while
|
||||
the translator, the pipeline cache and the save wrote to the same volume. A save's `FlushFileBuffers` and
|
||||
`MOVEFILE_WRITE_THROUGH`: 93-135 ms. Saving the pipeline cache under its lock on the present thread froze the replay
|
||||
every 20 s. File I/O goes to a background thread; results come back in memory.
|
||||
- **Waits.** Short waits spin on the counter; waits over 1 ms use the high-resolution timer. A waitable timer wakes
|
||||
0.3-0.5 ms late; the SDK's 20 us sleeps took 460-515 us. A 1 ms timer thread does not help: it gave more slow frames.
|
||||
- **GPU allocations.** VMA's `vkAllocateMemory` of a 256 MiB block under its lock: 8-23 ms. A new block on the render
|
||||
thread is a hitch.
|
||||
- **128-bit division.** `(unsigned __int128)t * num / den` compiles to the library call `__udivti3`; counter
|
||||
conversions that way took a tenth of the game threads' time in a fight. Use the precomputed scale (`platform.h`
|
||||
`PlatScale`).
|
||||
- **Per-draw copies and counters.** A pipeline-state copy per draw, a global atomic counter, a scan of every page each
|
||||
frame: on a weak CPU each is frames.
|
||||
- **Memory kept per frame.** Anything allocated per texture initialization or per frame must be freed. One copy kept
|
||||
per texture init grew host memory by ~44 MiB a game minute until a commit failure after hours (#670). Check private
|
||||
memory over a few idle minutes for a PR that stores per-object state.
|
||||
- **Scheduling.** Any timing or scheduling change is checked for slow frames. A preempt check at every host call made
|
||||
one bug rarer but added 50-75 ms CPU hitches and was reverted.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Correctness: checks that keep behaviour intact
|
||||
|
||||
## Lifter and generated C
|
||||
|
||||
- Generated C is never edited by hand: change the generator in `recomp/`.
|
||||
- `recomp/layout.py` alone decides module bases.
|
||||
- Every change to the lifter or the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) passes the differential tests
|
||||
against Unicorn, all run with `.venv`:
|
||||
|
||||
| Test | Checks |
|
||||
|---|---|
|
||||
| `tests\diff\lift.py` | instructions sampled from the modules; `--coverage` lists the words the lifter rejects |
|
||||
| `tests\diff\simd.py` | the whole FP/SIMD encoding space |
|
||||
| `tests\diff\atomic.py` | exclusive load/store and ARMv8.1 atomics |
|
||||
| `tests\diff\func.py` | whole functions as the generator emits them: loops, jump tables, calls |
|
||||
| `tests\flags_check.py` | the function-local flag helpers against the packed ones |
|
||||
| `tests\excl_mt.py` | the exclusive monitor under contention and ABA, on several threads |
|
||||
| `tests\jumptables_check.py` | `recomp/jumptables.py` against Unicorn: 0 disagreements on 1.2.1 and 1.4.3 |
|
||||
|
||||
- A `--cpu v2` build (no AVX2) passes the same tests with `--cflags=-march=x86-64-v2`. Players without AVX exist.
|
||||
- A compile error in generated C on a line the generator could not have written is a bit flip on disk or in memory.
|
||||
Regenerate and compare hashes; leave `recomp/` alone.
|
||||
|
||||
## Runtime
|
||||
|
||||
- From the build dir: `totk.exe --selftest`, `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex`, `--probe-shader`
|
||||
pass.
|
||||
- Settings: `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`.
|
||||
- Button prompts: `.venv-py314\Scripts\python tests\romfs_prompts_test.py`. Build dirs carry only `romfs-patches`; to
|
||||
test PC or Xbox prompts in game, generate the overlay first:
|
||||
`.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts pc --out build\<dir>\romfs-patches-pc`,
|
||||
then restart the game.
|
||||
- Installer: `tests\installer_gui_test.py` (event model, form, catalogs, cancel, display scale).
|
||||
- Shaders: `tools\shader_census.py --version <ver> --translate`, `tools\xlate_smoke.py`. A translator patch
|
||||
(`tools/xlate/patches/`) needs `build/xlate` rebuilt and changes shader hashes: anything keyed to a shader hash
|
||||
breaks with it.
|
||||
|
||||
## GPU portability
|
||||
|
||||
- New Vulkan code queries the feature, extension, format or limit it uses and keeps a working path or a logged
|
||||
fallback without it (provoking vertex and subgroup size pinning in `vk_pipeline.c` are examples).
|
||||
- Never hardcode what one GPU reports: alignments, subgroup size (RDNA4 may run wave64, old AMD Vega is wave64 only),
|
||||
depth formats, `maxPushDescriptors`.
|
||||
- One queue: every submit and present takes `vk_queue_lock`. Presenting from its own thread on a second queue gave
|
||||
device lost on GTX 10-series in another port.
|
||||
- RDNA1/2 bugs do not reproduce on RDNA4 (another driver branch); NVIDIA bugs do not reproduce on AMD. A Ryzen 7000
|
||||
iGPU is RDNA2 and runs the RDNA1/2 driver branch: a cheap testbed for those reports.
|
||||
- Validation: a `--validation` run in the world shows no new messages.
|
||||
|
||||
## Maxwell / NVN behaviour
|
||||
|
||||
The port must do what the Switch GPU does, not what Vulkan does by default. The full list with sources:
|
||||
[docs/maxwell.md](../../docs/maxwell.md), section 7. The traps a PR breaks most easily:
|
||||
|
||||
- Provoking vertex: Vulkan takes the first vertex, NVN the last.
|
||||
- Unwritten vertex outputs read (0, 0, 0, 1).
|
||||
- A colour target outside the fragment shader's output map keeps its texel (write mask 0). AMD writes garbage there.
|
||||
- Feedback loops: the G-buffer pass reads its own targets. It needs the feedback layout and a by-region barrier.
|
||||
- Depth bias argument order, depth mode, depth clamp: checked against deko3d.
|
||||
- Report timestamps: the engine's GPU time comes from them, and dynamic resolution and game speed follow it.
|
||||
|
||||
## Timers and the real-time step
|
||||
|
||||
- With `realtime` set, the game steps by a measured ratio, not whole 30 Hz units. A countdown remainder in
|
||||
(0, FLT_EPSILON] is never stepped again, and a consumer testing `> 0` sees it active forever. That was "weapons
|
||||
cannot be changed after a Fuse until a reload" (#18).
|
||||
- Symptoms of this class: stuck until a reload or teleport, locked after an action, works sometimes.
|
||||
- A PR touching the frame step, the pacer or the fps limit is checked for this class.
|
||||
|
||||
## Threads and scheduling
|
||||
|
||||
- ARMv8 `stlr` then `ldar` is ordered (RCsc); C11 release/acquire lets x86 reorder that store and load. Changes to
|
||||
the lifter's barriers or atomics pass `tests\diff\atomic.py` and `tests\excl_mt.py`.
|
||||
- A scheduler change is judged from one `TOTK_TRACE=sched` run by how long a ready, better thread waits for a core it
|
||||
may use, plus the slow-frame count. Not by statistics over many runs.
|
||||
- A crash that looks like a race: rule out the save first. One reported crash was the save itself; it crashed the
|
||||
same way on a real Switch.
|
||||
|
||||
## Files and caches
|
||||
|
||||
- A crash leaves zero-filled files. Caches are validated on load, not trusted because they exist.
|
||||
- The pipeline cache is dropped when vendor, device or `pipelineCacheUUID` change.
|
||||
- Unicode paths: W APIs with UTF-8 to UTF-16 conversion.
|
||||
- A failure that happens once and does not come back: rerun before debugging it as a code bug. Broken caches and bit
|
||||
flips happen.
|
||||
|
||||
## Game content boundary
|
||||
|
||||
- The repository is public. No game files, and nothing extracted or generated outside `build/` and `generated/`.
|
||||
- Keys never appear in logs, commands, files, issues or comments.
|
||||
- Every file the generator emits carries the `GENERATED_MARKER` defined in `tools/check_content.py`.
|
||||
- `python tools/check_content.py --staged` before every commit. CI runs the same check.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Knowledge base
|
||||
|
||||
Before investigating a symptom, or trusting a PR that "fixes" it, look for it here. Search by what the player sees
|
||||
(black, magenta, flicker, slow motion, missing, stale, streaks) and by the object (sky, grass, shadow, fog, HUD).
|
||||
|
||||
## Invariants of recent changes
|
||||
|
||||
A PR that touches these areas must keep them.
|
||||
|
||||
- **Queue order.** When anything is submitted, every earlier part is already on the queue. Present, uploads, readbacks
|
||||
from guest threads and the `vkDeviceWaitIdle` sites rely on it. The recorder thread now begins, ends and submits
|
||||
the parts it records; the submitting thread still waits for that submission, so the order holds.
|
||||
`TOTK_REC_END=0` restores the old split.
|
||||
- **GPU-written memory is never frozen.** Storage-buffer stores, copies and fills land in imported guest memory
|
||||
without a fault or a write-watch bit, so a region only the GPU writes looks idle. Such regions are never frozen
|
||||
read-only, and a frozen region is thawed before a GPU write. LAS Recomp never changes the protection of imported
|
||||
memory because of NVIDIA. A suspected cause of NVIDIA "write invalid" device faults (#788, #802), not proven.
|
||||
- **VRAM pressure.** Under pressure, released textures whose storage the game has given to another texture go first:
|
||||
30 s after release and once no finalized slot has bound them for 600 submits. Clean mirrored entries wait 5 s.
|
||||
Both stop once use is back under the line. A PR touching texture lifetime is tested with `TOTK_VRAM_LIMIT_MIB`.
|
||||
- **Texture pins are freed.** Each pin counts the recording copies naming it and is freed 1200 submits after its last
|
||||
use unless the replay holds it as a render target. Before that, pins leaked ~44 MiB a game minute (#670).
|
||||
|
||||
## Fixed bug classes
|
||||
|
||||
If a PR touches this code, confirm the fix still holds.
|
||||
|
||||
| Symptom | Cause | Where the fix is |
|
||||
|---|---|---|
|
||||
| lime silhouettes in grass; rectangles in the sky | a target outside the output map must keep its texel; zero-filling it wiped the half-res depth | `vk_pipeline.c` `target_wmask` |
|
||||
| stale grass maps in the far terrain for a second after a hit | write-back of a texture the CPU had rewritten | `vk_texture.c` `write_back` |
|
||||
| cut grass stays standing; fire does not spread (#132) | CPU reads of GPU-rendered textures without an invalidate | `vk_texture.c` `vk_texture_write_back_small`, `nvn_ext.c` `nvn_pool_invalidate` |
|
||||
| cloud and fog volumes empty for the session | a one-shot draw skipped instead of waiting for its pipeline | `vk_exec_draw.c` `prepare_draw` |
|
||||
| tile rectangles | storage buffers bound by Ryujinx first-use order, not the c0 slot | `vk_exec_draw.c` `ssbo_slot` |
|
||||
| black dialog background and loading map | 3D targets rejected, the grading LUT never written | `vk_exec.c` `resolve_target` |
|
||||
| streaks on cliffs | primitive restart ignored | `vk_exec_draw.c` `restart_for` |
|
||||
| streaky map screen | the newest texture picked at shared storage | `vk_texture.c` `vk_texture_by_id` |
|
||||
| dark 16x16 blocks on the ground, sage spirit not drawn | G-buffer feedback loop | `vk_exec_draw.c`, `TOTK_FEEDBACK` |
|
||||
| magenta cave walls, brown sky | CPU-filled textures without a flush | write-watched mirrors, `vk_memory.c`, `vk_texture.c` |
|
||||
| black 3D scene | deferred texture binds | `nvn_api_cmd.c` `Bind*Deferred` |
|
||||
| HUD prompt boxes, black stamina wheel | `nvnTextureCompare` ignored the channel mapping | `nvn_api_obj.c` |
|
||||
| crash on world load | `GetCommandMemoryUsed` did not follow the driver's sizes; the game sizes model heaps from it | `nvn_api_cmd.c` |
|
||||
| 20 fps | a yield back-off starting before 2 ms | `kernel.c` SVC 0x0B: no back-off |
|
||||
| weapons locked after a Fuse (#18) | timer remainder in (0, FLT_EPSILON] with the real-time step | patch under the `realtime` setting |
|
||||
| magenta/blue garbage or device lost on RDNA1/2 (#5) | writes into 3D BC block views | in `dev` |
|
||||
| black window after a driver update | a pipeline cache from another driver | `vk_pipeline.c` drops the cache by UUID |
|
||||
|
||||
## Known open risks
|
||||
|
||||
A PR touching these needs stronger proof.
|
||||
|
||||
- Buffer textures are not replayed and bind a stand-in: light and decal lists may read 0.
|
||||
- New GPU-only colour images start undefined; feedback effects may show garbage.
|
||||
- Clears with a partial channel or stencil mask are skipped (`W_CLEAR_MASK`, `W_STENCIL_MASK`).
|
||||
- 8-bit indices are widened on the CPU at record time; a copy in the same submission gives stale indices.
|
||||
- `UpdateUniformBuffer` is a copy between full barriers and ends the render pass (fps outdoors).
|
||||
- Report timestamps carry host time at fence completion: the engine's GPU time is wrong.
|
||||
- The game's SVC-free spin loops burn a host core (843 in `main`).
|
||||
- Images aliased onto one GPU-only pool allocation do not see each other's data on AMD.
|
||||
|
||||
## Past PR decisions
|
||||
|
||||
| PR | Decision | Why |
|
||||
|---|---|---|
|
||||
| #690 shrine snapshots | rejected | a pass restart per copy, a `VkPipeState` copy per draw, broken `bind_sig` skip; fps loss |
|
||||
| #674 | rejected | a global STXR counter on the hot path |
|
||||
| #660 | rejected | the preview loses anti-aliasing |
|
||||
| #713, #576 | rejected | low memory block 10 GiB for everyone; should be 6 GiB plus the extra where needed |
|
||||
| #573 | rejected | dynamic resolution off by default for everyone, the 1.4.3 site never run |
|
||||
| #568, #569 | rejected | fake FSR4 and frame generation |
|
||||
| #487 | rejected | wrong metrics: percentiles over window means, counters over the wrong set |
|
||||
| #451, #380 | rejected | not painless to take |
|
||||
| #566, #594, #534 DLSS | taken with changes | #594's shared paths gated on DLSS being on; nothing changes without DLSS |
|
||||
| #489 | partly taken | the runtime part checked by a frame dump; the installer's DLL download was the owner's call, its pinned hash re-checked |
|
||||
| #519, #582 amiibo | rewritten by us | took the data-format fixes; left out the features and the Linux event rework |
|
||||
| #483 vs #585 | #585's mapping kept | #483's stick glyph mapping was rewritten unasked while #585 already had it right |
|
||||
| #528 rain | taken after a check | code review fine, merged only after a run on a rain save |
|
||||
| #767 | taken with our fix | the default was skipped with `prompts = xbox|pc` when those folders did not exist |
|
||||
| #625 frame queue | taken, off by default | a new frame queue, one field run with it on |
|
||||
| #743 UB overlay | waiting | +25% in a Depths fight on an RTX 4080; needs review, fight runs x2 and shots on AMD |
|
||||
|
||||
## Our mistakes, not to repeat
|
||||
|
||||
- **"-11%" was an A/B artifact** of a moving scene. Whole runs off/on showed zero.
|
||||
- **Micro-optimizations from a flat profile** (1-3% of samples each) gave 26.2 against 26.4 ms on the weak laptop:
|
||||
nothing. Nobody had computed a ceiling.
|
||||
- **A scheduler patch merged on run statistics** added 50-75 ms hitches. Slow frames are a hard bar.
|
||||
- **Ten game runs on lock theories** where one Ghidra read of the game's code narrowed it at once.
|
||||
- **45 minutes of builds and benches** on an idea its own first thread census had refuted: the game pins 53 of 56
|
||||
threads itself. The cheapest refuting check goes first; when it refutes, stop and write the idea down.
|
||||
- **Tests of a bug that could not appear on the hardware at hand** (NVIDIA paging on an AMD machine): 245k tokens and
|
||||
37 minutes for nothing. Ask first where the bug reproduces.
|
||||
- **Turning the recorder off for a vendor** instead of finding its bug. Stopped: the feature gets fixed, not silenced.
|
||||
- **Bisecting code when every build crashed the same way at once.** The cause was a bad page of a game file in the OS
|
||||
file cache. When all builds fail identically, suspect inputs and caches first.
|
||||
- **A duplicate issue closed with a bare "Same as #N"** lost the logs and links. Move everything useful to the issue
|
||||
that stays open first: hardware, driver, version, when it happens, workarounds, links from comments.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Per-PR checklist
|
||||
|
||||
## Before reading
|
||||
|
||||
- [ ] Fix or feature? Fixes first, ordered by how many players they reach.
|
||||
- [ ] Where does the bug reproduce: OS, GPU vendor and generation, game version, settings? Can the hardware at hand
|
||||
show it?
|
||||
- [ ] Is the symptom in [04-knowledge.md](04-knowledge.md) or `docs/maxwell.md`?
|
||||
- [ ] Another open PR on the same topic? Compare them.
|
||||
- [ ] Feature: is there a design in `docs/milestones/`?
|
||||
|
||||
## Reading the diff
|
||||
|
||||
- [ ] The whole diff read, not the description: races, edge cases, lock order, leaks.
|
||||
- [ ] Behaviour checked against `refs/` (Ryujinx, yuzu, LAS, SavageWind, nxnative, MacNativeGames), the Vulkan spec,
|
||||
`docs/maxwell.md`.
|
||||
- [ ] Hot-path cost: no file I/O, GPU allocations, waits, per-draw copies or global atomics on game or render threads.
|
||||
- [ ] No memory kept per frame or per object without a free.
|
||||
- [ ] Failure modes: device lost, hang, stale data, CPUs without AVX, NVIDIA/Intel/old AMD, Wine/Proton.
|
||||
- [ ] New Vulkan code queries the feature and has a fallback; nothing hardcoded for one GPU.
|
||||
- [ ] Invariants kept: queue order, GPU-written memory never frozen, VRAM pressure path, pins freed.
|
||||
- [ ] Players without the feature see no change. The default for everyone stays for one setup's sake.
|
||||
- [ ] New behaviour behind a switch. A new setting described in `totk.ini.example` and `docs/settings.md`.
|
||||
- [ ] Player-visible text in the catalogs, in every menu language (`runtime/ui_text.c`, `installer/text/*.json`).
|
||||
- [ ] Copied code: allowed licence, header kept, row in `THIRD_PARTY_NOTICES.md`. No GPL code.
|
||||
- [ ] Generated C not edited by hand.
|
||||
- [ ] Parts we cannot run (Linux, HDR, RTX-only) gated away from everyone else and noted as unrun.
|
||||
|
||||
## Proof
|
||||
|
||||
- [ ] Repro before the fix and the same repro after, side by side.
|
||||
- [ ] Lifter or CPU headers: differential tests against Unicorn; `jumptables_check.py` 0 disagreements.
|
||||
- [ ] Runtime: `--selftest` and the four probes pass.
|
||||
- [ ] Settings: `--probe-cfg` and `tests\settings_doc_check.py`. Prompts: `romfs_prompts_test.py`, overlay generated
|
||||
for a game check.
|
||||
- [ ] Frame path: whole heavy-scene runs off/on, two each at least, by game time, quiet machine, warm cache prepared
|
||||
in that build dir. No fps loss, zero new slow frames.
|
||||
- [ ] Rendering: shots or a frame dump in a scene where the change acts; no black or flat frames; no new validation
|
||||
errors.
|
||||
- [ ] Perf PR: gain ceiling computed for weak hardware.
|
||||
|
||||
## Decision
|
||||
|
||||
- [ ] Taken, partly taken or rejected, with the reason written down and posted to the PR.
|
||||
- [ ] What missed the project's scheme brought to it while merging.
|
||||
- [ ] Bugs found outside the PR reported, not fixed on the spot.
|
||||
- [ ] Merged into `dev` from `task/<tag>`. `main` only with the owner's yes.
|
||||
- [ ] `python tools/check_content.py --staged` before the commit.
|
||||
- [ ] Commit message: a plain English sentence, no `feat:`/`fix:`.
|
||||
@@ -0,0 +1,32 @@
|
||||
# PR review kit
|
||||
|
||||
Rules and background for a tester or reviewer who takes community PRs into `dev`. One goal: no PR breaks the
|
||||
game's behaviour or costs frames on weak hardware.
|
||||
|
||||
## The short version
|
||||
|
||||
1. A PR description and "works for me" prove nothing. Read the diff yourself and check it against `refs/`, the
|
||||
Vulkan spec and upstream issues.
|
||||
2. Almost every change needs a test: a repro before and after, a probe, a differential test, or whole runs with the
|
||||
switch off and on.
|
||||
3. The developer's machine is fast; players run weak machines. "Neutral here" says nothing about them, in either
|
||||
direction.
|
||||
4. A player without the problem sees no change: the new path sits behind a switch and the old path stays as it was.
|
||||
5. Nothing goes into `main` without the project owner's explicit yes. Accepted work lives in `dev` first.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| [01-rules.md](01-rules.md) | triage order, what a PR needs, reasons to reject, merging and commits |
|
||||
| [02-performance.md](02-performance.md) | target hardware, gain ceiling, how to measure, what the hot path must not do |
|
||||
| [03-correctness.md](03-correctness.md) | lifter, tests and probes, GPU portability, Maxwell behaviour, timers, threads, game content |
|
||||
| [04-knowledge.md](04-knowledge.md) | invariants of recent changes, fixed bug classes, open risks, past PR decisions, our mistakes |
|
||||
| [05-checklist.md](05-checklist.md) | the per-PR checklist |
|
||||
|
||||
The public docs this kit builds on: [docs/architecture.md](../../docs/architecture.md),
|
||||
[docs/building.md](../../docs/building.md) ("Checks"), [docs/maxwell.md](../../docs/maxwell.md),
|
||||
[docs/settings.md](../../docs/settings.md), [docs/tester-guide.md](../../docs/tester-guide.md), `AGENTS.md`.
|
||||
Working with a coding agent: give it this folder and `AGENTS.md` before the first review.
|
||||
|
||||
State as of 2026-11-06. The code and `docs/` win over this text where they differ.
|
||||
@@ -0,0 +1,215 @@
|
||||
# Settings reference
|
||||
|
||||
`totk.ini.example` describes the settings for players. This page lists the rest: diagnostics, test hooks, paths and experiments. Set one as `name = value` in `totk.ini`, or as the environment variable `TOTK_NAME`, which wins over the file. Remove the line to go back to the default.
|
||||
|
||||
## Safety
|
||||
|
||||
- **Safe**: changes only what the row says, with no effect on stability or saves.
|
||||
- **Caution**: can slow the game, grow files, or change the picture, and undoes cleanly when removed.
|
||||
- **Risky**: can crash or hang the game, or break the picture or game logic, while it is set.
|
||||
- **Save risk**: can damage or lose saves, or point the game at other game or save files.
|
||||
- **Unknown**: the code did not settle it.
|
||||
|
||||
Many rows below are developer tools that only act together with another setting or a command-line mode (for example a trace mode or the DLSS path). The row says so where the code does. The rows come from reading the code, and most effects have not been checked in a run.
|
||||
|
||||
`tests/settings_doc_check.py` fails when a name in `known[]` (`runtime/cfg.c`) is described neither here nor in `totk.ini.example`, or when a default here differs from the code.
|
||||
|
||||
## Diagnostics and logging
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `astc_time` | `1` times the GPU dispatches of each ASTC texture decode and logs the time per block footprint and the largest call. | Adds GPU timestamp queries around every decode; if the queue has no timestamps or no `hostQueryReset`, the log says the decodes are not timed and nothing is measured. | Caution |
|
||||
| `bind_stats` | `1` counts bindings and descriptors per draw of each stage and how much of the bound state changes between draws. | Adds per-draw bookkeeping to the render thread and extra log output; frames get slower. | Caution |
|
||||
| `contact_debug` | `1` draws the contact-shadow factor in grey in place of the picture. | The frame shows a grey debug image while it is set; has no effect when contact shadows are off. | Caution |
|
||||
| `core_diag` | `1` records why ready guest threads wait for a virtual core and for how long, with a histogram every 60 presents. | Turns on host-call and SVC depth tracking and writes many log lines (one per present over 25 ms); frames get slower. | Caution |
|
||||
| `menu_cursor_log` | `1` logs what the menu cursor code reads at every screen change. | Only adds log lines. | Safe |
|
||||
| `perf_slow_ms` (default `50`) | Frames at least this many milliseconds long get a `perf: slow frame` log line with the time blocked per cause. | Changes how many frames are logged; a small value fills the log. | Safe |
|
||||
| `replay_check` | `1` reads every texture read-back a second time after a full drain of the replay queue and compares the two. | Stalls the replay queue at every read-back, so frames get much slower; mismatches are only counted and logged. | Caution |
|
||||
| `resolve_check` | `1` compares every cached pool lookup with the locked lookup and stops on a difference. | Slows every pool lookup; the game stops if the two ever differ. | Caution |
|
||||
| `speed_probe` | `1` adds the player's position and ground speed to the frame-rate line that `TOTK_TRACE=perf` prints every 120 presents. | Only adds log text; has no effect without `TOTK_TRACE=perf`. | Safe |
|
||||
|
||||
## Profiling and measurement
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `ab` | Splits game time into blocks of this many seconds in the order A B B A and lets settings written as `ab` take effect only in the B blocks. | Settings that accept `ab` flip on and off every few seconds, so frame times and the picture change with the block; without such a setting it does nothing. | Caution |
|
||||
| `crit` | `<first>-<last>` frame range (with `TOTK_TRACE=sched`) that records every state change and wake of the guest threads, then writes a file at the last frame. | Recording adds overhead to every thread switch in the range and writes one file; `tools/crit_path.py` reads it. | Caution |
|
||||
| `crit_out` (default `crit.bin`) | Path of the file that `crit` writes. | Only moves the file; nothing is written without `crit`. | Safe |
|
||||
| `profile` | Path of a sampling profile file; the profiler suspends threads to unwind their stacks. | Frames get slower while it samples and the profile file grows; `tools/prof_report.py` reads it. | Caution |
|
||||
| `profile_chains` | `1` keeps call chains of every thread in the profile. | Larger profile file and more work per sample; has no effect without `profile`. | Caution |
|
||||
| `profile_frames` | `<lo>-<hi>` samples only while presents lo to hi-1 are pending; `w<lo>-<hi>` uses world-clock seconds. | Limits the profile to a window; has no effect without `profile`. | Safe |
|
||||
| `profile_main` | `1` samples only the guest main thread. | Narrows the profile; has no effect without `profile`. | Safe |
|
||||
| `profile_render` | `1` samples only the render thread and keeps its call chains. | Narrows the profile and makes it larger; has no effect without `profile`. | Caution |
|
||||
|
||||
## Capture and frame dumps
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `dump` | Frame number to dump: every render pass target and storage image of that frame is copied out as PNG, with `passes.txt` and `images.txt`. | Writes many PNG files and shader hashes to disk and slows the dumped frame; size is capped by the dump code. | Caution |
|
||||
| `dump_buf` | `<addr>:<size>[:<off>:<stride>:<w>:<h>]` snapshots one GPU buffer range after every logged pass of a dump. | Writes `buf_<pass>.bin` files and `bufs.txt`; has no effect without `dump`. | Caution |
|
||||
| `dump_dir` (default `dump`) | Folder that receives the frame dump files. | Only moves the output. | Safe |
|
||||
| `dump_log` | Frame number; logs the frames from there up to the dumped frame into `passes.txt`, with PNGs only of targets up to 64K texels. | Writes a longer `passes.txt` and more PNGs; has no effect without `dump`. | Caution |
|
||||
| `flight_at` | Comma-separated frame numbers that trigger a flight-recorder dump. | Writes a dump folder when each frame is reached; has no effect unless the flight recorder (`TOTK_FLIGHT=1`) is on. | Caution |
|
||||
| `flight_dir` | Folder for flight-recorder dumps; the default is `flight` beside the folder of `totk.exe`. | Only moves the output. | Safe |
|
||||
| `flight_frames` (default `600`) | Number of presents the flight recorder keeps. | A larger ring keeps more history and more thumbnails in memory; has no effect unless `TOTK_FLIGHT=1`. | Caution |
|
||||
| `flight_thumbs` (default `1`) | `0` turns off the 320x180 thumbnail the flight recorder saves for each present. | Dumps hold only `frames.tsv`; thumbnails cost a GPU blit per present when on. | Safe |
|
||||
| `rdoc` | Comma-separated frame numbers to capture with RenderDoc, which is loaded before Vulkan starts. | The game runs under RenderDoc, with its capture options set to keep `VK_EXT_external_memory_host` available and to reference all resources, and captures are written to disk. | Caution |
|
||||
| `rdoc_dir` | Output folder for RenderDoc captures (the default is `build/captures`). | Only moves the output. | Safe |
|
||||
| `rdoc_dll` | Path of the RenderDoc library, for when it is not found by default. | A wrong path logs that the library cannot be loaded and no capture is taken. | Safe |
|
||||
| `shots` | `<lo>-<hi>:<step>` saves every step-th present in the range as `f<n>.png`, scaled to 480x270; `w<lo>-<hi>:<step>` takes them by world-clock seconds. | Writes one PNG per step and adds a readback of the window texture; the pictures clip highlights in HDR. | Caution |
|
||||
| `shots_dir` (default `.`) | Folder for the `shots` pictures; it is created when needed. | Only moves the output. | Safe |
|
||||
| `shots_full` | Set to any value to save `shots` at full size instead of 480x270. | Files get far larger and writing them takes longer; has no effect without `shots`. | Caution |
|
||||
|
||||
## Automated tests and test hooks
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `vram_limit_mib` | Pretends the device-local memory budget is at most this many MiB, for the paths that react to the budget (`tex_vram_pressure`, mirror refusal); the driver knows nothing of it. | Lets a large card drive those paths; a low value drops released textures and refuses mirrors that would have fit. | Caution |
|
||||
| `background_window` | `1` keeps the window behind the user's windows, never fullscreen at start, and F11 keeps its place; SDL windows ignore it. | The window does not come to the front or flash, so it is easy to miss; meant for unattended runs. | Caution |
|
||||
| `gpu` | `vulkan` or `null`: the graphics back end; `null` has no GPU, completes every submit at once and returns zeros for texture reads. | `null` shows no picture; any other value stops the start with an error. | Caution |
|
||||
| `input` | `none` reports no input from the keyboard, mouse or controllers. | The game gets no controller input and the menu cursor stops following the mouse; `input_script` still applies. | Caution |
|
||||
| `input_script` | Path of a file with lines `<frame> <button> [hold]` or `<frame> LSTICK or RSTICK <x> <y> [hold]` that are ORed into the real input. | The game presses those buttons by itself; a file that cannot be opened is logged and ignored. | Caution |
|
||||
| `menu_test` | `<frame>[:<frames per page>]` opens the menu at that frame, shows each tab for 30 frames (or the given count), saves one screenshot per tab and closes the menu. | The menu opens by itself and screenshots are written. | Caution |
|
||||
| `menu_test_dir` | Folder for the `menu_test` screenshots; the default is the working directory. | Only moves the output. | Safe |
|
||||
| `menu_test_set` | `<frame>:<key>=<value>[,...][;...]` changes settings live at that frame, with or without the menu open; `SAVE=1` also writes `totk.ini`. | Settings change during play without input, and `SAVE=1` rewrites the file. | Caution |
|
||||
| `probe_movie` | Text that picks the movie for `totk.exe --probe-movie`: the first file whose path contains it. | Only used by the movie decoder probe; ignored in normal play. | Safe |
|
||||
| `probe_movie_shot` | Path of a `.ppm` file that the movie probe writes the middle frame to. | Only used by the movie decoder probe. | Safe |
|
||||
| `probe_movie_bench` | `1` makes the movie probe also time the resampler that fits a movie to a game resolution of another shape, and print the time per frame. | Only used by the movie decoder probe. | Safe |
|
||||
| `swkbd_answer` | `<text>`, `<cancel>` or `<initial>`: answers the on-screen keyboard (for example a horse name) without opening a window. | The game gets that answer every time it asks for text. | Caution |
|
||||
| `swkbd_test` | `ok` or `cancel` opens the keyboard window and presses OK or Cancel on the first timer tick. | The keyboard window closes by itself. | Caution |
|
||||
| `test_oom` | `<site>:<n>[,...]` fails the first n allocations at a site: `commit` (guest memory), `staging` (upload buffers), `image` (texture images), `chunk` (draw data buffers). | Each forced failure takes the out-of-memory path: a memory release, a wait of 10 ms per retry and log lines; a large n stalls up to 30 s and then ends the game. | Caution |
|
||||
| `test_pad` | `<lx>,<ly>,<rx>,<ry>` attaches a virtual gamepad whose sticks rest at these raw SDL axis values (-32768 to 32767). | The game sees a controller with that drift or deflection; a malformed value is logged and ignored. | Caution |
|
||||
| `test_prepare_shaders` | `1` translates every recorded shader on all usable CPUs during start. | Start takes longer and fills the shader cache folder; the CPU is fully used while it runs. | Caution |
|
||||
|
||||
## Input
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `bind_<button>` | Remaps one Switch button or stick direction to `pad:<name>` or `key:<name>` entries; covers `bind_a`, `bind_b`, `bind_x`, `bind_y`, `bind_l`, `bind_r`, `bind_zl`, `bind_zr`, `bind_plus`, `bind_minus`, `bind_left`, `bind_up`, `bind_right`, `bind_down`, `bind_lclick`, `bind_rclick`, `bind_lstick_left`, `bind_lstick_up`, `bind_lstick_right`, `bind_lstick_down`, `bind_rstick_left`, `bind_rstick_up`, `bind_rstick_right`, `bind_rstick_down` and `bind_shake`. | A line replaces every default of that button; button prompts in the game still show the default layout. The Controls tab of the menu writes these lines; `docs/controls.md` lists the entry names. | Safe |
|
||||
|
||||
## Paths and files
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `amiibo_dir` | Folder the Amiibo tab starts in; the tab saves the folder you last browsed here. | Only changes which folder the tab lists first. | Safe |
|
||||
| `exefs` | Folder of the game's ExeFS (with `main.npdm`); a relative path is relative to `totk.exe`'s folder. | Without it, an `exefs` folder next to `totk.exe` is used, else the path set at build time. The game code is compiled into `totk.exe`, and the loader checks the files here by SHA-256, so files from another build or game version stop the start with a message. | Caution |
|
||||
| `mods_dir` (default `mods`) | Folder that holds one subfolder per player mod, beside `totk.exe`. | Mods found there replace game files; see `docs/mods.md`. | Caution |
|
||||
| `mods_disabled` | Names of mods that are switched off, separated by `\|`. | Listed mods are not applied; the Mods tab writes this line. | Safe |
|
||||
| `mods_order` | Mod names, top first, separated by `\|`; the top mod wins a file that two mods replace. | Changes which mod's file the game gets; unlisted mods go below the listed ones, by name. | Safe |
|
||||
| `pipeline_cache` | Path of the Vulkan pipeline cache file (default `pipeline-cache.bin` beside `totk.exe`). | Moves the cache; a new path starts empty and pipelines are built again. | Caution |
|
||||
| `profile_dir` | Folder of the game profile, which holds the user account id that save data is named after; the default is a `profile` folder beside `totk.exe`, else the build-time path. | Another folder means a different set of saves. | Save risk |
|
||||
| `romfs` | Folder of the game's RomFS (with `romfs.bin`); a relative path is relative to `totk.exe`'s folder. | Without it, a `romfs` folder next to `totk.exe` is used, else the build-time path; a missing image stops the start with a message. | Save risk |
|
||||
| `romfs_index` | Path of `romfs-index.bin`, which `tools/romfs_index.py` wrote; the default is beside `totk.exe`. | The index is checked against the image by SHA-256 and size; a mismatch stops the start. | Caution |
|
||||
| `romfs_mmap` | `1` maps the RomFS image into memory instead of reading it with positional reads. | Changes how the image is read; the log reports when the mapped path is on. | Caution |
|
||||
| `romfs_mods` | Folder of an overlay with a `manifest.tsv` of sizes and SHA-256 hashes; its files are served in place of the originals. | A missing file, size or hash mismatch, or a changed original stops the start; player mods win over this overlay. | Save risk |
|
||||
| `romfs_patches` | Folder of an overlay with a `manifest.tsv`, served in place of the original files; the default is `romfs-patches` beside the index. | Same checks as `romfs_mods`; while it is set the Xbox and PC button prompts are off, and the menu says so. | Save risk |
|
||||
| `romfs_proof_dir` | Folder for the `.romfs-verified-v1` proof file of a full verification; the default is beside the index. | Only moves one small file. | Safe |
|
||||
| `romfs_verify` | `full` hashes the whole RomFS image at start; `force` hashes it again even when a proof file exists. | Start takes many minutes for a 17 GB image; a proof file is written so later starts skip the hash. | Caution |
|
||||
| `shader_cache` | Folder of the translated shader cache (default `shader-cache` beside `totk.exe`). | A new folder starts empty, so shaders are translated again during play. | Caution |
|
||||
| `shared_fonts` | Folder of the extracted system fonts; the default is `shared-fonts` beside `totk.exe`, else beside the RomFS. | Simplified Chinese, Traditional Chinese and Korean need these fonts: without them the game exits before its first frame in those languages. Other languages do not use them. | Caution |
|
||||
|
||||
## Shader translation
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `xlate` | Path of the shader translator program; the default is `totk_xlate` beside `totk.exe`, else under `build/xlate`. | Shaders are translated by that program; if it is missing the log says so and graphics shaders cannot be translated. | Risky |
|
||||
| `xlate_id` | Text or 64 hex digits that identify the translator build in shader cache keys; the default comes from an `.id` file beside the translator, else a hash of the program. | A value that two different builds share lets the cache serve shaders translated by the other build. | Caution |
|
||||
| `xlate_keep` (default `0`) | `1` keeps the translation job of a last-frame variant as `<key>.job` with its code. | Writes extra files to the shader cache folder; meant for running the translator by hand. | Caution |
|
||||
| `xlate_procs` (default `2`) | Number of translator processes allowed during play. | More processes translate new shaders sooner and use more CPU and memory; values below 1 count as 1. | Caution |
|
||||
|
||||
## Graphics experiments
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `block_views_3d` (default `0`) | `1` gives 3D block-compressed images block views too. | On some AMD cards a GPU write past the end of the image's memory loses the Vulkan device, and the game stops. | Risky |
|
||||
| `deswizzle` (default `auto`) | Where block-linear textures are deswizzled: `auto` or `gpu` uses a compute pass where the texture's storage is an imported pool and the CPU elsewhere, `cpu` always uses the CPU, `check` uses the GPU and also deswizzles every upload on the CPU and compares the two. | `cpu` brings back the long frames on entering new areas (the replay thread deswizzles every texture); `check` waits for the GPU at every upload and is for testing only. The log names the mode in use. | Caution |
|
||||
| `dyn_delta` (default `1`) | Records only the pipeline dynamic states (cull mode, depth and stencil tests, vertex input, blend enables, equations and write masks, ...) whose values changed since the last draw in the command buffer, also across pipeline changes; `0` records the full set at every change of pipeline state, as before; `ab` alternates with the `ab` blocks. | Fewer commands per pipeline change on the render and recorder threads; a driver that loses dynamic state across pipeline binds would draw with stale state. `TOTK_TRACE=perf` prints the calls and commands per submission. | Caution |
|
||||
| `feedback` | `0` samples a target that an earlier draw of the same pass wrote with no barrier; `restart` ends and restarts the pass behind a full barrier; otherwise the feedback-loop layout is used when the GPU supports it. | `0` brings back the old behaviour that the barrier fixed, so the picture can break in passes that read a target they draw into; `restart` costs extra passes. | Risky |
|
||||
| `flush_span` (default `0`) | `1` copies one span from the lowest to the highest flushed address of a pool, as an older version did. | Copies far more data to the GPU in crowded towns: an estimated 30-50 ms of GPU time per large copy over PCIe 3.0 x8. | Caution |
|
||||
| `shadow_snap` (default `1`) | Shadow storage (Linux, Wine): `1` syncs the pools from per-submission write-watch snapshots, each 2 MiB region queried at most once per submission and shared with the texture checks; `0` queries each pool at every part of a submission, as before. | `1` cut the replay thread's write tracking from about 4.4 to 1.3 ms a frame on a Steam Deck; `0` is for comparison runs. Under Wine/Proton the default is `0` until it has been measured there; `1` turns it on. No effect where shadow storage is off (Windows by default). | Caution |
|
||||
| `freeze_idle` (default `60`) | Number of submissions a 2 MiB region stays unwritten before it is made read-only; `0` turns the freeze off. | A larger value keeps more regions writable and costs more write tracking; a write to a frozen region costs about 60 microseconds. | Caution |
|
||||
| `gpl_draw_build` (default `1`) | `0` leaves the missing shader libraries of a waiting draw to its background job instead of building them in parallel at once. | Draws whose pipeline is not ready are skipped meanwhile, until the job finishes. | Caution |
|
||||
| `lib_prebuild_keep` (default `0`) | Number of recorded graphics shader library variants that stay in memory after the boot prebuild. | Each kept library costs 140-460 KB of memory; the driver cache already makes a rebuild cheap. | Caution |
|
||||
| `minimal_layout` (default `1`) | `0` skips the compare against one-block tiles when a texture's memory layout is chosen. | The code calls it a negative control for fresh sessions; the comment does not say what the picture does. | Unknown |
|
||||
| `mirror_gap` (default `2`) | Hot pools at most this many 2 MiB regions apart share one write-watch query. | Changes the number of queries per submission; negative values count as 0. | Caution |
|
||||
| `movie_host` (default `1`) | `0` leaves every movie decoder to the game's own library. | The code comment says that library aborts at the first movie, so the game stops at the first cutscene. | Risky |
|
||||
| `movie_pace` (default `1`) | `0` hands every decoded movie frame over at the next present instead of when it is due. | Movies play at the game's frame rate rather than their own, so their speed no longer matches the sound. | Caution |
|
||||
| `pcss_k` (default `0.5`) | Depth-to-penumbra factor of soft shadows at 100 percent, for tuning. | Changes how wide soft shadows blur; has no effect while `pcss` is `0`. | Caution |
|
||||
| `pipe_sync` | Any value other than `0` or empty stops graphics pipelines from being built in the background. | Pipelines are built on the render path, which causes stutter when new effects appear. | Caution |
|
||||
| `push_stage` | `fs` pushes the descriptor set from the fragment stage instead of the vertex stage. | The log names the stage in use; the code does not say what changes in the picture. | Unknown |
|
||||
| `rec_async` (default `1`) | `0` makes the submitting thread wait until the recorder thread has recorded and submitted each part; `ab` alternates with the `ab` blocks. | With `0` the submitting thread waits for every part (about 0.3 ms a frame in a town on a Ryzen 7900X). | Caution |
|
||||
| `rec_end` (default `1`) | `0` lets the submitting thread begin, end and submit a recorded part again, as older versions did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
|
||||
| `rec_spin_us` (default `60`) | Microseconds the recorder thread polls for the next packets before it sleeps; `0` polls 4096 pauses instead; `ab` polls only in the B blocks of `ab`. With fewer than 6 logical processors it always polls 4096 pauses. | A longer poll saves an OS wake-up per packet and takes more CPU time; a shorter one sleeps sooner. | Caution |
|
||||
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which released textures are dropped, 0 to 100; `0` is off. First those the game has overwritten with a later texture and left alone for 30 s, then those that can be uploaded again. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets the card spill into system memory. Also a slider in the menu. | Caution |
|
||||
| `txd_transpose` (default `1`) | `0` turns off the swap of gradient components for gradient texture samples. | Terrain textures turned across the screen show crawling patches again. | Caution |
|
||||
| `ub_overlay` (default `1`) | `0` copies each uniform update inside a pass into its buffer at once, ending the pass with a full barrier; `ab` alternates with the `ab` blocks. | With `0` scenes with many updated blocks (the terrain, the sky islands) take longer on the GPU; `TOTK_TRACE=perf` prints how many updates stayed in their pass. | Caution |
|
||||
| `ub_overlay_out` (default `1`) | `0` copies each uniform update between passes into its buffer at once, behind a full barrier of its own; `ab` alternates with the `ab` blocks. Needs `ub_overlay`. | With `0` a run of updates before a pass costs a full GPU barrier each; `TOTK_TRACE=perf` prints how many updates between passes were overlaid. | Caution |
|
||||
| `ubo_snapshot` (default `1`) | `0` binds live memory for uniform blocks; `ab` alternates with the `ab` blocks. | Without snapshots late draws read another object's matrix, so objects can be drawn in the wrong place. | Risky |
|
||||
| `vk_no_float64` (default `0`) | `1` clears the 64-bit float feature, so the translator lowers FP64 to FP32. | Shaders change, so the shader cache entries for it are made again; precision of FP64 shaders drops. | Caution |
|
||||
| `vk_ssbo_align` (default unset) | Power of two that storage buffer offsets are treated as needing, to test the unaligned path. | The translator and the draw code round storage offsets down to it; shaders are translated again. | Caution |
|
||||
| `vk_subgroup` (default `32`) | Subgroup size to pin where the driver allows it. | A value other than 32 changes what the translator is told about the GPU, and shaders are translated again. | Caution |
|
||||
| `vram_headroom` (default `1`) | `0` turns off the spare half-block of video memory that a background thread keeps per memory type. | Texture allocations on the render thread can wait for a new block again. | Caution |
|
||||
| `vs_outs` | `all` declares all 32 vertex shader output locations; otherwise only the union of the paired fragment inputs. | Larger vertex shaders and new pipelines; unwritten outputs read the same as before. | Caution |
|
||||
| `wb_exact` (default `1`) | `0` decides write-back of GPU-written textures by page only. | A target that shares a page with data the CPU writes can go unwritten, so a name label next to the reticle can blink. | Caution |
|
||||
| `wb_merge` (default `1`) | `0` copies GPU-written ranges back as plain copies instead of writing word by word the words that changed. | In mirror memory mode the GPU's copy can overwrite words the CPU wrote, which breaks effects such as falling rain and waterfall splashes. | Caution |
|
||||
|
||||
## Motion vectors and DLSS
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `dlaa_jitter` (default `1`) | `0` turns off the camera jitter fed to DLSS. | DLSS gets no jitter between frames; a test setting. | Caution |
|
||||
| `dlaa_maskdebug` (default `0`) | `1` shows the reactive mask in place of the picture. | The frame shows a debug image while it is set. | Caution |
|
||||
| `dlaa_mv` (default `1`) | `0` sends zero motion vectors to DLSS. | DLSS sees no motion, so moving content can smear; a test setting. | Caution |
|
||||
| `dlaa_mvdebug` (default `0`) | `1` shows the motion vectors in place of the picture, x in red and y in green. | The frame shows a debug image while it is set. | Caution |
|
||||
| `dlaa_survey` (default `0`) | `1` logs what is drawn into the light target that DLSS reads. | Adds log output and per-draw work. | Caution |
|
||||
| `dlaa_times` (default `0`) | `1` times the DLSS passes on the GPU. | Adds GPU timestamps and log lines. | Caution |
|
||||
| `dlss_reactive` (default `0`) | `1` builds a reactive mask for DLSS where it ran. | Changes how DLSS blends moving areas; a test setting. | Caution |
|
||||
| `mv_check` | Frame interval; every that many DLSS frames copies the light target and motion images to host memory and logs how well the vectors explain the picture. | Costs a GPU-to-host copy and a comparison on the completion thread at each interval. | Caution |
|
||||
| `mv_survey` (default `0`) | `1` notes the uniform blocks of each scene draw and compares them with those of earlier frames. | Adds per-draw work and log output. | Caution |
|
||||
| `mvobj_all` (default `0`) | `1` also lists draws for object motion that read no object blocks (terrain and grass). | More draws are checked and listed, which costs time. | Caution |
|
||||
| `mvobj_banks` (default `0`) | `1` logs which words of the other uniform banks changed for each scene draw. | Adds log output and per-draw work. | Caution |
|
||||
| `mvobj_equal` (default `0`) | `1` uses an EQUAL depth compare for the object motion pass, for comparison. | Changes which pixels get object vectors; the code comment names the default as the one that hides objects behind nearer geometry. | Caution |
|
||||
| `mvobj_exact` | Hex bank mask; only draws whose position uses exactly these banks get object motion. | Fewer draws get object vectors, so their motion falls back to the camera's. | Caution |
|
||||
| `mvobj_exec` (default `1`) | `0` lists object motion draws but never records them. | Objects get camera motion only. | Caution |
|
||||
| `mvobj_noc4` (default `0`) | `1` draws again positions that have no scene camera bank. | Those draws get this frame's view, so they can show wrong motion. | Caution |
|
||||
| `mvobj_only` | Hex bank mask; only draws whose position reads no bank outside the mask get object motion. | Fewer draws get object vectors. | Caution |
|
||||
| `mvobj_times` (default `0`) | `1` adds timings to the object-motion summary. | The clock reads themselves cost frame time. | Caution |
|
||||
| `mvprev_check` (default `0`) | `1` verifies the stored first bytes of each previous-frame table entry. | The code comment measures 3.6 ms per frame. | Caution |
|
||||
| `mvprev_grow` (default `1`) | `0` keeps only three banks in the previous-frame tables. | Object motion that depends on the other banks is no longer tracked. | Caution |
|
||||
| `mvprev_skip` | Hex bank mask of banks left out of the previous-frame copy, replacing the default. | Different banks are tracked, which changes object motion. | Caution |
|
||||
| `mvprev_trust` | Hex bank mask of banks whose found previous-frame copy is taken without the check. | A copy that does not match is used anyway, which can give wrong motion. | Caution |
|
||||
|
||||
## Scheduling and host
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `app_pool` | Size in bytes of the guest application memory pool; accepts decimal or `0x` hex. | The default depends on the resolution settings; `0` stops the start with a message, and too small a pool leaves the game without memory. | Risky |
|
||||
| `audio_off` | Any value mixes audio without opening an output device. | The game runs in silence and reports two channels; same as `audio = none`. | Caution |
|
||||
| `kspin_us` (default `20`) | Microseconds a guest wait or a wait for a virtual core polls before it sleeps; `0` turns the poll off; `ab` polls only in the B blocks of `ab`. Off with fewer than 6 logical processors. | A wake within the poll costs no OS wake-up; a longer poll takes more CPU time from other threads. | Caution |
|
||||
| `mem_reserve_mib` (default `1536`) | MiB of system memory kept free for Windows, other programs and the game's own heap when the renderer sizes what it keeps in device memory, where device memory takes system memory (integrated GPUs, and discrete GPUs whose driver charges it to the process). | A larger value keeps fewer released textures and spare blocks when the system runs short; a smaller one lets the game use more before it holds back. | Caution |
|
||||
| `one_per_core` (default `0`) | `1` keeps the process on the first logical processor of each physical core; `ab` switches it with the `ab` blocks. | Busy threads stop sharing a core with a sibling thread, which can help or hurt on the CPU; it only applies when the CPU has hyperthreads. | Caution |
|
||||
| `pcore` (default `1`) | On a hybrid CPU (performance and efficiency cores), the game's main and render threads and the replay, recorder and present threads prefer the performance cores; `0` turns that off. | With `0` Windows may run those threads on efficiency cores; no effect on a CPU with one kind of core. The log says which cores were found. | Caution |
|
||||
| `reexec` | Counter of address-space retries that the runtime sets itself when it starts the process again. | The counter is cleared when the window is recreated; a value of 20 or more stops the start with a fatal error. | Risky |
|
||||
| `replay_chunks` (default `0`) | Most chunks a submission is cut into when `replay_workers` is set; `0` uses twice the number of recording threads. | Fewer chunks leave threads idle at the end of a submission, more add scan and submit work per chunk; no effect while `replay_workers` is `0`. | Caution |
|
||||
| `replay_chunk_draws` (default `128`) | Least draws a chunk of the chunked replay holds before it may end at a pass. | Small values make many short chunks, large ones leave big submissions on few threads; no effect while `replay_workers` is `0`. | Caution |
|
||||
| `replay_state_check` (default `0`) | `1` compares the replay state after the last chunk of a chunked submission with the state of the scan and logs differences. | Adds a comparison per submission and log lines on a mismatch; no effect while `replay_workers` is `0`. | Caution |
|
||||
| `replay_skew_us` (default `0`) | Debug: the even chunks of a chunked submission wait this many microseconds before they record, so the chunk after each one records first. | Shows a recording that depends on which thread gets to a chunk first in every submission; slows the replay; no effect with fewer than 2 `replay_workers`. | Caution |
|
||||
| `restart_wait` | Set by the runtime when it restarts itself; names the old process by id and creation time so the new process waits for it to exit. | Meant for the runtime's own use; the code does not describe a manual value. | Unknown |
|
||||
| `spin` (default `1`) | `0` makes a guest thread that polls memory keep its virtual core. | Polling threads can starve the thread whose item they wait for, so frames can stall. | Caution |
|
||||
| `spin_handoff_us` (default `50`) | Microseconds a polling wait lasts before it gives its virtual core to a waiting thread. | A shorter value hands cores over sooner; a longer one keeps them longer. | Caution |
|
||||
| `tex_block_mib` (default `64`) | Block size in MiB of the pool that holds the game's textures; images of a quarter block or more get memory of their own. `0` puts textures in the default pools with the buffers. | Larger blocks hold more memory that no texture uses; smaller ones mean more allocations from the driver. | Caution |
|
||||
| `tex_released_mib` (default `256`) | MiB of textures the game released that are kept for reuse; older ones beyond it are dropped. `0` keeps up to 2048 of them, whatever their size. | A larger value keeps more device memory and process commit; a smaller one makes the game create more textures again. | Caution |
|
||||
| `vma_block_mib` (default `64`) | Block size in MiB that the Vulkan memory allocator takes from the driver for buffers and other images on heaps over 1 GiB; larger allocations than half a block get memory of their own. `0` uses the allocator's 256. | Larger blocks hold more memory that nothing uses; smaller ones mean more allocations from the driver. | Caution |
|
||||
|
||||
## Generated-code patches
|
||||
|
||||
These names are code patch sets built into the generated game code (`recomp/patches-*.txt`). `rt_patches_init` reads the environment name at start, then the resolution and mouse code switches some sets itself.
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `aspect_patch` (default `0`) | Patch set that feeds the camera the output's aspect ratio when it is not 16:9. | The resolution code switches this set on or off itself at start, so a value here is replaced; the code does not settle what a manual value does. | Unknown |
|
||||
| `fixed_res_patch` (default `0`) | Patch set that holds the game's dynamic resolution at 100 percent (the scene tier). | The resolution code switches this set from `dynamic_res` (on at `0`); a manual value is replaced. | Unknown |
|
||||
| `fov_patch` (default `0`) | Patch set that scales the camera's vertical view angle by the field-of-view setting. | The resolution code switches this set itself whenever the field of view is not 100 percent; a manual value is replaced. | Unknown |
|
||||
| `mouse_direct_patch` (default `1`) | Patch set of the hooks in the game's camera code behind `mouse_mode = direct` on game 1.4.3. | `0` stops `mouse_mode = direct` from taking effect; other mouse modes are unaffected. | Safe |
|
||||
| `patch_output` (default `0`) | Patch set that makes the game use the configured output size for its swapchain and display configuration. | The resolution code switches this set whenever the output differs from the base size; a manual value is replaced. | Unknown |
|
||||
| `patch_scene` (default `0`) | Patch set that makes the game use the configured scene render size. | The resolution code switches this set from the render size setting; a manual value is replaced. | Unknown |
|
||||
@@ -0,0 +1,109 @@
|
||||
"""The display scale of the installer window: process DPI awareness and the pixel sizes that follow it.
|
||||
|
||||
Windows draws a DPI-unaware process at 96 DPI and stretches its windows to the display's scale, which is what made the
|
||||
setup window blurry at 125% and above. gui.py calls enable() before it imports tkinter, so the process is system DPI
|
||||
aware before its first window exists; Tk then reads the display's DPI when it starts (`tk scaling` is 2.667 at 200%)
|
||||
and every font size in points comes out at the right size. The pixel sizes of the layout have no such scaling of their
|
||||
own, so gui.py passes them through px().
|
||||
|
||||
System DPI awareness, not per-monitor: Tk 9.0.4, the one the release bundles, has no per-monitor support (its DLL
|
||||
imports no DPI-awareness function), so it draws everything at the system DPI. Per-monitor awareness would leave the
|
||||
window crisp on a display at another scale but with the primary display's font sizes; system awareness draws the
|
||||
primary display exactly and lets Windows stretch the window only on a display whose scale differs from it, as before.
|
||||
|
||||
On Linux the window stays 1:1 (px returns n as it is): Tk and the desktop scale what they draw.
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
|
||||
DPI_96 = 96 # the scale Windows calls 100%
|
||||
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
|
||||
PROCESS_SYSTEM_DPI_AWARE = 1 # SetProcessDpiAwareness' value for the same thing (Windows 8.1)
|
||||
LOGPIXELSX = 88 # GetDeviceCaps' horizontal DPI, the only source before Windows 10 1607
|
||||
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 # ctypes.WinDLL's winmode: System32 only, never the installer's folder
|
||||
|
||||
_scale = None
|
||||
|
||||
|
||||
def enable():
|
||||
"""Make the process system DPI aware, then return the display scale (1.0 at 96 DPI). Must run before the first
|
||||
window; a later call only returns the scale."""
|
||||
global _scale
|
||||
if _scale is None:
|
||||
dpi = _system_dpi() if os.name == 'nt' else DPI_96
|
||||
_scale = dpi / DPI_96
|
||||
return _scale
|
||||
|
||||
|
||||
def scale():
|
||||
"""The display scale: 1.0 at 100%, 2.0 at 200%."""
|
||||
return enable()
|
||||
|
||||
|
||||
def px(n):
|
||||
"""n pixels at 100% as this display's pixels: 14 stays 14 at 100% and becomes 28 at 200%."""
|
||||
return int(round(n * enable()))
|
||||
|
||||
|
||||
def awareness():
|
||||
"""What Windows says the process is: 0 unaware, 1 system aware, 2 per-monitor aware."""
|
||||
if os.name != 'nt':
|
||||
return 0
|
||||
try: # Windows 10 1607
|
||||
user32 = ctypes.WinDLL('user32')
|
||||
user32.GetThreadDpiAwarenessContext.restype = ctypes.c_void_p
|
||||
user32.GetAwarenessFromDpiAwarenessContext.argtypes = [ctypes.c_void_p]
|
||||
return int(user32.GetAwarenessFromDpiAwarenessContext(user32.GetThreadDpiAwarenessContext()))
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 8.1
|
||||
value = ctypes.c_int()
|
||||
shcore = ctypes.WinDLL('shcore', winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)
|
||||
shcore.GetProcessDpiAwareness.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)]
|
||||
if shcore.GetProcessDpiAwareness(None, ctypes.byref(value)) == 0:
|
||||
return value.value
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows Vista
|
||||
return 1 if ctypes.WinDLL('user32').IsProcessDPIAware() else 0
|
||||
except (AttributeError, OSError):
|
||||
return 0
|
||||
|
||||
|
||||
def _system_dpi():
|
||||
"""Make the process system DPI aware and return the display DPI. Each call that this Windows version has is tried;
|
||||
one failing means the process already is aware (a manifest, or the call before it) and changes nothing."""
|
||||
try:
|
||||
user32 = ctypes.WinDLL('user32', use_last_error=True)
|
||||
except OSError:
|
||||
return DPI_96
|
||||
try: # Windows 10 1607, and the only way to ask for the system context explicitly
|
||||
user32.SetProcessDpiAwarenessContext.argtypes = [ctypes.c_void_p]
|
||||
user32.SetProcessDpiAwarenessContext.restype = ctypes.c_bool
|
||||
user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE))
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 8.1
|
||||
ctypes.WinDLL('shcore', winmode=LOAD_LIBRARY_SEARCH_SYSTEM32).SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows Vista
|
||||
user32.SetProcessDPIAware()
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 10 1607
|
||||
return int(user32.GetDpiForSystem()) or DPI_96
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # before that
|
||||
gdi32 = ctypes.WinDLL('gdi32')
|
||||
user32.GetDC.restype = ctypes.c_void_p
|
||||
user32.GetDC.argtypes = [ctypes.c_void_p]
|
||||
user32.ReleaseDC.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
gdi32.GetDeviceCaps.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
dc = user32.GetDC(None)
|
||||
dpi = gdi32.GetDeviceCaps(ctypes.c_void_p(dc), LOGPIXELSX)
|
||||
user32.ReleaseDC(None, ctypes.c_void_p(dc))
|
||||
return dpi or DPI_96
|
||||
except (AttributeError, OSError):
|
||||
return DPI_96
|
||||
+124
-48
@@ -3,7 +3,8 @@
|
||||
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the optional firmware
|
||||
folder, the game language and the install folder, runs `python -m installer ...` as a subprocess and shows its JSON
|
||||
events. Decisions live in guilogic.py; this file only draws. The window text is in
|
||||
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids.
|
||||
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids. On Windows the window follows the
|
||||
display's DPI (installer/dpi.py): fonts and paddings grow with the scale, so Windows does not stretch it.
|
||||
|
||||
--selftest [--shots DIR] build every screen in every language and close by itself (PNG per screen with --shots)
|
||||
--lang en|ru|pt-BR|zh-Hans window language (default: the Windows display language)
|
||||
@@ -15,9 +16,18 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from . import dpi
|
||||
|
||||
# Windows fixes the DPI awareness of a process when its first window is created, and Tk reads the display's DPI when it
|
||||
# starts: both happen inside the first tkinter.Tk(), so this call comes before tkinter is imported at all.
|
||||
dpi.enable()
|
||||
|
||||
import tkinter as tk # noqa: E402 (after dpi.enable, see above)
|
||||
from tkinter import filedialog, messagebox, ttk # noqa: E402
|
||||
|
||||
try:
|
||||
from PIL import ImageGrab # only the screenshots of --selftest --shots need it; the release has no PIL
|
||||
@@ -26,13 +36,28 @@ except ImportError:
|
||||
|
||||
from . import ini
|
||||
from . import layout as layouts
|
||||
from . import recent
|
||||
from .guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, UI_LANGUAGE_LABELS, UI_LANGUAGES, Catalog, Form, Model,
|
||||
Process, amount, default_game_language, describe_event, parse_line, windows_ui_language)
|
||||
|
||||
POLL_MS = 100
|
||||
GLYPH = {PENDING: '\u25cb', RUNNING: '\u25b6', DONE: '\u2714', SKIPPED: '\u2013', FAILED: '\u2716'}
|
||||
COLOR = {PENDING: '#808080', RUNNING: '#1a5fb4', DONE: '#26a269', SKIPPED: '#808080', FAILED: '#c01c28'}
|
||||
PAD = 14
|
||||
# The window's pixels at 100%: Tk scales the fonts by itself (tk scaling), these through installer/dpi.py.
|
||||
PAD = dpi.px(14) # padding of the window and its frames
|
||||
WRAP = dpi.px(620) # wrap width of the long labels
|
||||
|
||||
RECENT_WAIT = 1.5 # seconds the window waits for the recorded paths
|
||||
|
||||
|
||||
def load_recent(wait=RECENT_WAIT):
|
||||
"""recent.load() in a daemon thread, waited for at most `wait` seconds: a recorded path on a disconnected network
|
||||
share can block its stat call for a long time. Nothing is prefilled when it has not returned by then."""
|
||||
out = {}
|
||||
thread = threading.Thread(target=lambda: out.update(recent.load()), daemon=True)
|
||||
thread.start()
|
||||
thread.join(wait)
|
||||
return {} if thread.is_alive() else out
|
||||
|
||||
|
||||
def fix_tcl_paths():
|
||||
@@ -57,6 +82,11 @@ class App:
|
||||
self.bound = [] # (widget, option, id, params) retranslated on a language switch
|
||||
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'firmware', 'dest')}
|
||||
self.vars['lang'] = tk.StringVar()
|
||||
self.recorded = {} # base/update paths prefilled from the recorded ones, by field
|
||||
self.touched = set() # fields the user picked or typed
|
||||
self.programmatic = False
|
||||
for name in ('base', 'update'):
|
||||
self.vars[name].trace_add('write', lambda *_, n=name: self.touched.add(n) if not self.programmatic else None)
|
||||
self.lang_code = default_game_language(ui_lang)
|
||||
self.screen = None
|
||||
self.fitted = None # the (width, height) fit last gave the window
|
||||
@@ -74,7 +104,7 @@ class App:
|
||||
self.mono_font = 'Consolas' if os.name == 'nt' else 'DejaVu Sans Mono'
|
||||
style = ttk.Style(root)
|
||||
style.configure('Title.TLabel', font=(self.ui_font, 15, 'bold'))
|
||||
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=8)
|
||||
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=dpi.px(8))
|
||||
style.configure('Err.TLabel', foreground='#c01c28')
|
||||
style.configure('Version.TLabel', foreground='#808080')
|
||||
|
||||
@@ -84,7 +114,7 @@ class App:
|
||||
self.title.pack(side='left')
|
||||
version = ttk.Label(top, style='Version.TLabel')
|
||||
self.bind_text(version, 'text', 'ui.version', version=layout.release)
|
||||
version.pack(side='left', padx=(10, 0), pady=(6, 0))
|
||||
version.pack(side='left', padx=(dpi.px(10), 0), pady=(dpi.px(6), 0))
|
||||
sw = ttk.Frame(top)
|
||||
sw.pack(side='right')
|
||||
self.ui_var = tk.StringVar(value=self.cat.lang) # the window language code; the box shows its name
|
||||
@@ -103,6 +133,15 @@ class App:
|
||||
self.bind_text(self.title, 'text', 'ui.rebuild_title')
|
||||
self.vars['dest'].set(str(rebuild))
|
||||
root.after(POLL_MS, self.start)
|
||||
else:
|
||||
# recorded sources fill only empty fields; dest goes last, its trace runs refresh_form (update hint and
|
||||
# recorded language). In update mode filled files are harmless: Extract.current hashes one only when its
|
||||
# size or mtime differ from the recorded ones.
|
||||
last = load_recent()
|
||||
self.recorded = {n: last[n] for n in ('base', 'update') if n in last and not self.vars[n].get().strip()}
|
||||
for name in ('base', 'update', 'keys', 'firmware', 'dest'):
|
||||
if name in last and not self.vars[name].get().strip():
|
||||
self.set_prefill(name, last[name])
|
||||
|
||||
# ---- text -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -185,19 +224,20 @@ class App:
|
||||
self.root.geometry(f'{max(w, self.root.winfo_width())}x{max(h, self.root.winfo_height())}')
|
||||
|
||||
def notice(self, parent):
|
||||
w = ttk.Label(parent, style='Notice.TLabel', wraplength=620, justify='left')
|
||||
w = ttk.Label(parent, style='Notice.TLabel', wraplength=WRAP, justify='left')
|
||||
self.bind_text(w, 'text', 'ui.notice')
|
||||
return w
|
||||
|
||||
def build_form(self):
|
||||
f = ttk.Frame(self.body)
|
||||
gap = dpi.px(4) # between the form's rows
|
||||
bar = ttk.Frame(f) # packed first so pack shrinks the text above it, never the buttons
|
||||
bar.pack(side='bottom', fill='x', pady=(12, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(12), 0))
|
||||
self.start_button = self.button(bar, 'ui.start', self.start)
|
||||
self.start_button.pack(side='right')
|
||||
self.form_intro = self.label(f, 'ui.intro', wraplength=620, justify='left')
|
||||
self.form_intro.pack(anchor='w', pady=(0, 8))
|
||||
self.notice(f).pack(fill='x', pady=(0, 10))
|
||||
self.form_intro = self.label(f, 'ui.intro', wraplength=WRAP, justify='left')
|
||||
self.form_intro.pack(anchor='w', pady=(0, dpi.px(8)))
|
||||
self.notice(f).pack(fill='x', pady=(0, dpi.px(10)))
|
||||
grid = ttk.Frame(f)
|
||||
grid.pack(fill='x')
|
||||
grid.columnconfigure(1, weight=1)
|
||||
@@ -206,44 +246,44 @@ class App:
|
||||
self.form_rows = {}
|
||||
for i, (name, label_id, pick_id) in enumerate(rows):
|
||||
lab = self.label(grid, label_id)
|
||||
lab.grid(row=i, column=0, sticky='w', pady=4, padx=(0, 10))
|
||||
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
lab.grid(row=i, column=0, sticky='w', pady=gap, padx=(0, dpi.px(10)))
|
||||
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=gap)
|
||||
b = self.button(grid, 'ui.browse', lambda n=name, p=pick_id: self.pick_file(n, p))
|
||||
b.grid(row=i, column=2, padx=(8, 0))
|
||||
b.grid(row=i, column=2, padx=(dpi.px(8), 0))
|
||||
self.form_rows[name] = lab
|
||||
i = len(rows)
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(8, 0))
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=gap)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=gap)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(dpi.px(8), 0))
|
||||
i += 1
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=4)
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=gap)
|
||||
self.lang_box = ttk.Combobox(grid, textvariable=self.vars['lang'], state='readonly', width=30)
|
||||
self.lang_box.grid(row=i, column=1, sticky='w', pady=4)
|
||||
self.lang_box.grid(row=i, column=1, sticky='w', pady=gap)
|
||||
self.lang_touched = False
|
||||
self.lang_box.bind('<<ComboboxSelected>>', lambda _e: setattr(self, 'lang_touched', True))
|
||||
self.fill_languages()
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(8, 0))
|
||||
self.label(f, 'ui.compressed_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=gap)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=gap)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.compressed_help', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
# On by default and named with its source and licence: the player sees what is downloaded and can untick it
|
||||
self.dlss_var = tk.BooleanVar(value=True)
|
||||
dlss = ttk.Checkbutton(f, variable=self.dlss_var)
|
||||
self.bind_text(dlss, 'text', 'ui.dlss')
|
||||
dlss.pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.dlss_help', wraplength=620, justify='left').pack(anchor='w', padx=(22, 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(8, 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=620, justify='left')
|
||||
self.form_error.pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.keys_private', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
dlss.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.dlss_help', wraplength=WRAP, justify='left').pack(anchor='w', padx=(dpi.px(22), 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=WRAP, justify='left')
|
||||
self.form_error.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.keys_private', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
return f
|
||||
|
||||
def build_run(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f)
|
||||
bar.pack(side='bottom', fill='x', pady=(8, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(8), 0))
|
||||
self.cancel_button = self.button(bar, 'ui.cancel', self.cancel)
|
||||
self.cancel_button.pack(side='right')
|
||||
self.log_toggle = self.button(bar, 'ui.show_log', self.toggle_log)
|
||||
@@ -252,14 +292,22 @@ class App:
|
||||
self.run_header.pack(anchor='w')
|
||||
self.step_rows = {}
|
||||
self.steps_box = ttk.Frame(f)
|
||||
self.steps_box.pack(fill='x', pady=(8, 8))
|
||||
self.phase_label = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.steps_box.pack(fill='x', pady=dpi.px(8))
|
||||
self.phase_label = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.phase_label.pack(anchor='w')
|
||||
self.bar = ttk.Progressbar(f, mode='determinate', maximum=1000)
|
||||
self.bar.pack(fill='x', pady=(4, 6))
|
||||
self.space_label = ttk.Label(f, wraplength=620, justify='left')
|
||||
bar_holder = ttk.Frame(f)
|
||||
bar_holder.pack(fill='x', pady=(dpi.px(4), dpi.px(6)))
|
||||
self.bar = ttk.Progressbar(bar_holder, mode='determinate', maximum=1000)
|
||||
self.bar.pack(fill='both', expand=True)
|
||||
if os.name == 'nt' and ttk.Style(f).theme_use() == 'vista':
|
||||
# The vista theme draws the bar 22 px high at every DPI and has no thickness option: the holder gives it the
|
||||
# height it would have at 100%, scaled with the display. Every other theme draws its own height, and the
|
||||
# holder follows it.
|
||||
bar_holder.pack_propagate(False)
|
||||
bar_holder.configure(height=dpi.px(22))
|
||||
self.space_label = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.space_label.pack(anchor='w')
|
||||
self.warn_label = ttk.Label(f, wraplength=620, justify='left', foreground='#9a6700')
|
||||
self.warn_label = ttk.Label(f, wraplength=WRAP, justify='left', foreground='#9a6700')
|
||||
self.warn_label.pack(anchor='w')
|
||||
self.log_frame = ttk.Frame(f)
|
||||
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=(self.mono_font, 9))
|
||||
@@ -273,22 +321,22 @@ class App:
|
||||
def build_finish(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f)
|
||||
bar.pack(side='bottom', fill='x', pady=(12, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(12), 0))
|
||||
self.button(bar, 'ui.close', self.on_close).pack(side='right')
|
||||
self.open_button = self.button(bar, 'ui.open_folder', self.open_folder)
|
||||
self.open_button.pack(side='right', padx=(0, 8))
|
||||
self.open_button.pack(side='right', padx=(0, dpi.px(8)))
|
||||
self.back_button = self.button(bar, 'ui.back', lambda: self.show('form'))
|
||||
self.back_button.pack(side='left')
|
||||
if self.rebuild: # a failed or cancelled rebuild: the game starts with the code it has
|
||||
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, 8))
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=620, justify='left')
|
||||
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, dpi.px(8)))
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=WRAP, justify='left')
|
||||
self.finish_title.pack(anchor='w')
|
||||
self.finish_text = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.finish_text.pack(anchor='w', pady=(8, 8))
|
||||
self.finish_text = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.finish_text.pack(anchor='w', pady=dpi.px(8))
|
||||
self.finish_steps = ttk.Frame(f)
|
||||
self.finish_steps.pack(fill='x', pady=(0, 8))
|
||||
self.finish_steps.pack(fill='x', pady=(0, dpi.px(8)))
|
||||
self.finish_notice = self.notice(f)
|
||||
self.finish_notice.pack(fill='x', pady=(8, 0))
|
||||
self.finish_notice.pack(fill='x', pady=(dpi.px(8), 0))
|
||||
return f
|
||||
|
||||
# ---- form -----------------------------------------------------------------------------------------------------
|
||||
@@ -298,9 +346,35 @@ class App:
|
||||
firmware=self.vars['firmware'].get(), lang=self.selected_lang(), dest=self.vars['dest'].get(),
|
||||
dlss=self.dlss_var.get())
|
||||
|
||||
def set_prefill(self, name, value):
|
||||
self.programmatic = True
|
||||
try:
|
||||
self.vars[name].set(value)
|
||||
finally:
|
||||
self.programmatic = False
|
||||
|
||||
def match_prefill(self, state):
|
||||
"""Prefilled base/update stay only while the chosen install recorded the same file (name and size, one stat);
|
||||
another install would otherwise extract again from them. Fields the user touched are left alone."""
|
||||
for name, value in self.recorded.items():
|
||||
if name in self.touched:
|
||||
continue
|
||||
keep = True
|
||||
if state is not None:
|
||||
known = (state.get('inputs') or {}).get(name)
|
||||
try:
|
||||
keep = (isinstance(known, dict) and Path(value).name == known.get('name')
|
||||
and Path(value).stat().st_size == known.get('size'))
|
||||
except (OSError, ValueError):
|
||||
keep = False
|
||||
want = value if keep else ''
|
||||
if self.vars[name].get() != want:
|
||||
self.set_prefill(name, want)
|
||||
|
||||
def refresh_form(self):
|
||||
form = self.form()
|
||||
state = form.installed_state()
|
||||
self.match_prefill(state)
|
||||
if state is not None:
|
||||
self.update_hint.configure(text=self.cat('ui.update_mode'))
|
||||
recorded = ini.canonical_language(state.get('lang'))
|
||||
@@ -339,6 +413,7 @@ class App:
|
||||
self.fit()
|
||||
return
|
||||
self.form_error.configure(text='')
|
||||
recent.save(form.dest, {name: getattr(form, name) for name in recent.SOURCES})
|
||||
# in a repository the Python running this window: the tools' embeddable Python there has no ._pth entry for
|
||||
# the repository, so `-m installer` would not find the package
|
||||
python = Path(sys.executable) if self.layout.dev else self.layout.python
|
||||
@@ -365,7 +440,7 @@ class App:
|
||||
self.step_rows = {}
|
||||
for s in steps:
|
||||
row = ttk.Frame(self.steps_box)
|
||||
row.pack(fill='x', pady=1)
|
||||
row.pack(fill='x', pady=dpi.px(1))
|
||||
glyph = ttk.Label(row, width=3)
|
||||
glyph.pack(side='left')
|
||||
name = ttk.Label(row)
|
||||
@@ -450,7 +525,7 @@ class App:
|
||||
def toggle_log(self):
|
||||
self.log_open = not self.log_open
|
||||
if self.log_open:
|
||||
self.log_frame.pack(fill='both', expand=True, pady=(8, 0))
|
||||
self.log_frame.pack(fill='both', expand=True, pady=(dpi.px(8), 0))
|
||||
else:
|
||||
self.log_frame.pack_forget()
|
||||
self.bound = [b for b in self.bound if b[0] is not self.log_toggle]
|
||||
@@ -587,6 +662,7 @@ def grab(root, path):
|
||||
def selftest(layout, shots=None):
|
||||
"""Every screen in every language; the window closes itself. Returns 0 when nothing raised."""
|
||||
fix_tcl_paths()
|
||||
os.environ[recent.ENV] = os.path.join(tempfile.mkdtemp(prefix='totk-selftest-'), 'setup.json')
|
||||
root = tk.Tk()
|
||||
root.geometry('+60+40')
|
||||
app = App(root, 'en', layout)
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
No console window. The two message strings come from the catalog (installer/text/<lang>.json, ids launcher.title and
|
||||
launcher.python_missing); tools/make_release.py writes them into launcher_text.h before it compiles this file.
|
||||
Build: llvm/bin/llvm-windres -I runtime -O coff launcher.rc -o launcher_rc.o, then
|
||||
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o TotK-Setup.exe */
|
||||
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o TotK-Setup.exe
|
||||
|
||||
System DPI aware, like the window's Python process (installer/dpi.py): Windows draws the error box of fail() at the
|
||||
display's scale instead of stretching it, which it would at 125% and above. */
|
||||
#include <windows.h>
|
||||
#include <wchar.h>
|
||||
|
||||
@@ -38,6 +41,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, i
|
||||
STARTUPINFOW si = { sizeof si };
|
||||
PROCESS_INFORMATION pi;
|
||||
(void)instance; (void)previous; (void)command_line; (void)show;
|
||||
SetProcessDPIAware(); /* before the message box exists; the call fails harmlessly when a manifest set it */
|
||||
if (n == 0 || n >= PATH_CHARS) { fail(); return 1; }
|
||||
while (n > 0 && folder[n - 1] != L'\\') n--;
|
||||
folder[n > 0 ? n - 1 : 0] = 0;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""The last install folder and source paths, in a per-user file, so an update, a reinstall or a switch to another game
|
||||
version starts with the form filled in.
|
||||
|
||||
{"format": 1, "dest": ..., "base": ..., "update": ..., "keys": ..., "firmware": ...}
|
||||
|
||||
Absolute paths only. The keys entry is the path of the keys file, never its contents.
|
||||
|
||||
Windows: %LOCALAPPDATA%\TotK Recomp\setup.json; elsewhere $XDG_STATE_HOME/totk-recomp/setup.json (default
|
||||
~/.local/state/totk-recomp/setup.json). TOTK_SETUP_RECENT names another file (tests, --selftest). Nothing else is
|
||||
stored: the language comes from the install's own totk-install.json. A file of format 0 (only "dest") still loads.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from . import state
|
||||
|
||||
FORMAT = 1
|
||||
ENV = 'TOTK_SETUP_RECENT'
|
||||
FILES = ('base', 'update', 'keys') # recorded when they are files
|
||||
SOURCES = FILES + ('firmware',)
|
||||
FIELDS = ('dest',) + SOURCES
|
||||
|
||||
|
||||
def path():
|
||||
override = os.environ.get(ENV)
|
||||
if override:
|
||||
return Path(override)
|
||||
if sys.platform == 'win32':
|
||||
base = os.environ.get('LOCALAPPDATA') or str(Path.home() / 'AppData' / 'Local')
|
||||
return Path(base) / 'TotK Recomp' / 'setup.json'
|
||||
base = os.environ.get('XDG_STATE_HOME') or str(Path.home() / '.local' / 'state')
|
||||
return Path(base) / 'totk-recomp' / 'setup.json'
|
||||
|
||||
|
||||
def usable(name, value):
|
||||
"""value when it is a non-empty absolute path of the right kind for the field, else None. One stat call per
|
||||
path, no file is opened or hashed; a path the OS rejects (embedded NUL, unreachable share) counts as missing."""
|
||||
if not isinstance(value, str) or not value or not os.path.isabs(value):
|
||||
return None
|
||||
try:
|
||||
p = Path(value)
|
||||
if name == 'dest':
|
||||
ok = (p / state.NAME).is_file()
|
||||
elif name == 'firmware':
|
||||
ok = p.is_dir()
|
||||
else:
|
||||
ok = p.is_file()
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return value if ok else None
|
||||
|
||||
|
||||
def read():
|
||||
try:
|
||||
data = json.loads(path().read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def load():
|
||||
"""The recorded entries that still exist, by field name. Never raises."""
|
||||
data = read()
|
||||
return {name: data[name] for name in FIELDS if usable(name, data.get(name))}
|
||||
|
||||
|
||||
def save(dest, sources=None):
|
||||
"""Records dest and the non-empty, existing entries of sources (base, update, keys, firmware); an entry left out
|
||||
or empty keeps its saved value. Atomic; a failure to write is ignored, the window works without it."""
|
||||
target = path()
|
||||
tmp = target.with_name(target.name + '.tmp')
|
||||
data = {name: v for name, v in read().items() if name in FIELDS and isinstance(v, str)}
|
||||
data['dest'] = os.path.abspath(dest)
|
||||
for name, value in (sources or {}).items():
|
||||
if name in SOURCES and isinstance(value, str) and value.strip():
|
||||
value = os.path.abspath(value.strip())
|
||||
if usable(name, value):
|
||||
data[name] = value
|
||||
try:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp.write_text(json.dumps({'format': FORMAT, **data}), encoding='utf-8')
|
||||
os.replace(tmp, target)
|
||||
except OSError:
|
||||
pass
|
||||
+65
-19
@@ -8,7 +8,8 @@ with the bundled clang (hostcheck.c).
|
||||
|
||||
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, shared-fonts/ (Chinese and Korean text), totk.exe
|
||||
and its DLLs, nvngx_dlss.dll (with --dlss), romfs-index.bin, romfs-patches/, romfs-patches-xbox/,
|
||||
romfs-patches-xbox-label/, romfs-patches-pc/, totk_xlate.exe, the
|
||||
romfs-patches-xbox-label/, romfs-patches-pc/, romfs-patches-<style>-<w>x<h>/ (each of the three for each shaped size),
|
||||
totk_xlate.exe, the
|
||||
shader state of totk.exe --prepare-shaders, totk.ini, totk-install.json.
|
||||
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), unpacked/ (the NSP of an NSZ
|
||||
or XCZ input until the extraction is done), gen/ (C), build/ (objects), logs/. The cleanup keeps gen/, build/ and
|
||||
@@ -16,9 +17,10 @@ logs/: the next run lifts into the same gen/, where recomp/gen.py rewrites only
|
||||
in build/ recompiles only those. A change of the mods' code cheats then costs the lift (about a minute) and the chunks
|
||||
it touches, not a full compile.
|
||||
|
||||
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status (tools/exefs_patches.py) and rebuild.txt, the command that starts this setup in
|
||||
its rebuild mode on the install folder (installer/gui.py --rebuild), one argument per line; totk.exe runs it at start
|
||||
when an enabled mod has code cheats the build lacks (runtime/cheats.c).
|
||||
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status and exefs.data (tools/exefs_patches.py) and
|
||||
rebuild.txt, the command that starts this setup in its rebuild mode on the install folder (installer/gui.py --rebuild),
|
||||
one argument per line; totk.exe runs it at start when an enabled mod has code cheats the build lacks
|
||||
(runtime/cheats.c).
|
||||
"""
|
||||
import hashlib
|
||||
import http.client
|
||||
@@ -167,7 +169,8 @@ def run_tool(ctx, step, script, args, parse=None, cwd=None):
|
||||
def free_bytes(path):
|
||||
"""Free bytes on the disk of path (its nearest existing parent) and that parent's device."""
|
||||
p = Path(path)
|
||||
while not p.exists():
|
||||
# An unavailable drive or share has no existing ancestor: let disk_usage report its OS error at the root.
|
||||
while not p.exists() and p.parent != p:
|
||||
p = p.parent
|
||||
return shutil.disk_usage(p).free, os.stat(p).st_dev
|
||||
|
||||
@@ -855,6 +858,12 @@ def exefs_status(ctx):
|
||||
return ctx.dest / 'build-cache' / 'exefs.status'
|
||||
|
||||
|
||||
def exefs_data(ctx):
|
||||
"""The data writes of the mods' ExeFS patch files (tools/exefs_patches.py --data), which runtime/mods.c stores into
|
||||
main before the game runs."""
|
||||
return ctx.dest / 'build-cache' / 'exefs.data'
|
||||
|
||||
|
||||
def cheats_status(ctx):
|
||||
"""The build's cheat status file (tools/cheats.py --status), which runtime/cheats.c reads: in build-cache, not in a
|
||||
mod's own cheats folder, which the player (or TKMM's merge) may empty."""
|
||||
@@ -869,7 +878,7 @@ class Lift(Step):
|
||||
# fingerprint takes a file's name and content only; the paths under mods/ go in too, because tools/cheats.py
|
||||
# and tools/exefs_patches.py name each patch set after its mod folder, so a renamed folder needs a lift
|
||||
files = cheat_files(ctx) + exefs_files(ctx)
|
||||
return fingerprint('lift-4', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
|
||||
return fingerprint('lift-5', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
|
||||
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *files,
|
||||
*(f.relative_to(ctx.dest / 'mods').as_posix() for f in files))
|
||||
|
||||
@@ -890,36 +899,54 @@ class Lift(Step):
|
||||
if m:
|
||||
rep.progress('lift', 'emit', int(m.group(1)), int(m.group(2)), 'chunks')
|
||||
|
||||
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets (tools/cheats.py,
|
||||
# tools/exefs_patches.py); their status files tell the runtime what the build has. Both are the player's
|
||||
# downloads, so files the tools refuse cost those cheats or patches, not the install: the game is built
|
||||
# without them and the setup warns.
|
||||
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets and data writes
|
||||
# (tools/cheats.py, tools/exefs_patches.py); their status files tell the runtime what the build has. Both are
|
||||
# the player's downloads, so files the tools refuse cost those cheats or patches, not the install: the game is
|
||||
# built without them and the setup warns.
|
||||
plain = layout.patches(game_version(ctx))
|
||||
stages = [('cheats.py', 'patches-cheats.txt', cheats_status(ctx), cheat_files(ctx), 'warn.cheats_skipped'),
|
||||
('exefs_patches.py', 'patches-exefs.txt', exefs_status(ctx), exefs_files(ctx), 'warn.exefs_skipped')]
|
||||
# (tool, patch file it writes, its outputs for the runtime: the status first, then the data file, the files it
|
||||
# reads, the warning when it fails)
|
||||
stages = [('cheats.py', 'patches-cheats.txt', [cheats_status(ctx)], cheat_files(ctx), 'warn.cheats_skipped'),
|
||||
('exefs_patches.py', 'patches-exefs.txt', [exefs_status(ctx), exefs_data(ctx)], exefs_files(ctx),
|
||||
'warn.exefs_skipped')]
|
||||
patches, skipped = plain, {}
|
||||
for tool, out, status, inputs, warn in stages:
|
||||
status.unlink(missing_ok=True) # no status: the runtime treats every such file as not built
|
||||
|
||||
def drop(outputs): # no status: the runtime treats every such file as not built
|
||||
for f in outputs:
|
||||
f.unlink(missing_ok=True)
|
||||
|
||||
for tool, out, outputs, inputs, warn in stages:
|
||||
drop(outputs)
|
||||
if not inputs:
|
||||
continue
|
||||
code, log = run_tool(ctx, 'lift', layout.tools / tool,
|
||||
['--exefs', ctx.dest / 'exefs', '--mods', ctx.dest / 'mods', '--patches', patches,
|
||||
'--out', ctx.temp / out, '--status', status])
|
||||
'--out', ctx.temp / out, '--status', outputs[0],
|
||||
*(['--data', outputs[1]] if len(outputs) > 1 else [])])
|
||||
if code:
|
||||
status.unlink(missing_ok=True)
|
||||
drop(outputs)
|
||||
skipped[warn] = log
|
||||
else:
|
||||
patches = ctx.temp / out
|
||||
args = ['--exefs', ctx.dest / 'exefs', '--out', gen]
|
||||
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', patches], parse)
|
||||
if code and patches != plain:
|
||||
for tool, out, status, inputs, warn in stages:
|
||||
for tool, out, outputs, inputs, warn in stages:
|
||||
if inputs and warn not in skipped:
|
||||
status.unlink(missing_ok=True)
|
||||
drop(outputs)
|
||||
skipped[warn] = log
|
||||
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', plain], parse)
|
||||
if code:
|
||||
raise InstallError('lift.failed', code=code, log=str(log))
|
||||
# The mods' data writes must stay off the bytes the lifted code reads as constants (its jump tables), which only
|
||||
# the lift knows: gen.py lists them in folded.txt, and a file that writes them becomes unsupported
|
||||
if exefs_data(ctx).is_file():
|
||||
code, log = run_tool(ctx, 'lift', layout.tools / 'exefs_patches.py',
|
||||
['--exefs', ctx.dest / 'exefs', '--status', exefs_status(ctx), '--data',
|
||||
exefs_data(ctx), '--folded', gen / 'main' / 'folded.txt'])
|
||||
if code:
|
||||
drop([exefs_status(ctx), exefs_data(ctx)])
|
||||
skipped.setdefault('warn.exefs_skipped', log)
|
||||
for warn, log in skipped.items():
|
||||
rep.warning(warn, log=str(log))
|
||||
(gen / STAMP).write_text(ctx.keys['lift'] + '\n')
|
||||
@@ -1072,7 +1099,7 @@ class Prepare(Step):
|
||||
# input_prompts_folder). Both are written on every install, so the menu choices work without another run of
|
||||
# the setup. Optional: when one fails, the game keeps the Switch prompts with that layout and the menu says the
|
||||
# files are missing, so the setup warns once and goes on. romfs-patches-pc likewise, with keyboard and mouse
|
||||
# prompts for prompts = pc (16:9 only: another shape takes make_romfs_patches.py --prompts pc --aspect).
|
||||
# prompts for prompts = pc.
|
||||
failed = None
|
||||
for style in ('xbox', 'xbox-label', 'pc'):
|
||||
out = dest / f'romfs-patches-{style}'
|
||||
@@ -1083,6 +1110,25 @@ class Prepare(Step):
|
||||
if code:
|
||||
remove(out)
|
||||
failed = failed or (code, log)
|
||||
# Each prompts' folder once more for each shape of ASPECT_SIZES, romfs-patches-<style>-<w>x<h> with that
|
||||
# shape's UI layout (--prompts <style> --aspect), which runtime/romfs.c overlay_init loads in place of
|
||||
# romfs-patches-<w>x<h>: without it a player with Xbox or PC prompts on a screen that is not 16:9 chose between
|
||||
# the prompts and the screen's shape. Only the Common archive differs from romfs-patches-<style>; --reuse links
|
||||
# the fonts, messages and size table from there instead of compressing them again, so each folder takes
|
||||
# seconds and a few MiB. Optional like romfs-patches-<style>, and skipped without it.
|
||||
for style in ('xbox', 'xbox-label', 'pc'):
|
||||
plain = dest / f'romfs-patches-{style}'
|
||||
for size in ASPECT_SIZES:
|
||||
out = dest / f'romfs-patches-{style}-{size}'
|
||||
remove(out)
|
||||
if not (plain / 'manifest.tsv').is_file():
|
||||
continue
|
||||
code, log = run_tool(ctx, f'prepare-{style}-{size}', layout.tools / 'make_romfs_patches.py',
|
||||
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
|
||||
out, '--prompts', style, '--aspect', size, '--reuse', plain])
|
||||
if code:
|
||||
remove(out)
|
||||
failed = failed or (code, log)
|
||||
if failed:
|
||||
rep.warning('warn.xbox_prompts_failed', code=failed[0], log=str(failed[1]))
|
||||
|
||||
|
||||
+41
-2
@@ -9,6 +9,9 @@ Output, under --out:
|
||||
<mod>/<mod>_NNNN.c one `void f_<addr>(Cpu *c)` per guest function, about --chunk-bytes of C per file
|
||||
<mod>/<mod>_table.c sorted <mod>_fn_addrs[] / <mod>_fn_ptrs[] / <mod>_fn_count for rt_dispatch
|
||||
<mod>/manifest.json chunk list, function count, statistics and timings
|
||||
<mod>/folded.txt the module's bytes the generated code took as constants: what the jump tables read at
|
||||
lift time, as module offsets [start, end); tools/exefs_patches.py keeps a mod's data
|
||||
writes off them
|
||||
patches.h code patch sets for the runtime (recomp/patches.py; no sets without --patches)
|
||||
|
||||
Function discovery runs in this process, vectorised over the module's .text words. Function starts are the module
|
||||
@@ -555,7 +558,8 @@ def _retry(fn, *args):
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ workers
|
||||
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter},
|
||||
# 'reads' {name: [(start, end)]} module bytes read since the last chunk
|
||||
|
||||
|
||||
def _worker_init(mods, starts, table, opts, jobs):
|
||||
@@ -565,6 +569,7 @@ def _worker_init(mods, starts, table, opts, jobs):
|
||||
_W['patches'] = table
|
||||
_W['opts'] = opts # Emitter keyword arguments
|
||||
_W['emitters'] = {}
|
||||
_W['reads'] = {}
|
||||
|
||||
|
||||
def _emitter(name):
|
||||
@@ -572,12 +577,31 @@ def _emitter(name):
|
||||
if e is None:
|
||||
path, base = _W['mods'][name]
|
||||
mod = Module(name, path, base, 'c')
|
||||
# Module.read is how the jump tables (recomp/jumptables.py _eval) read .text and .rodata at lift time: the
|
||||
# bytes it returns become part of the generated code, so <mod>/folded.txt lists them
|
||||
read, reads = mod.read, _W['reads'].setdefault(name, [])
|
||||
|
||||
def recorded(a, n, signed=False):
|
||||
reads.append((a - base, a - base + n))
|
||||
return read(a, n, signed=signed)
|
||||
mod.read = recorded
|
||||
e = _W['emitters'][name] = Emitter(mod, set(_W['starts'][name].tolist()),
|
||||
_W['patches'].for_module(name, base),
|
||||
spins=_W['patches'].spins_for(name, base), **_W['opts'])
|
||||
return e
|
||||
|
||||
|
||||
def merge_ranges(ranges):
|
||||
"""Sorted [start, end) ranges with overlapping and touching ones joined."""
|
||||
out = []
|
||||
for s, e in sorted(ranges):
|
||||
if out and s <= out[-1][1]:
|
||||
out[-1][1] = max(out[-1][1], e)
|
||||
else:
|
||||
out.append([s, e])
|
||||
return out
|
||||
|
||||
|
||||
def _emit_chunk(task):
|
||||
"""Worker: write one chunk. Returns its statistics."""
|
||||
name, number, path, fns = task
|
||||
@@ -595,9 +619,12 @@ def _emit_chunk(task):
|
||||
written = write_if_changed(Path(path), text)
|
||||
except OSError as ex:
|
||||
written, error = False, f'{path}: {ex}'
|
||||
reads = _W['reads'].get(name, [])
|
||||
folded = merge_ranges(reads)
|
||||
reads.clear()
|
||||
return {'module': name, 'number': number, 'file': Path(path).name, 'functions': len(fns), 'bytes': len(text),
|
||||
'written': written, 'error': error, 'seconds': time.perf_counter() - t0, 'stats': dict(e.stats),
|
||||
'unsupported': dict(e.unsupported)}
|
||||
'unsupported': dict(e.unsupported), 'folded': folded}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ driver
|
||||
@@ -613,6 +640,14 @@ def table_text(name, addrs):
|
||||
return ''.join(lines)
|
||||
|
||||
|
||||
def folded_text(name, ranges):
|
||||
"""<mod>/folded.txt: the module offsets [start, end) of the bytes the jump tables read at lift time, in hex."""
|
||||
lines = [f'# {GENERATED_MARKER}: {name} bytes the generated code took as constants (jump tables), module offsets '
|
||||
f'[start, end), generated by recomp/gen.py from game code. Do not edit or commit.']
|
||||
lines += [f'{s:x} {e:x}' for s, e in ranges]
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def remove_stale(moddir, name, nchunks, errors):
|
||||
"""Delete chunks an earlier run wrote beyond this run's count, and temporary files an interrupted run left."""
|
||||
pat = re.compile(re.escape(name) + r'_(\d{4})\.c')
|
||||
@@ -714,6 +749,7 @@ def main():
|
||||
# Largest chunks first, so the pool does not end on a long tail.
|
||||
tasks.sort(key=lambda t: -t[0])
|
||||
results = {name: [] for name in info}
|
||||
folded = {name: [] for name in info}
|
||||
done_at, errors = {}, []
|
||||
t_emit = time.perf_counter()
|
||||
ctx = mp.get_context('spawn')
|
||||
@@ -724,9 +760,11 @@ def main():
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
try:
|
||||
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
|
||||
# Its folded bytes come with its result: a chunk emitted again reads them again in its new worker.
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
|
||||
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
|
||||
results[r['module']].append(r)
|
||||
folded[r['module']] += r['folded']
|
||||
if r['error']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
@@ -753,6 +791,7 @@ def main():
|
||||
unsupported.update(r['unsupported'])
|
||||
t = time.perf_counter()
|
||||
table_written = write_if_changed(moddir / f'{name}_table.c', table_text(name, d['emitted']))
|
||||
write_if_changed(moddir / 'folded.txt', folded_text(name, merge_ranges(folded[name])))
|
||||
removed = remove_stale(moddir, name, d['chunks'], errors)
|
||||
manifest = {
|
||||
'module': name,
|
||||
|
||||
@@ -55,6 +55,21 @@ site cloth main 8222f0 9401d7cf @page:x8 bd400104 1e240842 1e240863 9401d7cf #
|
||||
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
|
||||
site bounce main bda798 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
|
||||
|
||||
# Climbing (issues #35, #276, #1001): the climb controller's velocity step (main+0x1918220, the same code as 1.4.3
|
||||
# main+0x1f49754) multiplies the requested velocity by a factor that makes up for a movement loss growing with the
|
||||
# frame rate: 1.272 - 0.362 (r - 1) clamped to [0.5, 1.5] (r = VFR +0, the 30 Hz ratio times the time scale). Right at
|
||||
# 30 fps, 78% of the speed at 60 and 30% at 240. Below r = 1 use 1.4.3's factor, 1.272 + 1.189 (1 - r) at most 2
|
||||
# (Nintendo's 60 fps fit), and below r = 0.5 the 1.4.3 set climb's g(r) = 0.66265 / r + 0.4982 + 0.0848 r, at most 8.
|
||||
# Without flags: h = max(min(1.4.3 factor, 2), g - 16 max(r - 0.5, 0)), s0 = max(s0, h), s1 = max(1.5, h), and the
|
||||
# following fmin s1, s0, s1 gives max(min(s0, 1.5), h). For r >= 1 h is at most the shipping value, so 30 fps and
|
||||
# slower keep the factor bit for bit. x8 is dead after the load at 0x19182c0; s5-s7 and s16 are written before any
|
||||
# later read in this function.
|
||||
# g takes u = max(page +0, r): the patch page's 30 Hz frame ratio without the time scale (runtime/frame_rate.c), so
|
||||
# slow motion at 30 or 60 fps keeps the shipping factor and only a real frame rate above 60 raises it; the runtime
|
||||
# switches the set off without the real-time step, where the page is not written.
|
||||
set climb TOTK_CLIMB_FIX 1
|
||||
site climb main 19182cc 1e2f1001 @page:x8 bd400111 1e296a31 1e2e1005 1e3138a5 52862508 72a7e308 1e270106 1e2608a5 1e2528a5 529a1ca8 72a7f448 1e270106 1e2628a5 1e201006 1e2678a5 52946dc8 72a7e528 1e270106 1e3118c6 52828248 72a7dfe8 1e270107 1e2728c6 529573e8 72a7b5a8 1e270107 1f1118e6 1e283a27 2f00e410 1e3068e7 1e261010 1f1098e6 1e241010 1e3078c6 1e2668a5 1e256800 1e2f1001 1e256821 # fmov s1, #1.5 -> s5 = h(r); s0 = max(s0, s5); s1 = max(1.5, s5)
|
||||
|
||||
# Cursor and aim steps (upstream game_patches.c patch_cursor_speed, 363a6eb). These updates read the game's 30 Hz frame
|
||||
# ratio for their smoothing but leave it out of the step itself, so the map cursor and the vertical Ultrahand aim moved
|
||||
# 2x as fast at 60 fps and 4x at 120 as at the shipping 30 fps (issue #44). Each site multiplies the step by that ratio,
|
||||
@@ -65,6 +80,22 @@ site cursor main ec3d9c bd400361 f94002e9 bd400930 1e300800 bd400361 # map pan
|
||||
site cursor main 1d48d64 1e2a198c 1e2a198c bd400a90 1e30098c # fdiv s12, s12, s10, then s12 *= [x20 + 8]: vertical Ultrahand aim
|
||||
site cursor main 1d484e4 1e200923 1e200923 bd400a90 1e300863 # fmul s3, s9, s0, then s3 *= [x20 + 8]: vertical gyro aim (Recall)
|
||||
|
||||
# Gear rotation check (issues #330, #984). The update of ActorCheckRotateModule (main+0x21350c0; vtable +0x110, called
|
||||
# by the AI node dispatchers main+0x11f5a40 / 0x2adbcf8 with x1 = the update context) reports a gear or water wheel as
|
||||
# turning when the angle it turned since the previous update is above RotateStartDeg / 30, the angle of one 30 Hz
|
||||
# frame. Above 30 fps a gear turns less per update than that, reads as stopped, and the shrine doors that wait on it
|
||||
# (Riogok, Turakamik, the Wind Temple) never open. 1.4.3 (main+0x273d080) divides the angle by the context's +8, the
|
||||
# frame delta in 30 Hz frames (the AI root sets it from its owner's virtual +0x30, main+0x106acc4, and the frame
|
||||
# counter node SelectorGenericFrameCounter, main+0x10b66f8, adds it up per update). 1.2.1 gets the same context and
|
||||
# ignores it: the update overwrites x1 with its first instruction. The first site keeps ctx+8 in s12 (the prologue has
|
||||
# saved d12, the function first writes it at the second site, and callees preserve it); the second multiplies the
|
||||
# threshold by it, which is 1.4.3's test with both sides multiplied by the delta. s4 is free after the fdiv: the next
|
||||
# read follows a write in the callee main+0x11c2014. The context follows the measured step with the set realtime and is
|
||||
# the whole-vsync ratio without it (0.25 at 120 fps, 0.5 at 60 fps measured), so the set works either way.
|
||||
set gears TOTK_HIGHFPS_FIX 1
|
||||
site gears main 21350e0 f9402001 bd40082c f9402001 # ldr x1, [x0, #0x40], after ldr s12, [x1, #8] (frame delta)
|
||||
site gears main 21352d0 1e24180c 1e241804 1e2c088c # fdiv s12, s0, s4 -> fdiv s4, s0, s4; fmul s12, s4, s12
|
||||
|
||||
# Output size (upstream games/totk/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
|
||||
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
|
||||
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
|
||||
@@ -81,6 +112,14 @@ site scene main cc3f68 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
|
||||
site scene main cc3f70 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
|
||||
site scene main cc3f74 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
|
||||
|
||||
# Fixed scene size (MacNativeGames game_patches_v143.c patch_fixed_res, 1.4.3 main+0x5848a8; runtime/resolution.c
|
||||
# res_dynres_live switches the set on unless TOTK_DYNAMIC_RES=1). The game's dynamic resolution controller
|
||||
# (main+0xcc26f4, called once per frame from the renderer at main+0xcc0fa0) steps the scene through tiers of 100, 90 and
|
||||
# 80 % by a cost model that grows with the scene width squared (runtime/resolution.c res_dynres_live), and the renderer stores scene width * tier / output width at +0xccc (main+0xcc1150).
|
||||
# Every path leaves through one epilogue, where the tier's scale moves into the result: 1.0 there makes every tier 100 %.
|
||||
set fixres TOTK_FIXED_RES_PATCH 0
|
||||
site fixres main cc2c00 2a1703e9 52a7f009 # mov w9, w23 -> mov w9, #0x3f800000 (1.0f)
|
||||
|
||||
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches.c, the 1.2.1 sites of aspect_install;
|
||||
# runtime/resolution.c res_init writes the output's width / height to page +56 and switches the set on). The same four
|
||||
# sites as patches-v143.txt, with the same original words: two camera sites (update and setup) load the camera's copy of
|
||||
@@ -103,6 +142,104 @@ set pooloff TOTK_POOL_OFFSET_PATCH 1
|
||||
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
|
||||
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
|
||||
|
||||
# Direct mouse camera (runtime/mouse_direct.c; the same hooks as patches-v143.txt, mapped by enclosing function to the
|
||||
# 1.2.1 main, each original word checked). The camera input update (main+0xd18088, 1.4.3 0x874c1c) after its dead zone,
|
||||
# clamp and StickExponent keeps x in s11 (1.4.3 s9); the host callback performs the displaced store and adds raw mouse
|
||||
# counts. The free orbit (main+0xae28b8) keeps its final angles at [sp+0x3c] / [sp+0x40], Ultrahand's curve results
|
||||
# (main+0x1768ebc) are s9 and s15. The host functions are at page +0xfd0 .. +0xf80; the page is main+0x472b000.
|
||||
set mouse_direct TOTK_MOUSE_DIRECT_PATCH 1
|
||||
site mouse_direct main d18140 bd04228b 94e84fa4 # camera input: str s11, [x20, #0x420]
|
||||
site mouse_direct main d181fc 6d4123e9 94e84f71 # camera input activity: ldp d9, d8, [sp, #0x10]
|
||||
site mouse_direct main ae28e8 f9400408 94f125b2 # free orbit begin: ldr x8, [x0, #8]
|
||||
site mouse_direct main ae355c bd403fed 94f12291 # free orbit angles: ldr s13, [sp, #0x3c]
|
||||
site mouse_direct main 176949c b9001be9 94bf0abd # Ultrahand input: str w9, [sp, #0x18]
|
||||
# The aiming, scope, ability, horse and Purah Pad cameras: per axis, the stick load and the angle the turn is added to,
|
||||
# each a bl to page +0xf80 told apart by the return address (runtime/mouse_direct.c load_sites_121). Where 1.2.1 adds
|
||||
# the turn in a shared helper (main+0x1bb70e8, 0x1bd0d00: *p = wrap(*p + s0)), the hook is on the turn the helper takes
|
||||
# (Horse) or on the store of the helper's result (Aiming's pitch target). Every enclosing function saves x30 in its
|
||||
# prologue and reads it only to return.
|
||||
site mouse_direct main 1d34338 bd4426c1 94a7df12 # Aiming: ldr s1, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d34374 bd015660 94a7df03 # Aiming: str s0, [x19, #0x154] (pitch target: +0x15c + turn from a helper)
|
||||
site mouse_direct main 1d34584 bd4422c1 94a7de7f # Aiming: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d345d0 bc5b83a1 94a7de6c # Aiming: ldur s1, [x29, #-0x48] (yaw)
|
||||
site mouse_direct main 1d35fd4 bd4422e0 94a7d7eb # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 1d35fe0 bd4426e9 94a7d7e8 # AimingTelescope: ldr s9, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 1d36280 bd416260 94a7d740 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 1d36224 bd405be1 94a7d757 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
|
||||
site mouse_direct main 1d48478 bd4422cf 94a78ec2 # SpecialPower: ldr s15, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d48480 bd4426cc 94a78ec0 # SpecialPower: ldr s12, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d48bb4 bd416262 94a78cf3 # SpecialPower: ldr s2, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 1d48b38 bc5a83a1 94a78d12 # SpecialPower: ldur s1, [x29, #-0x58] (yaw)
|
||||
site mouse_direct main 1d4070c bd442288 94a7ae1d # Horse: ldr s8, [x20, #0x420] (stick x)
|
||||
site mouse_direct main 1d40740 bd401be0 94a7ae10 # Horse: ldr s0, [sp, #0x18] (yaw turn, which a helper adds to +0x68)
|
||||
site mouse_direct main 1d408f8 bd4426a8 94a7ada2 # Horse: ldr s8, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d409ac bd406661 94a7ad75 # Horse: ldr s1, [x19, #0x64] (pitch)
|
||||
site mouse_direct main 1d48ccc bd4422c9 94a78cad # SpecialPower offset mode: ldr s9, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d48cf0 bd41da61 94a78ca4 # SpecialPower offset mode: ldr s1, [x19, #0x1d8] (offset x)
|
||||
site mouse_direct main 1d48cfc bd4426c9 94a78ca1 # SpecialPower offset mode: ldr s9, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d48d18 bd41de61 94a78c9a # SpecialPower offset mode: ldr s1, [x19, #0x1dc] (offset y)
|
||||
site mouse_direct main 1d485f8 bd400400 94a78e62 # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
|
||||
site mouse_direct main 1d37b44 bd4426ad 94a7d10f # CameraFinder: ldr s13, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d37b68 bd405261 94a7d106 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
|
||||
site mouse_direct main 1d37bcc bd4422ad 94a7d0ed # CameraFinder: ldr s13, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 1d37bf4 bd405be1 94a7d0e3 # CameraFinder: ldr s1, [sp, #0x58] (yaw)
|
||||
# The other player cameras. ClimbObj, EventMultiTalk and EventTalkManual load an angle with a pre-indexed load whose
|
||||
# write-back the code after it needs; the mov before it adds the offset instead and the hook loads [base] (no register
|
||||
# between the two words, no branch into them). Wakeboard, ShieldSurfing and Tail read the stick twice, once for its
|
||||
# length and once for its angle: the first pair gives the same values without degrees of its own (REPEAT).
|
||||
site mouse_direct main 1d38e08 bd442301 94a7cc5e # ClimbObj: ldr s1, [x24, #0x420]
|
||||
site mouse_direct main 1d38e10 bd409261 94a7cc5c # ClimbObj: ldr s1, [x19, #0x90] (yaw)
|
||||
site mouse_direct main 1d38dac bd442701 94a7cc75 # ClimbObj: ldr s1, [x24, #0x424]
|
||||
site mouse_direct main 1d38da4 aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
|
||||
site mouse_direct main 1d38db8 bc488ec1 94a7cc72 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
|
||||
site mouse_direct main 1d50738 bd4422c0 94a76e12 # Wakeboard: ldr s0, [x22, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d50740 bd4426c1 94a76e10 # Wakeboard: ldr s1, [x22, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d50768 bd4426c0 94a76e06 # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 1d5076c bd4422c1 94a76e05 # Wakeboard: ldr s1, [x22, #0x420]
|
||||
site mouse_direct main 1d50e5c bd40da60 94a76c49 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
|
||||
site mouse_direct main 1d508bc bd401fa0 94a76db1 # Wakeboard: ldr s0, [x29, #0x1c] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 1d50f20 bd403bec 94a76c18 # Wakeboard: ldr s12, [sp, #0x38] (yaw)
|
||||
site mouse_direct main 1d51e10 bd4422cd 94a7685c # WaterfallClimb: ldr s13, [x22, #0x420]
|
||||
site mouse_direct main 1d51fb8 b9403be8 94a767f2 # WaterfallClimb: ldr w8, [sp, #0x38] (yaw)
|
||||
site mouse_direct main 1d51f48 bd4426c9 94a7680e # WaterfallClimb: ldr s9, [x22, #0x424]
|
||||
site mouse_direct main 1d4a11c bd442280 94a78799 # Tail: ldr s0, [x20, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d4a124 bd442681 94a78797 # Tail: ldr s1, [x20, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d4a14c bd442680 94a7878d # Tail (climbing): ldr s0, [x20, #0x424] (stick y)
|
||||
site mouse_direct main 1d4a150 bd442281 94a7878c # Tail: ldr s1, [x20, #0x420]
|
||||
site mouse_direct main 1d4a4a0 bd40c260 94a786b8 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
|
||||
site mouse_direct main 1d4a514 bd40b661 94a7869b # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
|
||||
site mouse_direct main 1d4c930 bd442320 94a77d94 # LockOn / Target: ldr s0, [x25, #0x420]
|
||||
site mouse_direct main 1d4c958 bd442321 94a77d8a # LockOn / Target: ldr s1, [x25, #0x420] (stick x)
|
||||
site mouse_direct main 1d4c97c bd402be0 94a77d81 # LockOn / Target: ldr s0, [sp, #0x28] (yaw turn)
|
||||
site mouse_direct main 1d4c98c bd402be9 94a77d7d # LockOn / Target: ldr s9, [sp, #0x28] (yaw turn)
|
||||
site mouse_direct main 1d4cd5c bd442721 94a77c89 # LockOn / Target: ldr s1, [x25, #0x424] (stick y)
|
||||
site mouse_direct main 1d4cd80 bd402be0 94a77c80 # LockOn / Target: ldr s0, [sp, #0x28] (pitch turn)
|
||||
site mouse_direct main 1d4cd90 bd402beb 94a77c7c # LockOn / Target: ldr s11, [sp, #0x28] (pitch turn)
|
||||
site mouse_direct main 1d443e8 bd4426a9 94a79ee6 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d44430 bd419a61 94a79ed4 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
|
||||
site mouse_direct main 1d44444 bd4422a9 94a79ecf # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 1d44490 bd419e61 94a79ebc # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
|
||||
site mouse_direct main 1d418dc bd44234b 94a7a9a9 # HorseLockOnEmpty: ldr s11, [x26, #0x420]
|
||||
site mouse_direct main 1d3a484 bd44230b 94a7c6bf # EventMultiTalk: ldr s11, [x24, #0x420]
|
||||
site mouse_direct main 1d3a3f8 bd44270b 94a7c6e2 # EventMultiTalk: ldr s11, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 1d3a418 aa1303f7 91017277 # EventMultiTalk: mov x23, x19 -> add x23, x19, #0x5c
|
||||
site mouse_direct main 1d3a420 bc45cee1 94a7c6d8 # EventMultiTalk: ldr s1, [x23, #0x5c]! (pitch), as ldr s1, [x23]
|
||||
site mouse_direct main 1d3a4dc aa1303f6 91019276 # EventMultiTalk: mov x22, x19 -> add x22, x19, #0x64
|
||||
site mouse_direct main 1d3a4e4 bc464ec1 94a7c6a7 # EventMultiTalk: ldr s1, [x22, #0x64]! (yaw), as ldr s1, [x22]
|
||||
site mouse_direct main 1d3e2b0 bd442729 94a7b734 # EventTalkManual: ldr s9, [x25, #0x424] (stick y)
|
||||
site mouse_direct main 1d3e2d4 aa1303f8 9101d278 # EventTalkManual: mov x24, x19 -> add x24, x19, #0x74
|
||||
site mouse_direct main 1d3e2d8 bc474f01 94a7b72a # EventTalkManual: ldr s1, [x24, #0x74]! (pitch), as ldr s1, [x24]
|
||||
site mouse_direct main 1d3e33c bd442329 94a7b711 # EventTalkManual: ldr s9, [x25, #0x420] (stick x)
|
||||
site mouse_direct main 1d3e360 aa1303f6 9101f276 # EventTalkManual: mov x22, x19 -> add x22, x19, #0x7c
|
||||
site mouse_direct main 1d3e364 bc47cec1 94a7b707 # EventTalkManual: ldr s1, [x22, #0x7c]! (yaw), as ldr s1, [x22]
|
||||
site mouse_direct main 1d46108 bd4422e0 94a7979e # ShieldSurfing: ldr s0, [x23, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d46110 bd4426e1 94a7979c # ShieldSurfing: ldr s1, [x23, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d46138 bd4426e0 94a79792 # ShieldSurfing: ldr s0, [x23, #0x424] (stick y, kept)
|
||||
site mouse_direct main 1d4613c bd4422e1 94a79791 # ShieldSurfing: ldr s1, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 1d46788 bd40de60 94a795fe # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
|
||||
site mouse_direct main 1d46910 bd401fa0 94a7959c # ShieldSurfing: ldr s0, [x29, #0x1c] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 1d467b4 bd403bec 94a795f3 # ShieldSurfing: ldr s12, [sp, #0x38] (yaw)
|
||||
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
|
||||
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
|
||||
spin main ac600 ac620 ac630 # 13 instructions; loads
|
||||
|
||||
@@ -82,6 +82,13 @@ site scene main 585e50 bd0cc660 2d420460 # str s0, [x19, #0xcc4] -> ldp s0, s
|
||||
site scene main 585e54 1e220140 bd0cc660 # scvtf s0, w10 -> str s0, [x19, #0xcc4]
|
||||
site scene main 585e60 bd0cca60 bd0cca61 # str s0, [x19, #0xcc8] -> str s1, [x19, #0xcc8]
|
||||
|
||||
# Fixed scene size (MacNativeGames game_patches_v143.c patch_fixed_res; runtime/resolution.c res_dynres_live switches
|
||||
# the set on unless TOTK_DYNAMIC_RES=1). The dynamic resolution controller (main+0x584300, 1.2.1 0xcc26f4) steps the
|
||||
# scene through tiers of 100, 90 and 80 % by a cost model that grows with the scene width squared; at its one epilogue the tier's scale moves into
|
||||
# the result, and 1.0 there makes every tier 100 %. The original word is MacNativeGames' check against 1.4.3.
|
||||
set fixres TOTK_FIXED_RES_PATCH 0
|
||||
site fixres main 5848a8 2a1703e9 52a7f009 # mov w9, w23 -> mov w9, #0x3f800000 (1.0f)
|
||||
|
||||
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches_v143.c and aspect_patch.h
|
||||
# aspect_install / aspect_stub; runtime/resolution.c res_init writes the output's width / height to page +56 and
|
||||
# switches the set on). Each site leaves the ratio sead's perspective projection (+0xb0) takes in s0: two camera sites
|
||||
@@ -111,7 +118,85 @@ site mouse_direct main 874cb4 bd042289 94c9acc7
|
||||
site mouse_direct main 874d7c 6d40a3e9 94c9ac91
|
||||
site mouse_direct main 18e444 f9400408 94e546db
|
||||
site mouse_direct main 18f108 bd4047ec 94e543a6
|
||||
# Keep native camera-distance easing when direct mouse orbit bypasses the stick integrators.
|
||||
site mouse_direct main 18ea34 b9418a88 94e5454f # orbit state: ldr w8, [x20, #0x188]
|
||||
site mouse_direct main 1d59460 b9004be8 94761acc
|
||||
# The aiming, scope, ability, horse and Purah Pad camera cameras: per axis, the float load the stick turn is computed
|
||||
# from (the gamepad's part only) and the load of the angle it is added to (plus the mouse's degrees), each a bl to page
|
||||
# +0xf80, told apart by the return address (runtime/mouse_direct.c camera_load, load_sites). Every enclosing function
|
||||
# saves x30 in its prologue, so the bl's link register write is harmless.
|
||||
site mouse_direct main 23beb94 bd4426c1 945c84fb # Aiming: ldr s1, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 23bebc8 bd415e61 945c84ee # Aiming: ldr s1, [x19, #0x15c] (pitch)
|
||||
site mouse_direct main 23bee2c bd4422c1 945c8455 # Aiming: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 23beeec bc5b83a1 945c8425 # Aiming: ldur s1, [x29, #-0x48] (yaw)
|
||||
site mouse_direct main 23c0a10 bd4422e0 945c7d5c # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23c0a1c bd4426eb 945c7d59 # AimingTelescope: ldr s11, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23c0b00 bd416260 945c7d20 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 23c0e80 bd405be1 945c7c40 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
|
||||
site mouse_direct main 23d2eec bd4422e8 945c3425 # SpecialPower: ldr s8, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23d2ef4 bd4426ee 945c3423 # SpecialPower: ldr s14, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23d3158 bd416260 945c338a # SpecialPower: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 23d34cc bc5b03a1 945c32ad # SpecialPower: ldur s1, [x29, #-0x50] (yaw)
|
||||
site mouse_direct main 23cb274 bd442288 945c5343 # Horse: ldr s8, [x20, #0x420] (stick x)
|
||||
site mouse_direct main 23cb2a0 bd406a61 945c5338 # Horse: ldr s1, [x19, #0x68] (yaw)
|
||||
site mouse_direct main 23cb3f8 bd4426a8 945c52e2 # Horse: ldr s8, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23cb424 bd406661 945c52d7 # Horse: ldr s1, [x19, #0x64] (pitch)
|
||||
site mouse_direct main 23d2f30 bd4422e8 945c3414 # SpecialPower offset mode: ldr s8, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23d2f5c bd41da62 945c3409 # SpecialPower offset mode: ldr s2, [x19, #0x1d8] (offset x)
|
||||
site mouse_direct main 23d2f6c bd4426e8 945c3405 # SpecialPower offset mode: ldr s8, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23d2f98 bd41de62 945c33fa # SpecialPower offset mode: ldr s2, [x19, #0x1dc] (offset y)
|
||||
site mouse_direct main 23d3210 bd400400 945c335c # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
|
||||
site mouse_direct main 23c2890 bd4426ad 945c75bc # CameraFinder: ldr s13, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23c28b4 bd405261 945c75b3 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
|
||||
site mouse_direct main 23c2934 bd4422ad 945c7593 # CameraFinder: ldr s13, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 23c2964 bd404be1 945c7587 # CameraFinder: ldr s1, [sp, #0x48] (yaw)
|
||||
# The other player cameras (runtime/mouse_direct.c load_sites; a ROLE_WATCH load stays the game's and only names its
|
||||
# mode in the log when the mouse was in it):
|
||||
site mouse_direct main 23c3dd0 bd442301 945c706c # ClimbObj: ldr s1, [x24, #0x420]
|
||||
site mouse_direct main 23c3dd8 bd409261 945c706a # ClimbObj: ldr s1, [x19, #0x90] (yaw)
|
||||
site mouse_direct main 23c3d64 bd442701 945c7087 # ClimbObj: ldr s1, [x24, #0x424]
|
||||
# ClimbObj's pitch is a pre-indexed load, ldr s1, [x22, #0x88]!, whose write-back the code after it needs (ldp and
|
||||
# str through x22). A host call cannot write x22: the lifter keeps X19-X29 in locals across a call. So the mov before
|
||||
# it adds the 0x88 instead (x22 = x19 + 0x88) and the hook loads [x22] with no write-back.
|
||||
site mouse_direct main 23c3d5c aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
|
||||
site mouse_direct main 23c3d70 bc488ec1 945c7084 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
|
||||
site mouse_direct main 23da694 bd4422c1 945c163b # Wakeboard: ldr s1, [x22, #0x420]
|
||||
site mouse_direct main 23da698 bd4426c0 945c163a # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 23da840 bd40da60 945c15d0 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
|
||||
site mouse_direct main 23da894 bd4017e0 945c15bb # Wakeboard: ldr s0, [sp, #0x14] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 23daed8 bd403fe9 945c142a # Wakeboard: ldr s9, [sp, #0x3c] (yaw)
|
||||
site mouse_direct main 23dced8 bd4422cd 945c0c2a # WaterfallClimb: ldr s13, [x22, #0x420]
|
||||
site mouse_direct main 23dd070 b85cc3a8 945c0bc4 # WaterfallClimb: ldur w8, [x29, #-0x34] (yaw)
|
||||
site mouse_direct main 23dd008 bd4426c9 945c0bde # WaterfallClimb: ldr s9, [x22, #0x424]
|
||||
site mouse_direct main 23d4b14 bd44228c 945c2d1b # Tail: ldr s12, [x20, #0x420]
|
||||
site mouse_direct main 23d4b1c bd44268d 945c2d19 # Tail (climbing): ldr s13, [x20, #0x424] (stick y)
|
||||
site mouse_direct main 23d4f34 bd40c260 945c2c13 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
|
||||
site mouse_direct main 23d5028 bd40b661 945c2bd6 # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
|
||||
site mouse_direct main 23d756c bd442300 945c2285 # LockOn / Target: ldr s0, [x24, #0x420]
|
||||
site mouse_direct main 23d7594 bd442301 945c227b # LockOn / Target: ldr s1, [x24, #0x420] (stick x)
|
||||
site mouse_direct main 23d75b4 bd4023e0 945c2273 # LockOn / Target: ldr s0, [sp, #0x20] (yaw turn)
|
||||
site mouse_direct main 23d75c4 bd4023e9 945c226f # LockOn / Target: ldr s9, [sp, #0x20] (yaw turn)
|
||||
site mouse_direct main 23d7998 bd442701 945c217a # LockOn / Target: ldr s1, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 23d79b8 bd4023e0 945c2172 # LockOn / Target: ldr s0, [sp, #0x20] (pitch turn)
|
||||
site mouse_direct main 23d79c8 bd4023ea 945c216e # LockOn / Target: ldr s10, [sp, #0x20] (pitch turn)
|
||||
site mouse_direct main 23cecb4 bd4426a9 945c44b3 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23cecf8 bd419a61 945c44a2 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
|
||||
site mouse_direct main 23ced0c bd4422a9 945c449d # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 23ced4c bd419e61 945c448d # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
|
||||
site mouse_direct main 23cc4b4 bd44232b 945c4eb3 # HorseLockOnEmpty: ldr s11, [x25, #0x420]
|
||||
site mouse_direct main 23c54d4 bd4422eb 945c6aab # EventMultiTalk: ldr s11, [x23, #0x420]
|
||||
site mouse_direct main 23c542c bd4426eb 945c6ad5 # EventMultiTalk: ldr s11, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23c546c bd405e61 945c6ac5 # EventMultiTalk: ldr s1, [x19, #0x5c] (pitch)
|
||||
site mouse_direct main 23c5554 bd406661 945c6a8b # EventMultiTalk: ldr s1, [x19, #0x64] (yaw)
|
||||
site mouse_direct main 23c90ac bd442709 945c5bb5 # EventTalkManual: ldr s9, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 23c90d8 bd407661 945c5baa # EventTalkManual: ldr s1, [x19, #0x74] (pitch)
|
||||
site mouse_direct main 23c9140 bd442309 945c5b90 # EventTalkManual: ldr s9, [x24, #0x420] (stick x)
|
||||
site mouse_direct main 23c9170 bd407e61 945c5b84 # EventTalkManual: ldr s1, [x19, #0x7c] (yaw)
|
||||
site mouse_direct main 23d0100 bd4422c1 945c3fa0 # ShieldSurfing: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 23d0104 bd4426c0 945c3f9f # ShieldSurfing: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 23d02ac bd40de60 945c3f35 # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
|
||||
site mouse_direct main 23d0a34 bd4013e0 945c3d53 # ShieldSurfing: ldr s0, [sp, #0x10] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 23d087c bd403fe9 945c3dc1 # ShieldSurfing: ldr s9, [sp, #0x3c] (yaw)
|
||||
|
||||
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
|
||||
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
|
||||
@@ -1001,3 +1086,19 @@ site cascade main 6fabb8 52a83408 @page:x8 b9404108 # mov w8, #20.0 -> ldr w8
|
||||
site cascade main 6fac40 52a84408 @page:x8 b9404508 # mov w8, #40.0 -> ldr w8, [page+68]
|
||||
site cascade main 6facc8 52a85408 @page:x8 b9404908 # mov w8, #80.0 -> ldr w8, [page+72]
|
||||
site cascade main 6fad4c 52a86408 @page:x8 b9404d08 # mov w8, #160.0 -> ldr w8, [page+76]
|
||||
|
||||
# Climbing (issues #35, #276, #1001): the climb controller's velocity step (main+0x1f49754, shared by wall and ladder
|
||||
# climbing, climb jumps and climb-overs) multiplies the requested velocity by a factor that makes up for a movement loss
|
||||
# growing with the frame rate. Below ratio 1 the factor is 1.272 + 1.189 (1 - r) clamped to [1, 2] (r = VFR +0, the
|
||||
# 30 Hz ratio times the time scale): Nintendo's fit for 30 and 60 fps, flat at 2 from about 77 fps, so Link climbs at
|
||||
# 63% of the speed at 120 fps and 34% at 240. Below r = 0.5 take g(r) = 0.66265 / r + 0.4982 + 0.0848 r instead,
|
||||
# fitted to the replayed ladder climb at 120 and 240 fps and equal to the shipping 1.8665 at r = 0.5; at most 8. The
|
||||
# site computes g with 16 * max(r - 0.5, 0) subtracted, so g stays under the shipping factor for r >= 0.5, and folds it
|
||||
# into the existing clamp without flags: s0 = max(s0, g) (the value clamped), s1 = max(2, g) (the bound). r >= 0.5
|
||||
# keeps the shipping factor bit for bit. x8 is dead after the constant load at 0x1f497e8, s2-s4 are loaded again at
|
||||
# 0x1f49844 before any read.
|
||||
# g takes u = max(page +0, r): the patch page's 30 Hz frame ratio without the time scale (runtime/frame_rate.c), so
|
||||
# slow motion at 30 or 60 fps keeps the shipping factor and only a real frame rate above 60 raises it; the runtime
|
||||
# switches the set off without the real-time step, where the page is not written.
|
||||
set climb TOTK_CLIMB_FIX 1
|
||||
site climb main 1f497f4 1e201001 @page:x8 bd400104 1e286884 52946dc8 72a7e528 1e270102 1e241842 52828248 72a7dfe8 1e270103 1e232842 529573e8 72a7b5a8 1e270103 1f040862 1e2a3883 2f00e404 1e246863 1e261004 1f048862 1e241004 1e247842 1e226800 1e201001 1e226821 # fmov s1, #2.0 -> s2 = g(u); s0 = max(s0, s2); s1 = max(2.0, s2)
|
||||
@@ -0,0 +1,111 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/bl_deswizzle.comp; do not edit.
|
||||
// source sha256 bd2065290e315e85d2fcdb609d812c15ba9a109d7506b3105850a5860677b0ef
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t bl_deswizzle_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000000d0, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0007000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000001f, 0x00060010,
|
||||
0x00000004, 0x00000011, 0x00000040, 0x00000001, 0x00000001, 0x00040047, 0x0000000b, 0x0000000b,
|
||||
0x0000001a, 0x00030047, 0x00000010, 0x00000002, 0x00050048, 0x00000010, 0x00000000, 0x00000023,
|
||||
0x00000000, 0x00050048, 0x00000010, 0x00000001, 0x00000023, 0x00000004, 0x00050048, 0x00000010,
|
||||
0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x00000010, 0x00000003, 0x00000023, 0x0000000c,
|
||||
0x00050048, 0x00000010, 0x00000004, 0x00000023, 0x00000010, 0x00050048, 0x00000010, 0x00000005,
|
||||
0x00000023, 0x00000014, 0x00050048, 0x00000010, 0x00000006, 0x00000023, 0x00000018, 0x00050048,
|
||||
0x00000010, 0x00000007, 0x00000023, 0x0000001c, 0x00050048, 0x00000010, 0x00000008, 0x00000023,
|
||||
0x00000020, 0x00050048, 0x00000010, 0x00000009, 0x00000023, 0x00000024, 0x00050048, 0x00000010,
|
||||
0x0000000a, 0x00000023, 0x00000028, 0x00040047, 0x0000001f, 0x0000000b, 0x0000001b, 0x00040047,
|
||||
0x000000ad, 0x00000006, 0x00000010, 0x00030047, 0x000000ae, 0x00000002, 0x00040048, 0x000000ae,
|
||||
0x00000000, 0x00000019, 0x00050048, 0x000000ae, 0x00000000, 0x00000023, 0x00000000, 0x00030047,
|
||||
0x000000b0, 0x00000019, 0x00040047, 0x000000b0, 0x00000021, 0x00000001, 0x00040047, 0x000000b0,
|
||||
0x00000022, 0x00000000, 0x00040047, 0x000000c1, 0x00000006, 0x00000010, 0x00030047, 0x000000c2,
|
||||
0x00000002, 0x00040048, 0x000000c2, 0x00000000, 0x00000018, 0x00050048, 0x000000c2, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00030047, 0x000000c4, 0x00000018, 0x00040047, 0x000000c4, 0x00000021,
|
||||
0x00000000, 0x00040047, 0x000000c4, 0x00000022, 0x00000000, 0x00040047, 0x000000cd, 0x0000000b,
|
||||
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
|
||||
0x00000020, 0x00000000, 0x00040017, 0x00000009, 0x00000006, 0x00000003, 0x00040020, 0x0000000a,
|
||||
0x00000001, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000001, 0x0004002b, 0x00000006,
|
||||
0x0000000c, 0x00000001, 0x00040020, 0x0000000d, 0x00000001, 0x00000006, 0x000d001e, 0x00000010,
|
||||
0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006,
|
||||
0x00000006, 0x00000006, 0x00000006, 0x00040020, 0x00000011, 0x00000009, 0x00000010, 0x0004003b,
|
||||
0x00000011, 0x00000012, 0x00000009, 0x00040015, 0x00000013, 0x00000020, 0x00000001, 0x0004002b,
|
||||
0x00000013, 0x00000014, 0x0000000a, 0x00040020, 0x00000015, 0x00000009, 0x00000006, 0x0004002b,
|
||||
0x00000006, 0x00000019, 0x00000000, 0x0004002b, 0x00000006, 0x0000001d, 0x00000040, 0x0004003b,
|
||||
0x0000000a, 0x0000001f, 0x00000001, 0x0004002b, 0x00000013, 0x00000024, 0x00000002, 0x00020014,
|
||||
0x00000027, 0x0004002b, 0x00000013, 0x0000002d, 0x00000006, 0x0004002b, 0x00000013, 0x00000032,
|
||||
0x00000007, 0x0004002b, 0x00000006, 0x00000037, 0x00000020, 0x0004002b, 0x00000006, 0x00000048,
|
||||
0x00000005, 0x0004002b, 0x00000006, 0x0000004c, 0x0000001f, 0x0004002b, 0x00000013, 0x0000004f,
|
||||
0x00000008, 0x0004002b, 0x00000013, 0x00000052, 0x00000009, 0x0004002b, 0x00000006, 0x0000006a,
|
||||
0x00000004, 0x0004002b, 0x00000006, 0x0000006f, 0x00000002, 0x0004002b, 0x00000006, 0x00000078,
|
||||
0x00000008, 0x0004002b, 0x00000006, 0x00000083, 0x00000003, 0x0004002b, 0x00000013, 0x00000093,
|
||||
0x00000003, 0x0004002b, 0x00000013, 0x0000009b, 0x00000004, 0x0004002b, 0x00000013, 0x000000a4,
|
||||
0x00000005, 0x00040017, 0x000000ac, 0x00000006, 0x00000004, 0x0003001d, 0x000000ad, 0x000000ac,
|
||||
0x0003001e, 0x000000ae, 0x000000ad, 0x00040020, 0x000000af, 0x0000000c, 0x000000ae, 0x0004003b,
|
||||
0x000000af, 0x000000b0, 0x0000000c, 0x0004002b, 0x00000013, 0x000000b1, 0x00000000, 0x0004002b,
|
||||
0x00000013, 0x000000b2, 0x00000001, 0x0003001d, 0x000000c1, 0x000000ac, 0x0003001e, 0x000000c2,
|
||||
0x000000c1, 0x00040020, 0x000000c3, 0x0000000c, 0x000000c2, 0x0004003b, 0x000000c3, 0x000000c4,
|
||||
0x0000000c, 0x00040020, 0x000000c9, 0x0000000c, 0x000000ac, 0x0006002c, 0x00000009, 0x000000cd,
|
||||
0x0000001d, 0x0000000c, 0x0000000c, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x000300f7, 0x000000ce, 0x00000000, 0x000300fb, 0x00000019, 0x000000cf,
|
||||
0x000200f8, 0x000000cf, 0x00050041, 0x0000000d, 0x0000000e, 0x0000000b, 0x0000000c, 0x0004003d,
|
||||
0x00000006, 0x0000000f, 0x0000000e, 0x00050041, 0x00000015, 0x00000016, 0x00000012, 0x00000014,
|
||||
0x0004003d, 0x00000006, 0x00000017, 0x00000016, 0x00050084, 0x00000006, 0x00000018, 0x0000000f,
|
||||
0x00000017, 0x00050041, 0x0000000d, 0x0000001a, 0x0000000b, 0x00000019, 0x0004003d, 0x00000006,
|
||||
0x0000001b, 0x0000001a, 0x00050080, 0x00000006, 0x0000001c, 0x00000018, 0x0000001b, 0x00050084,
|
||||
0x00000006, 0x0000001e, 0x0000001c, 0x0000001d, 0x00050041, 0x0000000d, 0x00000020, 0x0000001f,
|
||||
0x00000019, 0x0004003d, 0x00000006, 0x00000021, 0x00000020, 0x00050080, 0x00000006, 0x00000022,
|
||||
0x0000001e, 0x00000021, 0x00050041, 0x00000015, 0x00000025, 0x00000012, 0x00000024, 0x0004003d,
|
||||
0x00000006, 0x00000026, 0x00000025, 0x000500ae, 0x00000027, 0x00000028, 0x00000022, 0x00000026,
|
||||
0x000300f7, 0x0000002a, 0x00000000, 0x000400fa, 0x00000028, 0x00000029, 0x0000002a, 0x000200f8,
|
||||
0x00000029, 0x000200f9, 0x000000ce, 0x000200f8, 0x0000002a, 0x00050041, 0x00000015, 0x0000002e,
|
||||
0x00000012, 0x0000002d, 0x0004003d, 0x00000006, 0x0000002f, 0x0000002e, 0x000500c4, 0x00000006,
|
||||
0x00000030, 0x0000000c, 0x0000002f, 0x00050041, 0x00000015, 0x00000033, 0x00000012, 0x00000032,
|
||||
0x0004003d, 0x00000006, 0x00000034, 0x00000033, 0x000500c4, 0x00000006, 0x00000035, 0x0000000c,
|
||||
0x00000034, 0x00050080, 0x00000006, 0x0000003c, 0x0000002f, 0x00000034, 0x000500c4, 0x00000006,
|
||||
0x0000003d, 0x00000037, 0x0000003c, 0x00050086, 0x00000006, 0x00000041, 0x00000022, 0x0000003d,
|
||||
0x00050089, 0x00000006, 0x00000045, 0x00000022, 0x0000003d, 0x000500c2, 0x00000006, 0x00000049,
|
||||
0x00000045, 0x00000048, 0x000500c7, 0x00000006, 0x0000004d, 0x00000045, 0x0000004c, 0x00050041,
|
||||
0x00000015, 0x00000050, 0x00000012, 0x0000004f, 0x0004003d, 0x00000006, 0x00000051, 0x00000050,
|
||||
0x00050041, 0x00000015, 0x00000053, 0x00000012, 0x00000052, 0x0004003d, 0x00000006, 0x00000054,
|
||||
0x00000053, 0x00050084, 0x00000006, 0x00000055, 0x00000051, 0x00000054, 0x00050086, 0x00000006,
|
||||
0x00000059, 0x00000041, 0x00000055, 0x00050089, 0x00000006, 0x0000005d, 0x00000041, 0x00000055,
|
||||
0x00050086, 0x00000006, 0x00000062, 0x0000005d, 0x00000051, 0x00050089, 0x00000006, 0x00000067,
|
||||
0x0000005d, 0x00000051, 0x00050084, 0x00000006, 0x0000006b, 0x00000067, 0x0000006a, 0x000500c2,
|
||||
0x00000006, 0x0000006d, 0x0000004d, 0x0000006a, 0x000500c7, 0x00000006, 0x0000006e, 0x0000006d,
|
||||
0x0000000c, 0x00050084, 0x00000006, 0x00000070, 0x0000006e, 0x0000006f, 0x00050080, 0x00000006,
|
||||
0x00000071, 0x0000006b, 0x00000070, 0x000500c2, 0x00000006, 0x00000073, 0x0000004d, 0x0000000c,
|
||||
0x000500c7, 0x00000006, 0x00000074, 0x00000073, 0x0000000c, 0x00050080, 0x00000006, 0x00000075,
|
||||
0x00000071, 0x00000074, 0x00050084, 0x00000006, 0x00000079, 0x00000062, 0x00000078, 0x00050084,
|
||||
0x00000006, 0x0000007b, 0x00000079, 0x00000030, 0x00050089, 0x00000006, 0x0000007e, 0x00000049,
|
||||
0x00000030, 0x00050084, 0x00000006, 0x0000007f, 0x0000007e, 0x00000078, 0x00050080, 0x00000006,
|
||||
0x00000080, 0x0000007b, 0x0000007f, 0x000500c2, 0x00000006, 0x00000082, 0x0000004d, 0x0000006f,
|
||||
0x000500c7, 0x00000006, 0x00000084, 0x00000082, 0x00000083, 0x00050084, 0x00000006, 0x00000085,
|
||||
0x00000084, 0x0000006f, 0x00050080, 0x00000006, 0x00000086, 0x00000080, 0x00000085, 0x000500c7,
|
||||
0x00000006, 0x00000088, 0x00000045, 0x0000000c, 0x00050080, 0x00000006, 0x00000089, 0x00000086,
|
||||
0x00000088, 0x00050084, 0x00000006, 0x0000008d, 0x00000059, 0x00000035, 0x00050086, 0x00000006,
|
||||
0x00000090, 0x00000049, 0x00000030, 0x00050080, 0x00000006, 0x00000091, 0x0000008d, 0x00000090,
|
||||
0x00050041, 0x00000015, 0x00000094, 0x00000012, 0x00000093, 0x0004003d, 0x00000006, 0x00000095,
|
||||
0x00000094, 0x000500ae, 0x00000027, 0x00000096, 0x00000075, 0x00000095, 0x000400a8, 0x00000027,
|
||||
0x00000097, 0x00000096, 0x000300f7, 0x00000099, 0x00000000, 0x000400fa, 0x00000097, 0x00000098,
|
||||
0x00000099, 0x000200f8, 0x00000098, 0x00050041, 0x00000015, 0x0000009c, 0x00000012, 0x0000009b,
|
||||
0x0004003d, 0x00000006, 0x0000009d, 0x0000009c, 0x000500ae, 0x00000027, 0x0000009e, 0x00000089,
|
||||
0x0000009d, 0x000200f9, 0x00000099, 0x000200f8, 0x00000099, 0x000700f5, 0x00000027, 0x0000009f,
|
||||
0x00000096, 0x0000002a, 0x0000009e, 0x00000098, 0x000400a8, 0x00000027, 0x000000a0, 0x0000009f,
|
||||
0x000300f7, 0x000000a2, 0x00000000, 0x000400fa, 0x000000a0, 0x000000a1, 0x000000a2, 0x000200f8,
|
||||
0x000000a1, 0x00050041, 0x00000015, 0x000000a5, 0x00000012, 0x000000a4, 0x0004003d, 0x00000006,
|
||||
0x000000a6, 0x000000a5, 0x000500ae, 0x00000027, 0x000000a7, 0x00000091, 0x000000a6, 0x000200f9,
|
||||
0x000000a2, 0x000200f8, 0x000000a2, 0x000700f5, 0x00000027, 0x000000a8, 0x0000009f, 0x00000099,
|
||||
0x000000a7, 0x000000a1, 0x000300f7, 0x000000aa, 0x00000000, 0x000400fa, 0x000000a8, 0x000000a9,
|
||||
0x000000aa, 0x000200f8, 0x000000a9, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000aa, 0x00050041,
|
||||
0x00000015, 0x000000b3, 0x00000012, 0x000000b2, 0x0004003d, 0x00000006, 0x000000b4, 0x000000b3,
|
||||
0x00050041, 0x00000015, 0x000000b6, 0x00000012, 0x0000009b, 0x0004003d, 0x00000006, 0x000000b7,
|
||||
0x000000b6, 0x00050084, 0x00000006, 0x000000b8, 0x00000091, 0x000000b7, 0x00050080, 0x00000006,
|
||||
0x000000ba, 0x000000b8, 0x00000089, 0x00050084, 0x00000006, 0x000000bd, 0x000000ba, 0x00000095,
|
||||
0x00050080, 0x00000006, 0x000000be, 0x000000b4, 0x000000bd, 0x00050080, 0x00000006, 0x000000c0,
|
||||
0x000000be, 0x00000075, 0x00050041, 0x00000015, 0x000000c5, 0x00000012, 0x000000b1, 0x0004003d,
|
||||
0x00000006, 0x000000c6, 0x000000c5, 0x00050080, 0x00000006, 0x000000c8, 0x000000c6, 0x00000022,
|
||||
0x00060041, 0x000000c9, 0x000000ca, 0x000000c4, 0x000000b1, 0x000000c8, 0x0004003d, 0x000000ac,
|
||||
0x000000cb, 0x000000ca, 0x00060041, 0x000000c9, 0x000000cc, 0x000000b0, 0x000000b1, 0x000000c0,
|
||||
0x0003003e, 0x000000cc, 0x000000cb, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000ce, 0x000100fd,
|
||||
0x00010038,
|
||||
};
|
||||
@@ -307,6 +307,7 @@ void rt_frame_presented(const tex_t *t, uint64_t n) {
|
||||
fflush(stderr);
|
||||
fs_save_flush();
|
||||
vk_pipeline_save(); // no-op without the Vulkan back end
|
||||
playtime_save();
|
||||
prof_write();
|
||||
fflush(stderr);
|
||||
plat_exit(0);
|
||||
|
||||
+100
-28
@@ -7,6 +7,7 @@
|
||||
// cfg_get handed out stays valid) and cfg_save writes only the changed keys back, keeping the rest of the file.
|
||||
// Readers take no lock: an entry is complete before n_entries counts it, and value pointers are swapped atomically.
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -28,7 +29,8 @@ static PlatMutex set_lock = PLAT_MUTEX_INIT; // cfg_set, cfg_reset, cfg_save
|
||||
static int save_ok = 1; // the last cfg_save_to wrote (or had nothing to write)
|
||||
|
||||
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line, and gets a
|
||||
// description in totk.ini.example or docs/settings.md (tests/settings_doc_check.py checks both).
|
||||
static const char *const known[] = {
|
||||
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_OFF", "AUDIO_PREFILL_MS",
|
||||
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
||||
@@ -36,39 +38,41 @@ static const char *const known[] = {
|
||||
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
|
||||
"BIND_SHAKE",
|
||||
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
|
||||
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
|
||||
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLIMB_FIX", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
|
||||
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES",
|
||||
"DESC_TEMPLATE", "DESWIZZLE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "DYNAMIC_RES", "DYN_DELTA", "EXEFS", "FEEDBACK", "FIXED_RES_PATCH", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FRAME_QUEUE",
|
||||
"FREEZE_IDLE", "FREE_CORES",
|
||||
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
|
||||
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "KSPIN_US", "LANG", "LIB_PREBUILD",
|
||||
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
|
||||
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"MEM_RESERVE_MIB", "MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_ASPECT", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
|
||||
"MV_SURVEY",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCORE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_BENCH", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_ASYNC", "REC_END", "REC_SPIN_US", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_CHUNKS", "REPLAY_CHUNK_DRAWS", "REPLAY_SKEW_US", "REPLAY_STATE_CHECK", "REPLAY_WORKERS",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
||||
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
|
||||
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
|
||||
"SHADER_MEMO", "SHADOW_DEFER", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHADOW_SNAP", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
|
||||
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
|
||||
"SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"SWKBD_ANSWER", "SYNC_AHEAD",
|
||||
"SWKBD_TEST", "TEST_OOM", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_BLOCK_MIB", "TEX_HEAP_EXTRA_MB", "TEX_MEMO", "TEX_RELEASED_MIB", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UB_OVERLAY", "UB_OVERLAY_OUT", "UPSCALER", "VB_USED", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VMA_BLOCK_MIB", "VOLUME", "VRAM_HEADROOM", "VRAM_LIMIT_MIB", "VS_OUTS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
};
|
||||
|
||||
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
|
||||
static const struct { const char *key, *section; } sections[] = {
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"},
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"FRAME_QUEUE", "frame rate"},
|
||||
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
||||
{"CLIMB_FIX", "frame rate"},
|
||||
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
||||
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"SHARPNESS", "display"}, {"DYNAMIC_RES", "display"}, {"ASPECT", "display"}, {"MOVIE_ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"COLOR_PRESET", "display"},
|
||||
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
|
||||
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
|
||||
@@ -79,25 +83,26 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
|
||||
{"PCSS", "display"},
|
||||
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
|
||||
{"TEX_HEAP_EXTRA_MB", "mods"},
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
||||
{"STICK_SENS", "input"},
|
||||
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"MOUSE_AIM_SCALE", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
|
||||
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
|
||||
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
|
||||
{"BIND_RIGHT", "input"}, {"BIND_DOWN", "input"}, {"BIND_LSTICK_LEFT", "input"}, {"BIND_LSTICK_UP", "input"},
|
||||
{"BIND_LSTICK_RIGHT", "input"}, {"BIND_LSTICK_DOWN", "input"}, {"BIND_RSTICK_LEFT", "input"},
|
||||
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"}, {"BIND_SHAKE", "input"},
|
||||
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"FREE_CORES", "performance"},
|
||||
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"REPLAY_WORKERS", "performance"}, {"FREE_CORES", "performance"},
|
||||
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"TEX_RELEASED_MIB", "performance"}, {"TEX_BLOCK_MIB", "performance"}, {"VMA_BLOCK_MIB", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"VB_USED", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"SYNC_AHEAD", "performance"}, {"SHADOW_DEFER", "performance"}, {"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_MERGE", "diagnostics"}, {"WB_EXACT", "diagnostics"},
|
||||
@@ -115,11 +120,12 @@ static const CfgMeta metas[] = {
|
||||
{"VK_PRESENT", "mailbox", "mailbox|fifo|immediate", LIVE}, {"GPU_INDEX", "", NULL, RESTART},
|
||||
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
|
||||
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
|
||||
{"DYNAMIC_RES", "1", "0|1", LIVE}, // resolution.c res_dynres_live
|
||||
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
|
||||
{"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance|ultra_performance", LIVE},
|
||||
{"DLSS_PRESET", "default", "default|k|j", LIVE},
|
||||
{"DLSS_CHAR_MASK", "65", "0|35|65|100", LIVE}, {"FXAA", "1", "0|1", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE}, {"MOVIE_ASPECT", "keep", "keep|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
||||
{"ANISO", "game", "game|2|4|8|16", RESTART}, // vk_texture.c sampler_features
|
||||
// colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses
|
||||
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
|
||||
@@ -134,10 +140,10 @@ static const CfgMeta metas[] = {
|
||||
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
|
||||
{"PCSS", "0", "0..200", RESTART},
|
||||
// Game
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE}, {"FRAME_QUEUE", "0", "0|1", LIVE},
|
||||
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
|
||||
{"MODS_REBUILD", "auto", "auto|off", RESTART},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART}, {"CLIMB_FIX", "1", "0|1", RESTART},
|
||||
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR", RESTART},
|
||||
{"OPERATION_MODE", "docked", "docked|handheld", RESTART},
|
||||
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
|
||||
@@ -149,7 +155,7 @@ static const CfgMeta metas[] = {
|
||||
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", RESTART},
|
||||
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
|
||||
{"MOUSE_MODE", "direct", "stick|direct", LIVE},
|
||||
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
|
||||
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MOUSE_AIM_SCALE", "1", "0.1..3", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
|
||||
{"WALK_KEY", "none", NULL, LIVE}, {"WALK_SCALE", "0.51", "0.2..0.9", LIVE},
|
||||
{"MENU_LANG", "", "en|ru|pt-BR|zh-Hans", LIVE},
|
||||
{"AMIIBO_DIR", "", NULL, LIVE},
|
||||
@@ -159,6 +165,10 @@ static const CfgMeta metas[] = {
|
||||
{"DISCORD_IMAGE", "totk_512x512", NULL, LIVE},
|
||||
// Advanced: scheduling
|
||||
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
|
||||
{"REPLAY_WORKERS", "0", "0..16", RESTART}, {"REPLAY_CHUNKS", "0", "0..64", RESTART},
|
||||
{"REPLAY_CHUNK_DRAWS", "128", "1..100000", RESTART}, {"REPLAY_STATE_CHECK", "0", "0|1", RESTART},
|
||||
{"REPLAY_SKEW_US", "0", "0..100000", RESTART},
|
||||
{"SYNC_AHEAD", "0", "0|1", RESTART}, {"SHADOW_DEFER", "1", "0|1", RESTART},
|
||||
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
|
||||
// shaders and pipelines
|
||||
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "fullscreen", "strict|any|fullscreen", RESTART},
|
||||
@@ -166,10 +176,12 @@ static const CfgMeta metas[] = {
|
||||
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "1", "0|1", RESTART},
|
||||
// textures and memory
|
||||
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
|
||||
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
||||
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART},
|
||||
{"TEX_RELEASED_MIB", "256", "0..65536", RESTART}, {"TEX_BLOCK_MIB", "64", "0..1024", RESTART}, {"VMA_BLOCK_MIB", "64", "0..1024", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
||||
// render fixes
|
||||
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
|
||||
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
|
||||
{"VB_USED", "1", "0|1", RESTART},
|
||||
{"DESC_TEMPLATE", "1", "0|1", RESTART},
|
||||
{"PUSH_SKIP", "1", "0|1", RESTART}, {"SPLIT_DRAWS", "64", "0..1000", RESTART},
|
||||
{"SPLIT_US", "1000", "0..20000", RESTART},
|
||||
@@ -327,9 +339,68 @@ static const char *raw_get(const char *name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ---- the screen's shape (desktop): a primary display of another shape than 16:9 gives output_res a default of that
|
||||
// shape while neither the environment nor totk.ini sets it, so the first start fills the screen instead of a 16:9
|
||||
// picture between black bars. The sizes are the ones the installer lays the HUD out for (installer/steps.py
|
||||
// ASPECT_SIZES, ui/menu.cpp row_output_res): of the display's shape within 1%, the smallest at least 1080 high that fits
|
||||
// the display, else the largest that fits. A 16:9 display, an unknown one or one of another shape keeps 1920x1080.
|
||||
// Xbox and keyboard prompts have a folder for each of these sizes (romfs-patches-xbox-<w>x<h>, -xbox-label-<w>x<h>,
|
||||
// -pc-<w>x<h>, installer/steps.py Prepare): they keep 1920x1080 only when the one for the display's size is missing,
|
||||
// as in a source build without make_romfs_patches.py --prompts <style> --aspect or an install by an older setup,
|
||||
// where a default of another shape would trade the prompts the player chose for the Switch ones.
|
||||
static const unsigned SHAPED[][2] = {
|
||||
{2560, 1080}, {3440, 1440}, {3840, 1600}, {5120, 2160}, // 21:9
|
||||
{3840, 1080}, {5120, 1440}, // 32:9
|
||||
{1280, 800}, {1680, 1050}, {1920, 1200}, {2560, 1600}, // 16:10
|
||||
{1920, 1280}, {2160, 1440}, {2256, 1504}, {2880, 1920}, {3000, 2000}, // 3:2
|
||||
};
|
||||
static char screen_res[16]; // set once by cfg_init, "" for none
|
||||
// romfs-patches-xbox-, romfs-patches-xbox-label- and romfs-patches-pc-<screen_res> have a manifest (set once by
|
||||
// cfg_init)
|
||||
static const char *const screen_folders[3] = {"romfs-patches-xbox", "romfs-patches-xbox-label", "romfs-patches-pc"};
|
||||
static int screen_prompts[3];
|
||||
|
||||
// Whether the prompts in effect have their folder at screen_res. The same picks as input_map.c input_prompts_pick and
|
||||
// input_prompts_folder: an unknown or empty value is the Nintendo prompts, an unset one Xbox while the pad layout is
|
||||
// "xbox_prompts"; the Xbox prompts take romfs-patches-xbox with the Nintendo pad layout (or "xbox_prompts") and
|
||||
// romfs-patches-xbox-label with any other.
|
||||
static int shape_keeps_prompts(void) {
|
||||
const char *p = raw_get(PREFIX "PROMPTS"), *l = raw_get(PREFIX "PAD_LAYOUT");
|
||||
if (p && *p && !strcasecmp(p, "pc")) return screen_prompts[2];
|
||||
int xbox = p && *p ? !strcasecmp(p, "xbox") : l && !strcasecmp(l, "xbox_prompts");
|
||||
if (!xbox) return 1;
|
||||
int by_position = l && (!strcasecmp(l, "nintendo") || !strcasecmp(l, "xbox_prompts"));
|
||||
return screen_prompts[by_position ? 0 : 1];
|
||||
}
|
||||
|
||||
static void screen_default(void) {
|
||||
int sw, sh;
|
||||
if (!plat_screen_size(&sw, &sh)) return;
|
||||
double r = (double)sw / sh;
|
||||
if (fabs(r / (16.0 / 9.0) - 1) <= 0.01) return;
|
||||
int best = -1, fit = -1;
|
||||
for (int i = 0; i < (int)(sizeof SHAPED / sizeof *SHAPED); i++) {
|
||||
unsigned w = SHAPED[i][0], h = SHAPED[i][1];
|
||||
if (fabs((double)w / h / r - 1) > 0.01 || w > (unsigned)sw || h > (unsigned)sh) continue;
|
||||
if (fit < 0 || w > SHAPED[fit][0]) fit = i; // the largest that fits
|
||||
if (h >= 1080 && (best < 0 || w < SHAPED[best][0])) best = i;
|
||||
}
|
||||
if (best < 0) best = fit;
|
||||
if (best < 0) return;
|
||||
snprintf(screen_res, sizeof screen_res, "%ux%u", SHAPED[best][0], SHAPED[best][1]);
|
||||
for (int i = 0; i < 3; i++)
|
||||
screen_prompts[i] = romfs_shaped_prompts_ready(screen_folders[i], SHAPED[best][0], SHAPED[best][1]);
|
||||
if (!raw_get(PREFIX "OUTPUT_RES") && shape_keeps_prompts())
|
||||
rt_log("cfg: the display is %dx%d: output_res defaults to %s, its shape (totk.ini output_res sets another)", sw,
|
||||
sh, screen_res);
|
||||
}
|
||||
|
||||
const char *cfg_get(const char *name) {
|
||||
const char *v = raw_get(name);
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN) || !profile_of()) return v;
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN)) return v;
|
||||
if (!profile_of())
|
||||
return screen_res[0] && !strcasecmp(name + PREFIX_LEN, "OUTPUT_RES") && shape_keeps_prompts() ? screen_res
|
||||
: NULL;
|
||||
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
|
||||
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
|
||||
return NULL;
|
||||
@@ -704,6 +775,7 @@ static void load_ini(void) {
|
||||
|
||||
void cfg_init(void) {
|
||||
load_ini();
|
||||
if (!profile_of()) screen_default();
|
||||
if (profile_of())
|
||||
rt_log("cfg: device profile Steam Deck: defaults for the keys totk.ini does not set (TOTK_DEVICE_PROFILE=desktop "
|
||||
"turns it off)");
|
||||
|
||||
@@ -230,6 +230,16 @@ int probe_cfg(void) {
|
||||
dn[KEY_MOUSE_R] = dn[KEY_Z] = 1;
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_Y | 1ull << B_ZL), "keys: right mouse button and Z both press ZL");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_T] = 1;
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_Y, "keys: T is Y too (the left button is A in the menus)");
|
||||
plat_setenv("TOTK_BIND_X", "key:t");
|
||||
input_map_configure();
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_X, "bind: bind_X = key:t takes T from Y's defaults");
|
||||
dn[KEY_T] = 0, dn[KEY_MOUSE_L] = 1;
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_Y, "bind: Y keeps the left mouse button");
|
||||
plat_setenv("TOTK_BIND_X", NULL);
|
||||
input_map_reset();
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_X] = dn[KEY_TAB] = dn[KEY_M] = dn[KEY_LCTRL] = dn[KEY_3] = 1;
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_ZR | 1ull << B_PLUS | 1ull << B_MINUS | 1ull << B_STICK_L | 1ull << B_RIGHT),
|
||||
"keys: X ZR, Tab Plus, M Minus, Left Ctrl stick click, 3 D-pad right");
|
||||
|
||||
+83
-9
@@ -10,8 +10,9 @@
|
||||
// (tools/cheats.py has the format). A cheat whose status line is missing or has another content hash was added or
|
||||
// edited after the build: a data cheat runs anyway, a code cheat waits for the setup to run again.
|
||||
//
|
||||
// Data cheats run in a VM after Atmosphere's dmnt cheat VM (opcodes 0-A; the 0xC.. and 0xF.. extensions are not
|
||||
// supported), every presented frame while their switch is on, with registers cleared before each cheat. Memory is the
|
||||
// Data cheats run in a VM after Atmosphere's dmnt cheat VM (opcodes 0-A and C0-C3; C4 and the 0xF.. extensions are not
|
||||
// supported), every presented frame while their switch is on, with registers and saved registers cleared before each
|
||||
// cheat; the static registers (C3) stay, shared by all cheats, as dmnt keeps them. Memory is the
|
||||
// guest's, mapped at its own addresses: main's image is written with its pages made writable for the write (read-only
|
||||
// data included, as dmnt writes through the debugger), everything else after mem_query says the guest may read or write
|
||||
// it. Buttons are input_poll's HidNpadButton bits, the same as the cheat format's key masks; they read 0 while the menu
|
||||
@@ -53,6 +54,7 @@ static int g_n, g_cap, g_vm; // g_vm: some cheat needs cheats_frame
|
||||
static char g_file[64]; // "<build id>.txt"
|
||||
static char g_dir[PLAT_PATH_MAX]; // the folder of totk.exe, with a trailing separator
|
||||
static int g_files; // mods whose file was read
|
||||
static uint64_t g_static[256]; // C3's static registers: 0x00-0x7F loaded by cheats, 0x80-0xFF stored by them
|
||||
static uint64_t g_main, g_main_size, g_code_end; // main's image; its code with the padding up to .rodata
|
||||
|
||||
static uint32_t fnv(const char *s, size_t n) {
|
||||
@@ -96,6 +98,11 @@ static int op_len(const uint32_t *w, int left) {
|
||||
case 5: case 7: n = 2; break;
|
||||
case 9: n = 1 + (((f >> 8) & 0xF) ? (width == 8 ? 2 : 1) : 0); break;
|
||||
case 0xA: n = ((f >> 8) & 0xF) == 2 || ((f >> 8) & 0xF) == 4 || ((f >> 8) & 0xF) == 5 ? 2 : 1; break;
|
||||
case 0xC: // C0TcSX..: an address word for operand types 0 and 2, the value (two words at width 8) for 4
|
||||
if (((f >> 24) & 0xF) != 0) n = 1;
|
||||
else if (((f >> 8) & 0xF) == 0 || ((f >> 8) & 0xF) == 2) n = 2;
|
||||
else n = ((f >> 8) & 0xF) == 4 ? (((f >> 20) & 0xF) == 8 ? 3 : 2) : 1;
|
||||
break;
|
||||
default: n = 1; break;
|
||||
}
|
||||
return n <= left ? n : 0;
|
||||
@@ -105,12 +112,15 @@ static int op_len(const uint32_t *w, int left) {
|
||||
static const char *vm_unsupported(const uint32_t *w, char *buf, int n) {
|
||||
uint32_t f = w[0];
|
||||
int t = f >> 28, width = (f >> 24) & 0xF;
|
||||
if (t > 0xA || (t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
|
||||
int ext = f >> 24; // 0xC0-0xC3 run; C0's width is at bits 20-23 and its operand type (at most 5) at bits 8-11
|
||||
if (ext == 0xC0) t = -1, width = (f >> 20) & 0xF;
|
||||
if ((t > 0xA && (ext < 0xC1 || ext > 0xC3)) || (t == -1 && ((f >> 8) & 0xF) > 5) ||
|
||||
(t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
|
||||
snprintf(buf, n, "opcode %08X is not supported", f);
|
||||
return buf;
|
||||
}
|
||||
if ((t <= 1 || t == 5 || t == 6 || t == 7 || t == 9 || t == 0xA) && width != 1 && width != 2 && width != 4 &&
|
||||
width != 8) {
|
||||
width != 8) { // C0 too (t -1)
|
||||
snprintf(buf, n, "opcode %08X has a bad width", f);
|
||||
return buf;
|
||||
}
|
||||
@@ -227,7 +237,9 @@ static int compare(int cond, uint64_t a, uint64_t b) {
|
||||
}
|
||||
|
||||
// From i (after a failed condition, or an else reached while running), the opcode to continue at: after the
|
||||
// matching else (else_ok) or end.
|
||||
// matching else (else_ok) or end. Conditions 1, 8 and C0 open a block, as in Atmosphere's dmnt
|
||||
// (stratosphere/dmnt/source/cheat/impl/dmnt_cheat_vm.cpp, CheatVirtualMachine::SkipConditionalBlock), which steps
|
||||
// over whole opcodes so that an operand word reading 20000000 ends nothing.
|
||||
static int skip(const uint32_t *w, int n, int i, int else_ok) {
|
||||
int depth = 0;
|
||||
while (i < n) {
|
||||
@@ -236,17 +248,41 @@ static int skip(const uint32_t *w, int n, int i, int else_ok) {
|
||||
uint32_t f = w[i];
|
||||
int t = f >> 28;
|
||||
i += len;
|
||||
if (t == 1 || t == 8) depth++;
|
||||
else if (t == 2 && ((f >> 24) & 0xF) == 0 && depth-- == 0) return i;
|
||||
else if (t == 2 && depth == 0 && else_ok) return i;
|
||||
if (t == 1 || t == 8 || f >> 24 == 0xC0) depth++;
|
||||
else if (t == 2 && ((f >> 24) & 0xF) == 1) { // else; any other 2x000000 ends, as dmnt decodes is_else
|
||||
if (depth == 0 && else_ok) return i;
|
||||
} else if (t == 2 && depth-- == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static uint64_t imm64(const uint32_t *w) { return (uint64_t)w[0] << 32 | w[1]; }
|
||||
|
||||
// C0TcSX..: if register S <cond c> operand X, all cut to width T; the operand as Atmosphere's dmnt reads it
|
||||
// (stratosphere/dmnt/source/cheat/impl/dmnt_cheat_vm.cpp, CheatVirtualMachine::DecodeNextOpcode and Execute,
|
||||
// BeginRegisterConditionalBlock). X: 0 region M + a, 1 region M + register r, 2 register R + a, 3 register R +
|
||||
// register r, 4 the value V, 5 register R. M, R at bits 4-7, r and the top bits of a at bits 0-3. A read the guest
|
||||
// cannot make compares as 0: dmnt ignores the read's result and compares the zero it started with.
|
||||
static int reg_cond(const uint32_t *w, const uint64_t *r) {
|
||||
uint32_t f = w[0];
|
||||
int width = (f >> 20) & 0xF, cond = (f >> 16) & 0xF, a4 = (f >> 4) & 0xF, a0 = f & 0xF;
|
||||
uint64_t src = masked(width, r[(f >> 12) & 0xF]), b = 0, addr;
|
||||
switch ((f >> 8) & 0xF) { // w[1] exists for types 0, 2 and 4 only (op_len)
|
||||
case 0: addr = region(a4) + (((uint64_t)a0 << 32) | w[1]); break;
|
||||
case 1: addr = region(a4) + r[a0]; break;
|
||||
case 2: addr = r[a4] + (((uint64_t)a0 << 32) | w[1]); break;
|
||||
case 3: addr = r[a4] + r[a0]; break;
|
||||
case 4: return compare(cond, src, width == 8 ? imm64(w + 1) : masked(width, w[1]));
|
||||
default: return compare(cond, src, masked(width, r[a4]));
|
||||
}
|
||||
if (!rd(addr, width, &b)) b = 0;
|
||||
return compare(cond, src, b);
|
||||
}
|
||||
|
||||
static void run(const Cheat *c, uint64_t keys) {
|
||||
uint64_t r[16] = {0};
|
||||
uint64_t r[16] = {0}, saved[16] = {0};
|
||||
int loop[16] = {0};
|
||||
const uint32_t *w = c->words;
|
||||
int n = c->n, budget = OPS_PER_RUN;
|
||||
@@ -323,6 +359,44 @@ static void run(const Cheat *c, uint64_t keys) {
|
||||
if (z) r[y] += (uint64_t)width;
|
||||
break;
|
||||
}
|
||||
case 0xC: // C1-C3 as dmnt's Execute runs SaveRestoreRegister(Mask) and ReadWriteStaticRegister
|
||||
switch (f >> 24) {
|
||||
case 0xC0:
|
||||
if (!reg_cond(w + i, r)) {
|
||||
i = skip(w, n, i + len, 1);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 0xC1: { // C10D0Sx0: x 0 register D = saved S, 1 saved D = register S, 2 saved D = 0,
|
||||
// 3 register D = 0
|
||||
int d = (f >> 16) & 0xF, s = (f >> 8) & 0xF, op = (f >> 4) & 0xF;
|
||||
if (op == 1) saved[d] = r[s];
|
||||
else if (op == 2) saved[d] = 0;
|
||||
else if (op == 3) r[d] = 0;
|
||||
else r[d] = saved[s]; // dmnt restores for any other x too
|
||||
break;
|
||||
}
|
||||
case 0xC2: { // C2x0XXXX: for each register in the mask X, x as C1 with D = S = that register
|
||||
int op = (f >> 20) & 0xF;
|
||||
for (int k = 0; k < 16; k++) {
|
||||
if (!(f >> k & 1)) continue;
|
||||
if (op == 1) saved[k] = r[k];
|
||||
else if (op == 2) saved[k] = 0;
|
||||
else if (op == 3) r[k] = 0;
|
||||
else r[k] = saved[k];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xC3: { // C3000XXx: static register XX < 0x80 into register x, else register x into it
|
||||
int s = (f >> 4) & 0xFF;
|
||||
if (s < 0x80) r[f & 0xF] = g_static[s];
|
||||
else g_static[s] = r[f & 0xF];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,11 @@ void frame_step_init(void) {
|
||||
rt_patch_disable(cloth);
|
||||
rt_log("frame rate: cloth clock off, it needs the real-time step (TOTK_REALTIME=0)");
|
||||
}
|
||||
int climb = rt_patch_set("climb"); // reads the page's frame ratio, written only with the real-time step
|
||||
if (climb >= 0 && !rt_patch_enabled(realtime) && rt_patch_enabled(climb)) {
|
||||
rt_patch_disable(climb);
|
||||
rt_log("frame rate: climb speed fix off, it needs the real-time step (TOTK_REALTIME=0)");
|
||||
}
|
||||
if (rt_patch_enabled(realtime)) {
|
||||
step_word = (_Atomic uint64_t *)rt_patch_page();
|
||||
step_interval = (_Atomic uint32_t *)((uint8_t *)rt_patch_page() + 8);
|
||||
|
||||
+26
-5
@@ -33,9 +33,20 @@
|
||||
#define VSYNC_HZ 60
|
||||
|
||||
// ---- queue lock
|
||||
// The recorder queues the parts it records and submits them later (vk_rec.c rec_submit): the first lock of a thread
|
||||
// waits until the parts queued before it are submitted, so whatever it puts on the queue comes after them, as when
|
||||
// the submitting thread submitted them itself. Not inside a held lock: the recorder takes this lock to submit.
|
||||
static PlatRMutex g_ql;
|
||||
void vk_queue_lock(void) { plat_rmutex_lock(&g_ql); }
|
||||
void vk_queue_unlock(void) { plat_rmutex_unlock(&g_ql); }
|
||||
static _Thread_local int ql_depth;
|
||||
void vk_queue_lock(void) {
|
||||
if (!ql_depth) rec_queue_wait();
|
||||
plat_rmutex_lock(&g_ql);
|
||||
ql_depth++;
|
||||
}
|
||||
void vk_queue_unlock(void) {
|
||||
ql_depth--;
|
||||
plat_rmutex_unlock(&g_ql);
|
||||
}
|
||||
|
||||
// ---- window state shared with the back end
|
||||
static VkSurfaceKHR g_surface;
|
||||
@@ -72,6 +83,7 @@ void gfx_window_closed(void) {
|
||||
rt_log("gfx: window closed; %s", restart ? "restarting" : "exiting");
|
||||
fs_save_flush();
|
||||
vk_pipeline_save();
|
||||
playtime_save();
|
||||
prof_write(); // plat_exit runs no atexit handlers
|
||||
if (restart) {
|
||||
plat_setenv("TOTK_REEXEC", NULL); // the new process counts its own address-space retries (memory.c)
|
||||
@@ -235,6 +247,7 @@ static void window_settings_live(void) {
|
||||
if (started && v == seen) return;
|
||||
seen = v;
|
||||
res_fov_live();
|
||||
res_dynres_live();
|
||||
const char *e = cfg_value("TOTK_VK_PRESENT");
|
||||
if (!started || strncmp(e, present, sizeof present - 1)) {
|
||||
snprintf(present, sizeof present, "%s", e);
|
||||
@@ -295,8 +308,7 @@ static void free_capture(void) {
|
||||
g_cap_valid = 0;
|
||||
}
|
||||
|
||||
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held.
|
||||
static int create_swapchain(void) {
|
||||
static int build_swapchain(void) {
|
||||
flip_locked(); // the acquired image goes out before its swapchain is replaced
|
||||
VkCtx *c = vk_ctx();
|
||||
VkSurfaceCapabilitiesKHR caps;
|
||||
@@ -364,10 +376,19 @@ static int create_swapchain(void) {
|
||||
}
|
||||
free_capture();
|
||||
vk_queue_unlock();
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held. The stale mark
|
||||
// goes before the surface is read: a resize during the build keeps it. The switch to fullscreen at start lands there
|
||||
// on the window thread, and the driver does not report the swapchain as out of date.
|
||||
static int create_swapchain(void) {
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
if (build_swapchain()) return 1;
|
||||
plat_xchg32(&g_dirty, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sync_barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags2 s0,
|
||||
VkAccessFlags2 a0, VkPipelineStageFlags2 s1, VkAccessFlags2 a1) {
|
||||
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = s0, .srcAccessMask = a0,
|
||||
|
||||
@@ -333,10 +333,12 @@ static void window_main(void *arg) {
|
||||
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
if (!background) {
|
||||
// Pending before the raise: the activation it gets is the first WM_ACTIVATEAPP, which takes the fullscreen
|
||||
// setting; set after it, fullscreen waited for the next activation and the game started in a window
|
||||
g_fs_pending = cfg_fullscreen();
|
||||
raise_window(g_hwnd); // a frontend's player may have no way to reach a window left behind
|
||||
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
|
||||
FlashWindowEx(&fw);
|
||||
g_fs_pending = cfg_fullscreen();
|
||||
}
|
||||
note_applied();
|
||||
refresh_rate_update(g_hwnd, 1);
|
||||
|
||||
@@ -36,6 +36,10 @@ void perf_end(int kind, uint64_t t0); // counts render-thread time onl
|
||||
void perf_replay(uint64_t t0); // replay (NVN -> Vulkan recording) time on any thread, waits inside it included
|
||||
void perf_replay_thread(void); // the calling thread replays for the render thread: perf_end counts its waits
|
||||
void perf_main_thread(void); // the calling thread is the guest main thread (TOTK_PROFILE_MAIN)
|
||||
// The calling thread is one every frame goes through: on a hybrid CPU it prefers the P-cores (main.c, TOTK_PCORE);
|
||||
// the first thread of each role logs it.
|
||||
enum { PCORE_MAIN, PCORE_RENDER, PCORE_REPLAY, PCORE_RECORDER, PCORE_PRESENT, PCORE_N };
|
||||
void perf_core_thread(int role);
|
||||
// Events per 60-frame window: graphics pipelines finished by the background workers, draws skipped while pending,
|
||||
// first texture uploads and re-uploads after CPU writes, draws skipped while a shader translates in the background,
|
||||
// draws that waited for their pending pipeline and got it, draws into a target the previous frame did not render that
|
||||
@@ -167,6 +171,26 @@ void mem_thaw(uint64_t a, uint64_t n); // makes the frozen r
|
||||
void mem_freeze_stats(uint64_t *frozen, uint64_t *freezes, uint64_t *fault_thaws, uint64_t *other_thaws);
|
||||
// regions frozen now; freezes and thaws since the last call
|
||||
void mem_kernel_free(void *p, uint64_t size); // TLR/TLS reuse after thread exit
|
||||
int mem_guest_readable(uint64_t a, uint64_t n); // [a, a+n) can be read without a fault: inside a loaded
|
||||
// module or the committed heap answered from the layout,
|
||||
// anything else by plat_mem_readable (an OS query)
|
||||
|
||||
// ---- short out-of-memory spells (oom.c)
|
||||
enum { RT_OOM_COMMIT, RT_OOM_STAGING, RT_OOM_IMAGE, RT_OOM_CHUNK, RT_OOM_SITES };
|
||||
extern volatile int32_t rt_oom_req; // set by rt_oom_request; vk_texture_new_submit takes it
|
||||
void rt_oom_request(void); // asks the Vulkan back end to give memory back at its next
|
||||
// submit (released textures, headroom blocks); any thread,
|
||||
// any lock held
|
||||
int rt_oom_wait(const char *what, uint64_t bytes, int (*retry)(void *arg), void *arg);
|
||||
// after a failed allocation: requests a release, then calls
|
||||
// retry every 10 ms for up to 30 s; 1 once retry returned 1,
|
||||
// 0 after the wait (the caller ends the game). Logs the
|
||||
// first 8 with the commit numbers
|
||||
void *rt_realloc_wait(void *old, size_t n, const char *what); // realloc, NULL only after rt_oom_wait gave up
|
||||
int rt_oom_forced(int site); // 1: fail this attempt at site (TOTK_TEST_OOM=<site>:<n>,
|
||||
// sites commit, staging, image, chunk; or rt_oom_force)
|
||||
void rt_oom_force(int site, int n); // probes: fail the next n attempts at site
|
||||
int rt_oom_forced_left(int site); // forced failures still to come at site
|
||||
|
||||
// ---- modules (loader.c)
|
||||
typedef struct {
|
||||
@@ -257,6 +281,12 @@ void cam_pack_served(void); // romfs.c serves the
|
||||
// ---- game resolution (resolution.c): TOTK_OUTPUT_RES, TOTK_RENDER_SCALE
|
||||
uint64_t res_heap_extra_bytes(void); // added to the application pool (mem_init), 2 MiB steps
|
||||
uint64_t res_heap_extra_kib(void); // added to the HeapDef texture heap and its parent; 0 = none
|
||||
uint64_t res_tex_heap_extra_bytes(void); // TOTK_TEX_HEAP_EXTRA_MB: added to the application pool and
|
||||
// the heap cap (mem_init), 2 MiB steps; 0 = none
|
||||
int res_heapdef_tex_grown(const void *in, size_t n); // the HeapDef table (.byml.zs) grows TextureHeap over the
|
||||
// shipped size (an HD texture pack's)
|
||||
int res_heapdef_plain(const void *in, size_t n); // a readable HeapDef table whose TextureHeap is not
|
||||
// grown (a content mod's)
|
||||
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); // the HeapDef table (.byml.zs) with
|
||||
// the grown heaps, malloc'd; 0 = cannot (logged)
|
||||
void res_heapdef_served(void); // romfs.c serves the grown table
|
||||
@@ -269,8 +299,13 @@ void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene
|
||||
// until res_init switched the sets on)
|
||||
int res_is_16x9(unsigned w, unsigned h); // within 1% of 16:9 (the test res_init and romfs.c share)
|
||||
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
|
||||
int romfs_shaped_ready(unsigned w, unsigned h); // romfs-patches-<w>x<h> has a manifest (romfs.c; works
|
||||
// before fs_init)
|
||||
int romfs_shaped_prompts_ready(const char *folder, unsigned w, unsigned h); // the same for <folder>-<w>x<h>, a
|
||||
// prompts' folder (romfs-patches-xbox, -xbox-label, -pc)
|
||||
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
|
||||
void res_fov_live(void); // TOTK_FOV to the camera (set fov); on a settings change
|
||||
void res_dynres_live(void); // TOTK_DYNAMIC_RES to the game (set fixres); on a settings change
|
||||
// ---- code patches (patches.c) and the frame step (frame_rate.c)
|
||||
void rt_patches_init(void); // after ldr_load: switches from the environment, the patch
|
||||
// page committed; logs the sets
|
||||
@@ -313,6 +348,10 @@ int probe_cfg(void); // cfg_probe.c
|
||||
int probe_dpi(void); // dpi_probe_win.c (Windows only)
|
||||
int probe_mods(void); // mods_probe.c
|
||||
void discord_init(void); // discord_rpc.c: the Rich Presence thread, game runs only
|
||||
void playtime_init(void); // playtime.c: the play time thread, game runs only
|
||||
void playtime_save(void); // playtime.c: writes <profile>/playtime.txt; at exit
|
||||
int playtime_get(uint64_t *total_s, uint64_t *session_s); // playtime.c: all runs and this one; 0 without a profile
|
||||
int probe_playtime(void); // playtime.c: the file, the ticks and the save
|
||||
int probe_discord(void); // discord_rpc.c
|
||||
int probe_loader(void), probe_kernel(void), probe_ipc(void), probe_services(void), probe_fs(void);
|
||||
int probe_gfx_abi(void); // gfx_abi_probe.c
|
||||
|
||||
+24
-5
@@ -35,7 +35,7 @@ typedef struct {
|
||||
const char *name;
|
||||
GuestFn fn;
|
||||
uint64_t original; // observers: the guest definition rt_original calls
|
||||
uint64_t calls;
|
||||
uint64_t calls; // with TOTK_TRACE=host only (hostfn_report)
|
||||
int blocking; // runs with the virtual core given up (hostfn_blocking)
|
||||
} Slot;
|
||||
static Slot slots[MAX_HOSTFN];
|
||||
@@ -44,6 +44,21 @@ static RtHostEntry added[MAX_ADDED];
|
||||
static int nadded;
|
||||
static int sealed;
|
||||
|
||||
// The call count (hostfn_calls): one counter per calling thread, each on its own cache line, written only by that
|
||||
// thread. A locked add on a per-function counter beside the read-mostly slot fields made the game threads share and
|
||||
// bounce those lines on every host call (1.4.3 town: about 1% of the game threads' time in hostfn_entry).
|
||||
#define MAX_COUNTERS 4096
|
||||
typedef struct { _Alignas(64) uint64_t n; } Counter;
|
||||
static Counter counters[MAX_COUNTERS];
|
||||
static uint32_t ncounters;
|
||||
static uint64_t shared_calls; // the threads past MAX_COUNTERS, counted with an atomic add
|
||||
static __thread uint64_t *thread_calls;
|
||||
|
||||
__attribute__((noinline)) static uint64_t *counter_new(void) {
|
||||
uint32_t i = __atomic_fetch_add(&ncounters, 1, __ATOMIC_RELAXED);
|
||||
return thread_calls = i < MAX_COUNTERS ? &counters[i].n : &shared_calls;
|
||||
}
|
||||
|
||||
static GuestFn table_find(const RtHostEntry *start, const RtHostEntry *stop, const char *name) {
|
||||
__asm__("" : "+r"(start), "+r"(stop)); // separate objects: keep the compiler from reasoning about them
|
||||
for (const RtHostEntry *p = start; p < stop; p++)
|
||||
@@ -146,8 +161,11 @@ static void hostfn_entry(Cpu *c) {
|
||||
uint64_t i = (c->pc - RT_HOSTFN_BASE) >> 4;
|
||||
if (__builtin_expect((c->pc & 15) || i >= (uint64_t)nslots, 0)) bad_entry(c);
|
||||
Slot *s = &slots[i];
|
||||
uint64_t n = __atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED);
|
||||
if (__builtin_expect(!n && (rt_trace & RT_TRACE_HOSTCALL), 0)) {
|
||||
uint64_t *k = thread_calls;
|
||||
if (__builtin_expect(!k, 0)) k = counter_new();
|
||||
if (__builtin_expect(k == &shared_calls, 0)) __atomic_fetch_add(k, 1, __ATOMIC_RELAXED);
|
||||
else __atomic_store_n(k, *k + 1, __ATOMIC_RELAXED);
|
||||
if (__builtin_expect(rt_trace & RT_TRACE_HOSTCALL, 0) && !__atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED)) {
|
||||
char lr[96];
|
||||
rt_log("host call %s from %s", s->name, ldr_describe(c->x[30] - 4, lr, sizeof lr));
|
||||
}
|
||||
@@ -178,8 +196,9 @@ void hostfn_seal(void) {
|
||||
}
|
||||
|
||||
uint64_t hostfn_calls(void) {
|
||||
uint64_t n = 0;
|
||||
for (int i = 0; i < nslots; i++) n += __atomic_load_n(&slots[i].calls, __ATOMIC_RELAXED);
|
||||
uint32_t m = __atomic_load_n(&ncounters, __ATOMIC_RELAXED);
|
||||
uint64_t n = __atomic_load_n(&shared_calls, __ATOMIC_RELAXED);
|
||||
for (uint32_t i = 0; i < m && i < MAX_COUNTERS; i++) n += __atomic_load_n(&counters[i].n, __ATOMIC_RELAXED);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
+54
-3
@@ -541,6 +541,35 @@ static int32_t shake_stick(int held) {
|
||||
return (now - from) / SHAKE_SIDE_NS % 2 ? STICK_MAX : -STICK_MAX;
|
||||
}
|
||||
|
||||
// The scroll wheel on the Purah Pad's map, which zooms with the right stick's y (up: in): each notch is a full-tilt
|
||||
// pulse of MAP_ZOOM_PULSE_NS on that stick with MAP_ZOOM_GAP_NS of neutral after it, so a step of the zoom and a
|
||||
// held zoom both see one notch as one push. A fast scroll's notches wait their turn (at most MAP_ZOOM_QUEUE), a turn the
|
||||
// other way drops them. Returns the stick y for this poll, 0 between pulses and off the map. Under lock.
|
||||
#define MAP_ZOOM_PULSE_NS 100000000ull
|
||||
#define MAP_ZOOM_GAP_NS 50000000ull
|
||||
#define MAP_ZOOM_QUEUE 4
|
||||
static int32_t map_zoom_stick(int32_t notches, int on_map) {
|
||||
static int32_t queued, dir;
|
||||
static uint64_t until; // the end of the pulse (dir != 0) or of the neutral after it
|
||||
uint64_t now = plat_now_ns();
|
||||
if (!on_map) { queued = dir = 0, until = 0; return 0; }
|
||||
if (notches) {
|
||||
if (queued && (notches > 0) != (queued > 0)) queued = 0;
|
||||
queued += notches;
|
||||
queued = queued > MAP_ZOOM_QUEUE ? MAP_ZOOM_QUEUE : queued < -MAP_ZOOM_QUEUE ? -MAP_ZOOM_QUEUE : queued;
|
||||
}
|
||||
if (dir) {
|
||||
if (now < until) return dir * STICK_MAX;
|
||||
dir = 0, until = now + MAP_ZOOM_GAP_NS;
|
||||
return 0;
|
||||
}
|
||||
if (now < until || !queued) return 0;
|
||||
dir = queued > 0 ? 1 : -1;
|
||||
queued -= dir;
|
||||
until = now + MAP_ZOOM_PULSE_NS;
|
||||
return dir * STICK_MAX;
|
||||
}
|
||||
|
||||
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
|
||||
static int pad_physical(uint8_t pd[PAD_COUNT]) {
|
||||
if ((!pad || !SDL_GamepadConnected(pad)) && plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
|
||||
@@ -579,6 +608,8 @@ int input_pad_stick_percent(double pct[2]) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
static uint64_t g_buttons_last; // the merged buttons of the last poll (input_buttons_last)
|
||||
|
||||
static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
|
||||
uint64_t b = 0;
|
||||
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
|
||||
@@ -592,17 +623,22 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
if (ikb_foreground()) {
|
||||
uint8_t down[KEY_COUNT] = {0};
|
||||
// Mouse buttons count only while the cursor is captured, and a button already held when the capture began
|
||||
// (the click that refocused the window) is ignored until it is released.
|
||||
// (the click that refocused the window) is ignored until it is released. While a game menu has the pointer
|
||||
// (menu_cursor.c) and it is over the window, the left and right buttons are that menu's A and B, and the
|
||||
// middle and side buttons keep their bindings: bound to L and R they turn the menu's tabs.
|
||||
static int was_captured;
|
||||
static uint8_t mouse_ignore[KEY_COUNT];
|
||||
int cap = ikb_captured();
|
||||
float pointer_x, pointer_y;
|
||||
int menu_pointer = !cap && menu_cursor_pointer() && ikb_pointer(&pointer_x, &pointer_y);
|
||||
for (int k = 1; k < KEY_COUNT; k++) {
|
||||
down[k] = ikb_down(k);
|
||||
kbm_used |= down[k];
|
||||
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
|
||||
if (cap && !was_captured) mouse_ignore[k] = down[k];
|
||||
if (!down[k]) mouse_ignore[k] = 0;
|
||||
if (!cap || mouse_ignore[k]) down[k] = 0;
|
||||
int counts = cap || (menu_pointer && k != KEY_MOUSE_L && k != KEY_MOUSE_R);
|
||||
if (!counts || mouse_ignore[k]) down[k] = 0;
|
||||
}
|
||||
was_captured = cap;
|
||||
b |= input_map_buttons(down);
|
||||
@@ -617,10 +653,22 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
int selector = b & (1ull << B_L) ? 1 : b & (1ull << B_UP) ? 2 :
|
||||
b & (1ull << B_LEFT) ? 3 : b & (1ull << B_RIGHT) ? 4 : 0;
|
||||
// Only while the wheel or a strip is on screen (menu_cursor.c reads the focused screen): L also soothes the
|
||||
// horse and turns menu tabs, the D-pad does other things too, and then the mouse stays on the camera.
|
||||
// Before the screens were read (the menu cursor off) the buttons alone decide, as before.
|
||||
if (selector && menu_cursor_shortcut() == 0) selector = 0;
|
||||
mouse_camera(k, selector);
|
||||
pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
// On the map the left stick moves the cursor and the right one zooms: the mouse moves the cursor there,
|
||||
// the scroll wheel zooms (map_zoom_stick). Elsewhere the wheel is unbound; its notches are dropped.
|
||||
int map = menu_cursor_map();
|
||||
if (map) pick(&sl[0], &sl[1], k[0], k[1]);
|
||||
else pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
int32_t zoom = map_zoom_stick(ikb_wheel_take(), map);
|
||||
if (zoom) pick(&sr[0], &sr[1], 0, zoom);
|
||||
int32_t shake = shake_stick(input_map_held(down, B_SHAKE));
|
||||
if (shake) sr[0] = shake, sr[1] = 0;
|
||||
// A mouse shake (mouse_direct.c) moves the stick with the mouse, side by side, and lets go when it rests
|
||||
else if (input_direct_mouse_shake()) sr[0] = input_direct_mouse_shake() * STICK_MAX, sr[1] = 0;
|
||||
static uint32_t travel_seen;
|
||||
uint32_t travel = ikb_mouse_travel();
|
||||
if (travel - travel_seen >= MOUSE_USED) kbm_used = 1;
|
||||
@@ -644,9 +692,12 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
if (overlay) menu_perf_cycle();
|
||||
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
|
||||
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
|
||||
__atomic_store_n(&g_buttons_last, b, __ATOMIC_RELAXED);
|
||||
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
|
||||
}
|
||||
|
||||
uint64_t input_buttons_last(void) { return __atomic_load_n(&g_buttons_last, __ATOMIC_RELAXED); }
|
||||
|
||||
// Text entry (swkbd_*.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
|
||||
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
|
||||
// g_block: 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
|
||||
|
||||
@@ -23,10 +23,18 @@ int input_direct_mouse_selector_active(void);
|
||||
// Consume direct mouse counts, including one-shot MOUSE events from an input script. enabled=0 discards scripted
|
||||
// counts while leaving the real mouse for menu input. Live counts require cursor capture and foreground focus.
|
||||
void input_direct_mouse_sample(int enabled, int32_t *dx, int32_t *dy);
|
||||
// The buttons (B_* bits) of the last poll, keys and gamepad merged; any thread (mouse_direct.c: ZR for the bow's scale)
|
||||
uint64_t input_buttons_last(void);
|
||||
// mouse_direct.c: the right stick's side a mouse shake gives while Ultrahand holds an object (-1 / +1), 0 while the
|
||||
// mouse does not shake; input.c puts it on the stick
|
||||
int input_direct_mouse_shake(void);
|
||||
// menu_cursor.c: the L ability wheel or a D-pad strip has the focus: 1 / 0, -1 with the menu cursor off (not read)
|
||||
int menu_cursor_shortcut(void);
|
||||
// menu_cursor.c: the pointer in the game's menus (1.4.3). poll: once per pad read, adds a click's A or B; pointer: a
|
||||
// menu has the pointer, so the mouse camera's capture is released
|
||||
void menu_cursor_poll(uint64_t *buttons);
|
||||
int menu_cursor_pointer(void);
|
||||
int menu_cursor_map(void); // the Purah Pad's map is open: the mouse's stick is the left one (its cursor)
|
||||
// The same for host UI (the text entry): never blocked. input_block(1) while a text entry is open: input_poll then
|
||||
// reports nothing; input_block(0) when it closes: nothing until all buttons and stick directions are released.
|
||||
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry);
|
||||
|
||||
+19
-3
@@ -16,7 +16,7 @@
|
||||
#define DEFAULT_KEYS \
|
||||
{KEY_W, B_SL_UP}, {KEY_A, B_SL_LEFT}, {KEY_S, B_SL_DOWN}, {KEY_D, B_SL_RIGHT}, /* move */ \
|
||||
{KEY_UP, B_SR_UP}, {KEY_LEFT, B_SR_LEFT}, {KEY_DOWN, B_SR_DOWN}, {KEY_RIGHT, B_SR_RIGHT}, /* camera, second source */ \
|
||||
{KEY_MOUSE_L, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
|
||||
{KEY_MOUSE_L, B_Y}, {KEY_T, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
|
||||
{KEY_SPACE, B_X}, {KEY_LSHIFT, B_B}, {KEY_E, B_A}, {KEY_Q, B_L}, {KEY_R, B_R}, \
|
||||
{KEY_LCTRL, B_STICK_L}, {KEY_C, B_STICK_R}, \
|
||||
{KEY_1, B_LEFT}, {KEY_2, B_UP}, {KEY_3, B_RIGHT}, {KEY_4, B_DOWN}, \
|
||||
@@ -31,7 +31,7 @@
|
||||
{PAD_ZR, B_ZR}
|
||||
#define NINTENDO_PADS DEFAULT_PADS(B_B, B_A, B_Y, B_X)
|
||||
#define XBOX_PADS DEFAULT_PADS(B_A, B_B, B_X, B_Y)
|
||||
enum { DEFAULT_KEY_COUNT = 27, DEFAULT_PAD_COUNT = 16 };
|
||||
enum { DEFAULT_KEY_COUNT = 28, DEFAULT_PAD_COUNT = 16 };
|
||||
_Static_assert(sizeof (Binding[]){DEFAULT_KEYS} / sizeof (Binding) == DEFAULT_KEY_COUNT, "DEFAULT_KEY_COUNT");
|
||||
|
||||
static const InputMap defaults = {{DEFAULT_KEYS}, DEFAULT_KEY_COUNT, {{NINTENDO_PADS}, {XBOX_PADS}},
|
||||
@@ -166,11 +166,27 @@ int input_map_bind(const char *button, const char *list) {
|
||||
|
||||
void input_map_configure(void) {
|
||||
input_map_reset();
|
||||
uint8_t lined[ACTION_COUNT] = {0};
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
char var[32];
|
||||
snprintf(var, sizeof var, "TOTK_BIND_%s", action_names[a]);
|
||||
const char *v = cfg_get(var);
|
||||
if (v && *v && input_map_bind(action_names[a], v)) rt_log("input: %s = %s", var + 5, v);
|
||||
if (v && *v && input_map_bind(action_names[a], v)) lined[a] = 1, rt_log("input: %s = %s", var + 5, v);
|
||||
}
|
||||
// A key a line binds leaves the defaults of the actions without a line, as the remapping rows move a key: with
|
||||
// bind_X = key:t, T presses X only, not also Y, whose defaults hold it.
|
||||
uint8_t bound[KEY_COUNT] = {0}, list[INPUT_BINDING_MAX];
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
if (!lined[a]) continue;
|
||||
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX);
|
||||
for (int i = 0; i < n; i++) bound[list[i]] = 1;
|
||||
}
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
if (lined[a]) continue;
|
||||
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX), w = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!bound[list[i]]) list[w++] = list[i];
|
||||
if (w < n) keys_put(&input_map, a, list, w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ const char *input_pad_name(int pad);
|
||||
// skipped; when none is valid the action keeps its bindings. 1 when applied.
|
||||
int input_map_bind(const char *button, const char *list);
|
||||
// Applies the TOTK_BIND_<action> settings (cfg_get) over the defaults: at start, and again when one of them changes.
|
||||
// A key a line binds leaves the defaults of the actions that have no line.
|
||||
void input_map_configure(void);
|
||||
|
||||
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts", 0.1.9's name for the Nintendo
|
||||
|
||||
+113
-7
@@ -1,11 +1,13 @@
|
||||
// The SDL3 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): SDL scancodes to
|
||||
// the key ids of input_map.h, the mouse camera's relative motion and the cursor capture (SDL's relative mouse mode).
|
||||
// Key and button state comes from the window's events (window thread) and is read by any thread. Keys and their map:
|
||||
// docs/controls.md.
|
||||
// the key ids of input_map.h, the mouse camera's relative motion, the cursor capture (SDL's relative mouse mode), the
|
||||
// pointer's place in the window (the game's menus, menu_cursor.c) and the mouse travel (the last-device switch,
|
||||
// input_kbm_last). Key and button state comes from the window's events (window thread) and is read by any thread. Keys
|
||||
// and their map: docs/controls.md.
|
||||
//
|
||||
// The capture follows the Win32 back end's rules (input_win.c): captured while the window has the focus, the mouse
|
||||
// camera is on and no text entry or menu is open; Esc releases it until a click in the window (and closing the menu
|
||||
// opened by that Esc gives it back); losing the focus releases it until the window has the focus again.
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
@@ -21,8 +23,18 @@ static SDL_Window *win;
|
||||
static volatile uint8_t key_held[KEY_COUNT];
|
||||
static volatile int32_t esc_held, f8_held, focused;
|
||||
static volatile int32_t mouse_dx, mouse_dy; // relative motion since the last ikb_mouse_take, whole counts
|
||||
static volatile int32_t mouse_wheel; // wheel turned while captured, in 1/120 notches (ikb_wheel_take)
|
||||
static float rem_x, rem_y; // window thread: the fractions SDL reported beyond them
|
||||
static volatile int32_t mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
|
||||
static float travel_rem; // window thread: the fraction of a count beyond mouse_travel
|
||||
static volatile int32_t captured; // relative mouse mode is on
|
||||
// The pointer's place in the window, as fractions of its size packed 32.32 fixed point (x in the high word), written
|
||||
// by the window thread from the mouse events and read by the game thread (menu_cursor.c through ikb_pointer).
|
||||
// ptr_have is 0 whenever the pointer is not over the window, as the Win32 back end's client-rect test.
|
||||
static volatile uint64_t ptr_pos;
|
||||
static volatile int32_t ptr_have;
|
||||
static volatile int32_t warp_pending; // ikb_pointer_set asked the window thread to move the pointer
|
||||
static float warp_nx, warp_ny; // its target, read by the window thread once warp_pending is seen
|
||||
enum { CUR_REL = 1, CUR_ESC = 2 }; // as input_win.c: released by Esc or a click elsewhere; by Esc
|
||||
static volatile int32_t cur_rel, cur_suspend, cur_pending;
|
||||
static uint32_t wake_event; // SDL_RegisterEvents: wakes the window thread for a cursor change
|
||||
@@ -133,6 +145,39 @@ static void cursor_apply(void) {
|
||||
plat_xchg32(&captured, want);
|
||||
}
|
||||
|
||||
// The pointer's place in the window (the window thread): x and y are SDL window units, the same space the mouse
|
||||
// events report, so dividing by the window's size makes fractions of it whatever the pixel density is. At and past
|
||||
// the edges the fraction is clamped to 0..1, where a letterboxed picture covers no item anyway.
|
||||
static void pointer_store(float x, float y) {
|
||||
if (!win) return;
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSize(win, &w, &h);
|
||||
if (w < 1 || h < 1) return;
|
||||
double fx = x / w, fy = y / h;
|
||||
if (!(fx >= 0)) fx = 0; // also NaN
|
||||
else if (fx > 1) fx = 1;
|
||||
if (!(fy >= 0)) fy = 0;
|
||||
else if (fy > 1) fy = 1;
|
||||
__atomic_store_n(&ptr_pos, (uint64_t)(uint32_t)(fx * 4294967295.0) << 32 | (uint32_t)(fy * 4294967295.0),
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&ptr_have, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// A warp asked for by ikb_pointer_set, done here as every window call: on the window thread, and after the cursor
|
||||
// change of the same menu opening has been applied. SDL's warp makes a motion event where it works (X11); where it
|
||||
// does not (some Wayland compositors) the stored place stands, so the item under the pointer is the place asked for
|
||||
// even if the visible cursor stayed behind.
|
||||
static void pointer_apply(void) {
|
||||
if (!plat_xchg32(&warp_pending, 0)) return;
|
||||
if (!win || !focused) return;
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSize(win, &w, &h);
|
||||
if (w < 1 || h < 1) return;
|
||||
float x = warp_nx * w, y = warp_ny * h;
|
||||
SDL_WarpMouseInWindow(win, x, y);
|
||||
pointer_store(x, y);
|
||||
}
|
||||
|
||||
// Any thread: the window thread applies the change at its next event.
|
||||
void ikb_cursor_update(void) {
|
||||
plat_xchg32(&cur_pending, 1);
|
||||
@@ -158,11 +203,13 @@ void input_sdl_window(SDL_Window *w) {
|
||||
win = w;
|
||||
wake_event = SDL_RegisterEvents(1);
|
||||
plat_xchg32(&focused, (SDL_GetWindowFlags(w) & SDL_WINDOW_INPUT_FOCUS) != 0);
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE); // the pointer's place is unknown until it enters or moves
|
||||
cursor_apply();
|
||||
}
|
||||
|
||||
void input_sdl_idle(void) {
|
||||
if (cur_pending) cursor_apply();
|
||||
pointer_apply();
|
||||
}
|
||||
|
||||
void input_sdl_event(const SDL_Event *e) {
|
||||
@@ -186,9 +233,19 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
int k = mouse_key(e->button.button);
|
||||
if (k) key_held[k] = (uint8_t)down;
|
||||
if (down && (cur_rel & CUR_REL)) plat_xchg32(&cur_rel, 0); // a click in the window captures again
|
||||
if (!captured) pointer_store(e->button.x, e->button.y); // a click can come without a motion first
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
case SDL_EVENT_MOUSE_MOTION: {
|
||||
// The travel counts any motion, captured or not, as the Win32 back end's raw input does: it is what makes a
|
||||
// moved mouse the device used last, so the menu pointer appears without a click or a key.
|
||||
travel_rem += fabsf(e->motion.xrel) + fabsf(e->motion.yrel);
|
||||
int32_t t = (int32_t)travel_rem;
|
||||
if (t) {
|
||||
travel_rem -= t;
|
||||
plat_add32(&mouse_travel, t);
|
||||
}
|
||||
if (!captured) pointer_store(e->motion.x, e->motion.y); // in relative mode x and y are not the pointer's
|
||||
if (captured) {
|
||||
rem_x += e->motion.xrel;
|
||||
rem_y += e->motion.yrel;
|
||||
@@ -198,6 +255,22 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
if (dy) plat_add32(&mouse_dy, dy);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_WINDOW_MOUSE_ENTER: { // entering without moving yet: the place is where the enter found the pointer
|
||||
float x, y;
|
||||
SDL_GetMouseState(&x, &y); // the window with mouse focus is this one during its own enter event
|
||||
pointer_store(x, y);
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_WHEEL: // notches, + away from the player (a flipped wheel reports the other sign)
|
||||
if (captured) {
|
||||
float y = e->wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -e->wheel.y : e->wheel.y;
|
||||
plat_add32(&mouse_wheel, (int32_t)(y * 120));
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
plat_xchg32(&focused, 1);
|
||||
break;
|
||||
@@ -207,9 +280,11 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
memset((void *)key_held, 0, sizeof key_held); // the key-ups go elsewhere
|
||||
plat_xchg32(&esc_held, 0);
|
||||
plat_xchg32(&f8_held, 0);
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE); // the pointer left with the focus, leave or not
|
||||
break;
|
||||
}
|
||||
cursor_apply();
|
||||
pointer_apply(); // after cursor_apply: a menu opening uncaptures first, then the pointer moves to its middle
|
||||
}
|
||||
|
||||
// ---- input_wsys.h
|
||||
@@ -217,11 +292,42 @@ int ikb_foreground(void) { return focused != 0; }
|
||||
int ikb_down(int key) { return key > 0 && key < KEY_COUNT && key_held[key]; }
|
||||
int ikb_escape(void) { return esc_held != 0; }
|
||||
int ikb_captured(void) { return captured != 0; }
|
||||
int ikb_pointer(float *nx, float *ny) { (void)nx, (void)ny; return 0; } // (menu_cursor.c: Windows only for now)
|
||||
void ikb_pointer_set(float nx, float ny) { (void)nx, (void)ny; }
|
||||
uint32_t ikb_mouse_travel(void) { return 0; } // (the last-device switch: keys and buttons only for now)
|
||||
|
||||
// The pointer's place in the window as fractions of its size (menu_cursor.c maps them onto the picture the present
|
||||
// drew). 0 whenever the pointer is not over this window or the window has not the focus, as the Win32 back end's
|
||||
// GetCursorPos test.
|
||||
int ikb_pointer(float *nx, float *ny) {
|
||||
if (!focused || !__atomic_load_n(&ptr_have, __ATOMIC_ACQUIRE)) return 0;
|
||||
uint64_t p = __atomic_load_n(&ptr_pos, __ATOMIC_ACQUIRE);
|
||||
*nx = (float)(uint32_t)(p >> 32) / 4294967295.0f;
|
||||
*ny = (float)(uint32_t)p / 4294967295.0f;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Moves the pointer there (menu_cursor.c: to the middle when a menu opens). The window thread warps at its next
|
||||
// event, as SDL asks for its video thread; until then the warp target stands as the pointer's place.
|
||||
void ikb_pointer_set(float nx, float ny) {
|
||||
if (!(nx >= 0 && nx <= 1) || !(ny >= 0 && ny <= 1)) return;
|
||||
warp_nx = nx, warp_ny = ny;
|
||||
__atomic_store_n(&warp_pending, 1, __ATOMIC_RELEASE);
|
||||
if (wake_event) {
|
||||
SDL_Event e = {.user = {.type = wake_event}};
|
||||
SDL_PushEvent(&e);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ikb_mouse_travel(void) { return (uint32_t)__atomic_load_n(&mouse_travel, __ATOMIC_RELAXED); }
|
||||
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
|
||||
*dx = plat_xchg32(&mouse_dx, 0);
|
||||
*dy = plat_xchg32(&mouse_dy, 0);
|
||||
}
|
||||
|
||||
int32_t ikb_wheel_take(void) {
|
||||
int32_t o, n;
|
||||
do {
|
||||
o = mouse_wheel;
|
||||
n = o % 120; // the part of a notch stays for the next call
|
||||
} while (!__atomic_compare_exchange_n(&mouse_wheel, &o, n, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST));
|
||||
return o / 120;
|
||||
}
|
||||
@@ -84,6 +84,7 @@ uint32_t input_key_char(int key) {
|
||||
// ---- mouse camera: raw motion accumulated by the window thread, one stick sample per presented frame
|
||||
static volatile LONG mouse_dx, mouse_dy; // raw counts since the last frame sample
|
||||
static volatile LONG mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
|
||||
static volatile LONG mouse_wheel; // wheel turned while captured, in WHEEL_DELTA units (ikb_wheel_take)
|
||||
static volatile LONG cur_captured; // cursor hidden and confined to the client area
|
||||
static volatile LONG cur_active; // WM_ACTIVATEAPP: the window's application is active
|
||||
// Bits: CUR_REL, Esc or activation by a click on the title bar or border: until a client click; CUR_ESC, set with CUR_REL
|
||||
@@ -147,6 +148,9 @@ int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
|
||||
InterlockedAdd(&mouse_dy, y);
|
||||
}
|
||||
}
|
||||
// The wheel comes in the same message (usButtonData, signed, + away from the player)
|
||||
if (cur_captured && ri.header.dwType == RIM_TYPEMOUSE && (ri.data.mouse.usButtonFlags & RI_MOUSE_WHEEL))
|
||||
InterlockedAdd(&mouse_wheel, (SHORT)ri.data.mouse.usButtonData);
|
||||
break; // DefWindowProc must see WM_INPUT for cleanup
|
||||
}
|
||||
case WM_SETCURSOR:
|
||||
@@ -234,6 +238,15 @@ void ikb_mouse_take(int32_t *dx, int32_t *dy) {
|
||||
*dy = InterlockedExchange(&mouse_dy, 0);
|
||||
}
|
||||
|
||||
int32_t ikb_wheel_take(void) {
|
||||
LONG o, n;
|
||||
do {
|
||||
o = mouse_wheel;
|
||||
n = o % WHEEL_DELTA; // the part of a notch stays for the next call
|
||||
} while (InterlockedCompareExchange(&mouse_wheel, n, o) != o);
|
||||
return (int32_t)(o / WHEEL_DELTA);
|
||||
}
|
||||
|
||||
void ikb_cursor_update(void) { cursor_update(gfx_window_handle()); }
|
||||
|
||||
void ikb_menu_closed(void) { cur_rel_update(rel_menu_closed); }
|
||||
@@ -13,6 +13,8 @@ void ikb_pointer_set(float nx, float ny); // moves the pointer there (the wind
|
||||
int ikb_captured(void); // the cursor is captured for the mouse camera
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy); // raw mouse motion since the last call, in counts
|
||||
uint32_t ikb_mouse_travel(void); // raw counts the mouse moved in total (|dx| + |dy|), captured or not
|
||||
int32_t ikb_wheel_take(void); // whole wheel notches turned while captured since the last call (+ away
|
||||
// from the player); a high-resolution wheel's part of a notch is kept
|
||||
void ikb_cursor_update(void); // what input_cursor_allowed says may have changed
|
||||
void ikb_menu_closed(void); // the menu closed: a capture that Esc gave up comes back
|
||||
|
||||
|
||||
@@ -246,6 +246,44 @@ static void wake_sync_waiters(void) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- a short poll before a sleep
|
||||
// Most guest waits and waits for a virtual core end within microseconds. The thread polls for TOTK_KSPIN_US (default
|
||||
// 20) with G released before it sleeps on its condition variable: a wake in that time costs no OS wake-up (4-12 us
|
||||
// on a desktop Ryzen, more from a deep C-state) and the waker's signal finds nobody asleep. Off with fewer than 6 logical
|
||||
// processors, where the poll would take a CPU that a running thread needs; TOTK_KSPIN_US=0 turns it off. Savage Wind
|
||||
// Recomp polls the same way before it parks (runtime/ksync.c cond_wait). TOTK_KSPIN_US=ab polls 20 us in the
|
||||
// B blocks of TOTK_AB only.
|
||||
static int64_t g_kspin_ns = -1;
|
||||
static int g_kspin_ab;
|
||||
|
||||
static uint64_t kspin_ns(void) {
|
||||
int64_t v = __atomic_load_n(&g_kspin_ns, __ATOMIC_RELAXED);
|
||||
if (v < 0) {
|
||||
const char *e = cfg_get("TOTK_KSPIN_US");
|
||||
g_kspin_ab = e && !strcmp(e, "ab");
|
||||
long long us = e && *e && !g_kspin_ab ? atoll(e) : 20;
|
||||
uint32_t cpus = plat_cpu_usable();
|
||||
v = us <= 0 || cpus < 6 ? 0 : us * 1000;
|
||||
__atomic_store_n(&g_kspin_ns, v, __ATOMIC_RELAXED);
|
||||
rt_log("kernel: waits poll %lld us before they sleep%s (TOTK_KSPIN_US, %u logical processors)",
|
||||
(long long)(v / 1000), g_kspin_ab ? " in the B blocks" : "", cpus);
|
||||
}
|
||||
return v && (!g_kspin_ab || rt_ab()) ? (uint64_t)v : 0;
|
||||
}
|
||||
|
||||
// Holding G: releases it and polls *p until it reaches at_least or ns pass, then takes G again. 1 if it was reached.
|
||||
static int kspin(const int *p, int at_least, uint64_t ns) {
|
||||
uint64_t end = kern_now_ns() + ns;
|
||||
int ok = 0;
|
||||
unlock();
|
||||
for (unsigned i = 1; !(ok = __atomic_load_n(p, __ATOMIC_ACQUIRE) >= at_least); i++) {
|
||||
plat_pause();
|
||||
if (!(i & 31) && kern_now_ns() >= end) break;
|
||||
}
|
||||
lock();
|
||||
return ok;
|
||||
}
|
||||
|
||||
// SleepConditionVariableSRW counts milliseconds: round up (late is legal, early is not) and re-check the deadline
|
||||
// against the performance counter, because the wait may also end early.
|
||||
static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
|
||||
@@ -262,6 +300,8 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
|
||||
uint64_t now = kern_now_ns();
|
||||
int forever = ns < 0 || (uint64_t)ns > UINT64_MAX - now;
|
||||
uint64_t deadline = forever ? 0 : now + (uint64_t)ns;
|
||||
uint64_t poll = kspin_ns();
|
||||
if (poll) kspin(&t->woke, 1, forever || poll < (uint64_t)ns ? poll : (uint64_t)ns);
|
||||
while (!t->woke) {
|
||||
if (forever) {
|
||||
plat_cond_wait(&t->cv, &G);
|
||||
@@ -991,6 +1031,9 @@ static void core_take(RtThread *t) {
|
||||
while (__atomic_load_n(&t->cur_core, __ATOMIC_RELAXED) < 0 && kern_now_ns() < end) plat_pause();
|
||||
lock();
|
||||
if (t->cur_core >= 0) g_ap_spin_ok++;
|
||||
} else if (t->cur_core < 0) { // a holder may block or yield any moment (kspin_ns)
|
||||
uint64_t poll = kspin_ns();
|
||||
if (poll) kspin(&t->cur_core, 0, poll);
|
||||
}
|
||||
int sampled = 0;
|
||||
while (t->cur_core < 0) {
|
||||
@@ -1518,6 +1561,7 @@ uint32_t kern_main_thread(Cpu **cpu) {
|
||||
t->cpu->sp = t->stack_top;
|
||||
t->tid = plat_thread_id();
|
||||
perf_main_thread();
|
||||
perf_core_thread(PCORE_MAIN);
|
||||
PlatIntr *self = ap_open_self();
|
||||
lock();
|
||||
t->hthr = self;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "kernel.h"
|
||||
#include "layout.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define STACK 0x10000
|
||||
@@ -682,6 +683,25 @@ static int cmp_u64(const void *a, const void *b) {
|
||||
return x < y ? -1 : x > y;
|
||||
}
|
||||
|
||||
// A refused commit waits and retries (memory.c commit, oom.c rt_oom_wait): three forced refusals, then the page maps.
|
||||
static void test_oom_commit(void) {
|
||||
const RtGuestLayout *g = mem_layout();
|
||||
uint64_t a = g->alias.base + g->alias.size - 0x10000;
|
||||
rt_oom_force(RT_OOM_COMMIT, 3);
|
||||
uint64_t t0 = plat_now_ms();
|
||||
uint32_t r = mem_map_physical(a, 0x1000);
|
||||
uint64_t ms = plat_now_ms() - t0;
|
||||
CHECK(r == 0 && rt_oom_forced_left(RT_OOM_COMMIT) == 0 && ms >= 20,
|
||||
"MapPhysicalMemory after 3 refused commits: result %#x, %d refusals left, %llu ms", r,
|
||||
rt_oom_forced_left(RT_OOM_COMMIT), (unsigned long long)ms);
|
||||
rt_oom_force(RT_OOM_COMMIT, 0);
|
||||
if (r == 0) {
|
||||
*(volatile uint64_t *)(uintptr_t)a = 1;
|
||||
CHECK(mem_unmap_physical(a, 0x1000) == 0, "UnmapPhysicalMemory after the refused commits");
|
||||
}
|
||||
printf("oom: commit waited %llu ms through 3 refusals\n", (unsigned long long)ms);
|
||||
}
|
||||
|
||||
static void test_platform(void) {
|
||||
PlatThread *t = plat_thread_new(plat_worker, (void *)(intptr_t)1, "probe-kernel plat");
|
||||
CHECK(t, "plat_thread_new");
|
||||
@@ -719,6 +739,90 @@ static void test_platform(void) {
|
||||
CHECK(over[N / 2] < 500000, "plat_sleep_ns(1 ms) median overshoot %.3f ms (want < 0.5)", over[N / 2] / 1e6);
|
||||
}
|
||||
|
||||
// ---- crash report (main.c crash_handler): a child faults on a guest thread with known values in rbx and x19; the
|
||||
// report must hold the host registers, the faulting instruction's bytes and the memory both values point at.
|
||||
#define CRASH_CHILD_ENV "TOTK_PROBE_KERNEL_CRASH_CHILD"
|
||||
static uint64_t crash_host_words[6] = {0, 0, 0x5ca1ab1e0c0de001ull, 0, 0, 0};
|
||||
static uint64_t crash_guest_words[6] = {0, 0, 0x5ca1ab1e0c0de002ull, 0, 0, 0};
|
||||
|
||||
// The compound shape note (main.c crash_compound_shape, 1.2.1): a shape of 3 records whose child 0 has no shape, child
|
||||
// 1 has one but its live bit is clear, child 2 is live and filled.
|
||||
static uint64_t crash_shape[0x1a0 / 8], crash_records[3 * 0x78 / 8], crash_child_shape;
|
||||
|
||||
static void crash_shape_fill(Cpu *c) {
|
||||
uint8_t *s = (uint8_t *)crash_shape, *r = (uint8_t *)crash_records;
|
||||
static const char *const names[3] = {"PartA", "PartB", "PartC"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint64_t name = (uint64_t)(uintptr_t)(r + i * 0x78 + 0x14);
|
||||
memcpy(r + i * 0x78 + 8, &name, 8);
|
||||
memcpy(r + i * 0x78 + 0x14, names[i], strlen(names[i]) + 1);
|
||||
}
|
||||
uint64_t child = (uint64_t)(uintptr_t)&crash_child_shape, vt = MOD_MAIN_BASE + 0x45e1218;
|
||||
uint64_t rec = (uint64_t)(uintptr_t)crash_records;
|
||||
uint32_t n = 3, bits = 0x5;
|
||||
memcpy(r + 0x78, &child, 8);
|
||||
memcpy(r + 2 * 0x78, &child, 8);
|
||||
memcpy(s, &vt, 8);
|
||||
memcpy(s + 0x78, &n, 4);
|
||||
memcpy(s + 0x80, &rec, 8);
|
||||
memcpy(s + 0x88, &bits, 4);
|
||||
memcpy(s + 0x190, &n, 4);
|
||||
c->x[20] = (uint64_t)(uintptr_t)crash_shape;
|
||||
c->x[21] = 0x1000038;
|
||||
c->x[30] = MOD_MAIN_BASE + 0xe056d4;
|
||||
}
|
||||
|
||||
static void crash_child_main(Cpu *c) {
|
||||
const char *mode = getenv(CRASH_CHILD_ENV);
|
||||
if (mode && !strcmp(mode, "shape")) crash_shape_fill(c);
|
||||
c->x[19] = (uint64_t)(uintptr_t)&crash_guest_words[2];
|
||||
c->sp -= 0x100; // a frame on the guest stack, as a crashing function has
|
||||
__asm__ volatile("movq %0, %%rbx\n\tmovq 0x10, %%rax" : : "r"(&crash_host_words[2]) : "rax", "rbx", "memory");
|
||||
}
|
||||
|
||||
// The report line that holds needle, NULL when none does
|
||||
static const char *report_line(const char *out, const char *needle, char *line, size_t cap) {
|
||||
const char *p = strstr(out, needle);
|
||||
if (!p) return NULL;
|
||||
while (p > out && p[-1] != '\n') p--;
|
||||
size_t n = strcspn(p, "\r\n");
|
||||
snprintf(line, cap, "%.*s", (int)(n < cap ? n : cap - 1), p);
|
||||
return line;
|
||||
}
|
||||
|
||||
static void test_crash_report(void) {
|
||||
static char out[65536];
|
||||
char line[512], want[64];
|
||||
int code = plat_run_self(CRASH_CHILD_ENV, "1", out, sizeof out, 30000);
|
||||
CHECK(code == 71 && strstr(out, "FATAL: access violation") && strstr(out, "reading address 0x10"),
|
||||
"crash child: exit code %d, no access violation at 0x10 reported", code);
|
||||
snprintf(want, sizeof want, "rbx %016llx", (unsigned long long)(uintptr_t)&crash_host_words[2]);
|
||||
CHECK(strstr(out, want), "crash report: no host register line with %s", want);
|
||||
CHECK(report_line(out, "host code at rip-", line, sizeof line) && strstr(line, "| 48 8b 04 25 10 00 00 00"),
|
||||
"crash report: the faulting instruction's bytes are missing");
|
||||
const char *h = report_line(out, "*5ca1ab1e0c0de001", line, sizeof line);
|
||||
CHECK(h && strstr(h, "rbx"), "crash report: no memory line for rbx (%s)", h ? h : "none");
|
||||
const char *g = report_line(out, "*5ca1ab1e0c0de002", line, sizeof line);
|
||||
CHECK(g && strstr(g, "x19"), "crash report: no memory line for x19 (%s)", g ? g : "none");
|
||||
CHECK(strstr(out, "guest stack sp+0x00:"), "crash report: no guest stack words");
|
||||
CHECK(report_line(out, " memory commit: this process ", line, sizeof line) && !strstr(line, "process -1 MB"),
|
||||
"crash report: no memory commit line");
|
||||
printf("crash report: exit code %d, host registers, code bytes, memory at rbx and x19, guest stack, commit %s\n", code,
|
||||
failures ? "checked with failures" : "present");
|
||||
if (rt_game_version != 121) return;
|
||||
code = plat_run_self(CRASH_CHILD_ENV, "shape", out, sizeof out, 30000);
|
||||
CHECK(report_line(out, " compound shape ", line, sizeof line) && strstr(line, "3 children, 3 records") &&
|
||||
strstr(line, "live bits: 00000005"),
|
||||
"crash report: no compound shape line (%s)", strstr(out, "compound shape") ? line : "none");
|
||||
CHECK(report_line(out, "child 0:", line, sizeof line) && strstr(line, "shape 0 \"PartA\""),
|
||||
"crash report: child 0 should have no shape (%s)", line);
|
||||
CHECK(report_line(out, "child 1:", line, sizeof line) && strstr(line, "(off) \"PartB\""),
|
||||
"crash report: child 1 should be off (%s)", line);
|
||||
CHECK(report_line(out, "child 2:", line, sizeof line) && !strstr(line, "(off)") && strstr(line, "\"PartC\""),
|
||||
"crash report: child 2 should be live (%s)", line);
|
||||
printf("crash report, compound shape: exit code %d, %s\n", code, failures ? "checked with failures" : "children listed");
|
||||
}
|
||||
|
||||
int probe_kernel(void) {
|
||||
a_lock_waiter = hostfn_register("probe-kernel lock waiter", lock_waiter);
|
||||
a_cv_waiter = hostfn_register("probe-kernel condvar waiter", cv_waiter);
|
||||
@@ -742,7 +846,15 @@ int probe_kernel(void) {
|
||||
printf("probe-kernel: FAILED (no PLT stub for %s in main)\n", TICK_SYMBOL);
|
||||
return 1;
|
||||
}
|
||||
const char *child = getenv(CRASH_CHILD_ENV);
|
||||
if (child && *child) {
|
||||
kern_run_on_guest_main(crash_child_main);
|
||||
printf("crash child: returned without a fault\n");
|
||||
return 1;
|
||||
}
|
||||
test_platform();
|
||||
test_oom_commit();
|
||||
test_crash_report();
|
||||
kern_run_on_guest_main(probe_main);
|
||||
RtStats s = rt_stats();
|
||||
printf("stats: %llu SVCs, %llu threads, %llu host calls\n", (unsigned long long)s.svcs,
|
||||
|
||||
+163
-12
@@ -2,10 +2,12 @@
|
||||
//
|
||||
// totk.exe [--exefs DIR] [--romfs DIR] [--profile DIR] <mode>
|
||||
// --selftest layout, dispatch table and host-function registry; needs no game files
|
||||
// --probe-mods the mods folder scan, order and switches (mods.c); needs no game files
|
||||
// --probe-mods the mods folder scan, order and switches (mods.c), the size table entries of the members of
|
||||
// mods' packs (restbl_probe.c); needs no game files
|
||||
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
|
||||
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
|
||||
// --probe-discord the Rich Presence IPC exchange; skips without a Discord client; no game files
|
||||
// --probe-playtime the play time file, its ticks and saves (playtime.c); needs no game files
|
||||
// --probe-dpi the process DPI awareness (per-monitor v2) and the primary monitor's size; no game files
|
||||
// --probe-gfx-abi NVN object layouts against nvn.h's reserved sizes; needs no game files
|
||||
// --probe-nvn, --probe-nvn-tex, --probe-window, --probe-shader, --probe-vkmem, --probe-vkexec graphics probes
|
||||
@@ -19,7 +21,8 @@
|
||||
//
|
||||
// Also here: the runtime services the generated code calls besides rt_dispatch and rt_svc (rt_brk, rt_udf,
|
||||
// rt_ticks, the function profiler hook), logging, SHA-256, and the crash reporter (a vectored exception handler that
|
||||
// names the lifted function, the guest registers and the guest frame-pointer chain).
|
||||
// names the lifted function, the host and guest registers, the machine code around the fault, the guest frame-pointer
|
||||
// chain and the memory the registers point at).
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -27,6 +30,7 @@
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "layout.h"
|
||||
#include "platform/platform.h"
|
||||
#include "sha256.h"
|
||||
|
||||
@@ -186,8 +190,8 @@ static int by_host(const void *a, const void *b) {
|
||||
}
|
||||
|
||||
// The lifted function that contains a host code address, by the nearest function start below it; 0 outside the lifted
|
||||
// code.
|
||||
static uint64_t lifted_function(uintptr_t rip) {
|
||||
// code. *host, when given, gets the function's host start.
|
||||
static uint64_t lifted_function_at(uintptr_t rip, uintptr_t *host) {
|
||||
if (!fn_rev) {
|
||||
static RtModule mods[MAX_MODULES];
|
||||
int n = rt_linked_modules(mods, MAX_MODULES);
|
||||
@@ -209,11 +213,113 @@ static uint64_t lifted_function(uintptr_t rip) {
|
||||
if (fn_rev[mid].host <= rip) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
if (lo && host) *host = fn_rev[lo - 1].host;
|
||||
return lo ? fn_rev[lo - 1].guest : 0;
|
||||
}
|
||||
|
||||
static uint64_t lifted_function(uintptr_t rip) { return lifted_function_at(rip, NULL); }
|
||||
|
||||
static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); }
|
||||
|
||||
// The machine code around the faulting instruction: 32 bytes before rip, then 32 from rip. The user's totk.exe is built
|
||||
// on their machine, so its offsets need not match ours; these bytes and the offset into the lifted function find the
|
||||
// same instruction in a local build of that version, and with it the guest instruction and the operands' registers.
|
||||
static void crash_code_bytes(uintptr_t rip) {
|
||||
uint8_t b[64];
|
||||
int before = 32;
|
||||
if (!plat_mem_read(rip - 32, b, sizeof b)) {
|
||||
before = 0; // rip near the start of a mapping: the bytes from rip only
|
||||
if (!plat_mem_read(rip, b + 32, 32)) return;
|
||||
}
|
||||
rt_logf(" host code at rip-%d:", before);
|
||||
for (int i = 32 - before; i < 64; i++) rt_logf("%s%02x", i == 32 ? " | " : " ", b[i]);
|
||||
rt_log_raw("\n", 1);
|
||||
}
|
||||
|
||||
// 64 bytes around each register value that points at readable memory, as 8-byte words from 16 bytes below the value,
|
||||
// the word holding the value marked with *. Host and guest registers that land on the same window share a line. A
|
||||
// guest pointer is a host address here (the guest address space is mapped one to one), so both kinds are read alike.
|
||||
// The guest registers come from the Cpu, which the lifted code writes back only at calls and returns, so the host
|
||||
// registers carry the faulting function's live values.
|
||||
typedef struct { uint64_t start; char names[56]; uint64_t w[8]; } CrashWin;
|
||||
|
||||
static void crash_win_add(CrashWin *wins, int *n, int cap, const char *name, uint64_t v) {
|
||||
if (v < 0x10000 || v > 0x00007fffffffffffull) return; // small integers and non-canonical values
|
||||
uint64_t start = (v & ~7ull) - 16;
|
||||
for (int i = 0; i < *n; i++)
|
||||
if (wins[i].start == start) {
|
||||
size_t len = strlen(wins[i].names);
|
||||
if (len + strlen(name) + 2 < sizeof wins[i].names)
|
||||
snprintf(wins[i].names + len, sizeof wins[i].names - len, " %s", name);
|
||||
return;
|
||||
}
|
||||
if (*n == cap || !plat_mem_read(start, wins[*n].w, sizeof wins[*n].w)) return;
|
||||
wins[*n].start = start;
|
||||
snprintf(wins[*n].names, sizeof wins[*n].names, "%s", name);
|
||||
(*n)++;
|
||||
}
|
||||
|
||||
static void crash_memory(const PlatCrash *x, const Cpu *c) {
|
||||
static CrashWin wins[64]; // the crash handler runs once (busy flag), so static storage needs no allocation
|
||||
static const char *const gpr_names[16] = PLAT_GPR_NAMES;
|
||||
int n = 0;
|
||||
char name[8];
|
||||
if (x->has_regs)
|
||||
for (int i = 0; i < 16; i++) crash_win_add(wins, &n, 64, gpr_names[i], x->gpr[i]);
|
||||
if (c) {
|
||||
for (int i = 0; i < 31; i++) snprintf(name, sizeof name, "x%d", i), crash_win_add(wins, &n, 64, name, c->x[i]);
|
||||
crash_win_add(wins, &n, 64, "sp", c->sp);
|
||||
}
|
||||
if (!n) return;
|
||||
rt_logf(" memory at the register values (8-byte words from value-16, * the value's word):\n");
|
||||
for (int i = 0; i < n; i++) {
|
||||
rt_logf(" %016llx %-12s", (unsigned long long)wins[i].start, wins[i].names);
|
||||
for (int k = 0; k < 8; k++) rt_logf(" %s%016llx", k == 2 ? "*" : "", (unsigned long long)wins[i].w[k]);
|
||||
rt_log_raw("\n", 1);
|
||||
}
|
||||
// The guest stack above sp: the crashing function's spills and locals, and the callers' saved registers
|
||||
uint64_t s[16];
|
||||
if (c && plat_mem_read(c->sp, s, sizeof s))
|
||||
for (int i = 0; i < 16; i += 4)
|
||||
rt_logf(" guest stack sp+0x%02x: %016llx %016llx %016llx %016llx\n", i * 8, (unsigned long long)s[i],
|
||||
(unsigned long long)s[i + 1], (unsigned long long)s[i + 2], (unsigned long long)s[i + 3]);
|
||||
}
|
||||
|
||||
// 1.2.1: a fault right after a compound physics shape was asked for its first child's material (the call returning to
|
||||
// main+0xe056d4 reads the result without a check). The shape (vtable main+0x45e1218, built by main+0x7359fc) is in x20
|
||||
// there. It returns null when no child slot is both live (bitmap at +0x88) and filled (shape pointer at +0 of the
|
||||
// 0x78-byte records at +0x80). Every child is filled when the shape is built, so the list tells children removed later
|
||||
// (null shape) from children switched off (clear bit), and the names name the object. Runs only in the crash handler.
|
||||
static void crash_compound_shape(const Cpu *c) {
|
||||
if (!c || rt_game_version != 121 || c->x[30] != MOD_MAIN_BASE + 0xe056d4) return;
|
||||
uint64_t shape = c->x[20];
|
||||
if (!readable(shape, 0x1a0) || LD64(shape) != MOD_MAIN_BASE + 0x45e1218) {
|
||||
rt_logf(" compound shape: x20 %#llx is not one\n", (unsigned long long)shape);
|
||||
return;
|
||||
}
|
||||
uint32_t children = LD32(shape + 0x190), records = LD32(shape + 0x78);
|
||||
uint64_t rec = LD64(shape + 0x80);
|
||||
rt_logf(" compound shape %#llx (holder %#llx): %u children, %u records at %#llx, live bits:",
|
||||
(unsigned long long)shape, (unsigned long long)(c->x[21] - 0x38), children, records,
|
||||
(unsigned long long)rec);
|
||||
uint32_t n = children > 0x400 ? 0x400 : children; // the bitmap holds 0x400 bits
|
||||
for (uint32_t w = 0; w < (n + 31) / 32; w++) rt_logf(" %08x", LD32(shape + 0x88 + 4 * w));
|
||||
rt_log_raw("\n", 1);
|
||||
if (records > 0x400 || !readable(rec, (uint64_t)records * 0x78)) return;
|
||||
for (uint32_t i = 0; i < records; i++) {
|
||||
uint64_t r = rec + (uint64_t)i * 0x78, child = LD64(r), s = LD64(r + 8);
|
||||
char name[33];
|
||||
size_t k = 0;
|
||||
for (; k < 32 && readable(s + k, 1); k++) {
|
||||
name[k] = *(const char *)(uintptr_t)(s + k);
|
||||
if (!name[k]) break;
|
||||
}
|
||||
name[k] = 0;
|
||||
int live = LD32(shape + 0x88 + 4 * (i >> 5)) >> (i & 31) & 1;
|
||||
rt_logf(" child %3u: shape %#llx%s \"%s\"\n", i, (unsigned long long)child, live ? "" : " (off)", name);
|
||||
}
|
||||
}
|
||||
|
||||
static void crash_handler(const PlatCrash *x) {
|
||||
static volatile int32_t busy;
|
||||
if (plat_xchg32(&busy, 1)) plat_exit(71);
|
||||
@@ -226,23 +332,48 @@ static void crash_handler(const PlatCrash *x) {
|
||||
rt_logf(", %s address %#llx", x->access == 1 ? "writing" : x->access == 8 ? "executing" : "reading",
|
||||
(unsigned long long)x->addr);
|
||||
rt_log_raw("\n", 1);
|
||||
uint64_t f = x->module ? 0 : lifted_function(rip);
|
||||
if (f) rt_logf(" in lifted function f_%llx = %s\n", (unsigned long long)f, ldr_describe(f, d, sizeof d));
|
||||
// abort and invalid parameter: where the host code called it; a fault in another module: the host calls that led
|
||||
// into it
|
||||
if (x->cxx_type)
|
||||
rt_logf(" C++ type %s%s%s\n", x->cxx_type, x->cxx_what ? ", what(): " : "", x->cxx_what ? x->cxx_what : "");
|
||||
// A driver that runs out of memory faults or throws (std::bad_alloc) instead of returning an error: the commit
|
||||
// tells that apart from a driver bug, as rt_fatal's line does for our own failed allocations.
|
||||
double proc, used, limit;
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
rt_logf(" memory commit: this process %.0f MB, the system %.0f of %.0f MB\n", proc, used, limit);
|
||||
uintptr_t fstart = 0;
|
||||
uint64_t f = x->module ? 0 : lifted_function_at(rip, &fstart);
|
||||
if (f)
|
||||
rt_logf(" in lifted function f_%llx = %s, host +%#llx into it\n", (unsigned long long)f,
|
||||
ldr_describe(f, d, sizeof d), (unsigned long long)(rip - fstart));
|
||||
if (x->has_regs) {
|
||||
static const char *const gpr_names[16] = PLAT_GPR_NAMES;
|
||||
for (int i = 0; i < 16; i++)
|
||||
rt_logf(" %-3s %016llx%s", gpr_names[i], (unsigned long long)x->gpr[i], i % 4 == 3 ? "\n" : "");
|
||||
crash_code_bytes(rip);
|
||||
}
|
||||
// abort and invalid parameter: where the host code called it; a fault in another module: the driver code and the
|
||||
// host calls that led into it
|
||||
uintptr_t exe = plat_exe_base();
|
||||
for (int i = 0; i < x->nframes; i++) {
|
||||
const char *m = x->frame_module[i];
|
||||
if (m) {
|
||||
rt_logf(" host frame %2d: %s+%#llx\n", i, *m ? m : "outside every module",
|
||||
(unsigned long long)x->frame_offset[i]);
|
||||
continue;
|
||||
}
|
||||
uint64_t g = lifted_function(x->frames[i]);
|
||||
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - exe));
|
||||
if (g) rt_logf(" in lifted function f_%llx", (unsigned long long)g);
|
||||
rt_log_raw("\n", 1);
|
||||
}
|
||||
if (x->frames_left_out) rt_logf(" (%d frames in other modules left out)\n", x->frames_left_out);
|
||||
Cpu *c = kern_cpu();
|
||||
if (c) {
|
||||
for (int i = 0; i < 31; i++)
|
||||
rt_logf(" x%-2d %016llx%s", i, (unsigned long long)c->x[i], i % 4 == 3 ? "\n" : "");
|
||||
rt_logf(" sp %016llx\n pc %016llx lr %s\n", (unsigned long long)c->sp, (unsigned long long)c->pc,
|
||||
ldr_describe(c->x[30], d, sizeof d));
|
||||
// pc: the guest code running, or the host function a guest call entered (its name tells the host call apart)
|
||||
rt_logf(" sp %016llx\n pc %016llx", (unsigned long long)c->sp, (unsigned long long)c->pc);
|
||||
if (hostfn_name(c->pc)) rt_logf(" (%s)", ldr_describe(c->pc, d, sizeof d));
|
||||
rt_logf(" lr %s\n", ldr_describe(c->x[30], d, sizeof d));
|
||||
uint64_t fp = c->x[29];
|
||||
for (int n = 0; n < 32 && fp && !(fp & 7) && readable(fp, 16); n++) {
|
||||
uint64_t next = LD64(fp), lr = LD64(fp + 8);
|
||||
@@ -253,6 +384,8 @@ static void crash_handler(const PlatCrash *x) {
|
||||
} else {
|
||||
rt_logf(" not on a guest thread\n");
|
||||
}
|
||||
crash_memory(x, c);
|
||||
crash_compound_shape(c);
|
||||
kern_free_cores_note();
|
||||
fflush(stderr);
|
||||
plat_exit(71);
|
||||
@@ -395,7 +528,9 @@ static void check_memory(void) {
|
||||
CHECK(q.state == 0 && q.base == g->alias.base && q.size == g->alias.size, "QueryMemory on the free alias region");
|
||||
CHECK(mem_map_physical(a, 0x1000) == 0 && LD64(a + 0x10) == 0 && mem_unmap_physical(a, 0x1000) == 0,
|
||||
"remapped physical memory is not zeroed");
|
||||
CHECK(mem_map_physical(a, 0x100000000ull) == 0x10801, "MapPhysicalMemory beyond the pool is not LimitReached");
|
||||
// past the pool whatever its size (TOTK_APP_POOL, TOTK_TEX_HEAP_EXTRA_MB): 4 GiB fits a grown one
|
||||
CHECK(mem_map_physical(a, (g->pool + 0x100000) & ~0xfffffull) == 0x10801,
|
||||
"MapPhysicalMemory beyond the pool is not LimitReached");
|
||||
mem_query(RT_HOSTFN_BASE + 0x40, &q);
|
||||
CHECK(q.state == 0x10 && q.base == RT_HOSTFN_BASE, "QueryMemory on the host-function window");
|
||||
mem_query(g->alias.base + g->alias.size + 0x1000, &q);
|
||||
@@ -462,7 +597,7 @@ static int probe_crash(int null) {
|
||||
|
||||
static int usage(const char *argv0) {
|
||||
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
|
||||
"--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-cfg | --probe-dpi | --probe-mods | --probe-playtime | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
|
||||
"--probe-vkexec | --probe-astc | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-discord | "
|
||||
"--prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
|
||||
@@ -517,6 +652,20 @@ int main(int argc, char **argv) {
|
||||
// Timed guest waits (kernel.c block_inner) sleep in whole milliseconds; at the default 15.6 ms tick a 1 ms wait
|
||||
// costs a full tick. TOTK_TIMER_1MS=0 keeps the default for comparison.
|
||||
if (cfg_bool("TOTK_TIMER_1MS")) plat_timer_1ms();
|
||||
int power = plat_power_full();
|
||||
if (power >= 0)
|
||||
rt_log("power: %s", power ? "OS power throttling off (EcoQoS, timer resolution in the background)"
|
||||
: "OS power throttling: the opt-out was refused");
|
||||
// Hybrid CPUs: the threads every frame goes through prefer the P-cores (perf_core_thread). TOTK_PCORE=0: off.
|
||||
if (rt_knob("PCORE", "1")) {
|
||||
char why[160];
|
||||
int n = plat_perf_core_setup(why, sizeof why);
|
||||
if (n) rt_log("power: %s; the game's main and render threads and the vk replay, recorder and present threads "
|
||||
"prefer the %d P-core ones (TOTK_PCORE=0 turns it off)", why, n);
|
||||
else rt_log("power: %s: no P-core preference", why);
|
||||
} else {
|
||||
rt_log("power: TOTK_PCORE=0: no P-core preference");
|
||||
}
|
||||
cfg = (RtConfig){.exefs = rt_default_exefs, .romfs = rt_default_romfs, .profile = rt_default_profile,
|
||||
.gpu = cfg_get("TOTK_GPU")};
|
||||
portable_paths(&cfg); // command-line paths below still win
|
||||
@@ -552,6 +701,7 @@ int main(int argc, char **argv) {
|
||||
if (!mode) {
|
||||
cfg.boot = cfg.seconds > 0; // --seconds starts the game like --boot, with its checkpoints
|
||||
discord_init(); // the Rich Presence thread (discord_rpc.c); it reads totk.ini itself and idles while off
|
||||
playtime_init(); // the play time thread (playtime.c): the Game tab's play time across runs
|
||||
rt_boot();
|
||||
return 0;
|
||||
}
|
||||
@@ -565,6 +715,7 @@ int main(int argc, char **argv) {
|
||||
#endif
|
||||
if (!strcmp(mode, "--probe-mods")) return probe_mods();
|
||||
if (!strcmp(mode, "--probe-discord")) return probe_discord();
|
||||
if (!strcmp(mode, "--probe-playtime")) return probe_playtime();
|
||||
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
|
||||
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
|
||||
if (!strcmp(mode, "--probe-services")) return probe_services();
|
||||
|
||||
+71
-17
@@ -20,11 +20,11 @@
|
||||
#define PAGE 0x1000ull
|
||||
#define GiB (1ull << 30)
|
||||
#define LOW_SEARCH_FROM 0x100000000ull
|
||||
#define LOW_SIZE (6 * GiB)
|
||||
#define LOW_BASE (6 * GiB) // the stack and heap block without a texture heap extra
|
||||
#define STACK_OFF 0x10000000ull
|
||||
#define STACK_SIZE 0x10000000ull
|
||||
#define HEAP_OFF (STACK_OFF + STACK_SIZE + 0x10000000ull)
|
||||
#define HEAP_CAP (4 * GiB)
|
||||
#define HEAP_CAP (4 * GiB) // without TOTK_TEX_HEAP_EXTRA_MB, which raises it by its own size (mem_init)
|
||||
#define ALIAS_SIZE (64 * GiB)
|
||||
#define ALIAS_ALIGN (4 * GiB)
|
||||
#define ALIAS_PAGES (ALIAS_SIZE / PAGE)
|
||||
@@ -48,6 +48,9 @@ enum { ATTR_PERMISSION_LOCKED = 16 };
|
||||
|
||||
static RtGuestLayout L;
|
||||
static uint64_t mods_lo, mods_hi; // fixed module window
|
||||
// The stack and heap block: LOW_BASE, plus the texture heap extra (tex_heap_extra_mb or a mod's HeapDef,
|
||||
// res_tex_heap_extra_bytes) rounded up to a GiB, so only a player who grows the texture heap reserves more (mem_init).
|
||||
static uint64_t low_size = LOW_BASE;
|
||||
static PlatMutex lock = PLAT_MUTEX_INIT;
|
||||
static uint64_t heap_size, alias_bytes;
|
||||
static uint8_t *alias_pages; // 1 per mapped 4 KiB page of the alias region
|
||||
@@ -78,7 +81,7 @@ static volatile int64_t frz_count[4]; // freezes; thaws by a write fault, by m
|
||||
|
||||
static int watched(uint64_t addr, uint64_t size) {
|
||||
const RtGuestLayout *g = &L;
|
||||
return (addr >= g->aslr.base && size <= LOW_SIZE && addr - g->aslr.base <= LOW_SIZE - size) ||
|
||||
return (addr >= g->aslr.base && size <= low_size && addr - g->aslr.base <= low_size - size) ||
|
||||
(addr >= g->alias.base && size <= ALIAS_SIZE && addr - g->alias.base <= ALIAS_SIZE - size);
|
||||
}
|
||||
|
||||
@@ -172,12 +175,29 @@ static int thaw_fault(uint64_t a) {
|
||||
}
|
||||
|
||||
// Mapping changes thaw the regions they touch and run under frz_lock, so no freeze sees them half done.
|
||||
static void commit(uint64_t a, uint64_t n, const char *what) {
|
||||
typedef struct { uint64_t a, n; } CommitTry;
|
||||
static int commit_try(void *arg) {
|
||||
const CommitTry *t = arg;
|
||||
if (rt_oom_forced(RT_OOM_COMMIT)) return 0;
|
||||
plat_mutex_lock(&frz_lock);
|
||||
thaw_range_locked(a, n, 3);
|
||||
int ok = !n || plat_mem_commit(a, n);
|
||||
thaw_range_locked(t->a, t->n, 3);
|
||||
int ok = !t->n || plat_mem_commit(t->a, t->n);
|
||||
plat_mutex_unlock(&frz_lock);
|
||||
if (!ok) rt_fatal("memory: commit %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// A refused commit waits for memory (rt_oom_wait): the system's commit ran out for a moment (a page file growing, the
|
||||
// driver's device memory charged to this process), and the Vulkan back end gives memory back at its next submit. The
|
||||
// wait holds the caller's lock (lock or klock: only memory SVCs and kernel-region allocations wait behind it) but not
|
||||
// frz_lock, which write faults on frozen regions need. Every caller waits, the boot's ones too: there a refusal has the
|
||||
// same cause, and the wait only delays an end the game would reach anyway.
|
||||
static void commit(uint64_t a, uint64_t n, const char *what) {
|
||||
CommitTry t = {a, n};
|
||||
if (commit_try(&t)) return;
|
||||
char w[64];
|
||||
snprintf(w, sizeof w, "memory: commit of %s at %#llx", what, (unsigned long long)a);
|
||||
if (!rt_oom_wait(w, n, commit_try, &t))
|
||||
rt_fatal("memory: commit %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
|
||||
}
|
||||
|
||||
static void decommit(uint64_t a, uint64_t n, const char *what) {
|
||||
@@ -221,10 +241,15 @@ void mem_init(void) {
|
||||
|
||||
// Stack and heap block, then the alias region above it, both searched below the module window.
|
||||
uint64_t low = 0, alias = 0;
|
||||
for (uint64_t at = LOW_SEARCH_FROM; at + LOW_SIZE <= mods_lo && !low; at += GiB)
|
||||
if (reserve_at(at, LOW_SIZE, 1)) low = at;
|
||||
if (!low) reexec_or_fail("a 6 GiB stack and heap block below 2^39");
|
||||
for (uint64_t at = align_up(low + LOW_SIZE, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
|
||||
low_size = LOW_BASE + align_up(res_tex_heap_extra_bytes(), GiB);
|
||||
for (uint64_t at = LOW_SEARCH_FROM; at + low_size <= mods_lo && !low; at += GiB)
|
||||
if (reserve_at(at, low_size, 1)) low = at;
|
||||
if (!low) {
|
||||
char what[64];
|
||||
snprintf(what, sizeof what, "a %llu GiB stack and heap block below 2^39", (unsigned long long)(low_size / GiB));
|
||||
reexec_or_fail(what);
|
||||
}
|
||||
for (uint64_t at = align_up(low + low_size, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
|
||||
if (reserve_at(at, ALIAS_SIZE, 1)) alias = at;
|
||||
if (!alias) reexec_or_fail("a 64 GiB alias region below 2^39");
|
||||
|
||||
@@ -238,11 +263,11 @@ void mem_init(void) {
|
||||
L = (RtGuestLayout){
|
||||
.aslr = {low, RT_ADDRESS_LIMIT - low},
|
||||
.stack = {low + STACK_OFF, STACK_SIZE},
|
||||
.heap = {low + HEAP_OFF, HEAP_CAP},
|
||||
.heap = {low + HEAP_OFF, HEAP_CAP + res_tex_heap_extra_bytes()},
|
||||
.alias = {alias, ALIAS_SIZE},
|
||||
.kernel = {RT_KERNEL_BASE, RT_KERNEL_SIZE},
|
||||
.hostfn = {RT_HOSTFN_BASE, RT_HOSTFN_SIZE},
|
||||
.pool = pool ? strtoull(pool, NULL, 0) : DEFAULT_POOL + res_heap_extra_bytes(), // resolution.c
|
||||
.pool = pool ? strtoull(pool, NULL, 0) : DEFAULT_POOL + res_heap_extra_bytes() + res_tex_heap_extra_bytes(),
|
||||
};
|
||||
if (!L.pool) rt_fatal("memory: TOTK_APP_POOL is 0");
|
||||
}
|
||||
@@ -276,7 +301,7 @@ static int inside(uint64_t a, uint64_t n, RtRange r) { return a >= r.base && n <
|
||||
// ---- 0x01 SetHeapSize
|
||||
uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
|
||||
plat_mutex_lock(&lock);
|
||||
if (size > HEAP_CAP || size % 0x200000 || size + image_bytes() > L.pool) {
|
||||
if (size > L.heap.size || size % 0x200000 || size + image_bytes() > L.pool) {
|
||||
plat_mutex_unlock(&lock);
|
||||
return R_INVALID_STATE;
|
||||
}
|
||||
@@ -288,6 +313,35 @@ uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
|
||||
return R_OK;
|
||||
}
|
||||
|
||||
// The runtime's readers of game objects (menu_cursor.c, mouse_direct.c) check every pointer before they follow it. An
|
||||
// OS query (plat_mem_readable: VirtualQuery) costs tens of microseconds in this process, and a reader that checks a
|
||||
// few dozen pointers at a time stalls the game thread for milliseconds. The game's objects live in the committed heap
|
||||
// and its globals in the modules' images, which are readable for as long as they are mapped: those answer from the
|
||||
// layout. Any other range asks the OS. The heap only shrinks when the game asks for it (mem_set_heap_size), not while
|
||||
// it runs.
|
||||
int mem_guest_readable(uint64_t a, uint64_t n) {
|
||||
static uint64_t fast, slow;
|
||||
if (a + n < a) return 0;
|
||||
uint64_t hs = __atomic_load_n(&heap_size, __ATOMIC_ACQUIRE);
|
||||
int ok = a >= L.heap.base && a + n <= L.heap.base + hs;
|
||||
for (int i = 0; !ok && i < ldr_count(); i++) {
|
||||
const RtLoaded *m = ldr_module(i);
|
||||
ok = a >= m->base && a + n <= m->base + m->size;
|
||||
}
|
||||
if (ok) {
|
||||
if (__atomic_add_fetch(&fast, 1, __ATOMIC_RELAXED) == 100000)
|
||||
rt_log("memory: 100000 guest reads checked by the layout, %llu by the OS",
|
||||
(unsigned long long)__atomic_load_n(&slow, __ATOMIC_RELAXED));
|
||||
} else {
|
||||
ok = plat_mem_readable(a, n);
|
||||
uint64_t s = __atomic_add_fetch(&slow, 1, __ATOMIC_RELAXED);
|
||||
if (!(s & (s - 1)) && s >= 1024) // 1024, 2048, 4096...: how often the layout could not answer
|
||||
rt_log("memory: guest reads checked by the layout %llu, by the OS %llu",
|
||||
(unsigned long long)__atomic_load_n(&fast, __ATOMIC_RELAXED), (unsigned long long)s);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- module data permissions (0x02, 0x03) and module pages for QueryMemory
|
||||
static int module_index(uint64_t a) {
|
||||
for (int i = 0; i < ldr_count(); i++) {
|
||||
@@ -477,7 +531,7 @@ static void taken_gap(uint64_t a, uint64_t *base, uint64_t *size) {
|
||||
uint64_t lo = 0, hi = RT_ADDRESS_LIMIT;
|
||||
RtRange known[3 + MOD_COUNT + 2];
|
||||
int n = 0;
|
||||
known[n++] = (RtRange){L.aslr.base, LOW_SIZE};
|
||||
known[n++] = (RtRange){L.aslr.base, low_size};
|
||||
known[n++] = L.alias;
|
||||
known[n++] = L.kernel;
|
||||
known[n++] = L.hostfn;
|
||||
@@ -520,14 +574,14 @@ uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
|
||||
r.base = 0;
|
||||
r.size = low;
|
||||
r.state = ST_FREE;
|
||||
} else if (a < low + LOW_SIZE) {
|
||||
} else if (a < low + low_size) {
|
||||
uint64_t heap = L.heap.base;
|
||||
if (a >= heap && a < heap + heap_size) {
|
||||
r = (RtMemoryInfo){.base = heap, .size = heap_size, .state = ST_NORMAL, .perm = PERM_RW};
|
||||
} else if (a < heap) {
|
||||
r = (RtMemoryInfo){.base = low, .size = heap - low, .state = ST_FREE};
|
||||
} else {
|
||||
r = (RtMemoryInfo){.base = heap + heap_size, .size = low + LOW_SIZE - heap - heap_size, .state = ST_FREE};
|
||||
r = (RtMemoryInfo){.base = heap + heap_size, .size = low + low_size - heap - heap_size, .state = ST_FREE};
|
||||
}
|
||||
} else if (a - L.alias.base < L.alias.size) {
|
||||
uint64_t p = (a - L.alias.base) / PAGE;
|
||||
|
||||
+64
-6
@@ -53,7 +53,7 @@ enum { PANE_PARENT = 0x18, PANE_SIZE = 0x50, PANE_FLAG = 0x58, PANE_ALPHA = 0x59
|
||||
static uint64_t main_lo, main_hi;
|
||||
|
||||
static int ok_at(uint64_t p, uint64_t n) {
|
||||
return p >= 0x10000 && p < (1ull << 47) && (p & 7) == 0 && plat_mem_readable(p, n);
|
||||
return p >= 0x10000 && p < (1ull << 47) && (p & 7) == 0 && mem_guest_readable(p, n);
|
||||
}
|
||||
static uint64_t rd64(uint64_t a) { uint64_t v; memcpy(&v, (const void *)(uintptr_t)a, 8); return v; }
|
||||
static uint32_t rd32(uint64_t a) { uint32_t v; memcpy(&v, (const void *)(uintptr_t)a, 4); return v; }
|
||||
@@ -157,11 +157,45 @@ static Focus focus_now(void) {
|
||||
static void layout_name(uint64_t scr, char *out, size_t n) {
|
||||
snprintf(out, n, "?");
|
||||
uint64_t lyt = rd64(scr + SCR_LAYOUT), name = ok_at(lyt, LYT_NAME + 8) ? rd64(lyt + LYT_NAME) : 0;
|
||||
if (name < 0x10000 || name >= (1ull << 47) || !plat_mem_readable(name, n)) return;
|
||||
if (name < 0x10000 || name >= (1ull << 47) || !mem_guest_readable(name, n)) return;
|
||||
memcpy(out, (const void *)(uintptr_t)name, n - 1);
|
||||
out[n - 1] = 0;
|
||||
for (char *q = out; *q; q++) if (*q < 0x20 || *q > 0x7e) { *q = 0; break; }
|
||||
}
|
||||
// The open screens: eui::ScreenMgr's screen array, count at +0x8 and its pointers at +0x10 (1.0.0's layout, which the
|
||||
// 1.4.3 constructor keeps). The map has no focus box, so it is found here: the Purah Pad's frame (AppMenuOverlay_00)
|
||||
// with AppMap_00 and the menu's background AppMenuBG_00 open. The Scope opens the frame over AppMap_00 without that
|
||||
// background (Weeping of the Empire's engine/runtime/totk_menu_cursor.cpp census tells them apart the same way).
|
||||
// TOTK_MENU_CURSOR_LOG=1 logs the open screens whenever the set changes.
|
||||
enum { SM_COUNT = 0x8, SM_SCREENS = 0x10 };
|
||||
static int map_open(void) {
|
||||
uint64_t sm = screen_mgr();
|
||||
if (!sm) return 0;
|
||||
int32_t n = (int32_t)rd32(sm + SM_COUNT);
|
||||
uint64_t arr = rd64(sm + SM_SCREENS);
|
||||
if (n <= 0 || n > 1024 || !ok_at(arr, 8ull * (uint64_t)n)) return 0;
|
||||
const char *e = cfg_get("TOTK_MENU_CURSOR_LOG");
|
||||
int verbose = e && !strcmp(e, "1"), pad = 0, map = 0, bg = 0;
|
||||
char list[1024];
|
||||
size_t len = 0;
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
uint64_t scr = rd64(arr + 8ull * (uint64_t)i);
|
||||
if (!game_object(scr, SCR_REQUEST + 1) || rd8(scr + SCR_STATE) != SCR_OPENED) continue;
|
||||
char nm[48];
|
||||
layout_name(scr, nm, sizeof nm);
|
||||
pad |= !strcmp(nm, "AppMenuOverlay_00");
|
||||
map |= !strcmp(nm, "AppMap_00");
|
||||
bg |= !strcmp(nm, "AppMenuBG_00");
|
||||
if (!verbose) continue;
|
||||
for (const char *c = nm; *c; c++) h = (h ^ (uint8_t)*c) * 1099511628211ull;
|
||||
if (len < sizeof list - 64) len += (size_t)snprintf(list + len, sizeof list - len, "%s%s", len ? " " : "", nm);
|
||||
}
|
||||
static uint64_t last;
|
||||
static int lines;
|
||||
if (verbose && h != last && lines < 200) last = h, lines++, rt_log("menu_cursor: open screens: %s", list);
|
||||
return pad && map && bg;
|
||||
}
|
||||
static void layout_size(uint64_t scr, float *lw, float *lh) {
|
||||
*lw = 1920, *lh = 1080;
|
||||
uint64_t lyt = rd64(scr + SCR_LAYOUT);
|
||||
@@ -245,8 +279,10 @@ static int gameplay_screen(const char *nm) {
|
||||
}
|
||||
|
||||
static volatile int32_t g_pointer; // a menu has the pointer: input_cursor_allowed releases the capture
|
||||
static volatile int32_t g_map; // the Purah Pad's map is open (not the Scope): the mouse moves its cursor
|
||||
|
||||
int menu_cursor_pointer(void) { return __atomic_load_n(&g_pointer, __ATOMIC_RELAXED); }
|
||||
int menu_cursor_map(void) { return __atomic_load_n(&g_map, __ATOMIC_RELAXED); }
|
||||
|
||||
static int enabled(void) {
|
||||
if (!RT_V143) return 0;
|
||||
@@ -282,29 +318,40 @@ static void log_screen(const Focus *f, const char *nm, int n, float lw, float lh
|
||||
// Once per pad read on the game thread: the pointer's hover and clicks, as pad buttons (A, B) for this read
|
||||
void menu_cursor_poll(uint64_t *buttons) {
|
||||
static int inited, pending_a, held_l, held_r;
|
||||
static uint64_t pending_node, pending_until, hold_until, hold_bit;
|
||||
static uint64_t pending_node, pending_until, hold_until, hold_bit, menu_seen;
|
||||
static float last_px = -1, last_py = -1;
|
||||
if (!inited) {
|
||||
inited = 1;
|
||||
main_lo = MOD_MAIN_BASE, main_hi = MOD_MAIN_BASE + MAIN_SIZE;
|
||||
if (RT_V143) rt_log("menu_cursor: on for 1.4.3 (TOTK_MENU_CURSOR=0 turns it off)");
|
||||
}
|
||||
if (!enabled()) { set_pointer(0); return; }
|
||||
if (!enabled()) { set_pointer(0); __atomic_store_n(&g_map, 0, __ATOMIC_RELAXED); return; }
|
||||
Focus f = focus_now();
|
||||
char nm[48] = "";
|
||||
if (f.screen) layout_name(f.screen, nm, sizeof nm);
|
||||
// The open screens are scanned every 100 ms, not at every pad read: each screen costs memory queries
|
||||
uint64_t now = plat_now_ms();
|
||||
static uint64_t map_at;
|
||||
static int map, map_logged;
|
||||
if (!map_at || now - map_at >= 100) map_at = now, map = map_open();
|
||||
if (map && !map_logged) map_logged = 1, rt_log("menu_cursor: the map is open: the mouse moves its cursor");
|
||||
__atomic_store_n(&g_map, map, __ATOMIC_RELAXED);
|
||||
// With the gamepad as the last device the menus stay as they are on the console: no pointer, the capture kept.
|
||||
// Moving the mouse (or a key) makes keyboard and mouse the last device and the pointer comes.
|
||||
int menu = f.screen && !gameplay_screen(nm) && ikb_foreground() && input_kbm_last();
|
||||
set_pointer(menu);
|
||||
if (menu) menu_seen = now;
|
||||
// A menu's focus gap (a tab switch, a popup opening: no focus box for a frame or two) keeps the pointer where it
|
||||
// is, for up to 300 ms: releasing it there captured it again and centred it when the next tab took the focus
|
||||
int gap = !menu && !f.screen && menu_seen && now - menu_seen < 300 && ikb_foreground() && input_kbm_last();
|
||||
set_pointer(menu || gap);
|
||||
int l = ikb_down(KEY_MOUSE_L), r = ikb_down(KEY_MOUSE_R);
|
||||
int click_l = l && !held_l, click_r = r && !held_r;
|
||||
held_l = l, held_r = r;
|
||||
uint64_t now = plat_now_ms();
|
||||
if (now < hold_until) { // a click's button, held for 60 ms so the game sees one press whatever its read rate
|
||||
*buttons |= hold_bit;
|
||||
return;
|
||||
}
|
||||
if (gap) return; // a click's focus request waits for the next screen
|
||||
if (!menu) { pending_a = 0; last_px = last_py = -1; return; }
|
||||
if (pending_a) { // the focus asked for by a click: A once it is there
|
||||
if (rd64(f.control + CTL_ACTIVE) == pending_node) {
|
||||
@@ -338,3 +385,14 @@ void menu_cursor_poll(uint64_t *buttons) {
|
||||
else pending_a = 1, pending_node = at, pending_until = now + 300;
|
||||
}
|
||||
}
|
||||
|
||||
// The L ability wheel or a D-pad strip (materials, weapons, shields, bows) has the focus: 1 / 0, -1 with the menu
|
||||
// cursor off (the screens are not read). input.c asks while L or the D-pad is held, before it gives the mouse to their
|
||||
// selectors: L also soothes the horse and turns menu tabs, the D-pad does other things too.
|
||||
int menu_cursor_shortcut(void) {
|
||||
if (!enabled()) return -1;
|
||||
Focus f = focus_now();
|
||||
char nm[48] = "";
|
||||
if (f.screen) layout_name(f.screen, nm, sizeof nm);
|
||||
return f.screen && (!strcmp(nm, "ShortCutMainItem_00") || !strcmp(nm, "ShortCut_00"));
|
||||
}
|
||||
+248
-45
@@ -15,7 +15,9 @@
|
||||
// when its build id is main's (an IPS file's name, a pchtxt file's @nsobid line); others are another version's and
|
||||
// counted. The setup builds every mod's files into patch sets and writes build-cache/exefs.status next to totk.exe
|
||||
// (tools/exefs_patches.py has the format): a file whose line is missing or has another content hash waits for the
|
||||
// rebuild at start (cheats.c), and its mod stays off until then.
|
||||
// rebuild at start (cheats.c), and its mod stays off until then. A file's writes into main's data (.rodata, .data,
|
||||
// .bss) are lines of build-cache/exefs.data, stored into the loaded main before the game runs (mods_exefs_data), as
|
||||
// emulators apply a patch file to the module before its dynamic linker runs.
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -170,22 +172,26 @@ static int same_build(const char *hex, size_t n) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// build-cache/exefs.status next to totk.exe, read once; "" when there is none.
|
||||
static const char *exefs_status(void) {
|
||||
static char *text;
|
||||
if (text) return text;
|
||||
// build-cache/<name> next to totk.exe, read once into *text; "" when there is none (mods_probe_cache sets the text).
|
||||
static char *status_text, *data_text;
|
||||
static const char *build_cache(const char *name, char **text) {
|
||||
if (*text) return *text;
|
||||
char path[2100];
|
||||
uint64_t n = 0;
|
||||
if (plat_exe_path(path, sizeof path)) {
|
||||
char *cut = strrchr(path, '/'), *back = strrchr(path, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (cut) snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/exefs.status");
|
||||
text = (char *)plat_file_read(path, &n);
|
||||
if (cut && snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/%s", name) <
|
||||
(int)(sizeof path - (size_t)(cut + 1 - path)))
|
||||
*text = (char *)plat_file_read(path, &n);
|
||||
}
|
||||
if (!text) text = calloc(1, 1);
|
||||
return text;
|
||||
if (!*text) *text = calloc(1, 1);
|
||||
return *text;
|
||||
}
|
||||
|
||||
static const char *exefs_status(void) { return build_cache("exefs.status", &status_text); }
|
||||
static const char *exefs_data(void) { return build_cache("exefs.data", &data_text); }
|
||||
|
||||
// The status line of key: its kind, built-in sets and note, NUL-terminated copies; 0 when none.
|
||||
static int status_line(const char *key, const char *content, char *kind, char *over, char *note) {
|
||||
char want[20];
|
||||
@@ -194,8 +200,9 @@ static int status_line(const char *key, const char *content, char *kind, char *o
|
||||
const char *end = strchr(line, '\n');
|
||||
size_t len = end ? (size_t)(end - line) : strlen(line);
|
||||
if (len > 18 && !memcmp(line, want, 18)) {
|
||||
char copy[600];
|
||||
snprintf(copy, sizeof copy, "%.*s", (int)(len < sizeof copy ? len : sizeof copy - 1), line + 18);
|
||||
char copy[600]; // the line after the two hashes, without the next line
|
||||
size_t rest = len - 18;
|
||||
snprintf(copy, sizeof copy, "%.*s", (int)(rest < sizeof copy ? rest : sizeof copy - 1), line + 18);
|
||||
char *t1 = strchr(copy, '\t'), *t2 = t1 ? strchr(t1 + 1, '\t') : NULL;
|
||||
if (!t1 || !t2) return 0;
|
||||
*t1 = *t2 = 0;
|
||||
@@ -269,9 +276,14 @@ static int exefs_file_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
|
||||
m->sets[m->nsets++] = (uint32_t)strtoul(key, NULL, 16);
|
||||
size_t k = strlen(m->overrides);
|
||||
if (*over) snprintf(m->overrides + k, sizeof m->overrides - k, "%s%s", k ? "," : "", over);
|
||||
} else if (!strcmp(kind_s, "data") && m->ndata < (int)(sizeof m->data / sizeof *m->data)) {
|
||||
m->data[m->ndata++] = (uint32_t)strtoul(key, NULL, 16);
|
||||
} else if (!strcmp(kind_s, "other")) {
|
||||
m->other_version++;
|
||||
}
|
||||
unsigned long bytes; // a code or data file's data writes: exefs.data must hold them all (mods_exefs_data)
|
||||
if ((!strcmp(kind_s, "code") || !strcmp(kind_s, "data")) && sscanf(note, "data %lu", &bytes) == 1)
|
||||
m->data_bytes += (uint32_t)bytes;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -326,34 +338,62 @@ static int scan_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
if (m->cheats[0]) plat_fs_list(m->cheats, cheats_cb, &m->kind);
|
||||
int unsupported = exefs_check(m, path);
|
||||
// a mod whose code part the port cannot use is off as a whole: its RomFS files may expect that code
|
||||
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
|
||||
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
|
||||
: unsupported ? MOD_EXEFS_UNSUPPORTED
|
||||
: m->pending ? MOD_EXEFS_LATER
|
||||
: m->nsets || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
|
||||
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
|
||||
: MOD_EMPTY;
|
||||
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
|
||||
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
|
||||
: unsupported ? MOD_EXEFS_UNSUPPORTED
|
||||
: m->pending ? MOD_EXEFS_LATER
|
||||
: m->nsets || m->ndata || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
|
||||
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
|
||||
: MOD_EMPTY;
|
||||
m->enabled = !listed(disabled, m->name);
|
||||
nmods++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mods_scan(void) {
|
||||
static int done;
|
||||
if (done) return;
|
||||
done = 1;
|
||||
// The mods folder into out: TOTK_MODS_DIR, else <dir of totk.exe>/mods. 0 when the exe's path is unknown.
|
||||
static int mods_folder(char *out, size_t cap) {
|
||||
const char *d = cfg_get("TOTK_MODS_DIR");
|
||||
if (d && *d) {
|
||||
snprintf(dir, sizeof dir, "%s", d);
|
||||
} else {
|
||||
char exe[2048];
|
||||
if (!plat_exe_path(exe, sizeof exe)) return;
|
||||
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (!cut) return;
|
||||
*cut = 0;
|
||||
snprintf(dir, sizeof dir, "%s/mods", exe);
|
||||
}
|
||||
if (d && *d) return snprintf(out, cap, "%s", d) < (int)cap;
|
||||
char exe[2048];
|
||||
if (!plat_exe_path(exe, sizeof exe)) return 0;
|
||||
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (!cut) return 0;
|
||||
*cut = 0;
|
||||
return snprintf(out, cap, "%s/mods", exe) < (int)cap;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *folder, *disabled;
|
||||
void (*cb)(const char *name, const char *romfs, void *ctx);
|
||||
void *ctx;
|
||||
} Peek;
|
||||
|
||||
static int peek_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
const Peek *pk = ctx;
|
||||
if (kind != PLAT_FS_DIR || listed(pk->disabled, name)) return 1;
|
||||
char path[2100], romfs[2048];
|
||||
if (snprintf(path, sizeof path, "%s/%s", pk->folder, name) >= (int)sizeof path) return 1;
|
||||
find_root(path, "romfs", romfs, sizeof romfs);
|
||||
if (!romfs[0]) find_root(path, "romfslite", romfs, sizeof romfs);
|
||||
if (romfs[0]) pk->cb(name, romfs, pk->ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx) {
|
||||
char folder[2048];
|
||||
if (!mods_folder(folder, sizeof folder) || !plat_dir_exists(folder)) return;
|
||||
Peek pk = {folder, cfg_get("TOTK_MODS_DISABLED"), cb, ctx};
|
||||
plat_fs_list(folder, peek_cb, &pk);
|
||||
}
|
||||
|
||||
static int scanned;
|
||||
|
||||
void mods_scan(void) {
|
||||
if (scanned) return;
|
||||
scanned = 1;
|
||||
if (!mods_folder(dir, sizeof dir)) return;
|
||||
if (!plat_dir_exists(dir)) {
|
||||
rt_log("mods: no folder %s", dir);
|
||||
return;
|
||||
@@ -376,18 +416,171 @@ void mods_scan(void) {
|
||||
rt_log("mods: %d folders in %s", nmods, dir);
|
||||
}
|
||||
|
||||
void mods_exefs_switch(void) {
|
||||
mods_scan();
|
||||
for (int i = 0; i < nmods; i++) {
|
||||
const ModInfo *m = &mods[i];
|
||||
if (!start_enabled[i] || m->problem != MOD_OK || !m->nsets) continue;
|
||||
for (int k = 0; k < m->nsets; k++) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof name, "mod_%08x", m->sets[k]);
|
||||
int set = rt_patch_set(name);
|
||||
if (set >= 0) rt_patch_switch(set, 1);
|
||||
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
|
||||
// ---- ExeFS data writes (build-cache/exefs.data; tools/exefs_patches.py data_line has the format)
|
||||
|
||||
// 1 when key names one of m's usable ExeFS patch files: one with a patch set, or one with data writes only.
|
||||
static int owns(const ModInfo *m, uint32_t key) {
|
||||
for (int k = 0; k < m->nsets; k++)
|
||||
if (m->sets[k] == key) return 1;
|
||||
for (int k = 0; k < m->ndata; k++)
|
||||
if (m->data[k] == key) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int hex_digit(char c) {
|
||||
return c >= '0' && c <= '9' ? c - '0'
|
||||
: c >= 'a' && c <= 'f' ? c - 'a' + 10
|
||||
: c >= 'A' && c <= 'F' ? c - 'A' + 10
|
||||
: -1;
|
||||
}
|
||||
|
||||
// One line: "<file hash>\t<content hash>\t<offset in main>\t<bytes>[\tbase]", all hex. 0 when malformed.
|
||||
typedef struct { char key[9], content[9]; uint64_t off; uint8_t bytes[256]; int n, base; } DataRun;
|
||||
static int parse_run(const char *line, size_t len, DataRun *r) {
|
||||
char copy[1100], *f[6];
|
||||
if (len >= sizeof copy) return 0;
|
||||
memcpy(copy, line, len);
|
||||
copy[len] = 0;
|
||||
int nf = 0;
|
||||
for (char *p = copy; p && nf < 6; nf++) {
|
||||
f[nf] = p;
|
||||
p = strchr(p, '\t');
|
||||
if (p) *p++ = 0;
|
||||
}
|
||||
if (nf < 4 || nf > 5 || strlen(f[0]) != 8 || strlen(f[1]) != 8) return 0;
|
||||
memcpy(r->key, f[0], 9);
|
||||
memcpy(r->content, f[1], 9);
|
||||
char *end;
|
||||
r->off = strtoull(f[2], &end, 16);
|
||||
size_t hex = strlen(f[3]);
|
||||
if (!*f[2] || *end || !hex || hex % 2 || hex / 2 > sizeof r->bytes) return 0;
|
||||
r->n = (int)(hex / 2);
|
||||
for (int i = 0; i < r->n; i++) {
|
||||
int hi = hex_digit(f[3][2 * i]), lo = hex_digit(f[3][2 * i + 1]);
|
||||
if (hi < 0 || lo < 0) return 0;
|
||||
r->bytes[i] = (uint8_t)(hi << 4 | lo);
|
||||
}
|
||||
r->base = nf == 5;
|
||||
return nf == 4 || (!strcmp(f[4], "base") && r->n == 8);
|
||||
}
|
||||
|
||||
// Stores n bytes into the loaded main at a: a read-only page (.rodata, loader.c seal) is writable for its part of the
|
||||
// copy only, as cheats.c wr_main does. 0 when a page is neither readable nor writable.
|
||||
static int store_main(uint64_t a, const uint8_t *from, int n) {
|
||||
while (n > 0) {
|
||||
uint64_t page = a & ~0xFFFull, end;
|
||||
int k = (int)(page + 0x1000 - a);
|
||||
if (k > n) k = n;
|
||||
int prot = plat_mem_state(page, page + 0x1000, &end);
|
||||
if (prot != PLAT_PROT_READ && prot != PLAT_PROT_RW) return 0;
|
||||
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return 0;
|
||||
memcpy((void *)(uintptr_t)a, from, (size_t)k);
|
||||
if (prot == PLAT_PROT_READ) plat_mem_protect(page, 0x1000, PLAT_PROT_READ);
|
||||
a += (uint64_t)k;
|
||||
from += k;
|
||||
n -= k;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
|
||||
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
|
||||
if (!store) store = store_main;
|
||||
long total = 0, found = 0;
|
||||
for (int pass = 0; pass < 2; pass++) { // pass 0 checks every line, pass 1 stores
|
||||
// exefs.data must hold every byte the status lines count: one missing or cut short would leave the code
|
||||
// patches on without their data
|
||||
if (pass && found != (long)m->data_bytes) {
|
||||
rt_log("mods: %s: build-cache/exefs.data holds %ld of the %u bytes its ExeFS patch files write", m->name,
|
||||
found, m->data_bytes);
|
||||
return -1;
|
||||
}
|
||||
for (const char *line = exefs_data(), *next; *line; line = next) {
|
||||
const char *nl = strchr(line, '\n');
|
||||
size_t len = nl ? (size_t)(nl - line) : strlen(line);
|
||||
next = nl ? nl + 1 : line + len;
|
||||
if (len && line[len - 1] == '\r') len--;
|
||||
char key[9];
|
||||
snprintf(key, sizeof key, "%.*s", (int)(len < 8 ? len : 8), line);
|
||||
if (!len || *line == '#' || !owns(m, (uint32_t)strtoul(key, NULL, 16))) continue;
|
||||
DataRun r;
|
||||
char kind[16], over[256], note[256];
|
||||
// a line whose file the status does not list as built (another content) or that leaves main's data: the
|
||||
// two build-cache files and this build disagree
|
||||
if (!parse_run(line, len, &r) || !status_line(r.key, r.content, kind, over, note) ||
|
||||
(strcmp(kind, "code") && strcmp(kind, "data")) || r.off < code_end || r.off > size ||
|
||||
(uint64_t)r.n > size - r.off) {
|
||||
rt_log("mods: %s: a line of build-cache/exefs.data does not fit this build: %.*s", m->name, (int)len,
|
||||
line);
|
||||
return -1;
|
||||
}
|
||||
if (!pass) {
|
||||
found += r.n;
|
||||
continue;
|
||||
}
|
||||
if (r.base) { // a DT_RELR word holds its addend; the dynamic linker adds the module's base
|
||||
uint64_t w;
|
||||
memcpy(&w, r.bytes, 8);
|
||||
w += base;
|
||||
memcpy(r.bytes, &w, 8);
|
||||
}
|
||||
if (!store(base + r.off, r.bytes, r.n)) {
|
||||
rt_log("mods: %s: cannot write main+%#llx", m->name, (unsigned long long)r.off);
|
||||
return -1;
|
||||
}
|
||||
total += r.n;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static char *dup_text(const char *s) {
|
||||
if (!s) return NULL;
|
||||
size_t n = strlen(s) + 1;
|
||||
char *d = malloc(n);
|
||||
if (d) memcpy(d, s, n);
|
||||
return d;
|
||||
}
|
||||
|
||||
void mods_probe_cache(const char *status, const char *data) {
|
||||
free(status_text);
|
||||
free(data_text);
|
||||
status_text = dup_text(status);
|
||||
data_text = dup_text(data);
|
||||
}
|
||||
|
||||
void mods_rescan(void) {
|
||||
free(mods);
|
||||
mods = NULL;
|
||||
nmods = 0;
|
||||
scanned = 0;
|
||||
}
|
||||
|
||||
// Switches the patch set mod_<key> on; 0 when this build lacks it (logged).
|
||||
static int set_on(const ModInfo *m, uint32_t key) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof name, "mod_%08x", key);
|
||||
int set = rt_patch_set(name);
|
||||
if (set >= 0) rt_patch_switch(set, 1);
|
||||
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
|
||||
return set >= 0;
|
||||
}
|
||||
|
||||
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
|
||||
int (*store)(uint64_t addr, const uint8_t *bytes, int n)) {
|
||||
// bottom first: the top mod's data writes land last and win, as its RomFS files do. Patch sets are only switches
|
||||
// (rt_patch_switch), so their order does not matter.
|
||||
for (int i = nmods - 1; i >= 0; i--) {
|
||||
const ModInfo *m = &mods[i];
|
||||
if (!start_enabled[i] || m->problem != MOD_OK || !(m->nsets || m->ndata)) continue;
|
||||
// the data first: a mod whose data writes cannot all be stored keeps its code patches off too, as half of a
|
||||
// patch file is worse than none
|
||||
long stored = size ? mods_exefs_data(m, base, size, code_end, store) : 0;
|
||||
if (stored < 0) {
|
||||
rt_log("mods: %s: its ExeFS patch files stay off: their data writes could not all be stored", m->name);
|
||||
continue;
|
||||
}
|
||||
for (int k = 0; k < m->nsets; k++) set_on(m, m->sets[k]);
|
||||
char over[256];
|
||||
snprintf(over, sizeof over, "%s", m->overrides);
|
||||
for (char *s = strtok(over, ","); s; s = strtok(NULL, ",")) {
|
||||
@@ -395,10 +588,20 @@ void mods_exefs_switch(void) {
|
||||
if (set >= 0) rt_patch_switch(set, 0);
|
||||
rt_log("mods: %s replaces the port's patch set %s; it stays off", m->name, s);
|
||||
}
|
||||
rt_log("mods: %s: %d ExeFS patch files on", m->name, m->nsets);
|
||||
rt_log("mods: %s: %d ExeFS patch files on, %ld bytes of main's data stored", m->name, m->nsets + m->ndata,
|
||||
stored);
|
||||
}
|
||||
}
|
||||
|
||||
void mods_exefs_switch(void) {
|
||||
mods_scan();
|
||||
const RtLoaded *main_mod = NULL;
|
||||
for (int i = 0; ldr_module(i); i++)
|
||||
if (!strcmp(ldr_module(i)->name, "main")) main_mod = ldr_module(i);
|
||||
if (main_mod) mods_exefs_apply(main_mod->base, main_mod->size, main_mod->text_end, NULL);
|
||||
else mods_exefs_apply(0, 0, 0, NULL);
|
||||
}
|
||||
|
||||
int mods_exefs_pending(uint32_t *key) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < nmods; i++) {
|
||||
|
||||
+27
-2
@@ -2,7 +2,8 @@
|
||||
// them: <mod>/romfs, <mod>/exefs and <mod>/cheats, the Atmosphere layout (atmosphere/contents/<title id>/romfs,
|
||||
// atmosphere/exefs_patches/<name>/*.ips) or TKMM's merged output (romfs or romfslite, exefs, cheats). ExeFS patch
|
||||
// files (.ips, .pchtxt) for the game's main are patch sets the setup builds into the game (tools/exefs_patches.py),
|
||||
// switched with their mod (mods_exefs_switch). The list is scanned once at start (romfs.c overlay_init);
|
||||
// switched with their mod (mods_exefs_switch), and data writes stored into main before the game runs (mods_exefs_data).
|
||||
// The list is scanned once at start (romfs.c overlay_init);
|
||||
// its order and switches live in totk.ini [mods] (TOTK_MODS_ORDER: names top first, separated by '|';
|
||||
// TOTK_MODS_DISABLED: names switched off). The top mod wins a file two mods replace.
|
||||
#pragma once
|
||||
@@ -36,10 +37,17 @@ typedef struct {
|
||||
uint32_t pending, pending_key; // ExeFS patch files the build lacks; FNV-1a of their names and contents
|
||||
int nsets;
|
||||
uint32_t sets[16]; // the built patch sets mod_<hash> of its ExeFS patch files
|
||||
int ndata;
|
||||
uint32_t data[16]; // the hashes of its ExeFS patch files with data writes only (no set)
|
||||
uint32_t data_bytes; // the bytes its usable ExeFS patch files write into main's data, as their
|
||||
// status lines count them ("data <bytes>")
|
||||
char overrides[256]; // the port's own patch sets its patches replace, comma separated
|
||||
} ModInfo;
|
||||
|
||||
void mods_scan(void); // once; later calls return at once
|
||||
// Every switched-on mod folder's RomFS root, without the rest of the scan (no ExeFS checks, no order): for mem_init,
|
||||
// which runs before the paths mods_scan needs are set (resolution.c mod_tex_heap_kib). cb gets the name and root.
|
||||
void mods_peek_romfs(void (*cb)(const char *name, const char *romfs, void *ctx), void *ctx);
|
||||
const char *mods_dir(void); // the folder scanned (default: <dir of totk.exe>/mods)
|
||||
int mods_count(void);
|
||||
ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
|
||||
@@ -48,8 +56,25 @@ ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
|
||||
void mods_set_enabled(int i, int on);
|
||||
void mods_move(int i, int delta); // -1 up, +1 down; nothing at the ends
|
||||
int mods_changed(void); // 1 when the order or a switch differs from the start
|
||||
// After rt_patches_init: the patch sets of the enabled mods' ExeFS patches on, the port's own sets they replace off.
|
||||
// After ldr_load and rt_patches_init, before any game code: the patch sets of the enabled mods' ExeFS patches on, the
|
||||
// port's own sets they replace off, and their data writes stored into main (mods_exefs_data).
|
||||
void mods_exefs_switch(void);
|
||||
// What mods_exefs_switch does with main's image at base (size bytes, code below code_end; size 0: no data stored), for
|
||||
// every enabled mod from the bottom one up, so that the top mod's data writes win where two mods write the same bytes.
|
||||
// store as for mods_exefs_data.
|
||||
void mods_exefs_apply(uint64_t base, uint64_t size, uint64_t code_end,
|
||||
int (*store)(uint64_t addr, const uint8_t *bytes, int n));
|
||||
// Stores the data writes of mod m's ExeFS patch files (build-cache/exefs.data) into main's image at base: size bytes,
|
||||
// code below code_end. A DT_RELR word gets base added, as the dynamic linker adds it. Every line is checked before the
|
||||
// first store: one that falls outside [code_end, size) or does not belong to a usable file of m keeps them all out.
|
||||
// store writes n bytes at a guest address, 1 on success; NULL: into the loaded main, whose .rodata is read-only.
|
||||
// Returns the bytes stored, -1 when a line kept them out or a store failed. --probe-mods passes a buffer and memcpy.
|
||||
long mods_exefs_data(const ModInfo *m, uint64_t base, uint64_t size, uint64_t code_end,
|
||||
int (*store)(uint64_t addr, const uint8_t *bytes, int n));
|
||||
// --probe-mods: the texts of build-cache/exefs.status and exefs.data instead of the files (NULL: read the file), and a
|
||||
// fresh scan at the next mods_scan.
|
||||
void mods_probe_cache(const char *status, const char *data);
|
||||
void mods_rescan(void);
|
||||
// The ExeFS patch files of the enabled mods that the build lacks, their FNV-1a mixed into *key (cheats.c starts the
|
||||
// rebuild for them).
|
||||
int mods_exefs_pending(uint32_t *key);
|
||||
|
||||
+93
-3
@@ -1,5 +1,6 @@
|
||||
// --probe-mods: the mods folder scan, its order and switches (mods.c). Builds a mods folder of its own next to totk.exe,
|
||||
// scans it and removes it. Needs no game files.
|
||||
// scans it and removes it. Also the resource size table's entries of the members of the mods' packs (restbl_probe.c).
|
||||
// Needs no game files.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -7,6 +8,7 @@
|
||||
#include "host.h"
|
||||
#include "mods.h"
|
||||
#include "platform/platform.h"
|
||||
#include "restbl.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL mods: %s\n", what); } } while (0)
|
||||
@@ -37,6 +39,93 @@ static int remove_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint32_t fnv(const char *p, size_t n) {
|
||||
uint32_t h = 0x811C9DC5u;
|
||||
for (size_t i = 0; i < n; i++) h = (h ^ (uint8_t)p[i]) * 0x01000193u;
|
||||
return h;
|
||||
}
|
||||
|
||||
static ModInfo *by_name(const char *name) {
|
||||
for (int i = 0; mods_at(i); i++)
|
||||
if (!strcmp(mods_at(i)->name, name)) return mods_at(i);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// mods_exefs_data's store into a buffer standing in for main
|
||||
static int store_buffer(uint64_t a, const uint8_t *p, int n) {
|
||||
memcpy((void *)(uintptr_t)a, p, (size_t)n);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A second scan of a folder of its own: data writes of ExeFS patch files (build-cache/exefs.data, given as text).
|
||||
static void probe_data(const char *root) {
|
||||
remove_tree(root);
|
||||
plat_file_mkdir_p(root);
|
||||
const char *bid = *mods_main_build_id() ? mods_main_build_id() : "0";
|
||||
char n_ips[160], p_ips[160], q_ips[160], x_ips[160], y_ips[160];
|
||||
snprintf(n_ips, sizeof n_ips, "N/exefs/%s.ips", bid);
|
||||
snprintf(p_ips, sizeof p_ips, "P/exefs/%s.ips", bid);
|
||||
snprintf(q_ips, sizeof q_ips, "Q/exefs/%s.ips", bid);
|
||||
snprintf(x_ips, sizeof x_ips, "X/exefs/%s.ips", bid);
|
||||
snprintf(y_ips, sizeof y_ips, "Y/exefs/%s.ips", bid);
|
||||
const char *files[] = {n_ips, p_ips, q_ips, x_ips, y_ips};
|
||||
for (size_t i = 0; i < sizeof files / sizeof *files; i++) put(root, files[i]);
|
||||
uint32_t kn = fnv(n_ips, strlen(n_ips)), kp = fnv(p_ips, strlen(p_ips)), kq = fnv(q_ips, strlen(q_ips));
|
||||
uint32_t kx = fnv(x_ips, strlen(x_ips)), ky = fnv(y_ips, strlen(y_ips));
|
||||
uint32_t c = fnv("probe", 5);
|
||||
char status[512], data[768];
|
||||
snprintf(status, sizeof status,
|
||||
"%08X\t%08X\tdata\t-\tdata 11\n%08X\t%08X\tdata\t-\tdata 1\n%08X\t%08X\tdata\t-\tdata 4\n"
|
||||
"%08X\t%08X\tdata\t-\tdata 1\n%08X\t%08X\tdata\t-\tdata 1\n",
|
||||
kn, c, kp, c, kq, c, kx, c, ky, c);
|
||||
snprintf(data, sizeof data,
|
||||
"# file hash, content hash, offset in main, bytes[, base]\n"
|
||||
"%08X\t%08X\t10\t414243\n" // N: three bytes of data
|
||||
"%08X\t%08X\t18\t0800000000000000\tbase\r\n" // N: a DT_RELR word, 8 plus main's base (a CRLF line)
|
||||
"DEADBEEF\t%08X\t20\t99\n" // another file's: not N's
|
||||
"%08X\t%08X\t4\t77\n" // P: below main's data
|
||||
"%08X\t%08X\t30\t66\n" // Q: 1 of the 4 bytes its status counts
|
||||
"%08X\t%08X\t28\tBB\n" // X: the byte Y writes too
|
||||
"%08X\t%08X\t28\tAA\n", // Y, above X in the order: its byte stays
|
||||
kn, c, kn, c, c, kp, c, kq, c, kx, c, ky, c);
|
||||
mods_probe_cache(status, data);
|
||||
cfg_set("TOTK_MODS_DIR", root);
|
||||
cfg_set("TOTK_MODS_ORDER", "Y"); // Y on top, the rest by name: Y N P Q X
|
||||
cfg_set("TOTK_MODS_DISABLED", "");
|
||||
mods_rescan();
|
||||
mods_scan();
|
||||
ModInfo *n = by_name("N"), *p = by_name("P"), *q = by_name("Q");
|
||||
CHECK(n && p && q && by_name("X") && by_name("Y"), "second scan: five mods");
|
||||
if (n && p && q) {
|
||||
CHECK(n->kind == MOD_EXEFS && n->problem == MOD_OK && n->ndata == 1 && !n->nsets,
|
||||
"an ExeFS patch file with data writes only makes a usable mod");
|
||||
uint8_t img[0x40];
|
||||
memset(img, 0, sizeof img);
|
||||
uint64_t base = (uint64_t)(uintptr_t)img, word;
|
||||
long stored = mods_exefs_data(n, base, sizeof img, 8, store_buffer);
|
||||
memcpy(&word, img + 0x18, 8);
|
||||
CHECK(stored == 11 && !memcmp(img + 0x10, "ABC", 3) && word == base + 8 && !img[0x20],
|
||||
"data writes: bytes as they are, a DT_RELR word rebased, other files' lines left");
|
||||
memset(img, 0x5A, sizeof img);
|
||||
int untouched = 1;
|
||||
CHECK(mods_exefs_data(p, base, sizeof img, 8, store_buffer) == -1, "a line below main's data keeps all out");
|
||||
CHECK(n->data_bytes == 11 && q->data_bytes == 4, "the status lines count the data bytes");
|
||||
CHECK(mods_exefs_data(q, base, sizeof img, 8, store_buffer) == -1,
|
||||
"a data file holding fewer bytes than the status counts keeps all out");
|
||||
for (size_t i = 0; i < sizeof img; i++) untouched &= img[i] == 0x5A;
|
||||
CHECK(untouched, "nothing stored when a line does not fit or bytes are missing");
|
||||
// every enabled mod as at start: X and Y write the same byte, and Y, the higher one, wins as its RomFS files
|
||||
// would; P and Q stay out as above
|
||||
memset(img, 0, sizeof img);
|
||||
CHECK(EQ(mods_at(0)->name, "Y") && EQ(mods_at(4)->name, "X"), "mods_order puts Y on top and X at the bottom");
|
||||
mods_exefs_apply(base, sizeof img, 8, store_buffer);
|
||||
CHECK(img[0x28] == 0xAA, "two mods write the same byte: the top mod's byte stays");
|
||||
CHECK(!memcmp(img + 0x10, "ABC", 3) && !img[0x30], "the other mods' data as each one stores it alone");
|
||||
}
|
||||
mods_probe_cache(NULL, NULL);
|
||||
remove_tree(root);
|
||||
}
|
||||
|
||||
// s ends in tail, either path separator matching either.
|
||||
static int ends_with(const char *s, const char *tail) {
|
||||
size_t n = strlen(s), t = strlen(tail);
|
||||
@@ -124,7 +213,8 @@ int probe_mods(void) {
|
||||
CHECK(EQ(cfg_get("TOTK_MODS_DISABLED"), "A"), "switching A off writes it");
|
||||
CHECK(mods_changed(), "changes seen");
|
||||
}
|
||||
remove_tree(root);
|
||||
probe_data(root);
|
||||
printf("mods: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails ? 1 : 0;
|
||||
int table = restbl_probe();
|
||||
return fails || table ? 1 : 0;
|
||||
}
|
||||
+809
-47
File diff suppressed because it is too large.
Load diff
+9
-1
@@ -29,10 +29,18 @@ typedef struct {
|
||||
enum { MOVIE_EV_INPUT, MOVIE_EV_OUTPUT, MOVIE_EV_FORMAT, MOVIE_EV_ERROR }; // DecoderEvents: EventType * per event
|
||||
enum { MOVIE_TYPE_VP9 = 3, MOVIE_MODE_BUFFER = 1, MOVIE_FORMAT_NV12 = 4, MOVIE_FLAG_EOS = 4 };
|
||||
|
||||
typedef struct { uint64_t frames, decode_ns, max_ns, errors, dropped, handed; } MovieStats;
|
||||
typedef struct { uint64_t frames, decode_ns, max_ns, errors, dropped, handed, fit_ns; } MovieStats;
|
||||
// fit_ns: the part of decode_ns spent writing the NV12 frame
|
||||
|
||||
void movie_set_guest(const MovieGuest *g); // probe: stand-ins instead of the modules
|
||||
void movie_set_clock(uint64_t (*clock)(void)); // probe: a fake clock (ns) for the pacing; NULL = real
|
||||
void movie_set_pace(int on); // probe: 0 hands every decoded frame over at the next tick
|
||||
int movie_stats(uint64_t obj, MovieStats *out); // counters of the decoder at guest object obj; 0 if none
|
||||
int movie_quiet(uint64_t obj); // 1 when its worker is idle and cannot take more input now
|
||||
|
||||
// Keeping a movie's shape on an output of another shape (ov_movie.c): the picture is shrunk inside the frame, with
|
||||
// black (Y 16, U = V 128) around it. movie_fit_new returns NULL when the shapes agree within 1%: copy the frame.
|
||||
typedef struct MovieFit MovieFit;
|
||||
MovieFit *movie_fit_new(int w, int h, unsigned out_w, unsigned out_h);
|
||||
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst);
|
||||
void movie_fit_free(MovieFit *f);
|
||||
@@ -9,6 +9,7 @@
|
||||
// Without the RomFS (no game data) it prints why and exits 0. TOTK_PROBE_MOVIE_SHOT=<file.ppm> saves the middle frame.
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -267,7 +268,104 @@ static int paced(uint64_t obj, const uint8_t *data, const Track *t, int limit, u
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The shape-keeping resampler (ov_movie.c movie_fit_new / movie_fit_apply) on a synthetic 1920x1080 I420 picture, which
|
||||
// needs no game data. The planes are gradients, which a box filter keeps linear, so every output sample has an expected
|
||||
// value: luma rises left to right and top to bottom, U rises left to right, V falls top to bottom. A 16:9 output must give
|
||||
// no plan; the others must give bars (Y 16, U = V 128) that are symmetric, a picture of the movie's shape once the game
|
||||
// stretches the frame, and samples of the picture where the gradients say. TOTK_PROBE_MOVIE_BENCH=1 also times the frame.
|
||||
#define FIT_CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("probe-movie: FAILED: "); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
rc = 1; \
|
||||
goto done; \
|
||||
} \
|
||||
} while (0)
|
||||
static int check_fit(void) {
|
||||
enum { W = 1920, H = 1080, CW = W / 2, CH = H / 2 };
|
||||
int rc = 0;
|
||||
MovieFit *f = NULL;
|
||||
uint8_t *y = malloc((size_t)W * H), *u = malloc((size_t)CW * CH), *v = malloc((size_t)CW * CH);
|
||||
uint8_t *out = malloc((size_t)W * H * 3 / 2);
|
||||
FIT_CHECK(y && u && v && out, "fit test buffers");
|
||||
for (int j = 0; j < H; j++)
|
||||
for (int i = 0; i < W; i++) y[j * W + i] = (uint8_t)(32 + 150.0 * (i + 0.5) / W + 60.0 * (j + 0.5) / H + 0.5);
|
||||
for (int j = 0; j < CH; j++)
|
||||
for (int i = 0; i < CW; i++) {
|
||||
u[j * CW + i] = (uint8_t)(40 + 150.0 * (i + 0.5) / CW + 0.5);
|
||||
v[j * CW + i] = (uint8_t)(220 - 150.0 * (j + 0.5) / CH + 0.5);
|
||||
}
|
||||
const uint8_t *plane[3] = {y, u, v};
|
||||
const int stride[3] = {W, CW, CW};
|
||||
FIT_CHECK(!movie_fit_new(W, H, 1920, 1080) && !movie_fit_new(W, H, 2560, 1440), "16:9 outputs need no plan");
|
||||
static const unsigned outs[][2] = {{5120, 1440}, {3440, 1440}, {1280, 800}, {1920, 1200}};
|
||||
for (int k = 0; k < 4; k++) {
|
||||
unsigned ow = outs[k][0], oh = outs[k][1];
|
||||
f = movie_fit_new(W, H, ow, oh);
|
||||
FIT_CHECK(f, "%ux%u: no plan", ow, oh);
|
||||
memset(out, 0xAA, (size_t)W * H * 3 / 2); // a byte nothing else produces: every pixel must be written
|
||||
movie_fit_apply(f, plane, stride, out);
|
||||
const uint8_t *uv = out + (size_t)W * H;
|
||||
int l = 0, r = W - 1, t = 0, b = H - 1; // the picture's extent: where luma leaves 16, on the middle row and column
|
||||
while (l < W && out[(H / 2) * W + l] == 16) l++;
|
||||
while (r >= 0 && out[(H / 2) * W + r] == 16) r--;
|
||||
while (t < H && out[t * W + W / 2] == 16) t++;
|
||||
while (b >= 0 && out[b * W + W / 2] == 16) b--;
|
||||
int pw = r - l + 1, ph = b - t + 1;
|
||||
FIT_CHECK(pw > 2 && ph > 2, "%ux%u: no picture found", ow, oh);
|
||||
FIT_CHECK(abs(l - (W - 1 - r)) <= 2 && abs(t - (H - 1 - b)) <= 2,
|
||||
"%ux%u: bars not symmetric: left %d right %d, top %d bottom %d", ow, oh, l, W - 1 - r, t, H - 1 - b);
|
||||
// On screen the picture covers pw/W of the width and ph/H of the height of an ow x oh canvas; 1 = the movie's shape.
|
||||
double shape = ((double)pw / W * ow) / ((double)ph / H * oh) / ((double)W / H);
|
||||
FIT_CHECK(fabs(shape - 1) < 0.01, "%ux%u: picture %dx%d shows at %.3f of the movie's shape", ow, oh, pw, ph, shape);
|
||||
FIT_CHECK(l == 0 || ph == H, "%ux%u: sideways bars with a squeezed height", ow, oh);
|
||||
for (int j = 0; j < H; j++) // everything outside the picture is black in luma and chroma, left and right alike
|
||||
for (int i = 0; i < W; i++)
|
||||
if (j < t || j > b || i < l || i > r)
|
||||
FIT_CHECK(out[j * W + i] == 16, "%ux%u: luma (%d,%d) is %d, not black", ow, oh, i, j, out[j * W + i]);
|
||||
for (int j = 0; j < CH; j++)
|
||||
for (int i = 0; i < CW; i++) {
|
||||
int outside = 2 * j + 1 < t || 2 * j > b || 2 * i + 1 < l || 2 * i > r;
|
||||
const uint8_t *c = uv + (size_t)j * W + 2 * i;
|
||||
if (outside) FIT_CHECK(c[0] == 128 && c[1] == 128, "%ux%u: chroma (%d,%d) is %d/%d, not black", ow, oh, i, j,
|
||||
c[0], c[1]);
|
||||
}
|
||||
static const double fr[] = {0.1, 0.35, 0.5, 0.8, 0.95}; // samples of the picture, as fractions of it
|
||||
for (int a = 0; a < 5; a++)
|
||||
for (int c = 0; c < 5; c++) {
|
||||
int px = (int)(pw * fr[a]), py = (int)(ph * fr[c]); // picture pixel
|
||||
double sx = (px + 0.5) * W / pw, sy = (py + 0.5) * H / ph; // where it comes from in the source
|
||||
double ey = 32 + 150.0 * sx / W + 60.0 * sy / H;
|
||||
int got = out[(t + py) * W + l + px];
|
||||
FIT_CHECK(fabs(got - ey) <= 2, "%ux%u: luma at picture (%d,%d) is %d, want %.1f", ow, oh, px, py, got, ey);
|
||||
int cx = px / 2, cy = py / 2, cpw = pw / 2, cph = ph / 2;
|
||||
double sux = (cx + 0.5) * CW / cpw, svy = (cy + 0.5) * CH / cph;
|
||||
const uint8_t *q = uv + (size_t)(t / 2 + cy) * W + 2 * (l / 2 + cx);
|
||||
double eu = 40 + 150.0 * sux / CW, ev = 220 - 150.0 * svy / CH;
|
||||
FIT_CHECK(fabs(q[0] - eu) <= 3 && fabs(q[1] - ev) <= 3,
|
||||
"%ux%u: chroma at picture (%d,%d) is U %d V %d, want U %.1f V %.1f", ow, oh, cx, cy, q[0], q[1], eu,
|
||||
ev);
|
||||
}
|
||||
if (cfg_get("TOTK_PROBE_MOVIE_BENCH")) {
|
||||
uint64_t t0 = plat_now_ns();
|
||||
for (int i = 0; i < 100; i++) movie_fit_apply(f, plane, stride, out);
|
||||
printf("probe-movie: fit %ux%u: %.2f ms per frame\n", ow, oh, (plat_now_ns() - t0) / 1e8);
|
||||
}
|
||||
printf("probe-movie: fit %ux%u: picture %dx%d at (%d,%d) of the frame, bars Y 16 / UV 128, gradients as expected\n",
|
||||
ow, oh, pw, ph, l, t);
|
||||
movie_fit_free(f);
|
||||
f = NULL;
|
||||
}
|
||||
done:
|
||||
movie_fit_free(f);
|
||||
free(y), free(u), free(v), free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int probe_movie(void) {
|
||||
if (check_fit()) return 1;
|
||||
const char *dir = rt_config()->romfs;
|
||||
char image[PLAT_PATH_MAX];
|
||||
snprintf(image, sizeof image, "%s/romfs.bin", dir);
|
||||
|
||||
@@ -43,6 +43,15 @@ _Static_assert(sizeof(nvn_mapping_t) == 0x28, "NVNmappingRequest stride");
|
||||
|
||||
const pool_t *nvn_pool_find(uint64_t addr, uint64_t len); // GPU-visible pool holding [addr, addr+len], NULL if none
|
||||
uint64_t nvn_new_serial(void); // next tex_t / prog_t serial, from 1
|
||||
// TOTK_SYNC_AHEAD (nvn_api_core.c): nvnQueuePresentTexture notes the newest fence at each present; the newest
|
||||
// previous-frame value the game was told is done although it may not be (0: none). The back end's shadow copies
|
||||
// wait for it or are queued for the GPU (vk_sync.c prologue), so the GPU never reads CPU writes the game made under
|
||||
// that assumption too early.
|
||||
void nvn_sync_ahead_present(void);
|
||||
uint64_t nvn_sync_ahead_guard(void);
|
||||
int vk_mem_shadow_on(void); // vk_memory.c: 1 with shadow storage on (valid once the Vulkan back end is up)
|
||||
void vk_mem_shadow_fence(void); // vk_memory.c: returns once every shadow sync that read the old guard is done
|
||||
void gfx_replay_wait_value(uint64_t v); // vk_backend.c: the replay thread submitted value v
|
||||
uint64_t nvn_fence_last(void); // fence value of the newest submit (0 before the first)
|
||||
int nvn_depth_minus_w(void); // 1 after DeviceSetDepthMode(NEAR_IS_MINUS_W), else 0
|
||||
void nvn_guest_written(uint64_t addr, uint64_t size); // the host or the game wrote pool storage: back end re-reads
|
||||
|
||||
+144
-17
@@ -141,10 +141,14 @@ static void pool_drain(void) {
|
||||
// system-managed page file, so the same request succeeds a moment later, and a stall of some frames is better than
|
||||
// ending the game. Frees the pooled buffers first. NULL only after about 30 s without memory; the caller ends the game.
|
||||
// Never called with a lock held.
|
||||
static void big_drain(void);
|
||||
|
||||
static void *realloc_wait(void *old, size_t n, const char *what) {
|
||||
void *p = realloc(old, n);
|
||||
if (p) return p;
|
||||
pool_drain();
|
||||
big_drain();
|
||||
rt_oom_request(); // the Vulkan back end gives memory back at its next submit (oom.c)
|
||||
static uint32_t reports;
|
||||
int report = __atomic_fetch_add(&reports, 1, __ATOMIC_RELAXED) < 8;
|
||||
if (report) {
|
||||
@@ -406,8 +410,10 @@ void nvn_pins_stats(uint64_t *live, uint64_t *freed) {
|
||||
// adding control memory again releases the copies recorded there without walking every live copy. 4 KiB pages cost
|
||||
// about 500 lookups per AddControlMemory under the exclusive lock (1.4.3 field: 53 calls a frame, 2 MiB ranges, 3.7
|
||||
// copies per page), and the recording threads waited on it.
|
||||
// deferred: 0 unknown, 1 none, 2 some; pins: texture pins its ops name, counted in their refs (pin_refs)
|
||||
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins; } copy_prefix;
|
||||
// deferred: 0 unknown, 1 none, 2 some; pins: texture pins its ops name, counted in their refs; pin_v: those pins, in
|
||||
// record order, or NULL (then pin_refs walks the records); cap: bytes of the block from the large-block cache
|
||||
// (copy_block), 0 for a plain heap block
|
||||
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins, cap; const tex_t **pin_v; } copy_prefix;
|
||||
_Static_assert(sizeof(copy_prefix) % 8 == 0, "records stay 8-byte aligned");
|
||||
typedef struct { uintptr_t lo, hi; uint8_t *copy; } cmd_copy;
|
||||
typedef struct { cmd_copy *v; uint32_t n, cap; } copy_bucket;
|
||||
@@ -429,15 +435,117 @@ static void zombie_add(copy_prefix *pf) { // copy_lock held
|
||||
zombies[nzombies++] = pf;
|
||||
}
|
||||
|
||||
static void copy_free(copy_prefix *pf) { // copy_lock held
|
||||
if (pf->pins) pin_refs((const uint8_t *)(pf + 1), -1);
|
||||
// Blocks of large copies are kept for the next ones. 1.4.3 makes two copies of 0.5-2 MiB a frame (the per-frame
|
||||
// recordings that inline many others); above the heap's direct-allocation threshold (about 512 KiB) every malloc mapped
|
||||
// fresh zero-filled pages and every free unmapped them, so the copy ran at half the speed of a small one and the free
|
||||
// held copy_lock for tens of microseconds. A block is reused for a copy that fills at least half of it.
|
||||
#define BIG_MIN ((size_t)256 << 10) // smaller blocks come from the heap and go back to it
|
||||
#define BIG_MAX ((size_t)16 << 20) // a larger block is never kept
|
||||
#define BIG_SLOTS 8
|
||||
static PlatMutex big_lock = PLAT_MUTEX_INIT; // lock order: copy_lock, then big_lock
|
||||
static struct { copy_prefix *p; size_t cap; } big[BIG_SLOTS];
|
||||
static uint32_t nbig;
|
||||
|
||||
static void big_drain(void) {
|
||||
plat_mutex_lock(&big_lock);
|
||||
while (nbig) free(big[--nbig].p);
|
||||
plat_mutex_unlock(&big_lock);
|
||||
}
|
||||
|
||||
// A block for a copy of n bytes after its prefix; prefix.cap says where it goes back (copy_block_free). wait: a failed
|
||||
// allocation waits for memory (realloc_wait), which is never done with a lock held; else it ends the game at once.
|
||||
static copy_prefix *copy_block(size_t n, int wait, const char *what) {
|
||||
size_t total = sizeof(copy_prefix) + n, cap = 0;
|
||||
copy_prefix *pf = NULL;
|
||||
if (total >= BIG_MIN && total <= BIG_MAX) {
|
||||
plat_mutex_lock(&big_lock);
|
||||
uint32_t best = BIG_SLOTS;
|
||||
for (uint32_t i = 0; i < nbig; i++)
|
||||
if (big[i].cap >= total && big[i].cap / 2 <= total && (best == BIG_SLOTS || big[i].cap < big[best].cap))
|
||||
best = i;
|
||||
if (best < BIG_SLOTS) {
|
||||
pf = big[best].p;
|
||||
cap = big[best].cap;
|
||||
big[best] = big[--nbig];
|
||||
}
|
||||
plat_mutex_unlock(&big_lock);
|
||||
if (!pf) cap = (total + total / 8 + 0xffff) & ~(size_t)0xffff; // room for the next frame's slightly larger one
|
||||
}
|
||||
if (!pf) pf = wait ? realloc_wait(NULL, cap ? cap : total, what) : malloc(cap ? cap : total);
|
||||
if (!pf) rt_fatal("nvn: %s allocation (%zu bytes)", what, cap ? cap : total);
|
||||
pf->cap = cap;
|
||||
return pf;
|
||||
}
|
||||
|
||||
static void copy_block_free(copy_prefix *pf) {
|
||||
size_t cap = pf->cap;
|
||||
if (cap) {
|
||||
plat_mutex_lock(&big_lock);
|
||||
if (nbig < BIG_SLOTS) {
|
||||
big[nbig].p = pf;
|
||||
big[nbig++].cap = cap;
|
||||
pf = NULL;
|
||||
} else {
|
||||
uint32_t small = 0;
|
||||
for (uint32_t i = 1; i < nbig; i++)
|
||||
if (big[i].cap < big[small].cap) small = i;
|
||||
if (big[small].cap < cap) {
|
||||
copy_prefix *drop = big[small].p;
|
||||
big[small].p = pf;
|
||||
big[small].cap = cap;
|
||||
pf = drop;
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&big_lock);
|
||||
}
|
||||
free(pf);
|
||||
}
|
||||
|
||||
static void zombie_sweep(uint64_t replayed) { // copy_lock held
|
||||
// Counts (delta 1) or uncounts (-1) the pins a copy names: from its list, without walking its records. A copy of a
|
||||
// per-frame recording is 0.5-2 MiB of records naming a few pins; walking them again at every free cost about 40 us.
|
||||
static void copy_pins_ref(const copy_prefix *pf, int delta) {
|
||||
if (!pf->pins) return;
|
||||
if (!pf->pin_v) {
|
||||
pin_refs((const uint8_t *)(pf + 1), delta);
|
||||
return;
|
||||
}
|
||||
uint64_t now = __atomic_load_n(&nsubmits, __ATOMIC_RELAXED);
|
||||
for (uint64_t i = 0; i < pf->pins; i++) pin_ref(pf->pin_v[i], delta, now);
|
||||
}
|
||||
|
||||
// A copy in no map and no zombie list: nothing else can reach it, so copy_lock need not be held.
|
||||
static void copy_free(copy_prefix *pf) {
|
||||
copy_pins_ref(pf, -1);
|
||||
free((void *)pf->pin_v);
|
||||
copy_block_free(pf);
|
||||
}
|
||||
|
||||
// Copies retired under copy_lock and freed after it is released: the frees, the unpinning walk over their records and
|
||||
// the large blocks kept the lock exclusive for most of release_copies, while the game's other recording threads waited
|
||||
// for it in copy_touch (1.4.3 town: three threads, 0.6 ms a frame of waiting).
|
||||
typedef struct { copy_prefix **v; uint32_t n, cap; } dead_list;
|
||||
static __thread dead_list dead;
|
||||
|
||||
static void dead_add(copy_prefix *pf) {
|
||||
if (dead.n == dead.cap) {
|
||||
uint32_t cap = dead.cap ? dead.cap * 2 : 256;
|
||||
copy_prefix **v = realloc(dead.v, sizeof *v * cap);
|
||||
if (!v) { copy_free(pf); return; } // out of memory: free it here, under the lock, as before
|
||||
dead.v = v;
|
||||
dead.cap = cap;
|
||||
}
|
||||
dead.v[dead.n++] = pf;
|
||||
}
|
||||
|
||||
static void dead_free(void) { // copy_lock not held
|
||||
for (uint32_t i = 0; i < dead.n; i++) copy_free(dead.v[i]);
|
||||
dead.n = 0;
|
||||
}
|
||||
|
||||
static void zombie_sweep(uint64_t replayed) { // copy_lock held; the swept copies go to the dead list
|
||||
for (uint32_t i = 0; i < nzombies;) {
|
||||
if (zombies[i]->used_at <= replayed) {
|
||||
copy_free(zombies[i]);
|
||||
dead_add(zombies[i]);
|
||||
zombies[i] = zombies[--nzombies];
|
||||
} else {
|
||||
i++;
|
||||
@@ -477,13 +585,16 @@ static uint64_t copy_touch(uint64_t handle, uint64_t now, const char *what) {
|
||||
}
|
||||
|
||||
// The recording [p, p + n), OP_BEGIN to OP_END (the recording buffer is one block: recording emits no OP_JUMP), copied
|
||||
// into one host block of its exact size. One allocation and one copy: growing the copy record by record cost the
|
||||
// game's main thread about 1 ms a frame in reallocations and small copies (1.4.3, field).
|
||||
// into one host block (copy_block: its exact size, or a kept large block). One allocation and one copy: growing the
|
||||
// copy record by record cost the game's main thread about 1 ms a frame in reallocations and small copies (1.4.3,
|
||||
// field). The pins the records name are counted and listed (copy_prefix.pin_v) on the way.
|
||||
static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
|
||||
uint8_t *out = realloc_wait(NULL, sizeof(copy_prefix) + n, "command copy");
|
||||
if (!out) rt_fatal("nvn: command copy allocation");
|
||||
copy_prefix *block = copy_block(n, 1, "command copy");
|
||||
uint8_t *out = (uint8_t *)block;
|
||||
memcpy(out + sizeof(copy_prefix), p, n);
|
||||
static __thread struct { const tex_t **v; uint64_t cap; } named; // the pins of this recording, reused
|
||||
uint64_t pins_named = 0;
|
||||
int listed = 1; // 0 after a failed growth: the copy goes without a list
|
||||
for (size_t at = 0;;) { // the records stay checked: a corrupt one stops here, not in the replay
|
||||
op_hdr h;
|
||||
memcpy(&h, p + at, sizeof h);
|
||||
@@ -493,6 +604,14 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
|
||||
const tex_t *pinned[9];
|
||||
int k = op_pins(h.op, out + sizeof(copy_prefix) + at + 8, pinned);
|
||||
for (int i = 0; i < k; i++) pin_ref(pinned[i], 1, 0);
|
||||
if (k && listed && pins_named + (uint64_t)k > named.cap) {
|
||||
uint64_t cap = named.cap ? named.cap * 2 : 64;
|
||||
while (cap < pins_named + (uint64_t)k) cap *= 2;
|
||||
const tex_t **v = realloc((void *)named.v, sizeof *v * cap);
|
||||
if (v) named.v = v, named.cap = cap;
|
||||
else listed = 0;
|
||||
}
|
||||
if (k && listed) memcpy((void *)(named.v + pins_named), pinned, sizeof *pinned * (size_t)k);
|
||||
pins_named += (uint64_t)k;
|
||||
at += h.size;
|
||||
if (h.op == OP_END) {
|
||||
@@ -500,7 +619,10 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
*(copy_prefix *)out = (copy_prefix){.magic = COPY_MAGIC, .bytes = n, .pins = pins_named};
|
||||
const tex_t **pin_v = NULL;
|
||||
if (pins_named && listed && (pin_v = malloc(sizeof *pin_v * pins_named)))
|
||||
memcpy((void *)pin_v, (const void *)named.v, sizeof *pin_v * pins_named);
|
||||
*block = (copy_prefix){.magic = COPY_MAGIC, .bytes = n, .pins = pins_named, .cap = block->cap, .pin_v = pin_v};
|
||||
return out + sizeof(copy_prefix);
|
||||
}
|
||||
|
||||
@@ -550,7 +672,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
|
||||
copy_bytes -= pf->bytes;
|
||||
pf->magic = 0;
|
||||
if (pf->used_at > replayed) zombie_add(pf); // a queued submission still reads it
|
||||
else copy_free(pf);
|
||||
else dead_add(pf);
|
||||
b->v[i] = b->v[--b->n];
|
||||
}
|
||||
if (!b->n) {
|
||||
@@ -560,6 +682,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(©_lock);
|
||||
dead_free();
|
||||
}
|
||||
|
||||
// ---------- CommandBuffer: object and memory
|
||||
@@ -1182,11 +1305,14 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
|
||||
}
|
||||
out[i] = handles[i];
|
||||
if (pf->used_at > replayed && pf->used_at != now && pf->deferred != 1) {
|
||||
size_t sz = sizeof *pf + pf->bytes;
|
||||
copy_prefix *cl = malloc(sz);
|
||||
if (!cl) rt_fatal("nvn: out of memory (recording clone, %zu bytes)", sz);
|
||||
memcpy(cl, pf, sz);
|
||||
if (cl->pins) pin_refs((const uint8_t *)(cl + 1), 1);
|
||||
copy_prefix *cl = copy_block(pf->bytes, 0, "recording clone"); // copy_lock held
|
||||
size_t cap = cl->cap;
|
||||
memcpy(cl, pf, sizeof *pf + pf->bytes);
|
||||
cl->cap = cap;
|
||||
cl->pin_v = NULL;
|
||||
if (pf->pin_v && (cl->pin_v = malloc(sizeof *cl->pin_v * pf->pins)))
|
||||
memcpy((void *)cl->pin_v, (const void *)pf->pin_v, sizeof *cl->pin_v * pf->pins);
|
||||
copy_pins_ref(cl, 1);
|
||||
cl->magic = 0; // not a live handle: the game never sees it
|
||||
cl->used_at = now;
|
||||
zombie_add(cl);
|
||||
@@ -1197,6 +1323,7 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(©_lock);
|
||||
dead_free();
|
||||
for (int i = 0; i < n; i++) {
|
||||
int nd = resolve_deferred((const uint8_t *)(uintptr_t)out[i], 0);
|
||||
if (out[i] == handles[i]) ((copy_prefix *)(uintptr_t)out[i] - 1)->deferred = nd ? 2 : 1;
|
||||
|
||||
+67
-3
@@ -471,6 +471,49 @@ NVN_API(nvnQueueFenceSync) {
|
||||
s->fenced = 1;
|
||||
s->fence = nvn_fence_last();
|
||||
}
|
||||
// TOTK_SYNC_AHEAD=1: one more frame in flight. Each frame the game waits (one nvnSyncWait, frame_wait_lr) for
|
||||
// the frame it presented last; on the Switch that work is long done by then, here it is still in the replay and on the
|
||||
// GPU (about 11 ms from the submit), and the wait sets the frame time. With the switch on, that wait, and only that
|
||||
// one, waits for the frame before it instead. What the game reads back of GPU results after the wait can then be a
|
||||
// frame old. The game's CPU writes to memory the GPU still reads reach the GPU only through shadow copies, which are
|
||||
// queued for the GPU or wait (nvn_sync_ahead_guard). The wait still lasts until the replay thread submitted the asked
|
||||
// value, so no copy of that value itself picks up those writes. pa_fence: the newest fence at the latest present and
|
||||
// the one before, present thread only for writes. pa_guard: the newest value the game was told is done early.
|
||||
static uint64_t pa_fence[2], pa_guard;
|
||||
// The return address of that wait. 1.2.1: 0x7000b23dfc. 1.4.3: 0x70002db99c, the second of the two nvnSyncWait calls
|
||||
// in f_70002db8ac; like on 1.2.1 its sync comes from a two-entry ring indexed per frame, and the first call waits on
|
||||
// one object's sync behind a mode check. 0: no site known, the switch stays off.
|
||||
static uint64_t frame_wait_lr(void) {
|
||||
return rt_game_version == 121 ? 0x7000b23dfcull : rt_game_version == 143 ? 0x70002db99cull : 0;
|
||||
}
|
||||
static int sync_ahead(void) {
|
||||
static int on = -1;
|
||||
if (on < 0) {
|
||||
on = cfg_bool("TOTK_SYNC_AHEAD");
|
||||
// Without shadow storage the GPU reads the game's own memory, which the game writes ahead: nothing guards
|
||||
// that. The Vulkan back end sets the mode while it starts, before the game's first present or wait; the null
|
||||
// back end never does.
|
||||
if (on && !vk_mem_shadow_on()) {
|
||||
rt_log("nvn: TOTK_SYNC_AHEAD needs shadow storage (TOTK_SHADOW=1 on native Windows); "
|
||||
"the game waits for the GPU as before");
|
||||
on = 0;
|
||||
}
|
||||
if (on && !frame_wait_lr()) {
|
||||
rt_log("nvn: TOTK_SYNC_AHEAD: no frame wait site is known for this game version; the game waits for the "
|
||||
"GPU as before");
|
||||
on = 0;
|
||||
}
|
||||
if (on) rt_log("nvn: TOTK_SYNC_AHEAD: a wait for the frame presented last waits for the frame before it");
|
||||
}
|
||||
return on;
|
||||
}
|
||||
void nvn_sync_ahead_present(void) {
|
||||
if (!sync_ahead()) return;
|
||||
__atomic_store_n(&pa_fence[1], __atomic_load_n(&pa_fence[0], __ATOMIC_RELAXED), __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&pa_fence[0], nvn_fence_last(), __ATOMIC_RELEASE);
|
||||
}
|
||||
uint64_t nvn_sync_ahead_guard(void) { return __atomic_load_n(&pa_guard, __ATOMIC_ACQUIRE); }
|
||||
|
||||
// NVNsyncWaitResult nvnSyncWait(const NVNsync *s, uint64_t timeout_ns): a sync never fenced reports a timeout at once;
|
||||
// a fenced one is released only after the back end completed that fence value
|
||||
NVN_API(nvnSyncWait) {
|
||||
@@ -478,13 +521,34 @@ NVN_API(nvnSyncWait) {
|
||||
uint64_t timeout = arg_x(c, 1);
|
||||
if (!s->fenced) { ret_x(c, NVN_SYNC_WAIT_RESULT_TIMEOUT_EXPIRED); return; }
|
||||
const GfxBackend *b = gfx_backend();
|
||||
if (b->fence_done() >= s->fence) { b->cpu_synced(s->fence); ret_x(c, NVN_SYNC_WAIT_RESULT_ALREADY_SIGNALED); return; }
|
||||
uint64_t want = s->fence;
|
||||
// Only the game's per-frame wait lags, and not a poll (timeout 0).
|
||||
if (timeout && c->x[30] == frame_wait_lr() && sync_ahead() && b->fence_done() < want) {
|
||||
uint64_t p0 = __atomic_load_n(&pa_fence[0], __ATOMIC_ACQUIRE);
|
||||
uint64_t p1 = __atomic_load_n(&pa_fence[1], __ATOMIC_ACQUIRE);
|
||||
if (want <= p0 && want > p1) want = p1;
|
||||
}
|
||||
if (want != s->fence) { // the replay of the asked value is done first: the guard then covers every copy of it
|
||||
{
|
||||
NVN_BLOCKING();
|
||||
gfx_replay_wait_value(s->fence);
|
||||
}
|
||||
uint64_t g = __atomic_load_n(&pa_guard, __ATOMIC_RELAXED);
|
||||
while (s->fence > g &&
|
||||
!__atomic_compare_exchange_n(&pa_guard, &g, s->fence, 1, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {}
|
||||
vk_mem_shadow_fence(); // a shadow sync that read the old guard finishes before the game writes again
|
||||
}
|
||||
if (b->fence_done() >= want) {
|
||||
b->cpu_synced(want == s->fence ? s->fence : b->fence_done());
|
||||
ret_x(c, NVN_SYNC_WAIT_RESULT_ALREADY_SIGNALED);
|
||||
return;
|
||||
}
|
||||
int ok;
|
||||
{
|
||||
NVN_BLOCKING();
|
||||
ok = b->fence_wait(s->fence, timeout);
|
||||
ok = b->fence_wait(want, timeout);
|
||||
}
|
||||
if (ok) b->cpu_synced(s->fence);
|
||||
if (ok) b->cpu_synced(want == s->fence ? s->fence : b->fence_done());
|
||||
ret_x(c, ok ? NVN_SYNC_WAIT_RESULT_CONDITION_SATISFIED : NVN_SYNC_WAIT_RESULT_TIMEOUT_EXPIRED);
|
||||
}
|
||||
// void nvnEventBuilderSetDefaults(NVNeventBuilder *b)
|
||||
|
||||
+12
-5
@@ -11,6 +11,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "nvn_api.h"
|
||||
@@ -667,8 +668,12 @@ static void sleep_until(uint64_t deadline) {
|
||||
}
|
||||
|
||||
// NVNwindowAcquireTextureResult nvnWindowAcquireTexture(NVNwindow *w, NVNsync *s, int *index): blocks here, not in the
|
||||
// present, until the image's previous present plus one period has passed (the game often presents with a lock held).
|
||||
// That is the time the next frame goes to the display, so the image is free as soon as that time is reached.
|
||||
// present, until the image is free (the game often presents with a lock held). Without the frame queue that is the
|
||||
// image's previous present plus one period, when the next frame goes to the display, so a frame gets exactly one period
|
||||
// from acquire to present. Its present is a copy already on the queue by its display time, so with the frame queue
|
||||
// (TOTK_FRAME_QUEUE=1) the image is free from that time on: the game draws the next frame while the previous one waits
|
||||
// for its turn, and a frame that takes up to one more period still makes its slot, for up to one frame more latency.
|
||||
// Town at 4K with DLAA at 60 on an RTX 5070: frames over 17.5 ms 1-8 per 30 s with the queue against 25-33 without.
|
||||
NVN_API(nvnWindowAcquireTexture) {
|
||||
win_t *o = arg_p(c, 0);
|
||||
sync_t *s = arg_p(c, 1);
|
||||
@@ -693,8 +698,9 @@ NVN_API(nvnWindowAcquireTexture) {
|
||||
|
||||
// void nvnQueuePresentTexture(NVNqueue *q, NVNwindow *w, int index): queues the copy and returns at once. The display
|
||||
// time comes from the pacer: the back end's present thread holds the host present until then (back-edge pacing, as the
|
||||
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later the
|
||||
// image is free for the next acquire. Added latency: the slack of the frame (period minus its work), as on the Switch.
|
||||
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later (at that
|
||||
// time with the frame queue) the image is free for the next acquire. Added latency: the slack of the frame (period minus
|
||||
// its work), as on the Switch, plus up to a period with the frame queue.
|
||||
NVN_API(nvnQueuePresentTexture) {
|
||||
win_t *o = arg_p(c, 1);
|
||||
int idx = (int)arg_x(c, 2);
|
||||
@@ -707,9 +713,10 @@ NVN_API(nvnQueuePresentTexture) {
|
||||
const tex_t *t = (const tex_t *)o->tex[idx];
|
||||
uint64_t period = present_period(o), now = kern_now_ns();
|
||||
uint64_t slot = now + pace(&o->pace, now, period);
|
||||
nvn_sync_ahead_present();
|
||||
gfx_backend()->present(t, o->crop, slot);
|
||||
o->ticket[idx] = o->presents;
|
||||
o->avail[idx] = slot + period;
|
||||
o->avail[idx] = cfg_bool("TOTK_FRAME_QUEUE") ? slot : slot + period;
|
||||
frame_step_present(slot, o->interval); // frame_rate.c: the real-time step follows the display times
|
||||
rt_frame_presented(t, o->presents);
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Short out-of-memory spells: a failed allocation waits for memory instead of ending the game.
|
||||
//
|
||||
// Windows refuses commit beyond the current limit while it grows a system-managed page file, and on some drivers device
|
||||
// memory counts as process commit (vk_memory.c vk_mem_host_probe), so for a moment every allocation of the process can
|
||||
// fail and the same request succeeds a little later. The failure sites (memory.c commit, vk_memory.c vk_staging,
|
||||
// vk_texture.c create_tex, vk_exec_draw.c transient chunks, nvn_api_cmd.c realloc_wait) ask the Vulkan back end to give
|
||||
// memory back (rt_oom_request: released textures and headroom blocks, at its next submit, where its locks are free) and
|
||||
// retry for up to OOM_WAIT_MS before they end the game.
|
||||
//
|
||||
// TOTK_TEST_OOM=<site>:<n>[,<site>:<n>...] (sites commit, staging, image, chunk) fails the first n attempts at a site, so
|
||||
// the wait can be tested without running out of memory; the probes arm it with rt_oom_force.
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define OOM_WAIT_MS 30000u
|
||||
#define OOM_STEP_MS 10u
|
||||
#define OOM_REPORTS 8
|
||||
#define OOM_REQUEST_EVERY 100 // retries between two release requests: a second, since a release only frees what is old enough
|
||||
|
||||
volatile int32_t rt_oom_req;
|
||||
|
||||
static const char *const site_name[RT_OOM_SITES] = {"commit", "staging", "image", "chunk"};
|
||||
static volatile int32_t force_left[RT_OOM_SITES];
|
||||
static volatile int32_t forced_parsed;
|
||||
static PlatOnce force_once = PLAT_ONCE_INIT;
|
||||
|
||||
static void force_parse(void *arg) {
|
||||
(void)arg;
|
||||
const char *s = cfg_get("TOTK_TEST_OOM");
|
||||
while (s && *s) {
|
||||
const char *colon = strchr(s, ':'), *end = strchr(s, ',');
|
||||
if (!end) end = s + strlen(s);
|
||||
if (colon && colon < end) {
|
||||
for (int i = 0; i < RT_OOM_SITES; i++)
|
||||
if ((size_t)(colon - s) == strlen(site_name[i]) && !strncmp(s, site_name[i], (size_t)(colon - s))) {
|
||||
int n = atoi(colon + 1);
|
||||
__atomic_store_n(&force_left[i], n > 0 ? n : 0, __ATOMIC_RELAXED);
|
||||
rt_log("oom: TOTK_TEST_OOM fails the first %d %s attempts", n, site_name[i]);
|
||||
}
|
||||
}
|
||||
s = *end ? end + 1 : end;
|
||||
}
|
||||
__atomic_store_n(&forced_parsed, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
int rt_oom_forced(int site) {
|
||||
if (!__atomic_load_n(&forced_parsed, __ATOMIC_ACQUIRE)) plat_once(&force_once, force_parse, NULL);
|
||||
if (__atomic_load_n(&force_left[site], __ATOMIC_RELAXED) <= 0) return 0;
|
||||
return plat_dec32(&force_left[site]) >= 0;
|
||||
}
|
||||
|
||||
void rt_oom_force(int site, int n) {
|
||||
if (!__atomic_load_n(&forced_parsed, __ATOMIC_ACQUIRE)) plat_once(&force_once, force_parse, NULL);
|
||||
__atomic_store_n(&force_left[site], n, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
int rt_oom_forced_left(int site) {
|
||||
int n = __atomic_load_n(&force_left[site], __ATOMIC_RELAXED);
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
void rt_oom_request(void) { __atomic_store_n(&rt_oom_req, 1, __ATOMIC_RELEASE); }
|
||||
|
||||
int rt_oom_wait(const char *what, uint64_t bytes, int (*retry)(void *arg), void *arg) {
|
||||
rt_oom_request();
|
||||
static volatile int32_t reports;
|
||||
int report = plat_inc32(&reports) <= OOM_REPORTS;
|
||||
double proc, used, limit;
|
||||
if (report) {
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
rt_log("%s of %llu bytes failed (memory commit: this process %.0f MB, the system %.0f of %.0f MB); waiting up "
|
||||
"to %u s for memory", what, (unsigned long long)bytes, proc, used, limit, OOM_WAIT_MS / 1000);
|
||||
}
|
||||
uint64_t t0 = plat_now_ms();
|
||||
for (int n = 1; plat_now_ms() - t0 < OOM_WAIT_MS; n++) {
|
||||
plat_sleep_ms(OOM_STEP_MS);
|
||||
if (retry(arg)) {
|
||||
if (report)
|
||||
rt_log("%s of %llu bytes succeeded after %llu ms", what, (unsigned long long)bytes,
|
||||
(unsigned long long)(plat_now_ms() - t0));
|
||||
return 1;
|
||||
}
|
||||
if (n % OOM_REQUEST_EVERY == 0) rt_oom_request();
|
||||
}
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
rt_log("%s of %llu bytes still failed after %u s (memory commit: this process %.0f MB, the system %.0f of %.0f MB)",
|
||||
what, (unsigned long long)bytes, OOM_WAIT_MS / 1000, proc, used, limit);
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct { void *old, *p; size_t n; } Realloc;
|
||||
static int realloc_try(void *arg) {
|
||||
Realloc *r = arg;
|
||||
return (r->p = realloc(r->old, r->n)) != NULL;
|
||||
}
|
||||
|
||||
void *rt_realloc_wait(void *old, size_t n, const char *what) {
|
||||
void *p = realloc(old, n);
|
||||
if (p || !n) return p;
|
||||
Realloc r = {old, NULL, n};
|
||||
return rt_oom_wait(what, n, realloc_try, &r) ? r.p : NULL;
|
||||
}
|
||||
+196
-4
@@ -73,6 +73,7 @@ typedef struct {
|
||||
unsigned owed; // events to signal on the next guest thread (bit per MOVIE_EV_*)
|
||||
uint64_t t0; // host time of the first frame handed over (pacing)
|
||||
int64_t pts0; // its presentation time (us)
|
||||
MovieFit *fit; // picture shrunk inside the frame; NULL = the shapes agree, copy
|
||||
MovieStats st;
|
||||
PlatMutex mu;
|
||||
PlatCond work, idle;
|
||||
@@ -207,6 +208,173 @@ static void to_nv12(const vpx_image_t *img, uint8_t *dst, int w, int h) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Keeping the movie's shape. The game draws a decoded frame over the whole canvas, so on an output that is not 16:9
|
||||
// (output_res in totk.ini) the 1920x1080 movies would be stretched. The frame keeps its size; the picture is shrunk
|
||||
// inside it and the rest is black, so that after the game's stretch the picture has the movie's own shape again: a wider
|
||||
// output gets a squeezed picture with black at the left and right, a taller one (16:10) black above and below.
|
||||
// The shrink is a box filter (each output pixel averages the source pixels it covers, weights in 1/4096), separable, with
|
||||
// the per-column and per-row source spans and weights built once per movie (movie_fit_new), so the frame loop only
|
||||
// multiplies and adds.
|
||||
enum { FIT_ONE = 4096, FIT_Y_BLACK = 16, FIT_UV_BLACK = 128 }; // black of limited-range video: Y 16, U = V 128
|
||||
typedef struct { int n, taps; int *start; uint16_t *wt; } FitAxis; // output i: taps weights from source start[i]
|
||||
struct MovieFit {
|
||||
int w, h, x0, y0, pw, ph; // frame, and the picture's corner and size in it (all even)
|
||||
FitAxis lx, ly, cx, cy; // luma, chroma; columns, rows; n = 0 where the axis is kept
|
||||
uint8_t *tmp; // horizontally shrunk rows, before the vertical pass
|
||||
uint32_t *acc; // one row of the vertical sum
|
||||
};
|
||||
|
||||
// Box filter from src_n to dst_n (dst_n < src_n): output i covers [i, i + 1) * src_n / dst_n of the source.
|
||||
static int axis_build(FitAxis *a, int src_n, int dst_n) {
|
||||
double s = (double)src_n / dst_n;
|
||||
a->n = dst_n;
|
||||
a->taps = (int)s + 2 > src_n ? src_n : (int)s + 2;
|
||||
a->start = malloc((size_t)dst_n * sizeof *a->start);
|
||||
a->wt = malloc((size_t)dst_n * (size_t)a->taps * sizeof *a->wt);
|
||||
if (!a->start || !a->wt) return 0;
|
||||
for (int i = 0; i < dst_n; i++) {
|
||||
double lo = i * s, hi = (i + 1) * s;
|
||||
int st = (int)lo;
|
||||
if (st > src_n - a->taps) st = src_n - a->taps;
|
||||
a->start[i] = st;
|
||||
int sum = 0, big = 0;
|
||||
uint16_t *w = a->wt + (size_t)i * a->taps;
|
||||
for (int t = 0; t < a->taps; t++) {
|
||||
double ov = (st + t + 1 < hi ? st + t + 1 : hi) - (st + t > lo ? st + t : lo);
|
||||
w[t] = ov > 0 ? (uint16_t)(ov / s * FIT_ONE + 0.5) : 0;
|
||||
sum += w[t];
|
||||
if (w[t] > w[big]) big = t;
|
||||
}
|
||||
w[big] = (uint16_t)(w[big] + FIT_ONE - sum); // the weights add up to exactly 1
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static void axis_free(FitAxis *a) {
|
||||
free(a->start);
|
||||
free(a->wt);
|
||||
}
|
||||
|
||||
void movie_fit_free(MovieFit *f) {
|
||||
if (!f) return;
|
||||
axis_free(&f->lx), axis_free(&f->ly), axis_free(&f->cx), axis_free(&f->cy);
|
||||
free(f->tmp);
|
||||
free(f->acc);
|
||||
free(f);
|
||||
}
|
||||
|
||||
// The plan for a w x h movie shown on an ow x oh output; NULL when the shapes agree within 1% (the frame is copied).
|
||||
MovieFit *movie_fit_new(int w, int h, unsigned ow, unsigned oh) {
|
||||
if (w < 16 || h < 16 || (w | h) & 1 || !ow || !oh) return NULL;
|
||||
double movie = (double)w / h, out = (double)ow / oh, r = out / movie;
|
||||
if (r > 0.99 && r < 1.01) return NULL;
|
||||
MovieFit *f = calloc(1, sizeof *f);
|
||||
if (!f) {
|
||||
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
|
||||
return NULL;
|
||||
}
|
||||
f->w = w, f->h = h, f->pw = w, f->ph = h;
|
||||
if (r > 1) f->pw = (int)(w / r / 2 + 0.5) * 2; // wider output: squeeze sideways
|
||||
else f->ph = (int)(h * r / 2 + 0.5) * 2; // taller output: squeeze vertically
|
||||
if (f->pw < 2) f->pw = 2;
|
||||
if (f->ph < 2) f->ph = 2;
|
||||
f->x0 = (w - f->pw) / 2 & ~1;
|
||||
f->y0 = (h - f->ph) / 2 & ~1;
|
||||
int ok = 1;
|
||||
if (f->pw < w) ok = axis_build(&f->lx, w, f->pw) && axis_build(&f->cx, w / 2, f->pw / 2);
|
||||
if (f->ph < h) ok = ok && axis_build(&f->ly, h, f->ph) && axis_build(&f->cy, h / 2, f->ph / 2);
|
||||
f->tmp = malloc((size_t)f->pw * h);
|
||||
f->acc = malloc((size_t)f->pw * sizeof *f->acc);
|
||||
if (!ok || !f->tmp || !f->acc) {
|
||||
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
|
||||
movie_fit_free(f);
|
||||
return NULL;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
// One output row of the horizontal pass: dst gets the shrunk src row (dstep 2 writes every other byte, for U and V).
|
||||
// The tap count is a constant in the 3 and 4 tap copies (what 2x and below squeezes need), so the compiler unrolls the sum.
|
||||
static inline __attribute__((always_inline)) void fit_row_taps(const FitAxis *a, const uint8_t *src, uint8_t *dst,
|
||||
int dstep, int taps) {
|
||||
for (int i = 0; i < a->n; i++) {
|
||||
const uint8_t *p = src + a->start[i];
|
||||
const uint16_t *w = a->wt + (size_t)i * taps;
|
||||
uint32_t v = FIT_ONE / 2;
|
||||
for (int t = 0; t < taps; t++) v += w[t] * p[t];
|
||||
dst[(size_t)i * dstep] = (uint8_t)(v / FIT_ONE);
|
||||
}
|
||||
}
|
||||
static void fit_row_h(const FitAxis *a, const uint8_t *src, uint8_t *dst, int dstep) {
|
||||
if (a->taps == 3) fit_row_taps(a, src, dst, dstep, 3);
|
||||
else if (a->taps == 4) fit_row_taps(a, src, dst, dstep, 4);
|
||||
else fit_row_taps(a, src, dst, dstep, a->taps);
|
||||
}
|
||||
|
||||
// One plane (sw x sh) shrunk into the pw x ph picture whose corner (ox, oy) is in dst; ax/ay NULL = that axis is kept.
|
||||
static void fit_plane(const MovieFit *f, const uint8_t *src, int ss, int sw, int sh, uint8_t *dst, int ds, int dstep,
|
||||
int ox, int oy, const FitAxis *ax, const FitAxis *ay, int pw, int ph) {
|
||||
uint8_t *base = dst + (size_t)oy * ds + (size_t)ox * dstep;
|
||||
if (!ay) { // sideways only: straight into the frame
|
||||
for (int y = 0; y < sh; y++) {
|
||||
const uint8_t *row = src + (size_t)y * ss;
|
||||
uint8_t *o = base + (size_t)y * ds;
|
||||
if (ax) fit_row_h(ax, row, o, dstep);
|
||||
else if (dstep == 1) memcpy(o, row, (size_t)sw);
|
||||
else for (int x = 0; x < sw; x++) o[(size_t)x * dstep] = row[x];
|
||||
}
|
||||
return;
|
||||
}
|
||||
const uint8_t *in = src;
|
||||
int is = ss;
|
||||
if (ax) {
|
||||
for (int y = 0; y < sh; y++) fit_row_h(ax, src + (size_t)y * ss, f->tmp + (size_t)y * pw, 1);
|
||||
in = f->tmp, is = pw;
|
||||
}
|
||||
for (int y = 0; y < ph; y++) {
|
||||
const uint16_t *w = ay->wt + (size_t)y * ay->taps;
|
||||
const uint8_t *r0 = in + (size_t)ay->start[y] * is;
|
||||
uint8_t *o = base + (size_t)y * ds;
|
||||
if (ay->taps == 3) { // the common case, summed in registers
|
||||
const uint8_t *r1 = r0 + is, *r2 = r1 + is;
|
||||
for (int x = 0; x < pw; x++)
|
||||
o[(size_t)x * dstep] = (uint8_t)((FIT_ONE / 2 + w[0] * r0[x] + w[1] * r1[x] + w[2] * r2[x]) / FIT_ONE);
|
||||
continue;
|
||||
}
|
||||
memset(f->acc, 0, (size_t)pw * sizeof *f->acc);
|
||||
for (int t = 0; t < ay->taps; t++) {
|
||||
const uint8_t *row = r0 + (size_t)t * is;
|
||||
uint32_t k = w[t];
|
||||
for (int x = 0; x < pw; x++) f->acc[x] += k * row[x];
|
||||
}
|
||||
for (int x = 0; x < pw; x++) o[(size_t)x * dstep] = (uint8_t)((f->acc[x] + FIT_ONE / 2) / FIT_ONE);
|
||||
}
|
||||
}
|
||||
|
||||
// I420 planes -> packed NV12 with the picture shrunk inside black bars: w x h of Y, then w x h/2 of interleaved U, V.
|
||||
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst) {
|
||||
int w = f->w, h = f->h, cw = w / 2, ch = h / 2;
|
||||
uint8_t *uv = dst + (size_t)w * h;
|
||||
// Black only where the picture does not go: whole rows above and below it, the strips left and right of it.
|
||||
int rx = f->x0 + f->pw, ry = f->y0 + f->ph;
|
||||
memset(dst, FIT_Y_BLACK, (size_t)f->y0 * w);
|
||||
memset(dst + (size_t)ry * w, FIT_Y_BLACK, (size_t)(h - ry) * w);
|
||||
memset(uv, FIT_UV_BLACK, (size_t)(f->y0 / 2) * w);
|
||||
memset(uv + (size_t)(ry / 2) * w, FIT_UV_BLACK, (size_t)(ch - ry / 2) * w);
|
||||
for (int y = f->y0; y < ry; y++) {
|
||||
memset(dst + (size_t)y * w, FIT_Y_BLACK, (size_t)f->x0);
|
||||
memset(dst + (size_t)y * w + rx, FIT_Y_BLACK, (size_t)(w - rx));
|
||||
}
|
||||
for (int y = f->y0 / 2; y < ry / 2; y++) {
|
||||
memset(uv + (size_t)y * w, FIT_UV_BLACK, (size_t)f->x0);
|
||||
memset(uv + (size_t)y * w + rx, FIT_UV_BLACK, (size_t)(w - rx));
|
||||
}
|
||||
fit_plane(f, plane[0], stride[0], w, h, dst, w, 1, f->x0, f->y0, f->lx.n ? &f->lx : NULL, f->ly.n ? &f->ly : NULL,
|
||||
f->pw, f->ph);
|
||||
for (int c = 0; c < 2; c++)
|
||||
fit_plane(f, plane[1 + c], stride[1 + c], cw, ch, uv + c, w, 2, f->x0 / 2, f->y0 / 2,
|
||||
f->cx.n ? &f->cx : NULL, f->cy.n ? &f->cy : NULL, f->pw / 2, f->ph / 2);
|
||||
}
|
||||
|
||||
static int free_out(const Movie *m) {
|
||||
for (int i = 0; i < OUT_BUFS; i++)
|
||||
if (!m->out_held[i]) return i;
|
||||
@@ -250,7 +418,9 @@ static void worker(void *arg) {
|
||||
slot = free_out(m);
|
||||
m->out_held[slot] = 1; // reserved: in no queue, so the game cannot release it
|
||||
}
|
||||
uint64_t gen = m->gen, dt = 0;
|
||||
uint64_t gen = m->gen, dt = 0, fit_ns = 0;
|
||||
// A plan made for another frame size (a start that found the worker busy) is not used: it would overrun dst.
|
||||
const MovieFit *fit = m->fit && m->fit->w == m->w && m->fit->h == m->h ? m->fit : NULL;
|
||||
int got = 0, w = m->w, h = m->h;
|
||||
uint8_t *dst = slot >= 0 ? m->out[slot] : NULL;
|
||||
vpx_codec_err_t e = VPX_CODEC_OK;
|
||||
@@ -265,7 +435,12 @@ static void worker(void *arg) {
|
||||
const vpx_image_t *img;
|
||||
while ((img = vpx_codec_get_frame(&m->vpx, &it)) != NULL) {
|
||||
if (img->fmt != VPX_IMG_FMT_I420 || (int)img->d_w != w || (int)img->d_h != h) bad = "layout";
|
||||
else if (!got) to_nv12(img, dst, w, h);
|
||||
else if (!got) {
|
||||
uint64_t t1 = kern_now_ns();
|
||||
if (fit) movie_fit_apply(fit, (const uint8_t *const *)img->planes, img->stride, dst);
|
||||
else to_nv12(img, dst, w, h);
|
||||
fit_ns = kern_now_ns() - t1;
|
||||
}
|
||||
got++;
|
||||
}
|
||||
}
|
||||
@@ -279,6 +454,7 @@ static void worker(void *arg) {
|
||||
m->owed |= 1u << MOVIE_EV_INPUT;
|
||||
if (p.size > 0) {
|
||||
m->st.decode_ns += dt;
|
||||
m->st.fit_ns += fit_ns;
|
||||
if (dt > m->st.max_ns) m->st.max_ns = dt;
|
||||
if (bad) rt_fatal("movie: VP9 frame layout differs from the configured %dx%d I420", w, h);
|
||||
if (e != VPX_CODEC_OK || !got) {
|
||||
@@ -302,8 +478,10 @@ static void worker(void *arg) {
|
||||
push_decoded(m, slot, 0, p.time, frame_bytes(m));
|
||||
m->st.frames++;
|
||||
if (m->st.frames % 600 == 0)
|
||||
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f)", (unsigned long long)m->st.frames,
|
||||
m->st.decode_ns / 1e6 / (double)m->st.frames, m->st.max_ns / 1e6);
|
||||
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f), of it %.2f ms %s",
|
||||
(unsigned long long)m->st.frames, m->st.decode_ns / 1e6 / (double)m->st.frames,
|
||||
m->st.max_ns / 1e6, m->st.fit_ns / 1e6 / (double)m->st.frames,
|
||||
m->fit ? "fitting the picture" : "converting");
|
||||
}
|
||||
}
|
||||
if (p.flags & MOVIE_FLAG_EOS && !m->eos_in) {
|
||||
@@ -355,6 +533,8 @@ static void release_codec(Movie *m) { // m->mu held, worker quiesced
|
||||
free(m->out[i]);
|
||||
m->out[i] = NULL;
|
||||
}
|
||||
movie_fit_free(m->fit);
|
||||
m->fit = NULL;
|
||||
m->configured = m->started = 0;
|
||||
reset_queues(m);
|
||||
}
|
||||
@@ -597,6 +777,18 @@ static void ov_start(Cpu *c) {
|
||||
plat_mutex_lock(&m->mu);
|
||||
int st = ST_NOT_CONFIGURED;
|
||||
if (m->configured) {
|
||||
if (!m->started && !m->busy) { // the worker is idle: it reads m->fit only when it takes an input
|
||||
movie_fit_free(m->fit);
|
||||
m->fit = NULL;
|
||||
unsigned out[2];
|
||||
res_wanted(out);
|
||||
if (strcmp(cfg_value("TOTK_MOVIE_ASPECT"), "stretch")) {
|
||||
m->fit = movie_fit_new(m->w, m->h, out[0], out[1]);
|
||||
if (m->fit)
|
||||
rt_log("movie: %dx%d picture kept in shape on the %ux%u output (%dx%d inside the frame)", m->w,
|
||||
m->h, out[0], out[1], m->fit->pw, m->fit->ph);
|
||||
}
|
||||
}
|
||||
m->started = 1;
|
||||
m->owed |= 1u << MOVIE_EV_INPUT;
|
||||
plat_cond_signal(&m->work);
|
||||
|
||||
+7
-1
@@ -929,8 +929,14 @@ static void probe_menu_text(void) {
|
||||
cfg_set("TOTK_MENU_LANG", "zh-Hans");
|
||||
CHECK(menu_text(UI_MENU_TITLE) == ui_text_in("zh-Hans", UI_MENU_TITLE), "menu_text ignores MENU_LANG=zh-Hans");
|
||||
cfg_set("TOTK_MENU_LANG", "");
|
||||
// the system language as ui_text.c resolve_menu_lang takes it: the tag, else its base language (ru-RU: ru), else en
|
||||
const char *sys_lang = ui_text_lang(plat_ui_language());
|
||||
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang : "en";
|
||||
char sys_base[16] = "";
|
||||
size_t base_n = strcspn(sys_lang, "-");
|
||||
if (sys_lang[base_n] && base_n < sizeof sys_base) memcpy(sys_base, sys_lang, base_n);
|
||||
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang
|
||||
: *sys_base && ui_text_defined(sys_base, UI_MENU_TITLE) ? sys_base
|
||||
: "en";
|
||||
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(auto_lang, UI_MENU_CONTINUE), "menu_text auto ignores the system language");
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,15 @@ uint64_t perf_begin(void) { return kern_now_ns(); }
|
||||
void perf_replay_thread(void) { prof_replay_tid = plat_thread_id(); }
|
||||
void perf_main_thread(void) { prof_main_tid = plat_thread_id(); }
|
||||
|
||||
void perf_core_thread(int role) {
|
||||
static const char *const names[] = {"game main", "game render", "vk replay", "vk recorder", "vk present"};
|
||||
static int32_t said;
|
||||
int r = plat_thread_perf_core();
|
||||
if (r < 0 || role < 0 || role >= PCORE_N) return;
|
||||
if (__atomic_fetch_or(&said, 1 << role, __ATOMIC_RELAXED) & (1 << role)) return;
|
||||
rt_log("power: the %s thread %s the P-cores", names[role], r ? "prefers" : "could not select");
|
||||
}
|
||||
|
||||
void perf_end(int kind, uint64_t t0) {
|
||||
if (!t0) return;
|
||||
uint32_t tid = plat_thread_id();
|
||||
|
||||
@@ -270,7 +270,8 @@ int plat_intr_resume(PlatIntr *t, uint64_t to) {
|
||||
// ---- fibers: a stack switch of our own. glibc's swapcontext saves and restores the signal mask with a system call
|
||||
// on every switch, and the game switches fibers often. fiber_swap saves the callee-saved registers, the MXCSR and the
|
||||
// x87 control word on the current stack, stores the stack pointer in *save and loads the other one. A new fiber's
|
||||
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which calls fn(arg) (r12 and r13).
|
||||
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which realigns the stack and calls fn(arg)
|
||||
// (r12 and r13).
|
||||
struct PlatFiber {
|
||||
void *sp;
|
||||
void *stack; // mapped stack (NULL for a thread's own context)
|
||||
@@ -312,6 +313,11 @@ __asm__(".text\n"
|
||||
".type plat_fiber_entry, @function\n"
|
||||
".p2align 4\n"
|
||||
"plat_fiber_entry:\n"
|
||||
// fiber_swap's ret lands here with rsp = top - 8, as at the entry of a called function. The call below must
|
||||
// be made with rsp 16-byte aligned so that fn starts with rsp = 8 mod 16, as the System V ABI demands; without
|
||||
// the realignment every 16-byte local of fn is off by 8, which the VEX instructions of a v3 build tolerate
|
||||
// but the SSE ones of a v2 or k10 build (movdqa, pcmpeqb with a memory operand) fault on.
|
||||
" andq $-16, %rsp\n"
|
||||
" movq %r13, %rdi\n"
|
||||
" callq *%r12\n"
|
||||
" ud2\n"
|
||||
@@ -475,6 +481,14 @@ uint64_t plat_counter_hz(void) { return 1000000000u; }
|
||||
// inherit it from the thread that creates them.
|
||||
void plat_timer_1ms(void) { prctl(PR_SET_TIMERSLACK, 1000ul, 0, 0, 0); }
|
||||
|
||||
int plat_power_full(void) { return -1; }
|
||||
|
||||
int plat_perf_core_setup(char *why, size_t n) {
|
||||
snprintf(why, n, "not done on Linux");
|
||||
return 0;
|
||||
}
|
||||
int plat_thread_perf_core(void) { return -1; }
|
||||
|
||||
// clock_nanosleep wakes within the timer slack (50 us by default) of its deadline; the last PLAT_SPIN_NS are spent
|
||||
// yielding so the deadline is met closely, as on Windows.
|
||||
#define PLAT_SPIN_NS 100000u
|
||||
@@ -943,6 +957,10 @@ static void fault_handler(int sig, siginfo_t *si, void *uc) {
|
||||
}
|
||||
if (sig == SIGSEGV || sig == SIGBUS)
|
||||
c.access = (err & 16) ? 8 : (err & 2) ? 1 : 0, c.addr = (uint64_t)(uintptr_t)si->si_addr;
|
||||
static const int gr[16] = {REG_RAX, REG_RCX, REG_RDX, REG_RBX, REG_RSP, REG_RBP, REG_RSI, REG_RDI,
|
||||
REG_R8, REG_R9, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15};
|
||||
for (int i = 0; i < 16; i++) c.gpr[i] = (uint64_t)u->uc_mcontext.gregs[gr[i]];
|
||||
c.has_regs = 1;
|
||||
crash_fn(&c);
|
||||
}
|
||||
signal(sig, SIG_DFL); // returns into the faulting instruction, which faults again with the default action
|
||||
@@ -993,6 +1011,12 @@ int plat_mem_read(uint64_t addr, void *out, size_t n) {
|
||||
return process_vm_readv(getpid(), &local, 1, &remote, 1, 0) == (ssize_t)n;
|
||||
}
|
||||
|
||||
// Not known before the SDL window exists; the Steam Deck has its own profile (cfg.c)
|
||||
int plat_screen_size(int *w, int *h) {
|
||||
(void)w, (void)h;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n) {
|
||||
uint8_t b;
|
||||
uint64_t end = addr + (n ? n : 1);
|
||||
@@ -1444,6 +1468,41 @@ int plat_open_folder(const char *path) {
|
||||
void *plat_lib_open(const char *name) { return dlopen(name, RTLD_NOW | RTLD_LOCAL); }
|
||||
void *plat_lib_sym(void *lib, const char *name) { return lib ? dlsym(lib, name) : NULL; }
|
||||
|
||||
void plat_lib_error(char *out, size_t cap) {
|
||||
const char *e = dlerror();
|
||||
snprintf(out, cap, "%s", e ? e : "unknown error");
|
||||
}
|
||||
|
||||
int plat_ngx_core_path(char *out, size_t cap) { return snprintf(out, cap, "libnvidia-ngx.so.1") < (int)cap; }
|
||||
|
||||
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
|
||||
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
|
||||
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
|
||||
const unsigned char *u = (const unsigned char *)in;
|
||||
size_t n = 0;
|
||||
while (*u) {
|
||||
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
|
||||
if (more == 4) return 0;
|
||||
if (more) c &= 0x3F >> more;
|
||||
for (unsigned k = 0; k < more; k++, u++) {
|
||||
if ((*u & 0xC0) != 0x80) return 0;
|
||||
c = c << 6 | (*u & 0x3F);
|
||||
}
|
||||
if (n + 1 >= cap) return 0;
|
||||
out[n++] = (wchar_t)c;
|
||||
}
|
||||
out[n] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
|
||||
char dir[PLAT_PATH_MAX];
|
||||
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
|
||||
if (!cut) return 0;
|
||||
cut[1] = 0;
|
||||
return utf8_to_wide(dir, out, cap);
|
||||
}
|
||||
|
||||
// ---- machine load: /proc/stat and /proc/self/stat for the CPU, hwmon for the temperature, the amdgpu sysfs files of
|
||||
// the render device for the GPU.
|
||||
|
||||
@@ -1614,6 +1673,17 @@ void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
|
||||
if (private_bytes) *proc_mb = (double)(private_bytes >> 20);
|
||||
}
|
||||
|
||||
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room) {
|
||||
*commit_room = *phys_room = UINT64_MAX;
|
||||
char line[128];
|
||||
unsigned long long k;
|
||||
FILE *f = fopen("/proc/meminfo", "r");
|
||||
if (!f) return;
|
||||
while (fgets(line, sizeof line, f))
|
||||
if (sscanf(line, "MemAvailable: %llu kB", &k) == 1) *phys_room = (uint64_t)k << 10;
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// ---- child processes
|
||||
|
||||
struct PlatChild {
|
||||
|
||||
@@ -99,6 +99,12 @@ uint64_t plat_now_ns(void); // monotonic, high resolution, arbitra
|
||||
uint64_t plat_counter(void); // the OS's raw monotonic counter (QPC on Windows), plat_counter_hz per second
|
||||
uint64_t plat_counter_hz(void);
|
||||
void plat_timer_1ms(void); // asks the OS for timed waits accurate to 1 ms or better (call before threads)
|
||||
int plat_power_full(void); // opts the process out of OS power throttling: 1 done, 0 refused, -1 no such thing
|
||||
// Hybrid CPUs: finds the performance-core logical processors the process may use, once, before any thread asks; returns
|
||||
// their count (0: none to prefer, why says which case). plat_thread_perf_core: the calling thread prefers them; 1 done,
|
||||
// 0 refused, -1 nothing selected.
|
||||
int plat_perf_core_setup(char *why, size_t n);
|
||||
int plat_thread_perf_core(void);
|
||||
// t * num / den for a fixed num / den without a 128-bit division per call (__udivti3 took 5% of the game's running
|
||||
// samples through plat_now_ns and rt_ticks): t * q + (t * f >> 64) with q = num / den and f = (num % den) * 2^64 / den.
|
||||
// Monotonic in t and at most one below the exact quotient while t * (num % den) / den fits 64 bits.
|
||||
@@ -222,9 +228,10 @@ _Noreturn void plat_reexec(void);
|
||||
// - A fault in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
|
||||
// overflow) is reported as it happens.
|
||||
// - A fault in another module (a GPU driver, a system library) is reported only when nothing handles it: on Windows
|
||||
// from the unhandled-exception filter, with the return addresses in this executable on the faulting thread's stack,
|
||||
// so the report shows which host call ran into the driver; on Linux from the signal handler, without frames. Drivers
|
||||
// that raise and catch their own exceptions never reach fn.
|
||||
// from the unhandled-exception filter, with the return addresses on the faulting thread's stack in every module, so
|
||||
// the report shows which driver code failed and which host call ran into it; on Linux from the signal handler,
|
||||
// without frames. Drivers that raise and catch their own exceptions never reach fn. For a C++ exception thrown by
|
||||
// MSVC-built code (a driver) the report names its type and, for a std::exception, its what() text.
|
||||
// - On Windows abort() (a failed assert, std::terminate) and an invalid parameter to a C runtime function go there
|
||||
// too, with the host frames in this executable: the C runtime ends the process for both with 0xc0000409 and no
|
||||
// exception.
|
||||
@@ -234,12 +241,22 @@ typedef struct {
|
||||
uintptr_t rip, offset; // the faulting instruction, and its offset in its module
|
||||
int access; // the data access that faulted: 0 read, 1 write, 8 execute; -1 when not an access
|
||||
uint64_t addr; // its address when access >= 0
|
||||
int nframes; // return addresses in this executable, innermost first: for abort and invalid parameter
|
||||
uintptr_t frames[16]; // (rip is frames[0]) and for a fault in another module; 0 for a fault in this one
|
||||
int nframes; // return addresses, innermost first: for abort and invalid parameter (rip is frames[0])
|
||||
uintptr_t frames[32]; // and for a fault in another module; 0 for a fault in this one
|
||||
const char *frame_module[32]; // the module of each frame: NULL for this executable, "" for none
|
||||
uintptr_t frame_offset[32]; // each frame's offset in its module
|
||||
int frames_left_out; // frames in other modules past the limit the walk keeps, so this executable's still fit
|
||||
const char *module; // the module holding rip: NULL for this executable, "" for none (a jump to address 0)
|
||||
const char *cxx_type; // a C++ exception from MSVC-built code: the thrown type's decorated name (.?AV...@@)
|
||||
const char *cxx_what; // and what() when the type derives from std::exception; NULL when unknown
|
||||
int has_regs; // gpr holds the faulting thread's registers (a fault; not abort or invalid parameter)
|
||||
uint64_t gpr[16]; // rax rcx rdx rbx rsp rbp rsi rdi r8..r15, in the x86 encoding order
|
||||
} PlatCrash;
|
||||
#define PLAT_GPR_NAMES {"rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", \
|
||||
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"}
|
||||
void plat_crash_hook(void (*fn)(const PlatCrash *c));
|
||||
int plat_mem_read(uint64_t addr, void *out, size_t n); // copies n bytes, 0 instead of faulting on unmapped memory
|
||||
int plat_screen_size(int *w, int *h); // the primary display's current mode in pixels; 0 unknown
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n); // every page of [addr, addr + n) can be read; no fault
|
||||
void *plat_aligned_alloc(size_t size, size_t align); // free with plat_aligned_free; NULL on failure
|
||||
void plat_aligned_free(void *p);
|
||||
@@ -386,6 +403,16 @@ int plat_open_folder(const char *path); // shows the folder in
|
||||
// in the system directories only. NULL when missing.
|
||||
void *plat_lib_open(const char *name);
|
||||
void *plat_lib_sym(void *lib, const char *name);
|
||||
// Why the calling thread's last plat_lib_open failed, for a log line (the OS error code on Windows, dlerror on Linux).
|
||||
void plat_lib_error(char *out, size_t cap);
|
||||
|
||||
// ---- NVIDIA NGX (vk_dlaa.c). The path of the driver's NGX core library into out: on Windows _nvngx.dll in the folder
|
||||
// the driver records in the registry (NGXCore FullPath), on Linux libnvidia-ngx.so.1, which the dynamic loader finds
|
||||
// where the driver put it. 0 when the driver records none.
|
||||
int plat_ngx_core_path(char *out, size_t cap);
|
||||
// The running executable's folder with a trailing separator, as the wchar_t string NGX takes for its application data
|
||||
// path (UTF-16 on Windows, UTF-32 code points on Linux). 0 on failure or when out has no room.
|
||||
int plat_exe_dir_wide(wchar_t *out, size_t cap);
|
||||
|
||||
// ---- machine load for the performance overlay (perfmon.c's sampling thread; each call can take milliseconds). A number
|
||||
// the system does not give is -1.
|
||||
@@ -406,6 +433,9 @@ void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb);
|
||||
// Commit charge in MB: this process's private bytes, the system's commit in use and its limit (RAM plus page files).
|
||||
// An allocation fails when the system's reaches the limit, however much physical memory is free. -1 when unknown.
|
||||
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb);
|
||||
// What the system can still give, in bytes: commit (Windows: limit minus charge; Linux, which overcommits: UINT64_MAX)
|
||||
// and physical memory available without paging. UINT64_MAX when unknown.
|
||||
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room);
|
||||
// This process's memory in bytes: resident (the working set) and private (Windows: committed private bytes; Linux: the
|
||||
// resident anonymous pages). 0 when unknown.
|
||||
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes);
|
||||
|
||||
+228
-11
@@ -18,6 +18,13 @@
|
||||
|
||||
#include "platform.h"
|
||||
|
||||
// Hybrid laptops: the drivers read these exports of the executable when it starts and run it on the dedicated GPU.
|
||||
// NVIDIA Optimus can otherwise hide its GPU from a program it put on the integrated one, whatever the Windows and NVIDIA
|
||||
// settings say; AMD switchable graphics read the second one. TOTK_GPU_INDEX still picks another GPU. Savage Wind Recomp
|
||||
// exports the same two (runtime/platform/windows.c).
|
||||
__declspec(dllexport) DWORD NvOptimusEnablement = 1;
|
||||
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
|
||||
|
||||
_Static_assert(sizeof(PlatMutex) == sizeof(SRWLOCK), "PlatMutex holds an SRWLOCK");
|
||||
_Static_assert(sizeof(PlatCond) == sizeof(CONDITION_VARIABLE), "PlatCond holds a CONDITION_VARIABLE");
|
||||
|
||||
@@ -289,6 +296,98 @@ uint32_t plat_cpu_usable(void) {
|
||||
return plat_cpu_count();
|
||||
}
|
||||
|
||||
// ---- performance cores of a hybrid CPU
|
||||
// An Intel CPU with P-cores and E-cores (a Core 5 210H: 4 P-cores with two threads each, 4 E-cores) runs a thread on an
|
||||
// E-core at about 60% of a P-core's speed, and the frame waits for its slowest critical thread. The few threads every
|
||||
// frame goes through ask for the P-cores (plat_thread_perf_core): their selected CPU sets are the logical processors
|
||||
// above the lowest efficiency class that the process affinity allows. A thread's CPU sets bind it like an affinity
|
||||
// mask, except that processors the system reserves for itself are left out instead of failing. Nothing is selected
|
||||
// on a CPU with one efficiency class, or when the affinity already leaves only P-cores or only E-cores. LAS Recomp
|
||||
// does the same for its game, render and vsync threads (runtime/platform/windows.c plat_thread_perf_core).
|
||||
// Nothing is selected either below PC_MIN_CORES physical P-cores (an i5-1235U or Core 3 100U has 2): the five bound
|
||||
// threads (perf_core_thread) and a spinning recorder would crowd two cores and their hyperthreads while the E-cores
|
||||
// stay idle, which can be slower than letting the scheduler place them.
|
||||
#define PC_MIN_CORES 4
|
||||
static ULONG *pc_ids; // the CPU set ids of the allowed P-core logical processors; NULL: none selected
|
||||
static ULONG pc_n;
|
||||
|
||||
int plat_perf_core_setup(char *why, size_t n) {
|
||||
ULONG len = 0;
|
||||
GetSystemCpuSetInformation(NULL, 0, &len, GetCurrentProcess(), 0);
|
||||
uint8_t *buf = len ? malloc(len) : NULL;
|
||||
if (!buf || !GetSystemCpuSetInformation((PSYSTEM_CPU_SET_INFORMATION)buf, len, &len, GetCurrentProcess(), 0)) {
|
||||
free(buf);
|
||||
snprintf(why, n, "no CPU set information (error %lu)", GetLastError());
|
||||
return 0;
|
||||
}
|
||||
// The process affinity mask covers its first processor group only: it filters on a single-group system.
|
||||
DWORD_PTR proc = 0, sys = 0;
|
||||
int filter = GetActiveProcessorGroupCount() == 1 && GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys) && proc;
|
||||
int lo = 255, hi = -1, all = 0;
|
||||
for (ULONG off = 0; off < len;) {
|
||||
SYSTEM_CPU_SET_INFORMATION *i = (SYSTEM_CPU_SET_INFORMATION *)(buf + off);
|
||||
if (!i->Size) break;
|
||||
if (i->Type == CpuSetInformation) {
|
||||
int ec = i->CpuSet.EfficiencyClass;
|
||||
lo = ec < lo ? ec : lo, hi = ec > hi ? ec : hi, all++;
|
||||
}
|
||||
off += i->Size;
|
||||
}
|
||||
if (!all || hi <= lo) {
|
||||
free(buf);
|
||||
snprintf(why, n, "not a hybrid CPU (%d logical processors, one efficiency class)", all);
|
||||
return 0;
|
||||
}
|
||||
ULONG *ids = calloc((size_t)all, sizeof *ids);
|
||||
uint16_t *cores = calloc((size_t)all, sizeof *cores); // group << 8 | core index of each allowed physical P-core
|
||||
int np = 0, ne = 0, ok_p = 0, ok_e = 0, nc = 0;
|
||||
for (ULONG off = 0; ids && cores && off < len;) {
|
||||
SYSTEM_CPU_SET_INFORMATION *i = (SYSTEM_CPU_SET_INFORMATION *)(buf + off);
|
||||
if (!i->Size) break;
|
||||
if (i->Type == CpuSetInformation) {
|
||||
int perf = i->CpuSet.EfficiencyClass > lo;
|
||||
int allowed = !filter || (i->CpuSet.Group == 0 && i->CpuSet.LogicalProcessorIndex < 64 &&
|
||||
(proc >> i->CpuSet.LogicalProcessorIndex) & 1);
|
||||
perf ? np++ : ne++;
|
||||
if (allowed) perf ? ok_p++ : ok_e++;
|
||||
if (allowed && perf) {
|
||||
ids[pc_n++] = i->CpuSet.Id;
|
||||
uint16_t key = (uint16_t)(i->CpuSet.Group << 8 | i->CpuSet.CoreIndex);
|
||||
int k = 0;
|
||||
while (k < nc && cores[k] != key) k++;
|
||||
if (k == nc) cores[nc++] = key;
|
||||
}
|
||||
}
|
||||
off += i->Size;
|
||||
}
|
||||
int counted = ids && cores;
|
||||
free(buf);
|
||||
free(cores);
|
||||
if (!counted || !ok_p || !ok_e) {
|
||||
free(ids);
|
||||
pc_n = 0;
|
||||
snprintf(why, n, "hybrid CPU, %d P-core + %d E-core logical processors; the affinity allows %d + %d, so it decides",
|
||||
np, ne, ok_p, ok_e);
|
||||
return 0;
|
||||
}
|
||||
if (nc < PC_MIN_CORES) {
|
||||
free(ids);
|
||||
pc_n = 0;
|
||||
snprintf(why, n, "hybrid CPU, %d P-core + %d E-core logical processors on %d P-cores, fewer than %d", np, ne, nc,
|
||||
PC_MIN_CORES);
|
||||
return 0;
|
||||
}
|
||||
pc_ids = ids;
|
||||
snprintf(why, n, "hybrid CPU, %d P-core + %d E-core logical processors%s", np, ne,
|
||||
ok_p != np || ok_e != ne ? " (affinity-limited)" : "");
|
||||
return (int)pc_n;
|
||||
}
|
||||
|
||||
int plat_thread_perf_core(void) {
|
||||
if (!pc_ids) return -1;
|
||||
return SetThreadSelectedCpuSets(GetCurrentThread(), pc_ids, pc_n) ? 1 : 0;
|
||||
}
|
||||
|
||||
// PLAT_PRIO_* are the THREAD_PRIORITY_* values.
|
||||
void plat_thread_priority(int prio) { SetThreadPriority(GetCurrentThread(), prio); }
|
||||
void plat_thread_set_priority(PlatThread *t, int prio) { SetThreadPriority(t->h, prio); }
|
||||
@@ -319,6 +418,17 @@ uint64_t plat_counter_hz(void) { return qpc_freq(); }
|
||||
// The default timer tick is 15.6 ms; a 1 ms wait would cost a full tick.
|
||||
void plat_timer_1ms(void) { timeBeginPeriod(1); }
|
||||
|
||||
// Windows 11 runs the threads of a process it deems in the background at EcoQoS (on the E-cores of a hybrid Intel CPU,
|
||||
// at a low clock) and ignores its timer resolution while its window is hidden. The game is always latency-bound, so it
|
||||
// opts out of both. Windows before 10 1709 does not know the class: 0.
|
||||
int plat_power_full(void) {
|
||||
PROCESS_POWER_THROTTLING_STATE s = {
|
||||
.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
|
||||
.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
|
||||
.StateMask = 0};
|
||||
return SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &s, sizeof s) ? 1 : 0;
|
||||
}
|
||||
|
||||
static PlatScale qpc_ns; // set before main, so no thread reads it half written
|
||||
|
||||
__attribute__((constructor)) static void qpc_ns_init(void) { qpc_ns = plat_scale(1000000000u, qpc_freq()); }
|
||||
@@ -626,8 +736,21 @@ void plat_mem_fault_hook(int (*fn)(uint64_t addr)) {
|
||||
|
||||
static void (*crash_fn)(const PlatCrash *c);
|
||||
|
||||
// The faulting thread's general registers, for the report to tie the faulting instruction to its operands.
|
||||
static void regs_of(const CONTEXT *x, PlatCrash *c) {
|
||||
const DWORD64 r[16] = {x->Rax, x->Rcx, x->Rdx, x->Rbx, x->Rsp, x->Rbp, x->Rsi, x->Rdi,
|
||||
x->R8, x->R9, x->R10, x->R11, x->R12, x->R13, x->R14, x->R15};
|
||||
for (int i = 0; i < 16; i++) c->gpr[i] = r[i];
|
||||
c->has_regs = 1;
|
||||
}
|
||||
|
||||
static LONG WINAPI crash_filter(EXCEPTION_POINTERS *ep);
|
||||
|
||||
static LONG CALLBACK crash_veh(EXCEPTION_POINTERS *ep) {
|
||||
DWORD code = ep->ExceptionRecord->ExceptionCode;
|
||||
// Heap corruption is raised by the heap manager, which then ends the process itself: it never reaches the
|
||||
// unhandled-exception filter, so it is reported from here, wherever the heap was called from.
|
||||
if (code == 0xc0000374u) return crash_filter(ep); // STATUS_HEAP_CORRUPTION
|
||||
const char *what = code == EXCEPTION_ACCESS_VIOLATION ? "access violation"
|
||||
: code == EXCEPTION_ILLEGAL_INSTRUCTION ? "illegal instruction"
|
||||
: code == EXCEPTION_INT_DIVIDE_BY_ZERO ? "integer division by zero"
|
||||
@@ -643,6 +766,7 @@ static LONG CALLBACK crash_veh(EXCEPTION_POINTERS *ep) {
|
||||
PlatCrash c = {what, (uint32_t)code, rip, rip - (uintptr_t)mi.lpBaseOfDll, -1, 0};
|
||||
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2)
|
||||
c.access = (int)ep->ExceptionRecord->ExceptionInformation[0], c.addr = ep->ExceptionRecord->ExceptionInformation[1];
|
||||
regs_of(ep->ContextRecord, &c);
|
||||
crash_fn(&c);
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
@@ -674,16 +798,20 @@ static const char *image_name(const char *base) {
|
||||
return base + ((const IMAGE_EXPORT_DIRECTORY *)(base + d->VirtualAddress))->Name;
|
||||
}
|
||||
|
||||
// The return addresses in [lo, lo + size) on the crashed thread's stack, innermost first: unwinds the faulting context
|
||||
// The return addresses on the crashed thread's stack, innermost first, with their modules: unwinds the faulting context
|
||||
// with the x64 unwind tables; a function without a table entry (a leaf, or address 0 after a call through a null
|
||||
// pointer) has its return address on top of the stack. The walk stops at return address 0 (the thread's first frame)
|
||||
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting.
|
||||
static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t *out, int max) {
|
||||
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting. A driver can
|
||||
// nest deeper than the report holds, so frames in other modules stop being kept after the first 12 (counted in
|
||||
// frames_left_out) and the frames in this executable, which name the host call that ran into the driver, still fit.
|
||||
static void frames_of(const CONTEXT *at, PlatCrash *x) {
|
||||
enum { MAX = sizeof x->frames / sizeof *x->frames, OTHER_MAX = 12 };
|
||||
CONTEXT c = *at;
|
||||
ULONG_PTR low, high;
|
||||
GetCurrentThreadStackLimits(&low, &high);
|
||||
int n = 0;
|
||||
for (int i = 0; i < 256 && n < max && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
|
||||
void *exe = GetModuleHandleW(NULL);
|
||||
int other = 0;
|
||||
for (int i = 0; i < 256 && x->nframes < MAX && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
|
||||
DWORD64 img;
|
||||
RUNTIME_FUNCTION *fe = RtlLookupFunctionEntry(c.Rip, &img, NULL);
|
||||
if (fe) {
|
||||
@@ -694,9 +822,64 @@ static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t
|
||||
c.Rip = *(const DWORD64 *)c.Rsp;
|
||||
c.Rsp += 8;
|
||||
}
|
||||
if (c.Rip - lo < size) out[n++] = (uintptr_t)c.Rip;
|
||||
if (!c.Rip) break;
|
||||
void *base = NULL;
|
||||
RtlPcToFileHeader((void *)(uintptr_t)c.Rip, &base);
|
||||
if (base != exe && other++ >= OTHER_MAX) {
|
||||
x->frames_left_out++;
|
||||
continue;
|
||||
}
|
||||
int n = x->nframes++;
|
||||
x->frames[n] = (uintptr_t)c.Rip;
|
||||
x->frame_module[n] = base == exe ? NULL : base ? image_name(base) : "";
|
||||
x->frame_offset[n] = (uintptr_t)c.Rip - (uintptr_t)base;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// MSVC's C++ throw (0xe06d7363) passes the thrown object in ExceptionInformation[1], its ThrowInfo in [2] and the image
|
||||
// base the ThrowInfo's 32-bit offsets are relative to in [3] (magic 0x19930520..0x19930522 in [0]). The ThrowInfo lists
|
||||
// the types a catch may match, the thrown type first; each type descriptor holds a vtable pointer, a spare pointer and
|
||||
// the decorated name. A std::exception keeps its what() text in the pointer right after its vtable pointer (MSVC's
|
||||
// __std_exception_data). Every read is checked: the exception may come from a damaged heap.
|
||||
typedef struct { uint32_t attributes; int32_t unwind, forward_compat, catchable_types; } CxxThrowInfo;
|
||||
typedef struct { uint32_t properties; int32_t type, this_mdisp, this_pdisp, this_vdisp, size, copy; } CxxCatchableType;
|
||||
|
||||
// Copies the string at p into buf (cut to n - 1 characters), checking each page before reading it; NULL when the first
|
||||
// byte cannot be read.
|
||||
static const char *copy_cstr(uintptr_t p, char *buf, size_t n) {
|
||||
size_t i = 0;
|
||||
for (; p && i + 1 < n; i++) {
|
||||
if ((!i || !((p + i) & 4095)) && !plat_mem_readable(p + i, 1)) break;
|
||||
if (!(buf[i] = ((const char *)p)[i])) return buf;
|
||||
}
|
||||
buf[i] = 0;
|
||||
return i ? buf : NULL;
|
||||
}
|
||||
|
||||
static void cxx_info(const EXCEPTION_RECORD *er, PlatCrash *x) {
|
||||
if (er->NumberParameters < 4 || er->ExceptionInformation[0] - 0x19930520u > 2) return;
|
||||
uintptr_t obj = er->ExceptionInformation[1], info = er->ExceptionInformation[2],
|
||||
base = er->ExceptionInformation[3];
|
||||
if (!info || !plat_mem_readable(info, sizeof(CxxThrowInfo))) return;
|
||||
const CxxThrowInfo *ti = (const void *)info;
|
||||
uintptr_t arr = base + (uintptr_t)(intptr_t)ti->catchable_types;
|
||||
if (!ti->catchable_types || !plat_mem_readable(arr, 4)) return;
|
||||
int32_t n = *(const int32_t *)arr;
|
||||
if (n <= 0 || n > 64 || !plat_mem_readable(arr + 4, 4u * (uint64_t)n)) return;
|
||||
static char type[128], what[256]; // the crash report runs once: crash_fn ends the process
|
||||
int exception = 0;
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
uintptr_t ct = base + (uintptr_t)(intptr_t)((const int32_t *)(arr + 4))[i];
|
||||
if (!plat_mem_readable(ct, sizeof(CxxCatchableType))) continue;
|
||||
uintptr_t td = base + (uintptr_t)(intptr_t)((const CxxCatchableType *)ct)->type;
|
||||
char name[64];
|
||||
if (!copy_cstr(td + 16, i ? name : type, i ? sizeof name : sizeof type)) // past vtable and spare pointers
|
||||
continue;
|
||||
if (!i) x->cxx_type = type;
|
||||
if (!strcmp(i ? name : type, ".?AVexception@std@@")) exception = 1;
|
||||
}
|
||||
if (exception && obj && plat_mem_readable(obj + 8, 8))
|
||||
x->cxx_what = copy_cstr(*(const uintptr_t *)(obj + 8), what, sizeof what);
|
||||
}
|
||||
|
||||
// The last-chance filter: an exception no handler took, in any module. crash_veh reports faults in this executable as
|
||||
@@ -721,16 +904,15 @@ static LONG WINAPI crash_filter(EXCEPTION_POINTERS *ep) {
|
||||
(uint32_t)code, rip, 0, -1, 0};
|
||||
if (code == EXCEPTION_ACCESS_VIOLATION && er->NumberParameters >= 2)
|
||||
c.access = (int)er->ExceptionInformation[0], c.addr = er->ExceptionInformation[1];
|
||||
regs_of(ep->ContextRecord, &c);
|
||||
void *base = NULL;
|
||||
RtlPcToFileHeader((void *)rip, &base);
|
||||
MODULEINFO mi;
|
||||
uintptr_t exe = (uintptr_t)GetModuleHandleW(NULL);
|
||||
c.offset = rip - (uintptr_t)base;
|
||||
if (!base) c.module = "";
|
||||
else if ((uintptr_t)base != exe) c.module = image_name(base);
|
||||
if (c.module && GetModuleInformation(GetCurrentProcess(), (HMODULE)exe, &mi, sizeof mi))
|
||||
c.nframes = frames_in(ep->ContextRecord, exe, mi.SizeOfImage, c.frames,
|
||||
(int)(sizeof c.frames / sizeof *c.frames));
|
||||
if (c.module) frames_of(ep->ContextRecord, &c);
|
||||
if (code == 0xe06d7363u) cxx_info(er, &c);
|
||||
crash_fn(&c);
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
@@ -775,6 +957,14 @@ int plat_mem_read(uint64_t addr, void *out, size_t n) {
|
||||
return ReadProcessMemory(GetCurrentProcess(), (const void *)(uintptr_t)addr, out, n, &got) && got == n;
|
||||
}
|
||||
|
||||
// The display mode's own size: physical pixels whatever the process's DPI awareness and the display scale
|
||||
int plat_screen_size(int *w, int *h) {
|
||||
DEVMODEW dm = {.dmSize = sizeof dm};
|
||||
if (!EnumDisplaySettingsW(NULL, ENUM_CURRENT_SETTINGS, &dm) || !dm.dmPelsWidth || !dm.dmPelsHeight) return 0;
|
||||
*w = (int)dm.dmPelsWidth, *h = (int)dm.dmPelsHeight;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n) {
|
||||
for (uint64_t a = addr, end = addr + n; a < end || a == addr;) {
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
@@ -1381,6 +1571,27 @@ void *plat_lib_open(const char *name) {
|
||||
|
||||
void *plat_lib_sym(void *lib, const char *name) { return lib ? (void *)GetProcAddress((HMODULE)lib, name) : NULL; }
|
||||
|
||||
void plat_lib_error(char *out, size_t cap) { snprintf(out, cap, "error %lu", GetLastError()); }
|
||||
|
||||
int plat_ngx_core_path(char *out, size_t cap) {
|
||||
wchar_t dir[WPATH];
|
||||
DWORD n = sizeof dir;
|
||||
char d[PLAT_PATH_MAX];
|
||||
if (RegGetValueW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\NVIDIA Corporation\\Global\\NGXCore", L"FullPath", RRF_RT_REG_SZ,
|
||||
NULL, dir, &n) != ERROR_SUCCESS ||
|
||||
!WideCharToMultiByte(CP_UTF8, 0, dir, -1, d, (int)sizeof d, NULL, NULL))
|
||||
return 0;
|
||||
return snprintf(out, cap, "%s\\_nvngx.dll", d) < (int)cap;
|
||||
}
|
||||
|
||||
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
|
||||
DWORD len = GetModuleFileNameW(NULL, out, (DWORD)cap);
|
||||
if (!len || len >= cap) return 0;
|
||||
while (len && out[len - 1] != L'\\' && out[len - 1] != L'/') len--;
|
||||
out[len] = 0;
|
||||
return len != 0;
|
||||
}
|
||||
|
||||
// ---- machine load: GetSystemTimes for the CPU, PDH (pdh.dll, loaded on first use) for the thermal zones and the GPU
|
||||
// engine and adapter memory counters that Task Manager reads.
|
||||
|
||||
@@ -1573,6 +1784,12 @@ void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
|
||||
*proc_mb = (double)(pmc.PrivateUsage >> 20);
|
||||
}
|
||||
|
||||
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room) {
|
||||
*commit_room = *phys_room = UINT64_MAX;
|
||||
MEMORYSTATUSEX ms = {.dwLength = sizeof ms};
|
||||
if (GlobalMemoryStatusEx(&ms)) *commit_room = ms.ullAvailPageFile, *phys_room = ms.ullAvailPhys;
|
||||
}
|
||||
|
||||
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes) {
|
||||
PROCESS_MEMORY_COUNTERS_EX pm = {.cb = sizeof pm};
|
||||
*resident = *private_bytes = 0;
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// Play time: how long the player has spent in the game, across runs, shown on the Game tab of the settings menu
|
||||
// (ui/menu.cpp tab_game). The game itself keeps no play time a player can see, and the Switch's own play log is not
|
||||
// there to read, so the port counts it.
|
||||
//
|
||||
// One host thread (it never holds a virtual core, as discord_rpc.c's) wakes every second and adds the time since its
|
||||
// last wake when the window has the focus (gfx_window_focused) and the game presented at least one frame meanwhile
|
||||
// (gfx_stats). So the shader prewarm before the first frame, a window left in the background and a hung game do not
|
||||
// count; loading screens and the game's own menus do, as they present frames. One wake adds at most TICK_CAP_MS, so a
|
||||
// machine that slept with the game open adds two seconds, not the night.
|
||||
//
|
||||
// The total lives in <profile>/playtime.txt, next to account.uid and the saves, so it stays with them when the
|
||||
// install is updated or moved. It is plain text, "total_seconds <n>", written atomically (plat_file_write_atomic)
|
||||
// every SAVE_EVERY_MS while it grows and once more when the game exits (playtime_save from boot.c, svc.c and
|
||||
// gfx_present.c, next to prof_write); a crash loses at most SAVE_EVERY_MS. A file that does not read as that is kept
|
||||
// as playtime.txt.bad and the count starts again from zero. A scripted run (--seconds) reads the total and adds nothing.
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define TICK_MS 1000
|
||||
#define TICK_CAP_MS 2000
|
||||
#define SAVE_EVERY_MS 30000
|
||||
#define TOTAL_CAP_S (100ull * 365 * 24 * 3600) // a hundred years: anything larger is a damaged file
|
||||
|
||||
static PlatMutex lock = PLAT_MUTEX_INIT; // the totals and the file
|
||||
static uint64_t total_ms, session_ms, saved_ms;
|
||||
static int loaded; // 1 once the file was read (or found missing), -1 when there is no profile
|
||||
static char path[1100];
|
||||
|
||||
// The seconds of a playtime.txt: "total_seconds <n>", a line end or nothing after it, a UTF-8 byte order mark before it
|
||||
// (an editor may add one). 1 when the file reads so and n is at most TOTAL_CAP_S.
|
||||
static int parse(const uint8_t *b, uint64_t n, uint64_t *seconds) {
|
||||
if (n >= 3 && !memcmp(b, "\xEF\xBB\xBF", 3)) b += 3, n -= 3;
|
||||
char text[64] = {0};
|
||||
memcpy(text, b, n < sizeof text - 1 ? n : sizeof text - 1);
|
||||
unsigned long long s = 0;
|
||||
char tail = 0;
|
||||
if (sscanf(text, "total_seconds %llu%c", &s, &tail) < 1 || (tail && tail != '\n' && tail != '\r') ||
|
||||
s > TOTAL_CAP_S)
|
||||
return 0;
|
||||
*seconds = s;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Holding lock. dir: the profile directory.
|
||||
static void load(const char *dir) {
|
||||
if (loaded) return;
|
||||
if (!dir || !*dir || snprintf(path, sizeof path, "%s/playtime.txt", dir) >= (int)sizeof path) {
|
||||
loaded = -1;
|
||||
rt_log("playtime: no profile directory; play time is not counted");
|
||||
return;
|
||||
}
|
||||
loaded = 1;
|
||||
uint64_t n = 0, s = 0;
|
||||
uint8_t *b = plat_file_read(path, &n);
|
||||
if (!b) {
|
||||
rt_log("playtime: no %s yet; counting from zero", path);
|
||||
return;
|
||||
}
|
||||
if (parse(b, n, &s)) {
|
||||
free(b);
|
||||
total_ms = saved_ms = s * 1000;
|
||||
rt_log("playtime: %llu h %02llu min played before this run (%s)", (unsigned long long)(s / 3600),
|
||||
(unsigned long long)(s / 60 % 60), path);
|
||||
return;
|
||||
}
|
||||
char bad[1110];
|
||||
snprintf(bad, sizeof bad, "%s.bad", path);
|
||||
int kept = plat_file_write_atomic(bad, b, (size_t)n); // the whole file, for the player to recover the number
|
||||
free(b);
|
||||
rt_log("playtime: %s does not read as \"total_seconds <n>\"; %s, counting from zero", path,
|
||||
kept ? "kept as playtime.txt.bad" : "could not keep a copy");
|
||||
}
|
||||
|
||||
// Holding lock.
|
||||
static void save_locked(void) {
|
||||
if (loaded != 1 || total_ms / 1000 == saved_ms / 1000) return;
|
||||
char text[48];
|
||||
int n = snprintf(text, sizeof text, "total_seconds %" PRIu64 "\n", total_ms / 1000);
|
||||
if (plat_file_write_atomic(path, text, (size_t)n)) saved_ms = total_ms;
|
||||
else rt_log("playtime: cannot write %s", path);
|
||||
}
|
||||
|
||||
void playtime_save(void) {
|
||||
plat_mutex_lock(&lock);
|
||||
save_locked();
|
||||
plat_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
int playtime_get(uint64_t *total_s, uint64_t *session_s) {
|
||||
plat_mutex_lock(&lock);
|
||||
int ok = loaded == 1;
|
||||
*total_s = total_ms / 1000;
|
||||
*session_s = session_ms / 1000;
|
||||
plat_mutex_unlock(&lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Holding lock: the time since the last tick, d ms, when the game was played meanwhile; the file every SAVE_EVERY_MS.
|
||||
static void tick_locked(uint64_t now, uint64_t d, int playing, uint64_t *last_save) {
|
||||
if (d > TICK_CAP_MS) d = TICK_CAP_MS;
|
||||
if (playing) total_ms += d, session_ms += d;
|
||||
if (now - *last_save >= SAVE_EVERY_MS) save_locked(), *last_save = now;
|
||||
}
|
||||
|
||||
static void playtime_main(void *arg) {
|
||||
(void)arg;
|
||||
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
|
||||
uint64_t last = plat_now_ms(), last_save = last, frames = gfx_stats().frames;
|
||||
for (;;) {
|
||||
plat_sleep_ms(TICK_MS);
|
||||
uint64_t now = plat_now_ms(), f = gfx_stats().frames;
|
||||
int playing = f != frames && gfx_window_focused();
|
||||
plat_mutex_lock(&lock);
|
||||
tick_locked(now, now - last, playing, &last_save);
|
||||
plat_mutex_unlock(&lock);
|
||||
last = now, frames = f;
|
||||
}
|
||||
}
|
||||
|
||||
// The file is read at once, so the Game tab shows the total from the first frame. A scripted run (--seconds: benches,
|
||||
// menu shots, input scripts) shows it but adds nothing: those runs are not play.
|
||||
void playtime_init(void) {
|
||||
static int32_t started;
|
||||
if (plat_xchg32(&started, 1)) return;
|
||||
plat_mutex_lock(&lock);
|
||||
load(rt_config()->profile);
|
||||
int on = loaded == 1;
|
||||
plat_mutex_unlock(&lock);
|
||||
if (!on) return;
|
||||
if (rt_config()->seconds > 0) {
|
||||
rt_log("playtime: a scripted run (--seconds); play time is shown, not counted");
|
||||
return;
|
||||
}
|
||||
if (!plat_thread_spawn(playtime_main, NULL, "playtime", 0))
|
||||
rt_log("playtime: cannot start its thread; play time is not counted");
|
||||
}
|
||||
|
||||
// --probe-playtime: the file's parsing, a missing, a valid and a damaged file, the periodic save and the tick's cap, in
|
||||
// a directory next to totk.exe. Needs no game files.
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL playtime: %s\n", what); } } while (0)
|
||||
|
||||
static void reset(void) { total_ms = session_ms = saved_ms = 0, loaded = 0, path[0] = 0; }
|
||||
|
||||
static int file_is(const char *p, const char *want) {
|
||||
uint64_t n = 0;
|
||||
uint8_t *b = plat_file_read(p, &n);
|
||||
int ok = b && n == strlen(want) && !memcmp(b, want, n);
|
||||
free(b);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int probe_playtime(void) {
|
||||
uint64_t s = 0;
|
||||
CHECK(parse((const uint8_t *)"total_seconds 42\n", 17, &s) && s == 42, "a line with LF");
|
||||
CHECK(parse((const uint8_t *)"total_seconds 42\r\n", 18, &s) && s == 42, "a line with CRLF");
|
||||
CHECK(parse((const uint8_t *)"total_seconds 42", 16, &s) && s == 42, "no line end");
|
||||
CHECK(parse((const uint8_t *)"\xEF\xBB\xBFtotal_seconds 7\n", 19, &s) && s == 7, "a byte order mark");
|
||||
CHECK(!parse((const uint8_t *)"total_seconds 42x", 17, &s), "a character after the number");
|
||||
CHECK(!parse((const uint8_t *)"total_seconds -1\n", 17, &s), "a negative number");
|
||||
CHECK(!parse((const uint8_t *)"total_seconds 3153600001\n", 25, &s), "over a hundred years");
|
||||
CHECK(!parse((const uint8_t *)"seconds 42\n", 11, &s), "another key");
|
||||
CHECK(!parse((const uint8_t *)"", 0, &s), "an empty file");
|
||||
|
||||
char dir[1024];
|
||||
if (!plat_exe_path(dir, sizeof dir)) return 1;
|
||||
char *cut = plat_last_sep(dir);
|
||||
if (!cut) return 1;
|
||||
strcpy(cut + 1, "playtime-probe");
|
||||
plat_file_mkdir_p(dir);
|
||||
char file[1100], bad[1110];
|
||||
snprintf(file, sizeof file, "%s/playtime.txt", dir);
|
||||
snprintf(bad, sizeof bad, "%s.bad", file);
|
||||
plat_file_delete(file);
|
||||
plat_file_delete(bad);
|
||||
|
||||
uint64_t total, session, last_save = 0;
|
||||
reset();
|
||||
load(dir);
|
||||
CHECK(playtime_get(&total, &session) && total == 0, "a missing file counts from zero");
|
||||
tick_locked(1000, 1000, 1, &last_save);
|
||||
tick_locked(2000, 1000, 0, &last_save);
|
||||
CHECK(plat_file_size(file) < 0, "nothing is written before SAVE_EVERY_MS");
|
||||
tick_locked(2000 + 3600000, 3600000, 1, &last_save); // a machine that slept: one tick adds TICK_CAP_MS, and saves
|
||||
playtime_get(&total, &session);
|
||||
CHECK(total == 3 && session == 3, "played ticks count, an idle one does not, a long one is capped");
|
||||
CHECK(file_is(file, "total_seconds 3\n"), "the save after SAVE_EVERY_MS writes the total");
|
||||
|
||||
reset();
|
||||
load(dir);
|
||||
playtime_get(&total, &session);
|
||||
CHECK(total == 3 && session == 0, "the next run reads the total back");
|
||||
|
||||
static const char damaged[] = "total_seconds twelve\n";
|
||||
plat_file_write_atomic(file, damaged, sizeof damaged - 1);
|
||||
reset();
|
||||
load(dir);
|
||||
playtime_get(&total, &session);
|
||||
CHECK(total == 0, "a damaged file counts from zero");
|
||||
CHECK(file_is(bad, damaged), "a damaged file is kept whole as playtime.txt.bad");
|
||||
|
||||
plat_file_delete(file);
|
||||
plat_file_delete(bad);
|
||||
reset();
|
||||
fprintf(stderr, "playtime: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
+242
-33
@@ -14,10 +14,11 @@
|
||||
// 64-bit word, +40 the renderer (1.2.1) or display configuration (1.4.3) the patched code stores there.
|
||||
// +56 the camera's aspect ratio as a float when the output is not 16:9 (set aspect), +60 the field-of-view factor
|
||||
// (set fov, res_fov_live).
|
||||
// 1.4.3 scales the scene it reads by output width / 1920 (a 2560x1440 output renders its 1600x900 scene at 2128x1200;
|
||||
// MacNativeGames measured the same), so its page holds the wanted size divided by that factor and the game's 5/6 needs
|
||||
// no scene patch. 1.2.1 renders the size it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and
|
||||
// the scene set is on whenever the output differs from 1920x1080.
|
||||
// 1.4.3 scales the scene it reads by canvas width / the console mode's base width, 1920 docked and 1280 handheld (a
|
||||
// 2560x1440 output renders its 1600x900 scene at 2128x1200 docked; MacNativeGames measured the same), so its page holds
|
||||
// the wanted size divided by that factor (scene_scale) and the game's 5/6 needs no scene patch. 1.2.1 renders the size
|
||||
// it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and the scene set is on whenever the
|
||||
// output differs from 1920x1080.
|
||||
//
|
||||
// Heap: agl's DynamicTextureAllocator is one fixed heap, "DynamicTextureAllocatorMem" in
|
||||
// System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs (113664 KiB, carved from "Graphics", 201728 KiB). At 2560x1440
|
||||
@@ -39,6 +40,8 @@
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "mods.h"
|
||||
#include "ov.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define BASE_W 1920
|
||||
@@ -46,7 +49,10 @@
|
||||
#define SCENE_W 1600
|
||||
#define SCENE_H 900
|
||||
#define DTA_KIB 113664u // DynamicTextureAllocatorMem, shipped
|
||||
#define SCENE_MAX 4096 // per side, as MacNativeGames' TOTK_RENDER_RES
|
||||
#define TEX_HEAP_KIB 506880u // TextureHeap, shipped (1.2.1 and 1.4.3): what a mod's own HeapDef is measured against
|
||||
#define SCENE_MAX 8192 // per side
|
||||
#define SCENE_KEEP 4096 // per side: the heap bound shrinks no scene below this (scene_for)
|
||||
#define DTA_MAX_KIB 2031616u // DynamicTextureAllocatorMem grown at most to 1984 MiB, under 2 GiB (decide)
|
||||
#define SHADOW_BASE 960 // the sun shadow map's base size (its cascades: a D16 array of 4 layers)
|
||||
|
||||
static struct {
|
||||
@@ -89,12 +95,25 @@ static int dlss_dll(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// The scene size for a share k of the output, within SCENE_MAX per side.
|
||||
static void scene_for(double k, unsigned *w, unsigned *h) {
|
||||
static uint64_t shadow_kib(void);
|
||||
|
||||
// The scene size for a share k of the output, within SCENE_MAX a side and within what a texture heap of DTA_MAX_KIB
|
||||
// holds next to the output and the shadow map (res_heap_extra_kib's growth). 1.2.1 already misplaced textures in a
|
||||
// pool past 2 GiB, where a signed 32-bit offset turns negative (recomp/patches-v121.txt, set pooloff); larger heaps
|
||||
// have not been run. The heap bound never takes the scene below 4096 a side, the size earlier builds gave: an output
|
||||
// near 8K grows the heap past 2 GiB for its own targets, and its scene stays what it was. Needs R.shadow. 1 when the
|
||||
// scene was made smaller than k asks.
|
||||
static int scene_for(double k, unsigned *w, unsigned *h) {
|
||||
double sw = R.ow * k, sh = R.oh * k;
|
||||
double fit = fmin(1.0, fmin(SCENE_MAX / sw, SCENE_MAX / sh));
|
||||
double keep = fmin(1.0, fmin(SCENE_KEEP / sw, SCENE_KEEP / sh));
|
||||
double room = (double)(DTA_MAX_KIB - shadow_kib()) / DTA_KIB - 1.0 -
|
||||
fmax(0.0, (double)R.ow * R.oh / (BASE_W * BASE_H) - 1.0);
|
||||
double most = (1.0 + fmax(0.0, room)) * SCENE_W * SCENE_H; // scene pixels that fit
|
||||
if (sw * sh * fit * fit > most) fit = fmax(keep, sqrt(most / (sw * sh)));
|
||||
*w = (unsigned)(sw * fit + 0.5);
|
||||
*h = (unsigned)(sh * fit + 0.5);
|
||||
return fit < 1.0;
|
||||
}
|
||||
|
||||
// The scene's share of the output for a TOTK_DLSS mode; 0 for dlaa (the scene keeps base_k), while DLSS/DLAA is off
|
||||
@@ -120,6 +139,25 @@ static double dlss_share(const char *m) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A game resolution that is not 16:9 needs the HUD's layout for its shape (romfs-patches-<w>x<h>, romfs.c
|
||||
// overlay_init), which the installer makes but a developer build only has after tools/make_romfs_patches.py --aspect.
|
||||
// Without it the camera would widen while the 16:9 HUD stretched across the picture, so the game draws the largest
|
||||
// 16:9 size that fits inside the asked one and the camera aspect patch stays off (res_init); gfx_present.c shows the
|
||||
// picture with bars (present setting aspect). Not when TOTK_ROMFS_PATCHES names the overlay itself or the game version
|
||||
// has no camera aspect patch. TOTK_OUTPUT_RES is restart-only (cfg.c, RESTART), so this one decision holds all run.
|
||||
static void shape_fallback(void) {
|
||||
const char *own = cfg_get("TOTK_ROMFS_PATCHES");
|
||||
unsigned out[2] = {R.ow, R.oh};
|
||||
if (own || !res_wants_wide_overlay(out) || romfs_shaped_ready(R.ow, R.oh)) return;
|
||||
unsigned h = R.oh, w = R.ow;
|
||||
if ((uint64_t)w * 9 <= (uint64_t)h * 16) h = (unsigned)((uint64_t)w * 9 / 16) & ~1u;
|
||||
w = (unsigned)((uint64_t)h * 16 / 9) & ~1u;
|
||||
rt_log("resolution: no romfs-patches-%ux%u for the HUD at this shape; the game draws %ux%u (16:9) and the present "
|
||||
"adds bars. Make the layout with: tools/make_romfs_patches.py --version <v121|v143> --aspect %ux%u --out "
|
||||
"build/<version>/romfs-patches-%ux%u", R.ow, R.oh, w, h, R.ow, R.oh, R.ow, R.oh);
|
||||
R.ow = w, R.oh = h;
|
||||
}
|
||||
|
||||
static void decide(void) {
|
||||
if (R.decided) return;
|
||||
R.decided = 1;
|
||||
@@ -131,6 +169,7 @@ static void decide(void) {
|
||||
if (sscanf(o, "%ux%u%c", &w, &h, &tail) == 2 && w >= 1280 && h >= 720 && w <= 7680 && h <= 4320) {
|
||||
R.ow = w & ~1u;
|
||||
R.oh = h & ~1u;
|
||||
shape_fallback();
|
||||
} else {
|
||||
rt_log("resolution: TOTK_OUTPUT_RES=%s is not <width>x<height> in 1280x720..7680x4320; 1920x1080", o);
|
||||
}
|
||||
@@ -146,6 +185,12 @@ static void decide(void) {
|
||||
rt_log("resolution: TOTK_RENDER_SCALE=%s is not game or a percentage 25..400; game", s);
|
||||
}
|
||||
}
|
||||
const char *sr = cfg_value("TOTK_SHADOW_RES");
|
||||
if (strcmp(sr, "game")) {
|
||||
unsigned n = (unsigned)atoi(sr);
|
||||
if (n >= 256 && n <= 4096 && !(n & 7)) R.shadow = n;
|
||||
else rt_log("resolution: TOTK_SHADOW_RES=%s is not game or a size 256..4096 (a multiple of 8); game", sr);
|
||||
}
|
||||
// A DLSS mode other than dlaa sets the scene size instead. On 1.4.3 with DLSS able to run (DLAA on, the DLL there)
|
||||
// the mode can change while the game runs (res_dlss_mode): the scene patch is then always on and the heap is sized
|
||||
// for the largest scene the modes ask for. Everyone else keeps the game's own scene size and its dynamic resolution
|
||||
@@ -156,16 +201,14 @@ static void decide(void) {
|
||||
R.dlss_k = dlss_share(m);
|
||||
if (R.dlss_k) rt_log("resolution: DLSS %s: the scene at %.0f %% of the output (render_scale ignored)", m, R.dlss_k * 100);
|
||||
double sk = R.dlss_k ? R.dlss_k : k;
|
||||
if (R.live || R.dlss_k || R.ow * sk > SCENE_MAX || R.oh * sk > SCENE_MAX) R.scene_set = 1;
|
||||
scene_for(sk, &R.sw, &R.sh);
|
||||
if (R.live || R.dlss_k) R.scene_set = 1;
|
||||
if (scene_for(sk, &R.sw, &R.sh)) {
|
||||
R.scene_set = 1;
|
||||
rt_log("resolution: a %.0fx%.0f scene is larger than the port allows (8192 a side, a texture heap under 2 GiB "
|
||||
"past 4096 a side); %ux%u", R.ow * sk, R.oh * sk, R.sw, R.sh);
|
||||
}
|
||||
if (R.live) scene_for(fmax(k, 2.0 / 3.0), &R.hw, &R.hh);
|
||||
else R.hw = R.sw, R.hh = R.sh;
|
||||
const char *sr = cfg_value("TOTK_SHADOW_RES");
|
||||
if (strcmp(sr, "game")) {
|
||||
unsigned n = (unsigned)atoi(sr);
|
||||
if (n >= 256 && n <= 4096 && !(n & 7)) R.shadow = n;
|
||||
else rt_log("resolution: TOTK_SHADOW_RES=%s is not game or a size 256..4096 (a multiple of 8); game", sr);
|
||||
}
|
||||
}
|
||||
|
||||
// Texture heap for a sun shadow map above the base size: the four layers of 2 bytes, three maps in flight (the engine
|
||||
@@ -187,8 +230,46 @@ uint64_t res_heap_extra_kib(void) {
|
||||
return kib + ((shadow_kib() + 1023) & ~1023ull); // whole MiB
|
||||
}
|
||||
|
||||
// 1.4.3's factor between the scene size it reads from the page and the one it renders. The game's scene setter
|
||||
// (main+0x585e48 and the code before it) multiplies the page's size by the canvas width (display configuration +0x10)
|
||||
// over the current console mode's base width (+0xf4 of the mode's entry, mode 1 handheld: main+0x2dac1c stores
|
||||
// GetOperationMode != 1 at +0x546). The base is 1280 handheld and 1920 docked. The canvas is the output whenever the
|
||||
// scene patch is on: docked the game's own canvas is 1920, handheld res_init switches the output patch on (a 1280 base
|
||||
// with the game's 1280x720 handheld canvas would render R.ow / 1280 times the scene). 1.2.1 renders the size it reads.
|
||||
static double scene_scale(void) {
|
||||
if (!RT_V143) return 1.0;
|
||||
return (double)R.ow / (ov_handheld() ? 1280 : BASE_W);
|
||||
}
|
||||
|
||||
uint64_t res_heap_extra_bytes(void) { return ((res_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
|
||||
|
||||
// TOTK_TEX_HEAP_EXTRA_MB (0 to 4096): more room in "TextureHeap" and its parent "ゲームグラフィックス", the heap the
|
||||
// world's textures stream into. HD texture packs made for emulators with 8 GB of DRAM grow it by about 3.3 GiB in their
|
||||
// own HeapDef; without the room the game runs out and starts its panic Blood Moon over and over. The pool and the heap
|
||||
// cap grow by the same amount (memory.c mem_init).
|
||||
static uint64_t mod_tex_heap_kib(void);
|
||||
|
||||
static uint64_t cfg_tex_heap_kib(void) {
|
||||
const char *v = cfg_get("TOTK_TEX_HEAP_EXTRA_MB");
|
||||
if (!v || !*v) return 0;
|
||||
char *end;
|
||||
long mb = strtol(v, &end, 10);
|
||||
if (end == v || *end || mb < 0 || mb > 4096) {
|
||||
static int logged;
|
||||
if (!logged++) rt_log("resolution: TOTK_TEX_HEAP_EXTRA_MB=%s is not 0..4096; 0", v);
|
||||
return 0;
|
||||
}
|
||||
return (uint64_t)mb << 10;
|
||||
}
|
||||
|
||||
// The larger of TOTK_TEX_HEAP_EXTRA_MB and what an enabled mod's own HeapDef adds to the texture heap (mod_tex_heap_kib).
|
||||
static uint64_t tex_heap_extra_kib(void) {
|
||||
uint64_t c = cfg_tex_heap_kib(), m = mod_tex_heap_kib();
|
||||
return c > m ? c : m;
|
||||
}
|
||||
|
||||
uint64_t res_tex_heap_extra_bytes(void) { return ((tex_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
|
||||
|
||||
// ---- the HeapDef table. BYML version 2..7, little endian ("YB"): a hash node is {u8 0xC1, u24 count} and count
|
||||
// entries {u24 key index, u8 type, u32 value}; an int (type 0xD1) is stored in the entry, so a size changes in place.
|
||||
typedef struct { uint8_t *p; size_t n; uint32_t keys; } Byml;
|
||||
@@ -226,28 +307,103 @@ static size_t heap_size_field(const Byml *b, uint32_t heaps, const char *heap) {
|
||||
return at && u32_at(b, at, &node) ? field(b, node, "Size", 0xd1) : 0;
|
||||
}
|
||||
|
||||
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
uint64_t extra = res_heap_extra_kib();
|
||||
*out = NULL;
|
||||
if (!extra) return 0;
|
||||
// A HeapDef in b's place (zstd frame in[0..n) with its content size, no dictionary): b.p malloc'd and *heaps its
|
||||
// "Heaps" node; 0 when it is not one (b.p freed). why: what the log line says the caller keeps then.
|
||||
static int heapdef_open(const void *in, size_t n, Byml *b, uint32_t *heaps, const char *why) {
|
||||
b->p = NULL;
|
||||
if (ZSTD_getDictID_fromFrame(in, n)) {
|
||||
rt_log("resolution: the HeapDef table needs a zstd dictionary; the game keeps its own resolution");
|
||||
rt_log("resolution: the HeapDef table needs a zstd dictionary; %s", why);
|
||||
return 0;
|
||||
}
|
||||
unsigned long long raw = ZSTD_getFrameContentSize(in, n);
|
||||
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > (64u << 20)) {
|
||||
rt_log("resolution: the HeapDef table is not a zstd frame with a size; the game keeps its own resolution");
|
||||
rt_log("resolution: the HeapDef table is not a zstd frame with a size; %s", why);
|
||||
return 0;
|
||||
}
|
||||
Byml b = {malloc((size_t)raw), (size_t)raw, 0};
|
||||
if (!b.p) return 0;
|
||||
size_t got = ZSTD_decompress(b.p, b.n, in, n);
|
||||
uint32_t root, heaps = 0;
|
||||
*b = (Byml){malloc((size_t)raw), (size_t)raw, 0};
|
||||
if (!b->p) return 0;
|
||||
size_t got = ZSTD_decompress(b->p, b->n, in, n);
|
||||
uint32_t root;
|
||||
uint16_t ver = 0;
|
||||
if (!ZSTD_isError(got) && got == b.n && b.n >= 16) memcpy(&ver, b.p + 2, 2);
|
||||
size_t heaps_at = 0, dta_at = 0, gfx_at = 0;
|
||||
if (ver >= 2 && ver <= 7 && !memcmp(b.p, "YB", 2) && u32_at(&b, 4, &b.keys) && u32_at(&b, 12, &root) &&
|
||||
(heaps_at = field(&b, root, "Heaps", 0xc1)) && u32_at(&b, heaps_at, &heaps)) {
|
||||
if (!ZSTD_isError(got) && got == b->n && b->n >= 16) memcpy(&ver, b->p + 2, 2);
|
||||
size_t heaps_at = 0;
|
||||
if (ver >= 2 && ver <= 7 && !memcmp(b->p, "YB", 2) && u32_at(b, 4, &b->keys) && u32_at(b, 12, &root) &&
|
||||
(heaps_at = field(b, root, "Heaps", 0xc1)) && u32_at(b, heaps_at, heaps))
|
||||
return 1;
|
||||
free(b->p);
|
||||
b->p = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// HD texture packs made for the emulators ship their own HeapDef with "TextureHeap" and its parent grown (HDPacks.com's
|
||||
// 4K pack for TotK: by 3,419,000 KiB, 3.3 GiB, for the emulators' 8 GB memory option). Served as it is, the game asks
|
||||
// for that room and the application pool has none: it crashed at start (issue #673). So before mem_init sizes the pool,
|
||||
// every switched-on mod's HeapDef is read (mods.c mods_peek_romfs: mods_scan needs paths main sets later) and the
|
||||
// largest TextureHeap growth over the shipped size counts as TOTK_TEX_HEAP_EXTRA_MB (the larger of the two, at most
|
||||
// 4 GiB). romfs.c heapdef_overlay then serves another table grown by that amount in the pack's place: the table of the
|
||||
// highest switched-on mod below it whose texture heap is not grown, else the game's own, so the other heaps keep the
|
||||
// sizes the content that runs needs.
|
||||
#define HEAPDEF_REL "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
|
||||
static struct { int done; uint64_t kib; } MT;
|
||||
|
||||
// "TextureHeap" of the HeapDef table in[0..n) in KiB; 0 when the table cannot be read or has no such size (the failure
|
||||
// is logged with why).
|
||||
static int32_t heapdef_tex_kib(const void *in, size_t n, const char *why) {
|
||||
Byml b;
|
||||
uint32_t heaps;
|
||||
size_t th_at = 0;
|
||||
int32_t th = 0;
|
||||
if (!heapdef_open(in, n, &b, &heaps, why)) return 0;
|
||||
if ((th_at = heap_size_field(&b, heaps, "TextureHeap"))) memcpy(&th, b.p + th_at, 4);
|
||||
else rt_log("resolution: the HeapDef table has no TextureHeap size; %s", why);
|
||||
free(b.p);
|
||||
return th;
|
||||
}
|
||||
|
||||
static void mod_heapdef(const char *name, const char *romfs, void *ctx) {
|
||||
(void)ctx;
|
||||
char path[2300], why[400];
|
||||
if (snprintf(path, sizeof path, "%s/%s", romfs, HEAPDEF_REL) >= (int)sizeof path) return;
|
||||
uint64_t n = 0;
|
||||
uint8_t *in = plat_file_read(path, &n);
|
||||
if (!in) return;
|
||||
snprintf(why, sizeof why, "mod %s: its texture heap growth is left out", name);
|
||||
int32_t th = heapdef_tex_kib(in, (size_t)n, why);
|
||||
free(in);
|
||||
if (!th) return;
|
||||
int64_t grow = (int64_t)th - (int64_t)TEX_HEAP_KIB;
|
||||
if (grow > (int64_t)(4096u << 10)) grow = 4096u << 10;
|
||||
if (grow > 0 && (uint64_t)grow > MT.kib) MT.kib = (uint64_t)grow;
|
||||
rt_log("resolution: mod %s ships a HeapDef with TextureHeap %d KiB (the game's %u KiB)%s", name, th, TEX_HEAP_KIB,
|
||||
grow > 0 ? ": the port grows the texture heap and the pool by the difference" : "");
|
||||
}
|
||||
|
||||
static uint64_t mod_tex_heap_kib(void) {
|
||||
if (!MT.done) MT.done = 1, mods_peek_romfs(mod_heapdef, NULL);
|
||||
return MT.kib;
|
||||
}
|
||||
|
||||
// Only such a table is swapped for the game's own (romfs.c heapdef_overlay): a mod table that changes other heaps
|
||||
// keeps them. A table it cannot read stays the base (logged).
|
||||
int res_heapdef_tex_grown(const void *in, size_t n) {
|
||||
return heapdef_tex_kib(in, n, "the mod's table stays the base") > (int32_t)TEX_HEAP_KIB;
|
||||
}
|
||||
|
||||
// A table that can stand in for an HD pack's (romfs.c heapdef_overlay): readable, with TextureHeap at most the shipped
|
||||
// size. A content mod's table (The Second Quest grows RSDB and UI for its own files) is one.
|
||||
int res_heapdef_plain(const void *in, size_t n) {
|
||||
int32_t th = heapdef_tex_kib(in, n, "it does not stand in for the texture pack's table");
|
||||
return th > 0 && th <= (int32_t)TEX_HEAP_KIB;
|
||||
}
|
||||
|
||||
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
uint64_t extra = res_heap_extra_kib(), tex = tex_heap_extra_kib();
|
||||
*out = NULL;
|
||||
if (!extra && !tex) return 0;
|
||||
Byml b;
|
||||
uint32_t heaps = 0;
|
||||
size_t dta_at = 0, gfx_at = 0;
|
||||
if (heapdef_open(in, n, &b, &heaps, "the game keeps its own resolution")) {
|
||||
dta_at = heap_size_field(&b, heaps, "DynamicTextureAllocatorMem");
|
||||
gfx_at = heap_size_field(&b, heaps, "Graphics");
|
||||
}
|
||||
@@ -263,6 +419,26 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
int32_t dta2 = dta + (int32_t)extra, gfx2 = gfx + (int32_t)extra;
|
||||
memcpy(b.p + dta_at, &dta2, 4);
|
||||
memcpy(b.p + gfx_at, &gfx2, 4);
|
||||
int32_t th = 0, th2 = 0, gg = 0, gg2 = 0;
|
||||
if (tex) { // TOTK_TEX_HEAP_EXTRA_MB: the world texture heap and its parent grow by the same amount
|
||||
static const char parent[] = "\xe3\x82\xb2\xe3\x83\xbc\xe3\x83\xa0\xe3\x82\xb0\xe3\x83\xa9\xe3\x83\x95"
|
||||
"\xe3\x82\xa3\xe3\x83\x83\xe3\x82\xaf\xe3\x82\xb9"; // ゲームグラフィックス
|
||||
size_t th_at = heap_size_field(&b, heaps, "TextureHeap"), gg_at = heap_size_field(&b, heaps, parent);
|
||||
if (th_at && gg_at) {
|
||||
memcpy(&th, b.p + th_at, 4);
|
||||
memcpy(&gg, b.p + gg_at, 4);
|
||||
th2 = th + (int32_t)tex, gg2 = gg + (int32_t)tex;
|
||||
memcpy(b.p + th_at, &th2, 4);
|
||||
memcpy(b.p + gg_at, &gg2, 4);
|
||||
} else {
|
||||
rt_log("resolution: the HeapDef table has no TextureHeap sizes; TOTK_TEX_HEAP_EXTRA_MB left out");
|
||||
tex = 0;
|
||||
if (!extra) {
|
||||
free(b.p);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
size_t cap = ZSTD_compressBound(b.n);
|
||||
void *z = malloc(cap);
|
||||
size_t zn = z ? ZSTD_compress(z, cap, b.p, b.n, 9) : 0;
|
||||
@@ -273,7 +449,12 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
}
|
||||
*out = z;
|
||||
*out_n = zn;
|
||||
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx, gfx2);
|
||||
if (extra)
|
||||
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx,
|
||||
gfx2);
|
||||
if (tex)
|
||||
rt_log("resolution: HeapDef TextureHeap %d -> %d KiB and its parent %d -> %d KiB (TOTK_TEX_HEAP_EXTRA_MB or a "
|
||||
"mod's HeapDef)", th, th2, gg, gg2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -336,15 +517,20 @@ void res_init(void) {
|
||||
return;
|
||||
}
|
||||
uint8_t *page = rt_patch_page();
|
||||
double k = RT_V143 ? (double)R.ow / BASE_W : 1.0; // 1.4.3's own scaling of the scene with the output width
|
||||
double k = scene_scale();
|
||||
int scene_on = R.scene_set || (!RT_V143 && (R.ow != BASE_W || R.oh != BASE_H));
|
||||
// Handheld, 1.4.3 draws a 1280x720 canvas unless the output patch is on, whatever the output setting (measured: a
|
||||
// 1920x1080 output with the patch off composed the frame at 1280x720 and only the swapchain was 1920x1080). With
|
||||
// the scene patch on the output patch is switched on too, so the canvas is the output size that scene_scale and
|
||||
// the heap size (res_heap_extra_kib) assume.
|
||||
int output_on = R.ow != BASE_W || R.oh != BASE_H || (RT_V143 && scene_on && ov_handheld());
|
||||
float sc[2] = {(float)(R.sw / k), (float)(R.sh / k)};
|
||||
uint64_t out = pack(R.ow, R.oh), none = 0;
|
||||
memcpy(page + 16, sc, sizeof sc);
|
||||
memcpy(page + 32, &out, 8);
|
||||
memcpy(page + 40, &none, 8);
|
||||
memcpy(page + 48, &out, 8);
|
||||
rt_patch_switch(output, R.ow != BASE_W || R.oh != BASE_H);
|
||||
rt_patch_switch(output, output_on);
|
||||
rt_patch_switch(scene, scene_on);
|
||||
R.on = 1;
|
||||
if (R.live) g_fnprof = 1; // until the game sets its render size: res_fn_hit, or res_present gives up
|
||||
@@ -413,6 +599,29 @@ void res_fov_live(void) {
|
||||
rt_log("resolution: field of view %d%% of the game's", pct);
|
||||
}
|
||||
|
||||
// TOTK_DYNAMIC_RES = 0 holds the game's dynamic resolution at 100 % (recomp/patches-v121.txt and patches-v143.txt,
|
||||
// set fixres), so the scene renders at the size render_scale chose. The game picks its tier (100, 90 or 80 %) mostly
|
||||
// from a fixed cost model made for the Switch: one term grows with the object counts, one with (scene width / 1600)
|
||||
// squared. The GPU time it also measures (two NVN report timestamps, a frame apart) only lowers that prediction when the
|
||||
// GPU is slower than the model says, which a PC GPU never is, so the model alone decides. Measured on 1.2.1: a
|
||||
// 2867-wide scene stays at 100 %, one wider than about 2970 does not, and a 4267-wide one (5120x1440 at render_scale
|
||||
// 83) fits no tier and falls back to 80 % every frame, whatever the GPU's load. 1 (the default) leaves the game's own
|
||||
// stepping and the set off. Runs on a settings change (gfx_present.c window_settings_live), like res_fov_live.
|
||||
void res_dynres_live(void) {
|
||||
static int set = -2, shown = -1;
|
||||
if (set == -2) set = rt_patch_set("fixres");
|
||||
int dyn = cfg_bool("TOTK_DYNAMIC_RES");
|
||||
if (dyn == shown) return;
|
||||
shown = dyn;
|
||||
if (set < 0) {
|
||||
if (!dyn) rt_log("resolution: this game version has no fixed-resolution patch; the game keeps its dynamic resolution");
|
||||
return;
|
||||
}
|
||||
rt_patch_switch(set, !dyn);
|
||||
rt_log("resolution: dynamic resolution %s",
|
||||
dyn ? "on: the game's own 100, 90 or 80 %" : "off: the scene stays at 100 %");
|
||||
}
|
||||
|
||||
void res_wanted(unsigned out[2]) {
|
||||
decide();
|
||||
out[0] = R.ow, out[1] = R.oh;
|
||||
@@ -465,7 +674,7 @@ int res_dlss_mode(const char *m) {
|
||||
unsigned w, h;
|
||||
scene_for(k ? k : R.base_k, &w, &h);
|
||||
if (w == R.sw && h == R.sh) return 0;
|
||||
double s = RT_V143 ? (double)R.ow / BASE_W : 1.0;
|
||||
double s = scene_scale();
|
||||
float sc[2] = {(float)(w / s), (float)(h / s)};
|
||||
memcpy((uint8_t *)rt_patch_page() + 16, sc, sizeof sc);
|
||||
R.sw = w, R.sh = h;
|
||||
|
||||
@@ -234,3 +234,102 @@ uint32_t restbl_grow(const char *key, uint32_t entry, uint64_t old, uint64_t new
|
||||
uint64_t e = entry + ((new_size - old) * f + 999) / 1000;
|
||||
return clamp32((e + 31) & ~(uint64_t)31);
|
||||
}
|
||||
|
||||
// ---- packs
|
||||
// SARC: "SARC", u16 header size, u16 byte order mark 0xfeff, u32 archive size, u32 data offset, u16 version, u16 pad;
|
||||
// SFAT: "SFAT", u16 header size, u16 node count, u32 hash key, then per node {u32 name hash, u32 attributes (bit 24: it
|
||||
// has a name, bits 0-23: the name's offset in SFNT / 4), u32 data start, u32 data end} with the data offsets relative
|
||||
// to the archive's data offset; SFNT: "SFNT", u16 header size, u16 pad, then the NUL-terminated names.
|
||||
static uint32_t rd16(const uint8_t *p) { return p[0] | p[1] << 8; }
|
||||
|
||||
int restbl_sarc_each(const uint8_t *p, size_t n, RestblMember *each, void *ctx) {
|
||||
if (n < 0x14 || memcmp(p, "SARC", 4) || rd16(p + 6) != 0xfeff) return 0;
|
||||
size_t head = rd16(p + 4), data = rd32(p + 12);
|
||||
if (head + 12 > n || memcmp(p + head, "SFAT", 4) || data > n) return 0;
|
||||
size_t count = rd16(p + head + 6), nodes = head + rd16(p + head + 4), names = nodes + count * 16;
|
||||
if (names + 8 > n || memcmp(p + names, "SFNT", 4) || rd16(p + names + 4) < 8) return 0;
|
||||
names += rd16(p + names + 4);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const uint8_t *e = p + nodes + i * 16;
|
||||
uint32_t attr = rd32(e + 4), lo = rd32(e + 8), hi = rd32(e + 12);
|
||||
if (!(attr >> 24)) continue; // without a name: nothing the game looks up by path
|
||||
size_t s = names + (size_t)(attr & 0xffffff) * 4;
|
||||
if (s >= n || !memchr(p + s, 0, n - s) || lo > hi || hi > n - data) return 0;
|
||||
each((const char *)p + s, p + data + lo, hi - lo, ctx);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The decompressed size a zstd frame p[0..n) records (Frame_Content_Size), or 4 times n when it records none, as
|
||||
// romfs.c content_size takes a loose file's.
|
||||
static uint64_t frame_content_size(const uint8_t *p, size_t n) {
|
||||
static const uint8_t did_bytes[4] = {0, 1, 2, 4};
|
||||
if (n < 6 || rd32(p) != 0xFD2FB528u) return n;
|
||||
uint8_t fhd = p[4];
|
||||
int single = fhd >> 5 & 1, fcs = fhd >> 6;
|
||||
size_t at = 5 + !single + did_bytes[fhd & 3], len = fcs ? 1u << fcs : single;
|
||||
if (!len || at + len > n) return (uint64_t)n * 4;
|
||||
uint64_t v = 0;
|
||||
for (size_t i = 0; i < len; i++) v |= (uint64_t)p[at + i] << 8 * i;
|
||||
return len == 2 ? v + 256 : v;
|
||||
}
|
||||
|
||||
typedef struct { const char *name; uint64_t size; } Member;
|
||||
typedef struct { Member *m; size_t n, cap; int full; } Members;
|
||||
|
||||
static int ends_with(const char *s, const char *tail) {
|
||||
size_t n = strlen(s), k = strlen(tail);
|
||||
return n >= k && !strcmp(s + n - k, tail);
|
||||
}
|
||||
|
||||
// A member's size as the game unpacks it: a .zs member's content size, else its bytes (a .mc member's compressed ones).
|
||||
static void member_add(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
Members *ms = ctx;
|
||||
if (ms->n == ms->cap) {
|
||||
size_t cap = ms->cap ? ms->cap * 2 : 64;
|
||||
Member *grown = realloc(ms->m, cap * sizeof *grown);
|
||||
if (!grown) {
|
||||
ms->full = 1;
|
||||
return;
|
||||
}
|
||||
ms->m = grown, ms->cap = cap;
|
||||
}
|
||||
ms->m[ms->n++] = (Member){name, ends_with(name, ".zs") ? frame_content_size(data, size) : size};
|
||||
}
|
||||
|
||||
static int by_name(const void *a, const void *b) {
|
||||
return strcmp(((const Member *)a)->name, ((const Member *)b)->name);
|
||||
}
|
||||
|
||||
int restbl_pack_raise(Restbl *t, const Restbl *base, const uint8_t *mod, size_t mod_n, const uint8_t *orig,
|
||||
size_t orig_n, RestblNote *note, void *ctx) {
|
||||
Members m = {0}, o = {0};
|
||||
int raised = -1;
|
||||
if (!restbl_sarc_each(mod, mod_n, member_add, &m) || m.full ||
|
||||
(orig && (!restbl_sarc_each(orig, orig_n, member_add, &o) || o.full)))
|
||||
goto done;
|
||||
if (o.n) qsort(o.m, o.n, sizeof *o.m, by_name);
|
||||
raised = 0;
|
||||
for (size_t i = 0; i < m.n; i++) {
|
||||
char key[1024];
|
||||
uint32_t entry, before = 0, want;
|
||||
restbl_key(m.m[i].name, key, sizeof key);
|
||||
int mc = ends_with(m.m[i].name, ".mc");
|
||||
const Member *was = o.n ? bsearch(&m.m[i], o.m, o.n, sizeof *o.m, by_name) : NULL;
|
||||
if (was) {
|
||||
if (!restbl_get(base, key, &entry)) continue; // the game sizes it without the table (streamed)
|
||||
want = restbl_grow(key, entry, was->size, m.m[i].size, mc);
|
||||
} else { // new to this pack: no copy of the game's to grow from
|
||||
want = restbl_estimate(key, mc ? m.m[i].size * 8 : m.m[i].size);
|
||||
}
|
||||
restbl_get(t, key, &before);
|
||||
if (want && restbl_raise(t, key, want)) {
|
||||
raised++;
|
||||
if (note) note(key, before, want, ctx);
|
||||
}
|
||||
}
|
||||
done:
|
||||
free(m.m);
|
||||
free(o.m);
|
||||
return raised;
|
||||
}
|
||||
@@ -31,3 +31,20 @@ uint32_t restbl_estimate(const char *key, uint64_t size);
|
||||
// times the type's factor, never lowered. scaled: the sizes are compressed ones (a mesh codec .mc file), whose entry
|
||||
// grows in proportion.
|
||||
uint32_t restbl_grow(const char *key, uint32_t entry, uint64_t old, uint64_t new_size, int scaled);
|
||||
|
||||
// The members of the SARC archive p[0..n): each(name, data, size, ctx) for every named member, in the archive's order.
|
||||
// 0 when p is not a SARC archive or a member lies outside it.
|
||||
typedef void RestblMember(const char *name, const uint8_t *data, size_t size, void *ctx);
|
||||
int restbl_sarc_each(const uint8_t *p, size_t n, RestblMember *each, void *ctx);
|
||||
|
||||
// A pack (Pack/**/*.pack.zs, decompressed: a SARC archive) whose members the game loads as resources of their own, each
|
||||
// sized by its own entry. The entries of a mod's pack `mod` raised in t against the game's table `base` and the game's
|
||||
// copy of the pack `orig` (NULL for a pack the game lacks): a member that grew by restbl_grow, a member the game's copy
|
||||
// lacks by restbl_estimate, the others left as they are. note(key, entry before, entry after, ctx) for each raised
|
||||
// entry. The number raised; -1 when either pack is not a SARC archive.
|
||||
typedef void RestblNote(const char *key, uint32_t was, uint32_t now, void *ctx);
|
||||
int restbl_pack_raise(Restbl *t, const Restbl *base, const uint8_t *mod, size_t mod_n, const uint8_t *orig,
|
||||
size_t orig_n, RestblNote *note, void *ctx);
|
||||
|
||||
// restbl_probe.c, part of --probe-mods: the number of failed checks.
|
||||
int restbl_probe(void);
|
||||
@@ -0,0 +1,133 @@
|
||||
// The entries of the members of the mods' packs (restbl.c restbl_pack_raise), on archives and tables of its own; part
|
||||
// of --probe-mods (mods_probe.c). Needs no game files.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "restbl.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL restbl: %s\n", what); } } while (0)
|
||||
|
||||
// A member of a probe archive: its name and size; a .zs member is a zstd frame header recording `content` bytes:
|
||||
// fcs 4, a frame with a window descriptor and a 4-byte field; 2, a single-segment frame with a 2-byte field holding
|
||||
// content - 256.
|
||||
typedef struct { const char *name; size_t size, content; int fcs; } Member;
|
||||
|
||||
static void wr16(uint8_t *p, unsigned v) { p[0] = (uint8_t)v, p[1] = (uint8_t)(v >> 8); }
|
||||
static void wr32(uint8_t *p, uint32_t v) { wr16(p, v & 0xffff), wr16(p + 2, v >> 16); }
|
||||
|
||||
// A SARC archive of the members m[0..n); unnamed: the index of a member without a name, -1 for none. malloc'd.
|
||||
static uint8_t *sarc(const Member *m, int n, int unnamed, size_t *out_n) {
|
||||
size_t names = 0, data = 0;
|
||||
for (int i = 0; i < n; i++) names += (strlen(m[i].name) + 4) & ~(size_t)3, data += (m[i].size + 7) & ~(size_t)7;
|
||||
size_t sfat = 0x14, sfnt = sfat + 12 + 16 * (size_t)n, start = (sfnt + 8 + names + 15) & ~(size_t)15;
|
||||
size_t total = start + data;
|
||||
uint8_t *p = calloc(1, total);
|
||||
if (!p) return NULL;
|
||||
memcpy(p, "SARC", 4), wr16(p + 4, 0x14), wr16(p + 6, 0xfeff);
|
||||
wr32(p + 8, (uint32_t)total), wr32(p + 12, (uint32_t)start);
|
||||
memcpy(p + sfat, "SFAT", 4), wr16(p + sfat + 4, 12), wr16(p + sfat + 6, (unsigned)n), wr32(p + sfat + 8, 0x65);
|
||||
memcpy(p + sfnt, "SFNT", 4), wr16(p + sfnt + 4, 8);
|
||||
size_t name_at = 0, data_at = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint8_t *node = p + sfat + 12 + 16 * (size_t)i, *d = p + start + data_at;
|
||||
wr32(node + 4, i == unnamed ? 0 : 0x01000000u | (uint32_t)(name_at / 4));
|
||||
wr32(node + 8, (uint32_t)data_at), wr32(node + 12, (uint32_t)(data_at + m[i].size));
|
||||
memcpy(p + sfnt + 8 + name_at, m[i].name, strlen(m[i].name));
|
||||
memset(d, 0x5a, m[i].size);
|
||||
if (m[i].fcs == 4) wr32(d, 0xFD2FB528u), d[4] = 0x80, d[5] = 0x58, wr32(d + 6, (uint32_t)m[i].content);
|
||||
if (m[i].fcs == 2) wr32(d, 0xFD2FB528u), d[4] = 0x60, wr16(d + 5, (unsigned)(m[i].content - 256));
|
||||
name_at += (strlen(m[i].name) + 4) & ~(size_t)3;
|
||||
data_at += (m[i].size + 7) & ~(size_t)7;
|
||||
}
|
||||
*out_n = total;
|
||||
return p;
|
||||
}
|
||||
|
||||
// An empty table with the entries keys[i] = sizes[i].
|
||||
static Restbl *table(const char *const *keys, const uint32_t *sizes, int n) {
|
||||
uint8_t head[22] = "RESTBL";
|
||||
wr32(head + 6, 1), wr32(head + 10, 160), wr32(head + 14, 0), wr32(head + 18, 0);
|
||||
Restbl *t = restbl_parse(head, sizeof head);
|
||||
for (int i = 0; t && i < n; i++) restbl_raise(t, keys[i], sizes[i]);
|
||||
return t;
|
||||
}
|
||||
|
||||
static uint32_t get(const Restbl *t, const char *key) {
|
||||
uint32_t v = 0;
|
||||
return restbl_get(t, key, &v) ? v : 0;
|
||||
}
|
||||
|
||||
static int visited;
|
||||
static void count(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
(void)name, (void)data, (void)size, (void)ctx;
|
||||
visited++;
|
||||
}
|
||||
|
||||
static int notes;
|
||||
static void note(const char *key, uint32_t was, uint32_t now, void *ctx) {
|
||||
(void)key, (void)ctx;
|
||||
notes += now > was;
|
||||
}
|
||||
|
||||
int restbl_probe(void) {
|
||||
fails = 0;
|
||||
static const char *const keys[] = {"AI/Grow.root.ainb", "AI/Same.root.ainb", "AI/Shrink.root.ainb",
|
||||
"Component/Shared.bgyml", "Model/Mesh.bfres", "AI/Zs.ainb", "AI/Zs2.ainb"};
|
||||
static const uint32_t sizes[] = {13512, 5000, 6000, 9000, 30000, 4400, 3500};
|
||||
// The game's copy: an .ainb member a mod grows (by 288 bytes, past its entry's slack), one it keeps, one it
|
||||
// shrinks, one the table does not size, a mesh codec member and two compressed ones.
|
||||
static const Member orig[] = {{"AI/Grow.root.ainb", 13088, 0, 0}, {"AI/Same.root.ainb", 4600, 0, 0},
|
||||
{"AI/Shrink.root.ainb", 5600, 0, 0}, {"AI/NotSized.ainb", 100, 0, 0},
|
||||
{"Model/Mesh.bfres.mc", 1000, 0, 0}, {"AI/Zs.ainb.zs", 64, 4000, 4},
|
||||
{"AI/Zs2.ainb.zs", 64, 3900, 2}};
|
||||
// The mod's copy: those, plus members new to the pack (one the game sizes elsewhere, one it lacks, a streamed one).
|
||||
static const Member mod[] = {{"AI/Grow.root.ainb", 13376, 0, 0}, {"AI/Same.root.ainb", 4600, 0, 0},
|
||||
{"AI/Shrink.root.ainb", 5000, 0, 0}, {"AI/NotSized.ainb", 300, 0, 0},
|
||||
{"Component/New/X.bgyml", 1000, 0, 0}, {"Component/Shared.bgyml", 2000, 0, 0},
|
||||
{"Sound/Stream.bwav", 500, 0, 0}, {"Model/Mesh.bfres.mc", 1500, 0, 0},
|
||||
{"AI/Zs.ainb.zs", 64, 4500, 4}, {"AI/Zs2.ainb.zs", 64, 4200, 2}};
|
||||
size_t on = 0, mn = 0;
|
||||
uint8_t *op = sarc(orig, 7, -1, &on), *mp = sarc(mod, 10, 4, &mn); // the mod's New/X has no name: left alone
|
||||
Restbl *base = table(keys, sizes, 7), *t = table(keys, sizes, 7);
|
||||
CHECK(op && mp && base && t, "probe archives and tables");
|
||||
if (op && mp && base && t) {
|
||||
visited = 0;
|
||||
CHECK(restbl_sarc_each(mp, mn, count, NULL) && visited == 9, "SARC members: every named one");
|
||||
int r = restbl_pack_raise(t, base, mp, mn, op, on, note, NULL);
|
||||
CHECK(r == 5 && notes == 5, "five entries raised, each noted");
|
||||
CHECK(get(t, "AI/Grow.root.ainb") == 13824, "a member that grew: its entry plus the growth, 32-byte steps");
|
||||
CHECK(get(t, "AI/Same.root.ainb") == 5000 && get(t, "AI/Shrink.root.ainb") == 6000,
|
||||
"members that kept or lost bytes keep their entries");
|
||||
CHECK(!get(t, "AI/NotSized.ainb"), "a member the game sizes without the table gets no entry");
|
||||
CHECK(!get(t, "Component/New/X.bgyml"), "a member without a name is not looked up");
|
||||
CHECK(get(t, "Component/Shared.bgyml") == 16384, "a member new to the pack: the estimate of its type");
|
||||
CHECK(!get(t, "Sound/Stream.bwav"), "a streamed member new to the pack gets no entry");
|
||||
CHECK(get(t, "Model/Mesh.bfres") == 45000, "a mesh codec member's entry grows in proportion");
|
||||
CHECK(get(t, "AI/Zs.ainb") == 4928 && get(t, "AI/Zs2.ainb") == 3808,
|
||||
"a compressed member: the growth of the size its frame records (4- and 2-byte fields)");
|
||||
notes = 0;
|
||||
CHECK(restbl_pack_raise(t, base, mp, mn, op, on, note, NULL) == 0 && !notes, "a second pass raises nothing");
|
||||
Restbl *shipped = table(keys, (const uint32_t[]){20000, 5000, 6000, 9000, 30000, 4400, 3500}, 7);
|
||||
CHECK(shipped && restbl_pack_raise(shipped, base, mp, mn, op, on, NULL, NULL) == 4 &&
|
||||
get(shipped, "AI/Grow.root.ainb") == 20000,
|
||||
"an entry a mod's own table raised further stays");
|
||||
restbl_free(shipped);
|
||||
Restbl *added = table(keys, sizes, 7);
|
||||
CHECK(added && restbl_pack_raise(added, base, mp, mn, NULL, 0, NULL, NULL) == 7 &&
|
||||
get(added, "AI/Grow.root.ainb") == 24576 && get(added, "AI/NotSized.ainb") == 12288 &&
|
||||
get(added, "AI/Zs2.ainb") == 16384 && get(added, "Model/Mesh.bfres") == 30000,
|
||||
"a pack the game lacks: every member gets the estimate of its type, an entry above it stays");
|
||||
restbl_free(added);
|
||||
CHECK(restbl_pack_raise(t, base, (const uint8_t *)"SARC", 4, NULL, 0, NULL, NULL) == -1 &&
|
||||
restbl_pack_raise(t, base, mp, mn, mp, 0x20, NULL, NULL) == -1,
|
||||
"an archive that is not one, or whose game copy is cut short, raises nothing");
|
||||
}
|
||||
free(op);
|
||||
free(mp);
|
||||
restbl_free(base);
|
||||
restbl_free(t);
|
||||
printf("restbl: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails;
|
||||
}
|
||||
+230
-40
@@ -24,8 +24,8 @@
|
||||
// matches a RomFS path only without case is served under the RomFS spelling.
|
||||
//
|
||||
// The resource size table: with a player mod enabled, the game's table is rebuilt at start with the tables the mods
|
||||
// ship merged in and the entries of the mods' files raised to what they need (restbl_overlay), and served from
|
||||
// <profile>/mods/ in place of the game's or a mod's table.
|
||||
// ship merged in and the entries of the mods' files raised to what they need (restbl_overlay), the members of their
|
||||
// packs too (restbl_packs), and served from <profile>/mods/ in place of the game's or a mod's table.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -533,6 +533,22 @@ static void added_init(void) {
|
||||
rt_log("RomFS overlay: %zu files added to the RomFS", nadded);
|
||||
}
|
||||
|
||||
// RomFS file `rel` as the image holds it, whatever overlays replace it. malloc'd; NULL when it is missing, unreadable
|
||||
// or 16 MiB or more (logged with `keeps`, what the game does without it).
|
||||
static void *read_original(const char *rel, size_t *n, const char *keeps) {
|
||||
uint64_t i = lower(rel);
|
||||
if (i >= file_count || strcmp(entry_name(entry(i)), rel)) return NULL;
|
||||
uint64_t size = entry(i).size;
|
||||
void *in = size && size < (16u << 20) ? malloc((size_t)size) : NULL;
|
||||
if (!in || read_at(image, in, (uint32_t)size, entry(i).offset) != (int64_t)size) {
|
||||
free(in);
|
||||
rt_log("RomFS: cannot read %s; %s", rel, keeps);
|
||||
return NULL;
|
||||
}
|
||||
*n = (size_t)size;
|
||||
return in;
|
||||
}
|
||||
|
||||
// RomFS file `rel` as the game would read it: from an overlay that replaces it (*base), else from the image. malloc'd;
|
||||
// NULL when it is missing, unreadable or 16 MiB or more (logged with `keeps`, what the game does without it).
|
||||
static void *read_served(const char *rel, Overlay **base, size_t *n, const char *keeps) {
|
||||
@@ -621,10 +637,122 @@ static Restbl *restbl_unpack(const void *z, size_t n) {
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---- the members of the mods' packs (restbl_pack_raise). The game loads each member of a pack (Pack/**/*.pack.zs, a
|
||||
// SARC archive compressed with a dictionary of Pack/ZsDic.pack.zs) as a resource of its own, in a heap sized by the
|
||||
// member's own entry, so a mod that changes a member and ships no table for it (or an older one) leaves the game's
|
||||
// entry. A member that outgrew its entry's slack loads, then its heap runs out: TotK 1.4.3 writes through a null
|
||||
// pointer at main+0x21794c when the 0x68-byte object an .ainb member builds after its file no longer fits.
|
||||
#define PACK_MAX (256u << 20) // the largest pack taken, decompressed (AI.Global.Product.140.pack: 24 MiB)
|
||||
typedef struct { ZSTD_DDict *dict[4]; unsigned id[4]; int n; ZSTD_DCtx *dctx; } Packs;
|
||||
|
||||
// The bytes of zstd frame z[0..n), with the dictionary of ps its header names; malloc'd. NULL when it is not such a
|
||||
// frame with a recorded size of at most PACK_MAX.
|
||||
static uint8_t *pack_unpack(Packs *ps, const void *z, size_t n, size_t *out_n) {
|
||||
unsigned long long raw = ZSTD_getFrameContentSize(z, n);
|
||||
unsigned id = ZSTD_getDictID_fromFrame(z, n);
|
||||
ZSTD_DDict *dd = NULL;
|
||||
for (int i = 0; i < ps->n; i++)
|
||||
if (ps->id[i] == id) dd = ps->dict[i];
|
||||
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > PACK_MAX || (id && !dd) || !ps->dctx)
|
||||
return NULL;
|
||||
uint8_t *d = malloc(raw ? (size_t)raw : 1);
|
||||
size_t got = !d ? 0 : dd ? ZSTD_decompress_usingDDict(ps->dctx, d, (size_t)raw, z, n, dd)
|
||||
: ZSTD_decompressDCtx(ps->dctx, d, (size_t)raw, z, n);
|
||||
if (!d || ZSTD_isError(got) || got != raw) {
|
||||
free(d);
|
||||
return NULL;
|
||||
}
|
||||
*out_n = (size_t)raw;
|
||||
return d;
|
||||
}
|
||||
|
||||
static void pack_dict_add(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
Packs *ps = ctx;
|
||||
ZSTD_DDict *dd = ps->n < 4 && ends_with(name, ".zsdic") ? ZSTD_createDDict(data, size) : NULL; // a copy
|
||||
if (!dd) return;
|
||||
ps->dict[ps->n] = dd;
|
||||
ps->id[ps->n++] = ZSTD_getDictID_fromDDict(dd);
|
||||
}
|
||||
|
||||
// The dictionaries of Pack/ZsDic.pack.zs as served (zs.zsdic, pack.zsdic, bcett.byml.zsdic): a SARC compressed
|
||||
// without one. 0 when it holds none.
|
||||
static int packs_open(Packs *ps) {
|
||||
Overlay *base;
|
||||
size_t n = 0, rn = 0;
|
||||
ps->dctx = ZSTD_createDCtx();
|
||||
void *z = read_served("Pack/ZsDic.pack.zs", &base, &n, "the members of the mods' packs keep their entries");
|
||||
uint8_t *raw = z ? pack_unpack(ps, z, n, &rn) : NULL;
|
||||
int ok = raw && restbl_sarc_each(raw, rn, pack_dict_add, ps) && ps->n;
|
||||
free(z);
|
||||
free(raw);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void packs_close(Packs *ps) {
|
||||
for (int i = 0; i < ps->n; i++) ZSTD_freeDDict(ps->dict[i]);
|
||||
ZSTD_freeDCtx(ps->dctx);
|
||||
}
|
||||
|
||||
// Overlay o (a pack) and the game's copy of it, decompressed; NULL when o cannot be read or unpacked. *orig stays NULL
|
||||
// for a pack the game lacks or whose copy cannot be read.
|
||||
static uint8_t *pack_pair(Packs *ps, const Overlay *o, size_t *n, uint8_t **orig, size_t *orig_n) {
|
||||
uint64_t zn = 0;
|
||||
void *z = plat_file_read(o->path, &zn);
|
||||
uint8_t *mod = z ? pack_unpack(ps, z, (size_t)zn, n) : NULL;
|
||||
free(z);
|
||||
*orig = NULL;
|
||||
if (!mod || o->index == ADDED) return mod;
|
||||
Entry e = entry(o->index);
|
||||
void *g = e.size < READ_CHUNK ? malloc(e.size ? (size_t)e.size : 1) : NULL;
|
||||
if (g && read_at(image, g, (uint32_t)e.size, e.offset) == (int64_t)e.size)
|
||||
*orig = pack_unpack(ps, g, (size_t)e.size, orig_n);
|
||||
free(g);
|
||||
return mod;
|
||||
}
|
||||
|
||||
typedef struct { const char *pack, *mod; int lines; } PackNote;
|
||||
|
||||
static void pack_note(const char *key, uint32_t was, uint32_t now, void *ctx) {
|
||||
PackNote *pn = ctx;
|
||||
if (pn->lines++ < 16)
|
||||
rt_log("RomFS: resource size table: %s in %s (%s): entry %u -> %u", key, pn->pack, pn->mod, was, now);
|
||||
}
|
||||
|
||||
// The entries of the members of every pack a mod serves, raised in out against the game's table base; the number
|
||||
// raised, *packs the number of packs with one.
|
||||
static int restbl_packs(Restbl *out, const Restbl *base, int *packs, int *lines) {
|
||||
Packs ps = {0};
|
||||
int raised = 0, opened = 0, bad = 0;
|
||||
*packs = 0;
|
||||
for (size_t k = 0; k < noverlay; k++) {
|
||||
const Overlay *o = &overlays[k];
|
||||
if ((!o->mod && o->prio != 1) || !ends_with(o->name, ".pack.zs")) continue;
|
||||
if (!opened++ && !packs_open(&ps)) {
|
||||
rt_log("RomFS: no zstd dictionary in Pack/ZsDic.pack.zs; the members of mods' packs keep their entries");
|
||||
break;
|
||||
}
|
||||
// A pack whose game copy cannot be read counts as new: each member gets an estimate, which only raises entries.
|
||||
size_t n = 0, orig_n = 0;
|
||||
uint8_t *orig, *mod = pack_pair(&ps, o, &n, &orig, &orig_n);
|
||||
PackNote pn = {o->name, owner_name(o), *lines};
|
||||
int r = mod ? restbl_pack_raise(out, base, mod, n, orig, orig_n, pack_note, &pn) : -1;
|
||||
if (r < 0 && bad++ < 4) rt_log("RomFS: %s (%s) is not a pack this build reads; its members keep their entries",
|
||||
o->name, owner_name(o));
|
||||
*lines = pn.lines;
|
||||
raised += r > 0 ? r : 0;
|
||||
*packs += r > 0;
|
||||
free(mod);
|
||||
free(orig);
|
||||
}
|
||||
packs_close(&ps);
|
||||
return raised;
|
||||
}
|
||||
|
||||
// With a player mod (or the TOTK_ROMFS_MODS manifest) serving files: the game's table with every shipped table of this
|
||||
// version merged in (an entry larger than the game's wins), the entries a mod's table for another version gives for
|
||||
// that mod's files, and each served mod file's entry raised to what its size needs (restbl_grow for a file the game
|
||||
// has, restbl_estimate for an added one). Never lowers the game's entries.
|
||||
// has, restbl_estimate for an added one), the members of the packs mods serve too (restbl_packs). Never lowers the
|
||||
// game's entries.
|
||||
static void restbl_overlay(void) {
|
||||
int player = 0;
|
||||
for (size_t k = 0; k < noverlay; k++) player |= overlays[k].mod || overlays[k].prio == 1;
|
||||
@@ -691,8 +819,10 @@ static void restbl_overlay(void) {
|
||||
}
|
||||
raised += want && restbl_raise(out, key, want);
|
||||
}
|
||||
int packs = 0, lines = 0, members = restbl_packs(out, base, &packs, &lines);
|
||||
if (lines > 16) rt_log("RomFS: resource size table: %d more entries of pack members raised", lines - 16);
|
||||
restbl_free(base);
|
||||
if (!merged && !raised && !served) {
|
||||
if (!merged && !raised && !members && !served) {
|
||||
restbl_free(out);
|
||||
rt_log("RomFS: resource size table: the mods' files fit the game's entries (%.0f ms)", (now_s() - t0) * 1000);
|
||||
return;
|
||||
@@ -711,20 +841,59 @@ static void restbl_overlay(void) {
|
||||
}
|
||||
if (serve_generated(restbl_rel, served, z, zn, "mods", "merged resource sizes", keeps))
|
||||
rt_log("RomFS: resource size table: %d tables merged (%d entries), %d entries raised for mod files (%d "
|
||||
"added files estimated), %.0f ms", tables, merged, raised, estimated, (now_s() - t0) * 1000);
|
||||
"added files estimated), %d for members of %d mod packs, %.0f ms", tables, merged, raised, estimated,
|
||||
members, packs, (now_s() - t0) * 1000);
|
||||
}
|
||||
|
||||
// The HeapDef table a bigger game resolution needs (resolution.c): its texture heap and that heap's parent grow. The
|
||||
// base is an overlay that replaces the table (a mod's heap sizes stay), else the RomFS original; the grown table is
|
||||
// written to <profile>/resolution/ and served in the base's place.
|
||||
// The HeapDef table a bigger game resolution or TOTK_TEX_HEAP_EXTRA_MB needs (resolution.c): its texture heaps and
|
||||
// their parents grow. The base is an overlay that replaces the table (a mod's heap sizes stay), else the RomFS
|
||||
// original; the grown table is written to <profile>/resolution/ and served in the base's place. A served mod table
|
||||
// whose own TextureHeap is grown (an HD texture pack's, resolution.c res_heapdef_tex_grown) is not the base: its
|
||||
// growth is already in the amount (resolution.c mod_tex_heap_kib). In its place the table of the highest switched-on
|
||||
// mod below it that changes other heaps only grows by the amount (plain_mod_heapdef: with the pack above The Second
|
||||
// Quest, the default order for HDPacks.com's "4kTextures", the game's own sizes for RSDB and UI were too small for
|
||||
// that mod's files and the game crashed loading a save), else the RomFS original; either is served in the pack's
|
||||
// place. A mod table that changes other heaps only and is served itself stays the base.
|
||||
#define HEAPDEF "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
|
||||
|
||||
// The HeapDef table of the highest switched-on mod that changes other heaps only (malloc'd, *n, *who its name); NULL
|
||||
// when no mod has one. Mods above the served table ship none, so the first found is the highest below it.
|
||||
static void *plain_mod_heapdef(size_t *n, const char **who) {
|
||||
for (int i = 0; i < mods_count(); i++) {
|
||||
const ModInfo *m = mods_at(i);
|
||||
char path[2300];
|
||||
if (!m->enabled || !m->romfs[0] ||
|
||||
snprintf(path, sizeof path, "%s/%s", m->romfs, HEAPDEF) >= (int)sizeof path)
|
||||
continue;
|
||||
uint64_t fn = 0;
|
||||
uint8_t *f = plat_file_read(path, &fn);
|
||||
if (!f) continue;
|
||||
if (fn < (16u << 20) && res_heapdef_plain(f, (size_t)fn)) {
|
||||
*n = (size_t)fn;
|
||||
*who = m->name;
|
||||
return f;
|
||||
}
|
||||
free(f);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
static void heapdef_overlay(void) {
|
||||
if (!res_heap_extra_kib()) return;
|
||||
if (!res_heap_extra_kib() && !res_tex_heap_extra_bytes()) return;
|
||||
static const char keeps[] = "the game keeps its own resolution";
|
||||
Overlay *base;
|
||||
size_t n;
|
||||
void *in = read_served(HEAPDEF, &base, &n, keeps), *out = NULL;
|
||||
size_t out_n = 0;
|
||||
if (in && base && base->mod && res_heapdef_tex_grown(in, n)) {
|
||||
free(in);
|
||||
const char *who = NULL;
|
||||
if ((in = plain_mod_heapdef(&n, &who)))
|
||||
rt_log("RomFS: %s: the HeapDef of %s with the texture heap growth in place of the mod's table",
|
||||
mods_at(base->mod - 1)->name, who);
|
||||
else if ((in = read_original(HEAPDEF, &n, keeps)))
|
||||
rt_log("RomFS: %s: the game's own HeapDef with the mod's texture heap growth in place of the mod's table",
|
||||
mods_at(base->mod - 1)->name);
|
||||
}
|
||||
int ok = in && res_heapdef_build(in, n, &out, &out_n);
|
||||
free(in);
|
||||
if (ok && serve_generated(HEAPDEF, base, out, out_n, "resolution", "grown heaps", keeps)) res_heapdef_served();
|
||||
@@ -750,12 +919,11 @@ static void camera_overlay(void) {
|
||||
// The button prompts' folders (input_prompts_folder) and whether each had a manifest next to romfs-index.bin at start;
|
||||
// the one the settings asked for at start, the one overlay_init loaded (NULL: the game's own prompts) and why the first
|
||||
// did not load (ROMFS_PROMPTS_*). The menu compares them with the settings in effect (ui/menu.cpp prompts_note).
|
||||
// romfs-patches-pc keeps that name, but at a game resolution of another shape than 16:9 its files come from pc_dir,
|
||||
// romfs-patches-pc-<w>x<h> (overlay_init).
|
||||
static struct { const char *name; int ready; } prompts_folders[3];
|
||||
// Each folder keeps its name, but at a game resolution of another shape than 16:9 its files come from dir,
|
||||
// <name>-<w>x<h> (overlay_init), and ready is that one's.
|
||||
static struct { const char *name; int ready; char dir[64]; } prompts_folders[3];
|
||||
static const char *prompts_wanted, *prompts_loaded;
|
||||
static int prompts_off;
|
||||
static char pc_dir[64];
|
||||
|
||||
const char *romfs_prompts(void) { return prompts_loaded; }
|
||||
|
||||
@@ -795,15 +963,15 @@ static void overlay_init(void) {
|
||||
// the UI laid out for that shape (make_romfs_patches.py --aspect), when the game version has the camera aspect patch
|
||||
// the layout goes with (res_wants_wide_overlay).
|
||||
// With prompts = xbox, the Xbox prompts' folder for the pad layout in its place (input_prompts_folder): the same
|
||||
// patches with Xbox button icons (make_romfs_patches.py --prompts). The game reads the archives once at boot, so
|
||||
// the prompts stay until restart even though the layout itself changes live. Each folder replaces romfs-patches as
|
||||
// a whole and the setup builds no folder of another shape with Xbox prompts, so a game resolution that is not 16:9
|
||||
// keeps the Switch prompts. So does a RomFS mod (the TOTK_ROMFS_MODS manifest or a player mod) that replaces one of
|
||||
// the prompts' fonts: romfs-patches loads in their place, so the mod's file is not served beside prompts files made
|
||||
// for the game's own. A mod's resource size table is no clash: restbl_overlay merges it with the prompts' one.
|
||||
// prompts = pc takes romfs-patches-pc whatever the pad layout, and at a game resolution of another shape
|
||||
// romfs-patches-pc-<w>x<h> (make_romfs_patches.py --prompts pc --aspect): it carries that shape's UI layout too, so
|
||||
// it replaces romfs-patches-<w>x<h> as romfs-patches-pc replaces romfs-patches.
|
||||
// patches with Xbox button icons (make_romfs_patches.py --prompts); prompts = pc takes romfs-patches-pc whatever
|
||||
// the pad layout. The game reads the archives once at boot, so the prompts stay until restart even though the
|
||||
// layout itself changes live. Each folder replaces romfs-patches as a whole. At a game resolution of another shape
|
||||
// the prompts come from <folder>-<w>x<h> (make_romfs_patches.py --prompts <style> --aspect): it carries that
|
||||
// shape's UI layout too, so it replaces romfs-patches-<w>x<h> as the folder replaces romfs-patches. Without it the
|
||||
// Switch prompts stay with the shape's layout. So they do when a RomFS mod (the TOTK_ROMFS_MODS manifest or a
|
||||
// player mod) replaces one of the prompts' fonts: romfs-patches loads in their place, so the mod's file is not
|
||||
// served beside prompts files made for the game's own. A mod's resource size table is no clash: restbl_overlay
|
||||
// merges it with the prompts' one.
|
||||
char def[2100], path[2100];
|
||||
const char *patches = cfg_get("TOTK_ROMFS_PATCHES"), *mods = cfg_get("TOTK_ROMFS_MODS");
|
||||
int wide = 0;
|
||||
@@ -822,45 +990,47 @@ static void overlay_init(void) {
|
||||
static const char *const layouts[2] = {"nintendo", "xbox"}; // one of each Xbox folder, then the PC one
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const char *name = i < 2 ? input_prompts_folder(PROMPTS_XBOX, layouts[i]) : input_prompts_folder(PROMPTS_PC, 0);
|
||||
const char *dir = name;
|
||||
if (i == 2) {
|
||||
if (wide) snprintf(pc_dir, sizeof pc_dir, "%s-%ux%u", name, out[0], out[1]);
|
||||
else snprintf(pc_dir, sizeof pc_dir, "%s", name);
|
||||
dir = pc_dir;
|
||||
}
|
||||
char *dir = prompts_folders[i].dir;
|
||||
if (wide) snprintf(dir, sizeof prompts_folders[i].dir, "%s-%ux%u", name, out[0], out[1]);
|
||||
else snprintf(dir, sizeof prompts_folders[i].dir, "%s", name);
|
||||
prompts_folders[i].name = name;
|
||||
prompts_folders[i].ready =
|
||||
snprintf(path, sizeof path, "%s/%s/manifest.tsv", index_dir, dir) < (int)sizeof path && plat_file_exists(path);
|
||||
}
|
||||
const char *layout = cfg_value("TOTK_PAD_LAYOUT");
|
||||
int pick = input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout), shaped = wide && pick == PROMPTS_PC;
|
||||
int pick = input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout);
|
||||
const char *style = pick == PROMPTS_PC ? "keyboard and mouse" : "Xbox";
|
||||
prompts_wanted = input_prompts_folder(pick, layout);
|
||||
const char *wanted_dir = prompts_wanted; // the folder its files come from
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (prompts_wanted && !strcmp(prompts_folders[i].name, prompts_wanted)) wanted_dir = prompts_folders[i].dir;
|
||||
prompts_off = !prompts_wanted ? ROMFS_PROMPTS_OK
|
||||
: wide && !shaped ? ROMFS_PROMPTS_SHAPE
|
||||
: patches && !wide ? ROMFS_PROMPTS_PATCHES
|
||||
: !romfs_prompts_ready(prompts_wanted) ? (shaped ? ROMFS_PROMPTS_SHAPE : ROMFS_PROMPTS_MISSING)
|
||||
: !romfs_prompts_ready(prompts_wanted) ? (wide ? ROMFS_PROMPTS_SHAPE : ROMFS_PROMPTS_MISSING)
|
||||
: ROMFS_PROMPTS_OK;
|
||||
restbl_find();
|
||||
size_t first = noverlay;
|
||||
if (prompts_wanted && prompts_off == ROMFS_PROMPTS_OK) {
|
||||
// fits: its manifest path did
|
||||
snprintf(path, sizeof path, "%s/%s", index_dir, shaped ? pc_dir : prompts_wanted);
|
||||
snprintf(path, sizeof path, "%s/%s", index_dir, wanted_dir);
|
||||
if (overlay_load(path, 1, 0)) {
|
||||
prompts_loaded = prompts_wanted;
|
||||
rt_log("RomFS overlay: %s button prompts (%s)", style, path);
|
||||
if (shaped) patches = NULL; // in place of romfs-patches-<w>x<h>, whose UI layout it carries
|
||||
if (wide) patches = NULL; // in place of romfs-patches-<w>x<h>, whose UI layout it carries
|
||||
} else {
|
||||
prompts_off = ROMFS_PROMPTS_FAILED;
|
||||
rt_log("RomFS overlay: %s failed its check (run the setup again); romfs-patches loads instead", path);
|
||||
}
|
||||
} else if (prompts_wanted) {
|
||||
rt_log("RomFS overlay: prompts = %s, but %s; Switch prompts stay", pick == PROMPTS_PC ? "pc" : "xbox",
|
||||
prompts_off == ROMFS_PROMPTS_SHAPE ? (shaped ? "the game resolution has no romfs-patches-pc-<w>x<h> "
|
||||
"(tools/make_romfs_patches.py --prompts pc --aspect)"
|
||||
: "the game resolution takes its own UI layout")
|
||||
: prompts_off == ROMFS_PROMPTS_PATCHES ? "TOTK_ROMFS_PATCHES is set"
|
||||
: "their folder has no manifest.tsv (run the setup again)");
|
||||
char why[256];
|
||||
if (prompts_off == ROMFS_PROMPTS_SHAPE)
|
||||
snprintf(why, sizeof why, "the game resolution has no %s (tools/make_romfs_patches.py --prompts %s "
|
||||
"--aspect %ux%u)", wanted_dir, prompts_wanted + strlen("romfs-patches-"), out[0], out[1]);
|
||||
else
|
||||
snprintf(why, sizeof why, "%s", prompts_off == ROMFS_PROMPTS_PATCHES
|
||||
? "TOTK_ROMFS_PATCHES is set"
|
||||
: "their folder has no manifest.tsv (run the setup again)");
|
||||
rt_log("RomFS overlay: prompts = %s, but %s; Switch prompts stay", pick == PROMPTS_PC ? "pc" : "xbox", why);
|
||||
}
|
||||
size_t prompts_end = noverlay;
|
||||
char plain[2100]; // romfs-patches, when it has a manifest
|
||||
@@ -879,7 +1049,7 @@ static void overlay_init(void) {
|
||||
overlay_drop(first, prompts_end);
|
||||
prompts_loaded = NULL;
|
||||
prompts_off = ROMFS_PROMPTS_MODS;
|
||||
if (shaped) overlay_load(def, 0, 0);
|
||||
if (wide) overlay_load(def, 0, 0);
|
||||
else if (plain_ok) overlay_load(plain, 0, 0);
|
||||
}
|
||||
restbl_collect();
|
||||
@@ -911,6 +1081,26 @@ static void index_path(char *out, size_t cap) {
|
||||
strcpy(out + k, "romfs-index.bin");
|
||||
}
|
||||
|
||||
// 1 when <prefix>-<w>x<h> has a manifest next to romfs-index.bin. Works before fs_init: index_dir is not set that early
|
||||
// (cfg_init, res_init and resolution.c decide run first), so the index path is worked out again here.
|
||||
static int shaped_folder_ready(const char *prefix, unsigned w, unsigned h) {
|
||||
char dir[2100], path[2200];
|
||||
index_path(dir, sizeof dir);
|
||||
char *sep = plat_last_sep(dir);
|
||||
if (sep) *sep = 0;
|
||||
else strcpy(dir, ".");
|
||||
return snprintf(path, sizeof path, "%s/%s-%ux%u/manifest.tsv", dir, prefix, w, h) < (int)sizeof path &&
|
||||
plat_file_exists(path);
|
||||
}
|
||||
|
||||
// 1 when the game resolution w x h has the UI layout of its shape: romfs-patches-<w>x<h>, the folder overlay_init
|
||||
// looks for.
|
||||
int romfs_shaped_ready(unsigned w, unsigned h) { return shaped_folder_ready("romfs-patches", w, h); }
|
||||
|
||||
// 1 when it has a prompts' folder (input_prompts_folder) with that layout too: <folder>-<w>x<h>, which overlay_init
|
||||
// loads in its place with those prompts.
|
||||
int romfs_shaped_prompts_ready(const char *folder, unsigned w, unsigned h) { return shaped_folder_ready(folder, w, h); }
|
||||
|
||||
static void load_index(void) {
|
||||
char path[2100];
|
||||
index_path(path, sizeof path);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#version 450
|
||||
// Block-linear to linear copy of one texture level and layer on the GPU (vk_deswizzle.c), the same layout as
|
||||
// tex_deswizzle.c nvn_bl_offset: GOB = 64 bytes x 8 rows; a block is 1 GOB wide, 2^th GOBs high and 2^td deep, its
|
||||
// GOBs ordered y first, then z; blocks fill the level x first, then y, then z.
|
||||
// One invocation per 16-byte segment of the block-linear storage, in storage order, so the reads (from the pool's
|
||||
// imported buffer: guest memory, over PCIe on a discrete GPU) are contiguous; the writes go to a device-local buffer
|
||||
// with rows of pitch16 segments. Segments outside the level's rows, slices or row width are padding and skipped.
|
||||
layout(local_size_x = 64) in;
|
||||
layout(std430, set = 0, binding = 0) readonly buffer Src { uvec4 src[]; };
|
||||
layout(std430, set = 0, binding = 1) writeonly buffer Dst { uvec4 dst[]; };
|
||||
layout(push_constant) uniform Push {
|
||||
uint src_base; // first segment of the level and layer in the src binding
|
||||
uint dst_base; // first segment of its linear copy in the dst binding
|
||||
uint segs; // segments to visit: whole blocks covering the level
|
||||
uint pitch16; // segments per linear row (row bytes rounded up to 16)
|
||||
uint rows, slices, th, td;
|
||||
uint gobs_x, blocks_y;
|
||||
uint nx; // workgroups along x (the dispatch is 2D when one dimension is not enough)
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
uint s = (gl_WorkGroupID.y * pc.nx + gl_WorkGroupID.x) * 64u + gl_LocalInvocationID.x;
|
||||
if (s >= pc.segs) return;
|
||||
uint bh = 1u << pc.th, bd = 1u << pc.td;
|
||||
uint per_block = 32u << (pc.th + pc.td); // 512-byte GOBs of 32 segments each
|
||||
uint block = s / per_block, inb = s % per_block;
|
||||
uint gob = inb >> 5u, ing = inb & 31u;
|
||||
uint per_layer = pc.gobs_x * pc.blocks_y;
|
||||
uint bz = block / per_layer, rem = block % per_layer;
|
||||
uint by = rem / pc.gobs_x, bx = rem % pc.gobs_x;
|
||||
// Inside a GOB, segment bits: 4 = x % 64 / 32, 3..2 = y % 8 / 2, 1 = x % 32 / 16, 0 = y % 2.
|
||||
uint xseg = bx * 4u + ((ing >> 4u) & 1u) * 2u + ((ing >> 1u) & 1u);
|
||||
uint y = by * 8u * bh + (gob % bh) * 8u + ((ing >> 2u) & 3u) * 2u + (ing & 1u);
|
||||
uint z = bz * bd + gob / bh;
|
||||
if (xseg >= pc.pitch16 || y >= pc.rows || z >= pc.slices) return;
|
||||
dst[pc.dst_base + (z * pc.rows + y) * pc.pitch16 + xseg] = src[pc.src_base + s];
|
||||
}
|
||||
+14
-9
@@ -205,19 +205,21 @@ SN T FN(slow)(Cpu *c, int op, T a, T b, T d) {
|
||||
// it to fcmp one, a single ucomiss + jne for scalars and one vcmpps for vectors).
|
||||
#define FOK(r) ((__builtin_elementwise_abs(r) < FN(fv)(MINN)) | (__builtin_elementwise_abs(r) > FN(fv)(MINN)))
|
||||
SN T FN(redo)(Cpu *c, int op, T a, T b, T d, T r) {
|
||||
if (c->fpcr & FPCR_FZ) return FN(slow)(c, op, a, b, d);
|
||||
if (c->fpcr & (FPCR_FZ | FPCR_RM)) return FN(slow)(c, op, a, b, d);
|
||||
return FBAD(op, r) ? FN(fix)(c, op, a, b, d) : r;
|
||||
}
|
||||
|
||||
// Scalar FP op (constant op folds the selection). RMode = nearest runs on the host, with or without FZ.
|
||||
// Scalar FP op (constant op folds the selection). RMode = nearest runs on the host, with or without FZ. A directed
|
||||
// RMode is rare, so it shares the result check's branch: the host result, computed in round-to-nearest, is then
|
||||
// thrown away and redo recomputes in the FPCR mode. Min and max do not round, so RMode does not matter to them.
|
||||
SI T FN(op)(Cpu *c, int op, T a, T b, T d) {
|
||||
if (simd_directed(c)) return FN(slow)(c, op, a, b, d);
|
||||
if (FMINMAX(op)) {
|
||||
if (c->fpcr & FPCR_FZ) a = FN(flush)(a), b = FN(flush)(b);
|
||||
return FN(minmax)(c, op, a, b);
|
||||
}
|
||||
T r = FCALC(op, a, b, d, K, FMA_S);
|
||||
if (__builtin_expect(op == FOP_RSQRTS ? FREDO(op, r) : !FOK(r), 0)) r = FN(redo)(c, op, a, b, d, r);
|
||||
if (__builtin_expect((op == FOP_RSQRTS ? FREDO(op, r) : !FOK(r)) | simd_directed(c), 0))
|
||||
r = FN(redo)(c, op, a, b, d, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -237,9 +239,9 @@ SI VT FN(vflush)(VT a) {
|
||||
return (VT)FN(vsel)(den, u & SGN, u);
|
||||
}
|
||||
|
||||
// Vector FP op: native lanes; any lane FREDO flags (or FPCR.RMode) reruns all lanes through the scalar path.
|
||||
// Vector FP op: native lanes; any lane FREDO flags (or a directed FPCR.RMode, checked in the same branch) reruns all
|
||||
// lanes through the scalar path. Min and max ignore RMode, as in FN(op).
|
||||
SI VT FN(vop)(Cpu *c, int op, VT a, VT b, VT d) {
|
||||
if (simd_directed(c)) return FN(vslow)(c, op, a, b, d);
|
||||
VT r;
|
||||
if (FMINMAX(op)) {
|
||||
if (c->fpcr & FPCR_FZ) a = FN(vflush)(a), b = FN(vflush)(b);
|
||||
@@ -252,7 +254,8 @@ SI VT FN(vop)(Cpu *c, int op, VT a, VT b, VT d) {
|
||||
return r;
|
||||
}
|
||||
r = FCALC(op, a, b, d, VK, FMA_V);
|
||||
if (__builtin_expect(op == FOP_RSQRTS ? __builtin_reduce_or(FREDO(op, r)) != 0 : __builtin_reduce_and(FOK(r)) == 0, 0))
|
||||
if (__builtin_expect((op == FOP_RSQRTS ? __builtin_reduce_or(FREDO(op, r)) != 0 : __builtin_reduce_and(FOK(r)) == 0)
|
||||
| simd_directed(c), 0))
|
||||
return FN(vslow)(c, op, a, b, d);
|
||||
return r;
|
||||
}
|
||||
@@ -260,13 +263,15 @@ SI VT FN(vop)(Cpu *c, int op, VT a, VT b, VT d) {
|
||||
SI VT FN(vfz)(Cpu *c, VT a) { return c->fpcr & FPCR_FZ ? FN(vflush)(a) : a; }
|
||||
|
||||
// ---- compares
|
||||
// FPCR.FZ needs no flush here: rt_fpcr_sync (runtime/cpu.h) keeps MXCSR.DAZ equal to FPCR.FZ, and x86 compares
|
||||
// under DAZ read a denormal operand as a zero of its sign, as ARM compares do under FZ.
|
||||
SI uint64_t FN(cmp)(Cpu *c, T a, T b) { // FCMP NZCV
|
||||
a = FN(fz)(c, a), b = FN(fz)(c, b);
|
||||
(void)c;
|
||||
return a == b ? 0x60000000ull : a < b ? 0x80000000ull : a > b ? 0x20000000ull : 0x30000000ull;
|
||||
}
|
||||
// FCMEQ/FCMGE/FCMGT/FACGE/FACGT (op 0..4)
|
||||
SI VTU FN(vcmp)(Cpu *c, int op, VT a, VT b) {
|
||||
a = FN(vfz)(c, a), b = FN(vfz)(c, b);
|
||||
(void)c;
|
||||
if (op >= 3) a = __builtin_elementwise_abs(a), b = __builtin_elementwise_abs(b);
|
||||
switch (op) {
|
||||
case 0: return (VTU)(a == b);
|
||||
|
||||
@@ -216,6 +216,7 @@ static void svc_body(Cpu *c, uint32_t imm, uint64_t pc) {
|
||||
rt_log("guest ExitProcess at %s; exiting", ldr_describe(pc, at, sizeof at));
|
||||
fs_save_flush();
|
||||
vk_pipeline_save();
|
||||
playtime_save();
|
||||
prof_write();
|
||||
fflush(NULL);
|
||||
if (svc_test_exit_hook) return svc_test_exit_hook(0);
|
||||
|
||||
+1
-1
@@ -355,7 +355,7 @@ static int test_import(pool_t *pool) {
|
||||
GCHECK(n == 4 && pages[0] == a + 0x30000 && pages[3] == a + 0x33000, "CPU writes to 4 pages: %llu reported",
|
||||
(unsigned long long)n);
|
||||
vk_mem_resolve(a + 0x30000, 0x4000, &sp);
|
||||
vk_mem_shadow_sync();
|
||||
vk_mem_shadow_sync(0);
|
||||
copy_wait(sp.buffer, sp.offset, sb, 0, 0x4000);
|
||||
vmaInvalidateAllocation(c->vma, sa, 0, VK_WHOLE_SIZE);
|
||||
GCHECK(!memcmp(s, m + 0x30000, 0x4000), "GPU read of CPU-written heap differs");
|
||||
|
||||
@@ -36,6 +36,8 @@ static int g_selected_entry = -1;
|
||||
// The last action: a catalog id and the file or folder it names, formatted when drawn (the menu language can change)
|
||||
static UiTextId g_status_id = UI_MENU_AMIIBO_READY;
|
||||
static std::string g_status_arg;
|
||||
// Flag to request font rebuild (set when amiibo directory is refreshed with new files)
|
||||
static bool g_font_rebuild_requested = false;
|
||||
|
||||
static void set_status(UiTextId id, const std::string &arg = std::string()) {
|
||||
g_status_id = id;
|
||||
@@ -124,6 +126,9 @@ static void refresh_dir(void) {
|
||||
if (a.is_dir != b.is_dir) return a.is_dir;
|
||||
return a.name < b.name;
|
||||
});
|
||||
|
||||
// Request font rebuild to include any new filenames
|
||||
amiibo_request_font_rebuild();
|
||||
}
|
||||
|
||||
void tab_amiibo(float scale) {
|
||||
@@ -303,3 +308,44 @@ void tab_amiibo(float scale) {
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// Collect amiibo filenames for font glyph building (called from menu.cpp fonts_up)
|
||||
void amiibo_collect_filenames(void (*callback)(void *, const char *), void *ctx) {
|
||||
std::error_code ec;
|
||||
fs::path dir_path(g_current_dir);
|
||||
|
||||
if (!fs::exists(dir_path, ec) || !fs::is_directory(dir_path, ec)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &entry : fs::directory_iterator(dir_path, fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (ec) break;
|
||||
std::error_code status_ec;
|
||||
bool is_dir = entry.is_directory(status_ec);
|
||||
if (status_ec || is_dir) continue;
|
||||
|
||||
std::string ext = entry.path().extension().string();
|
||||
if (!iequals(ext, ".bin") && !iequals(ext, ".nfc")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string filename = entry.path().filename().string();
|
||||
if (!filename.empty()) {
|
||||
callback(ctx, filename.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request font atlas rebuild on next frame
|
||||
void amiibo_request_font_rebuild(void) {
|
||||
g_font_rebuild_requested = true;
|
||||
}
|
||||
|
||||
// Check and clear font rebuild request flag
|
||||
extern "C" bool amiibo_check_font_rebuild(void) {
|
||||
if (g_font_rebuild_requested) {
|
||||
g_font_rebuild_requested = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2,3 +2,9 @@
|
||||
#pragma once
|
||||
|
||||
void tab_amiibo(float scale);
|
||||
|
||||
// Collect amiibo filenames for font glyph building (called from menu.cpp fonts_up)
|
||||
void amiibo_collect_filenames(void (*callback)(void *, const char *), void *ctx);
|
||||
|
||||
// Request font atlas rebuild on next frame (called when amiibo directory is refreshed)
|
||||
void amiibo_request_font_rebuild(void);
|
||||
+50
-5
@@ -51,12 +51,15 @@ extern "C" {
|
||||
void rt_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void vk_queue_lock(void);
|
||||
void vk_queue_unlock(void);
|
||||
int vk_exec_replay_workers(void);
|
||||
void res_sizes(unsigned out[2], unsigned scene[2]);
|
||||
double frame_step_limit_hz(void);
|
||||
extern const int rt_game_version;
|
||||
extern const char rt_port_version[];
|
||||
void cheats_frame(uint64_t frame); // cheats.c
|
||||
bool tab_cheats(float scale); // ui/cheats_tab.cpp; true: restart into the rebuild
|
||||
int playtime_get(uint64_t *total_s, uint64_t *session_s); // playtime.c
|
||||
bool amiibo_check_font_rebuild(void); // ui/amiibo_page.cpp; check and clear font rebuild request
|
||||
}
|
||||
|
||||
// Roboto Medium (Apache-2.0, ui/fonts/LICENSE-Roboto.txt) with Latin and Cyrillic glyphs.
|
||||
@@ -352,6 +355,9 @@ static int fonts_up(float scale) {
|
||||
if (const CheatInfo *ci = cheats_info(k); ci && ci->name) b.AddText(ci->name);
|
||||
for (int k = 0; k < mods_count(); k++)
|
||||
if (const ModInfo *mi = mods_at(k); mi) b.AddText(mi->name);
|
||||
// Add amiibo filenames so they display correctly in the menu
|
||||
amiibo_collect_filenames(add_row_glyphs, &b);
|
||||
|
||||
b.BuildRanges(&ranges[i]);
|
||||
ImFontConfig mc;
|
||||
mc.MergeMode = true;
|
||||
@@ -662,12 +668,15 @@ static void tab_screen(float scale) {
|
||||
opt_group(UI_MENU_GRAPHICS);
|
||||
row_output_res();
|
||||
row_render_scale();
|
||||
opt_toggle("TOTK_DYNAMIC_RES", UI_MENU_DYNAMIC_RES, UI_MENU_DYNAMIC_RES_DESC);
|
||||
row_shadow_res();
|
||||
static const UiTextId up[] = {UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR};
|
||||
opt_choice("TOTK_UPSCALER", UI_MENU_UPSCALER, up, 2, UI_MENU_UPSCALER_DESC);
|
||||
opt_slider("TOTK_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
|
||||
static const UiTextId aspect[] = {UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH};
|
||||
opt_choice("TOTK_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
|
||||
static const UiTextId movie_aspect[] = {UI_MENU_MOVIE_ASPECT_KEEP, UI_MENU_MOVIE_ASPECT_STRETCH};
|
||||
opt_choice("TOTK_MOVIE_ASPECT", UI_MENU_MOVIE_ASPECT, movie_aspect, 2, UI_MENU_MOVIE_ASPECT_DESC);
|
||||
opt_slider("TOTK_FOV", UI_MENU_FOV, 70, 140, UI_MENU_FOV_DESC, 5, "%.0f %%");
|
||||
static const char *const aniso[] = {"game", "2", "4", "8", "16"};
|
||||
const char *aniso_names[] = {menu_text(UI_MENU_ANISO_GAME), "2x", "4x", "8x", "16x"};
|
||||
@@ -696,13 +705,28 @@ static void tab_screen(float scale) {
|
||||
opt_end();
|
||||
}
|
||||
|
||||
// Play time across runs (playtime.c), read only. Hidden while it is not counted (no profile directory).
|
||||
static void row_play_time(void) {
|
||||
uint64_t total, session;
|
||||
if (!playtime_get(&total, &session)) return;
|
||||
char value[64], run[64], note[96];
|
||||
snprintf(value, sizeof value, menu_text(UI_MENU_PLAY_TIME_HM), (unsigned long long)(total / 3600),
|
||||
(unsigned)(total / 60 % 60));
|
||||
snprintf(run, sizeof run, menu_text(UI_MENU_PLAY_TIME_HM), (unsigned long long)(session / 3600),
|
||||
(unsigned)(session / 60 % 60));
|
||||
snprintf(note, sizeof note, menu_text(UI_MENU_PLAY_TIME_SESSION), run);
|
||||
opt_text_row("##play_time", menu_text(UI_MENU_PLAY_TIME), value, note, menu_text(UI_MENU_PLAY_TIME_DESC), false);
|
||||
}
|
||||
|
||||
static void tab_game(float scale) {
|
||||
opt_begin("##game", scale);
|
||||
row_fps_limit();
|
||||
opt_toggle("TOTK_FRAME_QUEUE", UI_MENU_FRAME_QUEUE, UI_MENU_FRAME_QUEUE_DESC);
|
||||
row_game_lang();
|
||||
static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD};
|
||||
opt_choice("TOTK_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC);
|
||||
opt_toggle("TOTK_ASSUME_FOCUS", UI_MENU_ASSUME_FOCUS, UI_MENU_ASSUME_FOCUS_DESC);
|
||||
row_play_time();
|
||||
opt_end();
|
||||
}
|
||||
|
||||
@@ -773,8 +797,8 @@ static const char *prompts_note(bool *restart) {
|
||||
const char *layout = cfg_value("TOTK_PAD_LAYOUT"), *used = romfs_prompts();
|
||||
const char *want = input_prompts_folder(input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout), layout);
|
||||
if (want == used || (want && used && !strcmp(want, used))) return nullptr;
|
||||
// A game resolution of another shape needs a folder of its own for the PC prompts (romfs-patches-pc-<w>x<h>), which
|
||||
// the setup does not write: the shape is the reason then, not the setup.
|
||||
// A game resolution of another shape needs a folder of its own for the prompts (<folder>-<w>x<h>). The setup writes
|
||||
// one for each size the Screen tab offers; at any other size the shape is the reason, not the setup.
|
||||
int off = romfs_prompts_off(want);
|
||||
if (want && !romfs_prompts_ready(want) && off != ROMFS_PROMPTS_SHAPE) return menu_text(UI_MENU_PROMPTS_MISSING);
|
||||
switch (off) {
|
||||
@@ -887,6 +911,23 @@ static void row_threads(const char *key, UiTextId label, UiTextId desc) {
|
||||
opt_choice_list(key, label, vals, names, N, desc);
|
||||
}
|
||||
|
||||
// The chunked replay's recording threads: Off, then the counts; the note under the value is what this run uses, which
|
||||
// a change replaces only after a restart.
|
||||
static void row_replay_workers(void) {
|
||||
static const char *const vals[] = {"0", "2", "3", "4", "6", "8", "12", "16"};
|
||||
enum { N = sizeof vals / sizeof *vals };
|
||||
const char *names[N];
|
||||
names[0] = menu_text(UI_MENU_OFF);
|
||||
for (int i = 1; i < N; i++) names[i] = vals[i];
|
||||
static char now[64];
|
||||
char n[16];
|
||||
int w = vk_exec_replay_workers();
|
||||
snprintf(n, sizeof n, "%d", w);
|
||||
if (w >= 0) snprintf(now, sizeof now, menu_text(UI_MENU_REPLAY_WORKERS_NOW), w ? n : menu_text(UI_MENU_OFF));
|
||||
opt_choice_list("TOTK_REPLAY_WORKERS", UI_MENU_REPLAY_WORKERS, vals, names, N, UI_MENU_REPLAY_WORKERS_DESC, -1,
|
||||
w >= 0 ? now : nullptr);
|
||||
}
|
||||
|
||||
// A free-text key shown as Off (unset) / On (value set): On writes on_value, any set value reads as On.
|
||||
static void row_set_or_not(const char *key, UiTextId label, UiTextId desc, const char *on_value) {
|
||||
const char *vals[] = {"", on_value}, *names[] = {menu_text(UI_MENU_OFF), menu_text(UI_MENU_ON)};
|
||||
@@ -936,6 +977,7 @@ static void tab_advanced(float scale) {
|
||||
static const UiTextId preempt[] = {UI_MENU_PREEMPT_OFF, UI_MENU_PREEMPT_ON, UI_MENU_PREEMPT_INHERIT}; // 0|1|2
|
||||
opt_choice("TOTK_ASYNC_PREEMPT", UI_MENU_ASYNC_PREEMPT, preempt, 3, UI_MENU_ASYNC_PREEMPT_DESC);
|
||||
opt_toggle("TOTK_REPLAY_ASYNC", UI_MENU_REPLAY_ASYNC, UI_MENU_REPLAY_ASYNC_DESC);
|
||||
row_replay_workers();
|
||||
opt_toggle("TOTK_TIMER_1MS", UI_MENU_TIMER_1MS, UI_MENU_TIMER_1MS_DESC);
|
||||
|
||||
opt_group(UI_MENU_GROUP_SHADERS);
|
||||
@@ -1063,6 +1105,7 @@ static void mods_builtin_rows(void) {
|
||||
opt_toggle("TOTK_REALTIME", UI_MENU_REALTIME, UI_MENU_REALTIME_DESC);
|
||||
opt_toggle("TOTK_CLOTH_REALTIME", UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC);
|
||||
opt_toggle("TOTK_HIGHFPS_FIX", UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC);
|
||||
opt_toggle("TOTK_CLIMB_FIX", UI_MENU_CLIMB_FIX, UI_MENU_CLIMB_FIX_DESC);
|
||||
opt_button("##mods_quickmenu", UI_MENU_MODS_QUICKMENU, UI_MENU_MODS_QUICKMENU_DESC,
|
||||
menu_text(romfs_patches_loaded() ? UI_MENU_ON : UI_MENU_OFF));
|
||||
}
|
||||
@@ -1091,8 +1134,8 @@ static void tab_mods(float scale) {
|
||||
char code[512] = "", problem[512] = "";
|
||||
if (m->nsets && m->overrides[0])
|
||||
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_REPLACES_BUILTIN), m->nsets, m->overrides);
|
||||
else if (m->nsets)
|
||||
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_EXEFS_ON), m->nsets);
|
||||
else if (m->nsets || m->ndata)
|
||||
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_EXEFS_ON), m->nsets + m->ndata);
|
||||
switch (m->problem) {
|
||||
case MOD_EMPTY: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EMPTY)); break;
|
||||
case MOD_PLUGIN: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_SUBSDK)); break;
|
||||
@@ -1518,7 +1561,9 @@ extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const cha
|
||||
uint64_t t0 = plat_now_ns();
|
||||
if (!g_ctx_ok) make_context();
|
||||
float scale = menu_scale(t->height);
|
||||
if (!vk_up(t) || (g_font_scale != scale && !fonts_up(scale))) {
|
||||
// Check if amiibo page requested font rebuild (e.g., after refreshing directory with new files)
|
||||
bool amiibo_rebuild = amiibo_check_font_rebuild();
|
||||
if (!vk_up(t) || (g_font_scale != scale && !fonts_up(scale)) || (amiibo_rebuild && !fonts_up(scale))) {
|
||||
rt_log("menu: Dear ImGui Vulkan back end set-up failed; the menu stays closed");
|
||||
g_failed.store(1);
|
||||
set_open(0);
|
||||
|
||||
+98
-13
@@ -71,6 +71,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_RENDER_SCALE_DESC] = "Size of the 3D scene relative to the game resolution; the game's FSR scales it to "
|
||||
"that size. Above 100 % smooths jagged edges (supersampling) at a high GPU cost.",
|
||||
[UI_MENU_RENDER_SCALE_GAME] = "Game (83 %)",
|
||||
[UI_MENU_DYNAMIC_RES] = "Dynamic resolution",
|
||||
[UI_MENU_DYNAMIC_RES_DESC] = "Lets the game lower the 3D scene to 90 or 80 %. It judges by an estimate made for the Switch that ignores your GPU, so wide scenes drop even on a fast PC. Off keeps the render scale.",
|
||||
[UI_MENU_UPSCALER] = "Scaling to the window",
|
||||
[UI_MENU_UPSCALER_DESC] = "How the picture is scaled when the window is larger than the game resolution. FSR keeps "
|
||||
"edges and detail sharper than bilinear.",
|
||||
@@ -79,6 +81,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "When the window's shape differs from the game resolution's (a 16:9 picture on a 21:9 "
|
||||
"screen): keep the picture's shape with black bars, or stretch it over the window.",
|
||||
[UI_MENU_ASPECT_FIT] = "Black bars", [UI_MENU_ASPECT_STRETCH] = "Stretch",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Movie shape",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "When the game resolution is not 16:9, keep the cutscene movies' own shape with black "
|
||||
"bars, or stretch them over the whole picture. Applies from the next movie.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Black bars", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Stretch",
|
||||
[UI_MENU_FOV] = "Field of view",
|
||||
[UI_MENU_FOV_DESC] = "How much of the world the camera shows, in percent of the game's own. Game version 1.4.3 only.",
|
||||
[UI_MENU_SHADOW_RES] = "Shadow map size",
|
||||
@@ -168,12 +174,17 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
// the monitor's refresh rate; printf format with the rate in Hz when it is known
|
||||
[UI_MENU_FPS_DISPLAY] = "Monitor rate", [UI_MENU_FPS_DISPLAY_HZ] = "Monitor rate (%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Frame queue",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "The game starts a frame while the previous one waits for its turn on screen: far fewer "
|
||||
"dropped frames at the limit, up to one frame more input delay. Applies at once.",
|
||||
[UI_MENU_REALTIME] = "Game speed follows the clock",
|
||||
[UI_MENU_REALTIME_DESC] = "Steps the game by real time, so a slow frame does not slow the game down.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Cloth on the clock",
|
||||
[UI_MENU_CLOTH_REALTIME_DESC] = "Steps cloth and hair by real time as well.",
|
||||
[UI_MENU_HIGHFPS_FIX] = "High frame rate fixes",
|
||||
[UI_MENU_HIGHFPS_FIX_DESC] = "Corrects physics that misbehave above 30 frames per second, and objects that stay see-through or invisible above 60.",
|
||||
[UI_MENU_CLIMB_FIX] = "Climbing speed fix",
|
||||
[UI_MENU_CLIMB_FIX_DESC] = "Keeps climbing walls and ladders at full speed above 60 frames per second, so Link reaches the top.",
|
||||
[UI_MENU_GAME_LANG] = "Game language", [UI_MENU_GAME_LANG_DESC] = "Language of the game's text and voices.",
|
||||
[UI_MENU_GAME_LANG_AUTO] = "Automatic",
|
||||
[UI_MENU_OPERATION_MODE] = "Console mode",
|
||||
@@ -185,6 +196,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
"window loses focus, as on the Switch when Home is pressed; a load interrupted that "
|
||||
"way can stop for good.",
|
||||
|
||||
[UI_MENU_PLAY_TIME] = "Play time",
|
||||
[UI_MENU_PLAY_TIME_DESC] = "Time in the game over all runs, counted while the game window has the focus and the game "
|
||||
"draws frames. Kept in playtime.txt in the profile folder, next to the saves.",
|
||||
[UI_MENU_PLAY_TIME_HM] = "%llu h %02u min", [UI_MENU_PLAY_TIME_SESSION] = "this run: %s",
|
||||
[UI_MENU_MODS_BUILTIN] = "Built into the port", [UI_MENU_MODS_INSTALLED] = "Installed mods",
|
||||
[UI_MENU_MODS_NONE] = "No mods yet. Unpack a mod into the mods folder, one folder per mod with its romfs folder "
|
||||
"inside, and restart the game. TKMM can export its merged mods into a folder there too.",
|
||||
@@ -249,7 +264,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_MISSING] = "prompt files missing: run the setup again",
|
||||
[UI_MENU_PROMPTS_FAILED] = "prompt files damaged: run the setup again",
|
||||
[UI_MENU_PROMPTS_RESTART] = "prompts change after restart",
|
||||
[UI_MENU_PROMPTS_OFF_SHAPE] = "off at a game resolution not 16:9",
|
||||
[UI_MENU_PROMPTS_OFF_SHAPE] = "off at this game resolution (no layout made for it)",
|
||||
[UI_MENU_PROMPTS_OFF_MODS] = "off: a RomFS mod replaces their files",
|
||||
[UI_MENU_PROMPTS_OFF_PATCHES] = "off while TOTK_ROMFS_PATCHES is set",
|
||||
[UI_MENU_RESTART] = "Restart now",
|
||||
@@ -263,7 +278,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "How far a gamepad stick moves before the game sees it. Raise it if the character or the camera moves on its own: let go of the stick, read \"stick now\" and set the dead zone a few percent above it.",
|
||||
[UI_MENU_STICK_DEADZONE_NOW] = "stick now: %.0f %%",
|
||||
[UI_MENU_MOUSE_MODE] = "Mouse camera",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direct: the camera turns by how far the mouse moves, as in a PC game, with no stick dead zone or curve (game 1.4.3; other versions use stick emulation). Stick emulation: the mouse acts as the right stick.",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direct: the camera turns by how far the mouse moves, as in a PC game, with no stick dead zone or curve (games 1.4.3 and 1.2.1; 1.1.0 uses stick emulation). Stick emulation: the mouse acts as the right stick.",
|
||||
[UI_MENU_MOUSE_STICK] = "Stick emulation",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Direct",
|
||||
[UI_MENU_MENU_CURSOR] = "Mouse in the game's menus",
|
||||
@@ -375,6 +390,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PREEMPT_OFF] = "Off", [UI_MENU_PREEMPT_ON] = "On", [UI_MENU_PREEMPT_INHERIT] = "Inheritance only",
|
||||
[UI_MENU_REPLAY_ASYNC] = "GPU frame prep on its own thread",
|
||||
[UI_MENU_REPLAY_ASYNC_DESC] = "Prepares frames for the GPU on a separate thread instead of the game thread. Adds up to one frame of latency.",
|
||||
[UI_MENU_REPLAY_WORKERS] = "GPU frame prep threads",
|
||||
[UI_MENU_REPLAY_WORKERS_DESC] = "Records large frames for the GPU on several threads at once. Helps when the CPU limits the frame rate; needs 8 or more logical processors. Off records on one thread.",
|
||||
[UI_MENU_REPLAY_WORKERS_NOW] = "now: %s",
|
||||
[UI_MENU_TIMER_1MS] = "1 ms Windows timer",
|
||||
[UI_MENU_TIMER_1MS_DESC] = "Raises the system timer resolution to 1 ms for steadier frame pacing.",
|
||||
[UI_MENU_PIPE_WAIT] = "Waiting draw handling",
|
||||
@@ -426,7 +444,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_VK_DYN_VERTEX_DESC] = "Forces dynamic vertex input on or off; the default depends on the GPU.",
|
||||
[UI_MENU_VK_DYN_A2C] = "Dynamic alpha to coverage",
|
||||
[UI_MENU_VK_DYN_A2C_DESC] = "Forces dynamic alpha to coverage on or off. Default: off on AMD's driver for RDNA 2 and "
|
||||
"older, which draws characters black with it, on elsewhere.",
|
||||
"older, which draws characters black with it, and on Intel's Windows driver for "
|
||||
"integrated GPUs, which draws coloured noise on foliage; on elsewhere.",
|
||||
[UI_MENU_VK_SUBGROUP_PIN] = "Pin subgroup size", [UI_MENU_VK_SUBGROUP_PIN_DESC] = "Fixes the shader subgroup size.",
|
||||
[UI_MENU_TRACE] = "Performance log", [UI_MENU_TRACE_DESC] = "Writes frame timings to a file.",
|
||||
[UI_MENU_FLIGHT] = "Flight recorder",
|
||||
@@ -534,6 +553,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_RENDER_SCALE_DESC] = "Размер 3D-сцены относительно разрешения игры; FSR игры доводит её до этого размера. "
|
||||
"Выше 100 % сглаживает лесенки на краях (суперсэмплинг) ценой большой нагрузки на GPU.",
|
||||
[UI_MENU_RENDER_SCALE_GAME] = "Как в игре (83 %)",
|
||||
[UI_MENU_DYNAMIC_RES] = "Динамическое разрешение",
|
||||
[UI_MENU_DYNAMIC_RES_DESC] = "Игра снижает 3D-сцену до 90 или 80 %. Она судит по оценке для Switch, не учитывающей вашу видеокарту, поэтому широкие сцены снижаются даже на быстром ПК. Выключено: остаётся масштаб отрисовки.",
|
||||
[UI_MENU_UPSCALER] = "Масштабирование под окно",
|
||||
[UI_MENU_UPSCALER_DESC] = "Как картинка растягивается, когда окно больше разрешения игры. FSR сохраняет края и "
|
||||
"детали чётче билинейного.",
|
||||
@@ -542,6 +563,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "Когда форма окна отличается от разрешения игры (картинка 16:9 на экране 21:9): сохранить "
|
||||
"форму картинки с чёрными полосами или растянуть её на всё окно.",
|
||||
[UI_MENU_ASPECT_FIT] = "Чёрные полосы", [UI_MENU_ASPECT_STRETCH] = "Растянуть",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Форма видеороликов",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "Когда разрешение игры не 16:9: сохранить форму роликов с чёрными полосами или "
|
||||
"растянуть их на всю картинку. Действует со следующего ролика.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Чёрные полосы", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Растянуть",
|
||||
[UI_MENU_FOV] = "Поле зрения",
|
||||
[UI_MENU_FOV_DESC] = "Сколько мира показывает камера, в процентах от игрового. Только для версии игры 1.4.3.",
|
||||
[UI_MENU_SHADOW_RES] = "Размер карты теней",
|
||||
@@ -631,12 +656,17 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS",
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "Как у монитора", [UI_MENU_FPS_DISPLAY_HZ] = "Как у монитора (%.0f Гц)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Очередь кадров",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "Игра начинает кадр, пока предыдущий ждёт показа: намного меньше пропущенных кадров на "
|
||||
"лимите, задержка ввода больше не более чем на кадр. Применяется сразу.",
|
||||
[UI_MENU_REALTIME] = "Скорость игры по часам",
|
||||
[UI_MENU_REALTIME_DESC] = "Игра идёт по реальному времени, медленный кадр её не замедляет.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Ткань по часам",
|
||||
[UI_MENU_CLOTH_REALTIME_DESC] = "Ткань и волосы тоже идут по реальному времени.",
|
||||
[UI_MENU_HIGHFPS_FIX] = "Исправления для высокого FPS",
|
||||
[UI_MENU_HIGHFPS_FIX_DESC] = "Исправляет физику, которая ломается при частоте выше 30 кадров в секунду, и объекты, которые выше 60 остаются полупрозрачными или невидимыми.",
|
||||
[UI_MENU_CLIMB_FIX] = "Исправление скорости лазания",
|
||||
[UI_MENU_CLIMB_FIX_DESC] = "Сохраняет нормальную скорость лазания по стенам и лестницам выше 60 кадров в секунду, чтобы Линк добирался до верха.",
|
||||
[UI_MENU_GAME_LANG] = "Язык игры", [UI_MENU_GAME_LANG_DESC] = "Язык текста и озвучки игры.",
|
||||
[UI_MENU_GAME_LANG_AUTO] = "Автоматически",
|
||||
[UI_MENU_OPERATION_MODE] = "Режим консоли",
|
||||
@@ -648,6 +678,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
"когда окно теряет фокус, как на Switch при нажатии Home; прерванная так загрузка "
|
||||
"может остановиться насовсем.",
|
||||
|
||||
[UI_MENU_PLAY_TIME] = "Время в игре",
|
||||
[UI_MENU_PLAY_TIME_DESC] = "Время в игре за все запуски. Считается, пока окно игры в фокусе и игра рисует кадры. "
|
||||
"Хранится в playtime.txt в папке профиля, рядом с сохранениями.",
|
||||
[UI_MENU_PLAY_TIME_HM] = "%llu ч %02u мин", [UI_MENU_PLAY_TIME_SESSION] = "в этом запуске: %s",
|
||||
[UI_MENU_MODS_BUILTIN] = "Встроенные в порт", [UI_MENU_MODS_INSTALLED] = "Установленные моды",
|
||||
[UI_MENU_MODS_NONE] = "Модов пока нет. Распакуйте мод в папку mods, каждый мод в свою папку с папкой romfs внутри, и "
|
||||
"перезапустите игру. TKMM тоже может выгрузить туда объединённые моды отдельной папкой.",
|
||||
@@ -710,7 +744,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_MISSING] = "нет файлов подсказок: запустите установку снова",
|
||||
[UI_MENU_PROMPTS_FAILED] = "файлы подсказок повреждены: запустите установку снова",
|
||||
[UI_MENU_PROMPTS_RESTART] = "подсказки сменятся после перезапуска",
|
||||
[UI_MENU_PROMPTS_OFF_SHAPE] = "выкл. при разрешении игры не 16:9",
|
||||
[UI_MENU_PROMPTS_OFF_SHAPE] = "выкл. при этом разрешении игры (для него нет раскладки)",
|
||||
[UI_MENU_PROMPTS_OFF_MODS] = "выкл.: мод RomFS заменяет их файлы",
|
||||
[UI_MENU_PROMPTS_OFF_PATCHES] = "выкл., пока задан TOTK_ROMFS_PATCHES",
|
||||
[UI_MENU_RESTART] = "Перезапустить сейчас",
|
||||
@@ -724,7 +758,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно отклонить стик геймпада, чтобы игра это заметила. Увеличьте, если персонаж или камера двигаются сами: отпустите стик, посмотрите «стик сейчас» и поставьте мёртвую зону на несколько процентов выше.",
|
||||
[UI_MENU_STICK_DEADZONE_NOW] = "стик сейчас: %.0f %%",
|
||||
[UI_MENU_MOUSE_MODE] = "Управление мышью",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Прямое: камера поворачивается на столько, на сколько сдвинута мышь, как в играх для ПК, без мёртвой зоны и кривой стика (версия игры 1.4.3; на других версиях эмуляция стика). Эмуляция стика: мышь работает как правый стик.",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Прямое: камера поворачивается на столько, на сколько сдвинута мышь, как в играх для ПК, без мёртвой зоны и кривой стика (версии игры 1.4.3 и 1.2.1; на 1.1.0 эмуляция стика). Эмуляция стика: мышь работает как правый стик.",
|
||||
[UI_MENU_MOUSE_STICK] = "Эмуляция стика",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Прямое",
|
||||
[UI_MENU_MENU_CURSOR] = "Мышь в меню игры",
|
||||
@@ -840,6 +874,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PREEMPT_OFF] = "Выкл.", [UI_MENU_PREEMPT_ON] = "Вкл.", [UI_MENU_PREEMPT_INHERIT] = "Только наследование",
|
||||
[UI_MENU_REPLAY_ASYNC] = "Подготовка кадра для GPU в отдельном потоке",
|
||||
[UI_MENU_REPLAY_ASYNC_DESC] = "Кадр для GPU готовится в отдельном потоке, а не в потоке игры. Добавляет до одного кадра задержки.",
|
||||
[UI_MENU_REPLAY_WORKERS] = "Потоки подготовки кадра для GPU",
|
||||
[UI_MENU_REPLAY_WORKERS_DESC] = "Большие кадры для GPU записываются сразу в нескольких потоках. Помогает, когда частоту кадров ограничивает процессор; нужно 8 логических процессоров или больше. «Выкл.» — запись в одном потоке.",
|
||||
[UI_MENU_REPLAY_WORKERS_NOW] = "сейчас: %s",
|
||||
[UI_MENU_TIMER_1MS] = "Таймер Windows 1 мс",
|
||||
[UI_MENU_TIMER_1MS_DESC] = "Повышает точность системного таймера до 1 мс для ровного темпа кадров.",
|
||||
[UI_MENU_PIPE_WAIT] = "Обработка ожидающих отрисовок",
|
||||
@@ -891,8 +928,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_VK_DYN_VERTEX_DESC] = "Принудительно включает или выключает динамический ввод вершин; по умолчанию зависит от GPU.",
|
||||
[UI_MENU_VK_DYN_A2C] = "Динамический alpha to coverage",
|
||||
[UI_MENU_VK_DYN_A2C_DESC] = "Принудительно включает или выключает динамический alpha to coverage. По умолчанию "
|
||||
"выключен на драйвере AMD для RDNA 2 и старше, где с ним персонажи чёрные, и включён "
|
||||
"на остальных.",
|
||||
"выключен на драйвере AMD для RDNA 2 и старше, где с ним персонажи чёрные, и на "
|
||||
"драйвере Intel для Windows на встроенных GPU, где с ним цветной шум на листве; "
|
||||
"включён на остальных.",
|
||||
[UI_MENU_VK_SUBGROUP_PIN] = "Фиксировать размер подгруппы",
|
||||
[UI_MENU_VK_SUBGROUP_PIN_DESC] = "Фиксирует размер подгруппы шейдера.",
|
||||
[UI_MENU_TRACE] = "Журнал производительности", [UI_MENU_TRACE_DESC] = "Пишет время кадров в файл.",
|
||||
@@ -969,6 +1007,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_TAB_SCREEN] = "Tela", [UI_MENU_TAB_GAME] = "Jogo", [UI_MENU_TAB_SOUND] = "Som",
|
||||
[UI_MENU_TAB_CONTROLS] = "Controles", [UI_MENU_TAB_AMIIBO] = "Amiibo", [UI_MENU_TAB_MENU] = "Menu", [UI_MENU_TAB_ADVANCED] = "Avançado",
|
||||
[UI_MENU_TAB_MODS] = "Mods",
|
||||
[UI_MENU_PLAY_TIME] = "Tempo de jogo",
|
||||
[UI_MENU_PLAY_TIME_DESC] = "Tempo no jogo somando todas as sessões, contado enquanto a janela do jogo está em foco e o "
|
||||
"jogo desenha quadros. Fica em playtime.txt na pasta do perfil, junto dos saves.",
|
||||
[UI_MENU_PLAY_TIME_HM] = "%llu h %02u min", [UI_MENU_PLAY_TIME_SESSION] = "nesta sessão: %s",
|
||||
[UI_MENU_MODS_BUILTIN] = "Embutidos no port", [UI_MENU_MODS_INSTALLED] = "Mods instalados",
|
||||
[UI_MENU_MODS_NONE] = "Nenhum mod ainda. Descompacte um mod na pasta mods, uma pasta por mod com a pasta romfs dentro, "
|
||||
"e reinicie o jogo. O TKMM também pode exportar os mods mesclados para uma pasta ali.",
|
||||
@@ -1032,6 +1074,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
"tamanho. Acima de 100 % suaviza bordas serrilhadas (supersampling) com alto custo de "
|
||||
"GPU.",
|
||||
[UI_MENU_RENDER_SCALE_GAME] = "Jogo (83 %)",
|
||||
[UI_MENU_DYNAMIC_RES] = "Resolução dinâmica",
|
||||
[UI_MENU_DYNAMIC_RES_DESC] = "Deixa o jogo reduzir a cena 3D para 90 ou 80 %. Ele decide por uma estimativa feita para o Switch que ignora sua GPU, então cenas largas caem mesmo num PC rápido. Desligada, mantém a escala da renderização.",
|
||||
[UI_MENU_UPSCALER] = "Escala para a janela",
|
||||
[UI_MENU_UPSCALER_DESC] = "Como a imagem é ampliada quando a janela é maior que a resolução do jogo. O FSR mantém "
|
||||
"bordas e detalhes mais nítidos que o bilinear.",
|
||||
@@ -1040,6 +1084,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "Quando o formato da janela difere do da resolução do jogo (uma imagem 16:9 numa tela "
|
||||
"21:9): manter o formato da imagem com barras pretas ou esticá-la sobre a janela.",
|
||||
[UI_MENU_ASPECT_FIT] = "Barras pretas", [UI_MENU_ASPECT_STRETCH] = "Esticar",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Formato dos vídeos",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "Quando a resolução do jogo não é 16:9: manter o formato dos vídeos das cenas com "
|
||||
"barras pretas ou esticá-los sobre a imagem toda. Vale a partir do próximo vídeo.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Barras pretas", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Esticar",
|
||||
[UI_MENU_SHARPNESS] = "Nitidez do FSR",
|
||||
[UI_MENU_SHARPNESS_DESC] = "Aumento de nitidez após a ampliação do FSR; 0 desliga.",
|
||||
|
||||
@@ -1054,12 +1102,17 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
"a janela perde o foco, como no Switch ao apertar Home; um carregamento interrompido "
|
||||
"assim pode parar de vez.",
|
||||
[UI_MENU_FPS_DISPLAY] = "Taxa do monitor", [UI_MENU_FPS_DISPLAY_HZ] = "Taxa do monitor (%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Fila de quadros",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "O jogo começa um quadro enquanto o anterior espera a vez de aparecer: muito menos quadros "
|
||||
"perdidos no limite, com até um quadro a mais de atraso na entrada. Vale na hora.",
|
||||
[UI_MENU_REALTIME] = "Velocidade do jogo pelo relógio",
|
||||
[UI_MENU_REALTIME_DESC] = "Avança o jogo pelo tempo real, para que um quadro lento não deixe o jogo mais lento.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Tecidos pelo relógio",
|
||||
[UI_MENU_CLOTH_REALTIME_DESC] = "Avança tecidos e cabelos pelo tempo real também.",
|
||||
[UI_MENU_HIGHFPS_FIX] = "Correções para alta taxa de quadros",
|
||||
[UI_MENU_HIGHFPS_FIX_DESC] = "Corrige a física que se comporta mal acima de 30 quadros por segundo e objetos que ficam transparentes ou invisíveis acima de 60.",
|
||||
[UI_MENU_CLIMB_FIX] = "Correção da velocidade de escalada",
|
||||
[UI_MENU_CLIMB_FIX_DESC] = "Mantém a escalada de paredes e escadas na velocidade normal acima de 60 quadros por segundo, para que Link chegue ao topo.",
|
||||
[UI_MENU_GAME_LANG] = "Idioma do jogo", [UI_MENU_GAME_LANG_DESC] = "Idioma dos textos e das vozes do jogo.",
|
||||
[UI_MENU_GAME_LANG_AUTO] = "Automático",
|
||||
[UI_MENU_OPERATION_MODE] = "Modo do console",
|
||||
@@ -1067,6 +1120,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_MODE_DOCKED] = "Modo TV", [UI_MENU_MODE_HANDHELD] = "Modo portátil",
|
||||
|
||||
[UI_MENU_VOLUME] = "Volume", [UI_MENU_VOLUME_DESC] = "Volume geral do jogo.",
|
||||
[UI_MENU_AUDIO_CHANNELS] = "Alto-falantes",
|
||||
[UI_MENU_AUDIO_CHANNELS_DESC] = "Saída estéreo ou surround 5.1. Automático escolhe 5.1 quando a configuração de "
|
||||
"alto-falantes do Windows tem seis canais ou mais. Vale após reiniciar.",
|
||||
[UI_MENU_CHANNELS_AUTO] = "Automático", [UI_MENU_CHANNELS_STEREO] = "Estéreo", [UI_MENU_CHANNELS_51] = "Surround 5.1",
|
||||
|
||||
[UI_MENU_MOTION] = "Controles de movimento", [UI_MENU_MOTION_DESC] = "Mirar com o giroscópio do controle.",
|
||||
[UI_MENU_RUMBLE_STRENGTH] = "Intensidade da vibração",
|
||||
@@ -1198,6 +1255,9 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PREEMPT_OFF] = "Desligado", [UI_MENU_PREEMPT_ON] = "Ligado", [UI_MENU_PREEMPT_INHERIT] = "Só herança",
|
||||
[UI_MENU_REPLAY_ASYNC] = "Preparo de quadro da GPU em thread própria",
|
||||
[UI_MENU_REPLAY_ASYNC_DESC] = "Prepara os quadros para a GPU numa thread separada em vez da thread do jogo. Adiciona até um quadro de latência.",
|
||||
[UI_MENU_REPLAY_WORKERS] = "Threads de preparo de quadro da GPU",
|
||||
[UI_MENU_REPLAY_WORKERS_DESC] = "Grava quadros grandes para a GPU em várias threads ao mesmo tempo. Ajuda quando a CPU limita a taxa de quadros; precisa de 8 ou mais processadores lógicos. Desligado grava numa thread só.",
|
||||
[UI_MENU_REPLAY_WORKERS_NOW] = "agora: %s",
|
||||
[UI_MENU_TIMER_1MS] = "Timer do Windows de 1 ms",
|
||||
[UI_MENU_TIMER_1MS_DESC] = "Aumenta a resolução do timer do sistema para 1 ms, para um ritmo de quadros mais estável.",
|
||||
[UI_MENU_PIPE_WAIT] = "Tratamento de desenhos em espera",
|
||||
@@ -1251,7 +1311,9 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_VK_DYN_VERTEX_DESC] = "Força a entrada dinâmica de vértices ligada ou desligada; o padrão depende da GPU.",
|
||||
[UI_MENU_VK_DYN_A2C] = "Alpha to coverage dinâmico",
|
||||
[UI_MENU_VK_DYN_A2C_DESC] = "Força o alpha to coverage dinâmico ligado ou desligado. Padrão: desligado no driver da AMD "
|
||||
"para RDNA 2 e anteriores, que com ele desenha os personagens pretos, ligado nos demais.",
|
||||
"para RDNA 2 e anteriores, que com ele desenha os personagens pretos, e no driver da "
|
||||
"Intel para Windows em GPUs integradas, que desenha ruído colorido na folhagem; "
|
||||
"ligado nos demais.",
|
||||
[UI_MENU_VK_SUBGROUP_PIN] = "Fixar tamanho do subgrupo",
|
||||
[UI_MENU_VK_SUBGROUP_PIN_DESC] = "Fixa o tamanho do subgrupo dos shaders.",
|
||||
[UI_MENU_TRACE] = "Registro de desempenho", [UI_MENU_TRACE_DESC] = "Grava os tempos de quadro num arquivo.",
|
||||
@@ -1349,7 +1411,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_XBOX] = "Xbox",
|
||||
[UI_MENU_PROMPTS_PC] = "PC",
|
||||
[UI_MENU_MOUSE_MODE] = "Câmera com o mouse",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direta: a câmera gira o quanto o mouse se move, como num jogo de PC, sem zona morta nem curva do analógico (jogo 1.4.3; as outras versões usam a emulação do analógico). Emulação do analógico: o mouse age como o analógico direito.",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direta: a câmera gira o quanto o mouse se move, como num jogo de PC, sem zona morta nem curva do analógico (jogos 1.4.3 e 1.2.1; o 1.1.0 usa a emulação do analógico). Emulação do analógico: o mouse age como o analógico direito.",
|
||||
[UI_MENU_MOUSE_STICK] = "Emulação do analógico",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Direta",
|
||||
[UI_MENU_MENU_CURSOR] = "Mouse nos menus do jogo",
|
||||
@@ -1357,7 +1419,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_MISSING] = "faltam os arquivos dos ícones: execute a instalação de novo",
|
||||
[UI_MENU_PROMPTS_FAILED] = "arquivos dos ícones danificados: execute a instalação de novo",
|
||||
[UI_MENU_PROMPTS_RESTART] = "os ícones mudam após reiniciar",
|
||||
[UI_MENU_PROMPTS_OFF_SHAPE] = "desligados numa resolução de jogo que não é 16:9",
|
||||
[UI_MENU_PROMPTS_OFF_SHAPE] = "desligados nesta resolução de jogo (sem layout feito para ela)",
|
||||
[UI_MENU_PROMPTS_OFF_MODS] = "desligados: um mod de RomFS substitui os arquivos deles",
|
||||
[UI_MENU_PROMPTS_OFF_PATCHES] = "desligados enquanto TOTK_ROMFS_PATCHES está definido",
|
||||
[UI_MENU_RESTART] = "Reiniciar agora",
|
||||
@@ -1471,6 +1533,10 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_TAB_MENU] = "菜单",
|
||||
[UI_MENU_TAB_ADVANCED] = "高级",
|
||||
[UI_MENU_TAB_MODS] = "模组",
|
||||
[UI_MENU_PLAY_TIME] = "游戏时间",
|
||||
[UI_MENU_PLAY_TIME_DESC] = "所有运行累计的游戏时间,仅在游戏窗口处于焦点且游戏正在绘制画面时计时。"
|
||||
"保存在配置文件夹的 playtime.txt 中,与存档放在一起。",
|
||||
[UI_MENU_PLAY_TIME_HM] = "%llu 小时 %02u 分钟", [UI_MENU_PLAY_TIME_SESSION] = "本次运行:%s",
|
||||
[UI_MENU_MODS_BUILTIN] = "移植版内置", [UI_MENU_MODS_INSTALLED] = "已安装的模组",
|
||||
[UI_MENU_MODS_NONE] = "还没有模组。把模组解压到 mods 文件夹,每个模组一个文件夹,里面放 romfs 文件夹,然后重启游戏。"
|
||||
"TKMM 也可以把合并后的模组导出到那里的一个文件夹。",
|
||||
@@ -1539,6 +1605,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
"游戏的 FSR 会将其缩放到该尺寸。"
|
||||
"高于 100% 会平滑锯齿边缘(超采样),但 GPU 开销很高。",
|
||||
[UI_MENU_RENDER_SCALE_GAME] = "游戏(83%)",
|
||||
[UI_MENU_DYNAMIC_RES] = "动态分辨率",
|
||||
[UI_MENU_DYNAMIC_RES_DESC] = "让游戏把 3D 场景降到 90 % 或 80 %。它依据为 Switch 制定的估算,不考虑你的显卡,因此即使在高性能 PC 上宽场景也会降低。关闭时保持渲染比例。",
|
||||
[UI_MENU_UPSCALER] = "放大到窗口",
|
||||
[UI_MENU_UPSCALER_DESC] = "窗口大于游戏分辨率时画面的放大方式。"
|
||||
"FSR 比双线性更能保留边缘和细节。",
|
||||
@@ -1549,6 +1617,11 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
"用黑边保持画面比例,或拉伸铺满窗口。",
|
||||
[UI_MENU_ASPECT_FIT] = "黑边",
|
||||
[UI_MENU_ASPECT_STRETCH] = "拉伸",
|
||||
[UI_MENU_MOVIE_ASPECT] = "视频画面比例",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "当游戏分辨率不是 16:9 时:用黑边保持过场视频的原有比例,或拉伸铺满整个画面。"
|
||||
"从下一段视频起生效。",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "黑边",
|
||||
[UI_MENU_MOVIE_ASPECT_STRETCH] = "拉伸",
|
||||
[UI_MENU_FOV] = "视野",
|
||||
[UI_MENU_FOV_DESC] = "镜头能看到多大范围,以游戏自身视野的百分比表示。"
|
||||
"仅限游戏版本 1.4.3。",
|
||||
@@ -1582,6 +1655,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "显示器刷新率",
|
||||
[UI_MENU_FPS_DISPLAY_HZ] = "显示器刷新率(%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "帧队列",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "前一帧等待显示时游戏就开始绘制下一帧:在帧率上限下掉帧大幅减少,输入延迟最多增加一帧。立即生效。",
|
||||
[UI_MENU_REALTIME] = "游戏速度跟随时钟",
|
||||
[UI_MENU_REALTIME_DESC] = "按真实时间推进游戏,帧率低时游戏速度也不会变慢。",
|
||||
[UI_MENU_CLOTH_REALTIME] = "布料跟随时钟",
|
||||
@@ -1589,6 +1664,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_HIGHFPS_FIX] = "高帧率修正",
|
||||
[UI_MENU_HIGHFPS_FIX_DESC] = "修正帧率高于 30 时出现异常的物理效果,"
|
||||
"以及帧率高于 60 时一直半透明或不可见的物体。",
|
||||
[UI_MENU_CLIMB_FIX] = "攀爬速度修正",
|
||||
[UI_MENU_CLIMB_FIX_DESC] = "帧率高于 60 时让攀爬墙壁和梯子保持正常速度,林克能爬到顶。",
|
||||
[UI_MENU_GAME_LANG] = "游戏语言",
|
||||
[UI_MENU_GAME_LANG_DESC] = "游戏文本和语音的语言。",
|
||||
[UI_MENU_GAME_LANG_AUTO] = "自动",
|
||||
@@ -1603,6 +1680,9 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
"这样被打断的读取可能会永久卡住。",
|
||||
[UI_MENU_VOLUME] = "音量",
|
||||
[UI_MENU_VOLUME_DESC] = "游戏的主音量。",
|
||||
[UI_MENU_AUDIO_CHANNELS] = "扬声器",
|
||||
[UI_MENU_AUDIO_CHANNELS_DESC] = "立体声或 5.1 环绕声输出。选择自动时,若 Windows 扬声器设置有六个或更多声道则使用 5.1。重启后生效。",
|
||||
[UI_MENU_CHANNELS_AUTO] = "自动", [UI_MENU_CHANNELS_STEREO] = "立体声", [UI_MENU_CHANNELS_51] = "5.1 环绕声",
|
||||
[UI_MENU_MOTION] = "体感操作",
|
||||
[UI_MENU_MOTION_DESC] = "用手柄的陀螺仪瞄准。",
|
||||
[UI_MENU_RUMBLE_STRENGTH] = "振动强度",
|
||||
@@ -1791,6 +1871,10 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_REPLAY_ASYNC] = "GPU 帧准备独立线程",
|
||||
[UI_MENU_REPLAY_ASYNC_DESC] = "在单独的线程而不是游戏线程上为 GPU 准备帧。"
|
||||
"最多增加一帧延迟。",
|
||||
[UI_MENU_REPLAY_WORKERS] = "GPU 帧准备线程数",
|
||||
[UI_MENU_REPLAY_WORKERS_DESC] = "同时在多个线程上为 GPU 录制大帧。CPU 限制帧率时有帮助;需要 8 个或更多逻辑处理器。"
|
||||
"关:在单个线程上录制。",
|
||||
[UI_MENU_REPLAY_WORKERS_NOW] = "当前:%s",
|
||||
[UI_MENU_TIMER_1MS] = "1 ms Windows 计时器",
|
||||
[UI_MENU_TIMER_1MS_DESC] = "将系统计时器精度提高到 1 ms,使帧间隔更平稳。",
|
||||
[UI_MENU_PIPE_WAIT] = "等待中绘制的处理",
|
||||
@@ -1866,7 +1950,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_VK_DYN_VERTEX_DESC] = "强制开关动态顶点输入;默认值取决于显卡。",
|
||||
[UI_MENU_VK_DYN_A2C] = "动态 Alpha 覆盖",
|
||||
[UI_MENU_VK_DYN_A2C_DESC] = "强制开关动态 Alpha 覆盖。默认:AMD RDNA 2 及更早的驱动上为关闭,"
|
||||
"开启会导致人物变黑;其他显卡上为开启。",
|
||||
"开启会导致人物变黑;Intel 集成显卡的 Windows 驱动上也为关闭,开启会使植被出现彩色噪点;"
|
||||
"其他显卡上为开启。",
|
||||
[UI_MENU_VK_SUBGROUP_PIN] = "固定子群大小",
|
||||
[UI_MENU_VK_SUBGROUP_PIN_DESC] = "固定着色器的子群大小。",
|
||||
[UI_MENU_TRACE] = "性能日志",
|
||||
@@ -1955,7 +2040,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_XBOX] = "Xbox",
|
||||
[UI_MENU_PROMPTS_PC] = "PC",
|
||||
[UI_MENU_MOUSE_MODE] = "鼠标视角",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "直接:镜头按鼠标移动的距离转动,与电脑游戏相同,没有摇杆死区和响应曲线(游戏 1.4.3;其他版本使用摇杆模拟)。摇杆模拟:鼠标相当于右摇杆。",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "直接:镜头按鼠标移动的距离转动,与电脑游戏相同,没有摇杆死区和响应曲线(游戏 1.4.3 和 1.2.1;1.1.0 使用摇杆模拟)。摇杆模拟:鼠标相当于右摇杆。",
|
||||
[UI_MENU_MOUSE_STICK] = "摇杆模拟",
|
||||
[UI_MENU_MOUSE_DIRECT] = "直接",
|
||||
[UI_MENU_MENU_CURSOR] = "在游戏菜单中使用鼠标",
|
||||
@@ -1963,7 +2048,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_MISSING] = "缺少按键提示文件:请重新运行安装程序",
|
||||
[UI_MENU_PROMPTS_FAILED] = "按键提示文件已损坏:请重新运行安装程序",
|
||||
[UI_MENU_PROMPTS_RESTART] = "按键提示重启后生效",
|
||||
[UI_MENU_PROMPTS_OFF_SHAPE] = "游戏分辨率不是 16:9 时关闭",
|
||||
[UI_MENU_PROMPTS_OFF_SHAPE] = "此游戏分辨率下关闭(未为其制作布局)",
|
||||
[UI_MENU_PROMPTS_OFF_MODS] = "已关闭:RomFS 模组替换了它们的文件",
|
||||
[UI_MENU_PROMPTS_OFF_PATCHES] = "设置了 TOTK_ROMFS_PATCHES 时关闭",
|
||||
[UI_MENU_RESTART] = "立即重启",
|
||||
|
||||
@@ -35,8 +35,10 @@ typedef enum {
|
||||
UI_MENU_GRAPHICS, // group heading over the resolution rows
|
||||
UI_MENU_OUTPUT_RES, UI_MENU_OUTPUT_RES_DESC,
|
||||
UI_MENU_RENDER_SCALE, UI_MENU_RENDER_SCALE_DESC, UI_MENU_RENDER_SCALE_GAME,
|
||||
UI_MENU_DYNAMIC_RES, UI_MENU_DYNAMIC_RES_DESC,
|
||||
UI_MENU_UPSCALER, UI_MENU_UPSCALER_DESC, UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR,
|
||||
UI_MENU_ASPECT, UI_MENU_ASPECT_DESC, UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH,
|
||||
UI_MENU_MOVIE_ASPECT, UI_MENU_MOVIE_ASPECT_DESC, UI_MENU_MOVIE_ASPECT_KEEP, UI_MENU_MOVIE_ASPECT_STRETCH,
|
||||
UI_MENU_FOV, UI_MENU_FOV_DESC,
|
||||
UI_MENU_SHADOW_RES, UI_MENU_SHADOW_RES_DESC, UI_MENU_SHADOW_RES_GAME,
|
||||
UI_MENU_ANISO, UI_MENU_ANISO_DESC, UI_MENU_ANISO_GAME,
|
||||
@@ -62,12 +64,17 @@ typedef enum {
|
||||
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90,
|
||||
UI_MENU_FPS_120, UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_DISPLAY, UI_MENU_FPS_DISPLAY_HZ,
|
||||
UI_MENU_FRAME_QUEUE, UI_MENU_FRAME_QUEUE_DESC,
|
||||
UI_MENU_REALTIME, UI_MENU_REALTIME_DESC,
|
||||
UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC,
|
||||
UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC,
|
||||
UI_MENU_CLIMB_FIX, UI_MENU_CLIMB_FIX_DESC,
|
||||
UI_MENU_GAME_LANG, UI_MENU_GAME_LANG_DESC, UI_MENU_GAME_LANG_AUTO,
|
||||
UI_MENU_OPERATION_MODE, UI_MENU_OPERATION_MODE_DESC, UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD,
|
||||
UI_MENU_ASSUME_FOCUS, UI_MENU_ASSUME_FOCUS_DESC,
|
||||
UI_MENU_PLAY_TIME, UI_MENU_PLAY_TIME_DESC,
|
||||
UI_MENU_PLAY_TIME_HM, // hours and minutes: "%llu h %02u min" (unsigned long long hours, unsigned minutes)
|
||||
UI_MENU_PLAY_TIME_SESSION, // note under the total: "this run: %s" (the hours and minutes above)
|
||||
|
||||
// Mods: the port's own patches and the player's mods folder (mods.c)
|
||||
UI_MENU_MODS_BUILTIN, UI_MENU_MODS_INSTALLED,
|
||||
@@ -170,6 +177,7 @@ typedef enum {
|
||||
UI_MENU_ASYNC_PREEMPT, UI_MENU_ASYNC_PREEMPT_DESC, UI_MENU_PREEMPT_OFF, UI_MENU_PREEMPT_ON,
|
||||
UI_MENU_PREEMPT_INHERIT,
|
||||
UI_MENU_REPLAY_ASYNC, UI_MENU_REPLAY_ASYNC_DESC,
|
||||
UI_MENU_REPLAY_WORKERS, UI_MENU_REPLAY_WORKERS_DESC, UI_MENU_REPLAY_WORKERS_NOW,
|
||||
UI_MENU_TIMER_1MS, UI_MENU_TIMER_1MS_DESC,
|
||||
UI_MENU_PIPE_WAIT, UI_MENU_PIPE_WAIT_DESC, UI_MENU_PIPE_WAIT_DEFAULT, UI_MENU_PIPE_WAIT_DRAW,
|
||||
UI_MENU_PIPE_SKIP, UI_MENU_PIPE_SKIP_DESC, UI_MENU_PIPE_SKIP_STRICT, UI_MENU_PIPE_SKIP_ANY,
|
||||
|
||||
+105
-7
@@ -44,6 +44,7 @@ typedef struct VkCtx {
|
||||
// VK_FEEDBACK_RESTART (the pass ends and restarts behind the full barrier).
|
||||
int feedback; // VK_FEEDBACK_*
|
||||
uint32_t min_imported_host_alignment; // VK_EXT_external_memory_host
|
||||
VkDeviceSize vma_block; // VMA's block size for heaps over 1 GiB (TOTK_VMA_BLOCK_MIB, vk_device.c)
|
||||
int device_index; // index in vkEnumeratePhysicalDevices
|
||||
} VkCtx;
|
||||
enum { VK_FEEDBACK_OFF, VK_FEEDBACK_RESTART, VK_FEEDBACK_LAYOUT };
|
||||
@@ -58,9 +59,10 @@ typedef struct {
|
||||
int amd; // an AMD GPU (vendor 0x1002), any driver
|
||||
int amd_sc; // AMD's Windows driver branch for RDNA 1 and 2 ("AMD proprietary shader compiler")
|
||||
int mesa; // a Mesa driver the game runs on: RADV, ANV or NVK
|
||||
int intel_igpu_win; // Intel's Windows driver on an integrated GPU (UHD, Iris Xe, Core Ultra graphics)
|
||||
} VkCaps;
|
||||
extern VkCaps vk_caps;
|
||||
void vk_caps_driver(VkPhysicalDevice p); // first, before vkCreateDevice: vk_caps.amd, amd_sc, mesa
|
||||
void vk_caps_driver(VkPhysicalDevice p); // first, before vkCreateDevice: vk_caps.amd, amd_sc, mesa, intel_igpu_win
|
||||
void vk_caps_features(VkPhysicalDeviceFeatures *f); // before vkCreateDevice (TOTK_VK_NO_FLOAT64 clears shaderFloat64)
|
||||
// Before vkCreateDevice, with the driver's graphics pipeline library feature and properties: sets vk_caps.gpl,
|
||||
// 1 = enable the extensions (VK_KHR_pipeline_library, VK_EXT_graphics_pipeline_library) and the feature.
|
||||
@@ -226,6 +228,10 @@ enum { VK_MCLS_NONE, VK_MCLS_HOST_NOACC, VK_MCLS_HOST_UNC, VK_MCLS_HOST_CACHED,
|
||||
enum { VK_MUSE_VB, VK_MUSE_IB, VK_MUSE_UBO, VK_MUSE_SSBO, VK_MUSE_IND, VK_MUSE_COPY_SRC, VK_MUSE_GPU_WRITE,
|
||||
VK_MUSE_CPU_READ, VK_MUSE_N };
|
||||
void vk_mem_count(int cls, int use, uint64_t bytes);
|
||||
// A thread recording a chunk of a submission (vk_exec.c) counts into its own acc from vk_mem_count_local(acc) on, until
|
||||
// vk_mem_count_local(NULL); vk_mem_count_flush adds acc into the census and clears it.
|
||||
void vk_mem_count_local(int64_t (*acc)[VK_MUSE_N]);
|
||||
void vk_mem_count_flush(int64_t (*acc)[VK_MUSE_N]);
|
||||
// TOTK_TRACE=perf dirty census (vk_memory.c): the replay notes each imported pool it binds as a vertex, index, uniform,
|
||||
// storage or indirect buffer; at the start of every submit the pages the CPU wrote in such pools since the previous
|
||||
// submit are counted from the shared page generations and logged as `mem: dirty` every 60 frames. Nothing when tracing
|
||||
@@ -258,10 +264,14 @@ void vk_mem_gpu_wrote_shader(const VkMemSpan *sp, uint64_t size);
|
||||
// it records, ubo bindings take the copy (0: not copied, bind memory), GPU writes of the replay drop the copies they touch.
|
||||
void vk_usnap_begin(const uint64_t *handles, int n, uint64_t fence);
|
||||
void vk_usnap_end(uint64_t fence);
|
||||
int vk_usnap_find(uint64_t addr, uint64_t n, uint64_t align, VkDescriptorBufferInfo *out);
|
||||
// at: the copies dropped at or before it are not found (vk_usnap_drop_at); UINT32_MAX outside a chunked replay.
|
||||
int vk_usnap_find(uint64_t addr, uint64_t n, uint64_t at, uint64_t align, VkDescriptorBufferInfo *out);
|
||||
const uint8_t *vk_usnap_map(void); // a host copy of the current arena, at the offsets vk_usnap_find gives
|
||||
const uint8_t *vk_usnap_bytes(uint64_t addr, uint64_t n); // the copy of [addr, addr+n) as of the submit, NULL: none
|
||||
void vk_usnap_drop(uint64_t addr, uint64_t n); // the replay makes the GPU write [addr, addr+n) of guest memory
|
||||
// The state scan of a chunked replay (vk_exec.c): the op that writes [addr, addr+n) is the at-th that may; the chunks
|
||||
// pass at with the ops they record (VkExec.drop_n) and find the copies only before it.
|
||||
void vk_usnap_drop_at(uint64_t addr, uint64_t n, uint32_t at);
|
||||
uint64_t vk_usnap_gen(void); // changes whenever a binding may resolve differently
|
||||
void vk_usnap_shutdown(void); // vk_exec_draw_shutdown: frees the arenas
|
||||
// vk_mvprev.c: last frame's copy of the scene's object (c7) and bone (c5) blocks, for per-object motion vectors.
|
||||
@@ -280,7 +290,13 @@ void vk_mem_mirror_prologue(VkCommandBuffer cb);
|
||||
// Shadow storage (vk_memory.c): vk_sync.c calls vk_mem_shadow_sync right before each submit; the replay notes every GPU
|
||||
// write into CPU-visible memory at record time (vk_mem_gpu_pending) and reports it at the fence with vk_gpu_written,
|
||||
// which copies the bytes back into guest memory before telling the texture cache (vk_guest_written).
|
||||
void vk_mem_shadow_sync(void);
|
||||
void vk_mem_shadow_sync(uint64_t fence); // fence: the value of the part about to be submitted (0 untracked)
|
||||
// TOTK_SHADOW_DEFER: shadow updates that may not be written by the CPU while the GPU still reads the shadow are queued
|
||||
// by vk_mem_shadow_sync as ranges of guest memory. vk_mem_shadow_deferred says whether there are any;
|
||||
// vk_mem_shadow_prologue records the copies (staging -> shadow buffers) into the prologue command buffer, before
|
||||
// vk_mem_mirror_prologue's copies, which read the shadow.
|
||||
int vk_mem_shadow_deferred(void);
|
||||
void vk_mem_shadow_prologue(VkCommandBuffer cb, uint64_t fence);
|
||||
void vk_mem_gpu_pending(uint64_t addr, uint64_t size);
|
||||
void vk_gpu_written(uint64_t addr, uint64_t size);
|
||||
int vk_mem_flush_add_probe(void); // --probe-vkexec: the flush range list; 0 or the number of the failed check
|
||||
@@ -292,13 +308,24 @@ int vk_mem_class_of(uint64_t addr); // VK_MCLS_* of the pool at ad
|
||||
void vk_mem_census(uint64_t frame); // at each present: pool census and traffic, logged at frame 1800 and,
|
||||
// with TOTK_TRACE=perf, every 1800 frames
|
||||
// VRAM in use and its budget, summed over the DEVICE_LOCAL heaps. exact 1: VK_EXT_memory_budget (the whole process),
|
||||
// 0: this allocator's own allocations against the heap sizes. 0 when unknown.
|
||||
// 0: this allocator's own allocations against the heap sizes. 0 when unknown. Where device memory takes host memory
|
||||
// (vk_mem_host_room) the budget is at most the use plus that room.
|
||||
int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact);
|
||||
// Bytes the system can still give device memory beyond a reserve (TOTK_MEM_RESERVE_MIB) where device memory takes host
|
||||
// memory: a UMA GPU (commit and physical memory), or a discrete GPU whose device-local memory took process commit in
|
||||
// vk_mem_host_probe (commit). UINT64_MAX elsewhere.
|
||||
uint64_t vk_mem_host_room(void);
|
||||
void vk_mem_host_probe(void); // vk_device.c, after the device: measures that for a discrete GPU, once
|
||||
int vk_mem_uma(void); // 1: the device class is uma (vk_mem_class_log), its memory is RAM
|
||||
// VRAM headroom (vk_headroom.c): the VMA callbacks that see new device memory, and the thread that allocates the next
|
||||
// block of each memory type in use ahead of the render thread.
|
||||
const VmaDeviceMemoryCallbacks *vk_headroom_callbacks(void);
|
||||
void vk_headroom_start(void);
|
||||
void vk_headroom_stop(void); // before vmaDestroyAllocator
|
||||
// The texture pool (vk_memory.c vk_texture_image) gets its next block ahead too; pool NULL frees the anchor in it.
|
||||
void vk_headroom_pool(VmaPool pool, uint32_t type, VkDeviceSize block);
|
||||
void vk_headroom_release(void); // after a failed allocation: frees the spare blocks and keeps none for a while;
|
||||
// any thread that holds no VMA lock (takes only the headroom lock)
|
||||
int vk_mem_resolve(uint64_t addr, uint64_t size, VkMemSpan *out); // 1 when all of [addr, addr+size) is backed
|
||||
// Changes whenever vk_mem_resolve plus VK_MEM_USE may give another span for an address: a pool added, finalized or
|
||||
// mapped, a mirror created, refused or forgotten, a submission started.
|
||||
@@ -329,6 +356,17 @@ void vk_defer_image(VkImage img, VmaAllocation alloc); // alloc NULL: vkDestro
|
||||
void vk_defer_view(VkImageView view);
|
||||
void vk_exec_image_gone(VkImage image); // destroy_tex, texture lock held: the replay's views of image go with it
|
||||
void vk_defer_buffer(VkBuffer buf, VmaAllocation alloc);
|
||||
// free(p) once the GPU is past the submission being recorded, like the deferrals above: host records that a recording
|
||||
// may still read through a pointer taken before another thread dropped them (vk_texture.c destroy_tex).
|
||||
void vk_defer_free(void *p);
|
||||
// Device-local scratch for the upload batch (storage, transfer src and dst), from a pool of its own (vk_memory.c).
|
||||
VkResult vk_scratch_buffer(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc);
|
||||
// A game texture's image with memory of its own: the texture pool, or a dedicated allocation (vk_memory.c).
|
||||
VkResult vk_texture_image(const VkImageCreateInfo *ci, VkImage *img, VmaAllocation *alloc);
|
||||
// Staging and scratch bytes the open upload batch holds until it completes. Large texture loads submit the batch early
|
||||
// (vk_texture.c UPLOAD_HELD_MAX) so the GPU starts on it and its buffers come back before the pools need new blocks.
|
||||
VkDeviceSize vk_upload_held(void);
|
||||
void vk_upload_pools_init(void); // vk_device.c, after the allocator: the staging and scratch pools and their blocks
|
||||
// ---- ASTC (vk_astc.c). TOTK_ASTC = auto | gpu | native | cpu | check; see vk_astc.c.
|
||||
enum { VK_ASTC_CPU, VK_ASTC_GPU, VK_ASTC_NATIVE, VK_ASTC_CHECK };
|
||||
int vk_astc_mode(void); // decided once per device (logged); VK_ASTC_CPU before vk_init
|
||||
@@ -352,8 +390,36 @@ void vk_astc_time_enable(int on);
|
||||
int vk_astc_time_get(uint32_t bw, uint32_t bh, uint64_t *texels, uint64_t *calls, double *us); // totals so far
|
||||
void vk_astc_time_summary(void); // logs the totals
|
||||
void vk_astc_release(void); // vk_device.c, before the device goes: frees the decoder's objects
|
||||
void vk_mem_note_submit(uint64_t timeline_value); // vk_sync.c: after each submit that signals ctx->timeline with this value
|
||||
// ---- Block-linear deswizzle on the GPU (vk_deswizzle.c). TOTK_DESWIZZLE = auto | gpu | cpu | check.
|
||||
enum { VK_DESWZ_CPU, VK_DESWZ_GPU, VK_DESWZ_CHECK };
|
||||
int vk_deswz_mode(void); // decided once per device (logged); VK_DESWZ_CPU before vk_init
|
||||
// One level and layer: its storage starts at byte src of the bound source range and covers segs 16-byte segments
|
||||
// (whole blocks, cut at the storage end); its linear copy goes to byte dst of the destination, rows of pitch16 segments,
|
||||
// rows x slices of them. th, td, gobs_x, blocks_y: as tex_deswizzle.c nvn_bl_offset.
|
||||
typedef struct {
|
||||
VkDeviceSize src, dst;
|
||||
uint32_t segs, pitch16, rows, slices, th, td, gobs_x, blocks_y;
|
||||
} VkDeswzRegion;
|
||||
// Records into cb a dispatch per region reading [src_off, src_off + src_size) of src (a storage buffer) and writing
|
||||
// dst (device-local, dst_size bytes). The caller records the barriers around it. 0 when nothing was recorded: no
|
||||
// pipeline, src_off not 16-byte aligned, a range over the device's storage-buffer limit, a region outside the ranges.
|
||||
int vk_deswz_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_off, VkDeviceSize src_size, VkBuffer dst,
|
||||
VkDeviceSize dst_size, const VkDeswzRegion *rg, int n);
|
||||
// 1 when vk_deswz_record will take these regions (their dst offsets aside) with destinations of up to max_dst bytes.
|
||||
int vk_deswz_fits(VkDeviceSize src_off, VkDeviceSize src_size, const VkDeswzRegion *rg, int n, VkDeviceSize max_dst);
|
||||
void vk_deswz_release(void); // vk_device.c, before the device goes
|
||||
// 1 when sp (from vk_mem_resolve) is an imported pool's buffer over guest memory itself, so a GPU read in a submission
|
||||
// sees every CPU write made before the submit; 0 for device memory and shadow storage (synced after the upload batch).
|
||||
int vk_mem_span_live(const VkMemSpan *sp);
|
||||
// vk_sync.c, when it starts recording the submission that will signal ctx->timeline with this value: objects destroyed
|
||||
// from then on are freed once the timeline reaches it.
|
||||
void vk_mem_note_fence(uint64_t timeline_value);
|
||||
void vk_mem_fault_log(void); // vk_device_fault_report: the deferred-free state, lock-free
|
||||
void vk_mem_collect(void); // frees what has completed
|
||||
// After a failed allocation, on a thread that may hold the texture lock and the upload lock (cmd_lock) but no VMA
|
||||
// lock: frees the headroom blocks and the completed upload batches and deferred objects, and requests the release
|
||||
// of released textures at the next submit (rt_oom_request).
|
||||
void vk_oom_release(void);
|
||||
typedef struct { uint64_t imported_pools, physical_pools, virtual_pools, import_failures, upload_batches; } VkMemStats;
|
||||
void vk_mem_stats(VkMemStats *s);
|
||||
void vk_mem_shutdown(void); // before vk_shutdown: waits idle, frees every memory and texture object
|
||||
@@ -384,12 +450,16 @@ typedef struct {
|
||||
uint8_t scale; // 1, or TOTK_SHADOW_SCALE for a shadow map made larger than the game's (vk_texture.c
|
||||
// shadow_scale_of): width and height above are the larger ones, the game's regions
|
||||
// are multiplied by it
|
||||
uint8_t gone; // the entry was dropped (vk_texture.c destroy_tex, under the texture lock): image and
|
||||
// record live until the submission being recorded is done, and nothing may be cached
|
||||
// by its image handle, which the driver can hand out again after that
|
||||
} VkTexImage;
|
||||
// The window texture the backend presents (vk_backend.c): *image and *format become the float picture the game copied
|
||||
// into it, when the last draw into it was that copy and the swapchain is HDR10.
|
||||
void vk_hdr_present_source(const VkTexImage *win, VkImage *image, VkFormat *format);
|
||||
void vk_hdr_image_gone(VkImage image);
|
||||
const VkTexImage *vk_texture_get(const tex_t *t); // created and made current; NULL for BUFFER or unmapped formats
|
||||
const VkTexImage *vk_texture_revive(const tex_t *t, uint32_t pos); // the entry vk_texture_get would return if it exists, made live at op pos
|
||||
// A texture slot; a slot whose texture was finalized follows the live texture at the same storage (hle_nvn.c).
|
||||
const VkTexImage *vk_texture_by_id(int id, view_t *view, int *has_view);
|
||||
// The replay looks up many slots per draw: it takes the texture lock once around them (vk_texture_lock) and uses the
|
||||
@@ -397,6 +467,12 @@ const VkTexImage *vk_texture_by_id(int id, view_t *view, int *has_view);
|
||||
void vk_texture_lock(void);
|
||||
void vk_texture_unlock(void);
|
||||
const VkTexImage *vk_texture_by_id_locked(int id, view_t *view, int *has_view);
|
||||
// 1 when slot id's entry had its freshness check in this submission, so vk_texture_by_id_locked would only look it up.
|
||||
// No lock: a context of a chunked replay asks before taking the texture lock.
|
||||
int vk_texture_slot_checked(int id);
|
||||
// 1 when the state scan revived a texture at the slot's storage in this submission: the slot's answer depends on the op
|
||||
// a chunk records, so no memo holds it (vk_exec_draw.c); a chunk sees every revival up to its last op.
|
||||
int vk_texture_slot_moves(int id);
|
||||
VkSampler vk_sampler_by_id_locked(int id);
|
||||
// The slot's sampler with nearest filtering and no anisotropy: a depth texture read without a compare lands on one texel
|
||||
// on the Switch although the game binds a linear sampler, and MacNativeGames samples it the same way. VK_NULL_HANDLE
|
||||
@@ -428,6 +504,7 @@ void vk_texture_stats(uint64_t *images, uint64_t *uploads, uint64_t *reuploads);
|
||||
// (images not bound into pool memory).
|
||||
typedef struct { uint64_t live, released, own_live, own_released, bytes_live, bytes_released; } VkTexCensus;
|
||||
void vk_texture_census(VkTexCensus *s);
|
||||
void vk_texture_released_log(uint64_t frame); // mem: released lines of vram_log
|
||||
void vk_texture_shutdown(void); // vk_mem_shutdown calls it
|
||||
void vk_texture_new_submit(void); // a queue submit starts: draw bindings check storage again
|
||||
uint32_t vk_texture_epoch(void); // the write-watch snapshot epoch vk_texture_new_submit started
|
||||
@@ -458,9 +535,22 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t
|
||||
// batch) without a signal, and returns a new begun one for the rest of the submission. The submission's timeline value
|
||||
// is signalled by its last part, whose signal covers every earlier part in submission order.
|
||||
VkCommandBuffer vk_sync_split(void);
|
||||
// Inside vk_exec_record: submits cb, a part recorded and ended on another thread, after the upload batch and the
|
||||
// mirror copies queued so far, without a signal (the submitting thread, in the order of the submission).
|
||||
void vk_sync_part(VkCommandBuffer cb);
|
||||
// GPU time of a part recorded on another thread (vk_sync.c "GPU time"): the start timestamp after vkBeginCommandBuffer,
|
||||
// the pair or -1; the end before vkEndCommandBuffer.
|
||||
int vk_sync_ts_open(VkCommandBuffer cb);
|
||||
void vk_sync_ts_close(VkCommandBuffer cb, int pair, uint64_t fence);
|
||||
// Runs fn(arg) on the completion thread once fence is done on the GPU (event writes, counter reports, sync state).
|
||||
// Callbacks run in the order they were added, after those of earlier values; a value already done runs at once there.
|
||||
void vk_completion_add(uint64_t fence, void (*fn)(void *), void *arg);
|
||||
// Callbacks of a part recorded on another thread: between vk_completion_defer(d) and vk_completion_defer(NULL) the
|
||||
// calling thread's vk_completion_add calls collect in d, and vk_completion_flush adds them, in their order, when the
|
||||
// part takes its place in the submission (vk_exec.c record_chunks).
|
||||
typedef struct { void *items; int n, cap; } VkDeferred;
|
||||
void vk_completion_defer(VkDeferred *d);
|
||||
void vk_completion_flush(VkDeferred *d);
|
||||
// gfx_vulkan_backend (vk_backend.c): presents go to a present thread and run after the submissions made before them.
|
||||
void rdoc_init(void); // runtime/rdoc.c: TOTK_RDOC debug capture; before vkCreateInstance
|
||||
int rdoc_active(void);
|
||||
@@ -504,6 +594,7 @@ void vk_pipe_state_defaults(VkPipeState *s); // NVN defaults of every state ob
|
||||
enum { VK_PIPE_SUPPORT = GFX_RES_KINDS }; // VkPipeBinding.kind of the support buffer
|
||||
#define VK_PIPE_ZERO_BINDING 16 // vertex binding of shader inputs without an attribute: bind a zeroed
|
||||
// buffer of at least 16 bytes there (stride 0)
|
||||
#define VK_PIPE_ALL_BINDINGS 0x1ffffu // every vertex binding, streams 0-15 and the zero binding
|
||||
typedef struct {
|
||||
uint16_t binding; // in its set
|
||||
uint8_t stage; // GFX_STAGE_* that declared it (support buffer: the first stage)
|
||||
@@ -556,8 +647,15 @@ VkPipeline vk_pipeline_compute(const GfxShader *cs, const VkPipeInfo **info);
|
||||
// has them dynamic, depth clamp, polygon mode, blend enables, equations, write masks and alpha to coverage. The replay
|
||||
// sets the rest: vkCmdSetViewportWithCount, vkCmdSetScissorWithCount, depth bias, blend constants, stencil compare and
|
||||
// write masks and references; vertex buffers with vkCmdBindVertexBuffers2 (strides NULL when
|
||||
// vk_pipeline_vertex_input_dynamic(), else from VkPipeState.vstream).
|
||||
void vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s);
|
||||
// vk_pipeline_vertex_input_dynamic(), else from VkPipeState.vstream). Returns the vertex bindings (bit per binding) the
|
||||
// state's vertex input reads, whether set here or made into the pipeline.
|
||||
uint32_t vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s);
|
||||
// The replay's variant (one thread, TOTK_DYN_DELTA): valid 1 says every graphics pipeline bound in cb since the last call
|
||||
// was one of vk_pipeline_graphics*'s, which all declare the same dynamic states, and nothing else set them, so cb still
|
||||
// holds what that call recorded (Vulkan keeps dynamic state across such binds for the life of the command buffer);
|
||||
// then only the commands whose values differ are recorded. valid 0 (a new command buffer, or one whose state another
|
||||
// recorder changed) records everything. *cmds: the commands recorded (valid 0: 1). Returns as vk_pipeline_set_dynamic.
|
||||
uint32_t vk_pipeline_set_dynamic_delta(VkCommandBuffer cb, const VkPipeState *s, int valid, uint32_t *cmds);
|
||||
int vk_pipeline_vertex_input_dynamic(void); // 1: vertex input is VK_EXT_vertex_input_dynamic_state
|
||||
// Translator answers from the bound state: attr_type from the vertex attribute formats (vs), frag_out_type from the
|
||||
// colour target formats (fs). Other fields are left as they are; either pointer may be NULL.
|
||||
|
||||
+7
-9
@@ -300,17 +300,19 @@ static int create_pipeline(VkCtx *c) {
|
||||
// TOTK_ASTC_BC=1: the BC3 encoder, when the device samples BC3 (every desktop GPU does); 0 leaves ASTC as RGBA8.
|
||||
// Unset: on when the largest device-local heap is under 5 GiB. There RGBA8 ASTC, four times the size of BC3, fills
|
||||
// video memory (a GTX 1650 4 GiB went from 10-23 to 3-12 ms of GPU time a frame, issue #87); BC3 loses some precision
|
||||
// (normal maps), so GPUs with more memory keep RGBA8.
|
||||
// (normal maps), so GPUs with more memory keep RGBA8. A UMA GPU's heap is the machine's RAM however large it reads
|
||||
// (10.6 GiB on an ROG Ally X, 14.9 GiB on a Radeon 880M), so there BC3 is on too: on the RX 9070, BC3 took 737 MiB off
|
||||
// VMA's device-local allocations after a minute in the field, at the same GPU time.
|
||||
static int create_bc_pipeline(VkCtx *c) {
|
||||
const char *e = cfg_get("TOTK_ASTC_BC");
|
||||
VkDeviceSize vram = 0;
|
||||
for (uint32_t h = 0; h < c->mem_props.memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
|
||||
if (c->mem_props.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT && c->mem_props.memoryHeaps[h].size > vram)
|
||||
vram = c->mem_props.memoryHeaps[h].size;
|
||||
int on = e && *e ? *e != '0' : vram < (5ull << 30);
|
||||
int on = e && *e ? *e != '0' : vram < (5ull << 30) || vk_mem_uma();
|
||||
if (!(e && *e))
|
||||
rt_log("vk: TOTK_ASTC_BC unset: BC3 %s (largest device-local heap %llu MiB)", on ? "on" : "off",
|
||||
(unsigned long long)(vram >> 20));
|
||||
rt_log("vk: TOTK_ASTC_BC unset: BC3 %s (largest device-local heap %llu MiB%s)", on ? "on" : "off",
|
||||
(unsigned long long)(vram >> 20), vk_mem_uma() ? ", uma: system memory" : "");
|
||||
if (!on) return 0;
|
||||
VkFormatProperties p;
|
||||
vkGetPhysicalDeviceFormatProperties(c->phys, VK_FORMAT_BC3_SRGB_BLOCK, &p);
|
||||
@@ -463,13 +465,9 @@ int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, cons
|
||||
cap_in = (cap_in + al - 1) / al * al;
|
||||
cap_out = (cap_out + al - 1) / al * al;
|
||||
if (!bc) cap_bc = 0;
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = cap_in + cap_out + cap_bc,
|
||||
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
||||
VmaAllocationCreateInfo aci = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
VkBuffer sb;
|
||||
VmaAllocation sa;
|
||||
if (vmaCreateBuffer(c->vma, &bi, &aci, &sb, &sa, NULL) != VK_SUCCESS) {
|
||||
if (vk_scratch_buffer(cap_in + cap_out + cap_bc, &sb, &sa) != VK_SUCCESS) {
|
||||
if (plat_inc32(&n_fail) <= 16)
|
||||
rt_log("vk: ASTC scratch buffer of %llu bytes failed: texture left undecoded",
|
||||
(unsigned long long)(cap_in + cap_out));
|
||||
|
||||
@@ -49,6 +49,7 @@ static void wait_until(uint64_t at) {
|
||||
|
||||
static void present_main(void *arg) {
|
||||
(void)arg;
|
||||
perf_core_thread(PCORE_PRESENT);
|
||||
uint64_t last = 0, last_queued = 0;
|
||||
plat_mutex_lock(&pl);
|
||||
for (;;) {
|
||||
@@ -159,6 +160,7 @@ static PlatCond rcv = PLAT_COND_INIT; // item queued, item taken, item done
|
||||
static ReplayItem rq[RQ_CAP];
|
||||
static int rq_head, rq_n, r_busy, r_started;
|
||||
static uint64_t r_presents_queued, r_presents_done, r_submit_done, game_frames;
|
||||
static uint64_t r_value_done; // under rl: the newest timeline value the replay thread submitted
|
||||
// Under rl: calls, calls that waited and the time waited per cause, since the last replay_log line and over the run.
|
||||
static uint64_t dr_calls[DR_N], dr_waits[DR_N], dr_ns[DR_N], dr_tcalls[DR_N], dr_twaits[DR_N], dr_tns[DR_N];
|
||||
// TOTK_REPLAY_CHECK: ReadTexels compared with a read after a full drain (under rl): checked, differing, differing
|
||||
@@ -191,6 +193,15 @@ uint64_t gfx_replayed_submits(void) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// TOTK_SYNC_AHEAD (nvn_api_core.c nvnSyncWait): waits until the replay thread submitted value v, so every shadow copy
|
||||
// of v was made before the game writes memory v reads. The caller has given its core up.
|
||||
void gfx_replay_wait_value(uint64_t v) {
|
||||
if (!gfx_replay_async()) return;
|
||||
plat_mutex_lock(&rl);
|
||||
while (r_value_done < v) plat_cond_wait(&rcv, &rl);
|
||||
plat_mutex_unlock(&rl);
|
||||
}
|
||||
|
||||
static uint64_t submit_sync(const uint64_t *handles, int n) {
|
||||
vk_sync_start();
|
||||
vk_present_drain();
|
||||
@@ -201,6 +212,7 @@ static uint64_t submit_sync(const uint64_t *handles, int n) {
|
||||
static void replay_main(void *arg) {
|
||||
(void)arg;
|
||||
perf_replay_thread();
|
||||
perf_core_thread(PCORE_REPLAY);
|
||||
plat_mutex_lock(&rl);
|
||||
for (;;) {
|
||||
while (!rq_n) plat_cond_wait(&rcv, &rl);
|
||||
@@ -225,6 +237,7 @@ static void replay_main(void *arg) {
|
||||
present_image(it.tex, it.has_crop ? it.crop : NULL, it.at, it.queued, frame, it.value);
|
||||
}
|
||||
plat_mutex_lock(&rl);
|
||||
if (it.kind != RI_RELEASE) r_value_done = it.value;
|
||||
if (it.kind == RI_SUBMIT) r_submit_done = it.submit_no;
|
||||
else r_presents_done++;
|
||||
r_busy = 0;
|
||||
@@ -326,6 +339,8 @@ void gfx_exit_log(void) {
|
||||
}
|
||||
|
||||
static void vk_be_present(const tex_t *t, const int crop[4], uint64_t at_ns) {
|
||||
static _Thread_local int pcore; // the game's render thread: the one that presents
|
||||
if (!pcore) pcore = 1, perf_core_thread(PCORE_RENDER);
|
||||
vk_sync_start();
|
||||
if (!gfx_replay_async()) {
|
||||
uint64_t frame = __atomic_add_fetch(&gfx_counters.frames, 1, __ATOMIC_RELAXED);
|
||||
|
||||
@@ -45,6 +45,8 @@ void vk_caps_driver(VkPhysicalDevice p) {
|
||||
vk_caps.amd_sc = drv.driverID == VK_DRIVER_ID_AMD_PROPRIETARY && strstr(drv.driverInfo, "shader compiler");
|
||||
vk_caps.mesa = drv.driverID == VK_DRIVER_ID_MESA_RADV || drv.driverID == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA ||
|
||||
drv.driverID == VK_DRIVER_ID_MESA_NVK;
|
||||
vk_caps.intel_igpu_win = drv.driverID == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS &&
|
||||
p2.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU;
|
||||
}
|
||||
|
||||
// TOTK_GPL=0 turns graphics pipeline libraries off; they are on wherever the driver has fast linking. AMD's RDNA 1/2
|
||||
|
||||
+74
-49
@@ -7,9 +7,13 @@
|
||||
// passes behind nearby geometry: the small shadows at feet, under stones and in grass that the shadow maps are too
|
||||
// coarse for. Transparent surfaces, the sky and post-processing come after, untouched.
|
||||
//
|
||||
// Inputs from the game, read from the fragment stage's uniform block c4 bound in the composite pass (guest memory,
|
||||
// so it may be one frame newer than the replay): the projection scales c4[7].x and c4[8].y, the far plane c4[14].y,
|
||||
// and the first sun cascade's depth row c4[44].xyz, whose negated direction points from a view-space position toward
|
||||
// vk_contact_plan decides that in replay order and vk_contact_exec records it: a chunked replay's scan
|
||||
// plans the march (vk_exec.c scan_contact) and the chunk that holds its op records it.
|
||||
//
|
||||
// Inputs from the game, read where the march is planned from the fragment stage's uniform block c4 bound in the
|
||||
// composite pass (guest memory, so it may be one frame newer than the replay): the projection scales c4[7].x and
|
||||
// c4[8].y, the far plane c4[14].y, and the first sun cascade's depth row c4[44].xyz, whose negated direction points from
|
||||
// a view-space position toward
|
||||
// the sun (the G-buffer shaders compare dot(c4[44].xyz, pos) + c4[44].w with the cascade map). The scene colour images
|
||||
// get STORAGE usage when the setting is on (vk_texture.c create_tex).
|
||||
#include "vk.h"
|
||||
@@ -23,22 +27,22 @@
|
||||
#include "vk_exec.h"
|
||||
|
||||
static struct {
|
||||
int tried, ok, strength, debug, logged;
|
||||
int read, tried, ok, strength, debug, logged;
|
||||
PFN_vkCmdPushDescriptorSetKHR push;
|
||||
VkDescriptorSetLayout dsl;
|
||||
VkPipelineLayout layout;
|
||||
VkPipeline pipe;
|
||||
VkSampler sampler;
|
||||
const VkTexImage *scene, *depth; // the last G-buffer's scene colour and linear depth
|
||||
uint32_t w, h;
|
||||
VkExecTarget scene, depth; // the last G-buffer's scene colour and linear depth
|
||||
int pending; // the composite into scene ran; the march waits for the next pass
|
||||
uint64_t c4;
|
||||
} C;
|
||||
|
||||
int vk_contact_strength(void) {
|
||||
if (!C.tried) {
|
||||
if (!C.read) { // once: the scan asks at every draw
|
||||
int s = cfg_int("TOTK_CONTACT_SHADOWS");
|
||||
C.strength = s < 0 ? 0 : s > 100 ? 100 : s;
|
||||
C.read = 1;
|
||||
}
|
||||
return C.strength;
|
||||
}
|
||||
@@ -90,16 +94,16 @@ static int init(VkCtx *c) {
|
||||
}
|
||||
|
||||
// A G-buffer pass: a B10G11R11 scene colour, an R32F linear depth and a D32F depth, all one size, at least 640 wide.
|
||||
static int is_gbuffer(const VkExec *ex, const VkTexImage **scene, const VkTexImage **depth) {
|
||||
static int is_gbuffer(const VkExec *ex, const VkExecTarget **scene, const VkExecTarget **depth) {
|
||||
*scene = *depth = NULL;
|
||||
if (ex->pass_noatt || ex->pass_w < 640 || ex->depth.format != VK_FORMAT_D32_SFLOAT || ex->depth.level) return 0;
|
||||
for (uint32_t i = 0; i < ex->pass_ncolor && i < 8; i++) {
|
||||
const VkExecTarget *t = &ex->color[i];
|
||||
if (!t->image || t->level || t->width != ex->pass_w || t->height != ex->pass_h) continue;
|
||||
if (t->format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && !*scene) *scene = t->img;
|
||||
if (t->format == VK_FORMAT_R32_SFLOAT && !*depth) *depth = t->img;
|
||||
if (t->format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && !*scene) *scene = t;
|
||||
if (t->format == VK_FORMAT_R32_SFLOAT && !*depth) *depth = t;
|
||||
}
|
||||
return *scene && *depth && ((*scene)->usage & VK_IMAGE_USAGE_STORAGE_BIT);
|
||||
return *scene && *depth && (*scene)->img && ((*scene)->img->usage & VK_IMAGE_USAGE_STORAGE_BIT);
|
||||
}
|
||||
|
||||
static void mem_barrier(VkCommandBuffer cb) {
|
||||
@@ -112,30 +116,66 @@ static void mem_barrier(VkCommandBuffer cb) {
|
||||
vkCmdPipelineBarrier2(cb, &d);
|
||||
}
|
||||
|
||||
void vk_contact_flush(VkExec *ex) {
|
||||
if (!C.pending) return;
|
||||
// The lighting composite: the remembered scene colour as the only target, with a D16 depth of its size.
|
||||
static int is_composite(const VkExec *ex) {
|
||||
return C.scene.image && ex->pass_ncolor >= 1 && ex->color[0].image == C.scene.image && !ex->color[0].level &&
|
||||
ex->depth.format == VK_FORMAT_D16_UNORM && ex->pass_w == C.scene.width && ex->pass_h == C.scene.height;
|
||||
}
|
||||
|
||||
int vk_contact_plan(const VkExec *ex, int pass, VkExecJob *j) {
|
||||
if (!vk_contact_strength()) return 0;
|
||||
if (pass) {
|
||||
const VkExecTarget *scene, *depth;
|
||||
// The camera block: c4 of the fragment stage, 0x940 bytes. The composite binds it only after its pass begins, so
|
||||
// the newest one seen at any pass start serves.
|
||||
const p_bind_buf *b = &ex->ubo[1][1];
|
||||
if (b->addr && b->size == 0x940) C.c4 = b->addr;
|
||||
if (is_gbuffer(ex, &scene, &depth)) {
|
||||
C.pending = 0;
|
||||
C.scene = *scene, C.depth = *depth;
|
||||
return 0;
|
||||
}
|
||||
if (is_composite(ex)) {
|
||||
C.pending = C.c4 != 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (!C.pending) return 0;
|
||||
C.pending = 0;
|
||||
if (ex->in_pass) return;
|
||||
const float *u = (const float *)(uintptr_t)C.c4;
|
||||
float lx = -u[44 * 4 + 0], ly = -u[44 * 4 + 1], lz = -u[44 * 4 + 2], ll = sqrtf(lx * lx + ly * ly + lz * lz);
|
||||
float px = u[7 * 4 + 0], py = u[8 * 4 + 1], zfar = u[14 * 4 + 1];
|
||||
if (!(ll > 1e-6f) || !(px > 0) || !(py > 0) || !(zfar > 1)) return;
|
||||
VkImageView in = vk_exec_target_view(C.depth, VK_FORMAT_R32_SFLOAT, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
if (!(ll > 1e-6f) || !(px > 0) || !(py > 0) || !(zfar > 1)) return 0;
|
||||
float w = (float)C.scene.width, h = (float)C.scene.height;
|
||||
*j = (VkExecJob){.scene = C.scene, .depth = C.depth,
|
||||
.pc = {px, py, zfar, (float)C.strength / 100.0f,
|
||||
lx / ll, ly / ll, lz / ll, 0.6f,
|
||||
w, h, 1.0f / w, 1.0f / h,
|
||||
0, 0, 0, 0}};
|
||||
return 1;
|
||||
}
|
||||
|
||||
void vk_contact_exec(VkExec *ex, const VkExecJob *j) {
|
||||
if (!C.tried) init(vk_ctx());
|
||||
if (!C.ok) return;
|
||||
VkImageView in = vk_exec_target_view(j->depth.img, VK_FORMAT_R32_SFLOAT, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
0, 0, 1, 0);
|
||||
VkImageView out = vk_exec_target_view(C.scene, VK_FORMAT_B10G11R11_UFLOAT_PACK32, VK_IMAGE_VIEW_TYPE_2D,
|
||||
VkImageView out = vk_exec_target_view(j->scene.img, VK_FORMAT_B10G11R11_UFLOAT_PACK32, VK_IMAGE_VIEW_TYPE_2D,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1, 0);
|
||||
if (!in || !out) return;
|
||||
// A full barrier that also takes the images in the feedback layout back to GENERAL (vk_exec.c barrier_to): the
|
||||
// G-buffer's linear depth and scene colour are such images once a draw sampled them in their own pass, and the march
|
||||
// binds them in GENERAL. A memory barrier alone left them in ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT.
|
||||
vk_exec_barrier(ex);
|
||||
VkCommandBuffer cb = ex->cb;
|
||||
float pcv[16] = {px, py, zfar, (float)C.strength / 100.0f,
|
||||
lx / ll, ly / ll, lz / ll, 0.6f,
|
||||
(float)C.w, (float)C.h, 1.0f / (float)C.w, 1.0f / (float)C.h,
|
||||
(float)C.debug, 0, 0, 0};
|
||||
float pcv[16];
|
||||
memcpy(pcv, j->pc, sizeof pcv);
|
||||
pcv[12] = (float)C.debug;
|
||||
if (!C.logged) {
|
||||
C.logged = 1;
|
||||
rt_log("vk: contact shadows on a %ux%u scene, sun toward %.3f %.3f %.3f (view space), far %g", C.w, C.h,
|
||||
pcv[4], pcv[5], pcv[6], zfar);
|
||||
rt_log("vk: contact shadows on a %ux%u scene, sun toward %.3f %.3f %.3f (view space), far %g", j->scene.width, j->scene.height,
|
||||
pcv[4], pcv[5], pcv[6], pcv[2]);
|
||||
}
|
||||
mem_barrier(cb); // the compose passes' writes, before the march reads and writes them
|
||||
VkDescriptorImageInfo ii = {C.sampler, in, VK_IMAGE_LAYOUT_GENERAL}, oi = {VK_NULL_HANDLE, out, VK_IMAGE_LAYOUT_GENERAL};
|
||||
VkWriteDescriptorSet w[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 0, .descriptorCount = 1,
|
||||
@@ -145,36 +185,21 @@ void vk_contact_flush(VkExec *ex) {
|
||||
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, C.pipe);
|
||||
rec_push(C.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, C.layout, 0, 2, w);
|
||||
vkCmdPushConstants(cb, C.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pcv, pcv);
|
||||
vkCmdDispatch(cb, (C.w + 7) / 8, (C.h + 7) / 8, 1);
|
||||
vkCmdDispatch(cb, (j->scene.width + 7) / 8, (j->scene.height + 7) / 8, 1);
|
||||
mem_barrier(cb); // and what follows sees the darkened colour
|
||||
ex->recorded += 6;
|
||||
vk_exec_draw_new_cb(); // the game's bound compute pipeline and descriptors were replaced: bind them again
|
||||
}
|
||||
|
||||
// The lighting composite: the remembered scene colour as the only target, with a D16 depth of its size.
|
||||
static int is_composite(const VkExec *ex) {
|
||||
return C.scene && ex->pass_ncolor >= 1 && ex->color[0].img == C.scene && !ex->color[0].level &&
|
||||
ex->depth.format == VK_FORMAT_D16_UNORM && ex->pass_w == C.w && ex->pass_h == C.h;
|
||||
vk_exec_draw_new_cb(ex); // the game's bound compute pipeline and descriptors were replaced: bind them again
|
||||
}
|
||||
|
||||
// The sequential replay's hooks; a chunk leaves the marches to the scan.
|
||||
void vk_contact_pass_begin(VkExec *ex) {
|
||||
if (!C.tried && !init(vk_ctx())) return;
|
||||
if (!C.ok) return;
|
||||
const VkTexImage *scene, *depth;
|
||||
// The camera block: c4 of the fragment stage, 0x940 bytes. The composite binds it only after its pass begins, so
|
||||
// the newest one seen at any pass start serves.
|
||||
const p_bind_buf *b = &ex->ubo[1][1];
|
||||
if (b->addr && b->size == 0x940) C.c4 = b->addr;
|
||||
if (is_gbuffer(ex, &scene, &depth)) {
|
||||
C.pending = 0;
|
||||
C.scene = scene, C.depth = depth, C.w = ex->pass_w, C.h = ex->pass_h;
|
||||
return;
|
||||
}
|
||||
if (is_composite(ex)) {
|
||||
C.pending = C.c4 != 0;
|
||||
return;
|
||||
}
|
||||
vk_contact_flush(ex);
|
||||
VkExecJob j;
|
||||
if (!ex->chunked && vk_contact_plan(ex, 1, &j)) vk_contact_exec(ex, &j);
|
||||
}
|
||||
|
||||
void vk_contact_flush(VkExec *ex) {
|
||||
VkExecJob j;
|
||||
if (!ex->chunked && C.pending && vk_contact_plan(ex, 0, &j)) vk_contact_exec(ex, &j);
|
||||
}
|
||||
|
||||
void vk_contact_release(void) {
|
||||
|
||||
+3
-3
@@ -91,7 +91,7 @@ void vk_crumbs_report(void) {
|
||||
// records changed pipeline state also notes the shaders of a pass's first draw. A full barrier already precedes every
|
||||
// pass and dispatch (vk_exec.c vk_exec_pass_begin, vk_exec_draw.c op_dispatch), so the end-of-pipe write adds no wait.
|
||||
static VkPassMark ring[VK_PASS_MARKS];
|
||||
static uint32_t seq; // marks made; only the replay thread writes it
|
||||
static uint32_t seq; // marks made (atomic: the recording threads of a chunked replay)
|
||||
|
||||
// TOTK_PASS_CRUMBS: unset, buffer marks where VK_AMD_buffer_marker exists; 1, also NVIDIA checkpoints; 0, neither.
|
||||
int vk_pass_marks_want_nv(void) {
|
||||
@@ -124,8 +124,8 @@ void vk_pass_mark_cmd(VkCommandBuffer cb, uint32_t id, int end) {
|
||||
|
||||
VkPassMark *vk_pass_mark_begin(VkCommandBuffer cb, int kind, uint64_t fence) {
|
||||
if (!vk_pass_marks_on || !cb) return NULL;
|
||||
uint32_t id = ++seq;
|
||||
if (!id) id = ++seq; // 0 means no mark
|
||||
uint32_t id = __atomic_add_fetch(&seq, 1, __ATOMIC_RELAXED); // the recording threads of a chunked replay
|
||||
if (!id) id = __atomic_add_fetch(&seq, 1, __ATOMIC_RELAXED); // 0 means no mark
|
||||
VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
|
||||
m->id = id, m->kind = (uint8_t)kind, m->fence = fence, m->draws = UINT32_MAX;
|
||||
rec_pass_mark(cb, id, 0);
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// Block-linear textures deswizzled by the GPU (runtime/shaders/bl_deswizzle.comp): vk_texture.c upload binds the
|
||||
// texture's storage in its pool's imported buffer and lets a compute pass write the linear copy that a buffer-to-image
|
||||
// copy (or the ASTC decoder) then takes, instead of tex_deswizzle.c walking every 16 bytes on the replay thread
|
||||
// (50-236 ms frames on entering new areas, 2026-11-07 play sessions). The same design as MacNativeGames' TotK port
|
||||
// (d2r_gfx nvn_mtl.m: deswizzle from the pool at GPU execution time), written for Vulkan from our CPU layout code.
|
||||
//
|
||||
// TOTK_DESWIZZLE = auto (default) / gpu: the compute pass where the storage is an imported pool (vk_mem_span_live),
|
||||
// the CPU otherwise; cpu: always the CPU; check: gpu, and every upload is also deswizzled on the CPU and compared
|
||||
// (waits for the GPU; for testing only).
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "bl_deswizzle_spv.h"
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
#include "vk_rec.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t src_base, dst_base, segs, pitch16, rows, slices, th, td, gobs_x, blocks_y, nx;
|
||||
} Push;
|
||||
|
||||
static PlatMutex g_lock = PLAT_MUTEX_INIT;
|
||||
static struct {
|
||||
VkDevice dev;
|
||||
int mode; // VK_DESWZ_*; -1 before the first vk_deswz_mode with a device
|
||||
VkDescriptorSetLayout dsl;
|
||||
VkPipelineLayout layout;
|
||||
VkPipeline pipe;
|
||||
PFN_vkCmdPushDescriptorSetKHR push;
|
||||
} G = {.mode = -1};
|
||||
static volatile int64_t n_regions, n_bytes;
|
||||
|
||||
static int create_pipeline(VkCtx *c) {
|
||||
G.push = (PFN_vkCmdPushDescriptorSetKHR)vkGetDeviceProcAddr(c->device, "vkCmdPushDescriptorSetKHR");
|
||||
if (!G.push) return 0;
|
||||
VkDescriptorSetLayoutBinding b[2];
|
||||
for (int i = 0; i < 2; i++)
|
||||
b[i] = (VkDescriptorSetLayoutBinding){.binding = (uint32_t)i, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT};
|
||||
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
||||
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
|
||||
.bindingCount = 2, .pBindings = b};
|
||||
if (vkCreateDescriptorSetLayout(c->device, &dci, NULL, &G.dsl) != VK_SUCCESS) return 0;
|
||||
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push)};
|
||||
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
||||
.pSetLayouts = &G.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
|
||||
if (vkCreatePipelineLayout(c->device, &li, NULL, &G.layout) != VK_SUCCESS) return 0;
|
||||
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof bl_deswizzle_spv,
|
||||
.pCode = bl_deswizzle_spv};
|
||||
VkShaderModule m = VK_NULL_HANDLE;
|
||||
if (vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS) return 0;
|
||||
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
||||
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
|
||||
.layout = G.layout};
|
||||
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &G.pipe);
|
||||
vkDestroyShaderModule(c->device, m, NULL);
|
||||
if (r != VK_SUCCESS) G.pipe = VK_NULL_HANDLE;
|
||||
return r == VK_SUCCESS;
|
||||
}
|
||||
|
||||
int vk_deswz_mode(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c) return VK_DESWZ_CPU;
|
||||
plat_mutex_lock_shared(&g_lock);
|
||||
int m = G.dev == c->device ? G.mode : -1;
|
||||
plat_mutex_unlock_shared(&g_lock);
|
||||
if (m >= 0) return m;
|
||||
plat_mutex_lock(&g_lock);
|
||||
if (G.dev != c->device) { // the old device's objects went with it (probes recreate the device)
|
||||
memset(&G, 0, sizeof G);
|
||||
G.dev = c->device;
|
||||
G.mode = -1;
|
||||
}
|
||||
if (G.mode < 0) {
|
||||
const char *e = cfg_get("TOTK_DESWIZZLE");
|
||||
if (!e || !*e) e = "auto";
|
||||
int want = !strcmp(e, "cpu") || !strcmp(e, "0") ? VK_DESWZ_CPU : !strcmp(e, "check") ? VK_DESWZ_CHECK : VK_DESWZ_GPU;
|
||||
if (strcmp(e, "auto") && strcmp(e, "gpu") && strcmp(e, "1") && strcmp(e, "cpu") && strcmp(e, "0") &&
|
||||
strcmp(e, "check"))
|
||||
rt_log("vk: TOTK_DESWIZZLE=%s is not auto, gpu, cpu or check: auto", e);
|
||||
if (want != VK_DESWZ_CPU && !create_pipeline(c)) {
|
||||
rt_log("vk: the deswizzle compute pass is unavailable on %s: textures deswizzle on the CPU", c->props.deviceName);
|
||||
want = VK_DESWZ_CPU;
|
||||
}
|
||||
G.mode = want;
|
||||
rt_log("vk: block-linear textures deswizzle on the %s%s", want == VK_DESWZ_CPU ? "CPU" : "GPU where the storage is "
|
||||
"an imported pool", want == VK_DESWZ_CHECK ? ", checked against the CPU (TOTK_DESWIZZLE=check)" : "");
|
||||
}
|
||||
m = G.mode;
|
||||
plat_mutex_unlock(&g_lock);
|
||||
return m;
|
||||
}
|
||||
|
||||
static uint32_t max_groups_x(const VkCtx *c) {
|
||||
uint32_t m = c->props.limits.maxComputeWorkGroupCount[0];
|
||||
return m > 65535 ? 65535 : m;
|
||||
}
|
||||
|
||||
// The bound source range starts at the alignment below src_off.
|
||||
static VkDeviceSize src_lo(const VkCtx *c, VkDeviceSize src_off) {
|
||||
VkDeviceSize al = c->props.limits.minStorageBufferOffsetAlignment;
|
||||
if (al < 16) al = 16;
|
||||
return src_off / al * al;
|
||||
}
|
||||
|
||||
int vk_deswz_fits(VkDeviceSize src_off, VkDeviceSize src_size, const VkDeswzRegion *rg, int n, VkDeviceSize max_dst) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c || !n || (src_off & 15) || vk_deswz_mode() == VK_DESWZ_CPU) return 0;
|
||||
VkDeviceSize range = src_off + src_size - src_lo(c, src_off), lim = c->props.limits.maxStorageBufferRange;
|
||||
if (range > lim || max_dst > lim) return 0;
|
||||
uint32_t maxg = max_groups_x(c);
|
||||
for (int i = 0; i < n; i++) { // the 32-bit segment indices of the shader stay inside the bindings
|
||||
uint64_t groups = ((uint64_t)rg[i].segs + 63) / 64, out = (uint64_t)rg[i].pitch16 * rg[i].rows * rg[i].slices;
|
||||
if ((rg[i].src & 15) || rg[i].src + (uint64_t)rg[i].segs * 16 > src_size || !rg[i].segs ||
|
||||
(src_off - src_lo(c, src_off) + rg[i].src) / 16 + rg[i].segs > UINT32_MAX || out > UINT32_MAX ||
|
||||
out * 16 > max_dst || (groups + maxg - 1) / maxg > c->props.limits.maxComputeWorkGroupCount[1])
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int vk_deswz_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_off, VkDeviceSize src_size, VkBuffer dst,
|
||||
VkDeviceSize dst_size, const VkDeswzRegion *rg, int n) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!vk_deswz_fits(src_off, src_size, rg, n, dst_size)) return 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
if ((rg[i].dst & 15) || rg[i].dst + (uint64_t)rg[i].pitch16 * 16 * rg[i].rows * rg[i].slices > dst_size) return 0;
|
||||
plat_mutex_lock_shared(&g_lock);
|
||||
VkPipeline pipe = G.dev == c->device ? G.pipe : VK_NULL_HANDLE;
|
||||
plat_mutex_unlock_shared(&g_lock);
|
||||
if (!pipe) return 0;
|
||||
VkDeviceSize lo = src_lo(c, src_off), range = src_off + src_size - lo;
|
||||
uint32_t maxg = max_groups_x(c);
|
||||
VkDescriptorBufferInfo dbi[2] = {{src, lo, range}, {dst, 0, dst_size}};
|
||||
VkWriteDescriptorSet w[2];
|
||||
for (int i = 0; i < 2; i++)
|
||||
w[i] = (VkWriteDescriptorSet){.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = (uint32_t)i,
|
||||
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.pBufferInfo = &dbi[i]};
|
||||
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
|
||||
rec_push(G.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, G.layout, 0, 2, w);
|
||||
uint64_t bytes = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const VkDeswzRegion *r = &rg[i];
|
||||
uint64_t groups = ((uint64_t)r->segs + 63) / 64;
|
||||
uint32_t nx = groups < maxg ? (uint32_t)groups : maxg, ny = (uint32_t)((groups + nx - 1) / nx);
|
||||
Push pc = {.src_base = (uint32_t)((src_off - lo + r->src) / 16), .dst_base = (uint32_t)(r->dst / 16),
|
||||
.segs = r->segs, .pitch16 = r->pitch16, .rows = r->rows, .slices = r->slices, .th = r->th, .td = r->td,
|
||||
.gobs_x = r->gobs_x, .blocks_y = r->blocks_y, .nx = nx};
|
||||
vkCmdPushConstants(cb, G.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
|
||||
vkCmdDispatch(cb, nx, ny, 1);
|
||||
bytes += (uint64_t)r->segs * 16;
|
||||
}
|
||||
int64_t before = plat_add64(&n_regions, n) - n;
|
||||
plat_add64(&n_bytes, (int64_t)bytes);
|
||||
if (!before || (before + n) / 4096 != before / 4096)
|
||||
rt_log("vk: deswizzle: %lld regions, %.1f MiB of block-linear storage deswizzled on the GPU so far",
|
||||
(long long)(before + n), (double)n_bytes / 1048576.0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void vk_deswz_release(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
plat_mutex_lock(&g_lock);
|
||||
if (c && G.dev == c->device) {
|
||||
if (G.pipe) vkDestroyPipeline(c->device, G.pipe, NULL);
|
||||
if (G.layout) vkDestroyPipelineLayout(c->device, G.layout, NULL);
|
||||
if (G.dsl) vkDestroyDescriptorSetLayout(c->device, G.dsl, NULL);
|
||||
}
|
||||
memset(&G, 0, sizeof G);
|
||||
G.mode = -1;
|
||||
plat_mutex_unlock(&g_lock);
|
||||
}
|
||||
+98
-10
@@ -16,9 +16,69 @@ static VkCtx g_vk;
|
||||
static int g_ready;
|
||||
static PlatMutex g_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
// ---- GPU address map for device faults
|
||||
// VK_EXT_device_address_binding_report: the driver reports each bind and unbind of an object's GPU virtual range through
|
||||
// the debug messenger. The newest AB_N events stay in a ring written without a lock (one atomic add each), so a device
|
||||
// fault names the object behind its address and whether it was unbound before the fault, which is memory freed while
|
||||
// a submission still used it. One event per image, buffer or memory block made or freed, none per draw.
|
||||
// TOTK_ADDR_REPORT=0 turns it off.
|
||||
#define AB_N (1u << 15)
|
||||
typedef struct { uint64_t seq, base, size, handle, issued, done; int32_t type; uint32_t unbind; } AbEvent;
|
||||
static AbEvent *ab_ring;
|
||||
static uint64_t ab_head;
|
||||
static int ab_want; // the instance has the messenger for it (create_instance)
|
||||
|
||||
static void ab_record(const VkDebugUtilsMessengerCallbackDataEXT *d) {
|
||||
const VkDeviceAddressBindingCallbackDataEXT *b = NULL;
|
||||
for (const VkBaseInStructure *p = d->pNext; p; p = p->pNext)
|
||||
if (p->sType == VK_STRUCTURE_TYPE_DEVICE_ADDRESS_BINDING_CALLBACK_DATA_EXT) b = (const void *)p;
|
||||
if (!b || !ab_ring) return;
|
||||
uint64_t i = __atomic_fetch_add(&ab_head, 1, __ATOMIC_RELAXED);
|
||||
AbEvent *e = &ab_ring[i & (AB_N - 1)];
|
||||
__atomic_store_n(&e->seq, 0, __ATOMIC_RELAXED);
|
||||
e->base = b->baseAddress, e->size = b->size;
|
||||
e->handle = d->objectCount ? d->pObjects[0].objectHandle : 0;
|
||||
e->type = d->objectCount ? (int32_t)d->pObjects[0].objectType : 0;
|
||||
e->unbind = b->bindingType == VK_DEVICE_ADDRESS_BINDING_TYPE_UNBIND_EXT;
|
||||
e->issued = vk_sync_issued(), e->done = vk_sync_fence_done();
|
||||
__atomic_store_n(&e->seq, i + 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
static const char *ab_type_name(int32_t t, char *buf, size_t n) {
|
||||
switch (t) {
|
||||
case VK_OBJECT_TYPE_IMAGE: return "image";
|
||||
case VK_OBJECT_TYPE_BUFFER: return "buffer";
|
||||
case VK_OBJECT_TYPE_DEVICE_MEMORY: return "memory";
|
||||
default: snprintf(buf, n, "object type %d", (int)t); return buf;
|
||||
}
|
||||
}
|
||||
|
||||
// The newest events whose range meets the precision-aligned range around a fault address, newest first.
|
||||
static void ab_report(uint64_t addr, uint64_t precision) {
|
||||
if (!ab_ring) return;
|
||||
uint64_t lo = precision > 1 ? addr & ~(precision - 1) : addr, hi = lo + (precision > 1 ? precision : 1);
|
||||
uint64_t head = __atomic_load_n(&ab_head, __ATOMIC_ACQUIRE), n = head < AB_N ? head : AB_N;
|
||||
int found = 0;
|
||||
for (uint64_t k = 1; k <= n && found < 6; k++) {
|
||||
const AbEvent *e = &ab_ring[(head - k) & (AB_N - 1)];
|
||||
if (__atomic_load_n(&e->seq, __ATOMIC_ACQUIRE) != head - k + 1) continue;
|
||||
if (e->base >= hi || e->base + e->size <= lo) continue;
|
||||
found++;
|
||||
char tn[32];
|
||||
rt_log("vk: fault address %#llx: %s %#llx [%#llx, +%#llx] %s %llu address events before the fault, "
|
||||
"submitted %llu, done %llu", (unsigned long long)addr, ab_type_name(e->type, tn, sizeof tn),(unsigned long long)e->handle,
|
||||
(unsigned long long)e->base, (unsigned long long)e->size, e->unbind ? "unbound" : "bound",
|
||||
(unsigned long long)k, (unsigned long long)e->issued, (unsigned long long)e->done);
|
||||
}
|
||||
if (!found) rt_log("vk: fault address %#llx: no binding in the last %llu address events", (unsigned long long)addr,
|
||||
(unsigned long long)n);
|
||||
}
|
||||
|
||||
static VKAPI_ATTR VkBool32 VKAPI_CALL debug_cb(VkDebugUtilsMessageSeverityFlagBitsEXT sev, VkDebugUtilsMessageTypeFlagsEXT type,
|
||||
const VkDebugUtilsMessengerCallbackDataEXT *d, void *user) {
|
||||
(void)type, (void)user;
|
||||
(void)user;
|
||||
if (type & VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT) { ab_record(d); return VK_FALSE; }
|
||||
if (sev < VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) return VK_FALSE; // info, asked for the address events
|
||||
int err = (sev & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0;
|
||||
if (err) plat_inc32(&g_vk.validation_errors);
|
||||
static volatile int32_t logged;
|
||||
@@ -76,7 +136,10 @@ static int create_instance(void) {
|
||||
rt_log("vk: TOTK_VK_VALIDATION set but %s is not installed; continuing without", layers[0]);
|
||||
validate = 0;
|
||||
}
|
||||
if (validate) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
|
||||
const char *ar = cfg_get("TOTK_ADDR_REPORT");
|
||||
ab_want = !(ar && !strcmp(ar, "0")) && instance_ext_present(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
||||
if (ab_want && !ab_ring && !(ab_ring = calloc(AB_N, sizeof *ab_ring))) ab_want = 0;
|
||||
if (validate || ab_want) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
|
||||
if (vk_hdr_wanted() && instance_ext_present(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) // HDR10 surface formats
|
||||
ext[n_ext++] = VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME;
|
||||
vk_dlaa_instance_exts(ext, &n_ext, 12);
|
||||
@@ -89,16 +152,18 @@ static int create_instance(void) {
|
||||
VkResult r = vkCreateInstance(&ci, NULL, &g_vk.instance);
|
||||
if (r != VK_SUCCESS) { rt_log("vk: vkCreateInstance failed (%d); is a Vulkan 1.3 driver installed?", (int)r); return 0; }
|
||||
g_vk.validation = validate;
|
||||
if (validate) {
|
||||
if (validate || ab_want) {
|
||||
PFN_vkCreateDebugUtilsMessengerEXT f =
|
||||
(PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(g_vk.instance, "vkCreateDebugUtilsMessengerEXT");
|
||||
VkDebugUtilsMessengerCreateInfoEXT mi = {
|
||||
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
|
||||
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
|
||||
.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
|
||||
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
|
||||
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT |
|
||||
(ab_want ? VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT : 0),
|
||||
.messageType = (validate ? VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
|
||||
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT : 0) |
|
||||
(ab_want ? VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT : 0),
|
||||
.pfnUserCallback = debug_cb};
|
||||
if (f) f(g_vk.instance, &mi, NULL, &g_vk.messenger);
|
||||
if (!f || f(g_vk.instance, &mi, NULL, &g_vk.messenger) != VK_SUCCESS) ab_want = 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -217,6 +282,7 @@ typedef struct {
|
||||
VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT gpl;
|
||||
VkPhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT fbl;
|
||||
VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR lr;
|
||||
VkPhysicalDeviceAddressBindingReportFeaturesEXT abr;
|
||||
} FeatureChain;
|
||||
|
||||
static int create_device(void) {
|
||||
@@ -251,6 +317,13 @@ static int create_device(void) {
|
||||
// Ryujinx's SPIR-V declares fragment shader interlock whenever the host reports it (vk_pipeline.c spirv_fixup).
|
||||
OPTIONAL_EXT(has_fragment_shader_interlock, VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, fsi,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT);
|
||||
// The GPU address map for device faults (ab_record), with the messenger made for it
|
||||
int has_abr = 0;
|
||||
if (ab_want && has_ext(ep, ne, VK_EXT_DEVICE_ADDRESS_BINDING_REPORT_EXTENSION_NAME)) {
|
||||
has_abr = 1;
|
||||
ext[n_ext++] = VK_EXT_DEVICE_ADDRESS_BINDING_REPORT_EXTENSION_NAME;
|
||||
CHAIN(abr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ADDRESS_BINDING_REPORT_FEATURES_EXT);
|
||||
}
|
||||
// Feedback loops (vk.h VkCtx.feedback): the layout, and barriers inside dynamic rendering
|
||||
int has_fbl = 0, has_lr = 0;
|
||||
if (has_ext(ep, ne, VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME) &&
|
||||
@@ -314,6 +387,10 @@ static int create_device(void) {
|
||||
else if (!h->textureCompressionBC) miss = "textureCompressionBC";
|
||||
if (miss) { rt_log("vk: required feature missing on %s: %s", g_vk.props.deviceName, miss); free(c); return 0; }
|
||||
if (!c->fault.deviceFault) g_vk.has_device_fault = 0; // vkGetDeviceFaultInfoEXT needs the feature, not just the extension
|
||||
rt_log("vk: GPU address map for device faults: %s",
|
||||
has_abr && c->abr.reportAddressBinding ? "on (VK_EXT_device_address_binding_report; TOTK_ADDR_REPORT=0 turns it off)"
|
||||
: !ab_want ? "off (TOTK_ADDR_REPORT=0 or no VK_EXT_debug_utils)"
|
||||
: "unavailable (no VK_EXT_device_address_binding_report)");
|
||||
g_vk.has_buffer_device_address = c->f12.bufferDeviceAddress;
|
||||
g_vk.has_astc_ldr = h->textureCompressionASTC_LDR;
|
||||
{
|
||||
@@ -352,8 +429,14 @@ static int create_device(void) {
|
||||
g_vk.min_imported_host_alignment = (uint32_t)emh.minImportedHostPointerAlignment;
|
||||
vk_caps_init(&g_vk);
|
||||
|
||||
// Blocks of 64 MiB instead of VMA's 256 (TOTK_VMA_BLOCK_MIB, 0 = VMA's). Since the textures moved to their own pool
|
||||
// (vk_memory.c vk_texture_image), the default device-local blocks hold mirrors and other buffers: on the RX 9070
|
||||
// after 10 minutes in the field one 256 MiB block held 53 MiB of them, and the headroom thread kept half a block of
|
||||
// each device-local type ahead (128 MiB each). Larger buffers than half a block get memory of their own.
|
||||
g_vk.vma_block = (VkDeviceSize)cfg_int("TOTK_VMA_BLOCK_MIB") << 20;
|
||||
if (!g_vk.vma_block) g_vk.vma_block = 256ull << 20; // VMA_DEFAULT_LARGE_HEAP_BLOCK_SIZE
|
||||
VmaAllocatorCreateInfo ai = {.physicalDevice = g_vk.phys, .device = g_vk.device, .instance = g_vk.instance,
|
||||
.vulkanApiVersion = VK_API_VERSION_1_3,
|
||||
.vulkanApiVersion = VK_API_VERSION_1_3, .preferredLargeHeapBlockSize = g_vk.vma_block,
|
||||
.flags = (g_vk.has_buffer_device_address ? VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT : 0) |
|
||||
(g_vk.has_memory_budget ? VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT : 0),
|
||||
.pDeviceMemoryCallbacks = vk_headroom_callbacks()};
|
||||
@@ -376,6 +459,7 @@ static void destroy_all(void) {
|
||||
gfx_window_release_vk();
|
||||
if (g_vk.device) vkDeviceWaitIdle(g_vk.device);
|
||||
if (g_vk.device) vk_astc_release();
|
||||
if (g_vk.device) vk_deswz_release();
|
||||
if (g_vk.device) vk_crumbs_release();
|
||||
if (g_vk.timeline) vkDestroySemaphore(g_vk.device, g_vk.timeline, NULL);
|
||||
vk_headroom_stop();
|
||||
@@ -397,7 +481,8 @@ int vk_init(void) {
|
||||
plat_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
memset(&g_vk, 0, sizeof g_vk);
|
||||
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_headroom_start(), vk_dlaa_init();
|
||||
if (create_instance() && pick_device() && create_device())
|
||||
g_ready = 1, vk_mem_host_probe(), vk_upload_pools_init(), vk_headroom_start(), vk_dlaa_init();
|
||||
else destroy_all();
|
||||
}
|
||||
int ok = g_ready;
|
||||
@@ -437,6 +522,7 @@ void vk_device_fault_report(VkResult r) {
|
||||
done = 1;
|
||||
vk_crumbs_report();
|
||||
vk_pass_marks_report();
|
||||
vk_mem_fault_log();
|
||||
VkCtx *c = vk_ctx();
|
||||
PFN_vkGetDeviceFaultInfoEXT get =
|
||||
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
|
||||
@@ -461,9 +547,11 @@ void vk_device_fault_report(VkResult r) {
|
||||
rt_log("vk: device fault: \"%s\" (%u address infos, %u vendor infos, vendor binary %llu bytes)", info.description,
|
||||
n.addressInfoCount, n.vendorInfoCount, (unsigned long long)binary);
|
||||
// The faulting access lies in the precision-aligned range around the reported address.
|
||||
for (uint32_t i = 0; i < n.addressInfoCount; i++)
|
||||
for (uint32_t i = 0; i < n.addressInfoCount; i++) {
|
||||
rt_log("vk: device fault address %u: %s at %#llx, precision %#llx", i, fault_address_type(ai[i].addressType),
|
||||
(unsigned long long)ai[i].reportedAddress, (unsigned long long)ai[i].addressPrecision);
|
||||
ab_report(ai[i].reportedAddress, ai[i].addressPrecision);
|
||||
}
|
||||
for (uint32_t i = 0; i < n.vendorInfoCount; i++)
|
||||
rt_log("vk: device fault vendor info %u: \"%s\" code %#llx data %#llx", i, vi[i].description,
|
||||
(unsigned long long)vi[i].vendorFaultCode, (unsigned long long)vi[i].vendorFaultData);
|
||||
|
||||
+132
-86
@@ -11,9 +11,6 @@
|
||||
// layout with a set-then-get round trip before anything relies on it.
|
||||
//
|
||||
// Where DLAA runs in the frame, the jitter and the motion vectors: see the sections below.
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -115,31 +112,19 @@ int vk_dlaa_wanted(void) {
|
||||
static int core_load(void) {
|
||||
if (N.tried) return N.loaded;
|
||||
N.tried = 1;
|
||||
#ifdef _WIN32
|
||||
char dir[MAX_PATH] = {0}, path[MAX_PATH + 32];
|
||||
DWORD n = sizeof dir;
|
||||
if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\NVIDIA Corporation\\Global\\NGXCore", "FullPath", RRF_RT_REG_SZ,
|
||||
NULL, dir, &n) != ERROR_SUCCESS) {
|
||||
char path[PLAT_PATH_MAX];
|
||||
if (!plat_ngx_core_path(path, sizeof path)) {
|
||||
rt_log("dlaa: the driver has no NGX core (registry NGXCore missing): DLAA off");
|
||||
return 0;
|
||||
}
|
||||
snprintf(path, sizeof path, "%s\\_nvngx.dll", dir);
|
||||
N.core = LoadLibraryA(path);
|
||||
if (!N.core) {
|
||||
rt_log("dlaa: cannot load %s (error %lu): DLAA off", path, GetLastError());
|
||||
return 0;
|
||||
}
|
||||
#define FN(f, name) N.f = (void *)GetProcAddress((HMODULE)N.core, name)
|
||||
#else
|
||||
// The driver's NGX core; the dynamic loader finds it where the driver put it (/usr/lib, /usr/lib/x86_64-linux-gnu).
|
||||
const char *path = "libnvidia-ngx.so.1";
|
||||
N.core = plat_lib_open(path);
|
||||
if (!N.core) {
|
||||
rt_log("dlaa: cannot load %s (not an NVIDIA driver, or one without NGX): DLAA off", path);
|
||||
char why[256];
|
||||
plat_lib_error(why, sizeof why);
|
||||
rt_log("dlaa: cannot load %s (%s; not an NVIDIA driver, or one without NGX): DLAA off", path, why);
|
||||
return 0;
|
||||
}
|
||||
#define FN(f, name) N.f = plat_lib_sym(N.core, name)
|
||||
#endif
|
||||
FN(init, "NVSDK_NGX_VULKAN_Init_Ext2");
|
||||
FN(req_ext, "NVSDK_NGX_VULKAN_RequiredExtensions");
|
||||
FN(caps, "NVSDK_NGX_VULKAN_GetCapabilityParameters");
|
||||
@@ -203,46 +188,14 @@ static int vtable_ok(void) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
|
||||
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
|
||||
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
|
||||
const unsigned char *u = (const unsigned char *)in;
|
||||
size_t n = 0;
|
||||
while (*u) {
|
||||
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
|
||||
if (more == 4) return 0;
|
||||
if (more) c &= 0x3F >> more;
|
||||
for (unsigned k = 0; k < more; k++, u++) {
|
||||
if ((*u & 0xC0) != 0x80) return 0;
|
||||
c = c << 6 | (*u & 0x3F);
|
||||
}
|
||||
if (n + 1 >= cap) return 0;
|
||||
out[n++] = (wchar_t)c;
|
||||
}
|
||||
out[n] = 0;
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
void vk_dlaa_init(void) {
|
||||
if (!vk_dlaa_wanted() || !core_load()) return;
|
||||
#ifdef _WIN32
|
||||
wchar_t wdir[MAX_PATH];
|
||||
DWORD len = GetModuleFileNameW(NULL, wdir, MAX_PATH);
|
||||
while (len && wdir[len - 1] != L'\\') len--;
|
||||
wdir[len] = 0;
|
||||
#else
|
||||
// The executable's folder, where NGX looks for libnvidia-ngx-dlss.so and writes its logs, as a wchar_t string:
|
||||
// 32-bit code points here, as the Linux core expects.
|
||||
char dir[PLAT_PATH_MAX];
|
||||
// The executable's folder, where NGX looks for the DLSS model library and writes its logs.
|
||||
wchar_t wdir[PLAT_PATH_MAX];
|
||||
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
|
||||
if (!cut || !utf8_to_wide((cut[1] = 0, dir), wdir, PLAT_PATH_MAX)) {
|
||||
if (!plat_exe_dir_wide(wdir, PLAT_PATH_MAX)) {
|
||||
rt_log("dlaa: cannot name the executable's folder for NGX: DLAA off");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// 231313132: the application id NVIDIA's DLSS samples use for builds without an NVIDIA-issued id
|
||||
const VkCtx *c = vk_ctx();
|
||||
NgxResult r = N.init(231313132ull, wdir, c->instance, c->phys, c->device, vkGetInstanceProcAddr,
|
||||
@@ -289,7 +242,11 @@ int vk_dlaa_available(void) { return vk_dlaa_wanted() && N.available; }
|
||||
static struct {
|
||||
VkExecTarget color, depth; // the armed pair, valid in the frame it was armed
|
||||
int armed, done, gbuf; // this frame: armed on the light target, DLAA ran, the G-buffer was drawn
|
||||
uint32_t gbuf_w; // the widest G-buffer's width this frame
|
||||
VkImage nodepth; // this frame: a G-buffer-size target drawn without depth while nothing was armed
|
||||
int ended, up; // this frame: the scene is drawn (jitter over), DLSS wrote the scaled output
|
||||
uint64_t end_fence; // a chunked replay: the submission and op the scan ended the scene at (ended_at)
|
||||
uint32_t end_pos;
|
||||
struct Size { // a DLSS feature and its work images per scene size (dynamic resolution flips it)
|
||||
uint32_t w, h, ow, oh; // render size, output size
|
||||
VkImage out, mv;
|
||||
@@ -604,7 +561,7 @@ static int mv_pass(VkExec *ex, struct Size *e, VkImageView zv, float jx, float j
|
||||
VkCommandBuffer cb = ex->cb;
|
||||
uint32_t w = e->w, h = e->h;
|
||||
VkDescriptorBufferInfo cam;
|
||||
if (!mv_init() || !D.cam_addr || !vk_usnap_find(D.cam_addr, CAM_BLOCK, 4, &cam)) {
|
||||
if (!mv_init() || !D.cam_addr || !vk_usnap_find(D.cam_addr, CAM_BLOCK, UINT32_MAX, 4, &cam)) {
|
||||
D.mv_none++;
|
||||
D.cam_prev_ok = 0;
|
||||
e->chars_ok = 0;
|
||||
@@ -824,10 +781,9 @@ static struct Snap *snap_for(VkCommandBuffer cb, uint32_t w, uint32_t h, VkForma
|
||||
}
|
||||
|
||||
// At arming: the light target as it is before the transparents, outside any pass.
|
||||
static void mask_snapshot(VkExec *ex) {
|
||||
static void mask_snapshot(VkExec *ex, const VkExecTarget *t) {
|
||||
R.cur = NULL;
|
||||
if (mask_init() <= 0) return;
|
||||
const VkExecTarget *t = &D.color;
|
||||
struct Snap *s = snap_for(ex->cb, t->width, t->height, t->format);
|
||||
if (!s) return;
|
||||
vk_exec_barrier(ex);
|
||||
@@ -876,14 +832,33 @@ static float halton(uint32_t i, uint32_t b) {
|
||||
return r;
|
||||
}
|
||||
|
||||
// The scene is drawn at the op ex records. A chunked replay's scan ends it ahead of the chunks that still record the
|
||||
// scene's draws, so a chunk compares its op with the scan's (the chunks after it are taken once the scan is past their
|
||||
// ops); a later submission of the frame is past it.
|
||||
static int ended_at(const VkExec *ex) {
|
||||
if (!__atomic_load_n(&D.ended, __ATOMIC_ACQUIRE)) return 0;
|
||||
return !ex->chunked || ex->fence != D.end_fence || ex->pos >= D.end_pos;
|
||||
}
|
||||
|
||||
void vk_dlaa_viewport(VkExec *ex, VkViewport *v) {
|
||||
if (!D.jit_ok || D.ended || !ex->depth.image || ex->depth.image != D.jdepth) return;
|
||||
if (!D.jit_ok || !ex->depth.image || ex->depth.image != D.jdepth || ended_at(ex)) return;
|
||||
v->x += D.jx, v->y += D.jy;
|
||||
D.ux = D.jx, D.uy = D.jy, D.jit_used = 1;
|
||||
D.ux = D.jx, D.uy = D.jy;
|
||||
__atomic_store_n(&D.jit_used, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
// The scene camera: c4 of the last scene draw before the scene ends. A chunked replay's scan notes it in replay order
|
||||
// (vk_dlaa_scan_draw): chunks recording side by side would leave the block of whichever drew last.
|
||||
static void note_camera(const VkExec *ex) {
|
||||
const p_bind_buf *b = &ex->ubo[0][1]; // constant bank c4: the replay keeps user banks from c3 on
|
||||
const VkExecTarget *z = &ex->depth;
|
||||
if (b->size == 0x940 && b->addr && z->image && z->width >= DLAA_SCENE_MIN_W && z->format == VK_FORMAT_D32_SFLOAT &&
|
||||
(!D.jdepth || z->image == D.jdepth))
|
||||
D.cam_addr = b->addr;
|
||||
}
|
||||
|
||||
void vk_dlaa_draw(VkExec *ex) {
|
||||
if (!dl_active() || D.ended) return;
|
||||
if (!dl_active() || ended_at(ex)) return;
|
||||
static int survey = -1; // TOTK_DLAA_SURVEY=1 (test): what is drawn into the armed light target
|
||||
if (survey < 0) survey = rt_knob("DLAA_SURVEY", "0");
|
||||
static uint64_t dfn[16]; // scene draws writing depth, by depth test (reversed depth: GREATER/GEQUAL)
|
||||
@@ -914,11 +889,11 @@ void vk_dlaa_draw(VkExec *ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const p_bind_buf *b = &ex->ubo[0][1]; // constant bank c4: the replay keeps user banks from c3 on
|
||||
const VkExecTarget *z = &ex->depth;
|
||||
if (b->size == 0x940 && b->addr && z->image && z->width >= DLAA_SCENE_MIN_W && z->format == VK_FORMAT_D32_SFLOAT &&
|
||||
(!D.jdepth || z->image == D.jdepth))
|
||||
D.cam_addr = b->addr;
|
||||
if (!ex->chunked) note_camera(ex);
|
||||
}
|
||||
|
||||
void vk_dlaa_scan_draw(const VkExec *ex) {
|
||||
if (dl_active() && !D.ended) note_camera(ex);
|
||||
}
|
||||
|
||||
// The jitter moves on right after DLAA ran, not at the present: the replay can record the next frame's scene before
|
||||
@@ -1092,7 +1067,7 @@ static void evaluate(VkExec *ex, const VkExecTarget *dst) {
|
||||
full_barrier(cb);
|
||||
if (dst != t) vk_dlaa_swap_img = t->img, vk_dlaa_swap_view = e->out_view; // the scaling draw reads DLSS's picture
|
||||
ex->recorded += 4;
|
||||
vk_exec_draw_new_cb(); // NGX bound pipelines and descriptor sets of its own: the next draw binds everything again
|
||||
vk_exec_draw_new_cb(ex); // NGX bound pipelines and descriptor sets of its own: the next draw binds everything again
|
||||
D.jdepth = z->image;
|
||||
next_jitter(w, ow);
|
||||
if (++D.evals == 1 || D.evals == 300 || D.evals % 3600 == 0) // 300: one early line with the frames counted
|
||||
@@ -1109,51 +1084,110 @@ static int scene_color(VkFormat f) {
|
||||
return f == VK_FORMAT_B10G11R11_UFLOAT_PACK32 || f == VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
}
|
||||
|
||||
void vk_dlaa_pass(VkExec *ex) {
|
||||
int vk_dlaa_plan(const VkExec *ex, VkExecJob *j) {
|
||||
const VkExecTarget *c = &ex->color[0], *z = &ex->depth;
|
||||
if (!dl_active() || D.done) return;
|
||||
if (!dl_active() || D.done) return 0;
|
||||
// The G-buffer: several colour targets with a D32F depth of at least DLAA_SCENE_MIN_W wide. Only the light target drawn after it
|
||||
// arms the frame; at night the game draws into the light target alone with the scene depth before the G-buffer too,
|
||||
// and DLAA then ran at a small pass ahead of the whole scene, which reached the screen jittered.
|
||||
if (ex->pass_ncolor >= 3 && z->image && z->format == VK_FORMAT_D32_SFLOAT && z->width >= DLAA_SCENE_MIN_W) {
|
||||
D.gbuf_w = D.gbuf && D.gbuf_w > z->width ? D.gbuf_w : z->width;
|
||||
D.gbuf = 1, D.armed = 0;
|
||||
// target 0 is the object-class buffer (RGBA8, the scene size): the characters' bias mask reads its green. A
|
||||
// smaller pass of the same shape (640x360, seen while loading) does not replace the scene's in the same frame.
|
||||
if (c->image && c->format == VK_FORMAT_R8G8B8A8_UNORM && c->width == z->width && c->height == z->height &&
|
||||
c->samples <= 1 && !c->is3d && (!D.ids_ok || c->width >= D.ids.width))
|
||||
D.ids = *c, D.ids_ok = 1;
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
// A pass into one scene colour target of the G-buffer's size or larger without depth while nothing is armed ends
|
||||
// the scene once the bloom chain follows with still nothing armed: in the inventory (1.4.3) the preview's light
|
||||
// target is drawn that way before the bloom, and with the scene depth only after it, while the menu is composed;
|
||||
// DLAA ran on the finished menu picture with the previous frame's history, which left stray dark pixels around Link.
|
||||
// Arming cancels the end, on that target too: at some camera angles the field's effects composite (a full-screen
|
||||
// draw without depth into the light target) comes before the first pass with the scene depth, and with that target
|
||||
// barred from arming DLSS stopped for as long as the view held (in the inventory the bloom chain always ended the
|
||||
// scene first, in both modes, menu opening included). While the scope zooms in, a second G-buffer pass disarms the
|
||||
// frame, the scene is copied into an output-sized target without depth, and only then is the light target drawn
|
||||
// again with the scene depth.
|
||||
// The G-buffer's width is the widest of the frame: the game also draws a half-size G-buffer, after which a
|
||||
// half-size pass without depth ended the scene at some camera angles.
|
||||
if (D.gbuf && !D.armed && !D.ended && !z->image && ex->pass_ncolor == 1 && c->image && scene_color(c->format) &&
|
||||
c->width >= D.gbuf_w && !c->level)
|
||||
D.nodepth = c->image;
|
||||
if (D.nodepth && !D.armed && !D.ended && ex->pass_w && ex->pass_w * 4 <= D.gbuf_w && c->image &&
|
||||
scene_color(c->format)) {
|
||||
D.end_fence = ex->fence, D.end_pos = ex->pos;
|
||||
__atomic_store_n(&D.ended, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
// the scene depth is D32F; a D16 target of the scene size also serves one composite pass before the transparents.
|
||||
// A smaller pair never takes over an armed one: the camera rune draws a half-size RGBA16F target with a half-size
|
||||
// D32F depth after the lighting (852x480 under a 1704x960 scene), and DLSS ran on that and wrote its picture into
|
||||
// the scene's own light target, which left every object black.
|
||||
if (D.gbuf && ex->pass_ncolor == 1 && c->image && scene_color(c->format) && z->image && z->format == VK_FORMAT_D32_SFLOAT &&
|
||||
c->width == z->width && c->height == z->height && c->width >= DLAA_SCENE_MIN_W &&
|
||||
// the scene's own light target, which left every object black. Nor does a pair narrower than the frame's widest
|
||||
// G-buffer arm at all: the game draws its particles into a half-size RGBA16F target with a half-size D32F depth, and
|
||||
// with the scope open, where the light target is drawn with a D16 depth and never arms, that effects pass was the
|
||||
// first pair to arm; DLSS then scaled the effects into the light target, which filled the lens with garbage in the
|
||||
// DLAA mode.
|
||||
if (D.gbuf && !D.ended && ex->pass_ncolor == 1 && c->image && scene_color(c->format) && z->image && z->format == VK_FORMAT_D32_SFLOAT &&
|
||||
c->width == z->width && c->height == z->height && c->width >= DLAA_SCENE_MIN_W && c->width >= D.gbuf_w &&
|
||||
(!D.armed || (c->width >= D.color.width && c->height >= D.color.height)) &&
|
||||
c->samples <= 1 && !c->is3d) {
|
||||
int first = !D.armed;
|
||||
D.color = *c, D.depth = *z, D.armed = 1;
|
||||
if (first) mask_snapshot(ex); // the lit scene, before the transparents (reactive mask)
|
||||
return;
|
||||
D.color = *c, D.depth = *z, D.armed = 1, D.nodepth = VK_NULL_HANDLE;
|
||||
if (!first) return 0;
|
||||
*j = (VkExecJob){.kind = VK_JOB_DLSS_MASK, .scene = *c}; // the lit scene, before the transparents (reactive mask)
|
||||
return 1;
|
||||
}
|
||||
if (!D.armed) return;
|
||||
if (!D.armed) return 0;
|
||||
if (!D.ended) {
|
||||
if (!ex->pass_w || ex->pass_w * 4 > D.color.width) return;
|
||||
D.ended = 1;
|
||||
// The bloom chain's first pass ends the scene: a pass at most a quarter of the scene's width into a scene
|
||||
// colour target. With the camera rune or the scope open the game draws a 160-wide depth-only pass and 160x90
|
||||
// RG16F and RGBA8 passes right after the lighting, then more geometry and the transparents; DLSS ended the
|
||||
// scene at those, the next G-buffer pass disarmed it and the scaling pass found nothing to evaluate.
|
||||
if (!ex->pass_w || ex->pass_w * 4 > D.color.width || !c->image || !scene_color(c->format)) return 0;
|
||||
D.end_fence = ex->fence, D.end_pos = ex->pos;
|
||||
__atomic_store_n(&D.ended, 1, __ATOMIC_RELEASE);
|
||||
unsigned o[2], s[2];
|
||||
res_sizes(o, s);
|
||||
if (D.color.width >= o[0]) D.done = 1, evaluate(ex, NULL); // DLAA in place; else wait for the scaling pass
|
||||
return;
|
||||
if (D.color.width < o[0]) return 0; // wait for the scaling pass
|
||||
D.done = 1;
|
||||
*j = (VkExecJob){.kind = VK_JOB_DLSS}; // DLAA in place
|
||||
return 1;
|
||||
}
|
||||
// the game's scaling pass: one colour target of the output size, no depth
|
||||
unsigned o[2], s[2];
|
||||
res_sizes(o, s);
|
||||
if (ex->pass_ncolor == 1 && c->image && !z->image && scene_color(c->format) && c->width > D.color.width &&
|
||||
c->width == o[0] && c->height == o[1] && c->samples <= 1 && !c->is3d) {
|
||||
D.done = D.up = 1;
|
||||
evaluate(ex, c);
|
||||
D.done = 1;
|
||||
*j = (VkExecJob){.kind = VK_JOB_DLSS, .scene = *c};
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The scan resolved its targets without making entries (vk_texture_revive); a record made on a chunk takes their images
|
||||
// now, as the scan's pointers may be stale by then.
|
||||
static void target_now(VkExecTarget *t) {
|
||||
const VkTexImage *i = t->tex ? vk_texture_get(t->tex) : NULL;
|
||||
if (i) t->img = i, t->image = i->image;
|
||||
}
|
||||
|
||||
void vk_dlaa_exec(VkExec *ex, const VkExecJob *j) {
|
||||
VkExecTarget t = j->scene;
|
||||
if (ex->chunked) {
|
||||
target_now(&t), target_now(&D.color), target_now(&D.depth);
|
||||
if (D.ids_ok) target_now(&D.ids);
|
||||
}
|
||||
if (j->kind == VK_JOB_DLSS_MASK) return mask_snapshot(ex, &t);
|
||||
if (!t.tex) return evaluate(ex, NULL);
|
||||
D.up = 1;
|
||||
evaluate(ex, &t);
|
||||
}
|
||||
|
||||
void vk_dlaa_pass(VkExec *ex) {
|
||||
VkExecJob j;
|
||||
if (!ex->chunked && vk_dlaa_plan(ex, &j)) vk_dlaa_exec(ex, &j);
|
||||
}
|
||||
|
||||
// TOTK_DLSS, read at each present (the menu switches it live): the scene size of the mode (resolution.c) and the
|
||||
@@ -1196,6 +1230,7 @@ void vk_dlaa_frame(void) {
|
||||
D.on = on;
|
||||
// either way the next DLAA frame starts a new history from an unjittered frame
|
||||
D.reset = 1, D.cam_prev_ok = 0, D.jit_ok = D.jit_used = D.armed = D.done = D.gbuf = D.ended = D.up = 0;
|
||||
D.nodepth = VK_NULL_HANDLE;
|
||||
D.hold = 0;
|
||||
return;
|
||||
}
|
||||
@@ -1204,7 +1239,7 @@ void vk_dlaa_frame(void) {
|
||||
if (D.jit_used && !D.done) D.hold = 60, D.holds++; // jittered but not resolved: that frame shook
|
||||
else if (D.hold) D.hold--;
|
||||
if (!D.done) D.jit_ok = 0;
|
||||
D.armed = D.done = D.jit_used = D.gbuf = D.ended = D.up = D.ids_ok = 0;
|
||||
D.armed = D.done = D.jit_used = D.gbuf = D.ended = D.up = D.ids_ok = 0, D.nodepth = VK_NULL_HANDLE;
|
||||
D.cam_addr = 0;
|
||||
}
|
||||
|
||||
@@ -1225,18 +1260,29 @@ void vk_dlaa_frame(void) {
|
||||
static const uint64_t FXAA_FS[] = {0xf450003ebeb3dc52ull, 0x0a12f4bc2a9a4fbeull, 0x5c43ae18948d52cbull,
|
||||
0xaad42238801d31d8ull};
|
||||
|
||||
// Chunks of one submission look shaders up side by side: an entry's hash is stored before its shader, and entries are
|
||||
// added under the lock.
|
||||
static uint64_t spirv_hash(const GfxShader *s) {
|
||||
static struct { const GfxShader *s; uint64_t h; } tab[8192];
|
||||
static PlatMutex lk = PLAT_MUTEX_INIT;
|
||||
uintptr_t k = ((uintptr_t)s >> 4) * 0x9E3779B97F4A7C15ull;
|
||||
for (uint32_t i = 0, j = (uint32_t)(k >> 51); i < 8192; i++, j = (j + 1) & 8191) {
|
||||
if (tab[j].s == s) return tab[j].h;
|
||||
if (!tab[j].s) {
|
||||
const GfxShader *e = __atomic_load_n(&tab[j].s, __ATOMIC_ACQUIRE);
|
||||
if (e == s) return tab[j].h;
|
||||
if (e) continue;
|
||||
plat_mutex_lock(&lk);
|
||||
e = tab[j].s;
|
||||
if (!e) {
|
||||
uint64_t words = 0, h = 0xcbf29ce484222325ull;
|
||||
const uint8_t *b = (const uint8_t *)gfx_shader_spirv(s, &words);
|
||||
for (uint64_t n = 0; b && n < words * 4; n++) h = (h ^ b[n]) * 0x100000001b3ull;
|
||||
tab[j].s = s, tab[j].h = h;
|
||||
tab[j].h = h;
|
||||
__atomic_store_n(&tab[j].s, s, __ATOMIC_RELEASE);
|
||||
plat_mutex_unlock(&lk);
|
||||
return h;
|
||||
}
|
||||
plat_mutex_unlock(&lk);
|
||||
if (e == s) return tab[j].h;
|
||||
}
|
||||
return 0; // table full: no skip
|
||||
}
|
||||
|
||||
@@ -18,11 +18,17 @@ int vk_dlaa_available(void);
|
||||
// scene's light target or, at the first quarter-width pass after it, runs DLAA on that target. Each present ends the
|
||||
// frame (vk_dlaa_frame).
|
||||
typedef struct VkExec VkExec;
|
||||
typedef struct VkExecJob VkExecJob;
|
||||
void vk_dlaa_pass(VkExec *ex);
|
||||
// The same decision for a chunked replay's scan, in replay order: 1 with the record due before this pass (j), which
|
||||
// vk_dlaa_exec makes on the chunk that holds the op (vk_exec.c run_job).
|
||||
int vk_dlaa_plan(const VkExec *ex, VkExecJob *j);
|
||||
void vk_dlaa_exec(VkExec *ex, const VkExecJob *j);
|
||||
void vk_dlaa_frame(void);
|
||||
// vk_exec_draw.c, each draw before its fixed state is recorded: notes the scene camera block (vk_dlaa_draw), and moves
|
||||
// the viewport of the scene's draws by the frame's jitter (vk_dlaa_viewport, when the viewport is recorded).
|
||||
void vk_dlaa_draw(VkExec *ex);
|
||||
void vk_dlaa_scan_draw(const VkExec *ex); // a chunked replay's scan, at each draw: the camera note of vk_dlaa_draw
|
||||
void vk_dlaa_viewport(VkExec *ex, VkViewport *v);
|
||||
// vk_mvsurvey.c (TOTK_MV_SURVEY): notes the uniform blocks of each scene draw, beside vk_dlaa_draw; each present
|
||||
// compares them with the previous frames' and logs a summary now and then.
|
||||
|
||||
+884
-122
File diff suppressed because it is too large.
Load diff
+100
-13
@@ -2,10 +2,12 @@
|
||||
// handles of a submission, keeps render targets, passes, clears and fixed-function state; draws, binds and dispatches
|
||||
// (vk_exec_draw.c) and copies, counters, events and conditional rendering (vk_exec_xfer.c) plug in through the two
|
||||
// hooks at the bottom.
|
||||
// Runs on the submitting thread under vk_sync_submit's lock: no locking inside.
|
||||
// Runs on the submitting thread under vk_sync_submit's lock; a chunked replay (TOTK_REPLAY_WORKERS, vk_exec.c) records
|
||||
// on several threads, each with a VkExec of its own, and shares only the locked tables named where they are declared.
|
||||
// The op loop follows MacNativeGames nvn_mtl.m (MIT, Copyright (c) 2026 jhaizhou) and the pass handling LAS Recomp
|
||||
// (MIT, Copyright (c) 2026 Peiikai); vk_exec.c names the sources.
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "nvn_api.h"
|
||||
@@ -38,8 +40,36 @@ typedef struct {
|
||||
uint8_t scale; // VkTexImage.scale
|
||||
} VkExecTarget;
|
||||
|
||||
// The replay state. One instance lives for the run: NVN state is queue state and carries over from one submission to
|
||||
// the next, as in MacNativeGames' and LAS Recomp's replays. The "per command buffer" fields are reset by vk_exec_record.
|
||||
// TOTK_TRACE=gfx: replay counters of the current frame, logged and reset at each present (vk_exec_frame_log).
|
||||
typedef struct {
|
||||
uint64_t passes, draws_seen, draws, clears, copies, dispatches, submits;
|
||||
uint64_t fb_draws, fb_barriers, fb_restarts, fb_layouts; // feedback loops: draws sampling a written target of
|
||||
// their pass, in-pass barriers, pass restarts, layout
|
||||
// transitions
|
||||
} VkExecFrame;
|
||||
|
||||
// A record the scan of a chunked replay planned before an op: a contact shadow march (vk_contact.c: vk_contact_plan
|
||||
// decides it in replay order, vk_contact_exec records it), or DLSS's copy of the lit scene or its evaluation
|
||||
// (vk_dlaa.c: vk_dlaa_plan, vk_dlaa_exec). The chunk that holds the op records it.
|
||||
enum { VK_JOB_CONTACT, VK_JOB_DLSS_MASK, VK_JOB_DLSS };
|
||||
typedef struct VkExecJob {
|
||||
const void *op; // vk_exec.c: the op it precedes in a scanned submission
|
||||
int kind; // VK_JOB_*
|
||||
int chunk; // vk_exec.c: the chunk that holds the op
|
||||
VkExecTarget scene, depth; // contact: the scene colour and the linear depth; DLSS: the light target to copy
|
||||
// (mask) or the scaling pass's target (evaluation; no tex: in place)
|
||||
float pc[16]; // the march's push constants
|
||||
} VkExecJob;
|
||||
|
||||
// An OP_UPDATE_UB of the submission: [lo, hi) of guest memory takes data (vk_exec_draw.c ubo_ptr reads them).
|
||||
typedef struct { uint64_t lo, hi; const uint8_t *data; } VkExecUpd;
|
||||
|
||||
struct VkExecDraw; // vk_exec_draw.c: the memos and command buffer state of one recording context
|
||||
|
||||
// The replay state and one recording context. NVN state is queue state and carries over from one submission to the
|
||||
// next, as in MacNativeGames' and LAS Recomp's replays. The "per command buffer" fields are reset by vk_exec_record.
|
||||
// The fields from rt up to views are that queue state (VK_EXEC_STATE_BEGIN/END); the rest belongs to the context that
|
||||
// records with it.
|
||||
typedef struct VkExec {
|
||||
// ---- per command buffer
|
||||
VkCommandBuffer cb; // being recorded
|
||||
@@ -94,8 +124,14 @@ typedef struct VkExec {
|
||||
int render_disabled; // OP_RENDER_ENABLE false or a failed OP_RENDER_COND: clears and draws skip
|
||||
p_render_cond render_cond; // last OP_RENDER_COND
|
||||
int has_render_cond;
|
||||
// samples-passed counting (vk_exec_xfer.c): a reset since the last report, a report since the last reset, the game
|
||||
// reported twice without a reset (every draw counts from then on)
|
||||
int oq_counting, oq_reported, oq_keep;
|
||||
|
||||
// ---- internal to vk_exec.c
|
||||
VkExecFrame fr; // counters of this context, added to vk_exec_frame at the end of a submission
|
||||
int split_draws, part_start; // draws and dispatches since the last part; recorded at its start
|
||||
uint64_t part_t0; // replay time at its start
|
||||
void *views; // image views created for this command buffer (freed at its fence)
|
||||
int nviews, cap_views;
|
||||
int barrier_at; // recorded right after the last full barrier (-1: none yet)
|
||||
@@ -112,8 +148,30 @@ typedef struct VkExec {
|
||||
const void *fb_fs; // feedback_scan's memo: fragment shader, texture bind serial, pass serial
|
||||
uint32_t fb_serial, fb_pass, fb_sampled;
|
||||
uint32_t tex_serial, pass_serial; // bumped by every texture or image bind and pass begin
|
||||
|
||||
// ---- vk_exec_xfer.c: the occlusion query of the open pass and the queries closed since the last report or reset
|
||||
int oq_open; // slot of the active query, -1 none
|
||||
uint32_t *oq_seg, oq_nseg, oq_capseg;
|
||||
int oq_lost; // the segment missed a draw (ring full)
|
||||
|
||||
struct VkExecDraw *draw; // vk_exec_draw.c, made on the context's first draw op
|
||||
|
||||
// ---- a chunk of a scanned submission (vk_exec.c): the scan's answers, taken in op order
|
||||
int chunked; // records a chunk: what depends on earlier chunks comes from the scan
|
||||
const VkExecUpd *upd; // the submission's inline uniform updates
|
||||
const uint8_t *cond; // its RENDER_COND results (1 renders)
|
||||
const VkExecJob *jobs; // its planned records (contact shadows, DLSS)
|
||||
uint32_t njobs;
|
||||
uint32_t upd_n, cond_i, job_i; // updates, results and records before the op being recorded
|
||||
uint32_t drop_n; // ops before it that may write uniform ranges (vk_usnap_drop_at)
|
||||
const uint32_t *wpos; // the positions of the draws and dispatches among those ops (vk_exec_draw_writes)
|
||||
uint32_t wpos_i, wpos_end; // the next of them, and the end of the chunk's
|
||||
uint32_t pos; // ops of the submission up to and with it (the scan's too)
|
||||
} VkExec;
|
||||
|
||||
#define VK_EXEC_STATE_BEGIN offsetof(VkExec, rt)
|
||||
#define VK_EXEC_STATE_END offsetof(VkExec, fr)
|
||||
|
||||
// Begins dynamic rendering with the current targets unless a pass is open (a full memory barrier precedes it). Call
|
||||
// before a draw or an attachment clear.
|
||||
void vk_exec_pass_begin(VkExec *ex);
|
||||
@@ -122,6 +180,12 @@ void vk_exec_pass_end(VkExec *ex);
|
||||
// Full memory barrier (all commands, all memory); ends the open pass first. Copies and dispatches order themselves with
|
||||
// it. Skipped when nothing was recorded since the last one.
|
||||
void vk_exec_barrier(VkExec *ex);
|
||||
// UpdateUniformBuffer overlays (vk_exec_draw.c): the copies into the real buffers that updates inside a pass left for
|
||||
// the next barrier (barrier_to records them first), and a GPU write of another kind into [addr, addr+n) of this
|
||||
// submission (a copy, a fill, a storage write), which retires the overlays it touches.
|
||||
void vk_exec_ub_flush(VkExec *ex);
|
||||
void vk_exec_ub_foreign(VkExec *ex, uint64_t addr, uint64_t n);
|
||||
void vk_exec_ub_perf(void);
|
||||
// vk_texture.c (TOTK_TEX_ALIAS): commands that must run at the replay's current position. vk_exec_inline_begin returns
|
||||
// the command buffer being recorded after a full barrier (an open pass is ended; *reopen says so), or VK_NULL_HANDLE
|
||||
// when this thread is not recording a replay. vk_exec_inline_end begins the pass again when it was open.
|
||||
@@ -130,9 +194,16 @@ VkCommandBuffer vk_exec_inline_begin(int *reopen);
|
||||
void vk_exec_inline_end(int reopen);
|
||||
// vk_exec_draw.c: forget what the command buffer being recorded has bound (pipeline, sets, dynamic state, vertex
|
||||
// buffers): the replay continues in a new one (vk_sync_split).
|
||||
void vk_exec_draw_new_cb(void);
|
||||
void vk_exec_draw_new_cb(VkExec *ex);
|
||||
// The recording threads of the chunked replay as this run read TOTK_REPLAY_WORKERS (0 off), -1 before the first
|
||||
// submission read it (the settings menu shows it).
|
||||
int vk_exec_replay_workers(void);
|
||||
// vk_exec_draw.c, for the state scan of a chunked replay: the storage ranges the shaders of a draw (dispatch 0) or a
|
||||
// dispatch at state s write, as {address, size} into out; their count, or -1 when there are more than max or a stage's
|
||||
// shader is not known yet (no context has translated it). Reads uniform blocks through s->upd like a chunk.
|
||||
int vk_exec_draw_writes(const VkExec *s, int dispatch, uint64_t (*out)[2], int max);
|
||||
// vk_exec_draw.c: the vertex and fragment shader of the last draw recorded into this command buffer (NULL: none yet).
|
||||
void vk_exec_draw_shaders(const GfxShader **vs, const GfxShader **fs);
|
||||
void vk_exec_draw_shaders(const VkExec *ex, const GfxShader **vs, const GfxShader **fs);
|
||||
VkBuffer vk_exec_inline_buffer(VkDeviceSize size);
|
||||
// vk_exec.c: the stand-in a 3D BC render target is drawn through goes with its image (vk_exec_image_gone).
|
||||
void vk_exec_bc_gone(VkImage image);
|
||||
@@ -149,16 +220,15 @@ int vk_exec_targets_recent(VkExec *ex, int any);
|
||||
// The current pass's targets with their formats, extents and the frames they were last rendered in (diagnostics).
|
||||
void vk_exec_targets_describe(VkExec *ex, char *buf, size_t cap);
|
||||
|
||||
// TOTK_TRACE=gfx: replay counters of the current frame, logged and reset at each present (vk_exec_frame_log).
|
||||
typedef struct {
|
||||
uint64_t passes, draws_seen, draws, clears, copies, dispatches, submits;
|
||||
uint64_t fb_draws, fb_barriers, fb_restarts, fb_layouts; // feedback loops: draws sampling a written target of
|
||||
// their pass, in-pass barriers, pass restarts, layout
|
||||
// transitions
|
||||
} VkExecFrame;
|
||||
extern VkExecFrame vk_exec_frame;
|
||||
void vk_exec_frame_written(const tex_t *t); // vk_texture_gpu_written: t rendered or copied into
|
||||
uint64_t vk_exec_recording_fence(void); // fence of the submission this thread records, else 0
|
||||
uint32_t vk_exec_replay_pos(void); // the op this thread records in a chunk, else UINT32_MAX
|
||||
// A chunk that is about to make a texture entry whose content depends on the textures at its storage (vk_texture.c
|
||||
// entry_of): whether a chunk before it is still being recorded, and the wait until none is. The caller holds no lock
|
||||
// but the texture lock, which it releases for the wait.
|
||||
int vk_exec_chunk_behind(void);
|
||||
void vk_exec_chunk_settle(void);
|
||||
void vk_exec_frame_log(const tex_t *presented, uint64_t frame);
|
||||
|
||||
// TOTK_DUMP=<frame> (vk_dump.c): between presents frame-1 and frame, every pass's targets and every dispatch's storage
|
||||
@@ -182,6 +252,7 @@ void vk_c0_fill(uint8_t bank[VK_C0_BYTES], int stage, const p_bind_buf ssbo[16],
|
||||
// vk_exec_draw.c: OP_DRAW, OP_DRAW_IDX, OP_DISPATCH, OP_BIND_* (program, state, vattr, vstream, vbuf, ubo, ssbo, tex,
|
||||
// samp, img), OP_UPDATE_UB, OP_DRAW_IDX_IND, OP_DISPATCH_IND. payload: nvn_op_payload of the record.
|
||||
void vk_exec_op_draw(VkExec *ex, uint32_t op, const void *payload);
|
||||
int vk_exec_bind(VkExec *ex, uint32_t op, const void *payload); // a bind op's writes into ex; 1: VkExecDraw.bind_gen
|
||||
// The aspect a sampled view of img reads (vk_exec_draw.c get_view): stencil for a view in STENCIL depth-stencil mode.
|
||||
VkImageAspectFlags vk_exec_view_aspect(const VkTexImage *img, const view_t *v, int hv);
|
||||
VkImageView vk_exec_target_view(const VkTexImage *img, VkFormat format, VkImageViewType type, uint32_t aspect,
|
||||
@@ -193,10 +264,26 @@ void vk_exec_op_xfer(VkExec *ex, uint32_t op, const void *payload);
|
||||
// sample DLSS's output vk_dlaa_swap_view in its place, so the game's scaling draw scales DLSS's picture.
|
||||
extern const VkTexImage *vk_dlaa_swap_img;
|
||||
extern VkImageView vk_dlaa_swap_view;
|
||||
// vk_contact.c: a pass about to begin (its targets resolved, no pass open), and a dispatch or copy about to be recorded
|
||||
// The state scan's part of the transfer ops: render enable, conditional rendering (returns the condition: 1 renders),
|
||||
// samples-passed counting state and the pending reports conditions read. Returns 1 for every other op.
|
||||
// The state scan of a chunked replay (vk_exec.c): the textures op uses go live in replay order (vk_texture_revive); pos:
|
||||
// the op's place in the submission. 1 when one of them has no entry yet.
|
||||
int vk_exec_xfer_revive(uint32_t op, const void *payload, uint32_t pos);
|
||||
int vk_exec_xfer_scan(VkExec *ex, uint32_t op, const void *payload);
|
||||
// The guest memory a COPY_B2B, COPY_T2B or CLEAR_BUF op makes the GPU write, as far as the scan can tell without the
|
||||
// texture's image: [*addr, *addr + return value).
|
||||
uint64_t vk_exec_xfer_written(const VkExec *ex, uint32_t op, const void *payload, uint64_t *addr);
|
||||
// vk_contact.c: a pass about to begin (its targets resolved, no pass open), and a dispatch or copy about to be recorded.
|
||||
// vk_contact_plan is what they do in replay order (pass: the first; else the second), for the scan; 1: a march is due (j).
|
||||
void vk_contact_pass_begin(VkExec *ex);
|
||||
void vk_contact_flush(VkExec *ex);
|
||||
int vk_contact_plan(const VkExec *ex, int pass, VkExecJob *j);
|
||||
void vk_contact_exec(VkExec *ex, const VkExecJob *j);
|
||||
// vk_exec_xfer.c, samples-passed counting: a draw about to be recorded in the open pass (begins an occlusion query
|
||||
// when counting and none is active), and the open pass about to end (ends the active query).
|
||||
void vk_exec_oq_draw(VkExec *ex);
|
||||
void vk_exec_oq_pass_end(VkExec *ex);
|
||||
void vk_exec_oq_forget(VkExec *ex);
|
||||
void vk_exec_oq_flush(VkExec *ex); // the end of a chunk: its closed queries counted as a segment without a report // vk_exec_xfer_shutdown: the context's queries belonged to the old pool
|
||||
// vk_exec.c: fn on every recording context (teardown, the device idle).
|
||||
void vk_exec_contexts(void (*fn)(VkExec *ex));
|
||||
+1018
-369
File diff suppressed because it is too large.
Load diff
@@ -968,6 +968,9 @@ int probe_vkexec(void) {
|
||||
const GfxBackend *b = &gfx_vulkan_backend;
|
||||
uint64_t heap = 0;
|
||||
CHECK(mem_set_heap_size(16ull << 20, &heap) == 0 && heap, "SetHeapSize 16 MiB");
|
||||
// Out-of-memory waits (oom.c): the first two staging buffers, the first texture image and the first draw-data chunk
|
||||
// fail; every part below passes through the wait or the retry that follows.
|
||||
rt_oom_force(RT_OOM_STAGING, 2), rt_oom_force(RT_OOM_IMAGE, 1), rt_oom_force(RT_OOM_CHUNK, 1);
|
||||
uint8_t *guest = (uint8_t *)(uintptr_t)heap;
|
||||
uint64_t first = 0, last = 0;
|
||||
int w = 0, h = 0, inside = 0, xfer_bytes = 0;
|
||||
@@ -989,6 +992,12 @@ int probe_vkexec(void) {
|
||||
vk_exec_xfer_shutdown();
|
||||
vk_backend_shutdown();
|
||||
long errors = vk_validation_errors_total();
|
||||
int oom_left[3] = {rt_oom_forced_left(RT_OOM_STAGING), rt_oom_forced_left(RT_OOM_IMAGE), rt_oom_forced_left(RT_OOM_CHUNK)};
|
||||
if (!bad && !errors && (oom_left[0] || oom_left[1] || oom_left[2])) {
|
||||
printf("probe-vkexec: FAILED (forced allocation failures never reached: staging %d, image %d, chunk %d)\n",
|
||||
oom_left[0], oom_left[1], oom_left[2]);
|
||||
return 1;
|
||||
}
|
||||
if (bad || errors) {
|
||||
printf("probe-vkexec: FAILED (%s, %ld validation errors)\n",
|
||||
sync_bad ? "a sync check failed" : draw_bad ? "a draw check failed" : bad ? "a clear or xfer check failed" : "checks passed", errors);
|
||||
@@ -1034,5 +1043,7 @@ int probe_vkexec(void) {
|
||||
"finished mark is the last pass, ring records exact); the report reads:\n%s\n", marks_text);
|
||||
printf("probe-vkexec: empty pool part passed (a zero-byte pool at the start of another: no Vulkan objects, lookups "
|
||||
"there find the other pool before and after its Finalize)\n");
|
||||
printf("probe-vkexec: out-of-memory part passed (2 failed staging buffers waited for, 1 failed texture image retried, "
|
||||
"1 failed draw-data chunk waited for)\n");
|
||||
return 0;
|
||||
}
|
||||
+115
-38
@@ -72,7 +72,8 @@ static void guest_written_cb(void *arg) {
|
||||
|
||||
// The GPU wrote [addr, addr+size) of CPU-visible memory: tell the texture cache once the fence is reached.
|
||||
static void note_gpu_write(VkExec *ex, uint64_t addr, uint64_t size) {
|
||||
vk_usnap_drop(addr, size);
|
||||
if (!ex->chunked) vk_usnap_drop(addr, size); // a chunk: the scan dropped it in its place
|
||||
vk_exec_ub_foreign(ex, addr, size);
|
||||
if (nvn_gpu_only(addr)) return;
|
||||
Range *r = malloc(sizeof *r);
|
||||
if (!r) rt_fatal("vk: out of memory");
|
||||
@@ -128,13 +129,6 @@ static int oq_state; // 0 not set up, 1 counting available,
|
||||
static VkQueryControlFlags oq_flags;
|
||||
static volatile uint8_t oq_busy[OQ_N]; // from begin until the completion thread read and reset the slot
|
||||
static uint32_t oq_next; // replay: next ring slot
|
||||
static int oq_counting; // replay: draws are counted (a reset since the last report, or oq_keep)
|
||||
static int oq_reported; // replay: a samples-passed report since the last reset
|
||||
static int oq_keep; // replay: the game reported twice without a reset: count every draw
|
||||
static int oq_open = -1; // replay: the slot whose query is active in the open pass
|
||||
static uint32_t *oq_seg; // replay: slots closed since the last report or reset
|
||||
static uint32_t oq_nseg, oq_capseg;
|
||||
static int oq_lost; // replay: the segment missed a draw (ring full)
|
||||
static uint64_t oq_count; // completion thread: samples since the last reset
|
||||
|
||||
typedef struct { uint64_t addr; int reset, lost; uint32_t n; uint32_t q[]; } OqSegment;
|
||||
@@ -165,39 +159,49 @@ static int oq_ready(void) {
|
||||
}
|
||||
|
||||
static void oq_close(VkExec *ex) {
|
||||
if (oq_open < 0) return;
|
||||
vkCmdEndQuery(ex->cb, oq_pool, (uint32_t)oq_open);
|
||||
if (ex->oq_open < 0) return;
|
||||
vkCmdEndQuery(ex->cb, oq_pool, (uint32_t)ex->oq_open);
|
||||
ex->recorded++;
|
||||
if (oq_nseg == oq_capseg) {
|
||||
oq_capseg = oq_capseg ? oq_capseg * 2 : 64;
|
||||
oq_seg = realloc(oq_seg, sizeof *oq_seg * oq_capseg);
|
||||
if (!oq_seg) rt_fatal("vk: out of memory");
|
||||
if (ex->oq_nseg == ex->oq_capseg) {
|
||||
ex->oq_capseg = ex->oq_capseg ? ex->oq_capseg * 2 : 64;
|
||||
ex->oq_seg = realloc(ex->oq_seg, sizeof *ex->oq_seg * ex->oq_capseg);
|
||||
if (!ex->oq_seg) rt_fatal("vk: out of memory");
|
||||
}
|
||||
oq_seg[oq_nseg++] = (uint32_t)oq_open;
|
||||
oq_open = -1;
|
||||
ex->oq_seg[ex->oq_nseg++] = (uint32_t)ex->oq_open;
|
||||
ex->oq_open = -1;
|
||||
}
|
||||
|
||||
void vk_exec_oq_draw(VkExec *ex) {
|
||||
if (!oq_counting || oq_open >= 0) return;
|
||||
uint32_t q = oq_next % OQ_N;
|
||||
if (__atomic_load_n(&oq_busy[q], __ATOMIC_ACQUIRE)) {
|
||||
if (!ex->oq_counting || ex->oq_open >= 0) return;
|
||||
uint32_t q = __atomic_fetch_add(&oq_next, 1, __ATOMIC_RELAXED) % OQ_N; // threads recording chunks take slots too
|
||||
if (__atomic_exchange_n(&oq_busy[q], 1, __ATOMIC_ACQUIRE)) {
|
||||
warn_once(W_QUERY, "occlusion query ring full: draws left uncounted");
|
||||
oq_lost = 1;
|
||||
ex->oq_lost = 1;
|
||||
return;
|
||||
}
|
||||
oq_busy[q] = 1;
|
||||
oq_next++;
|
||||
vkCmdBeginQuery(ex->cb, oq_pool, q, oq_flags);
|
||||
ex->recorded++;
|
||||
oq_open = (int)q;
|
||||
ex->oq_open = (int)q;
|
||||
}
|
||||
|
||||
void vk_exec_oq_pass_end(VkExec *ex) { oq_close(ex); }
|
||||
|
||||
static void oq_segment(VkExec *ex, uint64_t addr, int reset);
|
||||
|
||||
// The queries a chunk closed: the completion thread adds them to the count before the next chunk's (vk_exec.c).
|
||||
void vk_exec_oq_flush(VkExec *ex) {
|
||||
if (ex->oq_nseg || ex->oq_lost) oq_segment(ex, 0, 0);
|
||||
}
|
||||
|
||||
void vk_exec_oq_forget(VkExec *ex) {
|
||||
ex->oq_counting = ex->oq_reported = ex->oq_keep = 0, ex->oq_open = -1;
|
||||
ex->oq_nseg = 0, ex->oq_lost = 0;
|
||||
}
|
||||
|
||||
void vk_exec_xfer_shutdown(void) {
|
||||
if (oq_state > 0) vkDestroyQueryPool(vk_ctx()->device, oq_pool, NULL);
|
||||
oq_pool = VK_NULL_HANDLE, oq_state = 0, oq_next = 0, oq_counting = oq_reported = oq_keep = 0, oq_open = -1;
|
||||
oq_nseg = 0, oq_lost = 0, oq_count = 0;
|
||||
oq_pool = VK_NULL_HANDLE, oq_state = 0, oq_next = 0, oq_count = 0;
|
||||
vk_exec_contexts(vk_exec_oq_forget);
|
||||
memset((void *)oq_busy, 0, sizeof oq_busy);
|
||||
}
|
||||
|
||||
@@ -239,30 +243,34 @@ static void oq_cb(void *arg) {
|
||||
// Hands the segment to the completion thread with a report (addr; 0: no CPU-visible memory) or a reset.
|
||||
static void oq_segment(VkExec *ex, uint64_t addr, int reset) {
|
||||
oq_close(ex);
|
||||
OqSegment *s = malloc(sizeof *s + sizeof *s->q * oq_nseg);
|
||||
OqSegment *s = malloc(sizeof *s + sizeof *s->q * ex->oq_nseg);
|
||||
if (!s) rt_fatal("vk: out of memory");
|
||||
s->addr = addr, s->reset = reset, s->lost = oq_lost, s->n = oq_nseg;
|
||||
memcpy(s->q, oq_seg, sizeof *s->q * oq_nseg);
|
||||
oq_nseg = 0, oq_lost = 0;
|
||||
s->addr = addr, s->reset = reset, s->lost = ex->oq_lost, s->n = ex->oq_nseg;
|
||||
memcpy(s->q, ex->oq_seg, sizeof *s->q * ex->oq_nseg);
|
||||
ex->oq_nseg = 0, ex->oq_lost = 0;
|
||||
vk_completion_add(ex->fence, oq_cb, s);
|
||||
}
|
||||
|
||||
static void reset_counter(VkExec *ex, const p_reset_counter *p) {
|
||||
if (p->type != NVN_COUNTER_TYPE_SAMPLES_PASSED || !oq_ready()) return;
|
||||
oq_segment(ex, 0, 1);
|
||||
oq_counting = 1, oq_reported = 0;
|
||||
ex->oq_counting = 1, ex->oq_reported = 0;
|
||||
}
|
||||
|
||||
// A samples-passed report: the segment goes with it (addr 0: no CPU-visible memory) and counting stops until the next
|
||||
// reset, unless reports came without resets before.
|
||||
static void oq_report_state(VkExec *ex) {
|
||||
if (ex->oq_reported && !ex->oq_keep) {
|
||||
ex->oq_keep = 1;
|
||||
LOG_ONCE("vk: samples passed reported twice without a reset: counting every draw from now on");
|
||||
}
|
||||
ex->oq_reported = 1;
|
||||
if (!ex->oq_keep) ex->oq_counting = 0;
|
||||
}
|
||||
|
||||
static void oq_report(VkExec *ex, uint64_t addr) {
|
||||
oq_segment(ex, addr, 0);
|
||||
if (oq_reported && !oq_keep) {
|
||||
oq_keep = 1;
|
||||
rt_log("vk: samples passed reported twice without a reset: counting every draw from now on");
|
||||
}
|
||||
oq_reported = 1;
|
||||
if (!oq_keep) oq_counting = 0;
|
||||
oq_report_state(ex);
|
||||
}
|
||||
|
||||
static void signal_event(VkExec *ex, const p_signal_event *p) {
|
||||
@@ -289,7 +297,7 @@ static void report_counter(VkExec *ex, const p_report_counter *p) {
|
||||
*a = p->addr;
|
||||
vk_completion_add(ex->fence, counter_cb, a);
|
||||
}
|
||||
pending[pending_head++ % PENDING] = (typeof(pending[0])){p->addr, ex->fence, samples};
|
||||
if (!ex->chunked) pending[pending_head++ % PENDING] = (typeof(pending[0])){p->addr, ex->fence, samples}; // else the scan's
|
||||
}
|
||||
|
||||
// ---- conditional rendering
|
||||
@@ -611,8 +619,77 @@ void vk_exec_op_xfer(VkExec *ex, uint32_t op, const void *payload) {
|
||||
case OP_RENDER_COND:
|
||||
ex->render_cond = *(const p_render_cond *)payload;
|
||||
ex->has_render_cond = 1;
|
||||
ex->render_disabled = !cond_enabled(payload);
|
||||
ex->render_disabled = !(ex->chunked ? ex->cond[ex->cond_i++] : cond_enabled(payload));
|
||||
break;
|
||||
default: rt_fatal("vk: op %s is not a transfer", nvn_op_name(op));
|
||||
}
|
||||
}
|
||||
|
||||
static int revive_new(const tex_t *t, uint32_t pos) { return t && t->mem && !vk_texture_revive(t, pos); }
|
||||
|
||||
int vk_exec_xfer_revive(uint32_t op, const void *payload, uint32_t pos) {
|
||||
if (op == OP_COPY_T2T) {
|
||||
int n = revive_new(((const p_copy_t2t *)payload)->src, pos);
|
||||
return revive_new(((const p_copy_t2t *)payload)->dst, pos) | n;
|
||||
}
|
||||
if (op == OP_COPY_B2T || op == OP_COPY_T2B) return revive_new(((const p_copy_b2t *)payload)->tex, pos);
|
||||
if (op == OP_CLEAR_TEX) return revive_new(((const p_clear_tex *)payload)->tex, pos);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t vk_exec_xfer_written(const VkExec *ex, uint32_t op, const void *payload, uint64_t *addr) {
|
||||
VkMemSpan m;
|
||||
if (op == OP_COPY_B2B) {
|
||||
const p_copy_b2b *p = payload;
|
||||
*addr = p->dst;
|
||||
return vk_mem_resolve(p->src, 1, &m) && vk_mem_resolve(p->dst, 1, &m) ? p->size : 0;
|
||||
}
|
||||
if (op == OP_CLEAR_BUF) {
|
||||
const p_clear_buf *p = payload;
|
||||
*addr = p->addr;
|
||||
return (p->addr | p->size) & 3 || !vk_mem_resolve(p->addr, 1, &m) ? 0 : p->size;
|
||||
}
|
||||
// COPY_T2B: the buffer side of copy_buffer_image from the texture descriptor (sub_resolve, buffer_side)
|
||||
const p_copy_b2t *p = payload;
|
||||
const tex_t *t = p->tex;
|
||||
int bw, bh, bpb = t ? nvn_fmt_bpb(t->d.format, &bw, &bh) : 0;
|
||||
*addr = p->src;
|
||||
if (!bpb) return 0;
|
||||
uint32_t level = p->has_view && p->view.set_levels ? p->view.base_level : 0;
|
||||
uint32_t lw = max1((uint32_t)t->d.width >> level), lh = max1((uint32_t)t->d.height >> level);
|
||||
const NVNcopyRegion *r = &p->r;
|
||||
uint32_t w = r->width > 0 ? (uint32_t)r->width : r->xoffset < (int32_t)lw ? lw - (uint32_t)r->xoffset : 0;
|
||||
uint32_t h = r->height > 0 ? (uint32_t)r->height : r->yoffset < (int32_t)lh ? lh - (uint32_t)r->yoffset : 0;
|
||||
uint32_t nz = r->depth > 0 ? (uint32_t)r->depth : 1;
|
||||
uint32_t wb = (w + (uint32_t)bw - 1) / (uint32_t)bw, hb = (h + (uint32_t)bh - 1) / (uint32_t)bh;
|
||||
if (!wb || !hb) return 0;
|
||||
uint64_t row = ex->copy_stride > 0 ? (uint64_t)ex->copy_stride : (uint64_t)wb * (uint64_t)bpb;
|
||||
return row * hb * (nz - 1) + row * (hb - 1) + (uint64_t)wb * (uint64_t)bpb;
|
||||
}
|
||||
|
||||
int vk_exec_xfer_scan(VkExec *ex, uint32_t op, const void *payload) {
|
||||
switch (op) {
|
||||
case OP_RENDER_ENABLE: ex->render_disabled = !((const p_flag *)payload)->value; break;
|
||||
case OP_RENDER_COND: {
|
||||
int on = cond_enabled(payload);
|
||||
ex->render_cond = *(const p_render_cond *)payload;
|
||||
ex->has_render_cond = 1;
|
||||
ex->render_disabled = !on;
|
||||
return on;
|
||||
}
|
||||
case OP_RESET_COUNTER:
|
||||
if (((const p_reset_counter *)payload)->type == NVN_COUNTER_TYPE_SAMPLES_PASSED && oq_ready())
|
||||
ex->oq_counting = 1, ex->oq_reported = 0;
|
||||
break;
|
||||
case OP_REPORT_COUNTER: {
|
||||
const p_report_counter *p = payload;
|
||||
int samples = p->type == NVN_COUNTER_TYPE_SAMPLES_PASSED && oq_ready();
|
||||
VkMemSpan m;
|
||||
if (!nvn_gpu_only(p->addr) && !vk_mem_resolve(p->addr, sizeof(NVNcounterData), &m)) break; // the recording stops
|
||||
if (samples) oq_report_state(ex);
|
||||
if (!nvn_gpu_only(p->addr)) pending[pending_head++ % PENDING] = (typeof(pending[0])){p->addr, ex->fence, samples};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user