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
+5
-5
@@ -14,7 +14,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|
||||
|---|---|---|---|
|
||||
| Left stick | left stick | W A S D | move |
|
||||
| Right stick | right stick | mouse; arrow keys as a second source | camera |
|
||||
| Y | west | left mouse button | attack |
|
||||
| Y | west | left mouse button, T | attack |
|
||||
| ZL | left trigger (over 50%) | right mouse button, Z | shield, lock on |
|
||||
| ZR | right trigger (over 50%) | F, X | bow |
|
||||
| X | north | Space | jump |
|
||||
@@ -29,7 +29,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|
||||
| Minus | Back | M | map |
|
||||
| Right stick shaken left and right | shake the right stick | G | detach a fused part from what Ultrahand holds |
|
||||
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. T duplicates Y for the game's menus, where the left mouse button is A while the pointer is shown (sort the inventory with T). The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
|
||||
## Xbox button prompts
|
||||
|
||||
@@ -46,7 +46,7 @@ Esc is unbound in the game; it opens the settings menu and releases the mouse cu
|
||||
| Plus, Minus | Menu, View | Menu, View |
|
||||
| D-pad | cross | cross |
|
||||
|
||||
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters are right but the diamond keeps the Switch positions: A shows on its right, while the button labelled A is at the bottom. The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
|
||||
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters stay and the diamond is laid out as an Xbox pad: A at the bottom, B on the right, X on the left and Y at the top, the button the prompt names lit in its place, its bar reaching left from X and right from the others (`tools/make_romfs_patches.py`, `bflyt_xbox_diamond`: the panes of `blyt/DoCommand_00.bflyt` take their partner's place, A's of B and X's of Y). The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
|
||||
|
||||
The prompts are part of the game's fonts, textures and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the prompt the game draws for it, which then names the wrong button. Use the Xbox prompts with the default bindings.
|
||||
|
||||
@@ -96,7 +96,7 @@ The tab writes `totk.ini` lines, one per Switch button or stick direction: `bind
|
||||
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
|
||||
- `none` leaves the button unbound.
|
||||
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above, less any key a line binds: with `bind_X = key:t`, T presses X only, not also Y. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
|
||||
|
||||
A pad that reports its face buttons swapped (A/B and X/Y, as some GuliKit models do) is fixed with:
|
||||
|
||||
@@ -197,7 +197,7 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
|
||||
|
||||
## Mouse in the game's menus
|
||||
|
||||
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
|
||||
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The middle and side mouse buttons keep their bindings (bound to L and R they turn the tabs), and T presses Y (sort). The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
|
||||
|
||||
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`TOTK_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `TOTK_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Rules for taking a PR
|
||||
|
||||
## 1. What to review first
|
||||
|
||||
1. Fixes before features. A PR that fixes a bug, a glitch or a crash goes first; feature PRs wait.
|
||||
2. Order fixes by reach: how many players the bug hits and how badly. Windows and default settings first, rare
|
||||
hardware and unusual setups later.
|
||||
3. Scale the depth of the checks to the reach. A crash many players hit and anything on the frame path get the full
|
||||
checks. A rare case (a CPU without AVX, an unusual setup) gets the minimal fix, one targeted check and a short
|
||||
note. A crash fix on rare hardware is still worth taking: cut the checks around it, not the fix.
|
||||
4. Before testing, find out where the bug reproduces: OS, GPU vendor and generation, game version, settings. If the
|
||||
symptom cannot appear on the hardware at hand (an NVIDIA paging bug on an AMD machine), say so and do not run
|
||||
tests that cannot decide it.
|
||||
5. Check a feature PR against the designs in `docs/milestones/`. A feature with a design (mods, cheats) goes in as
|
||||
part of that design.
|
||||
6. Several PRs on one topic: compare them before taking either. Another PR may already do it right.
|
||||
|
||||
## 2. How to read the diff
|
||||
|
||||
- The author's claim is a hypothesis. Read the whole diff: races, wrong semantics, edge cases, leaks, lock order.
|
||||
- Check the behaviour against evidence: how yuzu, Ryujinx, LAS Recomp, SavageWindRecomp, nxnative and MacNativeGames
|
||||
do the same thing in `refs/` (`tools/fetch_refs.sh` recreates it); the Vulkan spec; hardware docs
|
||||
([docs/maxwell.md](../../docs/maxwell.md)); upstream issues.
|
||||
- Weigh two things for every change: its cost on the hot path, and how it fails (device lost, hang, stale data, weak
|
||||
or non-AVX CPUs, other GPU vendors, Wine/Proton).
|
||||
- Read the code a hypothesis depends on before running the game: the game's functions in Ghidra or
|
||||
`generated/<ver>`, our path in `runtime/`. A run confirms what the code could not rule out. It is one run, not a
|
||||
series of variants.
|
||||
- Order the work so the cheapest check that could refute the PR's idea runs first. Refuted: stop, write down why,
|
||||
no build, no benches.
|
||||
|
||||
## 3. What a PR needs
|
||||
|
||||
- **Proof.** The bug reproduced before the fix, the same repro after, both results side by side: a screenshot, a log
|
||||
line, a probe, a differential test. A fix without a reproduced failure is not proof. Where a repro is impossible
|
||||
on the hardware at hand, say so; the fix is then taken only on strong static evidence in the code. Where a repro is
|
||||
risky (device lost or a TDR can hang the machine), write the recipe and let the owner decide.
|
||||
- **Frame-path changes** get whole runs of the heavy scene with the switch off and on, at least two each
|
||||
([02-performance.md](02-performance.md)). Zero new slow frames is a hard bar.
|
||||
- **A switch.** A change on the hot path or in rendering sits behind an env or `totk.ini` switch, so a player can turn
|
||||
it off and we can compare.
|
||||
- **Nothing changes for players outside the feature.** The DLSS PRs touched shared code; they went in once every
|
||||
path that runs without DLSS was gated on DLSS being on.
|
||||
- **A fix for one setup keeps the default for everyone.** The PRs that raised the low memory block from 6 to 10 GiB for
|
||||
all players were rejected; the right shape is 6 GiB plus the extra where it is needed.
|
||||
- **Parts we cannot run** (Linux-only code, an HDR display, RTX-only paths) are gated so they cannot touch anyone
|
||||
else, or wait for a report from someone with that hardware. Note them as unrun.
|
||||
- **Project tests** from [03-correctness.md](03-correctness.md): lifter differential tests, probes, `--selftest`,
|
||||
`tools/check_content.py --staged`.
|
||||
- **Text the player sees** goes into the catalog: `runtime/ui_text.h` ids and `runtime/ui_text.c` entries in every
|
||||
language the menu has (English, Russian, Brazilian Portuguese, Simplified Chinese), and `installer/text/*.json` for
|
||||
the installer. No hardcoded sentences, no second string table.
|
||||
- **A new setting** is described in `totk.ini.example` and [docs/settings.md](../../docs/settings.md).
|
||||
- **Licences.** MIT, BSD, ISC, zlib and Apache-2.0 code may be copied, with its header and a row in
|
||||
`THIRD_PARTY_NOTICES.md`. yuzu, suyu and other GPL code is read for behaviour only; we write our own.
|
||||
|
||||
## 4. Reasons to reject or rework
|
||||
|
||||
- No test or repro for a change that is not trivial.
|
||||
- Slow: a state copy per draw, a global atomic counter on the hot path, synchronous file I/O or a GPU allocation on a
|
||||
game or render thread, new slow frames.
|
||||
- Changes behaviour for everyone to serve one setup, or is on by default without a run in a scene where it acts.
|
||||
- "Fixes" a crash by turning a feature off for a vendor. A crash that goes away with a feature off points at the bug;
|
||||
the feature gets fixed.
|
||||
- Breaks another feature (a preview that loses anti-aliasing).
|
||||
- Fake features: labelled FSR4 or frame generation, and not that.
|
||||
- Wrong measurements: percentiles over window means, counters over the wrong set.
|
||||
- Generated C edited by hand instead of the generator (`recomp/`).
|
||||
- A disallowed licence or pasted GPL code.
|
||||
|
||||
## 5. Finishing a good PR
|
||||
|
||||
A good PR that misses the project's scheme (hardcoded text, conflicts, layout, comment style) is brought to it while
|
||||
merging, not sent back to the author for another round. When only part of a PR is good, take that part and note what
|
||||
was left out and why.
|
||||
|
||||
A bug found outside the PR under review is not fixed on the spot: search the open PRs on that topic, report the bug
|
||||
with them, and let the owner decide.
|
||||
|
||||
## 6. Merging, branches, commits
|
||||
|
||||
- Each PR or wave of PRs gets its own branch `task/<tag>` from a fresh `dev` and its own build dir. It merges into
|
||||
`dev`.
|
||||
- Once the review is done, the symptom is gone on one paired shot and the probes pass, merge into `dev` and hand the
|
||||
build to players: they test in game faster and wider than our routes. The wider checks (other routes, validation,
|
||||
fps cost) follow as a separate diff. Speed never skips the review of the diff itself.
|
||||
- `main` only with the owner's explicit yes. Push only when told to.
|
||||
- A commit message is a plain English sentence, no `feat:`/`fix:` prefixes.
|
||||
- The version lives in the repo-root `VERSION` file; the owner decides when it moves.
|
||||
- After a merge, record which PRs went in, what was reworked, what was rejected and why. The reason for a rejection
|
||||
goes into the PR as a plain English comment.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Performance: no lost frames on weak hardware
|
||||
|
||||
## Who we tune for
|
||||
|
||||
- The goal is many more fps on modest hardware, and Steam Deck later (Zen 2 4c/8t APU, shared LPDDR5, the exe runs
|
||||
through Proton/Wine).
|
||||
- The developer's machine (Ryzen 9 7900X + RX 9070) is fast and its numbers do not carry over. A laptop with an
|
||||
i7-9750H and a GTX 1050 Ti is a sample of a weak machine, not the target to fit constants to.
|
||||
- What weak hardware measured:
|
||||
- GTX 1050 Ti (4 GiB, 214 MiB BAR, no ReBAR): the GPU read imported host pools over PCIe, 110-120 ms a frame
|
||||
against 4.3 ms on the RX 9070. The VRAM mirror brought it to 10-11 ms; that machine is now CPU-bound at 25-45 fps,
|
||||
replay 25-30 ms a frame.
|
||||
- The same write-watch scan costs about 3x the CPU time there.
|
||||
- NVIDIA and desktop AMD GPUs have no ASTC: textures decode in a compute shader. CPU decodes before that made
|
||||
0.4-0.7 s load frames on the 1050 Ti.
|
||||
- Wine emulates `GetWriteWatch` with `mprotect` and a fault per first write. Anything relying on cheap OS memory
|
||||
tricks (write-watch, `VirtualProtect` freezing, fault handlers) is checked against Wine's implementation.
|
||||
- The port uses about half of even a weak CPU, so the frame's critical path is the limit, not throughput. TotK keys
|
||||
per-core data by `GetCurrentProcessorNumber` without locks, so the game's threads share three virtual cores on any
|
||||
host. In a fight the main thread and five job workers keep 2.6 of the 3 busy: guest code speed sets the fps.
|
||||
- Perf work builds and measures 1.4.3. A 1.2.1 build is for a change about to merge with a large gain.
|
||||
|
||||
## Gain ceiling first
|
||||
|
||||
Before writing or taking a perf change, compute its ceiling: the share of the code it removes times the share of the
|
||||
frame that code holds on the critical path, on weak hardware. A ceiling of a few percent or less is not worth it.
|
||||
|
||||
- Use the critical path (`TOTK_CRIT` + `tools/crit_path.py`, heavy frames only), not one thread's profile. On the weak
|
||||
laptop the submit thread is 55% replay, yet the critical path of heavy frames is 67% guest code, 15%
|
||||
`nvnSyncWait`, 13% core waits and wake latency.
|
||||
- Changes that can move the frame by 10% or more are usually design changes. Polishing a hot function from a flat
|
||||
profile almost always gives zero.
|
||||
|
||||
## A neutral safe change stays
|
||||
|
||||
An optimization that measures neutral on the fast machine stays on by default when:
|
||||
|
||||
- upstream (`refs/`, other ports) reports a gain for it, especially on weak CPUs or GPUs;
|
||||
- the static read leaves no hazard, and the probe or differential test passes;
|
||||
- whole heavy-scene runs off/on x2 show no fps loss and no new slow frames;
|
||||
- a switch turns it off.
|
||||
|
||||
"No gain here" is not a reason to drop a safe change with upstream evidence. A regression or a hazard nobody ruled
|
||||
out is.
|
||||
|
||||
## How to measure
|
||||
|
||||
- **By game time, never by frame counts.** Run length, analysis windows, A/B blocks and input scripts go by the world
|
||||
clock (`--seconds`, `TOTK_CRIT=w<lo>-<hi>`, `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, `TOTK_SHOTS=w<lo>-<hi>:<step>`). The
|
||||
same frame count at another fps is another scene.
|
||||
- **Check a scripted run reached the world.** Title menus on a slow boot eat button presses; confirm the run is in the
|
||||
world before trusting its numbers.
|
||||
- **Whole runs off and on, two each at least.** An in-run A/B on a moving scene lies: the A and B blocks land on the
|
||||
same game seconds every run, so a content difference repeats. One in-run A/B showed "-11% replay"; whole runs gave
|
||||
5.84/5.73 against 5.76/5.73 ms, nothing. In-run A/B only on a static scene (standing still).
|
||||
- **Spread.** One script on near-identical builds gave 1% lows from 31.5 to 50 fps. Compare medians of three runs,
|
||||
never one run against one run.
|
||||
- **A quiet machine.** `tools/run_game.py --quiet` takes the machine-wide lock; no other `totk.exe`, build or test runs
|
||||
meanwhile. One game at a time.
|
||||
- **A warm shader cache, prepared properly.** Shader and pipeline caches are not copied between build dirs:
|
||||
`totk.exe --prepare-shaders` or `--warm-shaders` in that dir. The AMD driver cache is keyed to the exe path and
|
||||
limited in size: preparing another build dir can evict this one's libraries (40-220 ms per new effect in play).
|
||||
- **A heavy scene.** A fight with a large enemy and a crowd, fps limit at the maximum (`TOTK_FPS_LIMIT=360`). Compare
|
||||
one shot with the scene's reference before trusting numbers.
|
||||
- **No base runs by habit.** Run the candidate alone and judge it against the targets: max fps, zero slow frames
|
||||
(`perf: slow frame`), no black or flat frames, no new validation errors. Glitches are found by looking at shots and
|
||||
frame dumps, not by diffing metrics. Run a base build only when a specific question needs a side-by-side.
|
||||
|
||||
| What | How |
|
||||
|---|---|
|
||||
| fps, worst frame, render-thread buckets, GPU busy, VRAM | `--env TOTK_TRACE=perf,sched`, then `python tools/perf_summary.py <log> <lo> <hi>` |
|
||||
| the frame's critical path | `TOTK_CRIT=w<lo>-<hi>` + `TOTK_CRIT_OUT=<path>`, then `python tools/crit_path.py <file>` |
|
||||
| sampling profiler | `TOTK_PROFILE=<path>` + `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, then `tools/prof_report.py <profile> <totk.map>` |
|
||||
| A/B on a static scene | `TOTK_AB=<seconds>`, then `tools/ab_summary.py --skip <seconds>` |
|
||||
| VRAM pressure on a large card | `TOTK_VRAM_LIMIT_MIB=<MiB>` caps the budget the port sees |
|
||||
|
||||
## What the hot path must not do (Windows host)
|
||||
|
||||
- **Files.** The game threads (main, job workers) and the render thread never open, read, write or flush a file, and
|
||||
never wait for a thread that does. Opening a 20-byte file in a 139k-file folder: 0.1 ms idle, 11-40 ms in play while
|
||||
the translator, the pipeline cache and the save wrote to the same volume. A save's `FlushFileBuffers` and
|
||||
`MOVEFILE_WRITE_THROUGH`: 93-135 ms. Saving the pipeline cache under its lock on the present thread froze the replay
|
||||
every 20 s. File I/O goes to a background thread; results come back in memory.
|
||||
- **Waits.** Short waits spin on the counter; waits over 1 ms use the high-resolution timer. A waitable timer wakes
|
||||
0.3-0.5 ms late; the SDK's 20 us sleeps took 460-515 us. A 1 ms timer thread does not help: it gave more slow frames.
|
||||
- **GPU allocations.** VMA's `vkAllocateMemory` of a 256 MiB block under its lock: 8-23 ms. A new block on the render
|
||||
thread is a hitch.
|
||||
- **128-bit division.** `(unsigned __int128)t * num / den` compiles to the library call `__udivti3`; counter
|
||||
conversions that way took a tenth of the game threads' time in a fight. Use the precomputed scale (`platform.h`
|
||||
`PlatScale`).
|
||||
- **Per-draw copies and counters.** A pipeline-state copy per draw, a global atomic counter, a scan of every page each
|
||||
frame: on a weak CPU each is frames.
|
||||
- **Memory kept per frame.** Anything allocated per texture initialization or per frame must be freed. One copy kept
|
||||
per texture init grew host memory by ~44 MiB a game minute until a commit failure after hours (#670). Check private
|
||||
memory over a few idle minutes for a PR that stores per-object state.
|
||||
- **Scheduling.** Any timing or scheduling change is checked for slow frames. A preempt check at every host call made
|
||||
one bug rarer but added 50-75 ms CPU hitches and was reverted.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Correctness: checks that keep behaviour intact
|
||||
|
||||
## Lifter and generated C
|
||||
|
||||
- Generated C is never edited by hand: change the generator in `recomp/`.
|
||||
- `recomp/layout.py` alone decides module bases.
|
||||
- Every change to the lifter or the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) passes the differential tests
|
||||
against Unicorn, all run with `.venv`:
|
||||
|
||||
| Test | Checks |
|
||||
|---|---|
|
||||
| `tests\diff\lift.py` | instructions sampled from the modules; `--coverage` lists the words the lifter rejects |
|
||||
| `tests\diff\simd.py` | the whole FP/SIMD encoding space |
|
||||
| `tests\diff\atomic.py` | exclusive load/store and ARMv8.1 atomics |
|
||||
| `tests\diff\func.py` | whole functions as the generator emits them: loops, jump tables, calls |
|
||||
| `tests\flags_check.py` | the function-local flag helpers against the packed ones |
|
||||
| `tests\excl_mt.py` | the exclusive monitor under contention and ABA, on several threads |
|
||||
| `tests\jumptables_check.py` | `recomp/jumptables.py` against Unicorn: 0 disagreements on 1.2.1 and 1.4.3 |
|
||||
|
||||
- A `--cpu v2` build (no AVX2) passes the same tests with `--cflags=-march=x86-64-v2`. Players without AVX exist.
|
||||
- A compile error in generated C on a line the generator could not have written is a bit flip on disk or in memory.
|
||||
Regenerate and compare hashes; leave `recomp/` alone.
|
||||
|
||||
## Runtime
|
||||
|
||||
- From the build dir: `totk.exe --selftest`, `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex`, `--probe-shader`
|
||||
pass.
|
||||
- Settings: `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`.
|
||||
- Button prompts: `.venv-py314\Scripts\python tests\romfs_prompts_test.py`. Build dirs carry only `romfs-patches`; to
|
||||
test PC or Xbox prompts in game, generate the overlay first:
|
||||
`.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts pc --out build\<dir>\romfs-patches-pc`,
|
||||
then restart the game.
|
||||
- Installer: `tests\installer_gui_test.py` (event model, form, catalogs, cancel, display scale).
|
||||
- Shaders: `tools\shader_census.py --version <ver> --translate`, `tools\xlate_smoke.py`. A translator patch
|
||||
(`tools/xlate/patches/`) needs `build/xlate` rebuilt and changes shader hashes: anything keyed to a shader hash
|
||||
breaks with it.
|
||||
|
||||
## GPU portability
|
||||
|
||||
- New Vulkan code queries the feature, extension, format or limit it uses and keeps a working path or a logged
|
||||
fallback without it (provoking vertex and subgroup size pinning in `vk_pipeline.c` are examples).
|
||||
- Never hardcode what one GPU reports: alignments, subgroup size (RDNA4 may run wave64, old AMD Vega is wave64 only),
|
||||
depth formats, `maxPushDescriptors`.
|
||||
- One queue: every submit and present takes `vk_queue_lock`. Presenting from its own thread on a second queue gave
|
||||
device lost on GTX 10-series in another port.
|
||||
- RDNA1/2 bugs do not reproduce on RDNA4 (another driver branch); NVIDIA bugs do not reproduce on AMD. A Ryzen 7000
|
||||
iGPU is RDNA2 and runs the RDNA1/2 driver branch: a cheap testbed for those reports.
|
||||
- Validation: a `--validation` run in the world shows no new messages.
|
||||
|
||||
## Maxwell / NVN behaviour
|
||||
|
||||
The port must do what the Switch GPU does, not what Vulkan does by default. The full list with sources:
|
||||
[docs/maxwell.md](../../docs/maxwell.md), section 7. The traps a PR breaks most easily:
|
||||
|
||||
- Provoking vertex: Vulkan takes the first vertex, NVN the last.
|
||||
- Unwritten vertex outputs read (0, 0, 0, 1).
|
||||
- A colour target outside the fragment shader's output map keeps its texel (write mask 0). AMD writes garbage there.
|
||||
- Feedback loops: the G-buffer pass reads its own targets. It needs the feedback layout and a by-region barrier.
|
||||
- Depth bias argument order, depth mode, depth clamp: checked against deko3d.
|
||||
- Report timestamps: the engine's GPU time comes from them, and dynamic resolution and game speed follow it.
|
||||
|
||||
## Timers and the real-time step
|
||||
|
||||
- With `realtime` set, the game steps by a measured ratio, not whole 30 Hz units. A countdown remainder in
|
||||
(0, FLT_EPSILON] is never stepped again, and a consumer testing `> 0` sees it active forever. That was "weapons
|
||||
cannot be changed after a Fuse until a reload" (#18).
|
||||
- Symptoms of this class: stuck until a reload or teleport, locked after an action, works sometimes.
|
||||
- A PR touching the frame step, the pacer or the fps limit is checked for this class.
|
||||
|
||||
## Threads and scheduling
|
||||
|
||||
- ARMv8 `stlr` then `ldar` is ordered (RCsc); C11 release/acquire lets x86 reorder that store and load. Changes to
|
||||
the lifter's barriers or atomics pass `tests\diff\atomic.py` and `tests\excl_mt.py`.
|
||||
- A scheduler change is judged from one `TOTK_TRACE=sched` run by how long a ready, better thread waits for a core it
|
||||
may use, plus the slow-frame count. Not by statistics over many runs.
|
||||
- A crash that looks like a race: rule out the save first. One reported crash was the save itself; it crashed the
|
||||
same way on a real Switch.
|
||||
|
||||
## Files and caches
|
||||
|
||||
- A crash leaves zero-filled files. Caches are validated on load, not trusted because they exist.
|
||||
- The pipeline cache is dropped when vendor, device or `pipelineCacheUUID` change.
|
||||
- Unicode paths: W APIs with UTF-8 to UTF-16 conversion.
|
||||
- A failure that happens once and does not come back: rerun before debugging it as a code bug. Broken caches and bit
|
||||
flips happen.
|
||||
|
||||
## Game content boundary
|
||||
|
||||
- The repository is public. No game files, and nothing extracted or generated outside `build/` and `generated/`.
|
||||
- Keys never appear in logs, commands, files, issues or comments.
|
||||
- Every file the generator emits carries the `GENERATED_MARKER` defined in `tools/check_content.py`.
|
||||
- `python tools/check_content.py --staged` before every commit. CI runs the same check.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Knowledge base
|
||||
|
||||
Before investigating a symptom, or trusting a PR that "fixes" it, look for it here. Search by what the player sees
|
||||
(black, magenta, flicker, slow motion, missing, stale, streaks) and by the object (sky, grass, shadow, fog, HUD).
|
||||
|
||||
## Invariants of recent changes
|
||||
|
||||
A PR that touches these areas must keep them.
|
||||
|
||||
- **Queue order.** When anything is submitted, every earlier part is already on the queue. Present, uploads, readbacks
|
||||
from guest threads and the `vkDeviceWaitIdle` sites rely on it. The recorder thread now begins, ends and submits
|
||||
the parts it records; the submitting thread still waits for that submission, so the order holds.
|
||||
`TOTK_REC_END=0` restores the old split.
|
||||
- **GPU-written memory is never frozen.** Storage-buffer stores, copies and fills land in imported guest memory
|
||||
without a fault or a write-watch bit, so a region only the GPU writes looks idle. Such regions are never frozen
|
||||
read-only, and a frozen region is thawed before a GPU write. LAS Recomp never changes the protection of imported
|
||||
memory because of NVIDIA. A suspected cause of NVIDIA "write invalid" device faults (#788, #802), not proven.
|
||||
- **VRAM pressure.** Under pressure, released textures whose storage the game has given to another texture go first:
|
||||
30 s after release and once no finalized slot has bound them for 600 submits. Clean mirrored entries wait 5 s.
|
||||
Both stop once use is back under the line. A PR touching texture lifetime is tested with `TOTK_VRAM_LIMIT_MIB`.
|
||||
- **Texture pins are freed.** Each pin counts the recording copies naming it and is freed 1200 submits after its last
|
||||
use unless the replay holds it as a render target. Before that, pins leaked ~44 MiB a game minute (#670).
|
||||
|
||||
## Fixed bug classes
|
||||
|
||||
If a PR touches this code, confirm the fix still holds.
|
||||
|
||||
| Symptom | Cause | Where the fix is |
|
||||
|---|---|---|
|
||||
| lime silhouettes in grass; rectangles in the sky | a target outside the output map must keep its texel; zero-filling it wiped the half-res depth | `vk_pipeline.c` `target_wmask` |
|
||||
| stale grass maps in the far terrain for a second after a hit | write-back of a texture the CPU had rewritten | `vk_texture.c` `write_back` |
|
||||
| cut grass stays standing; fire does not spread (#132) | CPU reads of GPU-rendered textures without an invalidate | `vk_texture.c` `vk_texture_write_back_small`, `nvn_ext.c` `nvn_pool_invalidate` |
|
||||
| cloud and fog volumes empty for the session | a one-shot draw skipped instead of waiting for its pipeline | `vk_exec_draw.c` `prepare_draw` |
|
||||
| tile rectangles | storage buffers bound by Ryujinx first-use order, not the c0 slot | `vk_exec_draw.c` `ssbo_slot` |
|
||||
| black dialog background and loading map | 3D targets rejected, the grading LUT never written | `vk_exec.c` `resolve_target` |
|
||||
| streaks on cliffs | primitive restart ignored | `vk_exec_draw.c` `restart_for` |
|
||||
| streaky map screen | the newest texture picked at shared storage | `vk_texture.c` `vk_texture_by_id` |
|
||||
| dark 16x16 blocks on the ground, sage spirit not drawn | G-buffer feedback loop | `vk_exec_draw.c`, `TOTK_FEEDBACK` |
|
||||
| magenta cave walls, brown sky | CPU-filled textures without a flush | write-watched mirrors, `vk_memory.c`, `vk_texture.c` |
|
||||
| black 3D scene | deferred texture binds | `nvn_api_cmd.c` `Bind*Deferred` |
|
||||
| HUD prompt boxes, black stamina wheel | `nvnTextureCompare` ignored the channel mapping | `nvn_api_obj.c` |
|
||||
| crash on world load | `GetCommandMemoryUsed` did not follow the driver's sizes; the game sizes model heaps from it | `nvn_api_cmd.c` |
|
||||
| 20 fps | a yield back-off starting before 2 ms | `kernel.c` SVC 0x0B: no back-off |
|
||||
| weapons locked after a Fuse (#18) | timer remainder in (0, FLT_EPSILON] with the real-time step | patch under the `realtime` setting |
|
||||
| magenta/blue garbage or device lost on RDNA1/2 (#5) | writes into 3D BC block views | in `dev` |
|
||||
| black window after a driver update | a pipeline cache from another driver | `vk_pipeline.c` drops the cache by UUID |
|
||||
|
||||
## Known open risks
|
||||
|
||||
A PR touching these needs stronger proof.
|
||||
|
||||
- Buffer textures are not replayed and bind a stand-in: light and decal lists may read 0.
|
||||
- New GPU-only colour images start undefined; feedback effects may show garbage.
|
||||
- Clears with a partial channel or stencil mask are skipped (`W_CLEAR_MASK`, `W_STENCIL_MASK`).
|
||||
- 8-bit indices are widened on the CPU at record time; a copy in the same submission gives stale indices.
|
||||
- `UpdateUniformBuffer` is a copy between full barriers and ends the render pass (fps outdoors).
|
||||
- Report timestamps carry host time at fence completion: the engine's GPU time is wrong.
|
||||
- The game's SVC-free spin loops burn a host core (843 in `main`).
|
||||
- Images aliased onto one GPU-only pool allocation do not see each other's data on AMD.
|
||||
|
||||
## Past PR decisions
|
||||
|
||||
| PR | Decision | Why |
|
||||
|---|---|---|
|
||||
| #690 shrine snapshots | rejected | a pass restart per copy, a `VkPipeState` copy per draw, broken `bind_sig` skip; fps loss |
|
||||
| #674 | rejected | a global STXR counter on the hot path |
|
||||
| #660 | rejected | the preview loses anti-aliasing |
|
||||
| #713, #576 | rejected | low memory block 10 GiB for everyone; should be 6 GiB plus the extra where needed |
|
||||
| #573 | rejected | dynamic resolution off by default for everyone, the 1.4.3 site never run |
|
||||
| #568, #569 | rejected | fake FSR4 and frame generation |
|
||||
| #487 | rejected | wrong metrics: percentiles over window means, counters over the wrong set |
|
||||
| #451, #380 | rejected | not painless to take |
|
||||
| #566, #594, #534 DLSS | taken with changes | #594's shared paths gated on DLSS being on; nothing changes without DLSS |
|
||||
| #489 | partly taken | the runtime part checked by a frame dump; the installer's DLL download was the owner's call, its pinned hash re-checked |
|
||||
| #519, #582 amiibo | rewritten by us | took the data-format fixes; left out the features and the Linux event rework |
|
||||
| #483 vs #585 | #585's mapping kept | #483's stick glyph mapping was rewritten unasked while #585 already had it right |
|
||||
| #528 rain | taken after a check | code review fine, merged only after a run on a rain save |
|
||||
| #767 | taken with our fix | the default was skipped with `prompts = xbox|pc` when those folders did not exist |
|
||||
| #625 frame queue | taken, off by default | a new frame queue, one field run with it on |
|
||||
| #743 UB overlay | waiting | +25% in a Depths fight on an RTX 4080; needs review, fight runs x2 and shots on AMD |
|
||||
|
||||
## Our mistakes, not to repeat
|
||||
|
||||
- **"-11%" was an A/B artifact** of a moving scene. Whole runs off/on showed zero.
|
||||
- **Micro-optimizations from a flat profile** (1-3% of samples each) gave 26.2 against 26.4 ms on the weak laptop:
|
||||
nothing. Nobody had computed a ceiling.
|
||||
- **A scheduler patch merged on run statistics** added 50-75 ms hitches. Slow frames are a hard bar.
|
||||
- **Ten game runs on lock theories** where one Ghidra read of the game's code narrowed it at once.
|
||||
- **45 minutes of builds and benches** on an idea its own first thread census had refuted: the game pins 53 of 56
|
||||
threads itself. The cheapest refuting check goes first; when it refutes, stop and write the idea down.
|
||||
- **Tests of a bug that could not appear on the hardware at hand** (NVIDIA paging on an AMD machine): 245k tokens and
|
||||
37 minutes for nothing. Ask first where the bug reproduces.
|
||||
- **Turning the recorder off for a vendor** instead of finding its bug. Stopped: the feature gets fixed, not silenced.
|
||||
- **Bisecting code when every build crashed the same way at once.** The cause was a bad page of a game file in the OS
|
||||
file cache. When all builds fail identically, suspect inputs and caches first.
|
||||
- **A duplicate issue closed with a bare "Same as #N"** lost the logs and links. Move everything useful to the issue
|
||||
that stays open first: hardware, driver, version, when it happens, workarounds, links from comments.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Per-PR checklist
|
||||
|
||||
## Before reading
|
||||
|
||||
- [ ] Fix or feature? Fixes first, ordered by how many players they reach.
|
||||
- [ ] Where does the bug reproduce: OS, GPU vendor and generation, game version, settings? Can the hardware at hand
|
||||
show it?
|
||||
- [ ] Is the symptom in [04-knowledge.md](04-knowledge.md) or `docs/maxwell.md`?
|
||||
- [ ] Another open PR on the same topic? Compare them.
|
||||
- [ ] Feature: is there a design in `docs/milestones/`?
|
||||
|
||||
## Reading the diff
|
||||
|
||||
- [ ] The whole diff read, not the description: races, edge cases, lock order, leaks.
|
||||
- [ ] Behaviour checked against `refs/` (Ryujinx, yuzu, LAS, SavageWind, nxnative, MacNativeGames), the Vulkan spec,
|
||||
`docs/maxwell.md`.
|
||||
- [ ] Hot-path cost: no file I/O, GPU allocations, waits, per-draw copies or global atomics on game or render threads.
|
||||
- [ ] No memory kept per frame or per object without a free.
|
||||
- [ ] Failure modes: device lost, hang, stale data, CPUs without AVX, NVIDIA/Intel/old AMD, Wine/Proton.
|
||||
- [ ] New Vulkan code queries the feature and has a fallback; nothing hardcoded for one GPU.
|
||||
- [ ] Invariants kept: queue order, GPU-written memory never frozen, VRAM pressure path, pins freed.
|
||||
- [ ] Players without the feature see no change. The default for everyone stays for one setup's sake.
|
||||
- [ ] New behaviour behind a switch. A new setting described in `totk.ini.example` and `docs/settings.md`.
|
||||
- [ ] Player-visible text in the catalogs, in every menu language (`runtime/ui_text.c`, `installer/text/*.json`).
|
||||
- [ ] Copied code: allowed licence, header kept, row in `THIRD_PARTY_NOTICES.md`. No GPL code.
|
||||
- [ ] Generated C not edited by hand.
|
||||
- [ ] Parts we cannot run (Linux, HDR, RTX-only) gated away from everyone else and noted as unrun.
|
||||
|
||||
## Proof
|
||||
|
||||
- [ ] Repro before the fix and the same repro after, side by side.
|
||||
- [ ] Lifter or CPU headers: differential tests against Unicorn; `jumptables_check.py` 0 disagreements.
|
||||
- [ ] Runtime: `--selftest` and the four probes pass.
|
||||
- [ ] Settings: `--probe-cfg` and `tests\settings_doc_check.py`. Prompts: `romfs_prompts_test.py`, overlay generated
|
||||
for a game check.
|
||||
- [ ] Frame path: whole heavy-scene runs off/on, two each at least, by game time, quiet machine, warm cache prepared
|
||||
in that build dir. No fps loss, zero new slow frames.
|
||||
- [ ] Rendering: shots or a frame dump in a scene where the change acts; no black or flat frames; no new validation
|
||||
errors.
|
||||
- [ ] Perf PR: gain ceiling computed for weak hardware.
|
||||
|
||||
## Decision
|
||||
|
||||
- [ ] Taken, partly taken or rejected, with the reason written down and posted to the PR.
|
||||
- [ ] What missed the project's scheme brought to it while merging.
|
||||
- [ ] Bugs found outside the PR reported, not fixed on the spot.
|
||||
- [ ] Merged into `dev` from `task/<tag>`. `main` only with the owner's yes.
|
||||
- [ ] `python tools/check_content.py --staged` before the commit.
|
||||
- [ ] Commit message: a plain English sentence, no `feat:`/`fix:`.
|
||||
@@ -0,0 +1,32 @@
|
||||
# PR review kit
|
||||
|
||||
Rules and background for a tester or reviewer who takes community PRs into `dev`. One goal: no PR breaks the
|
||||
game's behaviour or costs frames on weak hardware.
|
||||
|
||||
## The short version
|
||||
|
||||
1. A PR description and "works for me" prove nothing. Read the diff yourself and check it against `refs/`, the
|
||||
Vulkan spec and upstream issues.
|
||||
2. Almost every change needs a test: a repro before and after, a probe, a differential test, or whole runs with the
|
||||
switch off and on.
|
||||
3. The developer's machine is fast; players run weak machines. "Neutral here" says nothing about them, in either
|
||||
direction.
|
||||
4. A player without the problem sees no change: the new path sits behind a switch and the old path stays as it was.
|
||||
5. Nothing goes into `main` without the project owner's explicit yes. Accepted work lives in `dev` first.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| [01-rules.md](01-rules.md) | triage order, what a PR needs, reasons to reject, merging and commits |
|
||||
| [02-performance.md](02-performance.md) | target hardware, gain ceiling, how to measure, what the hot path must not do |
|
||||
| [03-correctness.md](03-correctness.md) | lifter, tests and probes, GPU portability, Maxwell behaviour, timers, threads, game content |
|
||||
| [04-knowledge.md](04-knowledge.md) | invariants of recent changes, fixed bug classes, open risks, past PR decisions, our mistakes |
|
||||
| [05-checklist.md](05-checklist.md) | the per-PR checklist |
|
||||
|
||||
The public docs this kit builds on: [docs/architecture.md](../../docs/architecture.md),
|
||||
[docs/building.md](../../docs/building.md) ("Checks"), [docs/maxwell.md](../../docs/maxwell.md),
|
||||
[docs/settings.md](../../docs/settings.md), [docs/tester-guide.md](../../docs/tester-guide.md), `AGENTS.md`.
|
||||
Working with a coding agent: give it this folder and `AGENTS.md` before the first review.
|
||||
|
||||
State as of 2026-11-06. The code and `docs/` win over this text where they differ.
|
||||
@@ -74,6 +74,7 @@ Many rows below are developer tools that only act together with another setting
|
||||
| `menu_test_set` | `<frame>:<key>=<value>[,...][;...]` changes settings live at that frame, with or without the menu open; `SAVE=1` also writes `totk.ini`. | Settings change during play without input, and `SAVE=1` rewrites the file. | Caution |
|
||||
| `probe_movie` | Text that picks the movie for `totk.exe --probe-movie`: the first file whose path contains it. | Only used by the movie decoder probe; ignored in normal play. | Safe |
|
||||
| `probe_movie_shot` | Path of a `.ppm` file that the movie probe writes the middle frame to. | Only used by the movie decoder probe. | Safe |
|
||||
| `probe_movie_bench` | `1` makes the movie probe also time the resampler that fits a movie to a game resolution of another shape, and print the time per frame. | Only used by the movie decoder probe. | Safe |
|
||||
| `swkbd_answer` | `<text>`, `<cancel>` or `<initial>`: answers the on-screen keyboard (for example a horse name) without opening a window. | The game gets that answer every time it asks for text. | Caution |
|
||||
| `swkbd_test` | `ok` or `cancel` opens the keyboard window and presses OK or Cancel on the first timer tick. | The keyboard window closes by itself. | Caution |
|
||||
| `test_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 |
|
||||
@@ -120,6 +121,7 @@ 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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -136,6 +138,7 @@ Many rows below are developer tools that only act together with another setting
|
||||
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which released textures are dropped, 0 to 100; `0` is off. First those the game has overwritten with a later texture and left alone for 30 s, then those that can be uploaded again. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets the card spill into system memory. Also a slider in the menu. | Caution |
|
||||
| `txd_transpose` (default `1`) | `0` turns off the swap of gradient components for gradient texture samples. | Terrain textures turned across the screen show crawling patches again. | Caution |
|
||||
| `ub_overlay` (default `1`) | `0` copies each uniform update inside a pass into its buffer at once, ending the pass with a full barrier; `ab` alternates with the `ab` blocks. | With `0` scenes with many updated blocks (the terrain, the sky islands) take longer on the GPU; `TOTK_TRACE=perf` prints how many updates stayed in their pass. | Caution |
|
||||
| `ub_overlay_out` (default `1`) | `0` copies each uniform update between passes into its buffer at once, behind a full barrier of its own; `ab` alternates with the `ab` blocks. Needs `ub_overlay`. | With `0` a run of updates before a pass costs a full GPU barrier each; `TOTK_TRACE=perf` prints how many updates between passes were overlaid. | Caution |
|
||||
| `ubo_snapshot` (default `1`) | `0` binds live memory for uniform blocks; `ab` alternates with the `ab` blocks. | Without snapshots late draws read another object's matrix, so objects can be drawn in the wrong place. | Risky |
|
||||
| `vk_no_float64` (default `0`) | `1` clears the 64-bit float feature, so the translator lowers FP64 to FP32. | Shaders change, so the shader cache entries for it are made again; precision of FP64 shaders drops. | Caution |
|
||||
| `vk_ssbo_align` (default unset) | Power of two that storage buffer offsets are treated as needing, to test the unaligned path. | The translator and the draw code round storage offsets down to it; shaders are translated again. | Caution |
|
||||
|
||||
@@ -111,6 +111,8 @@ site mouse_direct main 874cb4 bd042289 94c9acc7
|
||||
site mouse_direct main 874d7c 6d40a3e9 94c9ac91
|
||||
site mouse_direct main 18e444 f9400408 94e546db
|
||||
site mouse_direct main 18f108 bd4047ec 94e543a6
|
||||
# Keep native camera-distance easing when direct mouse orbit bypasses the stick integrators.
|
||||
site mouse_direct main 18ea34 b9418a88 94e5454f # orbit state: ldr w8, [x20, #0x188]
|
||||
site mouse_direct main 1d59460 b9004be8 94761acc
|
||||
# The aiming, scope, ability, horse and Purah Pad camera cameras: per axis, the float load the stick turn is computed
|
||||
# from (the gamepad's part only) and the load of the angle it is added to (plus the mouse's degrees), each a bl to page
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/bl_deswizzle.comp; do not edit.
|
||||
// source sha256 bd2065290e315e85d2fcdb609d812c15ba9a109d7506b3105850a5860677b0ef
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t bl_deswizzle_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000000d0, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0007000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000001f, 0x00060010,
|
||||
0x00000004, 0x00000011, 0x00000040, 0x00000001, 0x00000001, 0x00040047, 0x0000000b, 0x0000000b,
|
||||
0x0000001a, 0x00030047, 0x00000010, 0x00000002, 0x00050048, 0x00000010, 0x00000000, 0x00000023,
|
||||
0x00000000, 0x00050048, 0x00000010, 0x00000001, 0x00000023, 0x00000004, 0x00050048, 0x00000010,
|
||||
0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x00000010, 0x00000003, 0x00000023, 0x0000000c,
|
||||
0x00050048, 0x00000010, 0x00000004, 0x00000023, 0x00000010, 0x00050048, 0x00000010, 0x00000005,
|
||||
0x00000023, 0x00000014, 0x00050048, 0x00000010, 0x00000006, 0x00000023, 0x00000018, 0x00050048,
|
||||
0x00000010, 0x00000007, 0x00000023, 0x0000001c, 0x00050048, 0x00000010, 0x00000008, 0x00000023,
|
||||
0x00000020, 0x00050048, 0x00000010, 0x00000009, 0x00000023, 0x00000024, 0x00050048, 0x00000010,
|
||||
0x0000000a, 0x00000023, 0x00000028, 0x00040047, 0x0000001f, 0x0000000b, 0x0000001b, 0x00040047,
|
||||
0x000000ad, 0x00000006, 0x00000010, 0x00030047, 0x000000ae, 0x00000002, 0x00040048, 0x000000ae,
|
||||
0x00000000, 0x00000019, 0x00050048, 0x000000ae, 0x00000000, 0x00000023, 0x00000000, 0x00030047,
|
||||
0x000000b0, 0x00000019, 0x00040047, 0x000000b0, 0x00000021, 0x00000001, 0x00040047, 0x000000b0,
|
||||
0x00000022, 0x00000000, 0x00040047, 0x000000c1, 0x00000006, 0x00000010, 0x00030047, 0x000000c2,
|
||||
0x00000002, 0x00040048, 0x000000c2, 0x00000000, 0x00000018, 0x00050048, 0x000000c2, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00030047, 0x000000c4, 0x00000018, 0x00040047, 0x000000c4, 0x00000021,
|
||||
0x00000000, 0x00040047, 0x000000c4, 0x00000022, 0x00000000, 0x00040047, 0x000000cd, 0x0000000b,
|
||||
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
|
||||
0x00000020, 0x00000000, 0x00040017, 0x00000009, 0x00000006, 0x00000003, 0x00040020, 0x0000000a,
|
||||
0x00000001, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000001, 0x0004002b, 0x00000006,
|
||||
0x0000000c, 0x00000001, 0x00040020, 0x0000000d, 0x00000001, 0x00000006, 0x000d001e, 0x00000010,
|
||||
0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006,
|
||||
0x00000006, 0x00000006, 0x00000006, 0x00040020, 0x00000011, 0x00000009, 0x00000010, 0x0004003b,
|
||||
0x00000011, 0x00000012, 0x00000009, 0x00040015, 0x00000013, 0x00000020, 0x00000001, 0x0004002b,
|
||||
0x00000013, 0x00000014, 0x0000000a, 0x00040020, 0x00000015, 0x00000009, 0x00000006, 0x0004002b,
|
||||
0x00000006, 0x00000019, 0x00000000, 0x0004002b, 0x00000006, 0x0000001d, 0x00000040, 0x0004003b,
|
||||
0x0000000a, 0x0000001f, 0x00000001, 0x0004002b, 0x00000013, 0x00000024, 0x00000002, 0x00020014,
|
||||
0x00000027, 0x0004002b, 0x00000013, 0x0000002d, 0x00000006, 0x0004002b, 0x00000013, 0x00000032,
|
||||
0x00000007, 0x0004002b, 0x00000006, 0x00000037, 0x00000020, 0x0004002b, 0x00000006, 0x00000048,
|
||||
0x00000005, 0x0004002b, 0x00000006, 0x0000004c, 0x0000001f, 0x0004002b, 0x00000013, 0x0000004f,
|
||||
0x00000008, 0x0004002b, 0x00000013, 0x00000052, 0x00000009, 0x0004002b, 0x00000006, 0x0000006a,
|
||||
0x00000004, 0x0004002b, 0x00000006, 0x0000006f, 0x00000002, 0x0004002b, 0x00000006, 0x00000078,
|
||||
0x00000008, 0x0004002b, 0x00000006, 0x00000083, 0x00000003, 0x0004002b, 0x00000013, 0x00000093,
|
||||
0x00000003, 0x0004002b, 0x00000013, 0x0000009b, 0x00000004, 0x0004002b, 0x00000013, 0x000000a4,
|
||||
0x00000005, 0x00040017, 0x000000ac, 0x00000006, 0x00000004, 0x0003001d, 0x000000ad, 0x000000ac,
|
||||
0x0003001e, 0x000000ae, 0x000000ad, 0x00040020, 0x000000af, 0x0000000c, 0x000000ae, 0x0004003b,
|
||||
0x000000af, 0x000000b0, 0x0000000c, 0x0004002b, 0x00000013, 0x000000b1, 0x00000000, 0x0004002b,
|
||||
0x00000013, 0x000000b2, 0x00000001, 0x0003001d, 0x000000c1, 0x000000ac, 0x0003001e, 0x000000c2,
|
||||
0x000000c1, 0x00040020, 0x000000c3, 0x0000000c, 0x000000c2, 0x0004003b, 0x000000c3, 0x000000c4,
|
||||
0x0000000c, 0x00040020, 0x000000c9, 0x0000000c, 0x000000ac, 0x0006002c, 0x00000009, 0x000000cd,
|
||||
0x0000001d, 0x0000000c, 0x0000000c, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x000300f7, 0x000000ce, 0x00000000, 0x000300fb, 0x00000019, 0x000000cf,
|
||||
0x000200f8, 0x000000cf, 0x00050041, 0x0000000d, 0x0000000e, 0x0000000b, 0x0000000c, 0x0004003d,
|
||||
0x00000006, 0x0000000f, 0x0000000e, 0x00050041, 0x00000015, 0x00000016, 0x00000012, 0x00000014,
|
||||
0x0004003d, 0x00000006, 0x00000017, 0x00000016, 0x00050084, 0x00000006, 0x00000018, 0x0000000f,
|
||||
0x00000017, 0x00050041, 0x0000000d, 0x0000001a, 0x0000000b, 0x00000019, 0x0004003d, 0x00000006,
|
||||
0x0000001b, 0x0000001a, 0x00050080, 0x00000006, 0x0000001c, 0x00000018, 0x0000001b, 0x00050084,
|
||||
0x00000006, 0x0000001e, 0x0000001c, 0x0000001d, 0x00050041, 0x0000000d, 0x00000020, 0x0000001f,
|
||||
0x00000019, 0x0004003d, 0x00000006, 0x00000021, 0x00000020, 0x00050080, 0x00000006, 0x00000022,
|
||||
0x0000001e, 0x00000021, 0x00050041, 0x00000015, 0x00000025, 0x00000012, 0x00000024, 0x0004003d,
|
||||
0x00000006, 0x00000026, 0x00000025, 0x000500ae, 0x00000027, 0x00000028, 0x00000022, 0x00000026,
|
||||
0x000300f7, 0x0000002a, 0x00000000, 0x000400fa, 0x00000028, 0x00000029, 0x0000002a, 0x000200f8,
|
||||
0x00000029, 0x000200f9, 0x000000ce, 0x000200f8, 0x0000002a, 0x00050041, 0x00000015, 0x0000002e,
|
||||
0x00000012, 0x0000002d, 0x0004003d, 0x00000006, 0x0000002f, 0x0000002e, 0x000500c4, 0x00000006,
|
||||
0x00000030, 0x0000000c, 0x0000002f, 0x00050041, 0x00000015, 0x00000033, 0x00000012, 0x00000032,
|
||||
0x0004003d, 0x00000006, 0x00000034, 0x00000033, 0x000500c4, 0x00000006, 0x00000035, 0x0000000c,
|
||||
0x00000034, 0x00050080, 0x00000006, 0x0000003c, 0x0000002f, 0x00000034, 0x000500c4, 0x00000006,
|
||||
0x0000003d, 0x00000037, 0x0000003c, 0x00050086, 0x00000006, 0x00000041, 0x00000022, 0x0000003d,
|
||||
0x00050089, 0x00000006, 0x00000045, 0x00000022, 0x0000003d, 0x000500c2, 0x00000006, 0x00000049,
|
||||
0x00000045, 0x00000048, 0x000500c7, 0x00000006, 0x0000004d, 0x00000045, 0x0000004c, 0x00050041,
|
||||
0x00000015, 0x00000050, 0x00000012, 0x0000004f, 0x0004003d, 0x00000006, 0x00000051, 0x00000050,
|
||||
0x00050041, 0x00000015, 0x00000053, 0x00000012, 0x00000052, 0x0004003d, 0x00000006, 0x00000054,
|
||||
0x00000053, 0x00050084, 0x00000006, 0x00000055, 0x00000051, 0x00000054, 0x00050086, 0x00000006,
|
||||
0x00000059, 0x00000041, 0x00000055, 0x00050089, 0x00000006, 0x0000005d, 0x00000041, 0x00000055,
|
||||
0x00050086, 0x00000006, 0x00000062, 0x0000005d, 0x00000051, 0x00050089, 0x00000006, 0x00000067,
|
||||
0x0000005d, 0x00000051, 0x00050084, 0x00000006, 0x0000006b, 0x00000067, 0x0000006a, 0x000500c2,
|
||||
0x00000006, 0x0000006d, 0x0000004d, 0x0000006a, 0x000500c7, 0x00000006, 0x0000006e, 0x0000006d,
|
||||
0x0000000c, 0x00050084, 0x00000006, 0x00000070, 0x0000006e, 0x0000006f, 0x00050080, 0x00000006,
|
||||
0x00000071, 0x0000006b, 0x00000070, 0x000500c2, 0x00000006, 0x00000073, 0x0000004d, 0x0000000c,
|
||||
0x000500c7, 0x00000006, 0x00000074, 0x00000073, 0x0000000c, 0x00050080, 0x00000006, 0x00000075,
|
||||
0x00000071, 0x00000074, 0x00050084, 0x00000006, 0x00000079, 0x00000062, 0x00000078, 0x00050084,
|
||||
0x00000006, 0x0000007b, 0x00000079, 0x00000030, 0x00050089, 0x00000006, 0x0000007e, 0x00000049,
|
||||
0x00000030, 0x00050084, 0x00000006, 0x0000007f, 0x0000007e, 0x00000078, 0x00050080, 0x00000006,
|
||||
0x00000080, 0x0000007b, 0x0000007f, 0x000500c2, 0x00000006, 0x00000082, 0x0000004d, 0x0000006f,
|
||||
0x000500c7, 0x00000006, 0x00000084, 0x00000082, 0x00000083, 0x00050084, 0x00000006, 0x00000085,
|
||||
0x00000084, 0x0000006f, 0x00050080, 0x00000006, 0x00000086, 0x00000080, 0x00000085, 0x000500c7,
|
||||
0x00000006, 0x00000088, 0x00000045, 0x0000000c, 0x00050080, 0x00000006, 0x00000089, 0x00000086,
|
||||
0x00000088, 0x00050084, 0x00000006, 0x0000008d, 0x00000059, 0x00000035, 0x00050086, 0x00000006,
|
||||
0x00000090, 0x00000049, 0x00000030, 0x00050080, 0x00000006, 0x00000091, 0x0000008d, 0x00000090,
|
||||
0x00050041, 0x00000015, 0x00000094, 0x00000012, 0x00000093, 0x0004003d, 0x00000006, 0x00000095,
|
||||
0x00000094, 0x000500ae, 0x00000027, 0x00000096, 0x00000075, 0x00000095, 0x000400a8, 0x00000027,
|
||||
0x00000097, 0x00000096, 0x000300f7, 0x00000099, 0x00000000, 0x000400fa, 0x00000097, 0x00000098,
|
||||
0x00000099, 0x000200f8, 0x00000098, 0x00050041, 0x00000015, 0x0000009c, 0x00000012, 0x0000009b,
|
||||
0x0004003d, 0x00000006, 0x0000009d, 0x0000009c, 0x000500ae, 0x00000027, 0x0000009e, 0x00000089,
|
||||
0x0000009d, 0x000200f9, 0x00000099, 0x000200f8, 0x00000099, 0x000700f5, 0x00000027, 0x0000009f,
|
||||
0x00000096, 0x0000002a, 0x0000009e, 0x00000098, 0x000400a8, 0x00000027, 0x000000a0, 0x0000009f,
|
||||
0x000300f7, 0x000000a2, 0x00000000, 0x000400fa, 0x000000a0, 0x000000a1, 0x000000a2, 0x000200f8,
|
||||
0x000000a1, 0x00050041, 0x00000015, 0x000000a5, 0x00000012, 0x000000a4, 0x0004003d, 0x00000006,
|
||||
0x000000a6, 0x000000a5, 0x000500ae, 0x00000027, 0x000000a7, 0x00000091, 0x000000a6, 0x000200f9,
|
||||
0x000000a2, 0x000200f8, 0x000000a2, 0x000700f5, 0x00000027, 0x000000a8, 0x0000009f, 0x00000099,
|
||||
0x000000a7, 0x000000a1, 0x000300f7, 0x000000aa, 0x00000000, 0x000400fa, 0x000000a8, 0x000000a9,
|
||||
0x000000aa, 0x000200f8, 0x000000a9, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000aa, 0x00050041,
|
||||
0x00000015, 0x000000b3, 0x00000012, 0x000000b2, 0x0004003d, 0x00000006, 0x000000b4, 0x000000b3,
|
||||
0x00050041, 0x00000015, 0x000000b6, 0x00000012, 0x0000009b, 0x0004003d, 0x00000006, 0x000000b7,
|
||||
0x000000b6, 0x00050084, 0x00000006, 0x000000b8, 0x00000091, 0x000000b7, 0x00050080, 0x00000006,
|
||||
0x000000ba, 0x000000b8, 0x00000089, 0x00050084, 0x00000006, 0x000000bd, 0x000000ba, 0x00000095,
|
||||
0x00050080, 0x00000006, 0x000000be, 0x000000b4, 0x000000bd, 0x00050080, 0x00000006, 0x000000c0,
|
||||
0x000000be, 0x00000075, 0x00050041, 0x00000015, 0x000000c5, 0x00000012, 0x000000b1, 0x0004003d,
|
||||
0x00000006, 0x000000c6, 0x000000c5, 0x00050080, 0x00000006, 0x000000c8, 0x000000c6, 0x00000022,
|
||||
0x00060041, 0x000000c9, 0x000000ca, 0x000000c4, 0x000000b1, 0x000000c8, 0x0004003d, 0x000000ac,
|
||||
0x000000cb, 0x000000ca, 0x00060041, 0x000000c9, 0x000000cc, 0x000000b0, 0x000000b1, 0x000000c0,
|
||||
0x0003003e, 0x000000cc, 0x000000cb, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000ce, 0x000100fd,
|
||||
0x00010038,
|
||||
};
|
||||
+3
-3
@@ -40,8 +40,8 @@ static const char *const known[] = {
|
||||
"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",
|
||||
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"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",
|
||||
"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",
|
||||
@@ -59,7 +59,7 @@ static const char *const known[] = {
|
||||
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "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", "UPSCALER", "VB_USED", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"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",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
|
||||
@@ -230,6 +230,16 @@ int probe_cfg(void) {
|
||||
dn[KEY_MOUSE_R] = dn[KEY_Z] = 1;
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_Y | 1ull << B_ZL), "keys: right mouse button and Z both press ZL");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_T] = 1;
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_Y, "keys: T is Y too (the left button is A in the menus)");
|
||||
plat_setenv("TOTK_BIND_X", "key:t");
|
||||
input_map_configure();
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_X, "bind: bind_X = key:t takes T from Y's defaults");
|
||||
dn[KEY_T] = 0, dn[KEY_MOUSE_L] = 1;
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_Y, "bind: Y keeps the left mouse button");
|
||||
plat_setenv("TOTK_BIND_X", NULL);
|
||||
input_map_reset();
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_X] = dn[KEY_TAB] = dn[KEY_M] = dn[KEY_LCTRL] = dn[KEY_3] = 1;
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_ZR | 1ull << B_PLUS | 1ull << B_MINUS | 1ull << B_STICK_L | 1ull << B_RIGHT),
|
||||
"keys: X ZR, Tab Plus, M Minus, Left Ctrl stick click, 3 D-pad right");
|
||||
|
||||
+7
-2
@@ -623,17 +623,22 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
if (ikb_foreground()) {
|
||||
uint8_t down[KEY_COUNT] = {0};
|
||||
// Mouse buttons count only while the cursor is captured, and a button already held when the capture began
|
||||
// (the click that refocused the window) is ignored until it is released.
|
||||
// (the click that refocused the window) is ignored until it is released. While a game menu has the pointer
|
||||
// (menu_cursor.c) and it is over the window, the left and right buttons are that menu's A and B, and the
|
||||
// middle and side buttons keep their bindings: bound to L and R they turn the menu's tabs.
|
||||
static int was_captured;
|
||||
static uint8_t mouse_ignore[KEY_COUNT];
|
||||
int cap = ikb_captured();
|
||||
float pointer_x, pointer_y;
|
||||
int menu_pointer = !cap && menu_cursor_pointer() && ikb_pointer(&pointer_x, &pointer_y);
|
||||
for (int k = 1; k < KEY_COUNT; k++) {
|
||||
down[k] = ikb_down(k);
|
||||
kbm_used |= down[k];
|
||||
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
|
||||
if (cap && !was_captured) mouse_ignore[k] = down[k];
|
||||
if (!down[k]) mouse_ignore[k] = 0;
|
||||
if (!cap || mouse_ignore[k]) down[k] = 0;
|
||||
int counts = cap || (menu_pointer && k != KEY_MOUSE_L && k != KEY_MOUSE_R);
|
||||
if (!counts || mouse_ignore[k]) down[k] = 0;
|
||||
}
|
||||
was_captured = cap;
|
||||
b |= input_map_buttons(down);
|
||||
|
||||
+19
-3
@@ -16,7 +16,7 @@
|
||||
#define DEFAULT_KEYS \
|
||||
{KEY_W, B_SL_UP}, {KEY_A, B_SL_LEFT}, {KEY_S, B_SL_DOWN}, {KEY_D, B_SL_RIGHT}, /* move */ \
|
||||
{KEY_UP, B_SR_UP}, {KEY_LEFT, B_SR_LEFT}, {KEY_DOWN, B_SR_DOWN}, {KEY_RIGHT, B_SR_RIGHT}, /* camera, second source */ \
|
||||
{KEY_MOUSE_L, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
|
||||
{KEY_MOUSE_L, B_Y}, {KEY_T, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
|
||||
{KEY_SPACE, B_X}, {KEY_LSHIFT, B_B}, {KEY_E, B_A}, {KEY_Q, B_L}, {KEY_R, B_R}, \
|
||||
{KEY_LCTRL, B_STICK_L}, {KEY_C, B_STICK_R}, \
|
||||
{KEY_1, B_LEFT}, {KEY_2, B_UP}, {KEY_3, B_RIGHT}, {KEY_4, B_DOWN}, \
|
||||
@@ -31,7 +31,7 @@
|
||||
{PAD_ZR, B_ZR}
|
||||
#define NINTENDO_PADS DEFAULT_PADS(B_B, B_A, B_Y, B_X)
|
||||
#define XBOX_PADS DEFAULT_PADS(B_A, B_B, B_X, B_Y)
|
||||
enum { DEFAULT_KEY_COUNT = 27, DEFAULT_PAD_COUNT = 16 };
|
||||
enum { DEFAULT_KEY_COUNT = 28, DEFAULT_PAD_COUNT = 16 };
|
||||
_Static_assert(sizeof (Binding[]){DEFAULT_KEYS} / sizeof (Binding) == DEFAULT_KEY_COUNT, "DEFAULT_KEY_COUNT");
|
||||
|
||||
static const InputMap defaults = {{DEFAULT_KEYS}, DEFAULT_KEY_COUNT, {{NINTENDO_PADS}, {XBOX_PADS}},
|
||||
@@ -166,11 +166,27 @@ int input_map_bind(const char *button, const char *list) {
|
||||
|
||||
void input_map_configure(void) {
|
||||
input_map_reset();
|
||||
uint8_t lined[ACTION_COUNT] = {0};
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
char var[32];
|
||||
snprintf(var, sizeof var, "TOTK_BIND_%s", action_names[a]);
|
||||
const char *v = cfg_get(var);
|
||||
if (v && *v && input_map_bind(action_names[a], v)) rt_log("input: %s = %s", var + 5, v);
|
||||
if (v && *v && input_map_bind(action_names[a], v)) lined[a] = 1, rt_log("input: %s = %s", var + 5, v);
|
||||
}
|
||||
// A key a line binds leaves the defaults of the actions without a line, as the remapping rows move a key: with
|
||||
// bind_X = key:t, T presses X only, not also Y, whose defaults hold it.
|
||||
uint8_t bound[KEY_COUNT] = {0}, list[INPUT_BINDING_MAX];
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
if (!lined[a]) continue;
|
||||
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX);
|
||||
for (int i = 0; i < n; i++) bound[list[i]] = 1;
|
||||
}
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
if (lined[a]) continue;
|
||||
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX), w = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!bound[list[i]]) list[w++] = list[i];
|
||||
if (w < n) keys_put(&input_map, a, list, w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ const char *input_pad_name(int pad);
|
||||
// skipped; when none is valid the action keeps its bindings. 1 when applied.
|
||||
int input_map_bind(const char *button, const char *list);
|
||||
// Applies the TOTK_BIND_<action> settings (cfg_get) over the defaults: at start, and again when one of them changes.
|
||||
// A key a line binds leaves the defaults of the actions that have no line.
|
||||
void input_map_configure(void);
|
||||
|
||||
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts", 0.1.9's name for the Nintendo
|
||||
|
||||
+15
-4
@@ -228,23 +228,34 @@ static void crash_handler(const PlatCrash *x) {
|
||||
rt_logf(", %s address %#llx", x->access == 1 ? "writing" : x->access == 8 ? "executing" : "reading",
|
||||
(unsigned long long)x->addr);
|
||||
rt_log_raw("\n", 1);
|
||||
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));
|
||||
// abort and invalid parameter: where the host code called it; a fault in another module: the host calls that led
|
||||
// into it
|
||||
// abort and invalid parameter: where the host code called it; a fault in another module: the driver code and the
|
||||
// host calls that led into it
|
||||
uintptr_t exe = plat_exe_base();
|
||||
for (int i = 0; i < x->nframes; i++) {
|
||||
const char *m = x->frame_module[i];
|
||||
if (m) {
|
||||
rt_logf(" host frame %2d: %s+%#llx\n", i, *m ? m : "outside every module",
|
||||
(unsigned long long)x->frame_offset[i]);
|
||||
continue;
|
||||
}
|
||||
uint64_t g = lifted_function(x->frames[i]);
|
||||
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - exe));
|
||||
if (g) rt_logf(" in lifted function f_%llx", (unsigned long long)g);
|
||||
rt_log_raw("\n", 1);
|
||||
}
|
||||
if (x->frames_left_out) rt_logf(" (%d frames in other modules left out)\n", x->frames_left_out);
|
||||
Cpu *c = kern_cpu();
|
||||
if (c) {
|
||||
for (int i = 0; i < 31; i++)
|
||||
rt_logf(" x%-2d %016llx%s", i, (unsigned long long)c->x[i], i % 4 == 3 ? "\n" : "");
|
||||
rt_logf(" sp %016llx\n pc %016llx lr %s\n", (unsigned long long)c->sp, (unsigned long long)c->pc,
|
||||
ldr_describe(c->x[30], d, sizeof d));
|
||||
// pc: the guest code running, or the host function a guest call entered (its name tells the host call apart)
|
||||
rt_logf(" sp %016llx\n pc %016llx", (unsigned long long)c->sp, (unsigned long long)c->pc);
|
||||
if (hostfn_name(c->pc)) rt_logf(" (%s)", ldr_describe(c->pc, d, sizeof d));
|
||||
rt_logf(" lr %s\n", ldr_describe(c->x[30], d, sizeof d));
|
||||
uint64_t fp = c->x[29];
|
||||
for (int n = 0; n < 32 && fp && !(fp & 7) && readable(fp, 16); n++) {
|
||||
uint64_t next = LD64(fp), lr = LD64(fp + 8);
|
||||
|
||||
+49
-16
@@ -119,6 +119,7 @@ void input_direct_mouse_row(int32_t dx, float sensitivity, uint64_t now, InputMo
|
||||
#define HOST_MOUSE_ORBIT_BEGIN (RT_PATCH_PAGE + 0xfb0)
|
||||
#define HOST_MOUSE_ORBIT_ANGLES (RT_PATCH_PAGE + 0xfa0)
|
||||
#define HOST_MOUSE_HAND (RT_PATCH_PAGE + 0xf90)
|
||||
#define HOST_MOUSE_ORBIT_STATE (RT_PATCH_PAGE + 0xf70)
|
||||
static _Thread_local uint64_t mouse_input_object;
|
||||
|
||||
// Where a game version keeps what the hooks read and write (recomp/patches-v143.txt, patches-v121.txt). The camera
|
||||
@@ -162,8 +163,8 @@ static void sample_mark_taken(uint64_t generation) {
|
||||
static _Thread_local struct { uint64_t object; MouseSample mouse; } orbit;
|
||||
typedef struct {
|
||||
uint64_t object, generation, input, seen;
|
||||
float pitch, original_target;
|
||||
int hold_pitch, target_saved, resume;
|
||||
float pitch;
|
||||
int hold_pitch, resume;
|
||||
} OrbitTrack;
|
||||
static OrbitTrack orbit_seen[16];
|
||||
|
||||
@@ -268,19 +269,17 @@ static uint32_t player_state_logged(void) {
|
||||
return f;
|
||||
}
|
||||
|
||||
// main+0x191d78 builds the pitch target from +0x15c and +0x11c; the orbit update filters +0x108 toward it and adds
|
||||
// (1 - +0x88) * +0x10c at output. Updating only +0x108 makes the camera spring back to the unchanged target.
|
||||
// The orbit output adds (1 - +0x88) * +0x10c to its filtered pitch at +0x108.
|
||||
// Hold the final angle in orbit_angles. Do not overwrite the configured target
|
||||
// at +0x15c: main+0x191534 also uses it as the reference for camera distance.
|
||||
// Replacing that reference with the held pitch changes the native return from
|
||||
// aiming and collision close-ups, even though no distance field is written.
|
||||
static float orbit_pitch_offset(uint64_t object) {
|
||||
return (1 - guest_float(object + 0x88)) * guest_float(object + 0x10c);
|
||||
}
|
||||
|
||||
static void orbit_pitch_target(uint64_t object, float pitch) {
|
||||
guest_float_store(object + 0x15c, pitch - orbit_pitch_offset(object) - guest_float(object + 0x11c));
|
||||
}
|
||||
|
||||
static void orbit_pitch_release(OrbitTrack *track) {
|
||||
if (track->target_saved) guest_float_store(track->object + 0x15c, track->original_target);
|
||||
track->hold_pitch = track->target_saved = track->resume = 0;
|
||||
track->hold_pitch = track->resume = 0;
|
||||
}
|
||||
|
||||
// The pitch the camera has now (the filtered pitch plus its vertical offset), as orbit_angles leaves it
|
||||
@@ -288,8 +287,8 @@ static float orbit_pitch_now(uint64_t object) {
|
||||
return wrap_degrees(guest_float(object + 0x108) + orbit_pitch_offset(object));
|
||||
}
|
||||
|
||||
// The camera stays where it is now: the mouse holds the pitch the camera has (orbit_begin writes its target), so
|
||||
// neither the game's default target nor the mouse's target from before an aim or a lock-on pulls it back.
|
||||
// Resume from the pitch the camera reached during the handoff. orbit_angles
|
||||
// holds this final angle without replacing the native configuration reference.
|
||||
static void orbit_pitch_settle(OrbitTrack *track) {
|
||||
track->pitch = orbit_pitch_now(track->object);
|
||||
track->hold_pitch = 1, track->resume = 0;
|
||||
@@ -471,7 +470,7 @@ static void orbit_begin(Cpu *c) {
|
||||
memcpy(&override, (void *)(uintptr_t)(input + 0x15c), 1);
|
||||
plat_mutex_lock(&sample_lock);
|
||||
OrbitTrack *track = orbit_track(object);
|
||||
if (track->input != input) track->hold_pitch = track->target_saved = track->resume = 0;
|
||||
if (track->input != input) track->hold_pitch = track->resume = 0;
|
||||
uint64_t now = plat_now_ns();
|
||||
// A gamepad's vertical input gives the pitch back to the game's own target. The game's lock-on or override
|
||||
// (aiming) drives the camera itself: the mouse lets go without restoring the target it had before, and
|
||||
@@ -484,7 +483,6 @@ static void orbit_begin(Cpu *c) {
|
||||
track->seen = now;
|
||||
take = moved || track->hold_pitch;
|
||||
if (take) track->input = input;
|
||||
if (track->hold_pitch) orbit_pitch_target(object, track->pitch);
|
||||
plat_mutex_unlock(&sample_lock);
|
||||
if (moved) { // the mouse's part of this step's stick goes to the final angles instead
|
||||
guest_float_store(input + 0x420, m.pad[0]);
|
||||
@@ -500,6 +498,42 @@ static void orbit_begin(Cpu *c) {
|
||||
c->x[8] = guest_pointer(object + 8); // displaced ldr x8, [x0, #8]
|
||||
}
|
||||
|
||||
// The native manual-orbit state (4) integrates both pitch and distance from
|
||||
// the stick. Direct mouse orbit bypasses those integrators, so entering that
|
||||
// state during an aim return can latch the close-up's radius indefinitely.
|
||||
// Select the native automatic-distance state (3) at the existing state decision
|
||||
// point, before its transition initializer runs (main+0x18ea34, 1.4.3).
|
||||
// This retains the game's own radius curve, transition easing and collision.
|
||||
static void orbit_state(Cpu *c) {
|
||||
uint64_t object = c->x[20];
|
||||
uint32_t override;
|
||||
memcpy(&override, (void *)(uintptr_t)(object + 0x188), sizeof override);
|
||||
if (rt_game_version == 143 && direct_mode() && (uint32_t)c->x[10] == 4 && !override) {
|
||||
MouseSample m = sample_read();
|
||||
uint64_t input = guest_pointer(object + 0x20);
|
||||
if (m.object == input && m.camera_enabled && plat_now_ns() - m.when < 150000000ull &&
|
||||
m.pad[0] == 0 && m.pad[1] == 0 &&
|
||||
!(input_buttons_last() & ((1ull << B_ZR) | (1ull << B_R)))) {
|
||||
uint64_t held = guest_pointer(input + 0x148);
|
||||
uint8_t flags;
|
||||
memcpy(&flags, (void *)(uintptr_t)(input + 0x15c), 1);
|
||||
plat_mutex_lock(&sample_lock);
|
||||
OrbitTrack *track = orbit_track(object);
|
||||
int owned = track->input == input && track->hold_pitch;
|
||||
plat_mutex_unlock(&sample_lock);
|
||||
if (owned && !(held & 0x20) && !(flags & 0x40)) {
|
||||
c->x[10] = 3;
|
||||
static int logged;
|
||||
if (!logged) {
|
||||
logged = 1;
|
||||
rt_log("mouse: native distance transition retained for direct orbit");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c->x[8] = override; // displaced ldr w8, [x20, #0x188]
|
||||
}
|
||||
|
||||
// The final pitch and yaw are on the stack (1.4.3 sp + 0x44 / 0x48, 1.2.1 sp + 0x3c / 0x40)
|
||||
static void orbit_angles(Cpu *c) {
|
||||
const MouseLayout *ml = mouse_layout();
|
||||
@@ -526,9 +560,7 @@ static void orbit_angles(Cpu *c) {
|
||||
guest_float_store(yaw_at, wrap_degrees(yaw + yaw_delta));
|
||||
guest_float_store(object + 0x108, wrap_degrees(new_pitch - orbit_pitch_offset(object)));
|
||||
guest_float_store(object + 0x120, wrap_degrees(guest_float(object + 0x120) + yaw_delta));
|
||||
if (!track->target_saved) track->original_target = guest_float(object + 0x15c), track->target_saved = 1;
|
||||
track->pitch = new_pitch; track->input = orbit.mouse.object; track->hold_pitch = 1;
|
||||
orbit_pitch_target(object, new_pitch);
|
||||
static int logged;
|
||||
if (!logged) {
|
||||
logged = 1;
|
||||
@@ -1062,6 +1094,7 @@ __attribute__((constructor)) static void mouse_direct_register(void) {
|
||||
rt_register_host(HOST_MOUSE_DIRECT, camera_input);
|
||||
rt_register_host(HOST_MOUSE_ACTIVITY, camera_activity);
|
||||
rt_register_host(HOST_MOUSE_ORBIT_BEGIN, orbit_begin);
|
||||
if (rt_game_version == 143) rt_register_host(HOST_MOUSE_ORBIT_STATE, orbit_state);
|
||||
rt_register_host(HOST_MOUSE_ORBIT_ANGLES, orbit_angles);
|
||||
rt_register_host(HOST_MOUSE_HAND, hand_input);
|
||||
rt_register_host(HOST_MOUSE_LOAD, camera_load);
|
||||
|
||||
+7
-1
@@ -929,8 +929,14 @@ static void probe_menu_text(void) {
|
||||
cfg_set("TOTK_MENU_LANG", "zh-Hans");
|
||||
CHECK(menu_text(UI_MENU_TITLE) == ui_text_in("zh-Hans", UI_MENU_TITLE), "menu_text ignores MENU_LANG=zh-Hans");
|
||||
cfg_set("TOTK_MENU_LANG", "");
|
||||
// the system language as ui_text.c resolve_menu_lang takes it: the tag, else its base language (ru-RU: ru), else en
|
||||
const char *sys_lang = ui_text_lang(plat_ui_language());
|
||||
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang : "en";
|
||||
char sys_base[16] = "";
|
||||
size_t base_n = strcspn(sys_lang, "-");
|
||||
if (sys_lang[base_n] && base_n < sizeof sys_base) memcpy(sys_base, sys_lang, base_n);
|
||||
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang
|
||||
: *sys_base && ui_text_defined(sys_base, UI_MENU_TITLE) ? sys_base
|
||||
: "en";
|
||||
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(auto_lang, UI_MENU_CONTINUE), "menu_text auto ignores the system language");
|
||||
}
|
||||
|
||||
|
||||
@@ -1456,6 +1456,41 @@ int plat_open_folder(const char *path) {
|
||||
void *plat_lib_open(const char *name) { return dlopen(name, RTLD_NOW | RTLD_LOCAL); }
|
||||
void *plat_lib_sym(void *lib, const char *name) { return lib ? dlsym(lib, name) : NULL; }
|
||||
|
||||
void plat_lib_error(char *out, size_t cap) {
|
||||
const char *e = dlerror();
|
||||
snprintf(out, cap, "%s", e ? e : "unknown error");
|
||||
}
|
||||
|
||||
int plat_ngx_core_path(char *out, size_t cap) { return snprintf(out, cap, "libnvidia-ngx.so.1") < (int)cap; }
|
||||
|
||||
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
|
||||
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
|
||||
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
|
||||
const unsigned char *u = (const unsigned char *)in;
|
||||
size_t n = 0;
|
||||
while (*u) {
|
||||
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
|
||||
if (more == 4) return 0;
|
||||
if (more) c &= 0x3F >> more;
|
||||
for (unsigned k = 0; k < more; k++, u++) {
|
||||
if ((*u & 0xC0) != 0x80) return 0;
|
||||
c = c << 6 | (*u & 0x3F);
|
||||
}
|
||||
if (n + 1 >= cap) return 0;
|
||||
out[n++] = (wchar_t)c;
|
||||
}
|
||||
out[n] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
|
||||
char dir[PLAT_PATH_MAX];
|
||||
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
|
||||
if (!cut) return 0;
|
||||
cut[1] = 0;
|
||||
return utf8_to_wide(dir, out, cap);
|
||||
}
|
||||
|
||||
// ---- machine load: /proc/stat and /proc/self/stat for the CPU, hwmon for the temperature, the amdgpu sysfs files of
|
||||
// the render device for the GPU.
|
||||
|
||||
|
||||
@@ -222,9 +222,10 @@ _Noreturn void plat_reexec(void);
|
||||
// - A fault in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
|
||||
// overflow) is reported as it happens.
|
||||
// - A fault in another module (a GPU driver, a system library) is reported only when nothing handles it: on Windows
|
||||
// from the unhandled-exception filter, with the return addresses in this executable on the faulting thread's stack,
|
||||
// so the report shows which host call ran into the driver; on Linux from the signal handler, without frames. Drivers
|
||||
// that raise and catch their own exceptions never reach fn.
|
||||
// from the unhandled-exception filter, with the return addresses on the faulting thread's stack in every module, so
|
||||
// the report shows which driver code failed and which host call ran into it; on Linux from the signal handler,
|
||||
// without frames. Drivers that raise and catch their own exceptions never reach fn. For a C++ exception thrown by
|
||||
// MSVC-built code (a driver) the report names its type and, for a std::exception, its what() text.
|
||||
// - On Windows abort() (a failed assert, std::terminate) and an invalid parameter to a C runtime function go there
|
||||
// too, with the host frames in this executable: the C runtime ends the process for both with 0xc0000409 and no
|
||||
// exception.
|
||||
@@ -234,9 +235,14 @@ typedef struct {
|
||||
uintptr_t rip, offset; // the faulting instruction, and its offset in its module
|
||||
int access; // the data access that faulted: 0 read, 1 write, 8 execute; -1 when not an access
|
||||
uint64_t addr; // its address when access >= 0
|
||||
int nframes; // return addresses in this executable, innermost first: for abort and invalid parameter
|
||||
uintptr_t frames[16]; // (rip is frames[0]) and for a fault in another module; 0 for a fault in this one
|
||||
int nframes; // return addresses, innermost first: for abort and invalid parameter (rip is frames[0])
|
||||
uintptr_t frames[32]; // and for a fault in another module; 0 for a fault in this one
|
||||
const char *frame_module[32]; // the module of each frame: NULL for this executable, "" for none
|
||||
uintptr_t frame_offset[32]; // each frame's offset in its module
|
||||
int frames_left_out; // frames in other modules past the limit the walk keeps, so this executable's still fit
|
||||
const char *module; // the module holding rip: NULL for this executable, "" for none (a jump to address 0)
|
||||
const char *cxx_type; // a C++ exception from MSVC-built code: the thrown type's decorated name (.?AV...@@)
|
||||
const char *cxx_what; // and what() when the type derives from std::exception; NULL when unknown
|
||||
} PlatCrash;
|
||||
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
|
||||
@@ -387,6 +393,16 @@ int plat_open_folder(const char *path); // shows the folder in
|
||||
// in the system directories only. NULL when missing.
|
||||
void *plat_lib_open(const char *name);
|
||||
void *plat_lib_sym(void *lib, const char *name);
|
||||
// Why the calling thread's last plat_lib_open failed, for a log line (the OS error code on Windows, dlerror on Linux).
|
||||
void plat_lib_error(char *out, size_t cap);
|
||||
|
||||
// ---- NVIDIA NGX (vk_dlaa.c). The path of the driver's NGX core library into out: on Windows _nvngx.dll in the folder
|
||||
// the driver records in the registry (NGXCore FullPath), on Linux libnvidia-ngx.so.1, which the dynamic loader finds
|
||||
// where the driver put it. 0 when the driver records none.
|
||||
int plat_ngx_core_path(char *out, size_t cap);
|
||||
// The running executable's folder with a trailing separator, as the wchar_t string NGX takes for its application data
|
||||
// path (UTF-16 on Windows, UTF-32 code points on Linux). 0 on failure or when out has no room.
|
||||
int plat_exe_dir_wide(wchar_t *out, size_t cap);
|
||||
|
||||
// ---- machine load for the performance overlay (perfmon.c's sampling thread; each call can take milliseconds). A number
|
||||
// the system does not give is -1.
|
||||
|
||||
+89
-11
@@ -674,16 +674,20 @@ static const char *image_name(const char *base) {
|
||||
return base + ((const IMAGE_EXPORT_DIRECTORY *)(base + d->VirtualAddress))->Name;
|
||||
}
|
||||
|
||||
// The return addresses in [lo, lo + size) on the crashed thread's stack, innermost first: unwinds the faulting context
|
||||
// The return addresses on the crashed thread's stack, innermost first, with their modules: unwinds the faulting context
|
||||
// with the x64 unwind tables; a function without a table entry (a leaf, or address 0 after a call through a null
|
||||
// pointer) has its return address on top of the stack. The walk stops at return address 0 (the thread's first frame)
|
||||
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting.
|
||||
static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t *out, int max) {
|
||||
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting. A driver can
|
||||
// nest deeper than the report holds, so frames in other modules stop being kept after the first 12 (counted in
|
||||
// frames_left_out) and the frames in this executable, which name the host call that ran into the driver, still fit.
|
||||
static void frames_of(const CONTEXT *at, PlatCrash *x) {
|
||||
enum { MAX = sizeof x->frames / sizeof *x->frames, OTHER_MAX = 12 };
|
||||
CONTEXT c = *at;
|
||||
ULONG_PTR low, high;
|
||||
GetCurrentThreadStackLimits(&low, &high);
|
||||
int n = 0;
|
||||
for (int i = 0; i < 256 && n < max && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
|
||||
void *exe = GetModuleHandleW(NULL);
|
||||
int other = 0;
|
||||
for (int i = 0; i < 256 && x->nframes < MAX && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
|
||||
DWORD64 img;
|
||||
RUNTIME_FUNCTION *fe = RtlLookupFunctionEntry(c.Rip, &img, NULL);
|
||||
if (fe) {
|
||||
@@ -694,9 +698,64 @@ static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t
|
||||
c.Rip = *(const DWORD64 *)c.Rsp;
|
||||
c.Rsp += 8;
|
||||
}
|
||||
if (c.Rip - lo < size) out[n++] = (uintptr_t)c.Rip;
|
||||
if (!c.Rip) break;
|
||||
void *base = NULL;
|
||||
RtlPcToFileHeader((void *)(uintptr_t)c.Rip, &base);
|
||||
if (base != exe && other++ >= OTHER_MAX) {
|
||||
x->frames_left_out++;
|
||||
continue;
|
||||
}
|
||||
int n = x->nframes++;
|
||||
x->frames[n] = (uintptr_t)c.Rip;
|
||||
x->frame_module[n] = base == exe ? NULL : base ? image_name(base) : "";
|
||||
x->frame_offset[n] = (uintptr_t)c.Rip - (uintptr_t)base;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// MSVC's C++ throw (0xe06d7363) passes the thrown object in ExceptionInformation[1], its ThrowInfo in [2] and the image
|
||||
// base the ThrowInfo's 32-bit offsets are relative to in [3] (magic 0x19930520..0x19930522 in [0]). The ThrowInfo lists
|
||||
// the types a catch may match, the thrown type first; each type descriptor holds a vtable pointer, a spare pointer and
|
||||
// the decorated name. A std::exception keeps its what() text in the pointer right after its vtable pointer (MSVC's
|
||||
// __std_exception_data). Every read is checked: the exception may come from a damaged heap.
|
||||
typedef struct { uint32_t attributes; int32_t unwind, forward_compat, catchable_types; } CxxThrowInfo;
|
||||
typedef struct { uint32_t properties; int32_t type, this_mdisp, this_pdisp, this_vdisp, size, copy; } CxxCatchableType;
|
||||
|
||||
// Copies the string at p into buf (cut to n - 1 characters), checking each page before reading it; NULL when the first
|
||||
// byte cannot be read.
|
||||
static const char *copy_cstr(uintptr_t p, char *buf, size_t n) {
|
||||
size_t i = 0;
|
||||
for (; p && i + 1 < n; i++) {
|
||||
if ((!i || !((p + i) & 4095)) && !plat_mem_readable(p + i, 1)) break;
|
||||
if (!(buf[i] = ((const char *)p)[i])) return buf;
|
||||
}
|
||||
buf[i] = 0;
|
||||
return i ? buf : NULL;
|
||||
}
|
||||
|
||||
static void cxx_info(const EXCEPTION_RECORD *er, PlatCrash *x) {
|
||||
if (er->NumberParameters < 4 || er->ExceptionInformation[0] - 0x19930520u > 2) return;
|
||||
uintptr_t obj = er->ExceptionInformation[1], info = er->ExceptionInformation[2],
|
||||
base = er->ExceptionInformation[3];
|
||||
if (!info || !plat_mem_readable(info, sizeof(CxxThrowInfo))) return;
|
||||
const CxxThrowInfo *ti = (const void *)info;
|
||||
uintptr_t arr = base + (uintptr_t)(intptr_t)ti->catchable_types;
|
||||
if (!ti->catchable_types || !plat_mem_readable(arr, 4)) return;
|
||||
int32_t n = *(const int32_t *)arr;
|
||||
if (n <= 0 || n > 64 || !plat_mem_readable(arr + 4, 4u * (uint64_t)n)) return;
|
||||
static char type[128], what[256]; // the crash report runs once: crash_fn ends the process
|
||||
int exception = 0;
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
uintptr_t ct = base + (uintptr_t)(intptr_t)((const int32_t *)(arr + 4))[i];
|
||||
if (!plat_mem_readable(ct, sizeof(CxxCatchableType))) continue;
|
||||
uintptr_t td = base + (uintptr_t)(intptr_t)((const CxxCatchableType *)ct)->type;
|
||||
char name[64];
|
||||
if (!copy_cstr(td + 16, i ? name : type, i ? sizeof name : sizeof type)) // past vtable and spare pointers
|
||||
continue;
|
||||
if (!i) x->cxx_type = type;
|
||||
if (!strcmp(i ? name : type, ".?AVexception@std@@")) exception = 1;
|
||||
}
|
||||
if (exception && obj && plat_mem_readable(obj + 8, 8))
|
||||
x->cxx_what = copy_cstr(*(const uintptr_t *)(obj + 8), what, sizeof what);
|
||||
}
|
||||
|
||||
// The last-chance filter: an exception no handler took, in any module. crash_veh reports faults in this executable as
|
||||
@@ -723,14 +782,12 @@ static LONG WINAPI crash_filter(EXCEPTION_POINTERS *ep) {
|
||||
c.access = (int)er->ExceptionInformation[0], c.addr = er->ExceptionInformation[1];
|
||||
void *base = NULL;
|
||||
RtlPcToFileHeader((void *)rip, &base);
|
||||
MODULEINFO mi;
|
||||
uintptr_t exe = (uintptr_t)GetModuleHandleW(NULL);
|
||||
c.offset = rip - (uintptr_t)base;
|
||||
if (!base) c.module = "";
|
||||
else if ((uintptr_t)base != exe) c.module = image_name(base);
|
||||
if (c.module && GetModuleInformation(GetCurrentProcess(), (HMODULE)exe, &mi, sizeof mi))
|
||||
c.nframes = frames_in(ep->ContextRecord, exe, mi.SizeOfImage, c.frames,
|
||||
(int)(sizeof c.frames / sizeof *c.frames));
|
||||
if (c.module) frames_of(ep->ContextRecord, &c);
|
||||
if (code == 0xe06d7363u) cxx_info(er, &c);
|
||||
crash_fn(&c);
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
@@ -1389,6 +1446,27 @@ void *plat_lib_open(const char *name) {
|
||||
|
||||
void *plat_lib_sym(void *lib, const char *name) { return lib ? (void *)GetProcAddress((HMODULE)lib, name) : NULL; }
|
||||
|
||||
void plat_lib_error(char *out, size_t cap) { snprintf(out, cap, "error %lu", GetLastError()); }
|
||||
|
||||
int plat_ngx_core_path(char *out, size_t cap) {
|
||||
wchar_t dir[WPATH];
|
||||
DWORD n = sizeof dir;
|
||||
char d[PLAT_PATH_MAX];
|
||||
if (RegGetValueW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\NVIDIA Corporation\\Global\\NGXCore", L"FullPath", RRF_RT_REG_SZ,
|
||||
NULL, dir, &n) != ERROR_SUCCESS ||
|
||||
!WideCharToMultiByte(CP_UTF8, 0, dir, -1, d, (int)sizeof d, NULL, NULL))
|
||||
return 0;
|
||||
return snprintf(out, cap, "%s\\_nvngx.dll", d) < (int)cap;
|
||||
}
|
||||
|
||||
int plat_exe_dir_wide(wchar_t *out, size_t cap) {
|
||||
DWORD len = GetModuleFileNameW(NULL, out, (DWORD)cap);
|
||||
if (!len || len >= cap) return 0;
|
||||
while (len && out[len - 1] != L'\\' && out[len - 1] != L'/') len--;
|
||||
out[len] = 0;
|
||||
return len != 0;
|
||||
}
|
||||
|
||||
// ---- machine load: GetSystemTimes for the CPU, PDH (pdh.dll, loaded on first use) for the thermal zones and the GPU
|
||||
// engine and adapter memory counters that Task Manager reads.
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#version 450
|
||||
// Block-linear to linear copy of one texture level and layer on the GPU (vk_deswizzle.c), the same layout as
|
||||
// tex_deswizzle.c nvn_bl_offset: GOB = 64 bytes x 8 rows; a block is 1 GOB wide, 2^th GOBs high and 2^td deep, its
|
||||
// GOBs ordered y first, then z; blocks fill the level x first, then y, then z.
|
||||
// One invocation per 16-byte segment of the block-linear storage, in storage order, so the reads (from the pool's
|
||||
// imported buffer: guest memory, over PCIe on a discrete GPU) are contiguous; the writes go to a device-local buffer
|
||||
// with rows of pitch16 segments. Segments outside the level's rows, slices or row width are padding and skipped.
|
||||
layout(local_size_x = 64) in;
|
||||
layout(std430, set = 0, binding = 0) readonly buffer Src { uvec4 src[]; };
|
||||
layout(std430, set = 0, binding = 1) writeonly buffer Dst { uvec4 dst[]; };
|
||||
layout(push_constant) uniform Push {
|
||||
uint src_base; // first segment of the level and layer in the src binding
|
||||
uint dst_base; // first segment of its linear copy in the dst binding
|
||||
uint segs; // segments to visit: whole blocks covering the level
|
||||
uint pitch16; // segments per linear row (row bytes rounded up to 16)
|
||||
uint rows, slices, th, td;
|
||||
uint gobs_x, blocks_y;
|
||||
uint nx; // workgroups along x (the dispatch is 2D when one dimension is not enough)
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
uint s = (gl_WorkGroupID.y * pc.nx + gl_WorkGroupID.x) * 64u + gl_LocalInvocationID.x;
|
||||
if (s >= pc.segs) return;
|
||||
uint bh = 1u << pc.th, bd = 1u << pc.td;
|
||||
uint per_block = 32u << (pc.th + pc.td); // 512-byte GOBs of 32 segments each
|
||||
uint block = s / per_block, inb = s % per_block;
|
||||
uint gob = inb >> 5u, ing = inb & 31u;
|
||||
uint per_layer = pc.gobs_x * pc.blocks_y;
|
||||
uint bz = block / per_layer, rem = block % per_layer;
|
||||
uint by = rem / pc.gobs_x, bx = rem % pc.gobs_x;
|
||||
// Inside a GOB, segment bits: 4 = x % 64 / 32, 3..2 = y % 8 / 2, 1 = x % 32 / 16, 0 = y % 2.
|
||||
uint xseg = bx * 4u + ((ing >> 4u) & 1u) * 2u + ((ing >> 1u) & 1u);
|
||||
uint y = by * 8u * bh + (gob % bh) * 8u + ((ing >> 2u) & 3u) * 2u + (ing & 1u);
|
||||
uint z = bz * bd + gob / bh;
|
||||
if (xseg >= pc.pitch16 || y >= pc.rows || z >= pc.slices) return;
|
||||
dst[pc.dst_base + (z * pc.rows + y) * pc.pitch16 + xseg] = src[pc.src_base + s];
|
||||
}
|
||||
@@ -1100,6 +1100,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_MODE_DOCKED] = "Modo TV", [UI_MENU_MODE_HANDHELD] = "Modo portátil",
|
||||
|
||||
[UI_MENU_VOLUME] = "Volume", [UI_MENU_VOLUME_DESC] = "Volume geral do jogo.",
|
||||
[UI_MENU_AUDIO_CHANNELS] = "Alto-falantes",
|
||||
[UI_MENU_AUDIO_CHANNELS_DESC] = "Saída estéreo ou surround 5.1. Automático escolhe 5.1 quando a configuração de "
|
||||
"alto-falantes do Windows tem seis canais ou mais. Vale após reiniciar.",
|
||||
[UI_MENU_CHANNELS_AUTO] = "Automático", [UI_MENU_CHANNELS_STEREO] = "Estéreo", [UI_MENU_CHANNELS_51] = "Surround 5.1",
|
||||
|
||||
[UI_MENU_MOTION] = "Controles de movimento", [UI_MENU_MOTION_DESC] = "Mirar com o giroscópio do controle.",
|
||||
[UI_MENU_RUMBLE_STRENGTH] = "Intensidade da vibração",
|
||||
@@ -1647,6 +1651,9 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
"这样被打断的读取可能会永久卡住。",
|
||||
[UI_MENU_VOLUME] = "音量",
|
||||
[UI_MENU_VOLUME_DESC] = "游戏的主音量。",
|
||||
[UI_MENU_AUDIO_CHANNELS] = "扬声器",
|
||||
[UI_MENU_AUDIO_CHANNELS_DESC] = "立体声或 5.1 环绕声输出。选择自动时,若 Windows 扬声器设置有六个或更多声道则使用 5.1。重启后生效。",
|
||||
[UI_MENU_CHANNELS_AUTO] = "自动", [UI_MENU_CHANNELS_STEREO] = "立体声", [UI_MENU_CHANNELS_51] = "5.1 环绕声",
|
||||
[UI_MENU_MOTION] = "体感操作",
|
||||
[UI_MENU_MOTION_DESC] = "用手柄的陀螺仪瞄准。",
|
||||
[UI_MENU_RUMBLE_STRENGTH] = "振动强度",
|
||||
|
||||
+43
-1
@@ -329,6 +329,15 @@ void vk_defer_image(VkImage img, VmaAllocation alloc); // alloc NULL: vkDestro
|
||||
void vk_defer_view(VkImageView view);
|
||||
void vk_exec_image_gone(VkImage image); // destroy_tex, texture lock held: the replay's views of image go with it
|
||||
void vk_defer_buffer(VkBuffer buf, VmaAllocation alloc);
|
||||
// free(p) once the GPU is past the submission being recorded, like the deferrals above: host records that a recording
|
||||
// may still read through a pointer taken before another thread dropped them (vk_texture.c destroy_tex).
|
||||
void vk_defer_free(void *p);
|
||||
// Device-local scratch for the upload batch (storage, transfer src and dst), from a pool of its own (vk_memory.c).
|
||||
VkResult vk_scratch_buffer(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc);
|
||||
// Staging and scratch bytes the open upload batch holds until it completes. Large texture loads submit the batch early
|
||||
// (vk_texture.c UPLOAD_HELD_MAX) so the GPU starts on it and its buffers come back before the pools need new blocks.
|
||||
VkDeviceSize vk_upload_held(void);
|
||||
void vk_upload_pools_init(void); // vk_device.c, after the allocator: the staging and scratch pools and their blocks
|
||||
// ---- ASTC (vk_astc.c). TOTK_ASTC = auto | gpu | native | cpu | check; see vk_astc.c.
|
||||
enum { VK_ASTC_CPU, VK_ASTC_GPU, VK_ASTC_NATIVE, VK_ASTC_CHECK };
|
||||
int vk_astc_mode(void); // decided once per device (logged); VK_ASTC_CPU before vk_init
|
||||
@@ -352,7 +361,31 @@ void vk_astc_time_enable(int on);
|
||||
int vk_astc_time_get(uint32_t bw, uint32_t bh, uint64_t *texels, uint64_t *calls, double *us); // totals so far
|
||||
void vk_astc_time_summary(void); // logs the totals
|
||||
void vk_astc_release(void); // vk_device.c, before the device goes: frees the decoder's objects
|
||||
void vk_mem_note_submit(uint64_t timeline_value); // vk_sync.c: after each submit that signals ctx->timeline with this value
|
||||
// ---- Block-linear deswizzle on the GPU (vk_deswizzle.c). TOTK_DESWIZZLE = auto | gpu | cpu | check.
|
||||
enum { VK_DESWZ_CPU, VK_DESWZ_GPU, VK_DESWZ_CHECK };
|
||||
int vk_deswz_mode(void); // decided once per device (logged); VK_DESWZ_CPU before vk_init
|
||||
// One level and layer: its storage starts at byte src of the bound source range and covers segs 16-byte segments
|
||||
// (whole blocks, cut at the storage end); its linear copy goes to byte dst of the destination, rows of pitch16 segments,
|
||||
// rows x slices of them. th, td, gobs_x, blocks_y: as tex_deswizzle.c nvn_bl_offset.
|
||||
typedef struct {
|
||||
VkDeviceSize src, dst;
|
||||
uint32_t segs, pitch16, rows, slices, th, td, gobs_x, blocks_y;
|
||||
} VkDeswzRegion;
|
||||
// Records into cb a dispatch per region reading [src_off, src_off + src_size) of src (a storage buffer) and writing
|
||||
// dst (device-local, dst_size bytes). The caller records the barriers around it. 0 when nothing was recorded: no
|
||||
// pipeline, src_off not 16-byte aligned, a range over the device's storage-buffer limit, a region outside the ranges.
|
||||
int vk_deswz_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_off, VkDeviceSize src_size, VkBuffer dst,
|
||||
VkDeviceSize dst_size, const VkDeswzRegion *rg, int n);
|
||||
// 1 when vk_deswz_record will take these regions (their dst offsets aside) with destinations of up to max_dst bytes.
|
||||
int vk_deswz_fits(VkDeviceSize src_off, VkDeviceSize src_size, const VkDeswzRegion *rg, int n, VkDeviceSize max_dst);
|
||||
void vk_deswz_release(void); // vk_device.c, before the device goes
|
||||
// 1 when sp (from vk_mem_resolve) is an imported pool's buffer over guest memory itself, so a GPU read in a submission
|
||||
// sees every CPU write made before the submit; 0 for device memory and shadow storage (synced after the upload batch).
|
||||
int vk_mem_span_live(const VkMemSpan *sp);
|
||||
// vk_sync.c, when it starts recording the submission that will signal ctx->timeline with this value: objects destroyed
|
||||
// from then on are freed once the timeline reaches it.
|
||||
void vk_mem_note_fence(uint64_t timeline_value);
|
||||
void vk_mem_fault_log(void); // vk_device_fault_report: the deferred-free state, lock-free
|
||||
void vk_mem_collect(void); // frees what has completed
|
||||
typedef struct { uint64_t imported_pools, physical_pools, virtual_pools, import_failures, upload_batches; } VkMemStats;
|
||||
void vk_mem_stats(VkMemStats *s);
|
||||
@@ -384,6 +417,9 @@ typedef struct {
|
||||
uint8_t scale; // 1, or TOTK_SHADOW_SCALE for a shadow map made larger than the game's (vk_texture.c
|
||||
// shadow_scale_of): width and height above are the larger ones, the game's regions
|
||||
// are multiplied by it
|
||||
uint8_t gone; // the entry was dropped (vk_texture.c destroy_tex, under the texture lock): image and
|
||||
// record live until the submission being recorded is done, and nothing may be cached
|
||||
// by its image handle, which the driver can hand out again after that
|
||||
} VkTexImage;
|
||||
// The window texture the backend presents (vk_backend.c): *image and *format become the float picture the game copied
|
||||
// into it, when the last draw into it was that copy and the swapchain is HDR10.
|
||||
@@ -561,6 +597,12 @@ VkPipeline vk_pipeline_compute(const GfxShader *cs, const VkPipeInfo **info);
|
||||
// vk_pipeline_vertex_input_dynamic(), else from VkPipeState.vstream). Returns the vertex bindings (bit per binding) the
|
||||
// state's vertex input reads, whether set here or made into the pipeline.
|
||||
uint32_t vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s);
|
||||
// The replay's variant (one thread, TOTK_DYN_DELTA): valid 1 says every graphics pipeline bound in cb since the last call
|
||||
// was one of vk_pipeline_graphics*'s, which all declare the same dynamic states, and nothing else set them, so cb still
|
||||
// holds what that call recorded (Vulkan keeps dynamic state across such binds for the life of the command buffer);
|
||||
// then only the commands whose values differ are recorded. valid 0 (a new command buffer, or one whose state another
|
||||
// recorder changed) records everything. *cmds: the commands recorded (valid 0: 1). Returns as vk_pipeline_set_dynamic.
|
||||
uint32_t vk_pipeline_set_dynamic_delta(VkCommandBuffer cb, const VkPipeState *s, int valid, uint32_t *cmds);
|
||||
int vk_pipeline_vertex_input_dynamic(void); // 1: vertex input is VK_EXT_vertex_input_dynamic_state
|
||||
// Translator answers from the bound state: attr_type from the vertex attribute formats (vs), frag_out_type from the
|
||||
// colour target formats (fs). Other fields are left as they are; either pointer may be NULL.
|
||||
|
||||
+1
-5
@@ -463,13 +463,9 @@ int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, cons
|
||||
cap_in = (cap_in + al - 1) / al * al;
|
||||
cap_out = (cap_out + al - 1) / al * al;
|
||||
if (!bc) cap_bc = 0;
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = cap_in + cap_out + cap_bc,
|
||||
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
||||
VmaAllocationCreateInfo aci = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
VkBuffer sb;
|
||||
VmaAllocation sa;
|
||||
if (vmaCreateBuffer(c->vma, &bi, &aci, &sb, &sa, NULL) != VK_SUCCESS) {
|
||||
if (vk_scratch_buffer(cap_in + cap_out + cap_bc, &sb, &sa) != VK_SUCCESS) {
|
||||
if (plat_inc32(&n_fail) <= 16)
|
||||
rt_log("vk: ASTC scratch buffer of %llu bytes failed: texture left undecoded",
|
||||
(unsigned long long)(cap_in + cap_out));
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// Block-linear textures deswizzled by the GPU (runtime/shaders/bl_deswizzle.comp): vk_texture.c upload binds the
|
||||
// texture's storage in its pool's imported buffer and lets a compute pass write the linear copy that a buffer-to-image
|
||||
// copy (or the ASTC decoder) then takes, instead of tex_deswizzle.c walking every 16 bytes on the replay thread
|
||||
// (50-236 ms frames on entering new areas, 2026-11-07 play sessions). The same design as MacNativeGames' TotK port
|
||||
// (d2r_gfx nvn_mtl.m: deswizzle from the pool at GPU execution time), written for Vulkan from our CPU layout code.
|
||||
//
|
||||
// TOTK_DESWIZZLE = auto (default) / gpu: the compute pass where the storage is an imported pool (vk_mem_span_live),
|
||||
// the CPU otherwise; cpu: always the CPU; check: gpu, and every upload is also deswizzled on the CPU and compared
|
||||
// (waits for the GPU; for testing only).
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "bl_deswizzle_spv.h"
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
#include "vk_rec.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t src_base, dst_base, segs, pitch16, rows, slices, th, td, gobs_x, blocks_y, nx;
|
||||
} Push;
|
||||
|
||||
static PlatMutex g_lock = PLAT_MUTEX_INIT;
|
||||
static struct {
|
||||
VkDevice dev;
|
||||
int mode; // VK_DESWZ_*; -1 before the first vk_deswz_mode with a device
|
||||
VkDescriptorSetLayout dsl;
|
||||
VkPipelineLayout layout;
|
||||
VkPipeline pipe;
|
||||
PFN_vkCmdPushDescriptorSetKHR push;
|
||||
} G = {.mode = -1};
|
||||
static volatile int64_t n_regions, n_bytes;
|
||||
|
||||
static int create_pipeline(VkCtx *c) {
|
||||
G.push = (PFN_vkCmdPushDescriptorSetKHR)vkGetDeviceProcAddr(c->device, "vkCmdPushDescriptorSetKHR");
|
||||
if (!G.push) return 0;
|
||||
VkDescriptorSetLayoutBinding b[2];
|
||||
for (int i = 0; i < 2; i++)
|
||||
b[i] = (VkDescriptorSetLayoutBinding){.binding = (uint32_t)i, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT};
|
||||
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
||||
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
|
||||
.bindingCount = 2, .pBindings = b};
|
||||
if (vkCreateDescriptorSetLayout(c->device, &dci, NULL, &G.dsl) != VK_SUCCESS) return 0;
|
||||
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push)};
|
||||
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
||||
.pSetLayouts = &G.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
|
||||
if (vkCreatePipelineLayout(c->device, &li, NULL, &G.layout) != VK_SUCCESS) return 0;
|
||||
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof bl_deswizzle_spv,
|
||||
.pCode = bl_deswizzle_spv};
|
||||
VkShaderModule m = VK_NULL_HANDLE;
|
||||
if (vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS) return 0;
|
||||
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
||||
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
|
||||
.layout = G.layout};
|
||||
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &G.pipe);
|
||||
vkDestroyShaderModule(c->device, m, NULL);
|
||||
if (r != VK_SUCCESS) G.pipe = VK_NULL_HANDLE;
|
||||
return r == VK_SUCCESS;
|
||||
}
|
||||
|
||||
int vk_deswz_mode(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c) return VK_DESWZ_CPU;
|
||||
plat_mutex_lock_shared(&g_lock);
|
||||
int m = G.dev == c->device ? G.mode : -1;
|
||||
plat_mutex_unlock_shared(&g_lock);
|
||||
if (m >= 0) return m;
|
||||
plat_mutex_lock(&g_lock);
|
||||
if (G.dev != c->device) { // the old device's objects went with it (probes recreate the device)
|
||||
memset(&G, 0, sizeof G);
|
||||
G.dev = c->device;
|
||||
G.mode = -1;
|
||||
}
|
||||
if (G.mode < 0) {
|
||||
const char *e = cfg_get("TOTK_DESWIZZLE");
|
||||
if (!e || !*e) e = "auto";
|
||||
int want = !strcmp(e, "cpu") || !strcmp(e, "0") ? VK_DESWZ_CPU : !strcmp(e, "check") ? VK_DESWZ_CHECK : VK_DESWZ_GPU;
|
||||
if (strcmp(e, "auto") && strcmp(e, "gpu") && strcmp(e, "1") && strcmp(e, "cpu") && strcmp(e, "0") &&
|
||||
strcmp(e, "check"))
|
||||
rt_log("vk: TOTK_DESWIZZLE=%s is not auto, gpu, cpu or check: auto", e);
|
||||
if (want != VK_DESWZ_CPU && !create_pipeline(c)) {
|
||||
rt_log("vk: the deswizzle compute pass is unavailable on %s: textures deswizzle on the CPU", c->props.deviceName);
|
||||
want = VK_DESWZ_CPU;
|
||||
}
|
||||
G.mode = want;
|
||||
rt_log("vk: block-linear textures deswizzle on the %s%s", want == VK_DESWZ_CPU ? "CPU" : "GPU where the storage is "
|
||||
"an imported pool", want == VK_DESWZ_CHECK ? ", checked against the CPU (TOTK_DESWIZZLE=check)" : "");
|
||||
}
|
||||
m = G.mode;
|
||||
plat_mutex_unlock(&g_lock);
|
||||
return m;
|
||||
}
|
||||
|
||||
static uint32_t max_groups_x(const VkCtx *c) {
|
||||
uint32_t m = c->props.limits.maxComputeWorkGroupCount[0];
|
||||
return m > 65535 ? 65535 : m;
|
||||
}
|
||||
|
||||
// The bound source range starts at the alignment below src_off.
|
||||
static VkDeviceSize src_lo(const VkCtx *c, VkDeviceSize src_off) {
|
||||
VkDeviceSize al = c->props.limits.minStorageBufferOffsetAlignment;
|
||||
if (al < 16) al = 16;
|
||||
return src_off / al * al;
|
||||
}
|
||||
|
||||
int vk_deswz_fits(VkDeviceSize src_off, VkDeviceSize src_size, const VkDeswzRegion *rg, int n, VkDeviceSize max_dst) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c || !n || (src_off & 15) || vk_deswz_mode() == VK_DESWZ_CPU) return 0;
|
||||
VkDeviceSize range = src_off + src_size - src_lo(c, src_off), lim = c->props.limits.maxStorageBufferRange;
|
||||
if (range > lim || max_dst > lim) return 0;
|
||||
uint32_t maxg = max_groups_x(c);
|
||||
for (int i = 0; i < n; i++) { // the 32-bit segment indices of the shader stay inside the bindings
|
||||
uint64_t groups = ((uint64_t)rg[i].segs + 63) / 64, out = (uint64_t)rg[i].pitch16 * rg[i].rows * rg[i].slices;
|
||||
if ((rg[i].src & 15) || rg[i].src + (uint64_t)rg[i].segs * 16 > src_size || !rg[i].segs ||
|
||||
(src_off - src_lo(c, src_off) + rg[i].src) / 16 + rg[i].segs > UINT32_MAX || out > UINT32_MAX ||
|
||||
out * 16 > max_dst || (groups + maxg - 1) / maxg > c->props.limits.maxComputeWorkGroupCount[1])
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int vk_deswz_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_off, VkDeviceSize src_size, VkBuffer dst,
|
||||
VkDeviceSize dst_size, const VkDeswzRegion *rg, int n) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!vk_deswz_fits(src_off, src_size, rg, n, dst_size)) return 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
if ((rg[i].dst & 15) || rg[i].dst + (uint64_t)rg[i].pitch16 * 16 * rg[i].rows * rg[i].slices > dst_size) return 0;
|
||||
plat_mutex_lock_shared(&g_lock);
|
||||
VkPipeline pipe = G.dev == c->device ? G.pipe : VK_NULL_HANDLE;
|
||||
plat_mutex_unlock_shared(&g_lock);
|
||||
if (!pipe) return 0;
|
||||
VkDeviceSize lo = src_lo(c, src_off), range = src_off + src_size - lo;
|
||||
uint32_t maxg = max_groups_x(c);
|
||||
VkDescriptorBufferInfo dbi[2] = {{src, lo, range}, {dst, 0, dst_size}};
|
||||
VkWriteDescriptorSet w[2];
|
||||
for (int i = 0; i < 2; i++)
|
||||
w[i] = (VkWriteDescriptorSet){.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = (uint32_t)i,
|
||||
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.pBufferInfo = &dbi[i]};
|
||||
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
|
||||
rec_push(G.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, G.layout, 0, 2, w);
|
||||
uint64_t bytes = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const VkDeswzRegion *r = &rg[i];
|
||||
uint64_t groups = ((uint64_t)r->segs + 63) / 64;
|
||||
uint32_t nx = groups < maxg ? (uint32_t)groups : maxg, ny = (uint32_t)((groups + nx - 1) / nx);
|
||||
Push pc = {.src_base = (uint32_t)((src_off - lo + r->src) / 16), .dst_base = (uint32_t)(r->dst / 16),
|
||||
.segs = r->segs, .pitch16 = r->pitch16, .rows = r->rows, .slices = r->slices, .th = r->th, .td = r->td,
|
||||
.gobs_x = r->gobs_x, .blocks_y = r->blocks_y, .nx = nx};
|
||||
vkCmdPushConstants(cb, G.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
|
||||
vkCmdDispatch(cb, nx, ny, 1);
|
||||
bytes += (uint64_t)r->segs * 16;
|
||||
}
|
||||
int64_t before = plat_add64(&n_regions, n) - n;
|
||||
plat_add64(&n_bytes, (int64_t)bytes);
|
||||
if (!before || (before + n) / 4096 != before / 4096)
|
||||
rt_log("vk: deswizzle: %lld regions, %.1f MiB of block-linear storage deswizzled on the GPU so far",
|
||||
(long long)(before + n), (double)n_bytes / 1048576.0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void vk_deswz_release(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
plat_mutex_lock(&g_lock);
|
||||
if (c && G.dev == c->device) {
|
||||
if (G.pipe) vkDestroyPipeline(c->device, G.pipe, NULL);
|
||||
if (G.layout) vkDestroyPipelineLayout(c->device, G.layout, NULL);
|
||||
if (G.dsl) vkDestroyDescriptorSetLayout(c->device, G.dsl, NULL);
|
||||
}
|
||||
memset(&G, 0, sizeof G);
|
||||
G.mode = -1;
|
||||
plat_mutex_unlock(&g_lock);
|
||||
}
|
||||
+90
-9
@@ -16,9 +16,69 @@ static VkCtx g_vk;
|
||||
static int g_ready;
|
||||
static PlatMutex g_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
// ---- GPU address map for device faults
|
||||
// VK_EXT_device_address_binding_report: the driver reports each bind and unbind of an object's GPU virtual range through
|
||||
// the debug messenger. The newest AB_N events stay in a ring written without a lock (one atomic add each), so a device
|
||||
// fault names the object behind its address and whether it was unbound before the fault, which is memory freed while
|
||||
// a submission still used it. One event per image, buffer or memory block made or freed, none per draw.
|
||||
// TOTK_ADDR_REPORT=0 turns it off.
|
||||
#define AB_N (1u << 15)
|
||||
typedef struct { uint64_t seq, base, size, handle, issued, done; int32_t type; uint32_t unbind; } AbEvent;
|
||||
static AbEvent *ab_ring;
|
||||
static uint64_t ab_head;
|
||||
static int ab_want; // the instance has the messenger for it (create_instance)
|
||||
|
||||
static void ab_record(const VkDebugUtilsMessengerCallbackDataEXT *d) {
|
||||
const VkDeviceAddressBindingCallbackDataEXT *b = NULL;
|
||||
for (const VkBaseInStructure *p = d->pNext; p; p = p->pNext)
|
||||
if (p->sType == VK_STRUCTURE_TYPE_DEVICE_ADDRESS_BINDING_CALLBACK_DATA_EXT) b = (const void *)p;
|
||||
if (!b || !ab_ring) return;
|
||||
uint64_t i = __atomic_fetch_add(&ab_head, 1, __ATOMIC_RELAXED);
|
||||
AbEvent *e = &ab_ring[i & (AB_N - 1)];
|
||||
__atomic_store_n(&e->seq, 0, __ATOMIC_RELAXED);
|
||||
e->base = b->baseAddress, e->size = b->size;
|
||||
e->handle = d->objectCount ? d->pObjects[0].objectHandle : 0;
|
||||
e->type = d->objectCount ? (int32_t)d->pObjects[0].objectType : 0;
|
||||
e->unbind = b->bindingType == VK_DEVICE_ADDRESS_BINDING_TYPE_UNBIND_EXT;
|
||||
e->issued = vk_sync_issued(), e->done = vk_sync_fence_done();
|
||||
__atomic_store_n(&e->seq, i + 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
static const char *ab_type_name(int32_t t, char *buf, size_t n) {
|
||||
switch (t) {
|
||||
case VK_OBJECT_TYPE_IMAGE: return "image";
|
||||
case VK_OBJECT_TYPE_BUFFER: return "buffer";
|
||||
case VK_OBJECT_TYPE_DEVICE_MEMORY: return "memory";
|
||||
default: snprintf(buf, n, "object type %d", (int)t); return buf;
|
||||
}
|
||||
}
|
||||
|
||||
// The newest events whose range meets the precision-aligned range around a fault address, newest first.
|
||||
static void ab_report(uint64_t addr, uint64_t precision) {
|
||||
if (!ab_ring) return;
|
||||
uint64_t lo = precision > 1 ? addr & ~(precision - 1) : addr, hi = lo + (precision > 1 ? precision : 1);
|
||||
uint64_t head = __atomic_load_n(&ab_head, __ATOMIC_ACQUIRE), n = head < AB_N ? head : AB_N;
|
||||
int found = 0;
|
||||
for (uint64_t k = 1; k <= n && found < 6; k++) {
|
||||
const AbEvent *e = &ab_ring[(head - k) & (AB_N - 1)];
|
||||
if (__atomic_load_n(&e->seq, __ATOMIC_ACQUIRE) != head - k + 1) continue;
|
||||
if (e->base >= hi || e->base + e->size <= lo) continue;
|
||||
found++;
|
||||
char tn[32];
|
||||
rt_log("vk: fault address %#llx: %s %#llx [%#llx, +%#llx] %s %llu address events before the fault, "
|
||||
"submitted %llu, done %llu", (unsigned long long)addr, ab_type_name(e->type, tn, sizeof tn),(unsigned long long)e->handle,
|
||||
(unsigned long long)e->base, (unsigned long long)e->size, e->unbind ? "unbound" : "bound",
|
||||
(unsigned long long)k, (unsigned long long)e->issued, (unsigned long long)e->done);
|
||||
}
|
||||
if (!found) rt_log("vk: fault address %#llx: no binding in the last %llu address events", (unsigned long long)addr,
|
||||
(unsigned long long)n);
|
||||
}
|
||||
|
||||
static VKAPI_ATTR VkBool32 VKAPI_CALL debug_cb(VkDebugUtilsMessageSeverityFlagBitsEXT sev, VkDebugUtilsMessageTypeFlagsEXT type,
|
||||
const VkDebugUtilsMessengerCallbackDataEXT *d, void *user) {
|
||||
(void)type, (void)user;
|
||||
(void)user;
|
||||
if (type & VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT) { ab_record(d); return VK_FALSE; }
|
||||
if (sev < VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) return VK_FALSE; // info, asked for the address events
|
||||
int err = (sev & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0;
|
||||
if (err) plat_inc32(&g_vk.validation_errors);
|
||||
static volatile int32_t logged;
|
||||
@@ -76,7 +136,10 @@ static int create_instance(void) {
|
||||
rt_log("vk: TOTK_VK_VALIDATION set but %s is not installed; continuing without", layers[0]);
|
||||
validate = 0;
|
||||
}
|
||||
if (validate) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
|
||||
const char *ar = cfg_get("TOTK_ADDR_REPORT");
|
||||
ab_want = !(ar && !strcmp(ar, "0")) && instance_ext_present(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
||||
if (ab_want && !ab_ring && !(ab_ring = calloc(AB_N, sizeof *ab_ring))) ab_want = 0;
|
||||
if (validate || ab_want) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
|
||||
if (vk_hdr_wanted() && instance_ext_present(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) // HDR10 surface formats
|
||||
ext[n_ext++] = VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME;
|
||||
vk_dlaa_instance_exts(ext, &n_ext, 12);
|
||||
@@ -89,16 +152,18 @@ static int create_instance(void) {
|
||||
VkResult r = vkCreateInstance(&ci, NULL, &g_vk.instance);
|
||||
if (r != VK_SUCCESS) { rt_log("vk: vkCreateInstance failed (%d); is a Vulkan 1.3 driver installed?", (int)r); return 0; }
|
||||
g_vk.validation = validate;
|
||||
if (validate) {
|
||||
if (validate || ab_want) {
|
||||
PFN_vkCreateDebugUtilsMessengerEXT f =
|
||||
(PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(g_vk.instance, "vkCreateDebugUtilsMessengerEXT");
|
||||
VkDebugUtilsMessengerCreateInfoEXT mi = {
|
||||
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
|
||||
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
|
||||
.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
|
||||
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
|
||||
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT |
|
||||
(ab_want ? VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT : 0),
|
||||
.messageType = (validate ? VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
|
||||
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT : 0) |
|
||||
(ab_want ? VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT : 0),
|
||||
.pfnUserCallback = debug_cb};
|
||||
if (f) f(g_vk.instance, &mi, NULL, &g_vk.messenger);
|
||||
if (!f || f(g_vk.instance, &mi, NULL, &g_vk.messenger) != VK_SUCCESS) ab_want = 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -217,6 +282,7 @@ typedef struct {
|
||||
VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT gpl;
|
||||
VkPhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT fbl;
|
||||
VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR lr;
|
||||
VkPhysicalDeviceAddressBindingReportFeaturesEXT abr;
|
||||
} FeatureChain;
|
||||
|
||||
static int create_device(void) {
|
||||
@@ -251,6 +317,13 @@ static int create_device(void) {
|
||||
// Ryujinx's SPIR-V declares fragment shader interlock whenever the host reports it (vk_pipeline.c spirv_fixup).
|
||||
OPTIONAL_EXT(has_fragment_shader_interlock, VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, fsi,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT);
|
||||
// The GPU address map for device faults (ab_record), with the messenger made for it
|
||||
int has_abr = 0;
|
||||
if (ab_want && has_ext(ep, ne, VK_EXT_DEVICE_ADDRESS_BINDING_REPORT_EXTENSION_NAME)) {
|
||||
has_abr = 1;
|
||||
ext[n_ext++] = VK_EXT_DEVICE_ADDRESS_BINDING_REPORT_EXTENSION_NAME;
|
||||
CHAIN(abr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ADDRESS_BINDING_REPORT_FEATURES_EXT);
|
||||
}
|
||||
// Feedback loops (vk.h VkCtx.feedback): the layout, and barriers inside dynamic rendering
|
||||
int has_fbl = 0, has_lr = 0;
|
||||
if (has_ext(ep, ne, VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME) &&
|
||||
@@ -314,6 +387,10 @@ static int create_device(void) {
|
||||
else if (!h->textureCompressionBC) miss = "textureCompressionBC";
|
||||
if (miss) { rt_log("vk: required feature missing on %s: %s", g_vk.props.deviceName, miss); free(c); return 0; }
|
||||
if (!c->fault.deviceFault) g_vk.has_device_fault = 0; // vkGetDeviceFaultInfoEXT needs the feature, not just the extension
|
||||
rt_log("vk: GPU address map for device faults: %s",
|
||||
has_abr && c->abr.reportAddressBinding ? "on (VK_EXT_device_address_binding_report; TOTK_ADDR_REPORT=0 turns it off)"
|
||||
: !ab_want ? "off (TOTK_ADDR_REPORT=0 or no VK_EXT_debug_utils)"
|
||||
: "unavailable (no VK_EXT_device_address_binding_report)");
|
||||
g_vk.has_buffer_device_address = c->f12.bufferDeviceAddress;
|
||||
g_vk.has_astc_ldr = h->textureCompressionASTC_LDR;
|
||||
{
|
||||
@@ -376,6 +453,7 @@ static void destroy_all(void) {
|
||||
gfx_window_release_vk();
|
||||
if (g_vk.device) vkDeviceWaitIdle(g_vk.device);
|
||||
if (g_vk.device) vk_astc_release();
|
||||
if (g_vk.device) vk_deswz_release();
|
||||
if (g_vk.device) vk_crumbs_release();
|
||||
if (g_vk.timeline) vkDestroySemaphore(g_vk.device, g_vk.timeline, NULL);
|
||||
vk_headroom_stop();
|
||||
@@ -397,7 +475,7 @@ int vk_init(void) {
|
||||
plat_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
memset(&g_vk, 0, sizeof g_vk);
|
||||
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_headroom_start(), vk_dlaa_init();
|
||||
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_upload_pools_init(), vk_headroom_start(), vk_dlaa_init();
|
||||
else destroy_all();
|
||||
}
|
||||
int ok = g_ready;
|
||||
@@ -437,6 +515,7 @@ void vk_device_fault_report(VkResult r) {
|
||||
done = 1;
|
||||
vk_crumbs_report();
|
||||
vk_pass_marks_report();
|
||||
vk_mem_fault_log();
|
||||
VkCtx *c = vk_ctx();
|
||||
PFN_vkGetDeviceFaultInfoEXT get =
|
||||
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
|
||||
@@ -461,9 +540,11 @@ void vk_device_fault_report(VkResult r) {
|
||||
rt_log("vk: device fault: \"%s\" (%u address infos, %u vendor infos, vendor binary %llu bytes)", info.description,
|
||||
n.addressInfoCount, n.vendorInfoCount, (unsigned long long)binary);
|
||||
// The faulting access lies in the precision-aligned range around the reported address.
|
||||
for (uint32_t i = 0; i < n.addressInfoCount; i++)
|
||||
for (uint32_t i = 0; i < n.addressInfoCount; i++) {
|
||||
rt_log("vk: device fault address %u: %s at %#llx, precision %#llx", i, fault_address_type(ai[i].addressType),
|
||||
(unsigned long long)ai[i].reportedAddress, (unsigned long long)ai[i].addressPrecision);
|
||||
ab_report(ai[i].reportedAddress, ai[i].addressPrecision);
|
||||
}
|
||||
for (uint32_t i = 0; i < n.vendorInfoCount; i++)
|
||||
rt_log("vk: device fault vendor info %u: \"%s\" code %#llx data %#llx", i, vi[i].description,
|
||||
(unsigned long long)vi[i].vendorFaultCode, (unsigned long long)vi[i].vendorFaultData);
|
||||
|
||||
+7
-54
@@ -11,9 +11,6 @@
|
||||
// layout with a set-then-get round trip before anything relies on it.
|
||||
//
|
||||
// Where DLAA runs in the frame, the jitter and the motion vectors: see the sections below.
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -115,31 +112,19 @@ int vk_dlaa_wanted(void) {
|
||||
static int core_load(void) {
|
||||
if (N.tried) return N.loaded;
|
||||
N.tried = 1;
|
||||
#ifdef _WIN32
|
||||
char dir[MAX_PATH] = {0}, path[MAX_PATH + 32];
|
||||
DWORD n = sizeof dir;
|
||||
if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\NVIDIA Corporation\\Global\\NGXCore", "FullPath", RRF_RT_REG_SZ,
|
||||
NULL, dir, &n) != ERROR_SUCCESS) {
|
||||
char path[PLAT_PATH_MAX];
|
||||
if (!plat_ngx_core_path(path, sizeof path)) {
|
||||
rt_log("dlaa: the driver has no NGX core (registry NGXCore missing): DLAA off");
|
||||
return 0;
|
||||
}
|
||||
snprintf(path, sizeof path, "%s\\_nvngx.dll", dir);
|
||||
N.core = LoadLibraryA(path);
|
||||
if (!N.core) {
|
||||
rt_log("dlaa: cannot load %s (error %lu): DLAA off", path, GetLastError());
|
||||
return 0;
|
||||
}
|
||||
#define FN(f, name) N.f = (void *)GetProcAddress((HMODULE)N.core, name)
|
||||
#else
|
||||
// The driver's NGX core; the dynamic loader finds it where the driver put it (/usr/lib, /usr/lib/x86_64-linux-gnu).
|
||||
const char *path = "libnvidia-ngx.so.1";
|
||||
N.core = plat_lib_open(path);
|
||||
if (!N.core) {
|
||||
rt_log("dlaa: cannot load %s (not an NVIDIA driver, or one without NGX): DLAA off", path);
|
||||
char why[256];
|
||||
plat_lib_error(why, sizeof why);
|
||||
rt_log("dlaa: cannot load %s (%s; not an NVIDIA driver, or one without NGX): DLAA off", path, why);
|
||||
return 0;
|
||||
}
|
||||
#define FN(f, name) N.f = plat_lib_sym(N.core, name)
|
||||
#endif
|
||||
FN(init, "NVSDK_NGX_VULKAN_Init_Ext2");
|
||||
FN(req_ext, "NVSDK_NGX_VULKAN_RequiredExtensions");
|
||||
FN(caps, "NVSDK_NGX_VULKAN_GetCapabilityParameters");
|
||||
@@ -203,46 +188,14 @@ static int vtable_ok(void) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
|
||||
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
|
||||
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
|
||||
const unsigned char *u = (const unsigned char *)in;
|
||||
size_t n = 0;
|
||||
while (*u) {
|
||||
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
|
||||
if (more == 4) return 0;
|
||||
if (more) c &= 0x3F >> more;
|
||||
for (unsigned k = 0; k < more; k++, u++) {
|
||||
if ((*u & 0xC0) != 0x80) return 0;
|
||||
c = c << 6 | (*u & 0x3F);
|
||||
}
|
||||
if (n + 1 >= cap) return 0;
|
||||
out[n++] = (wchar_t)c;
|
||||
}
|
||||
out[n] = 0;
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
void vk_dlaa_init(void) {
|
||||
if (!vk_dlaa_wanted() || !core_load()) return;
|
||||
#ifdef _WIN32
|
||||
wchar_t wdir[MAX_PATH];
|
||||
DWORD len = GetModuleFileNameW(NULL, wdir, MAX_PATH);
|
||||
while (len && wdir[len - 1] != L'\\') len--;
|
||||
wdir[len] = 0;
|
||||
#else
|
||||
// The executable's folder, where NGX looks for libnvidia-ngx-dlss.so and writes its logs, as a wchar_t string:
|
||||
// 32-bit code points here, as the Linux core expects.
|
||||
char dir[PLAT_PATH_MAX];
|
||||
// The executable's folder, where NGX looks for the DLSS model library and writes its logs.
|
||||
wchar_t wdir[PLAT_PATH_MAX];
|
||||
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
|
||||
if (!cut || !utf8_to_wide((cut[1] = 0, dir), wdir, PLAT_PATH_MAX)) {
|
||||
if (!plat_exe_dir_wide(wdir, PLAT_PATH_MAX)) {
|
||||
rt_log("dlaa: cannot name the executable's folder for NGX: DLAA off");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// 231313132: the application id NVIDIA's DLSS samples use for builds without an NVIDIA-issued id
|
||||
const VkCtx *c = vk_ctx();
|
||||
NgxResult r = N.init(231313132ull, wdir, c->instance, c->phys, c->device, vkGetInstanceProcAddr,
|
||||
|
||||
@@ -183,6 +183,7 @@ static void memory_barrier(VkCommandBuffer cb, VkPipelineStageFlags2 src, VkAcce
|
||||
// be made (logged). Replay thread.
|
||||
static int bc_standin(const VkTexImage *img, uint32_t level, VkFormat fmt, BcStandIn *out) {
|
||||
static int warned;
|
||||
if (img->gone) return 0; // dropped since the lookup: a stand-in kept by its handle would outlive it
|
||||
plat_mutex_lock(&bcs_lock);
|
||||
int full = nbcs == BC_STANDINS;
|
||||
for (int i = 0; i < nbcs; i++)
|
||||
|
||||
+87
-22
@@ -99,6 +99,8 @@ static struct {
|
||||
VkPipeline pipe[2]; // bound per bind point: 0 graphics, 1 compute
|
||||
VkPipeState st; // the state vk_pipeline_set_dynamic last recorded
|
||||
int st_valid;
|
||||
int dyn_valid; // vk_pipeline_set_dynamic_delta's record of this command buffer's dynamic state
|
||||
// holds (TOTK_DYN_DELTA); pipeline binds keep it, the memset clears it
|
||||
uint32_t vmask; // the vertex bindings st's vertex input reads (valid with st_valid)
|
||||
uint32_t vres; // the streams whose vb entry was resolved from vin (others: the zero buffer)
|
||||
VkBuffer vb[17]; // vertex buffers last bound
|
||||
@@ -139,6 +141,10 @@ static struct {
|
||||
uint64_t draws, bind[2], desc[2], push, push_skip, set_new, set_reuse, ac_miss, same_prog, same_all, bind_skip;
|
||||
} BS = {-1};
|
||||
|
||||
// TOTK_DYN_DELTA (prepare_draw): the mode and, for the perf line, the set_dynamic calls, the commands they recorded
|
||||
// (a full call counts 1) and the calls that recorded none.
|
||||
static struct { int mode; uint64_t calls, cmds, empty; } DD = {-1};
|
||||
|
||||
// The inputs of the last graphics bind_resources (bind_sig) and what its build counted for the traffic census, which a
|
||||
// skipped build counts again. RS_BYTES bounds the bound-slot bytes a pipeline reads (RsList): per binding at most one
|
||||
// c0 storage table, otherwise one bound range or handle per descriptor.
|
||||
@@ -175,7 +181,8 @@ static struct {
|
||||
struct { uint64_t dst; VkBuffer src; VkDeviceSize soff; VkMemSpan sp; uint64_t size; } p[OV_N]; // to copy
|
||||
struct { uint64_t lo, hi; } f[OV_FOREIGN]; // GPU writes of other kinds
|
||||
// since the last perf line (vk_exec_ub_perf)
|
||||
uint64_t c_update, c_overlaid, c_flush, c_bind, c_merged, c_mixed, c_inpass, c_outside, c_early;
|
||||
uint64_t c_update, c_overlaid, c_overlaid_out, c_flush, c_bind, c_merged, c_mixed, c_inpass, c_outside, c_early,
|
||||
c_order;
|
||||
} OV = {.mode = -1};
|
||||
|
||||
// A binding without a size reaches to the end of its backing: as far as the overlays go, for ov_at.
|
||||
@@ -586,6 +593,10 @@ static VkImageView get_view(VkExec *ex, const VkTexImage *img, const view_t *v,
|
||||
if (img->block_views) ci.pNext = &use; // the image's COLOR_ATTACHMENT usage is its block views' (vk_texture.c)
|
||||
VkImageView view;
|
||||
if (vkCreateImageView(vk_ctx()->device, &ci, NULL, &view) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
if (img->gone) { // dropped since the lookup: its views were purged and a new one would outlive the image
|
||||
vk_defer_view(view);
|
||||
return view;
|
||||
}
|
||||
ViewNode *n = malloc(sizeof *n);
|
||||
if (!n) rt_fatal("vk: out of memory");
|
||||
*n = (ViewNode){nvn_hm_get(&views_by_image, &img->image), k, view};
|
||||
@@ -612,11 +623,15 @@ VkImageView vk_exec_target_view(const VkTexImage *img, VkFormat format, VkImageV
|
||||
if (blocks) ci.pNext = &use; // not the BC image's sampling and transfer usages
|
||||
if (vkCreateImageView(vk_ctx()->device, &ci, NULL, &view) != VK_SUCCESS)
|
||||
rt_fatal("vk: render target view (format %d) could not be created", (int)format);
|
||||
ViewNode *n = malloc(sizeof *n);
|
||||
if (!n) rt_fatal("vk: out of memory");
|
||||
*n = (ViewNode){nvn_hm_get(&views_by_image, &img->image), k, view};
|
||||
nvn_hm_put(&views_by_image, &img->image, n);
|
||||
nvn_hm_put(&view_of, &k, (void *)view);
|
||||
if (img->gone) { // dropped since resolve_target (get_view)
|
||||
vk_defer_view(view);
|
||||
} else {
|
||||
ViewNode *n = malloc(sizeof *n);
|
||||
if (!n) rt_fatal("vk: out of memory");
|
||||
*n = (ViewNode){nvn_hm_get(&views_by_image, &img->image), k, view};
|
||||
nvn_hm_put(&views_by_image, &img->image, n);
|
||||
nvn_hm_put(&view_of, &k, (void *)view);
|
||||
}
|
||||
}
|
||||
vk_texture_unlock();
|
||||
return view;
|
||||
@@ -2289,8 +2304,20 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
|
||||
if (!p || !info) return warn_once(W_PIPELINE, "a graphics pipeline could not be created: draws with it are skipped"), 0;
|
||||
bind_pipeline(ex, VK_PIPELINE_BIND_POINT_GRAPHICS, p);
|
||||
if (!B.st_valid || memcmp(&B.st, &s, sizeof s)) {
|
||||
B.vmask = vk_pipeline_set_dynamic(ex->cb, &s);
|
||||
ex->recorded++;
|
||||
// TOTK_DYN_DELTA: only the dynamic states whose values changed since the last call in this command buffer. A
|
||||
// pipeline bind clears st_valid but not dyn_valid: every pipeline here declares the same dynamic states, which
|
||||
// the bind leaves as they were (the viewport, scissor and stencil values of record_fixed rely on that too).
|
||||
if (DD.mode < 0) DD.mode = rt_knob("DYN_DELTA", "1");
|
||||
uint32_t cmds = 1;
|
||||
if (RT_KNOB_ON(DD.mode)) {
|
||||
B.vmask = vk_pipeline_set_dynamic_delta(ex->cb, &s, B.dyn_valid, &cmds);
|
||||
B.dyn_valid = 1;
|
||||
} else {
|
||||
B.vmask = vk_pipeline_set_dynamic(ex->cb, &s);
|
||||
B.dyn_valid = 0; // the delta's record is stale from here on
|
||||
}
|
||||
DD.calls++, DD.cmds += cmds, DD.empty += !cmds;
|
||||
ex->recorded += cmds != 0;
|
||||
B.st = s, B.st_valid = 1;
|
||||
if (ex->mark && vk_exec_frame.draws == ex->mark_draws0) ex->mark->first[0] = vs, ex->mark->first[1] = fs;
|
||||
}
|
||||
@@ -3254,11 +3281,11 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
|
||||
// it read the new bytes, those before it the old ones. Replayed as a buffer copy at that point it needs a barrier
|
||||
// against the earlier draws, and a barrier ends the open pass; the terrain and the sky islands' meshes update before
|
||||
// most of their draws, about 200 updates inside passes in a field submission, each a pass end, a full barrier and a
|
||||
// pass begin. Here an update inside a pass goes into the scratch buffer as an overlay: the later bindings of a range it
|
||||
// covers bind the scratch bytes (ub_bind, from ubo_info and bind_range), and the copies into the real buffers wait for
|
||||
// the next full barrier, where the pass has ended anyway (vk_exec_ub_flush from vk_exec.c barrier_to: a pass end, a
|
||||
// dispatch, a transfer, the end of the submission), behind one barrier of their own. Each draw reads the bytes it would
|
||||
// have read with the copy in between.
|
||||
// pass begin. Here an update inside a pass, and with TOTK_UB_OVERLAY_OUT one between passes, goes into the scratch
|
||||
// buffer as an overlay: the later bindings of a range it covers bind the scratch bytes (ub_bind, from ubo_info and
|
||||
// bind_range), and the copies into the real buffers wait for the next full barrier, where the pass has ended anyway
|
||||
// (vk_exec_ub_flush from vk_exec.c barrier_to: a pass end, a dispatch, a transfer, the end of the submission), behind
|
||||
// one barrier of their own. Each draw reads the bytes it would have read with the copy in between.
|
||||
// - A GPU write of another kind (a copy, a fill, a shader's storage write) is ordered with the copies only by the
|
||||
// command buffer. vk_exec_ub_foreign notes it and retires the overlays it touches; an update over such bytes takes
|
||||
// the old path. A shader that writes a range with copies pending gets them first (ub_written: the pass restarts).
|
||||
@@ -3274,6 +3301,16 @@ static int ov_on(void) {
|
||||
return RT_KNOB_ON(OV.mode);
|
||||
}
|
||||
|
||||
// TOTK_UB_OVERLAY_OUT: updates between passes become overlays too. Copied at once, each needs a full barrier of its
|
||||
// own, and the copy it records makes the next update or pass begin need another: a run of k updates before a pass cost
|
||||
// k + 1 barriers. Every GPU reader of the real buffer after them comes behind barrier_to, which records the copies
|
||||
// first (a pass begin, a dispatch, a transfer, the end of the submission), so they cost one barrier for the run.
|
||||
static int ov_out_on(void) {
|
||||
static int mode = -1;
|
||||
if (mode < 0) mode = rt_knob("UB_OVERLAY_OUT", "1");
|
||||
return RT_KNOB_ON(mode);
|
||||
}
|
||||
|
||||
static void ov_sub(const VkExec *ex) { // the submission being replayed
|
||||
if (OV.fence == ex->fence) return;
|
||||
if (OV.np) // barrier_to records them at the latest at the submission's closing barrier
|
||||
@@ -3349,27 +3386,53 @@ void vk_exec_ub_flush(VkExec *ex) {
|
||||
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
|
||||
vkCmdPipelineBarrier2(ex->cb, &di);
|
||||
ex->recorded++;
|
||||
// Copies without a barrier between them may run in any order, and ov_add only drops an older copy of exactly the
|
||||
// same range: a copy that overlaps one recorded since the last barrier waits for it, so the newer bytes land last.
|
||||
VkMemoryBarrier2 ww = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT};
|
||||
VkDependencyInfo wdi = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &ww};
|
||||
uint32_t since = 0; // the first copy recorded after the last barrier
|
||||
for (uint32_t i = 0; i < OV.np; i++) {
|
||||
for (uint32_t k = since; k < i; k++)
|
||||
if (OV.p[i].dst < OV.p[k].dst + OV.p[k].size && OV.p[k].dst < OV.p[i].dst + OV.p[i].size) {
|
||||
vkCmdPipelineBarrier2(ex->cb, &wdi);
|
||||
ex->recorded++;
|
||||
OV.c_order++;
|
||||
since = i;
|
||||
break;
|
||||
}
|
||||
VkBufferCopy cp = {OV.p[i].soff, OV.p[i].sp.offset, OV.p[i].size};
|
||||
vkCmdCopyBuffer(ex->cb, OV.p[i].src, OV.p[i].sp.buffer, 1, &cp);
|
||||
ex->recorded++;
|
||||
}
|
||||
OV.np = 0;
|
||||
// The full barrier barrier_to records next makes the copied bytes visible: later bindings read the real buffer,
|
||||
// as after an update copied at once, and need no overlay lookups or merged copies (ov_at answers 0 for them, so
|
||||
// the descriptor build skip rebuilds the draws that bound an overlay)
|
||||
OV.n = 0;
|
||||
ov_span();
|
||||
OV.c_flush++;
|
||||
}
|
||||
|
||||
// TOTK_TRACE=perf, every 600 submissions: per submission, the updates and how many stayed in their pass.
|
||||
void vk_exec_ub_perf(void) {
|
||||
static unsigned subs;
|
||||
static unsigned subs, dsubs;
|
||||
if (DD.mode >= 0 && ++dsubs % 600 == 0 && DD.calls) {
|
||||
rt_log("perf: dynamic state per submission: %.1f calls, %.1f commands (a full set counts 1), %.1f calls with "
|
||||
"nothing changed%s", DD.calls / 600.0, DD.cmds / 600.0, DD.empty / 600.0,
|
||||
DD.mode == 2 ? " (TOTK_DYN_DELTA=ab)" : DD.mode ? "" : " (TOTK_DYN_DELTA=0)");
|
||||
DD.calls = DD.cmds = DD.empty = 0;
|
||||
}
|
||||
if (OV.mode <= 0 || ++subs % 600) return;
|
||||
const double k = 1 / 600.0;
|
||||
rt_log("perf: uniform updates per submission %.1f: %.1f overlaid in a pass (copied in %.2f batches), %.1f copied "
|
||||
"in a pass, %.1f outside one; bindings from an overlay %.1f (%.1f merged, %.1f over other GPU writes), %.2f "
|
||||
"batches brought forward for a storage write",
|
||||
OV.c_update * k, OV.c_overlaid * k, OV.c_flush * k, OV.c_inpass * k, OV.c_outside * k, OV.c_bind * k,
|
||||
OV.c_merged * k, OV.c_mixed * k, OV.c_early * k);
|
||||
OV.c_update = OV.c_overlaid = OV.c_flush = OV.c_bind = OV.c_merged = OV.c_mixed = 0;
|
||||
OV.c_inpass = OV.c_outside = OV.c_early = 0;
|
||||
rt_log("perf: uniform updates per submission %.1f: %.1f overlaid (%.1f of them between passes; copied in %.2f "
|
||||
"batches), %.1f copied in a pass, %.1f outside one; bindings from an overlay %.1f (%.1f merged, %.1f over "
|
||||
"other GPU writes), %.2f batches brought forward for a storage write, %.2f barriers between overlapping copies",
|
||||
OV.c_update * k, OV.c_overlaid * k, OV.c_overlaid_out * k, OV.c_flush * k, OV.c_inpass * k, OV.c_outside * k,
|
||||
OV.c_bind * k, OV.c_merged * k, OV.c_mixed * k, OV.c_early * k, OV.c_order * k);
|
||||
OV.c_update = OV.c_overlaid = OV.c_overlaid_out = OV.c_flush = OV.c_bind = OV.c_merged = OV.c_mixed = 0;
|
||||
OV.c_inpass = OV.c_outside = OV.c_early = OV.c_order = 0;
|
||||
}
|
||||
|
||||
// The scratch bytes of a uniform or storage binding [addr, addr+n) an overlay of this submission touches: 1 with *out
|
||||
@@ -3433,7 +3496,8 @@ static void op_update_ub(VkExec *ex, const p_update_ub *u) {
|
||||
if (ov_on()) {
|
||||
ov_sub(ex);
|
||||
OV.c_update++;
|
||||
if (ex->in_pass && OV.n < OV_N && OV.np < OV_N && !ov_foreign_hits(u->dst, u->dst + u->size)) {
|
||||
int here = ex->in_pass || ov_out_on();
|
||||
if (here && OV.n < OV_N && OV.np < OV_N && !ov_foreign_hits(u->dst, u->dst + u->size)) {
|
||||
uint8_t *d = scratch(ex, u->size, &b, &o);
|
||||
memcpy(d, u->data, u->size);
|
||||
VK_MEM_PICK(sp);
|
||||
@@ -3444,6 +3508,7 @@ static void op_update_ub(VkExec *ex, const p_update_ub *u) {
|
||||
note_written(ex, u->dst, u->size);
|
||||
upd_note(ex, u);
|
||||
OV.c_overlaid++;
|
||||
if (!ex->in_pass) OV.c_overlaid_out++;
|
||||
return;
|
||||
}
|
||||
if (ex->in_pass) OV.c_inpass++;
|
||||
|
||||
+1
-1
@@ -272,7 +272,7 @@ void vk_hdr_draw(VkExec *ex, const VkPipeInfo *info) {
|
||||
}
|
||||
if (ntex == 1 && slot >= 0 && slot < 128 && ex->tex[1][slot]) {
|
||||
const VkTexImage *s = vk_texture_by_id(NVN_HANDLE_TEX(ex->tex[1][slot]), NULL, NULL);
|
||||
if (s && is_float(s->format) && s->width == t->width && s->height == t->height && s->samples == 1)
|
||||
if (s && !s->gone && is_float(s->format) && s->width == t->width && s->height == t->height && s->samples == 1)
|
||||
src = s->image, fmt = s->format;
|
||||
}
|
||||
plat_mutex_lock(&rec_lock);
|
||||
|
||||
+132
-27
@@ -78,8 +78,9 @@ static int shadow_on; // shadow mode: imported pools get shad
|
||||
static int shadow_pool(VkCtx *c, PoolRec *r, uint64_t addr, uint64_t size);
|
||||
static void shadow_touch(PoolRec *r);
|
||||
static void shadow_forget(PoolRec *r);
|
||||
static void mirror_retiring(PoolRec *r); // vk_pool_fini: kept until mirror_retire (or vk_mem_shutdown)
|
||||
static void mirror_retire(void *arg); // completion thread: destroys a finalized mirrored pool's record
|
||||
static void pool_retiring(PoolRec *r); // vk_pool_fini: kept until pool_retire (or vk_mem_shutdown)
|
||||
static void pool_retire(void *arg); // completion thread: destroys a finalized pool's record
|
||||
static uint64_t retire_value(void); // the timeline value after which a finalized pool's record may go
|
||||
static void grow(void **arr, int *cap, size_t elem);
|
||||
|
||||
static PlatMutex pool_lock = PLAT_MUTEX_INIT;
|
||||
@@ -333,10 +334,14 @@ void vk_pool_fini(pool_t *p) {
|
||||
vk_queue_unlock();
|
||||
}
|
||||
vk_mem_collect();
|
||||
// A mirrored pool's record and buffers outlive the submission being recorded, whose sync copies and write-backs may
|
||||
// name them (the game frees a pool only once the GPU is done with it, but its CPU writes may still be queued).
|
||||
if (r->mbuf) mirror_retiring(r), vk_completion_add(vk_sync_issued() + 1, mirror_retire, r);
|
||||
else destroy_rec(c, r);
|
||||
// The record, its buffers and its memory outlive the submission being recorded, of any pool kind. The wait above
|
||||
// covers submitted work only. With the synchronous replay another game thread may be recording right now, and that
|
||||
// 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.
|
||||
pool_retiring(r);
|
||||
vk_completion_add(retire_value(), pool_retire, r);
|
||||
}
|
||||
|
||||
// Requests were checked by the front end (nvn_api_core.c, MNG totk_vpool.c); apply them in order.
|
||||
@@ -416,6 +421,10 @@ int vk_mem_resolve(uint64_t addr, uint64_t size, VkMemSpan *out) {
|
||||
return resolve_locked(addr, size, out);
|
||||
}
|
||||
|
||||
int vk_mem_span_live(const VkMemSpan *sp) {
|
||||
return sp->buffer && sp->pool && !sp->alloc && !shadow_on && ((const PoolRec *)sp->pool)->kind == POOL_IMPORT;
|
||||
}
|
||||
|
||||
// The pool index lookup under the lock; a non-overlapped import or physical pool goes into the address's cache slot.
|
||||
static int resolve_locked(uint64_t addr, uint64_t size, VkMemSpan *out) {
|
||||
memset(out, 0, sizeof *out);
|
||||
@@ -507,6 +516,9 @@ static void dirty_frame(uint64_t frame);
|
||||
static void mirror_log(uint64_t frame);
|
||||
static void shadow_log(uint64_t frame);
|
||||
static int dead_pending(void);
|
||||
// Deferrals made off the recording thread while another thread recorded a submission (cmd_lock, defer), the newest
|
||||
// one's stamp: the case the stamp from the recording start covers. In the census line and the device fault report.
|
||||
static uint64_t n_cross_defer, cross_defer_tl;
|
||||
|
||||
static void vram_log(uint64_t frame);
|
||||
|
||||
@@ -542,10 +554,11 @@ void vk_mem_census(uint64_t frame) {
|
||||
}
|
||||
plat_mutex_unlock_shared(&pool_lock);
|
||||
rt_log("mem: census at frame %llu: import %llu pools %.1f MiB (%d without a GPU buffer), physical %llu pools %.1f MiB, "
|
||||
"virtual %llu pools %.1f MiB of address space",
|
||||
"virtual %llu pools %.1f MiB of address space; %llu objects deferred off the recording thread during a recording",
|
||||
(unsigned long long)frame, (unsigned long long)count[POOL_IMPORT], total[POOL_IMPORT] / 1048576.0,
|
||||
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)count[POOL_VIRT], total[POOL_VIRT] / 1048576.0,
|
||||
(unsigned long long)__atomic_load_n(&n_cross_defer, __ATOMIC_RELAXED));
|
||||
static const char *const kinds[] = {"", "import", "physical", "virtual"};
|
||||
for (int j = 0; j < ng; j++) {
|
||||
char f[160];
|
||||
@@ -1390,19 +1403,19 @@ static void mirror_forget(PoolRec *r) {
|
||||
}
|
||||
}
|
||||
|
||||
// Finalized mirrored pools waiting for mirror_retire; vk_mem_shutdown destroys those whose callback never ran.
|
||||
// Finalized pools waiting for pool_retire; vk_mem_shutdown destroys those whose callback never ran.
|
||||
static PoolRec **retiring;
|
||||
static int nretiring, cretiring;
|
||||
static PlatMutex retire_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
static void mirror_retiring(PoolRec *r) {
|
||||
static void pool_retiring(PoolRec *r) {
|
||||
plat_mutex_lock(&retire_lock);
|
||||
if (nretiring == cretiring) grow((void **)&retiring, &cretiring, sizeof *retiring);
|
||||
retiring[nretiring++] = r;
|
||||
plat_mutex_unlock(&retire_lock);
|
||||
}
|
||||
|
||||
static void mirror_retire(void *arg) {
|
||||
static void pool_retire(void *arg) {
|
||||
plat_mutex_lock(&retire_lock);
|
||||
for (int i = 0; i < nretiring; i++)
|
||||
if (retiring[i] == arg) { retiring[i] = retiring[--nretiring]; break; }
|
||||
@@ -2152,9 +2165,10 @@ typedef struct {
|
||||
uint64_t seq;
|
||||
Staging *stg;
|
||||
int nstg, cap;
|
||||
VkDeviceSize held; // staging and scratch bytes freed when the batch completes (vk_upload_held)
|
||||
} Batch;
|
||||
|
||||
enum { DEAD_IMAGE = 1, DEAD_IMAGE_VMA, DEAD_VIEW, DEAD_BUFFER_VMA };
|
||||
enum { DEAD_IMAGE = 1, DEAD_IMAGE_VMA, DEAD_VIEW, DEAD_BUFFER_VMA, DEAD_HOST }; // DEAD_HOST: free(handle)
|
||||
typedef struct { int kind; uint64_t handle; VmaAllocation alloc; uint64_t seq, tl; } Dead;
|
||||
|
||||
static PlatRMutex cmd_lock;
|
||||
@@ -2169,7 +2183,8 @@ static int nspare, cap_spare;
|
||||
static uint64_t next_seq, done_seq; // batches handed out / completed (all batches up to done_seq)
|
||||
static Dead *dead;
|
||||
static int ndead, cap_dead;
|
||||
static uint64_t tl_noted; // newest timeline value vk_sync.c reported submitted
|
||||
static uint64_t tl_noted; // value of the submission being recorded, else of the last one (vk_mem_note_fence)
|
||||
static _Thread_local uint64_t tl_noted_here; // the value this thread noted: it records that submission
|
||||
|
||||
static void cmd_init(void *p) {
|
||||
(void)p;
|
||||
@@ -2212,6 +2227,10 @@ static void collect_locked(VkCtx *c) {
|
||||
for (int i = 0; i < ndead; i++) {
|
||||
Dead *d = &dead[i];
|
||||
if (d->seq > done_seq || d->tl > tl) { dead[k++] = *d; continue; }
|
||||
if (d->kind == DEAD_HOST) {
|
||||
free((void *)(uintptr_t)d->handle);
|
||||
continue;
|
||||
}
|
||||
rec_check_handle(d->kind == DEAD_VIEW ? REC_VIEW : d->kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, d->handle, 1,
|
||||
d->tl, tl);
|
||||
switch (d->kind) {
|
||||
@@ -2305,10 +2324,68 @@ void *vk_staging(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc) {
|
||||
else {
|
||||
if (open_b.nstg == open_b.cap) grow((void **)&open_b.stg, &open_b.cap, sizeof *open_b.stg);
|
||||
open_b.stg[open_b.nstg++] = (Staging){b, a};
|
||||
open_b.held += bi.size;
|
||||
}
|
||||
return info.pMappedData;
|
||||
}
|
||||
|
||||
// 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
|
||||
// of SCR_BLOCK is kept from the first use; larger buffers than half a block go to the default pool.
|
||||
#define SCR_BLOCK (64ull << 20)
|
||||
static VmaPool scr_pool;
|
||||
static int scr_pool_tried;
|
||||
|
||||
VkResult vk_scratch_buffer(VkDeviceSize size, VkBuffer *buf, VmaAllocation *alloc) {
|
||||
VkCtx *c = vk_ctx();
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = size ? size : 16,
|
||||
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
||||
VmaAllocationCreateInfo aci = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
if (!__atomic_load_n(&scr_pool_tried, __ATOMIC_ACQUIRE)) {
|
||||
plat_once(&cmd_once, cmd_init, NULL);
|
||||
plat_rmutex_lock(&cmd_lock);
|
||||
if (!scr_pool_tried) {
|
||||
// two blocks with 8 GiB or more of device-local memory: fight2's first world frame deswizzles about
|
||||
// 280 MiB, more than the GPU finishes while the replay records, and the pool grew by four blocks there
|
||||
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;
|
||||
uint32_t type;
|
||||
VmaPoolCreateInfo pi = {.blockSize = SCR_BLOCK, .minBlockCount = vram >= (8ull << 30) ? 2 : 1};
|
||||
if (vmaFindMemoryTypeIndexForBufferInfo(c->vma, &bi, &aci, &type) == VK_SUCCESS) {
|
||||
pi.memoryTypeIndex = type;
|
||||
if (vmaCreatePool(c->vma, &pi, &scr_pool) != VK_SUCCESS) scr_pool = NULL;
|
||||
}
|
||||
rt_log("vk: upload scratch pool %s (%u blocks of %llu MiB kept)", scr_pool ? "created" : "unavailable: default pool",
|
||||
(unsigned)pi.minBlockCount, (unsigned long long)(SCR_BLOCK >> 20));
|
||||
__atomic_store_n(&scr_pool_tried, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
}
|
||||
if (scr_pool && bi.size <= SCR_BLOCK / 2) aci.pool = scr_pool;
|
||||
VkResult r = vmaCreateBuffer(c->vma, &bi, &aci, buf, alloc, NULL);
|
||||
if (r == VK_SUCCESS && open_active) open_b.held += bi.size; // the caller defers its free to the open batch
|
||||
return r;
|
||||
}
|
||||
|
||||
VkDeviceSize vk_upload_held(void) { return open_active ? open_b.held : 0; }
|
||||
|
||||
// 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).
|
||||
void vk_upload_pools_init(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c) return;
|
||||
VkBuffer b;
|
||||
VmaAllocation a;
|
||||
vk_staging(16, &b, &a);
|
||||
vmaDestroyBuffer(c->vma, b, a);
|
||||
if (vk_scratch_buffer(16, &b, &a) == VK_SUCCESS) vmaDestroyBuffer(c->vma, b, a);
|
||||
}
|
||||
|
||||
uint64_t vk_upload_flush(int wait) {
|
||||
plat_once(&cmd_once, cmd_init, NULL);
|
||||
plat_rmutex_lock(&cmd_lock);
|
||||
@@ -2346,20 +2423,21 @@ static void defer(int kind, uint64_t handle, VmaAllocation alloc) {
|
||||
plat_rmutex_lock(&cmd_lock);
|
||||
if (ndead == cap_dead) grow((void **)&dead, &cap_dead, sizeof *dead);
|
||||
// The open batch may record into the object: it counts as the newest user. The GPU must also be past every
|
||||
// submission that names it. tl_noted covers the submitted ones; an object the replay lets go of while it records
|
||||
// a submission (vk_exec_recording_fence) may be named by that one too, not yet submitted: vk_texture.c
|
||||
// slot_resolve hands a draw a released entry, and entry_of destroys released entries at that address later in
|
||||
// the same submission. Stamped with the last submitted value, the object went as soon as the GPU reached it,
|
||||
// before the submission that names it ran, and with the recorder thread (vk_rec.c) behind, before the recorder
|
||||
// made the call that names it. LAS Recomp and Savage Wind Recomp stamp every deferral with the submission being
|
||||
// recorded (runtime/nvn/vulkan/textures.c defer: vk_pending_seq). TOTK_DEFER_STAMP=0 brings back the old stamp.
|
||||
static int stamp = -1;
|
||||
if (stamp < 0) stamp = rt_knob("DEFER_STAMP", "1") != 0;
|
||||
uint64_t tl = tl_noted, rf = stamp ? vk_exec_recording_fence() : 0;
|
||||
if (rf > tl) tl = rf;
|
||||
// submission that names it, including the one being recorded, which may name it and is not submitted yet:
|
||||
// vk_texture.c slot_resolve hands a draw a released entry, and entry_of, sweep_released and the VRAM pressure drop
|
||||
// destroy released entries. tl_noted is that submission's value from the start of its recording (vk_sync.c
|
||||
// vk_sync_submit), whichever thread destroys the object. With the synchronous replay a game thread finalizing a
|
||||
// texture (vk_backend.c vk_be_texture_release) destroys entries while another game thread records; stamped with
|
||||
// the last submitted value, as the recording thread's own fence was the only one seen, the object was freed once
|
||||
// the GPU reached that value, before the submission naming it ran: "write invalid" device faults on NVIDIA. LAS
|
||||
// Recomp and Savage Wind Recomp stamp every deferral with the submission being recorded (runtime/nvn/vulkan/
|
||||
// textures.c defer: vk_pending_seq).
|
||||
uint64_t tl = tl_noted;
|
||||
if (kind != DEAD_HOST && tl > vk_sync_issued() && tl_noted_here != tl) n_cross_defer++, cross_defer_tl = tl;
|
||||
dead[ndead++] = (Dead){kind, handle, alloc, open_active ? open_b.seq : next_seq, tl};
|
||||
rec_check_handle(kind == DEAD_VIEW ? REC_VIEW : kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, handle, 0, tl,
|
||||
vk_sync_fence_done());
|
||||
if (kind != DEAD_HOST)
|
||||
rec_check_handle(kind == DEAD_VIEW ? REC_VIEW : kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, handle, 0, tl,
|
||||
vk_sync_fence_done());
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
}
|
||||
|
||||
@@ -2378,14 +2456,38 @@ void vk_defer_view(VkImageView view) { if (view) defer(DEAD_VIEW, (uint64_t)(uin
|
||||
void vk_defer_buffer(VkBuffer buf, VmaAllocation alloc) {
|
||||
if (buf) defer(DEAD_BUFFER_VMA, (uint64_t)(uintptr_t)buf, alloc);
|
||||
}
|
||||
void vk_defer_free(void *p) { if (p) defer(DEAD_HOST, (uint64_t)(uintptr_t)p, NULL); }
|
||||
|
||||
void vk_mem_note_submit(uint64_t timeline_value) {
|
||||
// The device fault report: no lock, since a thread that holds cmd_lock may be waiting for the report's own lock
|
||||
// (vk_upload_flush reports a failed submit with cmd_lock held).
|
||||
void vk_mem_fault_log(void) {
|
||||
rt_log("vk: deferred frees at the fault: %d pending, stamp %llu, submitted %llu, done %llu; %llu deferred off the "
|
||||
"recording thread during a recording (newest stamp %llu)",
|
||||
__atomic_load_n(&ndead, __ATOMIC_RELAXED), (unsigned long long)__atomic_load_n(&tl_noted, __ATOMIC_RELAXED),
|
||||
(unsigned long long)vk_sync_issued(), (unsigned long long)vk_sync_fence_done(),
|
||||
(unsigned long long)__atomic_load_n(&n_cross_defer, __ATOMIC_RELAXED),
|
||||
(unsigned long long)__atomic_load_n(&cross_defer_tl, __ATOMIC_RELAXED));
|
||||
}
|
||||
|
||||
void vk_mem_note_fence(uint64_t timeline_value) {
|
||||
plat_once(&cmd_once, cmd_init, NULL);
|
||||
plat_rmutex_lock(&cmd_lock);
|
||||
if (timeline_value > tl_noted) tl_noted = timeline_value;
|
||||
tl_noted_here = timeline_value;
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
}
|
||||
|
||||
// The later of the submission being recorded (tl_noted, noted from its start: vk_sync.c vk_sync_submit) and the next
|
||||
// one to be submitted.
|
||||
static uint64_t retire_value(void) {
|
||||
plat_once(&cmd_once, cmd_init, NULL);
|
||||
plat_rmutex_lock(&cmd_lock);
|
||||
uint64_t v = vk_sync_issued() + 1;
|
||||
if (tl_noted > v) v = tl_noted;
|
||||
plat_rmutex_unlock(&cmd_lock);
|
||||
return v;
|
||||
}
|
||||
|
||||
void vk_mem_collect(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c) return;
|
||||
@@ -2434,6 +2536,9 @@ void vk_mem_shutdown(void) {
|
||||
if (stg_pool) vmaDestroyPool(c->vma, stg_pool); // after retire: no staging buffer is left in it
|
||||
stg_pool = NULL;
|
||||
stg_pool_tried = 0;
|
||||
if (scr_pool) vmaDestroyPool(c->vma, scr_pool); // deferred scratch buffers went with collect_locked above
|
||||
scr_pool = NULL;
|
||||
scr_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);
|
||||
|
||||
+159
-10
@@ -101,6 +101,12 @@ static struct {
|
||||
VkPipeStats stats;
|
||||
} P = {.lock = PLAT_MUTEX_INIT};
|
||||
|
||||
// Builds into P.cache take it shared, vk_pipeline_save takes it to copy the cache out. Vulkan lets
|
||||
// vkGetPipelineCacheData run alongside builds into a cache created without the EXTERNALLY_SYNCHRONIZED bit, but
|
||||
// Intel's Windows driver (101.7085, Iris Xe) faults in it (igvk64.dll, reading 0x170) when the first autosave meets the
|
||||
// boot's pipeline builds (#681). Nothing is taken while it is held.
|
||||
static PlatMutex cache_use = PLAT_MUTEX_INIT;
|
||||
|
||||
typedef struct Layout {
|
||||
struct Layout *next;
|
||||
uint64_t hash;
|
||||
@@ -1440,7 +1446,9 @@ static VkPipeline create_graphics(const VkPipeState *s, const GKey *k, const Lay
|
||||
if (k->fbl & 1) pi.flags |= VK_PIPELINE_CREATE_COLOR_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
|
||||
if (k->fbl & 2) pi.flags |= VK_PIPELINE_CREATE_DEPTH_STENCIL_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT;
|
||||
uint64_t p0 = perf_begin();
|
||||
plat_mutex_lock_shared(&cache_use);
|
||||
VkResult r = vkCreateGraphicsPipelines(P.device, P.cache, 1, &pi, NULL, &pipe);
|
||||
plat_mutex_unlock_shared(&cache_use);
|
||||
perf_end(PERF_PIPE, p0); // counts on the render thread only, so background builds stay out of it
|
||||
if (r != VK_SUCCESS) {
|
||||
rt_log("vk_pipeline: vkCreateGraphicsPipelines failed (%d)", (int)r);
|
||||
@@ -1591,7 +1599,9 @@ static VkPipeline lib_create(int kind, const VkPipeState *s, const GKey *k, VkPi
|
||||
ri.depthAttachmentFormat = (VkFormat)k->depth, ri.stencilAttachmentFormat = (VkFormat)k->stencil;
|
||||
pi.pColorBlendState = &cb, pi.pMultisampleState = &ms;
|
||||
}
|
||||
plat_mutex_lock_shared(&cache_use);
|
||||
if (vkCreateGraphicsPipelines(P.device, P.cache, 1, &pi, NULL, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;
|
||||
plat_mutex_unlock_shared(&cache_use);
|
||||
out:
|
||||
if (m) vkDestroyShaderModule(P.device, m, NULL);
|
||||
free(patched);
|
||||
@@ -1803,7 +1813,8 @@ static VkPipeline gpl_link(const VkPipeState *s, const GKey *k, const Layout *lc
|
||||
// shaders (gfx_shader_get results) live for the process (gfx_shader_drop_memory is the shader probe's, which builds no
|
||||
// pipelines), layouts until vk_pipeline_shutdown, which drains the queue first. Workers call vkCreateGraphicsPipelines
|
||||
// without P.lock: P.cache is created without VK_PIPELINE_CACHE_CREATE_EXTERNALLY_SYNCHRONIZED_BIT, so the driver
|
||||
// synchronises it, vkGetPipelineCacheData in vk_pipeline_save included. Compute pipelines stay synchronous: a skipped
|
||||
// synchronises the builds among themselves. vk_pipeline_save still keeps them apart from vkGetPipelineCacheData (see
|
||||
// cache_use). Compute pipelines stay synchronous: a skipped
|
||||
// dispatch can change what the game computes.
|
||||
//
|
||||
// Two queues: draws' misses (A.head) and the boot prebuild (A.low, see "pipeline keys" below). Workers take the low
|
||||
@@ -2512,7 +2523,7 @@ static void prebuild_start(void) {
|
||||
plat_mutex_unlock(&P.lock);
|
||||
}
|
||||
|
||||
// A compute pipeline for cs in layout l; VK_NULL_HANDLE on failure. No lock needed (P.cache is driver-synchronised).
|
||||
// A compute pipeline for cs in layout l; VK_NULL_HANDLE on failure. Takes cache_use shared, like the graphics builds.
|
||||
static VkPipeline compute_create(const GfxShader *cs, const Layout *l) {
|
||||
uint64_t words;
|
||||
const uint32_t *code = gfx_shader_spirv(cs, &words);
|
||||
@@ -2525,7 +2536,9 @@ static VkPipeline compute_create(const GfxShader *cs, const Layout *l) {
|
||||
.layout = l->layout};
|
||||
pin_subgroup32(&ci.stage, &rqc, code, words);
|
||||
VkPipeline pipe = VK_NULL_HANDLE;
|
||||
plat_mutex_lock_shared(&cache_use);
|
||||
if (vkCreateComputePipelines(P.device, P.cache, 1, &ci, NULL, &pipe) != VK_SUCCESS) pipe = VK_NULL_HANDLE;
|
||||
plat_mutex_unlock_shared(&cache_use);
|
||||
vkDestroyShaderModule(P.device, m, NULL);
|
||||
return pipe;
|
||||
}
|
||||
@@ -3168,6 +3181,118 @@ uint32_t vk_pipeline_set_dynamic(VkCommandBuffer cb, const VkPipeState *s) {
|
||||
return streams;
|
||||
}
|
||||
|
||||
// The values vk_pipeline_set_dynamic records for a state, as the commands take them.
|
||||
typedef struct {
|
||||
VkCullModeFlags cull;
|
||||
VkFrontFace front;
|
||||
VkPrimitiveTopology topo;
|
||||
VkBool32 dt, dw, st, bias, discard, restart, clamp, a2c;
|
||||
VkCompareOp df, scmp[2];
|
||||
VkStencilOp sop[2][3]; // fail, pass, depth fail; front, back
|
||||
VkPolygonMode poly;
|
||||
int n; // colour attachments en, eq and wm hold
|
||||
VkBool32 en[8];
|
||||
VkColorBlendEquationEXT eq[8];
|
||||
VkColorComponentFlags wm[8];
|
||||
} DynVals;
|
||||
|
||||
static void dyn_vals(const VkPipeState *s, DynVals *o) {
|
||||
const poly_st *pl = &s->poly;
|
||||
const ds_st *d = &s->ds;
|
||||
memset(o, 0, sizeof *o);
|
||||
o->cull = (VkCullModeFlags)(pl->cull & 3);
|
||||
o->front = pl->front == NVN_FRONT_FACE_CW ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
o->topo = vk_pipeline_topology(s->prim);
|
||||
o->dt = d->dt, o->dw = d->dw, o->st = d->st;
|
||||
o->df = compare_op(d->df);
|
||||
o->sop[0][0] = stencil_op(d->fail_f), o->sop[0][1] = stencil_op(d->zpass_f), o->sop[0][2] = stencil_op(d->zfail_f);
|
||||
o->sop[1][0] = stencil_op(d->fail_b), o->sop[1][1] = stencil_op(d->zpass_b), o->sop[1][2] = stencil_op(d->zfail_b);
|
||||
o->scmp[0] = compare_op(d->sf_f), o->scmp[1] = compare_op(d->sf_b);
|
||||
unsigned mode = pl->mode <= NVN_POLYGON_MODE_FILL ? pl->mode : NVN_POLYGON_MODE_FILL;
|
||||
o->bias = (pl->offset_en >> mode & 1) != 0;
|
||||
o->discard = s->raster_discard != 0, o->restart = s->prim_restart != 0;
|
||||
o->clamp = s->depth_clamp != 0, o->a2c = s->ms.a2c != 0;
|
||||
o->poly = polygon_mode(pl->mode);
|
||||
o->n = target_count(s);
|
||||
for (int i = 0; i < o->n; i++) {
|
||||
const blend_st *b = &s->blend[i];
|
||||
o->en[i] = (s->color.blend_en >> i & 1) && blendable(target_format(s->color_fmt[i], NULL, NULL));
|
||||
o->eq[i] = (VkColorBlendEquationEXT){blend_factor(b->src_rgb), blend_factor(b->dst_rgb), blend_op(b->eq_rgb),
|
||||
blend_factor(b->src_a), blend_factor(b->dst_a), blend_op(b->eq_a)};
|
||||
o->wm[i] = target_wmask(s, i);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
DynVals v;
|
||||
const GfxShader *vs;
|
||||
uint32_t vattr[16];
|
||||
vs_st vstream[16];
|
||||
uint32_t streams;
|
||||
} DC;
|
||||
|
||||
uint32_t vk_pipeline_set_dynamic_delta(VkCommandBuffer cb, const VkPipeState *s, int valid, uint32_t *cmds) {
|
||||
*cmds = 0;
|
||||
if (!ensure_init()) return VK_PIPE_ALL_BINDINGS;
|
||||
if (!valid) { // a new command buffer, or one whose dynamic state someone else set: everything, as before
|
||||
uint32_t streams = vk_pipeline_set_dynamic(cb, s);
|
||||
dyn_vals(s, &DC.v);
|
||||
DC.vs = s->vs, DC.streams = streams;
|
||||
memcpy(DC.vattr, s->vattr, sizeof DC.vattr), memcpy(DC.vstream, s->vstream, sizeof DC.vstream);
|
||||
*cmds = 1;
|
||||
return streams;
|
||||
}
|
||||
DynVals v;
|
||||
dyn_vals(s, &v);
|
||||
const DynVals *o = &DC.v;
|
||||
uint32_t k = 0;
|
||||
if (v.cull != o->cull) vkCmdSetCullMode(cb, v.cull), k++;
|
||||
if (v.front != o->front) vkCmdSetFrontFace(cb, v.front), k++;
|
||||
if (v.topo != o->topo) vkCmdSetPrimitiveTopology(cb, v.topo), k++;
|
||||
if (v.dt != o->dt) vkCmdSetDepthTestEnable(cb, v.dt), k++;
|
||||
if (v.dw != o->dw) vkCmdSetDepthWriteEnable(cb, v.dw), k++;
|
||||
if (v.df != o->df) vkCmdSetDepthCompareOp(cb, v.df), k++;
|
||||
if (v.st != o->st) vkCmdSetStencilTestEnable(cb, v.st), k++;
|
||||
for (int f = 0; f < 2; f++)
|
||||
if (memcmp(v.sop[f], o->sop[f], sizeof v.sop[f]) || v.scmp[f] != o->scmp[f])
|
||||
vkCmdSetStencilOp(cb, f ? VK_STENCIL_FACE_BACK_BIT : VK_STENCIL_FACE_FRONT_BIT, v.sop[f][0], v.sop[f][1],
|
||||
v.sop[f][2], v.scmp[f]),
|
||||
k++;
|
||||
if (v.bias != o->bias) vkCmdSetDepthBiasEnable(cb, v.bias), k++;
|
||||
if (v.discard != o->discard) vkCmdSetRasterizerDiscardEnable(cb, v.discard), k++;
|
||||
if (v.restart != o->restart) vkCmdSetPrimitiveRestartEnable(cb, v.restart), k++;
|
||||
// The vertex input is made from the vertex shader's inputs and the attribute and stream states alone
|
||||
// (vertex_input): a draw that keeps all three keeps it, and the bindings it reads.
|
||||
if (s->vs != DC.vs || memcmp(s->vattr, DC.vattr, sizeof DC.vattr) || memcmp(s->vstream, DC.vstream, sizeof DC.vstream)) {
|
||||
uint32_t streams = VK_PIPE_ALL_BINDINGS;
|
||||
if (s->vs) {
|
||||
VtxInput vi;
|
||||
vertex_input(s, &vi);
|
||||
if (P.dyn_vi) rec_vertex_input(P.set_vi, cb, vi.n_bind, vi.b, vi.n_attr, vi.a), k++;
|
||||
streams = 0;
|
||||
for (uint32_t i = 0; i < vi.n_bind; i++) streams |= 1u << vi.b[i].binding;
|
||||
}
|
||||
DC.vs = s->vs, DC.streams = streams;
|
||||
memcpy(DC.vattr, s->vattr, sizeof DC.vattr), memcpy(DC.vstream, s->vstream, sizeof DC.vstream);
|
||||
}
|
||||
if (P.dyn_clamp && v.clamp != o->clamp) rec_bool(P.set_clamp, cb, v.clamp), k++;
|
||||
if (P.dyn_poly && v.poly != o->poly) rec_polygon_mode(P.set_poly, cb, v.poly), k++;
|
||||
if (P.dyn_a2c && v.a2c != o->a2c) rec_bool((PFN_vkCmdSetDepthClampEnableEXT)P.set_a2c, cb, v.a2c), k++;
|
||||
// Attachments past o->n may hold nothing yet in this command buffer: a state with more targets sets all of them.
|
||||
// One with fewer leaves the ones past its count as they were, which no draw of it reads.
|
||||
uint32_t n = (uint32_t)v.n;
|
||||
int all = v.n > o->n;
|
||||
if (n && P.dyn_blend_en && (all || memcmp(v.en, o->en, n * sizeof *v.en))) rec_blend_enable(P.set_blend_en, cb, 0, n, v.en), k++;
|
||||
if (n && P.dyn_blend_eq && (all || memcmp(v.eq, o->eq, n * sizeof *v.eq))) rec_blend_equation(P.set_blend_eq, cb, 0, n, v.eq), k++;
|
||||
if (n && P.dyn_wmask && (all || memcmp(v.wm, o->wm, n * sizeof *v.wm))) rec_write_mask(P.set_wmask, cb, 0, n, v.wm), k++;
|
||||
DC.v = v; // the invariant now covers [0, v.n) only
|
||||
*cmds = k;
|
||||
return DC.streams;
|
||||
}
|
||||
|
||||
// ---- statistics, persistence, teardown
|
||||
|
||||
void vk_pipeline_stats(VkPipeStats *s) {
|
||||
@@ -3178,7 +3303,8 @@ void vk_pipeline_stats(VkPipeStats *s) {
|
||||
|
||||
static PlatMutex save_lock = PLAT_MUTEX_INIT; // one cache save at a time; shutdown takes it to destroy the cache
|
||||
|
||||
void vk_pipeline_save(void) {
|
||||
// One cache save; wait: queue for cache_use (the exit saves) rather than try for it (the autosave).
|
||||
static void cache_save(int wait) {
|
||||
if (P.ready) keys_flush();
|
||||
if (P.ready && P.gpl) {
|
||||
rt_log("vk_pipeline: gpl: %llu fast links (%llu by workers; avg %.2f ms, max %.2f ms, %llu failed), %llu draws "
|
||||
@@ -3205,23 +3331,46 @@ void vk_pipeline_save(void) {
|
||||
}
|
||||
gfx_shader_record_flush(1);
|
||||
if (!P.ready || !P.cache || !P.cache_path[0]) return;
|
||||
// Not under P.lock: the driver synchronises P.cache, and holding P.lock through the copy and the file write (about
|
||||
// 60 ms on the 1.4.3 fight) stopped every pipeline lookup of the replay, a freeze at each 20 s autosave.
|
||||
// Not under P.lock: holding it through the copy and the file write (about 60 ms on the 1.4.3 fight) stopped every
|
||||
// pipeline lookup of the replay, a freeze at each 20 s autosave. Only the copy takes cache_use, so no pipeline is
|
||||
// being built into P.cache while the driver reads it.
|
||||
plat_mutex_lock(&save_lock);
|
||||
if (!P.cache) { // vk_pipeline_shutdown destroyed it
|
||||
plat_mutex_unlock(&save_lock);
|
||||
return;
|
||||
}
|
||||
uint64_t t0 = qpc();
|
||||
if (wait) {
|
||||
plat_mutex_lock(&cache_use);
|
||||
} else {
|
||||
// The autosave never queues for cache_use: a queued exclusive request on an SRW lock holds back new shared
|
||||
// ones, so the drawing thread's next build would wait for a worker's build to end. It tries for about 2 s
|
||||
// and otherwise saves on its next round.
|
||||
int got = 0;
|
||||
for (int i = 0; i < 1000 && !(got = plat_mutex_trylock(&cache_use)); i++) plat_sleep_ms(2);
|
||||
if (!got) {
|
||||
rt_log("vk_pipeline: cache save put off, pipelines were being built for %.0f ms", ms_since(t0));
|
||||
plat_mutex_unlock(&save_lock);
|
||||
return;
|
||||
}
|
||||
}
|
||||
uint64_t t1 = qpc();
|
||||
size_t size = 0;
|
||||
void *data = NULL;
|
||||
if (vkGetPipelineCacheData(P.device, P.cache, &size, NULL) == VK_SUCCESS && size && (data = malloc(size)) &&
|
||||
vkGetPipelineCacheData(P.device, P.cache, &size, data) == VK_SUCCESS) {
|
||||
if (!plat_file_write_atomic(P.cache_path, data, size)) rt_log("vk_pipeline: cannot write %s", P.cache_path);
|
||||
}
|
||||
int ok = vkGetPipelineCacheData(P.device, P.cache, &size, NULL) == VK_SUCCESS && size && (data = malloc(size)) &&
|
||||
vkGetPipelineCacheData(P.device, P.cache, &size, data) == VK_SUCCESS;
|
||||
plat_mutex_unlock(&cache_use);
|
||||
// The copy time is how long a pipeline build on the drawing thread (a compute miss, under P.lock) can wait for
|
||||
// this save; the wait is how long the save waited for builds in flight.
|
||||
rt_log("vk_pipeline: cache %s: %.1f MB copied in %.1f ms, after %.1f ms waiting for builds", wait ? "save" : "autosave",
|
||||
(double)size / 1e6, ms_since(t1), (double)(t1 - t0) / 1e6);
|
||||
if (ok && !plat_file_write_atomic(P.cache_path, data, size)) rt_log("vk_pipeline: cannot write %s", P.cache_path);
|
||||
free(data);
|
||||
plat_mutex_unlock(&save_lock);
|
||||
}
|
||||
|
||||
void vk_pipeline_save(void) { cache_save(1); }
|
||||
|
||||
// The game never shuts the device down (boot.c and the window end the process), so the cache is saved while it
|
||||
// runs: at most every 20 s, and only after new pipelines were built. New pipeline keys are appended every 2 s.
|
||||
static void autosave_once(void) {
|
||||
@@ -3235,7 +3384,7 @@ static void autosave_once(void) {
|
||||
vk_pipeline_stats(&s);
|
||||
if (s.graphics + s.compute == last_n) return;
|
||||
last_n = s.graphics + s.compute, last_ms = now;
|
||||
vk_pipeline_save();
|
||||
cache_save(0);
|
||||
}
|
||||
|
||||
// On its own thread: the save (vkGetPipelineCacheData and the file write, about 60 ms) and the key appends (a flush to
|
||||
|
||||
+4
-3
@@ -309,7 +309,7 @@ void vk_sync_start(void) {
|
||||
if (reserved < now) reserved = now;
|
||||
stop = 0;
|
||||
plat_mutex_unlock(&st_lock);
|
||||
vk_mem_note_submit(now);
|
||||
vk_mem_note_fence(now);
|
||||
completion_thread = plat_thread_new(completion_main, c, "vk completion");
|
||||
if (!completion_thread) rt_fatal("vk: cannot start the completion thread");
|
||||
__atomic_store_n(&started, 1, __ATOMIC_RELEASE);
|
||||
@@ -379,7 +379,6 @@ VkCommandBuffer vk_sync_split(void) {
|
||||
vk_device_fault_report(r);
|
||||
rt_fatal("vk: vkQueueSubmit2 of a part of value %llu failed (%d)", (unsigned long long)cur_fence, (int)r);
|
||||
}
|
||||
vk_mem_note_submit(cur_fence); // objects destroyed from here on may be in use by the part just submitted
|
||||
CmdSlot *s = take_slot(c, cur_fence); // the submitted part keeps its slot until the value is reached
|
||||
rec_begin(s->cb, cur_fence, VK_CRUMB_SUBMIT, cur_fence);
|
||||
ts_begin(s->cb);
|
||||
@@ -404,6 +403,9 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
vk_mem_dirty_submit(); // TOTK_TRACE=perf: what a mirror would upload before this submit
|
||||
uint64_t fence = __atomic_load_n(&issued, __ATOMIC_ACQUIRE) + 1;
|
||||
cur_fence = fence;
|
||||
// Before the first object is resolved: what any thread destroys from here on waits for this value (vk_memory.c
|
||||
// defer), since the recording may already name it.
|
||||
vk_mem_note_fence(fence);
|
||||
CmdSlot *s = take_slot(c, fence);
|
||||
uint64_t t0 = perf_begin();
|
||||
rec_begin(s->cb, fence, VK_CRUMB_SUBMIT, fence); // begun by the recorder when it records the part
|
||||
@@ -434,7 +436,6 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
rt_fatal("vk: vkQueueSubmit2 of value %llu failed (%d)", (unsigned long long)fence, (int)r);
|
||||
}
|
||||
s->fence = recorded ? fence : 0;
|
||||
vk_mem_note_submit(fence);
|
||||
plat_mutex_lock(&st_lock);
|
||||
__atomic_store_n(&issued, fence, __ATOMIC_RELEASE);
|
||||
plat_cond_broadcast(&cv_work);
|
||||
|
||||
+219
-5
@@ -597,10 +597,16 @@ static void destroy_tex(Tex *e) {
|
||||
if (p->next_at == e) { p->next_at = e->next_at; break; }
|
||||
}
|
||||
if (e->released) nreleased--, rel_unlink(e);
|
||||
// Another thread may hold &e->pub, taken under the lock and used after it (vk_texture_get, vk_texture_by_id: render
|
||||
// targets for a whole pass, vk_exec.c resolve_target, vk_texture_storage_bound_locked writes through it). Freed
|
||||
// here, that thread read a stale VkImage from freed memory, made views of it and cached them by that handle. The
|
||||
// record goes with the image, once the submission being recorded is done; gone keeps views of it out of the caches.
|
||||
e->pub.gone = 1;
|
||||
vk_exec_image_gone(e->pub.image);
|
||||
vk_defer_image(e->pub.image, e->alloc);
|
||||
free(e->snap);
|
||||
free(e);
|
||||
e->snap = NULL;
|
||||
vk_defer_free(e);
|
||||
}
|
||||
|
||||
// Released entries with another descriptor at the same address are replaced; past RELEASED_MAX released entries the
|
||||
@@ -819,15 +825,20 @@ static void astc_check(const Tex *e, const uint8_t *raw, const VkAstcRegion *ar,
|
||||
// large ASTC array decodes to RGBA8 in one go: a staging buffer of 676682336 bytes failed (-2) on an Intel UHD with
|
||||
// 8 GB of shared memory and the game closed before the title screen.
|
||||
#define UPLOAD_PART (32ull << 20) // fits the staging pool (vk_staging takes up to half of its 64 MiB block)
|
||||
// The open upload batch is submitted between chunks (and parts of the CPU path) once its staging and scratch pass this:
|
||||
// on fight2's first world frame one batch took hundreds of MiB, and the staging and scratch pools grew by 64 MiB blocks
|
||||
// on the replay thread. Submitting earlier keeps the queue order (the batch still precedes the part that samples the
|
||||
// images) and lets the GPU finish and return the buffers sooner.
|
||||
#define UPLOAD_HELD_MAX (48ull << 20)
|
||||
|
||||
// Copies the whole storage into the image (every level and layer the image has).
|
||||
static void upload(Tex *e) {
|
||||
static int upload_gpu(Tex *e);
|
||||
|
||||
// The CPU side of upload: deswizzles (and decodes or expands) into staging buffers, then copies them into the image.
|
||||
static void upload_cpu(Tex *e) {
|
||||
static volatile int32_t n_bad, n_undecoded, n_split;
|
||||
const tex_t *t = &e->t;
|
||||
const nvn_texdesc *d = &t->d;
|
||||
int bw, bh, bpb = nvn_fmt_bpb(d->format, &bw, &bh);
|
||||
if (!bpb || !t->mem) return;
|
||||
uint64_t p0 = perf_begin(), u0 = kern_now_ns();
|
||||
int linear = nvn_tex_is_linear(d), is3d = d->target == NVN_TEXTURE_TARGET_3D, one_d = e->pub.type == VK_IMAGE_TYPE_1D;
|
||||
uint32_t levels = linear ? 1 : e->pub.levels, layers = linear ? 1 : e->pub.layers;
|
||||
int astc = e->f.astc_bw != 0, rgb = e->f.expand_rgb;
|
||||
@@ -908,6 +919,7 @@ static void upload(Tex *e) {
|
||||
}
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
|
||||
vk_upload_end();
|
||||
if (!acheck && vk_upload_held() > UPLOAD_HELD_MAX) vk_upload_flush(0);
|
||||
}
|
||||
if (parts > 1)
|
||||
log_limited(&n_split, "texture %#llx fmt %#x %ux%u, %u levels, %u layers: %.1f MiB staged in %d parts",
|
||||
@@ -924,7 +936,201 @@ static void upload(Tex *e) {
|
||||
free(raw_copy);
|
||||
free(ar);
|
||||
free(rg);
|
||||
}
|
||||
|
||||
// Block-linear storage in an imported pool, deswizzled by the GPU (vk_deswizzle.c): the replay thread only records
|
||||
// the dispatches, the image copies and (ASTC) the decode into the upload batch the CPU path also uses, which runs before
|
||||
// the part that samples the image. The GPU reads the pool when the batch runs, as the Switch's GPU reads it at
|
||||
// execution; a CPU write after make_fresh's write-watch reset still marks the pages and brings another upload. Each
|
||||
// chunk of regions gets a device-local scratch buffer of at most DESWZ_CHUNK bytes (one region, when larger), freed
|
||||
// once the batch completes. TOTK_DESWIZZLE=check compares every chunk with the CPU deswizzle (waits for the GPU).
|
||||
// 0 when not handled, before anything is recorded: the CPU path takes the texture.
|
||||
#define DESWZ_CHUNK (32ull << 20)
|
||||
|
||||
// Uploads by path since the last perf summary (TOTK_TRACE=perf): 0 GPU, else why the CPU took it.
|
||||
enum { UP_GPU, UP_MODE, UP_LINEAR, UP_RGB, UP_ASTC_CPU, UP_BPB, UP_NOT_LIVE, UP_GEOM, UP_LIMITS, UP_SCRATCH, UP_WHY };
|
||||
static const char *const up_why_name[UP_WHY] = {"gpu", "cpu mode", "linear", "rgb", "astc on cpu", "bytes per block",
|
||||
"not an imported pool", "layout", "device limits", "scratch"};
|
||||
static uint64_t up_n[UP_WHY], up_bytes[UP_WHY], up_ns[UP_WHY];
|
||||
static int up_why; // upload_gpu's reason for returning 0 (texture lock held)
|
||||
|
||||
static int upload_gpu(Tex *e) {
|
||||
static volatile int32_t n_scratch_fail, n_undecoded, n_bad;
|
||||
static volatile int64_t n_checked, n_differ;
|
||||
const tex_t *t = &e->t;
|
||||
const nvn_texdesc *d = &t->d;
|
||||
int mode = vk_deswz_mode();
|
||||
up_why = mode == VK_DESWZ_CPU ? UP_MODE : nvn_tex_is_linear(d) ? UP_LINEAR : e->f.expand_rgb ? UP_RGB : UP_GPU;
|
||||
if (up_why) return 0;
|
||||
int astc = e->f.astc_bw != 0, agpu = astc && vk_astc_gpu();
|
||||
if (astc && (!agpu || vk_astc_mode() == VK_ASTC_CHECK)) return up_why = UP_ASTC_CPU, 0; // or ASTC's check mode
|
||||
int bw, bh, bpb = nvn_fmt_bpb(d->format, &bw, &bh);
|
||||
if (!bpb || 16 % bpb) return up_why = UP_BPB, 0; // the rows of the linear copy are padded to 16 bytes
|
||||
VkMemSpan sp;
|
||||
if (!vk_mem_resolve(e->pub.addr, t->L.size, &sp) || !vk_mem_span_live(&sp)) return up_why = UP_NOT_LIVE, 0;
|
||||
int one_d = e->pub.type == VK_IMAGE_TYPE_1D, is3d = d->target == NVN_TEXTURE_TARGET_3D;
|
||||
uint32_t levels = e->pub.levels, layers = e->pub.layers, n = levels * layers, k = 0;
|
||||
VkDeswzRegion *dr = calloc(n, sizeof *dr);
|
||||
VkBufferImageCopy *rg = calloc(n, sizeof *rg);
|
||||
VkAstcRegion *ar = agpu ? calloc(n, sizeof *ar) : NULL;
|
||||
if (!dr || !rg || (agpu && !ar)) rt_fatal("vk: out of memory (texture upload regions)");
|
||||
int ok = 1;
|
||||
VkDeviceSize max_dst = DESWZ_CHUNK;
|
||||
for (uint32_t lv = 0; ok && lv < levels; lv++)
|
||||
for (uint32_t ly = 0; ok && ly < layers; ly++) {
|
||||
uint32_t row_bytes, rows, slices, gbpb;
|
||||
uint64_t base;
|
||||
unsigned th, td;
|
||||
uint32_t w = mip(d->width, lv), h = one_d ? 1 : mip(d->height, lv), sl = is3d ? mip(d->depth, lv) : 1;
|
||||
if (nvn_deswizzle_geom(d, lv, ly, &row_bytes, &rows, &slices, &base, &th, &td, &gbpb) || gbpb != (uint32_t)bpb ||
|
||||
base >= t->L.size || rows != (h + bh - 1) / bh || slices != sl) {
|
||||
ok = 0;
|
||||
break;
|
||||
}
|
||||
uint64_t bhg = 1ull << th, bdg = 1ull << td, gobs_x = (row_bytes + 63) / 64;
|
||||
uint64_t blocks_y = (rows + 8 * bhg - 1) / (8 * bhg), blocks_z = (slices + bdg - 1) / bdg;
|
||||
uint64_t segs = gobs_x * blocks_y * blocks_z * (32ull << (th + td)), room = (t->L.size - base) / 16;
|
||||
if (segs > room) segs = room;
|
||||
uint32_t pitch16 = (row_bytes + 15) / 16;
|
||||
VkDeviceSize out = (VkDeviceSize)pitch16 * 16 * rows * slices;
|
||||
if (segs > UINT32_MAX || gobs_x * blocks_y > UINT32_MAX) { ok = 0; break; }
|
||||
if (out > max_dst) max_dst = out;
|
||||
dr[k] = (VkDeswzRegion){.src = base, .segs = (uint32_t)segs, .pitch16 = pitch16, .rows = rows, .slices = slices,
|
||||
.th = th, .td = td, .gobs_x = (uint32_t)gobs_x, .blocks_y = (uint32_t)blocks_y};
|
||||
rg[k] = (VkBufferImageCopy){.bufferRowLength = pitch16 * 16 / (uint32_t)bpb * (uint32_t)bw,
|
||||
.bufferImageHeight = rows * (uint32_t)bh,
|
||||
.imageSubresource = {e->pub.aspect, lv, ly, 1}, .imageExtent = {w, h, sl}};
|
||||
if (agpu) ar[k] = (VkAstcRegion){.w = w, .h = h, .slices = sl, .level = lv, .layer = ly};
|
||||
k++;
|
||||
}
|
||||
if (!ok || k != n || !vk_deswz_fits(sp.offset, t->L.size, dr, (int)n, max_dst)) {
|
||||
free(dr), free(rg), free(ar);
|
||||
return up_why = !ok || k != n ? UP_GEOM : UP_LIMITS, 0;
|
||||
}
|
||||
VkCtx *c = vk_ctx();
|
||||
VkCommandBuffer cb = vk_upload_begin();
|
||||
// Earlier GPU writes into the pool (copies, shaders) before the reads; the image's earlier uses before its copy.
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
for (uint32_t k0 = 0, k1; k0 < n; k0 = k1) {
|
||||
VkDeviceSize size = 0;
|
||||
for (k1 = k0; k1 < n; k1++) {
|
||||
VkDeviceSize out = (VkDeviceSize)dr[k1].pitch16 * 16 * dr[k1].rows * dr[k1].slices;
|
||||
if (k1 > k0 && size + out > DESWZ_CHUNK) break;
|
||||
dr[k1].dst = size, rg[k1].bufferOffset = size;
|
||||
if (agpu) ar[k1].src = size;
|
||||
size += out;
|
||||
}
|
||||
VkBuffer sb;
|
||||
VmaAllocation sa;
|
||||
if (vk_scratch_buffer(size, &sb, &sa) != VK_SUCCESS) {
|
||||
log_limited(&n_scratch_fail, "texture %#llx: deswizzle scratch of %llu bytes failed%s",
|
||||
(unsigned long long)e->pub.addr, (unsigned long long)size,
|
||||
k0 ? "; its later levels left as they were" : "; deswizzled on the CPU");
|
||||
if (!k0) {
|
||||
vk_upload_end();
|
||||
free(dr), free(rg), free(ar);
|
||||
return up_why = UP_SCRATCH, 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!vk_deswz_record(cb, sp.buffer, sp.offset, t->L.size, sb, size, dr + k0, (int)(k1 - k0))) {
|
||||
log_limited(&n_bad, "texture %#llx: deswizzle of levels %u-%u not recorded; left as they were",
|
||||
(unsigned long long)e->pub.addr, k0, k1 - 1); // vk_deswz_fits passed: not expected
|
||||
vk_defer_buffer(sb, sa);
|
||||
break;
|
||||
}
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
|
||||
if (agpu) {
|
||||
uint64_t a0 = perf_begin();
|
||||
if (!vk_astc_record(cb, sb, size, ar + k0, (int)(k1 - k0), e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb,
|
||||
e->f.astc_bc, e->pub.image, e->pub.aspect, VK_NULL_HANDLE))
|
||||
log_limited(&n_undecoded, "ASTC texture %#llx (fmt %#x %ux%u) not decoded on the GPU: left as it was",
|
||||
(unsigned long long)e->pub.addr, d->format, d->width, d->height);
|
||||
perf_end(PERF_ASTC, a0);
|
||||
} else {
|
||||
vkCmdCopyBufferToImage(cb, sb, e->pub.image, VK_IMAGE_LAYOUT_GENERAL, k1 - k0, rg + k0);
|
||||
}
|
||||
if (mode == VK_DESWZ_CHECK) { // the linear copy back to the host, compared with the CPU deswizzle
|
||||
VkBuffer rb;
|
||||
VmaAllocation ra;
|
||||
const uint8_t *rmap = vk_staging(size, &rb, &ra);
|
||||
VkBufferCopy cp = {0, 0, size};
|
||||
vkCmdCopyBuffer(cb, sb, rb, 1, &cp);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT,
|
||||
VK_ACCESS_2_HOST_READ_BIT);
|
||||
vk_defer_buffer(sb, sa);
|
||||
vk_upload_end();
|
||||
vk_upload_flush(1);
|
||||
vmaInvalidateAllocation(c->vma, ra, 0, VK_WHOLE_SIZE);
|
||||
for (uint32_t i = k0; i < k1; i++) {
|
||||
const VkDeswzRegion *r = &dr[i];
|
||||
size_t pitch = (size_t)r->pitch16 * 16, bytes = pitch * r->rows * r->slices;
|
||||
uint8_t *cpu = malloc(bytes);
|
||||
if (!cpu) rt_fatal("vk: out of memory (deswizzle check)");
|
||||
uint32_t row_bytes = 0, rows, slices, gbpb;
|
||||
uint64_t base;
|
||||
unsigned th, td;
|
||||
nvn_deswizzle_geom(d, rg[i].imageSubresource.mipLevel, rg[i].imageSubresource.baseArrayLayer, &row_bytes,
|
||||
&rows, &slices, &base, &th, &td, &gbpb);
|
||||
nvn_deswizzle(d, rg[i].imageSubresource.mipLevel, rg[i].imageSubresource.baseArrayLayer, t->mem, cpu, pitch);
|
||||
uint64_t bad = 0, first = UINT64_MAX;
|
||||
for (uint64_t row = 0; row < (uint64_t)r->rows * r->slices; row++)
|
||||
if (memcmp(cpu + row * pitch, rmap + r->dst + row * pitch, row_bytes)) {
|
||||
bad++;
|
||||
if (first == UINT64_MAX) first = row;
|
||||
}
|
||||
free(cpu);
|
||||
plat_add64(&n_checked, 1);
|
||||
if (bad) {
|
||||
plat_add64(&n_differ, 1);
|
||||
rt_log("vk: deswizzle check: texture %#llx fmt %#x %ux%u level %u layer %u: %llu of %u rows differ, the "
|
||||
"first %llu (th %u td %u, %u rows of %u bytes, %u slices)", (unsigned long long)e->pub.addr,
|
||||
d->format, d->width, d->height, rg[i].imageSubresource.mipLevel,
|
||||
rg[i].imageSubresource.baseArrayLayer, (unsigned long long)bad, r->rows * r->slices,
|
||||
(unsigned long long)first, r->th, r->td, r->rows, row_bytes, r->slices);
|
||||
}
|
||||
}
|
||||
vmaDestroyBuffer(c->vma, rb, ra);
|
||||
if ((n_checked & 1023) == 0 || n_checked < 2)
|
||||
rt_log("vk: deswizzle check: %lld regions compared, %lld differ", (long long)n_checked, (long long)n_differ);
|
||||
cb = vk_upload_begin();
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
continue;
|
||||
}
|
||||
vk_defer_buffer(sb, sa);
|
||||
if (k1 < n && vk_upload_held() > UPLOAD_HELD_MAX) { // submit what is recorded, go on in a new batch
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, ALL_RW);
|
||||
vk_upload_end();
|
||||
vk_upload_flush(0);
|
||||
cb = vk_upload_begin();
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
}
|
||||
}
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, ALL_RW);
|
||||
vk_upload_end();
|
||||
if (vk_upload_held() > UPLOAD_HELD_MAX) vk_upload_flush(0);
|
||||
free(dr), free(rg), free(ar);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Copies the whole storage into the image (every level and layer the image has): on the GPU where it can
|
||||
// (upload_gpu), else on the CPU.
|
||||
static void upload(Tex *e) {
|
||||
const tex_t *t = &e->t;
|
||||
const nvn_texdesc *d = &t->d;
|
||||
int bw, bh;
|
||||
if (!nvn_fmt_bpb(d->format, &bw, &bh) || !t->mem) return;
|
||||
uint64_t p0 = perf_begin(), u0 = kern_now_ns();
|
||||
int why = UP_GPU;
|
||||
if (!upload_gpu(e)) why = up_why, upload_cpu(e);
|
||||
uint64_t dt = kern_now_ns() - u0;
|
||||
up_n[why]++, up_bytes[why] += t->L.size, up_ns[why] += dt;
|
||||
if (e->uploaded) {
|
||||
n_reuploads++, perf_count(PERF_CNT_TEX_REUP);
|
||||
ru_n++, ru_bytes += t->L.size, ru_ns += dt;
|
||||
@@ -1404,6 +1610,14 @@ void vk_texture_new_submit(void) {
|
||||
(unsigned long long)f * 60, (unsigned long long)ru_n, ru_bytes / 1048576.0, ru_ns / 1e6,
|
||||
(unsigned long long)ru_wt_n, ru_wt_bytes / 1048576.0, ru_wt_ns / 1e6);
|
||||
ru_n = ru_bytes = ru_ns = ru_wt_n = ru_wt_bytes = ru_wt_ns = 0;
|
||||
char ub[512];
|
||||
size_t uo = 0;
|
||||
for (int i = 0; i < UP_WHY; i++)
|
||||
if (up_n[i] && uo < sizeof ub)
|
||||
uo += (size_t)snprintf(ub + uo, sizeof ub - uo, "%s %s %llu (%.1f MiB, %.2f ms)", uo ? "," : "",
|
||||
up_why_name[i], (unsigned long long)up_n[i], up_bytes[i] / 1048576.0, up_ns[i] / 1e6);
|
||||
if (uo) rt_log("perf: frame %llu: texture uploads by path:%s", (unsigned long long)f * 60, ub);
|
||||
memset(up_n, 0, sizeof up_n), memset(up_bytes, 0, sizeof up_bytes), memset(up_ns, 0, sizeof up_ns);
|
||||
uint64_t ck, calls, untr;
|
||||
vk_mem_snap_stats(&ck, &calls, &untr);
|
||||
rt_log("perf: frame %llu: binding storage checks %llu (texture %llu), write-watch snapshot queries %llu, untracked %llu",
|
||||
|
||||
+74
-12
@@ -18,7 +18,7 @@ const int rt_game_version = TEST_GAME_VERSION;
|
||||
static int raw_x, raw_y, takes, captured=1, foreground=1, allowed=1, mode=1, patched=1, selecting;
|
||||
static float ratio=0.5f;
|
||||
static uint64_t now=1000000000;
|
||||
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook, load_hook;
|
||||
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook, load_hook, state_hook;
|
||||
const char *cfg_get(const char *key) { (void)key; return mode ? "direct" : "stick"; }
|
||||
const char *cfg_value(const char *key) { return cfg_get(key); }
|
||||
double cfg_double(const char *key) { (void)key; return 1; }
|
||||
@@ -40,13 +40,14 @@ void *rt_patch_page(void) { return ∶ }
|
||||
void rt_register_host(uint64_t addr, GuestFn fn) {
|
||||
if(addr==HOST_MOUSE_DIRECT) hook=fn; else if(addr==HOST_MOUSE_ACTIVITY) activity_hook=fn;
|
||||
else if(addr==HOST_MOUSE_ORBIT_BEGIN) begin_hook=fn; else if(addr==HOST_MOUSE_ORBIT_ANGLES) angles_hook=fn;
|
||||
else if(addr==HOST_MOUSE_HAND) hand_hook=fn; else if(addr==HOST_MOUSE_LOAD) load_hook=fn; else abort();
|
||||
else if(addr==HOST_MOUSE_ORBIT_STATE) state_hook=fn; else if(addr==HOST_MOUSE_HAND) hand_hook=fn; else if(addr==HOST_MOUSE_LOAD) load_hook=fn; else abort();
|
||||
}
|
||||
void plat_mutex_lock(PlatMutex *m) { (void)m; }
|
||||
void plat_mutex_unlock(PlatMutex *m) { (void)m; }
|
||||
void rt_log(const char *fmt, ...) { (void)fmt; }
|
||||
float cam_speed_applied(void) { return 1; }
|
||||
uint64_t input_buttons_last(void) { return 0; }
|
||||
static uint64_t buttons;
|
||||
uint64_t input_buttons_last(void) { return buttons; }
|
||||
int mem_guest_readable(uint64_t a, uint64_t n) { (void)a; (void)n; return 0; } // no player: on-foot gains
|
||||
static float scalar(__uint128_t v) { float f; memcpy(&f,&v,4); return f; }
|
||||
#if TEST_GAME_VERSION == 143
|
||||
@@ -109,6 +110,7 @@ int main(void) {
|
||||
uint64_t input_address=(uintptr_t)target; memcpy(camera+0x20,&input_address,8);
|
||||
for(int rate=0;rate<3;rate++) {
|
||||
memset(orbit_seen,0,sizeof orbit_seen);
|
||||
guest_float_store((uintptr_t)camera+0x15c,0);
|
||||
ratio=30/rates[rate]; c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0;
|
||||
raw_x=100; raw_y=-50; hook(&c);
|
||||
guest_float_store((uintptr_t)camera+0x108,30); guest_float_store((uintptr_t)camera+0x120,40);
|
||||
@@ -118,16 +120,16 @@ int main(void) {
|
||||
c.x[20]=(uintptr_t)camera; c.sp=(uintptr_t)frame; angles_hook(&c);
|
||||
assert(fabsf(guest_float((uintptr_t)frame+0x44)-(30-50*DEG_PER_COUNT))<0.0001f);
|
||||
assert(fabsf(guest_float((uintptr_t)frame+0x48)-(40-100*DEG_PER_COUNT))<0.0001f);
|
||||
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(30-50*DEG_PER_COUNT))<0.0001f);
|
||||
assert(guest_float((uintptr_t)camera+0x15c)==0);
|
||||
c.x[0]=(uintptr_t)camera; begin_hook(&c); angles_hook(&c);
|
||||
assert(fabsf(guest_float((uintptr_t)frame+0x48)-(40-100*DEG_PER_COUNT))<0.0001f); // one sample cannot repeat next update
|
||||
// The next input frame is stationary and the game's parameter update rewrites the old default target.
|
||||
// Restoring the mouse's desired target makes the camera filter stay at the new pitch, not spring to zero.
|
||||
// The native target remains available for the distance controller.
|
||||
// Holding pitch happens at the final output, after the native filter.
|
||||
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_x=raw_y=0; hook(&c);
|
||||
guest_float_store((uintptr_t)camera+0x15c,0);
|
||||
c.x[0]=(uintptr_t)camera; begin_hook(&c);
|
||||
float current=guest_float((uintptr_t)camera+0x108), goal=guest_float((uintptr_t)camera+0x15c);
|
||||
assert(fabsf(goal-(30-50*DEG_PER_COUNT))<0.0001f && fabsf((current+goal)*0.5f-(30-50*DEG_PER_COUNT))<0.0001f);
|
||||
assert(goal==0 && fabsf(current-(30-50*DEG_PER_COUNT))<0.0001f);
|
||||
// A later game filter overwrites the pitch after the begin hook. Neither a stationary frame nor the next
|
||||
// tiny mouse delta may integrate from that recentered result.
|
||||
guest_float_store((uintptr_t)camera+0x108,0);
|
||||
@@ -137,7 +139,7 @@ int main(void) {
|
||||
guest_float_store((uintptr_t)frame+0x44,0);
|
||||
c.x[20]=(uintptr_t)camera; angles_hook(&c);
|
||||
assert(fabsf(guest_float((uintptr_t)frame+0x44)-(30-50*DEG_PER_COUNT))<0.0001f);
|
||||
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-((30-50*DEG_PER_COUNT)-19))<0.0001f);
|
||||
assert(guest_float((uintptr_t)camera+0x15c)==0);
|
||||
assert(fabsf(guest_float((uintptr_t)camera+0x108)-((30-50*DEG_PER_COUNT)-14))<0.0001f);
|
||||
for(int i=0;i<100;i++) {
|
||||
guest_float_store((uintptr_t)frame+0x44,0); angles_hook(&c);
|
||||
@@ -161,16 +163,76 @@ int main(void) {
|
||||
guest_float_store((uintptr_t)camera+0x108,3); // the game left the pitch at 3 + the offset 14
|
||||
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
|
||||
c.x[0]=(uintptr_t)camera; begin_hook(&c);
|
||||
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(17-14-5))<0.0001f);
|
||||
assert(guest_float((uintptr_t)camera+0x15c)==0);
|
||||
guest_float_store((uintptr_t)frame+0x44,0);
|
||||
c.x[20]=(uintptr_t)camera; angles_hook(&c);
|
||||
assert(fabsf(guest_float((uintptr_t)frame+0x44)-17)<0.0001f);
|
||||
guest_float_store((uintptr_t)camera+0x15c,0); // the game's parameter update writes its default target
|
||||
c.x[20]=(uintptr_t)target; hook(&c);
|
||||
c.x[0]=(uintptr_t)camera; begin_hook(&c);
|
||||
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(17-14-5))<0.0001f);
|
||||
assert(guest_float((uintptr_t)camera+0x15c)==0);
|
||||
// Aim/close-up handoff: the native camera can replace its parameter
|
||||
// reference on every frame. Mouse pitch hold must never overwrite it,
|
||||
// nor restore a stale reference when a controller takes over later.
|
||||
for(int i=0;i<20;i++) {
|
||||
float native_target=5+i, native_radius=2+i*0.1f;
|
||||
guest_float_store((uintptr_t)camera+0x15c,native_target);
|
||||
guest_float_store((uintptr_t)camera+0x164,native_radius);
|
||||
guest_float_store((uintptr_t)camera+0x130,native_radius-0.5f);
|
||||
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
|
||||
c.x[0]=(uintptr_t)camera; begin_hook(&c);
|
||||
assert(guest_float((uintptr_t)camera+0x15c)==native_target);
|
||||
guest_float_store((uintptr_t)frame+0x44,0); // native pitch filter
|
||||
c.x[20]=(uintptr_t)camera; angles_hook(&c);
|
||||
assert(fabsf(guest_float((uintptr_t)frame+0x44)-17)<0.0001f);
|
||||
assert(guest_float((uintptr_t)camera+0x15c)==native_target);
|
||||
assert(guest_float((uintptr_t)camera+0x164)==native_radius);
|
||||
assert(guest_float((uintptr_t)camera+0x130)==native_radius-0.5f);
|
||||
}
|
||||
float pad_pitch=0.5f; c.x[20]=(uintptr_t)target;
|
||||
memcpy(&c.v[10],&pad_pitch,4); hook(&c);
|
||||
c.x[0]=(uintptr_t)camera; begin_hook(&c);
|
||||
assert(guest_float((uintptr_t)camera+0x15c)==24);
|
||||
c.v[10]=0;
|
||||
memset(camera+0x88,0,4); memset(camera+0x10c,0,4); memset(camera+0x11c,0,4);
|
||||
}
|
||||
// The native manual-orbit branch keeps the distance it inherited from
|
||||
// aiming. Mouse-owned orbit must select the native automatic-distance
|
||||
// transition, before initialization; controller and scripted modes keep
|
||||
// their own state. The host hook never writes a distance or position.
|
||||
{
|
||||
assert(state_hook);
|
||||
c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0;
|
||||
raw_x=1; raw_y=0; hook(&c);
|
||||
c.x[0]=(uintptr_t)camera; begin_hook(&c);
|
||||
c.x[20]=(uintptr_t)camera; angles_hook(&c);
|
||||
uint32_t manual=4, override=0;
|
||||
memcpy(camera+0x180,&manual,4); memcpy(camera+0x188,&override,4);
|
||||
c.x[10]=4; expected=c; state_hook(&c);
|
||||
expected.x[8]=0; expected.x[10]=3;
|
||||
assert(!memcmp(&expected,&c,sizeof c));
|
||||
uint32_t unchanged; memcpy(&unchanged,camera+0x180,4); assert(unchanged==4);
|
||||
// The game's transition initializer, rather than this hook, owns the state.
|
||||
for(int state=0;state<=5;state++) if(state!=4) {
|
||||
c.x[10]=state; state_hook(&c); assert(c.x[10]==(uint64_t)state);
|
||||
}
|
||||
buttons=1ull<<B_ZR; c.x[10]=4; state_hook(&c); assert(c.x[10]==4);
|
||||
buttons=1ull<<B_R; state_hook(&c); assert(c.x[10]==4); buttons=0;
|
||||
uint8_t forced=0x40; memcpy(target+0x15c,&forced,1);
|
||||
state_hook(&c); assert(c.x[10]==4); forced=0; memcpy(target+0x15c,&forced,1);
|
||||
uint64_t recenter=0x20; memcpy(target+0x148,&recenter,8);
|
||||
state_hook(&c); assert(c.x[10]==4); recenter=0; memcpy(target+0x148,&recenter,8);
|
||||
override=1; memcpy(camera+0x188,&override,4);
|
||||
state_hook(&c); assert(c.x[10]==4 && c.x[8]==1);
|
||||
override=0; memcpy(camera+0x188,&override,4);
|
||||
// Any real controller input retains the native manual-distance behaviour.
|
||||
c.x[20]=(uintptr_t)target; float pad=0.25f;
|
||||
memcpy(&c.v[9],&pad,4); c.v[10]=0; hook(&c);
|
||||
c.x[20]=(uintptr_t)camera; c.x[10]=4; state_hook(&c); assert(c.x[10]==4);
|
||||
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
|
||||
c.x[20]=(uintptr_t)camera; c.x[10]=4; now+=150000000ull;
|
||||
state_hook(&c); assert(c.x[10]==4); now++;
|
||||
}
|
||||
// A gamepad-only step (no mouse motion, no pitch hold) leaves the camera input and the angles as the game made them.
|
||||
memset(orbit_seen,0,sizeof orbit_seen);
|
||||
c.x[20]=(uintptr_t)target; c.x[21]=0; raw_x=raw_y=0; c.v[9]=c.v[10]=0;
|
||||
@@ -233,7 +295,7 @@ int main(void) {
|
||||
assert(takes==before && raw_x==42); selecting=0; raw_x=0;
|
||||
puts("direct mouse: register contract, routing, linearity, frame ratio and capture checks passed");
|
||||
puts("direct mouse: final orbit angles, single-count motion, equal axes and held-object routing passed");
|
||||
puts("direct mouse: persistent pitch target, radial cursor hold and gamepad-only steps untouched passed");
|
||||
puts("direct mouse: pitch hold with native distance reference, radial cursor hold and gamepad-only steps untouched passed");
|
||||
puts("direct mouse: final-pitch recenter rejection, explicit handoff and bounded quick-list steps passed");
|
||||
// A shake: two direction changes of swings of at least 10 degrees within 0.35 s, then the stick follows the mouse
|
||||
// side by side and lets go when it rests; nothing plays on after the mouse
|
||||
@@ -437,7 +499,7 @@ int main(void) {
|
||||
#else
|
||||
// 1.2.1: the same hooks with that version's registers and stack slots (MouseLayout layout_121) and its load sites
|
||||
int main(void) {
|
||||
assert(hook && activity_hook && begin_hook && angles_hook && hand_hook && load_hook);
|
||||
assert(hook && activity_hook && begin_hook && angles_hook && hand_hook && load_hook && !state_hook);
|
||||
(void)takes;
|
||||
Cpu c, expected;
|
||||
unsigned char target[0x500]={0}, camera[0x300]={0}, hand[0x300]={0};
|
||||
|
||||
@@ -18,7 +18,7 @@ ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import worktree # noqa: E402
|
||||
|
||||
IGNORED = '\n'.join('/' + d + '/' for d in worktree.LINK_DIRS) + '\n/' + worktree.LINK_FILES[0] + '\n/.worktrees/\n'
|
||||
IGNORED = ''.join(f'/{p}\n' for p in [d + '/' for d in worktree.LINK_DIRS] + worktree.LINK_FILES + ['.worktrees/'])
|
||||
|
||||
|
||||
def git(cwd, *a):
|
||||
|
||||
+28
-5
@@ -323,6 +323,26 @@ def runtime_sources(skip=()):
|
||||
return sorted(p for pat in pats for p in RUNTIME.glob(pat) if p.name not in skip and not other_os(p.name))
|
||||
|
||||
|
||||
def install_stamp(out, runtime_lib):
|
||||
"""Write obj/runtime/inputs.sha256, one hash over the runtime library, the INSTALL_SIDE sources and every runtime
|
||||
header, and return its path. The release zip dates every file 1980-01-01 (make_release.py write_zip), so a new
|
||||
release extracted over an old Setup folder has sources and a library older than the objects and the totk.exe the
|
||||
last install left there. Ninja compares mtimes only: it kept the old install-side objects and linked them with the
|
||||
new library (undefined symbols), or skipped the link and kept the old totk.exe. The install-side objects and the
|
||||
link depend on this file, which is rewritten only when the hash changes, so new contents rebuild them whatever the
|
||||
file dates are."""
|
||||
files = sorted((p for p in RUNTIME.rglob('*') if p.is_file() and
|
||||
(p.suffix in ('.h', '.inc') or (p.parent == RUNTIME and p.name in INSTALL_SIDE))),
|
||||
key=lambda p: p.relative_to(RUNTIME).as_posix())
|
||||
h = hashlib.sha256()
|
||||
for p in files:
|
||||
h.update(f'{p.relative_to(RUNTIME).as_posix()} {file_sha256(p)}\n'.encode())
|
||||
h.update(f'{LIB_NAME} {file_sha256(runtime_lib)}\n'.encode())
|
||||
path = out / 'obj' / 'runtime' / 'inputs.sha256'
|
||||
write_if_changed(path, h.hexdigest() + '\n')
|
||||
return path
|
||||
|
||||
|
||||
def runtime_object(out, src):
|
||||
"""obj/runtime/<path below runtime>.o, so platform/win32.c cannot collide with a top-level win32.c."""
|
||||
src = Path(src)
|
||||
@@ -368,10 +388,11 @@ def port_version():
|
||||
return path.read_text(encoding='utf-8').strip() if path.is_file() else 'unknown'
|
||||
|
||||
|
||||
def ninja_file(gen, out, mods, pool_depth, skip=(), prebuilt=False, runtime_lib=None, make_lib=False):
|
||||
def ninja_file(gen, out, mods, pool_depth, skip=(), prebuilt=False, runtime_lib=None, make_lib=False, stamp=None):
|
||||
"""build.ninja text, and the object lists per module. With prebuilt the generated objects already exist in
|
||||
out/obj and are link inputs without a compile edge. runtime_lib: a prebuilt library replaces the runtime objects
|
||||
except INSTALL_SIDE. make_lib: build only that library, from the runtime objects except INSTALL_SIDE."""
|
||||
except INSTALL_SIDE. make_lib: build only that library, from the runtime objects except INSTALL_SIDE. stamp: the
|
||||
install_stamp file, an extra input of the runtime objects and of the link."""
|
||||
inc = f'-I {cmd_path(RUNTIME)} -I {cmd_path(gen)}'
|
||||
vma = f'{cmd_path(DEPS / "include")} -I {cmd_path(DEPS / "include" / "vma")}' if DEPS else cmd_path(vma_include())
|
||||
imgui = cmd_path(RUNTIME / 'third_party' / 'imgui')
|
||||
@@ -440,7 +461,8 @@ def ninja_file(gen, out, mods, pool_depth, skip=(), prebuilt=False, runtime_lib=
|
||||
lines.append(f'build {ninja_path(o)}: rc {ninja_path(src)} | {ninja_path(RUNTIME / "totk.manifest")} '
|
||||
f'{ninja_path(RUNTIME / "totk.ico")}')
|
||||
else:
|
||||
lines.append(f'build {ninja_path(o)}: cc_rt {ninja_path(src)}{pgo_dep}')
|
||||
stamp_dep = (' | ' if not pgo_dep else ' ') + ninja_path(stamp) if stamp else ''
|
||||
lines.append(f'build {ninja_path(o)}: cc_rt {ninja_path(src)}{pgo_dep}{stamp_dep}')
|
||||
rt.append(o)
|
||||
objs['runtime'] = rt
|
||||
every = [o for v in objs.values() for o in v]
|
||||
@@ -459,7 +481,7 @@ def ninja_file(gen, out, mods, pool_depth, skip=(), prebuilt=False, runtime_lib=
|
||||
f' command = $cc $ldflags @$out.rsp -Wl,--whole-archive {cmd_path(runtime_lib)} -Wl,--no-whole-archive $libs -o $out',
|
||||
' rspfile = $out.rsp', ' rspfile_content = $in_newline', ' description = LINK $out']
|
||||
lines += ['', f'build {ninja_path(exe)}: {rule} ' + ' '.join(ninja_path(o) for o in every) +
|
||||
(f' | {ninja_path(runtime_lib)}' if runtime_lib else ''),
|
||||
(f' | {ninja_path(runtime_lib)}' if runtime_lib else '') + (f' {ninja_path(stamp)}' if stamp else ''),
|
||||
'build objects: phony ' + ' '.join(ninja_path(o) for o in every), f'default {ninja_path(exe)}', '']
|
||||
return '\n'.join(lines), objs, exe
|
||||
|
||||
@@ -926,8 +948,9 @@ def main():
|
||||
if runtime_lib and missing:
|
||||
sys.exit(f'build_game: {", ".join(missing)} missing from {flag_path(RUNTIME)}: the release must ship every '
|
||||
f'INSTALL_SIDE source beside {runtime_lib.name}')
|
||||
stamp = install_stamp(out, runtime_lib) if runtime_lib else None
|
||||
text, objs, exe = ninja_file(gen, out, mods, jobs, skip, pairs is not None, runtime_lib,
|
||||
args.make_runtime_lib)
|
||||
args.make_runtime_lib, stamp)
|
||||
write_if_changed(out / 'build.ninja', text)
|
||||
|
||||
env = dict(os.environ)
|
||||
|
||||
@@ -35,8 +35,9 @@ from the game's own files on the player's machine, nothing added (tools/button_i
|
||||
stays right.
|
||||
- every other prompt: the glyphs U+E040 A, U+E041 B, U+E042 X, U+E043 Y of the button font trade places.
|
||||
- xbox-label, for pad_layout = xbox and auto. Those layouts put Switch A on the button labelled A (auto picks xbox for
|
||||
every pad not labelled the Nintendo way), so the face buttons keep their letters; the action prompt's diamond keeps
|
||||
the Switch positions (A on the right).
|
||||
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.
|
||||
--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
|
||||
@@ -170,6 +171,92 @@ def bflyt_letters(data, base, end, swap):
|
||||
return changed
|
||||
|
||||
|
||||
def bflyt_panes(data, base, end):
|
||||
"""name -> absolute offset of each pane section (pan1, pic1, txt1, wnd1, ...) of the BFLYT at data[base:end], and
|
||||
name -> absolute offset of each material of its mat1, and its texture names (txl1)."""
|
||||
assert data[base:base + 4] == b'FLYT'
|
||||
pos, panes, mats, textures = base + struct.unpack_from('<H', data, base + 6)[0], {}, {}, []
|
||||
while pos + 8 <= end:
|
||||
magic, size = data[pos:pos + 4], struct.unpack_from('<I', data, pos + 4)[0]
|
||||
if magic in (b'pan1', b'pic1', b'txt1', b'wnd1', b'bnd1', b'prt1', b'ali1', b'scr1'):
|
||||
panes[data[pos + 12:pos + 36].split(b'\0')[0].decode()] = pos
|
||||
elif magic == b'mat1':
|
||||
for i in range(struct.unpack_from('<H', data, pos + 8)[0]):
|
||||
at = pos + struct.unpack_from('<I', data, pos + 12 + 4 * i)[0]
|
||||
mats[data[at:at + 28].split(b'\0')[0].decode()] = at
|
||||
elif magic == b'txl1':
|
||||
for i in range(struct.unpack_from('<H', data, pos + 8)[0]):
|
||||
at = pos + 12 + struct.unpack_from('<I', data, pos + 12 + 4 * i)[0]
|
||||
textures.append(data[at:data.index(0, at)].decode())
|
||||
if not size:
|
||||
break
|
||||
pos += size
|
||||
return panes, mats, textures
|
||||
|
||||
|
||||
# The action prompt's diamond (blyt/DoCommand_00.bflyt) for pad_layout = xbox and auto, where Switch A, B, X and Y are
|
||||
# the buttons labelled so on an Xbox pad: A at the bottom, B on the right, X on the left, Y at the top. The layout has
|
||||
# one diamond per button the prompt can name (N_A_00, N_B_00, N_X_00, N_Y_00; N_StopA_00 is A's while held), each
|
||||
# with its button and bar (N_Btn<K>_00), the three other letters (T_<letter>_<n>) and a base image (P_Base<K>_00)
|
||||
# that lights the button's side. A Switch button's place on an Xbox pad is that of its partner on a Switch (A of B,
|
||||
# X of Y), so each piece takes the place and look its partner's piece has: the bar of a button on the left reaches
|
||||
# left (Y's on a Switch), the others reach right.
|
||||
DIAMOND_SLOTS = {'A': (39.0, 1.0), 'B': (2.0, -39.0), 'X': (1.0, 39.0), 'Y': (-38.0, 0.0)} # the letters' places
|
||||
DIAMOND_PARTNER = {'A': 'B', 'B': 'A', 'X': 'Y', 'Y': 'X'}
|
||||
# (pane, the pane whose place it takes): the buttons with their bars, and the flashes of a repeated press (Rush)
|
||||
DIAMOND_MOVES = [('N_BtnA_00', 'N_BtnB_00'), ('N_BtnA_01', 'N_BtnB_00'), ('N_CompanionBtnA_00', 'N_BtnB_00'),
|
||||
('N_BtnB_00', 'N_BtnA_00'), ('N_BtnX_00', 'N_BtnY_00'), ('N_BtnY_00', 'N_BtnX_00')]
|
||||
DIAMOND_MOVES += [(f'P_Capture{k}_{n:02}', f'P_Capture{DIAMOND_PARTNER[k]}_{n:02}') for k in 'ABXY' for n in (1, 2, 3)]
|
||||
# X and Y trade the side their bar reaches to: the flash, the bar (Y's mirrored) and the text (Y's right-aligned)
|
||||
DIAMOND_MIRROR = [('P_CaptureX_00', 'P_CaptureY_00'), ('W_BaseX_00', 'W_BaseY_00'), ('T_GuideX_00', 'T_GuideY_00'),
|
||||
('T_GuideX_TW', 'T_GuideY_TW')]
|
||||
# the base image: its texture (by its material) and its rotation and scale
|
||||
DIAMOND_BASES = [('P_BaseA_00', 'P_BaseB_00'), ('P_BaseA_01', 'P_BaseB_00'), ('P_BaseB_00', 'P_BaseA_00'),
|
||||
('P_BaseX_00', 'P_BaseY_00'), ('P_BaseY_00', 'P_BaseX_00')]
|
||||
|
||||
|
||||
def bflyt_xbox_diamond(data, base, end):
|
||||
"""Lay the action prompt's diamond in the BFLYT at data[base:end] (blyt/DoCommand_00.bflyt) out as an Xbox pad's
|
||||
face buttons (DIAMOND_* above); returns the panes changed. A pane section holds its origin at +0x09, translation
|
||||
(3 floats) at +0x2C, rotation (3) at +0x38 and scale (2) at +0x44; a text pane its text position at +0x5C; a
|
||||
picture pane its material index at +0x64, and a material its first texture's index at +0x2C."""
|
||||
panes, mats, textures = bflyt_panes(data, base, end)
|
||||
old = bytes(data) # every piece takes its partner's place as the game has it
|
||||
changed = []
|
||||
for name, at in panes.items():
|
||||
m = re.fullmatch(r'T_([ABXY])_\d+', name)
|
||||
if m: # a letter around a diamond: its partner's place
|
||||
struct.pack_into('<2f', data, at + 0x2C, *DIAMOND_SLOTS[DIAMOND_PARTNER[m[1]]])
|
||||
changed.append(name)
|
||||
for name, like in DIAMOND_MOVES:
|
||||
data[panes[name] + 0x2C:panes[name] + 0x34] = old[panes[like] + 0x2C:panes[like] + 0x34]
|
||||
changed.append(name)
|
||||
# A's hold flashes (N_LoopA_03) sit 129 right of the diamond, as A's repeated-press ones: the same move as those
|
||||
dx, dy = (a - b for a, b in zip(struct.unpack_from('<2f', old, panes['P_CaptureB_01'] + 0x2C),
|
||||
struct.unpack_from('<2f', old, panes['P_CaptureA_01'] + 0x2C)))
|
||||
x, y = struct.unpack_from('<2f', old, panes['N_LoopA_03'] + 0x2C)
|
||||
struct.pack_into('<2f', data, panes['N_LoopA_03'] + 0x2C, x + dx, y + dy)
|
||||
changed.append('N_LoopA_03')
|
||||
for a, b in DIAMOND_MIRROR:
|
||||
pa, pb = panes[a], panes[b]
|
||||
for lo, hi in ((0x09, 0x0A), (0x2C, 0x4C)) + (((0x5C, 0x5D),) if old[pa:pa + 4] == b'txt1' else ()):
|
||||
data[pa + lo:pa + hi], data[pb + lo:pb + hi] = old[pb + lo:pb + hi], old[pa + lo:pa + hi]
|
||||
changed += [a, b]
|
||||
in_order = list(mats.values())
|
||||
|
||||
def material(name): # the offset of a picture pane's material
|
||||
return in_order[struct.unpack_from('<H', old, panes[name] + 0x64)[0]]
|
||||
|
||||
for name, like in DIAMOND_BASES:
|
||||
assert old[panes[name]:panes[name] + 4] == b'pic1', name
|
||||
data[panes[name] + 0x38:panes[name] + 0x4C] = old[panes[like] + 0x38:panes[like] + 0x4C]
|
||||
mine, theirs = material(name), material(like)
|
||||
data[mine + 0x2C:mine + 0x2E] = old[theirs + 0x2C:theirs + 0x2E]
|
||||
assert textures[struct.unpack_from('<H', data, mine + 0x2C)[0]].startswith('Nt_KeyBtnSubBase'), name
|
||||
changed.append(name)
|
||||
return changed
|
||||
|
||||
|
||||
def msbt_letters(data, swap):
|
||||
"""Swap the one-letter messages labelled T_<letter>_<n> (A, B, X or Y) in the UTF-16 MSBT data (a bytearray) by
|
||||
swap; the labels changed. The game sets a layout's text panes from the messages of the same name in
|
||||
@@ -319,6 +406,9 @@ if args.prompts:
|
||||
letters = {}
|
||||
if by_position:
|
||||
bntx_swap(blarc, bntx, KEY_TEXTURES)
|
||||
diamond = []
|
||||
if args.prompts == 'xbox-label':
|
||||
diamond = bflyt_xbox_diamond(blarc, *members['blyt/DoCommand_00.bflyt'])
|
||||
if letter_panes:
|
||||
for member, (lo, hi) in members.items():
|
||||
if member.endswith('.bflyt'):
|
||||
@@ -350,7 +440,7 @@ if args.prompts:
|
||||
if grown:
|
||||
outputs[RESTBL] = icons.grow_restbl(unpack(RESTBL), grown)
|
||||
prompts = dict(style=args.prompts, textures=KEY_TEXTURES if by_position else [], letters=letters,
|
||||
messages=messages, hud=hud, icons=len(drawn), fonts=FONTS, grown=grown)
|
||||
diamond=len(diamond), messages=messages, hud=hud, icons=len(drawn), fonts=FONTS, grown=grown)
|
||||
|
||||
lines = []
|
||||
for path, contents in outputs.items():
|
||||
|
||||
Reference in new issue
Block a user