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.
|
||||
@@ -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,6 +85,8 @@ 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
|
||||
|
||||
+6
-25
@@ -160,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)
|
||||
|
||||
@@ -241,7 +220,9 @@ files); a change to the settings passes `totk.exe --probe-cfg` and `python tests
|
||||
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`.
|
||||
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.
|
||||
|
||||
+5
-3
@@ -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
|
||||
|
||||
|
||||
+6
-3
@@ -74,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.
|
||||
@@ -157,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)
|
||||
```
|
||||
|
||||
+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.
|
||||
@@ -77,6 +77,7 @@ Many rows below are developer tools that only act together with another setting
|
||||
| `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 |
|
||||
|
||||
@@ -121,9 +122,11 @@ Many rows below are developer tools that only act together with another setting
|
||||
| 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 |
|
||||
@@ -134,7 +137,9 @@ Many rows below are developer tools that only act together with another setting
|
||||
| `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 |
|
||||
@@ -180,11 +185,21 @@ Many rows below are developer tools that only act together with another setting
|
||||
| --- | --- | --- | --- |
|
||||
| `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
|
||||
|
||||
@@ -193,6 +208,7 @@ These names are code patch sets built into the generated game code (`recomp/patc
|
||||
| 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 |
|
||||
|
||||
+24
-3
@@ -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
|
||||
@@ -168,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
|
||||
|
||||
@@ -1097,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}'
|
||||
@@ -1108,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]))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1079,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)
|
||||
+47
-23
@@ -38,29 +38,29 @@ 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", "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", "DYN_DELTA", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"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_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_ASPECT", "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",
|
||||
"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_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
"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_HEAP_EXTRA_MB", "TEX_MEMO", "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", "VOLUME", "VRAM_HEADROOM", "VRAM_LIMIT_MIB", "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",
|
||||
};
|
||||
@@ -69,9 +69,10 @@ static const char *const known[] = {
|
||||
static const struct { const char *key, *section; } sections[] = {
|
||||
{"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"}, {"MOVIE_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"},
|
||||
@@ -95,13 +96,13 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"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"},
|
||||
{"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"},
|
||||
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "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"},
|
||||
@@ -119,6 +120,7 @@ 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},
|
||||
@@ -141,7 +143,7 @@ static const CfgMeta metas[] = {
|
||||
{"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)
|
||||
@@ -163,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},
|
||||
@@ -170,7 +176,8 @@ 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},
|
||||
@@ -337,8 +344,10 @@ static const char *raw_get(const char *name) {
|
||||
// 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 or keyboard prompts keep 1920x1080 too: the setup builds their folders for 16:9 only (romfs.c overlay_init), so
|
||||
// a default of another shape would trade the prompts the player chose for the Switch ones.
|
||||
// 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
|
||||
@@ -346,10 +355,22 @@ static const unsigned SHAPED[][2] = {
|
||||
{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];
|
||||
|
||||
static int switch_prompts(void) {
|
||||
// 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");
|
||||
return (!p || !strcasecmp(p, "nintendo")) && !(l && !strcasecmp(l, "xbox_prompts"));
|
||||
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) {
|
||||
@@ -367,7 +388,9 @@ static void screen_default(void) {
|
||||
if (best < 0) best = fit;
|
||||
if (best < 0) return;
|
||||
snprintf(screen_res, sizeof screen_res, "%ux%u", SHAPED[best][0], SHAPED[best][1]);
|
||||
if (!raw_get(PREFIX "OUTPUT_RES") && switch_prompts())
|
||||
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);
|
||||
}
|
||||
@@ -376,7 +399,8 @@ const char *cfg_get(const char *name) {
|
||||
const char *v = raw_get(name);
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN)) return v;
|
||||
if (!profile_of())
|
||||
return screen_res[0] && !strcasecmp(name + PREFIX_LEN, "OUTPUT_RES") && switch_prompts() ? screen_res : NULL;
|
||||
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;
|
||||
|
||||
+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);
|
||||
|
||||
+14
-2
@@ -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;
|
||||
@@ -236,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);
|
||||
|
||||
@@ -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
|
||||
@@ -171,6 +175,23 @@ int mem_guest_readable(uint64_t a, uint64_t n); // [a, a+n) can be re
|
||||
// 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 {
|
||||
const char *name; uint64_t base, size, text_end, data_off; // text_end, data_off: offsets from base
|
||||
@@ -280,8 +301,11 @@ int res_is_16x9(unsigned w, unsigned h); // within 1% of 16:9
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+139
-5
@@ -21,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>
|
||||
@@ -29,6 +30,7 @@
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "layout.h"
|
||||
#include "platform/platform.h"
|
||||
#include "sha256.h"
|
||||
|
||||
@@ -188,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);
|
||||
@@ -211,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);
|
||||
@@ -230,8 +334,22 @@ static void crash_handler(const PlatCrash *x) {
|
||||
rt_log_raw("\n", 1);
|
||||
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 : "");
|
||||
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));
|
||||
// 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();
|
||||
@@ -266,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);
|
||||
@@ -532,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
|
||||
|
||||
+21
-4
@@ -175,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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -148,6 +148,7 @@ static void *realloc_wait(void *old, size_t n, const char *what) {
|
||||
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) {
|
||||
|
||||
+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)
|
||||
|
||||
@@ -713,6 +713,7 @@ 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] = cfg_bool("TOTK_FRAME_QUEUE") ? slot : slot + period;
|
||||
|
||||
+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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -481,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
|
||||
@@ -949,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
|
||||
@@ -1661,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.
|
||||
@@ -243,7 +249,11 @@ typedef struct {
|
||||
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
|
||||
@@ -423,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -780,6 +904,7 @@ 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);
|
||||
uintptr_t exe = (uintptr_t)GetModuleHandleW(NULL);
|
||||
@@ -1659,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;
|
||||
|
||||
+78
-18
@@ -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
|
||||
@@ -40,6 +41,7 @@
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "mods.h"
|
||||
#include "ov.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define BASE_W 1920
|
||||
@@ -48,7 +50,9 @@
|
||||
#define SCENE_H 900
|
||||
#define DTA_KIB 113664u // DynamicTextureAllocatorMem, shipped
|
||||
#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 4096 // per side, as MacNativeGames' TOTK_RENDER_RES
|
||||
#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 {
|
||||
@@ -91,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
|
||||
@@ -168,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
|
||||
@@ -178,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
|
||||
@@ -209,6 +230,17 @@ 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
|
||||
@@ -485,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
|
||||
@@ -562,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;
|
||||
@@ -614,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;
|
||||
|
||||
+44
-36
@@ -919,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; }
|
||||
|
||||
@@ -964,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;
|
||||
@@ -991,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
|
||||
@@ -1048,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();
|
||||
@@ -1080,19 +1081,26 @@ static void index_path(char *out, size_t cap) {
|
||||
strcpy(out + k, "romfs-index.bin");
|
||||
}
|
||||
|
||||
// 1 when the game resolution w x h has the UI layout of its shape: romfs-patches-<w>x<h> next to romfs-index.bin with a
|
||||
// manifest, the folder overlay_init looks for. Works before fs_init: index_dir is not set that early (res_init and
|
||||
// resolution.c decide run first), so the index path is worked out again here.
|
||||
int romfs_shaped_ready(unsigned w, unsigned h) {
|
||||
// 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/romfs-patches-%ux%u/manifest.tsv", dir, w, h) < (int)sizeof path &&
|
||||
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);
|
||||
|
||||
+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);
|
||||
|
||||
+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);
|
||||
+30
-3
@@ -51,6 +51,7 @@ 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;
|
||||
@@ -58,6 +59,7 @@ 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.
|
||||
@@ -353,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;
|
||||
@@ -663,6 +668,7 @@ 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);
|
||||
@@ -791,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) {
|
||||
@@ -905,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)};
|
||||
@@ -954,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);
|
||||
@@ -1081,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));
|
||||
}
|
||||
@@ -1536,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);
|
||||
|
||||
+43
-9
@@ -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.",
|
||||
@@ -181,6 +183,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[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",
|
||||
@@ -260,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",
|
||||
@@ -386,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",
|
||||
@@ -437,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",
|
||||
@@ -545,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 сохраняет края и "
|
||||
"детали чётче билинейного.",
|
||||
@@ -655,6 +665,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[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] = "Режим консоли",
|
||||
@@ -732,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] = "Перезапустить сейчас",
|
||||
@@ -862,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] = "Обработка ожидающих отрисовок",
|
||||
@@ -913,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] = "Пишет время кадров в файл.",
|
||||
@@ -1058,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.",
|
||||
@@ -1093,6 +1111,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[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",
|
||||
@@ -1235,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",
|
||||
@@ -1288,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.",
|
||||
@@ -1394,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",
|
||||
@@ -1580,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 比双线性更能保留边缘和细节。",
|
||||
@@ -1637,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] = "自动",
|
||||
@@ -1842,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] = "等待中绘制的处理",
|
||||
@@ -1917,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] = "性能日志",
|
||||
@@ -2014,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,6 +35,7 @@ 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,
|
||||
@@ -67,6 +68,7 @@ typedef enum {
|
||||
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,
|
||||
@@ -175,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,
|
||||
|
||||
+57
-4
@@ -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.
|
||||
@@ -334,6 +361,8 @@ void vk_defer_buffer(VkBuffer buf, VmaAllocation alloc);
|
||||
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);
|
||||
@@ -387,6 +416,10 @@ int vk_mem_span_live(const VkMemSpan *sp);
|
||||
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
|
||||
@@ -426,6 +459,7 @@ typedef struct {
|
||||
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
|
||||
@@ -433,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
|
||||
@@ -495,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);
|
||||
|
||||
+6
-4
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+9
-2
@@ -429,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()};
|
||||
@@ -475,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_upload_pools_init(), 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;
|
||||
|
||||
+125
-32
@@ -242,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;
|
||||
@@ -557,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;
|
||||
@@ -777,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);
|
||||
@@ -829,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)
|
||||
@@ -867,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
|
||||
@@ -1045,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
|
||||
@@ -1062,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
|
||||
@@ -1149,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;
|
||||
}
|
||||
@@ -1157,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;
|
||||
}
|
||||
|
||||
@@ -1178,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.
|
||||
|
||||
+877
-122
File diff suppressed because it is too large.
Load diff
+94
-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);
|
||||
@@ -136,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);
|
||||
@@ -155,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
|
||||
@@ -188,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,
|
||||
@@ -199,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));
|
||||
+680
-380
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;
|
||||
}
|
||||
+114
-38
@@ -72,7 +72,7 @@ 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);
|
||||
@@ -129,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;
|
||||
@@ -166,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);
|
||||
}
|
||||
|
||||
@@ -240,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) {
|
||||
@@ -290,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
|
||||
@@ -612,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;
|
||||
}
|
||||
+5
-2
@@ -243,7 +243,9 @@ void vk_hdr_shader_swap(uint32_t **spirv, uint64_t *words) {
|
||||
int vk_hdr_track; // vk_exec_draw.c calls vk_hdr_draw: set at the first present with TOTK_HDR=1
|
||||
static volatile int32_t g_active; // the swapchain is HDR10 (gfx_present.c vk_hdr_set_active)
|
||||
static PlatMutex rec_lock = PLAT_MUTEX_INIT;
|
||||
static struct { VkImage win, src; VkFormat fmt; } recs[4]; // per presented window image: what the last draw copied
|
||||
// Per presented window image: what the last draw in replay order copied, and where that draw is. Chunks of a replay
|
||||
// record at once, so a draw of an earlier chunk can get here after one of a later chunk.
|
||||
static struct { VkImage win, src; VkFormat fmt; uint64_t fence; uint32_t pos; } recs[4];
|
||||
static int nrecs;
|
||||
|
||||
void vk_hdr_set_active(int on) { plat_xchg32(&g_active, on); }
|
||||
@@ -277,7 +279,8 @@ void vk_hdr_draw(VkExec *ex, const VkPipeInfo *info) {
|
||||
}
|
||||
plat_mutex_lock(&rec_lock);
|
||||
for (int i = 0; i < nrecs; i++)
|
||||
if (recs[i].win == t->image) recs[i].src = src, recs[i].fmt = fmt;
|
||||
if (recs[i].win == t->image && (ex->fence > recs[i].fence || (ex->fence == recs[i].fence && ex->pos >= recs[i].pos)))
|
||||
recs[i].src = src, recs[i].fmt = fmt, recs[i].fence = ex->fence, recs[i].pos = ex->pos;
|
||||
plat_mutex_unlock(&rec_lock);
|
||||
}
|
||||
|
||||
|
||||
+67
-19
@@ -12,7 +12,9 @@
|
||||
// a block per memory type is held back.
|
||||
// Under VRAM pressure no headroom is kept: the spare block counts against the budget that decides whether a pool gets
|
||||
// its VRAM mirror (vk_memory.c), so while use plus two blocks passes 85% of the budget the anchor goes and nothing new
|
||||
// is allocated.
|
||||
// is allocated. The same holds when the system runs short of memory (room), and for HR_HOLD_MS after an allocation
|
||||
// failed anywhere (vk_headroom_release).
|
||||
// The texture pool (vk_memory.c vk_texture_image) is kept ahead the same way, with its own anchor of half its block.
|
||||
// TOTK_VRAM_HEADROOM=0 turns it off.
|
||||
|
||||
#include "cfg.h"
|
||||
@@ -22,6 +24,7 @@
|
||||
|
||||
#define HR_PERIOD_MS 500
|
||||
#define HR_SMALL_HEAP (1ull << 30) // VMA_SMALL_HEAP_MAX_SIZE: smaller heaps use blocks of a heap eighth
|
||||
#define HR_HOLD_MS 30000u
|
||||
|
||||
static struct {
|
||||
int on, stop;
|
||||
@@ -33,6 +36,11 @@ static struct {
|
||||
volatile int64_t made; // blocks this thread brought in
|
||||
volatile int64_t other; // device memory of 32 MiB or more other threads allocated (VMA blocks, large dedicated)
|
||||
VmaAllocation anchor[64]; // per memory type: the half block that keeps the newest spare block from being deleted
|
||||
VmaPool pool; // the texture pool, its memory type and block size, and its anchor
|
||||
uint32_t pool_type;
|
||||
VkDeviceSize pool_block;
|
||||
VmaAllocation pool_anchor;
|
||||
volatile uint64_t hold_until; // plat_now_ms before which no spare block is kept (vk_headroom_release)
|
||||
} H;
|
||||
|
||||
static void VKAPI_PTR on_allocate(VmaAllocator a, uint32_t type, VkDeviceMemory m, VkDeviceSize size, void *user) {
|
||||
@@ -55,29 +63,36 @@ const VmaDeviceMemoryCallbacks *vk_headroom_callbacks(void) {
|
||||
|
||||
static VkDeviceSize block_size(const VkCtx *c, uint32_t type) {
|
||||
VkDeviceSize heap = c->mem_props.memoryHeaps[c->mem_props.memoryTypes[type].heapIndex].size;
|
||||
return heap <= HR_SMALL_HEAP ? heap / 8 : 256ull << 20; // VMA's default preferredLargeHeapBlockSize
|
||||
return heap <= HR_SMALL_HEAP ? heap / 8 : c->vma_block; // VMA's preferredLargeHeapBlockSize
|
||||
}
|
||||
|
||||
// The spare block is memory the game does not use yet; where device memory takes host memory it is kept only while the
|
||||
// system has four blocks to spare beyond the reserve (vk_mem_host_room). On the RX 9070 the spare blocks held 1.4 GB
|
||||
// of commit.
|
||||
static int room(const VkCtx *c, uint32_t type, VkDeviceSize block) {
|
||||
VmaBudget b[VK_MAX_MEMORY_HEAPS];
|
||||
vmaGetHeapBudgets(c->vma, b);
|
||||
const VmaBudget *h = &b[c->mem_props.memoryTypes[type].heapIndex];
|
||||
return h->budget && h->usage + 2 * block <= h->budget / 100 * 85;
|
||||
return h->budget && h->usage + 2 * block <= h->budget / 100 * 85 && vk_mem_host_room() >= 4 * block;
|
||||
}
|
||||
|
||||
static void drop_anchor(const VkCtx *c, uint32_t type) {
|
||||
if (H.anchor[type]) vmaFreeMemory(c->vma, H.anchor[type]);
|
||||
H.anchor[type] = NULL;
|
||||
static void drop_anchor(const VkCtx *c, VmaAllocation *anchor) {
|
||||
if (*anchor) vmaFreeMemory(c->vma, *anchor);
|
||||
*anchor = NULL;
|
||||
}
|
||||
|
||||
static void top_up(const VkCtx *c, uint32_t type) {
|
||||
VkDeviceSize block = block_size(c, type);
|
||||
if (!room(c, type, block)) {
|
||||
drop_anchor(c, type);
|
||||
// pool NULL: the default pool of the type
|
||||
static void top_up(const VkCtx *c, uint32_t type, VmaPool pool, VkDeviceSize block, VmaAllocation *anchor) {
|
||||
// Device-local types only: textures and mirrors grow there. Host memory types are woken by the staging pool's own
|
||||
// blocks (vk_memory.c vk_staging), while their default pool held 4 MiB: on the RX 9070 the spare blocks of types 1
|
||||
// and 3 held 512 MiB of commit for nothing.
|
||||
if (!(c->mem_props.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) ||
|
||||
plat_now_ms() < __atomic_load_n(&H.hold_until, __ATOMIC_RELAXED) || !room(c, type, block)) {
|
||||
drop_anchor(c, anchor);
|
||||
return;
|
||||
}
|
||||
VkMemoryRequirements req = {.size = block / 2, .alignment = 256, .memoryTypeBits = 1u << type};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_UNKNOWN};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_UNKNOWN, .pool = pool};
|
||||
VmaAllocation a;
|
||||
int64_t before = H.made;
|
||||
if (vmaAllocateMemory(c->vma, &req, &ai, &a, NULL) != VK_SUCCESS) return;
|
||||
@@ -85,12 +100,13 @@ static void top_up(const VkCtx *c, uint32_t type) {
|
||||
vmaFreeMemory(c->vma, a);
|
||||
return;
|
||||
}
|
||||
drop_anchor(c, type);
|
||||
H.anchor[type] = a;
|
||||
drop_anchor(c, anchor);
|
||||
*anchor = a;
|
||||
static volatile int32_t logged;
|
||||
if (plat_inc32(&logged) <= 32)
|
||||
rt_log("vk: headroom: a %.0f MiB block of memory type %u allocated ahead (%lld so far; %lld allocations of 32 MiB "
|
||||
"or more on other threads)", (double)block / 1048576.0, type, (long long)H.made, (long long)H.other);
|
||||
rt_log("vk: headroom: a %.0f MiB block of memory type %u%s allocated ahead (%lld so far; %lld allocations of "
|
||||
"32 MiB or more on other threads)", (double)block / 1048576.0, type, pool ? " (texture pool)" : "",
|
||||
(long long)H.made, (long long)H.other);
|
||||
}
|
||||
|
||||
static void headroom_main(void *arg) {
|
||||
@@ -105,10 +121,15 @@ static void headroom_main(void *arg) {
|
||||
plat_mutex_lock(&H.lock);
|
||||
const VkCtx *c = H.stop ? NULL : vk_ctx();
|
||||
for (uint32_t t = 0; c && t < c->mem_props.memoryTypeCount && t < 64; t++)
|
||||
if (watch >> t & 1) top_up(c, t);
|
||||
if (watch >> t & 1) {
|
||||
top_up(c, t, NULL, block_size(c, t), &H.anchor[t]);
|
||||
if (H.pool && t == H.pool_type) top_up(c, t, H.pool, H.pool_block, &H.pool_anchor);
|
||||
}
|
||||
int stop = H.stop;
|
||||
if (stop && (c = vk_ctx()))
|
||||
for (uint32_t t = 0; t < 64; t++) drop_anchor(c, t);
|
||||
if (stop && (c = vk_ctx())) {
|
||||
for (uint32_t t = 0; t < 64; t++) drop_anchor(c, &H.anchor[t]);
|
||||
drop_anchor(c, &H.pool_anchor);
|
||||
}
|
||||
plat_mutex_unlock(&H.lock);
|
||||
if (stop) return;
|
||||
}
|
||||
@@ -120,11 +141,38 @@ void vk_headroom_stop(void) {
|
||||
plat_mutex_lock(&H.lock);
|
||||
H.stop = 1;
|
||||
const VkCtx *c = vk_ctx();
|
||||
for (uint32_t t = 0; c && t < 64; t++) drop_anchor(c, t);
|
||||
for (uint32_t t = 0; c && t < 64; t++) drop_anchor(c, &H.anchor[t]);
|
||||
if (c) drop_anchor(c, &H.pool_anchor);
|
||||
plat_mutex_unlock(&H.lock);
|
||||
plat_event_set(&H.ev);
|
||||
}
|
||||
|
||||
void vk_headroom_pool(VmaPool pool, uint32_t type, VkDeviceSize block) {
|
||||
if (!H.started) return;
|
||||
plat_mutex_lock(&H.lock);
|
||||
const VkCtx *c = vk_ctx();
|
||||
if (c) drop_anchor(c, &H.pool_anchor);
|
||||
H.pool = pool, H.pool_type = type, H.pool_block = block;
|
||||
plat_mutex_unlock(&H.lock);
|
||||
}
|
||||
|
||||
// After a failed allocation: the spare blocks go at once (an empty VMA block takes the next allocation of its type or
|
||||
// is given back), and none is kept for HR_HOLD_MS. H.lock is held only around VMA calls, so the caller may hold any lock
|
||||
// of ours, just no VMA lock.
|
||||
void vk_headroom_release(void) {
|
||||
__atomic_store_n(&H.hold_until, plat_now_ms() + HR_HOLD_MS, __ATOMIC_RELAXED);
|
||||
if (!H.started) return;
|
||||
int n = 0;
|
||||
plat_mutex_lock(&H.lock);
|
||||
const VkCtx *c = H.stop ? NULL : vk_ctx();
|
||||
for (uint32_t t = 0; c && t < 64; t++)
|
||||
if (H.anchor[t]) drop_anchor(c, &H.anchor[t]), n++;
|
||||
if (c && H.pool_anchor) drop_anchor(c, &H.pool_anchor), n++;
|
||||
plat_mutex_unlock(&H.lock);
|
||||
static volatile int32_t logged;
|
||||
if (n && plat_inc32(&logged) <= 8) rt_log("vk: headroom: %d spare blocks freed after a failed allocation", n);
|
||||
}
|
||||
|
||||
void vk_headroom_start(void) {
|
||||
if (!H.on || H.started) return;
|
||||
plat_mutex_init(&H.lock);
|
||||
|
||||
+470
-31
@@ -71,6 +71,9 @@ typedef struct PoolRec {
|
||||
uint8_t *smap; // its mapping; offsets as in buf (from import_lo)
|
||||
uint32_t sgen; // gen_lock: every page write stamped up to this generation is in the shadow
|
||||
volatile uint8_t stouched; // on the touched list: the GPU may use it in the submission being recorded
|
||||
uint64_t suse; // shadow_lock: the newest timeline value whose submission reads the shadow
|
||||
uint64_t sdefer; // shadow_lock: the newest value whose prologue carries a deferred copy into the
|
||||
// shadow (TOTK_SHADOW_DEFER); 0 never
|
||||
} PoolRec;
|
||||
|
||||
static void mirror_forget(PoolRec *r); // pool_lock exclusive: off the mirrored list
|
||||
@@ -339,7 +342,8 @@ void vk_pool_fini(pool_t *p) {
|
||||
// recording may already name this pool: a mirrored pool's sync copies and write-backs, an import pool's buffer
|
||||
// through a texture upload or a released cache entry (vk_texture.c slot_resolve), a physical pool's memory under
|
||||
// the images vk_texture_range_gone just deferred to that submission. The game frees a pool only once its own GPU
|
||||
// work is done, so only the port's work can name it. Freed here, the submission ran with freed memory.
|
||||
// work is done, so only the port's work can name it. Freed here, the submission ran with freed memory. A shadow
|
||||
// with a deferred copy (TOTK_SHADOW_DEFER) is named by the queue (dq) until the prologue of that submission.
|
||||
pool_retiring(r);
|
||||
vk_completion_add(retire_value(), pool_retire, r);
|
||||
}
|
||||
@@ -490,12 +494,24 @@ int vk_mem_class_of(uint64_t addr) {
|
||||
// ---------------------------------------------------------------- census
|
||||
static volatile int64_t traffic[VK_MCLS_N][VK_MUSE_N];
|
||||
|
||||
static _Thread_local int64_t (*count_to)[VK_MUSE_N];
|
||||
|
||||
// Every caller is the replay (vk_exec*, under vk_sync_submit's lock), about ten adds per draw: one writer at a time, so
|
||||
// a plain add, not a locked one. An add racing the census's reset may be lost.
|
||||
// a plain add, not a locked one. An add racing the census's reset may be lost. Threads recording chunks in parallel
|
||||
// count into their own arrays (vk_mem_count_local), added with atomics once per chunk.
|
||||
void vk_mem_count(int cls, int use, uint64_t bytes) {
|
||||
if ((unsigned)cls < VK_MCLS_N && (unsigned)use < VK_MUSE_N)
|
||||
__atomic_store_n(&traffic[cls][use], __atomic_load_n(&traffic[cls][use], __ATOMIC_RELAXED) + (int64_t)bytes,
|
||||
__ATOMIC_RELAXED);
|
||||
if ((unsigned)cls >= VK_MCLS_N || (unsigned)use >= VK_MUSE_N) return;
|
||||
if (count_to) count_to[cls][use] += (int64_t)bytes;
|
||||
else __atomic_store_n(&traffic[cls][use], __atomic_load_n(&traffic[cls][use], __ATOMIC_RELAXED) + (int64_t)bytes,
|
||||
__ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
void vk_mem_count_local(int64_t (*acc)[VK_MUSE_N]) { count_to = acc; }
|
||||
|
||||
void vk_mem_count_flush(int64_t (*acc)[VK_MUSE_N]) {
|
||||
for (int c = 0; c < VK_MCLS_N; c++)
|
||||
for (int u = 0; u < VK_MUSE_N; u++)
|
||||
if (acc[c][u]) __atomic_fetch_add(&traffic[c][u], acc[c][u], __ATOMIC_RELAXED), acc[c][u] = 0;
|
||||
}
|
||||
|
||||
static void flag_names(uint32_t f, char *buf, size_t n) {
|
||||
@@ -559,6 +575,25 @@ void vk_mem_census(uint64_t frame) {
|
||||
n_import_failures, (unsigned long long)count[POOL_PHYS], total[POOL_PHYS] / 1048576.0,
|
||||
(unsigned long long)count[POOL_VIRT], total[POOL_VIRT] / 1048576.0,
|
||||
(unsigned long long)__atomic_load_n(&n_cross_defer, __ATOMIC_RELAXED));
|
||||
// The baseline for the commit a crash report prints: a crash far above it points at growth during play.
|
||||
double proc, used, limit;
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
uint64_t room = vk_mem_host_room();
|
||||
rt_log("mem: commit at frame %llu: this process %.0f MB, the system %.0f of %.0f MB; room for device memory %s%.0f MiB",
|
||||
(unsigned long long)frame, proc, used, limit, room == UINT64_MAX ? "not counted " : "",
|
||||
room == UINT64_MAX ? 0.0 : room / 1048576.0);
|
||||
// Per heap, what VMA holds (blocks, and the allocations inside them) beside what the driver counts for the process:
|
||||
// usage above VMA's blocks is the driver's own memory (pipelines, command buffers, descriptor pools).
|
||||
VkCtx *vc = vk_ctx();
|
||||
if (vc && vc->vma) {
|
||||
VmaBudget vb[VK_MAX_MEMORY_HEAPS];
|
||||
vmaGetHeapBudgets(vc->vma, vb);
|
||||
for (uint32_t h = 0; h < vc->mem_props.memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
|
||||
rt_log("mem: heap %u (flags %#x) at frame %llu: vma %u blocks %.0f MiB, %u allocations %.0f MiB; driver usage "
|
||||
"%.0f of %.0f MiB", h, vc->mem_props.memoryHeaps[h].flags, (unsigned long long)frame,
|
||||
vb[h].statistics.blockCount, vb[h].statistics.blockBytes / 1048576.0, vb[h].statistics.allocationCount,
|
||||
vb[h].statistics.allocationBytes / 1048576.0, vb[h].usage / 1048576.0, vb[h].budget / 1048576.0);
|
||||
}
|
||||
static const char *const kinds[] = {"", "import", "physical", "virtual"};
|
||||
for (int j = 0; j < ng; j++) {
|
||||
char f[160];
|
||||
@@ -590,6 +625,79 @@ static uint64_t vram_limit(void) {
|
||||
return (uint64_t)limit;
|
||||
}
|
||||
|
||||
// Device memory that takes host memory. A UMA GPU's memory is the machine's RAM. On Windows the AMD driver charges
|
||||
// device-local allocations to the process commit one to one (RX 9070: VRAM use and private bytes grew by the same
|
||||
// 1164, 256 and 256 MiB), so on a 16 GB machine the commit limit is reached while the VRAM budget shows gigabytes free,
|
||||
// and the driver then fails inside a call (#285: std::bad_alloc in amdvlk64.dll under gpl_link). Whether a discrete
|
||||
// GPU's driver does so is measured once (vk_mem_host_probe), not assumed per vendor: an RTX 5080 log (#398) showed
|
||||
// 10.4 GB of process commit at frame 1200, about what the RX 9070 holds with its VRAM counted. Linux overcommits, so
|
||||
// there only a UMA GPU's physical memory counts (plat_mem_room).
|
||||
enum { HB_NONE, HB_COMMIT, HB_UMA, HB_PROBE };
|
||||
static int host_backed;
|
||||
static uint64_t room_reserve;
|
||||
static volatile uint64_t room_cached, room_ms;
|
||||
|
||||
uint64_t vk_mem_host_room(void) {
|
||||
if (!host_backed) return UINT64_MAX;
|
||||
uint64_t now = plat_now_ms(), at = __atomic_load_n(&room_ms, __ATOMIC_ACQUIRE);
|
||||
if (at && now - at < 250) return __atomic_load_n(&room_cached, __ATOMIC_RELAXED); // a query costs a system call
|
||||
uint64_t commit, phys;
|
||||
plat_mem_room(&commit, &phys);
|
||||
uint64_t r = host_backed == HB_UMA && phys < commit ? phys : commit;
|
||||
r = r == UINT64_MAX ? r : r > room_reserve ? r - room_reserve : 0;
|
||||
__atomic_store_n(&room_cached, r, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&room_ms, now | 1, __ATOMIC_RELEASE);
|
||||
return r;
|
||||
}
|
||||
|
||||
int vk_mem_uma(void) { return host_backed == HB_UMA; }
|
||||
|
||||
// A discrete GPU: one device-local allocation of HB_PROBE_SIZE, and the process commit before and after it. The driver
|
||||
// charges it at allocation or not at all as far as this sees; one that charged only at first use would read as not
|
||||
// counted, as before the probe. Another thread's allocation in between could only add, and half the size is the line.
|
||||
#define HB_PROBE_SIZE (128ull << 20)
|
||||
void vk_mem_host_probe(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c || host_backed != HB_PROBE) return;
|
||||
host_backed = HB_NONE;
|
||||
uint64_t commit, phys;
|
||||
plat_mem_room(&commit, &phys);
|
||||
if (commit == UINT64_MAX) return; // no commit limit (Linux)
|
||||
uint32_t type = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < c->mem_props.memoryTypeCount && type == UINT32_MAX; i++)
|
||||
if ((c->mem_props.memoryTypes[i].propertyFlags & (VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) ==
|
||||
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
|
||||
type = i;
|
||||
VkMemoryAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = HB_PROBE_SIZE,
|
||||
.memoryTypeIndex = type};
|
||||
uint64_t r0, p0, r1, p1;
|
||||
VkDeviceMemory m;
|
||||
plat_process_memory(&r0, &p0);
|
||||
if (type == UINT32_MAX || !p0 || vkAllocateMemory(c->device, &ai, NULL, &m) != VK_SUCCESS) {
|
||||
rt_log("mem: device memory against system memory: not measured (no device-local type or allocation failed): not counted");
|
||||
return;
|
||||
}
|
||||
plat_process_memory(&r1, &p1);
|
||||
vkFreeMemory(c->device, m, NULL);
|
||||
int64_t d = (int64_t)(p1 - p0);
|
||||
host_backed = d >= (int64_t)HB_PROBE_SIZE / 2 ? HB_COMMIT : HB_NONE;
|
||||
rt_log("mem: device memory against system memory: %s (a %llu MiB device-local allocation took %.0f MiB of process "
|
||||
"commit), reserve %llu MiB (TOTK_MEM_RESERVE_MIB)", host_backed == HB_COMMIT ? "commit" : "not counted",
|
||||
(unsigned long long)(HB_PROBE_SIZE >> 20), d / 1048576.0, (unsigned long long)(room_reserve >> 20));
|
||||
}
|
||||
|
||||
// The budget is at most the use plus what the system can still give, so the paths that react to VRAM pressure (the
|
||||
// mirror refusal, released textures, the headroom block) react to a system short of memory too.
|
||||
static void host_cap(uint64_t used, uint64_t *budget) {
|
||||
uint64_t room = vk_mem_host_room();
|
||||
if (room == UINT64_MAX || used + room >= *budget) return;
|
||||
*budget = used + room ? used + room : 1;
|
||||
static volatile int32_t logged;
|
||||
if (!plat_xchg32(&logged, 1))
|
||||
rt_log("mem: system memory limits the device-local budget: %.0f MiB in use, %.0f MiB more beyond a reserve of "
|
||||
"%.0f MiB (logged once)", used / 1048576.0, room / 1048576.0, room_reserve / 1048576.0);
|
||||
}
|
||||
|
||||
int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
|
||||
VkCtx *c = vk_ctx();
|
||||
*used = *budget = 0, *exact = 0;
|
||||
@@ -605,6 +713,7 @@ int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
|
||||
*used += b.heapUsage[h], *budget += b.heapBudget[h];
|
||||
*exact = 1;
|
||||
if (limit && *budget > limit) *budget = limit;
|
||||
if (*budget) host_cap(*used, budget);
|
||||
return *budget != 0;
|
||||
}
|
||||
VmaBudget vb[VK_MAX_MEMORY_HEAPS];
|
||||
@@ -613,6 +722,7 @@ int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
|
||||
if (mp->memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
|
||||
*used += vb[h].statistics.blockBytes, *budget += mp->memoryHeaps[h].size;
|
||||
if (limit && *budget > limit) *budget = limit;
|
||||
if (*budget) host_cap(*used, budget);
|
||||
return *budget != 0;
|
||||
}
|
||||
|
||||
@@ -891,6 +1001,18 @@ static int nspend, cspend;
|
||||
static int64_t s_live_n, s_live_bytes; // shadow_lock
|
||||
static uint64_t s_syncs, s_copied, s_queries, s_untracked, s_wb_bytes, s_ticks; // shadow_lock: since the last log
|
||||
|
||||
// TOTK_SHADOW_DEFER (with TOTK_SYNC_AHEAD): a shadow update the CPU must not write yet is queued instead and executed
|
||||
// by the GPU, in queue order, at the start of the prologue of the part submitted next (vk_mem_shadow_prologue).
|
||||
// defer_on: the switch is on. sdefer_now: the pool being synced is deferred, and sdefer_fence the value its copies
|
||||
// belong to. Replay thread (sub_lock): the queue.
|
||||
typedef struct { PoolRec *r; uint64_t addr, len; } DRange;
|
||||
static int defer_on;
|
||||
static int sdefer_now;
|
||||
static uint64_t sdefer_fence;
|
||||
static DRange *dq; // guest ranges to copy into their pools' shadows
|
||||
static int ndq, cdq;
|
||||
static uint64_t s_defer_pools, s_defer_bytes, s_defer_copies, s_wait_ticks, s_waits; // shadow_lock: since the last log
|
||||
|
||||
static int shadow_pool(VkCtx *c, PoolRec *r, uint64_t addr, uint64_t size) {
|
||||
r->import_lo = addr & ~0xfffull;
|
||||
r->import_hi = (addr + size + 0xfff) & ~0xfffull;
|
||||
@@ -949,13 +1071,52 @@ static void shadow_copy(PoolRec *r, uint64_t a, uint64_t b, int i) {
|
||||
if (spend[i].hi < b) shadow_copy(r, spend[i].hi, b, i + 1);
|
||||
return;
|
||||
}
|
||||
if (sdefer_now) { // the GPU copies it later: the bytes are read from guest memory when the prologue is recorded
|
||||
if (ndq == cdq) grow((void **)&dq, &cdq, sizeof *dq);
|
||||
dq[ndq++] = (DRange){r, a, b - a};
|
||||
r->sdefer = sdefer_fence;
|
||||
s_defer_bytes += b - a;
|
||||
return;
|
||||
}
|
||||
memcpy(r->smap + (a - r->import_lo), (const void *)(uintptr_t)a, b - a);
|
||||
s_copied += b - a;
|
||||
}
|
||||
|
||||
// TOTK_SHADOW_SNAP (default 1 on a native host, 0 under Wine, where it has not been measured): a pool's sync takes its written pages from the per-submission write-watch snapshots
|
||||
// (snap_regions, shared with the texture checks): each 2 MiB region is queried at most once per guest submission, however
|
||||
// many pools share it and however many parts of the submission touch them. The GPU then reads a pool as of the first
|
||||
// part that syncs it, nearer the submit call, the moment the game means (vk_usnap.c). 0 queries each pool at every
|
||||
// sync, as before. Measured on a Steam Deck at Lookout Landing: 716 pool syncs and 729 queries a frame took 4.8 ms of
|
||||
// the replay thread to copy 2.3 MiB, the query walk costing about 4 us per MiB there; with the snapshots, about 58
|
||||
// queries and 1.7 ms for the same bytes.
|
||||
static int shadow_snap = -1;
|
||||
static void shadow_copy_newer(PoolRec *r, uint32_t since);
|
||||
|
||||
// shadow_lock and gen_lock held: stamps the pool's written pages and copies those newer than its shadow.
|
||||
static void shadow_sync_pool(PoolRec *r) {
|
||||
uint64_t lo = r->import_lo, hi = r->import_hi;
|
||||
if (shadow_snap < 0) {
|
||||
shadow_snap = rt_knob("SHADOW_SNAP", plat_is_wine() ? "0" : "1");
|
||||
rt_log("mem: shadow sync: %s (TOTK_SHADOW_SNAP%s)",
|
||||
shadow_snap ? "per-submission region snapshots" : "a query per pool at every part",
|
||||
plat_is_wine() ? "; off by default under Wine" : "");
|
||||
}
|
||||
uint32_t epoch = shadow_snap ? vk_texture_epoch() : 0;
|
||||
uint64_t r0 = lo >> SNAP_SHIFT, r1 = (hi - 1) >> SNAP_SHIFT;
|
||||
if (epoch && r1 < SNAP_REGIONS) {
|
||||
s_queries += snap_regions(r0, r1, epoch); // stamps what the snapshot found, if the region was not seen yet
|
||||
int untracked = 0;
|
||||
for (uint64_t k = r0; k <= r1; k++) untracked |= snap_untracked[k];
|
||||
uint32_t since = r->sgen;
|
||||
r->sgen = gen_now;
|
||||
if (untracked) { // nothing tells which pages changed: all of them
|
||||
s_untracked++;
|
||||
shadow_copy(r, lo, hi, 0);
|
||||
return;
|
||||
}
|
||||
shadow_copy_newer(r, since);
|
||||
return;
|
||||
}
|
||||
uint32_t g = ++gen_now;
|
||||
int tracked = 1;
|
||||
for (uint64_t a = lo; a < hi;) { // one query per run of regions that are not frozen
|
||||
@@ -974,6 +1135,12 @@ static void shadow_sync_pool(PoolRec *r) {
|
||||
shadow_copy(r, lo, hi, 0);
|
||||
return;
|
||||
}
|
||||
shadow_copy_newer(r, since);
|
||||
}
|
||||
|
||||
// shadow_lock and gen_lock held: copies the pool's pages stamped after since into its shadow, in runs.
|
||||
static void shadow_copy_newer(PoolRec *r, uint32_t since) {
|
||||
uint64_t lo = r->import_lo, hi = r->import_hi;
|
||||
uint64_t run = 0;
|
||||
for (uint64_t k = lo >> REG_SHIFT; k <= (hi - 1) >> REG_SHIFT; k++) {
|
||||
uint64_t a = k << REG_SHIFT, b = a + (1ull << REG_SHIFT);
|
||||
@@ -993,13 +1160,76 @@ static void shadow_sync_pool(PoolRec *r) {
|
||||
if (run) shadow_copy(r, run, hi, 0);
|
||||
}
|
||||
|
||||
void vk_mem_shadow_sync(void) {
|
||||
if (!shadow_on) return;
|
||||
uint64_t t0 = qpc();
|
||||
// TOTK_SYNC_AHEAD (nvn_api_core.c): the game may have been told a value is done (at most the guard) that the GPU still
|
||||
// runs, and then write memory that value reads. The CPU copies its writes into a shadow, so while the guard is not done
|
||||
// a shadow with any reader in flight must not be written by the CPU: that reader may be the guarded value or an older
|
||||
// one (the parts of one submission share a value), and a pool records only its newest reader. A shadow with no reader
|
||||
// in flight is copied at once.
|
||||
// With TOTK_SHADOW_DEFER the update of such a shadow is queued instead (this function). Without it the replay thread
|
||||
// waits (shadow_wait_guard).
|
||||
|
||||
// shadow_lock held: whether a shadow this sync is about to update has a reader in flight.
|
||||
static int shadow_reader_busy(uint64_t done) {
|
||||
for (int i = 0; i < nstouch; i++)
|
||||
if (stouch[i]->suse > done) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// shadow_lock held (released while waiting), no TOTK_SHADOW_DEFER: waits until the guard is done when a shadow to
|
||||
// update has a reader in flight, then re-reads the guard under the lock, since the game may have raised it meanwhile. The wait
|
||||
// runs without shadow_lock: the completion thread needs it for the fence callbacks (vk_gpu_written). *g and *done are
|
||||
// current on return.
|
||||
static void shadow_wait_guard(uint64_t fence, uint64_t *g, uint64_t *done) {
|
||||
while (*g > *done && (!fence || *g < fence) && shadow_reader_busy(*done)) {
|
||||
uint64_t want = *g;
|
||||
plat_mutex_unlock(&shadow_lock);
|
||||
uint64_t t0 = qpc();
|
||||
vk_sync_fence_wait(want, UINT64_MAX);
|
||||
uint64_t dt = qpc() - t0;
|
||||
plat_mutex_lock(&shadow_lock);
|
||||
s_wait_ticks += dt, s_waits++;
|
||||
*g = nvn_sync_ahead_guard();
|
||||
*done = vk_sync_fence_done();
|
||||
}
|
||||
}
|
||||
|
||||
// shadow_lock held, TOTK_SHADOW_DEFER: the pool's update goes through the queue when a reader of it is in flight while
|
||||
// the guard is (see above), or when an earlier deferred copy into it has not executed yet: a direct memcpy must never
|
||||
// land before it, or the queued copy would overwrite newer bytes with older ones.
|
||||
static int shadow_defers(const PoolRec *r, uint64_t g, uint64_t done) {
|
||||
if (r->sdefer > done) return 1;
|
||||
return g > done && r->suse > done;
|
||||
}
|
||||
|
||||
// nvnSyncWait raised the guard: a sync that read the old guard holds shadow_lock until its copies are made, so taking
|
||||
// the lock once here makes the game return from the wait only after such a sync finished. Later syncs see the new
|
||||
// guard.
|
||||
void vk_mem_shadow_fence(void) {
|
||||
plat_mutex_lock(&shadow_lock);
|
||||
plat_mutex_unlock(&shadow_lock);
|
||||
}
|
||||
|
||||
int vk_mem_shadow_on(void) { return shadow_on; }
|
||||
|
||||
void vk_mem_shadow_sync(uint64_t fence) {
|
||||
if (!shadow_on) return;
|
||||
plat_mutex_lock(&shadow_lock);
|
||||
// The guard is read under the lock: see vk_mem_shadow_fence.
|
||||
uint64_t g = nvn_sync_ahead_guard(), done = g ? vk_sync_fence_done() : 0;
|
||||
if (!defer_on && nstouch) shadow_wait_guard(fence, &g, &done);
|
||||
uint64_t t0 = qpc();
|
||||
if (nstouch) {
|
||||
plat_mutex_lock(&gen_lock);
|
||||
for (int i = 0; i < nstouch; i++) shadow_sync_pool(stouch[i]), stouch[i]->stouched = 0;
|
||||
for (int i = 0; i < nstouch; i++) {
|
||||
PoolRec *r = stouch[i];
|
||||
sdefer_now = defer_on && fence && shadow_defers(r, g, done);
|
||||
sdefer_fence = fence;
|
||||
int before = ndq;
|
||||
shadow_sync_pool(r), r->stouched = 0;
|
||||
if (ndq > before) s_defer_pools++;
|
||||
sdefer_now = 0;
|
||||
if (fence > r->suse) r->suse = fence;
|
||||
}
|
||||
plat_mutex_unlock(&gen_lock);
|
||||
s_syncs += (uint64_t)nstouch;
|
||||
nstouch = 0;
|
||||
@@ -1008,6 +1238,8 @@ void vk_mem_shadow_sync(void) {
|
||||
plat_mutex_unlock(&shadow_lock);
|
||||
}
|
||||
|
||||
int vk_mem_shadow_deferred(void) { return ndq; }
|
||||
|
||||
void vk_mem_gpu_pending(uint64_t addr, uint64_t size) {
|
||||
if (!shadow_on || !size) return;
|
||||
plat_mutex_lock(&shadow_lock);
|
||||
@@ -1043,7 +1275,12 @@ static void shadow_log(uint64_t frame) {
|
||||
(unsigned long long)frame, (long long)s_live_n, s_live_bytes / 1048576.0, (unsigned long long)s_syncs,
|
||||
(unsigned long long)s_queries, s_copied / 1048576.0, (unsigned long long)s_untracked, s_wb_bytes / 1048576.0,
|
||||
qpc_ms(s_ticks), nspend);
|
||||
if (defer_on || s_waits) // only with TOTK_SYNC_AHEAD: the shadow log of everyone else stays as it was
|
||||
rt_log("mem: shadow deferred (%s): %llu pools, %.1f MiB in %llu copies; replay waited for a reader %llu times, "
|
||||
"%.2f ms", defer_on ? "on" : "off", (unsigned long long)s_defer_pools, s_defer_bytes / 1048576.0,
|
||||
(unsigned long long)s_defer_copies, (unsigned long long)s_waits, qpc_ms(s_wait_ticks));
|
||||
s_syncs = s_queries = s_copied = s_untracked = s_wb_bytes = s_ticks = 0;
|
||||
s_defer_pools = s_defer_bytes = s_defer_copies = s_wait_ticks = s_waits = 0;
|
||||
plat_mutex_unlock(&shadow_lock);
|
||||
}
|
||||
|
||||
@@ -1212,6 +1449,10 @@ static VkBufferCopy *mregions; // replay: vkCmdCopyBuffer regions
|
||||
static int cregions;
|
||||
static PlatMutex flush_lock = PLAT_MUTEX_INIT;
|
||||
static int flush_cap = FLUSH_RANGES; // ranges a pool keeps; 1 with TOTK_FLUSH_SPAN=1
|
||||
// The first uses of a submission's mirrors, the copies they queue, the GPU writes noted and their snapshots (pend, wbl, snl, mrep): threads
|
||||
// recording chunks of one submission take it. A mirror synced in this submission is read without it (vk_mem_use): its
|
||||
// acc_sub is stored last. Before pool_lock, gen_lock and flush_lock.
|
||||
static PlatMutex mirror_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
typedef struct {
|
||||
uint64_t subs, syncs, upload_bytes, upload_copies, first_n, first_bytes, wb_bytes, wb_copies, wb_ranges;
|
||||
@@ -1255,6 +1496,8 @@ static void mirror_mode(int discrete) {
|
||||
#else
|
||||
shadow_on = !sh || !*sh || strcmp(sh, "0");
|
||||
#endif
|
||||
defer_on = shadow_on && cfg_bool("TOTK_SYNC_AHEAD") && cfg_bool("TOTK_SHADOW_DEFER");
|
||||
if (defer_on) rt_log("mem: TOTK_SHADOW_DEFER: shadow updates that wait for a reader are queued for the GPU instead");
|
||||
if (shadow_on) rt_log("mem: shadow storage: CPU-visible pools are host-visible copies of guest memory, not imports%s",
|
||||
sh && *sh ? " (TOTK_SHADOW)" : "");
|
||||
rt_log("mem: mode %s (%s): %s", mode == 2 ? "verify" : mode ? "mirror" : "import", why,
|
||||
@@ -1336,16 +1579,21 @@ static void refuse(PoolRec *r, int why, const char *what) {
|
||||
static void mirror_create(PoolRec *r) {
|
||||
VkCtx *c = vk_ctx();
|
||||
uint64_t n = r->import_hi - r->import_lo;
|
||||
r->mstate = MS_REFUSED;
|
||||
__atomic_add_fetch(&mirror_gen, 1, __ATOMIC_RELEASE);
|
||||
if (r->kind != POOL_IMPORT || !r->buf || !n) return;
|
||||
if (r->kind != POOL_IMPORT || !r->buf || !n) {
|
||||
__atomic_store_n(&r->mstate, MS_REFUSED, __ATOMIC_RELEASE);
|
||||
return;
|
||||
}
|
||||
plat_mutex_lock_shared(&pool_lock);
|
||||
int i = pidx_upper(&pool_index, r->lo) - 1;
|
||||
while (i >= 0 && pool_index.e[i].buf != r) i--;
|
||||
int alone = i >= 0 && (i == 0 || pool_index.maxhi[i - 1] <= pool_index.e[i].lo) &&
|
||||
(i + 1 == pool_index.n || pool_index.e[i + 1].lo >= pool_index.e[i].hi);
|
||||
plat_mutex_unlock_shared(&pool_lock);
|
||||
if (i < 0) return; // finalized meanwhile
|
||||
if (i < 0) { // finalized meanwhile
|
||||
__atomic_store_n(&r->mstate, MS_REFUSED, __ATOMIC_RELEASE);
|
||||
return;
|
||||
}
|
||||
if (!alone) return refuse(r, MR_OVERLAP, "it overlaps another pool");
|
||||
uint64_t r0 = r->import_lo >> SNAP_SHIFT, r1 = (r->import_hi - 1) >> SNAP_SHIFT;
|
||||
if (r1 >= SNAP_REGIONS) return refuse(r, MR_UNTRACKED, "outside the write-watched address space");
|
||||
@@ -1383,9 +1631,9 @@ static void mirror_create(PoolRec *r) {
|
||||
mlist[at] = r;
|
||||
nm++;
|
||||
plat_mutex_unlock(&pool_lock);
|
||||
r->acc_sub = msub;
|
||||
r->mstate = MS_ON;
|
||||
range_add(&pend, &npend, &cpend, r, 0, n);
|
||||
__atomic_store_n(&r->acc_sub, msub, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&r->mstate, MS_ON, __ATOMIC_RELEASE);
|
||||
mrep.first_n++, mrep.first_bytes += n;
|
||||
plat_inc64(&m_live_n);
|
||||
plat_add64(&m_live_bytes, (int64_t)n);
|
||||
@@ -1426,7 +1674,6 @@ static void pool_retire(void *arg) {
|
||||
// Replay thread, the first access of this submission: queues the copies of the pages written since the last sync.
|
||||
static void mirror_sync(PoolRec *r) {
|
||||
uint64_t t0 = qpc();
|
||||
r->acc_sub = msub;
|
||||
uint64_t lo = r->import_lo, hi = r->import_hi;
|
||||
uint64_t r0 = lo >> SNAP_SHIFT, r1 = (hi - 1) >> SNAP_SHIFT;
|
||||
int excl = 0;
|
||||
@@ -1475,21 +1722,40 @@ static void mirror_sync(PoolRec *r) {
|
||||
}
|
||||
mrep.syncs++;
|
||||
mrep.sync_t += qpc() - t0;
|
||||
__atomic_store_n(&r->acc_sub, msub, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// 1 when r is settled for this submission without mirror_lock: refused, or mirrored and synced (*on).
|
||||
static int mirror_settled(PoolRec *r, int *on) {
|
||||
int st = __atomic_load_n(&r->mstate, __ATOMIC_ACQUIRE);
|
||||
*on = st == MS_ON;
|
||||
return st == MS_REFUSED || (st == MS_ON && __atomic_load_n(&r->acc_sub, __ATOMIC_ACQUIRE) == msub);
|
||||
}
|
||||
|
||||
void vk_mem_use(VkMemSpan *sp) {
|
||||
PoolRec *r = sp->pool;
|
||||
if (r->mstate == MS_NONE) mirror_create(r);
|
||||
if (r->mstate != MS_ON) return;
|
||||
if (r->acc_sub != msub) mirror_sync(r);
|
||||
sp->buffer = r->mbuf;
|
||||
int on;
|
||||
if (!mirror_settled(r, &on)) {
|
||||
plat_mutex_lock(&mirror_lock);
|
||||
if (r->mstate == MS_NONE) mirror_create(r);
|
||||
on = r->mstate == MS_ON;
|
||||
if (on && r->acc_sub != msub) mirror_sync(r);
|
||||
plat_mutex_unlock(&mirror_lock);
|
||||
}
|
||||
if (on) sp->buffer = r->mbuf;
|
||||
}
|
||||
|
||||
void vk_mem_pick(VkMemSpan *sp) {
|
||||
PoolRec *r = sp->pool;
|
||||
if (!r || r->mstate != MS_ON) return;
|
||||
if (r->acc_sub != msub) mirror_sync(r);
|
||||
sp->buffer = r->mbuf;
|
||||
int on;
|
||||
if (!r) return;
|
||||
if (!mirror_settled(r, &on)) {
|
||||
plat_mutex_lock(&mirror_lock);
|
||||
on = r->mstate == MS_ON;
|
||||
if (on && r->acc_sub != msub) mirror_sync(r);
|
||||
plat_mutex_unlock(&mirror_lock);
|
||||
}
|
||||
if (on) sp->buffer = r->mbuf;
|
||||
}
|
||||
|
||||
int vk_mem_mirror_probe(uint64_t addr) {
|
||||
@@ -1753,10 +2019,14 @@ static void gpu_wrote(const VkMemSpan *sp, uint64_t size, int shader) {
|
||||
PoolRec *r = sp->pool;
|
||||
if (r && size) gpu_writes(r, sp->offset, size);
|
||||
if (!r || r->mstate != MS_ON || sp->buffer != r->mbuf || !size) return;
|
||||
plat_mutex_lock(&mirror_lock);
|
||||
// A binding to the end of its pool (no size) can span hundreds of MiB: those stay copies.
|
||||
if (shader && !(sp->offset & 3) && !(size & 3) && size <= MERGE_MAX && merging() && snap_add(r, sp->offset, size))
|
||||
if (shader && !(sp->offset & 3) && !(size & 3) && size <= MERGE_MAX && merging() && snap_add(r, sp->offset, size)) {
|
||||
plat_mutex_unlock(&mirror_lock);
|
||||
return;
|
||||
}
|
||||
range_add(&wbl, &nwbl, &cwbl, r, sp->offset, size);
|
||||
plat_mutex_unlock(&mirror_lock);
|
||||
}
|
||||
void vk_mem_gpu_wrote(const VkMemSpan *sp, uint64_t size) { gpu_wrote(sp, size, 0); }
|
||||
void vk_mem_gpu_wrote_shader(const VkMemSpan *sp, uint64_t size) { gpu_wrote(sp, size, 1); }
|
||||
@@ -1888,10 +2158,19 @@ void vk_mem_mirror_begin(void) {
|
||||
mrep.presnap_t += qpc() - t0;
|
||||
}
|
||||
|
||||
int vk_mem_mirror_pending(void) { return npend || snl_copied < nsnl; }
|
||||
int vk_mem_mirror_pending(void) {
|
||||
plat_mutex_lock(&mirror_lock);
|
||||
int n = npend || snl_copied < nsnl;
|
||||
plat_mutex_unlock(&mirror_lock);
|
||||
return n;
|
||||
}
|
||||
|
||||
void vk_mem_mirror_prologue(VkCommandBuffer cb) {
|
||||
if (!npend && snl_copied == nsnl) return;
|
||||
plat_mutex_lock(&mirror_lock);
|
||||
if (!npend && snl_copied == nsnl) {
|
||||
plat_mutex_unlock(&mirror_lock);
|
||||
return;
|
||||
}
|
||||
mbarrier(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
copy_ranges(cb, pend, npend, 1, &mrep.upload_bytes, &mrep.upload_copies);
|
||||
@@ -1904,6 +2183,7 @@ void vk_mem_mirror_prologue(VkCommandBuffer cb) {
|
||||
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
|
||||
npend = 0;
|
||||
mrep.prologues++;
|
||||
plat_mutex_unlock(&mirror_lock);
|
||||
}
|
||||
|
||||
// ---- verify mode
|
||||
@@ -2135,6 +2415,9 @@ void vk_mem_class_log(const VkPhysicalDeviceProperties *p, const VkPhysicalDevic
|
||||
else if (host_heap < 0) cls = "uma", why = "every heap is device-local";
|
||||
else cls = "discrete", why = "device-local heap separate from host memory";
|
||||
int discrete = cls[0] == 'd';
|
||||
host_backed = !discrete ? HB_UMA : HB_PROBE; // a discrete GPU is measured once the device exists
|
||||
const char *rs = cfg_get("TOTK_MEM_RESERVE_MIB");
|
||||
room_reserve = (rs && *rs ? strtoull(rs, NULL, 10) : 1536) << 20; // Windows, other programs, our own heap
|
||||
char line[2048];
|
||||
size_t k = (size_t)snprintf(line, sizeof line, "mem: class %s (%s; device type %d %s, %s)", cls, why, t, tn, p->deviceName);
|
||||
for (uint32_t h = 0; h < m->memoryHeapCount; h++)
|
||||
@@ -2154,6 +2437,9 @@ void vk_mem_class_log(const VkPhysicalDeviceProperties *p, const VkPhysicalDevic
|
||||
f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT ? " LAZY" : "");
|
||||
}
|
||||
rt_log("%s", line);
|
||||
if (host_backed == HB_UMA)
|
||||
rt_log("mem: device memory against system memory: commit and physical memory (uma), reserve %llu MiB "
|
||||
"(TOTK_MEM_RESERVE_MIB)", (unsigned long long)(room_reserve >> 20));
|
||||
mirror_mode(discrete);
|
||||
}
|
||||
|
||||
@@ -2291,6 +2577,20 @@ void vk_upload_end(void) { plat_rmutex_unlock(&cmd_lock); }
|
||||
static VmaPool stg_pool;
|
||||
static int stg_pool_tried;
|
||||
|
||||
typedef struct { const VkBufferCreateInfo *bi; const VmaAllocationCreateInfo *aci; VkBuffer b; VmaAllocation a;
|
||||
VmaAllocationInfo info; VkResult r; } StgTry;
|
||||
static int staging_try(void *arg) {
|
||||
StgTry *t = arg;
|
||||
vk_mem_collect(); // upload batches and deferred objects the GPU finished meanwhile
|
||||
t->r = rt_oom_forced(RT_OOM_STAGING) ? VK_ERROR_OUT_OF_DEVICE_MEMORY
|
||||
: vmaCreateBuffer(vk_ctx()->vma, t->bi, t->aci, &t->b, &t->a, &t->info);
|
||||
return t->r == VK_SUCCESS;
|
||||
}
|
||||
|
||||
// A failed staging buffer waits for memory (rt_oom_wait) before it ends the game: the headroom blocks go and the GPU's
|
||||
// finished batches are freed at once, released textures at the next submit (vk_oom_release). The caller may hold the
|
||||
// texture lock and cmd_lock (vk_upload_begin), both of which vk_oom_release may take after them, and no VMA lock. The open
|
||||
// batch is not submitted: its caller records into it after this returns.
|
||||
void *vk_staging(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc) {
|
||||
VkCtx *c = vk_ctx();
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = size ? size : 16,
|
||||
@@ -2314,11 +2614,22 @@ void *vk_staging(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc) {
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
}
|
||||
if (stg_pool && bi.size <= STG_BLOCK / 2) aci.pool = stg_pool;
|
||||
VmaAllocationInfo info;
|
||||
VkBuffer b;
|
||||
VmaAllocation a;
|
||||
VkResult r = vmaCreateBuffer(c->vma, &bi, &aci, &b, &a, &info);
|
||||
if (r != VK_SUCCESS) rt_fatal("vk: staging buffer of %llu bytes failed (%d)", (unsigned long long)size, (int)r);
|
||||
StgTry st = {.bi = &bi, .aci = &aci};
|
||||
st.r = rt_oom_forced(RT_OOM_STAGING) ? VK_ERROR_OUT_OF_DEVICE_MEMORY
|
||||
: vmaCreateBuffer(c->vma, &bi, &aci, &st.b, &st.a, &st.info);
|
||||
if (st.r != VK_SUCCESS) {
|
||||
VkResult first = st.r;
|
||||
if (first == VK_ERROR_OUT_OF_HOST_MEMORY || first == VK_ERROR_OUT_OF_DEVICE_MEMORY) {
|
||||
vk_oom_release();
|
||||
rt_oom_wait("vk: staging buffer", bi.size, staging_try, &st);
|
||||
}
|
||||
if (st.r != VK_SUCCESS)
|
||||
rt_fatal("vk: staging buffer of %llu bytes failed (%d, first %d)", (unsigned long long)size, (int)st.r,
|
||||
(int)first);
|
||||
}
|
||||
VkBuffer b = st.b;
|
||||
VmaAllocation a = st.a;
|
||||
VmaAllocationInfo info = st.info;
|
||||
*buf = b;
|
||||
if (alloc) *alloc = a; // the caller destroys it (vmaDestroyBuffer) after vk_upload_flush(1)
|
||||
else {
|
||||
@@ -2329,6 +2640,61 @@ void *vk_staging(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc) {
|
||||
return info.pMappedData;
|
||||
}
|
||||
|
||||
typedef struct { VkBuffer b; VmaAllocation a; } StgFree;
|
||||
static void free_shadow_stg(void *arg) {
|
||||
StgFree *f = arg;
|
||||
vmaDestroyBuffer(vk_ctx()->vma, f->b, f->a);
|
||||
free(f);
|
||||
}
|
||||
|
||||
// Records the deferred shadow copies (vk_mem_shadow_sync queued the ranges): the guest bytes go into staging buffers
|
||||
// now, the GPU copies them into the pools' shadows. The barrier before waits for every earlier command, writes and
|
||||
// reads alike (a read of the shadow must be over before the copy overwrites it); the one after makes the copies visible
|
||||
// to the rest of the queue, including the mirror copies that follow in this command buffer. The staging buffers are
|
||||
// freed when the value completes. Replay thread, sub_lock held.
|
||||
void vk_mem_shadow_prologue(VkCommandBuffer cb, uint64_t fence) {
|
||||
if (!ndq) return;
|
||||
mbarrier(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT,
|
||||
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
const uint64_t lim = STG_BLOCK / 2;
|
||||
uint64_t copies = 0;
|
||||
int i = 0;
|
||||
uint64_t at_i = 0; // bytes of dq[i] already recorded
|
||||
while (i < ndq) {
|
||||
uint64_t total = 0, jo = at_i; // how much the next staging buffer holds
|
||||
for (int j = i; j < ndq && total < lim;) {
|
||||
uint64_t left = dq[j].len - jo, take = left < lim - total ? left : lim - total;
|
||||
total += take;
|
||||
if (take == left) j++, jo = 0;
|
||||
else jo += take;
|
||||
}
|
||||
VkBuffer sb;
|
||||
VmaAllocation sa;
|
||||
uint8_t *p = vk_staging(total, &sb, &sa);
|
||||
for (uint64_t at = 0; at < total;) {
|
||||
const DRange *d = &dq[i];
|
||||
uint64_t left = d->len - at_i, take = left < total - at ? left : total - at;
|
||||
memcpy(p + at, (const void *)(uintptr_t)(d->addr + at_i), take);
|
||||
VkBufferCopy bc = {at, d->addr + at_i - d->r->import_lo, take};
|
||||
vkCmdCopyBuffer(cb, sb, d->r->buf, 1, &bc);
|
||||
copies++;
|
||||
at += take, at_i += take;
|
||||
if (at_i == d->len) i++, at_i = 0;
|
||||
}
|
||||
vmaFlushAllocation(vk_ctx()->vma, sa, 0, VK_WHOLE_SIZE);
|
||||
StgFree *f = malloc(sizeof *f);
|
||||
if (!f) rt_fatal("vk: out of memory");
|
||||
f->b = sb, f->a = sa;
|
||||
vk_completion_add(fence, free_shadow_stg, f);
|
||||
}
|
||||
mbarrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
|
||||
ndq = 0;
|
||||
plat_mutex_lock(&shadow_lock);
|
||||
s_defer_copies += copies;
|
||||
plat_mutex_unlock(&shadow_lock);
|
||||
}
|
||||
|
||||
// Device-local scratch of the upload batch (the GPU deswizzle's linear copies, the ASTC decoder's blocks and texels):
|
||||
// a pool of its own, like staging, so the replay thread neither waits on the default pool's lock while another thread
|
||||
// makes a block there (the headroom thread: about 8 ms for 256 MiB) nor makes one itself for a few MiB. One block
|
||||
@@ -2373,6 +2739,66 @@ VkResult vk_scratch_buffer(VkDeviceSize size, VkBuffer *buf, VmaAllocation *allo
|
||||
|
||||
VkDeviceSize vk_upload_held(void) { return open_active ? open_b.held : 0; }
|
||||
|
||||
// Own memory of the game's textures (vk_texture.c create_tex): a pool of its own with blocks of TOTK_TEX_BLOCK_MIB
|
||||
// (default 64; 0 puts textures back in the default pools). In the default pools textures shared 256 MiB blocks with the
|
||||
// mirrors and other buffers, and a block went back to the driver only once all of them were gone: on the RX 9070,
|
||||
// after 4.5 minutes of roaming the field, 3 of the 9 device-local blocks were 13-27% full, held by 87-223 small
|
||||
// textures and buffers each, and the full ones had hundreds of free gaps under 1 MiB, so blocks held 800 MiB more than
|
||||
// their allocations. Where device memory takes process commit (vk_mem_host_probe) that commit backs nothing. A smaller
|
||||
// block, holding textures only, empties sooner and wastes less when it does not. With the pool the same 10-minute route
|
||||
// ended with 807 MiB of blocks beyond their allocations instead of 1037, the texture pool's 22 blocks mostly 75-100%
|
||||
// full, and the slow frames did not grow.
|
||||
// An image of a quarter block or more gets memory of its own (a dedicated allocation) that goes back to the driver
|
||||
// with the image; so do images the driver prefers that for, and images the pool's memory type does not take. VMA
|
||||
// checks neither for a custom pool (VmaAllocator_T::AllocateMemory takes the pool's type as it is), so the image's
|
||||
// requirements are read here first.
|
||||
static VmaPool tex_pool;
|
||||
static uint32_t tex_type;
|
||||
static VkDeviceSize tex_block;
|
||||
static int tex_pool_tried;
|
||||
|
||||
static void tex_pool_init(VkCtx *c, const VkImageCreateInfo *ci) {
|
||||
plat_once(&cmd_once, cmd_init, NULL);
|
||||
plat_rmutex_lock(&cmd_lock);
|
||||
if (!tex_pool_tried) {
|
||||
tex_block = (VkDeviceSize)cfg_int("TOTK_TEX_BLOCK_MIB") << 20; // schema default 64
|
||||
VmaAllocationCreateInfo aci = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
VmaPoolCreateInfo pi = {.blockSize = tex_block};
|
||||
if (tex_block && vmaFindMemoryTypeIndexForImageInfo(c->vma, ci, &aci, &tex_type) == VK_SUCCESS) {
|
||||
pi.memoryTypeIndex = tex_type;
|
||||
if (vmaCreatePool(c->vma, &pi, &tex_pool) != VK_SUCCESS) tex_pool = NULL;
|
||||
}
|
||||
if (tex_pool) {
|
||||
vk_headroom_pool(tex_pool, tex_type, tex_block);
|
||||
rt_log("vk: texture pool: blocks of %llu MiB in memory type %u; images of %llu MiB or more get memory of "
|
||||
"their own (TOTK_TEX_BLOCK_MIB)", (unsigned long long)(tex_block >> 20), tex_type,
|
||||
(unsigned long long)(tex_block >> 22));
|
||||
} else {
|
||||
rt_log("vk: texture pool %s: textures go to the default pools (TOTK_TEX_BLOCK_MIB)",
|
||||
tex_block ? "unavailable" : "off");
|
||||
}
|
||||
__atomic_store_n(&tex_pool_tried, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
}
|
||||
|
||||
VkResult vk_texture_image(const VkImageCreateInfo *ci, VkImage *img, VmaAllocation *alloc) {
|
||||
VkCtx *c = vk_ctx();
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
if (!__atomic_load_n(&tex_pool_tried, __ATOMIC_ACQUIRE)) tex_pool_init(c, ci);
|
||||
if (tex_pool) {
|
||||
VkDeviceImageMemoryRequirements q = {.sType = VK_STRUCTURE_TYPE_DEVICE_IMAGE_MEMORY_REQUIREMENTS, .pCreateInfo = ci};
|
||||
VkMemoryDedicatedRequirements dr = {.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS};
|
||||
VkMemoryRequirements2 mr = {.sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2, .pNext = &dr};
|
||||
vkGetDeviceImageMemoryRequirements(c->device, &q, &mr);
|
||||
if (dr.requiresDedicatedAllocation || dr.prefersDedicatedAllocation || mr.memoryRequirements.size >= tex_block / 4)
|
||||
ai.flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT;
|
||||
else if (mr.memoryRequirements.memoryTypeBits >> tex_type & 1)
|
||||
ai = (VmaAllocationCreateInfo){.usage = VMA_MEMORY_USAGE_UNKNOWN, .pool = tex_pool}; // the pool's type
|
||||
}
|
||||
return vmaCreateImage(c->vma, ci, &ai, img, alloc, NULL);
|
||||
}
|
||||
|
||||
// At device creation: the staging and scratch pools with their kept blocks, made through a first small buffer each
|
||||
// (freed at once), so the first world frame does not allocate them (staging was first used there once the GPU took
|
||||
// the early texture uploads).
|
||||
@@ -2497,6 +2923,15 @@ void vk_mem_collect(void) {
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
}
|
||||
|
||||
// Lock order: the texture lock, then cmd_lock (create_tex records under the first), then the headroom lock, which is
|
||||
// held only around VMA calls. A caller holding cmd_lock takes it again (recursive) and keeps the fence thread's own
|
||||
// collect (vk_sync.c) out meanwhile, so this collects for it.
|
||||
void vk_oom_release(void) {
|
||||
rt_oom_request();
|
||||
vk_headroom_release();
|
||||
vk_mem_collect();
|
||||
}
|
||||
|
||||
void vk_mem_stats(VkMemStats *s) {
|
||||
memset(s, 0, sizeof *s);
|
||||
plat_mutex_lock_shared(&pool_lock);
|
||||
@@ -2539,11 +2974,15 @@ void vk_mem_shutdown(void) {
|
||||
if (scr_pool) vmaDestroyPool(c->vma, scr_pool); // deferred scratch buffers went with collect_locked above
|
||||
scr_pool = NULL;
|
||||
scr_pool_tried = 0;
|
||||
vk_headroom_pool(NULL, 0, 0); // frees the headroom's anchor in the texture pool
|
||||
if (tex_pool) vmaDestroyPool(c->vma, tex_pool); // textures went with vk_texture_shutdown, deferred ones above
|
||||
tex_pool = NULL;
|
||||
tex_pool_tried = 0;
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
plat_mutex_lock(&pool_lock);
|
||||
for (int i = 0; i < pool_index.n; i++) destroy_rec(c, pool_index.e[i].buf);
|
||||
pool_index.n = 0;
|
||||
nm = npend = nwbl = nsnl = snl_copied = 0; // mirrors went with their pools
|
||||
nm = npend = nwbl = nsnl = snl_copied = ndq = 0; // mirrors went with their pools
|
||||
plat_mutex_lock(&snap_pool_lock);
|
||||
for (int i = 0; i < nsnap_pool; i++) vmaDestroyBuffer(c->vma, snap_pool[i].buf, snap_pool[i].alloc);
|
||||
nsnap_pool = 0, snap_pool_bytes = 0;
|
||||
|
||||
+30
-28
@@ -1,7 +1,7 @@
|
||||
// Generated by tools/make_mvobj_fs.py: the per-object motion fragment shader (vk_mvobj.c). Do not edit.
|
||||
#define MVOBJ_FS_LOCATION_WORD 24 // the input Location literal, patched to the variant's
|
||||
static const uint32_t mvobj_fs_spv[] = {
|
||||
0x07230203, 0x00010000, 0x00000000, 0x00000034, 0x00000000, 0x00020011, 0x00000001, 0x0003000e,
|
||||
0x07230203, 0x00010000, 0x00000000, 0x00000036, 0x00000000, 0x00020011, 0x00000001, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x0008000f, 0x00000004, 0x00000001, 0x6e69616d, 0x00000000, 0x0000000e,
|
||||
0x0000000f, 0x00000010, 0x00030010, 0x00000001, 0x00000007, 0x00040047, 0x0000000e, 0x0000001e,
|
||||
0x00000000, 0x00040047, 0x0000000f, 0x0000000b, 0x0000000f, 0x00040047, 0x00000010, 0x0000001e,
|
||||
@@ -9,31 +9,33 @@ static const uint32_t mvobj_fs_spv[] = {
|
||||
0x00000000, 0x00040047, 0x00000011, 0x00000022, 0x00000000, 0x00040047, 0x00000011, 0x00000021,
|
||||
0x00000001, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000004,
|
||||
0x00000020, 0x00040017, 0x00000005, 0x00000004, 0x00000002, 0x00040017, 0x00000006, 0x00000004,
|
||||
0x00000004, 0x00040015, 0x00000007, 0x00000020, 0x00000000, 0x00020014, 0x00000008, 0x00040020,
|
||||
0x00000009, 0x00000001, 0x00000006, 0x00040020, 0x0000000a, 0x00000003, 0x00000006, 0x0003001e,
|
||||
0x0000000b, 0x00000006, 0x00040020, 0x0000000c, 0x00000002, 0x0000000b, 0x00040020, 0x00000017,
|
||||
0x00000002, 0x00000006, 0x0004002b, 0x00000004, 0x00000012, 0x3f000000, 0x0004002b, 0x00000004,
|
||||
0x00000013, 0x00000000, 0x0004002b, 0x00000004, 0x00000014, 0x358637bd, 0x0004002b, 0x00000007,
|
||||
0x00000015, 0x00000000, 0x0007002c, 0x00000006, 0x00000016, 0x00000013, 0x00000013, 0x00000013,
|
||||
0x00000013, 0x0004003b, 0x00000009, 0x0000000e, 0x00000001, 0x0004003b, 0x00000009, 0x0000000f,
|
||||
0x00000001, 0x0004003b, 0x0000000a, 0x00000010, 0x00000003, 0x0004003b, 0x0000000c, 0x00000011,
|
||||
0x00000002, 0x00050036, 0x00000002, 0x00000001, 0x00000000, 0x00000003, 0x000200f8, 0x00000018,
|
||||
0x00050041, 0x00000017, 0x00000019, 0x00000011, 0x00000015, 0x0004003d, 0x00000006, 0x0000001a,
|
||||
0x00000019, 0x0004003d, 0x00000006, 0x0000001b, 0x0000000e, 0x0004003d, 0x00000006, 0x0000001c,
|
||||
0x0000000f, 0x0007004f, 0x00000005, 0x0000001d, 0x0000001c, 0x0000001c, 0x00000000, 0x00000001,
|
||||
0x0007004f, 0x00000005, 0x0000001e, 0x0000001a, 0x0000001a, 0x00000002, 0x00000003, 0x00050083,
|
||||
0x00000005, 0x0000001f, 0x0000001d, 0x0000001e, 0x00050051, 0x00000004, 0x00000020, 0x0000001b,
|
||||
0x00000000, 0x00050051, 0x00000004, 0x00000021, 0x0000001b, 0x00000001, 0x00050051, 0x00000004,
|
||||
0x00000022, 0x0000001b, 0x00000003, 0x00050088, 0x00000004, 0x00000023, 0x00000020, 0x00000022,
|
||||
0x00050088, 0x00000004, 0x00000024, 0x00000021, 0x00000022, 0x00050051, 0x00000004, 0x00000025,
|
||||
0x0000001a, 0x00000000, 0x00050051, 0x00000004, 0x00000026, 0x0000001a, 0x00000001, 0x00050085,
|
||||
0x00000004, 0x00000027, 0x00000023, 0x00000012, 0x00050081, 0x00000004, 0x00000028, 0x00000027,
|
||||
0x00000012, 0x00050085, 0x00000004, 0x00000029, 0x00000028, 0x00000025, 0x00050085, 0x00000004,
|
||||
0x0000002a, 0x00000024, 0x00000012, 0x00050083, 0x00000004, 0x0000002b, 0x00000012, 0x0000002a,
|
||||
0x00050085, 0x00000004, 0x0000002c, 0x0000002b, 0x00000026, 0x00050051, 0x00000004, 0x0000002d,
|
||||
0x0000001f, 0x00000000, 0x00050051, 0x00000004, 0x0000002e, 0x0000001f, 0x00000001, 0x00050083,
|
||||
0x00000004, 0x0000002f, 0x00000029, 0x0000002d, 0x00050083, 0x00000004, 0x00000030, 0x0000002c,
|
||||
0x0000002e, 0x00070050, 0x00000006, 0x00000031, 0x0000002f, 0x00000030, 0x00000013, 0x00000013,
|
||||
0x000500ba, 0x00000008, 0x00000032, 0x00000022, 0x00000014, 0x000600a9, 0x00000006, 0x00000033,
|
||||
0x00000032, 0x00000031, 0x00000016, 0x0003003e, 0x00000010, 0x00000033, 0x000100fd, 0x00010038
|
||||
0x00000004, 0x00040015, 0x00000007, 0x00000020, 0x00000000, 0x00020014, 0x00000008, 0x00040017,
|
||||
0x00000018, 0x00000008, 0x00000004, 0x00040020, 0x00000009, 0x00000001, 0x00000006, 0x00040020,
|
||||
0x0000000a, 0x00000003, 0x00000006, 0x0003001e, 0x0000000b, 0x00000006, 0x00040020, 0x0000000c,
|
||||
0x00000002, 0x0000000b, 0x00040020, 0x00000017, 0x00000002, 0x00000006, 0x0004002b, 0x00000004,
|
||||
0x00000012, 0x3f000000, 0x0004002b, 0x00000004, 0x00000013, 0x00000000, 0x0004002b, 0x00000004,
|
||||
0x00000014, 0x358637bd, 0x0004002b, 0x00000007, 0x00000015, 0x00000000, 0x0007002c, 0x00000006,
|
||||
0x00000016, 0x00000013, 0x00000013, 0x00000013, 0x00000013, 0x0004003b, 0x00000009, 0x0000000e,
|
||||
0x00000001, 0x0004003b, 0x00000009, 0x0000000f, 0x00000001, 0x0004003b, 0x0000000a, 0x00000010,
|
||||
0x00000003, 0x0004003b, 0x0000000c, 0x00000011, 0x00000002, 0x00050036, 0x00000002, 0x00000001,
|
||||
0x00000000, 0x00000003, 0x000200f8, 0x00000019, 0x00050041, 0x00000017, 0x0000001a, 0x00000011,
|
||||
0x00000015, 0x0004003d, 0x00000006, 0x0000001b, 0x0000001a, 0x0004003d, 0x00000006, 0x0000001c,
|
||||
0x0000000e, 0x0004003d, 0x00000006, 0x0000001d, 0x0000000f, 0x0007004f, 0x00000005, 0x0000001e,
|
||||
0x0000001d, 0x0000001d, 0x00000000, 0x00000001, 0x0007004f, 0x00000005, 0x0000001f, 0x0000001b,
|
||||
0x0000001b, 0x00000002, 0x00000003, 0x00050083, 0x00000005, 0x00000020, 0x0000001e, 0x0000001f,
|
||||
0x00050051, 0x00000004, 0x00000021, 0x0000001c, 0x00000000, 0x00050051, 0x00000004, 0x00000022,
|
||||
0x0000001c, 0x00000001, 0x00050051, 0x00000004, 0x00000023, 0x0000001c, 0x00000003, 0x00050088,
|
||||
0x00000004, 0x00000024, 0x00000021, 0x00000023, 0x00050088, 0x00000004, 0x00000025, 0x00000022,
|
||||
0x00000023, 0x00050051, 0x00000004, 0x00000026, 0x0000001b, 0x00000000, 0x00050051, 0x00000004,
|
||||
0x00000027, 0x0000001b, 0x00000001, 0x00050085, 0x00000004, 0x00000028, 0x00000024, 0x00000012,
|
||||
0x00050081, 0x00000004, 0x00000029, 0x00000028, 0x00000012, 0x00050085, 0x00000004, 0x0000002a,
|
||||
0x00000029, 0x00000026, 0x00050085, 0x00000004, 0x0000002b, 0x00000025, 0x00000012, 0x00050083,
|
||||
0x00000004, 0x0000002c, 0x00000012, 0x0000002b, 0x00050085, 0x00000004, 0x0000002d, 0x0000002c,
|
||||
0x00000027, 0x00050051, 0x00000004, 0x0000002e, 0x00000020, 0x00000000, 0x00050051, 0x00000004,
|
||||
0x0000002f, 0x00000020, 0x00000001, 0x00050083, 0x00000004, 0x00000030, 0x0000002a, 0x0000002e,
|
||||
0x00050083, 0x00000004, 0x00000031, 0x0000002d, 0x0000002f, 0x00070050, 0x00000006, 0x00000032,
|
||||
0x00000030, 0x00000031, 0x00000013, 0x00000013, 0x000500ba, 0x00000008, 0x00000033, 0x00000023,
|
||||
0x00000014, 0x00070050, 0x00000018, 0x00000034, 0x00000033, 0x00000033, 0x00000033, 0x00000033,
|
||||
0x000600a9, 0x00000006, 0x00000035, 0x00000034, 0x00000032, 0x00000016, 0x0003003e, 0x00000010,
|
||||
0x00000035, 0x000100fd, 0x00010038
|
||||
};
|
||||
@@ -157,10 +157,10 @@ void vk_mvsurvey_draw(VkExec *ex) {
|
||||
k->size = (uint32_t)u->size;
|
||||
uint32_t want = u->size < SNAP ? (uint32_t)u->size : SNAP;
|
||||
VkDescriptorBufferInfo bi;
|
||||
const uint8_t *src = vk_usnap_find(u->addr, want, 1, &bi) ? vk_usnap_map() + bi.offset : NULL;
|
||||
const uint8_t *src = vk_usnap_find(u->addr, want, UINT32_MAX, 1, &bi) ? vk_usnap_map() + bi.offset : NULL;
|
||||
k->have = src ? want : 0;
|
||||
if (src) memcpy(k->bytes, src, want);
|
||||
const uint8_t *all = vk_usnap_find(u->addr, u->size, 1, &bi) ? vk_usnap_map() + bi.offset : NULL;
|
||||
const uint8_t *all = vk_usnap_find(u->addr, u->size, UINT32_MAX, 1, &bi) ? vk_usnap_map() + bi.offset : NULL;
|
||||
k->mats = all ? lead_mats((const float *)all, (uint32_t)(u->size / 4)) : 0;
|
||||
if (all) {
|
||||
Stat *s = &S.st[b];
|
||||
|
||||
+22
-10
@@ -266,13 +266,19 @@ static int ensure_init(void) {
|
||||
}
|
||||
// TOTK_VK_DYN_VERTEX=0/1 forces dynamic vertex input off or on. Default off on Intel's frozen Windows driver for its
|
||||
// older integrated GPUs (UHD 630, Ice Lake: Vulkan 1.3.215 and older), which draws nothing with it; LAS Recomp
|
||||
// turns it off there for the same reason.
|
||||
// turns it off there for the same reason. Also off on Intel's current Windows driver for its integrated GPUs
|
||||
// (vk_caps.intel_igpu_win): on UHD Graphics (i3-1215U, 101.6129; i3-1305U, 101.7088) and Iris Xe (101.7092) it
|
||||
// drew black rectangles over walls, trees and floating islands, and vk_dyn_vertex = 0 removed them (issues #970,
|
||||
// #992, #81). Mesa's ANV on the same GPUs keeps it: a report there (#989) had both switches off and its glitch stayed.
|
||||
const char *dv = cfg_get("TOTK_VK_DYN_VERTEX"); // no fixed default: it depends on the GPU, see above
|
||||
int intel_legacy = c->props.vendorID == 0x8086 && c->props.apiVersion <= VK_MAKE_API_VERSION(0, 1, 3, 215);
|
||||
int dv_on = dv && *dv ? strcmp(dv, "0") != 0 : !intel_legacy;
|
||||
int dv_on = dv && *dv ? strcmp(dv, "0") != 0 : !intel_legacy && !vk_caps.intel_igpu_win;
|
||||
P.dyn_vi = dv_on && c->has_vertex_input_dynamic_state && PROC(set_vi, "vkCmdSetVertexInputEXT");
|
||||
if (!dv_on && c->has_vertex_input_dynamic_state)
|
||||
rt_log("vk_pipeline: dynamic vertex input off (%s)", dv && *dv ? "TOTK_VK_DYN_VERTEX=0" : "Intel's older Windows driver draws nothing with it");
|
||||
rt_log("vk_pipeline: dynamic vertex input off (%s)",
|
||||
dv && *dv ? "TOTK_VK_DYN_VERTEX=0"
|
||||
: intel_legacy ? "Intel's older Windows driver draws nothing with it"
|
||||
: "Intel's Windows driver for integrated GPUs draws black patches with it");
|
||||
if (c->has_eds3) {
|
||||
P.dyn_clamp = e3.extendedDynamicState3DepthClampEnable && P.f_depth_clamp && PROC(set_clamp, "vkCmdSetDepthClampEnableEXT");
|
||||
P.dyn_poly = e3.extendedDynamicState3PolygonMode && PROC(set_poly, "vkCmdSetPolygonModeEXT");
|
||||
@@ -285,12 +291,17 @@ static int ensure_init(void) {
|
||||
// RDNA 2 and older (vk_caps.amd_sc), which applies alpha to coverage while the dynamic state says off: on an RX 590
|
||||
// (24.9.1) TotK, which never turns it on, drew Link and the NPCs with depth but no colour (black silhouettes) and
|
||||
// dithered other surfaces into checkerboards (issue #67, RX 470/570/580). Taken out of the dynamic state, it goes
|
||||
// into the pipeline key and the characters draw.
|
||||
// into the pipeline key and the characters draw. Also off on Intel's Windows driver for its integrated GPUs
|
||||
// (vk_caps.intel_igpu_win): UHD Graphics and Iris Xe drew coloured noise on branches, foliage and wood with it, and
|
||||
// vk_dyn_a2c = 0 removed it (issues #970, #992, #419).
|
||||
const char *da = cfg_get("TOTK_VK_DYN_A2C"); // no fixed default: it depends on the driver, see above
|
||||
int da_on = da && *da ? strcmp(da, "0") != 0 : !vk_caps.amd_sc;
|
||||
int da_on = da && *da ? strcmp(da, "0") != 0 : !vk_caps.amd_sc && !vk_caps.intel_igpu_win;
|
||||
if (!da_on && P.dyn_a2c) {
|
||||
P.dyn_a2c = 0;
|
||||
rt_log("vk_pipeline: dynamic alpha to coverage off (%s)", da && *da ? "TOTK_VK_DYN_A2C=0" : "AMD's driver for RDNA 2 and older");
|
||||
rt_log("vk_pipeline: dynamic alpha to coverage off (%s)",
|
||||
da && *da ? "TOTK_VK_DYN_A2C=0"
|
||||
: vk_caps.amd_sc ? "AMD's driver for RDNA 2 and older"
|
||||
: "Intel's Windows driver for integrated GPUs draws coloured noise with it");
|
||||
}
|
||||
#undef PROC
|
||||
static const VkDynamicState core[] = {
|
||||
@@ -3223,10 +3234,11 @@ static void dyn_vals(const VkPipeState *s, DynVals *o) {
|
||||
}
|
||||
}
|
||||
|
||||
// What the replay's command buffer holds, as vk_pipeline_set_dynamic_delta last recorded it (the replay thread only):
|
||||
// the values, the inputs of the vertex input and the vertex bindings it reads. Invariant while the caller passes
|
||||
// valid 1: cb holds DC.v for every dynamic state of P.dyn, colour attachments [0, DC.v.n) included.
|
||||
static struct {
|
||||
// What the replay's command buffer holds, as vk_pipeline_set_dynamic_delta last recorded it: the values, the inputs of
|
||||
// the vertex input and the vertex bindings it reads. Invariant while the caller passes valid 1: cb holds DC.v for every
|
||||
// dynamic state of P.dyn, colour attachments [0, DC.v.n) included. Per thread: the threads of a chunked replay
|
||||
// (vk_exec.c) each record their own command buffers, and a new one starts with valid 0.
|
||||
static _Thread_local struct {
|
||||
DynVals v;
|
||||
const GfxShader *vs;
|
||||
uint32_t vattr[16];
|
||||
|
||||
+99
-27
@@ -39,15 +39,24 @@ static int rec_live; // producer: rec_begin started the record
|
||||
static int end_mode = -1; // TOTK_REC_END (rec_own)
|
||||
static int sub_pending; // producer: the recorder ended a part that rec_submit has not handed over yet
|
||||
static volatile VkResult r_sub_res; // recorder: the result of its last vkQueueSubmit2, read after the drain
|
||||
static volatile VkResult r_sub_err; // recorder: the first vkQueueSubmit2 that failed (rec_submit reports it)
|
||||
static volatile uint64_t q_subs; // producer: submissions queued for the recorder (rec_submit)
|
||||
static volatile uint64_t r_subs; // recorder: submissions it handed to the driver (submit_run)
|
||||
static _Thread_local int on_rec; // the calling thread is the recorder
|
||||
static int rec_async = -1; // TOTK_REC_ASYNC (rec_submit)
|
||||
static struct { uint64_t n, ns; } RQ_; // waits in rec_queue_wait, logged by rec_stats_log
|
||||
static PlatThread *rec_thread;
|
||||
static int rec_prio; // the submitting thread's priority, which the recorder takes
|
||||
static int rec_chk; // TOTK_REC_CHECK (rec_check_handle)
|
||||
static uint64_t rec_delay_ns; // TOTK_REC_DELAY_US (rec_main)
|
||||
static uint64_t rec_spin_ns; // TOTK_REC_SPIN_US (rec_main); 0: 4096 pauses
|
||||
static int rec_spin_ab; // TOTK_REC_SPIN_US=ab: the timed poll in the B blocks of TOTK_AB only
|
||||
static volatile uint64_t part_h0; // Q.put_h at rec_begin: where the packets of the part being recorded start
|
||||
static volatile uint64_t part_fence; // the value of the part being recorded (rec_begin)
|
||||
static uint64_t r_sleeps; // recorder: sleeps on wake_ev, read by rec_stats_log
|
||||
|
||||
// Producer statistics, logged by rec_stats_log.
|
||||
static struct { _Alignas(64) uint64_t packets, words, drains, fallbacks, drain_ns, full_waits, parts, parts_on, submits; } RS_;
|
||||
static struct { _Alignas(64) uint64_t packets, words, drains, fallbacks, drain_ns, full_waits, parts, parts_on, submits, wakes; } RS_;
|
||||
|
||||
enum {
|
||||
R_PAD, R_BeginQuery, R_BeginRendering, R_BindDescriptorSets, R_BindIndexBuffer, R_BindPipeline, R_BindVertexBuffers2,
|
||||
@@ -71,15 +80,33 @@ static void exec_packet(uint32_t op, void *p);
|
||||
static void rec_main(void *arg) {
|
||||
(void)arg;
|
||||
plat_thread_priority(rec_prio);
|
||||
on_rec = 1;
|
||||
perf_core_thread(PCORE_RECORDER);
|
||||
uint64_t t = 0;
|
||||
for (;;) {
|
||||
uint64_t h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);
|
||||
if (t == h) {
|
||||
for (int i = 0; i < 4096 && t == h; i++) _mm_pause(), h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);
|
||||
// Poll for the next packets before sleeping: a sleep costs the producer an event set and the recorder
|
||||
// an OS wake-up. The poll is timed (TOTK_REC_SPIN_US, default 60) because a pause takes 8-150 cycles
|
||||
// depending on the CPU: 4096 of them are ~140 us on a Coffee Lake i7 and a few us on Zen 4. With fewer than
|
||||
// 6 logical processors it stays 4096 pauses. Savage Wind Recomp's recorder polls 60 us the same way.
|
||||
if (rec_spin_ns && (!rec_spin_ab || rt_ab())) {
|
||||
uint64_t t0 = now_ns();
|
||||
for (unsigned i = 1; t == h; i++) {
|
||||
_mm_pause();
|
||||
h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);
|
||||
if (!(i & 31) && now_ns() - t0 >= rec_spin_ns) break;
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < 4096 && t == h; i++) _mm_pause(), h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
if (t == h) {
|
||||
__atomic_store_n(&Q.sleeping, 1, __ATOMIC_SEQ_CST);
|
||||
h = __atomic_load_n(&Q.head, __ATOMIC_SEQ_CST);
|
||||
if (t == h) plat_event_wait_ns(&wake_ev, 2000000); // a missed wake costs at most 2 ms
|
||||
if (t == h) {
|
||||
__atomic_add_fetch(&r_sleeps, 1, __ATOMIC_RELAXED);
|
||||
plat_event_wait_ns(&wake_ev, 2000000); // a missed wake costs at most 2 ms
|
||||
}
|
||||
__atomic_store_n(&Q.sleeping, 0, __ATOMIC_RELAXED);
|
||||
continue;
|
||||
}
|
||||
@@ -103,7 +130,7 @@ static void rec_main(void *arg) {
|
||||
|
||||
static void wake(void) {
|
||||
__atomic_thread_fence(__ATOMIC_SEQ_CST);
|
||||
if (__atomic_load_n(&Q.sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
|
||||
if (__atomic_load_n(&Q.sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev), RS_.wakes++;
|
||||
}
|
||||
|
||||
// Room for need words: the tail read last leaves it in all but about one packet in 16 MiB of them.
|
||||
@@ -143,14 +170,21 @@ static void *put(uint32_t op, size_t bytes) {
|
||||
// rec_drain), and the packets that begin and end a part go out at once (pub_now), so the recorder begins and ends
|
||||
// command buffers as early as before; TOTK_REC_CHECK publishes every packet, since rec_check_handle searches the ring
|
||||
// up to head.
|
||||
// The batch is 64 words (512 bytes) where the recorder polls before it sleeps, 8192 (64 KiB) where it does not (fewer
|
||||
// than 6 usable logical processors, rec_spin_ns): there it is mostly asleep when a batch goes out, and each publish
|
||||
// cost the producer an event set and the recorder a wake-up. Savage Wind Recomp measured that at 6.7% of its render
|
||||
// thread's busy time on 4 cores and moved to 64 KiB there (runtime/nvn/vulkan/record.c rec_init, publish_every).
|
||||
// TOTK_REC_PUB=<words> sets the batch; =ab: 64 words in the A blocks of TOTK_AB, 8192 in the B blocks.
|
||||
static int pub_ab;
|
||||
static void pub_now(void) {
|
||||
Q.pub_h = Q.put_h;
|
||||
__atomic_store_n(&Q.head, Q.put_h, __ATOMIC_RELEASE);
|
||||
if (__atomic_load_n(&Q.sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev);
|
||||
if (__atomic_load_n(&Q.sleeping, __ATOMIC_RELAXED)) plat_event_set(&wake_ev), RS_.wakes++;
|
||||
}
|
||||
|
||||
static void pub(void) {
|
||||
if (Q.put_h - Q.pub_h >= Q.pub_words) pub_now();
|
||||
uint64_t w = pub_ab ? (rt_ab() ? 8192 : 64) : Q.pub_words;
|
||||
if (Q.put_h - Q.pub_h >= w) pub_now();
|
||||
}
|
||||
|
||||
void rec_drain(void) {
|
||||
@@ -184,24 +218,26 @@ typedef struct { VkCommandBuffer cb; int begin, crumb; uint64_t n; } A_BeginCb;
|
||||
void rec_begin(VkCommandBuffer cb, uint64_t fence, int crumb, uint64_t n) {
|
||||
static int mode = -1;
|
||||
if (mode < 0) {
|
||||
// Default off on AMD GPUs. With the recorder those drivers lost the device or hung the GPU at startup on
|
||||
// RDNA 2, 3 and 4, under AMD's Windows driver and under RADV (issues #5, #40, #277 and #283: a black screen,
|
||||
// or VK_ERROR_DEVICE_LOST with a breadcrumb naming a part of a replay submission, and vk_recorder = 0 gets
|
||||
// past it). Keeping objects until their submission completed and ending a part from the recorder
|
||||
// (rec_end_cb) fixed the white scene on an RX 6700 XT, but not this: 0.2.8 turned the recorder on for every
|
||||
// GPU and the AMD reports came back the same day. TOTK_VK_RECORDER=1 turns it on for an A/B run.
|
||||
const char *set = cfg_get("TOTK_VK_RECORDER");
|
||||
mode = rt_knob("VK_RECORDER", vk_caps.amd ? "0" : "1");
|
||||
if (!mode && vk_caps.amd && !(set && *set))
|
||||
rt_log("vk_rec: no recorder thread on AMD GPUs (TOTK_VK_RECORDER=1 turns it on)");
|
||||
// On by default on every GPU. 0.2.8 turned it on for AMD too, and some RDNA 2, 3 and 4 systems lost the device
|
||||
// or hung the GPU at startup (issues #5, #40, #277 and #283: a black screen, or VK_ERROR_DEVICE_LOST with a
|
||||
// breadcrumb naming a part of a replay submission), so 0.3.x kept it off on AMD. Since then the recorder
|
||||
// begins, ends and submits its own parts (TOTK_REC_END, which ended a GPU hang on AMD at split submissions),
|
||||
// and AMD players who turned it on reported more FPS, so it is on again; TOTK_VK_RECORDER=0 turns it off
|
||||
// where the device is still lost.
|
||||
mode = rt_knob("VK_RECORDER", "1");
|
||||
if (mode) {
|
||||
plat_event_init(&wake_ev, 0, 0);
|
||||
rec_prio = plat_thread_priority_get();
|
||||
rec_chk = rt_knob("REC_CHECK", "0"); // debug switches, read before the recorder starts
|
||||
const char *d = cfg_get("TOTK_REC_DELAY_US");
|
||||
rec_delay_ns = d && *d && atoi(d) > 0 ? (uint64_t)atoi(d) * 1000 : 0;
|
||||
const char *sp = cfg_get("TOTK_REC_SPIN_US");
|
||||
rec_spin_ab = sp && !strcmp(sp, "ab");
|
||||
int spin_us = sp && *sp && !rec_spin_ab ? atoi(sp) : 60;
|
||||
rec_spin_ns = spin_us > 0 && plat_cpu_usable() >= 6 ? (uint64_t)spin_us * 1000 : 0;
|
||||
const char *pw = cfg_get("TOTK_REC_PUB"); // words per publish (pub); 0: every packet
|
||||
Q.pub_words = rec_chk ? 0 : pw && *pw ? (uint64_t)atoi(pw) : 64;
|
||||
pub_ab = !rec_chk && pw && !strcmp(pw, "ab");
|
||||
Q.pub_words = rec_chk ? 0 : pw && *pw && !pub_ab ? (uint64_t)atoi(pw) : rec_spin_ns ? 64 : 8192;
|
||||
if (Q.pub_words > RING_WORDS / 8) Q.pub_words = RING_WORDS / 8;
|
||||
rec_thread = plat_thread_new(rec_main, NULL, "vk recorder");
|
||||
if (!rec_thread) {
|
||||
@@ -211,8 +247,13 @@ void rec_begin(VkCommandBuffer cb, uint64_t fence, int crumb, uint64_t n) {
|
||||
rt_log("vk_rec: the replay's Vulkan commands are made by a recorder thread%s",
|
||||
mode == 2 ? " in the B blocks (TOTK_AB)" : "");
|
||||
if (rec_chk) rt_log("vk_rec: TOTK_REC_CHECK: deferred and destroyed objects are searched in the ring");
|
||||
if (Q.pub_words != 64) rt_log("vk_rec: TOTK_REC_PUB: head published every %llu words",
|
||||
(unsigned long long)Q.pub_words);
|
||||
if (rec_spin_ns)
|
||||
rt_log("vk_rec: the recorder polls %llu us before it sleeps%s (TOTK_REC_SPIN_US)",
|
||||
(unsigned long long)(rec_spin_ns / 1000), rec_spin_ab ? " in the B blocks" : "");
|
||||
else
|
||||
rt_log("vk_rec: the recorder polls 4096 pauses before it sleeps (TOTK_REC_SPIN_US)");
|
||||
if (pub_ab) rt_log("vk_rec: head published every 64 words, 8192 in the B blocks (TOTK_REC_PUB=ab)");
|
||||
else rt_log("vk_rec: head published every %llu words (TOTK_REC_PUB)", (unsigned long long)Q.pub_words);
|
||||
if (rec_delay_ns) rt_log("vk_rec: TOTK_REC_DELAY_US: the recorder waits %llu us before each batch",
|
||||
(unsigned long long)(rec_delay_ns / 1000));
|
||||
}
|
||||
@@ -253,7 +294,7 @@ void rec_end(void) {
|
||||
// submission was split (vk_sync.c vk_sync_split): the GPU hung or the scene turned green, then white. Moving the end
|
||||
// fixed that; begin and submit followed. TOTK_REC_END=0 brings the old split back: the submitting thread begins,
|
||||
// ends and submits every part.
|
||||
// The line is written only when a part was recorded by the recorder: with the recorder off (the default on AMD) the
|
||||
// The line is written only when a part was recorded by the recorder: with the recorder off (TOTK_VK_RECORDER=0) the
|
||||
// switch changes nothing, and reports read an unconditional line as proof that the recorder was running (#315).
|
||||
typedef struct { int kind; uint64_t n; } A_EndCb;
|
||||
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n) {
|
||||
@@ -295,11 +336,20 @@ VkResult rec_submit(const VkSubmitInfo2 *si) {
|
||||
a->ncb = si->commandBufferInfoCount, a->nsig = si->signalSemaphoreInfoCount;
|
||||
for (uint32_t i = 0; i < a->ncb; i++) a->cbi[i] = si->pCommandBufferInfos[i];
|
||||
if (a->nsig) a->sig = si->pSignalSemaphoreInfos[0];
|
||||
pub();
|
||||
// Waited for here, so the queue holds every earlier submission when the caller goes on, as with the recorder off:
|
||||
// the present, uploads, readbacks and device waits on other threads rely on that order.
|
||||
rec_drain();
|
||||
__atomic_store_n(&q_subs, q_subs + 1, __ATOMIC_RELEASE);
|
||||
RS_.submits++;
|
||||
// TOTK_REC_ASYNC=1 (default): the submitting thread goes on while the recorder catches up with the part and submits
|
||||
// it; it waited here for 0.29 ms a frame in the town on a Ryzen 7900X, 0.34 ms limited to 4 cores. The queue order
|
||||
// the present, uploads, readbacks and device waits rely on holds through vk_queue_lock (rec_queue_wait). The values
|
||||
// the part signals are counted as issued at once (vk_sync.c): waiting for one on the host before its submission is
|
||||
// legal for a timeline semaphore. =0: waits here for the submission, as before; =ab with the A/B blocks.
|
||||
if (rec_async < 0) rec_async = rt_knob("REC_ASYNC", "1");
|
||||
if (RT_KNOB_ON(rec_async)) {
|
||||
pub_now();
|
||||
return __atomic_load_n(&r_sub_err, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
pub();
|
||||
rec_drain();
|
||||
return r_sub_res;
|
||||
}
|
||||
|
||||
@@ -308,17 +358,37 @@ static void submit_run(const A_Submit *a) {
|
||||
.pCommandBufferInfos = a->cbi, .signalSemaphoreInfoCount = a->nsig,
|
||||
.pSignalSemaphoreInfos = a->nsig ? &a->sig : NULL};
|
||||
vk_queue_lock();
|
||||
r_sub_res = vkQueueSubmit2(vk_ctx()->gfx_queue, 1, &si, VK_NULL_HANDLE);
|
||||
VkResult r = vkQueueSubmit2(vk_ctx()->gfx_queue, 1, &si, VK_NULL_HANDLE);
|
||||
vk_queue_unlock();
|
||||
r_sub_res = r;
|
||||
if (r != VK_SUCCESS && __atomic_load_n(&r_sub_err, __ATOMIC_RELAXED) == VK_SUCCESS)
|
||||
__atomic_store_n(&r_sub_err, r, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&r_subs, r_subs + 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
void rec_queue_wait(void) {
|
||||
if (!rec_thread || on_rec) return;
|
||||
uint64_t want = __atomic_load_n(&q_subs, __ATOMIC_ACQUIRE);
|
||||
if (__atomic_load_n(&r_subs, __ATOMIC_ACQUIRE) >= want) return;
|
||||
uint64_t t0 = now_ns();
|
||||
for (int i = 0; __atomic_load_n(&r_subs, __ATOMIC_ACQUIRE) < want; i++)
|
||||
if (i < 20000) _mm_pause();
|
||||
else plat_yield();
|
||||
__atomic_add_fetch(&RQ_.n, 1, __ATOMIC_RELAXED);
|
||||
__atomic_add_fetch(&RQ_.ns, now_ns() - t0, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
void rec_stats_log(void) {
|
||||
if (!(rt_trace & RT_TRACE_PERF) || !RS_.parts) return;
|
||||
rt_log("vk_rec: %llu of %llu parts recorded by the recorder: %llu packets (%.1f MiB), %llu drains (%llu for a call "
|
||||
"without a packet) waited %.2f ms in all, ring full %llu times, %llu submissions made by the recorder",
|
||||
"without a packet) waited %.2f ms in all, ring full %llu times, %llu submissions made by the recorder, "
|
||||
"recorder slept %llu times and was woken %llu times, queue users waited for its submissions %llu times (%.2f ms)",
|
||||
(unsigned long long)RS_.parts_on, (unsigned long long)RS_.parts, (unsigned long long)RS_.packets,
|
||||
RS_.words * 8 / 1048576.0, (unsigned long long)RS_.drains, (unsigned long long)RS_.fallbacks,
|
||||
RS_.drain_ns / 1e6, (unsigned long long)RS_.full_waits, (unsigned long long)RS_.submits);
|
||||
RS_.drain_ns / 1e6, (unsigned long long)RS_.full_waits, (unsigned long long)RS_.submits,
|
||||
(unsigned long long)__atomic_exchange_n(&r_sleeps, 0, __ATOMIC_RELAXED), (unsigned long long)RS_.wakes,
|
||||
(unsigned long long)__atomic_exchange_n(&RQ_.n, 0, __ATOMIC_RELAXED),
|
||||
__atomic_exchange_n(&RQ_.ns, 0, __ATOMIC_RELAXED) / 1e6);
|
||||
memset(&RS_, 0, sizeof RS_);
|
||||
}
|
||||
|
||||
@@ -381,6 +451,8 @@ void rec_check_log(void) {
|
||||
}
|
||||
|
||||
#define OWNED(cb) (rec_live && (cb) == rec_cb)
|
||||
|
||||
int rec_owns(VkCommandBuffer cb) { return OWNED(cb); }
|
||||
// A call the ring takes no packet for: everything queued first, then the call here.
|
||||
#define FALLBACK() (rec_drain(), RS_.fallbacks++)
|
||||
|
||||
|
||||
+10
-3
@@ -5,8 +5,10 @@
|
||||
// command buffer (uploads, the window, probes) are made at once. A call the ring has no packet for (a structure with a
|
||||
// pNext chain) waits for the recorder to empty the ring and is then made at once on the calling thread. With
|
||||
// TOTK_REC_END=1 (default) the recorder also begins, ends and submits the parts it records, so a part's command buffer
|
||||
// never changes threads; rec_submit returns once the recorder submitted it. The submitting thread allocates from the
|
||||
// part pool only while the recorder is idle (vk_sync.c claim_slot).
|
||||
// never changes threads. rec_submit queues the submission and returns at once (TOTK_REC_ASYNC=1, default; 0: once the
|
||||
// recorder submitted it); every other user of the queue waits in vk_queue_lock until the recorder submitted what was
|
||||
// queued before (rec_queue_wait), so queue order stays the order of the calls. The submitting thread allocates from
|
||||
// the part pool only while the recorder is idle (vk_sync.c claim_slot).
|
||||
//
|
||||
// Every vkCmd* the runtime calls through vk.h goes through the rc_* wrappers below (the macros at the end); the
|
||||
// extension commands called through function pointers have rec_* wrappers of their own. TOTK_VK_RECORDER=0 makes every
|
||||
@@ -24,9 +26,14 @@ void rec_end(void); // waits for the recorder; no-op when rec_
|
||||
// (1: rec_submit hands the part over); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
|
||||
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n);
|
||||
// vkQueueSubmit2 of si (at most two command buffers, one signal, no wait, no fence) under vk_queue_lock: by the
|
||||
// recorder after a part it ended, waited for, else at once.
|
||||
// recorder after a part it ended, else at once. Queued for the recorder, it returns the first failure of an earlier
|
||||
// submission the recorder made (VK_SUCCESS if none), since this one's result is not known yet.
|
||||
VkResult rec_submit(const VkSubmitInfo2 *si);
|
||||
void rec_drain(void); // waits until the recorder made every call queued so far
|
||||
// vk_queue_lock, before it takes the lock (not on the recorder, not when the thread holds the lock already): waits
|
||||
// until the recorder submitted every submission queued so far.
|
||||
void rec_queue_wait(void);
|
||||
int rec_owns(VkCommandBuffer cb); // the recorder makes cb's calls: a direct call into cb follows rec_drain
|
||||
void rec_stats_log(void); // TOTK_TRACE=perf: packets, bytes, drains and waits since the last call
|
||||
// TOTK_REC_CHECK=1 (debug, recorder on): vk_memory.c reports each object it defers (destroy 0, stamped tl) or destroys
|
||||
// (destroy 1) with the value the GPU reached (gpu); a packet of the part being recorded that still names it is logged
|
||||
|
||||
+91
-14
@@ -56,7 +56,7 @@ static VkQueryPool ts_pool;
|
||||
static int ts_state; // 0 not set up, 1 on, -1 off
|
||||
static double ts_period; // ns per tick
|
||||
static uint64_t ts_mask; // valid bits of a timestamp
|
||||
static uint32_t ts_next; // submitting thread (sub_lock): next pair
|
||||
static uint32_t ts_next; // next pair (atomic: parts recorded on other threads take pairs too)
|
||||
static volatile uint8_t ts_busy[TS_PAIRS]; // written until the completion thread read the pair
|
||||
static int ts_cur = -1; // the pair of the part being recorded, -1 untimed
|
||||
static uint64_t ts_last_end; // completion thread: end of the latest part read (ticks)
|
||||
@@ -175,6 +175,12 @@ static void ts_read_cb(void *arg) { // completion thread, in submission order
|
||||
free(t);
|
||||
}
|
||||
|
||||
static int ts_take(void) {
|
||||
uint32_t p = __atomic_fetch_add(&ts_next, 1, __ATOMIC_RELAXED) % TS_PAIRS;
|
||||
if (__atomic_exchange_n(&ts_busy[p], 1, __ATOMIC_ACQUIRE)) { plat_inc64(&ts_missed); return -1; }
|
||||
return (int)p;
|
||||
}
|
||||
|
||||
// At the start of a command buffer (sub_lock held): the pair it times, -1 untimed.
|
||||
static int ts_open(VkCommandBuffer cb) {
|
||||
if (ts_state != 1 || !perf_gpu_timing()) {
|
||||
@@ -182,10 +188,9 @@ static int ts_open(VkCommandBuffer cb) {
|
||||
ts_setup(vk_ctx()); // the overlay turned full: timestamps from now on
|
||||
if (ts_state != 1) return -1;
|
||||
}
|
||||
uint32_t p = ts_next % TS_PAIRS;
|
||||
if (__atomic_load_n(&ts_busy[p], __ATOMIC_ACQUIRE)) { plat_inc64(&ts_missed); return -1; }
|
||||
ts_next++;
|
||||
ts_busy[p] = 1;
|
||||
int t = ts_take();
|
||||
if (t < 0) return -1;
|
||||
uint32_t p = (uint32_t)t;
|
||||
vkCmdResetQueryPool(cb, ts_pool, 2 * p, 2);
|
||||
vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, ts_pool, 2 * p);
|
||||
return (int)p;
|
||||
@@ -203,6 +208,17 @@ static void ts_close(VkCommandBuffer cb, int pair, uint64_t fence) {
|
||||
vk_completion_add(fence, ts_read_cb, t);
|
||||
}
|
||||
|
||||
int vk_sync_ts_open(VkCommandBuffer cb) {
|
||||
if (__atomic_load_n(&ts_state, __ATOMIC_ACQUIRE) != 1 || !perf_gpu_timing()) return -1;
|
||||
int p = ts_take();
|
||||
if (p < 0) return -1;
|
||||
vkCmdResetQueryPool(cb, ts_pool, 2 * (uint32_t)p, 2);
|
||||
vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, ts_pool, 2 * (uint32_t)p);
|
||||
return p;
|
||||
}
|
||||
|
||||
void vk_sync_ts_close(VkCommandBuffer cb, int pair, uint64_t fence) { ts_close(cb, pair, fence); }
|
||||
|
||||
// The pair of the part being recorded.
|
||||
static void ts_begin(VkCommandBuffer cb) { ts_cur = ts_open(cb); }
|
||||
|
||||
@@ -349,26 +365,59 @@ static CmdSlot *take_slot(VkCtx *c, uint64_t fence) {
|
||||
// submitted right before the part that needs them and timed like a part; VK_NULL_HANDLE when there are none. After the
|
||||
// part was recorded, sub_lock held; may move slots[].
|
||||
// First brings the shadow storage up to date with the CPU's writes, which needs no command buffer.
|
||||
static VkCommandBuffer prologue(VkCtx *c, uint64_t fence) {
|
||||
vk_mem_shadow_sync(); // shadow storage: the CPU's writes go in before the part is submitted
|
||||
if (!vk_mem_mirror_pending()) return VK_NULL_HANDLE;
|
||||
// direct: the calling thread records, ends and submits it itself while the part being replayed stays the recorder's
|
||||
// (vk_sync_part, which the caller made the recorder idle for).
|
||||
static VkCommandBuffer prologue(VkCtx *c, uint64_t fence, int direct) {
|
||||
// Shadow storage: the CPU's writes go in before the part is submitted (TOTK_SYNC_AHEAD: queued for the GPU or
|
||||
// made after the GPU finished the readers the game may think done, vk_memory.c).
|
||||
vk_mem_shadow_sync(fence);
|
||||
if (!vk_mem_mirror_pending() && !vk_mem_shadow_deferred()) return VK_NULL_HANDLE;
|
||||
int i = claim_slot(c, fence); // before reading slots: the claim may move it
|
||||
VkCommandBuffer cb = slots[i].cb;
|
||||
rec_begin(cb, fence, 0, 0); // a command buffer of the part pool: the recorder's like the parts
|
||||
if (direct) {
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
vkBeginCommandBuffer(cb, &bi);
|
||||
} else {
|
||||
rec_begin(cb, fence, 0, 0); // a command buffer of the part pool: the recorder's like the parts
|
||||
}
|
||||
int pair = ts_open(cb);
|
||||
vk_mem_shadow_prologue(cb, fence); // before the mirror copies, which read the shadow these bring up to date
|
||||
vk_mem_mirror_prologue(cb);
|
||||
ts_close(cb, pair, fence);
|
||||
if (!rec_end_cb(cb, 0, 0)) vkEndCommandBuffer(cb);
|
||||
if (direct || !rec_end_cb(cb, 0, 0)) vkEndCommandBuffer(cb);
|
||||
return cb;
|
||||
}
|
||||
|
||||
// A chunk of a chunked replay (vk_exec.c chunks_submit), while the submission's own part is open on the recorder. The
|
||||
// recorder begins, records, ends and submits the parts it owns (vk_rec.c rec_end_cb), so the prologue stays off it:
|
||||
// the recorder is made idle first, as the part pool allows one thread at a time, and the prologue is recorded, ended
|
||||
// and submitted here without taking the recorder's part away.
|
||||
void vk_sync_part(VkCommandBuffer cb) {
|
||||
VkCtx *c = vk_ctx();
|
||||
vk_upload_flush(0);
|
||||
rec_drain();
|
||||
VkCommandBuffer pro = prologue(c, cur_fence, 1);
|
||||
VkCommandBufferSubmitInfo cbi[2] = {{.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = pro},
|
||||
{.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = cb}};
|
||||
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .commandBufferInfoCount = pro ? 2 : 1,
|
||||
.pCommandBufferInfos = pro ? cbi : cbi + 1};
|
||||
vk_queue_lock();
|
||||
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
|
||||
vk_queue_unlock();
|
||||
if (r != VK_SUCCESS) {
|
||||
vk_device_fault_report(r);
|
||||
rt_fatal("vk: vkQueueSubmit2 of a part of value %llu failed (%d)", (unsigned long long)cur_fence, (int)r);
|
||||
}
|
||||
}
|
||||
|
||||
VkCommandBuffer vk_sync_split(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
VkCommandBuffer cb = slots[cur_slot].cb;
|
||||
ts_end(cb, cur_fence);
|
||||
if (!rec_end_cb(cb, 0, 0)) vkEndCommandBuffer(cb); // every command of the part is in cb, ended by its recorder
|
||||
vk_upload_flush(0); // the uploads this part asked for precede it in queue order
|
||||
VkCommandBuffer pro = prologue(c, cur_fence);
|
||||
VkCommandBuffer pro = prologue(c, cur_fence, 0);
|
||||
VkCommandBufferSubmitInfo cbi[2] = {{.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = pro},
|
||||
{.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = cb}};
|
||||
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .commandBufferInfoCount = pro ? 2 : 1,
|
||||
@@ -419,7 +468,7 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
perf_replay(t0);
|
||||
// After the recording: the uploads it asked for (and every earlier one) precede it in queue order.
|
||||
vk_upload_flush(0);
|
||||
VkCommandBuffer pro = prologue(c, fence);
|
||||
VkCommandBuffer pro = prologue(c, fence, 0);
|
||||
s = &slots[cur_slot];
|
||||
VkCommandBufferSubmitInfo cbi[2] = {{.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = pro},
|
||||
{.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = s->cb}};
|
||||
@@ -446,8 +495,11 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
return fence;
|
||||
}
|
||||
|
||||
void vk_completion_add(uint64_t fence, void (*fn)(void *), void *arg) {
|
||||
plat_mutex_lock(&st_lock);
|
||||
static _Thread_local VkDeferred *deferred;
|
||||
|
||||
void vk_completion_defer(VkDeferred *d) { deferred = d; }
|
||||
|
||||
static void queue_put(uint64_t fence, void (*fn)(void *), void *arg) { // st_lock held
|
||||
if (qhead + qn == qcap) {
|
||||
if (qhead) {
|
||||
memmove(queue, queue + qhead, sizeof *queue * (size_t)qn);
|
||||
@@ -463,9 +515,34 @@ void vk_completion_add(uint64_t fence, void (*fn)(void *), void *arg) {
|
||||
// The completion thread sleeps only while nothing is issued beyond done: a callback for a later value (the replay's,
|
||||
// for the submission it records: counter reports, freed resources) becomes due through the submit, which wakes it.
|
||||
if (fence <= done) plat_cond_broadcast(&cv_work);
|
||||
}
|
||||
|
||||
void vk_completion_add(uint64_t fence, void (*fn)(void *), void *arg) {
|
||||
VkDeferred *d = deferred;
|
||||
if (d) {
|
||||
if (d->n == d->cap) {
|
||||
d->cap = d->cap ? d->cap * 2 : 64;
|
||||
d->items = realloc(d->items, sizeof(Pending) * (size_t)d->cap);
|
||||
if (!d->items) rt_fatal("vk: out of memory (completion queue)");
|
||||
}
|
||||
((Pending *)d->items)[d->n++] = (Pending){fence, fn, arg};
|
||||
return;
|
||||
}
|
||||
plat_mutex_lock(&st_lock);
|
||||
queue_put(fence, fn, arg);
|
||||
plat_mutex_unlock(&st_lock);
|
||||
}
|
||||
|
||||
void vk_completion_flush(VkDeferred *d) {
|
||||
plat_mutex_lock(&st_lock);
|
||||
for (int i = 0; i < d->n; i++) {
|
||||
const Pending *p = &((Pending *)d->items)[i];
|
||||
queue_put(p->fence, p->fn, p->arg);
|
||||
}
|
||||
plat_mutex_unlock(&st_lock);
|
||||
d->n = 0;
|
||||
}
|
||||
|
||||
static int fence_wait_impl(uint64_t value, uint64_t timeout_ns);
|
||||
|
||||
int vk_sync_fence_wait(uint64_t value, uint64_t timeout_ns) {
|
||||
|
||||
+226
-46
@@ -46,6 +46,8 @@ typedef struct Tex {
|
||||
uint32_t wgen; // page generation of the last upload check
|
||||
uint32_t checked; // submit_epoch of the last check from a draw binding
|
||||
int uploaded, released;
|
||||
uint32_t rev_epoch, rev_pos; // the state scan made it live in that submission at that op (vk_texture_revive)
|
||||
uint32_t own_epoch, own_pos; // a chunk made it GPU-owned in that submission at that op (vk_texture_gpu_written)
|
||||
uint32_t n_up; // uploads of this entry (vk_texture_debug)
|
||||
int last_ch; // last vk_mem_changed result of make_fresh: 1 written, 0 clean, -1 not tracked
|
||||
struct Tex *next_at; // other entries at the same storage address
|
||||
@@ -62,6 +64,7 @@ typedef struct Tex {
|
||||
uint32_t wt_n; // WriteTexels into the storage since the last upload
|
||||
struct Tex *rel_prev, *rel_next; // while released: the list of released entries, oldest first
|
||||
uint64_t rel_ms; // while released: when (coarse_ms)
|
||||
uint64_t rel_bytes; // while released: its own memory, counted in rel_bytes_total
|
||||
uint64_t use_seq; // order of the last creation or take-back among all entries
|
||||
uint64_t rel_seq; // while released: use_seq of the newest entry placed before this release; an entry
|
||||
// with a later use_seq on overlapping storage replaced this one's bytes
|
||||
@@ -93,6 +96,7 @@ static SampSlot *sslots;
|
||||
static int nsslots;
|
||||
static nvn_hmap samplers = {.klen = sizeof(samp_par)};
|
||||
static int nreleased;
|
||||
static uint64_t rel_bytes_total; // own memory of the released entries
|
||||
static Tex *rel_head, *rel_tail; // released entries in release order (sweep_released drops from the head)
|
||||
static uint64_t n_uploads, n_reuploads;
|
||||
// Re-uploads since the last perf line (tex_lock): all, and those of textures a WriteTexels wrote since their last upload.
|
||||
@@ -122,10 +126,12 @@ static uint64_t n_checks; // draw-binding storage checks (make_fresh_snap) si
|
||||
// which emptied every cache keyed on the global generations.
|
||||
#define AGEN_N 4096
|
||||
static uint32_t agen[AGEN_N];
|
||||
static uint32_t arev[AGEN_N]; // per storage address (hashed): the submit_epoch of the state scan's last revival there
|
||||
// Per slot id, for vk_texture_slot_token without the lock: the registration serial (bits 16-47; 0: never registered)
|
||||
// and the address generation index (bits 0-15), one word so a reader gets a consistent pair. Fixed-size (slot ids are
|
||||
// 20 bits), unlike tslots, which moves when it grows.
|
||||
static uint64_t *sinfo;
|
||||
static uint32_t *sepoch; // per slot: the submit_epoch its entry was checked in (vk_texture_slot_checked), 0 none
|
||||
static uint32_t reg_serial;
|
||||
|
||||
static uint32_t agen_idx(uint64_t addr) { return (uint32_t)(addr * 0x9E3779B97F4A7C15ull >> 52); }
|
||||
@@ -143,11 +149,17 @@ static void tex_init(void *p) {
|
||||
__atomic_store_n(&tex_ready, 1, __ATOMIC_RELEASE);
|
||||
return;
|
||||
}
|
||||
static _Thread_local int tex_depth; // how often this thread holds tex_lock (entry_of releases it for a wait)
|
||||
static _Thread_local uint32_t tex_settles; // entry_of waits of this thread (vk_texture_by_id_locked)
|
||||
static void lock(void) {
|
||||
if (!__atomic_load_n(&tex_ready, __ATOMIC_ACQUIRE)) plat_once(&tex_once, tex_init, NULL);
|
||||
plat_rmutex_lock(&tex_lock);
|
||||
tex_depth++;
|
||||
}
|
||||
static void unlock(void) {
|
||||
tex_depth--;
|
||||
plat_rmutex_unlock(&tex_lock);
|
||||
}
|
||||
static void unlock(void) { plat_rmutex_unlock(&tex_lock); }
|
||||
|
||||
static uint32_t umax(uint32_t a, uint32_t b) { return a > b ? a : b; }
|
||||
static uint32_t mip(uint32_t v, unsigned lv) { return umax(v >> lv, 1); }
|
||||
@@ -447,8 +459,19 @@ static Tex *create_tex(const tex_t *t, const TexKey *k, int defer_init) {
|
||||
}
|
||||
}
|
||||
if (!img) {
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
VkResult r = vmaCreateImage(c->vma, &ci, &ai, &img, &e->alloc, NULL);
|
||||
VkResult r = rt_oom_forced(RT_OOM_IMAGE) ? VK_ERROR_OUT_OF_DEVICE_MEMORY
|
||||
: vk_texture_image(&ci, &img, &e->alloc);
|
||||
// Out of memory: one more try after the headroom blocks and the finished deferred objects are freed
|
||||
// (vk_oom_release, which takes cmd_lock after the texture lock held here); released textures go at the next
|
||||
// submit. No wait: the texture is asked for again at its next use, while a wait would hold the texture lock.
|
||||
if (r == VK_ERROR_OUT_OF_HOST_MEMORY || r == VK_ERROR_OUT_OF_DEVICE_MEMORY) {
|
||||
static volatile int32_t n_retried;
|
||||
vk_oom_release();
|
||||
VkResult again = vk_texture_image(&ci, &img, &e->alloc);
|
||||
log_limited(&n_retried, "image creation failed (%d), tried again after a memory release: %s (%ux%ux%u fmt %#x)",
|
||||
(int)r, again == VK_SUCCESS ? "created" : "failed again", w, h, depth, d->format);
|
||||
r = again;
|
||||
}
|
||||
if (r != VK_SUCCESS) {
|
||||
log_limited(&n_failed, "image creation failed (%d): nvn fmt %#x %ux%ux%u target %d levels %u layers %u", (int)r,
|
||||
d->format, w, h, depth, d->target, levels, layers);
|
||||
@@ -528,7 +551,11 @@ static void own_remove(Tex *e) {
|
||||
plat_mutex_unlock(&owned_lock);
|
||||
}
|
||||
|
||||
static uint64_t own_bytes(const Tex *e);
|
||||
|
||||
static void rel_append(Tex *e) {
|
||||
e->rel_bytes = own_bytes(e);
|
||||
rel_bytes_total += e->rel_bytes;
|
||||
e->rel_next = NULL;
|
||||
e->rel_prev = rel_tail;
|
||||
if (rel_tail) rel_tail->rel_next = e;
|
||||
@@ -537,6 +564,8 @@ static void rel_append(Tex *e) {
|
||||
}
|
||||
|
||||
static void rel_unlink(Tex *e) {
|
||||
rel_bytes_total -= e->rel_bytes;
|
||||
e->rel_bytes = 0;
|
||||
if (e->rel_prev) e->rel_prev->rel_next = e->rel_next;
|
||||
else rel_head = e->rel_next;
|
||||
if (e->rel_next) e->rel_next->rel_prev = e->rel_prev;
|
||||
@@ -634,12 +663,19 @@ static void sweep_released(void) {
|
||||
// CLEAN_AGE_MS: without the wait a fight re-created about 500 of them per 10 s. Any other render target lives only in its
|
||||
// image: dropping one that was merely old brought the texture back empty (black flashes in a Depths boss fight, ~170
|
||||
// "left uninitialised" aliases a minute).
|
||||
//
|
||||
// TOTK_TEX_RELEASED_MIB (default 256; 0 = no cap): the same drops run while the released entries hold more own memory than
|
||||
// that, under pressure or not. A 10-minute field run on the RX 9070 kept 818 MiB of them, 730 MiB with their storage
|
||||
// taken by a later texture, while 23579 of 23660 take-backs came within a second and none after 120 s. With the
|
||||
// cap the same route ended with 216 MiB of them, 1.1 GB less process commit and 1 GB less in VMA blocks; 15 dropped
|
||||
// textures were created again, and the slow frames stayed the same.
|
||||
#define PRESSURE_STEP 32
|
||||
#define CLEAN_AGE_MS 5000u
|
||||
#define DEAD_AGE_MS 30000u
|
||||
#define DEAD_QUIET_SUBMITS 600u
|
||||
#define DEAD_RETRY_QUERIES 8 // budget queries to skip after a search that found nothing
|
||||
static int vram_pressure = -1;
|
||||
static uint64_t released_cap;
|
||||
static uint64_t n_pressure_drops, n_pressure_overwritten;
|
||||
|
||||
static int tex_by_addr(const void *a, const void *b) {
|
||||
@@ -693,16 +729,19 @@ static int dead_candidate(const Tex *e) {
|
||||
// Drops overwritten entries until `need` bytes are free or the step is used up. The sort runs only under pressure, while
|
||||
// an old enough entry exists, and rests for a while when it finds nothing to drop. Returns the bytes freed.
|
||||
static uint64_t pressure_search_ns; // longest search since the last mem: released line
|
||||
static uint64_t drop_overwritten(uint64_t need, int *dropped) {
|
||||
// step: PRESSURE_STEP, or OOM_STEP from oom_release, which neither waits out nor starts the rest.
|
||||
#define OOM_STEP 256
|
||||
static uint64_t drop_overwritten(uint64_t need, int *dropped, int step) {
|
||||
static int rest;
|
||||
if (rest > 0) { rest--; return 0; }
|
||||
int urgent = step > PRESSURE_STEP;
|
||||
if (rest > 0 && !urgent) { rest--; return 0; }
|
||||
if (!rel_head || coarse_ms - rel_head->rel_ms < DEAD_AGE_MS) return 0;
|
||||
uint64_t t0 = plat_now_ns(), freed = 0;
|
||||
AddrIndex x;
|
||||
if (!addr_index(&x)) return 0;
|
||||
Tex *cand[PRESSURE_STEP];
|
||||
Tex *cand[OOM_STEP];
|
||||
int nc = 0;
|
||||
for (Tex *e = rel_head; e && nc < PRESSURE_STEP && coarse_ms - e->rel_ms >= DEAD_AGE_MS; e = e->rel_next) {
|
||||
for (Tex *e = rel_head; e && nc < step && coarse_ms - e->rel_ms >= DEAD_AGE_MS; e = e->rel_next) {
|
||||
if (!dead_candidate(e)) continue;
|
||||
int lo = 0, hi = x.n; // the first entry at e's address, then e among those
|
||||
while (lo < hi) {
|
||||
@@ -720,35 +759,62 @@ static uint64_t drop_overwritten(uint64_t need, int *dropped) {
|
||||
destroy_tex(cand[i]);
|
||||
(*dropped)++, n_pressure_drops++, n_pressure_overwritten++;
|
||||
}
|
||||
if (!nc) rest = DEAD_RETRY_QUERIES;
|
||||
if (!nc && !urgent) rest = DEAD_RETRY_QUERIES;
|
||||
uint64_t ns = plat_now_ns() - t0;
|
||||
if (ns > pressure_search_ns) pressure_search_ns = ns;
|
||||
return freed;
|
||||
}
|
||||
|
||||
// Clean released entries, oldest first: mirrored ones the GPU never wrote, which come back from their storage.
|
||||
static uint64_t drop_clean(uint64_t need, uint64_t freed, int *dropped, int step) {
|
||||
for (Tex *e = rel_head, *next; e && freed < need && *dropped < step && coarse_ms - e->rel_ms >= CLEAN_AGE_MS; e = next) {
|
||||
next = e->rel_next;
|
||||
if (!e->pub.mirrored || e->pub.gpu_owned || e->gpu_dirty) continue;
|
||||
freed += own_bytes(e);
|
||||
dropped_note(e);
|
||||
destroy_tex(e);
|
||||
(*dropped)++, n_pressure_drops++;
|
||||
}
|
||||
return freed;
|
||||
}
|
||||
|
||||
static void sweep_pressure(void) { // tex_lock held
|
||||
if (vram_pressure < 0) {
|
||||
const char *s = cfg_get("TOTK_TEX_VRAM_PRESSURE");
|
||||
vram_pressure = s && *s ? atoi(s) : 90;
|
||||
if (vram_pressure < 0 || vram_pressure > 100) vram_pressure = 90;
|
||||
rt_log("vk: released textures under VRAM pressure: %s%d%%", vram_pressure ? "dropped above " : "off", vram_pressure);
|
||||
const char *m = cfg_get("TOTK_TEX_RELEASED_MIB");
|
||||
released_cap = (m && *m ? strtoull(m, NULL, 10) : 256) << 20;
|
||||
rt_log("vk: released textures under VRAM pressure: %s%d%%; above %llu MiB of them: %s (TOTK_TEX_RELEASED_MIB)",
|
||||
vram_pressure ? "dropped above " : "off", vram_pressure, (unsigned long long)(released_cap >> 20),
|
||||
released_cap ? "dropped" : "no cap");
|
||||
}
|
||||
if (!vram_pressure || !rel_head || submit_epoch & 7) return; // the budget query every 8 submits
|
||||
if (!rel_head || submit_epoch & 7) return; // the budget query every 8 submits
|
||||
uint64_t need = released_cap && rel_bytes_total > released_cap ? rel_bytes_total - released_cap : 0;
|
||||
uint64_t used, budget;
|
||||
int exact;
|
||||
if (!vk_mem_vram(&used, &budget, &exact)) return;
|
||||
uint64_t line = budget / 100 * (uint64_t)vram_pressure;
|
||||
if (used <= line) return;
|
||||
int dropped = 0;
|
||||
uint64_t need = used - line, freed = drop_overwritten(need, &dropped);
|
||||
for (Tex *e = rel_head, *next; e && freed < need && dropped < PRESSURE_STEP && coarse_ms - e->rel_ms >= CLEAN_AGE_MS; e = next) {
|
||||
next = e->rel_next;
|
||||
if (!e->pub.mirrored || e->pub.gpu_owned || e->gpu_dirty) continue;
|
||||
freed += own_bytes(e);
|
||||
dropped_note(e);
|
||||
destroy_tex(e);
|
||||
dropped++, n_pressure_drops++;
|
||||
if (vram_pressure && vk_mem_vram(&used, &budget, &exact)) {
|
||||
uint64_t line = budget / 100 * (uint64_t)vram_pressure;
|
||||
if (used > line && used - line > need) need = used - line;
|
||||
}
|
||||
if (!need) return;
|
||||
int dropped = 0;
|
||||
uint64_t freed = drop_overwritten(need, &dropped, PRESSURE_STEP);
|
||||
drop_clean(need, freed, &dropped, PRESSURE_STEP);
|
||||
}
|
||||
|
||||
// rt_oom_request (an allocation failed somewhere, oom.c): the same two kinds as sweep_pressure, as many as OOM_STEP
|
||||
// whatever the budget says, and the headroom blocks. Never GPU-written content that lives only in its image. The memory
|
||||
// comes back once the submission being recorded is done (destroy_tex defers the images). Texture lock held.
|
||||
static void oom_release(void) {
|
||||
int dropped = 0;
|
||||
uint64_t freed = drop_overwritten(UINT64_MAX, &dropped, OOM_STEP);
|
||||
int overwritten = dropped;
|
||||
freed = drop_clean(UINT64_MAX, freed, &dropped, OOM_STEP);
|
||||
vk_headroom_release();
|
||||
static volatile int32_t n_logged;
|
||||
log_limited(&n_logged, "out of memory: %d released textures dropped (%d overwritten, %d clean), %.0f MiB, %d kept",
|
||||
dropped, overwritten, dropped - overwritten, freed / 1048576.0, nreleased);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- uploads
|
||||
@@ -1231,10 +1297,20 @@ static int alias_compatible(const Tex *e, const Tex *src) {
|
||||
return eb && eb == sb && ew == sw && eh == sh;
|
||||
}
|
||||
|
||||
// src holds content the storage bytes do not: the GPU wrote it and, for a mirrored texture, it was not written back
|
||||
// and the CPU has not rewritten the storage since (the new entry's upload then reads the newer bytes).
|
||||
static int alias_has_gpu_content(const Tex *src) {
|
||||
if (!src->pub.gpu_owned) return 0;
|
||||
// Whether e is live at op pos of a chunked replay: the state scan is ahead of the chunks, and an entry it revived at a
|
||||
// later op is still released for this one. Two 4K targets at one address, RGBA8 and R8 with memory of their own, were
|
||||
// swapped whenever the scan got past the second's SET_RT before a chunk sampling the storage got its turn, and large
|
||||
// parts of the scene flickered. UINT32_MAX: the state now.
|
||||
static int live_at(const Tex *e, uint32_t pos) {
|
||||
return !e->released && !(e->rev_epoch == submit_epoch && e->rev_pos > pos);
|
||||
}
|
||||
|
||||
// src holds content the storage bytes do not, at op pos of a chunked replay (UINT32_MAX: the state now): the GPU wrote
|
||||
// it and, for a mirrored texture, it was not written back and the CPU has not rewritten the storage since (the new
|
||||
// entry's upload then reads the newer bytes). A chunk at a later op that rendered src first does not count: in replay
|
||||
// order that render comes after the new entry's first use.
|
||||
static int alias_has_gpu_content(const Tex *src, uint32_t pos) {
|
||||
if (!src->pub.gpu_owned || (src->own_epoch == submit_epoch && src->own_pos > pos)) return 0;
|
||||
if (!src->pub.mirrored) return 1;
|
||||
uint32_t now;
|
||||
return src->gpu_dirty && !storage_rewritten(src, src->gpu_gen, &now);
|
||||
@@ -1288,10 +1364,45 @@ static int alias_copy(Tex *e, const Tex *src) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void entry_live(Tex *e) {
|
||||
if (!e->released) return;
|
||||
takeback_n[e->pub.gpu_owned != 0][age_bucket(coarse_ms - e->rel_ms)]++;
|
||||
e->use_seq = ++use_counter;
|
||||
e->released = 0, nreleased--, rel_unlink(e), tex_gen_bump(e->key.addr);
|
||||
}
|
||||
|
||||
// A live colour entry at e's storage of e's size, as of op pos: the new entry may take its content (alias_copy).
|
||||
static int alias_candidate(const TexKey *k, const tex_t *t, uint32_t pos) {
|
||||
for (const Tex *p = nvn_hm_get(&by_addr, &k->addr); p; p = p->next_at)
|
||||
if (live_at(p, pos) && !p->f.depth && !p->f.stencil && p->t.L.size == t->L.size) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Tex *entry_of(const tex_t *t) {
|
||||
TexKey k;
|
||||
make_key(t, &k);
|
||||
Tex *e = nvn_hm_get(&by_key, &k);
|
||||
uint32_t pos = vk_exec_replay_pos();
|
||||
// A chunk of a chunked replay makes the entry as of its own op: whether it takes the content of a texture at its
|
||||
// storage (alias_copy) depends on what the chunks before it render there, and those may still be recording on
|
||||
// other threads. Taken from a render they had not recorded yet, the new image got the storage's old bytes instead:
|
||||
// on an RX 7800 XT the grass near Link was missing in 3 of 29 launches with 4 threads (a map texture drawn once
|
||||
// per tile, kept from then on). With a live colour texture of its size at the storage, the chunk waits until the
|
||||
// chunks before it are recorded, the texture lock released for the wait (as often as this thread holds it).
|
||||
if (!e && pos != UINT32_MAX && tex_alias != 0 && alias_candidate(&k, t, pos) && vk_exec_chunk_behind()) {
|
||||
tex_t own = *t; // t may point into tslots, which a registration moves while the lock is released
|
||||
t = &own;
|
||||
int d = tex_depth;
|
||||
for (int i = 0; i < d; i++) unlock();
|
||||
vk_exec_chunk_settle();
|
||||
for (int i = 0; i < d; i++) lock();
|
||||
tex_settles++;
|
||||
if ((e = nvn_hm_get(&by_key, &k))) { // another chunk made it meanwhile
|
||||
entry_live(e);
|
||||
e->t = *t;
|
||||
return e;
|
||||
}
|
||||
}
|
||||
if (!e) {
|
||||
if (tex_alias < 0) {
|
||||
tex_alias = cfg_bool("TOTK_TEX_ALIAS");
|
||||
@@ -1309,8 +1420,9 @@ static Tex *entry_of(const tex_t *t) {
|
||||
nx = p->next_at;
|
||||
if (p->released) { destroy_tex(p); continue; }
|
||||
alias = 1;
|
||||
if (!live_at(p, pos)) continue; // the scan revives it at a later op: released at this chunk's
|
||||
if (tex_alias && (!src || (same_but_swizzle(&p->key, &k) && !same_but_swizzle(&src->key, &k))) &&
|
||||
alias_compatible(e, p) && alias_has_gpu_content(p))
|
||||
alias_compatible(e, p) && alias_has_gpu_content(p, pos))
|
||||
src = p;
|
||||
}
|
||||
if (alias) perf_count(PERF_CNT_TEX_ALIAS);
|
||||
@@ -1336,11 +1448,7 @@ static Tex *entry_of(const tex_t *t) {
|
||||
nvn_hm_put(&by_key, &k, e);
|
||||
tex_gen_bump(k.addr);
|
||||
}
|
||||
if (e->released) {
|
||||
takeback_n[e->pub.gpu_owned != 0][age_bucket(coarse_ms - e->rel_ms)]++;
|
||||
e->use_seq = ++use_counter;
|
||||
e->released = 0, nreleased--, rel_unlink(e), tex_gen_bump(k.addr);
|
||||
}
|
||||
entry_live(e);
|
||||
e->t = *t;
|
||||
return e;
|
||||
}
|
||||
@@ -1366,6 +1474,25 @@ const VkTexImage *vk_texture_get(const tex_t *t) {
|
||||
return e ? &e->pub : NULL;
|
||||
}
|
||||
|
||||
// The replay's state scan (vk_exec.c): the entry vk_texture_get would return, neither created nor made current; the
|
||||
// recording does that. It is revived here, in replay order, and remembers the op it happened at: a chunk may bind a
|
||||
// finalized target's slot before the chunk that renders it reached its SET_RT, or long after the scan went past a later
|
||||
// target at the same storage, and slot_resolve has to answer as the replay would at the chunk's own op (live_at).
|
||||
const VkTexImage *vk_texture_revive(const tex_t *t, uint32_t pos) {
|
||||
if (!t || !t->mem) return NULL;
|
||||
TexKey k;
|
||||
make_key(t, &k);
|
||||
lock();
|
||||
Tex *e = nvn_hm_get(&by_key, &k);
|
||||
if (e && e->released) {
|
||||
e->rev_epoch = submit_epoch, e->rev_pos = pos;
|
||||
__atomic_store_n(&arev[agen_idx(k.addr)], submit_epoch, __ATOMIC_RELEASE);
|
||||
entry_live(e);
|
||||
}
|
||||
unlock();
|
||||
return e ? &e->pub : NULL;
|
||||
}
|
||||
|
||||
// Render targets the CPU reads without NVN's invalidate (vk_texture_write_back_small); small_pending: one may be dirty.
|
||||
// wb_limit: targets up to this size (bytes) always go back when the CPU waits for a fence, TOTK_WB_FENCE_LIMIT, read
|
||||
// once. A larger one goes back unless the GPU wrote it in each of the last WB_RUN frames (wb_wanted). wb_trace:
|
||||
@@ -1398,6 +1525,10 @@ void vk_texture_gpu_written(const tex_t *t) {
|
||||
Tex *e = entry_of(t);
|
||||
if (e) {
|
||||
make_fresh(e); // a partial render keeps what the storage held before
|
||||
if (!e->pub.gpu_owned) { // entry_of's alias choice of a chunk at an earlier op leaves this render out
|
||||
uint32_t pos = vk_exec_replay_pos();
|
||||
e->own_epoch = pos == UINT32_MAX ? 0 : submit_epoch, e->own_pos = pos;
|
||||
}
|
||||
e->pub.gpu_owned = 1;
|
||||
uint64_t f = vk_exec_recording_fence();
|
||||
if (f > e->wr_fence) e->wr_fence = f;
|
||||
@@ -1468,7 +1599,9 @@ void vk_texture_register(int id, const tex_t *t, const view_t *v) {
|
||||
}
|
||||
if (!sinfo) {
|
||||
uint64_t *p = calloc(1 << 20, sizeof *p);
|
||||
if (!p) rt_fatal("vk: out of memory for texture slot tokens");
|
||||
uint32_t *q = calloc(1 << 20, sizeof *q);
|
||||
if (!p || !q) rt_fatal("vk: out of memory for texture slot tokens");
|
||||
__atomic_store_n(&sepoch, q, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&sinfo, p, __ATOMIC_RELEASE);
|
||||
}
|
||||
TexSlot *s = &tslots[id];
|
||||
@@ -1484,15 +1617,15 @@ void vk_texture_register(int id, const tex_t *t, const view_t *v) {
|
||||
unlock();
|
||||
}
|
||||
|
||||
// The entry slot s resolves to, and the view and has_view a caller gets with it (the slot's, with the descriptor's
|
||||
// swizzle when the entry is another descriptor at the same storage). NULL when the slot names no texture.
|
||||
static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view) {
|
||||
// The entry slot s resolves to at op pos, and the view and has_view a caller gets with it (the slot's, with the
|
||||
// descriptor's swizzle when the entry is another descriptor at the same storage). NULL when the slot names no texture.
|
||||
static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view, uint32_t pos) {
|
||||
*view = s->v;
|
||||
*has_view = s->has_view;
|
||||
TexKey k;
|
||||
make_key(&s->t, &k);
|
||||
Tex *e = nvn_hm_get(&by_key, &k);
|
||||
if (!e || e->released) { // MNG hle_nvn.c: a finalized slot follows the live texture at its storage
|
||||
if (!e || !live_at(e, pos)) { // MNG hle_nvn.c: a finalized slot follows the live texture at its storage
|
||||
// Prefer the live entry that differs from the slot descriptor only in its swizzle: transient render targets
|
||||
// share storage, and the newest one at the address is usually another size. The map screen samples its
|
||||
// 2250x1080 map target through an (R,G,B,1) descriptor and followed a 960x60 target at the same address.
|
||||
@@ -1505,13 +1638,14 @@ static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view) {
|
||||
Tex *own = e, *any = NULL;
|
||||
e = NULL;
|
||||
for (Tex *p = nvn_hm_get(&by_addr, &k.addr); p; p = p->next_at) {
|
||||
if (p->released || linear_format(p->key.d.format) != linear_format(k.d.format)) continue;
|
||||
if (!live_at(p, pos) || linear_format(p->key.d.format) != linear_format(k.d.format)) continue;
|
||||
if (!any) any = p;
|
||||
if (same_but_swizzle(&p->key, &k)) { e = p; break; }
|
||||
}
|
||||
if (!e) e = any ? any : own;
|
||||
}
|
||||
if (!e) e = s->t.mem ? entry_of(&s->t) : NULL;
|
||||
TexSlot copy; // entry_of may release the lock (a chunk's wait), and a registration then moves tslots
|
||||
if (!e && s->t.mem) copy = *s, s = ©, e = entry_of(&s->t);
|
||||
// A followed texture is another descriptor at the same storage: its image holds the content, but sampling takes the
|
||||
// slot descriptor's swizzle. The UI renders text into RGBA8 and samples it through a (A,A,A,A) descriptor; with the
|
||||
// render target's RGBA swizzle the text blur saw R = 1 everywhere and drew solid boxes behind tips and labels.
|
||||
@@ -1553,14 +1687,38 @@ const VkTexImage *vk_texture_by_id(int id, view_t *view, int *has_view) {
|
||||
const VkTexImage *vk_texture_by_id_locked(int id, view_t *view, int *has_view) {
|
||||
if (id < 0 || id >= ntslots || !tslots[id].used) return NULL;
|
||||
TexSlot *s = &tslots[id];
|
||||
uint32_t g = agen[sinfo[id] & 0xffff];
|
||||
uint32_t idx = sinfo[id] & 0xffff, g = agen[idx];
|
||||
uint32_t pos = arev[idx] == submit_epoch ? vk_exec_replay_pos() : UINT32_MAX;
|
||||
if (pos != UINT32_MAX) { // what the storage holds depends on the op: chunks at other ops share no answer in s
|
||||
view_t v;
|
||||
int hv;
|
||||
Tex *f = slot_resolve(s, &v, &hv, pos);
|
||||
s = &tslots[id]; // slot_resolve may have released the lock
|
||||
if (f && (!f->uploaded || f->checked != submit_epoch)) {
|
||||
make_fresh_snap(f);
|
||||
f->checked = submit_epoch;
|
||||
}
|
||||
if (view) *view = has_view ? v : s->v;
|
||||
if (has_view) *has_view = view ? hv : s->has_view;
|
||||
return f ? &f->pub : NULL;
|
||||
}
|
||||
if (!s->ce || s->cgen != g) { // resolved again: an entry at its address was created, dropped or released, or the
|
||||
view_t v; // slot registered (s->ce cleared); an empty result is not kept: image creation may
|
||||
int hv; // succeed later
|
||||
s->ce = slot_resolve(s, &v, &hv); // entry_of may create an entry and bump g: g read before it
|
||||
uint32_t settles = tex_settles;
|
||||
Tex *r = slot_resolve(s, &v, &hv, UINT32_MAX); // entry_of may create an entry and bump g: g read before it
|
||||
s = &tslots[id];
|
||||
if (settles != tex_settles) { // the lock was released for a wait: the slot may be another texture by now
|
||||
if (r && (!r->uploaded || r->checked != submit_epoch)) make_fresh_snap(r), r->checked = submit_epoch;
|
||||
if (view) *view = has_view ? v : s->v;
|
||||
if (has_view) *has_view = view ? hv : s->has_view;
|
||||
return r ? &r->pub : NULL;
|
||||
}
|
||||
s->ce = r;
|
||||
s->cview = v, s->chas_view = (uint8_t)hv;
|
||||
s->cgen = g;
|
||||
s->cepoch = 0;
|
||||
__atomic_store_n(&sepoch[id], 0, __ATOMIC_RELAXED);
|
||||
}
|
||||
Tex *e = s->ce;
|
||||
// Bindings of one submit share one check, answered from the submit's write-watch snapshot (guest writes during a
|
||||
@@ -1572,6 +1730,7 @@ const VkTexImage *vk_texture_by_id_locked(int id, view_t *view, int *has_view) {
|
||||
e->checked = submit_epoch;
|
||||
}
|
||||
s->cepoch = submit_epoch;
|
||||
__atomic_store_n(&sepoch[id], submit_epoch, __ATOMIC_RELEASE);
|
||||
}
|
||||
if (view) *view = has_view ? s->cview : s->v;
|
||||
if (has_view) *has_view = view ? s->chas_view : s->has_view;
|
||||
@@ -1593,11 +1752,26 @@ int vk_texture_slot_swz(int id, uint8_t swz[4]) {
|
||||
|
||||
uint32_t vk_texture_epoch(void) { return __atomic_load_n(&submit_epoch, __ATOMIC_RELAXED); }
|
||||
|
||||
int vk_texture_slot_moves(int id) {
|
||||
const uint64_t *si = __atomic_load_n(&sinfo, __ATOMIC_ACQUIRE);
|
||||
if (!si || id < 0 || id >= (1 << 20)) return 0;
|
||||
uint64_t v = __atomic_load_n(&si[id], __ATOMIC_RELAXED);
|
||||
return __atomic_load_n(&arev[v & 0xffff], __ATOMIC_ACQUIRE) == __atomic_load_n(&submit_epoch, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
int vk_texture_slot_checked(int id) {
|
||||
const uint32_t *e = __atomic_load_n(&sepoch, __ATOMIC_ACQUIRE);
|
||||
return e && id >= 0 && id < (1 << 20) &&
|
||||
__atomic_load_n(&e[id], __ATOMIC_ACQUIRE) == __atomic_load_n(&submit_epoch, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
void vk_texture_new_submit(void) {
|
||||
lock();
|
||||
if (!++submit_epoch) submit_epoch = 1;
|
||||
coarse_ms = plat_now_ms();
|
||||
sweep_pressure();
|
||||
if (__atomic_load_n(&rt_oom_req, __ATOMIC_RELAXED) && __atomic_exchange_n(&rt_oom_req, 0, __ATOMIC_ACQUIRE))
|
||||
oom_release();
|
||||
static uint64_t log_at;
|
||||
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) / 60;
|
||||
if ((rt_trace & RT_TRACE_PERF) && f != log_at && tex_alias > 0) {
|
||||
@@ -1624,8 +1798,9 @@ void vk_texture_new_submit(void) {
|
||||
(unsigned long long)f * 60, (unsigned long long)ck, (unsigned long long)n_checks, (unsigned long long)calls,
|
||||
(unsigned long long)untr);
|
||||
n_checks = 0;
|
||||
rt_log("perf: frame %llu: released textures kept %d, dropped under VRAM pressure %llu (%llu overwritten)",
|
||||
(unsigned long long)f * 60, nreleased, (unsigned long long)n_pressure_drops,
|
||||
rt_log("perf: frame %llu: released textures kept %d (%.0f MiB), dropped under VRAM pressure or the cap %llu "
|
||||
"(%llu overwritten)", (unsigned long long)f * 60, nreleased, rel_bytes_total / 1048576.0,
|
||||
(unsigned long long)n_pressure_drops,
|
||||
(unsigned long long)n_pressure_overwritten);
|
||||
n_pressure_drops = n_pressure_overwritten = 0;
|
||||
}
|
||||
@@ -1921,7 +2096,12 @@ static void write_back_n(Tex **v, int n) {
|
||||
if (np) vk_upload_flush(1);
|
||||
for (int i = 0; i < np; i++) {
|
||||
WbPart *q = &p[i];
|
||||
uint8_t *buf = malloc(q->g.img * (size_t)q->g.nz);
|
||||
size_t bytes = q->g.img * (size_t)q->g.nz;
|
||||
uint8_t *buf = malloc(bytes);
|
||||
if (!buf) { // waits for memory with the texture lock held, after the release that needs no submit
|
||||
vk_oom_release();
|
||||
buf = rt_realloc_wait(NULL, bytes, "vk: texture write-back buffer");
|
||||
}
|
||||
if (!buf) rt_fatal("vk: out of memory for a texture write-back");
|
||||
texels_read_take(q->e, &q->g, &q->t, buf);
|
||||
storage_texels(&q->e->t, &q->g, &q->r, buf, 1);
|
||||
@@ -2417,8 +2597,8 @@ void vk_texture_shutdown(void) {
|
||||
by_addr = (nvn_hmap){.klen = 8};
|
||||
samplers = (nvn_hmap){.klen = sizeof(samp_par)};
|
||||
free(tslots), free(sslots);
|
||||
tslots = NULL, sslots = NULL, ntslots = nsslots = 0, nreleased = 0;
|
||||
tslots = NULL, sslots = NULL, ntslots = nsslots = 0, nreleased = 0, rel_bytes_total = 0;
|
||||
rel_head = rel_tail = NULL;
|
||||
if (sinfo) memset(sinfo, 0, sizeof *sinfo << 20); // no slot is registered: tokens 0
|
||||
if (sinfo) memset(sinfo, 0, sizeof *sinfo << 20), memset(sepoch, 0, sizeof *sepoch << 20); // no slot is registered: tokens 0
|
||||
unlock();
|
||||
}
|
||||
+24
-12
@@ -11,11 +11,13 @@
|
||||
// that submission's write-back, the mirror holds them until then.
|
||||
// - A GPU write within the submission (UpdateUniformBuffer, a storage buffer a shader writes, a buffer copy or fill,
|
||||
// an image-to-buffer copy: vk_usnap_drop) drops the copies it touches from then on, so later draws read the result.
|
||||
// A chunked replay (vk_exec.c) records the ops out of order: its scan drops at the position of the op
|
||||
// (vk_usnap_drop_at, scan_writes for the shaders' stores), and a chunk finds a copy only at a position before that.
|
||||
// - The arena goes back to a free list once the submission's fence is done (completion thread).
|
||||
// The design follows MacNativeGames' TOTK_UBO_SNAPSHOT (games/totk/third_party/d2r_gfx/gfx/nvn/nvn_mtl.m usnap_take,
|
||||
// usnap_drop; MIT, Copyright (c) 2026 jhaizhou); the code is ours.
|
||||
// The copy is taken where vk_exec_record starts: at the submit call when the replay runs on the submitting thread, later
|
||||
// with TOTK_REPLAY_ASYNC. TOTK_UBO_SNAPSHOT=0 binds live memory as before; =ab alternates (TOTK_AB).
|
||||
// with TOTK_REPLAY_ASYNC; a chunked replay takes it on a recording thread while its scan runs. TOTK_UBO_SNAPSHOT=0 binds live memory as before; =ab alternates (TOTK_AB).
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -42,7 +44,7 @@ typedef struct Arena {
|
||||
} Arena;
|
||||
|
||||
typedef struct { uint64_t lo, hi; } Rng;
|
||||
typedef struct { uint64_t lo, hi, off; int dropped; } Reg;
|
||||
typedef struct { uint64_t lo, hi, off, drop_at; } Reg; // drop_at: dropped from this position on; UINT64_MAX not
|
||||
typedef struct { uint64_t lo, hi, fence; } Wrote;
|
||||
|
||||
static struct {
|
||||
@@ -62,6 +64,7 @@ static struct {
|
||||
|
||||
static Arena *free_arenas;
|
||||
static PlatMutex free_lock = PLAT_MUTEX_INIT;
|
||||
static PlatMutex drop_lock = PLAT_MUTEX_INIT; // threads recording chunks of one submission drop concurrently
|
||||
|
||||
static void *grow(void *p, int *cap, size_t elem) {
|
||||
int n = *cap ? *cap * 2 : 1024;
|
||||
@@ -279,7 +282,7 @@ void vk_usnap_begin(const uint64_t *handles, int n, uint64_t fence) {
|
||||
continue;
|
||||
}
|
||||
if (U.nreg == U.creg) U.reg = grow(U.reg, &U.creg, sizeof *U.reg);
|
||||
U.reg[U.nreg++] = (Reg){lo, hi, total, 0};
|
||||
U.reg[U.nreg++] = (Reg){lo, hi, total, UINT64_MAX};
|
||||
total += (hi - lo + ALIGN - 1) & ~(uint64_t)(ALIGN - 1);
|
||||
}
|
||||
if (!U.nreg) return;
|
||||
@@ -313,29 +316,38 @@ static int first_above(uint64_t a) {
|
||||
return lo;
|
||||
}
|
||||
|
||||
int vk_usnap_find(uint64_t addr, uint64_t n, uint64_t align, VkDescriptorBufferInfo *out) {
|
||||
int vk_usnap_find(uint64_t addr, uint64_t n, uint64_t at, uint64_t align, VkDescriptorBufferInfo *out) {
|
||||
if (!U.on || !n) return 0;
|
||||
int i = first_above(addr);
|
||||
if (i >= U.nreg) return 0;
|
||||
const Reg *r = &U.reg[i];
|
||||
if (r->dropped || addr < r->lo || addr + n > r->hi) return 0;
|
||||
if (__atomic_load_n(&r->drop_at, __ATOMIC_RELAXED) <= at || addr < r->lo || addr + n > r->hi) return 0;
|
||||
uint64_t off = r->off + (addr - r->lo);
|
||||
if (off % align) return 0;
|
||||
*out = (VkDescriptorBufferInfo){U.arena->buf, off, n};
|
||||
U.hits++;
|
||||
__atomic_fetch_add(&U.hits, 1, __ATOMIC_RELAXED);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void vk_usnap_drop(uint64_t addr, uint64_t n) {
|
||||
static void drop(uint64_t addr, uint64_t n, uint64_t at) {
|
||||
if (!U.mode || !n) return;
|
||||
plat_mutex_lock(&drop_lock);
|
||||
if (U.nwr == U.cwr) U.wr = grow(U.wr, &U.cwr, sizeof *U.wr);
|
||||
U.wr[U.nwr++] = (Wrote){addr, addr + n, U.fence};
|
||||
if (!U.on) return;
|
||||
for (int i = first_above(addr); i < U.nreg && U.reg[i].lo < addr + n; i++)
|
||||
if (!U.reg[i].dropped) U.reg[i].dropped = 1, U.drops++, U.gen++;
|
||||
if (U.on)
|
||||
for (int i = first_above(addr); i < U.nreg && U.reg[i].lo < addr + n; i++)
|
||||
if (at < U.reg[i].drop_at) {
|
||||
if (U.reg[i].drop_at == UINT64_MAX) U.drops++;
|
||||
__atomic_store_n(&U.reg[i].drop_at, at, __ATOMIC_RELAXED);
|
||||
__atomic_add_fetch(&U.gen, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
plat_mutex_unlock(&drop_lock);
|
||||
}
|
||||
|
||||
uint64_t vk_usnap_gen(void) { return U.gen; }
|
||||
void vk_usnap_drop(uint64_t addr, uint64_t n) { drop(addr, n, 0); }
|
||||
void vk_usnap_drop_at(uint64_t addr, uint64_t n, uint32_t at) { drop(addr, n, at); }
|
||||
|
||||
uint64_t vk_usnap_gen(void) { return __atomic_load_n(&U.gen, __ATOMIC_RELAXED); }
|
||||
|
||||
// the host copy where there is one (DLSS), else the device copy
|
||||
static uint8_t *host_copy(void) { return U.arena->cpu ? U.arena->cpu : U.arena->map; }
|
||||
@@ -344,7 +356,7 @@ const uint8_t *vk_usnap_map(void) { return U.arena ? host_copy() : NULL; }
|
||||
|
||||
const uint8_t *vk_usnap_bytes(uint64_t addr, uint64_t n) {
|
||||
VkDescriptorBufferInfo bi;
|
||||
return vk_usnap_find(addr, n, 1, &bi) ? host_copy() + bi.offset : NULL;
|
||||
return vk_usnap_find(addr, n, UINT32_MAX, 1, &bi) ? host_copy() + bi.offset : NULL;
|
||||
}
|
||||
|
||||
// The arenas back on the free list (vk_exec_draw_shutdown: the device is idle, every completion ran).
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
"""The data cheat VM of runtime/cheats.c and the opcode tables of tools/cheats.py, without game files.
|
||||
|
||||
tools/cheats.py: opcode lengths and which opcodes the VM runs (vm_ok), and how a whole cheat is classified.
|
||||
runtime/cheats.c, compiled into a harness with stubs for the rest of the runtime: the same cheats are classified and
|
||||
measured opcode by opcode (op_len must give the lengths tools/cheats.py gives), then run on a fake main image and heap:
|
||||
opcode C0 with every operand type, width and condition, its else and the skip over a failed block (nested, and with
|
||||
operand words that look like 20000000), and C1-C3.
|
||||
|
||||
Run with .venv: python tests/cheats_vm_check.py [--cc <clang.exe>]
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import cheats # noqa: E402
|
||||
|
||||
# Issue #973: two cheats for 1.1.0 whose only opcode the VM lacked was C046B400 FFFFFFFF (if r[B] as u32 != -1).
|
||||
REPORTED = '''
|
||||
[Zonai Devices 98]
|
||||
580F0000 04646C60
|
||||
580F1000 00000138
|
||||
580F1000 000011A8
|
||||
300C0000 0000003C
|
||||
989BF000
|
||||
540B1000 00000000
|
||||
C046B400 FFFFFFFF
|
||||
640F1000 00000000 00000062
|
||||
21000000
|
||||
780F0000 00000008
|
||||
20000000
|
||||
310C0000
|
||||
[All Materials Collected 98]
|
||||
580F0000 04646C60
|
||||
580F1000 00000138
|
||||
580F1000 00000F28
|
||||
300C0000 000001FE
|
||||
989BF000
|
||||
540B1000 00000000
|
||||
C046B400 FFFFFFFF
|
||||
640F1000 00000000 00000062
|
||||
21000000
|
||||
780F0000 00000008
|
||||
20000000
|
||||
310C0000
|
||||
'''
|
||||
|
||||
# (words, kind, note substring): every C0 operand type and width, C1-C3, and what stays unsupported.
|
||||
CLASSIFY = [
|
||||
('C0150010 00000010', 'data', ''), # X=0 memory + offset, 1 byte
|
||||
('C025011B', 'data', ''), # X=1 memory + offset register, 2 bytes
|
||||
('C04512B0 00000010', 'data', ''), # X=2 register + offset
|
||||
('C08513BC', 'data', ''), # X=3 register + offset register
|
||||
('C0151400 00000005', 'data', ''), # X=4 static, 1 byte: one value word
|
||||
('C0851400 00000001 00000002', 'data', ''), # X=4 static, 8 bytes: two value words
|
||||
('C04515B0', 'data', ''), # X=5 other register
|
||||
('C1030A10', 'data', ''),
|
||||
('C2100C00', 'data', ''),
|
||||
('C3000801', 'data', ''),
|
||||
('C0451600', 'unsupported', 'not supported'), # X=6 is no operand type
|
||||
('C0351400 00000005', 'unsupported', 'bad width'),
|
||||
('C4000000', 'unsupported', 'not supported'),
|
||||
('FF000000', 'unsupported', 'not supported'),
|
||||
('FF100000', 'unsupported', 'not supported'),
|
||||
]
|
||||
|
||||
|
||||
class StubMain:
|
||||
"""No code in main: tools/cheats.py code_writes sees no instruction writes."""
|
||||
|
||||
def in_code(self, off):
|
||||
return False
|
||||
|
||||
|
||||
def py_classify(words):
|
||||
c = cheats.Cheat('t', False, 1)
|
||||
c.words = words
|
||||
cheats.classify(StubMain(), c)
|
||||
return c.kind, c.note
|
||||
|
||||
|
||||
def words_of(text):
|
||||
return [int(w, 16) for w in text.split()]
|
||||
|
||||
|
||||
def check_python():
|
||||
for c in cheats.parse(REPORTED, 'mod'):
|
||||
kind, note = py_classify(c.words)
|
||||
assert (kind, note) == ('data', ''), f'tools/cheats.py: {c.name}: {kind} {note}'
|
||||
for text, kind, note in CLASSIFY:
|
||||
got = py_classify(words_of(text))
|
||||
assert got[0] == kind and note in got[1], f'tools/cheats.py: {text}: {got}, want {kind} {note}'
|
||||
if kind == 'data':
|
||||
assert [len(w) for _, w in cheats.ops(words_of(text))] == [len(words_of(text))], text
|
||||
print('cheats_vm_check: tools/cheats.py ok', flush=True)
|
||||
|
||||
|
||||
HARNESS = r'''
|
||||
#include <assert.h>
|
||||
#include "cheats.c"
|
||||
|
||||
// ---- the rest of the runtime, as far as cheats.c reaches it
|
||||
static uint8_t heap[0x1000] __attribute__((aligned(4096)));
|
||||
static uint8_t image[0x1000] __attribute__((aligned(4096)));
|
||||
const char *cfg_get(const char *name) { (void)name; return NULL; }
|
||||
const char *cfg_value(const char *name) { (void)name; return ""; }
|
||||
void cfg_set(const char *name, const char *value) { (void)name; (void)value; }
|
||||
const RtConfig *rt_config(void) { static RtConfig c; return &c; }
|
||||
const RtGuestLayout *mem_layout(void) { static RtGuestLayout l; l.heap.base = (uintptr_t)heap; return &l; }
|
||||
uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
|
||||
memset(out, 0, sizeof *out);
|
||||
if (a < (uintptr_t)heap || a >= (uintptr_t)heap + sizeof heap) return 1;
|
||||
out->base = (uintptr_t)heap, out->size = sizeof heap, out->state = 1, out->perm = 3;
|
||||
return 0;
|
||||
}
|
||||
void rt_fatal(const char *fmt, ...) { (void)fmt; abort(); }
|
||||
void rt_log(const char *fmt, ...) { (void)fmt; }
|
||||
void prof_write(void) {}
|
||||
int ldr_count(void) { return 0; }
|
||||
const RtLoaded *ldr_module(int i) { (void)i; return NULL; }
|
||||
int rt_patch_set(const char *name) { (void)name; return -1; }
|
||||
int rt_patch_enabled(int set) { (void)set; return 0; }
|
||||
void rt_patch_switch(int set, int on) { (void)set; (void)on; }
|
||||
void input_poll(uint64_t *b, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) { *b = 0, *lx = *ly = *rx = *ry = 0; }
|
||||
void mods_scan(void) {}
|
||||
int mods_count(void) { return 0; }
|
||||
ModInfo *mods_at(int i) { (void)i; return NULL; }
|
||||
int mods_exefs_pending(uint32_t *key) { (void)key; return 0; }
|
||||
int plat_exe_path(char *out, size_t cap) { (void)out; (void)cap; return 0; }
|
||||
PlatFile *plat_file_open(const char *path) { (void)path; return NULL; }
|
||||
void plat_file_close(PlatFile *f) { (void)f; }
|
||||
int64_t plat_file_pread(PlatFile *f, void *buf, uint32_t n, uint64_t off) { (void)f; (void)buf; (void)n; (void)off; return -1; }
|
||||
uint8_t *plat_file_read(const char *path, uint64_t *size) { (void)path; (void)size; return NULL; }
|
||||
int plat_file_write(const char *path, const void *data, size_t n) { (void)path; (void)data; (void)n; return 0; }
|
||||
int plat_file_exists(const char *path) { (void)path; return 0; }
|
||||
int plat_fs_unlink(const char *path) { (void)path; return 0; }
|
||||
int plat_fs_list(const char *dir, int (*cb)(const char *, int, uint64_t, void *), void *ctx) { (void)dir; (void)cb; (void)ctx; return 0; }
|
||||
int plat_spawn(const char *const *argv, const char *cwd) { (void)argv; (void)cwd; return 0; }
|
||||
_Noreturn void plat_exit(int code) { exit(code); }
|
||||
int plat_mem_state(uint64_t addr, uint64_t limit, uint64_t *end) { (void)addr; *end = limit; return PLAT_PROT_RW; }
|
||||
int plat_mem_protect(uint64_t addr, uint64_t n, int prot) { (void)addr; (void)n; (void)prot; return 1; }
|
||||
|
||||
#include "cases.h"
|
||||
|
||||
// The last cheat of text, parsed as cheats_init parses a file.
|
||||
static Cheat *load(const char *text) {
|
||||
char *s = strdup(text);
|
||||
parse(s, "t");
|
||||
free(s);
|
||||
return &g_cheats[g_n - 1];
|
||||
}
|
||||
|
||||
static uint32_t u32(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return v; }
|
||||
static uint64_t u64(const uint8_t *p) { uint64_t v; memcpy(&v, p, 8); return v; }
|
||||
|
||||
// Marks after the condition: 1 at heap+0x100 when it held, else 2 at heap+0x104; 3 at heap+0x108 after the end.
|
||||
#define MARKS " 04100000 00000100 00000001 21000000 04100000 00000104 00000002 20000000 04100000 00000108 00000003"
|
||||
|
||||
// Runs setup + cond + MARKS; want: 1 the condition holds, 0 it fails.
|
||||
static void cond(const char *setup, const char *op, int want) {
|
||||
char text[1024];
|
||||
snprintf(text, sizeof text, "[c]\n%s %s%s\n", setup, op, MARKS);
|
||||
memset(heap + 0x100, 0, 0x10);
|
||||
run(load(text), 0);
|
||||
uint32_t held = u32(heap + 0x100), other = u32(heap + 0x104), end = u32(heap + 0x108);
|
||||
if (held != (want ? 1u : 0u) || other != (want ? 0u : 2u) || end != 3) {
|
||||
fprintf(stderr, "%s %s: marks %u %u %u, want %d\n", setup, op, held, other, end, want);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
g_main = (uintptr_t)image, g_main_size = sizeof image, g_code_end = g_main; // no code: nothing is a code cheat
|
||||
for (int k = 0; k < (int)(sizeof cases / sizeof *cases); k++) {
|
||||
Cheat *c = load(cases[k].text);
|
||||
CheatKind kind = classify(c);
|
||||
if (kind != cases[k].kind || !strstr(c->note, cases[k].note)) {
|
||||
fprintf(stderr, "classify %s: %d (%s), want %d (%s)\n", cases[k].text, kind, c->note, cases[k].kind, cases[k].note);
|
||||
abort();
|
||||
}
|
||||
for (int i = 0, j = 0; kind == CHEAT_DATA && i < c->n; j++) {
|
||||
int len = op_len(c->words + i, c->n - i);
|
||||
if (len != cases[k].lens[j]) {
|
||||
fprintf(stderr, "op_len %s, opcode %d: %d, tools/cheats.py %d\n", cases[k].text, j, len, cases[k].lens[j]);
|
||||
abort();
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
char hb[64], hb10[64]; // r[B] = the heap's address, r[B] = the heap + 0x10
|
||||
snprintf(hb, sizeof hb, "400B0000 %08X %08X", (unsigned)((uintptr_t)heap >> 32), (unsigned)(uintptr_t)heap);
|
||||
snprintf(hb10, sizeof hb10, "400B0000 %08X %08X", (unsigned)((uintptr_t)heap >> 32), (unsigned)((uintptr_t)heap + 0x10));
|
||||
uint64_t v = 0x1122334455667705ull;
|
||||
memcpy(heap + 0x10, &v, 8); // heap+0x10: 05 77 66 55 44 33 22 11
|
||||
memcpy(image + 0x20, &v, 8);
|
||||
|
||||
// X=0 memory + offset: heap (M=1) and main (M=0), each width
|
||||
cond("400A0000 00000000 00000005", "C015A010 00000010", 1);
|
||||
cond("400A0000 00000000 00000005", "C016A010 00000010", 0);
|
||||
cond("400A0000 00000000 00007705", "C025A010 00000010", 1);
|
||||
cond("400A0000 00000000 55667705", "C045A000 00000020", 1);
|
||||
cond("400A0000 11223344 55667705", "C085A000 00000020", 1);
|
||||
cond("400A0000 11223344 55667706", "C085A000 00000020", 0);
|
||||
// X=1 memory + offset register
|
||||
cond("400A0000 00000000 55667705 400C0000 00000000 00000010", "C045A11C", 1);
|
||||
cond("400A0000 00000000 55667705 400C0000 00000000 00000014", "C045A11C", 0);
|
||||
// X=2 register + offset, X=3 register + offset register
|
||||
char setup[256];
|
||||
snprintf(setup, sizeof setup, "400A0000 00000000 55667705 %s", hb);
|
||||
cond(setup, "C045A2B0 00000010", 1);
|
||||
cond(setup, "C045A2B0 00000011", 0);
|
||||
snprintf(setup, sizeof setup, "400A0000 00000000 55667705 %s 400C0000 00000000 00000010", hb);
|
||||
cond(setup, "C045A3BC", 1);
|
||||
snprintf(setup, sizeof setup, "400A0000 00000000 00000005 %s 400C0000 00000000 00000010", hb10);
|
||||
cond(setup, "C015A3BC", 0); // heap+0x20 holds 0
|
||||
// an address the guest cannot read compares as 0, as dmnt's read leaves its zeroed value
|
||||
cond("400A0000 00000000 00000000 400B0000 00000000 00000000", "C045A2B0 00000000", 1);
|
||||
cond("400A0000 00000000 00000001 400B0000 00000000 00000000", "C045A2B0 00000000", 0);
|
||||
// X=4 static: the register and the value cut to the width
|
||||
cond("400A0000 FFFFFFFF FFFFFF05", "C015A400 00000005", 1);
|
||||
cond("400A0000 FFFFFFFF FFFFFF05", "C015A400 12345605", 1);
|
||||
cond("400A0000 FFFFFFFF FFFF7705", "C025A400 00007705", 1);
|
||||
cond("400A0000 FFFFFFFF 55667705", "C045A400 55667705", 1);
|
||||
cond("400A0000 00000001 00000002", "C085A400 00000001 00000002", 1);
|
||||
cond("400A0000 00000001 00000002", "C085A400 00000000 00000002", 0);
|
||||
// X=5 other register, cut to the width
|
||||
cond("400A0000 00000000 00000005 400B0000 00000001 00000005", "C045A5B0", 1);
|
||||
cond("400A0000 00000000 00000005 400B0000 00000001 00000005", "C085A5B0", 0);
|
||||
// the six conditions, register 5 against 4, 5 and 6; condition 0 or 7 never holds
|
||||
static const int want[8][3] = {{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 0, 1}, {0, 1, 1}, {0, 1, 0}, {1, 0, 1}, {0, 0, 0}};
|
||||
for (int c = 0; c < 8; c++)
|
||||
for (int k = 0; k < 3; k++) {
|
||||
char op[32];
|
||||
snprintf(op, sizeof op, "C04%XA400 %08X", c, 4 + k);
|
||||
cond("400A0000 00000000 00000005", op, want[c][k]);
|
||||
}
|
||||
// a failed block skips a nested C0 with its else and end, and C0 operand words that read 20000000 / 21000000
|
||||
cond("400A0000 00000000 00000005",
|
||||
"C046A400 00000005 C045A400 00000005 04100000 00000100 00000009 21000000 20000000"
|
||||
" C085A400 21000000 20000000 20000000 C045A010 20000000 20000000 C045A2B0 20000000 20000000", 0);
|
||||
cond("400A0000 00000000 00000005", "C045A400 00000005 C046A400 00000005 21000000 20000000", 1);
|
||||
|
||||
// C1: save r[A] in slot 3, clear r[A], restore slot 3 into r[B]; clear slot 3 and restore it into r[C]
|
||||
load("[s]\n400A0000 00000000 0000002A C1030A10 C10A0030 C10B0300 C1030020 C10C0300"
|
||||
" A4A00410 00000110 A4B00410 00000114 A4C00410 00000118\n");
|
||||
memcpy(heap + 0x110, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", 12);
|
||||
run(&g_cheats[g_n - 1], 0);
|
||||
assert(u32(heap + 0x110) == 0 && u32(heap + 0x114) == 0x2A && u32(heap + 0x118) == 0);
|
||||
// C2: save A and B, clear A, restore A only; clear the saved B, restore B
|
||||
load("[m]\n400A0000 00000000 00000007 400B0000 00000000 00000008 C2100C00 C2300C00 C2000400 C2200800 C2000800"
|
||||
" A4A00410 00000110 A4B00410 00000114\n");
|
||||
memset(heap + 0x110, 0xFF, 8);
|
||||
run(&g_cheats[g_n - 1], 0);
|
||||
assert(u32(heap + 0x110) == 7 && u32(heap + 0x114) == 0);
|
||||
// C3: r[1] into static register 0x80, which stays across runs; static register 0x05 (none sets it) into r[2]
|
||||
load("[st]\n40010000 00000000 00000033 40020000 00000000 00000044 C3000801 C3000052 A8200410 00000120\n");
|
||||
memset(heap + 0x120, 0xFF, 8);
|
||||
run(&g_cheats[g_n - 1], 0);
|
||||
assert(g_static[0x80] == 0x33 && u64(heap + 0x120) == 0);
|
||||
// saved registers start at 0 in each run, like the registers
|
||||
load("[z]\nC10A0300 A8A00410 00000128\n");
|
||||
memset(heap + 0x128, 0xFF, 8);
|
||||
run(&g_cheats[g_n - 1], 0);
|
||||
assert(u64(heap + 0x128) == 0);
|
||||
|
||||
puts("cheats_vm_check: runtime/cheats.c ok");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
KIND = {'data': 'CHEAT_DATA', 'unsupported': 'CHEAT_UNSUPPORTED'}
|
||||
|
||||
|
||||
def cases_h():
|
||||
"""The C table: every classify case and each reported cheat, with tools/cheats.py's kind and opcode lengths."""
|
||||
rows = [(' '.join(f'{w:08X}' for w in c.words), 'data', '') for c in cheats.parse(REPORTED, 'mod')] + CLASSIFY
|
||||
out = ['static const struct { const char *text; CheatKind kind; const char *note; int lens[64]; } cases[] = {']
|
||||
for text, kind, note in rows:
|
||||
try:
|
||||
lens = [len(w) for _, w in cheats.ops(words_of(text))]
|
||||
except ValueError:
|
||||
lens = []
|
||||
out.append(f' {{"[t]\\n{text}\\n", {KIND[kind]}, "{note}", {{{", ".join(map(str, lens)) or "0"}}}}},')
|
||||
return '\n'.join(out + ['};', ''])
|
||||
|
||||
|
||||
def check_c(cc):
|
||||
with tempfile.TemporaryDirectory(prefix='totk-cheats-') as tmp:
|
||||
folder = Path(tmp)
|
||||
(folder / 'check.c').write_text(HARNESS)
|
||||
(folder / 'cases.h').write_text(cases_h())
|
||||
exe = folder / ('check.exe' if os.name == 'nt' else 'check')
|
||||
subprocess.run([cc, '-O1', '-g', '-Wall', '-Wextra', '-Wno-unused-function', '-I', str(folder),
|
||||
'-I', str(ROOT / 'runtime'), str(folder / 'check.c'), '-o', str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--cc', default='clang')
|
||||
args = ap.parse_args()
|
||||
check_python()
|
||||
check_c(args.cc)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -489,6 +489,29 @@ class CleanupTest(unittest.TestCase):
|
||||
self.assertEqual(self.run_cleanup(), 'cleanup.romfs_unreadable')
|
||||
|
||||
|
||||
class FreeSpaceTest(unittest.TestCase):
|
||||
def test_missing_folders_use_the_nearest_existing_parent(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
parent = Path(d)
|
||||
path = parent / 'missing' / 'install'
|
||||
with mock.patch.object(steps.shutil, 'disk_usage', return_value=SimpleNamespace(free=1234)) as usage:
|
||||
self.assertEqual(steps.free_bytes(path), (1234, parent.stat().st_dev))
|
||||
usage.assert_called_once_with(parent)
|
||||
self.assertFalse(path.parent.exists())
|
||||
|
||||
def test_missing_root_reports_the_filesystem_error(self):
|
||||
path = Path(ROOT.anchor) / 'missing' / 'install'
|
||||
root = Path(path.anchor)
|
||||
error = FileNotFoundError(2, 'No such file or directory', str(root))
|
||||
# Bound the failed existence checks so the old parent walk cannot hang the test suite.
|
||||
with mock.patch.object(Path, 'exists', side_effect=[False] * (len(path.parents) + 1)):
|
||||
with mock.patch.object(steps.shutil, 'disk_usage', side_effect=error) as usage:
|
||||
with self.assertRaises(FileNotFoundError) as raised:
|
||||
steps.free_bytes(path)
|
||||
self.assertIs(raised.exception, error)
|
||||
usage.assert_called_once_with(root)
|
||||
|
||||
|
||||
class BaseGameCheckTest(unittest.TestCase):
|
||||
"""Issue #261: an update NSP chosen as the base game was reported as "not Tears of the Kingdom"."""
|
||||
BASE, UPDATE = '0100F2C0115B6000', '0100F2C0115B6800'
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""The AppImage entry must ignore the user's site-packages (issue 985).
|
||||
|
||||
AppRun starts the bundled Python with `-m installer.gui`. Without PYTHONNOUSERSITE, a PyPI package named `installer`
|
||||
in the user site shadows the project's own installer package and the window fails with "No module named installer.gui".
|
||||
|
||||
The test reads the export lines from AppRun, then runs Python with that environment against a fake user site holding
|
||||
a shadowing `installer` package (no `gui`) and a fake bundle holding the real layout. The bundle is added the way
|
||||
totk-release.pth adds it, after the user site, so the user site wins unless AppRun turns it off.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
|
||||
import make_release_linux # noqa: E402
|
||||
|
||||
line = 'export PYTHONNOUSERSITE=1'
|
||||
lines = make_release_linux.APPRUN.splitlines()
|
||||
|
||||
# Environment from AppRun's export lines. Values that refer to $HERE point into the mount and are not needed here.
|
||||
env = dict(os.environ)
|
||||
env.pop('PYTHONNOUSERSITE', None)
|
||||
for text in lines:
|
||||
if text.startswith('export ') and '=' in text and '$' not in text:
|
||||
name, value = text[len('export '):].split('=', 1)
|
||||
env[name] = value
|
||||
env.pop('PYTHONPATH', None)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
user_base = tmp / 'userbase'
|
||||
env['PYTHONUSERBASE'] = str(user_base)
|
||||
user_site = Path(subprocess.run(
|
||||
[sys.executable, '-c', 'import site; print(site.getusersitepackages())'],
|
||||
env=env, capture_output=True, text=True, check=True, cwd=tmp).stdout.strip())
|
||||
shadow = user_site / 'installer'
|
||||
shadow.mkdir(parents=True)
|
||||
(shadow / '__init__.py').write_text('# stands in for the PyPI "installer" package: no gui submodule\n')
|
||||
|
||||
bundle = tmp / 'bundle'
|
||||
(bundle / 'installer').mkdir(parents=True)
|
||||
(bundle / 'installer' / '__init__.py').write_text('')
|
||||
(bundle / 'installer' / 'gui.py').write_text('print("gui imported")\n')
|
||||
|
||||
code = 'import site, sys; site.addsitedir(sys.argv[1]); import installer.gui'
|
||||
result = subprocess.run([sys.executable, '-c', code, str(bundle)],
|
||||
env=env, capture_output=True, text=True, cwd=tmp)
|
||||
assert result.returncode == 0, f'installer.gui not importable with AppRun env: {result.stderr.strip()}'
|
||||
|
||||
# Checked last so the import behaviour is what fails on the base commit; this line guards the mechanism.
|
||||
assert line in lines, f'AppRun does not set {line!r}; AppRun lines: {lines}'
|
||||
|
||||
print('AppRun sets PYTHONNOUSERSITE=1 and bundled installer.gui imports past a user-site shadow')
|
||||
@@ -13,6 +13,7 @@ import importlib.util
|
||||
import io
|
||||
import json
|
||||
import random
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -299,7 +300,7 @@ LETTERS_2 = {ord('T'): (560, polygon((230, 0), (330, 0), (330, 600), (520, 600),
|
||||
# The other characters the PC prompts' keys show (button_icons.PC_LABELS of the defaults): a block with a counter, each
|
||||
# its own size.
|
||||
LETTERS_2.update({ord(ch): (560, lambda pen, i=i: [rect(pen, 60, 0, 300 + 10 * i, 700), rect(pen, 120, 100, 240, 600, True)])
|
||||
for i, ch in enumerate('ACDEFMQSW1234⇧G')})
|
||||
for i, ch in enumerate('ACDEFGHIMQSW1234')})
|
||||
PC_PANES = {'A': 'E', 'B': '↑', 'X': '▬', 'Y': 'L'} # the diamond's letters with the default keys
|
||||
|
||||
|
||||
@@ -572,6 +573,34 @@ class PromptsTest(unittest.TestCase):
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertEqual((out / 'manifest.tsv').read_bytes(), (self.outs['v121', 'xbox'][0] / 'manifest.tsv').read_bytes())
|
||||
|
||||
def test_reuse_links_the_files_that_match_and_writes_the_same_overlay(self):
|
||||
romfs = self.romfs['v143']
|
||||
pc, pc_summary = self.outs['v143', 'pc']
|
||||
source = self.base / 'reuse' / 'source' # a copy, so the class's folder is never linked or rewritten
|
||||
shutil.copytree(pc, source)
|
||||
out = self.base / 'reuse' / 'pc'
|
||||
proc = romfs.run(out, '--prompts', 'pc', '--reuse', str(source))
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertEqual((out / 'manifest.tsv').read_bytes(), (pc / 'manifest.tsv').read_bytes())
|
||||
self.assertEqual(sorted(json.loads(proc.stdout.strip().splitlines()[-1])['reused']), sorted(manifest(pc)))
|
||||
self.assertEqual(pc_summary['reused'], [])
|
||||
# From the Xbox prompts only what both share is taken; the rest is made, and the overlay is the same.
|
||||
xbox = self.outs['v143', 'xbox'][0]
|
||||
out = self.base / 'reuse' / 'from-xbox'
|
||||
proc = romfs.run(out, '--prompts', 'pc', '--reuse', str(xbox))
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertEqual((out / 'manifest.tsv').read_bytes(), (pc / 'manifest.tsv').read_bytes())
|
||||
shared = {p for p, row in manifest(pc).items() if manifest(xbox).get(p) == row}
|
||||
self.assertEqual(set(json.loads(proc.stdout.strip().splitlines()[-1])['reused']), shared)
|
||||
# A new run into the source folder replaces its files instead of writing through the links.
|
||||
before = {p: (self.base / 'reuse' / 'pc' / p).read_bytes() for p in manifest(pc)}
|
||||
ini = self.base / 'reuse' / 'totk.ini'
|
||||
ini.write_text('[input]\nbind_A = key:F\n', encoding='utf-8')
|
||||
proc = romfs.run(source, '--prompts', 'pc', '--binds', str(ini))
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertNotEqual((source / 'manifest.tsv').read_bytes(), (pc / 'manifest.tsv').read_bytes())
|
||||
self.assertEqual({p: (self.base / 'reuse' / 'pc' / p).read_bytes() for p in manifest(pc)}, before)
|
||||
|
||||
def test_unrecognised_romfs_fails_before_writing(self):
|
||||
cases = {'no text fonts': (dict(text_fonts=False), icons.TEXT_FONTS[0]),
|
||||
'no size entry': (dict(restbl_skips=(REGIONAL_FONT,)), 'has no entry in the resource size table'),
|
||||
|
||||
+5
-4
@@ -46,8 +46,9 @@ of its processes (Windows job object accounting: committed private bytes).
|
||||
--ninja PATH names the Ninja to run; without MSYS2 the bin/ninja.exe of the release folder (tools/make_release.py) is used.
|
||||
--toolchain DIR builds with an llvm-mingw tree (DIR/bin/clang.exe) instead of MSYS2 UCRT64 clang. Ninja still comes
|
||||
from MSYS2. The libraries and headers MSYS2 supplies (Vulkan, SDL3, Opus, libvpx, VMA) then come from --deps DIR
|
||||
(default toolchain/deps-mingw: include/, lib/ with the import libraries and libvpx.a, bin/ with the DLLs), and the C++
|
||||
runtime is libc++ from llvm-mingw. Such a build never reuses another build's generated objects.
|
||||
(default toolchain/deps-mingw: include/, lib/ with the import libraries and libvpx.a, bin/ with the DLLs copied next to
|
||||
totk.exe: SDL3.dll, libopus-0.dll and, when SDL3.dll comes from MSYS2, the libiconv-2.dll it imports, without which
|
||||
the exe stops at start with 0xc000007b or a missing-DLL error), and the C++ runtime is libc++ from llvm-mingw. Such a build never reuses another build's generated objects.
|
||||
|
||||
Split build. --make-runtime-lib compiles only runtime/ into <out>/libtotkrt.a (what CI prebuilds). --runtime-lib PATH
|
||||
then compiles only the generated C and the install-time files (modules.c and INSTALL_SIDE, which bake the player's
|
||||
@@ -274,8 +275,8 @@ def copy_romfs_patches(version, out):
|
||||
menus' captured background turned black). Copies every romfs-patches* folder of build/<version> that has a
|
||||
manifest, the same way when it differs: romfs-patches, romfs-patches-xbox, romfs-patches-xbox-label and
|
||||
romfs-patches-pc (the same with Xbox or keyboard and mouse button prompts, make_romfs_patches.py --prompts) and
|
||||
the ones of another shape, romfs-patches-<w>x<h> and romfs-patches-pc-<w>x<h> (make_romfs_patches.py --aspect),
|
||||
which runtime/romfs.c overlay_init needs for a game resolution that is not 16:9."""
|
||||
the ones of another shape, romfs-patches-<w>x<h> and romfs-patches-<style>-<w>x<h> (make_romfs_patches.py
|
||||
--aspect), which runtime/romfs.c overlay_init needs for a game resolution that is not 16:9."""
|
||||
names = sorted(p.name for p in (ROOT / 'build' / version).glob('romfs-patches*') if p.is_dir())
|
||||
for name in names:
|
||||
src = ROOT / 'build' / version / name
|
||||
|
||||
@@ -195,7 +195,7 @@ PC_BINDS = {
|
||||
}
|
||||
# What a key shows: its letter, a symbol or a short name (key names as in totk.ini; the rest show their name).
|
||||
PC_LABELS = {
|
||||
'lshift': '⇧', 'rshift': '⇧', 'lctrl': 'CTRL', 'rctrl': 'CTRL', 'lalt': 'ALT', 'tab': 'TAB',
|
||||
'lshift': 'SHIFT', 'rshift': 'SHIFT', 'lctrl': 'CTRL', 'rctrl': 'CTRL', 'lalt': 'ALT', 'tab': 'TAB',
|
||||
'enter': 'ENT', 'backspace': 'BKSP', 'capslock': 'CAPS', 'insert': 'INS', 'delete': 'DEL', 'home': 'HOME',
|
||||
'end': 'END', 'pageup': 'PGUP', 'pagedown': 'PGDN', 'up': '↑', 'down': '↓', 'left': '←',
|
||||
'right': '→', 'num_add': '+', 'num_sub': '-', 'num_mul': '*', 'num_div': '/', 'num_decimal': '.',
|
||||
@@ -284,6 +284,13 @@ def pc_icons(g, adv, binds=PC_BINDS):
|
||||
polygon(ReverseContourPen(pen), [(bx0, by0), (bx1, by0), (bx1, by1), (bx1 - t, by1), (bx1 - t, by0 + t),
|
||||
(bx0 + t, by0 + t), (bx0 + t, by1), (bx0, by1)])
|
||||
return
|
||||
if key in ('lshift', 'rshift'): # a solid arrow and the name under it: the arrow alone read as a direction
|
||||
cx, ab, at, hw, sw = (x0 + x1) / 2, y0 + h * 0.46, y0 + h * 0.86, w * 0.20, w * 0.09
|
||||
neck = ab + h * 0.18
|
||||
polygon(ReverseContourPen(pen), [(cx - sw, ab), (cx + sw, ab), (cx + sw, neck), (cx + hw, neck), (cx, at),
|
||||
(cx - hw, neck), (cx - sw, neck)])
|
||||
g.hole(pen, 'SHIFT', cx, y0 + h * 0.25, h * 0.21, w * 0.84)
|
||||
return
|
||||
label = PC_LABELS.get(key, key.upper())
|
||||
cap = h * (0.52 if len(label) == 1 else 0.36 if len(label) == 2 else 0.26)
|
||||
g.hole(pen, label, (x0 + x1) / 2, (y0 + y1) / 2, cap, w * 0.74)
|
||||
|
||||
+7
-2
@@ -134,6 +134,9 @@ def ops(words):
|
||||
else:
|
||||
t = (w >> 24) & 0xFF if t == 0xC else (w >> 20) & 0xFFF
|
||||
n = 1
|
||||
if t == 0xC0: # C0TcSX..: an address word for operand types 0 and 2, the value (two at width 8) for 4
|
||||
x = (w >> 8) & 0xF
|
||||
n = 2 if x in (0, 2) else (3 if (w >> 20) & 0xF == 8 else 2) if x == 4 else 1
|
||||
if i + n > len(words):
|
||||
raise ValueError(f'opcode {w:08X} is cut short')
|
||||
out.append((t, words[i:i + n]))
|
||||
@@ -141,7 +144,7 @@ def ops(words):
|
||||
return out
|
||||
|
||||
|
||||
VM_TYPES = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA}
|
||||
VM_TYPES = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xC0, 0xC1, 0xC2, 0xC3}
|
||||
|
||||
|
||||
def vm_ok(o):
|
||||
@@ -151,10 +154,12 @@ def vm_ok(o):
|
||||
return f'opcode {w[0]:08X} is not supported'
|
||||
if t == 5 and (w[0] >> 12) & 0xF > 1:
|
||||
return f'opcode {w[0]:08X} (load from a register offset) is not supported'
|
||||
if t == 0xA and (w[0] >> 8) & 0xF > 5:
|
||||
if t in (0xA, 0xC0) and (w[0] >> 8) & 0xF > 5:
|
||||
return f'opcode {w[0]:08X} is not supported'
|
||||
if t in (0, 1, 5, 6, 7, 9, 0xA) and (w[0] >> 24) & 0xF not in (1, 2, 4, 8):
|
||||
return f'opcode {w[0]:08X} has a bad width'
|
||||
if t == 0xC0 and (w[0] >> 20) & 0xF not in (1, 2, 4, 8):
|
||||
return f'opcode {w[0]:08X} has a bad width'
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ t_pin, t_pout, t_struct, t_pu, t_pf_u = a.id(), a.id(), a.id(), a.id(), a.id()
|
||||
v_prev, v_frag, v_out, v_p = a.id(), a.id(), a.id(), a.id()
|
||||
c_half, c_zero, c_eps, c_u0, c_vzero = a.id(), a.id(), a.id(), a.id(), a.id()
|
||||
t_pv4u = a.id()
|
||||
t_bv4 = a.id()
|
||||
a.op('entry', OpEntryPoint, 4, main, *string('main'), v_prev, v_frag, v_out) # Fragment
|
||||
a.op('exec', OpExecutionMode, main, 7) # OriginUpperLeft
|
||||
a.loc_ins = a.op('dec', OpDecorate, v_prev, Location, 0)
|
||||
@@ -96,6 +97,7 @@ a.op('types', OpTypeVector, t_v2, t_f, 2)
|
||||
a.op('types', OpTypeVector, t_v4, t_f, 4)
|
||||
a.op('types', OpTypeInt, t_u, 32, 0)
|
||||
a.op('types', OpTypeBool, t_bool)
|
||||
a.op('types', OpTypeVector, t_bv4, t_bool, 4)
|
||||
a.op('types', OpTypePointer, t_pin, Input, t_v4)
|
||||
a.op('types', OpTypePointer, t_pout, Output, t_v4)
|
||||
a.op('types', OpTypeStruct, t_struct, t_v4)
|
||||
@@ -149,7 +151,10 @@ a.op(F, OpFSub, t_f, mx, x3, wx)
|
||||
a.op(F, OpFSub, t_f, my, y3, wy)
|
||||
mv = a.id(); a.op(F, OpCompositeConstruct, t_v4, mv, mx, my, c_zero, c_zero)
|
||||
ok = a.id(); a.op(F, OpFOrdGreaterThan, t_bool, ok, pw, c_eps)
|
||||
res = a.id(); a.op(F, OpSelect, t_v4, res, ok, mv, c_vzero)
|
||||
# SPIR-V 1.0 (the module's version): OpSelect of a vector takes a condition vector of the same size; a scalar
|
||||
# condition is allowed only from 1.4 on, and spirv-val (the validation layer) rejects the module otherwise.
|
||||
okv = a.id(); a.op(F, OpCompositeConstruct, t_bv4, okv, ok, ok, ok, ok)
|
||||
res = a.id(); a.op(F, OpSelect, t_v4, res, okv, mv, c_vzero)
|
||||
a.op(F, OpStore, v_out, res)
|
||||
a.op(F, OpReturn)
|
||||
a.op(F, OpFunctionEnd)
|
||||
|
||||
+18
-7
@@ -54,6 +54,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import build_game # noqa: E402
|
||||
import build_xlate # noqa: E402
|
||||
import check_content # noqa: E402
|
||||
import pgo # noqa: E402
|
||||
import shader_list # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -63,7 +64,7 @@ WORK = MAIN_ROOT / 'toolchain' / 'release-work' # unpacked downloads
|
||||
PGO_DIR = MAIN_ROOT / 'build' / 'pgo' # pgo-<version>.profdata + .json (tools/pgo.py merge)
|
||||
|
||||
MSYS2 = 'https://repo.msys2.org/mingw/ucrt64/'
|
||||
SDL_VERSION = '3.4.18'
|
||||
SDL_VERSION = '3.4.20'
|
||||
LLVM_VERSION = '20261020'
|
||||
MNG_COMMIT = 'b75be8cfa704df3701e86130c8ced268e79d6b11' # MacNativeGames, the shader list (refs/MacNativeGames)
|
||||
# name: (url, sha256)
|
||||
@@ -76,9 +77,9 @@ PINS = {
|
||||
f'llvm-mingw-{LLVM_VERSION}-ucrt-x86_64.zip',
|
||||
'fd5462b7f5d93b2ff981fcd3f9c7e427e47f3203c64ea91c27bcc444eebd74cd'),
|
||||
'sdl3-dll': (f'https://github.com/libsdl-org/SDL/releases/download/release-{SDL_VERSION}/SDL3-{SDL_VERSION}-win32-x64.zip',
|
||||
'75c2c0fc74e7d1206aaedc22893a35887332f4f85f5c8b74fddc10d177985f2c'),
|
||||
'368bed8b24e44ad9ea372990814f2343fcfb9e468abb7f8f4a3bc146c7304752'),
|
||||
'sdl3-devel': (f'https://github.com/libsdl-org/SDL/releases/download/release-{SDL_VERSION}/SDL3-devel-{SDL_VERSION}-mingw.tar.gz',
|
||||
'049b9dd711e3fb3f8d84feb998dfec54a75e17cf539c4a80ff4d058d839771cf'),
|
||||
'd2d3bf3fb97a90b22e908a306d7362631ef1bab229d988fe6669595dc29cd9ed'),
|
||||
'ninja': ('https://github.com/ninja-build/ninja/releases/download/v1.13.2/ninja-win.zip',
|
||||
'07fc8261b42b20e71d1720b39068c2e14ffcee6396b76fb7a795fb460b78dc65'),
|
||||
'opus': (MSYS2 + 'mingw-w64-ucrt-x86_64-opus-1.6.1-2-any.pkg.tar.zst',
|
||||
@@ -98,7 +99,7 @@ PINS = {
|
||||
}
|
||||
# The component MSI inside the python.org installer that carries Tcl/Tk and tkinter (Burn names its payloads a0..a27).
|
||||
TCLTK_PAYLOAD = ('a12', 'ac87609db3f715404dd41a989c2f1706ea1512f3d86217f938d11c63af8eb505')
|
||||
SDL3_DLL_SHA256 = '2b4b128d8d510cc2d442693cf848c050d700d43dc667afe9e5fd82b0570d667e' # SDL3.dll inside the SDL zip
|
||||
SDL3_DLL_SHA256 = '66844f10251cc1c10619ed5d8e4e3224f564900ee6d69ba87d63de782addae40' # SDL3.dll inside the SDL zip
|
||||
|
||||
# llvm-mingw: what clang and lld need to compile and link totk.exe for x86_64-w64-mingw32 (dynamic LLVM, libc++, compiler-rt).
|
||||
LLVM_BIN = {'clang.exe', 'clang++.exe', 'clang-23.exe', 'ld.lld.exe', 'llvm-ar.exe', 'llvm-ranlib.exe', 'llvm-nm.exe',
|
||||
@@ -385,11 +386,13 @@ def lifted_version():
|
||||
|
||||
|
||||
def profiles():
|
||||
"""{version tag: .profdata} of build/pgo (tools/pgo.py merge). A profile trained on another recomp/ than the one
|
||||
shipped still works but misses the functions whose generated C changed since: warn, so the next release retrains."""
|
||||
"""{version tag: .profdata} of build/pgo (tools/pgo.py merge). A profile trained on another recomp/, or with other
|
||||
runtime headers inlined into the generated C, than the ones shipped still works but misses the functions whose
|
||||
code changed since: warn, so the next release retrains."""
|
||||
tree = subprocess.run(['git', '-C', str(ROOT), 'rev-parse', 'HEAD:recomp'], capture_output=True, text=True).stdout.strip()
|
||||
dirty = subprocess.run(['git', '-C', str(ROOT), 'status', '--porcelain', '--', 'recomp'], capture_output=True,
|
||||
text=True).stdout.strip()
|
||||
retrain = 'build without a profile; retrain (docs/pgo.md)'
|
||||
found = {}
|
||||
for p in sorted(PGO_DIR.glob('pgo-*.profdata')):
|
||||
rec = p.with_suffix('.json')
|
||||
@@ -397,7 +400,15 @@ def profiles():
|
||||
if record.get('recomp_tree') != tree or dirty:
|
||||
print(f'make_release: warning: {p.name} was trained on recomp tree {record.get("recomp_tree", "unknown")[:12]}, '
|
||||
f'this is {tree[:12]}{" with local changes" if dirty else ""}: functions whose generated C changed '
|
||||
'build without a profile; retrain (docs/building.md, "Profile-guided build")', file=sys.stderr)
|
||||
f'{retrain}', file=sys.stderr)
|
||||
stale = pgo.stale_headers(record, ROOT)
|
||||
if stale is None:
|
||||
print(f'make_release: warning: {p.name} has no runtime header digests (merged before they were recorded): '
|
||||
'a change to a header the generated C includes would go unnoticed; re-merge or retrain',
|
||||
file=sys.stderr)
|
||||
elif stale:
|
||||
print(f'make_release: warning: {p.name} was trained with other {", ".join(stale)}: every lifted function '
|
||||
f'that inlines them may {retrain}', file=sys.stderr)
|
||||
found[p.stem.removeprefix('pgo-')] = p
|
||||
if not found:
|
||||
print(f'make_release: no execution profile in {PGO_DIR}: installs build the game at -O1', file=sys.stderr)
|
||||
|
||||
@@ -426,6 +426,8 @@ APPRUN = '''#!/bin/sh
|
||||
# The AppImage entry: the setup window, run by the bundled Python from the read-only mount (no bytecode written there).
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
# Ignore ~/.local/lib/pythonX.Y/site-packages: a user-installed package such as PyPI's `installer` would shadow ours
|
||||
export PYTHONNOUSERSITE=1
|
||||
export TCL_LIBRARY="$HERE/python/lib/tcl9.0"
|
||||
export TK_LIBRARY="$HERE/python/lib/tk9.0"
|
||||
exec "$HERE/python/bin/python3" -m installer.gui "$@"
|
||||
|
||||
@@ -38,13 +38,18 @@ from the game's own files on the player's machine, nothing added (tools/button_i
|
||||
every pad not labelled the Nintendo way), so the face buttons keep their letters. The action prompt's diamond is
|
||||
laid out as an Xbox pad's (bflyt_xbox_diamond): A at the bottom, B on the right, X on the left, Y at the top, the
|
||||
button the prompt names lit in its place with its bar.
|
||||
--xbox-prompts is the older name of --prompts xbox.
|
||||
--xbox-prompts is the older name of --prompts xbox. With --aspect, romfs-patches-xbox-<w>x<h> or
|
||||
romfs-patches-xbox-label-<w>x<h>: the same prompts with that shape's UI layout.
|
||||
--prompts pc: keyboard and mouse prompts, for prompts = pc (romfs-patches-pc, or romfs-patches-pc-<w>x<h> with
|
||||
--aspect): every button of the button font is drawn as the key or mouse button the keyboard defaults put on it
|
||||
(button_icons.pc_icons; --binds <totk.ini> takes the first key of its bind_ lines instead), and so are the button
|
||||
guide's images (A, B, X, Y, L, ZL and the stick's two). The one-letter panes around the action prompt name the keys
|
||||
with one character of the Normal font (E, an arrow for Shift, a bar for Space, L and R for the mouse buttons).
|
||||
|
||||
--reuse <folder>: an overlay made before, such as romfs-patches-pc for romfs-patches-pc-<w>x<h>. Each file of it whose
|
||||
contents match the new one is hard-linked (copied where links fail) instead of compressed again; the summary's reused
|
||||
lists them. The output is byte for byte the same as without it.
|
||||
|
||||
Input: build/extracted/<version>/romfs (romfs.bin and index.json, verified by tools/extract.py). Output:
|
||||
build/<version>/romfs-patches/<path> by default (romfs-patches-<style> with --prompts), and <out>/manifest.tsv (path, size, SHA-256 of the overlay, SHA-256 of the original). Run with .venv-py314.
|
||||
"""
|
||||
@@ -53,6 +58,7 @@ import compression.zstd as zstd
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
from pathlib import Path
|
||||
@@ -70,6 +76,8 @@ ap.add_argument('--prompts', choices=('xbox', 'xbox-label', 'pc'),
|
||||
'pc for the keyboard and mouse')
|
||||
ap.add_argument('--binds', type=Path, help='--prompts pc: a totk.ini whose bind_ lines the key icons follow')
|
||||
ap.add_argument('--xbox-prompts', dest='prompts', action='store_const', const='xbox', help='the same as --prompts xbox')
|
||||
ap.add_argument('--reuse', type=Path,
|
||||
help='an overlay folder made before: its files with the same contents are linked, not compressed again')
|
||||
args = ap.parse_args()
|
||||
args.romfs = args.romfs or root / 'build/extracted' / args.version / 'romfs'
|
||||
args.out = args.out or root / 'build' / args.version / (f'romfs-patches-{args.prompts}' if args.prompts else 'romfs-patches')
|
||||
@@ -442,19 +450,46 @@ if args.prompts:
|
||||
prompts = dict(style=args.prompts, textures=KEY_TEXTURES if by_position else [], letters=letters,
|
||||
diamond=len(diamond), messages=messages, hud=hud, icons=len(drawn), fonts=FONTS, grown=grown)
|
||||
|
||||
lines = []
|
||||
def reusable(path, contents, dictionary):
|
||||
"""The --reuse folder's copy of path when it holds these contents under the same dictionary, else None. The level 19
|
||||
compression is most of a run (about 80 s for the four fonts of 1.4.3), and romfs-patches-<style>-<w>x<h> differs
|
||||
from romfs-patches-<style> in the Common archive alone, so the setup makes the fifteen shapes from the 16:9
|
||||
folder."""
|
||||
if not args.reuse or not (args.reuse / path).is_file():
|
||||
return None
|
||||
data = (args.reuse / path).read_bytes()
|
||||
try:
|
||||
same = (zstd.get_frame_info(data).dictionary_id == zstd.get_frame_info(originals[path]).dictionary_id
|
||||
and zstd.decompress(data, zstd_dict=dictionary) == contents)
|
||||
except zstd.ZstdError:
|
||||
return None
|
||||
return data if same else None
|
||||
|
||||
|
||||
lines, reused = [], []
|
||||
for path, contents in outputs.items():
|
||||
raw = originals[path]
|
||||
dictionary = dictionaries.get(zstd.get_frame_info(raw).dictionary_id)
|
||||
data = zstd.compress(contents, level=19, zstd_dict=dictionary)
|
||||
assert zstd.decompress(data, zstd_dict=dictionary) == contents
|
||||
assert zstd.get_frame_info(data).dictionary_id == zstd.get_frame_info(raw).dictionary_id
|
||||
target = args.out / path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
# Unlinked first: a file linked from another folder (--reuse) is not written through, which would break the other
|
||||
# folder's manifest.
|
||||
target.unlink(missing_ok=True)
|
||||
data = reusable(path, contents, dictionary)
|
||||
if data is not None:
|
||||
try: # a hard link takes no room; a volume without them gets a copy
|
||||
os.link(args.reuse / path, target)
|
||||
except OSError:
|
||||
target.write_bytes(data)
|
||||
reused.append(path)
|
||||
else:
|
||||
data = zstd.compress(contents, level=19, zstd_dict=dictionary)
|
||||
assert zstd.decompress(data, zstd_dict=dictionary) == contents
|
||||
assert zstd.get_frame_info(data).dictionary_id == zstd.get_frame_info(raw).dictionary_id
|
||||
target.write_bytes(data)
|
||||
lines.append('\t'.join([path, str(len(data)), hashlib.sha256(data).hexdigest(), hashlib.sha256(raw).hexdigest()]))
|
||||
(args.out / 'manifest.tsv').write_text('# path\tsize\tsha256\toriginal_sha256 (tools/make_romfs_patches.py)\n'
|
||||
+ ''.join(line + '\n' for line in lines))
|
||||
print(json.dumps(dict(status='pass', path=ARCHIVE, members=patched, changed_bytes=len(diff),
|
||||
ultrawide_layout=dict(aspect=args.aspect, members=len(layout)) if args.aspect else None,
|
||||
prompts=prompts)))
|
||||
prompts=prompts, reused=reused)))
|
||||
+58
-6
@@ -6,18 +6,24 @@ An instrumented build (build_game.py --pgo gen) writes <build>/pgo/default_<id>.
|
||||
minute and at exit); successive runs of the same exe add to that file. llvm-profdata comes from the llvm-mingw toolchain
|
||||
the instrumented build used: its raw format is tied to the compiler version.
|
||||
|
||||
Beside the profile goes <out>.json, the training record: the game version and the git tree of recomp/ at the merge.
|
||||
The profile matches the generated C of that generator; tools/make_release.py ships it only while recomp/ is unchanged,
|
||||
since llvm-profdata matches functions by name and control-flow hash and a regenerated function silently loses its
|
||||
counts. Merge right after the training runs, from the commit they were built at.
|
||||
Beside the profile goes <out>.json, the training record: the game version, the git tree of recomp/ at the merge and
|
||||
the digests of the runtime/ headers the generated C includes (gen_headers; their inline helpers end up inside every
|
||||
lifted function). The profile matches the generated C of that generator and those headers; tools/make_release.py warns
|
||||
when either changed, since llvm-profdata matches functions by name and control-flow hash and a changed function
|
||||
silently loses its counts. Merge right after the training runs, from the checkout and commit they were built at.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import build_game # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
EXE = '.exe' if sys.platform == 'win32' else ''
|
||||
DEFAULT_DIR = ROOT / 'build' / 'pgo' # tools/make_release.py takes pgo-<version>.profdata from here
|
||||
@@ -44,6 +50,51 @@ def recomp_tree():
|
||||
text=True).stdout.strip() or None
|
||||
|
||||
|
||||
def header_digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
|
||||
|
||||
|
||||
def gen_headers(build):
|
||||
"""{runtime-relative path: digest} of the runtime/ headers the generated objects of an instrumented build include,
|
||||
from its Ninja deps log. Their inline helpers (simd_fp.h, cpu.h, ...) become part of every lifted function before
|
||||
the profile is matched, so a change to one invalidates the profile as surely as a generator change."""
|
||||
env = dict(os.environ, PATH=str(build_game.UCRT) + os.pathsep + os.environ.get('PATH', ''))
|
||||
r = subprocess.run([str(build_game.NINJA), '-f', str(build / 'build.ninja'), '-t', 'deps'], cwd=ROOT, env=env,
|
||||
capture_output=True, text=True)
|
||||
if r.returncode:
|
||||
sys.exit(f'ninja -t deps failed in {build}: {(r.stdout + r.stderr).strip()[-500:]}')
|
||||
runtime = (ROOT / 'runtime').resolve()
|
||||
found, cur = {}, False
|
||||
for line in r.stdout.splitlines():
|
||||
if not line.startswith(' '):
|
||||
target = line.split(': #deps')[0].replace('\\', '/')
|
||||
cur = '/obj/' in target and '/obj/runtime/' not in target
|
||||
continue
|
||||
if not cur:
|
||||
continue
|
||||
p = Path(line.strip())
|
||||
p = (p if p.is_absolute() else ROOT / p).resolve()
|
||||
try:
|
||||
rel = p.relative_to(runtime).as_posix()
|
||||
except ValueError:
|
||||
continue
|
||||
if rel not in found:
|
||||
found[rel] = header_digest(p)
|
||||
if not found:
|
||||
sys.exit(f'no runtime/ header in the deps log of {build}: was it built from this checkout?')
|
||||
return dict(sorted(found.items()))
|
||||
|
||||
|
||||
def stale_headers(record, root):
|
||||
"""The headers of a training record that differ in root/runtime now (missing ones included); None when the record
|
||||
predates the header list."""
|
||||
headers = record.get('gen_headers')
|
||||
if headers is None:
|
||||
return None
|
||||
rt = root / 'runtime'
|
||||
return [h for h, d in headers.items() if not (rt / h).is_file() or header_digest(rt / h) != d]
|
||||
|
||||
|
||||
def merge(build, out, version, toolchain):
|
||||
raw = sorted((build / 'pgo').glob('*.profraw'))
|
||||
if not raw:
|
||||
@@ -51,6 +102,7 @@ def merge(build, out, version, toolchain):
|
||||
tree = recomp_tree()
|
||||
if not tree:
|
||||
sys.exit('recomp/ has uncommitted changes: the record could not name the generator the profile matches')
|
||||
headers = gen_headers(build)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = out.with_suffix('.profdata.tmp')
|
||||
cmd = [str(llvm_profdata(toolchain)), 'merge', '--failure-mode=all', '-o', str(tmp), *map(str, raw)]
|
||||
@@ -60,11 +112,11 @@ def merge(build, out, version, toolchain):
|
||||
sys.exit(f'llvm-profdata merge failed: {(p.stdout + p.stderr).strip()[-2000:]}')
|
||||
tmp.replace(out)
|
||||
record = {'version': version, 'recomp_tree': tree, 'merged': datetime.date.today().isoformat(),
|
||||
'raw_files': len(raw)}
|
||||
'raw_files': len(raw), 'gen_headers': headers}
|
||||
out.with_suffix('.json').write_text(json.dumps(record, indent=1) + '\n', encoding='utf-8')
|
||||
size = sum(f.stat().st_size for f in raw)
|
||||
print(f'{out}: {len(raw)} raw files ({size / 2**20:.1f} MiB) -> {out.stat().st_size / 2**20:.1f} MiB; '
|
||||
f'recomp tree {tree[:12]}')
|
||||
f'recomp tree {tree[:12]}, {len(headers)} runtime headers')
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
+34
-6
@@ -38,6 +38,11 @@
|
||||
# Replay NVN submissions on their own thread, at most one frame behind the game, 0 or 1. Default: off. Helps where the
|
||||
# game waits for the GPU; adds up to a frame of latency.
|
||||
# replay_async = 0
|
||||
# Record each large submission in chunks on this many threads (0 to 16; 0 = one thread, the default). The chunks go to
|
||||
# the GPU in order. In the town at 4K on an RTX 5070, 4 cut the replay from 8.2 to 4.5 ms a frame (71 to 79 fps); more
|
||||
# threads add little. Needs 8 or more logical processors to help. Contact shadows and DLSS (dlaa = 1) run in chunks too.
|
||||
# Read at start.
|
||||
# replay_workers = 0
|
||||
# EXPERIMENTAL, THE GAME CRASHES WITH 1. 1 lifts the virtual core gate: guest threads run without the console's limit
|
||||
# of one thread per core, and the game keeps per-core data without locks. With 1, 1.2.1 and 1.4.3 crash or hang,
|
||||
# usually at start (#212). Keep 0 to play.
|
||||
@@ -45,6 +50,19 @@
|
||||
# Windows timer resolution of 1 ms for steadier frame pacing (0 = the Windows default).
|
||||
# timer_1ms = 1
|
||||
|
||||
# One frame ahead of the GPU.
|
||||
# Let the game build the next frame while the GPU still draws the previous one, 0 or 1. Default: off. Each frame the
|
||||
# game waits once for the GPU to finish the frame it presented last; with 1 that wait covers the frame before it
|
||||
# instead. On Great Sky Island (Linux, RX 9070 XT) with shadow_defer = 1 this took about 73 to 100 fps. Costs one more
|
||||
# frame of input delay, and frame times vary more from frame to frame than with 0. GPU results the game reads back after that wait, for example occlusion queries, can be a frame old.
|
||||
# Works on 1.2.1 and 1.4.3; 1.1.0 ignores it. Needs shadow storage, which is on by default on Linux and Wine and needs
|
||||
# the environment variable TOTK_SHADOW=1 on native Windows; without it the setting is ignored and the log says so.
|
||||
# sync_ahead = 0
|
||||
# With sync_ahead = 1: when the game changes memory the GPU still reads, queue the copy into the GPU's shadow of that
|
||||
# memory for the GPU to run in order, 0 or 1. Default: on. With 0 the replay thread waits for the GPU to finish
|
||||
# reading instead, which costs part of the gain.
|
||||
# shadow_defer = 1
|
||||
|
||||
# Shaders and pipelines.
|
||||
# Draws whose pipeline is still building: default waits on a reserved worker; draw is the previous scheduling.
|
||||
# pipe_wait = default
|
||||
@@ -109,12 +127,15 @@
|
||||
# Vulkan.
|
||||
# Vulkan validation layer: checks every call and slows the game down a lot. Needs the layer installed (Vulkan SDK).
|
||||
# vk_validation = 0
|
||||
# A thread of its own makes the Vulkan calls of each frame, 0 or 1. Default: on, except on AMD GPUs.
|
||||
# A thread of its own makes the Vulkan calls of each frame, 0 or 1. Default: on. Set 0 if an AMD GPU shows a black
|
||||
# screen or stops with VK_ERROR_DEVICE_LOST.
|
||||
# vk_recorder = 1
|
||||
# Dynamic vertex input, 0 or 1. Default: on, except on Intel's older integrated GPU drivers (Vulkan 1.3.215 and older).
|
||||
# Dynamic vertex input, 0 or 1. Default: on, except on Intel's Windows driver for integrated GPUs (UHD, Iris Xe),
|
||||
# which draws black patches with it.
|
||||
# vk_dyn_vertex = 1
|
||||
# Dynamic alpha to coverage, 0 or 1. Default: on, except on AMD's driver for RDNA 2 and older (RX 400 to RX 6000),
|
||||
# which applies alpha to coverage while the dynamic state says off and draws the characters black.
|
||||
# which applies alpha to coverage while the dynamic state says off and draws the characters black, and on Intel's
|
||||
# Windows driver for integrated GPUs (UHD, Iris Xe), which draws coloured noise on foliage with it.
|
||||
# vk_dyn_a2c = 1
|
||||
# Pin the shader subgroup size to 32 when the GPU allows it.
|
||||
# vk_subgroup_pin = 1
|
||||
@@ -150,12 +171,19 @@
|
||||
# fits inside it (3440x1080 gives 1920x1080) and the present adds bars; build the layout with
|
||||
# tools/make_romfs_patches.py --aspect WxH to use the full shape. Read at start. Default: 1920x1080, or on a primary
|
||||
# display of one of those shapes the smallest of its sizes at least 1080 high that fits the display (1920x1280 on a 3:2
|
||||
# laptop, 2560x1080 on a 21:9 monitor), so the picture fills the screen without black bars; with prompts = xbox or pc it
|
||||
# stays 1920x1080, the only shape the setup builds those prompts for.
|
||||
# laptop, 2560x1080 on a 21:9 monitor), so the picture fills the screen without black bars, with prompts = xbox or pc
|
||||
# too (on an install by an older setup, which built the Xbox prompts for 16:9 only, prompts = xbox keeps 1920x1080).
|
||||
# output_res = 1920x1080
|
||||
# 3D render scale: the scene's size relative to the game resolution, game (5/6, the game's own 1600x900 at 1080p) or
|
||||
# a percentage (25 to 400, at most 4096 pixels a side); above 100 supersamples the scene (anti-aliasing). Read at start.
|
||||
# a percentage (25 to 400); above 100 supersamples the scene (anti-aliasing). The scene stays within 8192 pixels a side
|
||||
# and within a texture heap under 2 GiB (8192x2304 at most on a 5120x1440 output); the log says when it is smaller.
|
||||
# Read at start.
|
||||
# render_scale = game
|
||||
# Dynamic resolution: 1 lets the game lower the scene to 90 or 80 % of that size, 0 keeps it at 100 %. The game decides
|
||||
# by a cost estimate made for the Switch, which grows with the scene's width and ignores how fast your GPU is: a
|
||||
# scene wider than about 3000 pixels can drop to 80 % on any GPU. 1 is the default. 1.2.1 and 1.4.3. Changes in
|
||||
# the menu take effect at once.
|
||||
# dynamic_res = 1
|
||||
# Scaling to the window when it is larger than the game resolution: fsr (AMD FSR 1) or bilinear, and FSR's sharpening
|
||||
# from 0 (off) to 100. Changes while the game runs take effect at once.
|
||||
# upscaler = fsr
|
||||
|
||||
Reference in new issue
Block a user