mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 18:01:03 +00:00
Compare commits
No files matched your search
@@ -17,17 +17,18 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
- Native code, not emulation
|
||||
- Game updates 1.2.1 and 1.4.3, with saves that carry over between them
|
||||
- 60 fps by default; up to 360 fps or the monitor's refresh rate (experimental)
|
||||
- 60 fps by default; up to 144 fps or the monitor's refresh rate (experimental)
|
||||
- Custom resolutions (1080p, 2k...), supersampling and AMD FSR 1 upscaling
|
||||
- In-game settings menu (F1) and performance overlay (F10)
|
||||
- Keyboard, mouse and gamepad with remapping, rumble and gyro
|
||||
- Xbox button prompts: a layout that keeps the Switch button positions and shows the Xbox labels
|
||||
- Shaders prepared at install instead of during play
|
||||
|
||||
## Planned
|
||||
|
||||
- Linux and the Steam Deck
|
||||
- Mods made for emulators: unpack a mod into a `mods` folder
|
||||
- Custom aspect ratios, including ultrawide
|
||||
- Ultrawide on 1.2.1 (1.4.3 has it for 2560x1080, 3440x1440, 3840x1600 and 5120x2160; other shapes get the wide camera with a 16:9 HUD)
|
||||
- VRR
|
||||
|
||||
## Installing
|
||||
|
||||
@@ -9,6 +9,9 @@ Code copied or adapted from another project is listed here with its source, the
|
||||
| LAS Recomp host runtime: NSO loader, host-function table, guest-space reservation and re-execution, guest thread start | https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f`, Copyright (c) 2026 Peiikai | MIT | `runtime/loader.c`, `runtime/hostfn.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/boot.c`; `runtime/vk_device.c`, `runtime/gfx_window.c`, `runtime/tex_astc.c` (`runtime/nvn/astc.c`; its decode ported to GLSL in `runtime/shaders/astc_decode.comp`, compiled into `runtime/astc_decode_spv.h`, and its tables copied into `runtime/vk_astc.c`), `runtime/vk_formats.c` (format table of `runtime/nvn/vulkan/textures.c`), `runtime/vk_memory.c` (page write generations of `runtime/nvn/vulkan/exec.c`, deferred destruction of `textures.c`), `runtime/vk_texture.c` (image creation, uploads and samplers of `runtime/nvn/vulkan/textures.c`), `runtime/vk_sync.c` (timeline submissions and waits of `runtime/nvn/vulkan/exec.c`), `runtime/vk_pipeline.c` (blend, compare and stencil mappings, vertex format table with its 3-component fallback and pipeline cache file of `runtime/nvn/vulkan/exec.c` and `device.c`), `runtime/vk_exec.c` (pass begin and end, scissor clip and attachment clears of `runtime/nvn/vulkan/exec.c`), `runtime/vk_exec_xfer.c` (copies of `runtime/nvn/vulkan/exec.c`), `runtime/vk_exec_draw.c` (uniform buffer spans, texture and sampler descriptors and dispatch flow of `runtime/nvn/vulkan/exec.c`); in-game menu: `runtime/ui/menu.cpp` (Dear ImGui Vulkan set-up of `runtime/nvn/vulkan/imgui_glue.cpp`) |
|
||||
| MacNativeGames TotK host runtime: dynamic linking, identity check, module permissions, guest memory and SVCs, threads, IPC, audio renderer, system services, RomFS and save storage, boot sequence, the NVN proc name list | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/runtime/`, `games/totk/scripts/prepare_runtime.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/loader.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/svc.c`, `runtime/ipc.c`, `runtime/ipc.h`, `runtime/audren.c`, `runtime/audren.h`, `runtime/audio_mixer.c`, `runtime/audio_effects.c`, `runtime/audio_effects.h`, `runtime/audio_reverb.c`, `runtime/audio_reverb.h` (SDL3 output replaces CoreAudio), `runtime/ov_system.c` (also `shared_font.c`: the BFTTF layout behind nn::pl), `runtime/ov_input.c` (also `hid_feedback.c`: vibration and six-axis handles and calls), `runtime/motion_fusion.c` (six-axis fusion of `motion_fusion.c`), `runtime/ov_media.c` (hardware Opus decoder over libopus), `runtime/swkbd_win.c` (`swkbd_host.c`: ShowKeyboardArg offsets, UTF-8/UTF-16 conversion, disabled-key filter, result codes; Win32 window replaces the macOS text bar), `runtime/ov_gfx.c`, `runtime/romfs.c`, `runtime/romfs_path.h`, `runtime/fs_host.c`, `runtime/fs.h`, `runtime/boot.c`; `runtime/gfx_window.c`; `tools/gen_nvn_procs.py`, `runtime/nvn_procs.inc` (`NVN/names.txt` and the `NVN_EXTRA` names of `totk_nvn_ext.c`) |
|
||||
| MacNativeGames TotK frame-rate patches: 60 fps present-interval controller, real-time frame step and its present sampling, hair and cloth clock, trampoline slow-motion check (instruction words and 1.2.1 sites), player position chain; quick-menu capture background RomFS overlay | https://github.com/ly05010419/MacNativeGames @ `ebe621de99bfc48f3c42f2d2aa73e04fbcc6eae2` (`games/totk/runtime/game_patches.c`, `game_version.h`, `world_survey.c`, `games/totk/scripts/make_romfs_patches.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/frame_rate.c`, `recomp/patches-v121.txt`, `tools/make_romfs_patches.py` |
|
||||
| MacNativeGames TotK 1.4.3 fade fix: the four distance / LOD and pre-actor fade sites, their instruction words and the floor of the fade step at 1 | https://github.com/ly05010419/MacNativeGames @ `3e9e7f67fdfdd3404134dcfa0f24969a9425a33f` (`games/totk/runtime/game_patches_v143.c` patch_fade_increment), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set fade; the floor without flags, clz >> 5, as in pull request #78) |
|
||||
| MacNativeGames TotK aspect ratio: the camera and viewport sites that hand the perspective projection its aspect ratio, and the UI layout edits for a canvas wider than 16:9 | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/runtime/aspect_patch.h`, `aspect_math.h`, `game_patches_v143.c`, `games/totk/scripts/ultrawide_layout.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set aspect), `tools/ultrawide_layout.py`, `tools/make_romfs_patches.py` (--aspect) |
|
||||
| TOTK-Ultrawide, the community mod for emulators: the list of UI panes the ultrawide layout edits and the distances they move (data only, no code copied; the edit code is our own, and MacNativeGames' `ultrawide_layout.py` in the row above already carries the same values). The mod's repository URL and licence version are not recorded in this repository | TOTK-Ultrawide (community mod; URL not recorded here) | GPL (version not recorded here) | `tools/ultrawide_layout.py` (`KEEP_ROOT`, `edits`, `anim_edits`) |
|
||||
| MacNativeGames TotK virtual-core preemption: asynchronous preemption of an owner in top-level game code, priority inheritance through guest mutexes, the SDK's preemption-disable protocol (TLR+0x100/+0x102, SVC 0x36), the core-wait diagnostics | https://github.com/ly05010419/MacNativeGames @ `33e8fa83052ea1ae09a3583137008e9797e12950` (`games/totk/runtime/threads.c`, commits 8bf0893 and 19730cd), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/kernel.c` (SuspendThread and a register-saving stub replace SIGUSR2), `runtime/hostfn.c`, `runtime/svc.c` (host depth) |
|
||||
| MacNativeGames RomFS index builder (`TOTKRFS1` layout) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/scripts/prepare_romfs_backend.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/romfs_index.py` |
|
||||
| d2r-mobile host thread environment (TLR, TLS, thread object), the origin of MacNativeGames' thread layer; d2r-mobile NVN host object layouts, command record format and NVN front end, as adapted for TotK in MacNativeGames' `d2r_gfx` copy | https://github.com/jhaizhou-ops/d2r-mobile @ `53e7c5bb9ca3ea53e2160005be8fbe920cd9cb53` (`dm/host/thread.c`, `dm/host/hle_thread.c`); the NVN layouts and front end via https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/nvn/nvn_obj.h`, the two types of `nvn_tex.h`, `hle_nvn.c`, `totk_nvn_ext.c`, `nvn_impl.h`, `nvn_hmap.h`, `nvn_pool_index.h`, the request checks and mapping semantics of `totk_vpool.c`, the generation stamps of `nvn_dirty.c`, texture mirroring and texel copies of `nvn_mtl.m`; licence in `games/totk/third_party/d2r_gfx/LICENSE`), Copyright (c) 2026 jhaizhou | MIT | `runtime/kernel.c`; `runtime/nvn_obj.h`; `runtime/nvn_api.h`, `runtime/nvn_api_core.c`, `runtime/nvn_hmap.h`, `runtime/nvn_pool_index.h`; `runtime/nvn_api_obj.c` (also `nvn_pace.h`); `runtime/nvn_api_cmd.c`, `runtime/nvn_ext.c`; `runtime/vk_memory.c`, `runtime/vk_texture.c`; `runtime/vk_sync.c` (side effects written after GPU completion, `hle_nvn.c`); `runtime/vk_pipeline.c` (pipeline key and state mapping of `nvn_mtl.m` `pso_key_fill` / `pso_desc`, its `vfmt` vertex formats and the missing-varying fix-up of `msl_varyings`); `runtime/vk_exec.c`, `runtime/vk_exec.h` (op loop, pure-state ops and state defaults of `nvn_mtl.m` `mtl_exec` / `state_op` / `state_reset`); `runtime/vk_exec_xfer.c` (copy, clear, event, counter and conditional-render semantics of `nvn_mtl.m` `copy_t2b` / `copy_b2t` / `copy_t2t` / `clear_tex` / `cond_eval`); `runtime/vk_exec_draw.c` (bind ops, draw and dispatch flow, vertex streams of `nvn_mtl.m` `state_op` / `bind_vertex_streams` / `draw_inner` / `dispatch`) |
|
||||
|
||||
@@ -82,7 +82,7 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
- Vulkan 1.3 back end in place of MacNativeGames' Metal one. At `QueueSubmitCommands` the recorded NVN commands are replayed into Vulkan command buffers (`vk_exec*.c`).
|
||||
- Memory pools. Apple's unified memory let MacNativeGames use the game's pools zero-copy. The back end imports the pools as host memory (`VK_EXT_external_memory_host`). On a discrete GPU, pools bound as buffers are mirrored in device-local memory and the pages the CPU wrote (write watch, above) are uploaded before a replay; idle pool regions are made read-only so the write-watch queries skip them (`vk_memory.c`). GPU-rendered textures are written back when the CPU reads them: on a pool invalidate, on a CPU copy, and for small targets once the CPU waits for a fence (`vk_texture.c`).
|
||||
- Replay thread. `TOTK_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is off by default because no timing has shown a gain yet.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once, and the submit waits until the ring is empty. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. `TOTK_VK_RECORDER=0` makes every call on the submitting thread.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. `TOTK_VK_RECORDER=0` makes every call on the submitting thread.
|
||||
- Uniform snapshot. The game rewrites per-object uniform blocks on other threads soon after it submits the frame
|
||||
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
|
||||
binds from CPU-written pools is copied, and the draws bind the copy (`vk_usnap.c`, after MacNativeGames'
|
||||
@@ -93,7 +93,14 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
- Resolution. Patch sets make the game read its output, swapchain and scene sizes from the patch page
|
||||
(MacNativeGames' sites); `resolution.c` sets them from `output_res` and `render_scale` and grows the texture heap
|
||||
and the application pool to match. When the window is larger than the output, `vk_fsr.c` runs AMD FSR 1 (EASU,
|
||||
RCAS) on the present.
|
||||
RCAS) on the present. An `output_res` that is not 16:9 (more than 1% off, `res_is_16x9`) also switches on the
|
||||
`aspect` patch set (1.4.3 only), which hands the output's ratio to the camera, and `romfs.c` `overlay_init` loads the
|
||||
`romfs-patches-<w>x<h>` overlay for it when it exists (`tools/make_romfs_patches.py --aspect`, laid out by
|
||||
`tools/ultrawide_layout.py`; the installer builds four sizes: 2560x1080, 3440x1440, 3840x1600, 5120x2160). Other
|
||||
shapes, such as 16:10 or 32:9, get the wide camera and the 16:9 HUD stretched over it. 1.2.1 has no camera patch: a
|
||||
wide size there keeps the 16:9 view. The `aspect` setting (`fit`, the default; `stretch`) decides what the present does
|
||||
when the window's shape differs from the output's: `fit` letterboxes, where a 16:9 game in a wider window used to be
|
||||
stretched.
|
||||
- Occlusion queries never block the CPU; a result that is not ready counts as visible.
|
||||
- ASTC textures are uploaded as they are where the device reports `textureCompressionASTC_LDR`, and decoded by a compute shader everywhere else (`vk_astc.c`, `TOTK_ASTC`). The CPU decoder (`tex_astc*.c`) is the fallback and the test oracle.
|
||||
- Draw state. A draw whose program, bound state and textures repeat reuses the previous draw's shaders and skips rebuilding its descriptor sets; descriptor sets are written through update templates (`vk_exec_draw.c`, `vk_pipeline.c`).
|
||||
|
||||
@@ -89,6 +89,9 @@ From the repository root, in PowerShell. Every step that depends on the game ver
|
||||
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
|
||||
`--patches` builds in the frame-rate, resolution and other patch sets, switchable at run time. The link copies
|
||||
`SDL3.dll`, `libiconv-2.dll` and `libopus-0.dll` next to the exe.
|
||||
For Xbox button prompts, run `make_romfs_patches.py` once more with `--xbox-prompts`: it writes
|
||||
`build\<ver>\romfs-patches-xbox` ([controls.md](controls.md), "Xbox button prompts"); `build_game.py` copies it next
|
||||
to `totk.exe` like `romfs-patches`.
|
||||
|
||||
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
|
||||
for the shared shader list:
|
||||
|
||||
+20
-2
@@ -7,8 +7,9 @@ Input comes from the first SDL3 gamepad, the keyboard and the mouse, merged into
|
||||
- `nintendo`: by position, as on a Switch (the table below).
|
||||
- `xbox`: south A, east B, west X, north Y, as an Xbox pad labels them; on a PlayStation pad cross is A, circle B, square X, triangle Y.
|
||||
- `auto` (default): `nintendo` for a pad whose east button SDL reports labelled A (Switch Pro Controller, Nintendo-layout pads), `xbox` for the others.
|
||||
- `xbox_prompts`: the buttons as `nintendo`, and the game's prompts name the Xbox labels (see Xbox button prompts below).
|
||||
|
||||
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. The game's button prompts stay Switch letters either way.
|
||||
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. With `nintendo`, `xbox` and `auto` the game's button prompts stay Switch letters; `xbox_prompts` changes them.
|
||||
|
||||
| Switch | Gamepad (by position, Nintendo layout) | Keyboard and mouse | Action in the world |
|
||||
|---|---|---|---|
|
||||
@@ -30,6 +31,19 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|
||||
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
|
||||
## Xbox button prompts
|
||||
|
||||
`pad_layout = xbox_prompts` (*Xbox prompts (A right)* on the Controls tab) is for an Xbox pad played by position. The buttons work as with `nintendo`: the right button, labelled B, does Switch A (interact, confirm), and the bottom one, labelled A, does Switch B (run, cancel). The letters the game shows are swapped to match the labels, A with B and X with Y, so each prompt names the button to press. The other layouts leave the prompts alone: with `xbox` the Switch letters already match the labels.
|
||||
|
||||
The swapped letters are part of the game's textures, fonts 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 letter the game draws for it, and the prompt then names the wrong button. Use `xbox_prompts` with the default bindings.
|
||||
|
||||
The buttons change at once, but the game reads its UI archive and fonts once at boot, so the prompts change at the next start (the menu says *prompts change after restart*). `xbox_prompts` loads `romfs-patches-xbox` next to `romfs-index.bin` in place of `romfs-patches` (`runtime/romfs.c`, `overlay_init`): the same 60 fps patches, with the swap made from the game's own files by `tools/make_romfs_patches.py --xbox-prompts`, which the setup runs next to the plain one:
|
||||
|
||||
- the action prompt (`DoCommand_00`) draws the letter images `Nt_KeyTexA/B/X/Y_00`, whose image data is swapped, in a diamond that names the other face buttons. The diamond's letters are text the game takes from `LayoutMsg/DoCommand_00.msbt` of each language's `Mals` archive (and the layout's own text panes behind it); both are swapped. The diamond's shape shows the button's position and stays.
|
||||
- every other prompt is text in the button font (`scft/External.bfotf` in each `Font/Font*.Nin_NX_NVN.bfarc.zs`, glyphs U+E040 to U+E043); the A and B, X and Y glyphs are swapped.
|
||||
|
||||
Without the folder (an install made by an older setup, or one where the setup could not prepare it and showed a warning) the menu says the files are missing, the log has `RomFS overlay: pad_layout = xbox_prompts, but ...`, and the Switch prompts stay; running the setup again on the install folder writes it. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --xbox-prompts` writes `build\<ver>\romfs-patches-xbox`.
|
||||
|
||||
## Window keys
|
||||
|
||||
| Key | Gamepad | Action |
|
||||
@@ -46,7 +60,7 @@ The *Controls* tab of the settings menu lists, under the button layout, every Sw
|
||||
|
||||
The tab writes `totk.ini` lines, one per Switch button or stick direction: `bind_<button> = <entry>, <entry>, ...`. The button is `A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK`, or a stick direction `LSTICK_LEFT LSTICK_UP LSTICK_RIGHT LSTICK_DOWN` (movement, W A S D by default) and `RSTICK_...` (camera, the arrow keys). An entry is `pad:<name>` or `key:<name>`:
|
||||
|
||||
- pad names: `south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down` (`zl`, `zr` are the triggers)
|
||||
- pad names: `south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down lpaddle1 rpaddle1 lpaddle2 rpaddle2` (`zl`, `zr` are the triggers; the paddles are the back buttons of an Xbox Elite, DualSense Edge or Steam Controller, upper pair 1, lower pair 2, and no Switch button has them by default)
|
||||
- key names: `a` to `z`, `0` to `9`, `f2` to `f7`, `f9`, `f12`, `num0` to `num9`, `space tab enter backspace lshift rshift lctrl rctrl lalt capslock insert delete home end pageup pagedown up down left right`, `num_add num_sub num_mul num_div num_decimal`, `minus equals lbracket rbracket semicolon apostrophe comma period slash backslash grave`, `mouse_l mouse_r mouse_m mouse_x1 mouse_x2`
|
||||
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
|
||||
- `none` leaves the button unbound.
|
||||
@@ -68,6 +82,8 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
|
||||
|
||||
`TOTK_MOUSE_SENS` (0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `TOTK_INPUT=none` disables the capture too.
|
||||
|
||||
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it.
|
||||
|
||||
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
|
||||
|
||||
## Scripted input
|
||||
@@ -85,6 +101,8 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
|
||||
|
||||
`TOTK_INPUT_RECORD=<file>` (`input_record` in `totk.ini`, or Input recording in the menu) writes the merged input in the same format, one line per button press and per stick segment: events before the world clock starts by tick, the rest timed; a button or stick held when the world clock starts is cut there and goes on as a timed event. The file replays as a script, which makes it the repro to attach to a bug report.
|
||||
|
||||
`TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry>` attaches an SDL virtual gamepad at start whose sticks rest at these raw SDL axis values (-32768 to 32767, down is +y), so a drifting or held stick can be tested without a controller: the values go through the gamepad mapping and `TOTK_STICK_DEADZONE` like a real pad's, and `TOTK_INPUT_RECORD` shows what reached the game.
|
||||
|
||||
## RenderDoc capture (blocked)
|
||||
|
||||
`TOTK_RDOC=<frame>[,...]` (`runtime/rdoc_win.c`; `TOTK_RDOC_DIR`, `TOTK_RDOC_DLL`) loads RenderDoc before `vkCreateInstance`. RenderDoc hides `VK_EXT_external_memory_host`, which the back end requires for guest pool imports (`runtime/vk_memory.c`), so device creation fails under it. A capture needs a copy-based pool fallback first.
|
||||
+6
-2
@@ -24,7 +24,7 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
|
||||
exefs/, romfs/ extracted from the player's files
|
||||
profile/ saves
|
||||
logs/ totk.log of the last run, totk.1.log to totk.4.log of the runs before it
|
||||
shader and pipeline caches prepared by the setup
|
||||
shader and pipeline caches prepared by the setup; shader-cache/shader-cache.pack holds the translations
|
||||
totk-install.json what built this folder: release version, input hashes, finished steps
|
||||
```
|
||||
|
||||
@@ -50,7 +50,11 @@ keeps `profile/`. Saves carry over both ways: 1.4.3 converts a 1.2.1 save on loa
|
||||
hashes and the game version's patch page) and link
|
||||
them with the prebuilt runtime library into `totk.exe` (`build_game.py --runtime-lib`; about 3 min on 24 cores).
|
||||
5. **Prepare** the RomFS patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
|
||||
`totk.exe --prepare-shaders`) in the install folder.
|
||||
`totk.exe --prepare-shaders`) in the install folder, then fold the shader cache's loose files (four per shader, about
|
||||
117,000) into `shader-cache/shader-cache.pack` (`tools/pack_shader_cache.py`). totk.exe reads the pack first and
|
||||
writes the translations it makes in play as loose files; running the tool again with the game closed folds those in.
|
||||
The patches are written twice: `romfs-patches` and `romfs-patches-xbox` (`--xbox-prompts`, the A/B and X/Y prompts
|
||||
swapped for `pad_layout = xbox_prompts`).
|
||||
6. **Clean up** the temp folder (generated C and objects, about 1.3 GB).
|
||||
|
||||
A cancelled or failed setup resumes from the last finished step (`totk-install.json`).
|
||||
|
||||
+3
-2
@@ -266,8 +266,9 @@ class App:
|
||||
state = form.installed_state()
|
||||
if state is not None:
|
||||
self.update_hint.configure(text=self.cat('ui.update_mode'))
|
||||
if state.get('lang') in ini.LANGUAGES and not self.lang_touched:
|
||||
self.vars['lang'].set(self.lang_label(state['lang']))
|
||||
recorded = ini.canonical_language(state.get('lang'))
|
||||
if recorded in ini.LANGUAGES and not self.lang_touched:
|
||||
self.vars['lang'].set(self.lang_label(recorded))
|
||||
else:
|
||||
self.update_hint.configure(text='')
|
||||
self.fit()
|
||||
|
||||
+12
-4
@@ -9,19 +9,27 @@ import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# runtime/ov_system.c: the codes TOTK_LANG accepts.
|
||||
# runtime/ov_system.c: the codes TOTK_LANG offers. Each language appears once, under the code the game accepts.
|
||||
LANGUAGES = ('ja', 'en-US', 'en-GB', 'fr', 'fr-CA', 'de', 'it', 'es', 'es-419', 'nl', 'pt', 'pt-BR', 'ru', 'ko',
|
||||
'zh-CN', 'zh-TW', 'zh-Hans', 'zh-Hant')
|
||||
'zh-Hans', 'zh-Hant')
|
||||
# Older installer builds wrote these two codes; they are read as the canonical ones and rewritten on the next install.
|
||||
ALIASES = {'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant'}
|
||||
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the installer does not extract.
|
||||
NEED_SHARED_FONTS = ('ko', 'zh-CN', 'zh-TW', 'zh-Hans', 'zh-Hant')
|
||||
NEED_SHARED_FONTS = ('ko', 'zh-Hans', 'zh-Hant')
|
||||
|
||||
ACTIVE = re.compile(r'^(\s*)(?:totk_)?lang(\s*=\s*)(.*?)(\s*)$', re.IGNORECASE)
|
||||
COMMENTED = re.compile(r'^\s*[#;]\s*(?:totk_)?lang\s*=.*$', re.IGNORECASE)
|
||||
SECTION = re.compile(r'^\s*\[(.*)\]\s*$')
|
||||
|
||||
|
||||
def canonical_language(lang):
|
||||
"""The code LANGUAGES lists for lang: an alias of an older installer build is mapped, anything else is kept."""
|
||||
return ALIASES.get(lang, lang)
|
||||
|
||||
|
||||
def set_language(text, lang):
|
||||
"""text with `lang = <lang>`; see the module docstring."""
|
||||
"""text with `lang = <lang>` (an alias written as its canonical code); see the module docstring."""
|
||||
lang = canonical_language(lang)
|
||||
lines = text.splitlines()
|
||||
for i, line in enumerate(lines):
|
||||
m = ACTIVE.match(line)
|
||||
|
||||
+3
-1
@@ -72,7 +72,9 @@ class Layout:
|
||||
def missing(self):
|
||||
"""Paths the steps need that do not exist."""
|
||||
need = [self.python, self.tools / 'extract.py', self.tools / 'build_game.py', self.tools / 'prepare_shaders.py',
|
||||
self.tools / 'make_romfs_patches.py', self.recomp / 'gen.py', self.clang,
|
||||
self.tools / 'pack_shader_cache.py', self.tools / 'make_romfs_patches.py', self.tools / 'ultrawide_layout.py',
|
||||
self.recomp / 'gen.py',
|
||||
self.clang,
|
||||
self.deps / 'include', self.deps / 'lib', self.runtime, self.runtime_lib, self.ninja, self.hactool,
|
||||
self.xlate, *self.dlls, *(f(v) for v in SUPPORTED for f in (self.patches, self.expected))]
|
||||
if self.shader_list:
|
||||
|
||||
+52
-3
@@ -6,7 +6,8 @@ tools/prepare_shaders.py, whose output goes to <temp>/logs/<step>-<tool>.log and
|
||||
here: the host check is a small C program built with the bundled clang (hostcheck.c).
|
||||
|
||||
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, totk.exe and its DLLs, romfs-index.bin,
|
||||
romfs-patches/, totk_xlate.exe, the shader state of totk.exe --prepare-shaders, totk.ini, totk-install.json.
|
||||
romfs-patches/, romfs-patches-xbox/, totk_xlate.exe, the shader state of totk.exe --prepare-shaders, totk.ini,
|
||||
totk-install.json.
|
||||
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), gen/ (C), build/ (objects),
|
||||
logs/ (kept by the cleanup).
|
||||
"""
|
||||
@@ -203,7 +204,7 @@ class Check(Step):
|
||||
raise InstallError('check.dest_not_folder', path=str(dest))
|
||||
if ctx.mode == 'install' and dest.is_dir() and any(dest.iterdir()):
|
||||
raise InstallError('check.dest_not_empty', path=str(dest))
|
||||
lang = ctx.options.lang or ctx.state.get('lang')
|
||||
lang = ini.canonical_language(ctx.options.lang or ctx.state.get('lang'))
|
||||
if not lang:
|
||||
raise InstallError('check.lang_missing')
|
||||
if lang not in ini.LANGUAGES:
|
||||
@@ -581,13 +582,17 @@ class Compile(Step):
|
||||
TRANSLATED = re.compile(r'gfx_shader: prepare: (\d+) of (\d+) pairs')
|
||||
|
||||
|
||||
WIDE_SIZES = ('2560x1080', '3440x1440', '3840x1600', '5120x2160') # runtime/ui/menu.cpp row_output_res
|
||||
|
||||
|
||||
class Prepare(Step):
|
||||
id = 'prepare'
|
||||
|
||||
def key(self, ctx):
|
||||
layout = ctx.layout
|
||||
xlate_id = layout.xlate.with_suffix('.id')
|
||||
tools = [layout.tools / n for n in ('prepare_shaders.py', 'shader_census.py', 'make_romfs_patches.py')]
|
||||
tools = [layout.tools / n for n in ('prepare_shaders.py', 'shader_census.py', 'make_romfs_patches.py',
|
||||
'pack_shader_cache.py', 'ultrawide_layout.py')]
|
||||
return fingerprint('prepare-1', ctx.keys['compile'], layout.xlate, xlate_id, shader_list_text(layout), *tools)
|
||||
|
||||
def current(self, ctx, record):
|
||||
@@ -617,6 +622,28 @@ class Prepare(Step):
|
||||
dest / 'romfs-patches'])
|
||||
if code:
|
||||
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
|
||||
# The same overlay with the UI laid out for the 21:9 game resolutions the settings menu offers (runtime/romfs.c
|
||||
# overlay_init picks the one that matches). Only 1.4.3 has the camera aspect patch (recomp/patches-v143.txt) the
|
||||
# layouts go with, and ultrawide_layout.py is checked against 1.4.3's layout archive: 1.2.1 builds none.
|
||||
for size in WIDE_SIZES:
|
||||
remove(dest / f'romfs-patches-{size}')
|
||||
if version_tag(game_version(ctx)) != 'v143':
|
||||
continue
|
||||
code, log = run_tool(ctx, 'prepare', layout.tools / 'make_romfs_patches.py',
|
||||
['--version', 'v143', '--romfs', dest / 'romfs', '--out',
|
||||
dest / f'romfs-patches-{size}', '--aspect', size])
|
||||
if code:
|
||||
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
|
||||
# romfs-patches-xbox: the same with the A/B and X/Y prompts swapped, for pad_layout = xbox_prompts (written on
|
||||
# every install, so the menu choice works without another run of the setup). Optional: when it fails, the game
|
||||
# keeps the Switch prompts and the menu says the files are missing, so the setup warns and goes on.
|
||||
remove(dest / 'romfs-patches-xbox')
|
||||
code, log = run_tool(ctx, 'prepare-xbox', layout.tools / 'make_romfs_patches.py',
|
||||
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
|
||||
dest / 'romfs-patches-xbox', '--xbox-prompts'])
|
||||
if code:
|
||||
remove(dest / 'romfs-patches-xbox')
|
||||
rep.warning('warn.xbox_prompts_failed', code=code, log=str(log))
|
||||
|
||||
shader_list = layout.shader_list
|
||||
if not shader_list:
|
||||
@@ -638,6 +665,12 @@ class Prepare(Step):
|
||||
dest / 'exefs', '--list', shader_list], parse)
|
||||
if code or not (dest / 'shader-prepared.bin').is_file():
|
||||
raise InstallError('prepare.shaders_failed', code=code, log=str(log))
|
||||
|
||||
# One shader-cache.pack instead of four files per shader (~117,000): totk.exe reads the pack first.
|
||||
rep.progress('prepare', 'pack', force=True)
|
||||
code, log = run_tool(ctx, 'prepare', layout.tools / 'pack_shader_cache.py', [dest / 'shader-cache'])
|
||||
if code:
|
||||
raise InstallError('prepare.pack_failed', code=code, log=str(log))
|
||||
return {'xlate_sha256': file_sha256(xlate)}
|
||||
|
||||
|
||||
@@ -661,8 +694,24 @@ class Cleanup(Step):
|
||||
remove(stale)
|
||||
for name in ('gen', 'build', 'check', 'shader_list.tsv'):
|
||||
remove(ctx.temp / name)
|
||||
romfs_problem(dest / 'romfs', expected_update(ctx))
|
||||
return {}
|
||||
|
||||
|
||||
def romfs_problem(romfs, expected):
|
||||
"""Raise cleanup.romfs_unreadable unless romfs/ still holds the verified image. The game opens romfs.bin at start;
|
||||
an install whose romfs/ went empty and unreadable after the extraction verified it (a folder Windows was still
|
||||
deleting, #107) reported success and the game stopped at its first file read."""
|
||||
try:
|
||||
ok = extract.romfs_verified(romfs, expected) and (romfs / 'romfs.bin').stat().st_size == expected['romfs']['size']
|
||||
if ok:
|
||||
with open(romfs / 'romfs.bin', 'rb') as f:
|
||||
f.read(1)
|
||||
except (OSError, ValueError):
|
||||
ok = False
|
||||
if not ok:
|
||||
raise InstallError('cleanup.romfs_unreadable', path=str(romfs))
|
||||
|
||||
|
||||
def all_steps():
|
||||
return [Check(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
@@ -26,6 +26,7 @@
|
||||
"phase.prepare.seed": "Reading the shaders from the game data",
|
||||
"phase.prepare.translate": "Translating the shaders",
|
||||
"phase.prepare.libraries": "Building the shaders for the graphics driver",
|
||||
"phase.prepare.pack": "Packing the shader cache into one file",
|
||||
"phase.cleanup.ini": "Writing totk.ini",
|
||||
"phase.cleanup.temp": "Removing temporary files",
|
||||
|
||||
@@ -75,10 +76,13 @@
|
||||
"prepare.shader_list_missing": "The shader list is missing.",
|
||||
"prepare.romfs_patches_failed": "Writing the game data patches failed (exit code {code}). Details: {log}",
|
||||
"prepare.shaders_failed": "Preparing the shaders failed (exit code {code}). Details: {log}",
|
||||
"cleanup.romfs_unreadable": "The game data in {path} is missing or cannot be read after the install. Restart Windows, then run the installer again on the same folder: it extracts the game data again and keeps the rest.",
|
||||
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
|
||||
"cleanup.ini_failed": "totk.ini cannot be written: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, which the installer does not extract; some text may not show.",
|
||||
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
|
||||
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
|
||||
|
||||
"ui.title": "TotK Setup",
|
||||
"ui.version": "version {version}",
|
||||
@@ -166,8 +170,6 @@
|
||||
"lang.pt-BR": "Portuguese (Brazil)",
|
||||
"lang.ru": "Russian",
|
||||
"lang.ko": "Korean",
|
||||
"lang.zh-CN": "Chinese (Simplified, China)",
|
||||
"lang.zh-TW": "Chinese (Traditional, Taiwan)",
|
||||
"lang.zh-Hans": "Chinese (Simplified)",
|
||||
"lang.zh-Hant": "Chinese (Traditional)"
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
"phase.prepare.seed": "Чтение шейдеров из данных игры",
|
||||
"phase.prepare.translate": "Трансляция шейдеров",
|
||||
"phase.prepare.libraries": "Сборка шейдеров для драйвера видеокарты",
|
||||
"phase.prepare.pack": "Упаковка кэша шейдеров в один файл",
|
||||
"phase.cleanup.ini": "Запись totk.ini",
|
||||
"phase.cleanup.temp": "Удаление временных файлов",
|
||||
|
||||
@@ -75,10 +76,13 @@
|
||||
"prepare.shader_list_missing": "Нет списка шейдеров.",
|
||||
"prepare.romfs_patches_failed": "Не удалось записать патчи данных игры (код выхода {code}). Подробности: {log}",
|
||||
"prepare.shaders_failed": "Не удалось подготовить шейдеры (код выхода {code}). Подробности: {log}",
|
||||
"cleanup.romfs_unreadable": "Данные игры в {path} пропали или не читаются после установки. Перезагрузите Windows и запустите установщик снова на ту же папку: он заново распакует данные игры, остальное сохранится.",
|
||||
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
|
||||
"cleanup.ini_failed": "Не удаётся записать totk.ini: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, которые установка не извлекает; часть текста может не отображаться.",
|
||||
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
|
||||
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
|
||||
|
||||
"ui.title": "Установка TotK",
|
||||
"ui.version": "версия {version}",
|
||||
@@ -166,8 +170,6 @@
|
||||
"lang.pt-BR": "Португальский (Бразилия)",
|
||||
"lang.ru": "Русский",
|
||||
"lang.ko": "Корейский",
|
||||
"lang.zh-CN": "Китайский (упрощённый, КНР)",
|
||||
"lang.zh-TW": "Китайский (традиционный, Тайвань)",
|
||||
"lang.zh-Hans": "Китайский (упрощённый)",
|
||||
"lang.zh-Hant": "Китайский (традиционный)"
|
||||
}
|
||||
+22
-2
@@ -39,6 +39,7 @@ sys.path.insert(0, str(ROOT / 'tools'))
|
||||
sys.path.insert(0, str(ROOT / 'recomp'))
|
||||
|
||||
import flags # noqa: E402
|
||||
import hostmem # noqa: E402
|
||||
import jumptables # noqa: E402
|
||||
import patches # noqa: E402
|
||||
import regs # noqa: E402
|
||||
@@ -587,12 +588,29 @@ def remove_stale(moddir, name, nchunks, errors):
|
||||
return removed
|
||||
|
||||
|
||||
# Peak memory of one emit worker on 1.4.3 is 1.3 GiB (1.5 GiB with margin); the parent holds about 0.7 GiB.
|
||||
WORKER_MEM = 1536 << 20
|
||||
MEM_SHARE = 0.8 # of the free memory the workers may take, the rest is for the system and other programs
|
||||
|
||||
|
||||
def pick_jobs(requested):
|
||||
"""Worker count and the reason for it: --jobs if given, else the CPU count capped by the free memory."""
|
||||
if requested:
|
||||
return requested, '--jobs'
|
||||
cpus = os.cpu_count() or 4
|
||||
free = hostmem.available_ram()
|
||||
if free is None:
|
||||
return cpus, f'{cpus} CPUs, free memory unknown'
|
||||
by_mem = max(1, int(free * MEM_SHARE) // WORKER_MEM)
|
||||
return min(cpus, by_mem), f'{cpus} CPUs, {free / 2**30:.1f} GiB free memory allows {by_mem}'
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--exefs', type=Path, required=True)
|
||||
ap.add_argument('--out', type=Path, required=True)
|
||||
ap.add_argument('--modules', default='main,subsdk0,sdk')
|
||||
ap.add_argument('--jobs', type=int, default=os.cpu_count() or 4)
|
||||
ap.add_argument('--jobs', type=int, help='emit workers (default: by free memory and CPU count)')
|
||||
ap.add_argument('--limit', type=int, default=0, help='emit only the first N functions of each module')
|
||||
ap.add_argument('--chunk-fns', type=int, default=3000, help='functions per chunk at most')
|
||||
ap.add_argument('--chunk-bytes', type=int, default=3000000, help='estimated bytes of C per chunk')
|
||||
@@ -651,7 +669,9 @@ def main():
|
||||
done_at, errors = {}, []
|
||||
t_emit = time.perf_counter()
|
||||
ctx = mp.get_context('spawn')
|
||||
with ctx.Pool(max(1, args.jobs), initializer=_worker_init, initargs=(paths, starts_abs, table, opts)) as pool:
|
||||
jobs, why = pick_jobs(args.jobs)
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts)) as pool:
|
||||
for i, r in enumerate(pool.imap_unordered(_emit_chunk, [t for _, t in tasks], chunksize=1)):
|
||||
results[r['module']].append(r)
|
||||
if r['error']:
|
||||
|
||||
@@ -47,6 +47,18 @@ site cloth main 28bc98 9400546a @page:x8 bd400104 1e240842 1e240863 9400546a #
|
||||
# s1 is set again by the fmov two instructions on.
|
||||
site bounce main 562a64 bd402920 bd402920 bd403921 1e210800 # ldr s0, [x9, #0x28]; ldr s1, [x9, #0x38]; fmul s0, s0, s1
|
||||
|
||||
# Fades (MacNativeGames game_patches_v143.c patch_fade_increment, upstream 3e9e7f67): the distance/LOD mesh fades and
|
||||
# the pre-actor fades add trunc(2.1 * ratio) to a counter each frame: 2 at 30 fps, 1 at 60, 0 above about 63.5 fps
|
||||
# (ratio below 1 / 2.1), where a fade stops and its object stays translucent or invisible until a slower frame moves
|
||||
# it on. Each site floors the increment at 1 without the flags: clz is 32 only for 0, so increment += clz >> 5. The
|
||||
# scratch register is one the next instruction writes (w10 at 0x0a7ac4: ldrb w10; w11 at the pre-actor sites: add
|
||||
# w11), or at 0x95dd24 a temporary the function no longer reads (add, strb, return).
|
||||
set fade TOTK_HIGHFPS_FIX 1
|
||||
site fade main 0a7ac4 1e380008 1e380008 5ac0110a 53057d4a 0b0a0108 # fcvtzs w8, s0; clz w10, w8; lsr w10, w10, #5; add w8, w8, w10
|
||||
site fade main 95dd24 1e380008 1e380008 5ac0110a 53057d4a 0b0a0108 # the same, second mesh fade
|
||||
site fade main 6155e4 1e38000a 1e38000a 5ac0114b 53057d6b 0b0b014a # fcvtzs w10, s0; clz w11, w10; lsr w11, w11, #5; add w10, w10, w11
|
||||
site fade main 615660 1e38000a 1e38000a 5ac0114b 53057d6b 0b0b014a # the same, second pre-actor fade path
|
||||
|
||||
# Output size (upstream game_patches_v143.c patch_output_res; runtime/resolution.c switches the set on). The swapchain
|
||||
# size nnMain writes comes from page +48; the display configuration's canvas (+0x10) and active output (+0x508) from
|
||||
# page +32, and the configuration goes to page +40. x9 is dead at the publication point.
|
||||
@@ -61,6 +73,18 @@ site scene main 585e50 bd0cc660 2d420460 # str s0, [x19, #0xcc4] -> ldp s0, s
|
||||
site scene main 585e54 1e220140 bd0cc660 # scvtf s0, w10 -> str s0, [x19, #0xcc4]
|
||||
site scene main 585e60 bd0cca60 bd0cca61 # str s0, [x19, #0xcc8] -> str s1, [x19, #0xcc8]
|
||||
|
||||
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches_v143.c and aspect_patch.h
|
||||
# aspect_install / aspect_stub; runtime/resolution.c res_init writes the output's width / height to page +56 and
|
||||
# switches the set on). Each site leaves the ratio sead's perspective projection (+0xb0) takes in s0: two camera sites
|
||||
# (update and setup) load the camera's copy of DefaultAspect 1.777778, two viewport routines divide width by height.
|
||||
# The replacement loads the ratio from the page instead, so a 21:9 output shows more of the world at the sides with
|
||||
# the vertical view kept (Hor+). x10 is kept on the stack, so no register liveness is assumed.
|
||||
set aspect TOTK_ASPECT_PATCH 0
|
||||
site aspect main 628c74 bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera update)
|
||||
site aspect main b43460 bd406ac0 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x22, #0x68] (camera setup)
|
||||
site aspect main 1ff04f4 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
site aspect main 253fbb0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
|
||||
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
|
||||
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
|
||||
# its wrapper ZSTD_decompress_usingDDict (main+0x3a1e0, which tail-branches to it) goes to a host function on the patch
|
||||
|
||||
+147
-7
@@ -3,8 +3,10 @@
|
||||
// HDR void extents, random blocks kept by what they exercise (1-4 partitions, dual plane with 1-3 partitions, every
|
||||
// weight grid and quantization the random modes reach), then plain random blocks (mostly error blocks, HDR endpoint
|
||||
// modes among them). Each case decodes two regions into two mip levels of an RGBA8 image with cropped edges, and
|
||||
// compares both the decoder's texels (readback) and what the image holds afterwards. Also times one 2048x2048 texture
|
||||
// on the CPU (astc_decode_ldr_mt) and on the GPU (wall time of the upload batch).
|
||||
// compares both the decoder's texels (readback) and what the image holds afterwards. The same regions also go through
|
||||
// the BC3 encoder (TOTK_ASTC_BC) into a BC3 image, whose blocks are decoded here and held to error bounds (bc_check).
|
||||
// Also times one 2048x2048 texture on the CPU (astc_decode_ldr_mt) and on the GPU (wall time of the upload batch).
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -26,7 +28,8 @@ static const uint8_t dims[14][2] = {{4, 4}, {5, 4}, {5, 5}, {6, 5}, {6, 6}, {8,
|
||||
// Buckets of valid blocks: partitions 1-4 single plane, then dual plane with 1-3 partitions.
|
||||
enum { NB = 7, PER_BUCKET = 48 };
|
||||
static const char *const bucket_name[NB] = {"1p", "2p", "3p", "4p", "dual 1p", "dual 2p", "dual 3p"};
|
||||
static uint64_t g_bucket[NB], g_void, g_err, g_blocks, g_texels;
|
||||
static uint64_t g_bucket[NB], g_void, g_err, g_blocks, g_texels, g_bc_cases;
|
||||
static double g_bc_db = 99; // the lowest BC3 PSNR of all cases
|
||||
|
||||
static int dual_bit(uint32_t m) {
|
||||
if (!(m & 3) && (m >> 7 & 3) == 2) return 0; // the 6..10 x 6..10 modes have no dual plane
|
||||
@@ -77,9 +80,9 @@ typedef struct {
|
||||
VmaAllocation alloc;
|
||||
} Img;
|
||||
|
||||
static int make_image(VkCtx *c, uint32_t w, uint32_t h, uint32_t levels, int srgb, Img *o) {
|
||||
static int make_image(VkCtx *c, uint32_t w, uint32_t h, uint32_t levels, VkFormat format, Img *o) {
|
||||
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, .extent = {w, h, 1},
|
||||
.format = format, .extent = {w, h, 1},
|
||||
.mipLevels = levels, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||
@@ -120,7 +123,7 @@ static double now_ms(void) {
|
||||
static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32_t w0, uint32_t h0, uint32_t w1,
|
||||
uint32_t h1, uint32_t bw, uint32_t bh, int srgb, uint8_t *out_rb, uint8_t *out_img, double *ms) {
|
||||
Img im;
|
||||
if (!make_image(c, w0, h0, 2, srgb, &im)) return 0;
|
||||
if (!make_image(c, w0, h0, 2, srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, &im)) return 0;
|
||||
size_t t0 = (size_t)w0 * h0 * 4, t1 = (size_t)w1 * h1 * 4;
|
||||
VkCommandBuffer cb = vk_upload_begin();
|
||||
VkBuffer sb, rb, ib;
|
||||
@@ -132,7 +135,7 @@ static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32
|
||||
VkAstcRegion ar[2] = {{.src = 0, .w = w0, .h = h0, .slices = 1, .level = 0},
|
||||
{.src = n0 * 16, .w = w1, .h = h1, .slices = 1, .level = 1}};
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, im.img, VK_IMAGE_ASPECT_COLOR_BIT, rb);
|
||||
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, 0, im.img, VK_IMAGE_ASPECT_COLOR_BIT, rb);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
|
||||
VkBufferImageCopy cp[2] = {{.bufferOffset = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w0, h0, 1}},
|
||||
{.bufferOffset = t0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 1, 0, 1}, .imageExtent = {w1, h1, 1}}};
|
||||
@@ -152,6 +155,123 @@ static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The same two regions decoded and encoded to BC3 (TOTK_ASTC_BC) into a BC3 image; out: its blocks read back, level 0
|
||||
// then level 1, rows of 4x4 blocks. 0 on a Vulkan failure or when nothing was recorded.
|
||||
static int gpu_bc(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32_t w0, uint32_t h0, uint32_t w1, uint32_t h1,
|
||||
uint32_t bw, uint32_t bh, int srgb, uint8_t *out) {
|
||||
Img im;
|
||||
if (!make_image(c, w0, h0, 2, srgb ? VK_FORMAT_BC3_SRGB_BLOCK : VK_FORMAT_BC3_UNORM_BLOCK, &im)) return 0;
|
||||
size_t b0 = (size_t)((w0 + 3) / 4) * ((h0 + 3) / 4) * 16, b1 = (size_t)((w1 + 3) / 4) * ((h1 + 3) / 4) * 16;
|
||||
VkCommandBuffer cb = vk_upload_begin();
|
||||
VkBuffer sb, ib;
|
||||
VmaAllocation ia;
|
||||
uint8_t *stg = vk_staging((n0 + n1) * 16, &sb, NULL), *imap = vk_staging(b0 + b1, &ib, &ia);
|
||||
memcpy(stg, blk, (n0 + n1) * 16);
|
||||
to_general(cb, im.img);
|
||||
VkAstcRegion ar[2] = {{.src = 0, .w = w0, .h = h0, .slices = 1, .level = 0},
|
||||
{.src = n0 * 16, .w = w1, .h = h1, .slices = 1, .level = 1}};
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, 1, im.img, VK_IMAGE_ASPECT_COLOR_BIT, VK_NULL_HANDLE);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
|
||||
VkBufferImageCopy cp[2] = {{.bufferOffset = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w0, h0, 1}},
|
||||
{.bufferOffset = b0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 1, 0, 1}, .imageExtent = {w1, h1, 1}}};
|
||||
vkCmdCopyImageToBuffer(cb, im.img, VK_IMAGE_LAYOUT_GENERAL, ib, 2, 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_upload_end();
|
||||
vk_upload_flush(1);
|
||||
vmaInvalidateAllocation(c->vma, ia, 0, VK_WHOLE_SIZE);
|
||||
memcpy(out, imap, b0 + b1);
|
||||
vmaDestroyBuffer(c->vma, ib, ia);
|
||||
vk_defer_image(im.img, im.alloc);
|
||||
vk_upload_flush(1);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// One BC3 block to 16 RGBA8 texels, as the format defines it (565 endpoints widened by bit replication; the colour
|
||||
// block always in four-colour mode). pos: each texel's place on the line from endpoint 0 (0) to endpoint 1 (3).
|
||||
static void bc3_decode(const uint8_t b[16], uint8_t t[16][4], uint8_t ep[2][3], int pos[16]) {
|
||||
uint8_t a[8] = {b[0], b[1]}, p[4][3];
|
||||
if (a[0] > a[1])
|
||||
for (int i = 1; i < 7; i++) a[i + 1] = (uint8_t)(((7 - i) * a[0] + i * a[1] + 3) / 7);
|
||||
else {
|
||||
for (int i = 1; i < 5; i++) a[i + 1] = (uint8_t)(((5 - i) * a[0] + i * a[1] + 2) / 5);
|
||||
a[6] = 0, a[7] = 255;
|
||||
}
|
||||
uint64_t ai = 0;
|
||||
for (int i = 0; i < 6; i++) ai |= (uint64_t)b[2 + i] << 8 * i;
|
||||
for (int k = 0; k < 2; k++) {
|
||||
unsigned v = b[8 + 2 * k] | b[9 + 2 * k] << 8, r = v >> 11, g = v >> 5 & 63, bl = v & 31;
|
||||
p[k][0] = (uint8_t)(r << 3 | r >> 2), p[k][1] = (uint8_t)(g << 2 | g >> 4), p[k][2] = (uint8_t)(bl << 3 | bl >> 2);
|
||||
}
|
||||
for (int ch = 0; ch < 3; ch++)
|
||||
p[2][ch] = (uint8_t)((2 * p[0][ch] + p[1][ch] + 1) / 3), p[3][ch] = (uint8_t)((p[0][ch] + 2 * p[1][ch] + 1) / 3);
|
||||
uint32_t ci = b[12] | b[13] << 8 | b[14] << 16 | (uint32_t)b[15] << 24;
|
||||
static const int code_pos[4] = {0, 3, 1, 2};
|
||||
for (int i = 0; i < 16; i++) {
|
||||
memcpy(t[i], p[ci >> 2 * i & 3], 3);
|
||||
t[i][3] = a[ai >> 3 * i & 7];
|
||||
pos[i] = code_pos[ci >> 2 * i & 3];
|
||||
}
|
||||
memcpy(ep, p, 6);
|
||||
}
|
||||
|
||||
// BC3 is lossy, so these are bounds, not bit-exactness. In every block: alpha within a fourteenth of the block's alpha
|
||||
// range (the eight-value mode's half step); each texel on the palette entry nearest its projection onto the endpoint
|
||||
// line (within rounding, when the endpoints are apart); a block of one colour within the 565 rounding, alpha exact.
|
||||
// Per level, a PSNR floor against astc_decode_ldr for gross damage: the random blocks are noise, so it is low (19 dB
|
||||
// at worst for 5x5 level 1 on an RX 9070). *db: the level's PSNR.
|
||||
enum { BC_MIN_DB = 15 };
|
||||
static int bc_check(const uint8_t *want, const uint8_t *blocks, uint32_t w, uint32_t h, const char *what, double *db) {
|
||||
uint32_t bx = (w + 3) / 4, by = (h + 3) / 4;
|
||||
double se = 0;
|
||||
for (uint32_t y4 = 0; y4 < by; y4++)
|
||||
for (uint32_t x4 = 0; x4 < bx; x4++) {
|
||||
uint8_t t[16][4], ep[2][3], lo[4] = {255, 255, 255, 255}, hi[4] = {0};
|
||||
int pos[16];
|
||||
bc3_decode(blocks + 16 * ((size_t)y4 * bx + x4), t, ep, pos);
|
||||
double axis[3], len2 = 0;
|
||||
for (int ch = 0; ch < 3; ch++) axis[ch] = (double)ep[1][ch] - ep[0][ch], len2 += axis[ch] * axis[ch];
|
||||
for (int pass = 0; pass < 2; pass++)
|
||||
for (int i = 0; i < 16; i++) {
|
||||
uint32_t x = x4 * 4 + (i & 3), y = y4 * 4 + (i >> 2);
|
||||
if (x >= w || y >= h) continue;
|
||||
const uint8_t *s = want + 4 * ((size_t)y * w + x);
|
||||
if (!pass) {
|
||||
for (int ch = 0; ch < 4; ch++) lo[ch] = s[ch] < lo[ch] ? s[ch] : lo[ch], hi[ch] = s[ch] > hi[ch] ? s[ch] : hi[ch];
|
||||
continue;
|
||||
}
|
||||
int flat = !memcmp(lo, hi, 4), lim[4] = {5, 3, 5, 0};
|
||||
if (len2 >= 64) { // shorter lines: rounding alone can move a texel to the next entry
|
||||
double proj = 0;
|
||||
for (int ch = 0; ch < 3; ch++) proj += ((double)s[ch] - ep[0][ch]) * axis[ch];
|
||||
proj = proj / len2 * 3;
|
||||
proj = proj < 0 ? 0 : proj > 3 ? 3 : proj;
|
||||
if (fabs(pos[i] - proj) > 0.75) { // a wrong code mapping is a whole place off
|
||||
printf("probe-astc: FAIL BC3 %s: texel (%u,%u) on palette place %d, its projection is %.2f\n",
|
||||
what, x, y, pos[i], proj);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
for (int ch = 0; ch < 4; ch++) {
|
||||
int d = abs((int)t[i][ch] - s[ch]);
|
||||
se += (double)d * d;
|
||||
if ((ch == 3 && d > (hi[3] - lo[3]) / 14 + 1) || (flat && d > lim[ch])) {
|
||||
printf("probe-astc: FAIL BC3 %s: texel (%u,%u) channel %d: astc %02x%02x%02x%02x bc3 %02x%02x%02x%02x\n",
|
||||
what, x, y, ch, s[0], s[1], s[2], s[3], t[i][0], t[i][1], t[i][2], t[i][3]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
double mse = se / ((double)w * h * 4);
|
||||
*db = mse > 0 ? 10 * log10(255.0 * 255.0 / mse) : 99;
|
||||
if (*db < BC_MIN_DB) {
|
||||
printf("probe-astc: FAIL BC3 %s: PSNR %.1f dB, below %d\n", what, *db, BC_MIN_DB);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int first_diff(const uint8_t *a, const uint8_t *b, size_t texels, size_t *at) {
|
||||
for (size_t i = 0; i < texels; i++)
|
||||
if (memcmp(a + 4 * i, b + 4 * i, 4)) { *at = i; return 1; }
|
||||
@@ -189,6 +309,22 @@ static int test_case(VkCtx *c, uint32_t bw, uint32_t bh, int srgb) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bad && vk_astc_bc()) {
|
||||
size_t b0 = (size_t)((w0 + 3) / 4) * ((h0 + 3) / 4) * 16, b1 = (size_t)((w1 + 3) / 4) * ((h1 + 3) / 4) * 16;
|
||||
uint8_t *bc = malloc(b0 + b1);
|
||||
char what[2][48];
|
||||
double db[2] = {0, 0};
|
||||
for (int lv = 0; lv < 2; lv++) snprintf(what[lv], sizeof what[lv], "%ux%u %s level %d", bw, bh, srgb ? "sRGB" : "linear", lv);
|
||||
if (!gpu_bc(c, blk, n0, n1, w0, h0, w1, h1, bw, bh, srgb, bc)) {
|
||||
printf("probe-astc: FAIL BC3 %s: the encode was not recorded\n", what[0]);
|
||||
bad = 1;
|
||||
}
|
||||
bad = bad || bc_check(want, bc, w0, h0, what[0], &db[0]) || bc_check(want + t0 * 4, bc + b0, w1, h1, what[1], &db[1]);
|
||||
for (int lv = 0; lv < 2 && !bad; lv++)
|
||||
if (db[lv] < g_bc_db) g_bc_db = db[lv];
|
||||
g_bc_cases += !bad;
|
||||
free(bc);
|
||||
}
|
||||
g_blocks += n0 + n1, g_texels += t0 + t1;
|
||||
free(blk), free(want), free(rb), free(im);
|
||||
return bad;
|
||||
@@ -223,6 +359,7 @@ static void timing(VkCtx *c, uint32_t bw, uint32_t bh) {
|
||||
}
|
||||
|
||||
int probe_astc(void) {
|
||||
_putenv_s("TOTK_ASTC_BC", "1"); // also builds the BC3 encoder; the RGBA8 cases pass bc 0 and do not use it
|
||||
if (!vk_init()) { printf("probe-astc: FAIL vk_init\n"); return 1; }
|
||||
VkCtx *c = vk_ctx();
|
||||
printf("probe-astc: GPU %s, textureCompressionASTC_LDR %d, mode %d\n", c->props.deviceName, c->has_astc_ldr,
|
||||
@@ -236,6 +373,9 @@ int probe_astc(void) {
|
||||
for (int s = 0; s < 2 && !bad; s++) bad = test_case(c, dims[i][0], dims[i][1], s);
|
||||
for (int b = 0; b < NB && !bad; b++)
|
||||
if (!g_bucket[b]) { printf("probe-astc: FAIL no valid %s block was generated\n", bucket_name[b]); bad = 1; }
|
||||
if (!bad && !vk_astc_bc()) printf("probe-astc: BC3 encoder not available on this device: not checked\n");
|
||||
else if (!bad) printf("probe-astc: BC3 encode: %llu cases within the bounds, lowest PSNR %.1f dB (floor %d)\n",
|
||||
(unsigned long long)g_bc_cases, g_bc_db, BC_MIN_DB);
|
||||
if (!bad) {
|
||||
timing(c, 4, 4);
|
||||
timing(c, 6, 6);
|
||||
|
||||
+50
-8
@@ -9,6 +9,13 @@
|
||||
// single-producer single-consumer ring. The SDL audio thread drains that ring without taking any lock: an empty ring
|
||||
// plays silence and never blocks the render thread or the guest. TOTK_AUDIO=none (or TOTK_AUDIO_OFF) mixes without a
|
||||
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20) is the queued audio the output waits for after start or underrun.
|
||||
// The device's clock paces the render thread, as the audio hardware paces the Switch's DSP (audio_mixer_pace_ns): a
|
||||
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
|
||||
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart. LASRecomp and
|
||||
// SavageWindRecomp pace their renderers by the SDL stream the same way. Without a device, while the renderer is
|
||||
// stopped and while the device takes nothing (a stall), the render thread keeps real time on its own timer; what piled
|
||||
// up meanwhile is skipped on the SDL thread once the device resumes (more than QUEUE_SLACK_MS above the target plus the
|
||||
// device's request), as MacNativeGames' audio_mixer.c trims its ring after a device stall.
|
||||
#include <math.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
@@ -26,6 +33,8 @@
|
||||
|
||||
#define MAX_FRAME 1024
|
||||
#define RING_FRAMES 16384 // stereo frames, about 340 ms at 48 kHz
|
||||
#define QUEUE_SLACK_MS 40 // queued audio above the target plus the device's request that ring_pull skips
|
||||
#define STALL_MS 50 // the device took nothing this long (at least 4 periods): the timer paces
|
||||
#define SPLITTER_UNUSED 0xffffffffu
|
||||
#define dm_guest_write_prepare(p, n) ((void)(p), (void)(n)) // guest memory is plain host memory on Windows
|
||||
|
||||
@@ -43,8 +52,11 @@ typedef struct Mixer {
|
||||
SplitInfo *split; int nsplit; SplitDest *dest; int ndest;
|
||||
SDL_AudioStream *stream; int device_ok, rate;
|
||||
float ring[RING_FRAMES * 2];
|
||||
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero;
|
||||
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero, skipped_frames;
|
||||
_Atomic int playing; int ready; // ready: the callback's own state
|
||||
int target, period; // output target and device period, frames (output_open)
|
||||
uint64_t stall_ns;
|
||||
_Atomic uint64_t pulled_ns; // kern_now_ns of the device's last request, 0 before the first
|
||||
_Atomic uint64_t flush_until; _Atomic unsigned stop_epoch; unsigned callback_epoch;
|
||||
EffectState *effect; int *effect_order; unsigned effect_reported;
|
||||
uint64_t *sink_address; uint32_t *sink_size, *sink_offset, *sink_old, *sink_written;
|
||||
@@ -107,16 +119,23 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
|
||||
atomic_store_explicit(&m->rd, wr, memory_order_release);
|
||||
return;
|
||||
}
|
||||
uint64_t prefill = (uint64_t)m->frame * (uint64_t)m->prefill;
|
||||
if (prefill < frames) prefill = frames;
|
||||
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
|
||||
if (!m->ready) {
|
||||
uint64_t prefill = (uint64_t)m->frame * (uint64_t)m->prefill;
|
||||
if (prefill < frames) prefill = frames;
|
||||
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
|
||||
if (have < prefill) {
|
||||
memset(out, 0, (size_t)frames * 2 * sizeof(float));
|
||||
return;
|
||||
}
|
||||
m->ready = 1;
|
||||
}
|
||||
uint64_t keep = (prefill > (uint64_t)m->target ? prefill : (uint64_t)m->target) + frames;
|
||||
uint64_t slack = (uint64_t)m->rate * QUEUE_SLACK_MS / 1000;
|
||||
if (have > keep + slack) { // piled up while the device stalled: skip the oldest, keep target + request
|
||||
atomic_fetch_add(&m->skipped_frames, have - keep);
|
||||
rd = wr - keep;
|
||||
have = keep;
|
||||
}
|
||||
uint32_t n = frames < have ? frames : (uint32_t)have;
|
||||
uint64_t nonzero = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
@@ -139,6 +158,7 @@ static void SDLCALL sdl_pull(void *user, SDL_AudioStream *stream, int additional
|
||||
(void)total;
|
||||
Mixer *m = user;
|
||||
float tmp[2 * 1024];
|
||||
atomic_store_explicit(&m->pulled_ns, kern_now_ns(), memory_order_release);
|
||||
while (additional > 0) {
|
||||
uint32_t frames = (uint32_t)additional / (2 * sizeof(float));
|
||||
if (!frames) break;
|
||||
@@ -172,8 +192,28 @@ static void output_open(Mixer *m, int rate) {
|
||||
return;
|
||||
}
|
||||
m->device_ok = 1;
|
||||
SDL_AudioSpec dev;
|
||||
int period = 0;
|
||||
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(m->stream), &dev, &period) || period <= 0) period = m->frame;
|
||||
m->period = period;
|
||||
m->target = m->frame * m->prefill > 2 * period ? m->frame * m->prefill : 2 * period;
|
||||
if (m->target > RING_FRAMES / 2) m->target = RING_FRAMES / 2;
|
||||
uint64_t period_ns = (uint64_t)period * 1000000000ull / (uint64_t)rate;
|
||||
m->stall_ns = 4 * period_ns > STALL_MS * 1000000ull ? 4 * period_ns : STALL_MS * 1000000ull;
|
||||
const char *name = SDL_GetAudioDeviceName(SDL_GetAudioStreamDevice(m->stream));
|
||||
rt_log("audio: SDL device '%s' opened, %d Hz stereo float, prefill %d ms", name ? name : "?", rate, m->prefill * 5);
|
||||
rt_log("audio: SDL device '%s' opened, %d Hz stereo float, prefill %d ms, device period %d frames; the device paces "
|
||||
"rendering at %.1f ms queued", name ? name : "?", rate, m->prefill * 5, period, m->target * 1000.0 / rate);
|
||||
}
|
||||
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
|
||||
Mixer *m = r->mixer;
|
||||
if (!m || !m->device_ok || !atomic_load_explicit(&m->playing, memory_order_acquire)) return -1;
|
||||
uint64_t pulled = atomic_load_explicit(&m->pulled_ns, memory_order_acquire);
|
||||
if (!pulled || (now > pulled && now - pulled > m->stall_ns)) return -1;
|
||||
uint64_t have = atomic_load(&m->wr) - atomic_load_explicit(&m->rd, memory_order_acquire);
|
||||
if (have < (uint64_t)m->target) return 0;
|
||||
int64_t ns = (int64_t)((have - (uint64_t)m->target + 1) * 1000000000ull / (uint64_t)m->rate);
|
||||
return ns < 500000 ? 500000 : ns; // until the device has taken it below the target
|
||||
}
|
||||
|
||||
// Render thread only.
|
||||
@@ -209,12 +249,14 @@ static void level_note(AudioRenderer *r, Mixer *m, const float *lr, int frames)
|
||||
rt_log("audio: first nonzero output after %llu frames", (unsigned long long)r->elapsed_frames);
|
||||
}
|
||||
if (m->level_frames >= (uint64_t)r->param.sample_rate * 5) {
|
||||
uint64_t wr = atomic_load(&m->wr), queued = wr - atomic_load(&m->rd);
|
||||
rt_log("audio: level rms %.4f peak %.4f over %.1f s; written %llu frames, underruns %llu, overruns %llu, "
|
||||
"missing frames %llu, callback frames %llu (nonzero %llu)",
|
||||
"missing frames %llu, callback frames %llu (nonzero %llu), queued %.1f ms, skipped %llu frames",
|
||||
sqrt(m->level_sum / (double)m->level_count), m->level_peak, m->level_frames / (double)r->param.sample_rate,
|
||||
(unsigned long long)atomic_load(&m->wr), (unsigned long long)atomic_load(&m->underruns),
|
||||
(unsigned long long)wr, (unsigned long long)atomic_load(&m->underruns),
|
||||
(unsigned long long)atomic_load(&m->overruns), (unsigned long long)atomic_load(&m->missing_frames),
|
||||
(unsigned long long)atomic_load(&m->callback_frames), (unsigned long long)atomic_load(&m->callback_nonzero));
|
||||
(unsigned long long)atomic_load(&m->callback_frames), (unsigned long long)atomic_load(&m->callback_nonzero),
|
||||
queued * 1000.0 / r->param.sample_rate, (unsigned long long)atomic_load(&m->skipped_frames));
|
||||
m->level_sum = m->level_peak = 0;
|
||||
m->level_count = m->level_frames = 0;
|
||||
}
|
||||
|
||||
+21
-10
@@ -316,7 +316,8 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
|
||||
ReleaseSRWLockExclusive(&r->lock);
|
||||
}
|
||||
|
||||
// ---- render tick: one frame every sample_count / sample_rate seconds, on a host thread
|
||||
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
|
||||
// or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls
|
||||
static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds) {
|
||||
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return;
|
||||
double want = v->fraction + out_seconds * v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0);
|
||||
@@ -348,6 +349,12 @@ static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds)
|
||||
}
|
||||
}
|
||||
|
||||
static void wait_ns(HANDLE timer, uint64_t ns) {
|
||||
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)((ns + 99) / 100)}; // relative, 100 ns units
|
||||
if (!SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE)) rt_fatal("audren: SetWaitableTimer failed (error %lu)", GetLastError());
|
||||
WaitForSingleObject(timer, INFINITE);
|
||||
}
|
||||
|
||||
static DWORD WINAPI render_thread(void *p) {
|
||||
AudioRenderer *r = p;
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
||||
@@ -358,6 +365,19 @@ static DWORD WINAPI render_thread(void *p) {
|
||||
const double frame = (double)r->param.sample_count / r->param.sample_rate;
|
||||
uint64_t next = kern_now_ns();
|
||||
while (InterlockedCompareExchange(&r->running, 0, 0)) {
|
||||
uint64_t now = kern_now_ns();
|
||||
int64_t pace = audio_mixer_pace_ns(r, now);
|
||||
if (pace > 0) {
|
||||
wait_ns(timer, (uint64_t)pace);
|
||||
continue;
|
||||
}
|
||||
if (pace == 0) {
|
||||
next = now + frame_ns; // where the timer goes on if the device stops pacing
|
||||
} else {
|
||||
if (next > now) wait_ns(timer, next - now);
|
||||
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
|
||||
next += frame_ns;
|
||||
}
|
||||
AcquireSRWLockExclusive(&r->lock);
|
||||
if (r->state == 0) {
|
||||
voices_prepare(r);
|
||||
@@ -374,15 +394,6 @@ static DWORD WINAPI render_thread(void *p) {
|
||||
}
|
||||
ReleaseSRWLockExclusive(&r->lock);
|
||||
kern_event_signal(r->event);
|
||||
next += frame_ns;
|
||||
uint64_t now = kern_now_ns();
|
||||
if (next > now) {
|
||||
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)((next - now + 99) / 100)}; // relative, 100 ns units
|
||||
if (!SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE))
|
||||
rt_fatal("audren: SetWaitableTimer failed (error %lu)", GetLastError());
|
||||
WaitForSingleObject(timer, INFINITE);
|
||||
} else if (now - next > 100000000ull)
|
||||
next = now; // fell far behind (debugger, sleep): resynchronise
|
||||
}
|
||||
CloseHandle(timer);
|
||||
return 0;
|
||||
|
||||
@@ -180,5 +180,9 @@ void audio_mixer_update_effects(AudioRenderer *r);
|
||||
void audio_mixer_effect_result(AudioRenderer *r, int id, void *output, unsigned size);
|
||||
uint32_t audio_mixer_sink_written(AudioRenderer *r, int id);
|
||||
void audio_mixer_render(AudioRenderer *r, double seconds); // after audio_voices_prepare, before voices advance
|
||||
// Device pacing for the render thread at kern_now_ns `now`: 0 renders the next frame now, a positive value waits that
|
||||
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
|
||||
// device stalled).
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
|
||||
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
|
||||
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
|
||||
@@ -0,0 +1,253 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/bc3_encode.comp; do not edit.
|
||||
// source sha256 ac318d358136b39a113a18474a815f447223667657c90f9e0356a392639d8541
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t bc3_encode_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x00000314, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000007f, 0x00060010, 0x00000004,
|
||||
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00030047, 0x0000001b, 0x00000002, 0x00050048,
|
||||
0x0000001b, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000001b, 0x00000001, 0x00000023,
|
||||
0x00000004, 0x00050048, 0x0000001b, 0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x0000001b,
|
||||
0x00000003, 0x00000023, 0x0000000c, 0x00050048, 0x0000001b, 0x00000004, 0x00000023, 0x00000010,
|
||||
0x00050048, 0x0000001b, 0x00000005, 0x00000023, 0x00000014, 0x00040047, 0x0000002c, 0x00000006,
|
||||
0x00000004, 0x00030047, 0x0000002d, 0x00000002, 0x00040048, 0x0000002d, 0x00000000, 0x00000018,
|
||||
0x00050048, 0x0000002d, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x0000002f, 0x00000018,
|
||||
0x00040047, 0x0000002f, 0x00000021, 0x00000000, 0x00040047, 0x0000002f, 0x00000022, 0x00000000,
|
||||
0x00040047, 0x0000007f, 0x0000000b, 0x0000001c, 0x00040047, 0x000001e9, 0x00000006, 0x00000010,
|
||||
0x00030047, 0x000001ea, 0x00000002, 0x00040048, 0x000001ea, 0x00000000, 0x00000019, 0x00050048,
|
||||
0x000001ea, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x000001ec, 0x00000019, 0x00040047,
|
||||
0x000001ec, 0x00000021, 0x00000001, 0x00040047, 0x000001ec, 0x00000022, 0x00000000, 0x00040047,
|
||||
0x0000020c, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
|
||||
0x00040015, 0x00000006, 0x00000020, 0x00000000, 0x00030016, 0x00000008, 0x00000020, 0x00040017,
|
||||
0x00000009, 0x00000008, 0x00000004, 0x00040017, 0x00000010, 0x00000008, 0x00000003, 0x0008001e,
|
||||
0x0000001b, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00040020,
|
||||
0x0000001c, 0x00000009, 0x0000001b, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000009, 0x00040015,
|
||||
0x0000001e, 0x00000020, 0x00000001, 0x0004002b, 0x0000001e, 0x0000001f, 0x00000002, 0x00040020,
|
||||
0x00000020, 0x00000009, 0x00000006, 0x0004002b, 0x00000006, 0x00000023, 0x00000001, 0x0004002b,
|
||||
0x0000001e, 0x00000027, 0x00000003, 0x0003001d, 0x0000002c, 0x00000006, 0x0003001e, 0x0000002d,
|
||||
0x0000002c, 0x00040020, 0x0000002e, 0x0000000c, 0x0000002d, 0x0004003b, 0x0000002e, 0x0000002f,
|
||||
0x0000000c, 0x0004002b, 0x0000001e, 0x00000030, 0x00000000, 0x00040020, 0x0000003f, 0x0000000c,
|
||||
0x00000006, 0x0004002b, 0x00000008, 0x00000043, 0x437f0000, 0x00040017, 0x00000047, 0x00000006,
|
||||
0x00000003, 0x0004002b, 0x00000008, 0x0000004b, 0x00000000, 0x0004002b, 0x00000006, 0x00000057,
|
||||
0x00000000, 0x0004002b, 0x0000001e, 0x0000005a, 0x0000000b, 0x0004002b, 0x0000001e, 0x0000005e,
|
||||
0x00000005, 0x0004002b, 0x00000006, 0x00000061, 0x00000002, 0x0004002b, 0x00000006, 0x00000069,
|
||||
0x0000001f, 0x0004002b, 0x00000006, 0x00000070, 0x0000003f, 0x00040020, 0x0000007e, 0x00000001,
|
||||
0x00000047, 0x0004003b, 0x0000007e, 0x0000007f, 0x00000001, 0x0004002b, 0x00000006, 0x00000084,
|
||||
0x00000003, 0x0004002b, 0x00000006, 0x00000086, 0x00000004, 0x00020014, 0x00000088, 0x0004002b,
|
||||
0x0000001e, 0x0000008b, 0x00000004, 0x00040020, 0x0000009a, 0x00000007, 0x00000009, 0x0007002c,
|
||||
0x00000009, 0x0000009c, 0x00000043, 0x00000043, 0x00000043, 0x00000043, 0x0007002c, 0x00000009,
|
||||
0x0000009e, 0x0000004b, 0x0000004b, 0x0000004b, 0x0000004b, 0x0004002b, 0x00000006, 0x000000a7,
|
||||
0x00000010, 0x0004001c, 0x000000a9, 0x00000009, 0x000000a7, 0x00040020, 0x000000aa, 0x00000007,
|
||||
0x000000a9, 0x0004002b, 0x0000001e, 0x000000d0, 0x00000001, 0x00040020, 0x000000d7, 0x00000007,
|
||||
0x00000008, 0x00040017, 0x000000df, 0x00000006, 0x00000002, 0x0004002b, 0x0000001e, 0x000000e4,
|
||||
0x00000008, 0x0004002b, 0x00000008, 0x00000101, 0x40e00000, 0x0004002b, 0x00000006, 0x00000110,
|
||||
0x00000007, 0x0004002b, 0x00000006, 0x0000011f, 0x00000020, 0x0004002b, 0x00000008, 0x000001b5,
|
||||
0x358637bd, 0x0004002b, 0x00000008, 0x000001ca, 0x40400000, 0x00040017, 0x000001e8, 0x00000006,
|
||||
0x00000004, 0x0003001d, 0x000001e9, 0x000001e8, 0x0003001e, 0x000001ea, 0x000001e9, 0x00040020,
|
||||
0x000001eb, 0x0000000c, 0x000001ea, 0x0004003b, 0x000001eb, 0x000001ec, 0x0000000c, 0x0004002b,
|
||||
0x0000001e, 0x00000204, 0x00000010, 0x00040020, 0x00000209, 0x0000000c, 0x000001e8, 0x0004002b,
|
||||
0x00000006, 0x0000020b, 0x00000008, 0x0006002c, 0x00000047, 0x0000020c, 0x0000020b, 0x0000020b,
|
||||
0x00000023, 0x0006002c, 0x00000010, 0x00000302, 0x0000004b, 0x0000004b, 0x0000004b, 0x0006002c,
|
||||
0x00000010, 0x00000303, 0x00000043, 0x00000043, 0x00000043, 0x0004002b, 0x00000008, 0x00000304,
|
||||
0x3d800000, 0x0007002c, 0x00000009, 0x00000305, 0x00000304, 0x00000304, 0x00000304, 0x00000304,
|
||||
0x0006002c, 0x00000010, 0x00000306, 0x00000304, 0x00000304, 0x00000304, 0x0004002b, 0x00000008,
|
||||
0x00000309, 0x3df8f8fa, 0x0004002b, 0x00000008, 0x0000030a, 0x3e7cfcfe, 0x0006002c, 0x00000010,
|
||||
0x0000030b, 0x00000309, 0x0000030a, 0x00000309, 0x0004002b, 0x00000008, 0x0000030d, 0x41039ce7,
|
||||
0x0004002b, 0x00000008, 0x0000030f, 0x40818619, 0x0004002b, 0x00000006, 0x00000310, 0x00000013,
|
||||
0x0004002b, 0x00000006, 0x00000311, 0xfffffff0, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
|
||||
0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x000000aa, 0x000000ab, 0x00000007, 0x000300f7,
|
||||
0x0000020d, 0x00000000, 0x000300fb, 0x00000057, 0x0000020e, 0x000200f8, 0x0000020e, 0x0004003d,
|
||||
0x00000047, 0x00000080, 0x0000007f, 0x00050041, 0x00000020, 0x00000082, 0x0000001d, 0x00000027,
|
||||
0x0004003d, 0x00000006, 0x00000083, 0x00000082, 0x00050080, 0x00000006, 0x00000085, 0x00000083,
|
||||
0x00000084, 0x00050086, 0x00000006, 0x00000087, 0x00000085, 0x00000086, 0x00050051, 0x00000006,
|
||||
0x0000008a, 0x00000080, 0x00000000, 0x00050041, 0x00000020, 0x0000008c, 0x0000001d, 0x0000008b,
|
||||
0x0004003d, 0x00000006, 0x0000008d, 0x0000008c, 0x000500ae, 0x00000088, 0x0000008e, 0x0000008a,
|
||||
0x0000008d, 0x000400a8, 0x00000088, 0x0000008f, 0x0000008e, 0x000300f7, 0x00000091, 0x00000000,
|
||||
0x000400fa, 0x0000008f, 0x00000090, 0x00000091, 0x000200f8, 0x00000090, 0x00050051, 0x00000006,
|
||||
0x00000093, 0x00000080, 0x00000001, 0x000500ae, 0x00000088, 0x00000095, 0x00000093, 0x00000087,
|
||||
0x000200f9, 0x00000091, 0x000200f8, 0x00000091, 0x000700f5, 0x00000088, 0x00000096, 0x0000008e,
|
||||
0x0000020e, 0x00000095, 0x00000090, 0x000300f7, 0x00000098, 0x00000000, 0x000400fa, 0x00000096,
|
||||
0x00000097, 0x00000098, 0x000200f8, 0x00000097, 0x000200f9, 0x0000020d, 0x000200f8, 0x00000098,
|
||||
0x000200f9, 0x000000a1, 0x000200f8, 0x000000a1, 0x000700f5, 0x00000009, 0x000002b0, 0x0000009c,
|
||||
0x00000098, 0x000000c4, 0x000000a2, 0x000700f5, 0x00000009, 0x000002af, 0x0000009e, 0x00000098,
|
||||
0x000000c9, 0x000000a2, 0x000700f5, 0x00000009, 0x000002ae, 0x0000009e, 0x00000098, 0x000000ce,
|
||||
0x000000a2, 0x000700f5, 0x00000006, 0x000002ad, 0x00000057, 0x00000098, 0x000000d1, 0x000000a2,
|
||||
0x000500b0, 0x00000088, 0x000000a8, 0x000002ad, 0x000000a7, 0x000400f6, 0x000000a3, 0x000000a2,
|
||||
0x00000000, 0x000400fa, 0x000000a8, 0x000000a2, 0x000000a3, 0x000200f8, 0x000000a2, 0x00050084,
|
||||
0x00000006, 0x000000af, 0x0000008a, 0x00000086, 0x000500c7, 0x00000006, 0x000000b1, 0x000002ad,
|
||||
0x00000084, 0x00050080, 0x00000006, 0x000000b2, 0x000000af, 0x000000b1, 0x00050051, 0x00000006,
|
||||
0x000000b4, 0x00000080, 0x00000001, 0x00050084, 0x00000006, 0x000000b5, 0x000000b4, 0x00000086,
|
||||
0x000500c2, 0x00000006, 0x000000b7, 0x000002ad, 0x0000001f, 0x00050080, 0x00000006, 0x000000b8,
|
||||
0x000000b5, 0x000000b7, 0x00050051, 0x00000006, 0x000000bd, 0x00000080, 0x00000002, 0x00050041,
|
||||
0x00000020, 0x00000216, 0x0000001d, 0x0000001f, 0x0004003d, 0x00000006, 0x00000217, 0x00000216,
|
||||
0x00050082, 0x00000006, 0x00000218, 0x00000217, 0x00000023, 0x0007000c, 0x00000006, 0x00000219,
|
||||
0x00000001, 0x00000026, 0x000000b2, 0x00000218, 0x00050082, 0x00000006, 0x0000021d, 0x00000083,
|
||||
0x00000023, 0x0007000c, 0x00000006, 0x0000021e, 0x00000001, 0x00000026, 0x000000b8, 0x0000021d,
|
||||
0x00050041, 0x00000020, 0x0000021f, 0x0000001d, 0x00000030, 0x0004003d, 0x00000006, 0x00000220,
|
||||
0x0000021f, 0x00050084, 0x00000006, 0x00000224, 0x000000bd, 0x00000083, 0x00050080, 0x00000006,
|
||||
0x00000226, 0x00000224, 0x0000021e, 0x00050084, 0x00000006, 0x00000229, 0x00000226, 0x00000217,
|
||||
0x00050080, 0x00000006, 0x0000022a, 0x00000220, 0x00000229, 0x00050080, 0x00000006, 0x0000022c,
|
||||
0x0000022a, 0x00000219, 0x00060041, 0x0000003f, 0x0000022d, 0x0000002f, 0x00000030, 0x0000022c,
|
||||
0x0004003d, 0x00000006, 0x0000022e, 0x0000022d, 0x0006000c, 0x00000009, 0x0000022f, 0x00000001,
|
||||
0x00000040, 0x0000022e, 0x0005008e, 0x00000009, 0x00000230, 0x0000022f, 0x00000043, 0x00050041,
|
||||
0x0000009a, 0x000000bf, 0x000000ab, 0x000002ad, 0x0003003e, 0x000000bf, 0x00000230, 0x0004003d,
|
||||
0x00000009, 0x000000c3, 0x000000bf, 0x0007000c, 0x00000009, 0x000000c4, 0x00000001, 0x00000025,
|
||||
0x000002b0, 0x000000c3, 0x0004003d, 0x00000009, 0x000000c8, 0x000000bf, 0x0007000c, 0x00000009,
|
||||
0x000000c9, 0x00000001, 0x00000028, 0x000002af, 0x000000c8, 0x0004003d, 0x00000009, 0x000000cc,
|
||||
0x000000bf, 0x00050081, 0x00000009, 0x000000ce, 0x000002ae, 0x000000cc, 0x00050080, 0x00000006,
|
||||
0x000000d1, 0x000002ad, 0x000000d0, 0x000200f9, 0x000000a1, 0x000200f8, 0x000000a3, 0x00050085,
|
||||
0x00000009, 0x000000d5, 0x000002ae, 0x00000305, 0x00050051, 0x00000008, 0x000000d9, 0x000002af,
|
||||
0x00000003, 0x0004006d, 0x00000006, 0x000000da, 0x000000d9, 0x00050051, 0x00000008, 0x000000dd,
|
||||
0x000002b0, 0x00000003, 0x0004006d, 0x00000006, 0x000000de, 0x000000dd, 0x000500c4, 0x00000006,
|
||||
0x000000e5, 0x000000de, 0x000000e4, 0x000500c5, 0x00000006, 0x000000e6, 0x000000da, 0x000000e5,
|
||||
0x00050050, 0x000000df, 0x000000e7, 0x000000e6, 0x00000057, 0x000500ac, 0x00000088, 0x000000ea,
|
||||
0x000000da, 0x000000de, 0x000300f7, 0x000000ec, 0x00000000, 0x000400fa, 0x000000ea, 0x000000eb,
|
||||
0x000000ec, 0x000200f8, 0x000000eb, 0x00050082, 0x00000006, 0x000000f0, 0x000000da, 0x000000de,
|
||||
0x00040070, 0x00000008, 0x000000f1, 0x000000f0, 0x000200f9, 0x000000f3, 0x000200f8, 0x000000f3,
|
||||
0x000700f5, 0x000000df, 0x000002c8, 0x000000e7, 0x000000eb, 0x000002f1, 0x000000f6, 0x000700f5,
|
||||
0x00000006, 0x000002b1, 0x00000057, 0x000000eb, 0x00000143, 0x000000f6, 0x000500b0, 0x00000088,
|
||||
0x000000f9, 0x000002b1, 0x000000a7, 0x000400f6, 0x000000f5, 0x000000f6, 0x00000000, 0x000400fa,
|
||||
0x000000f9, 0x000000f4, 0x000000f5, 0x000200f8, 0x000000f4, 0x00040070, 0x00000008, 0x000000fc,
|
||||
0x000000da, 0x00060041, 0x000000d7, 0x000000fe, 0x000000ab, 0x000002b1, 0x00000084, 0x0004003d,
|
||||
0x00000008, 0x000000ff, 0x000000fe, 0x00050083, 0x00000008, 0x00000100, 0x000000fc, 0x000000ff,
|
||||
0x00050085, 0x00000008, 0x00000102, 0x00000100, 0x00000101, 0x00050088, 0x00000008, 0x00000104,
|
||||
0x00000102, 0x000000f1, 0x0006000c, 0x00000008, 0x00000105, 0x00000001, 0x00000001, 0x00000104,
|
||||
0x0008000c, 0x00000008, 0x00000106, 0x00000001, 0x0000002b, 0x00000105, 0x0000004b, 0x00000101,
|
||||
0x0004006d, 0x00000006, 0x00000107, 0x00000106, 0x000500aa, 0x00000088, 0x0000010a, 0x00000107,
|
||||
0x00000057, 0x000300f7, 0x0000010d, 0x00000000, 0x000400fa, 0x0000010a, 0x0000010c, 0x0000010e,
|
||||
0x000200f8, 0x0000010c, 0x000200f9, 0x0000010d, 0x000200f8, 0x0000010e, 0x000500aa, 0x00000088,
|
||||
0x00000111, 0x00000107, 0x00000110, 0x000300f7, 0x00000114, 0x00000000, 0x000400fa, 0x00000111,
|
||||
0x00000113, 0x00000115, 0x000200f8, 0x00000113, 0x000200f9, 0x00000114, 0x000200f8, 0x00000115,
|
||||
0x00050080, 0x00000006, 0x00000117, 0x00000107, 0x00000023, 0x000200f9, 0x00000114, 0x000200f8,
|
||||
0x00000114, 0x000700f5, 0x00000006, 0x000002db, 0x00000023, 0x00000113, 0x00000117, 0x00000115,
|
||||
0x000200f9, 0x0000010d, 0x000200f8, 0x0000010d, 0x000700f5, 0x00000006, 0x000002dc, 0x00000057,
|
||||
0x0000010c, 0x000002db, 0x00000114, 0x00050084, 0x00000006, 0x0000011c, 0x00000084, 0x000002b1,
|
||||
0x00050080, 0x00000006, 0x0000011d, 0x000000a7, 0x0000011c, 0x000500b0, 0x00000088, 0x00000120,
|
||||
0x0000011d, 0x0000011f, 0x000300f7, 0x00000122, 0x00000000, 0x000400fa, 0x00000120, 0x00000121,
|
||||
0x00000122, 0x000200f8, 0x00000121, 0x000500c4, 0x00000006, 0x00000125, 0x000002dc, 0x0000011d,
|
||||
0x00050051, 0x00000006, 0x00000127, 0x000002c8, 0x00000000, 0x000500c5, 0x00000006, 0x00000128,
|
||||
0x00000127, 0x00000125, 0x00060052, 0x000000df, 0x0000028e, 0x00000128, 0x000002c8, 0x00000000,
|
||||
0x000200f9, 0x00000122, 0x000200f8, 0x00000122, 0x000700f5, 0x000000df, 0x000002e3, 0x000002c8,
|
||||
0x0000010d, 0x0000028e, 0x00000121, 0x00050080, 0x00000006, 0x0000012b, 0x0000011c, 0x00000310,
|
||||
0x000500ac, 0x00000088, 0x0000012c, 0x0000012b, 0x0000011f, 0x000300f7, 0x0000012e, 0x00000000,
|
||||
0x000400fa, 0x0000012c, 0x0000012d, 0x0000012e, 0x000200f8, 0x0000012d, 0x0007000c, 0x00000006,
|
||||
0x00000131, 0x00000001, 0x00000026, 0x0000011d, 0x0000011f, 0x00050082, 0x00000006, 0x00000132,
|
||||
0x0000011f, 0x00000131, 0x000500c2, 0x00000006, 0x00000133, 0x000002dc, 0x00000132, 0x000500ac,
|
||||
0x00000088, 0x00000135, 0x0000011d, 0x0000011f, 0x000300f7, 0x00000138, 0x00000000, 0x000400fa,
|
||||
0x00000135, 0x00000137, 0x0000013b, 0x000200f8, 0x00000137, 0x00050080, 0x00000006, 0x0000013a,
|
||||
0x0000011c, 0x00000311, 0x000200f9, 0x00000138, 0x000200f8, 0x0000013b, 0x000200f9, 0x00000138,
|
||||
0x000200f8, 0x00000138, 0x000700f5, 0x00000006, 0x000002e1, 0x0000013a, 0x00000137, 0x00000057,
|
||||
0x0000013b, 0x000500c4, 0x00000006, 0x0000013d, 0x00000133, 0x000002e1, 0x00050051, 0x00000006,
|
||||
0x0000013f, 0x000002e3, 0x00000001, 0x000500c5, 0x00000006, 0x00000140, 0x0000013f, 0x0000013d,
|
||||
0x00060052, 0x000000df, 0x00000291, 0x00000140, 0x000002e3, 0x00000001, 0x000200f9, 0x0000012e,
|
||||
0x000200f8, 0x0000012e, 0x000700f5, 0x000000df, 0x000002f1, 0x000002e3, 0x00000122, 0x00000291,
|
||||
0x00000138, 0x000200f9, 0x000000f6, 0x000200f8, 0x000000f6, 0x00050080, 0x00000006, 0x00000143,
|
||||
0x000002b1, 0x000000d0, 0x000200f9, 0x000000f3, 0x000200f8, 0x000000f5, 0x000200f9, 0x000000ec,
|
||||
0x000200f8, 0x000000ec, 0x000700f5, 0x000000df, 0x000002c7, 0x000000e7, 0x000000a3, 0x000002c8,
|
||||
0x000000f5, 0x0008004f, 0x00000010, 0x00000146, 0x000002b0, 0x000002b0, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x0008004f, 0x00000010, 0x00000149, 0x000002af, 0x000002af, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x000200f9, 0x0000014d, 0x000200f8, 0x0000014d, 0x000700f5, 0x00000008, 0x000002bb,
|
||||
0x0000004b, 0x000000ec, 0x00000169, 0x0000014e, 0x000700f5, 0x00000008, 0x000002b7, 0x0000004b,
|
||||
0x000000ec, 0x00000162, 0x0000014e, 0x000700f5, 0x00000006, 0x000002b6, 0x00000057, 0x000000ec,
|
||||
0x0000016b, 0x0000014e, 0x000500b0, 0x00000088, 0x00000153, 0x000002b6, 0x000000a7, 0x000400f6,
|
||||
0x0000014f, 0x0000014e, 0x00000000, 0x000400fa, 0x00000153, 0x0000014e, 0x0000014f, 0x000200f8,
|
||||
0x0000014e, 0x00050041, 0x0000009a, 0x00000156, 0x000000ab, 0x000002b6, 0x0004003d, 0x00000009,
|
||||
0x00000157, 0x00000156, 0x0008004f, 0x00000010, 0x00000158, 0x00000157, 0x00000157, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x0008004f, 0x00000010, 0x0000015a, 0x000000d5, 0x000000d5, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x00050083, 0x00000010, 0x0000015b, 0x00000158, 0x0000015a, 0x00050051,
|
||||
0x00000008, 0x0000015d, 0x0000015b, 0x00000000, 0x00050051, 0x00000008, 0x0000015f, 0x0000015b,
|
||||
0x00000001, 0x00050085, 0x00000008, 0x00000160, 0x0000015d, 0x0000015f, 0x00050081, 0x00000008,
|
||||
0x00000162, 0x000002b7, 0x00000160, 0x00050051, 0x00000008, 0x00000164, 0x0000015b, 0x00000002,
|
||||
0x00050085, 0x00000008, 0x00000167, 0x00000164, 0x0000015f, 0x00050081, 0x00000008, 0x00000169,
|
||||
0x000002bb, 0x00000167, 0x00050080, 0x00000006, 0x0000016b, 0x000002b6, 0x000000d0, 0x000200f9,
|
||||
0x0000014d, 0x000200f8, 0x0000014f, 0x000500b8, 0x00000088, 0x0000016d, 0x000002b7, 0x0000004b,
|
||||
0x000300f7, 0x0000016f, 0x00000000, 0x000400fa, 0x0000016d, 0x0000016e, 0x0000016f, 0x000200f8,
|
||||
0x0000016e, 0x00050051, 0x00000008, 0x00000172, 0x000002b0, 0x00000000, 0x00050051, 0x00000008,
|
||||
0x00000174, 0x000002af, 0x00000000, 0x00060052, 0x00000010, 0x00000299, 0x00000174, 0x00000146,
|
||||
0x00000000, 0x00060052, 0x00000010, 0x0000029b, 0x00000172, 0x00000149, 0x00000000, 0x000200f9,
|
||||
0x0000016f, 0x000200f8, 0x0000016f, 0x000700f5, 0x00000010, 0x000002bd, 0x00000149, 0x0000014f,
|
||||
0x0000029b, 0x0000016e, 0x000700f5, 0x00000010, 0x000002bc, 0x00000146, 0x0000014f, 0x00000299,
|
||||
0x0000016e, 0x000500b8, 0x00000088, 0x00000179, 0x000002bb, 0x0000004b, 0x000300f7, 0x0000017b,
|
||||
0x00000000, 0x000400fa, 0x00000179, 0x0000017a, 0x0000017b, 0x000200f8, 0x0000017a, 0x00050051,
|
||||
0x00000008, 0x0000017e, 0x000002bc, 0x00000002, 0x00050051, 0x00000008, 0x00000180, 0x000002bd,
|
||||
0x00000002, 0x00060052, 0x00000010, 0x0000029f, 0x00000180, 0x000002bc, 0x00000002, 0x00060052,
|
||||
0x00000010, 0x000002a1, 0x0000017e, 0x000002bd, 0x00000002, 0x000200f9, 0x0000017b, 0x000200f8,
|
||||
0x0000017b, 0x000700f5, 0x00000010, 0x000002bf, 0x000002bc, 0x0000016f, 0x0000029f, 0x0000017a,
|
||||
0x000700f5, 0x00000010, 0x000002be, 0x000002bd, 0x0000016f, 0x000002a1, 0x0000017a, 0x00050083,
|
||||
0x00000010, 0x00000187, 0x000002be, 0x000002bf, 0x00050085, 0x00000010, 0x00000189, 0x00000187,
|
||||
0x00000306, 0x00050083, 0x00000010, 0x0000018d, 0x000002be, 0x00000189, 0x0008000c, 0x00000010,
|
||||
0x00000237, 0x00000001, 0x0000002b, 0x0000018d, 0x00000302, 0x00000303, 0x00050085, 0x00000010,
|
||||
0x0000023a, 0x00000237, 0x0000030b, 0x0006000c, 0x00000010, 0x0000023b, 0x00000001, 0x00000001,
|
||||
0x0000023a, 0x0004006d, 0x00000047, 0x0000023c, 0x0000023b, 0x00050051, 0x00000006, 0x0000023e,
|
||||
0x0000023c, 0x00000000, 0x000500c4, 0x00000006, 0x0000023f, 0x0000023e, 0x0000005a, 0x00050051,
|
||||
0x00000006, 0x00000241, 0x0000023c, 0x00000001, 0x000500c4, 0x00000006, 0x00000242, 0x00000241,
|
||||
0x0000005e, 0x000500c5, 0x00000006, 0x00000243, 0x0000023f, 0x00000242, 0x00050051, 0x00000006,
|
||||
0x00000245, 0x0000023c, 0x00000002, 0x000500c5, 0x00000006, 0x00000246, 0x00000243, 0x00000245,
|
||||
0x00050081, 0x00000010, 0x00000193, 0x000002bf, 0x00000189, 0x0008000c, 0x00000010, 0x0000024d,
|
||||
0x00000001, 0x0000002b, 0x00000193, 0x00000302, 0x00000303, 0x00050085, 0x00000010, 0x00000250,
|
||||
0x0000024d, 0x0000030b, 0x0006000c, 0x00000010, 0x00000251, 0x00000001, 0x00000001, 0x00000250,
|
||||
0x0004006d, 0x00000047, 0x00000252, 0x00000251, 0x00050051, 0x00000006, 0x00000254, 0x00000252,
|
||||
0x00000000, 0x000500c4, 0x00000006, 0x00000255, 0x00000254, 0x0000005a, 0x00050051, 0x00000006,
|
||||
0x00000257, 0x00000252, 0x00000001, 0x000500c4, 0x00000006, 0x00000258, 0x00000257, 0x0000005e,
|
||||
0x000500c5, 0x00000006, 0x00000259, 0x00000255, 0x00000258, 0x00050051, 0x00000006, 0x0000025b,
|
||||
0x00000252, 0x00000002, 0x000500c5, 0x00000006, 0x0000025c, 0x00000259, 0x0000025b, 0x000500ab,
|
||||
0x00000088, 0x00000199, 0x00000246, 0x0000025c, 0x000300f7, 0x0000019b, 0x00000000, 0x000400fa,
|
||||
0x00000199, 0x0000019a, 0x0000019b, 0x000200f8, 0x0000019a, 0x000500b0, 0x00000088, 0x0000019e,
|
||||
0x00000246, 0x0000025c, 0x000600a9, 0x00000006, 0x00000312, 0x0000019e, 0x00000246, 0x0000025c,
|
||||
0x000600a9, 0x00000006, 0x00000313, 0x0000019e, 0x0000025c, 0x00000246, 0x000500c2, 0x00000006,
|
||||
0x00000260, 0x00000313, 0x0000005a, 0x000500c7, 0x00000006, 0x00000261, 0x00000260, 0x00000069,
|
||||
0x00040070, 0x00000008, 0x00000262, 0x00000261, 0x00050085, 0x00000008, 0x00000264, 0x00000262,
|
||||
0x0000030d, 0x000500c2, 0x00000006, 0x00000266, 0x00000313, 0x0000005e, 0x000500c7, 0x00000006,
|
||||
0x00000267, 0x00000266, 0x00000070, 0x00040070, 0x00000008, 0x00000268, 0x00000267, 0x00050085,
|
||||
0x00000008, 0x0000026a, 0x00000268, 0x0000030f, 0x000500c7, 0x00000006, 0x0000026c, 0x00000313,
|
||||
0x00000069, 0x00040070, 0x00000008, 0x0000026d, 0x0000026c, 0x00050085, 0x00000008, 0x0000026f,
|
||||
0x0000026d, 0x0000030d, 0x00060050, 0x00000010, 0x00000270, 0x00000264, 0x0000026a, 0x0000026f,
|
||||
0x000500c2, 0x00000006, 0x00000274, 0x00000312, 0x0000005a, 0x000500c7, 0x00000006, 0x00000275,
|
||||
0x00000274, 0x00000069, 0x00040070, 0x00000008, 0x00000276, 0x00000275, 0x00050085, 0x00000008,
|
||||
0x00000278, 0x00000276, 0x0000030d, 0x000500c2, 0x00000006, 0x0000027a, 0x00000312, 0x0000005e,
|
||||
0x000500c7, 0x00000006, 0x0000027b, 0x0000027a, 0x00000070, 0x00040070, 0x00000008, 0x0000027c,
|
||||
0x0000027b, 0x00050085, 0x00000008, 0x0000027e, 0x0000027c, 0x0000030f, 0x000500c7, 0x00000006,
|
||||
0x00000280, 0x00000312, 0x00000069, 0x00040070, 0x00000008, 0x00000281, 0x00000280, 0x00050085,
|
||||
0x00000008, 0x00000283, 0x00000281, 0x0000030d, 0x00060050, 0x00000010, 0x00000284, 0x00000278,
|
||||
0x0000027e, 0x00000283, 0x00050083, 0x00000010, 0x000001b0, 0x00000284, 0x00000270, 0x00050094,
|
||||
0x00000008, 0x000001b4, 0x000001b0, 0x000001b0, 0x0007000c, 0x00000008, 0x000001b6, 0x00000001,
|
||||
0x00000028, 0x000001b4, 0x000001b5, 0x000200f9, 0x000001b8, 0x000200f8, 0x000001b8, 0x000700f5,
|
||||
0x00000006, 0x000002d0, 0x00000057, 0x0000019a, 0x000001e5, 0x000001bb, 0x000700f5, 0x00000006,
|
||||
0x000002c2, 0x00000057, 0x0000019a, 0x000001e7, 0x000001bb, 0x000500b0, 0x00000088, 0x000001be,
|
||||
0x000002c2, 0x000000a7, 0x000400f6, 0x000001ba, 0x000001bb, 0x00000000, 0x000400fa, 0x000001be,
|
||||
0x000001b9, 0x000001ba, 0x000200f8, 0x000001b9, 0x00050041, 0x0000009a, 0x000001c1, 0x000000ab,
|
||||
0x000002c2, 0x0004003d, 0x00000009, 0x000001c2, 0x000001c1, 0x0008004f, 0x00000010, 0x000001c3,
|
||||
0x000001c2, 0x000001c2, 0x00000000, 0x00000001, 0x00000002, 0x00050083, 0x00000010, 0x000001c5,
|
||||
0x000001c3, 0x00000270, 0x00050094, 0x00000008, 0x000001c7, 0x000001c5, 0x000001b0, 0x00050088,
|
||||
0x00000008, 0x000001c9, 0x000001c7, 0x000001b6, 0x00050085, 0x00000008, 0x000001cb, 0x000001c9,
|
||||
0x000001ca, 0x0006000c, 0x00000008, 0x000001cc, 0x00000001, 0x00000001, 0x000001cb, 0x0008000c,
|
||||
0x00000008, 0x000001cd, 0x00000001, 0x0000002b, 0x000001cc, 0x0000004b, 0x000001ca, 0x0004006d,
|
||||
0x00000006, 0x000001ce, 0x000001cd, 0x000500aa, 0x00000088, 0x000001d1, 0x000001ce, 0x00000057,
|
||||
0x000300f7, 0x000001d4, 0x00000000, 0x000400fa, 0x000001d1, 0x000001d3, 0x000001d5, 0x000200f8,
|
||||
0x000001d3, 0x000200f9, 0x000001d4, 0x000200f8, 0x000001d5, 0x000500aa, 0x00000088, 0x000001d7,
|
||||
0x000001ce, 0x00000084, 0x000300f7, 0x000001da, 0x00000000, 0x000400fa, 0x000001d7, 0x000001d9,
|
||||
0x000001db, 0x000200f8, 0x000001d9, 0x000200f9, 0x000001da, 0x000200f8, 0x000001db, 0x00050080,
|
||||
0x00000006, 0x000001dd, 0x000001ce, 0x00000023, 0x000200f9, 0x000001da, 0x000200f8, 0x000001da,
|
||||
0x000700f5, 0x00000006, 0x000002d2, 0x00000023, 0x000001d9, 0x000001dd, 0x000001db, 0x000200f9,
|
||||
0x000001d4, 0x000200f8, 0x000001d4, 0x000700f5, 0x00000006, 0x000002d3, 0x00000057, 0x000001d3,
|
||||
0x000002d2, 0x000001da, 0x00050084, 0x00000006, 0x000001e2, 0x00000061, 0x000002c2, 0x000500c4,
|
||||
0x00000006, 0x000001e3, 0x000002d3, 0x000001e2, 0x000500c5, 0x00000006, 0x000001e5, 0x000002d0,
|
||||
0x000001e3, 0x000200f9, 0x000001bb, 0x000200f8, 0x000001bb, 0x00050080, 0x00000006, 0x000001e7,
|
||||
0x000002c2, 0x000000d0, 0x000200f9, 0x000001b8, 0x000200f8, 0x000001ba, 0x000200f9, 0x0000019b,
|
||||
0x000200f8, 0x0000019b, 0x000700f5, 0x00000006, 0x000002cf, 0x00000057, 0x0000017b, 0x000002d0,
|
||||
0x000001ba, 0x000700f5, 0x00000006, 0x000002cd, 0x0000025c, 0x0000017b, 0x00000312, 0x000001ba,
|
||||
0x000700f5, 0x00000006, 0x000002cb, 0x00000246, 0x0000017b, 0x00000313, 0x000001ba, 0x00050041,
|
||||
0x00000020, 0x000001ed, 0x0000001d, 0x000000d0, 0x0004003d, 0x00000006, 0x000001ee, 0x000001ed,
|
||||
0x00050051, 0x00000006, 0x000001f0, 0x00000080, 0x00000002, 0x00050041, 0x00000020, 0x000001f1,
|
||||
0x0000001d, 0x0000005e, 0x0004003d, 0x00000006, 0x000001f2, 0x000001f1, 0x00050084, 0x00000006,
|
||||
0x000001f3, 0x000001f0, 0x000001f2, 0x00050080, 0x00000006, 0x000001f4, 0x000001ee, 0x000001f3,
|
||||
0x00050051, 0x00000006, 0x000001f6, 0x00000080, 0x00000001, 0x00050084, 0x00000006, 0x000001f9,
|
||||
0x000001f6, 0x0000008d, 0x00050080, 0x00000006, 0x000001fa, 0x000001f4, 0x000001f9, 0x00050080,
|
||||
0x00000006, 0x000001fd, 0x000001fa, 0x0000008a, 0x00050051, 0x00000006, 0x000001ff, 0x000002c7,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x00000201, 0x000002c7, 0x00000001, 0x000500c4, 0x00000006,
|
||||
0x00000205, 0x000002cd, 0x00000204, 0x000500c5, 0x00000006, 0x00000206, 0x000002cb, 0x00000205,
|
||||
0x00070050, 0x000001e8, 0x00000208, 0x000001ff, 0x00000201, 0x00000206, 0x000002cf, 0x00060041,
|
||||
0x00000209, 0x0000020a, 0x000001ec, 0x00000030, 0x000001fd, 0x0003003e, 0x0000020a, 0x00000208,
|
||||
0x000200f9, 0x0000020d, 0x000200f8, 0x0000020d, 0x000100fd, 0x00010038,
|
||||
};
|
||||
+17
-14
@@ -30,12 +30,12 @@ static int save_ok = 1; // the last cfg_save_to wrote (o
|
||||
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||
static const char *const known[] = {
|
||||
"AB", "APP_POOL", "ASSUME_FOCUS", "ASTC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_OFF", "AUDIO_PREFILL_MS",
|
||||
"AB", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_OFF", "AUDIO_PREFILL_MS",
|
||||
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
||||
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
|
||||
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
|
||||
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
|
||||
"BIND_ZL", "BIND_ZR", "CLOTH_REALTIME", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
|
||||
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
|
||||
"EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "FLIGHT_SPIKE_MS",
|
||||
"FLIGHT_THUMBS", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "GPL", "GPL_DRAW_BUILD", "GPU",
|
||||
"GPU_INDEX", "GPU_NAME", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
@@ -43,14 +43,14 @@ static const char *const known[] = {
|
||||
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PATCH_OUTPUT", "PATCH_SCENE", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REEXEC",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
||||
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
|
||||
"SHADER_MEMO", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR", "SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"SHADER_MEMO", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR", "SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
"WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
};
|
||||
|
||||
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
|
||||
@@ -59,11 +59,11 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
||||
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
||||
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
||||
{"SHARPNESS", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"},
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"MOUSE_SENS", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"STICK_DEADZONE", "input"}, {"MOUSE_SENS", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
|
||||
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
|
||||
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
|
||||
@@ -74,11 +74,12 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"VK_DYN_VERTEX", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"},
|
||||
};
|
||||
|
||||
// The settings the menu shows: the value in effect when the key is unset, and what the key accepts. The only place a
|
||||
@@ -93,8 +94,9 @@ static const CfgMeta metas[] = {
|
||||
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
|
||||
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
|
||||
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE},
|
||||
// Game
|
||||
{"FPS_LIMIT", "60", "30|60|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"FPS_LIMIT", "60", "30|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART},
|
||||
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR", RESTART},
|
||||
@@ -102,8 +104,8 @@ static const CfgMeta metas[] = {
|
||||
// Sound, Controls, Menu
|
||||
{"VOLUME", "100", "0..100", LIVE},
|
||||
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox", LIVE},
|
||||
{"MOUSE_SENS", "1", "0..10", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox|xbox_prompts", LIVE},
|
||||
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"MOUSE_SENS", "1", "0..10", LIVE},
|
||||
{"MENU_LANG", "", "en|ru", LIVE},
|
||||
// Advanced: scheduling
|
||||
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
|
||||
@@ -111,10 +113,10 @@ static const CfgMeta metas[] = {
|
||||
// shaders and pipelines
|
||||
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "fullscreen", "strict|any|fullscreen", RESTART},
|
||||
{"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART},
|
||||
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "1", "0|1", RESTART},
|
||||
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "", "0|1", RESTART},
|
||||
// textures and memory
|
||||
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
|
||||
{"TEX_ALIAS", "1", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
||||
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "0", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
||||
// render fixes
|
||||
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
|
||||
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
|
||||
@@ -126,6 +128,7 @@ static const CfgMeta metas[] = {
|
||||
{"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
|
||||
// diagnostics
|
||||
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
|
||||
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART},
|
||||
{"INPUT_RECORD", "", NULL, RESTART},
|
||||
};
|
||||
|
||||
|
||||
@@ -261,11 +261,25 @@ int probe_cfg(void) {
|
||||
CHECK((int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768, "mouse: clamped to the unit circle");
|
||||
input_mouse_stick(30, 0, 1.0 / 60, 0, st);
|
||||
CHECK(st[0] == 0 && st[1] == 0, "mouse: sensitivity 0 is off");
|
||||
input_pad_stick(3000, -2000, 0.15f, st);
|
||||
CHECK(st[0] == 0 && st[1] == 0, "pad stick: an 11% drift inside a 15% dead zone reads centred");
|
||||
input_pad_stick(4000, -3000, 0, st);
|
||||
CHECK(st[0] == 4000 && st[1] == -3000, "pad stick: dead zone 0 passes the stick unchanged");
|
||||
input_pad_stick(32767, 0, 0.15f, st);
|
||||
CHECK(st[0] == 32767 && st[1] == 0, "pad stick: full deflection still reaches full");
|
||||
input_pad_stick(-32768, -32768, 0.15f, st);
|
||||
CHECK(st[0] < 0 && st[1] < 0 && st[0] >= -32767 && (int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768,
|
||||
"pad stick: a square gate's corner stays on the unit circle");
|
||||
input_pad_stick(0, 32767 / 2 + 1, 0.2f, st);
|
||||
CHECK(abs(st[1] - 32767 * 3 / 8) <= 3, "pad stick: half deflection past a 20% zone is (0.5 - 0.2) / 0.8 of full");
|
||||
// Gamepad layouts (input_map.c)
|
||||
CHECK(input_pad_layout_pick("auto", 1) == PAD_LAYOUT_NINTENDO && input_pad_layout_pick(NULL, 0) == PAD_LAYOUT_XBOX,
|
||||
"pad layout: automatic is Nintendo for an east button labelled A, Xbox otherwise");
|
||||
CHECK(input_pad_layout_pick("Xbox", 1) == PAD_LAYOUT_XBOX && input_pad_layout_pick("nintendo", 0) == PAD_LAYOUT_NINTENDO,
|
||||
"pad layout: a set layout wins over the labels");
|
||||
CHECK(input_pad_layout_pick("xbox_prompts", 0) == PAD_LAYOUT_NINTENDO &&
|
||||
input_pad_layout_pick("Xbox_Prompts", 1) == PAD_LAYOUT_NINTENDO,
|
||||
"pad layout: Xbox prompts keep the Nintendo positions (only the prompts change, romfs.c overlay_init)");
|
||||
uint8_t pd[PAD_COUNT] = {0};
|
||||
pd[PAD_SOUTH] = pd[PAD_NORTH] = 1;
|
||||
CHECK(input_map_pad_buttons(pd, PAD_LAYOUT_NINTENDO) == (1ull << B_B | 1ull << B_X) &&
|
||||
@@ -280,6 +294,13 @@ int probe_cfg(void) {
|
||||
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_SOUTH] = 1}, PAD_LAYOUT_XBOX) == 0,
|
||||
"pad layout: a bind line replaces the layout's own button");
|
||||
input_map_reset();
|
||||
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_XBOX) == 0,
|
||||
"pad layout: a back paddle is unbound by default");
|
||||
CHECK(input_map_bind("ZL", "pad:default, pad:rpaddle1") &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_ZL] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL,
|
||||
"bind: pad:rpaddle1 adds a back paddle, pad:default keeps the trigger");
|
||||
input_map_reset();
|
||||
// Bind lines for stick directions and default parts; the remapping rows' edits (input_map.c)
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_I] = 1;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// is also fixed at init; they are looked up only after the flat table misses.
|
||||
#include "dispatch.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
@@ -51,6 +52,11 @@ void rt_fatal(const char *fmt, ...) {
|
||||
rt_vlogf(fmt, ap);
|
||||
rt_log_raw("\n", 1);
|
||||
va_end(ap);
|
||||
// A failed allocation (malloc, realloc, VirtualAlloc) means the commit limit was reached: this line tells a leak of
|
||||
// ours (process private bytes near the limit) from a machine short of page file.
|
||||
double proc, used, limit;
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
rt_log("fatal: memory commit: this process %.0f MB, the system %.0f of %.0f MB", proc, used, limit);
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_Exit(70);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// --probe-dpi: the process DPI awareness that totk.manifest declares, and what the primary monitor reports under it.
|
||||
// A DPI-unaware process gets every window and monitor size scaled down by the display scale and its output stretched
|
||||
// back up by Windows; the probe fails unless the process is per-monitor aware (v2). Needs no game files and no GPU.
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "host.h"
|
||||
|
||||
static const char *awareness_name(DPI_AWARENESS_CONTEXT ctx) {
|
||||
if (AreDpiAwarenessContextsEqual(ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) return "per-monitor aware v2";
|
||||
switch (GetAwarenessFromDpiAwarenessContext(ctx)) {
|
||||
case DPI_AWARENESS_UNAWARE: return "unaware";
|
||||
case DPI_AWARENESS_SYSTEM_AWARE: return "system aware";
|
||||
case DPI_AWARENESS_PER_MONITOR_AWARE: return "per-monitor aware v1";
|
||||
default: return "invalid";
|
||||
}
|
||||
}
|
||||
|
||||
int probe_dpi(void) {
|
||||
DPI_AWARENESS_CONTEXT ctx = GetThreadDpiAwarenessContext();
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
HMONITOR mon = MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
|
||||
int have = GetMonitorInfoW(mon, &mi);
|
||||
printf("dpi: awareness %s\n", awareness_name(ctx));
|
||||
printf("dpi: system dpi %u (scale %u%%)\n", GetDpiForSystem(), GetDpiForSystem() * 100 / 96);
|
||||
if (have)
|
||||
printf("dpi: primary monitor %ldx%ld at %ld,%ld; screen metrics %dx%d\n", mi.rcMonitor.right - mi.rcMonitor.left,
|
||||
mi.rcMonitor.bottom - mi.rcMonitor.top, mi.rcMonitor.left, mi.rcMonitor.top, GetSystemMetrics(SM_CXSCREEN),
|
||||
GetSystemMetrics(SM_CYSCREEN));
|
||||
int ok = AreDpiAwarenessContextsEqual(ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) && have;
|
||||
printf("probe-dpi: %s\n", ok ? "PASS" : "FAIL (the process is not per-monitor aware v2)");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ void frame_step_init(void) {
|
||||
|
||||
int frame_step_realtime(void) { return step_word != NULL; }
|
||||
|
||||
// The frame limit (menu, TOTK_FPS_LIMIT, live): 30, 60, 120, 144, any whole rate from 30 to 360, or display (the
|
||||
// The frame limit (menu, TOTK_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 360, or display (the
|
||||
// refresh rate of the window's monitor). Unset, the older TOTK_FPS60=0 still means 30. A limit of 30 switches the set
|
||||
// fps60 off (the shipping minimum interval 2), every other limit switches it on; the controller reads its minimum
|
||||
// interval from the patched sites each frame. With the real-time step the pacer holds frames to the limit; above 60
|
||||
|
||||
@@ -38,6 +38,8 @@ uint64_t romfs_file_count(void); // files in the index
|
||||
const char *romfs_entry(uint64_t i, uint64_t *offset, uint64_t *size); // path, image offset and size of index row i
|
||||
const char *romfs_image_path(void); // the romfs.bin path fs_init opened
|
||||
size_t romfs_overlay_count(void);
|
||||
int romfs_xbox_prompts_ready(void); // romfs-patches-xbox was there at start
|
||||
int romfs_xbox_prompts_active(void); // and was loaded (pad_layout = xbox_prompts at start)
|
||||
const char *romfs_overlay(size_t k, const char **file, uint64_t *size, int *is_added); // RomFS path of overlay k
|
||||
void fs_trace(const char *what, const char *path); // TOTK_TRACE=fs: one line per open
|
||||
|
||||
|
||||
+136
-11
@@ -18,6 +18,11 @@
|
||||
// (64 hex digits) overrides it. A translator change therefore changes every cache key without a hand-made KEY_VERSION
|
||||
// bump; KEY_VERSION is for changes on this side (key layout, how the output is used).
|
||||
//
|
||||
// Pack: tools/pack_shader_cache.py folds the .spv, .res.json, .use and in/*.in files into <cache>/shader-cache.pack
|
||||
// (one file instead of four per shader: ~117,000 files after an install) and deletes them; the installer runs it after
|
||||
// preparing the shaders. The pack is read-only here. Every read looks in the pack first and then for the loose file,
|
||||
// and new translations are written as loose files as before, until the tool runs again (section "cache pack").
|
||||
//
|
||||
// Stored inputs: every translation also writes <cache>/in/<input id>.in (magic "TKSI"; stage, the full answers, the
|
||||
// control section, the code up to its length, c1), so a pipeline key record (vk_pipeline.c), which names its shaders by
|
||||
// input id, can be translated again under a new translator without guest memory (gfx_shader_load_input,
|
||||
@@ -125,6 +130,94 @@ static INIT_ONCE paths_once = INIT_ONCE_STATIC_INIT;
|
||||
#define read_file plat_file_read
|
||||
#define write_file plat_file_write
|
||||
|
||||
// ---- cache pack (<cache>/shader-cache.pack, written by tools/pack_shader_cache.py)
|
||||
// Little-endian: "TKSPACK1", u32 count, u32 0, then count entries sorted by (kind, name), then the data. name is the
|
||||
// 32 bytes the loose file's 64 hex digits spell. The index is read once (48 bytes a file: ~5.6 MB after an install);
|
||||
// the data is read per lookup with positioned reads on one handle, safe from every thread. A pack that fails a check is
|
||||
// ignored as a whole and the loose files are used.
|
||||
enum { PACK_SPV = 1, PACK_RES = 2, PACK_USE = 3, PACK_IN = 4 };
|
||||
typedef struct { uint8_t name[32]; uint32_t kind, size; uint64_t offset; } PackEnt;
|
||||
_Static_assert(sizeof(PackEnt) == 48, "shader-cache.pack entry layout");
|
||||
static HANDLE pack_file = INVALID_HANDLE_VALUE;
|
||||
static PackEnt *pack_ix;
|
||||
static uint32_t pack_n;
|
||||
|
||||
static int pack_cmp(const PackEnt *a, uint32_t kind, const uint8_t name[32]) {
|
||||
if (a->kind != kind) return a->kind < kind ? -1 : 1;
|
||||
return memcmp(a->name, name, 32);
|
||||
}
|
||||
|
||||
static int pack_read_at(uint64_t offset, void *out, uint32_t n) {
|
||||
OVERLAPPED o = {0};
|
||||
o.Offset = (DWORD)offset, o.OffsetHigh = (DWORD)(offset >> 32);
|
||||
DWORD got = 0;
|
||||
return ReadFile(pack_file, out, n, &got, &o) && got == n;
|
||||
}
|
||||
|
||||
// Opens <cache>/shader-cache.pack and reads its index (init_paths). Nothing happens when there is none.
|
||||
static void pack_open(void) {
|
||||
char p[PATH_CAP + 32];
|
||||
wchar_t w[PATH_CAP + 32];
|
||||
snprintf(p, sizeof p, "%s\\shader-cache.pack", cache_dir);
|
||||
if (!MultiByteToWideChar(CP_UTF8, 0, p, -1, w, PATH_CAP + 32)) return;
|
||||
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (h == INVALID_HANDLE_VALUE) return;
|
||||
LARGE_INTEGER size;
|
||||
uint8_t head[16];
|
||||
uint32_t n = 0;
|
||||
const char *why = NULL;
|
||||
PackEnt *ix = NULL;
|
||||
pack_file = h;
|
||||
if (!GetFileSizeEx(h, &size) || !pack_read_at(0, head, sizeof head) || memcmp(head, "TKSPACK1", 8)) why = "header";
|
||||
if (!why) {
|
||||
memcpy(&n, head + 8, 4);
|
||||
if (n > (1u << 24) || (uint64_t)n * sizeof *ix + sizeof head > (uint64_t)size.QuadPart ||
|
||||
!(ix = malloc((size_t)n * sizeof *ix + 1)))
|
||||
why = "index size";
|
||||
else if (!pack_read_at(sizeof head, ix, n * (uint32_t)sizeof *ix)) why = "index read";
|
||||
}
|
||||
for (uint32_t i = 0; !why && i < n; i++) {
|
||||
if (ix[i].offset + ix[i].size > (uint64_t)size.QuadPart) why = "entry out of bounds";
|
||||
else if (i && pack_cmp(&ix[i - 1], ix[i].kind, ix[i].name) >= 0) why = "index order";
|
||||
}
|
||||
if (why) {
|
||||
rt_log("gfx_shader: %s is not a usable cache pack (%s); reading the loose files", p, why);
|
||||
free(ix);
|
||||
CloseHandle(h);
|
||||
pack_file = INVALID_HANDLE_VALUE;
|
||||
return;
|
||||
}
|
||||
pack_ix = ix, pack_n = n;
|
||||
rt_log("gfx_shader: cache pack %s: %u files", p, n);
|
||||
}
|
||||
|
||||
static const PackEnt *pack_find(uint32_t kind, const uint8_t name[32]) {
|
||||
size_t lo = 0, hi = pack_n;
|
||||
while (lo < hi) {
|
||||
size_t mid = lo + (hi - lo) / 2;
|
||||
int c = pack_cmp(&pack_ix[mid], kind, name);
|
||||
if (!c) return &pack_ix[mid];
|
||||
if (c < 0) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// A packed file as plat_file_read returns a loose one: malloc'd, NUL after the last byte; NULL when absent or empty.
|
||||
static uint8_t *pack_read(uint32_t kind, const uint8_t name[32], uint64_t *size) {
|
||||
const PackEnt *e = pack_find(kind, name);
|
||||
uint8_t *buf = e && e->size ? malloc((size_t)e->size + 1) : NULL;
|
||||
if (!buf) return NULL;
|
||||
if (!pack_read_at(e->offset, buf, e->size)) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
buf[e->size] = 0;
|
||||
*size = e->size;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Appends a UTF-8 string as a JSON string body: ASCII as is, quotes and backslashes escaped, the rest as \uXXXX
|
||||
// (UTF-16 units), so the service's stdin encoding does not matter.
|
||||
static void json_esc(char **o, const char *s) {
|
||||
@@ -218,6 +311,7 @@ static BOOL CALLBACK init_paths(PINIT_ONCE once, PVOID param, PVOID *ctx) {
|
||||
if (cache_cfg && *cache_cfg && strlen(cache_cfg) < sizeof cache_dir) strcpy(cache_dir, cache_cfg);
|
||||
else if (exe_dir[0]) snprintf(cache_dir, sizeof cache_dir, "%s\\shader-cache", exe_dir);
|
||||
xlate_identity();
|
||||
if (cache_dir[0]) pack_open();
|
||||
use_preload_start();
|
||||
return TRUE;
|
||||
}
|
||||
@@ -468,15 +562,15 @@ static int spirv_complete(const uint32_t *w, uint64_t n) {
|
||||
return i == n;
|
||||
}
|
||||
|
||||
// Reads <key>.spv and <key>.res.json; NULL if either is missing or malformed (the caller translates again and
|
||||
// overwrites both). iid: the input id, NULL when the caller sets it.
|
||||
static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
|
||||
// Reads <key>.spv and <key>.res.json from the pack (packed 1) or the loose files; NULL if either is missing or
|
||||
// malformed. iid: the input id, NULL when the caller sets it.
|
||||
static GfxShader *disk_load_from(const uint8_t key[32], const uint8_t *iid, int packed) {
|
||||
static volatile LONG n_bad;
|
||||
char hex[65], p[PATH_CAP + 80];
|
||||
key_hex(key, hex);
|
||||
uint64_t spv_size = 0, res_size = 0;
|
||||
snprintf(p, PATH_CAP + 80, "%s\\%s.res.json", cache_dir, hex);
|
||||
uint8_t *res = read_file(p, &res_size);
|
||||
uint8_t *res = packed ? pack_read(PACK_RES, key, &res_size) : read_file(p, &res_size);
|
||||
if (!res) return NULL;
|
||||
if (!reflection_valid((const char *)res, res_size)) {
|
||||
LONG n = InterlockedIncrement(&n_bad);
|
||||
@@ -485,7 +579,7 @@ static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
|
||||
return NULL;
|
||||
}
|
||||
snprintf(p, PATH_CAP + 80, "%s\\%s.spv", cache_dir, hex);
|
||||
uint8_t *spv = read_file(p, &spv_size);
|
||||
uint8_t *spv = packed ? pack_read(PACK_SPV, key, &spv_size) : read_file(p, &spv_size);
|
||||
uint32_t magic = 0;
|
||||
if (spv && spv_size >= 20) memcpy(&magic, spv, 4);
|
||||
if (!spv || spv_size % 4 || magic != 0x07230203 || !spirv_complete((const uint32_t *)spv, spv_size / 4)) {
|
||||
@@ -503,6 +597,13 @@ static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// The pack, then the loose files (a newer translation of a key the pack holds a bad copy of); NULL when neither has a
|
||||
// valid pair (the caller translates again and writes loose files).
|
||||
static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
|
||||
GfxShader *s = pack_n ? disk_load_from(key, iid, 1) : NULL;
|
||||
return s ? s : disk_load_from(key, iid, 0);
|
||||
}
|
||||
|
||||
// ---- request building
|
||||
|
||||
static const char *const tex_names[] = {NULL, "1d", "1darray", "buffer", "2d", "2darray", "2dms", "2dmsarray", "3d",
|
||||
@@ -751,10 +852,25 @@ static DWORD WINAPI use_preload(void *arg) {
|
||||
(void)arg;
|
||||
uint64_t t0 = plat_now_ns();
|
||||
char pattern[PATH_CAP + 16], p[PATH_CAP + 80];
|
||||
int n = 0, bad = 0;
|
||||
for (uint32_t i = 0; i < pack_n; i++) { // the packed masks; the loose files below add the masks found since
|
||||
uint32_t u[5];
|
||||
UseFile **slot, *x;
|
||||
if (pack_ix[i].kind != PACK_USE) continue;
|
||||
if (pack_ix[i].size != sizeof u || !pack_read_at(pack_ix[i].offset, u, sizeof u) || u[0] != USE_MAGIC ||
|
||||
u[1] != USE_VERSION) {
|
||||
bad++;
|
||||
continue;
|
||||
}
|
||||
if (!*(slot = use_file_slot(pack_ix[i].name)) && (x = malloc(sizeof *x))) {
|
||||
memcpy(x->name, pack_ix[i].name, 32);
|
||||
x->next = NULL, x->m.slots = u[2], x->m.attrs = u[3], x->m.fouts = u[4];
|
||||
*slot = x, n++;
|
||||
}
|
||||
}
|
||||
snprintf(pattern, sizeof pattern, "%s\\*.use", cache_dir);
|
||||
WIN32_FIND_DATAA fd;
|
||||
HANDLE f = FindFirstFileExA(pattern, FindExInfoBasic, &fd, FindExSearchNameMatch, NULL, FIND_FIRST_EX_LARGE_FETCH);
|
||||
int n = 0, bad = 0;
|
||||
if (f != INVALID_HANDLE_VALUE) {
|
||||
do {
|
||||
UseFile e = {0};
|
||||
@@ -824,7 +940,8 @@ static int use_find(const uint8_t h[32], UseMask *m) {
|
||||
char p[PATH_CAP + 80];
|
||||
use_path_of(name, p);
|
||||
uint64_t n = 0;
|
||||
uint32_t *f = (uint32_t *)read_file(p, &n);
|
||||
uint32_t *f = (uint32_t *)pack_read(PACK_USE, name, &n);
|
||||
if (!f) f = (uint32_t *)read_file(p, &n);
|
||||
known = f && n == 20 && f[0] == USE_MAGIC && f[1] == USE_VERSION;
|
||||
if (known) m->slots = f[2], m->attrs = f[3], m->fouts = f[4];
|
||||
free(f);
|
||||
@@ -1105,7 +1222,7 @@ static void in_store(const uint8_t iid[32], int stage, const uint8_t *control, c
|
||||
const GfxShaderAnswers *a) {
|
||||
char p[PATH_CAP + 80], tmp[PATH_CAP + 120];
|
||||
in_path(iid, p);
|
||||
if (file_exists(p)) return;
|
||||
if (pack_find(PACK_IN, iid) || file_exists(p)) return;
|
||||
GfxShaderAnswers zero = {0};
|
||||
InHeader h = {IN_MAGIC, IN_VERSION, (uint32_t)stage, r->length, r->c1_len, sizeof(GfxShaderAnswers)};
|
||||
size_t n = sizeof h + sizeof zero + CONTROL_SIZE + r->length + r->c1_len;
|
||||
@@ -1127,7 +1244,8 @@ static int in_load(const uint8_t iid[32], Inputs *in) {
|
||||
char p[PATH_CAP + 80];
|
||||
in_path(iid, p);
|
||||
uint64_t n = 0;
|
||||
uint8_t *f = read_file(p, &n);
|
||||
uint8_t *f = pack_read(PACK_IN, iid, &n);
|
||||
if (!f) f = read_file(p, &n);
|
||||
InHeader h;
|
||||
if (!f || n < sizeof h) return free(f), 0;
|
||||
memcpy(&h, f, sizeof h);
|
||||
@@ -1425,7 +1543,7 @@ static DWORD WINAPI warm_worker(void *arg) {
|
||||
key_of_input(iid, key);
|
||||
key_hex(key, hex);
|
||||
snprintf(p, PATH_CAP + 80, "%s\\%s.spv", cache_dir, hex);
|
||||
if (file_exists(p)) InterlockedIncrement(&w->other_cached);
|
||||
if (pack_find(PACK_SPV, key) || file_exists(p)) InterlockedIncrement(&w->other_cached);
|
||||
else gfx_shader_load_input(iid, 1);
|
||||
}
|
||||
InterlockedIncrement64(&w->done);
|
||||
@@ -1463,7 +1581,14 @@ static size_t other_inputs(const uint8_t (*named)[32], size_t n_named, uint8_t (
|
||||
char dir[PATH_CAP + 80];
|
||||
snprintf(dir, sizeof dir, "%s\\in", cache_dir);
|
||||
InList l = {.named = named, .n_named = n_named};
|
||||
plat_file_list(dir, ".in", in_collect, &l);
|
||||
char name[68];
|
||||
for (uint32_t i = 0; i < pack_n; i++) { // the packed inputs, under the name their loose file would have
|
||||
if (pack_ix[i].kind != PACK_IN) continue;
|
||||
key_hex(pack_ix[i].name, name);
|
||||
memcpy(name + 64, ".in", 4);
|
||||
if (!in_collect(name, &l)) break;
|
||||
}
|
||||
plat_file_list(dir, ".in", in_collect, &l); // in_store writes no loose file for a packed input: no duplicates
|
||||
return *out = l.v, l.n;
|
||||
}
|
||||
|
||||
|
||||
+114
-22
@@ -12,6 +12,10 @@
|
||||
// The in-game menu (ui/menu.h) draws over the game image in the present's command buffer, after the blit; its keyboard
|
||||
// and mouse come from the window procedure (ui/menu_win.cpp). The window's size and fullscreen state are the settings
|
||||
// TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, applied at start and whenever a cfg_set changes them (window_settings_live).
|
||||
//
|
||||
// The process is per-monitor DPI aware (totk.manifest): every size here is physical pixels, TOTK_WINDOW_SIZE included,
|
||||
// and the swapchain matches the screen pixel for pixel. A window moved to a monitor at another display scale keeps its
|
||||
// client size (WM_GETDPISCALEDSIZE); the menu takes its text size floor from the window's DPI (gfx_window_dpi).
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -48,6 +52,7 @@ static CRITICAL_SECTION g_start_lock; // gfx_window_start / vsync start
|
||||
static int g_started, g_wnd_failed;
|
||||
static int g_background; // TOTK_BACKGROUND_WINDOW: the window stays behind the user's windows
|
||||
static int g_fs_pending; // window thread: TOTK_FULLSCREEN waits for the first activation
|
||||
static volatile LONG g_dpi = USER_DEFAULT_SCREEN_DPI; // the window's DPI, set by the window thread
|
||||
|
||||
// ---- swapchain and present state (g_pl)
|
||||
static CRITICAL_SECTION g_pl;
|
||||
@@ -189,6 +194,18 @@ static WINDOWPLACEMENT g_saved_place = {sizeof g_saved_place}; // the window b
|
||||
|
||||
static int is_fullscreen(HWND h) { return !(GetWindowLongW(h, GWL_STYLE) & WS_OVERLAPPEDWINDOW); }
|
||||
|
||||
unsigned gfx_window_dpi(void) { return (unsigned)InterlockedCompareExchange(&g_dpi, 0, 0); }
|
||||
|
||||
// The outer size of a framed window whose client area is cw x ch. The frame is measured at the window's DPI, or the
|
||||
// system DPI before the window exists: plain AdjustWindowRect always takes the system DPI, which is wrong on a monitor
|
||||
// at another display scale.
|
||||
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
|
||||
RECT r = {0, 0, cw, ch};
|
||||
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, h ? GetDpiForWindow(h) : GetDpiForSystem());
|
||||
*ow = r.right - r.left;
|
||||
*oh = r.bottom - r.top;
|
||||
}
|
||||
|
||||
// TOTK_FULLSCREEN=1: borderless window over the monitor. TOTK_WINDOW_SIZE=<w>x<h>: client area of the window (the
|
||||
// swapchain follows the window; the game image is scaled to it).
|
||||
static int cfg_fullscreen(void) {
|
||||
@@ -203,6 +220,16 @@ static void cfg_window_size(int *w, int *h) {
|
||||
*h = y;
|
||||
}
|
||||
|
||||
// Windows turns the display off and sleeps after the idle timeout, and gamepad input does not count as activity. While
|
||||
// the window is up and not minimized it holds the display and the system on, as other games do. The state belongs to
|
||||
// the calling thread, so only the window thread (window_main and wnd_proc) calls this; the process exit releases it.
|
||||
static void keep_awake(int on) {
|
||||
static int cur = -1;
|
||||
if (on == cur) return;
|
||||
cur = on;
|
||||
SetThreadExecutionState(on ? ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED : ES_CONTINUOUS);
|
||||
}
|
||||
|
||||
// F11: borderless window over the whole monitor, and back to the saved window. The swapchain follows through WM_SIZE.
|
||||
// Not Alt+Enter: Enter is the Plus button (input_win.c). A background window (test runs) keeps its place behind the
|
||||
// user's windows and takes no focus.
|
||||
@@ -242,20 +269,18 @@ static void apply_settings(HWND h) {
|
||||
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = cfg_fullscreen(), fs_changed = fs != g_applied_fs;
|
||||
note_applied();
|
||||
if (size_changed) {
|
||||
int cw, ch;
|
||||
int cw, ch, ow, oh;
|
||||
cfg_window_size(&cw, &ch);
|
||||
RECT r = {0, 0, cw, ch};
|
||||
AdjustWindowRect(&r, WND_STYLE, FALSE);
|
||||
outer_size(h, cw, ch, &ow, &oh);
|
||||
if (is_fullscreen(h)) {
|
||||
RECT *n = &g_saved_place.rcNormalPosition;
|
||||
n->right = n->left + (r.right - r.left);
|
||||
n->bottom = n->top + (r.bottom - r.top);
|
||||
n->right = n->left + ow;
|
||||
n->bottom = n->top + oh;
|
||||
} else {
|
||||
RECT have;
|
||||
if (GetClientRect(h, &have) && (have.right != cw || have.bottom != ch)) {
|
||||
if (IsZoomed(h)) ShowWindow(h, SW_RESTORE);
|
||||
SetWindowPos(h, NULL, 0, 0, r.right - r.left, r.bottom - r.top,
|
||||
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
SetWindowPos(h, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,6 +397,7 @@ static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
|
||||
}
|
||||
return 0;
|
||||
case WM_SIZE:
|
||||
keep_awake(w != SIZE_MINIMIZED);
|
||||
if (w != SIZE_MINIMIZED) InterlockedExchange(&g_dirty, 1);
|
||||
refresh_rate_update(h, 0);
|
||||
return 0;
|
||||
@@ -381,6 +407,32 @@ static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
|
||||
case WM_DISPLAYCHANGE:
|
||||
refresh_rate_update(h, 1);
|
||||
break;
|
||||
case WM_GETDPISCALEDSIZE: // a framed window keeps its client size in pixels on a monitor at another scale
|
||||
if (!is_fullscreen(h) && !IsZoomed(h)) {
|
||||
RECT c, r;
|
||||
if (GetClientRect(h, &c)) {
|
||||
r = (RECT){0, 0, c.right, c.bottom};
|
||||
if (AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, LOWORD(w))) {
|
||||
*(SIZE *)l = (SIZE){r.right - r.left, r.bottom - r.top};
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // DefWindowProc: Windows scales the size with the DPI
|
||||
case WM_DPICHANGED: { // display scale changed, or the window reached a monitor at another scale
|
||||
const RECT *s = (const RECT *)l;
|
||||
InterlockedExchange(&g_dpi, LOWORD(w));
|
||||
rt_log("gfx: window DPI %u (%u%%)", LOWORD(w), LOWORD(w) * 100u / USER_DEFAULT_SCREEN_DPI);
|
||||
if (is_fullscreen(h)) { // keep covering the monitor the suggested rect lies on
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
if (GetMonitorInfoW(MonitorFromRect(s, MONITOR_DEFAULTTONEAREST), &mi))
|
||||
SetWindowPos(h, NULL, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
|
||||
mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
} else {
|
||||
SetWindowPos(h, NULL, s->left, s->top, s->right - s->left, s->bottom - s->top, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_ACTIVATEAPP: {
|
||||
int in = w != 0;
|
||||
InterlockedExchange(&g_focused, in);
|
||||
@@ -417,23 +469,25 @@ static DWORD WINAPI window_main(void *arg) {
|
||||
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
|
||||
.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW), .lpszClassName = L"TotKRecompWindow"};
|
||||
RegisterClassExW(&wc);
|
||||
int cw, ch;
|
||||
int cw, ch, ow, oh;
|
||||
cfg_window_size(&cw, &ch);
|
||||
RECT r = {0, 0, cw, ch};
|
||||
AdjustWindowRect(&r, WND_STYLE, FALSE);
|
||||
outer_size(NULL, cw, ch, &ow, &oh);
|
||||
// The window opens behind the user's windows without taking focus: it shows the game's blank loading frames for a
|
||||
// while, and must not cover what the player works on meanwhile. The game still counts itself in focus until the
|
||||
// first WM_ACTIVATEAPP, so loading goes on, and keyboard input waits for the foreground (input_win.c). Its taskbar
|
||||
// button flashes until it is brought up, and TOTK_FULLSCREEN waits for that first activation.
|
||||
// TOTK_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
|
||||
int background = g_background = cfg_get("TOTK_BACKGROUND_WINDOW") && atoi(cfg_get("TOTK_BACKGROUND_WINDOW"));
|
||||
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
r.right - r.left, r.bottom - r.top, NULL, NULL, inst, NULL);
|
||||
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
|
||||
NULL, inst, NULL);
|
||||
if (!g_hwnd) {
|
||||
rt_log("gfx: CreateWindowEx failed (%lu)", GetLastError());
|
||||
g_wnd_failed = 1;
|
||||
} else {
|
||||
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
// The frame was measured at the system DPI; the window may have opened on a monitor at another scale.
|
||||
InterlockedExchange(&g_dpi, (LONG)GetDpiForWindow(g_hwnd));
|
||||
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
if (!background) {
|
||||
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
|
||||
FlashWindowEx(&fw);
|
||||
@@ -441,6 +495,7 @@ static DWORD WINAPI window_main(void *arg) {
|
||||
}
|
||||
note_applied();
|
||||
refresh_rate_update(g_hwnd, 1);
|
||||
keep_awake(1);
|
||||
}
|
||||
SetEvent(g_wnd_ready);
|
||||
if (g_wnd_failed) return 1;
|
||||
@@ -490,6 +545,7 @@ static void flip_locked(void) {
|
||||
vk_queue_lock();
|
||||
VkResult r = vkQueuePresentKHR(vk_ctx()->gfx_queue, &pi);
|
||||
vk_queue_unlock();
|
||||
vk_device_fault_report(r);
|
||||
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) InterlockedExchange(&g_dirty, 1);
|
||||
}
|
||||
|
||||
@@ -740,6 +796,27 @@ static void save_capture(const char *path) {
|
||||
rt_log("gfx: menu test shot %s (%dx%d)", path, g_cap_w, g_cap_h);
|
||||
}
|
||||
|
||||
// Where the game's picture (cw x ch) goes on the sw x sh swapchain: TOTK_ASPECT=fit (default) keeps its shape and centres
|
||||
// it, with black bars at the sides (a 16:9 picture in a 21:9 window) or above and below; stretch fills the swapchain.
|
||||
// Bars under a pixel are not drawn. Returns 1 when the rectangle leaves bars to clear.
|
||||
static int fit_rect(int cw, int ch, int sw, int sh, int out[4]) {
|
||||
out[0] = 0, out[1] = 0, out[2] = sw, out[3] = sh;
|
||||
if (cw <= 0 || ch <= 0 || strcmp(cfg_value("TOTK_ASPECT"), "fit")) return 0;
|
||||
long long a = (long long)cw * sh, b = (long long)ch * sw;
|
||||
if (a < b) { // narrower than the window: bars at the sides
|
||||
int fw = (int)((a + ch / 2) / ch);
|
||||
if (fw >= sw - 1) return 0;
|
||||
out[0] = (sw - fw) / 2, out[2] = fw;
|
||||
} else if (a > b) { // wider: bars above and below
|
||||
int fh = (int)(((long long)ch * sw + cw / 2) / cw);
|
||||
if (fh >= sh - 1) return 0;
|
||||
out[1] = (sh - fh) / 2, out[3] = fh;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4], int defer) {
|
||||
int general = layout == VK_IMAGE_LAYOUT_GENERAL;
|
||||
gfx_window_start();
|
||||
@@ -763,6 +840,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
if (!create_swapchain()) goto out;
|
||||
r = vkAcquireNextImageKHR(c->device, g_sc, 1000000000ull, g_acquire[slot], VK_NULL_HANDLE, &idx);
|
||||
}
|
||||
vk_device_fault_report(r);
|
||||
if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) goto out; // lost, not ready: skip the frame
|
||||
if (r == VK_SUBOPTIMAL_KHR) InterlockedExchange(&g_dirty, 1);
|
||||
vkResetFences(c->device, 1, &g_fence[slot]);
|
||||
@@ -792,20 +870,32 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
vkBeginCommandBuffer(cb, &bi);
|
||||
uint64_t crumb = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
vk_crumb(cb, VK_CRUMB_PRESENT, crumb);
|
||||
VkImage dst = g_sc_img[idx];
|
||||
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
|
||||
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
const int rect[4] = {cx, cy, cw, ch};
|
||||
// FSR (TOTK_UPSCALER) when the swapchain is larger than the crop; else the bilinear blit
|
||||
int fsr = general && vk_fsr_record(cb, slot, src, fmt, w, h, rect, dst, g_sc_ext.width, g_sc_ext.height);
|
||||
g_scaler = fsr ? 2 : (uint32_t)cw != g_sc_ext.width || (uint32_t)ch != g_sc_ext.height;
|
||||
int to[4];
|
||||
if (fit_rect(cw, ch, (int)g_sc_ext.width, (int)g_sc_ext.height, to)) {
|
||||
// black bars: clear the whole image, then the picture's writes below wait for the clear
|
||||
VkClearColorValue black = {{0, 0, 0, 1}};
|
||||
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
vkCmdClearColorImage(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &all);
|
||||
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
}
|
||||
// FSR (TOTK_UPSCALER) when the picture's place on the swapchain is larger than the crop; else the bilinear blit
|
||||
int fsr = general && vk_fsr_record(cb, slot, src, fmt, w, h, rect, dst, to);
|
||||
g_scaler = fsr ? 2 : cw != to[2] || ch != to[3];
|
||||
if (!fsr) {
|
||||
if (general) // a GENERAL source (vk_texture.c images): earlier submissions' writes become visible to the blit here
|
||||
sync_barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
|
||||
VkImageBlit region = {
|
||||
.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{cx, cy, 0}, {cx + cw, cy + ch, 1}},
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{0, 0, 0}, {(int)g_sc_ext.width, (int)g_sc_ext.height, 1}}};
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{to[0], to[1], 0}, {to[0] + to[2], to[1] + to[3], 1}}};
|
||||
vkCmdBlitImage(cb, src, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_LINEAR);
|
||||
if (general) // later submissions that write the source wait for the blit's read
|
||||
@@ -852,6 +942,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
record_thumb(cb, dst, slot);
|
||||
}
|
||||
sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, last_stage, last_write, VK_PIPELINE_STAGE_2_NONE, 0);
|
||||
vk_crumb(cb, VK_CRUMB_PRESENT_END, crumb);
|
||||
vkEndCommandBuffer(cb);
|
||||
|
||||
VkSemaphoreSubmitInfo wait = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = g_acquire[slot],
|
||||
@@ -870,6 +961,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
g_flip_idx = idx;
|
||||
if (!defer) flip_locked();
|
||||
} else {
|
||||
vk_device_fault_report(r);
|
||||
rt_log("gfx: present submit failed (%d)", (int)r);
|
||||
}
|
||||
g_frame++;
|
||||
@@ -901,9 +993,9 @@ void vk_present_flip(void) {
|
||||
|
||||
void gfx_window_resize(int cw, int chh) {
|
||||
gfx_window_start();
|
||||
RECT r = {0, 0, cw, chh};
|
||||
AdjustWindowRect(&r, WND_STYLE, FALSE);
|
||||
SetWindowPos(g_hwnd, NULL, 0, 0, r.right - r.left, r.bottom - r.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
int ow, oh;
|
||||
outer_size(g_hwnd, cw, chh, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
EnterCriticalSection(&g_pl);
|
||||
create_swapchain();
|
||||
LeaveCriticalSection(&g_pl);
|
||||
@@ -979,8 +1071,8 @@ void gfx_window_start(void) {
|
||||
LeaveCriticalSection(&g_pl);
|
||||
RECT rc = {0};
|
||||
GetClientRect(g_hwnd, &rc);
|
||||
rt_log("gfx: window %ldx%ld, swapchain %ux%u format %d", rc.right, rc.bottom, g_sc_ext.width, g_sc_ext.height,
|
||||
(int)g_sc_fmt);
|
||||
rt_log("gfx: window %ldx%ld at DPI %u, swapchain %ux%u format %d", rc.right, rc.bottom, gfx_window_dpi(),
|
||||
g_sc_ext.width, g_sc_ext.height, (int)g_sc_fmt);
|
||||
}
|
||||
LeaveCriticalSection(&g_start_lock);
|
||||
vsync_ensure();
|
||||
|
||||
@@ -242,6 +242,9 @@ void res_heapdef_served(void); // romfs.c serves the
|
||||
void res_init(void); // after rt_patches_init: the patch page and the sets
|
||||
void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene sizes in effect (the game's own
|
||||
// until res_init switched the sets on)
|
||||
int res_is_16x9(unsigned w, unsigned h); // within 1% of 16:9 (the test res_init and romfs.c share)
|
||||
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
|
||||
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
|
||||
// ---- code patches (patches.c) and the frame step (frame_rate.c)
|
||||
void rt_patches_init(void); // after ldr_load: switches from the environment, the patch
|
||||
// page committed; logs the sets
|
||||
@@ -281,6 +284,7 @@ void rt_boot(void); // boot.c
|
||||
// modules, registers its host functions, then seals (hostfn_seal, or implicitly at the first rt_call). Return the
|
||||
// process exit code.
|
||||
int probe_cfg(void); // cfg_probe.c
|
||||
int probe_dpi(void); // dpi_probe.c
|
||||
int probe_loader(void), probe_kernel(void), probe_ipc(void), probe_services(void), probe_fs(void);
|
||||
int probe_gfx_abi(void); // gfx_abi_probe.c
|
||||
int probe_vkdevice(void); // vk_device.c
|
||||
|
||||
+11
-2
@@ -44,7 +44,7 @@ static const char *const action_names[ACTION_COUNT] = {
|
||||
"LSTICK_LEFT", "LSTICK_UP", "LSTICK_RIGHT", "LSTICK_DOWN", "RSTICK_LEFT", "RSTICK_UP", "RSTICK_RIGHT", "RSTICK_DOWN"};
|
||||
static const char *const pad_names[PAD_COUNT] = {
|
||||
NULL, "south", "east", "west", "north", "lstick", "rstick", "l", "r", "start", "back",
|
||||
"dpad_left", "dpad_up", "dpad_right", "dpad_down", "zl", "zr"};
|
||||
"dpad_left", "dpad_up", "dpad_right", "dpad_down", "zl", "zr", "lpaddle1", "rpaddle1", "lpaddle2", "rpaddle2"};
|
||||
static const char *const key_names[KEY_COUNT] = { // each group starts at its id; the ids of a group are consecutive
|
||||
[KEY_A] = "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u",
|
||||
"v", "w", "x", "y", "z",
|
||||
@@ -283,7 +283,7 @@ void input_map_line(const InputMap *m, int action, char *out, size_t n) {
|
||||
}
|
||||
|
||||
int input_pad_layout_pick(const char *setting, int east_labelled_a) {
|
||||
if (setting && !_stricmp(setting, "nintendo")) return PAD_LAYOUT_NINTENDO;
|
||||
if (setting && (!_stricmp(setting, "nintendo") || !_stricmp(setting, "xbox_prompts"))) return PAD_LAYOUT_NINTENDO;
|
||||
if (setting && !_stricmp(setting, "xbox")) return PAD_LAYOUT_XBOX;
|
||||
return east_labelled_a ? PAD_LAYOUT_NINTENDO : PAD_LAYOUT_XBOX;
|
||||
}
|
||||
@@ -325,6 +325,15 @@ void input_mouse_stick(int32_t dx, int32_t dy, double dt_s, float sens, int32_t
|
||||
out[1] = (int32_t)lrint(y);
|
||||
}
|
||||
|
||||
void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]) {
|
||||
double fx = x / (double)STICK_MAX, fy = y / (double)STICK_MAX, m = sqrt(fx * fx + fy * fy);
|
||||
out[0] = out[1] = 0;
|
||||
if (m <= dead) return;
|
||||
double k = ((m > 1 ? 1 : m) - dead) / (1 - dead) / m; // past the edge (a square gate's corner) the length is 1
|
||||
out[0] = (int32_t)lrint(fmax(-1, fmin(1, fx * k)) * STICK_MAX);
|
||||
out[1] = (int32_t)lrint(fmax(-1, fmin(1, fy * k)) * STICK_MAX);
|
||||
}
|
||||
|
||||
// Minus and Plus held together open or close the menu. *held: latched from the poll where both go down until neither is
|
||||
// down (the caller keeps it). Every poll of the latch reports neither to the game, so releasing one button first does not
|
||||
// leak the other; Minus alone before Plus reaches the game unchanged. With the menu open the game sees a neutral pad.
|
||||
|
||||
+12
-3
@@ -34,11 +34,14 @@ enum {
|
||||
};
|
||||
|
||||
// Gamepad inputs by position (Nintendo layout, as SDL names them), platform-neutral like the key ids. ZL and ZR are the
|
||||
// analog triggers; input_win.c translates SDL buttons and axes to these (one place).
|
||||
// analog triggers; input_win.c translates SDL buttons and axes to these (one place). The paddles are the back buttons
|
||||
// of an Xbox Elite, DualSense Edge or Steam Controller (SDL's LEFT_PADDLE1 and so on); no Switch button has them by
|
||||
// default, a bind line or the remapping rows add them.
|
||||
enum {
|
||||
PAD_NONE = 0,
|
||||
PAD_SOUTH, PAD_EAST, PAD_WEST, PAD_NORTH, PAD_LSTICK, PAD_RSTICK, PAD_L, PAD_R, PAD_START, PAD_BACK,
|
||||
PAD_DPAD_LEFT, PAD_DPAD_UP, PAD_DPAD_RIGHT, PAD_DPAD_DOWN, PAD_ZL, PAD_ZR,
|
||||
PAD_LPADDLE1, PAD_RPADDLE1, PAD_LPADDLE2, PAD_RPADDLE2,
|
||||
PAD_COUNT
|
||||
};
|
||||
|
||||
@@ -77,8 +80,10 @@ 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.
|
||||
void input_map_configure(void);
|
||||
|
||||
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); anything else ("auto", NULL) picks Nintendo
|
||||
// when the pad's east button is labelled A (a Switch Pro Controller, a GuliKit) and Xbox otherwise.
|
||||
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts" is the Nintendo layout with
|
||||
// the A/B and X/Y prompts swapped (runtime/romfs.c overlay_init), so the buttons map as "nintendo". Anything else
|
||||
// ("auto", NULL) picks Nintendo when the pad's east button is labelled A (a Switch Pro Controller, a GuliKit) and Xbox
|
||||
// otherwise.
|
||||
int input_pad_layout_pick(const char *setting, int east_labelled_a);
|
||||
// down[pad] != 0 for the gamepad inputs held. Returns the Switch button bits (0..15) the layout's pad table presses.
|
||||
uint64_t input_map_pad_buttons(const uint8_t down[PAD_COUNT], int layout);
|
||||
@@ -91,6 +96,10 @@ void input_map_stick(const uint8_t down[KEY_COUNT], int stick, int32_t out[2]);
|
||||
// Mouse motion in raw counts over dt_s seconds to right-stick deflection: full deflection at 3000 / sens counts/s.
|
||||
// Clamped to the unit circle, up (negative dy) is +y.
|
||||
void input_mouse_stick(int32_t dx, int32_t dy, double dt_s, float sens, int32_t out[2]);
|
||||
// A gamepad stick (x, y in -32768..32767, up is +y) through a round dead zone of `dead` (fraction of full deflection,
|
||||
// 0..0.5): inside the circle the stick reads 0, and the rest of the travel is scaled to start at 0 at the edge, so a
|
||||
// drifting stick rests at 0 and full deflection still reaches full. The result stays inside the unit square.
|
||||
void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]);
|
||||
|
||||
// ---- the remapping rows' edits, on a copy of input_map. Pure.
|
||||
// action's keys (table order) / its gamepad buttons in layout, at most max; returns how many.
|
||||
|
||||
+54
-8
@@ -19,7 +19,7 @@
|
||||
#include "ui/menu.h"
|
||||
#include "vk.h"
|
||||
|
||||
enum { STICK_MAX = 32767, STICK_DEAD = 2500, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
|
||||
enum { STICK_MAX = 32767, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
|
||||
|
||||
static SRWLOCK lock = SRWLOCK_INIT;
|
||||
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
|
||||
@@ -43,6 +43,8 @@ static const SDL_GamepadButton pad_sdl[PAD_COUNT] = {
|
||||
[PAD_R] = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, [PAD_START] = SDL_GAMEPAD_BUTTON_START, [PAD_BACK] = SDL_GAMEPAD_BUTTON_BACK,
|
||||
[PAD_DPAD_LEFT] = SDL_GAMEPAD_BUTTON_DPAD_LEFT, [PAD_DPAD_UP] = SDL_GAMEPAD_BUTTON_DPAD_UP,
|
||||
[PAD_DPAD_RIGHT] = SDL_GAMEPAD_BUTTON_DPAD_RIGHT, [PAD_DPAD_DOWN] = SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
||||
[PAD_LPADDLE1] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, [PAD_RPADDLE1] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1,
|
||||
[PAD_LPADDLE2] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, [PAD_RPADDLE2] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2,
|
||||
};
|
||||
|
||||
// TOTK_INPUT_SCRIPT=<file>: lines "<frame> <button> [hold]" or "<frame> LSTICK|RSTICK <x> <y> [hold]", ORed into the real input.
|
||||
@@ -428,9 +430,21 @@ static void mouse_camera(int32_t out[2]) {
|
||||
out[1] = held[1];
|
||||
}
|
||||
|
||||
static int32_t axis(int16_t v) {
|
||||
int32_t a = v < -STICK_MAX ? -STICK_MAX : v;
|
||||
return a > -STICK_DEAD && a < STICK_DEAD ? 0 : a;
|
||||
static float stick_dead = -1; // TOTK_STICK_DEADZONE as a fraction of full deflection; live (cfg_version)
|
||||
static unsigned stick_dead_seen;
|
||||
|
||||
static float stick_dead_get(void) {
|
||||
unsigned ver = cfg_version();
|
||||
if (stick_dead >= 0 && ver == stick_dead_seen) return stick_dead;
|
||||
stick_dead_seen = ver;
|
||||
double v = cfg_double("TOTK_STICK_DEADZONE");
|
||||
stick_dead = (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
|
||||
return stick_dead;
|
||||
}
|
||||
|
||||
// One gamepad stick through the dead zone; SDL's Y grows downwards, the Switch's upwards.
|
||||
static void stick(SDL_GamepadAxis ax, SDL_GamepadAxis ay, float dead, int32_t out[2]) {
|
||||
input_pad_stick(SDL_GetGamepadAxis(pad, ax), -(int32_t)SDL_GetGamepadAxis(pad, ay), dead, out);
|
||||
}
|
||||
|
||||
// Open the first gamepad when none is open or the open one went away; logs once per change.
|
||||
@@ -503,12 +517,44 @@ static void binds_refresh(void) {
|
||||
input_map_configure();
|
||||
}
|
||||
|
||||
// TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry> (test runs): an SDL virtual gamepad whose sticks rest at these raw SDL axis values
|
||||
// (-32768..32767, down is +y), attached before the first scan so it is the gamepad the game reads: drift and full
|
||||
// deflection go through the same mapping and dead zone as a real pad.
|
||||
static void test_pad_attach(void) {
|
||||
static SDL_Joystick *joy; // kept open: a virtual joystick's axes are set through an open handle
|
||||
const char *e = cfg_get("TOTK_TEST_PAD");
|
||||
int v[4];
|
||||
if (!e || !*e) return;
|
||||
if (sscanf(e, "%d,%d,%d,%d", &v[0], &v[1], &v[2], &v[3]) != 4) {
|
||||
rt_log("input: TOTK_TEST_PAD=%s is not <lx>,<ly>,<rx>,<ry>; no test gamepad", e);
|
||||
return;
|
||||
}
|
||||
SDL_VirtualJoystickDesc d;
|
||||
SDL_INIT_INTERFACE(&d);
|
||||
d.type = SDL_JOYSTICK_TYPE_GAMEPAD;
|
||||
d.naxes = SDL_GAMEPAD_AXIS_COUNT;
|
||||
d.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
|
||||
d.name = "TotK test gamepad";
|
||||
SDL_JoystickID id = SDL_AttachVirtualJoystick(&d);
|
||||
joy = id ? SDL_OpenJoystick(id) : NULL;
|
||||
if (!joy) {
|
||||
rt_log("input: test gamepad not attached (%s)", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
static const SDL_GamepadAxis axes[4] = {SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_RIGHTX,
|
||||
SDL_GAMEPAD_AXIS_RIGHTY};
|
||||
for (int i = 0; i < 4; i++)
|
||||
SDL_SetJoystickVirtualAxis(joy, axes[i], (Sint16)(v[i] < -32768 ? -32768 : v[i] > 32767 ? 32767 : v[i]));
|
||||
rt_log("input: test gamepad attached, sticks at %d,%d %d,%d", v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
static void start(void) {
|
||||
binds_refresh();
|
||||
const char *e = cfg_get("TOTK_INPUT");
|
||||
if (e && !strcmp(e, "none")) { state = -1; rt_log("input: disabled (TOTK_INPUT=none)"); return; }
|
||||
if (!SDL_Init(SDL_INIT_GAMEPAD)) { state = -1; rt_log("input: SDL gamepad init failed (%s), keyboard only", SDL_GetError()); }
|
||||
else state = 1;
|
||||
if (state == 1) test_pad_attach();
|
||||
if (state == 1) {
|
||||
scan_pads();
|
||||
if (!pad) rt_log("input: no gamepad (keyboard only until one is connected)");
|
||||
@@ -536,6 +582,7 @@ static int pad_physical(uint8_t pd[PAD_COUNT]) {
|
||||
for (int p = PAD_SOUTH; p < PAD_ZL; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]);
|
||||
pd[PAD_ZL] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_ON;
|
||||
pd[PAD_ZR] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_ON;
|
||||
for (int p = PAD_ZR + 1; p < PAD_COUNT; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]); // false without paddles
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -545,10 +592,9 @@ static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
|
||||
uint8_t pd[PAD_COUNT] = {0};
|
||||
if (!pad_physical(pd)) return 0;
|
||||
*b |= input_map_pad_buttons(pd, pad_layout_get());
|
||||
sl[0] = axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX));
|
||||
sl[1] = -axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY));
|
||||
sr[0] = axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTX));
|
||||
sr[1] = -axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTY));
|
||||
float dead = stick_dead_get();
|
||||
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, dead, sl);
|
||||
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, dead, sr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -4,6 +4,7 @@
|
||||
// --selftest layout, dispatch table and host-function registry; needs no game files
|
||||
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
|
||||
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
|
||||
// --probe-dpi the process DPI awareness (per-monitor v2) and the primary monitor's size; no game files
|
||||
// --probe-gfx-abi NVN object layouts against nvn.h's reserved sizes; needs no game files
|
||||
// --probe-nvn, --probe-nvn-tex, --probe-window, --probe-shader, --probe-vkmem, --probe-vkexec graphics probes
|
||||
// --probe-astc the compute ASTC decoder against astc_decode_ldr; needs no game files
|
||||
@@ -472,7 +473,7 @@ static int selftest(void) {
|
||||
|
||||
static int usage(const char *argv0) {
|
||||
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
|
||||
"--probe-cfg | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-cfg | --probe-dpi | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
|
||||
"--probe-vkexec | --probe-astc | --probe-movie | --prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
|
||||
return 2;
|
||||
@@ -559,6 +560,7 @@ int main(int argc, char **argv) {
|
||||
if (!strcmp(mode, "--selftest")) return selftest();
|
||||
if (!strcmp(mode, "--probe-loader")) return probe_loader();
|
||||
if (!strcmp(mode, "--probe-cfg")) return probe_cfg();
|
||||
if (!strcmp(mode, "--probe-dpi")) return probe_dpi();
|
||||
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
|
||||
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
|
||||
if (!strcmp(mode, "--probe-services")) return probe_services();
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
// axis, direction = the controller's three axes in a reference frame whose z is up. The orientation is integrated from
|
||||
// the gyro and, while the controller is not being accelerated (|a| within 15 % of 1 G), pulled toward the measured
|
||||
// gravity (Mahony, gain 0.5/s: about 2 s), as the Switch's sensor fusion does; yaw has no reference and drifts with the
|
||||
// gyro's bias. The newest MOTION_RING samples are kept; reads are newest first. The feeding thread writes, guest
|
||||
// threads read, both under the instance's SRW lock.
|
||||
// gyro's bias. The newest MOTION_RING samples are kept; reads are newest first. A restart (StartSixAxisSensor) drops
|
||||
// the samples before it: reads return nothing until the next sample, which starts a fresh frame and angle; sampling
|
||||
// numbers go on. The feeding thread writes, guest threads read, both under the instance's SRW lock.
|
||||
#include <windows.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
@@ -24,6 +25,7 @@ struct MotionFusion {
|
||||
SRWLOCK lock;
|
||||
MotionState ring[MOTION_RING];
|
||||
uint64_t produced, last_ns;
|
||||
uint64_t first; // produced at the last restart: older samples are not read
|
||||
double R[3][3]; // controller -> reference; row i = reference axis i in controller coordinates
|
||||
double angle[3];
|
||||
int restart;
|
||||
@@ -131,8 +133,8 @@ int motion_read(MotionFusion *m, MotionState *out, int max, uint64_t now) {
|
||||
if (max <= 0) return 0;
|
||||
int n = 0;
|
||||
AcquireSRWLockShared(&m->lock);
|
||||
if (m->produced && (now < m->last_ns || now - m->last_ns < STALE_NS)) {
|
||||
uint64_t have = m->produced < MOTION_RING ? m->produced : MOTION_RING;
|
||||
if (m->produced > m->first && (now < m->last_ns || now - m->last_ns < STALE_NS)) {
|
||||
uint64_t have = m->produced - m->first < MOTION_RING ? m->produced - m->first : MOTION_RING;
|
||||
n = (int)(have < (uint64_t)max ? have : (uint64_t)max);
|
||||
for (int i = 0; i < n; i++) out[i] = m->ring[(m->produced - 1 - (uint64_t)i) % MOTION_RING];
|
||||
}
|
||||
@@ -143,5 +145,8 @@ int motion_read(MotionFusion *m, MotionState *out, int max, uint64_t now) {
|
||||
void motion_restart(MotionFusion *m) {
|
||||
AcquireSRWLockExclusive(&m->lock);
|
||||
m->restart = 1;
|
||||
m->first = m->produced;
|
||||
m->last_ns = 0; // the first sample after the restart integrates nothing across the stop
|
||||
memset(m->angle, 0, sizeof m->angle);
|
||||
ReleaseSRWLockExclusive(&m->lock);
|
||||
}
|
||||
+16
-2
@@ -250,8 +250,10 @@ static void probe_oe(void) {
|
||||
{NULL, "en-US", "en-US"}, {NULL, "be-BY", "en-US"}, {NULL, "xx", "en-US"}, {NULL, "ja-JP", "ja"},
|
||||
{NULL, "fr-CA", "fr-CA"}, {NULL, "es-MX", "es-419"}, {NULL, "es_ES", "es"}, {NULL, "zh-Hant-TW", "zh-Hant"},
|
||||
{NULL, "zh-CN", "zh-Hans"}, {NULL, "en-GB", "en-GB"}, {NULL, "ru", "ru"}, {"ja", "fr-FR", "ja"},
|
||||
{"en-US", NULL, "en-US"}};
|
||||
{"en-US", NULL, "en-US"}, {"zh-CN", NULL, "zh-Hans"}, {"zh-TW", NULL, "zh-Hant"},
|
||||
{"zh-Hans", NULL, "zh-Hans"}, {"zh-Hant", NULL, "zh-Hant"}};
|
||||
int bad = 0;
|
||||
printf("oe: configured language (TOTK_LANG=%s) is %s\n", keep_lang ? keep_lang : "", lang());
|
||||
for (size_t i = 0; i < sizeof cases / sizeof *cases; i++) {
|
||||
set_env("TOTK_LANG", cases[i].lang);
|
||||
set_env("TOTK_LOCALE", cases[i].locale);
|
||||
@@ -550,9 +552,21 @@ static void probe_feedback(uint32_t pid, uint32_t style) {
|
||||
near3(ms[0].dir + 3, -1, 0, 0, 0.01) && near3(ms[0].dir + 6, 0, 0, 1, 0.01),
|
||||
"fusion quarter turn: %d states, sampling %lld, angle %.4f rev, x axis (%.3f %.3f %.3f)", n,
|
||||
(long long)ms[0].sampling, (double)ms[0].angle[2], (double)ms[0].dir[0], (double)ms[0].dir[1], (double)ms[0].dir[2]);
|
||||
double quarter = ms[0].angle[2];
|
||||
CHECK(motion_read(m, ms, 16, t + 300000000ull) == 0, "fusion states 300 ms after the last sample");
|
||||
// StartSixAxisSensor again in the middle of the turn (the bow drawn again): the states from before it are gone, and
|
||||
// the first one after it starts a fresh angle in a fresh frame; sampling numbers go on.
|
||||
for (int i = 0; i < 250; i++) motion_feed(m, down, turn, t += 4000000);
|
||||
motion_restart(m);
|
||||
int same = motion_read(m, ms, 16, t + 1000000);
|
||||
motion_feed(m, down, turn, t += 4000000);
|
||||
int next = motion_read(m, ms, 16, t + 1000000);
|
||||
CHECK(same == 0 && next == 1 && ms[0].sampling == 502 && fabs(ms[0].angle[2]) < 0.002 &&
|
||||
near3(ms[0].dir, 1, 0, 0, 0.01),
|
||||
"fusion restart: %d states in the frame of the Start, %d after one sample (sampling %lld, angle %.4f rev)", same,
|
||||
next, (long long)ms[0].sampling, (double)ms[0].angle[2]);
|
||||
printf("hid feedback: vibration handles %#x %#x, six-axis %#x, at rest when stopped; fusion quarter turn %.4f rev\n",
|
||||
vh[0], vh[1], sh[0], (double)ms[0].angle[2]);
|
||||
vh[0], vh[1], sh[0], quarter);
|
||||
}
|
||||
|
||||
static void probe_input(void) {
|
||||
|
||||
+7
-3
@@ -88,12 +88,16 @@ static const char *switch_language(const char *tag) {
|
||||
return "en-US";
|
||||
}
|
||||
|
||||
// The game's language as a Switch language code (TOTK_LANG, else TOTK_LOCALE, else the Windows user locale).
|
||||
// The game's language as a Switch language code (TOTK_LANG, else TOTK_LOCALE, else the Windows user locale). TotK
|
||||
// accepts "zh-Hans" and "zh-Hant" for Chinese and falls back to Japanese for "zh-CN" and "zh-TW", so those two
|
||||
// spellings, which older totk.ini files and menus wrote, are mapped to them.
|
||||
const char *ov_language(void) {
|
||||
static const char *const codes[] = {"ja", "en-US", "fr", "de", "it", "es", "zh-CN", "ko", "nl", "pt", "ru",
|
||||
"zh-TW", "en-GB", "fr-CA", "es-419", "zh-Hans", "zh-Hant", "pt-BR"};
|
||||
static const char *const codes[] = {"ja", "en-US", "fr", "de", "it", "es", "ko", "nl", "pt", "ru",
|
||||
"en-GB", "fr-CA", "es-419", "zh-Hans", "zh-Hant", "pt-BR"};
|
||||
const char *code = cfg_get("TOTK_LANG"); // no fixed default: TOTK_LOCALE or the Windows user locale decides when unset
|
||||
if (code && *code) {
|
||||
if (!strcmp(code, "zh-CN")) return "zh-Hans";
|
||||
if (!strcmp(code, "zh-TW")) return "zh-Hant";
|
||||
for (size_t i = 0; i < sizeof codes / sizeof *codes; i++)
|
||||
if (!strcmp(code, codes[i])) return codes[i];
|
||||
rt_fatal("TOTK_LANG '%s' is not a Switch language code", code);
|
||||
|
||||
@@ -116,6 +116,9 @@ void plat_cpu_load(PlatLoad *l, float *total_pct, float *proc_pct, float *temp_c
|
||||
void plat_gpu_load(PlatLoad *l, float *load_pct, float *vram_used_mb);
|
||||
// Physical memory in MB: in use and installed on the machine, and this process's working set.
|
||||
void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb);
|
||||
// Commit charge in MB: this process's private bytes, the system's commit in use and its limit (RAM plus page files).
|
||||
// An allocation fails when the system's reaches the limit, however much physical memory is free. -1 when unknown.
|
||||
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb);
|
||||
|
||||
// ---- child processes: a program with stdin and stdout piped to us (stderr discarded), no console window.
|
||||
typedef struct PlatChild PlatChild;
|
||||
|
||||
@@ -613,6 +613,18 @@ void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb) {
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc)) *proc_mb = (float)(pmc.WorkingSetSize >> 20);
|
||||
}
|
||||
|
||||
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
|
||||
*proc_mb = *used_mb = *limit_mb = -1;
|
||||
MEMORYSTATUSEX ms = {.dwLength = sizeof ms};
|
||||
if (GlobalMemoryStatusEx(&ms)) {
|
||||
*limit_mb = (double)(ms.ullTotalPageFile >> 20);
|
||||
*used_mb = (double)((ms.ullTotalPageFile - ms.ullAvailPageFile) >> 20);
|
||||
}
|
||||
PROCESS_MEMORY_COUNTERS_EX pmc;
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS *)&pmc, sizeof pmc))
|
||||
*proc_mb = (double)(pmc.PrivateUsage >> 20);
|
||||
}
|
||||
|
||||
// ---- child processes
|
||||
|
||||
struct PlatChild {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
//
|
||||
// Patch page: +16 the scene size {w, h} as floats, +32 the output size and +48 the swapchain size as {w, h} in one
|
||||
// 64-bit word, +40 the renderer (1.2.1) or display configuration (1.4.3) the patched code stores there.
|
||||
// +56 the camera's aspect ratio as a float when the output is not 16:9 (1.4.3, set aspect).
|
||||
// 1.4.3 scales the scene it reads by output width / 1920 (a 2560x1440 output renders its 1600x900 scene at 2128x1200;
|
||||
// MacNativeGames measured the same), so its page holds the wanted size divided by that factor and the game's 5/6 needs
|
||||
// no scene patch. 1.2.1 renders the size it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and
|
||||
@@ -218,10 +219,45 @@ void res_init(void) {
|
||||
rt_patch_switch(output, R.ow != BASE_W || R.oh != BASE_H);
|
||||
rt_patch_switch(scene, scene_on);
|
||||
R.on = 1;
|
||||
// Another shape than 16:9 (more than 1% off): the camera takes the output's ratio (recomp/patches-v143.txt, set
|
||||
// aspect), so a 21:9 output shows more of the world at the sides instead of a stretched 16:9 view. The HUD's layouts
|
||||
// for that shape come from romfs-patches-<w>x<h> (romfs.c overlay_init, tools/make_romfs_patches.py --aspect).
|
||||
double ratio = (double)R.ow / R.oh, game = (double)BASE_W / BASE_H;
|
||||
int aspect = rt_patch_set("aspect");
|
||||
if (!res_is_16x9(R.ow, R.oh)) {
|
||||
if (aspect < 0) {
|
||||
rt_log("resolution: output %ux%u is not 16:9 and this game version has no camera aspect patch; the view "
|
||||
"is stretched", R.ow, R.oh);
|
||||
} else {
|
||||
float r = (float)ratio;
|
||||
memcpy(page + 56, &r, sizeof r);
|
||||
rt_patch_switch(aspect, 1);
|
||||
rt_log("resolution: camera aspect ratio %.4f (16:9 is %.4f)", ratio, game);
|
||||
}
|
||||
} else {
|
||||
rt_patch_switch(aspect, 0); // the page holds no ratio
|
||||
}
|
||||
rt_log("resolution: output %ux%u, scene %ux%u%s; texture heap +%llu KiB", R.ow, R.oh, R.sw, R.sh,
|
||||
scene_on ? "" : " (the game's own 5/6 of the output)", (unsigned long long)res_heap_extra_kib());
|
||||
}
|
||||
|
||||
// 16:9 within 1% (1366x768 counts): the shape the game's own layouts and camera are made for.
|
||||
int res_is_16x9(unsigned w, unsigned h) {
|
||||
double rel = ((double)w / h) / ((double)BASE_W / BASE_H);
|
||||
return rel >= 0.99 && rel <= 1.01;
|
||||
}
|
||||
|
||||
// The game resolution asks for a romfs-patches-<w>x<h> overlay: it is not 16:9 and the game version has the camera
|
||||
// aspect patch (set aspect, 1.4.3 only); the wide UI layouts only fit a camera that follows the same shape.
|
||||
int res_wants_wide_overlay(const unsigned out[2]) {
|
||||
return !res_is_16x9(out[0], out[1]) && rt_patch_set("aspect") >= 0;
|
||||
}
|
||||
|
||||
void res_wanted(unsigned out[2]) {
|
||||
decide();
|
||||
out[0] = R.ow, out[1] = R.oh;
|
||||
}
|
||||
|
||||
void res_sizes(unsigned out[2], unsigned scene[2]) {
|
||||
decide();
|
||||
out[0] = R.on ? R.ow : BASE_W, out[1] = R.on ? R.oh : BASE_H;
|
||||
|
||||
+73
-16
@@ -244,7 +244,11 @@ static uint32_t overlay_slot(uint64_t i) {
|
||||
}
|
||||
|
||||
// ---- overlays
|
||||
static void overlay_load(const char *dir) {
|
||||
// Loads and checks one overlay folder. A failed check stops the game, except with optional set (the Xbox prompts
|
||||
// folder): the folder is dropped, the reason logged, and 0 returned. Returns 1 when the folder is loaded or has no
|
||||
// manifest.
|
||||
static int overlay_load(const char *dir, int optional) {
|
||||
#define BAD(...) do { if (!optional) rt_fatal(__VA_ARGS__); rt_log(__VA_ARGS__); goto bad; } while (0)
|
||||
char path[2100];
|
||||
need(snprintf(path, sizeof path, "%s/manifest.tsv", dir) < (int)sizeof path, "overlay path");
|
||||
wchar_t *w = fs_wide(path);
|
||||
@@ -252,7 +256,7 @@ static void overlay_load(const char *dir) {
|
||||
free(w);
|
||||
if (!m) {
|
||||
rt_log("RomFS overlay: none (%s)", path);
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
char line[8192];
|
||||
size_t first = noverlay;
|
||||
@@ -268,30 +272,32 @@ static void overlay_load(const char *dir) {
|
||||
uint64_t i = lower(name);
|
||||
int known = i < file_count && !strcmp(entry_name(entry(i)), name), add = !strcmp(orig, "-");
|
||||
if (!known && !add)
|
||||
rt_fatal("RomFS overlay: %s/manifest.tsv names %s, which the RomFS lacks; a new file has \"-\" as its "
|
||||
"original's SHA-256", dir, name);
|
||||
BAD("RomFS overlay: %s/manifest.tsv names %s, which the RomFS lacks; a new file has \"-\" as its "
|
||||
"original's SHA-256", dir, name);
|
||||
if (known && add)
|
||||
rt_fatal("RomFS overlay: %s/manifest.tsv adds %s, which the RomFS already has; give its original's SHA-256",
|
||||
dir, name);
|
||||
BAD("RomFS overlay: %s/manifest.tsv adds %s, which the RomFS already has; give its original's SHA-256",
|
||||
dir, name);
|
||||
Entry e = {0};
|
||||
if (known) {
|
||||
e = entry(i);
|
||||
sha_range(image, e.offset, e.size, hex, 0);
|
||||
if (strcmp(hex, orig)) rt_fatal("RomFS overlay: the original of %s has SHA-256 %s, %s/manifest.tsv says %s",
|
||||
name, hex, dir, orig);
|
||||
if (strcmp(hex, orig)) BAD("RomFS overlay: the original of %s has SHA-256 %s, %s/manifest.tsv says %s",
|
||||
name, hex, dir, orig);
|
||||
} else {
|
||||
i = ADDED;
|
||||
}
|
||||
char file[8200];
|
||||
need(snprintf(file, sizeof file, "%s/%s", dir, name) < (int)sizeof file, "overlay file path");
|
||||
HANDLE f = open_read(file, 1);
|
||||
if (f == INVALID_HANDLE_VALUE) rt_fatal("RomFS overlay: %s missing", file);
|
||||
if (f == INVALID_HANDLE_VALUE) BAD("RomFS overlay: %s missing", file);
|
||||
Identity id = identity_of(f);
|
||||
if (id.size != size) rt_fatal("RomFS overlay: %s has %llu bytes, its manifest says %llu", file,
|
||||
(unsigned long long)id.size, size);
|
||||
if (id.size != size) {
|
||||
CloseHandle(f);
|
||||
BAD("RomFS overlay: %s has %llu bytes, its manifest says %llu", file, (unsigned long long)id.size, size);
|
||||
}
|
||||
sha_range(f, 0, size, hex, 0);
|
||||
CloseHandle(f);
|
||||
if (strcmp(hex, sha)) rt_fatal("RomFS overlay: %s has SHA-256 %s, its manifest says %s", file, hex, sha);
|
||||
if (strcmp(hex, sha)) BAD("RomFS overlay: %s has SHA-256 %s, its manifest says %s", file, hex, sha);
|
||||
hashed += e.size + size;
|
||||
if (noverlay == overlay_cap) {
|
||||
overlay_cap = overlay_cap ? overlay_cap * 2 : 16;
|
||||
@@ -309,6 +315,16 @@ static void overlay_load(const char *dir) {
|
||||
noverlay - first - 16);
|
||||
if (noverlay > first) rt_log("RomFS overlay: %s checked, %zu files, %.1f MiB hashed in %.2f s", dir,
|
||||
noverlay - first, hashed / 1048576.0, now_s() - t0);
|
||||
return 1;
|
||||
bad:
|
||||
fclose(m);
|
||||
for (size_t k = first; k < noverlay; k++) {
|
||||
free((void *)overlays[k].path);
|
||||
free((void *)overlays[k].name);
|
||||
}
|
||||
noverlay = first;
|
||||
return 0;
|
||||
#undef BAD
|
||||
}
|
||||
|
||||
static int overlay_order(const void *a, const void *b) {
|
||||
@@ -407,18 +423,59 @@ static void heapdef_overlay(void) {
|
||||
res_heapdef_served();
|
||||
}
|
||||
|
||||
static int xbox_prompts_ready; // romfs-patches-xbox/manifest.tsv was next to romfs-index.bin at start
|
||||
static int xbox_prompts_active; // and overlay_init loaded it
|
||||
|
||||
int romfs_xbox_prompts_ready(void) { return xbox_prompts_ready; }
|
||||
int romfs_xbox_prompts_active(void) { return xbox_prompts_active; }
|
||||
|
||||
static void overlay_init(void) {
|
||||
// TOTK_ROMFS_PATCHES unset: romfs-patches next to romfs-index.bin if it has a manifest (tools/make_romfs_patches.py)
|
||||
char def[2100];
|
||||
// TOTK_ROMFS_PATCHES unset: romfs-patches next to romfs-index.bin if it has a manifest (tools/make_romfs_patches.py).
|
||||
// A game resolution that is not 16:9 takes romfs-patches-<w>x<h> instead when it is there: the same overlay with
|
||||
// the UI laid out for that shape (make_romfs_patches.py --aspect). 1.2.1 has no camera aspect patch, so it keeps
|
||||
// the 16:9 layout (res_wants_wide_overlay).
|
||||
// With pad_layout = xbox_prompts, romfs-patches-xbox in its place: the same patches with the A/B and X/Y prompts
|
||||
// swapped (make_romfs_patches.py --xbox-prompts). The game reads the archives once at boot, so the prompts stay
|
||||
// until restart even though the layout itself changes live. Each folder replaces romfs-patches as a whole and the
|
||||
// setup builds no wide folder with Xbox prompts, so a wide game resolution keeps the Switch prompts.
|
||||
char def[2100], xbox[2100];
|
||||
const char *patches = cfg_get("TOTK_ROMFS_PATCHES");
|
||||
if (!patches && snprintf(def, sizeof def, "%s/romfs-patches/manifest.tsv", index_dir) < (int)sizeof def &&
|
||||
int wide = 0;
|
||||
unsigned out[2];
|
||||
res_wanted(out);
|
||||
if (!patches && res_wants_wide_overlay(out)) {
|
||||
if (snprintf(def, sizeof def, "%s/romfs-patches-%ux%u/manifest.tsv", index_dir, out[0], out[1]) < (int)sizeof def &&
|
||||
GetFileAttributesA(def) != INVALID_FILE_ATTRIBUTES) {
|
||||
def[strlen(def) - strlen("/manifest.tsv")] = 0;
|
||||
patches = def, wide = 1;
|
||||
} else {
|
||||
rt_log("RomFS overlay: no romfs-patches-%ux%u for this game resolution, the UI keeps its 16:9 layout "
|
||||
"(tools/make_romfs_patches.py --aspect %ux%u)", out[0], out[1], out[0], out[1]);
|
||||
}
|
||||
}
|
||||
xbox_prompts_ready =
|
||||
snprintf(xbox, sizeof xbox, "%s/romfs-patches-xbox/manifest.tsv", index_dir) < (int)sizeof xbox &&
|
||||
GetFileAttributesA(xbox) != INVALID_FILE_ATTRIBUTES;
|
||||
int want_xbox = !_stricmp(cfg_value("TOTK_PAD_LAYOUT"), "xbox_prompts");
|
||||
if (!patches && want_xbox && xbox_prompts_ready) {
|
||||
xbox[strlen(xbox) - strlen("/manifest.tsv")] = 0;
|
||||
xbox_prompts_active = overlay_load(xbox, 1);
|
||||
if (xbox_prompts_active) rt_log("RomFS overlay: Xbox button prompts (%s)", xbox);
|
||||
else rt_log("RomFS overlay: %s failed its check (run the setup again); romfs-patches loads instead", xbox);
|
||||
} else if (want_xbox) {
|
||||
rt_log("RomFS overlay: pad_layout = xbox_prompts, but %s; Switch prompts stay",
|
||||
wide ? "the game resolution takes the wide UI layout"
|
||||
: patches ? "TOTK_ROMFS_PATCHES is set"
|
||||
: "romfs-patches-xbox has no manifest.tsv (run the setup again)");
|
||||
}
|
||||
if (!patches && !xbox_prompts_active && snprintf(def, sizeof def, "%s/romfs-patches/manifest.tsv", index_dir) < (int)sizeof def &&
|
||||
GetFileAttributesA(def) != INVALID_FILE_ATTRIBUTES) {
|
||||
def[strlen(def) - strlen("/manifest.tsv")] = 0;
|
||||
patches = def;
|
||||
}
|
||||
const char *dirs[2] = {patches, cfg_get("TOTK_ROMFS_MODS")};
|
||||
for (int i = 0; i < 2; i++)
|
||||
if (dirs[i] && *dirs[i] && strcmp(dirs[i], "0")) overlay_load(dirs[i]);
|
||||
if (dirs[i] && *dirs[i] && strcmp(dirs[i], "0")) overlay_load(dirs[i], 0);
|
||||
heapdef_overlay();
|
||||
added_init();
|
||||
if (noverlay) qsort(overlays, noverlay, sizeof *overlays, overlay_order);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// RGBA8 texels to BC3 blocks on the GPU, one invocation per 4x4 block: the second pass of the ASTC decode when
|
||||
// TOTK_ASTC_BC is on (vk_astc.c), so a decoded ASTC texture takes 8 bits per texel in video memory instead of 32.
|
||||
// Texels past the right or bottom edge repeat the last column or row.
|
||||
//
|
||||
// Colour: the BC1 four-colour block of the box around the block's colours, its diagonal turned to the sign of the
|
||||
// colours' covariance and inset by 1/16 of the range (the bounding-box method of stb_dxt and of yuzu's BC1 recompression),
|
||||
// then each texel takes the nearest of the four palette entries along that axis. Alpha: the eight-value BC3 alpha
|
||||
// block between the block's smallest and largest alpha. The bytes are what the texels hold, so an sRGB image keeps
|
||||
// sRGB-encoded values, as BC3_SRGB expects.
|
||||
// Compiled to runtime/bc3_encode_spv.h by tools/build_shaders.py; vk_astc.c records the dispatch.
|
||||
#version 450
|
||||
|
||||
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||
|
||||
layout(std430, set = 0, binding = 0) readonly buffer Texels { uint px[]; };
|
||||
layout(std430, set = 0, binding = 1) writeonly buffer Blocks { uvec4 blk[]; };
|
||||
|
||||
layout(push_constant) uniform Params {
|
||||
uint in_base; // first texel of slice 0 (index into px)
|
||||
uint out_base; // first block of slice 0 (uvec4 index into blk)
|
||||
uint width, height;
|
||||
uint bx; // blocks per row
|
||||
uint bslice; // blocks per slice
|
||||
} pc;
|
||||
|
||||
vec4 texel(uint x, uint y, uint z) {
|
||||
x = min(x, pc.width - 1u), y = min(y, pc.height - 1u);
|
||||
return unpackUnorm4x8(px[pc.in_base + (z * pc.height + y) * pc.width + x]) * 255.0;
|
||||
}
|
||||
|
||||
uint to565(vec3 c) {
|
||||
uvec3 q = uvec3(round(clamp(c, 0.0, 255.0) * vec3(31.0, 63.0, 31.0) / 255.0));
|
||||
return q.r << 11 | q.g << 5 | q.b;
|
||||
}
|
||||
|
||||
vec3 from565(uint v) {
|
||||
return vec3(float(v >> 11 & 31u) * 255.0 / 31.0, float(v >> 5 & 63u) * 255.0 / 63.0, float(v & 31u) * 255.0 / 31.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
uvec3 id = gl_GlobalInvocationID;
|
||||
uint by = (pc.height + 3u) / 4u;
|
||||
if (id.x >= pc.bx || id.y >= by) return;
|
||||
vec4 t[16];
|
||||
vec4 lo = vec4(255.0), hi = vec4(0.0), mean = vec4(0.0);
|
||||
for (uint i = 0u; i < 16u; i++) {
|
||||
t[i] = texel(id.x * 4u + (i & 3u), id.y * 4u + (i >> 2), id.z);
|
||||
lo = min(lo, t[i]), hi = max(hi, t[i]), mean += t[i];
|
||||
}
|
||||
mean /= 16.0;
|
||||
|
||||
// alpha: a0 > a1 selects the eight-value mode; index 0 is a0, 1 is a1, 2..7 the six steps from a0 to a1
|
||||
uint a0 = uint(hi.a), a1 = uint(lo.a);
|
||||
uvec2 ab = uvec2(a0 | a1 << 8, 0u);
|
||||
if (a0 > a1) {
|
||||
float range = float(a0 - a1);
|
||||
for (uint i = 0u; i < 16u; i++) {
|
||||
uint s = uint(clamp(round((float(a0) - t[i].a) * 7.0 / range), 0.0, 7.0)); // 0 = a0 .. 7 = a1
|
||||
uint code = s == 0u ? 0u : s == 7u ? 1u : s + 1u;
|
||||
uint bit = 16u + 3u * i;
|
||||
if (bit < 32u) ab.x |= code << bit;
|
||||
if (bit + 3u > 32u) ab.y |= code >> (32u - min(bit, 32u)) << (bit > 32u ? bit - 32u : 0u);
|
||||
}
|
||||
}
|
||||
|
||||
// colour: the box diagonal along the covariance of red and blue with green
|
||||
vec3 cmin = lo.rgb, cmax = hi.rgb;
|
||||
float crg = 0.0, cbg = 0.0;
|
||||
for (uint i = 0u; i < 16u; i++) {
|
||||
vec3 d = t[i].rgb - mean.rgb;
|
||||
crg += d.r * d.g, cbg += d.b * d.g;
|
||||
}
|
||||
if (crg < 0.0) { float s = cmin.r; cmin.r = cmax.r; cmax.r = s; }
|
||||
if (cbg < 0.0) { float s = cmin.b; cmin.b = cmax.b; cmax.b = s; }
|
||||
vec3 inset = (cmax - cmin) / 16.0;
|
||||
uint c0 = to565(cmax - inset), c1 = to565(cmin + inset);
|
||||
uint idx = 0u;
|
||||
if (c0 != c1) {
|
||||
if (c0 < c1) { uint s = c0; c0 = c1; c1 = s; } // c0 > c1: the four-colour mode
|
||||
vec3 e0 = from565(c0), e1 = from565(c1), axis = e1 - e0;
|
||||
float len2 = max(dot(axis, axis), 1e-6);
|
||||
for (uint i = 0u; i < 16u; i++) {
|
||||
uint s = uint(clamp(round(dot(t[i].rgb - e0, axis) / len2 * 3.0), 0.0, 3.0)); // 0 = e0 .. 3 = e1
|
||||
uint code = s == 0u ? 0u : s == 3u ? 1u : s + 1u; // palette order e0, e1, 2/3 e0 + 1/3 e1, 1/3 e0 + 2/3 e1
|
||||
idx |= code << (2u * i);
|
||||
}
|
||||
}
|
||||
blk[pc.out_base + id.z * pc.bslice + id.y * pc.bx + id.x] = uvec4(ab.x, ab.y, c0 | c1 << 16, idx);
|
||||
}
|
||||
+6
-3
@@ -217,8 +217,12 @@ static int text_entry_run(const SwkbdRequest *req, uint16_t *out, size_t cap, si
|
||||
Entry e = {.req = req, .armed = 0};
|
||||
const char *test = cfg_get("TOTK_SWKBD_TEST");
|
||||
if (test && *test) e.test = !strcmp(test, "cancel") ? -1 : 1;
|
||||
// The process is per-monitor DPI aware: the font and the layout below are measured at the game window's DPI, the
|
||||
// monitor the dialog opens on, not at the system DPI that plain SystemParametersInfo and AdjustWindowRectEx use.
|
||||
HWND owner = gfx_window_handle();
|
||||
UINT dpi = owner ? GetDpiForWindow(owner) : GetDpiForSystem();
|
||||
NONCLIENTMETRICSW ncm = {.cbSize = sizeof ncm};
|
||||
SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof ncm, &ncm, 0);
|
||||
SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof ncm, &ncm, 0, dpi);
|
||||
HFONT font = CreateFontIndirectW(&ncm.lfMessageFont);
|
||||
HDC dc = GetDC(NULL);
|
||||
HGDIOBJ old = SelectObject(dc, font);
|
||||
@@ -230,10 +234,9 @@ static int text_entry_run(const SwkbdRequest *req, uint16_t *out, size_t cap, si
|
||||
int y_prompt = pad, h_prompt = u * 5 / 2, y_edit = y_prompt + h_prompt + u / 4, h_edit = u * 9 / 5;
|
||||
int y_hint = y_edit + h_edit + u / 2, y_btn = y_hint + u * 3 / 2, ch = y_btn + bh + pad;
|
||||
|
||||
HWND owner = gfx_window_handle();
|
||||
RECT wr = {0, 0, cw + 2 * pad, ch};
|
||||
DWORD style = WS_POPUP | WS_CAPTION | WS_SYSMENU, ex = WS_EX_DLGMODALFRAME | WS_EX_CONTROLPARENT;
|
||||
AdjustWindowRectEx(&wr, style, FALSE, ex);
|
||||
AdjustWindowRectExForDpi(&wr, style, FALSE, ex, dpi);
|
||||
int ww = wr.right - wr.left, wh = wr.bottom - wr.top, x = CW_USEDEFAULT, y = CW_USEDEFAULT;
|
||||
RECT orc;
|
||||
if (owner && GetWindowRect(owner, &orc)) {
|
||||
|
||||
@@ -54,6 +54,7 @@ typedef struct TexVkFormat {
|
||||
uint8_t astc_bw, astc_bh; // nonzero: ASTC decoded into vk (RGBA8, same colour space) by vk_astc.c or tex_astc.c;
|
||||
// zero for native ASTC (vk == ideal, raw blocks as for BC)
|
||||
uint8_t astc_srgb;
|
||||
uint8_t astc_bc; // ASTC decoded and then encoded to BC3 (vk BC3, TOTK_ASTC_BC; vk_astc.c)
|
||||
uint8_t depth, stencil; // aspect flags of depth / stencil formats
|
||||
const char *note; // why a fallback or an expansion was chosen; NULL when the mapping is direct
|
||||
const char *reason; // unmapped formats only: why there is no Vulkan format
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<!-- Embedded by totk.rc. activeCodePage makes UTF-8 the process code page (Windows 10 1903 and later): the narrow file
|
||||
functions (fopen, CreateFileA, GetModuleFileNameA) and getenv then carry UTF-8, the same encoding platform/win32.c
|
||||
and fs_host.c convert with, so an install folder or user profile with a non-ASCII name works. -->
|
||||
and fs_host.c convert with, so an install folder or user profile with a non-ASCII name works.
|
||||
dpiAwareness PerMonitorV2 (dpiAware true/pm on Windows 10 before 1607): window, monitor and cursor sizes are physical
|
||||
pixels and the swapchain matches the screen. A DPI-unaware process gets them divided by the display scale and its
|
||||
image stretched back up by Windows, so 4K at 150% presented a 2560x1440 image, blurred. gfx_window.c follows a
|
||||
change of the window's DPI (WM_GETDPISCALEDSIZE, WM_DPICHANGED). -->
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
@@ -13,6 +17,8 @@
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -21,7 +21,8 @@ void rt_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
static_assert(UI_BIND_RSTICK_DOWN - UI_BIND_A == ACTION_COUNT - 1, "UI_BIND_* follow the actions");
|
||||
static_assert(UI_BIND_DESC_MINUS - UI_BIND_DESC_A == B_MINUS, "UI_BIND_DESC_* follow the actions");
|
||||
static_assert(UI_KEY_PAGEDOWN - UI_KEY_SPACE == KEY_PAGEDOWN - KEY_SPACE, "UI_KEY_* follow the key ids");
|
||||
static_assert(UI_PAD_ZR - UI_PAD_SOUTH == PAD_ZR - PAD_SOUTH, "UI_PAD_* follow the pad ids");
|
||||
static_assert(UI_PAD_RPADDLE2 - UI_PAD_SOUTH == PAD_RPADDLE2 - PAD_SOUTH && PAD_RPADDLE2 == PAD_COUNT - 1,
|
||||
"UI_PAD_* follow the pad ids");
|
||||
|
||||
enum { CAPTURE_SECONDS = 5 };
|
||||
// The capture window stays this long after the press: the press and its click reach ImGui a frame or two later and must
|
||||
|
||||
+33
-10
@@ -38,6 +38,9 @@ void vk_queue_unlock(void);
|
||||
void res_sizes(unsigned out[2], unsigned scene[2]);
|
||||
double frame_step_limit_hz(void);
|
||||
extern const int rt_game_version;
|
||||
// fs.h (romfs.c): romfs-patches-xbox was next to romfs-index.bin at start, and was loaded.
|
||||
int romfs_xbox_prompts_ready(void);
|
||||
int romfs_xbox_prompts_active(void);
|
||||
extern const char rt_port_version[];
|
||||
}
|
||||
|
||||
@@ -171,8 +174,13 @@ static ImGuiStyle g_base_style; // the menu's style at 1080 lines
|
||||
static uint64_t g_last_ns;
|
||||
static uint64_t g_cost_ns, g_cost_frames; // CPU time of layout and recording while open, logged per 600 frames
|
||||
|
||||
// Sizes are tuned for 1080 lines and follow the window's height; under 720 lines they stay at the 720 size.
|
||||
static float menu_scale(uint32_t h) { return (h >= 720 ? h : 720) / 1080.0f; }
|
||||
// Sizes are tuned for 1080 lines and follow the window's height. Under 720 lines at 100% display scale they stay at the
|
||||
// 720 size; that floor grows with the window's DPI, so a small window on a 150% or 200% display (sizes are physical
|
||||
// pixels) still gets text as large as the display scale asks for.
|
||||
static float menu_scale(uint32_t h) {
|
||||
float least = 720.0f * (float)gfx_window_dpi() / 96.0f;
|
||||
return ((float)h >= least ? (float)h : least) / 1080.0f;
|
||||
}
|
||||
|
||||
// SavageWindRecomp's colours: dark green panels, gold accents.
|
||||
static void style_init(ImGuiStyle &s) {
|
||||
@@ -409,10 +417,12 @@ static void row_window_size(void) {
|
||||
opt_choice_list("TOTK_WINDOW_SIZE", UI_MENU_WINDOW_SIZE, pv, pn, n, UI_MENU_WINDOW_SIZE_DESC);
|
||||
}
|
||||
|
||||
// The game resolution (resolution.c): 16:9 sizes from the game's own 1920x1080 up, and the current value when it is
|
||||
// The game resolution (resolution.c): 16:9 sizes from its smallest, 1280x720 (for weak GPUs), up, the common 21:9
|
||||
// monitor sizes (the camera and the HUD follow a wider shape, resolution.c res_init), and the current value when it is
|
||||
// another size.
|
||||
static void row_output_res(void) {
|
||||
static const char *const vals[] = {"1920x1080", "2560x1440", "3200x1800", "3840x2160"};
|
||||
static const char *const vals[] = {"1280x720", "1600x900", "1920x1080", "2560x1440", "3200x1800", "3840x2160",
|
||||
"2560x1080", "3440x1440", "3840x1600", "5120x2160"};
|
||||
enum { N = sizeof vals / sizeof *vals };
|
||||
char shown[N][32];
|
||||
const char *names[N];
|
||||
@@ -458,7 +468,7 @@ static void row_game_lang(void) {
|
||||
{"en-US", UI_MENU_LANGNAME_EN}, {"ja", UI_MENU_LANGNAME_JA}, {"fr", UI_MENU_LANGNAME_FR},
|
||||
{"de", UI_MENU_LANGNAME_DE}, {"it", UI_MENU_LANGNAME_IT}, {"es", UI_MENU_LANGNAME_ES},
|
||||
{"nl", UI_MENU_LANGNAME_NL}, {"ru", UI_MENU_LANGNAME_RU},
|
||||
{"zh-CN", UI_MENU_LANGNAME_ZH_HANS}, {"zh-TW", UI_MENU_LANGNAME_ZH_HANT}, {"ko", UI_MENU_LANGNAME_KO},
|
||||
{"zh-Hans", UI_MENU_LANGNAME_ZH_HANS}, {"zh-Hant", UI_MENU_LANGNAME_ZH_HANT}, {"ko", UI_MENU_LANGNAME_KO},
|
||||
};
|
||||
enum { N = sizeof langs / sizeof *langs + 1 };
|
||||
const char *vals[N], *names[N];
|
||||
@@ -476,8 +486,9 @@ static void row_game_lang(void) {
|
||||
|
||||
// The frame limit (frame_rate.c): unset, the older fps60 = 0 still means 30. The monitor choice shows the rate it gives.
|
||||
static void row_fps_limit() {
|
||||
static const char *const vals[] = {"30", "60", "120", "144", "display"};
|
||||
static const UiTextId ids[] = {UI_MENU_FPS_30, UI_MENU_FPS_60, UI_MENU_FPS_120, UI_MENU_FPS_144, UI_MENU_FPS_DISPLAY};
|
||||
static const char *const vals[] = {"30", "60", "90", "120", "144", "display"};
|
||||
static const UiTextId ids[] = {UI_MENU_FPS_30, UI_MENU_FPS_60, UI_MENU_FPS_90, UI_MENU_FPS_120, UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_DISPLAY};
|
||||
enum { N = sizeof vals / sizeof *vals };
|
||||
const char *names[N];
|
||||
for (int i = 0; i < N; i++) names[i] = menu_text(ids[i]);
|
||||
@@ -528,6 +539,8 @@ static void tab_screen(float scale) {
|
||||
static const UiTextId up[] = {UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR};
|
||||
opt_choice("TOTK_UPSCALER", UI_MENU_UPSCALER, up, 2, UI_MENU_UPSCALER_DESC);
|
||||
opt_slider("TOTK_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
|
||||
static const UiTextId aspect[] = {UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH};
|
||||
opt_choice("TOTK_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
|
||||
opt_end();
|
||||
}
|
||||
|
||||
@@ -549,8 +562,14 @@ static void tab_sound(float scale) {
|
||||
opt_end();
|
||||
}
|
||||
|
||||
// "in use: Xbox" under the layout when it is Automatic and a gamepad is connected; NULL otherwise.
|
||||
// The note under the layout. Xbox prompts (the Nintendo layout with the A/B and X/Y prompts swapped, romfs.c
|
||||
// overlay_init) change the buttons at once but the prompts only at the next start: "files missing" without
|
||||
// romfs-patches-xbox, "prompts change after restart" while the prompts in use differ from the choice. Automatic with a
|
||||
// gamepad connected: "in use: Xbox". NULL otherwise.
|
||||
static const char *pad_layout_note(void) {
|
||||
bool want = !_stricmp(cfg_value("TOTK_PAD_LAYOUT"), "xbox_prompts");
|
||||
if (want && !romfs_xbox_prompts_ready()) return menu_text(UI_MENU_PAD_LAYOUT_PROMPTS_MISSING);
|
||||
if (want != (romfs_xbox_prompts_active() != 0)) return menu_text(UI_MENU_PAD_LAYOUT_PROMPTS_RESTART);
|
||||
int got = input_pad_layout();
|
||||
if (got < 0 || strcmp(cfg_value("TOTK_PAD_LAYOUT"), "auto")) return nullptr;
|
||||
static char note[96];
|
||||
@@ -561,12 +580,14 @@ static const char *pad_layout_note(void) {
|
||||
|
||||
static void tab_controls(float scale) {
|
||||
opt_begin("##controls", scale);
|
||||
static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX};
|
||||
opt_choice("TOTK_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 3, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());
|
||||
static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX,
|
||||
UI_MENU_PAD_LAYOUT_XBOX_PROMPTS};
|
||||
opt_choice("TOTK_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 4, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());
|
||||
opt_toggle("TOTK_MOTION", UI_MENU_MOTION, UI_MENU_MOTION_DESC);
|
||||
opt_slider("TOTK_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%");
|
||||
opt_slider("TOTK_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f");
|
||||
opt_toggle("TOTK_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC);
|
||||
opt_slider("TOTK_STICK_DEADZONE", UI_MENU_STICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%");
|
||||
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
|
||||
binds_rows(scale);
|
||||
opt_end();
|
||||
@@ -663,6 +684,8 @@ static void tab_advanced(float scale) {
|
||||
UI_MENU_MEMORY_VERIFY};
|
||||
opt_choice("TOTK_MEMORY", UI_MENU_MEMORY, mem, 4, UI_MENU_MEMORY_DESC);
|
||||
opt_toggle("TOTK_TEX_ALIAS", UI_MENU_TEX_ALIAS, UI_MENU_TEX_ALIAS_DESC);
|
||||
opt_slider("TOTK_TEX_VRAM_PRESSURE", UI_MENU_TEX_VRAM_PRESSURE, 0, 100, UI_MENU_TEX_VRAM_PRESSURE_DESC, 1, "%.0f%%");
|
||||
opt_toggle("TOTK_ASTC_BC", UI_MENU_ASTC_BC, UI_MENU_ASTC_BC_DESC);
|
||||
|
||||
opt_group(UI_MENU_GROUP_FIXES);
|
||||
opt_toggle("TOTK_LOD_GUARD", UI_MENU_LOD_GUARD, UI_MENU_LOD_GUARD_DESC);
|
||||
|
||||
@@ -59,6 +59,7 @@ void gfx_window_apply_settings(void); // TOTK_FULLSCREEN or TOTK_WINDOW_SIZE
|
||||
void gfx_window_request_close(void); // as the window's close button: the process ends
|
||||
int gfx_window_monitor_size(int *w, int *h); // pixels of the monitor the window is on; 0 when unknown
|
||||
double gfx_window_refresh_hz(void); // its refresh rate (TOTK_FPS_LIMIT=display); 0 when unknown
|
||||
unsigned gfx_window_dpi(void); // the window's DPI (96 at 100% display scale); any thread
|
||||
// The present mode the swapchain got, as a TOTK_VK_PRESENT value ("fifo" when mailbox is missing); "" before the first
|
||||
// swapchain. Any thread.
|
||||
const char *gfx_window_present_mode(void);
|
||||
|
||||
+47
-12
@@ -52,7 +52,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_GRAPHICS] = "Picture",
|
||||
[UI_MENU_OUTPUT_RES] = "Game resolution",
|
||||
[UI_MENU_OUTPUT_RES_DESC] = "The size the game draws its picture and interface in. Above 1920 x 1080 the game needs "
|
||||
"more video memory and GPU time.",
|
||||
"more video memory and GPU time. A wider size (21:9) widens the camera's view and moves "
|
||||
"the HUD to the screen's corners.",
|
||||
[UI_MENU_RENDER_SCALE] = "3D render scale",
|
||||
[UI_MENU_RENDER_SCALE_DESC] = "Size of the 3D scene relative to the game resolution; the game's FSR scales it to "
|
||||
"that size. Above 100 % smooths jagged edges (supersampling) at a high GPU cost.",
|
||||
@@ -61,21 +62,25 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_UPSCALER_DESC] = "How the picture is scaled when the window is larger than the game resolution. FSR keeps "
|
||||
"edges and detail sharper than bilinear.",
|
||||
[UI_MENU_UPSCALER_FSR] = "AMD FSR 1", [UI_MENU_UPSCALER_BILINEAR] = "Bilinear",
|
||||
[UI_MENU_ASPECT] = "Picture shape",
|
||||
[UI_MENU_ASPECT_DESC] = "When the window's shape differs from the game resolution's (a 16:9 picture on a 21:9 "
|
||||
"screen): keep the picture's shape with black bars, or stretch it over the window.",
|
||||
[UI_MENU_ASPECT_FIT] = "Black bars", [UI_MENU_ASPECT_STRETCH] = "Stretch",
|
||||
[UI_MENU_SHARPNESS] = "FSR sharpness",
|
||||
[UI_MENU_SHARPNESS_DESC] = "Sharpening after FSR scaling; 0 turns it off.",
|
||||
|
||||
[UI_MENU_FPS_LIMIT] = "Frame rate limit",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "The most frames per second the game draws. Above 60 is experimental and needs Game speed "
|
||||
"follows the clock.",
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
// the monitor's refresh rate; printf format with the rate in Hz when it is known
|
||||
[UI_MENU_FPS_DISPLAY] = "Monitor rate", [UI_MENU_FPS_DISPLAY_HZ] = "Monitor rate (%.0f Hz)",
|
||||
[UI_MENU_REALTIME] = "Game speed follows the clock",
|
||||
[UI_MENU_REALTIME_DESC] = "Steps the game by real time, so a slow frame does not slow the game down.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Cloth on the clock",
|
||||
[UI_MENU_CLOTH_REALTIME_DESC] = "Steps cloth and hair by real time as well.",
|
||||
[UI_MENU_HIGHFPS_FIX] = "High frame rate physics fix",
|
||||
[UI_MENU_HIGHFPS_FIX_DESC] = "Corrects physics that misbehave above 30 frames per second.",
|
||||
[UI_MENU_HIGHFPS_FIX] = "High frame rate fixes",
|
||||
[UI_MENU_HIGHFPS_FIX_DESC] = "Corrects physics that misbehave above 30 frames per second, and objects that stay see-through or invisible above 60.",
|
||||
[UI_MENU_GAME_LANG] = "Game language", [UI_MENU_GAME_LANG_DESC] = "Language of the game's text and voices.",
|
||||
[UI_MENU_GAME_LANG_AUTO] = "Automatic",
|
||||
[UI_MENU_OPERATION_MODE] = "Console mode",
|
||||
@@ -94,9 +99,17 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PAD_LAYOUT] = "Button layout",
|
||||
[UI_MENU_PAD_LAYOUT_DESC] = "Which button is A. Nintendo: the right one, as on a Switch. Xbox: the bottom one, as "
|
||||
"labelled on an Xbox controller (cross on a PlayStation controller). Automatic picks "
|
||||
"Nintendo for controllers with Nintendo labels and Xbox for the others.",
|
||||
"Nintendo for controllers with Nintendo labels and Xbox for the others. Xbox prompts: "
|
||||
"A is the right one as with Nintendo, and the game shows the Xbox labels (B for A, A "
|
||||
"for B, Y for X, X for Y); the prompts change after a restart. The letters are part of the "
|
||||
"game's files, so a button you remap in Controls keeps the old letter on screen.",
|
||||
[UI_MENU_PAD_LAYOUT_AUTO] = "Automatic", [UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo (A right)",
|
||||
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A bottom)", [UI_MENU_PAD_LAYOUT_IN_USE] = "in use: %s",
|
||||
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A bottom)", [UI_MENU_PAD_LAYOUT_XBOX_PROMPTS] = "Xbox prompts (A right)",
|
||||
[UI_MENU_PAD_LAYOUT_IN_USE] = "in use: %s",
|
||||
[UI_MENU_PAD_LAYOUT_PROMPTS_MISSING] = "prompt files missing: run the setup again",
|
||||
[UI_MENU_PAD_LAYOUT_PROMPTS_RESTART] = "prompts change after restart",
|
||||
[UI_MENU_STICK_DEADZONE] = "Stick dead zone",
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "How far a gamepad stick moves before the game sees it. Raise it if the character or the camera moves on its own.",
|
||||
[UI_MENU_MOUSE_SENS] = "Mouse camera sensitivity",
|
||||
[UI_MENU_MOUSE_SENS_DESC] = "How fast the mouse turns the camera.",
|
||||
|
||||
@@ -142,6 +155,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_PAD_L] = "Left bumper", [UI_PAD_R] = "Right bumper", [UI_PAD_START] = "Start", [UI_PAD_BACK] = "Back",
|
||||
[UI_PAD_DPAD_LEFT] = "D-pad left", [UI_PAD_DPAD_UP] = "D-pad up", [UI_PAD_DPAD_RIGHT] = "D-pad right",
|
||||
[UI_PAD_DPAD_DOWN] = "D-pad down", [UI_PAD_ZL] = "Left trigger", [UI_PAD_ZR] = "Right trigger",
|
||||
[UI_PAD_LPADDLE1] = "Left back button", [UI_PAD_RPADDLE1] = "Right back button",
|
||||
[UI_PAD_LPADDLE2] = "Left back button 2", [UI_PAD_RPADDLE2] = "Right back button 2",
|
||||
[UI_PAD_CROSS] = "Cross", [UI_PAD_CIRCLE] = "Circle", [UI_PAD_SQUARE] = "Square", [UI_PAD_TRIANGLE] = "Triangle",
|
||||
|
||||
[UI_MENU_MENU_LANG] = "Menu language", [UI_MENU_MENU_LANG_DESC] = "Language of this menu and the overlay.",
|
||||
@@ -184,6 +199,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_MEMORY_AUTO] = "Automatic", [UI_MENU_MEMORY_IMPORT] = "Import", [UI_MENU_MEMORY_MIRROR] = "Mirror",
|
||||
[UI_MENU_MEMORY_VERIFY] = "Verify",
|
||||
[UI_MENU_TEX_ALIAS] = "Texture aliasing", [UI_MENU_TEX_ALIAS_DESC] = "A new texture created over a live texture's memory starts with that content instead of empty.",
|
||||
[UI_MENU_TEX_VRAM_PRESSURE] = "Texture memory limit", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Percent of video memory above which textures the game released are dropped (they come back from storage when used again). 0 keeps them.",
|
||||
[UI_MENU_ASTC_BC] = "Compress ASTC to BC3", [UI_MENU_ASTC_BC_DESC] = "With the GPU decoder: re-encodes decoded ASTC textures to BC3, a quarter of the video memory. For GPUs with 4 GB or less; textures look slightly blockier.",
|
||||
[UI_MENU_LOD_GUARD] = "LOD guard", [UI_MENU_LOD_GUARD_DESC] = "Prevents objects flashing in the wrong place or a flat far field for a frame, by waiting for the game to finish rewriting level-of-detail data before the GPU reads it.",
|
||||
[UI_MENU_FIX_C0] = "Constant buffer fix",
|
||||
[UI_MENU_FIX_C0_DESC] = "Fills the driver-reserved constant bank c0 (storage sizes, dispatch size, point size, base instance) instead of reading zeros.",
|
||||
@@ -270,7 +287,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_GRAPHICS] = "Изображение",
|
||||
[UI_MENU_OUTPUT_RES] = "Разрешение игры",
|
||||
[UI_MENU_OUTPUT_RES_DESC] = "Размер, в котором игра рисует картинку и интерфейс. Выше 1920 × 1080 игре нужно больше "
|
||||
"видеопамяти и времени GPU.",
|
||||
"видеопамяти и времени GPU. Более широкий размер (21:9) расширяет обзор камеры и "
|
||||
"переносит интерфейс в углы экрана.",
|
||||
[UI_MENU_RENDER_SCALE] = "Масштаб 3D-отрисовки",
|
||||
[UI_MENU_RENDER_SCALE_DESC] = "Размер 3D-сцены относительно разрешения игры; FSR игры доводит её до этого размера. "
|
||||
"Выше 100 % сглаживает лесенки на краях (суперсэмплинг) ценой большой нагрузки на GPU.",
|
||||
@@ -279,20 +297,24 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_UPSCALER_DESC] = "Как картинка растягивается, когда окно больше разрешения игры. FSR сохраняет края и "
|
||||
"детали чётче билинейного.",
|
||||
[UI_MENU_UPSCALER_FSR] = "AMD FSR 1", [UI_MENU_UPSCALER_BILINEAR] = "Билинейное",
|
||||
[UI_MENU_ASPECT] = "Форма картинки",
|
||||
[UI_MENU_ASPECT_DESC] = "Когда форма окна отличается от разрешения игры (картинка 16:9 на экране 21:9): сохранить "
|
||||
"форму картинки с чёрными полосами или растянуть её на всё окно.",
|
||||
[UI_MENU_ASPECT_FIT] = "Чёрные полосы", [UI_MENU_ASPECT_STRETCH] = "Растянуть",
|
||||
[UI_MENU_SHARPNESS] = "Резкость FSR",
|
||||
[UI_MENU_SHARPNESS_DESC] = "Повышение резкости после масштабирования FSR; 0 выключает его.",
|
||||
|
||||
[UI_MENU_FPS_LIMIT] = "Ограничение частоты кадров",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "Наибольшее число кадров в секунду. Выше 60 — экспериментальный режим, нужна «Скорость "
|
||||
"игры по часам».",
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "Как у монитора", [UI_MENU_FPS_DISPLAY_HZ] = "Как у монитора (%.0f Гц)",
|
||||
[UI_MENU_REALTIME] = "Скорость игры по часам",
|
||||
[UI_MENU_REALTIME_DESC] = "Игра идёт по реальному времени, медленный кадр её не замедляет.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Ткань по часам",
|
||||
[UI_MENU_CLOTH_REALTIME_DESC] = "Ткань и волосы тоже идут по реальному времени.",
|
||||
[UI_MENU_HIGHFPS_FIX] = "Исправление физики на высоком FPS",
|
||||
[UI_MENU_HIGHFPS_FIX_DESC] = "Исправляет физику, которая ломается при частоте выше 30 кадров в секунду.",
|
||||
[UI_MENU_HIGHFPS_FIX] = "Исправления для высокого FPS",
|
||||
[UI_MENU_HIGHFPS_FIX_DESC] = "Исправляет физику, которая ломается при частоте выше 30 кадров в секунду, и объекты, которые выше 60 остаются полупрозрачными или невидимыми.",
|
||||
[UI_MENU_GAME_LANG] = "Язык игры", [UI_MENU_GAME_LANG_DESC] = "Язык текста и озвучки игры.",
|
||||
[UI_MENU_GAME_LANG_AUTO] = "Автоматически",
|
||||
[UI_MENU_OPERATION_MODE] = "Режим консоли",
|
||||
@@ -311,9 +333,18 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PAD_LAYOUT] = "Раскладка кнопок",
|
||||
[UI_MENU_PAD_LAYOUT_DESC] = "Какая кнопка работает как A. Nintendo: правая, как на Switch. Xbox: нижняя, как "
|
||||
"подписано на контроллере Xbox (крест на контроллере PlayStation). Автоматически: "
|
||||
"Nintendo для контроллеров с подписями Nintendo, Xbox для остальных.",
|
||||
"Nintendo для контроллеров с подписями Nintendo, Xbox для остальных. Подсказки Xbox: "
|
||||
"A справа, как в Nintendo, а игра показывает надписи Xbox (B вместо A, A вместо B, Y "
|
||||
"вместо X, X вместо Y); подсказки меняются после перезапуска. Буквы записаны в файлах "
|
||||
"игры, поэтому у кнопки, переназначенной на вкладке управления, на экране остаётся "
|
||||
"прежняя буква.",
|
||||
[UI_MENU_PAD_LAYOUT_AUTO] = "Автоматически", [UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo (A справа)",
|
||||
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A снизу)", [UI_MENU_PAD_LAYOUT_IN_USE] = "сейчас: %s",
|
||||
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A снизу)", [UI_MENU_PAD_LAYOUT_XBOX_PROMPTS] = "Подсказки Xbox (A справа)",
|
||||
[UI_MENU_PAD_LAYOUT_IN_USE] = "сейчас: %s",
|
||||
[UI_MENU_PAD_LAYOUT_PROMPTS_MISSING] = "нет файлов подсказок: запустите установку снова",
|
||||
[UI_MENU_PAD_LAYOUT_PROMPTS_RESTART] = "подсказки сменятся после перезапуска",
|
||||
[UI_MENU_STICK_DEADZONE] = "Мёртвая зона стиков",
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно отклонить стик геймпада, чтобы игра это заметила. Увеличьте, если персонаж или камера двигаются сами.",
|
||||
[UI_MENU_MOUSE_SENS] = "Чувствительность мыши",
|
||||
[UI_MENU_MOUSE_SENS_DESC] = "Как быстро мышь поворачивает камеру.",
|
||||
|
||||
@@ -363,6 +394,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_PAD_DPAD_LEFT] = "Крестовина влево", [UI_PAD_DPAD_UP] = "Крестовина вверх",
|
||||
[UI_PAD_DPAD_RIGHT] = "Крестовина вправо", [UI_PAD_DPAD_DOWN] = "Крестовина вниз", [UI_PAD_ZL] = "Левый курок",
|
||||
[UI_PAD_ZR] = "Правый курок",
|
||||
[UI_PAD_LPADDLE1] = "Левая задняя кнопка", [UI_PAD_RPADDLE1] = "Правая задняя кнопка",
|
||||
[UI_PAD_LPADDLE2] = "Левая задняя кнопка 2", [UI_PAD_RPADDLE2] = "Правая задняя кнопка 2",
|
||||
[UI_PAD_CROSS] = "Крест", [UI_PAD_CIRCLE] = "Круг", [UI_PAD_SQUARE] = "Квадрат", [UI_PAD_TRIANGLE] = "Треугольник",
|
||||
|
||||
[UI_MENU_MENU_LANG] = "Язык меню", [UI_MENU_MENU_LANG_DESC] = "Язык этого меню и индикатора.",
|
||||
@@ -405,6 +438,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_MEMORY_AUTO] = "Автоматически", [UI_MENU_MEMORY_IMPORT] = "Импорт", [UI_MENU_MEMORY_MIRROR] = "Зеркало",
|
||||
[UI_MENU_MEMORY_VERIFY] = "Проверка",
|
||||
[UI_MENU_TEX_ALIAS] = "Наследование содержимого текстур", [UI_MENU_TEX_ALIAS_DESC] = "Новая текстура поверх памяти живой текстуры получает её содержимое, а не пустую.",
|
||||
[UI_MENU_TEX_VRAM_PRESSURE] = "Предел памяти текстур", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Процент видеопамяти, выше которого освобождённые игрой текстуры удаляются (при повторном использовании загружаются снова). 0 — не удалять.",
|
||||
[UI_MENU_ASTC_BC] = "Сжатие ASTC в BC3", [UI_MENU_ASTC_BC_DESC] = "С декодером на GPU: декодированные текстуры ASTC пересжимаются в BC3 и занимают вчетверо меньше видеопамяти. Для видеокарт с 4 ГБ и меньше; текстуры чуть более блочные.",
|
||||
[UI_MENU_LOD_GUARD] = "Защита LOD", [UI_MENU_LOD_GUARD_DESC] = "Не даёт объектам мелькать не на своём месте, а дальнему плану — на кадр становиться плоским: ждёт, пока игра допишет данные уровней детализации, и только потом их читает GPU.",
|
||||
[UI_MENU_FIX_C0] = "Исправление константного буфера",
|
||||
[UI_MENU_FIX_C0_DESC] = "Заполняет зарезервированный драйвером банк констант c0 (размеры хранилищ, размер запуска, размер точки, базовый экземпляр) вместо чтения нулей.",
|
||||
|
||||
+10
-4
@@ -35,10 +35,12 @@ typedef enum {
|
||||
UI_MENU_OUTPUT_RES, UI_MENU_OUTPUT_RES_DESC,
|
||||
UI_MENU_RENDER_SCALE, UI_MENU_RENDER_SCALE_DESC, UI_MENU_RENDER_SCALE_GAME,
|
||||
UI_MENU_UPSCALER, UI_MENU_UPSCALER_DESC, UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR,
|
||||
UI_MENU_ASPECT, UI_MENU_ASPECT_DESC, UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH,
|
||||
UI_MENU_SHARPNESS, UI_MENU_SHARPNESS_DESC,
|
||||
|
||||
// Game
|
||||
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_60, UI_MENU_FPS_120, UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_60, UI_MENU_FPS_90, UI_MENU_FPS_120,
|
||||
UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_DISPLAY, UI_MENU_FPS_DISPLAY_HZ,
|
||||
UI_MENU_REALTIME, UI_MENU_REALTIME_DESC,
|
||||
UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC,
|
||||
@@ -55,7 +57,9 @@ typedef enum {
|
||||
UI_MENU_RUMBLE_CURVE, UI_MENU_RUMBLE_CURVE_DESC,
|
||||
UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC,
|
||||
UI_MENU_PAD_LAYOUT, UI_MENU_PAD_LAYOUT_DESC, UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO,
|
||||
UI_MENU_PAD_LAYOUT_XBOX, UI_MENU_PAD_LAYOUT_IN_USE,
|
||||
UI_MENU_PAD_LAYOUT_XBOX, UI_MENU_PAD_LAYOUT_XBOX_PROMPTS, UI_MENU_PAD_LAYOUT_IN_USE,
|
||||
UI_MENU_PAD_LAYOUT_PROMPTS_MISSING, UI_MENU_PAD_LAYOUT_PROMPTS_RESTART,
|
||||
UI_MENU_STICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
|
||||
UI_MENU_MOUSE_SENS, UI_MENU_MOUSE_SENS_DESC,
|
||||
|
||||
// Controls tab: the remapping rows (ui/binds_page.cpp)
|
||||
@@ -82,10 +86,10 @@ typedef enum {
|
||||
UI_KEY_ENTER, UI_KEY_BACKSPACE, UI_KEY_RSHIFT, UI_KEY_RCTRL, UI_KEY_LALT, UI_KEY_CAPSLOCK, UI_KEY_INSERT,
|
||||
UI_KEY_DELETE, UI_KEY_HOME, UI_KEY_END, UI_KEY_PAGEUP, UI_KEY_PAGEDOWN,
|
||||
UI_KEY_NUM_FMT, // "Num %s": a numpad key
|
||||
// gamepad buttons, in pad id order PAD_SOUTH .. PAD_ZR, then the PlayStation face labels
|
||||
// gamepad buttons, in pad id order PAD_SOUTH .. PAD_RPADDLE2, then the PlayStation face labels
|
||||
UI_PAD_SOUTH, UI_PAD_EAST, UI_PAD_WEST, UI_PAD_NORTH, UI_PAD_LSTICK, UI_PAD_RSTICK, UI_PAD_L, UI_PAD_R,
|
||||
UI_PAD_START, UI_PAD_BACK, UI_PAD_DPAD_LEFT, UI_PAD_DPAD_UP, UI_PAD_DPAD_RIGHT, UI_PAD_DPAD_DOWN, UI_PAD_ZL,
|
||||
UI_PAD_ZR,
|
||||
UI_PAD_ZR, UI_PAD_LPADDLE1, UI_PAD_RPADDLE1, UI_PAD_LPADDLE2, UI_PAD_RPADDLE2,
|
||||
UI_PAD_CROSS, UI_PAD_CIRCLE, UI_PAD_SQUARE, UI_PAD_TRIANGLE,
|
||||
|
||||
// Menu tab
|
||||
@@ -117,6 +121,8 @@ typedef enum {
|
||||
UI_MENU_MEMORY, UI_MENU_MEMORY_DESC, UI_MENU_MEMORY_AUTO, UI_MENU_MEMORY_IMPORT, UI_MENU_MEMORY_MIRROR,
|
||||
UI_MENU_MEMORY_VERIFY,
|
||||
UI_MENU_TEX_ALIAS, UI_MENU_TEX_ALIAS_DESC,
|
||||
UI_MENU_TEX_VRAM_PRESSURE, UI_MENU_TEX_VRAM_PRESSURE_DESC,
|
||||
UI_MENU_ASTC_BC, UI_MENU_ASTC_BC_DESC,
|
||||
UI_MENU_LOD_GUARD, UI_MENU_LOD_GUARD_DESC,
|
||||
UI_MENU_FIX_C0, UI_MENU_FIX_C0_DESC,
|
||||
UI_MENU_FIX_ZERO, UI_MENU_FIX_ZERO_DESC,
|
||||
|
||||
+27
-6
@@ -36,7 +36,7 @@ typedef struct VkCtx {
|
||||
int validation; // layer active
|
||||
int has_custom_border_color, has_eds, has_eds2, has_eds3, has_vertex_input_dynamic_state,
|
||||
has_conditional_rendering, has_transform_feedback, has_provoking_vertex, has_device_fault, has_memory_budget,
|
||||
has_robustness2, has_buffer_device_address, has_fragment_shader_interlock,
|
||||
has_robustness2, has_buffer_device_address, has_fragment_shader_interlock, has_buffer_marker,
|
||||
has_astc_ldr; // textureCompressionASTC_LDR (enabled when reported)
|
||||
// Feedback loops (maxwell.md 1.9, vk_exec_draw.c feedback_prepare): a draw samples a target of its open pass that
|
||||
// an earlier draw of the pass wrote. TOTK_FEEDBACK=0: VK_FEEDBACK_OFF (sampled in GENERAL without a barrier, the
|
||||
@@ -56,8 +56,11 @@ typedef struct {
|
||||
uint32_t sg_size; // requiredSubgroupSize on sg_pin_stages (32)
|
||||
VkShaderStageFlags sg_pin_stages; // 0: no pin, stages run at the device's subgroupSize
|
||||
int gpl; // VK_EXT_graphics_pipeline_library enabled with fast linking (TOTK_GPL=0: off)
|
||||
int amd; // an AMD GPU (vendor 0x1002), any driver
|
||||
int amd_sc; // AMD's Windows driver branch for RDNA 1 and 2 ("AMD proprietary shader compiler")
|
||||
} VkCaps;
|
||||
extern VkCaps vk_caps;
|
||||
void vk_caps_driver(VkPhysicalDevice p); // first, before vkCreateDevice: vk_caps.amd, amd_sc
|
||||
void vk_caps_features(VkPhysicalDeviceFeatures *f); // before vkCreateDevice (TOTK_VK_NO_FLOAT64 clears shaderFloat64)
|
||||
// Before vkCreateDevice, with the driver's graphics pipeline library feature and properties: sets vk_caps.gpl,
|
||||
// 1 = enable the extensions (VK_KHR_pipeline_library, VK_EXT_graphics_pipeline_library) and the feature.
|
||||
@@ -69,6 +72,22 @@ VkCtx *vk_ctx(void); // NULL until vk_init() succeeded
|
||||
int vk_init(void); // idempotent; 1 on success, 0 on failure (reason logged with rt_log)
|
||||
void vk_shutdown(void);
|
||||
int vk_selftest(void); // 0 on success
|
||||
// Call with the result of a failed queue submit, present or wait before stopping on it: VK_ERROR_DEVICE_LOST logs
|
||||
// what VK_EXT_device_fault reports (description, faulting addresses with their precision, vendor codes) once per
|
||||
// process, after the GPU breadcrumbs (vk_crumbs_report). Any other result returns at once.
|
||||
void vk_device_fault_report(VkResult r);
|
||||
|
||||
// ---- GPU breadcrumbs (vk_crumbs.c): with VK_AMD_buffer_marker, each crumb has the GPU write its code into host memory
|
||||
// twice, when the command processor reaches it and when all work before it has finished. After a lost device the two
|
||||
// codes bracket the work that hung. Without the extension every call returns at once.
|
||||
enum { VK_CRUMB_SUBMIT = 1, VK_CRUMB_SUBMIT_END, VK_CRUMB_UPLOAD, VK_CRUMB_UPLOAD_END, VK_CRUMB_ASTC, VK_CRUMB_PRESENT,
|
||||
VK_CRUMB_PRESENT_END };
|
||||
void vk_crumbs_init(VkCtx *c); // after the allocator exists
|
||||
void vk_crumbs_release(void);
|
||||
// Records crumb kind with number n (a timeline value, a batch number; the low 28 bits are kept) into cb. Only the
|
||||
// thread that records cb may call it: never into a replay part while the recorder thread owns it (vk_rec.h).
|
||||
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n);
|
||||
void vk_crumbs_report(void); // logs the two codes and what they name
|
||||
|
||||
// ---- window and present (gfx_window.c). Host threads only.
|
||||
// vk_present_image: blits the crop rectangle {x, y, w, h} of src (NULL, or w/h <= 0: the whole w x h image), scaled
|
||||
@@ -81,11 +100,11 @@ void vk_present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, in
|
||||
// vk_present_image split in two: vk_present_blit submits the blit and leaves the present waiting, vk_present_flip
|
||||
// queues it. The next blit or a swapchain rebuild queues a waiting present first.
|
||||
void vk_present_blit(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4]);
|
||||
// FSR 1 on the present (vk_fsr.c): records EASU (+ RCAS) of src's crop rectangle {x, y, w, h} into dst, a dw x dh
|
||||
// swapchain image in TRANSFER_DST; src is in GENERAL and fmt its format. 0 = nothing recorded (the setting says
|
||||
// bilinear, no upscale is needed, or FSR is not available): the caller blits.
|
||||
// FSR 1 on the present (vk_fsr.c): records EASU (+ RCAS) of src's crop rectangle {x, y, w, h} into the rectangle out
|
||||
// {x, y, w, h} of dst, a swapchain image in TRANSFER_DST; src is in GENERAL and fmt its format. 0 = nothing recorded
|
||||
// (the setting says bilinear, no upscale is needed, or FSR is not available): the caller blits.
|
||||
int vk_fsr_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, int sw, int sh, const int crop[4],
|
||||
VkImage dst, uint32_t dw, uint32_t dh);
|
||||
VkImage dst, const int out[4]);
|
||||
void vk_fsr_release(void); // before the device goes away
|
||||
void vk_present_flip(void);
|
||||
// Every submission to ctx->gfx_queue (and xfer_queue when shared) takes this lock; vk_present_image and the swapchain
|
||||
@@ -223,6 +242,7 @@ 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
|
||||
int vk_astc_native(VkFormat astc_fmt); // 1: images of this ASTC format are created as such and get the raw blocks
|
||||
int vk_astc_gpu(void); // 1: ASTC images decoded to RGBA8 by the compute decoder (vk_astc_record)
|
||||
int vk_astc_bc(void); // 1: those images are BC3 instead, encoded after the decode (TOTK_ASTC_BC=1)
|
||||
// One image region decoded from raw blocks: tight rows of 16-byte blocks at byte src of the source buffer (slices one
|
||||
// after the other), w x h texels and slices deep, written at offset of level / layer.
|
||||
typedef struct { VkDeviceSize src; uint32_t w, h, slices, level, layer; VkOffset3D offset; } VkAstcRegion;
|
||||
@@ -230,9 +250,10 @@ typedef struct { VkDeviceSize src; uint32_t w, h, slices, level, layer; VkOffset
|
||||
// bytes of blocks) goes to a device-local scratch buffer, a dispatch per region decodes it, and the texels are copied
|
||||
// into img (VK_IMAGE_LAYOUT_GENERAL). The caller records its usual barriers around an image copy before and after the
|
||||
// call. readback (optional, host-visible, TRANSFER_DST): also gets the decoded texels, the regions one after the other.
|
||||
// bc: img is BC3 (TexVkFormat.astc_bc); the texels are encoded to BC3 blocks before the copy, and readback is ignored.
|
||||
// 0 when the decoder is unavailable or the scratch buffer cannot be allocated (nothing recorded).
|
||||
int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, const VkAstcRegion *rg, int n, uint32_t bw,
|
||||
uint32_t bh, int srgb, VkImage img, VkImageAspectFlags aspect, VkBuffer readback);
|
||||
uint32_t bh, int srgb, int bc, VkImage img, VkImageAspectFlags aspect, VkBuffer readback);
|
||||
void vk_astc_stats(uint64_t *dispatches, uint64_t *texels); // since start
|
||||
// TOTK_ASTC_TIME=1: GPU timestamps around each vk_astc_record's dispatches (logged per footprint and the largest one)
|
||||
void vk_astc_time_enable(int on);
|
||||
|
||||
+102
-12
@@ -1,6 +1,10 @@
|
||||
// ASTC textures on the GPU: the TOTK_ASTC knob, a compute decoder that turns raw blocks into the RGBA8 image the CPU
|
||||
// decoder (tex_astc.c) would have written, and the native ASTC formats where the device samples them itself.
|
||||
//
|
||||
// TOTK_ASTC_BC = 1 (gpu mode only, default 0): a second pass (runtime/shaders/bc3_encode.comp) encodes the decoded
|
||||
// texels to BC3 and the image is BC3, 8 bits per texel instead of 32. For GPUs with little video memory, at the cost of
|
||||
// BC3's quality: textures look slightly blockier, and data textures (normal maps) lose some precision.
|
||||
//
|
||||
// TOTK_ASTC = auto (default): native when the device reports textureCompressionASTC_LDR and the format's features,
|
||||
// else gpu. gpu: compute decode into RGBA8 (runtime/shaders/astc_decode.comp). native: like auto, but logs when the
|
||||
// device cannot. cpu: astc_decode_ldr_mt on the uploading thread, as before. check: gpu, and every upload is also
|
||||
@@ -17,6 +21,7 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include "astc_decode_spv.h"
|
||||
#include "bc3_encode_spv.h"
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "tex.h"
|
||||
@@ -99,12 +104,14 @@ static struct {
|
||||
VkDescriptorSetLayout dsl;
|
||||
VkPipelineLayout layout;
|
||||
VkPipeline pipe;
|
||||
VkPipeline bc_pipe; // the BC3 encoder (TOTK_ASTC_BC): the decoder's layout, bindings 0 and 1
|
||||
int bc; // 1: ASTC images are BC3 (vk_astc_bc)
|
||||
PFN_vkCmdPushDescriptorSetKHR push;
|
||||
VkBuffer tab; // device-local copy of the tables
|
||||
VmaAllocation tab_alloc;
|
||||
int tab_uploaded;
|
||||
} G = {.mode = -1};
|
||||
static uint64_t n_dispatch, n_texels;
|
||||
static uint64_t n_dispatch, n_texels, n_chunks; // n_chunks: the breadcrumb number of the next scratch chunk
|
||||
|
||||
// TOTK_ASTC_TIME=1 (or vk_astc_time_enable): timestamps around the dispatches of each scratch chunk (the decode
|
||||
// alone) and around a whole vk_astc_record call (one texture upload or WriteTexels: copies and barriers included),
|
||||
@@ -293,6 +300,36 @@ static int create_pipeline(VkCtx *c) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TOTK_ASTC_BC=1: the BC3 encoder, when the device samples BC3 (every desktop GPU does); 0 leaves ASTC as RGBA8.
|
||||
static int create_bc_pipeline(VkCtx *c) {
|
||||
const char *e = cfg_get("TOTK_ASTC_BC");
|
||||
if (!e || !*e || *e == '0') return 0;
|
||||
VkFormatProperties p;
|
||||
vkGetPhysicalDeviceFormatProperties(c->phys, VK_FORMAT_BC3_SRGB_BLOCK, &p);
|
||||
VkFormatFeatureFlags need = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
|
||||
if ((p.optimalTilingFeatures & need) != need) {
|
||||
rt_log("vk: TOTK_ASTC_BC: the device does not sample BC3; ASTC stays RGBA8");
|
||||
return 0;
|
||||
}
|
||||
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof bc3_encode_spv,
|
||||
.pCode = bc3_encode_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.bc_pipe);
|
||||
vkDestroyShaderModule(c->device, m, NULL);
|
||||
if (r != VK_SUCCESS) {
|
||||
G.bc_pipe = VK_NULL_HANDLE;
|
||||
rt_log("vk: TOTK_ASTC_BC: the BC3 encoder pipeline failed (%d); ASTC stays RGBA8", (int)r);
|
||||
return 0;
|
||||
}
|
||||
rt_log("vk: ASTC textures are encoded to BC3 after the decode (TOTK_ASTC_BC)");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int native_device(VkCtx *c) { return c->has_astc_ldr; }
|
||||
|
||||
static const char *mode_name(int m) {
|
||||
@@ -324,6 +361,7 @@ int vk_astc_mode(void) {
|
||||
rt_log("vk: ASTC compute decoder unavailable on %s: ASTC decodes on the CPU", c->props.deviceName);
|
||||
}
|
||||
if (G.failed && m != VK_ASTC_NATIVE) m = VK_ASTC_CPU;
|
||||
G.bc = m == VK_ASTC_GPU && G.pipe && create_bc_pipeline(c);
|
||||
G.mode = m;
|
||||
rt_log("vk: ASTC textures: %s (textureCompressionASTC_LDR %d)", mode_name(m), native_device(c));
|
||||
}
|
||||
@@ -352,6 +390,15 @@ int vk_astc_gpu(void) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
int vk_astc_bc(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c || vk_astc_mode() != VK_ASTC_GPU) return 0;
|
||||
AcquireSRWLockShared(&g_lock);
|
||||
int on = G.dev == c->device && G.bc;
|
||||
ReleaseSRWLockShared(&g_lock);
|
||||
return on;
|
||||
}
|
||||
|
||||
static void mem_barrier(VkCommandBuffer cb, VkPipelineStageFlags2 src, VkAccessFlags2 sa, VkPipelineStageFlags2 dst,
|
||||
VkAccessFlags2 da) {
|
||||
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = src, .srcAccessMask = sa,
|
||||
@@ -368,30 +415,47 @@ static VkDeviceSize region_out(const VkAstcRegion *r) { return (VkDeviceSize)r->
|
||||
static VkDeviceSize region_in(const VkAstcRegion *r, uint32_t bw, uint32_t bh) {
|
||||
return (VkDeviceSize)((r->w + bw - 1) / bw) * ((r->h + bh - 1) / bh) * r->slices * 16;
|
||||
}
|
||||
static VkDeviceSize region_bc(const VkAstcRegion *r) { return region_in(r, 4, 4); } // BC3: 16 bytes per 4x4 block
|
||||
|
||||
typedef struct { uint32_t in_base, out_base, width, height, bx, bslice; } BcPush; // bc3_encode.comp Params
|
||||
|
||||
int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, const VkAstcRegion *rg, int n, uint32_t bw,
|
||||
uint32_t bh, int srgb, VkImage img, VkImageAspectFlags aspect, VkBuffer readback) {
|
||||
static volatile long n_fail;
|
||||
uint32_t bh, int srgb, int bc, VkImage img, VkImageAspectFlags aspect, VkBuffer readback) {
|
||||
static volatile long n_fail, n_unaligned;
|
||||
(void)src_size;
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c || !vk_astc_gpu() || bw < 4 || bh < 4 || bw > 12 || bh > 12) return 0;
|
||||
AcquireSRWLockShared(&g_lock);
|
||||
VkPipeline pipe = G.dev == c->device ? G.pipe : VK_NULL_HANDLE;
|
||||
VkPipeline pipe = G.dev == c->device ? G.pipe : VK_NULL_HANDLE, bc_pipe = G.dev == c->device ? G.bc_pipe : VK_NULL_HANDLE;
|
||||
ReleaseSRWLockShared(&g_lock);
|
||||
if (!pipe) return 0;
|
||||
// chunks of consecutive regions; scratch = the largest chunk's blocks, then its texels
|
||||
VkDeviceSize out_total = 0, cap_in = 0, cap_out = 0, ci = 0, co = 0;
|
||||
if (!pipe || (bc && !bc_pipe)) return 0;
|
||||
if (bc) {
|
||||
readback = VK_NULL_HANDLE;
|
||||
for (int i = 0; i < n; i++) // a BC copy starts on a block corner; a WriteTexels region of a 5x5 or 6x6 footprint may not
|
||||
if ((rg[i].offset.x & 3) || (rg[i].offset.y & 3)) {
|
||||
if (InterlockedIncrement(&n_unaligned) <= 16)
|
||||
rt_log("vk: ASTC to BC3: region at %d,%d is not on a 4x4 block corner: texture left as it was",
|
||||
rg[i].offset.x, rg[i].offset.y);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// chunks of consecutive regions; scratch = the largest chunk's blocks, then its texels, then (bc) its BC3 blocks
|
||||
VkDeviceSize out_total = 0, cap_in = 0, cap_out = 0, cap_bc = 0, ci = 0, co = 0, cb3 = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
VkDeviceSize o = region_out(&rg[i]), b = region_in(&rg[i], bw, bh);
|
||||
if (co && co + o > CHUNK_OUT) ci = co = 0;
|
||||
ci += b, co += o, out_total += o;
|
||||
if (co && co + o > CHUNK_OUT) ci = co = cb3 = 0;
|
||||
ci += b, co += o, cb3 += region_bc(&rg[i]), out_total += o;
|
||||
if (ci > cap_in) cap_in = ci;
|
||||
if (co > cap_out) cap_out = co;
|
||||
if (cb3 > cap_bc) cap_bc = cb3;
|
||||
}
|
||||
if (!out_total) return 1;
|
||||
VkDeviceSize al = c->props.limits.minStorageBufferOffsetAlignment; // the texels bind at cap_in
|
||||
if (al > 1) cap_in = (cap_in + al - 1) / al * al;
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = cap_in + cap_out,
|
||||
VkDeviceSize al = c->props.limits.minStorageBufferOffsetAlignment; // the texels bind at cap_in, the BC3 blocks after them
|
||||
if (al < 16) al = 16;
|
||||
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};
|
||||
@@ -449,6 +513,7 @@ int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, cons
|
||||
bcp[i1 - i0] = (VkBufferCopy){rg[i1].src, in, region_in(&rg[i1], bw, bh)};
|
||||
in += region_in(&rg[i1], bw, bh), out += region_out(&rg[i1]);
|
||||
}
|
||||
vk_crumb(cb, VK_CRUMB_ASTC, ++n_chunks);
|
||||
if (chunks) // the previous chunk's dispatches and image copy are done with the scratch
|
||||
mem_barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT,
|
||||
@@ -484,6 +549,30 @@ int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, cons
|
||||
}
|
||||
if (slot >= 0)
|
||||
vkCmdWriteTimestamp2(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, tpool, 2 * (uint32_t)slot + 1);
|
||||
if (bc) { // the chunk's texels to BC3 blocks behind them; the image copy then takes the blocks
|
||||
mem_barrier(cb, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT);
|
||||
VkDescriptorBufferInfo bdi[2] = {{sb, cap_in, cap_out}, {sb, cap_in + cap_out, cap_bc}};
|
||||
VkWriteDescriptorSet bw2[2];
|
||||
for (int k = 0; k < 2; k++)
|
||||
bw2[k] = (VkWriteDescriptorSet){.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = (uint32_t)k,
|
||||
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.pBufferInfo = &bdi[k]};
|
||||
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, bc_pipe);
|
||||
rec_push(G.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, G.layout, 0, 2, bw2);
|
||||
VkDeviceSize to = 0, bo = 0;
|
||||
for (int i = i0; i < i1; i++) {
|
||||
const VkAstcRegion *r = &rg[i];
|
||||
uint32_t bx4 = (r->w + 3) / 4, by4 = (r->h + 3) / 4;
|
||||
BcPush pc = {.in_base = (uint32_t)(to / 4), .out_base = (uint32_t)(bo / 16), .width = r->w, .height = r->h,
|
||||
.bx = bx4, .bslice = bx4 * by4};
|
||||
vkCmdPushConstants(cb, G.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
|
||||
vkCmdDispatch(cb, (bx4 + 7) / 8, (by4 + 7) / 8, r->slices);
|
||||
VkBufferImageCopy *cp = &copies[i - i0];
|
||||
cp->bufferOffset = cap_in + cap_out + bo, cp->bufferRowLength = bx4 * 4, cp->bufferImageHeight = by4 * 4;
|
||||
to += region_out(r), bo += region_bc(r);
|
||||
}
|
||||
}
|
||||
mem_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);
|
||||
vkCmdCopyBufferToImage(cb, sb, img, VK_IMAGE_LAYOUT_GENERAL, (uint32_t)(i1 - i0), copies);
|
||||
@@ -524,6 +613,7 @@ void vk_astc_release(void) {
|
||||
AcquireSRWLockExclusive(&g_lock);
|
||||
if (c && G.dev == c->device) {
|
||||
if (G.pipe) vkDestroyPipeline(c->device, G.pipe, NULL);
|
||||
if (G.bc_pipe) vkDestroyPipeline(c->device, G.bc_pipe, NULL);
|
||||
if (G.layout) vkDestroyPipelineLayout(c->device, G.layout, NULL);
|
||||
if (G.dsl) vkDestroyDescriptorSetLayout(c->device, G.dsl, NULL);
|
||||
if (G.tab) vmaDestroyBuffer(c->vma, G.tab, G.tab_alloc);
|
||||
|
||||
+20
-4
@@ -6,8 +6,8 @@
|
||||
// - minStorageBufferOffsetAlignment above 4 (TOTK_VK_SSBO_ALIGN=<power of two> raises it): the translator computes
|
||||
// storage offsets from the guest address rounded down to that alignment (Ryujinx's unaligned storage path), and
|
||||
// vk_exec_draw.c ssbo_info binds the rounded-down address, so shader writes reach guest memory instead of a copy.
|
||||
// - no graphics pipeline library with fast linking (TOTK_GPL=0 forces it): vk_caps.gpl = 0, the extension stays
|
||||
// disabled, and pipelines are built whole.
|
||||
// - no graphics pipeline library with fast linking (TOTK_GPL=0 forces it; the default on AMD's RDNA 1/2 driver):
|
||||
// vk_caps.gpl = 0, the extension stays disabled, and pipelines are built whole.
|
||||
// - no 32-lane pin (no subgroupSizeControl, 32 outside min..max, or TOTK_VK_SUBGROUP_PIN=0): stages run at the
|
||||
// device's subgroupSize (SPIR-V 1.3 without a required size) and the translator is told that size.
|
||||
// TOTK_VK_SUBGROUP=<n> pins n instead of 32 where the driver allows it.
|
||||
@@ -36,11 +36,25 @@ void vk_caps_features(VkPhysicalDeviceFeatures *f) {
|
||||
vk_caps.float64 = f->shaderFloat64;
|
||||
}
|
||||
|
||||
void vk_caps_driver(VkPhysicalDevice p) {
|
||||
VkPhysicalDeviceDriverProperties drv = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
|
||||
VkPhysicalDeviceProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &drv};
|
||||
vkGetPhysicalDeviceProperties2(p, &p2);
|
||||
vk_caps.amd = p2.properties.vendorID == 0x1002;
|
||||
// AMD split RDNA 1 and 2 off into a driver branch of their own; its driverInfo reads
|
||||
// "26.10.1 (AMD proprietary shader compiler)" where the RDNA 3 and 4 driver names LLPC.
|
||||
vk_caps.amd_sc = drv.driverID == VK_DRIVER_ID_AMD_PROPRIETARY && strstr(drv.driverInfo, "shader compiler");
|
||||
}
|
||||
|
||||
// TOTK_GPL=0/1 forces graphics pipeline libraries off or on. Default off on AMD's RDNA 1/2 driver branch: every boot
|
||||
// that lost the device there had a cold pipeline cache and draws waiting on fast-linked pipelines, and a run with GPL
|
||||
// off booted (issue #5); Savage Wind Recomp turns GPL off on RDNA 4 under RADV for a GPU hang at the title screen.
|
||||
int vk_caps_gpl(const VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT *f,
|
||||
const VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT *p) {
|
||||
gpl_lib = f->graphicsPipelineLibrary;
|
||||
gpl_fast = p->graphicsPipelineLibraryFastLinking;
|
||||
vk_caps.gpl = gpl_lib && gpl_fast && cfg_bool("TOTK_GPL");
|
||||
const char *e = cfg_get("TOTK_GPL"); // no fixed default: it depends on the driver, see above
|
||||
vk_caps.gpl = gpl_lib && gpl_fast && (e && *e ? strcmp(e, "0") != 0 : !vk_caps.amd_sc);
|
||||
return vk_caps.gpl;
|
||||
}
|
||||
|
||||
@@ -117,8 +131,10 @@ void vk_caps_init(VkCtx *c) {
|
||||
has_ext(ep, ne, "VK_EXT_graphics_pipeline_library"), depth_ok(p, VK_FORMAT_D16_UNORM),
|
||||
depth_ok(p, VK_FORMAT_D24_UNORM_S8_UINT), depth_ok(p, VK_FORMAT_X8_D24_UNORM_PACK32), depth_ok(p, VK_FORMAT_D32_SFLOAT),
|
||||
depth_ok(p, VK_FORMAT_D32_SFLOAT_S8_UINT));
|
||||
const char *ge = cfg_get("TOTK_GPL");
|
||||
rt_log("vk caps: gpl %s fast-link %s (graphicsPipelineLibrary %u%s)", vk_caps.gpl ? "yes" : "no",
|
||||
gpl_fast ? "yes" : "no", (unsigned)gpl_lib, gpl_lib && gpl_fast && !vk_caps.gpl ? ", off by TOTK_GPL=0" : "");
|
||||
gpl_fast ? "yes" : "no", (unsigned)gpl_lib,
|
||||
!gpl_lib || !gpl_fast || vk_caps.gpl ? "" : ge && *ge ? ", off by TOTK_GPL=0" : ", off on AMD's RDNA 1/2 driver");
|
||||
rt_log("vk caps: fallbacks: float64 %s, storage buffers %s %u, subgroup pin %u on stages %#x, translator subgroup vs/fs/cs %u/%u/%u",
|
||||
vk_caps.float64 ? "native" : "softfloat64", vk_caps.sbuf_align ? "rounded down to" : "exact, alignment",
|
||||
(unsigned)l->minStorageBufferOffsetAlignment, want, (unsigned)vk_caps.sg_pin_stages, told[0], told[1], told[2]);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// GPU breadcrumbs for a lost device (vk.h vk_crumb). VK_AMD_buffer_marker writes a 32-bit code into a buffer from
|
||||
// the command stream: at TOP_OF_PIPE once the command processor reaches the marker, at BOTTOM_OF_PIPE once every
|
||||
// command before it has finished. Each crumb writes both, into two words of one host-coherent buffer, so after a
|
||||
// device loss word 0 names the newest point the GPU reached and word 1 the newest point it had finished: the hang lies
|
||||
// between them. A code is the crumb kind in the top 4 bits and its number in the low 28. The markers cost two small
|
||||
// writes per crumb, and crumbs mark submissions, upload batches, ASTC chunks and presents, not draws.
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
|
||||
static PFN_vkCmdWriteBufferMarkerAMD write_marker;
|
||||
static VkBuffer buf;
|
||||
static VmaAllocation alloc;
|
||||
static volatile uint32_t *words;
|
||||
|
||||
void vk_crumbs_init(VkCtx *c) {
|
||||
if (!c->has_buffer_marker) return;
|
||||
write_marker = (PFN_vkCmdWriteBufferMarkerAMD)vkGetDeviceProcAddr(c->device, "vkCmdWriteBufferMarkerAMD");
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = 16,
|
||||
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
|
||||
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
|
||||
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
|
||||
VmaAllocationInfo info;
|
||||
if (!write_marker || vmaCreateBuffer(c->vma, &bi, &ai, &buf, &alloc, &info) != VK_SUCCESS) {
|
||||
rt_log("vk: GPU breadcrumbs unavailable");
|
||||
write_marker = NULL;
|
||||
return;
|
||||
}
|
||||
words = info.pMappedData;
|
||||
words[0] = words[1] = 0;
|
||||
rt_log("vk: GPU breadcrumbs on (VK_AMD_buffer_marker)");
|
||||
}
|
||||
|
||||
void vk_crumbs_release(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (buf && c) vmaDestroyBuffer(c->vma, buf, alloc);
|
||||
buf = VK_NULL_HANDLE, alloc = NULL, words = NULL, write_marker = NULL;
|
||||
}
|
||||
|
||||
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n) {
|
||||
if (!write_marker || !cb) return;
|
||||
uint32_t code = (uint32_t)kind << 28 | (uint32_t)(n & 0x0fffffff);
|
||||
write_marker(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, buf, 0, code);
|
||||
write_marker(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, buf, 4, code);
|
||||
}
|
||||
|
||||
static void describe(uint32_t code, char *o, size_t size) {
|
||||
static const char *const kinds[] = {"nothing", "the start of a part of submission", "the end of submission",
|
||||
"the start of upload batch", "the end of upload batch",
|
||||
"ASTC decode chunk", "the start of present",
|
||||
"the end of present"};
|
||||
unsigned k = code >> 28;
|
||||
if (!code) snprintf(o, size, "nothing yet");
|
||||
else if (k < sizeof kinds / sizeof *kinds) snprintf(o, size, "%s %u", kinds[k], code & 0x0fffffff);
|
||||
else snprintf(o, size, "unknown code %#x", code);
|
||||
}
|
||||
|
||||
void vk_crumbs_report(void) {
|
||||
if (!words) return;
|
||||
char reached[96], finished[96];
|
||||
describe(words[0], reached, sizeof reached);
|
||||
describe(words[1], finished, sizeof finished);
|
||||
rt_log("vk: GPU breadcrumbs: the GPU last reached %s; everything before %s had finished", reached, finished);
|
||||
}
|
||||
@@ -233,6 +233,8 @@ static int create_device(void) {
|
||||
CHAIN(lr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR);
|
||||
}
|
||||
if (has_ext(ep, ne, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { g_vk.has_memory_budget = 1; ext[n_ext++] = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }
|
||||
// GPU breadcrumbs for a lost device (vk_crumbs.c): AMD's drivers and RADV report it.
|
||||
if (has_ext(ep, ne, VK_AMD_BUFFER_MARKER_EXTENSION_NAME)) { g_vk.has_buffer_marker = 1; ext[n_ext++] = VK_AMD_BUFFER_MARKER_EXTENSION_NAME; }
|
||||
// Graphics pipeline library: chained last so it can be dropped again when vk_caps_gpl declines it.
|
||||
void **gpl_link = NULL;
|
||||
if (has_ext(ep, ne, VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME) && has_ext(ep, ne, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME)) {
|
||||
@@ -246,6 +248,7 @@ static int create_device(void) {
|
||||
// The driver fills the chain with what it supports; the same chain then enables all of it (robustBufferAccess
|
||||
// stays on: guest shaders index constant buffers freely and out-of-range reads must stay harmless).
|
||||
vkGetPhysicalDeviceFeatures2(g_vk.phys, &c->f2);
|
||||
vk_caps_driver(g_vk.phys);
|
||||
vk_caps_features(&c->f2.features);
|
||||
if (gpl_link) {
|
||||
VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT gp = {
|
||||
@@ -267,6 +270,7 @@ static int create_device(void) {
|
||||
else if (!h->geometryShader) miss = "geometryShader";
|
||||
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
|
||||
g_vk.has_buffer_device_address = c->f12.bufferDeviceAddress;
|
||||
g_vk.has_astc_ldr = h->textureCompressionASTC_LDR;
|
||||
{
|
||||
@@ -319,6 +323,7 @@ static int create_device(void) {
|
||||
rt_log("vk: timeline semaphore creation failed (%d)", (int)r);
|
||||
return 0;
|
||||
}
|
||||
vk_crumbs_init(&g_vk);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -328,6 +333,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_crumbs_release();
|
||||
if (g_vk.timeline) vkDestroySemaphore(g_vk.device, g_vk.timeline, NULL);
|
||||
vk_headroom_stop();
|
||||
if (g_vk.vma) vmaDestroyAllocator(g_vk.vma);
|
||||
@@ -364,6 +370,65 @@ void vk_shutdown(void) {
|
||||
|
||||
VkCtx *vk_ctx(void) { return g_ready ? &g_vk : NULL; }
|
||||
|
||||
static const char *fault_address_type(VkDeviceFaultAddressTypeEXT t) {
|
||||
switch (t) {
|
||||
case VK_DEVICE_FAULT_ADDRESS_TYPE_NONE_EXT: return "none";
|
||||
case VK_DEVICE_FAULT_ADDRESS_TYPE_READ_INVALID_EXT: return "read invalid";
|
||||
case VK_DEVICE_FAULT_ADDRESS_TYPE_WRITE_INVALID_EXT: return "write invalid";
|
||||
case VK_DEVICE_FAULT_ADDRESS_TYPE_EXECUTE_INVALID_EXT: return "execute invalid";
|
||||
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_UNKNOWN_EXT: return "instruction pointer (unknown)";
|
||||
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_INVALID_EXT: return "instruction pointer (invalid)";
|
||||
case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_FAULT_EXT: return "instruction pointer (fault)";
|
||||
default: return "unknown type";
|
||||
}
|
||||
}
|
||||
|
||||
// The query runs once per process. Threads that hit the loss at the same time wait on the lock until it is logged, so
|
||||
// none of them reaches its rt_fatal and ends the process before the report is written.
|
||||
void vk_device_fault_report(VkResult r) {
|
||||
static SRWLOCK lock = SRWLOCK_INIT;
|
||||
static int done;
|
||||
if (r != VK_ERROR_DEVICE_LOST) return;
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
if (done) { ReleaseSRWLockExclusive(&lock); return; }
|
||||
done = 1;
|
||||
vk_crumbs_report();
|
||||
VkCtx *c = vk_ctx();
|
||||
PFN_vkGetDeviceFaultInfoEXT get =
|
||||
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
|
||||
: NULL;
|
||||
if (!get) {
|
||||
rt_log("vk: device lost; no fault report (VK_EXT_device_fault %s)", c && c->has_device_fault ? "has no entry point"
|
||||
: "not enabled");
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
return;
|
||||
}
|
||||
VkDeviceFaultCountsEXT n = {.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT};
|
||||
VkResult q = get(c->device, &n, NULL);
|
||||
VkDeviceFaultAddressInfoEXT *ai = calloc(n.addressInfoCount + 1, sizeof *ai);
|
||||
VkDeviceFaultVendorInfoEXT *vi = calloc(n.vendorInfoCount + 1, sizeof *vi);
|
||||
VkDeviceFaultInfoEXT info = {.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT, .pAddressInfos = ai, .pVendorInfos = vi};
|
||||
uint64_t binary = n.vendorBinarySize;
|
||||
n.vendorBinarySize = 0; // the vendor binary is not fetched; its size alone is logged
|
||||
if (q == VK_SUCCESS && ai && vi) q = get(c->device, &n, &info);
|
||||
if (q != VK_SUCCESS && q != VK_INCOMPLETE) {
|
||||
rt_log("vk: device lost; vkGetDeviceFaultInfoEXT failed (%d)", (int)q);
|
||||
} else {
|
||||
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++)
|
||||
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);
|
||||
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);
|
||||
}
|
||||
free(ai);
|
||||
free(vi);
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
}
|
||||
|
||||
int vk_selftest(void) {
|
||||
if (!vk_init()) return 1;
|
||||
VkCtx *c = &g_vk;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "host.h"
|
||||
#include "nvn_api.h"
|
||||
#include "nvn_tex.h"
|
||||
#include "platform/platform.h"
|
||||
#include "tex.h"
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
@@ -141,11 +142,163 @@ static int view_format(const tex_t *t, const view_t *v, int blocks, VkFormat *ou
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- 3D BC render targets
|
||||
// A 3D BC texture has no block views (vk_texture.c create_tex, TOTK_BLOCK_VIEWS_3D): on RDNA2 cards such an image
|
||||
// made BLOCK_TEXEL_VIEW_COMPATIBLE and 2D_ARRAY_COMPATIBLE took GPU writes past the end of its memory (issue #5). Draws
|
||||
// into it through an uncompressed view of its blocks (agl's BC4 encoder fills 32x32x32 and 64x64x64 volumes and their
|
||||
// levels at the logo) go to a stand-in: a 2D array of the blocks of one level in the view's format, one layer per
|
||||
// depth plane. It starts with the level's content, so a pass that writes part of a plane keeps the rest, and each pass
|
||||
// end copies the planes it drew into the level (a copy between a compressed and a size-compatible uncompressed format).
|
||||
typedef struct { VkImage bc, img; VmaAllocation alloc; VkFormat fmt; uint32_t level, w, h, d; } BcStandIn;
|
||||
#define BC_STANDINS 64 // one per level drawn: 6 volumes of up to 7 levels at the logo
|
||||
static BcStandIn bcs[BC_STANDINS];
|
||||
static int nbcs;
|
||||
static PlatMutex bcs_lock = PLAT_MUTEX_INIT; // the replay adds and reads; destroy_tex (any thread) removes
|
||||
|
||||
static void image_barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, uint32_t layers) {
|
||||
VkImageMemoryBarrier2 ib = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_NONE,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
|
||||
.oldLayout = from, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = img, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layers}};
|
||||
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &ib};
|
||||
vkCmdPipelineBarrier2(cb, &di);
|
||||
}
|
||||
|
||||
static void memory_barrier(VkCommandBuffer cb, VkPipelineStageFlags2 src, VkAccessFlags2 sa, VkPipelineStageFlags2 dst,
|
||||
VkAccessFlags2 da) {
|
||||
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = src, .srcAccessMask = sa,
|
||||
.dstStageMask = dst, .dstAccessMask = da};
|
||||
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
|
||||
vkCmdPipelineBarrier2(cb, &di);
|
||||
}
|
||||
|
||||
// The stand-in of level level of a 3D BC image for draws through view format fmt, made on first use; 0 when none can
|
||||
// be made (logged). Replay thread.
|
||||
static int bc_standin(const VkTexImage *img, uint32_t level, VkFormat fmt, BcStandIn *out) {
|
||||
static int warned;
|
||||
plat_mutex_lock(&bcs_lock);
|
||||
int full = nbcs == BC_STANDINS;
|
||||
for (int i = 0; i < nbcs; i++)
|
||||
if (bcs[i].bc == img->image && bcs[i].level == level && bcs[i].fmt == fmt) {
|
||||
*out = bcs[i];
|
||||
plat_mutex_unlock(&bcs_lock);
|
||||
return 1;
|
||||
}
|
||||
plat_mutex_unlock(&bcs_lock);
|
||||
if (full) {
|
||||
if (!warned++) rt_log("vk: more than %d 3D BC render targets: the others are left out", BC_STANDINS);
|
||||
return 0;
|
||||
}
|
||||
uint32_t lw = level_extent(img->width, level), lh = level_extent(img->height, level);
|
||||
BcStandIn s = {img->image, VK_NULL_HANDLE, NULL, fmt, level, (lw + 3) / 4, (lh + 3) / 4,
|
||||
level_extent(img->depth, level)};
|
||||
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = fmt,
|
||||
.extent = {s.w, s.h, 1}, .mipLevels = 1, .arrayLayers = s.d, .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
if (vmaCreateImage(vk_ctx()->vma, &ci, &ai, &s.img, &s.alloc, NULL) != VK_SUCCESS) {
|
||||
if (!warned++) rt_log("vk: stand-in for a 3D BC render target could not be created: left out");
|
||||
return 0;
|
||||
}
|
||||
// In the upload batch, which goes to the queue before this part: GENERAL, then the texture's blocks.
|
||||
VkCommandBuffer cb = vk_upload_begin();
|
||||
image_barrier(cb, s.img, VK_IMAGE_LAYOUT_UNDEFINED, s.d);
|
||||
memory_barrier(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
VkImageCopy rg = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1},
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, s.d},
|
||||
.extent = {lw, lh, s.d}}; // texels of the compressed source
|
||||
vkCmdCopyImage(cb, img->image, VK_IMAGE_LAYOUT_GENERAL, s.img, VK_IMAGE_LAYOUT_GENERAL, 1, &rg);
|
||||
memory_barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
|
||||
vk_upload_end();
|
||||
plat_mutex_lock(&bcs_lock);
|
||||
bcs[nbcs++] = s;
|
||||
plat_mutex_unlock(&bcs_lock);
|
||||
rt_log("vk: 3D BC render target %ux%ux%u level %u drawn through a %ux%u x %u stand-in (format %d)", img->width,
|
||||
img->height, img->depth, level, s.w, s.h, s.d, (int)fmt);
|
||||
*out = s;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// vk_exec_draw.c vk_exec_image_gone: the stand-in of a BC image that goes, freed once the GPU is done with it.
|
||||
void vk_exec_bc_gone(VkImage image) {
|
||||
plat_mutex_lock(&bcs_lock);
|
||||
for (int i = 0; i < nbcs; i++)
|
||||
if (bcs[i].bc == image) {
|
||||
vk_defer_image(bcs[i].img, bcs[i].alloc);
|
||||
bcs[i--] = bcs[--nbcs];
|
||||
}
|
||||
plat_mutex_unlock(&bcs_lock);
|
||||
}
|
||||
|
||||
// At a pass end: the planes the pass drew into a stand-in go to its texture.
|
||||
static void bc_copy_back(VkExec *ex, const VkExecTarget *t) {
|
||||
plat_mutex_lock(&bcs_lock);
|
||||
for (int i = 0; i < nbcs; i++) {
|
||||
if (bcs[i].img != t->image) continue;
|
||||
memory_barrier(ex->cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
VkImageCopy rg = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, t->base_layer, t->layers},
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, bcs[i].level, 0, 1},
|
||||
.dstOffset = {0, 0, (int32_t)t->base_layer},
|
||||
.extent = {bcs[i].w, bcs[i].h, t->layers}}; // texels of the uncompressed source
|
||||
vkCmdCopyImage(ex->cb, t->image, VK_IMAGE_LAYOUT_GENERAL, bcs[i].bc, VK_IMAGE_LAYOUT_GENERAL, 1, &rg);
|
||||
memory_barrier(ex->cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
|
||||
ex->recorded++;
|
||||
break;
|
||||
}
|
||||
plat_mutex_unlock(&bcs_lock);
|
||||
}
|
||||
|
||||
// A draw into a 3D BC image without block views through an uncompressed view of its blocks: the target is the
|
||||
// image's stand-in (bc_standin). 1 when o is filled (target or left out), 0 for any other target.
|
||||
static int bc_target(const tex_t *t, const view_t *v, int has_v, const VkTexImage *img, VkExecTarget *o) {
|
||||
int bw, bh;
|
||||
if (img->type != VK_IMAGE_TYPE_3D || img->block_views || !nvn_fmt_bpb(t->d.format, &bw, &bh) || bw != 4 || bh != 4)
|
||||
return 0;
|
||||
VkFormat fmt;
|
||||
BcStandIn s;
|
||||
uint32_t level = has_v && v->set_levels ? v->base_level : 0;
|
||||
if (level >= img->levels) {
|
||||
warn_once(W_LEVEL, "render target level %u of a %u-level texture: left out", level, img->levels);
|
||||
return 1;
|
||||
}
|
||||
if (!has_v || !v->set_format || !view_format(t, v, 1, &fmt) || !bc_standin(img, level, fmt, &s)) {
|
||||
warn_once(W_BLOCK_VIEW, "render target format %#x (%ux%u) through view format %#x: no block view, left out",
|
||||
t->d.format, t->d.width, t->d.height, has_v && v->set_format ? v->format : 0);
|
||||
return 1;
|
||||
}
|
||||
uint32_t base = 0, layers = s.d;
|
||||
if (v->set_layers) {
|
||||
base = v->min_layer;
|
||||
layers = v->nlayers ? v->nlayers : (base < s.d ? s.d - base : 0);
|
||||
}
|
||||
if (base >= s.d || !layers) return 1;
|
||||
if (base + layers > s.d) layers = s.d - base;
|
||||
o->image = s.img; // not img->image: view_of makes this command buffer's view of the stand-in
|
||||
o->img = img;
|
||||
o->format = fmt;
|
||||
o->view_type = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
|
||||
o->aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
o->level = 0, o->base_layer = base, o->layers = layers;
|
||||
o->width = s.w, o->height = s.h, o->samples = 1;
|
||||
o->nvn_format = v->format;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void resolve_target(const tex_t *t, const view_t *v, int has_v, int depth_slot, VkExecTarget *o) {
|
||||
memset(o, 0, sizeof *o);
|
||||
o->tex = t;
|
||||
if (!t) return;
|
||||
const VkTexImage *img = vk_texture_get(t);
|
||||
if (img && !depth_slot && bc_target(t, v, has_v, img, o)) return; // a 3D BC image: its stand-in
|
||||
VkImageUsageFlags need = depth_slot ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
int is3d = img && img->type == VK_IMAGE_TYPE_3D && !depth_slot; // slices drawn through 2D views (2D_ARRAY_COMPATIBLE)
|
||||
if (!img || !(img->usage & need) || (img->type != VK_IMAGE_TYPE_2D && !is3d)) {
|
||||
@@ -469,6 +622,9 @@ void vk_exec_pass_end(VkExec *ex) {
|
||||
vkCmdEndRendering(ex->cb);
|
||||
ex->recorded++;
|
||||
ex->in_pass = 0;
|
||||
for (int i = 0; i < ex->pass_ncolor && i < 8; i++) // stand-ins of 3D BC targets go to their textures
|
||||
if (ex->color[i].image && ex->color[i].img && ex->color[i].img->image != ex->color[i].image)
|
||||
bc_copy_back(ex, &ex->color[i]);
|
||||
if (vk_dump_armed) vk_dump_pass_end(ex);
|
||||
uint64_t stamp = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1;
|
||||
for (int i = 0; i < 8; i++)
|
||||
|
||||
@@ -124,6 +124,8 @@ void vk_exec_inline_end(int reopen);
|
||||
// buffers): the replay continues in a new one (vk_sync_split).
|
||||
void vk_exec_draw_new_cb(void);
|
||||
VkBuffer vk_exec_inline_buffer(VkDeviceSize size);
|
||||
// vk_exec.c: the stand-in a 3D BC render target is drawn through goes with its image (vk_exec_image_gone).
|
||||
void vk_exec_bc_gone(VkImage image);
|
||||
// Scissor 0 clipped to the render area of the open pass; extent 0: nothing to draw.
|
||||
VkRect2D vk_exec_scissor(const VkExec *ex);
|
||||
void vk_exec_noatt_dims(const VkExec *ex, uint32_t *w, uint32_t *h); // pass size when no attachment is set
|
||||
|
||||
+183
-2
@@ -366,10 +366,14 @@ static void dummies_init(void) {
|
||||
Z.ready = 1;
|
||||
}
|
||||
|
||||
// Releases the stand-ins. The device must be idle; the game path leaves them to process exit like the pipelines.
|
||||
static void twins_release(VkCtx *c);
|
||||
|
||||
// Releases the stand-ins and the depth twins. The device must be idle; the game path leaves them to process exit like
|
||||
// the pipelines.
|
||||
void vk_exec_draw_shutdown(void) {
|
||||
if (!Z.ready) return;
|
||||
VkCtx *c = vk_ctx();
|
||||
twins_release(c);
|
||||
for (int i = 0; i < DV_COUNT; i++) {
|
||||
if (Z.img[i].sview != Z.img[i].view) vkDestroyImageView(c->device, Z.img[i].sview, NULL);
|
||||
vkDestroyImageView(c->device, Z.img[i].view, NULL);
|
||||
@@ -560,6 +564,7 @@ VkImageView vk_exec_target_view(const VkTexImage *img, VkFormat format, VkImageV
|
||||
}
|
||||
|
||||
void vk_exec_image_gone(VkImage image) {
|
||||
vk_exec_bc_gone(image);
|
||||
if (!views_by_image.klen) return;
|
||||
for (ViewNode *n = nvn_hm_del(&views_by_image, &image), *next; n; n = next) {
|
||||
next = n->next;
|
||||
@@ -1146,7 +1151,8 @@ static struct {
|
||||
VkDescriptorImageInfo out;
|
||||
} TD[TD_N];
|
||||
|
||||
static void tex_info(VkExec *ex, const GfxShaderRes *r, uint64_t h, int storage, int separate, VkDescriptorImageInfo *out) {
|
||||
static void tex_info_lookup(VkExec *ex, const GfxShaderRes *r, uint64_t h, int storage, int separate,
|
||||
VkDescriptorImageInfo *out) {
|
||||
int td_on = td_mode();
|
||||
VkImage image;
|
||||
if (!h || storage || !td_on) return tex_info_full(ex, r, h, storage, separate, out, &image);
|
||||
@@ -1207,6 +1213,177 @@ static void tex_info_full(VkExec *ex, const GfxShaderRes *r, uint64_t h, int sto
|
||||
*out = (VkDescriptorImageInfo){separate || storage ? VK_NULL_HANDLE : samp, view, layout};
|
||||
}
|
||||
|
||||
// ---- depth twins: colour images under a compare sampler in a compute shader
|
||||
// TotK's compute passes sample an R32_SFLOAT texture through a shadow sampler. Maxwell compares any single-channel
|
||||
// float or unorm texel; Vulkan only formats with SAMPLED_IMAGE_DEPTH_COMPARISON (VUID-vkCmdDispatch-None-06479), which
|
||||
// AMD's drivers do not give R32_SFLOAT, so the result there is undefined. Such an image gets a depth twin of the same
|
||||
// size and bits (R32_SFLOAT -> D32_SFLOAT, R16_UNORM -> D16_UNORM), copied through a buffer right before each dispatch
|
||||
// that samples it (a dispatch is outside any render pass), and the dispatch samples the twin. Draws keep the colour
|
||||
// image: a copy cannot go inside their pass. Design: Savage Wind Recomp runtime/nvn/vulkan/exec.c depth_twin_update
|
||||
// (https://github.com/Revivedx/SavageWindRecomp, MIT); the code is ours. TOTK_DEPTH_TWIN=0: off.
|
||||
#define TWINS 8
|
||||
typedef struct { VkImage src, img; VmaAllocation mem; VkImageView view[2]; uint32_t w, h, layers; VkFormat fmt; } Twin;
|
||||
static Twin twins[TWINS];
|
||||
static unsigned twin_next;
|
||||
static int twin_dispatch; // op_dispatch*: the resources being bound are a dispatch's
|
||||
static VkBuffer twin_buf;
|
||||
static VmaAllocation twin_buf_mem;
|
||||
static VkDeviceSize twin_buf_cap;
|
||||
|
||||
static void twin_free(void *arg) { // completion thread, after the last submission that may sample it
|
||||
Twin *t = arg;
|
||||
VkCtx *c = vk_ctx();
|
||||
for (int k = 0; k < 2; k++)
|
||||
if (t->view[k]) vkDestroyImageView(c->device, t->view[k], NULL);
|
||||
vmaDestroyImage(c->vma, t->img, t->mem);
|
||||
free(t);
|
||||
}
|
||||
|
||||
typedef struct { VkBuffer buf; VmaAllocation mem; } TwinBuf;
|
||||
static void twin_buf_free(void *arg) { // completion thread, after the last submission that may copy through it
|
||||
TwinBuf *b = arg;
|
||||
vmaDestroyBuffer(vk_ctx()->vma, b->buf, b->mem);
|
||||
free(b);
|
||||
}
|
||||
|
||||
static VkFormat twin_format(const VkTexImage *img) {
|
||||
static int on = -1;
|
||||
if (on < 0) on = rt_knob("DEPTH_TWIN", "1");
|
||||
if (!on || (img->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || img->samples > 1 || img->depth > 1) return VK_FORMAT_UNDEFINED;
|
||||
VkFormat df = img->format == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT
|
||||
: img->format == VK_FORMAT_R16_UNORM ? VK_FORMAT_D16_UNORM : VK_FORMAT_UNDEFINED;
|
||||
if (!df) return df;
|
||||
static int compares[2] = {-1, -1}; // per source format: the driver compares it as it is
|
||||
int i = df == VK_FORMAT_D16_UNORM;
|
||||
if (compares[i] < 0) {
|
||||
VkFormatProperties3 p3 = {.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3};
|
||||
VkFormatProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2, .pNext = &p3};
|
||||
vkGetPhysicalDeviceFormatProperties2(vk_ctx()->phys, img->format, &p2);
|
||||
compares[i] = (p3.optimalTilingFeatures & VK_FORMAT_FEATURE_2_SAMPLED_IMAGE_DEPTH_COMPARISON_BIT) != 0;
|
||||
rt_log("vk_exec: %s under a compare sampler: %s", i ? "R16_UNORM" : "R32_SFLOAT",
|
||||
compares[i] ? "compared by the driver" : "sampled through a depth twin in compute passes");
|
||||
}
|
||||
return compares[i] ? VK_FORMAT_UNDEFINED : df;
|
||||
}
|
||||
|
||||
static void twins_release(VkCtx *c) {
|
||||
for (int i = 0; i < TWINS; i++) {
|
||||
for (int k = 0; k < 2; k++)
|
||||
if (twins[i].view[k]) vkDestroyImageView(c->device, twins[i].view[k], NULL);
|
||||
if (twins[i].img) vmaDestroyImage(c->vma, twins[i].img, twins[i].mem);
|
||||
}
|
||||
memset(twins, 0, sizeof twins);
|
||||
if (twin_buf) vmaDestroyBuffer(c->vma, twin_buf, twin_buf_mem);
|
||||
twin_buf = VK_NULL_HANDLE, twin_buf_mem = NULL, twin_buf_cap = 0;
|
||||
}
|
||||
|
||||
// The twin of img holding img's current texels, the copies recorded into ex->cb; NULL when img needs none or it cannot
|
||||
// be made.
|
||||
static Twin *twin_update(VkExec *ex, const VkTexImage *img) {
|
||||
VkFormat df = twin_format(img);
|
||||
if (!df) return NULL;
|
||||
VkCtx *c = vk_ctx();
|
||||
uint32_t bpp = df == VK_FORMAT_D32_SFLOAT ? 4 : 2, layers = img->layers ? img->layers : 1;
|
||||
VkDeviceSize bytes = (VkDeviceSize)img->width * img->height * layers * bpp;
|
||||
if (bytes > (64u << 20)) return NULL;
|
||||
if (bytes > twin_buf_cap) { // one copy buffer, grown; the old one goes after the submission being recorded
|
||||
VkDeviceSize want = twin_buf_cap ? twin_buf_cap * 2 : 1u << 16;
|
||||
while (want < bytes) want *= 2;
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = want,
|
||||
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
VkBuffer nb;
|
||||
VmaAllocation nm;
|
||||
if (vmaCreateBuffer(c->vma, &bi, &ai, &nb, &nm, NULL) != VK_SUCCESS) return NULL;
|
||||
if (twin_buf) {
|
||||
TwinBuf *old = malloc(sizeof *old);
|
||||
if (!old) rt_fatal("vk: out of memory (depth twin)");
|
||||
*old = (TwinBuf){twin_buf, twin_buf_mem};
|
||||
vk_completion_add(vk_sync_issued() + 1, twin_buf_free, old);
|
||||
}
|
||||
twin_buf = nb, twin_buf_mem = nm, twin_buf_cap = want;
|
||||
}
|
||||
Twin *t = NULL;
|
||||
for (int i = 0; i < TWINS && !t; i++)
|
||||
if (twins[i].img && twins[i].src == img->image && twins[i].w == img->width && twins[i].h == img->height &&
|
||||
twins[i].layers == layers && twins[i].fmt == df)
|
||||
t = &twins[i];
|
||||
if (!t) {
|
||||
t = &twins[twin_next++ % TWINS];
|
||||
if (t->img) { // the submission being recorded may still sample the replaced twin
|
||||
Twin *old = malloc(sizeof *old);
|
||||
if (!old) rt_fatal("vk: out of memory (depth twin)");
|
||||
*old = *t;
|
||||
vk_completion_add(vk_sync_issued() + 1, twin_free, old);
|
||||
}
|
||||
*t = (Twin){.src = img->image, .w = img->width, .h = img->height, .layers = layers, .fmt = df};
|
||||
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = df,
|
||||
.extent = {img->width, img->height, 1}, .mipLevels = 1, .arrayLayers = layers,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
if (vmaCreateImage(c->vma, &ci, &ai, &t->img, &t->mem, NULL) != VK_SUCCESS) {
|
||||
*t = (Twin){0};
|
||||
return NULL;
|
||||
}
|
||||
for (int k = 0; k < 2; k++) {
|
||||
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = t->img,
|
||||
.viewType = k ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, .format = df,
|
||||
.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, k ? layers : 1}};
|
||||
if (vkCreateImageView(c->device, &vi, NULL, &t->view[k]) != VK_SUCCESS) t->view[k] = VK_NULL_HANDLE;
|
||||
}
|
||||
static int nlog;
|
||||
if (nlog++ < 8)
|
||||
rt_log("vk_exec: depth twin %ux%ux%u %s for a compute pass's compare sampler", img->width, img->height,
|
||||
layers, df == VK_FORMAT_D32_SFLOAT ? "D32_SFLOAT" : "D16_UNORM");
|
||||
}
|
||||
// The source's earlier writes and the twin's earlier readers before the copies; the twin's old texels are dropped.
|
||||
VkMemoryBarrier2 m0 = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT};
|
||||
VkImageMemoryBarrier2 ib = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, .dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = t->img, .subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, layers}};
|
||||
VkDependencyInfo d0 = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &m0,
|
||||
.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &ib};
|
||||
vkCmdPipelineBarrier2(ex->cb, &d0);
|
||||
VkBufferImageCopy src = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layers},
|
||||
.imageExtent = {img->width, img->height, 1}};
|
||||
vkCmdCopyImageToBuffer(ex->cb, img->image, VK_IMAGE_LAYOUT_GENERAL, twin_buf, 1, &src);
|
||||
VkMemoryBarrier2 m1 = {.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_READ_BIT};
|
||||
VkDependencyInfo d1 = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &m1};
|
||||
vkCmdPipelineBarrier2(ex->cb, &d1);
|
||||
VkBufferImageCopy dst = {.imageSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, layers},
|
||||
.imageExtent = {img->width, img->height, 1}};
|
||||
vkCmdCopyBufferToImage(ex->cb, twin_buf, t->img, VK_IMAGE_LAYOUT_GENERAL, 1, &dst);
|
||||
// The twin's texels before the dispatch; the source's next writers and the buffer's next copy after these copies.
|
||||
VkMemoryBarrier2 m2 = {.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_ALL_COMMANDS_BIT,
|
||||
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT};
|
||||
VkDependencyInfo d2 = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &m2};
|
||||
vkCmdPipelineBarrier2(ex->cb, &d2);
|
||||
ex->recorded += 5;
|
||||
return t;
|
||||
}
|
||||
|
||||
// The image descriptor of slot h; in a dispatch, a compare-sampled colour image the driver cannot compare is swapped
|
||||
// for its depth twin.
|
||||
static void tex_info(VkExec *ex, const GfxShaderRes *r, uint64_t h, int storage, int separate, VkDescriptorImageInfo *out) {
|
||||
tex_info_lookup(ex, r, h, storage, separate, out);
|
||||
if (!twin_dispatch || storage || separate || !h || !(r->type & 0x400)) return;
|
||||
VkImageViewType vt = res_view_type(r->type);
|
||||
if (vt != VK_IMAGE_VIEW_TYPE_2D && vt != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return;
|
||||
const VkTexImage *img = vk_texture_by_id_locked((int)(h & 0xfffff), NULL, NULL);
|
||||
Twin *t = img ? twin_update(ex, img) : NULL;
|
||||
VkImageView v = t ? t->view[vt == VK_IMAGE_VIEW_TYPE_2D_ARRAY] : VK_NULL_HANDLE;
|
||||
if (v) out->imageView = v, out->imageLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
}
|
||||
|
||||
static VkDescriptorSet alloc_set(VkExec *ex, VkDescriptorSetLayout layout) {
|
||||
CbRes *r = cb_res(ex);
|
||||
VkCtx *c = vk_ctx();
|
||||
@@ -2076,7 +2253,9 @@ static void op_dispatch(VkExec *ex, const p_dispatch *d) {
|
||||
vk_exec_barrier(ex); // ends the pass; earlier writes are visible to the shader
|
||||
bind_pipeline(ex, VK_PIPELINE_BIND_POINT_COMPUTE, p);
|
||||
c0_dim[0] = (uint32_t)d->x, c0_dim[1] = (uint32_t)d->y, c0_dim[2] = (uint32_t)d->z;
|
||||
twin_dispatch = 1;
|
||||
bind_resources(ex, info);
|
||||
twin_dispatch = 0;
|
||||
vkCmdDispatch(ex->cb, (uint32_t)d->x, (uint32_t)d->y, (uint32_t)d->z);
|
||||
ex->recorded++;
|
||||
if (vk_dump_armed) vk_dump_dispatch(ex, cs, p, info, d);
|
||||
@@ -2102,7 +2281,9 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
|
||||
bind_pipeline(ex, VK_PIPELINE_BIND_POINT_COMPUTE, p);
|
||||
c0_dim[0] = c0_dim[1] = c0_dim[2] = 0;
|
||||
B.c0_valid[2] = 0; // a fresh c0 range: the copy below must not change one an earlier dispatch reads
|
||||
twin_dispatch = 1;
|
||||
bind_resources(ex, info);
|
||||
twin_dispatch = 0;
|
||||
if (B.c0_valid[2]) { // the shader binds c0
|
||||
VkBufferCopy cp = {ip.offset, B.c0_off[2] + 0x200, sizeof(VkDispatchIndirectCommand)};
|
||||
vkCmdCopyBuffer(ex->cb, ip.buffer, B.c0_buf[2], 1, &cp);
|
||||
|
||||
@@ -211,7 +211,10 @@ static void oq_cb(void *arg) {
|
||||
// completion thread, and with it every later callback, for good. It counts as visible (one sample).
|
||||
VkResult r = vkGetQueryPoolResults(dev, oq_pool, s->q[i], j - i, sizeof v, v, 2 * sizeof v[0],
|
||||
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||
if (r != VK_SUCCESS && r != VK_NOT_READY) rt_fatal("vk: occlusion query results failed (%d)", (int)r);
|
||||
if (r != VK_SUCCESS && r != VK_NOT_READY) {
|
||||
vk_device_fault_report(r);
|
||||
rt_fatal("vk: occlusion query results failed (%d)", (int)r);
|
||||
}
|
||||
vkResetQueryPool(dev, oq_pool, s->q[i], j - i);
|
||||
for (uint32_t k = i; k < j; k++) {
|
||||
if (v[2 * (k - i) + 1]) sum += v[2 * (k - i)];
|
||||
|
||||
@@ -126,6 +126,11 @@ int tex_vk_format(int nvn_format, TexVkFormat *o) {
|
||||
o->astc_bh = astc[i].bh;
|
||||
o->astc_srgb = (uint8_t)srgb;
|
||||
o->note = "no native ASTC: decoded to RGBA8 (vk_astc.c compute decoder, or tex_astc.c with TOTK_ASTC=cpu)";
|
||||
if (vk_astc_bc()) {
|
||||
o->vk = srgb ? VK_FORMAT_BC3_SRGB_BLOCK : VK_FORMAT_BC3_UNORM_BLOCK;
|
||||
o->astc_bc = 1;
|
||||
o->note = "no native ASTC: decoded and encoded to BC3 on the GPU (vk_astc.c, TOTK_ASTC_BC)";
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if ((f >= 0x51 && f <= 0x68) || f == 0x6B || f == 0x6C) {
|
||||
|
||||
+11
-5
@@ -4,8 +4,9 @@
|
||||
// both apply at the next frame.
|
||||
//
|
||||
// Per frame: EASU reads the source through a view of its own format (the same colour conversion the blit does) and
|
||||
// writes an RGBA16F image of the swapchain size; RCAS reads that and writes a second one; a 1:1 blit converts the
|
||||
// result into the swapchain image, which the caller left in TRANSFER_DST. The shaders (runtime/shaders/fsr_*.comp)
|
||||
// writes an RGBA16F image of the picture's size on the swapchain (all of it, or the part between black bars,
|
||||
// gfx_window.c fit_rect); RCAS reads that and writes a second one; a 1:1 blit converts the result into its place in
|
||||
// the swapchain image, which the caller left in TRANSFER_DST. The shaders (runtime/shaders/fsr_*.comp)
|
||||
// compute AMD's constants from the push constants. Cost: two compute passes over the swapchain size and one copy per
|
||||
// present; nothing on the game's draws.
|
||||
//
|
||||
@@ -99,7 +100,11 @@ static void free_images(VkCtx *c) {
|
||||
|
||||
static int ensure_images(VkCtx *c, uint32_t w, uint32_t h) {
|
||||
if (F.img[0] && F.w == w && F.h == h) return 1;
|
||||
vkDeviceWaitIdle(c->device); // a swapchain size change: earlier presents may still read the old images
|
||||
// A swapchain size change: earlier presents may still read the old images. vkDeviceWaitIdle needs every queue
|
||||
// externally synchronized, so it holds the queue lock like a submission.
|
||||
vk_queue_lock();
|
||||
vkDeviceWaitIdle(c->device);
|
||||
vk_queue_unlock();
|
||||
free_images(c);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
|
||||
@@ -152,8 +157,9 @@ static void bind_pass(VkCommandBuffer cb, VkPipeline p, VkImageView in, VkImageV
|
||||
}
|
||||
|
||||
int vk_fsr_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, int sw, int sh, const int crop[4],
|
||||
VkImage dst, uint32_t dw, uint32_t dh) {
|
||||
VkImage dst, const int out[4]) {
|
||||
int cw = crop[2], ch = crop[3];
|
||||
uint32_t dw = (uint32_t)out[2], dh = (uint32_t)out[3];
|
||||
if (strcmp(cfg_value("TOTK_UPSCALER"), "fsr") || (dw <= (uint32_t)cw && dh <= (uint32_t)ch) || !fmt) return 0;
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!F.tried) init(c);
|
||||
@@ -204,7 +210,7 @@ int vk_fsr_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt,
|
||||
VkImageBlit region = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
||||
.srcOffsets = {{0, 0, 0}, {(int)dw, (int)dh, 1}},
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
||||
.dstOffsets = {{0, 0, 0}, {(int)dw, (int)dh, 1}}};
|
||||
.dstOffsets = {{out[0], out[1], 0}, {out[0] + (int)dw, out[1] + (int)dh, 1}}};
|
||||
vkCmdBlitImage(cb, F.img[last], VK_IMAGE_LAYOUT_GENERAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion,
|
||||
VK_FILTER_NEAREST);
|
||||
return 1;
|
||||
|
||||
+6
-1
@@ -1657,6 +1657,7 @@ VkCommandBuffer vk_upload_begin(void) {
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
vkBeginCommandBuffer(open_b.cb, &bi);
|
||||
open_b.seq = ++next_seq;
|
||||
vk_crumb(open_b.cb, VK_CRUMB_UPLOAD, open_b.seq);
|
||||
open_active = 1;
|
||||
return open_b.cb;
|
||||
}
|
||||
@@ -1717,12 +1718,16 @@ uint64_t vk_upload_flush(int wait) {
|
||||
uint64_t seq = next_seq;
|
||||
if (c && open_active) {
|
||||
for (int i = 0; i < open_b.nstg; i++) vmaFlushAllocation(c->vma, open_b.stg[i].alloc, 0, VK_WHOLE_SIZE);
|
||||
vk_crumb(open_b.cb, VK_CRUMB_UPLOAD_END, open_b.seq);
|
||||
vkEndCommandBuffer(open_b.cb);
|
||||
VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &open_b.cb};
|
||||
vk_queue_lock();
|
||||
VkResult r = vkQueueSubmit(c->gfx_queue, 1, &si, open_b.fence);
|
||||
vk_queue_unlock();
|
||||
if (r != VK_SUCCESS) rt_fatal("vk: upload submit failed (%d)", (int)r);
|
||||
if (r != VK_SUCCESS) {
|
||||
vk_device_fault_report(r);
|
||||
rt_fatal("vk: upload submit failed (%d)", (int)r);
|
||||
}
|
||||
if (ninflight == cap_inflight) grow((void **)&inflight, &cap_inflight, sizeof *inflight);
|
||||
inflight[ninflight++] = open_b;
|
||||
open_active = 0;
|
||||
|
||||
+42
-2
@@ -12,6 +12,7 @@
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
|
||||
@@ -37,7 +38,7 @@ enum {
|
||||
R_SetDepthWriteEnable, R_SetFrontFace, R_SetPrimitiveRestartEnable, R_SetPrimitiveTopology,
|
||||
R_SetRasterizerDiscardEnable, R_SetScissorWithCount, R_SetStencilCompareMask, R_SetStencilOp, R_SetStencilReference,
|
||||
R_SetStencilTestEnable, R_SetStencilWriteMask, R_SetViewportWithCount, R_WriteTimestamp, R_WriteTimestamp2,
|
||||
R_PushTpl, R_Push, R_VertexInput, R_Bool, R_PolygonMode, R_BlendEnable, R_BlendEquation, R_WriteMask,
|
||||
R_PushTpl, R_Push, R_VertexInput, R_Bool, R_PolygonMode, R_BlendEnable, R_BlendEquation, R_WriteMask, R_EndCb,
|
||||
};
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
@@ -128,7 +129,14 @@ void rec_drain(void) {
|
||||
void rec_begin(VkCommandBuffer cb) {
|
||||
static int mode = -1;
|
||||
if (mode < 0) {
|
||||
mode = rt_knob("VK_RECORDER", "1");
|
||||
// Default off on AMD GPUs: with the recorder their drivers lost the device within the first frames or at menu
|
||||
// opens, 1 boot in 3 on an RX 9070 XT, and TOTK_VK_RECORDER=0 ended it for three users (issue #5); under RADV
|
||||
// the kernel saw an illegal opcode at the end of a command buffer (issue #40). Letting the recorder end its own
|
||||
// parts (rec_end_cb) fixed the white scene on an RX 6700 XT; the default stays off until more AMD cards run it.
|
||||
const char *set = cfg_get("TOTK_VK_RECORDER");
|
||||
mode = rt_knob("VK_RECORDER", vk_caps.amd ? "0" : "1");
|
||||
if (!mode && vk_caps.amd && !(set && *set))
|
||||
rt_log("vk_rec: no recorder thread on AMD GPUs (TOTK_VK_RECORDER=1 turns it on)");
|
||||
if (mode) {
|
||||
wake_ev = CreateEventA(NULL, FALSE, FALSE, NULL);
|
||||
rec_thread = wake_ev ? CreateThread(NULL, 0, rec_main, NULL, 0, NULL) : NULL;
|
||||
@@ -155,6 +163,32 @@ void rec_end(void) {
|
||||
rec_live = 0;
|
||||
}
|
||||
|
||||
// TOTK_REC_END (default 1): the thread that recorded the part also ends it, after the breadcrumb crumb (kind 0: none)
|
||||
// that vk_sync.c writes at the end of a submission. On an RX 6700 XT (driver 26.10.1, issue #5) parts the recorder
|
||||
// recorded and the submitting thread ended broke when a submission was split (vk_sync.c vk_sync_split): the GPU hung
|
||||
// or the scene turned green, then white. The handoff in rec_drain orders the two threads, so Vulkan allows the old
|
||||
// split; the driver did not cope with it. TOTK_REC_END=0 brings the old split back.
|
||||
typedef struct { int kind; uint64_t n; } A_EndCb;
|
||||
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n) {
|
||||
static int mode = -1;
|
||||
if (mode < 0) {
|
||||
mode = rt_knob("REC_END", "1");
|
||||
rt_log("vk_rec: a part the recorder records is ended by %s (TOTK_REC_END=%d)",
|
||||
mode ? "the recorder" : "the submitting thread", mode);
|
||||
}
|
||||
if (!rec_live || cb != rec_cb || !mode) {
|
||||
rec_end();
|
||||
if (crumb) vk_crumb(cb, crumb, n);
|
||||
return 0;
|
||||
}
|
||||
A_EndCb *a = put(R_EndCb, sizeof *a);
|
||||
*a = (A_EndCb){crumb, n};
|
||||
pub();
|
||||
rec_drain();
|
||||
rec_live = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void rec_stats_log(void) {
|
||||
if (!(rt_trace & RT_TRACE_PERF) || !RS_.parts) return;
|
||||
rt_log("vk_rec: %llu of %llu parts recorded by the recorder: %llu packets (%.1f MiB), %llu drains (%llu for a call "
|
||||
@@ -581,6 +615,12 @@ static void exec_packet(uint32_t op, void *p) {
|
||||
case R_BlendEnable: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEnableEXT)a->fn)(cb, a->first, a->n, a->v); break; }
|
||||
case R_BlendEquation: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEquationEXT)a->fn)(cb, a->first, a->n, a->v); break; }
|
||||
case R_WriteMask: { A_Targets *a = p; ((PFN_vkCmdSetColorWriteMaskEXT)a->fn)(cb, a->first, a->n, a->v); break; }
|
||||
case R_EndCb: {
|
||||
A_EndCb *a = p;
|
||||
if (a->kind) vk_crumb(cb, a->kind, a->n);
|
||||
vkEndCommandBuffer(cb);
|
||||
break;
|
||||
}
|
||||
default: rt_fatal("vk_rec: unknown packet %u", op);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -4,7 +4,8 @@
|
||||
// ring, in call order, and the recorder makes the real call in that order; calls into any other command buffer (uploads,
|
||||
// the window, probes) are made at once. A call the ring has no packet for (a structure with a pNext chain) waits for
|
||||
// the recorder to empty the ring and is then made at once on the calling thread. rec_end returns once the recorder made
|
||||
// every call, so ending, submitting and the command pool stay on the submitting thread.
|
||||
// every call. vk_sync.c ends a part through rec_end_cb: the recorder makes the last calls into a part it recorded,
|
||||
// vkEndCommandBuffer included; submitting and the command pool stay on the submitting thread.
|
||||
//
|
||||
// Every vkCmd* the runtime calls through vk.h goes through the rc_* wrappers below (the macros at the end); the
|
||||
// extension commands called through function pointers have rec_* wrappers of their own. TOTK_VK_RECORDER=0 makes every
|
||||
@@ -16,6 +17,9 @@
|
||||
|
||||
void rec_begin(VkCommandBuffer cb); // the submitting thread, sub_lock held
|
||||
void rec_end(void); // waits for the recorder; no-op when rec_begin did not start it
|
||||
// The end of part cb: the breadcrumb crumb (0: none, vk_crumbs.c), then vkEndCommandBuffer, both made by the recorder
|
||||
// when it records cb, and the wait for it (1); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
|
||||
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n);
|
||||
void rec_drain(void); // waits until the recorder made every call queued so far
|
||||
void rec_stats_log(void); // TOTK_TRACE=perf: packets, bytes, drains and waits since the last call
|
||||
|
||||
|
||||
+17
-7
@@ -186,7 +186,10 @@ static DWORD WINAPI completion_main(void *arg) {
|
||||
VkSemaphoreWaitInfo wi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, .semaphoreCount = 1,
|
||||
.pSemaphores = &c->timeline, .pValues = &want};
|
||||
VkResult r = vkWaitSemaphores(c->device, &wi, 100000000ull); // 100 ms: stop is checked between waits
|
||||
if (r != VK_SUCCESS && r != VK_TIMEOUT) rt_fatal("vk: vkWaitSemaphores failed (%d; device lost?)", (int)r);
|
||||
if (r != VK_SUCCESS && r != VK_TIMEOUT) {
|
||||
vk_device_fault_report(r);
|
||||
rt_fatal("vk: vkWaitSemaphores failed (%d; device lost?)", (int)r);
|
||||
}
|
||||
vkGetSemaphoreCounterValue(c->device, c->timeline, &reached);
|
||||
AcquireSRWLockExclusive(&st_lock);
|
||||
if (reached > issued) reached = issued;
|
||||
@@ -290,9 +293,8 @@ static VkCommandBuffer prologue(VkCtx *c, uint64_t fence) {
|
||||
VkCommandBuffer vk_sync_split(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
VkCommandBuffer cb = slots[cur_slot].cb;
|
||||
rec_end(); // every command of the part is in cb
|
||||
ts_end(cb, cur_fence);
|
||||
vkEndCommandBuffer(cb);
|
||||
if (!rec_end_cb(cb, 0, 0)) vkEndCommandBuffer(cb); // every command of the part is in cb, ended by its recorder
|
||||
vk_upload_flush(0); // the uploads this part asked for precede it in queue order
|
||||
VkCommandBuffer pro = prologue(c, cur_fence);
|
||||
VkCommandBufferSubmitInfo cbi[2] = {{.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = pro},
|
||||
@@ -302,12 +304,16 @@ VkCommandBuffer vk_sync_split(void) {
|
||||
vk_queue_lock();
|
||||
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
|
||||
vk_queue_unlock();
|
||||
if (r != VK_SUCCESS) rt_fatal("vk: vkQueueSubmit2 of a part of value %llu failed (%d)", (unsigned long long)cur_fence, (int)r);
|
||||
if (r != VK_SUCCESS) {
|
||||
vk_device_fault_report(r);
|
||||
rt_fatal("vk: vkQueueSubmit2 of a part of value %llu failed (%d)", (unsigned long long)cur_fence, (int)r);
|
||||
}
|
||||
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
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
vkBeginCommandBuffer(s->cb, &bi);
|
||||
vk_crumb(s->cb, VK_CRUMB_SUBMIT, cur_fence);
|
||||
ts_begin(s->cb);
|
||||
rec_begin(s->cb);
|
||||
return s->cb;
|
||||
@@ -336,14 +342,15 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
uint64_t t0 = perf_begin();
|
||||
vkBeginCommandBuffer(s->cb, &bi);
|
||||
vk_crumb(s->cb, VK_CRUMB_SUBMIT, fence);
|
||||
ts_begin(s->cb);
|
||||
rec_begin(s->cb);
|
||||
int recorded = vk_exec_record(s->cb, handles, n, fence);
|
||||
s = &slots[cur_slot]; // the last part (vk_sync_split)
|
||||
rec_end();
|
||||
if (recorded) ts_end(s->cb, fence);
|
||||
else ts_drop();
|
||||
vkEndCommandBuffer(s->cb);
|
||||
// The recorder is done with the part: the end-of-submission crumb, then the end, by the recorder when it records.
|
||||
if (!rec_end_cb(s->cb, VK_CRUMB_SUBMIT_END, fence)) vkEndCommandBuffer(s->cb);
|
||||
perf_replay(t0);
|
||||
// After the recording: the uploads it asked for (and every earlier one) precede it in queue order.
|
||||
vk_upload_flush(0);
|
||||
@@ -360,7 +367,10 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
|
||||
vk_queue_lock();
|
||||
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
|
||||
vk_queue_unlock();
|
||||
if (r != VK_SUCCESS) rt_fatal("vk: vkQueueSubmit2 of value %llu failed (%d)", (unsigned long long)fence, (int)r);
|
||||
if (r != VK_SUCCESS) {
|
||||
vk_device_fault_report(r);
|
||||
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);
|
||||
AcquireSRWLockExclusive(&st_lock);
|
||||
|
||||
+113
-12
@@ -313,7 +313,13 @@ static Tex *create_tex(const tex_t *t, const TexKey *k, int defer_init) {
|
||||
if ((d->flags & NVN_TEXTURE_FLAGS_IMAGE) && (ff & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) && samples == 1)
|
||||
usage |= VK_IMAGE_USAGE_STORAGE_BIT;
|
||||
VkImageCreateFlags flags = ds ? 0 : VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; // format-reinterpreting views
|
||||
if (type == VK_IMAGE_TYPE_2D && layers >= 6 && w == h && samples == 1) flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
|
||||
// Cube compatible only for cube targets: the replay makes cube views of nothing else (vk_exec_draw.c get_view checks
|
||||
// the image's own view type). The flag on every square array of 6+ layers changed how AMD's Windows driver lays such
|
||||
// an image out, and terrain material arrays sampled through it came out in rainbow blocks that moved with the mip
|
||||
// level (Savage Wind Recomp issue #59, BotW on an RX 580 and a 7800 XT).
|
||||
if (type == VK_IMAGE_TYPE_2D && layers >= 6 && w == h && samples == 1 &&
|
||||
(d->target == NVN_TEXTURE_TARGET_CUBEMAP || d->target == NVN_TEXTURE_TARGET_CUBEMAP_ARRAY))
|
||||
flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
|
||||
// NVN renders into a 3D texture slice by slice (a view's min layer is the depth plane, nvn_mtl.m), e.g. the
|
||||
// grading LUT of the menu background blur. Vulkan needs a 2D view of the slice for that.
|
||||
if (type == VK_IMAGE_TYPE_3D && (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)) flags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
|
||||
@@ -322,7 +328,18 @@ static Tex *create_tex(const tex_t *t, const TexKey *k, int defer_init) {
|
||||
.samples = (VkSampleCountFlagBits)samples, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = usage,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
int bw, bh, block_views = 0;
|
||||
if (!ds && samples == 1 && f.vk == f.ideal && nvn_fmt_bpb(d->format, &bw, &bh) && bw == 4 && bh == 4) {
|
||||
// TOTK_BLOCK_VIEWS_3D (default 0): a 3D BC image also gets block views. On RDNA2 cards (driver 26.10.1, issue #5)
|
||||
// the 32x32x32 BC4 volume the game creates at the logo, made BLOCK_TEXEL_VIEW_COMPATIBLE and 2D_ARRAY_COMPATIBLE,
|
||||
// took a GPU write just past the end of its memory: device lost at the first submit after it when the image ended
|
||||
// its memory block. Draws into a 3D BC image go through a stand-in instead (vk_exec.c bc_target).
|
||||
static int bv3d = -1;
|
||||
if (bv3d < 0) {
|
||||
bv3d = rt_knob("BLOCK_VIEWS_3D", "0");
|
||||
rt_log("vk: 3D BC textures %s (TOTK_BLOCK_VIEWS_3D=%d)", bv3d ? "get block views (renderable)" : "have no block views",
|
||||
bv3d);
|
||||
}
|
||||
if (!ds && samples == 1 && f.vk == f.ideal && nvn_fmt_bpb(d->format, &bw, &bh) && bw == 4 && bh == 4 &&
|
||||
(type != VK_IMAGE_TYPE_3D || bv3d)) {
|
||||
VkImageCreateInfo bc = ci; // BC formats: renderable through views of their blocks (block_views_ok)
|
||||
bc.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
bc.flags |= VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT;
|
||||
@@ -475,6 +492,36 @@ static void sweep_released(void) {
|
||||
while (nreleased > RELEASED_MAX && rel_head) destroy_tex(rel_head);
|
||||
}
|
||||
|
||||
// TOTK_TEX_VRAM_PRESSURE (percent of the device-local budget, default 90; 0 = off): above it, each submit drops up to
|
||||
// PRESSURE_STEP of the oldest released entries, so a 4 GiB card does not keep RELEASED_MAX released images (each ASTC
|
||||
// one decoded to RGBA8) while the heap spills into system memory. A few per submit, for the reason above. Only entries
|
||||
// that come back intact are dropped: mirrored ones the GPU never wrote, re-uploaded from their storage. A render target
|
||||
// lives only in its image; dropping it brought the texture back empty (black flashes in a Depths boss fight, ~170
|
||||
// "left uninitialised" aliases a minute).
|
||||
#define PRESSURE_STEP 32
|
||||
static int vram_pressure = -1;
|
||||
static uint64_t n_pressure_drops;
|
||||
|
||||
static void sweep_pressure(void) { // tex_lock held
|
||||
if (vram_pressure < 0) {
|
||||
const char *s = cfg_get("TOTK_TEX_VRAM_PRESSURE");
|
||||
vram_pressure = s && *s ? atoi(s) : 90;
|
||||
if (vram_pressure < 0 || vram_pressure > 100) vram_pressure = 90;
|
||||
rt_log("vk: released textures under VRAM pressure: %s%d%%", vram_pressure ? "dropped above " : "off", vram_pressure);
|
||||
}
|
||||
if (!vram_pressure || !rel_head || submit_epoch & 7) return; // the budget query every 8 submits
|
||||
uint64_t used, budget;
|
||||
int exact;
|
||||
if (!vk_mem_vram(&used, &budget, &exact) || used <= budget / 100 * (uint64_t)vram_pressure) return;
|
||||
int dropped = 0;
|
||||
for (Tex *e = rel_head, *next; e && dropped < PRESSURE_STEP; e = next) {
|
||||
next = e->rel_next;
|
||||
if (!e->pub.mirrored || e->pub.gpu_owned || e->gpu_dirty) continue;
|
||||
destroy_tex(e);
|
||||
dropped++, n_pressure_drops++;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- uploads
|
||||
static uint32_t texel_alpha_one(int format, uint32_t *elem) {
|
||||
switch (format) {
|
||||
@@ -610,7 +657,7 @@ static void upload(Tex *e) {
|
||||
int checked = 0;
|
||||
if (agpu) {
|
||||
uint64_t a0 = perf_begin();
|
||||
checked = vk_astc_record(cb, sb, total, ar, (int)n, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, e->pub.image,
|
||||
checked = vk_astc_record(cb, sb, total, ar, (int)n, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, e->f.astc_bc, e->pub.image,
|
||||
e->pub.aspect, rbuf);
|
||||
perf_end(PERF_ASTC, a0);
|
||||
if (!checked)
|
||||
@@ -694,6 +741,7 @@ static void make_fresh_snap(Tex *e) {
|
||||
// Bytes per block and block size of the image's VkFormat (ASTC decodes to RGBA8, RGB expands to RGBA); 0 when the
|
||||
// VkFormat substitutes another one of unknown size.
|
||||
static int img_block(const Tex *e, int *bw, int *bh) {
|
||||
if (e->f.astc_bc) { *bw = *bh = 4; return 16; }
|
||||
if (e->f.astc_bw) { *bw = *bh = 1; return 4; }
|
||||
int b = nvn_fmt_bpb(e->t.d.format, bw, bh);
|
||||
if (e->f.expand_rgb) { *bw = *bh = 1; return b / 3 * 4; }
|
||||
@@ -841,9 +889,22 @@ const VkTexImage *vk_texture_get(const tex_t *t) {
|
||||
}
|
||||
|
||||
// Small render targets the CPU reads without NVN's invalidate (vk_texture_write_back_small); small_pending: one may be
|
||||
// dirty.
|
||||
enum { WB_SMALL = 64 << 10 };
|
||||
// dirty. wb_limit is the largest target (bytes) that goes back when the CPU waits for a fence: TOTK_WB_FENCE_LIMIT,
|
||||
// read once. wb_trace: TOTK_TRACE=gfx, read together with it.
|
||||
enum { WB_SMALL_DEFAULT = 64 << 10 };
|
||||
static int small_pending;
|
||||
static uint64_t wb_limit = WB_SMALL_DEFAULT;
|
||||
static int wb_trace;
|
||||
static int wb_cfg_read;
|
||||
|
||||
static void wb_cfg_init(void) {
|
||||
if (wb_cfg_read) return;
|
||||
int v = cfg_int("TOTK_WB_FENCE_LIMIT");
|
||||
wb_limit = v < 0 ? 0 : (uint64_t)v;
|
||||
wb_trace = (rt_trace & RT_TRACE_GFX) != 0;
|
||||
wb_cfg_read = 1;
|
||||
if (wb_limit != WB_SMALL_DEFAULT) rt_log("vk: fence write-back limit %llu bytes", (unsigned long long)wb_limit);
|
||||
}
|
||||
|
||||
void vk_texture_gpu_written(const tex_t *t) {
|
||||
if (!t || !t->mem) return;
|
||||
@@ -860,7 +921,8 @@ void vk_texture_gpu_written(const tex_t *t) {
|
||||
storage_rewritten(e, 0, &e->gpu_gen); // CPU writes up to here are older than this render
|
||||
e->gpu_seq = ++gpu_writes;
|
||||
e->gpu_fence = vk_exec_recording_fence();
|
||||
if (e->t.L.size <= WB_SMALL) __atomic_store_n(&small_pending, 1, __ATOMIC_RELEASE);
|
||||
wb_cfg_init();
|
||||
if (e->t.L.size <= wb_limit) __atomic_store_n(&small_pending, 1, __ATOMIC_RELEASE);
|
||||
own_add(e);
|
||||
}
|
||||
}
|
||||
@@ -1032,6 +1094,7 @@ uint32_t vk_texture_epoch(void) { return __atomic_load_n(&submit_epoch, __ATOMIC
|
||||
void vk_texture_new_submit(void) {
|
||||
lock();
|
||||
if (!++submit_epoch) submit_epoch = 1;
|
||||
sweep_pressure();
|
||||
static uint64_t log_at;
|
||||
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) / 60;
|
||||
if ((rt_trace & RT_TRACE_PERF) && f != log_at && tex_alias > 0) {
|
||||
@@ -1050,6 +1113,9 @@ void vk_texture_new_submit(void) {
|
||||
(unsigned long long)f * 60, (unsigned long long)ck, (unsigned long long)n_checks, (unsigned long long)calls,
|
||||
(unsigned long long)untr);
|
||||
n_checks = 0;
|
||||
rt_log("perf: frame %llu: released textures kept %d, dropped under VRAM pressure %llu", (unsigned long long)f * 60,
|
||||
nreleased, (unsigned long long)n_pressure_drops);
|
||||
n_pressure_drops = 0;
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
@@ -1150,13 +1216,18 @@ static int image_texels(Tex *e, const Region *g, const NVNcopyRegion *r, uint8_t
|
||||
vmaFlushAllocation(c->vma, sa, 0, VK_WHOLE_SIZE);
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
int done = 0;
|
||||
if (agpu) {
|
||||
// A BC3 image takes whole 4x4 blocks, or a part that ends on the mip's edge; a region of a 5x5 or 6x6 footprint
|
||||
// may end inside a block (vk_astc_record checks the start corner).
|
||||
uint32_t mw = e->t.d.width >> g->level, mh = e->t.d.height >> g->level;
|
||||
int bc_cut = e->f.astc_bc && (((w & 3) && (uint32_t)r->xoffset + w != (mw ? mw : 1)) ||
|
||||
((h & 3) && (uint32_t)r->yoffset + h != (mh ? mh : 1)));
|
||||
if (agpu && !bc_cut) { // bc_cut: nothing recorded, logged below as not decoded
|
||||
VkAstcRegion *ar = calloc((size_t)g->nz, sizeof *ar);
|
||||
for (int z = 0; z < g->nz; z++)
|
||||
ar[z] = (VkAstcRegion){.src = slice * (size_t)z, .w = w, .h = h, .slices = 1, .level = rg[z].imageSubresource.mipLevel,
|
||||
.layer = rg[z].imageSubresource.baseArrayLayer, .offset = rg[z].imageOffset};
|
||||
uint64_t a0 = perf_begin();
|
||||
done = vk_astc_record(cb, sb, slice * (size_t)g->nz, ar, g->nz, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb,
|
||||
done = vk_astc_record(cb, sb, slice * (size_t)g->nz, ar, g->nz, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, e->f.astc_bc,
|
||||
e->pub.image, aspect, VK_NULL_HANDLE);
|
||||
perf_end(PERF_ASTC, a0);
|
||||
free(ar);
|
||||
@@ -1367,24 +1438,54 @@ void vk_texture_write_back(uint64_t addr, uint64_t size) {
|
||||
// TotK's grass system renders a 128x128 RGBA8 map of cut and trampled grass into a CPU-cached pool every frame and its
|
||||
// CPU code reads the pool bytes without nvnMemoryPoolInvalidateMappedRange (cut reactions: blades, insects, the cut
|
||||
// itself). On the Switch the bytes are there once the frame's fence has passed. So when the
|
||||
// CPU waits for a fence, mirrored targets up to WB_SMALL bytes rendered by work up to that fence go back to their
|
||||
// CPU waits for a fence, mirrored targets up to wb_limit bytes rendered by work up to that fence go back to their
|
||||
// storage, unless the CPU rewrote it since. Larger targets still go back on an invalidate, memcpy or ReadTexels.
|
||||
int vk_texture_small_dirty(void) { return __atomic_load_n(&small_pending, __ATOMIC_ACQUIRE); }
|
||||
|
||||
// TOTK_TRACE=gfx: a dirty target the fence write-back leaves alone because it is over the limit (what a CPU read
|
||||
// after the wait would miss), or sends back although it is over the default limit (what a raised limit changed). One
|
||||
// line per target address and outcome; the table is small and a full one stops logging.
|
||||
static void wb_note(const Tex *e, int skipped) {
|
||||
static uint64_t seen[2][64];
|
||||
static int n_seen[2];
|
||||
for (int i = 0; i < n_seen[skipped]; i++)
|
||||
if (seen[skipped][i] == e->pub.addr) return;
|
||||
if (n_seen[skipped] == 64) return;
|
||||
seen[skipped][n_seen[skipped]++] = e->pub.addr;
|
||||
const pool_t *p = e->t.pool;
|
||||
unsigned fl = p ? p->flags : 0;
|
||||
rt_log("vk: fence write-back %s %#llx %ux%u fmt %#x size %llu pool %s (limit %llu)",
|
||||
skipped ? "skipped-for-size" : "over-default", (unsigned long long)e->pub.addr, e->t.d.width, e->t.d.height,
|
||||
e->t.d.format, (unsigned long long)e->t.L.size,
|
||||
!p ? "none" : (fl & NVN_MEMORY_POOL_FLAGS_CPU_CACHED) ? "cpu-cached" : (fl & NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED) ? "cpu-uncached" : "other",
|
||||
(unsigned long long)wb_limit);
|
||||
}
|
||||
|
||||
void vk_texture_write_back_small(uint64_t fence) {
|
||||
lock();
|
||||
wb_cfg_init();
|
||||
__atomic_store_n(&small_pending, 0, __ATOMIC_RELAXED);
|
||||
AcquireSRWLockShared(&owned_lock);
|
||||
int n = 0, later = 0;
|
||||
for (int i = 0; i < nowned; i++) n += owned[i]->gpu_dirty && owned[i]->t.L.size <= WB_SMALL;
|
||||
for (int i = 0; i < nowned; i++) n += owned[i]->gpu_dirty && owned[i]->t.L.size <= wb_limit;
|
||||
Tex **hit = n ? malloc(sizeof *hit * (size_t)n) : NULL;
|
||||
if (n && !hit) rt_fatal("vk: out of memory for a texture write-back");
|
||||
n = 0;
|
||||
for (int i = 0; i < nowned; i++) {
|
||||
Tex *e = owned[i];
|
||||
if (!e->gpu_dirty || e->t.L.size > WB_SMALL) continue;
|
||||
if (!e->gpu_dirty) continue;
|
||||
if (e->t.L.size > wb_limit) {
|
||||
if (wb_trace && e->gpu_fence <= fence) {
|
||||
wb_note(e, 1);
|
||||
perf_count(PERF_CNT_WB_SKIP);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (e->gpu_fence > fence) later = 1;
|
||||
else hit[n++] = e;
|
||||
else {
|
||||
if (wb_trace && e->t.L.size > WB_SMALL_DEFAULT) wb_note(e, 0);
|
||||
hit[n++] = e;
|
||||
}
|
||||
}
|
||||
ReleaseSRWLockShared(&owned_lock);
|
||||
if (later) __atomic_store_n(&small_pending, 1, __ATOMIC_RELEASE);
|
||||
|
||||
+61
-1
@@ -14,7 +14,7 @@ from types import SimpleNamespace
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from installer import ini, runner, state # noqa: E402
|
||||
from installer import ini, runner, state, steps # noqa: E402
|
||||
from installer.events import Reporter # noqa: E402
|
||||
from installer.layout import Layout # noqa: E402
|
||||
|
||||
@@ -217,6 +217,16 @@ class IniTest(unittest.TestCase):
|
||||
self.assertEqual(ini.set_language('[language]\nlocale = de-DE\n', 'fr'),
|
||||
'[language]\nlang = fr\nlocale = de-DE\n')
|
||||
|
||||
def test_old_chinese_codes_are_written_as_the_canonical_ones(self):
|
||||
self.assertEqual(ini.canonical_language('zh-CN'), 'zh-Hans')
|
||||
self.assertEqual(ini.canonical_language('zh-TW'), 'zh-Hant')
|
||||
self.assertEqual(ini.canonical_language('ja'), 'ja')
|
||||
self.assertNotIn('zh-CN', ini.LANGUAGES)
|
||||
self.assertNotIn('zh-TW', ini.LANGUAGES)
|
||||
out = ini.set_language(self.EXAMPLE, 'zh-CN')
|
||||
self.assertEqual(ini.get_language(out), 'zh-Hans')
|
||||
self.assertEqual(ini.get_language(ini.set_language(out, 'zh-TW')), 'zh-Hant')
|
||||
|
||||
def test_write_keeps_user_lines(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d = Path(d)
|
||||
@@ -231,6 +241,56 @@ class IniTest(unittest.TestCase):
|
||||
self.assertEqual(ini.get_language(text), 'ja')
|
||||
|
||||
|
||||
class CleanupTest(unittest.TestCase):
|
||||
"""The real Cleanup step on a finished install whose romfs/ is intact, emptied or gone (#107)."""
|
||||
|
||||
def setUp(self):
|
||||
self.steps = steps
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
base = Path(self.tmp.name)
|
||||
self.dest, temp = base / 'dest', base / 'temp'
|
||||
(self.dest / 'romfs').mkdir(parents=True)
|
||||
temp.mkdir()
|
||||
image = b'romfs image'
|
||||
(self.dest / 'romfs' / 'romfs.bin').write_bytes(image)
|
||||
digest = steps.extract.sha256(self.dest / 'romfs' / 'romfs.bin')
|
||||
(self.dest / 'romfs' / 'index.json').write_text(json.dumps({'romfs_size': len(image), 'romfs_sha256': digest}))
|
||||
self.expected = {'romfs': {'size': len(image), 'sha256': digest}}
|
||||
self.ctx = SimpleNamespace(reporter=Reporter(out=io.StringIO(), log_path=base / 'x.log'), dest=self.dest,
|
||||
temp=temp, lang='en-US', state={},
|
||||
layout=SimpleNamespace(ini_example=ROOT / 'totk.ini.example'))
|
||||
self.saved = steps.expected_update
|
||||
steps.expected_update = lambda ctx: self.expected
|
||||
|
||||
def tearDown(self):
|
||||
self.steps.expected_update = self.saved
|
||||
self.ctx.reporter.close()
|
||||
self.tmp.cleanup()
|
||||
|
||||
def run_cleanup(self):
|
||||
try:
|
||||
self.steps.Cleanup().run(self.ctx)
|
||||
return 'ok'
|
||||
except self.steps.InstallError as e:
|
||||
return e.id
|
||||
|
||||
def test_intact_romfs_passes(self):
|
||||
self.assertEqual(self.run_cleanup(), 'ok')
|
||||
|
||||
def test_emptied_romfs_fails(self):
|
||||
for p in (self.dest / 'romfs').iterdir():
|
||||
p.unlink()
|
||||
self.assertEqual(self.run_cleanup(), 'cleanup.romfs_unreadable')
|
||||
|
||||
def test_missing_romfs_fails(self):
|
||||
self.steps.remove(self.dest / 'romfs')
|
||||
self.assertEqual(self.run_cleanup(), 'cleanup.romfs_unreadable')
|
||||
|
||||
def test_romfs_of_another_size_fails(self):
|
||||
(self.dest / 'romfs' / 'romfs.bin').write_bytes(b'short')
|
||||
self.assertEqual(self.run_cleanup(), 'cleanup.romfs_unreadable')
|
||||
|
||||
|
||||
class EventsTest(unittest.TestCase):
|
||||
def test_lines_are_json_and_the_log_is_english(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""tools/pack_shader_cache.py on a fake shader cache: the pack layout gfx_shader.c reads (sorted index, bounds, data),
|
||||
loose files folded in and deleted, a second run merging new loose files, a loose file winning over its packed copy,
|
||||
malformed files skipped and --keep. No game files; stdlib only.
|
||||
|
||||
Run: python tests/pack_shader_cache_test.py
|
||||
"""
|
||||
import hashlib
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import pack_shader_cache as psc # noqa: E402
|
||||
|
||||
USE = struct.pack('<5I', 0x4d554b54, 1, 3, 0, 1) # gfx_shader.c use_store
|
||||
|
||||
|
||||
def name(seed):
|
||||
return hashlib.sha256(seed.encode()).hexdigest()
|
||||
|
||||
|
||||
def read_pack(path):
|
||||
"""{(kind, name hex): bytes}, checking what gfx_shader.c pack_open checks."""
|
||||
b = path.read_bytes()
|
||||
assert b[:8] == psc.MAGIC
|
||||
count = struct.unpack_from('<I', b, 8)[0]
|
||||
out, last = {}, None
|
||||
for i in range(count):
|
||||
nm, kind, size, offset = struct.unpack_from('<32sIIQ', b, 16 + 48 * i)
|
||||
assert offset + size <= len(b) and offset >= 16 + 48 * count
|
||||
assert last is None or (last[0], last[1]) < (kind, nm), 'index not sorted by (kind, name)'
|
||||
last = (kind, nm)
|
||||
out[(kind, nm.hex())] = b[offset:offset + size]
|
||||
return out
|
||||
|
||||
|
||||
class PackTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.cache = Path(self.tmp.name) / 'shader-cache'
|
||||
(self.cache / 'in').mkdir(parents=True)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def shader(self, seed, spv=b'\x03\x02\x23\x07spirv', res=b'{"x":1}'):
|
||||
k = name(seed)
|
||||
(self.cache / f'{k}.spv').write_bytes(spv + seed.encode())
|
||||
(self.cache / f'{k}.res.json').write_bytes(res)
|
||||
(self.cache / f'{name("use" + seed)}.use').write_bytes(USE)
|
||||
(self.cache / 'in').mkdir(exist_ok=True) # in_store makes it again after a pack removed it
|
||||
(self.cache / 'in' / f'{name("in" + seed)}.in').write_bytes(b'TKSI' + seed.encode())
|
||||
return k
|
||||
|
||||
def test_pack_holds_every_file_and_deletes_the_loose_ones(self):
|
||||
keys = [self.shader(str(i)) for i in range(50)]
|
||||
(self.cache / 'notes.txt').write_text('kept')
|
||||
(self.cache / f'{keys[0]}.code').write_bytes(b'kept too')
|
||||
n, new, skipped, _ = psc.pack(self.cache)
|
||||
self.assertEqual((n, new, skipped), (200, 200, 0))
|
||||
got = read_pack(self.cache / psc.PACK)
|
||||
self.assertEqual(got[(1, keys[3])], b'\x03\x02\x23\x07spirv3')
|
||||
self.assertEqual(got[(2, keys[3])], b'{"x":1}')
|
||||
self.assertEqual(got[(3, name('use3'))], USE)
|
||||
self.assertEqual(got[(4, name('in3'))], b'TKSI3')
|
||||
left = sorted(p.name for p in self.cache.iterdir())
|
||||
self.assertEqual(left, sorted([psc.PACK, 'notes.txt', f'{keys[0]}.code'])) # in/ removed when empty
|
||||
|
||||
def test_second_run_merges_new_loose_files_and_loose_wins(self):
|
||||
a = self.shader('a')
|
||||
psc.pack(self.cache)
|
||||
b = self.shader('b')
|
||||
(self.cache / f'{a}.res.json').write_bytes(b'{"newer":1}')
|
||||
n, new, _, _ = psc.pack(self.cache)
|
||||
got = read_pack(self.cache / psc.PACK)
|
||||
self.assertEqual(n, 8)
|
||||
self.assertEqual(new, 5)
|
||||
self.assertEqual(got[(2, a)], b'{"newer":1}')
|
||||
self.assertEqual(got[(1, b)], b'\x03\x02\x23\x07spirvb')
|
||||
|
||||
def test_malformed_files_are_skipped_or_replaced_by_the_packed_copy(self):
|
||||
a = self.shader('a')
|
||||
psc.pack(self.cache)
|
||||
(self.cache / f'{name("usea")}.use').write_bytes(b'short') # bad loose copy: the packed one stays
|
||||
(self.cache / f'{name("bad")}.use').write_bytes(b'short') # bad and nowhere else: skipped
|
||||
(self.cache / f'{name("empty")}.spv').write_bytes(b'')
|
||||
n, _, skipped, _ = psc.pack(self.cache)
|
||||
got = read_pack(self.cache / psc.PACK)
|
||||
self.assertEqual(got[(3, name('usea'))], USE)
|
||||
self.assertNotIn((3, name('bad')), got)
|
||||
self.assertEqual((n, skipped), (4, 2))
|
||||
self.assertTrue((self.cache / f'{name("bad")}.use').exists()) # never deleted unless packed
|
||||
|
||||
def test_keep_leaves_the_loose_files(self):
|
||||
k = self.shader('a')
|
||||
psc.pack(self.cache, keep=True)
|
||||
self.assertTrue((self.cache / f'{k}.spv').exists())
|
||||
self.assertEqual(len(read_pack(self.cache / psc.PACK)), 4)
|
||||
|
||||
def test_a_broken_pack_is_rebuilt_from_the_loose_files(self):
|
||||
self.shader('a')
|
||||
(self.cache / psc.PACK).write_bytes(b'garbage')
|
||||
n, _, _, _ = psc.pack(self.cache)
|
||||
self.assertEqual(n, 4)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+17
-12
@@ -76,8 +76,11 @@ SDL3_DLLS = [UCRT / 'SDL3.dll', UCRT / 'libiconv-2.dll', UCRT / 'libopus-0.dll']
|
||||
# Flags for generated code as LAS Recomp builds it (Makefile GENFLAGS, Windows), plus -march for the FP/SIMD lowering.
|
||||
GEN_FLAGS = ['-O1', '-fno-strict-aliasing', '-Wno-return-type', '-Wno-unused-label', '-fvisibility=hidden',
|
||||
'-march=x86-64-v3']
|
||||
RT_FLAGS = ['-O2', '-g', '-Wall', '-fno-strict-aliasing', '-march=x86-64-v3']
|
||||
LINK_FLAGS = ['-fuse-ld=lld', '-Wl,--stack,16777216', '"-Wl,--Map=$builddir/totk.map"'] # the map feeds tools/prof_report.py
|
||||
# _WIN32_WINNT: Windows 10, the port's minimum. MSYS2's headers default to it, llvm-mingw's (the release toolchain) to
|
||||
# Windows 7, which hides the per-monitor DPI functions gfx_window.c and dpi_probe.c call.
|
||||
RT_FLAGS = ['-O2', '-g', '-Wall', '-fno-strict-aliasing', '-march=x86-64-v3', '-D_WIN32_WINNT=0x0A00']
|
||||
# -Xlinker passes the map path as one argument; -Wl, would split it at any comma in the build dir path
|
||||
LINK_FLAGS = ['-fuse-ld=lld', '-Wl,--stack,16777216', '-Xlinker', '"--Map=$builddir/totk.map"'] # the map feeds tools/prof_report.py
|
||||
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-lwinmm', '-lSDL3', '-lopus', '-l:libvpx.a', '-l:libzstd.a', '-l:libstdc++.a', '-l:libwinpthread.a'] # Vulkan back end; libvpx static (movies); libzstd static (resolution.c)
|
||||
VMA_INCLUDE = UCRT.parent / 'include' / 'vma' # vk_mem_alloc.h; MSYS2 package vulkan-memory-allocator
|
||||
# --toolchain: an llvm-mingw tree. The libraries, headers and DLLs MSYS2 supplies come from the deps directory instead.
|
||||
@@ -171,16 +174,18 @@ def write_if_changed(path, text):
|
||||
def copy_romfs_patches(version, out):
|
||||
"""totk.exe loads romfs-patches beside romfs-index.bin (runtime/romfs.c overlay_init). tools/make_romfs_patches.py
|
||||
writes them into build/<version> only, so a build in another directory ran without the 60 fps overlay (the quick
|
||||
menus' captured background turned black). Copies them when their manifest differs."""
|
||||
src = ROOT / 'build' / version / 'romfs-patches'
|
||||
dst = out / 'romfs-patches'
|
||||
if src.resolve() == dst.resolve() or not (src / 'manifest.tsv').is_file():
|
||||
return
|
||||
if (dst / 'manifest.tsv').is_file() and (dst / 'manifest.tsv').read_bytes() == (src / 'manifest.tsv').read_bytes():
|
||||
return
|
||||
shutil.rmtree(dst, ignore_errors=True)
|
||||
shutil.copytree(src, dst)
|
||||
print(f'romfs patches: copied from {src}')
|
||||
menus' captured background turned black). Copies them when their manifest differs; romfs-patches-xbox (the same with
|
||||
the A/B and X/Y prompts swapped, pad_layout = xbox_prompts) the same way when it was made."""
|
||||
for name in ('romfs-patches', 'romfs-patches-xbox'):
|
||||
src = ROOT / 'build' / version / name
|
||||
dst = out / name
|
||||
if src.resolve() == dst.resolve() or not (src / 'manifest.tsv').is_file():
|
||||
continue
|
||||
if (dst / 'manifest.tsv').is_file() and (dst / 'manifest.tsv').read_bytes() == (src / 'manifest.tsv').read_bytes():
|
||||
continue
|
||||
shutil.rmtree(dst, ignore_errors=True)
|
||||
shutil.copytree(src, dst)
|
||||
print(f'romfs patches: copied {name} from {src}')
|
||||
|
||||
|
||||
def c_string(s):
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Memory the machine can still give to new processes, for sizing worker pools (stdlib only, Windows)."""
|
||||
import ctypes
|
||||
import ctypes.wintypes as wt
|
||||
import sys
|
||||
|
||||
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9
|
||||
LIMIT_PROCESS_MEMORY = 0x100
|
||||
LIMIT_JOB_MEMORY = 0x200
|
||||
|
||||
|
||||
class _MemStatus(ctypes.Structure):
|
||||
_fields_ = [('dwLength', wt.DWORD), ('dwMemoryLoad', wt.DWORD)] + \
|
||||
[(n, ctypes.c_ulonglong) for n in ('total', 'avail', 'tpf', 'apf', 'tv', 'av', 'aev')]
|
||||
|
||||
|
||||
class _JobLimits(ctypes.Structure):
|
||||
_fields_ = [('PerProcessUserTimeLimit', ctypes.c_longlong), ('PerJobUserTimeLimit', ctypes.c_longlong),
|
||||
('LimitFlags', wt.DWORD), ('MinimumWorkingSetSize', ctypes.c_size_t),
|
||||
('MaximumWorkingSetSize', ctypes.c_size_t), ('ActiveProcessLimit', wt.DWORD),
|
||||
('Affinity', ctypes.c_size_t), ('PriorityClass', wt.DWORD), ('SchedulingClass', wt.DWORD),
|
||||
('r_ops', ctypes.c_ulonglong), ('w_ops', ctypes.c_ulonglong), ('o_ops', ctypes.c_ulonglong),
|
||||
('r_bytes', ctypes.c_ulonglong), ('w_bytes', ctypes.c_ulonglong), ('o_bytes', ctypes.c_ulonglong),
|
||||
('ProcessMemoryLimit', ctypes.c_size_t), ('JobMemoryLimit', ctypes.c_size_t),
|
||||
('PeakProcessMemoryUsed', ctypes.c_size_t), ('PeakJobMemoryUsed', ctypes.c_size_t)]
|
||||
|
||||
|
||||
def _job_headroom():
|
||||
"""Bytes left under the memory limit of the job object this process runs in, or None without such a limit."""
|
||||
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
k32.QueryInformationJobObject.argtypes = [wt.HANDLE, ctypes.c_int, ctypes.c_void_p, wt.DWORD, ctypes.c_void_p]
|
||||
info = _JobLimits()
|
||||
# A null handle asks about the job of the calling process.
|
||||
if not k32.QueryInformationJobObject(None, JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, ctypes.byref(info),
|
||||
ctypes.sizeof(info), None):
|
||||
return None
|
||||
if info.LimitFlags & LIMIT_JOB_MEMORY:
|
||||
return max(0, info.JobMemoryLimit - info.PeakJobMemoryUsed)
|
||||
if info.LimitFlags & LIMIT_PROCESS_MEMORY:
|
||||
return info.ProcessMemoryLimit
|
||||
return None
|
||||
|
||||
|
||||
def available_ram():
|
||||
"""Physical memory not in use right now, capped by the job memory limit if there is one. None off Windows."""
|
||||
if sys.platform != 'win32':
|
||||
return None
|
||||
status = _MemStatus()
|
||||
status.dwLength = ctypes.sizeof(status)
|
||||
if not ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)):
|
||||
return None
|
||||
headroom = _job_headroom()
|
||||
return status.avail if headroom is None else min(status.avail, headroom)
|
||||
@@ -97,8 +97,9 @@ LLVM_SKIP_DIRS = {'aarch64-w64-mingw32', 'armv7-w64-mingw32', 'i686-w64-mingw32'
|
||||
LLVM_SKIP_SUFFIXES = {'.idl', '.tlb'}
|
||||
|
||||
# tools/ files the installer steps run (and what they import), copied from the repo.
|
||||
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'check_content.py', 'extract.py', 'make_romfs_patches.py',
|
||||
'prepare_shaders.py', 'romfs_index.py', 'shader_census.py', 'shader_list.py']
|
||||
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'check_content.py', 'extract.py', 'hostmem.py', 'make_romfs_patches.py',
|
||||
'pack_shader_cache.py', 'prepare_shaders.py', 'romfs_index.py', 'shader_census.py', 'shader_list.py',
|
||||
'ultrawide_layout.py']
|
||||
TOOLS_DIRS = ['vendor/macnativegames', 'data'] # data/: expected hashes
|
||||
RUNTIME_SUFFIXES = {'.h', '.inc', '.ico'} # .ico: the installer window icon
|
||||
|
||||
|
||||
+308
-19
@@ -11,13 +11,33 @@ captured at once (upstream checked weapon, shield, material and ability wheel at
|
||||
The game-over flow fix of the same upstream script is 1.0.0 only (1.2.1 and 1.4.3 run DmF_SY_GameOver.110) and is
|
||||
left out.
|
||||
|
||||
--aspect <w>x<h> (a game resolution wider than 16:9, such as 3440x1440) also lays the UI out for that shape
|
||||
(tools/ultrawide_layout.py): menus stay 16:9 in the middle, full-screen backgrounds cover the width and the HUD moves
|
||||
to the screen's corners. totk.exe loads that overlay from romfs-patches-<w>x<h> when the game resolution is that size
|
||||
(runtime/romfs.c overlay_init); the camera's side of it is the patch set aspect (recomp/patches-v143.txt).
|
||||
|
||||
--xbox-prompts: the same overlay with the A and B, and the X and Y button prompts swapped, for pad_layout = xbox_prompts
|
||||
(runtime/romfs.c overlay_init loads romfs-patches-xbox in place of romfs-patches). That layout plays an Xbox pad by
|
||||
position, as nintendo does: the button that does Switch A is labelled B, and the prompts then name the label. Built from
|
||||
the game's own images and glyphs, nothing added:
|
||||
- the action prompt (blyt/DoCommand_00.bflyt) draws the letter images Nt_KeyTexA/B/X/Y_00 of timg/__Combined.bntx in
|
||||
the Common archive: their image data is swapped (same format, size and tiling). Around it, a diamond names the
|
||||
other face buttons with one-letter text panes (T_B_00, T_X_00, ...). The game fills them from the messages of the
|
||||
same name in LayoutMsg/DoCommand_00.msbt of each Mals/<language> archive, over the layout's own text: both have
|
||||
their letters swapped the same way. The diamond's shape (Nt_KeyBtnSubBase*) shows the button's position, which
|
||||
stays right.
|
||||
- every other prompt is text: the button glyphs U+E040 A, U+E041 B, U+E042 X, U+E043 Y of scft/External.bfotf, the
|
||||
same font in each Font/Font*.Nin_NX_NVN.bfarc.zs. Their CFF outlines and horizontal metrics are swapped.
|
||||
|
||||
Input: build/extracted/<version>/romfs (romfs.bin and index.json, verified by tools/extract.py). Output:
|
||||
build/<version>/romfs-patches/<path> by default, and <out>/manifest.tsv (path, size, SHA-256 of the overlay, SHA-256 of the original). Run with .venv-py314.
|
||||
build/<version>/romfs-patches/<path> by default (romfs-patches-xbox with --xbox-prompts), and <out>/manifest.tsv (path, size, SHA-256 of the overlay, SHA-256 of the original). Run with .venv-py314.
|
||||
"""
|
||||
import argparse
|
||||
import compression.zstd as zstd
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
@@ -25,17 +45,20 @@ root = Path(__file__).resolve().parents[1]
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--version', choices=('v121', 'v143'), default='v121')
|
||||
ap.add_argument('--romfs', type=Path, help='default build/extracted/<version>/romfs')
|
||||
ap.add_argument('--out', type=Path, help='default build/<version>/romfs-patches')
|
||||
ap.add_argument('--out', type=Path,
|
||||
help='default build/<version>/romfs-patches (romfs-patches-xbox with --xbox-prompts)')
|
||||
ap.add_argument('--aspect', help='<w>x<h> of a game resolution wider than 16:9: also lay the UI out for it')
|
||||
ap.add_argument('--xbox-prompts', action='store_true', help='swap the A/B and X/Y button prompts as well')
|
||||
args = ap.parse_args()
|
||||
args.romfs = args.romfs or root / 'build/extracted' / args.version / 'romfs'
|
||||
args.out = args.out or root / 'build' / args.version / 'romfs-patches'
|
||||
args.out = args.out or root / 'build' / args.version / ('romfs-patches-xbox' if args.xbox_prompts else 'romfs-patches')
|
||||
index = json.loads((args.romfs / 'index.json').read_text())
|
||||
assert index['status'] == 'pass'
|
||||
files = {f['path']: f for f in index['files']}
|
||||
|
||||
|
||||
def sarc_members(data):
|
||||
"""name -> (absolute start, end) of every SARC member."""
|
||||
def sarc_nodes(data):
|
||||
"""(name, node offset, absolute start, end) of every SARC member, in SFAT order."""
|
||||
assert data[:4] == b'SARC' and data[6:8] == b'\xff\xfe'
|
||||
header, size, start = struct.unpack_from('<HxxII', data, 4)
|
||||
assert size == len(data) and data[header:header + 4] == b'SFAT'
|
||||
@@ -43,14 +66,20 @@ def sarc_members(data):
|
||||
names = header + 12 + count * 16
|
||||
assert data[names:names + 4] == b'SFNT'
|
||||
names += 8
|
||||
out = {}
|
||||
out = []
|
||||
for i in range(count):
|
||||
_, attribute, lo, hi = struct.unpack_from('<4I', data, header + 12 + i * 16)
|
||||
node = header + 12 + i * 16
|
||||
_, attribute, lo, hi = struct.unpack_from('<4I', data, node)
|
||||
offset = names + (attribute & 0xffffff) * 4
|
||||
out[data[offset:data.index(0, offset, start)].decode()] = (start + lo, start + hi)
|
||||
out.append((data[offset:data.index(0, offset, start)].decode(), node, start + lo, start + hi))
|
||||
return out
|
||||
|
||||
|
||||
def sarc_members(data):
|
||||
"""name -> (absolute start, end) of every SARC member."""
|
||||
return {name: (lo, hi) for name, _, lo, hi in sarc_nodes(data)}
|
||||
|
||||
|
||||
def flvi_keys(data, base):
|
||||
"""(pane name, absolute key offset, keys) of every FLVI (visibility) step track in the BFLAN at data[base:]."""
|
||||
assert data[base:base + 4] == b'FLAN' and data[base + 4:base + 6] == b'\xff\xfe'
|
||||
@@ -77,12 +106,226 @@ def flvi_keys(data, base):
|
||||
return out
|
||||
|
||||
|
||||
def bntx_swap(data, base, pairs):
|
||||
"""Swap the image data of each (name, name) pair of textures in the BNTX at data[base:] (names without the ^
|
||||
suffix). Both of a pair must have the same header from flags to layout (format, size, tiling, mips) and image
|
||||
size, so the bytes of one are a valid image of the other."""
|
||||
assert data[base:base + 4] == b'BNTX' and data[base + 0x20:base + 0x24] == b'NX '
|
||||
count, table = struct.unpack_from('<IQ', data, base + 0x24)
|
||||
found = {}
|
||||
for k in range(count):
|
||||
info = base + struct.unpack_from('<Q', data, base + table + 8 * k)[0]
|
||||
assert data[info:info + 4] == b'BRTI'
|
||||
name = base + struct.unpack_from('<Q', data, info + 0x60)[0]
|
||||
name = data[name + 2:name + 2 + struct.unpack_from('<H', data, name)[0]].decode().split('^')[0]
|
||||
mips = base + struct.unpack_from('<Q', data, info + 0x70)[0]
|
||||
shape = bytes(data[info + 0x10:info + 0x3c]) + bytes(data[info + 0x50:info + 0x54])
|
||||
start, size = base + struct.unpack_from('<Q', data, mips)[0], struct.unpack_from('<I', data, info + 0x50)[0]
|
||||
found[name] = (shape, start, size)
|
||||
for a, b in pairs:
|
||||
assert a in found and b in found, f'{a} or {b} missing from the texture list'
|
||||
(shape_a, at_a, size), (shape_b, at_b, _) = found[a], found[b]
|
||||
assert shape_a == shape_b, f'{a} and {b} differ in format, size or tiling'
|
||||
data[at_a:at_a + size], data[at_b:at_b + size] = data[at_b:at_b + size], data[at_a:at_a + size]
|
||||
|
||||
|
||||
def bflyt_letters(data, base, end, swap):
|
||||
"""Swap the text of the one-letter text panes T_<letter>_<n> (A, B, X or Y) in the BFLYT at data[base:end] by
|
||||
swap (letter -> letter); the names of the panes changed. Same length (one UTF-16 letter and its terminator)."""
|
||||
assert data[base:base + 4] == b'FLYT'
|
||||
pos, changed = 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 == b'txt1':
|
||||
name = data[pos + 12:pos + 36].split(b'\0')[0].decode()
|
||||
length = struct.unpack_from('<H', data, pos + 0x56)[0]
|
||||
at = pos + struct.unpack_from('<I', data, pos + 0x64)[0]
|
||||
letter = data[at:at + 2].decode('utf-16-le') if length == 4 and at > pos else ''
|
||||
if letter in swap and re.fullmatch(r'T_[ABXY]_\d+', name):
|
||||
data[at:at + 2] = swap[letter].encode('utf-16-le')
|
||||
changed.append(name)
|
||||
if not size:
|
||||
break
|
||||
pos += size
|
||||
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
|
||||
Mals/<language>/LayoutMsg/<layout>.msbt, over the text stored in the layout."""
|
||||
assert data[:8] == b'MsgStdBn' and data[0x0c] == 1, 'not a UTF-16 MSBT'
|
||||
sections, pos = {}, 0x20
|
||||
for _ in range(struct.unpack_from('<H', data, 0x0e)[0]):
|
||||
size = struct.unpack_from('<I', data, pos + 4)[0]
|
||||
sections[bytes(data[pos:pos + 4])] = (pos + 16, size)
|
||||
pos = (pos + 16 + size + 15) & ~15
|
||||
labels, (base, _) = {}, sections[b'LBL1']
|
||||
for g in range(struct.unpack_from('<I', data, base)[0]):
|
||||
count, at = struct.unpack_from('<II', data, base + 4 + 8 * g)
|
||||
at += base
|
||||
for _ in range(count):
|
||||
length = data[at]
|
||||
labels[struct.unpack_from('<I', data, at + 1 + length)[0]] = data[at + 1:at + 1 + length].decode()
|
||||
at += 1 + length + 4
|
||||
(base, size), changed = sections[b'TXT2'], []
|
||||
count = struct.unpack_from('<I', data, base)[0]
|
||||
offsets = list(struct.unpack_from(f'<{count}I', data, base + 4)) + [size]
|
||||
for i in range(count):
|
||||
at, length = base + offsets[i], offsets[i + 1] - offsets[i]
|
||||
letter = data[at:at + 2].decode('utf-16-le') if length == 4 else ''
|
||||
if letter in swap and re.fullmatch(r'T_[ABXY]_\d+', labels.get(i, '')):
|
||||
data[at:at + 2] = swap[letter].encode('utf-16-le')
|
||||
changed.append(labels[i])
|
||||
return changed
|
||||
|
||||
|
||||
def cff_index(data, pos):
|
||||
"""([(start, end)] of every item, offSize, end) of the CFF INDEX at data[pos:]."""
|
||||
count = struct.unpack_from('>H', data, pos)[0]
|
||||
if not count:
|
||||
return [], 0, pos + 2
|
||||
size = data[pos + 2]
|
||||
offsets = [int.from_bytes(data[pos + 3 + i * size:pos + 3 + (i + 1) * size], 'big') for i in range(count + 1)]
|
||||
first = pos + 2 + (count + 1) * size
|
||||
return [(first + offsets[i], first + offsets[i + 1]) for i in range(count)], size, first + offsets[-1]
|
||||
|
||||
|
||||
def cff_charstrings(data, cff):
|
||||
"""The offset (from the CFF table) of the CharStrings INDEX: operator 17 of the Top DICT. CID-keyed fonts (ROS,
|
||||
operator 12 30) are refused: their glyphs can use different private dictionaries."""
|
||||
_, _, pos = cff_index(data, cff + data[cff + 2]) # Name INDEX after the header
|
||||
top, _, _ = cff_index(data, pos)
|
||||
(pos, end), operands, found = top[0], [], None
|
||||
while pos < end:
|
||||
b = data[pos]
|
||||
if b <= 21:
|
||||
op = (12, data[pos + 1]) if b == 12 else b
|
||||
pos += 2 if b == 12 else 1
|
||||
assert op != (12, 30), 'CID-keyed CFF'
|
||||
if op == 17:
|
||||
found = operands[-1]
|
||||
operands = []
|
||||
elif b == 28:
|
||||
operands.append(struct.unpack_from('>h', data, pos + 1)[0]); pos += 3
|
||||
elif b == 29:
|
||||
operands.append(struct.unpack_from('>i', data, pos + 1)[0]); pos += 5
|
||||
elif b == 30: # real number: nibbles up to an 0xf
|
||||
pos += 1
|
||||
while data[pos] & 0x0f != 0x0f and data[pos] & 0xf0 != 0xf0:
|
||||
pos += 1
|
||||
pos += 1
|
||||
operands.append(0)
|
||||
elif b <= 246:
|
||||
operands.append(b - 139); pos += 1
|
||||
elif b <= 250:
|
||||
operands.append((b - 247) * 256 + data[pos + 1] + 108); pos += 2
|
||||
else:
|
||||
operands.append(-(b - 251) * 256 - data[pos + 1] - 108); pos += 2
|
||||
assert found is not None, 'no CharStrings in the Top DICT'
|
||||
return found
|
||||
|
||||
|
||||
def otf_glyph(data, cmap, code):
|
||||
"""The glyph id of a code point in the format 4 (platform 3 encoding 1) subtable of the cmap at data[cmap:]."""
|
||||
for k in range(struct.unpack_from('>H', data, cmap + 2)[0]):
|
||||
platform, encoding, offset = struct.unpack_from('>HHI', data, cmap + 4 + 8 * k)
|
||||
p = cmap + offset
|
||||
if (platform, encoding) != (3, 1) or struct.unpack_from('>H', data, p)[0] != 4:
|
||||
continue
|
||||
seg = struct.unpack_from('>H', data, p + 6)[0] // 2
|
||||
for s in range(seg):
|
||||
end = struct.unpack_from('>H', data, p + 14 + 2 * s)[0]
|
||||
start = struct.unpack_from('>H', data, p + 16 + 2 * seg + 2 * s)[0]
|
||||
if not start <= code <= end:
|
||||
continue
|
||||
delta = struct.unpack_from('>h', data, p + 16 + 4 * seg + 2 * s)[0]
|
||||
at = p + 16 + 6 * seg + 2 * s
|
||||
ro = struct.unpack_from('>H', data, at)[0]
|
||||
glyph = code if not ro else struct.unpack_from('>H', data, at + ro + 2 * (code - start))[0]
|
||||
return (glyph + delta) & 0xffff if glyph else 0
|
||||
raise AssertionError(f'U+{code:04X} is not in the font')
|
||||
|
||||
|
||||
def otf_checksum(data):
|
||||
data = bytes(data) + bytes(-len(data) % 4)
|
||||
return sum(struct.unpack(f'>{len(data) // 4}I', data)) & 0xffffffff
|
||||
|
||||
|
||||
def bfotf_swap(data, pairs):
|
||||
"""The BFOTF data with the glyphs of each (code point, code point) pair swapped: CFF outlines and horizontal
|
||||
metrics, then the table checksums. A BFOTF is an OpenType font after an 8-byte header; from offset 4 on, every
|
||||
4-byte word is XORed with one key, which the size field gives away (it holds the font's length)."""
|
||||
key = bytes(x ^ y for x, y in zip(data[4:8], struct.pack('>I', len(data) - 8)))
|
||||
plain = bytearray(data[:4]) + bytearray(x ^ key[i % 4] for i, x in enumerate(data[4:]))
|
||||
font = plain[8:]
|
||||
assert font[:4] == b'OTTO', 'not a CFF OpenType font'
|
||||
tables = {}
|
||||
for k in range(struct.unpack_from('>H', font, 4)[0]):
|
||||
tag, _, offset, length = struct.unpack_from('>4sIII', font, 12 + 16 * k)
|
||||
tables[tag] = (12 + 16 * k, offset, length)
|
||||
cff = tables[b'CFF '][1]
|
||||
strings, size, end = cff_index(font, cff + cff_charstrings(font, cff))
|
||||
hmtx, metrics = tables[b'hmtx'][1], struct.unpack_from('>H', font, tables[b'hhea'][1] + 34)[0]
|
||||
order = list(range(len(strings)))
|
||||
for a, b in pairs:
|
||||
ga, gb = otf_glyph(font, tables[b'cmap'][1], a), otf_glyph(font, tables[b'cmap'][1], b)
|
||||
assert 0 < ga < len(strings) and 0 < gb < len(strings) and ga < metrics and gb < metrics, (a, b, ga, gb)
|
||||
order[ga], order[gb] = order[gb], order[ga]
|
||||
ma, mb = hmtx + 4 * ga, hmtx + 4 * gb
|
||||
font[ma:ma + 4], font[mb:mb + 4] = font[mb:mb + 4], font[ma:ma + 4]
|
||||
# The CharStrings INDEX again with the items in the new order: the same length, so nothing after it moves.
|
||||
body = b''.join(bytes(font[slice(*strings[g])]) for g in order)
|
||||
first = strings[0][0]
|
||||
assert first + len(body) == end
|
||||
font[first:end] = body
|
||||
at, base = first - (len(strings) + 1) * size, 1 # the offset array ends where the first item starts
|
||||
for i, g in enumerate(order + [None]):
|
||||
font[at + i * size:at + (i + 1) * size] = base.to_bytes(size, 'big')
|
||||
if g is not None:
|
||||
base += strings[g][1] - strings[g][0]
|
||||
head = tables[b'head'][1]
|
||||
struct.pack_into('>I', font, head + 8, 0)
|
||||
for entry, offset, length in tables.values():
|
||||
struct.pack_into('>I', font, entry + 4, otf_checksum(font[offset:offset + length]))
|
||||
struct.pack_into('>I', font, head + 8, (0xb1b0afba - otf_checksum(font)) & 0xffffffff)
|
||||
plain[8:] = font
|
||||
return bytes(plain[:4]) + bytes(x ^ key[i % 4] for i, x in enumerate(plain[4:]))
|
||||
|
||||
|
||||
def sarc_replace(data, new):
|
||||
"""The SARC data with the members named in new (name -> bytes) replaced. The header, SFAT and SFNT stay; the
|
||||
members are laid out again in their old order, each at an offset with at least the alignment its old one had."""
|
||||
start = struct.unpack_from('<I', data, 12)[0]
|
||||
out = bytearray(data[:start])
|
||||
for name, node, lo, hi in sorted(sarc_nodes(data), key=lambda n: n[2]):
|
||||
align = min(lo & -lo, 0x2000) if lo else 0x2000
|
||||
out += bytes(-len(out) % align)
|
||||
body = new.get(name, data[lo:hi])
|
||||
struct.pack_into('<II', out, node + 8, len(out) - start, len(out) - start + len(body))
|
||||
out += body
|
||||
struct.pack_into('<I', out, 8, len(out))
|
||||
return bytes(out)
|
||||
|
||||
|
||||
COMMON = {'v121': '110', 'v143': '140'} # the version in the name of each game's Common archive
|
||||
ARCHIVE = f'UI/LayoutArchive/Common.Product.{COMMON[args.version]}.Nin_NX_NVN.blarc.zs'
|
||||
MEMBERS = {
|
||||
'anim/ShortCutBG_00_In.bflan': ('C_CaptureBG_00', 'C_CaptureClearBG_00', 'P_CaptureClearBG_00'),
|
||||
'anim/AutoBuilder_00_In.bflan': ('C_CaptureBG_00',),
|
||||
}
|
||||
KEY_TEXTURES = (('Nt_KeyTexA_00', 'Nt_KeyTexB_00'), ('Nt_KeyTexX_00', 'Nt_KeyTexY_00'))
|
||||
KEY_GLYPHS = ((0xE040, 0xE041), (0xE042, 0xE043)) # A B, X Y in scft/External.bfotf
|
||||
KEY_LETTERS = {'A': 'B', 'B': 'A', 'X': 'Y', 'Y': 'X'} # the face-button diamond around the action prompt
|
||||
FONTS = sorted(p for p in files if re.fullmatch(r'Font/Font(_\w+)?\.Nin_NX_NVN\.bfarc\.zs', p))
|
||||
# The message archive of each language: the newest version in the RomFS (Mals/USen.Product.121.sarc.zs on 1.2.1).
|
||||
MALS = {}
|
||||
for p in files:
|
||||
m = re.fullmatch(r'Mals/(\w+)\.Product\.(\d+)\.sarc\.zs', p)
|
||||
if m and int(m[2]) > MALS.get(m[1], (0, ''))[0]:
|
||||
MALS[m[1]] = (int(m[2]), p)
|
||||
MALS = sorted(p for _, p in MALS.values())
|
||||
LAYOUT_MSG = 'LayoutMsg/DoCommand_00.msbt'
|
||||
|
||||
with (args.romfs / 'romfs.bin').open('rb') as image:
|
||||
def read(path):
|
||||
@@ -96,7 +339,8 @@ with (args.romfs / 'romfs.bin').open('rb') as image:
|
||||
for name, (lo, hi) in sarc_members(pack).items():
|
||||
d = zstd.ZstdDict(pack[lo:hi])
|
||||
dictionaries[d.dict_id] = d
|
||||
raw = read(ARCHIVE)
|
||||
originals = {path: read(path) for path in [ARCHIVE] + (FONTS + MALS if args.xbox_prompts else [])}
|
||||
raw = originals[ARCHIVE]
|
||||
info = zstd.get_frame_info(raw)
|
||||
dictionary = dictionaries[info.dictionary_id]
|
||||
original = zstd.decompress(raw, zstd_dict=dictionary)
|
||||
@@ -115,15 +359,60 @@ for member, panes in MEMBERS.items():
|
||||
changed.append(name)
|
||||
assert sorted(changed) == sorted(panes), (member, changed)
|
||||
patched[member] = changed
|
||||
layout = []
|
||||
if args.aspect:
|
||||
width, height = (int(v) for v in args.aspect.lower().split('x'))
|
||||
spec = importlib.util.spec_from_file_location('ultrawide_layout', Path(__file__).with_name('ultrawide_layout.py'))
|
||||
ultrawide = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ultrawide)
|
||||
layout = ultrawide.apply(blarc, members, width, height)
|
||||
diff = [i for i in range(len(original)) if original[i] != blarc[i]]
|
||||
assert diff and all(any(lo <= i < hi for lo, hi in (members[m] for m in MEMBERS)) for i in diff), diff
|
||||
data = zstd.compress(bytes(blarc), level=19, zstd_dict=dictionary)
|
||||
assert zstd.decompress(data, zstd_dict=dictionary) == bytes(blarc)
|
||||
assert zstd.get_frame_info(data).dictionary_id == info.dictionary_id
|
||||
target = args.out / ARCHIVE
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
line = '\t'.join([ARCHIVE, str(len(data)), hashlib.sha256(data).hexdigest(), hashlib.sha256(raw).hexdigest()])
|
||||
touched = [members[m] for m in (*MEMBERS, *layout)]
|
||||
assert diff and all(any(lo <= i < hi for lo, hi in touched) for i in diff), diff
|
||||
outputs = {ARCHIVE: bytes(blarc)} # RomFS path -> new contents (before compression)
|
||||
if args.xbox_prompts:
|
||||
bntx_swap(blarc, members['timg/__Combined.bntx'][0], KEY_TEXTURES)
|
||||
letters = {}
|
||||
for member, (lo, hi) in members.items():
|
||||
if member.endswith('.bflyt'):
|
||||
changed = bflyt_letters(blarc, lo, hi, KEY_LETTERS)
|
||||
if changed:
|
||||
letters[member] = changed
|
||||
assert 'blyt/DoCommand_00.bflyt' in letters, 'no letter panes in blyt/DoCommand_00.bflyt'
|
||||
outputs[ARCHIVE] = bytes(blarc)
|
||||
assert FONTS, 'no Font/Font*.Nin_NX_NVN.bfarc.zs in the RomFS'
|
||||
for path in FONTS:
|
||||
frame = zstd.get_frame_info(originals[path])
|
||||
bfarc = zstd.decompress(originals[path], zstd_dict=dictionaries.get(frame.dictionary_id))
|
||||
lo, hi = sarc_members(bfarc)['scft/External.bfotf']
|
||||
outputs[path] = sarc_replace(bfarc, {'scft/External.bfotf': bfotf_swap(bfarc[lo:hi], KEY_GLYPHS)})
|
||||
assert MALS, 'no Mals/<language>.Product.<version>.sarc.zs in the RomFS'
|
||||
messages = {}
|
||||
for path in MALS:
|
||||
frame = zstd.get_frame_info(originals[path])
|
||||
mals = bytearray(zstd.decompress(originals[path], zstd_dict=dictionaries.get(frame.dictionary_id)))
|
||||
lo, hi = sarc_members(mals)[LAYOUT_MSG]
|
||||
msbt = bytearray(mals[lo:hi])
|
||||
changed = msbt_letters(msbt, KEY_LETTERS)
|
||||
assert len(changed) == sum(len(v) for v in letters.values()), (path, changed)
|
||||
mals[lo:hi] = msbt
|
||||
outputs[path] = bytes(mals)
|
||||
messages[path] = len(changed)
|
||||
|
||||
lines = []
|
||||
for path, contents in outputs.items():
|
||||
raw = originals[path]
|
||||
dictionary = dictionaries.get(zstd.get_frame_info(raw).dictionary_id)
|
||||
data = zstd.compress(contents, level=19, zstd_dict=dictionary)
|
||||
assert zstd.decompress(data, zstd_dict=dictionary) == contents
|
||||
assert zstd.get_frame_info(data).dictionary_id == zstd.get_frame_info(raw).dictionary_id
|
||||
target = args.out / path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
lines.append('\t'.join([path, str(len(data)), hashlib.sha256(data).hexdigest(), hashlib.sha256(raw).hexdigest()]))
|
||||
(args.out / 'manifest.tsv').write_text('# path\tsize\tsha256\toriginal_sha256 (tools/make_romfs_patches.py)\n'
|
||||
+ line + '\n')
|
||||
print(json.dumps(dict(status='pass', path=ARCHIVE, members=patched, size=len(data), changed_bytes=len(diff))))
|
||||
+ ''.join(line + '\n' for line in lines))
|
||||
print(json.dumps(dict(status='pass', path=ARCHIVE, members=patched, changed_bytes=len(diff),
|
||||
ultrawide_layout=dict(aspect=args.aspect, members=len(layout)) if args.aspect else None,
|
||||
xbox_prompts=dict(textures=KEY_TEXTURES, letters=letters, messages=messages, fonts=FONTS)
|
||||
if args.xbox_prompts else None)))
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Fold the loose files of a shader cache into <cache>/shader-cache.pack and delete them.
|
||||
|
||||
The translator writes four files per shader into the cache (runtime/gfx_shader.c): <key>.spv, <key>.res.json,
|
||||
<name>.use and in/<input id>.in, about 117,000 files after an install, which are slow to copy, back up, scan and sync.
|
||||
totk.exe reads the pack first and the loose files after it, and keeps writing new translations as loose files, so
|
||||
this tool can run again at any time the game is closed: it merges the existing pack with the loose files (a loose file
|
||||
wins over a packed one of the same name) and writes the pack anew. The installer runs it after preparing the shaders.
|
||||
|
||||
Layout, little-endian (gfx_shader.c, section "cache pack"): b'TKSPACK1', u32 count, u32 0, then count entries of 48
|
||||
bytes sorted by (kind, name): name (the 32 bytes the file name's 64 hex digits spell), u32 kind (1 .spv, 2 .res.json,
|
||||
3 .use, 4 in/*.in), u32 size, u64 offset; then the data.
|
||||
|
||||
python tools\\pack_shader_cache.py <install folder>\\shader-cache [--keep]
|
||||
|
||||
--keep leaves the loose files in place. Close the game first: a running totk.exe has read the old pack's index.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MAGIC = b'TKSPACK1'
|
||||
PACK = 'shader-cache.pack'
|
||||
KINDS = {'.spv': 1, '.res.json': 2, '.use': 3, '.in': 4}
|
||||
NAME = re.compile(r'([0-9a-f]{64})(\.spv|\.res\.json|\.use|\.in)')
|
||||
USE_SIZE = 20 # gfx_shader.c use_store: magic, version, three masks
|
||||
|
||||
|
||||
def loose_files(cache):
|
||||
"""{(kind, name): path} of the cache's loose files; *.in only in in/."""
|
||||
found = {}
|
||||
for folder, kinds in ((cache, (1, 2, 3)), (cache / 'in', (4,))):
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
with os.scandir(folder) as it:
|
||||
for e in it:
|
||||
m = NAME.fullmatch(e.name)
|
||||
if not m or not e.is_file():
|
||||
continue
|
||||
kind = KINDS[m.group(2)]
|
||||
if kind in kinds:
|
||||
found[(kind, bytes.fromhex(m.group(1)))] = Path(e.path)
|
||||
return found
|
||||
|
||||
|
||||
def read_pack(path):
|
||||
"""{(kind, name): (offset, size)} of an existing pack; {} when there is none or it fails a check."""
|
||||
try:
|
||||
f = open(path, 'rb')
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
with f:
|
||||
size = os.fstat(f.fileno()).st_size
|
||||
head = f.read(16)
|
||||
if len(head) != 16 or head[:8] != MAGIC:
|
||||
print(f'{path}: not a cache pack, rebuilt from the loose files only', flush=True)
|
||||
return {}
|
||||
count = struct.unpack_from('<I', head, 8)[0]
|
||||
raw = f.read(48 * count)
|
||||
if len(raw) != 48 * count:
|
||||
print(f'{path}: truncated index, rebuilt from the loose files only', flush=True)
|
||||
return {}
|
||||
out = {}
|
||||
for i in range(count):
|
||||
name, kind, length, offset = struct.unpack_from('<32sIIQ', raw, 48 * i)
|
||||
if offset + length <= size:
|
||||
out[(kind, name)] = (offset, length)
|
||||
return out
|
||||
|
||||
|
||||
def valid(kind, data):
|
||||
return len(data) == USE_SIZE if kind == 3 else len(data) > 0
|
||||
|
||||
|
||||
def pack(cache, keep=False):
|
||||
cache = Path(cache)
|
||||
out = cache / PACK
|
||||
packed = read_pack(out)
|
||||
loose = loose_files(cache)
|
||||
keys = sorted(set(packed) | set(loose))
|
||||
tmp = out.with_name(PACK + '.tmp')
|
||||
index, written, skipped, offset = [], [], 0, 16 + 48 * len(keys)
|
||||
old = open(out, 'rb') if packed else None
|
||||
try:
|
||||
with open(tmp, 'wb') as f:
|
||||
f.write(bytes(offset)) # header and index, written once the offsets are known
|
||||
for key in keys:
|
||||
data = None
|
||||
if key in loose: # a valid loose file, else the packed copy, else nothing
|
||||
try:
|
||||
data = loose[key].read_bytes()
|
||||
except OSError:
|
||||
pass
|
||||
if data is not None and valid(key[0], data):
|
||||
written.append(loose[key])
|
||||
else:
|
||||
data = None
|
||||
if data is None and key in packed:
|
||||
old.seek(packed[key][0])
|
||||
data = old.read(packed[key][1])
|
||||
if data is None or not valid(key[0], data):
|
||||
skipped += 1
|
||||
continue
|
||||
f.write(data)
|
||||
index.append(struct.pack('<32sIIQ', key[1], key[0], len(data), offset))
|
||||
offset += len(data)
|
||||
f.seek(0)
|
||||
f.write(MAGIC + struct.pack('<II', len(index), 0) + b''.join(index))
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
finally:
|
||||
if old:
|
||||
old.close()
|
||||
os.replace(tmp, out)
|
||||
if not keep:
|
||||
for p in written:
|
||||
p.unlink(missing_ok=True)
|
||||
try:
|
||||
(cache / 'in').rmdir() # only when empty
|
||||
except OSError:
|
||||
pass
|
||||
return len(index), len(written), skipped, offset
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description='Fold a shader cache into shader-cache.pack.')
|
||||
ap.add_argument('cache', type=Path, help='the shader-cache folder next to totk.exe')
|
||||
ap.add_argument('--keep', action='store_true', help='keep the loose files')
|
||||
a = ap.parse_args()
|
||||
if not a.cache.is_dir():
|
||||
sys.exit(f'{a.cache} is not a folder')
|
||||
n, new, skipped, size = pack(a.cache, a.keep)
|
||||
print(f'{a.cache / PACK}: {n} files, {size / 2**20:.0f} MiB ({new} loose files folded in'
|
||||
f'{", " + str(skipped) + " unreadable or empty skipped" if skipped else ""})', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -91,6 +91,8 @@ def main():
|
||||
else:
|
||||
print(f'package_build: {f} not in {build}, skipped')
|
||||
n = copy_tree(build / 'romfs-patches', out / 'romfs-patches')
|
||||
if (build / 'romfs-patches-xbox').is_dir(): # pad_layout = xbox_prompts (make_romfs_patches.py --xbox-prompts)
|
||||
n += copy_tree(build / 'romfs-patches-xbox', out / 'romfs-patches-xbox')
|
||||
n += copy_tree(exefs, out / 'exefs')
|
||||
n += copy_tree(fonts, out / 'shared-fonts')
|
||||
if not a.no_shader_cache and (build / 'shader-cache').is_dir():
|
||||
|
||||
@@ -68,6 +68,15 @@ def read_list(path):
|
||||
return shader_list.history(ROOT / 'refs' / 'MacNativeGames')
|
||||
|
||||
|
||||
def pipeline_variants(key):
|
||||
"""The vertex and fragment variant names of an R/B row's pipeline key. Since MacNativeGames f024ef04 (2026-10-28) the
|
||||
TotK list keeps keys packed, "<vs variant>:<fs variant>:<the rest in hex>"; older revisions and contribution files
|
||||
hold the whole key in hex, "<vs variant>\\0<fs variant>\\0" + a Metal descriptor (docs/shader-list.md there)."""
|
||||
if ':' in key:
|
||||
return key.split(':', 2)[:2]
|
||||
return [n.decode('ascii', 'replace') for n in bytes.fromhex(key).split(b'\0')[:2]]
|
||||
|
||||
|
||||
def parse_list(text):
|
||||
"""V rows as (key, deps); priority per shader key from the warm rows (variant names start with the key)."""
|
||||
variants, prio = [], {}
|
||||
@@ -85,10 +94,9 @@ def parse_list(text):
|
||||
elif f[0] == 'C' and len(f) == 3:
|
||||
warm(f[1][:64], f[2])
|
||||
elif f[0] in ('R', 'B') and len(f) >= 3:
|
||||
names = bytes.fromhex(f[1]).split(b'\0')[:2] # "<vs variant>\0<fs variant>\0" + a Metal descriptor
|
||||
for n in names:
|
||||
for n in pipeline_variants(f[1]):
|
||||
if len(n) >= 64:
|
||||
warm(n[:64].decode('ascii', 'replace'), f[-1])
|
||||
warm(n[:64], f[-1])
|
||||
return variants, prio
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# Adapted from MacNativeGames games/totk/scripts/ultrawide_layout.py @ 933b6a0 (MIT, Copyright (c) 2026
|
||||
# MacNativeGames contributors). Used by tools/make_romfs_patches.py --aspect.
|
||||
"""The game's UI layouts for a canvas wider than 16:9 (output_res = 3440x1440 in totk.ini), edited in place in the
|
||||
decompressed UI/LayoutArchive/Common.Product.*.blarc (tools/make_romfs_patches.py --aspect).
|
||||
|
||||
The game lays its UI out on a 1920x1080 grid and maps that grid onto the canvas, so on a 21:9 canvas every layout comes
|
||||
out 34% too wide. Here each layout's root pane is narrowed to 16:9 (scale x = 16/9 over the aspect), backgrounds that
|
||||
must cover the whole screen are widened back, and the HUD's parts move out to the screen's corners. The pane list and
|
||||
the distances are the ones the community TOTK-Ultrawide mod for emulators found for 1.0.0 .. 1.2.1 (GPL; only those
|
||||
values are taken, no code; THIRD_PARTY_NOTICES.md). This is an independent implementation, checked against 1.4.3's
|
||||
Common.Product.140: every pane is found by name and every pane field and animation key holds the value the edit
|
||||
expects before it is written.
|
||||
"""
|
||||
import struct
|
||||
|
||||
# BFLYT pane sections (pan1, pic1, txt1, ...): name at +12 (24 bytes), translate x / y at +44 / +48, scale x / y at +68 / +72.
|
||||
PANE_MAGICS = (b'pan1', b'pic1', b'txt1', b'wnd1', b'bnd1', b'prt1', b'ali1', b'scr1')
|
||||
FIELD = {'x': 44, 'y': 48, 'scale_x': 68, 'scale_y': 72}
|
||||
|
||||
# Full-screen layouts whose root pane stays as wide as the canvas.
|
||||
KEEP_ROOT = {'AppMenuBG_00', 'PauseMenuBG_00', 'ShopMenuParasailBG_00', 'ShortCutBG_00', 'TitleMenuBG_00', 'EventFade_00',
|
||||
'EventFadeWipe_00', 'CameraShutter_00', 'PaAppPictureBookDetail_00', 'AppMenuOverlay_00'}
|
||||
|
||||
def edits(k, hud_corner):
|
||||
"""(layout, pane, field, value) for the aspect: k = 16/9 over the aspect (the root's scale), w = how far a 1920-wide
|
||||
layout's edge moves out in its own units (960 * (1 / k - 1)). OFF moves a pane out of sight."""
|
||||
w, OFF = 960 * (1 / k - 1), 10000.0
|
||||
wide = 1 / k
|
||||
out = [
|
||||
('AppAlbum_00', 'N_FadeInOut_00', 'scale_x', k), ('AppAlbum_00', 'N_Open_01', 'scale_x', wide),
|
||||
*[('AppMap_00', f'N_BlurInOut_0{i}', 'scale_x', wide) for i in range(4)],
|
||||
('AppMap_00', 'N_MiniMap_00', 'x', -270 * (1 - k)),
|
||||
('AppMenuOverlay_00', 'N_FadeInOut_00', 'scale_x', k), ('AppMenuOverlay_00', 'N_FadeInOut_01', 'scale_x', k),
|
||||
('AppMenuOverlay_00', 'N_Icon_00', 'scale_x', k), ('AppMenuOverlay_00', 'N_TitleOne_01', 'scale_x', k),
|
||||
('AppOpenDemoWindow_00', 'P_BG_00', 'scale_x', wide), ('AutoBuilder_00', 'N_BG_00', 'scale_x', wide),
|
||||
('CameraPointer_00', 'N_InOutScope_00', 'scale_x', wide), ('CameraPointer_00', 'N_InOutScope_00', 'scale_y', wide),
|
||||
('CameraPointer_00', 'N_Type_01', 'scale_x', k), ('CameraPointer_00', 'Pa_CameraFocus_00', 'scale_x', wide),
|
||||
('CameraPointer_00', 'S_Scissor_00', 'scale_x', wide), ('CameraPointer_00', 'W_Shadow_01', 'scale_x', wide),
|
||||
('CameraSystemWindow_00', 'N_AlbumCon_00', 'scale_x', k), ('CameraSystemWindow_00', 'N_InOut_00', 'scale_x', wide),
|
||||
('CameraSystemWindow_00', 'Pa_BtnL_00', 'scale_x', k), ('CameraSystemWindow_00', 'Pa_BtnR_00', 'scale_x', k),
|
||||
('CameraSystemWindow_00', 'T_Text_00', 'scale_x', k),
|
||||
('ChallengeLog_00', 'C_CaptureUp_00', 'x', OFF), ('ChallengeLog_00', 'C_CaptureUp_01', 'x', OFF),
|
||||
('GameOver_00', 'P_BG_00', 'scale_x', wide),
|
||||
('LoadingFade_00', 'N_Base_00', 'scale_x', wide), ('LoadingFade_00', 'N_Logo_00', 'x', w),
|
||||
('LoadingFade_00', 'N_Status_00', 'x', w), ('LoadingFade_00', 'N_Tips_00', 'x', -w),
|
||||
('LoadingFade_00', 'P_BG_00', 'scale_x', wide), ('LoadingFade_00', 'P_SideBGDeco_00', 'scale_x', k),
|
||||
('LoadingFade_00', 'Pa_IconPlayer_00', 'scale_x', k), ('LoadingFade_00', 'Pa_LargeDungeonMap_00', 'scale_x', k),
|
||||
('LoadingFade_00', 'Pa_LargeDungeonMap_01', 'scale_x', k), ('LoadingFade_00', 'Pa_Map_00', 'scale_x', k),
|
||||
('LoadingFade_00', 'Pa_Map_01', 'scale_x', k),
|
||||
('MessageLog_01', 'P_Capture_00', 'scale_x', wide), ('OwnedHorseList_00', 'N_BG_00', 'scale_x', wide),
|
||||
('PaCommonBtnTitle_00', 'B_Hit_00', 'x', OFF), ('PaCommonBtnTitle_00', 'N_Cursor_00', 'x', OFF),
|
||||
('PaLargeDungeonMap_00', 'P_BG_00', 'scale_x', wide), ('PaMessageBtn_00', 'B_Hit_00', 'x', OFF),
|
||||
('PaRecipeCardList_00', 'P_BG_00', 'scale_x', wide),
|
||||
('SaveLoadIcon_00', 'RootPane', 'x', -960 * (1 - k)),
|
||||
('ShopMenu_00', 'N_BG_00', 'scale_x', wide), ('StaffRoll_00', 'N_BG_00', 'scale_x', wide),
|
||||
]
|
||||
# Menus framed by two side decorations, a header and a background: the frame stays at the screen's edges.
|
||||
for layout, deco in (('PauseMenuOverlay_00', 'P_Deco{}_00'), ('SystemLoadList_00', 'P_Deco{}_00'),
|
||||
('SystemOption_00', 'P_Deco{}_00'), ('SystemSaveLoad_00', 'P_Deco{}_00')):
|
||||
out += [(layout, deco.format('L'), 'scale_x', k), (layout, deco.format('L'), 'x', -(675 + w) * k),
|
||||
(layout, deco.format('R'), 'scale_x', k), (layout, deco.format('R'), 'x', (737 + w) * k)]
|
||||
if layout == 'PauseMenuOverlay_00':
|
||||
out += [(layout, 'P_FooterBase_00', 'scale_x', wide), (layout, 'S_DecoScissor_00', 'scale_x', wide)]
|
||||
else:
|
||||
out += [(layout, 'A_Title_00', 'scale_x', k), (layout, 'N_Header_00', 'scale_x', wide),
|
||||
(layout, 'N_SystemBG_00', 'scale_x', wide)]
|
||||
out += [('SystemActionGuide_00', 'A_Title_00', 'scale_x', k), ('SystemActionGuide_00', 'N_Header_00', 'scale_x', wide),
|
||||
('SystemActionGuide_00', 'N_SystemBG_00', 'scale_x', wide),
|
||||
('SystemActionGuide_00', 'P_DecoL_01', 'scale_x', k), ('SystemActionGuide_00', 'P_DecoL_01', 'x', -(675 + w) * k),
|
||||
('SystemActionGuide_00', 'P_DecoL_02', 'scale_x', k), ('SystemActionGuide_00', 'P_DecoL_02', 'x', (675 + w) * k)]
|
||||
if hud_corner: # the HUD in the screen's corners instead of the middle 16:9
|
||||
right = 960 * (1 - k)
|
||||
out += [(name, 'RootPane', 'x', right) for name in ('EnvironmentInfo_00', 'EventSkip_00', 'InstantTipsAppMenu_00',
|
||||
'InstantTipsWithPict_00', 'InstantTips_00', 'NumDisplaySecond_00',
|
||||
'NumDisplay_00', 'PickUpWin_00')]
|
||||
out += [('Location_00', 'N_Small_00', 'x', -(388 + w)),
|
||||
('PlayerStatus3D_00', 'N_ExtraStaminaTarget_01', 'x', -(745 + w)),
|
||||
('PlayerStatus_00', 'N_DevilDemoTargetC_00', 'x', -(284 + w)),
|
||||
('PlayerStatus_00', 'N_DevilDemoTargetLT_00', 'x', -(576 + w)),
|
||||
('PlayerStatus_00', 'N_HeartGaugeTargetLT_00', 'x', -(576 + w)),
|
||||
('PlayerStatus_00', 'N_HeartPos_00', 'x', -w),
|
||||
('PlayerStatus_00', 'Pa_CompanionLife_00', 'x', -(840 + w))]
|
||||
return out
|
||||
|
||||
def anim_edits(k, hud_corner):
|
||||
"""(animation, byte offset, value the game ships, new value): floats inside BFLAN key tables."""
|
||||
w = 960 * (1 / k - 1)
|
||||
out = [('PaMapIconDragonTears_00_Zoom', 288, 1.08, 1.08 / k), ('PaMapIconDragonTears_00_Zoom', 300, 1.28, 1.28 / k)]
|
||||
if hud_corner: # the minimap's slide-in keys end at x 765, the right edge of a 16:9 layout
|
||||
out += [('AppMap_00_MiniMap', o, 765.0, 765 + w) for o in (872, 880, 1628, 1636, 3264, 3272, 3468, 3476)]
|
||||
return out
|
||||
|
||||
def panes(blarc, lo, hi):
|
||||
"""pane name -> absolute section offset, first occurrence, for the BFLYT at blarc[lo:hi]."""
|
||||
assert blarc[lo:lo + 4] == b'FLYT', 'not a BFLYT'
|
||||
head, count = struct.unpack_from('<H', blarc, lo + 6)[0], struct.unpack_from('<H', blarc, lo + 0x10)[0]
|
||||
pos, out = lo + head, {}
|
||||
for _ in range(count):
|
||||
magic, size = bytes(blarc[pos:pos + 4]), struct.unpack_from('<I', blarc, pos + 4)[0]
|
||||
if magic in PANE_MAGICS:
|
||||
out.setdefault(bytes(blarc[pos + 12:pos + 36]).split(b'\0')[0].decode(), pos)
|
||||
pos += size
|
||||
assert pos <= hi
|
||||
return out
|
||||
|
||||
def find_pane(found, layout, name):
|
||||
"""The pane of that exact name (a version that renamed it stops the script)."""
|
||||
if name not in found: raise SystemExit(f'{layout}: no pane {name}')
|
||||
return found[name]
|
||||
|
||||
def apply(blarc, members, width, height, hud_corner=True):
|
||||
"""Edits blarc (bytearray) in place for a width x height canvas. Returns the members it changed."""
|
||||
k = (16 / 9) / (width / height)
|
||||
assert k < 1, 'only for canvases wider than 16:9'
|
||||
changed, layouts = set(), {}
|
||||
unknown = sorted(n for n in KEEP_ROOT if f'blyt/{n}.bflyt' not in members)
|
||||
if unknown: raise SystemExit(f'KEEP_ROOT names no layout: {unknown}')
|
||||
def pane_offset(layout, pane):
|
||||
member = f'blyt/{layout}.bflyt'
|
||||
if member not in members: raise SystemExit(f'{layout}: no layout {member} in the archive')
|
||||
if member not in layouts: layouts[member] = panes(blarc, *members[member])
|
||||
changed.add(member)
|
||||
return find_pane(layouts[member], layout, pane)
|
||||
for member in sorted(m for m in members if m.startswith('blyt/') and m.endswith('.bflyt')):
|
||||
if member[5:-6] in KEEP_ROOT: continue
|
||||
at = pane_offset(member[5:-6], 'RootPane') + FIELD['scale_x']
|
||||
have = struct.unpack_from('<f', blarc, at)[0]
|
||||
if abs(have - 1.0) > 1e-3: raise SystemExit(f'{member[5:-6]} RootPane scale_x: {have}, expected 1.0')
|
||||
struct.pack_into('<f', blarc, at, k)
|
||||
# Every edit's value at k = 1 is the value the game ships (a scale of 1, the 16:9 position): checked before writing,
|
||||
# so a version with other numbers stops here instead of getting wrong positions. OFF moves a pane away: not checked.
|
||||
for (layout, pane, field, value), (_, _, _, shipped) in zip(edits(k, hud_corner), edits(1.0, hud_corner)):
|
||||
at = pane_offset(layout, pane) + FIELD[field]
|
||||
have = struct.unpack_from('<f', blarc, at)[0]
|
||||
if value != 10000.0 and abs(have - shipped) > 1e-3: raise SystemExit(f'{layout} {pane} {field}: {have}, expected {shipped}')
|
||||
struct.pack_into('<f', blarc, at, value)
|
||||
for anim, offset, shipped, value in anim_edits(k, hud_corner):
|
||||
member = f'anim/{anim}.bflan'
|
||||
if member not in members: raise SystemExit(f'{anim}: no animation {member} in the archive')
|
||||
lo, hi = members[member]
|
||||
if offset + 4 > hi - lo: raise SystemExit(f'{anim}: too short for +{offset}')
|
||||
have = struct.unpack_from('<f', blarc, lo + offset)[0]
|
||||
if abs(have - shipped) >= 1e-4: raise SystemExit(f'{anim} +{offset}: {have}, expected {shipped}')
|
||||
struct.pack_into('<f', blarc, lo + offset, value); changed.add(member)
|
||||
return sorted(changed)
|
||||
+28
-5
@@ -3,7 +3,7 @@
|
||||
# file. Keys are case-insensitive; '#' and ';' start a comment; [sections] only group lines.
|
||||
|
||||
[frame rate]
|
||||
# Frame rate limit: 30, 60, 120, 144, any whole rate from 30 to 360, or display (the refresh rate of the window's
|
||||
# Frame rate limit: 30, 60, 90, 120, 144, any whole rate from 30 to 360, or display (the refresh rate of the window's
|
||||
# monitor). Above 60 is experimental and needs realtime = 1. Changes while the game runs take effect at once.
|
||||
# fps_limit = 60
|
||||
# Older key, read only while fps_limit is unset: fps60 = 0 means a limit of 30.
|
||||
@@ -44,7 +44,7 @@
|
||||
# pipe_prebuild = 1
|
||||
# Threads that translate known shaders at start (1 to 64; default: half the CPU threads, at most 16).
|
||||
# prewarm_xlate = 8
|
||||
# Vulkan graphics pipeline library. 0 if the driver misbehaves with it.
|
||||
# Vulkan graphics pipeline library, 0 or 1. Default: on, except on AMD's driver for RDNA 1 and 2 (RX 5000 and 6000).
|
||||
# gpl = 1
|
||||
|
||||
# Textures and memory.
|
||||
@@ -87,6 +87,8 @@
|
||||
# Vulkan.
|
||||
# Vulkan validation layer: checks every call and slows the game down a lot. Needs the layer installed (Vulkan SDK).
|
||||
# vk_validation = 0
|
||||
# A thread of its own makes the Vulkan calls of each frame, 0 or 1. Default: on, except on AMD GPUs.
|
||||
# vk_recorder = 1
|
||||
# Dynamic vertex input, 0 or 1. Default: on, except on Intel's older integrated GPU drivers (Vulkan 1.3.215 and older).
|
||||
# vk_dyn_vertex = 1
|
||||
# Pin the shader subgroup size to 32 when the GPU allows it.
|
||||
@@ -95,7 +97,8 @@
|
||||
[display]
|
||||
# Presentation mode: mailbox (default when the driver offers it, vsync otherwise), fifo (vsync) or immediate.
|
||||
# vk_present = mailbox
|
||||
# Window client area in pixels (640x360 to 7680x4320), and borderless fullscreen. F11 switches fullscreen and saves it
|
||||
# Window client area in screen pixels (640x360 to 7680x4320), and borderless fullscreen. Windows display scaling does
|
||||
# not apply: at 150% scale, 1920x1080 covers the area 1280x720 covers at 100%. F11 switches fullscreen and saves it
|
||||
# here. Changes while the game runs take effect at once.
|
||||
# window_size = 1920x1080
|
||||
# fullscreen = 0
|
||||
@@ -106,7 +109,11 @@
|
||||
# gpu_index = 0
|
||||
# gpu_name = NVIDIA
|
||||
# Game resolution: the size the game draws its picture and interface in (1280x720 to 7680x4320). Above 1920x1080 the
|
||||
# game's texture heap and memory pool grow with it. Read at start.
|
||||
# game's texture heap and memory pool grow with it. A size wider than 16:9, such as 3440x1440 on a 21:9 monitor, widens
|
||||
# the camera's view to that shape and lays the HUD out for it. That is 1.4.3 only (1.2.1 keeps the 16:9 view), and the
|
||||
# installer prepares the UI layout for four sizes: 2560x1080, 3440x1440, 3840x1600 and 5120x2160. Any other shape
|
||||
# (16:10, 32:9) gets the camera's view but a 16:9 HUD, stretched over the picture, unless you build its layout with
|
||||
# tools/make_romfs_patches.py --aspect WxH (wider than 16:9 only). Read at start.
|
||||
# output_res = 1920x1080
|
||||
# 3D render scale: the scene's size relative to the game resolution, game (5/6, the game's own 1600x900 at 1080p) or
|
||||
# a percentage (25 to 400, at most 4096 pixels a side); above 100 supersamples the scene (anti-aliasing). Read at start.
|
||||
@@ -115,6 +122,11 @@
|
||||
# from 0 (off) to 100. Changes while the game runs take effect at once.
|
||||
# upscaler = fsr
|
||||
# sharpness = 80
|
||||
# Picture shape when the window's shape differs from the game resolution's (a 16:9 picture in a 21:9 fullscreen
|
||||
# window): fit, the default, keeps the shape, with black bars at the sides or above and below, so a 16:9 game in a wider
|
||||
# window is no longer stretched as it used to be; stretch fills the window. Changes while the game runs take effect at
|
||||
# once.
|
||||
# aspect = fit
|
||||
|
||||
[language]
|
||||
# Language: a Windows locale such as de-DE, or a Switch code (en-US, ja, zh-Hant, ...). Default: the Windows locale.
|
||||
@@ -145,8 +157,13 @@
|
||||
# rumble_triggers = 0
|
||||
# Which gamepad button is Switch A: nintendo (by position: east A, south B, west Y, north X), xbox (south A, east B,
|
||||
# west X, north Y, as an Xbox pad labels them; cross A on a PlayStation pad) or auto (nintendo for a pad with Nintendo
|
||||
# labels, xbox for the others). The bind_ lines below apply on top. Changes while the game runs take effect at once.
|
||||
# labels, xbox for the others). xbox_prompts: the buttons as nintendo, and the game's prompts swapped to the Xbox
|
||||
# labels (A with B, X with Y; it loads romfs-patches-xbox, docs/controls.md, "Xbox button prompts"). The bind_ lines
|
||||
# below apply on top. Changes while the game runs take effect at once; the xbox_prompts prompts at the next start.
|
||||
# pad_layout = auto
|
||||
# Gamepad stick dead zone in percent of full deflection (0 to 50). Raise it for a stick that drifts: inside the circle
|
||||
# the stick reads centred, and the rest of its travel is scaled to start there.
|
||||
# stick_deadzone = 8
|
||||
# Mouse camera sensitivity (0 to 10; 0 = no mouse camera and no cursor capture).
|
||||
# mouse_sens = 1
|
||||
# Write the input of this run to a file that replays as a script (docs/controls.md, "Scripted input"). Default: off.
|
||||
@@ -157,6 +174,7 @@
|
||||
# A line replaces that button's defaults (pad and keyboard); buttons without a line keep them. Stick directions take keys
|
||||
# only. Invalid entries are logged and skipped. Changes while the game runs take effect at once.
|
||||
# pad names: south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down
|
||||
# lpaddle1 rpaddle1 lpaddle2 rpaddle2 (the back buttons of an Xbox Elite, DualSense Edge or Steam Controller)
|
||||
# key names: a-z 0-9 f2-f7 f9 f12 num0-num9 space tab enter backspace lshift rshift lctrl rctrl lalt capslock insert
|
||||
# delete home end pageup pagedown up down left right num_add num_sub num_mul num_div num_decimal minus equals
|
||||
# lbracket rbracket semicolon apostrophe comma period slash backslash grave mouse_l mouse_r mouse_m mouse_x1 mouse_x2
|
||||
@@ -180,3 +198,8 @@
|
||||
# takes longer than flight_spike_ms (0 to 60000).
|
||||
# flight = 0
|
||||
# flight_spike_ms = 100
|
||||
# Largest render target (bytes, 0 to 1073741824) that goes back to its game memory when the game waits for a GPU fence.
|
||||
# A game that reads a larger target without invalidating it sees stale bytes at the default. With trace = gfx the log
|
||||
# names each dirty target over the limit that stays on the GPU ("fence write-back skipped-for-size") and each one over
|
||||
# 65536 bytes that went back ("over-default"). Raising it costs a copy per such target and wait.
|
||||
# wb_fence_limit = 65536
|
||||
Reference in new issue
Block a user