mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Compare commits
No files matched your search
@@ -23,10 +23,23 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last-good
|
||||
attributes:
|
||||
label: Last version that worked
|
||||
description: If an earlier version did not crash at this point, which one.
|
||||
- type: textarea
|
||||
id: system
|
||||
attributes:
|
||||
label: GPU, driver and Windows version
|
||||
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
|
||||
label: System info
|
||||
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. On a Steam Deck, add LCD or OLED.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: Mods and cheats
|
||||
description: The folders in mods/ with their download links, or "none". Say whether it also crashes without them.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -56,10 +69,32 @@ body:
|
||||
label: What you did before the crash
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### The log
|
||||
|
||||
- The log of the run that crashed is `logs\totk.log` in the folder of `totk.exe` (F1, Advanced tab, Open log folder). Copy it before you start the game again: the next start renames it to `totk.1.log`, and after five starts it is gone.
|
||||
- Attach the whole file. Do not cut or edit it: the crash report is at the end, the GPU and driver lines are at the start.
|
||||
- The log says `device lost` or `VK_ERROR_DEVICE_LOST` and the GPU is an NVIDIA one: set GPU pass marks to On (F1, Advanced tab, Diagnostics), restart the game, make it crash again and attach that log. The `vk: pass marks` lines then name what the GPU was drawing.
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Attach logs\totk.log from the folder of totk.exe. Copy it before you start the game again, because the next start overwrites it.
|
||||
description: Drag totk.log into this field.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Save and input recording
|
||||
|
||||
When the crash comes back at one place in the game (an area, a shrine, a cutscene, a quest step), the save that gets there lets us crash it on our machine.
|
||||
|
||||
1. Zip the `profile` folder next to `totk.exe` and attach it. Say which slot you load, or that you use the autosave.
|
||||
2. If it takes more than walking a few steps from the save: turn on Input recording (F1, Advanced tab, Diagnostics), restart the game, load the save, make it crash, and attach `input-record.txt` from the logs folder together with the `totk.log` of that run.
|
||||
- type: textarea
|
||||
id: save
|
||||
attributes:
|
||||
label: Save and input recording
|
||||
description: Drag the zipped profile folder and input-record.txt into this field, when the crash comes back at one place.
|
||||
@@ -23,24 +23,37 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last-good
|
||||
attributes:
|
||||
label: Last version that worked
|
||||
description: If an earlier version drew this correctly, which one.
|
||||
- type: textarea
|
||||
id: system
|
||||
attributes:
|
||||
label: GPU, driver and Windows version
|
||||
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
|
||||
label: System info
|
||||
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. It lists the settings you changed too. On a Steam Deck, add LCD or OLED.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: Mods and cheats
|
||||
description: The folders in mods/ with their download links, or "none". Say whether the glitch also shows without them. Turn off ReShade and other overlays that draw into the game before you report.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshot
|
||||
attributes:
|
||||
label: Screenshot or video
|
||||
description: Drag the file into this field. Say where in the game it was taken.
|
||||
description: Drag the file into this field. Say where in the game it was taken. Flicker needs a video.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: What looks wrong and when
|
||||
description: Does it show all the time, in one place, or after some time in play? Which settings did you change from the defaults?
|
||||
description: Does it show all the time, in one place, or after some time in play? Does it go away after a restart, or when you change a setting?
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -56,4 +69,6 @@ body:
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Attach logs\totk.log from the folder of totk.exe.
|
||||
description: Drag logs\totk.log from the folder of totk.exe into this field, from the run that showed the glitch. Copy it before you start the game again.
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Other bug
|
||||
description: Sound, controls, performance, the settings menu, the installer, or game logic.
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this bug.
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
options:
|
||||
- Audio
|
||||
- Controls and input
|
||||
- Performance and stutter
|
||||
- Settings menu and overlay
|
||||
- Installer
|
||||
- Game logic and physics
|
||||
- Something else
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: GPU, driver and Windows version
|
||||
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Can you keep playing?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No, it stops the game or a quest"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What happened and how to reproduce it
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Attach logs\totk.log from the folder of totk.exe.
|
||||
@@ -0,0 +1,108 @@
|
||||
name: Performance
|
||||
description: Low frame rate, freezes, hitches, stutter or long loading.
|
||||
labels: [performance]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first.
|
||||
|
||||
Shaders the setup did not prepare are built the first time they are needed, so a short hitch the first time an area or effect shows is expected. Report it when it happens again in the same place.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this problem.
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last-good
|
||||
attributes:
|
||||
label: Last version that ran better
|
||||
description: If an earlier version was faster in the same place, which one.
|
||||
- type: textarea
|
||||
id: system
|
||||
attributes:
|
||||
label: System info
|
||||
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. It lists the settings you changed too. On a Steam Deck, add LCD or OLED.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: machine
|
||||
attributes:
|
||||
label: Machine
|
||||
options:
|
||||
- Desktop PC
|
||||
- Laptop, on mains power
|
||||
- Laptop, on battery
|
||||
- Steam Deck
|
||||
- Other handheld PC
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: Mods and cheats
|
||||
description: The folders in mods/ with their download links, or "none".
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: kind
|
||||
attributes:
|
||||
label: What kind of problem?
|
||||
options:
|
||||
- The frame rate is low all the time
|
||||
- Short freezes or hitches that come back in the same place
|
||||
- The frame rate is fine, but motion is not smooth
|
||||
- It gets slower the longer I play
|
||||
- Loading takes long
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: where
|
||||
attributes:
|
||||
label: Where and what you were doing
|
||||
description: For example "Lookout Landing, walking to the tower", "the Depths below Hyrule Castle", "fighting a Talus in the Sky". How long into the session it started.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: overlay
|
||||
attributes:
|
||||
label: Performance overlay
|
||||
description: Press F10 twice (or both stick clicks) for the full panel. Stay a minute in the place and write down the frame rate, the mean and the longest frame time, or attach a screenshot of the panel.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Recording the log
|
||||
|
||||
1. In the settings menu (F1), Advanced tab, Diagnostics: turn on Performance log. For freezes and hitches, also turn on Flight recorder. Restart the game: both take effect at the next start.
|
||||
2. Play at least three minutes in the place where it happens. The log gets a frame-time summary every 60 s, and a line for every frame over 50 ms that names what the frame waited for.
|
||||
3. With the flight recorder on, press F8 right after a hitch. It also saves on its own after every frame longer than the freeze threshold (100 ms), at most once per 10 s.
|
||||
4. Close the game with Quit game in the menu or the window's close button. Do not end it from Task Manager: the last summary is written when the game exits.
|
||||
5. Copy `logs\totk.log` from the folder of `totk.exe` before you start the game again.
|
||||
6. Each flight recorder save is a folder; the log line `flight: dump` names it. Zip the folders and attach them.
|
||||
7. Turn both off again afterwards: they cost a little frame time.
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Drag totk.log into this field.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: flight
|
||||
attributes:
|
||||
label: Flight recorder saves
|
||||
description: Drag the zipped folders into this field, for freezes and hitches.
|
||||
@@ -0,0 +1,94 @@
|
||||
name: Other bug
|
||||
description: Sound, controls, the settings menu, the installer, or game logic.
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first. Crashes, graphics glitches and performance have their own templates.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this bug.
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
options:
|
||||
- Audio
|
||||
- Controls and input
|
||||
- Settings menu and overlay
|
||||
- Installer
|
||||
- Game logic and physics
|
||||
- Something else
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3". For the installer, the version of TotK-Setup.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last-good
|
||||
attributes:
|
||||
label: Last version that worked
|
||||
description: If an earlier version did this correctly, which one.
|
||||
- type: textarea
|
||||
id: system
|
||||
attributes:
|
||||
label: System info
|
||||
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. On a Steam Deck, add LCD or OLED. For the installer, write your OS and the install disk.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: Mods and cheats
|
||||
description: The folders in mods/ with their download links, or "none". Say whether it also happens without them.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Can you keep playing?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No, it stops the game or a quest"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What happened and how to reproduce it
|
||||
description: What you expected, what happened instead, and the steps that lead there.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### What to attach for your area
|
||||
|
||||
Close the game with Quit game in the menu or the window's close button, then copy `logs\totk.log` from the folder of `totk.exe` before you start the game again.
|
||||
|
||||
**Audio.** Open `totk.ini` in the folder of `totk.exe` and add the line `trace = audio`; when the file already has `trace = perf` (Performance log on), change it to `trace = perf,audio`. Start the game, play at least two minutes where the sound goes wrong and close the game. The log then has an `audio:` line every 5 s with the buffer underruns. Name the output device and how it is connected (speakers, headphones, USB, Bluetooth, HDMI), and attach a short video with sound if you can. Remove the line afterwards.
|
||||
|
||||
**Controls and input.** Name the controller and how it is connected (USB, Bluetooth, Steam Input). Does it also happen with keyboard and mouse?
|
||||
|
||||
**Controls, game logic, anything you can trigger again.** Turn on Input recording (F1, Advanced tab, Diagnostics), restart the game, load a save, make the bug happen and close the game. Attach `input-record.txt` from the logs folder and a zip of the save you loaded: the `profile` folder next to `totk.exe`. The recording replays what you did, so we can see the bug on our machine.
|
||||
|
||||
**Installer.** Attach the newest `install-*.log` from `temp\logs` in the folder of `TotK-Setup.exe` (on Linux: `.setup-temp/logs` in the install folder), and the log file the error message names.
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log and other files
|
||||
description: Drag totk.log and the files your area asks for into this field.
|
||||
validations:
|
||||
required: true
|
||||
File renamed without changes.
@@ -17,14 +17,19 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
- Native code, not emulation
|
||||
- Game updates 1.1.0, 1.2.1 and 1.4.3; 1.2.1 and 1.4.3 saves carry over between them
|
||||
- Takes NSP and XCI game files, and compressed NSZ and XCZ as they are: the setup unpacks them itself
|
||||
- 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
|
||||
- NVIDIA DLSS / DLAA on RTX GPUs: the setup downloads `nvngx_dlss.dll` from NVIDIA's DLSS SDK repository
|
||||
(github.com/NVIDIA/DLSS, NVIDIA RTX SDK license) when it finds an RTX GPU; untick it in the setup to skip it
|
||||
- HDR10 output with peak, game and HUD brightness settings
|
||||
- 16:10 (Steam Deck, handhelds), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
|
||||
- 16:10 (Steam Deck, handhelds), 3:2 (Surface, Framework), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
|
||||
- Anisotropic texture filtering up to 16x, for sharp ground into the distance
|
||||
- In-game settings menu (F1) and performance overlay (F10)
|
||||
- Discord Rich Presence ("Playing TotK Recomp" with the play time), off by default, own application id supported
|
||||
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
|
||||
- Keyboard, mouse and gamepad with remapping, rumble and gyro
|
||||
- Stereo or 5.1 surround sound output
|
||||
- Xbox button prompts: LB, RB, LT, RT, Menu and View, and A, B, X and Y as labelled on the button to press, with either button layout
|
||||
- Shaders prepared at install instead of during play
|
||||
- Mods made for emulators: unpack a mod into the `mods` folder next to `totk.exe`; switch and order them in the menu. RomFS
|
||||
@@ -44,7 +49,9 @@ Requirements: Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA machines
|
||||
|
||||
1. Download `TotK-Setup-<version>-windows.zip` from [Releases](../../releases/latest) and unpack it.
|
||||
2. Start `TotK-Setup.exe`. If SmartScreen warns about an unsigned app, choose *More info*, then *Run anyway*.
|
||||
3. Pick your game files, the language and an install folder, and press *Start*. The setup builds the game once.
|
||||
3. Pick your game files, the language and an install folder, and press *Start*. The setup builds the game once. With
|
||||
an NVIDIA RTX GPU it also downloads NVIDIA DLSS, unless you untick *Download NVIDIA DLSS*; turn DLSS on in the
|
||||
menu (F1).
|
||||
4. Play `totk.exe` from the install folder.
|
||||
|
||||
### Linux
|
||||
|
||||
@@ -10,6 +10,7 @@ Code copied or adapted from another project is listed here with its source, the
|
||||
| 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 1.2.1 cursor and aim steps: the map cursor, vertical Ultrahand aim and vertical gyro aim sites, their instruction words and the step scaled by the frame ratio | https://github.com/ly05010419/MacNativeGames @ `363a6eb50434387f7d623349934bcf1e910be48f` (`games/totk/runtime/game_patches.c` patch_cursor_speed), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v121.txt` (set cursor) |
|
||||
| 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) |
|
||||
| MacNativeGames TotK button icons: the Xbox controller's shoulder buttons, triggers, Menu, View and d-pad drawn with letters from the game's own text fonts, the .bfotf key, the button guide's images and their block-linear layout, the resource size table raised for the archives that grew | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/scripts/make_button_icons.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/button_icons.py`, `tools/make_romfs_patches.py` (--prompts) |
|
||||
| RenoDX TotK mod (game mod by souperman9, tonemapper by Musa, RenoDX framework by ShortFuse, RenoVK fork by Ritsu): the approach of the HDR tonemap (the game's 1 - e^-x curve kept below a pivot and continued along its tangent above it, the saturation grade applied to the colour scaled into 0..1) and the observation that the passes after the tonemap keep values above 1. No code copied; the shader is our own | https://github.com/clshortfuse/renodx @ `b6f87272ad1bfdc0e6444910a93606815944a085` (`src/games/totk/tonemap.slang`, `addon.cpp`), Copyright (c) 2025 Carlos Lopez Jr. | MIT | `runtime/shaders/hdr_tonemap.frag`, `runtime/vk_hdr.c` |
|
||||
@@ -103,3 +104,4 @@ folder's `licenses/` and in each component's own folder.
|
||||
| `totk_xlate` (NativeAOT linux-x64 build of `tools/xlate/`, linked with the folder's clang, lld and sysroot) | built by `tools/build_xlate.py --llvm --sysroot` | MIT | `bin/totk_xlate` |
|
||||
| AppImage type-2 runtime (static; includes libfuse and squashfuse), the AppImage's first part; appimagetool packs the folder and is not distributed | https://github.com/AppImage/type2-runtime | MIT; libfuse LGPL-2.1; squashfuse BSD-2-Clause | the AppImage header |
|
||||
| MacNativeGames shader variant list | as in the installer release row above | MIT | `data/shader_list.tsv` |
|
||||
| Weeping of the Empire menu cursor: eui's focus box read through ScreenMgr, BoxCursorMgr and the screens' node lists, AnimButton's hit test, the stale-hit-pane fallback to the parts panes, the three writes of moveBoxCursor, the structure offsets (1.0.0; the ScreenMgr instance placed in 1.4.3 by us) | https://github.com/daniel-madrid-07/weeping-of-the-empire @ `df0586cdc5702ca19bbebfa59697e0b6980f6237` (`engine/runtime/totk_menu_cursor.cpp`), Copyright (c) 2026 daniel-madrid-07 | MIT | `runtime/menu_cursor.c` |
|
||||
+11
-5
@@ -84,7 +84,7 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
- NVN front end from MacNativeGames (`nvn_api_*.c`, `nvn_obj.h`, texture layout and deswizzle). The game records command buffers through it; nothing reaches the GPU until the game submits.
|
||||
- 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.
|
||||
- Replay thread. `TOTK_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is off by default. Where the game waits for the GPU it overlaps the replay with the game threads (a GTX 1650 in the Depths, issue #88), a gain measured only there; while it was the default, a GTX 1660 Ti at the 60 fps cap with the GPU at 26 % reported uneven motion (#659). It adds up to a frame of latency; `replay_async = 1` turns it on.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. It is off by default on AMD GPUs, where the driver loses the device or hangs the GPU at startup with it (`TOTK_VK_RECORDER=1` forces it on, `=0` makes every call on the submitting thread).
|
||||
- Uniform snapshot. The game rewrites per-object uniform blocks on other threads soon after it submits the frame
|
||||
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
|
||||
@@ -99,8 +99,9 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
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, 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 ten sizes: 21:9 2560x1080, 3440x1440, 3840x1600, 5120x2160, 32:9
|
||||
3840x1080, 5120x1440 and 16:10 1280x800, 1680x1050, 1920x1200, 2560x1600). A wider layout moves the HUD to the
|
||||
`tools/ultrawide_layout.py`; the installer builds fifteen sizes: 21:9 2560x1080, 3440x1440, 3840x1600, 5120x2160,
|
||||
32:9 3840x1080, 5120x1440, 16:10 1280x800, 1680x1050, 1920x1200, 2560x1600 and 3:2 1920x1280, 2160x1440, 2256x1504,
|
||||
2880x1920, 3000x2000). A wider layout moves the HUD to the
|
||||
screen's corners, a narrower one to its top and bottom edges. Other sizes get the camera of their shape and the 16:9
|
||||
HUD stretched over it. 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
|
||||
@@ -117,8 +118,13 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
- DLAA (`vk_dlaa.c`, `dlaa`, off by default). NVIDIA DLSS at the scene's own size, through the driver's NGX core loaded
|
||||
at run time (no SDK code compiled in). It runs on the R11G11B10F light target right before the bloom chain's first
|
||||
pass, so post effects and the HUD read the anti-aliased picture. The scene's draws get a sub-pixel Halton viewport
|
||||
jitter; motion vectors are camera-only, reprojected from the scene depth through this and last frame's
|
||||
view-projection of the scene camera block (constant bank c4). `fxaa` switches the game's own FXAA pass.
|
||||
jitter; camera motion vectors are reprojected from the scene depth through this and last frame's view-projection of
|
||||
the scene camera block (constant bank c4), in double precision on the CPU. Per-object motion (`dlaa_objmv`,
|
||||
`vk_mvprev.c`, the per-object section of `vk_exec_draw.c`, translator patch 0015): solid scene draws whose position
|
||||
reads an object's own uniform blocks (bones c5, place c7, c6) are drawn a second time with a vertex shader variant
|
||||
that computes last frame's position from last frame's blocks (the game writes them into two copies that take turns;
|
||||
the uniform snapshot keeps both) and writes the difference into the motion image. `TOTK_MV_CHECK` measures the
|
||||
vectors against the picture, `TOTK_DLAA_TIMES` times the passes on the GPU. `fxaa` switches the game's own FXAA pass.
|
||||
- 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`).
|
||||
|
||||
+31
-2
@@ -115,7 +115,8 @@ of them.
|
||||
`SDL3.dll`, `libiconv-2.dll` and `libopus-0.dll` next to the exe.
|
||||
For Xbox button prompts, run `make_romfs_patches.py` twice more, with `--prompts xbox` and `--prompts xbox-label`:
|
||||
they write `build\<ver>\romfs-patches-xbox` and `romfs-patches-xbox-label` ([controls.md](controls.md), "Xbox
|
||||
button prompts"), which `build_game.py` copies next to `totk.exe` like `romfs-patches`.
|
||||
button prompts"), which `build_game.py` copies next to `totk.exe` like `romfs-patches`. `--prompts pc` writes
|
||||
`romfs-patches-pc`, the keyboard and mouse prompts ([controls.md](controls.md), "PC button prompts").
|
||||
|
||||
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
|
||||
for the shared shader list:
|
||||
@@ -150,6 +151,33 @@ your machines without MSYS2, Python or .NET. `romfs.bin` stays out unless you pa
|
||||
`romfs\` next to the exe or point `TOTK_ROMFS` at its folder. Run `totk.exe --prepare-shaders` once in the copied
|
||||
folder. The folder holds code and data derived from your game: for your own machines only.
|
||||
|
||||
### Profile-guided build
|
||||
|
||||
`build_game.py --pgo <file>.profdata` compiles the generated code at -O2 with an execution profile, and the runtime
|
||||
with it too: hot paths laid out straight, hot calls inlined, code that never ran compiled for size. On 1.4.3 the fight
|
||||
bench ran 12-15% faster per frame and a fight outside the training about 6%. The installer builds with
|
||||
`data/pgo-<ver>.profdata` of the release when the update has one, else at -O1.
|
||||
|
||||
A profile only fits the generated C it was trained on: functions are matched by name and control-flow hash, so a
|
||||
function whose C changes loses its counts without a warning. Retrain after a lifter or generator change, from a clean
|
||||
`recomp/`. The instrumented build needs llvm-mingw, as MSYS2's clang has no profile runtime:
|
||||
|
||||
```powershell
|
||||
$llvm = "toolchain\llvm-mingw-<date>\llvm-mingw-<date>-ucrt-x86_64"
|
||||
.venv\Scripts\python tools\build_game.py --version v143 --out build\v143-pgogen --toolchain $llvm --pgo gen
|
||||
```
|
||||
|
||||
Play `build\v143-pgogen\totk.exe` through varied scenes (fights, towns, riding, building, the Depths, menus) for 20-30
|
||||
minutes; it is slower than a normal build. It writes `build\v143-pgogen\pgo\*.profraw` every minute and at exit.
|
||||
Scripted runs from saves (`tools/run_game.py`) add to the same file. Then:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\pgo.py merge build\v143-pgogen --version v143
|
||||
```
|
||||
|
||||
writes `build\pgo\pgo-v143.profdata` and its record (`.json`: the `recomp/` tree it fits). `make_release.py` ships every
|
||||
profile of `build\pgo` in `data\` and warns when a record names another `recomp/` tree than the one released.
|
||||
|
||||
## Linux (in progress)
|
||||
|
||||
The runtime builds and runs on x86-64 Linux with a window, input and menu on SDL3 (`*_sdl.c`) in place of the Win32
|
||||
@@ -202,7 +230,8 @@ Before a commit:
|
||||
|
||||
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
|
||||
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
|
||||
files); a change to the settings passes `totk.exe --probe-cfg`.
|
||||
files); a change to the settings passes `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`, which fails
|
||||
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md).
|
||||
|
||||
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
|
||||
change passes these, all run with `.venv`:
|
||||
|
||||
+114
-5
@@ -27,12 +27,13 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|
||||
| D-pad left, up, right, down | D-pad | 1 2 3 4 | quick-select |
|
||||
| Plus | Start | Tab | menu, inventory |
|
||||
| Minus | Back | M | map |
|
||||
| Right stick shaken left and right | shake the right stick | G | detach a fused part from what Ultrahand holds |
|
||||
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
|
||||
## Xbox button prompts
|
||||
|
||||
`TOTK_PROMPTS` (`prompts`, the *Button prompts* row on the Controls tab, read at start) picks the controller whose buttons the game's prompts show: `nintendo` (default) keeps the game's own, `xbox` shows an Xbox controller's.
|
||||
`TOTK_PROMPTS` (`prompts`, the *Button prompts* row on the Controls tab, read at start) picks the controller whose buttons the game's prompts show: `nintendo` (default) keeps the game's own, `xbox` shows an Xbox controller's, `pc` the keyboard and mouse (PC button prompts, below).
|
||||
|
||||
- L, R, ZL, ZR, Plus and Minus become LB, RB, LT, RT, Menu and View, and the d-pad prompts a cross with an arrow for each direction meant. The letters on the keys are cut out in the game's own text font.
|
||||
- A, B, X and Y name the label of the button the pad layout presses, so each prompt names the button to press. `pad_layout = nintendo` plays an Xbox pad by position: the right button, labelled B, does Switch A (interact, confirm), and the bottom one, labelled A, does Switch B (run, cancel), so the prompts swap A with B and X with Y. `xbox` and `auto` put Switch A on the button labelled A, and the letters stay.
|
||||
@@ -56,6 +57,24 @@ The game reads its UI archive and fonts once at boot, so the prompts change at t
|
||||
|
||||
The Switch prompts stay, with a note under *Button prompts* and a `RomFS overlay: ...` line in the log, when the folder is missing (an install made by an older setup, or one where the setup could not prepare it and showed a warning; running the setup again on the install folder writes it), when it fails its check, with a game resolution of another shape than 16:9 (its own UI layout wins; 16:10 and 21:9), and when the `romfs_mods` folder replaces one of its files: `romfs-patches` then loads in its place, where two overlays replacing one file would stop the game. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts xbox` (and `--prompts xbox-label`) writes `build\<ver>\romfs-patches-xbox` (`-label`); it needs fontTools from `tools/requirements-py314.txt`. `--xbox-prompts` is the older name of `--prompts xbox`.
|
||||
|
||||
## PC button prompts
|
||||
|
||||
`prompts = pc` (*PC* on the *Button prompts* row) shows the key or mouse button the default keyboard bindings press each Switch button with, whatever the pad layout. The game loads `romfs-patches-pc` in place of `romfs-patches`, written by `tools/make_romfs_patches.py --prompts pc` (`button_icons.py`, `pc_icons`), which the setup runs next to the Xbox ones:
|
||||
|
||||
| Switch | Prompt |
|
||||
|---|---|
|
||||
| A, B, X, Y | E, Shift, Space, left click |
|
||||
| L, R, ZL, ZR | Q, R, right click, F |
|
||||
| Plus, Minus | Tab, M |
|
||||
| D-pad | 1 (left), 2 (up), 3 (right), 4 (down), as a cross or the keys of the directions meant |
|
||||
| Left stick, its press | W A S D (or the keys of the directions meant), Ctrl |
|
||||
| Right stick, its press | the mouse, with an arrow for each direction meant, C |
|
||||
|
||||
- The glyphs keep the width of the game's own, so the right-aligned guides do not run into their text. A key is a rounded square with its label cut out in the game's own text fonts; a mouse button is a mouse with that button filled and the others hollow.
|
||||
- The button guide in play draws every button from images of the Common archive (`Nt_KeyTexA/B/X/Y/L/ZL_00`, `Nt_KeyTexLSt_00` and `_01`, the stick tilted and pressed): all of them are painted with the same icons. The letters of the action prompt's diamond are one character of the Normal font each: E, an arrow for Shift, a bar for Space, L and R for the mouse buttons.
|
||||
- Like the Xbox prompts they follow the defaults and not the bindings. `--binds <totk.ini>` draws the first key of each `bind_` line of that file instead, for a folder made by hand.
|
||||
- At a game resolution of another shape than 16:9 the game loads `romfs-patches-pc-<w>x<h>` in place of `romfs-patches-<w>x<h>` when it is there: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts pc --aspect <w>x<h> --out <install>\romfs-patches-pc-<w>x<h>`. The setup does not write those, so without one the Switch prompts stay (*off at a game resolution not 16:9*).
|
||||
|
||||
## Window keys
|
||||
|
||||
| Key | Gamepad | Action |
|
||||
@@ -68,9 +87,9 @@ The Minus+Plus combo never reaches the game. In the menu the arrow keys, the D-p
|
||||
|
||||
## Remapping
|
||||
|
||||
The *Controls* tab of the settings menu lists, under the button layout, every Switch button and the eight stick directions the keys drive (movement and camera), each with two key cells and a controller cell. Select a cell, press A or Enter, then press the new key, mouse button or controller button; Esc or five seconds without a press cancel, and a controller button cancels a key cell. Delete, Backspace or X clears the selected cell. A key another action has moves to the new one (the description names the action it left); a controller button swaps with the one it replaces. A remapped controller button is physical: from then on the face buttons (A B X Y) stay where they are in both layouts, until the buttons are put back as the layout in use has them or *Reset all buttons* is chosen. F1, F8, F10, F11 and Esc cannot be bound (the menu, the flight recorder and the window keep them). Punctuation keys are named by their place on a US keyboard; the menu shows the character the keyboard layout in use puts there.
|
||||
The *Controls* tab of the settings menu lists, under the button layout, every Switch button, the eight stick directions the keys drive (movement and camera) and the shake key, each with two key cells and a controller cell. Select a cell, press A or Enter, then press the new key, mouse button or controller button; Esc or five seconds without a press cancel, and a controller button cancels a key cell. Delete, Backspace or X clears the selected cell. A key another action has moves to the new one (the description names the action it left); a controller button swaps with the one it replaces. A remapped controller button is physical: from then on the face buttons (A B X Y) stay where they are in both layouts, until the buttons are put back as the layout in use has them or *Reset all buttons* is chosen. F1, F8, F10, F11 and Esc cannot be bound (the menu, the flight recorder and the window keep them). Punctuation keys are named by their place on a US keyboard; the menu shows the character the keyboard layout in use puts there.
|
||||
|
||||
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>`:
|
||||
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), or `SHAKE` (keys only, G by default). 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 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`
|
||||
@@ -90,7 +109,85 @@ bind_Y = pad:north, key:mouse_l
|
||||
|
||||
## Mouse camera
|
||||
|
||||
Mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * TOTK_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
|
||||
`mouse_mode = direct` (the default) turns the camera by the raw mouse motion on games 1.4.3 and 1.2.1, through hooks in the game's camera code (`runtime/mouse_direct.c`, the `mouse_direct` sites of `recomp/patches-v143.txt` and `recomp/patches-v121.txt`). None of them changes a step of the gamepad alone. The free-orbit camera receives raw-count
|
||||
angular increments at its final spherical-angle output: 0.08 degrees per count on both axes at sensitivity 1 (about
|
||||
14.3 cm a turn at 800 DPI), with no time multiplier, curve or carry into later frames. The controller's persistent pitch/yaw move with
|
||||
the output. Mouse input is removed from its stick path first, so the camera's later vertical signed-square curve and
|
||||
velocity filters cannot process it a second time.
|
||||
The aiming camera (bow, throwing), the Purah Pad camera, the scope, the ability camera (Ultrahand, Fuse, Ascend,
|
||||
Recall, Autobuild), the horse camera, lock-on, the bow drawn while locked on, climbing (walls and objects), the
|
||||
waterfall's turn, shield surfing, the wakeboard and the dialogue cameras take the mouse the same way, at the same
|
||||
degrees per count: each turns by
|
||||
the stick times its own speeds and the frame time and adds that to a base angle, so the load of the stick it turns by
|
||||
gets the gamepad's part only and the load of that base angle gets the mouse's degrees (`camera_load`, two sites per
|
||||
axis per camera). Their speeds, time step, slow motion (bullet time) and, for the Purah Pad camera and the abilities,
|
||||
the eased curve their pitch moves along do not touch the mouse; their own limits, collision and blends apply to the
|
||||
result. Past the pitch limit of the scope or the ability camera the stick moves the camera's distance; the mouse's
|
||||
motion past the limit keeps doing that through the stick. Recall's cursor is the ability camera's offset mode: the
|
||||
camera keeps a direction, the stick moves the cursor across the screen (ReverseRecorderCursorSpeed, within
|
||||
ReverseRecorderCursorRange) and the camera follows it by up to ReverseRecorderCameraRange (10 degrees across, 7.8 up).
|
||||
The mouse moves the cursor so that its aim, the camera's follow plus the cursor's angle from the screen's centre (the
|
||||
offset taken as the screen's half width or height at the camera's 50 degree view and aspect ratio), turns as many
|
||||
degrees a count as the camera does elsewhere. The follow range is read from the game each step (until it has been read
|
||||
once, the mouse is stick input there). The stick loads that only test for input keep the mouse in the stick. A step with mouse motion that no
|
||||
direct hook took is logged (`mouse: step N with mouse motion took the stick path`, the first 30, then one in 600, with
|
||||
the player's state), so the cameras still on stick input show in `logs/totk.log`.
|
||||
Lock-on keeps pointing the camera at the target; the stick, and so the mouse, adds an offset around that direction,
|
||||
which the game limits and lets return: the mouse's degrees are the step's turn the game adds to the offset and judges
|
||||
the return by.
|
||||
Shield surfing and the wakeboard turn their pitch by the square of the stick's y and move a second camera parameter
|
||||
with it: their stick y keeps the mouse (that parameter and the camera's state follow it as before), the pitch gets
|
||||
the degrees, and the pitch turn the game made from y is cut to the gamepad's share (p |p| / t |t|). Their stick x, and
|
||||
the dialogue camera's (which is also its test for input), give a tiny value in the mouse's direction when the
|
||||
gamepad's axis is at rest, so the camera still sees input without a stick turn.
|
||||
Two inputs stay stick input: the waterfall's vertical axis moves the camera's height, not an angle, and the horse's
|
||||
lock-on without a target (HorseLockOnEmpty) turns the stick's position into an offset that returns when the stick
|
||||
does, which a mouse has no position for.
|
||||
Those receive input after
|
||||
the radial dead zone, unit-circle clamp and stick response exponent: x is 0.0343 stick units a count over the frame
|
||||
ratio (the stick turns yaw 70 x / 30 degrees a step) and y 70 / 51 times that, linear like their pitch (51 y / 30
|
||||
degrees a step), so both axes turn by the same amount for the same motion. `stick_sens` (which scales the game's
|
||||
stick turn rates) is divided out. While the player uses an ability and the ability camera's hooks do not take the
|
||||
mouse, its part is scaled by 1 / 0.8 and 70 / (1.5 x 51), the ability camera's own stick scales.
|
||||
The player's verbs are read every 50 ms from the player actor (the player root's GOT slot main+0x3969918, the
|
||||
component array +0x228, its AS component +0x158 and the verbs of its five slots). Ultrahand has a separate hook after
|
||||
its manipulation input's power curves; it gets 0.01 stick units a count over the frame ratio, at most three full tilts
|
||||
a step.
|
||||
`mouse_aim_scale` (0.1 to 3, default 1) multiplies the mouse's motion while the bow is drawn (the verb UseBow or
|
||||
DrawnBow with ZR held). The game's camera inversion and gamepad response remain in effect. `mouse_sens` scales raw
|
||||
counts, with the same meaning as in 0.3.10 (the free camera turns 0.08 degrees a count at 1, and now every other camera
|
||||
does too); its scale differs from the stick-emulation mode.
|
||||
The Controls tab switches between *Direct* and *Stick emulation* (`mouse_mode = stick`).
|
||||
The orbit camera keeps its desired pitch at the final angular output, including stationary frames, so a later game
|
||||
filter cannot recenter it. The persistent filter state accounts for the camera's current vertical offset instead of
|
||||
accumulating corrections. A gamepad vertical input releases that mouse target to the game's own. The game's
|
||||
lock-on, recenter and override (aiming) drive the camera themselves: the mouse stops holding without restoring its
|
||||
earlier target, and when they end (or after the camera went 100 ms without a step) it holds the pitch the camera has
|
||||
then, so the camera does not spring back to a pitch from before the aim or to the game's default after a shot.
|
||||
While the ability wheel is open (L held and its screen, ShortCutMainItem_00 or ShortCut_00, has the focus), mouse
|
||||
counts move its virtual cursor, which stays where it was when the mouse stops. L or the D-pad held with no wheel or
|
||||
strip on screen (L soothes the horse, turns menu tabs) leaves the mouse on the camera; with the menu cursor off
|
||||
(`menu_cursor = 0`) the screens are not read and the buttons alone decide.
|
||||
The horizontal quick lists (D-pad up/left/right: materials, shields and weapons) use separate 32 ms right-stick steps,
|
||||
with at least 32 ms of neutral between them. At sensitivity 1, 32 horizontal counts request a step; a large flick
|
||||
cannot queue continued scrolling after the mouse stops. Vertical movement is ignored in these lists. Switching or
|
||||
releasing a selector resets its cursor/step state without sending the last movement to the camera. Selector input is
|
||||
not consumed by the direct camera or Ultrahand hooks.
|
||||
To detach a fused part from what Ultrahand holds, shake the mouse left and right, or press G (`bind_SHAKE`). A press
|
||||
swings the right stick left and right for 0.8 s, holding the key keeps it swinging. With `mouse_mode = direct` a shake
|
||||
of the mouse while Ultrahand holds an object swings the right stick with it: once the swings (10 degrees of camera turn
|
||||
or more each, 125 counts at sensitivity 1) change direction twice within 0.35 s, the stick goes full tilt the
|
||||
way the mouse moves, each side held at least two frames so the game reads it, and goes neutral as soon as the mouse
|
||||
rests. Only your own swings reach the stick, so nothing keeps swinging after you stop; shake until the part comes off.
|
||||
The camera hooks ignore that swing (the mouse turns the camera in degrees already). Turning a held object back and
|
||||
forth at an ordinary pace is slower and does not count. The arrow keys or the gamepad's right stick shaken by hand
|
||||
work too.
|
||||
A step of the gamepad alone, with no mouse motion and no pitch the mouse holds, reaches the camera exactly as without
|
||||
the hooks.
|
||||
The mapping below is still used when the camera-input hook is unavailable or the game disables camera input.
|
||||
Game 1.1.0 has no hooks and keeps stick emulation whatever `mouse_mode` says. On 1.2.1 `mouse_aim_scale` has no effect (the bow-in-hand state is not read there).
|
||||
|
||||
In stick-emulation mode, mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * TOTK_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
|
||||
|
||||
`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.
|
||||
|
||||
@@ -98,6 +195,12 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
|
||||
|
||||
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`.
|
||||
|
||||
## Mouse in the game's menus
|
||||
|
||||
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
|
||||
|
||||
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`TOTK_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `TOTK_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
|
||||
|
||||
## Nintendo Switch 2 controllers
|
||||
|
||||
Windows does not present the Switch 2 Pro Controller as a gamepad SDL can open (the log says `input: no gamepad`), so it needs a bridge program that wakes it and creates a virtual gamepad. The DS4Windows fork by hbashton ([github.com/hbashton/DS4Windows](https://github.com/hbashton/DS4Windows), tested with release VIIPERRC4.5.1, a pre-release) with the output set to an emulated DualSense gives buttons, rumble, gyro and accelerometer, and reads the sticks through the controller's calibration (they rest at 0 to 2%). The game reads its virtual DualSense directly; the log then shows `input: gamepad "DualSense Wireless Controller" (rumble, gyro and accelerometer, ...)` and `input: motion sensors on`. Its release notes also list Joy-Con 2 support, which is untested here.
|
||||
@@ -111,14 +214,20 @@ Bridges that output a ViGEm DualShock 4 (ProCon2Windows, connectyourpro as teste
|
||||
```
|
||||
<frame> <button> [hold_frames] # A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK; hold defaults to 4
|
||||
<frame> LSTICK|RSTICK <x> <y> [hold_frames] # -32767..32767, up is +y
|
||||
<frame> MOUSE <dx> <dy> # direct-mode raw counts, once; down is +dy
|
||||
@<ms> <button> [hold_ms] # timed on the world clock
|
||||
@<ms> LSTICK|RSTICK <x> <y> [hold_ms]
|
||||
@<ms> MOUSE <dx> <dy>
|
||||
```
|
||||
|
||||
`<frame>` counts 60 Hz ticks of game time since the first frame (presented frames at 60 fps), so the title menus get the presses at the same game time at any frame limit. `@<ms>` counts milliseconds of the world clock: game time stepped since the end of the first save load (the player leaves the title screen's origin), so a timed event lands at the same game time at any frame rate and load length. A later load, such as fast travel, still shifts what follows. In a script with timed lines the untimed ones stop when the world clock starts, as the load ends at another tick in each run. Each event logs once (`input: script frame N tick T: A`). `tools/input/load_save.txt` loads the first save from the title screen.
|
||||
|
||||
`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.
|
||||
|
||||
In direct mouse mode the recording also writes one `MOUSE` event per consumed raw displacement. These are one-shot
|
||||
displacements rather than held stick segments. They replay without cursor capture, including with `input = none`;
|
||||
the game must be accepting camera input when the event reaches its camera update.
|
||||
|
||||
`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)
|
||||
|
||||
+42
-15
@@ -9,19 +9,21 @@ holds only the game; the installer and its tools stay outside it.
|
||||
1. Downloads `TotK-Setup-<version>-windows.zip` from GitHub Releases (none is published yet), unpacks it anywhere and
|
||||
starts `TotK-Setup.exe`.
|
||||
2. Picks:
|
||||
- the base game (XCI or NSP) and the 1.1.0, 1.2.1 or 1.4.3 update (NSP);
|
||||
- the base game (XCI or NSP) and the 1.1.0, 1.2.1 or 1.4.3 update (NSP); an NSZ or XCZ (compressed by the nsz
|
||||
tool) works as it is, the setup unpacks it itself;
|
||||
- their `prod.keys`;
|
||||
- optionally the firmware folder (`system/Contents/registered` of an unpacked firmware or an emulator's install),
|
||||
needed for Chinese, Korean and Traditional Chinese text;
|
||||
- the game language and the install folder.
|
||||
3. Waits for the setup, one time. The window shows each step and its progress.
|
||||
4. Plays `totk.exe` from the install folder. The installer and the source NSP or XCI can be deleted.
|
||||
4. Plays `totk.exe` from the install folder. The installer and the source NSP or XCI (or NSZ, XCZ) can be deleted.
|
||||
|
||||
The install folder (`main.c` `portable_paths()` reads this layout; it stays portable):
|
||||
|
||||
```
|
||||
<install folder>/
|
||||
totk.exe, SDL3.dll, libopus-0.dll, totk_xlate.exe
|
||||
nvngx_dlss.dll NVIDIA DLSS, downloaded by the setup's DLSS step when the player keeps it ticked
|
||||
totk.ini language; the other keys as commented defaults (totk.ini.example)
|
||||
exefs/, romfs/ extracted from the player's files
|
||||
shared-fonts/ the Switch system fonts, with Chinese, Korean or Traditional Chinese text
|
||||
@@ -36,7 +38,10 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
|
||||
|
||||
The player runs a newer installer on the same folder. It reads `totk-install.json`, keeps `romfs/`, `exefs/`,
|
||||
`shared-fonts/`, `profile/` and `totk.ini` after checking their hashes, rebuilds `totk.exe` and re-prepares the
|
||||
shaders. A newer release that changes nothing game-derived skips the rebuild.
|
||||
shaders. A newer release that changes nothing game-derived skips the rebuild. The window starts with the last install folder
|
||||
and the base, update, keys and firmware paths it ran with (paths only, never the keys file's contents), kept in
|
||||
`%LOCALAPPDATA%\TotK Recomp\setup.json` (`$XDG_STATE_HOME/totk-recomp/setup.json` elsewhere). Entries that no longer exist
|
||||
stay empty. A reinstall or a switch to another game version starts filled in.
|
||||
|
||||
Given an update file other than the recorded one (1.1.0, then 1.2.1, then 1.4.3), the installer extracts and builds again from it
|
||||
and keeps `profile/`. Saves carry over both ways between 1.2.1 and 1.4.3: 1.4.3 converts a 1.2.1 save on load, and 1.2.1
|
||||
@@ -44,7 +49,7 @@ loads the saves 1.4.3 wrote.
|
||||
|
||||
## Setup steps
|
||||
|
||||
1. **Check** before writing anything: the files open with the keys, the update is 1.1.0, 1.2.1 or 1.4.3 (by hash, as
|
||||
1. **Check** before writing anything: an NSZ or XCZ input is unpacked first (below), the files open with the keys, the update is 1.1.0, 1.2.1 or 1.4.3 (by hash, as
|
||||
`tools/extract.py`), the keys are new enough for the update, enough space is free on the target disk (about 36 GiB at the peak of a
|
||||
first install, 20 GiB after it), the CPU is at least x86-64-v2 (SSE4.2; with AVX2/FMA the faster `v3` build is used,
|
||||
otherwise the `v2` one), a Vulkan 1.3 GPU exists. Each failure is one clear message.
|
||||
@@ -65,13 +70,20 @@ loads the saves 1.4.3 wrote.
|
||||
`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 three times: `romfs-patches`, and `romfs-patches-xbox` and `romfs-patches-xbox-label`
|
||||
The patches are written four times: `romfs-patches`, `romfs-patches-xbox` and `romfs-patches-xbox-label`
|
||||
(`--prompts`: Xbox button prompts for `prompts = xbox`, the first for `pad_layout = nintendo`, the second for `xbox`
|
||||
and `auto`). The two Xbox folders are optional: when one cannot be made, the setup warns and finishes.
|
||||
and `auto`), and `romfs-patches-pc` (keyboard and mouse prompts for `prompts = pc`). The three prompt folders are
|
||||
optional: when one cannot be made, the setup warns and finishes.
|
||||
When the graphics driver crashes while building the shaders, `prepare_shaders.py` runs that part again without the
|
||||
variants it was building ([building.md](building.md), step 6), and the setup finishes with a warning that names how
|
||||
many were left out.
|
||||
7. **Clean up** the temp folder, except the generated C and the objects (about 1.3 GB), and write
|
||||
7. **DLSS** (the window's tick box, on by default, or `--dlss`; Windows with an NVIDIA RTX GPU): download
|
||||
`nvngx_dlss.dll` from NVIDIA's own DLSS SDK repository (github.com/NVIDIA/DLSS, a pinned tag, NVIDIA RTX SDK
|
||||
license) into the install folder and check its size and SHA-256 (`steps.py`, `Dlss`). The release does not ship
|
||||
the library; NGX loads it from the folder of totk.exe for `dlaa` and the DLSS modes. A DLSS library already in the
|
||||
folder (a newer one the player put there) is kept. Without an RTX GPU the step only logs and does nothing; a failed
|
||||
download warns, the game runs without DLSS, and the next run of the setup tries again.
|
||||
8. **Clean up** the temp folder, except the generated C and the objects (about 1.3 GB), and write
|
||||
`build-cache/rebuild.txt` in the install folder.
|
||||
|
||||
A cancelled or failed setup resumes from the last finished step (`totk-install.json`).
|
||||
@@ -120,9 +132,12 @@ python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR
|
||||
English as the game language, because Chinese game text needs the Switch shared fonts, which the setup extracts only
|
||||
from a firmware folder. A folder with
|
||||
`totk-install.json` needs only the folder. `--selftest [--shots DIR]` builds every screen in every language and
|
||||
closes itself.
|
||||
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the self-closing window and one end-to-end run
|
||||
of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its docstring).
|
||||
closes itself. The window is not stretched on a display whose scale is the system DPI: `installer/dpi.py` makes the
|
||||
process system DPI aware before Tk starts, Tk then draws the fonts at the display's scale and `dpi.px` sizes the
|
||||
paddings and wrap widths with it; the launcher does the same for its error box.
|
||||
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the display scale, the self-closing window and
|
||||
one end-to-end run of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its
|
||||
docstring).
|
||||
- `tests/installer_test.py`: bookkeeping with fake steps. `tests/installer_e2e.py`: a dev-mode run with the files of
|
||||
`config.local.toml`.
|
||||
|
||||
@@ -139,15 +154,17 @@ llvm/ llvm-mingw, x86_64-only subset (Apache-2.0 with LLVM excep
|
||||
runtime/ libtotkrt.a, libtotkrt-v2.a and the headers the generated code and modules.c/romfs.c/patches.c
|
||||
include
|
||||
bin/ totk_xlate.exe, hactool.exe, SDL3.dll, libopus-0.dll, ninja.exe
|
||||
data/ shader_list.tsv (MacNativeGames, MIT)
|
||||
data/ shader_list.tsv (MacNativeGames, MIT); pgo-<ver>.profdata + .json, the execution profile an
|
||||
update's code and the runtime libraries compile with (building.md, "Profile-guided build")
|
||||
THIRD_PARTY_NOTICES.md
|
||||
totk.ini.example, VERSION the default ini; the release string (the repository's VERSION plus the commit)
|
||||
```
|
||||
|
||||
- SDL3 comes from the upstream release, not MSYS2: MSYS2's DLL imports the LGPL libiconv.
|
||||
- Every bundled third-party piece gets a row in `THIRD_PARTY_NOTICES.md`.
|
||||
- The release contains nothing game-derived; `make_release.py` runs `tools/check_content.py` over the folder before it
|
||||
finishes.
|
||||
- The release contains no code, data, text or assets of the game; `make_release.py` runs `tools/check_content.py` over
|
||||
the folder before it finishes. The execution profiles name lifted functions by address (`f_<address>`) and hold
|
||||
branch counts and control-flow hashes of the generated C.
|
||||
|
||||
## Switch system fonts
|
||||
|
||||
@@ -201,7 +218,8 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
|
||||
|
||||
## Constraints
|
||||
|
||||
- The installer steps contain no Win32 calls: Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The installer steps contain no Win32 calls; only the window's DPI awareness (`installer/dpi.py`) is Windows-only, and
|
||||
it does nothing elsewhere. Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The keys file path is passed to the tools; key values never appear in the UI, logs or files.
|
||||
- The window is a front end over the CLI (`python -m installer --base ... --update ... --keys ... --lang ... --dest
|
||||
...`), which prints one JSON progress event per line. Tests and CI use the CLI.
|
||||
@@ -213,8 +231,17 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
|
||||
the file and asks to copy it again (`extract.DamagedFile`). A failed merge checks the base Program NCA alone to tell
|
||||
which file is damaged.
|
||||
- Unsigned exe: SmartScreen warns on first start.
|
||||
- NSZ and XCZ input (game files compressed by the nsz tool: NCAs stored as NCZ, their sections decrypted and
|
||||
compressed with zstd): the check step tells them by their content, not their name (an NSP or XCI holding NCZ files),
|
||||
so the file dialog has one filter for all four. It unpacks each into `<temp>/unpacked/<base|update>.nsp`
|
||||
(`tools/ncz.py`), an NSP of the NSZ's files or of the XCZ's secure partition, and the extraction reads that.
|
||||
Unpacking encrypts the sections again, so every NCA comes back byte for byte, and checks each against the hash its
|
||||
name carries; a mismatch is `check.input_damaged`. It needs the unpacked size free in the temp folder (checked
|
||||
first), runs at about 500 MB/s, and an interrupted setup reuses a finished NSP of the same file. The extract step
|
||||
deletes the folder when it is done. An XCZ update is refused like an XCI one.
|
||||
|
||||
## Later
|
||||
|
||||
- Re-warm the driver cache on start after a move, GPU or driver change.
|
||||
- NSZ and XCZ input.
|
||||
- Per-monitor DPI in the setup window: Tk 9 has none, so a display whose scale differs from the primary one still gets
|
||||
the window stretched.
|
||||
@@ -86,6 +86,9 @@ Source shorthands:
|
||||
| 3.7 | **Integer division** does not exist (compiled to `XMAD`/`IMAD` sequences); MUFU.RCP(0) = +Inf, LG2(0) = -Inf. | Same IEEE edge cases through `1/x`, `Log2`. | Low risk. | — |
|
||||
| 3.8 | **FP64** in some TotK cutscene shaders. | No FP64 on some hosts. | yuzu FP64 → FP32 (YZ-2306, PR 10693); ours: `softfloat64` job flag (`tools/xlate/Program.cs` `QueryHostSupportsShaderFloat64`). | — |
|
||||
| 3.9 | **Integer vertex inputs read as raw bits** (bone indices `R8UI`). | Float-declared inputs convert integers. | NXN Mario: stretched characters; re-translated with `uint` inputs. Ours: attribute types passed to `QueryGraphicsState` (`Program.cs`). | NXN |
|
||||
| 3.10 | **BFE / BFI fields past bit 31** (as PTX `bfe` / `bfi`): a field that runs past bit 31 is cut there; one that starts past it extracts 0 (signed: bit 31 repeated) and inserts nothing. TotK 1.4.3's Depths terrain vertex shaders (42 variants) take the position from a register, `((2y + x) * 8) & 0xff` with 5-bit x and y, so up to 248. | `OpBitField*Extract` / `OpBitFieldInsert` undefined when offset or offset + count exceeds 32; AMD's bitfield instructions take offset and width modulo 32. | Ryujinx passes the fields through. UltraTears saw the Depths walls and ground missing on the Steam Deck and a Linux RX 570 (RADV). Ours: clamped where the field is not a constant inside the word (translator patch 0012). RX 9070 on Windows drew the Depths the same before and after. | PTX ISA `bfe`, `bfi` |
|
||||
| 3.11 | **SHF by 0** gives the unshifted word (C for `SHF.L`, A for `SHF.R`). | Ryujinx's funnel shifts the other word by 32: undefined in SPIR-V; AMD shifts take the low 5 bits and give A \| C. | yuzu fixed Depths terrain gaps in its funnel shift (PR 10459). Ours: a select on a zero amount (translator patch 0013). In 1.4.3 every register SHF pairs its word with 0, so the result was right already. | — |
|
||||
| 3.12 | **Registers read before any write** on a path (paths the structurizer adds included) have no defined value in the game's code; UltraTears reports 0 from the NVIDIA driver. | An `OpVariable` without an initializer loads an undefined value; Mesa folds undefined values freely. | Ours: on Mesa drivers (RADV, ANV, NVK; `vk_caps_driver`) every function-local variable starts at 0 (`OpConstantNull`, job flag `zero_locals`, translator patch 0016; `TOTK_ZERO_LOCALS` overrides). Elsewhere the load stays undefined: a zero start on every driver (the old patch 0014) made the whole picture shake by about 0.9 output pixels a frame with DLSS or DLAA on an RTX 4080 SUPER (#612). It also moved the SPIR-V hash of the game's FXAA shader out of `FXAA_FS` (`vk_dlaa.c` `vk_dlaa_skip_draw`), the likely cause of the shake; that path is not traced. | — |
|
||||
|
||||
## 4. Memory model
|
||||
|
||||
|
||||
+4
-1
@@ -48,7 +48,10 @@ Two mods that edit the same game file do not combine: the higher one wins the wh
|
||||
(`.tkcl` files) go the same way.
|
||||
|
||||
The port merges the resource size table on its own: at every start with mods on, it combines the tables the mods ship
|
||||
and raises the entries for files a mod made larger or added. A separate RESTBL mod is not needed.
|
||||
and raises the entries for files a mod made larger or added, the files inside a mod's packs (`Pack\**\*.pack.zs`)
|
||||
included. A separate RESTBL mod is not needed. The table tells the game how much memory each file needs: a file that
|
||||
outgrew its entry crashes the game when it loads, which can be long after the start, when the game first needs it. The
|
||||
log line `RomFS: resource size table: ...` says how many entries were raised and names the first files inside packs.
|
||||
|
||||
## Code patches (ExeFS)
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# Settings reference
|
||||
|
||||
`totk.ini.example` describes the settings for players. This page lists the rest: diagnostics, test hooks, paths and experiments. Set one as `name = value` in `totk.ini`, or as the environment variable `TOTK_NAME`, which wins over the file. Remove the line to go back to the default.
|
||||
|
||||
## Safety
|
||||
|
||||
- **Safe**: changes only what the row says, with no effect on stability or saves.
|
||||
- **Caution**: can slow the game, grow files, or change the picture, and undoes cleanly when removed.
|
||||
- **Risky**: can crash or hang the game, or break the picture or game logic, while it is set.
|
||||
- **Save risk**: can damage or lose saves, or point the game at other game or save files.
|
||||
- **Unknown**: the code did not settle it.
|
||||
|
||||
Many rows below are developer tools that only act together with another setting or a command-line mode (for example a trace mode or the DLSS path). The row says so where the code does. The rows come from reading the code, and most effects have not been checked in a run.
|
||||
|
||||
`tests/settings_doc_check.py` fails when a name in `known[]` (`runtime/cfg.c`) is described neither here nor in `totk.ini.example`, or when a default here differs from the code.
|
||||
|
||||
## Diagnostics and logging
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `astc_time` | `1` times the GPU dispatches of each ASTC texture decode and logs the time per block footprint and the largest call. | Adds GPU timestamp queries around every decode; if the queue has no timestamps or no `hostQueryReset`, the log says the decodes are not timed and nothing is measured. | Caution |
|
||||
| `bind_stats` | `1` counts bindings and descriptors per draw of each stage and how much of the bound state changes between draws. | Adds per-draw bookkeeping to the render thread and extra log output; frames get slower. | Caution |
|
||||
| `contact_debug` | `1` draws the contact-shadow factor in grey in place of the picture. | The frame shows a grey debug image while it is set; has no effect when contact shadows are off. | Caution |
|
||||
| `core_diag` | `1` records why ready guest threads wait for a virtual core and for how long, with a histogram every 60 presents. | Turns on host-call and SVC depth tracking and writes many log lines (one per present over 25 ms); frames get slower. | Caution |
|
||||
| `menu_cursor_log` | `1` logs what the menu cursor code reads at every screen change. | Only adds log lines. | Safe |
|
||||
| `perf_slow_ms` (default `50`) | Frames at least this many milliseconds long get a `perf: slow frame` log line with the time blocked per cause. | Changes how many frames are logged; a small value fills the log. | Safe |
|
||||
| `replay_check` | `1` reads every texture read-back a second time after a full drain of the replay queue and compares the two. | Stalls the replay queue at every read-back, so frames get much slower; mismatches are only counted and logged. | Caution |
|
||||
| `resolve_check` | `1` compares every cached pool lookup with the locked lookup and stops on a difference. | Slows every pool lookup; the game stops if the two ever differ. | Caution |
|
||||
| `speed_probe` | `1` adds the player's position and ground speed to the frame-rate line that `TOTK_TRACE=perf` prints every 120 presents. | Only adds log text; has no effect without `TOTK_TRACE=perf`. | Safe |
|
||||
|
||||
## Profiling and measurement
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `ab` | Splits game time into blocks of this many seconds in the order A B B A and lets settings written as `ab` take effect only in the B blocks. | Settings that accept `ab` flip on and off every few seconds, so frame times and the picture change with the block; without such a setting it does nothing. | Caution |
|
||||
| `crit` | `<first>-<last>` frame range (with `TOTK_TRACE=sched`) that records every state change and wake of the guest threads, then writes a file at the last frame. | Recording adds overhead to every thread switch in the range and writes one file; `tools/crit_path.py` reads it. | Caution |
|
||||
| `crit_out` (default `crit.bin`) | Path of the file that `crit` writes. | Only moves the file; nothing is written without `crit`. | Safe |
|
||||
| `profile` | Path of a sampling profile file; the profiler suspends threads to unwind their stacks. | Frames get slower while it samples and the profile file grows; `tools/prof_report.py` reads it. | Caution |
|
||||
| `profile_chains` | `1` keeps call chains of every thread in the profile. | Larger profile file and more work per sample; has no effect without `profile`. | Caution |
|
||||
| `profile_frames` | `<lo>-<hi>` samples only while presents lo to hi-1 are pending; `w<lo>-<hi>` uses world-clock seconds. | Limits the profile to a window; has no effect without `profile`. | Safe |
|
||||
| `profile_main` | `1` samples only the guest main thread. | Narrows the profile; has no effect without `profile`. | Safe |
|
||||
| `profile_render` | `1` samples only the render thread and keeps its call chains. | Narrows the profile and makes it larger; has no effect without `profile`. | Caution |
|
||||
|
||||
## Capture and frame dumps
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `dump` | Frame number to dump: every render pass target and storage image of that frame is copied out as PNG, with `passes.txt` and `images.txt`. | Writes many PNG files and shader hashes to disk and slows the dumped frame; size is capped by the dump code. | Caution |
|
||||
| `dump_buf` | `<addr>:<size>[:<off>:<stride>:<w>:<h>]` snapshots one GPU buffer range after every logged pass of a dump. | Writes `buf_<pass>.bin` files and `bufs.txt`; has no effect without `dump`. | Caution |
|
||||
| `dump_dir` (default `dump`) | Folder that receives the frame dump files. | Only moves the output. | Safe |
|
||||
| `dump_log` | Frame number; logs the frames from there up to the dumped frame into `passes.txt`, with PNGs only of targets up to 64K texels. | Writes a longer `passes.txt` and more PNGs; has no effect without `dump`. | Caution |
|
||||
| `flight_at` | Comma-separated frame numbers that trigger a flight-recorder dump. | Writes a dump folder when each frame is reached; has no effect unless the flight recorder (`TOTK_FLIGHT=1`) is on. | Caution |
|
||||
| `flight_dir` | Folder for flight-recorder dumps; the default is `flight` beside the folder of `totk.exe`. | Only moves the output. | Safe |
|
||||
| `flight_frames` (default `600`) | Number of presents the flight recorder keeps. | A larger ring keeps more history and more thumbnails in memory; has no effect unless `TOTK_FLIGHT=1`. | Caution |
|
||||
| `flight_thumbs` (default `1`) | `0` turns off the 320x180 thumbnail the flight recorder saves for each present. | Dumps hold only `frames.tsv`; thumbnails cost a GPU blit per present when on. | Safe |
|
||||
| `rdoc` | Comma-separated frame numbers to capture with RenderDoc, which is loaded before Vulkan starts. | The game runs under RenderDoc, with its capture options set to keep `VK_EXT_external_memory_host` available and to reference all resources, and captures are written to disk. | Caution |
|
||||
| `rdoc_dir` | Output folder for RenderDoc captures (the default is `build/captures`). | Only moves the output. | Safe |
|
||||
| `rdoc_dll` | Path of the RenderDoc library, for when it is not found by default. | A wrong path logs that the library cannot be loaded and no capture is taken. | Safe |
|
||||
| `shots` | `<lo>-<hi>:<step>` saves every step-th present in the range as `f<n>.png`, scaled to 480x270; `w<lo>-<hi>:<step>` takes them by world-clock seconds. | Writes one PNG per step and adds a readback of the window texture; the pictures clip highlights in HDR. | Caution |
|
||||
| `shots_dir` (default `.`) | Folder for the `shots` pictures; it is created when needed. | Only moves the output. | Safe |
|
||||
| `shots_full` | Set to any value to save `shots` at full size instead of 480x270. | Files get far larger and writing them takes longer; has no effect without `shots`. | Caution |
|
||||
|
||||
## Automated tests and test hooks
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `background_window` | `1` keeps the window behind the user's windows, never fullscreen at start, and F11 keeps its place; SDL windows ignore it. | The window does not come to the front or flash, so it is easy to miss; meant for unattended runs. | Caution |
|
||||
| `gpu` | `vulkan` or `null`: the graphics back end; `null` has no GPU, completes every submit at once and returns zeros for texture reads. | `null` shows no picture; any other value stops the start with an error. | Caution |
|
||||
| `input` | `none` reports no input from the keyboard, mouse or controllers. | The game gets no controller input and the menu cursor stops following the mouse; `input_script` still applies. | Caution |
|
||||
| `input_script` | Path of a file with lines `<frame> <button> [hold]` or `<frame> LSTICK or RSTICK <x> <y> [hold]` that are ORed into the real input. | The game presses those buttons by itself; a file that cannot be opened is logged and ignored. | Caution |
|
||||
| `menu_test` | `<frame>[:<frames per page>]` opens the menu at that frame, shows each tab for 30 frames (or the given count), saves one screenshot per tab and closes the menu. | The menu opens by itself and screenshots are written. | Caution |
|
||||
| `menu_test_dir` | Folder for the `menu_test` screenshots; the default is the working directory. | Only moves the output. | Safe |
|
||||
| `menu_test_set` | `<frame>:<key>=<value>[,...][;...]` changes settings live at that frame, with or without the menu open; `SAVE=1` also writes `totk.ini`. | Settings change during play without input, and `SAVE=1` rewrites the file. | Caution |
|
||||
| `probe_movie` | Text that picks the movie for `totk.exe --probe-movie`: the first file whose path contains it. | Only used by the movie decoder probe; ignored in normal play. | Safe |
|
||||
| `probe_movie_shot` | Path of a `.ppm` file that the movie probe writes the middle frame to. | Only used by the movie decoder probe. | Safe |
|
||||
| `swkbd_answer` | `<text>`, `<cancel>` or `<initial>`: answers the on-screen keyboard (for example a horse name) without opening a window. | The game gets that answer every time it asks for text. | Caution |
|
||||
| `swkbd_test` | `ok` or `cancel` opens the keyboard window and presses OK or Cancel on the first timer tick. | The keyboard window closes by itself. | Caution |
|
||||
| `test_pad` | `<lx>,<ly>,<rx>,<ry>` attaches a virtual gamepad whose sticks rest at these raw SDL axis values (-32768 to 32767). | The game sees a controller with that drift or deflection; a malformed value is logged and ignored. | Caution |
|
||||
| `test_prepare_shaders` | `1` translates every recorded shader on all usable CPUs during start. | Start takes longer and fills the shader cache folder; the CPU is fully used while it runs. | Caution |
|
||||
|
||||
## Input
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `bind_<button>` | Remaps one Switch button or stick direction to `pad:<name>` or `key:<name>` entries; covers `bind_a`, `bind_b`, `bind_x`, `bind_y`, `bind_l`, `bind_r`, `bind_zl`, `bind_zr`, `bind_plus`, `bind_minus`, `bind_left`, `bind_up`, `bind_right`, `bind_down`, `bind_lclick`, `bind_rclick`, `bind_lstick_left`, `bind_lstick_up`, `bind_lstick_right`, `bind_lstick_down`, `bind_rstick_left`, `bind_rstick_up`, `bind_rstick_right`, `bind_rstick_down` and `bind_shake`. | A line replaces every default of that button; button prompts in the game still show the default layout. The Controls tab of the menu writes these lines; `docs/controls.md` lists the entry names. | Safe |
|
||||
|
||||
## Paths and files
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `amiibo_dir` | Folder the Amiibo tab starts in; the tab saves the folder you last browsed here. | Only changes which folder the tab lists first. | Safe |
|
||||
| `exefs` | Folder of the game's ExeFS (with `main.npdm`); a relative path is relative to `totk.exe`'s folder. | Without it, an `exefs` folder next to `totk.exe` is used, else the path set at build time. The game code is compiled into `totk.exe`, and the loader checks the files here by SHA-256, so files from another build or game version stop the start with a message. | Caution |
|
||||
| `mods_dir` (default `mods`) | Folder that holds one subfolder per player mod, beside `totk.exe`. | Mods found there replace game files; see `docs/mods.md`. | Caution |
|
||||
| `mods_disabled` | Names of mods that are switched off, separated by `\|`. | Listed mods are not applied; the Mods tab writes this line. | Safe |
|
||||
| `mods_order` | Mod names, top first, separated by `\|`; the top mod wins a file that two mods replace. | Changes which mod's file the game gets; unlisted mods go below the listed ones, by name. | Safe |
|
||||
| `pipeline_cache` | Path of the Vulkan pipeline cache file (default `pipeline-cache.bin` beside `totk.exe`). | Moves the cache; a new path starts empty and pipelines are built again. | Caution |
|
||||
| `profile_dir` | Folder of the game profile, which holds the user account id that save data is named after; the default is a `profile` folder beside `totk.exe`, else the build-time path. | Another folder means a different set of saves. | Save risk |
|
||||
| `romfs` | Folder of the game's RomFS (with `romfs.bin`); a relative path is relative to `totk.exe`'s folder. | Without it, a `romfs` folder next to `totk.exe` is used, else the build-time path; a missing image stops the start with a message. | Save risk |
|
||||
| `romfs_index` | Path of `romfs-index.bin`, which `tools/romfs_index.py` wrote; the default is beside `totk.exe`. | The index is checked against the image by SHA-256 and size; a mismatch stops the start. | Caution |
|
||||
| `romfs_mmap` | `1` maps the RomFS image into memory instead of reading it with positional reads. | Changes how the image is read; the log reports when the mapped path is on. | Caution |
|
||||
| `romfs_mods` | Folder of an overlay with a `manifest.tsv` of sizes and SHA-256 hashes; its files are served in place of the originals. | A missing file, size or hash mismatch, or a changed original stops the start; player mods win over this overlay. | Save risk |
|
||||
| `romfs_patches` | Folder of an overlay with a `manifest.tsv`, served in place of the original files; the default is `romfs-patches` beside the index. | Same checks as `romfs_mods`; while it is set the Xbox and PC button prompts are off, and the menu says so. | Save risk |
|
||||
| `romfs_proof_dir` | Folder for the `.romfs-verified-v1` proof file of a full verification; the default is beside the index. | Only moves one small file. | Safe |
|
||||
| `romfs_verify` | `full` hashes the whole RomFS image at start; `force` hashes it again even when a proof file exists. | Start takes many minutes for a 17 GB image; a proof file is written so later starts skip the hash. | Caution |
|
||||
| `shader_cache` | Folder of the translated shader cache (default `shader-cache` beside `totk.exe`). | A new folder starts empty, so shaders are translated again during play. | Caution |
|
||||
| `shared_fonts` | Folder of the extracted system fonts; the default is `shared-fonts` beside `totk.exe`, else beside the RomFS. | Simplified Chinese, Traditional Chinese and Korean need these fonts: without them the game exits before its first frame in those languages. Other languages do not use them. | Caution |
|
||||
|
||||
## Shader translation
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `xlate` | Path of the shader translator program; the default is `totk_xlate` beside `totk.exe`, else under `build/xlate`. | Shaders are translated by that program; if it is missing the log says so and graphics shaders cannot be translated. | Risky |
|
||||
| `xlate_id` | Text or 64 hex digits that identify the translator build in shader cache keys; the default comes from an `.id` file beside the translator, else a hash of the program. | A value that two different builds share lets the cache serve shaders translated by the other build. | Caution |
|
||||
| `xlate_keep` (default `0`) | `1` keeps the translation job of a last-frame variant as `<key>.job` with its code. | Writes extra files to the shader cache folder; meant for running the translator by hand. | Caution |
|
||||
| `xlate_procs` (default `2`) | Number of translator processes allowed during play. | More processes translate new shaders sooner and use more CPU and memory; values below 1 count as 1. | Caution |
|
||||
|
||||
## Graphics experiments
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `block_views_3d` (default `0`) | `1` gives 3D block-compressed images block views too. | On some AMD cards a GPU write past the end of the image's memory loses the Vulkan device, and the game stops. | Risky |
|
||||
| `feedback` | `0` samples a target that an earlier draw of the same pass wrote with no barrier; `restart` ends and restarts the pass behind a full barrier; otherwise the feedback-loop layout is used when the GPU supports it. | `0` brings back the old behaviour that the barrier fixed, so the picture can break in passes that read a target they draw into; `restart` costs extra passes. | Risky |
|
||||
| `flush_span` (default `0`) | `1` copies one span from the lowest to the highest flushed address of a pool, as an older version did. | Copies far more data to the GPU in crowded towns: an estimated 30-50 ms of GPU time per large copy over PCIe 3.0 x8. | Caution |
|
||||
| `freeze_idle` (default `60`) | Number of submissions a 2 MiB region stays unwritten before it is made read-only; `0` turns the freeze off. | A larger value keeps more regions writable and costs more write tracking; a write to a frozen region costs about 60 microseconds. | Caution |
|
||||
| `gpl_draw_build` (default `1`) | `0` leaves the missing shader libraries of a waiting draw to its background job instead of building them in parallel at once. | Draws whose pipeline is not ready are skipped meanwhile, until the job finishes. | Caution |
|
||||
| `lib_prebuild_keep` (default `0`) | Number of recorded graphics shader library variants that stay in memory after the boot prebuild. | Each kept library costs 140-460 KB of memory; the driver cache already makes a rebuild cheap. | Caution |
|
||||
| `minimal_layout` (default `1`) | `0` skips the compare against one-block tiles when a texture's memory layout is chosen. | The code calls it a negative control for fresh sessions; the comment does not say what the picture does. | Unknown |
|
||||
| `mirror_gap` (default `2`) | Hot pools at most this many 2 MiB regions apart share one write-watch query. | Changes the number of queries per submission; negative values count as 0. | Caution |
|
||||
| `movie_host` (default `1`) | `0` leaves every movie decoder to the game's own library. | The code comment says that library aborts at the first movie, so the game stops at the first cutscene. | Risky |
|
||||
| `movie_pace` (default `1`) | `0` hands every decoded movie frame over at the next present instead of when it is due. | Movies play at the game's frame rate rather than their own, so their speed no longer matches the sound. | Caution |
|
||||
| `pcss_k` (default `0.5`) | Depth-to-penumbra factor of soft shadows at 100 percent, for tuning. | Changes how wide soft shadows blur; has no effect while `pcss` is `0`. | Caution |
|
||||
| `pipe_sync` | Any value other than `0` or empty stops graphics pipelines from being built in the background. | Pipelines are built on the render path, which causes stutter when new effects appear. | Caution |
|
||||
| `push_stage` | `fs` pushes the descriptor set from the fragment stage instead of the vertex stage. | The log names the stage in use; the code does not say what changes in the picture. | Unknown |
|
||||
| `rec_end` (default `1`) | `0` lets the submitting thread end a recorded part again, as an older version did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
|
||||
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which unused released textures are dropped, 0 to 100; `0` is off. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets a 4 GiB card spill into system memory. Also a slider in the menu. | Caution |
|
||||
| `txd_transpose` (default `1`) | `0` turns off the swap of gradient components for gradient texture samples. | Terrain textures turned across the screen show crawling patches again. | Caution |
|
||||
| `ubo_snapshot` (default `1`) | `0` binds live memory for uniform blocks; `ab` alternates with the `ab` blocks. | Without snapshots late draws read another object's matrix, so objects can be drawn in the wrong place. | Risky |
|
||||
| `vk_no_float64` (default `0`) | `1` clears the 64-bit float feature, so the translator lowers FP64 to FP32. | Shaders change, so the shader cache entries for it are made again; precision of FP64 shaders drops. | Caution |
|
||||
| `vk_ssbo_align` (default unset) | Power of two that storage buffer offsets are treated as needing, to test the unaligned path. | The translator and the draw code round storage offsets down to it; shaders are translated again. | Caution |
|
||||
| `vk_subgroup` (default `32`) | Subgroup size to pin where the driver allows it. | A value other than 32 changes what the translator is told about the GPU, and shaders are translated again. | Caution |
|
||||
| `vram_headroom` (default `1`) | `0` turns off the spare half-block of video memory that a background thread keeps per memory type. | Texture allocations on the render thread can wait for a new block again. | Caution |
|
||||
| `vs_outs` | `all` declares all 32 vertex shader output locations; otherwise only the union of the paired fragment inputs. | Larger vertex shaders and new pipelines; unwritten outputs read the same as before. | Caution |
|
||||
| `wb_exact` (default `1`) | `0` decides write-back of GPU-written textures by page only. | A target that shares a page with data the CPU writes can go unwritten, so a name label next to the reticle can blink. | Caution |
|
||||
| `wb_merge` (default `1`) | `0` copies GPU-written ranges back as plain copies instead of writing word by word the words that changed. | In mirror memory mode the GPU's copy can overwrite words the CPU wrote, which breaks effects such as falling rain and waterfall splashes. | Caution |
|
||||
|
||||
## Motion vectors and DLSS
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `dlaa_jitter` (default `1`) | `0` turns off the camera jitter fed to DLSS. | DLSS gets no jitter between frames; a test setting. | Caution |
|
||||
| `dlaa_maskdebug` (default `0`) | `1` shows the reactive mask in place of the picture. | The frame shows a debug image while it is set. | Caution |
|
||||
| `dlaa_mv` (default `1`) | `0` sends zero motion vectors to DLSS. | DLSS sees no motion, so moving content can smear; a test setting. | Caution |
|
||||
| `dlaa_mvdebug` (default `0`) | `1` shows the motion vectors in place of the picture, x in red and y in green. | The frame shows a debug image while it is set. | Caution |
|
||||
| `dlaa_survey` (default `0`) | `1` logs what is drawn into the light target that DLSS reads. | Adds log output and per-draw work. | Caution |
|
||||
| `dlaa_times` (default `0`) | `1` times the DLSS passes on the GPU. | Adds GPU timestamps and log lines. | Caution |
|
||||
| `dlss_reactive` (default `0`) | `1` builds a reactive mask for DLSS where it ran. | Changes how DLSS blends moving areas; a test setting. | Caution |
|
||||
| `mv_check` | Frame interval; every that many DLSS frames copies the light target and motion images to host memory and logs how well the vectors explain the picture. | Costs a GPU-to-host copy and a comparison on the completion thread at each interval. | Caution |
|
||||
| `mv_survey` (default `0`) | `1` notes the uniform blocks of each scene draw and compares them with those of earlier frames. | Adds per-draw work and log output. | Caution |
|
||||
| `mvobj_all` (default `0`) | `1` also lists draws for object motion that read no object blocks (terrain and grass). | More draws are checked and listed, which costs time. | Caution |
|
||||
| `mvobj_banks` (default `0`) | `1` logs which words of the other uniform banks changed for each scene draw. | Adds log output and per-draw work. | Caution |
|
||||
| `mvobj_equal` (default `0`) | `1` uses an EQUAL depth compare for the object motion pass, for comparison. | Changes which pixels get object vectors; the code comment names the default as the one that hides objects behind nearer geometry. | Caution |
|
||||
| `mvobj_exact` | Hex bank mask; only draws whose position uses exactly these banks get object motion. | Fewer draws get object vectors, so their motion falls back to the camera's. | Caution |
|
||||
| `mvobj_exec` (default `1`) | `0` lists object motion draws but never records them. | Objects get camera motion only. | Caution |
|
||||
| `mvobj_noc4` (default `0`) | `1` draws again positions that have no scene camera bank. | Those draws get this frame's view, so they can show wrong motion. | Caution |
|
||||
| `mvobj_only` | Hex bank mask; only draws whose position reads no bank outside the mask get object motion. | Fewer draws get object vectors. | Caution |
|
||||
| `mvobj_times` (default `0`) | `1` adds timings to the object-motion summary. | The clock reads themselves cost frame time. | Caution |
|
||||
| `mvprev_check` (default `0`) | `1` verifies the stored first bytes of each previous-frame table entry. | The code comment measures 3.6 ms per frame. | Caution |
|
||||
| `mvprev_grow` (default `1`) | `0` keeps only three banks in the previous-frame tables. | Object motion that depends on the other banks is no longer tracked. | Caution |
|
||||
| `mvprev_skip` | Hex bank mask of banks left out of the previous-frame copy, replacing the default. | Different banks are tracked, which changes object motion. | Caution |
|
||||
| `mvprev_trust` | Hex bank mask of banks whose found previous-frame copy is taken without the check. | A copy that does not match is used anyway, which can give wrong motion. | Caution |
|
||||
|
||||
## Scheduling and host
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `app_pool` | Size in bytes of the guest application memory pool; accepts decimal or `0x` hex. | The default depends on the resolution settings; `0` stops the start with a message, and too small a pool leaves the game without memory. | Risky |
|
||||
| `audio_off` | Any value mixes audio without opening an output device. | The game runs in silence and reports two channels; same as `audio = none`. | Caution |
|
||||
| `one_per_core` (default `0`) | `1` keeps the process on the first logical processor of each physical core; `ab` switches it with the `ab` blocks. | Busy threads stop sharing a core with a sibling thread, which can help or hurt on the CPU; it only applies when the CPU has hyperthreads. | Caution |
|
||||
| `reexec` | Counter of address-space retries that the runtime sets itself when it starts the process again. | The counter is cleared when the window is recreated; a value of 20 or more stops the start with a fatal error. | Risky |
|
||||
| `restart_wait` | Set by the runtime when it restarts itself; names the old process by id and creation time so the new process waits for it to exit. | Meant for the runtime's own use; the code does not describe a manual value. | Unknown |
|
||||
| `spin` (default `1`) | `0` makes a guest thread that polls memory keep its virtual core. | Polling threads can starve the thread whose item they wait for, so frames can stall. | Caution |
|
||||
| `spin_handoff_us` (default `50`) | Microseconds a polling wait lasts before it gives its virtual core to a waiting thread. | A shorter value hands cores over sooner; a longer one keeps them longer. | Caution |
|
||||
|
||||
## Generated-code patches
|
||||
|
||||
These names are code patch sets built into the generated game code (`recomp/patches-*.txt`). `rt_patches_init` reads the environment name at start, then the resolution and mouse code switches some sets itself.
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `aspect_patch` (default `0`) | Patch set that feeds the camera the output's aspect ratio when it is not 16:9. | The resolution code switches this set on or off itself at start, so a value here is replaced; the code does not settle what a manual value does. | Unknown |
|
||||
| `fov_patch` (default `0`) | Patch set that scales the camera's vertical view angle by the field-of-view setting. | The resolution code switches this set itself whenever the field of view is not 100 percent; a manual value is replaced. | Unknown |
|
||||
| `mouse_direct_patch` (default `1`) | Patch set of the hooks in the game's camera code behind `mouse_mode = direct` on game 1.4.3. | `0` stops `mouse_mode = direct` from taking effect; other mouse modes are unaffected. | Safe |
|
||||
| `patch_output` (default `0`) | Patch set that makes the game use the configured output size for its swapchain and display configuration. | The resolution code switches this set whenever the output differs from the base size; a manual value is replaced. | Unknown |
|
||||
| `patch_scene` (default `0`) | Patch set that makes the game use the configured scene render size. | The resolution code switches this set from the render size setting; a manual value is replaced. | Unknown |
|
||||
@@ -4,7 +4,7 @@
|
||||
[--temp DIR]
|
||||
|
||||
Modules: layout (where the bundled tools are, release or repository), events (JSON progress on stdout and the log
|
||||
file), state (totk-install.json), runner (which steps run: resume and update), steps (the seven steps), ini (totk.ini),
|
||||
file), state (totk-install.json), runner (which steps run: resume and update), steps (the eight steps), ini (totk.ini),
|
||||
cli (the command line). text/en.json holds the English text of every message id the events carry.
|
||||
|
||||
Standard library only, apart from what the extraction tools import (cryptography). Runs on Python 3.14.
|
||||
|
||||
+10
-4
@@ -1,5 +1,5 @@
|
||||
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR]
|
||||
[--root DIR] [--temp DIR] [--verify]
|
||||
[--root DIR] [--temp DIR] [--verify] [--dlss]
|
||||
|
||||
A first install needs every option. --firmware names the Switch firmware folder (an unpacked firmware or an emulator's
|
||||
Contents/registered); the setup extracts the system shared fonts from it when the language is Chinese, Korean or
|
||||
@@ -7,6 +7,9 @@ Traditional Chinese. On a folder with totk-install.json (resume after a cancel o
|
||||
release) only --dest is needed: --lang defaults to the recorded language, and the game files are needed only when the
|
||||
extraction has to run again. Prints JSON events on stdout (events.py). Exit code: 0 done, 1 failed, 2 bad command
|
||||
line, 3 cancelled (Ctrl+C or Ctrl+Break).
|
||||
|
||||
--dlss downloads NVIDIA's DLSS library (nvngx_dlss.dll) from NVIDIA's DLSS SDK repository into the install folder on
|
||||
Windows with an NVIDIA RTX GPU (steps.py, Dlss).
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
@@ -19,7 +22,7 @@ from . import layout as layouts
|
||||
from . import state as statefile
|
||||
from .events import Reporter
|
||||
from .runner import Context, run_steps
|
||||
from .steps import all_steps
|
||||
from .steps import all_steps, migrate_state
|
||||
|
||||
EXIT = {'ok': 0, 'failed': 1, 'cancelled': 3}
|
||||
|
||||
@@ -47,8 +50,8 @@ def cancel(signum, frame):
|
||||
|
||||
def main(argv=None):
|
||||
ap = Parser(prog='python -m installer', description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--base', type=Path, help='base game, XCI or NSP')
|
||||
ap.add_argument('--update', type=Path, help='the update NSP: 1.1.0, 1.2.1 or 1.4.3')
|
||||
ap.add_argument('--base', type=Path, help='base game, XCI or NSP (or NSZ, XCZ: unpacked first)')
|
||||
ap.add_argument('--update', type=Path, help='the update NSP or NSZ: 1.1.0, 1.2.1 or 1.4.3')
|
||||
ap.add_argument('--keys', type=Path, help='prod.keys')
|
||||
ap.add_argument('--firmware', type=Path, help='Switch firmware folder (Contents/registered) for the shared fonts '
|
||||
'of Chinese, Korean and Traditional Chinese text')
|
||||
@@ -58,6 +61,8 @@ def main(argv=None):
|
||||
ap.add_argument('--temp', type=Path, help='temp folder (default: temp/ of the release on Windows, .setup-temp in '
|
||||
'the install folder on Linux, build/install-temp in a repository)')
|
||||
ap.add_argument('--verify', action='store_true', help='hash every kept file, also those this release made')
|
||||
ap.add_argument('--dlss', action='store_true', help="download NVIDIA's nvngx_dlss.dll (NVIDIA RTX SDK license) next "
|
||||
'to the game, for dlaa and the DLSS modes')
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
# The window cancels with Ctrl+Break to the installer's process group (CREATE_NEW_PROCESS_GROUP), which also reaches
|
||||
@@ -79,6 +84,7 @@ def main(argv=None):
|
||||
rep.emit('end', result='failed', seconds=0)
|
||||
return 1
|
||||
ctx = Context(layout, dest, temp, rep, args, state=state, verify=args.verify)
|
||||
migrate_state(ctx)
|
||||
steps = all_steps()
|
||||
rep.emit('start', release=layout.release, mode=ctx.mode, dev=layout.dev, dest=str(dest), log=str(log),
|
||||
steps=[s.id for s in steps])
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""The display scale of the installer window: process DPI awareness and the pixel sizes that follow it.
|
||||
|
||||
Windows draws a DPI-unaware process at 96 DPI and stretches its windows to the display's scale, which is what made the
|
||||
setup window blurry at 125% and above. gui.py calls enable() before it imports tkinter, so the process is system DPI
|
||||
aware before its first window exists; Tk then reads the display's DPI when it starts (`tk scaling` is 2.667 at 200%)
|
||||
and every font size in points comes out at the right size. The pixel sizes of the layout have no such scaling of their
|
||||
own, so gui.py passes them through px().
|
||||
|
||||
System DPI awareness, not per-monitor: Tk 9.0.4, the one the release bundles, has no per-monitor support (its DLL
|
||||
imports no DPI-awareness function), so it draws everything at the system DPI. Per-monitor awareness would leave the
|
||||
window crisp on a display at another scale but with the primary display's font sizes; system awareness draws the
|
||||
primary display exactly and lets Windows stretch the window only on a display whose scale differs from it, as before.
|
||||
|
||||
On Linux the window stays 1:1 (px returns n as it is): Tk and the desktop scale what they draw.
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
|
||||
DPI_96 = 96 # the scale Windows calls 100%
|
||||
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
|
||||
PROCESS_SYSTEM_DPI_AWARE = 1 # SetProcessDpiAwareness' value for the same thing (Windows 8.1)
|
||||
LOGPIXELSX = 88 # GetDeviceCaps' horizontal DPI, the only source before Windows 10 1607
|
||||
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 # ctypes.WinDLL's winmode: System32 only, never the installer's folder
|
||||
|
||||
_scale = None
|
||||
|
||||
|
||||
def enable():
|
||||
"""Make the process system DPI aware, then return the display scale (1.0 at 96 DPI). Must run before the first
|
||||
window; a later call only returns the scale."""
|
||||
global _scale
|
||||
if _scale is None:
|
||||
dpi = _system_dpi() if os.name == 'nt' else DPI_96
|
||||
_scale = dpi / DPI_96
|
||||
return _scale
|
||||
|
||||
|
||||
def scale():
|
||||
"""The display scale: 1.0 at 100%, 2.0 at 200%."""
|
||||
return enable()
|
||||
|
||||
|
||||
def px(n):
|
||||
"""n pixels at 100% as this display's pixels: 14 stays 14 at 100% and becomes 28 at 200%."""
|
||||
return int(round(n * enable()))
|
||||
|
||||
|
||||
def awareness():
|
||||
"""What Windows says the process is: 0 unaware, 1 system aware, 2 per-monitor aware."""
|
||||
if os.name != 'nt':
|
||||
return 0
|
||||
try: # Windows 10 1607
|
||||
user32 = ctypes.WinDLL('user32')
|
||||
user32.GetThreadDpiAwarenessContext.restype = ctypes.c_void_p
|
||||
user32.GetAwarenessFromDpiAwarenessContext.argtypes = [ctypes.c_void_p]
|
||||
return int(user32.GetAwarenessFromDpiAwarenessContext(user32.GetThreadDpiAwarenessContext()))
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 8.1
|
||||
value = ctypes.c_int()
|
||||
shcore = ctypes.WinDLL('shcore', winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)
|
||||
shcore.GetProcessDpiAwareness.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)]
|
||||
if shcore.GetProcessDpiAwareness(None, ctypes.byref(value)) == 0:
|
||||
return value.value
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows Vista
|
||||
return 1 if ctypes.WinDLL('user32').IsProcessDPIAware() else 0
|
||||
except (AttributeError, OSError):
|
||||
return 0
|
||||
|
||||
|
||||
def _system_dpi():
|
||||
"""Make the process system DPI aware and return the display DPI. Each call that this Windows version has is tried;
|
||||
one failing means the process already is aware (a manifest, or the call before it) and changes nothing."""
|
||||
try:
|
||||
user32 = ctypes.WinDLL('user32', use_last_error=True)
|
||||
except OSError:
|
||||
return DPI_96
|
||||
try: # Windows 10 1607, and the only way to ask for the system context explicitly
|
||||
user32.SetProcessDpiAwarenessContext.argtypes = [ctypes.c_void_p]
|
||||
user32.SetProcessDpiAwarenessContext.restype = ctypes.c_bool
|
||||
user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE))
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 8.1
|
||||
ctypes.WinDLL('shcore', winmode=LOAD_LIBRARY_SEARCH_SYSTEM32).SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows Vista
|
||||
user32.SetProcessDPIAware()
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 10 1607
|
||||
return int(user32.GetDpiForSystem()) or DPI_96
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # before that
|
||||
gdi32 = ctypes.WinDLL('gdi32')
|
||||
user32.GetDC.restype = ctypes.c_void_p
|
||||
user32.GetDC.argtypes = [ctypes.c_void_p]
|
||||
user32.ReleaseDC.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
gdi32.GetDeviceCaps.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
dc = user32.GetDC(None)
|
||||
dpi = gdi32.GetDeviceCaps(ctypes.c_void_p(dc), LOGPIXELSX)
|
||||
user32.ReleaseDC(None, ctypes.c_void_p(dc))
|
||||
return dpi or DPI_96
|
||||
except (AttributeError, OSError):
|
||||
return DPI_96
|
||||
+133
-50
@@ -3,7 +3,8 @@
|
||||
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the optional firmware
|
||||
folder, the game language and the install folder, runs `python -m installer ...` as a subprocess and shows its JSON
|
||||
events. Decisions live in guilogic.py; this file only draws. The window text is in
|
||||
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids.
|
||||
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids. On Windows the window follows the
|
||||
display's DPI (installer/dpi.py): fonts and paddings grow with the scale, so Windows does not stretch it.
|
||||
|
||||
--selftest [--shots DIR] build every screen in every language and close by itself (PNG per screen with --shots)
|
||||
--lang en|ru|pt-BR|zh-Hans window language (default: the Windows display language)
|
||||
@@ -15,9 +16,18 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from . import dpi
|
||||
|
||||
# Windows fixes the DPI awareness of a process when its first window is created, and Tk reads the display's DPI when it
|
||||
# starts: both happen inside the first tkinter.Tk(), so this call comes before tkinter is imported at all.
|
||||
dpi.enable()
|
||||
|
||||
import tkinter as tk # noqa: E402 (after dpi.enable, see above)
|
||||
from tkinter import filedialog, messagebox, ttk # noqa: E402
|
||||
|
||||
try:
|
||||
from PIL import ImageGrab # only the screenshots of --selftest --shots need it; the release has no PIL
|
||||
@@ -26,13 +36,28 @@ except ImportError:
|
||||
|
||||
from . import ini
|
||||
from . import layout as layouts
|
||||
from . import recent
|
||||
from .guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, UI_LANGUAGE_LABELS, UI_LANGUAGES, Catalog, Form, Model,
|
||||
Process, amount, default_game_language, describe_event, parse_line, windows_ui_language)
|
||||
|
||||
POLL_MS = 100
|
||||
GLYPH = {PENDING: '\u25cb', RUNNING: '\u25b6', DONE: '\u2714', SKIPPED: '\u2013', FAILED: '\u2716'}
|
||||
COLOR = {PENDING: '#808080', RUNNING: '#1a5fb4', DONE: '#26a269', SKIPPED: '#808080', FAILED: '#c01c28'}
|
||||
PAD = 14
|
||||
# The window's pixels at 100%: Tk scales the fonts by itself (tk scaling), these through installer/dpi.py.
|
||||
PAD = dpi.px(14) # padding of the window and its frames
|
||||
WRAP = dpi.px(620) # wrap width of the long labels
|
||||
|
||||
RECENT_WAIT = 1.5 # seconds the window waits for the recorded paths
|
||||
|
||||
|
||||
def load_recent(wait=RECENT_WAIT):
|
||||
"""recent.load() in a daemon thread, waited for at most `wait` seconds: a recorded path on a disconnected network
|
||||
share can block its stat call for a long time. Nothing is prefilled when it has not returned by then."""
|
||||
out = {}
|
||||
thread = threading.Thread(target=lambda: out.update(recent.load()), daemon=True)
|
||||
thread.start()
|
||||
thread.join(wait)
|
||||
return {} if thread.is_alive() else out
|
||||
|
||||
|
||||
def fix_tcl_paths():
|
||||
@@ -57,6 +82,11 @@ class App:
|
||||
self.bound = [] # (widget, option, id, params) retranslated on a language switch
|
||||
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'firmware', 'dest')}
|
||||
self.vars['lang'] = tk.StringVar()
|
||||
self.recorded = {} # base/update paths prefilled from the recorded ones, by field
|
||||
self.touched = set() # fields the user picked or typed
|
||||
self.programmatic = False
|
||||
for name in ('base', 'update'):
|
||||
self.vars[name].trace_add('write', lambda *_, n=name: self.touched.add(n) if not self.programmatic else None)
|
||||
self.lang_code = default_game_language(ui_lang)
|
||||
self.screen = None
|
||||
self.fitted = None # the (width, height) fit last gave the window
|
||||
@@ -74,7 +104,7 @@ class App:
|
||||
self.mono_font = 'Consolas' if os.name == 'nt' else 'DejaVu Sans Mono'
|
||||
style = ttk.Style(root)
|
||||
style.configure('Title.TLabel', font=(self.ui_font, 15, 'bold'))
|
||||
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=8)
|
||||
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=dpi.px(8))
|
||||
style.configure('Err.TLabel', foreground='#c01c28')
|
||||
style.configure('Version.TLabel', foreground='#808080')
|
||||
|
||||
@@ -84,7 +114,7 @@ class App:
|
||||
self.title.pack(side='left')
|
||||
version = ttk.Label(top, style='Version.TLabel')
|
||||
self.bind_text(version, 'text', 'ui.version', version=layout.release)
|
||||
version.pack(side='left', padx=(10, 0), pady=(6, 0))
|
||||
version.pack(side='left', padx=(dpi.px(10), 0), pady=(dpi.px(6), 0))
|
||||
sw = ttk.Frame(top)
|
||||
sw.pack(side='right')
|
||||
self.ui_var = tk.StringVar(value=self.cat.lang) # the window language code; the box shows its name
|
||||
@@ -103,6 +133,15 @@ class App:
|
||||
self.bind_text(self.title, 'text', 'ui.rebuild_title')
|
||||
self.vars['dest'].set(str(rebuild))
|
||||
root.after(POLL_MS, self.start)
|
||||
else:
|
||||
# recorded sources fill only empty fields; dest goes last, its trace runs refresh_form (update hint and
|
||||
# recorded language). In update mode filled files are harmless: Extract.current hashes one only when its
|
||||
# size or mtime differ from the recorded ones.
|
||||
last = load_recent()
|
||||
self.recorded = {n: last[n] for n in ('base', 'update') if n in last and not self.vars[n].get().strip()}
|
||||
for name in ('base', 'update', 'keys', 'firmware', 'dest'):
|
||||
if name in last and not self.vars[name].get().strip():
|
||||
self.set_prefill(name, last[name])
|
||||
|
||||
# ---- text -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -185,19 +224,20 @@ class App:
|
||||
self.root.geometry(f'{max(w, self.root.winfo_width())}x{max(h, self.root.winfo_height())}')
|
||||
|
||||
def notice(self, parent):
|
||||
w = ttk.Label(parent, style='Notice.TLabel', wraplength=620, justify='left')
|
||||
w = ttk.Label(parent, style='Notice.TLabel', wraplength=WRAP, justify='left')
|
||||
self.bind_text(w, 'text', 'ui.notice')
|
||||
return w
|
||||
|
||||
def build_form(self):
|
||||
f = ttk.Frame(self.body)
|
||||
gap = dpi.px(4) # between the form's rows
|
||||
bar = ttk.Frame(f) # packed first so pack shrinks the text above it, never the buttons
|
||||
bar.pack(side='bottom', fill='x', pady=(12, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(12), 0))
|
||||
self.start_button = self.button(bar, 'ui.start', self.start)
|
||||
self.start_button.pack(side='right')
|
||||
self.form_intro = self.label(f, 'ui.intro', wraplength=620, justify='left')
|
||||
self.form_intro.pack(anchor='w', pady=(0, 8))
|
||||
self.notice(f).pack(fill='x', pady=(0, 10))
|
||||
self.form_intro = self.label(f, 'ui.intro', wraplength=WRAP, justify='left')
|
||||
self.form_intro.pack(anchor='w', pady=(0, dpi.px(8)))
|
||||
self.notice(f).pack(fill='x', pady=(0, dpi.px(10)))
|
||||
grid = ttk.Frame(f)
|
||||
grid.pack(fill='x')
|
||||
grid.columnconfigure(1, weight=1)
|
||||
@@ -206,37 +246,44 @@ class App:
|
||||
self.form_rows = {}
|
||||
for i, (name, label_id, pick_id) in enumerate(rows):
|
||||
lab = self.label(grid, label_id)
|
||||
lab.grid(row=i, column=0, sticky='w', pady=4, padx=(0, 10))
|
||||
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
lab.grid(row=i, column=0, sticky='w', pady=gap, padx=(0, dpi.px(10)))
|
||||
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=gap)
|
||||
b = self.button(grid, 'ui.browse', lambda n=name, p=pick_id: self.pick_file(n, p))
|
||||
b.grid(row=i, column=2, padx=(8, 0))
|
||||
b.grid(row=i, column=2, padx=(dpi.px(8), 0))
|
||||
self.form_rows[name] = lab
|
||||
i = len(rows)
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(8, 0))
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=gap)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=gap)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(dpi.px(8), 0))
|
||||
i += 1
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=4)
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=gap)
|
||||
self.lang_box = ttk.Combobox(grid, textvariable=self.vars['lang'], state='readonly', width=30)
|
||||
self.lang_box.grid(row=i, column=1, sticky='w', pady=4)
|
||||
self.lang_box.grid(row=i, column=1, sticky='w', pady=gap)
|
||||
self.lang_touched = False
|
||||
self.lang_box.bind('<<ComboboxSelected>>', lambda _e: setattr(self, 'lang_touched', True))
|
||||
self.fill_languages()
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(8, 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(8, 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=620, justify='left')
|
||||
self.form_error.pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.keys_private', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=gap)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=gap)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.compressed_help', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
# On by default and named with its source and licence: the player sees what is downloaded and can untick it
|
||||
self.dlss_var = tk.BooleanVar(value=True)
|
||||
dlss = ttk.Checkbutton(f, variable=self.dlss_var)
|
||||
self.bind_text(dlss, 'text', 'ui.dlss')
|
||||
dlss.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.dlss_help', wraplength=WRAP, justify='left').pack(anchor='w', padx=(dpi.px(22), 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=WRAP, justify='left')
|
||||
self.form_error.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.keys_private', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
return f
|
||||
|
||||
def build_run(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f)
|
||||
bar.pack(side='bottom', fill='x', pady=(8, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(8), 0))
|
||||
self.cancel_button = self.button(bar, 'ui.cancel', self.cancel)
|
||||
self.cancel_button.pack(side='right')
|
||||
self.log_toggle = self.button(bar, 'ui.show_log', self.toggle_log)
|
||||
@@ -245,14 +292,22 @@ class App:
|
||||
self.run_header.pack(anchor='w')
|
||||
self.step_rows = {}
|
||||
self.steps_box = ttk.Frame(f)
|
||||
self.steps_box.pack(fill='x', pady=(8, 8))
|
||||
self.phase_label = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.steps_box.pack(fill='x', pady=dpi.px(8))
|
||||
self.phase_label = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.phase_label.pack(anchor='w')
|
||||
self.bar = ttk.Progressbar(f, mode='determinate', maximum=1000)
|
||||
self.bar.pack(fill='x', pady=(4, 6))
|
||||
self.space_label = ttk.Label(f, wraplength=620, justify='left')
|
||||
bar_holder = ttk.Frame(f)
|
||||
bar_holder.pack(fill='x', pady=(dpi.px(4), dpi.px(6)))
|
||||
self.bar = ttk.Progressbar(bar_holder, mode='determinate', maximum=1000)
|
||||
self.bar.pack(fill='both', expand=True)
|
||||
if os.name == 'nt' and ttk.Style(f).theme_use() == 'vista':
|
||||
# The vista theme draws the bar 22 px high at every DPI and has no thickness option: the holder gives it the
|
||||
# height it would have at 100%, scaled with the display. Every other theme draws its own height, and the
|
||||
# holder follows it.
|
||||
bar_holder.pack_propagate(False)
|
||||
bar_holder.configure(height=dpi.px(22))
|
||||
self.space_label = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.space_label.pack(anchor='w')
|
||||
self.warn_label = ttk.Label(f, wraplength=620, justify='left', foreground='#9a6700')
|
||||
self.warn_label = ttk.Label(f, wraplength=WRAP, justify='left', foreground='#9a6700')
|
||||
self.warn_label.pack(anchor='w')
|
||||
self.log_frame = ttk.Frame(f)
|
||||
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=(self.mono_font, 9))
|
||||
@@ -266,33 +321,60 @@ class App:
|
||||
def build_finish(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f)
|
||||
bar.pack(side='bottom', fill='x', pady=(12, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(12), 0))
|
||||
self.button(bar, 'ui.close', self.on_close).pack(side='right')
|
||||
self.open_button = self.button(bar, 'ui.open_folder', self.open_folder)
|
||||
self.open_button.pack(side='right', padx=(0, 8))
|
||||
self.open_button.pack(side='right', padx=(0, dpi.px(8)))
|
||||
self.back_button = self.button(bar, 'ui.back', lambda: self.show('form'))
|
||||
self.back_button.pack(side='left')
|
||||
if self.rebuild: # a failed or cancelled rebuild: the game starts with the code it has
|
||||
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, 8))
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=620, justify='left')
|
||||
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, dpi.px(8)))
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=WRAP, justify='left')
|
||||
self.finish_title.pack(anchor='w')
|
||||
self.finish_text = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.finish_text.pack(anchor='w', pady=(8, 8))
|
||||
self.finish_text = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.finish_text.pack(anchor='w', pady=dpi.px(8))
|
||||
self.finish_steps = ttk.Frame(f)
|
||||
self.finish_steps.pack(fill='x', pady=(0, 8))
|
||||
self.finish_steps.pack(fill='x', pady=(0, dpi.px(8)))
|
||||
self.finish_notice = self.notice(f)
|
||||
self.finish_notice.pack(fill='x', pady=(8, 0))
|
||||
self.finish_notice.pack(fill='x', pady=(dpi.px(8), 0))
|
||||
return f
|
||||
|
||||
# ---- form -----------------------------------------------------------------------------------------------------
|
||||
|
||||
def form(self):
|
||||
return Form(base=self.vars['base'].get(), update=self.vars['update'].get(), keys=self.vars['keys'].get(),
|
||||
firmware=self.vars['firmware'].get(), lang=self.selected_lang(), dest=self.vars['dest'].get())
|
||||
firmware=self.vars['firmware'].get(), lang=self.selected_lang(), dest=self.vars['dest'].get(),
|
||||
dlss=self.dlss_var.get())
|
||||
|
||||
def set_prefill(self, name, value):
|
||||
self.programmatic = True
|
||||
try:
|
||||
self.vars[name].set(value)
|
||||
finally:
|
||||
self.programmatic = False
|
||||
|
||||
def match_prefill(self, state):
|
||||
"""Prefilled base/update stay only while the chosen install recorded the same file (name and size, one stat);
|
||||
another install would otherwise extract again from them. Fields the user touched are left alone."""
|
||||
for name, value in self.recorded.items():
|
||||
if name in self.touched:
|
||||
continue
|
||||
keep = True
|
||||
if state is not None:
|
||||
known = (state.get('inputs') or {}).get(name)
|
||||
try:
|
||||
keep = (isinstance(known, dict) and Path(value).name == known.get('name')
|
||||
and Path(value).stat().st_size == known.get('size'))
|
||||
except (OSError, ValueError):
|
||||
keep = False
|
||||
want = value if keep else ''
|
||||
if self.vars[name].get() != want:
|
||||
self.set_prefill(name, want)
|
||||
|
||||
def refresh_form(self):
|
||||
form = self.form()
|
||||
state = form.installed_state()
|
||||
self.match_prefill(state)
|
||||
if state is not None:
|
||||
self.update_hint.configure(text=self.cat('ui.update_mode'))
|
||||
recorded = ini.canonical_language(state.get('lang'))
|
||||
@@ -303,9 +385,8 @@ class App:
|
||||
self.fit()
|
||||
|
||||
def pick_file(self, name, pick_id):
|
||||
types = [(self.cat('ui.type_game'), '*.nsp *.xci'), (self.cat('ui.type_all'), '*.*')]
|
||||
if name == 'update':
|
||||
types = [(self.cat('ui.type_nsp'), '*.nsp'), (self.cat('ui.type_all'), '*.*')]
|
||||
# One filter for every game file: the setup tells an NSZ or XCZ from a plain file by its content (Check.unpack)
|
||||
types = [(self.cat('ui.type_game'), '*.nsp *.xci *.nsz *.xcz'), (self.cat('ui.type_all'), '*.*')]
|
||||
if name == 'keys':
|
||||
types = [(self.cat('ui.type_keys'), '*.keys'), (self.cat('ui.type_all'), '*.*')]
|
||||
path = filedialog.askopenfilename(title=self.cat(pick_id), filetypes=types)
|
||||
@@ -332,6 +413,7 @@ class App:
|
||||
self.fit()
|
||||
return
|
||||
self.form_error.configure(text='')
|
||||
recent.save(form.dest, {name: getattr(form, name) for name in recent.SOURCES})
|
||||
# in a repository the Python running this window: the tools' embeddable Python there has no ._pth entry for
|
||||
# the repository, so `-m installer` would not find the package
|
||||
python = Path(sys.executable) if self.layout.dev else self.layout.python
|
||||
@@ -358,7 +440,7 @@ class App:
|
||||
self.step_rows = {}
|
||||
for s in steps:
|
||||
row = ttk.Frame(self.steps_box)
|
||||
row.pack(fill='x', pady=1)
|
||||
row.pack(fill='x', pady=dpi.px(1))
|
||||
glyph = ttk.Label(row, width=3)
|
||||
glyph.pack(side='left')
|
||||
name = ttk.Label(row)
|
||||
@@ -443,7 +525,7 @@ class App:
|
||||
def toggle_log(self):
|
||||
self.log_open = not self.log_open
|
||||
if self.log_open:
|
||||
self.log_frame.pack(fill='both', expand=True, pady=(8, 0))
|
||||
self.log_frame.pack(fill='both', expand=True, pady=(dpi.px(8), 0))
|
||||
else:
|
||||
self.log_frame.pack_forget()
|
||||
self.bound = [b for b in self.bound if b[0] is not self.log_toggle]
|
||||
@@ -537,7 +619,7 @@ class App:
|
||||
# ---- self test ---------------------------------------------------------------------------------------------------
|
||||
|
||||
SAMPLE_START = {'type': 'start', 'release': 'selftest', 'mode': 'install', 'dest': 'Z:/selftest/TotK', 'log': 'D:\\log.txt',
|
||||
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup']}
|
||||
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup']}
|
||||
SAMPLE_RUN = [
|
||||
SAMPLE_START,
|
||||
{'type': 'plan', 'run': ['check', 'fonts', 'extract'], 'skip': {}},
|
||||
@@ -580,6 +662,7 @@ def grab(root, path):
|
||||
def selftest(layout, shots=None):
|
||||
"""Every screen in every language; the window closes itself. Returns 0 when nothing raised."""
|
||||
fix_tcl_paths()
|
||||
os.environ[recent.ENV] = os.path.join(tempfile.mkdtemp(prefix='totk-selftest-'), 'setup.json')
|
||||
root = tk.Tk()
|
||||
root.geometry('+60+40')
|
||||
app = App(root, 'en', layout)
|
||||
|
||||
@@ -27,7 +27,7 @@ TEXT = Path(__file__).resolve().parent / 'text'
|
||||
UI_LANGUAGES = ('en', 'ru', 'pt-BR', 'zh-Hans')
|
||||
# what the language switch in the window's corner shows for each
|
||||
UI_LANGUAGE_LABELS = {'en': 'English', 'ru': 'Русский', 'pt-BR': 'Português (Brasil)', 'zh-Hans': '简体中文'}
|
||||
STEP_ORDER = ('check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup')
|
||||
STEP_ORDER = ('check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup')
|
||||
# message parameter -> catalog id prefix of the names that replace its raw values
|
||||
PARAM_NAMES = {'lang': 'lang.', 'known': 'lang.', 'step': 'step.', 'input': 'param.input.'}
|
||||
LANG_RU = 0x19 # primary language id of Russian in a Windows LANGID
|
||||
@@ -137,6 +137,7 @@ class Form:
|
||||
firmware: str = ''
|
||||
lang: str = ''
|
||||
dest: str = ''
|
||||
dlss: bool = False
|
||||
|
||||
def installed_state(self):
|
||||
"""The recorded state of the install folder (totk-install.json), or None. Never raises."""
|
||||
@@ -187,6 +188,8 @@ class Form:
|
||||
cmd += ['--temp', str(temp)]
|
||||
if verify_hashes:
|
||||
cmd.append('--verify')
|
||||
if self.dlss:
|
||||
cmd.append('--dlss')
|
||||
return cmd
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
No console window. The two message strings come from the catalog (installer/text/<lang>.json, ids launcher.title and
|
||||
launcher.python_missing); tools/make_release.py writes them into launcher_text.h before it compiles this file.
|
||||
Build: llvm/bin/llvm-windres -I runtime -O coff launcher.rc -o launcher_rc.o, then
|
||||
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o TotK-Setup.exe */
|
||||
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o TotK-Setup.exe
|
||||
|
||||
System DPI aware, like the window's Python process (installer/dpi.py): Windows draws the error box of fail() at the
|
||||
display's scale instead of stretching it, which it would at 125% and above. */
|
||||
#include <windows.h>
|
||||
#include <wchar.h>
|
||||
|
||||
@@ -38,6 +41,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, i
|
||||
STARTUPINFOW si = { sizeof si };
|
||||
PROCESS_INFORMATION pi;
|
||||
(void)instance; (void)previous; (void)command_line; (void)show;
|
||||
SetProcessDPIAware(); /* before the message box exists; the call fails harmlessly when a manifest set it */
|
||||
if (n == 0 || n >= PATH_CHARS) { fail(); return 1; }
|
||||
while (n > 0 && folder[n - 1] != L'\\') n--;
|
||||
folder[n > 0 ? n - 1 : 0] = 0;
|
||||
|
||||
+15
-3
@@ -12,6 +12,8 @@ Release (docs/installer.md "Release layout"), root = the unpacked folder:
|
||||
include/ + lib/ of Vulkan, SDL3, Opus, libvpx and VMA (build_game.py --deps)
|
||||
bin/ totk_xlate.exe (+ totk_xlate.id), hactool.exe, ninja.exe, SDL3.dll, libopus-0.dll
|
||||
data/shader_list.tsv the MacNativeGames shader list
|
||||
data/pgo-<v>.profdata the execution profile the update's code is compiled with (tools/pgo.py; Windows only);
|
||||
without one the game is built at -O1
|
||||
totk.ini.example
|
||||
VERSION the release version, one line
|
||||
temp/ the default temp folder (created)
|
||||
@@ -24,7 +26,7 @@ Dev mode, root = a repository checkout (no python/python.exe): build/py314-embed
|
||||
Python the release ships, with the same packages), else this Python; toolchain/llvm-mingw-*/ and toolchain/deps-mingw;
|
||||
runtime/ as the sources with build/install-rtlib/libtotkrt.a and libtotkrt-v2.a (tools/build_game.py --make-runtime-lib
|
||||
--out build/install-rtlib, once per --cpu); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/totk_xlate.exe; the shader list from
|
||||
refs/MacNativeGames; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
|
||||
refs/MacNativeGames; build/pgo/pgo-<v>.profdata when there is one; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
|
||||
dev mode takes clang, lld and ninja from PATH and the system's libraries (no deps folder, no SDL3 copy).
|
||||
"""
|
||||
import os
|
||||
@@ -66,6 +68,7 @@ class Layout:
|
||||
temp: Path | None = None
|
||||
sysroot: Path | None = None # Linux release: what the game links against, instead of the host's
|
||||
runtime_lib_v2: Path | None = None # libtotkrt-v2.a: the runtime for CPUs without AVX2 (build_game.py --cpu v2)
|
||||
pgo_dir: Path | None = None # pgo-<v>.profdata (tools/pgo.py); None: no profiles (Linux)
|
||||
|
||||
@property
|
||||
def clang(self):
|
||||
@@ -79,6 +82,12 @@ class Layout:
|
||||
return lib
|
||||
return None if self.dev else lib
|
||||
|
||||
def pgo(self, version):
|
||||
"""The execution profile build_game.py --pgo compiles the update's code with, None without one (the game is
|
||||
then built at -O1)."""
|
||||
p = self.pgo_dir / f'pgo-{version_tag(version)}.profdata' if self.pgo_dir else None
|
||||
return p if p and p.is_file() else None
|
||||
|
||||
def patches(self, version):
|
||||
"""The code patch table for recomp/gen.py."""
|
||||
return self.recomp / f'patches-{version_tag(version)}.txt'
|
||||
@@ -90,7 +99,8 @@ class Layout:
|
||||
def missing(self, cpu='v3'):
|
||||
"""Paths the steps need that do not exist. cpu picks the runtime library the host will use."""
|
||||
lib = self.runtime_lib_for(cpu)
|
||||
need = [self.python, self.tools / 'extract.py', self.tools / 'build_game.py', self.tools / 'prepare_shaders.py',
|
||||
need = [self.python, self.tools / 'extract.py', self.tools / 'ncz.py', self.tools / 'build_game.py',
|
||||
self.tools / 'prepare_shaders.py',
|
||||
self.tools / 'pack_shader_cache.py', self.tools / 'make_romfs_patches.py', self.tools / 'ultrawide_layout.py',
|
||||
self.tools / 'button_icons.py',
|
||||
self.tools / 'vendor' / 'macnativegames' / 'extract_shared_fonts.py',
|
||||
@@ -128,6 +138,7 @@ def release_layout(root):
|
||||
xlate=root / 'bin' / ('totk_xlate' + EXE),
|
||||
dlls=(root / 'bin' / 'SDL3.dll', root / 'bin' / 'libopus-0.dll') if WIN else (root / 'bin' / 'libSDL3.so.0',),
|
||||
shader_list=root / 'data' / 'shader_list.tsv', ini_example=root / 'totk.ini.example',
|
||||
pgo_dir=root / 'data' if WIN else None,
|
||||
# Linux: None, the install folder's .setup-temp (installer/cli.py): the AppImage is mounted read-only, and on
|
||||
# a Steam Deck /home is often too full for the generated C while the install folder is on the SD card
|
||||
temp=root / 'temp' if WIN else None,
|
||||
@@ -153,7 +164,8 @@ def dev_layout(root):
|
||||
ninja=UCRT / ('ninja' + EXE), hactool=root / 'toolchain' / 'hactool' / ('hactool' + EXE),
|
||||
xlate=root / 'build' / 'xlate' / ('totk_xlate' + EXE),
|
||||
dlls=tuple(sorted((deps / 'bin').glob('*.dll'))),
|
||||
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp')
|
||||
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp',
|
||||
pgo_dir=root / 'build' / 'pgo')
|
||||
|
||||
|
||||
def dev_commit(root):
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""The last install folder and source paths, in a per-user file, so an update, a reinstall or a switch to another game
|
||||
version starts with the form filled in.
|
||||
|
||||
{"format": 1, "dest": ..., "base": ..., "update": ..., "keys": ..., "firmware": ...}
|
||||
|
||||
Absolute paths only. The keys entry is the path of the keys file, never its contents.
|
||||
|
||||
Windows: %LOCALAPPDATA%\TotK Recomp\setup.json; elsewhere $XDG_STATE_HOME/totk-recomp/setup.json (default
|
||||
~/.local/state/totk-recomp/setup.json). TOTK_SETUP_RECENT names another file (tests, --selftest). Nothing else is
|
||||
stored: the language comes from the install's own totk-install.json. A file of format 0 (only "dest") still loads.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from . import state
|
||||
|
||||
FORMAT = 1
|
||||
ENV = 'TOTK_SETUP_RECENT'
|
||||
FILES = ('base', 'update', 'keys') # recorded when they are files
|
||||
SOURCES = FILES + ('firmware',)
|
||||
FIELDS = ('dest',) + SOURCES
|
||||
|
||||
|
||||
def path():
|
||||
override = os.environ.get(ENV)
|
||||
if override:
|
||||
return Path(override)
|
||||
if sys.platform == 'win32':
|
||||
base = os.environ.get('LOCALAPPDATA') or str(Path.home() / 'AppData' / 'Local')
|
||||
return Path(base) / 'TotK Recomp' / 'setup.json'
|
||||
base = os.environ.get('XDG_STATE_HOME') or str(Path.home() / '.local' / 'state')
|
||||
return Path(base) / 'totk-recomp' / 'setup.json'
|
||||
|
||||
|
||||
def usable(name, value):
|
||||
"""value when it is a non-empty absolute path of the right kind for the field, else None. One stat call per
|
||||
path, no file is opened or hashed; a path the OS rejects (embedded NUL, unreachable share) counts as missing."""
|
||||
if not isinstance(value, str) or not value or not os.path.isabs(value):
|
||||
return None
|
||||
try:
|
||||
p = Path(value)
|
||||
if name == 'dest':
|
||||
ok = (p / state.NAME).is_file()
|
||||
elif name == 'firmware':
|
||||
ok = p.is_dir()
|
||||
else:
|
||||
ok = p.is_file()
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return value if ok else None
|
||||
|
||||
|
||||
def read():
|
||||
try:
|
||||
data = json.loads(path().read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def load():
|
||||
"""The recorded entries that still exist, by field name. Never raises."""
|
||||
data = read()
|
||||
return {name: data[name] for name in FIELDS if usable(name, data.get(name))}
|
||||
|
||||
|
||||
def save(dest, sources=None):
|
||||
"""Records dest and the non-empty, existing entries of sources (base, update, keys, firmware); an entry left out
|
||||
or empty keeps its saved value. Atomic; a failure to write is ignored, the window works without it."""
|
||||
target = path()
|
||||
tmp = target.with_name(target.name + '.tmp')
|
||||
data = {name: v for name, v in read().items() if name in FIELDS and isinstance(v, str)}
|
||||
data['dest'] = os.path.abspath(dest)
|
||||
for name, value in (sources or {}).items():
|
||||
if name in SOURCES and isinstance(value, str) and value.strip():
|
||||
value = os.path.abspath(value.strip())
|
||||
if usable(name, value):
|
||||
data[name] = value
|
||||
try:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp.write_text(json.dumps({'format': FORMAT, **data}), encoding='utf-8')
|
||||
os.replace(tmp, target)
|
||||
except OSError:
|
||||
pass
|
||||
+196
-22
@@ -1,4 +1,4 @@
|
||||
"""The seven setup steps of docs/installer.md: check, fonts, extract, lift, compile, prepare, cleanup.
|
||||
"""The eight setup steps of docs/installer.md: check, fonts, extract, lift, compile, prepare, dlss, cleanup.
|
||||
|
||||
Extraction runs in this process through tools/extract.py (the package's sibling tools/, which ships with it); fonts
|
||||
runs the vendored extract_shared_fonts.py on the firmware folder; lift, compile and prepare run the bundled Python on
|
||||
@@ -7,18 +7,21 @@ recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and tools/prepar
|
||||
with the bundled clang (hostcheck.c).
|
||||
|
||||
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, shared-fonts/ (Chinese and Korean text), totk.exe
|
||||
and its DLLs, romfs-index.bin, romfs-patches/, romfs-patches-xbox/, romfs-patches-xbox-label/, totk_xlate.exe, the
|
||||
and its DLLs, nvngx_dlss.dll (with --dlss), romfs-index.bin, romfs-patches/, romfs-patches-xbox/,
|
||||
romfs-patches-xbox-label/, romfs-patches-pc/, totk_xlate.exe, the
|
||||
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/. The cleanup keeps gen/, build/ and logs/: the next run lifts into the same gen/, where recomp/gen.py rewrites
|
||||
only the chunks whose C changed, and Ninja in build/ recompiles only those. A change of the mods' code cheats then
|
||||
costs the lift (about a minute) and the chunks it touches, not a full compile.
|
||||
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), unpacked/ (the NSP of an NSZ
|
||||
or XCZ input until the extraction is done), gen/ (C), build/ (objects), logs/. The cleanup keeps gen/, build/ and
|
||||
logs/: the next run lifts into the same gen/, where recomp/gen.py rewrites only the chunks whose C changed, and Ninja
|
||||
in build/ recompiles only those. A change of the mods' code cheats then costs the lift (about a minute) and the chunks
|
||||
it touches, not a full compile.
|
||||
|
||||
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status (tools/exefs_patches.py) and rebuild.txt, the command that starts this setup in
|
||||
its rebuild mode on the install folder (installer/gui.py --rebuild), one argument per line; totk.exe runs it at start
|
||||
when an enabled mod has code cheats the build lacks (runtime/cheats.c).
|
||||
"""
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -28,6 +31,7 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from . import ini
|
||||
@@ -36,6 +40,7 @@ from .runner import InstallError, Step
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'tools'))
|
||||
import extract # noqa: E402
|
||||
import ncz # noqa: E402
|
||||
import shader_list # noqa: E402
|
||||
from extract_exefs import (common_tickets, container_entries, load_keys, nca_ctr_key, nca_inventory, # noqa: E402
|
||||
program_nca)
|
||||
@@ -174,6 +179,12 @@ def raise_if_damaged(error, name, path):
|
||||
raise InstallError('check.input_damaged', input=name, path=str(path), detail=str(error)) from None
|
||||
|
||||
|
||||
def game_file(ctx, name):
|
||||
"""The file the extraction reads for the base or the update: the NSP that Check.unpack made from an NSZ or XCZ,
|
||||
else the chosen file itself."""
|
||||
return getattr(ctx, 'unpacked', {}).get(name) or Path(getattr(ctx.options, name))
|
||||
|
||||
|
||||
def game_version(ctx):
|
||||
"""The update this install is built from: what the check identified, else the recorded one. An install made before
|
||||
1.4.3 support records no version, and that install is 1.2.1: name it here rather than leaning on SUPPORTED's order."""
|
||||
@@ -184,6 +195,25 @@ def expected_update(ctx):
|
||||
return extract.load_expected()[game_version(ctx)]
|
||||
|
||||
|
||||
# Releases before 0.3.1 recorded the 0x20000 update as 1.1.1 (pull request #220); it is 1.1.0 (#371), with the same
|
||||
# files, hashes and patch sites under new names.
|
||||
RENAMED_VERSIONS = {'1.1.1': '1.1.0'}
|
||||
|
||||
|
||||
def migrate_state(ctx):
|
||||
"""Read a recorded update by its current name. The extraction it recorded stays: its record gets the key of the
|
||||
renamed expected file, and Extract.current and the full check of output another release kept
|
||||
(runner.Context.verify_kept) still test the files against the expected hashes."""
|
||||
old = ctx.state.get('version')
|
||||
if old not in RENAMED_VERSIONS:
|
||||
return
|
||||
ctx.state['version'] = RENAMED_VERSIONS[old]
|
||||
record = ctx.state['steps'].get('extract')
|
||||
if record:
|
||||
record['key'] = Extract().key(ctx)
|
||||
ctx.reporter.log(f'state: the update recorded as {old} is {ctx.state["version"]}')
|
||||
|
||||
|
||||
def shader_list_text(layout):
|
||||
if layout.shader_list:
|
||||
return layout.shader_list.read_text(encoding='utf-8')
|
||||
@@ -432,14 +462,73 @@ class Check(Step):
|
||||
raise InstallError('check.keys_unreadable') from None
|
||||
if 'header_key' not in keys:
|
||||
raise InstallError('check.keys_no_header_key')
|
||||
self.unpack(ctx)
|
||||
total = 0
|
||||
for name, is_update in (('base', False), ('update', True)):
|
||||
path = Path(getattr(o, name))
|
||||
path = game_file(ctx, name)
|
||||
with path.open('rb') as f:
|
||||
total += self.open_input(f, path, keys, name, is_update)
|
||||
del keys
|
||||
return total
|
||||
|
||||
def unpack(self, ctx):
|
||||
"""Unpack an NSZ or XCZ (a game file compressed by the nsz tool) into <temp>/unpacked/<base|update>.nsp with
|
||||
tools/ncz.py, which checks every NCA against its hash; the extraction then reads that NSP. An earlier run's NSP
|
||||
of the same file is reused. The extract step removes the folder once it is done."""
|
||||
o, rep, folder = ctx.options, ctx.reporter, ctx.temp / 'unpacked'
|
||||
ctx.unpacked = {}
|
||||
jobs = []
|
||||
for name in ('base', 'update'):
|
||||
path = Path(getattr(o, name))
|
||||
try:
|
||||
found = ncz.compressed(path)
|
||||
if found is None:
|
||||
continue
|
||||
if name == 'update' and found[0] != 'nsz':
|
||||
raise InstallError('check.update_not_nsp')
|
||||
size = ncz.unpacked_size(path)
|
||||
except ValueError as e:
|
||||
raise_if_damaged(e, name, path)
|
||||
raise InstallError('check.not_game_file', input=name) from None
|
||||
st = path.stat()
|
||||
jobs.append((name, path, size, f'{path.resolve()}\n{st.st_size}\n{st.st_mtime_ns}\n{size}\n'))
|
||||
if not jobs:
|
||||
remove(folder)
|
||||
return
|
||||
done = []
|
||||
for name, path, size, tag in jobs:
|
||||
target, stamp = folder / f'{name}.nsp', folder / f'{name}.source'
|
||||
try:
|
||||
if target.stat().st_size == size and stamp.read_text(encoding='utf-8') == tag:
|
||||
done.append(name)
|
||||
ctx.unpacked[name] = target
|
||||
except OSError:
|
||||
pass
|
||||
need = sum(size for name, _, size, _ in jobs if name not in done)
|
||||
free, _ = free_bytes(folder)
|
||||
rep.log(f'unpack: {", ".join(name for name, *_ in jobs)}: {need / GiB:.1f} GiB to write, '
|
||||
f'{free / GiB:.1f} free')
|
||||
if need > free:
|
||||
raise InstallError('check.disk_space', path=str(folder), need_gb=round(need / 1e9, 1),
|
||||
free_gb=round(free / 1e9, 1))
|
||||
for name, path, size, tag in jobs:
|
||||
if name in done:
|
||||
continue
|
||||
target, stamp = folder / f'{name}.nsp', folder / f'{name}.source'
|
||||
remove(stamp)
|
||||
try:
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
ncz.unpack(path, target, lambda d, t, n=name: rep.progress('check', 'unpack_' + n, d, t, 'bytes'))
|
||||
stamp.write_text(tag, encoding='utf-8')
|
||||
except ncz.DamagedFile as e:
|
||||
raise InstallError('check.input_damaged', input=name, path=str(e.path), detail=e.detail) from None
|
||||
except ValueError as e:
|
||||
raise_if_damaged(e, name, path)
|
||||
raise InstallError('check.unpack_failed', input=name, detail=str(e)) from None
|
||||
except OSError as e:
|
||||
raise InstallError('check.unpack_failed', input=name, detail=str(e)) from None
|
||||
ctx.unpacked[name] = target
|
||||
|
||||
@staticmethod
|
||||
def open_input(f, path, keys, name, is_update):
|
||||
try:
|
||||
@@ -520,9 +609,10 @@ class Check(Step):
|
||||
out = work / name
|
||||
remove(out)
|
||||
try:
|
||||
extract.extract_exefs(Path(getattr(o, name)), Path(o.keys), out, logs, update=is_update)
|
||||
extract.extract_exefs(game_file(ctx, name), Path(o.keys), out, logs, update=is_update)
|
||||
except extract.DamagedFile as e:
|
||||
raise InstallError('check.input_damaged', input=name, path=str(e.path), detail=e.detail) from None
|
||||
raise InstallError('check.input_damaged', input=name, path=str(getattr(o, name)),
|
||||
detail=e.detail) from None
|
||||
except ValueError:
|
||||
raise InstallError('check.exefs_failed', input=name, log=str(logs / f'extract_exefs-{name}.log')) \
|
||||
from None
|
||||
@@ -641,9 +731,9 @@ class Extract(Step):
|
||||
def run(self, ctx):
|
||||
o, dest, rep = ctx.options, ctx.dest, ctx.reporter
|
||||
expected = expected_update(ctx)
|
||||
base, update, keys = Path(o.base), Path(o.update), Path(o.keys)
|
||||
keys = Path(o.keys)
|
||||
inputs = {}
|
||||
for name, path in (('base', base), ('update', update)):
|
||||
for name, path in (('base', Path(o.base)), ('update', Path(o.update))):
|
||||
known = ctx.state['inputs'].get(name, {})
|
||||
st = path.stat()
|
||||
if known.get('size') == st.st_size and known.get('mtime_ns') == st.st_mtime_ns and known.get('sha256'):
|
||||
@@ -653,6 +743,7 @@ class Extract(Step):
|
||||
inputs[name] = {'name': path.name, 'size': st.st_size, 'mtime_ns': st.st_mtime_ns, 'sha256': digest}
|
||||
ctx.state['inputs'] = inputs
|
||||
ctx.save()
|
||||
base, update = game_file(ctx, 'base'), game_file(ctx, 'update')
|
||||
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
for stale in dest.glob('update-merge-*'): # a killed merge leaves its NCA copies (and key copies) behind
|
||||
@@ -664,7 +755,7 @@ class Extract(Step):
|
||||
try:
|
||||
extract.extract_exefs(update, keys, ctx.temp / 'check' / 'update', ctx.temp / 'logs', update=True)
|
||||
except extract.DamagedFile as e:
|
||||
raise InstallError('check.input_damaged', input='update', path=str(e.path), detail=e.detail) from None
|
||||
raise InstallError('check.input_damaged', input='update', path=str(o.update), detail=e.detail) from None
|
||||
except ValueError:
|
||||
raise InstallError('check.exefs_failed', input='update',
|
||||
log=str(ctx.temp / 'logs' / 'extract_exefs-update.log')) from None
|
||||
@@ -686,6 +777,7 @@ class Extract(Step):
|
||||
raise InstallError('extract.romfs_mismatch', version=game_version(ctx), size=report['romfs_size'],
|
||||
sha256=report['romfs_sha256'][:16])
|
||||
(dest / 'profile').mkdir(exist_ok=True)
|
||||
remove(ctx.temp / 'unpacked') # the NSPs unpacked from an NSZ or XCZ (Check.unpack)
|
||||
st = (romfs / 'romfs.bin').stat()
|
||||
return {'romfs_size': st.st_size, 'romfs_mtime_ns': st.st_mtime_ns}
|
||||
|
||||
@@ -707,7 +799,8 @@ class Extract(Step):
|
||||
try:
|
||||
extract.merge_update_romfs(base, update, keys, ctx.layout.hactool, romfs, logs, phase=phase)
|
||||
except extract.DamagedFile as e:
|
||||
raise InstallError('check.input_damaged', input='base' if e.path == base else 'update', path=str(e.path),
|
||||
name = 'base' if e.path == base else 'update'
|
||||
raise InstallError('check.input_damaged', input=name, path=str(getattr(ctx.options, name)),
|
||||
detail=e.detail) from None
|
||||
except ValueError as e:
|
||||
raise InstallError('extract.romfs_merge_failed', detail=str(e), log=str(logs / 'merge_romfs-'
|
||||
@@ -852,7 +945,8 @@ class Compile(Step):
|
||||
id = 'compile'
|
||||
|
||||
def key(self, ctx):
|
||||
return fingerprint('compile-3', ctx.keys['lift'], runtime_key(ctx))
|
||||
return fingerprint('compile-3', ctx.keys['lift'], runtime_key(ctx),
|
||||
ctx.layout.pgo(game_version(ctx)) or 'no profile')
|
||||
|
||||
def current(self, ctx, record):
|
||||
exe = ctx.dest / ('totk' + EXE)
|
||||
@@ -878,10 +972,12 @@ class Compile(Step):
|
||||
rep.progress('compile', 'compile', force=True)
|
||||
cpu = getattr(ctx, 'cpu', 'v3')
|
||||
lib = layout.runtime_lib_for(cpu)
|
||||
pgo = layout.pgo(game_version(ctx))
|
||||
code, log = run_tool(ctx, 'compile', layout.tools / 'build_game.py', [
|
||||
'--gen', ctx.temp / 'gen', '--out', build, '--toolchain', layout.llvm, '--cpu', cpu,
|
||||
*(['--deps', layout.deps] if layout.deps else []), *(['--sysroot', layout.sysroot] if layout.sysroot else []),
|
||||
'--runtime', layout.runtime, *(['--runtime-lib', lib] if lib else ()), '--ninja', layout.ninja,
|
||||
'--runtime', layout.runtime, *(['--runtime-lib', lib] if lib else ()), *(['--pgo', pgo] if pgo else ()),
|
||||
'--ninja', layout.ninja,
|
||||
'--version', version_tag(game_version(ctx)), '--expected', layout.expected(game_version(ctx)),
|
||||
'--exefs', dest / 'exefs', '--romfs', dest / 'romfs',
|
||||
'--profile', dest / 'profile'], parse)
|
||||
@@ -907,9 +1003,11 @@ SKIPPED = re.compile(r'prepare-shaders: (\d+) variants skipped: the graphics dri
|
||||
|
||||
|
||||
# The game resolutions of another shape than 16:9 that runtime/ui/menu.cpp row_output_res lists: 21:9 and 32:9 monitors,
|
||||
# and 16:10 screens (the Steam Deck's 1280x800, handhelds and laptops)
|
||||
# 16:10 screens (the Steam Deck's 1280x800, handhelds and laptops) and 3:2 screens (Surface and Framework laptops and
|
||||
# tablets)
|
||||
ASPECT_SIZES = ('2560x1080', '3440x1440', '3840x1600', '5120x2160', '3840x1080', '5120x1440',
|
||||
'1280x800', '1680x1050', '1920x1200', '2560x1600')
|
||||
'1280x800', '1680x1050', '1920x1200', '2560x1600',
|
||||
'1920x1280', '2160x1440', '2256x1504', '2880x1920', '3000x2000')
|
||||
|
||||
|
||||
class Prepare(Step):
|
||||
@@ -973,9 +1071,10 @@ class Prepare(Step):
|
||||
# romfs-patches-xbox for the Nintendo pad layout, romfs-patches-xbox-label for xbox and auto (runtime/input_map.c
|
||||
# input_prompts_folder). Both are written on every install, so the menu choices work without another run of
|
||||
# the setup. Optional: when one fails, the game keeps the Switch prompts with that layout and the menu says the
|
||||
# files are missing, so the setup warns once and goes on.
|
||||
# files are missing, so the setup warns once and goes on. romfs-patches-pc likewise, with keyboard and mouse
|
||||
# prompts for prompts = pc (16:9 only: another shape takes make_romfs_patches.py --prompts pc --aspect).
|
||||
failed = None
|
||||
for style in ('xbox', 'xbox-label'):
|
||||
for style in ('xbox', 'xbox-label', 'pc'):
|
||||
out = dest / f'romfs-patches-{style}'
|
||||
remove(out)
|
||||
code, log = run_tool(ctx, f'prepare-{style}', layout.tools / 'make_romfs_patches.py',
|
||||
@@ -1022,7 +1121,82 @@ class Prepare(Step):
|
||||
return {'xlate_sha256': file_sha256(xlate)}
|
||||
|
||||
|
||||
# ---- 7. clean up
|
||||
# ---- 7. NVIDIA DLSS
|
||||
|
||||
# NVIDIA's DLSS library, which NGX loads from the folder of totk.exe (runtime/vk_dlaa.c). The setup does not ship it:
|
||||
# with --dlss it downloads NVIDIA's own copy from the DLSS SDK repository (NVIDIA RTX SDK license) at a pinned tag and
|
||||
# checks its hash. The game runs without it; dlaa and the DLSS modes then do nothing.
|
||||
DLSS_DLL = 'nvngx_dlss.dll'
|
||||
DLSS_VERSION = '310.9.1'
|
||||
DLSS_URL = f'https://raw.githubusercontent.com/NVIDIA/DLSS/v{DLSS_VERSION}/lib/Windows_x86_64/rel/{DLSS_DLL}'
|
||||
DLSS_SIZE = 58956912
|
||||
DLSS_SHA256 = '3975567b8943c53acce397f2b72380092f84f162d00b0d2c7d08a1025c563983'
|
||||
DLSS_LICENSE = 'https://github.com/NVIDIA/DLSS/blob/main/LICENSE.txt'
|
||||
|
||||
|
||||
def nvidia_rtx(host):
|
||||
"""An NVIDIA RTX GPU among the host check's Vulkan devices: the GPUs DLSS runs on."""
|
||||
return any('NVIDIA' in d['name'].upper() and 'RTX' in d['name'].upper() for d in (host or {}).get('devices', []))
|
||||
|
||||
|
||||
class Dlss(Step):
|
||||
"""With --dlss on Windows and an NVIDIA RTX GPU: nvngx_dlss.dll next to totk.exe. A DLSS library the player put
|
||||
there (another version) is kept. A failed download warns and is tried again on the next run; the game runs
|
||||
without it."""
|
||||
|
||||
id = 'dlss'
|
||||
|
||||
def key(self, ctx):
|
||||
if not getattr(ctx.options, 'dlss', False):
|
||||
return fingerprint('dlss-1', 'off')
|
||||
return fingerprint('dlss-1', DLSS_VERSION, DLSS_SHA256)
|
||||
|
||||
def current(self, ctx, record):
|
||||
if record.get('failed'):
|
||||
return False
|
||||
return not record.get('installed') or (ctx.dest / DLSS_DLL).is_file()
|
||||
|
||||
def run(self, ctx):
|
||||
if not getattr(ctx.options, 'dlss', False):
|
||||
return {}
|
||||
if os.name != 'nt':
|
||||
return {'skipped': 'not_windows'}
|
||||
if not nvidia_rtx(getattr(ctx, 'host', None)):
|
||||
# The box is ticked by default, so most players without an RTX GPU never chose it: a log line, no warning
|
||||
ctx.reporter.log('dlss: no NVIDIA RTX GPU among the Vulkan devices, nothing downloaded')
|
||||
return {'skipped': 'no_rtx'}
|
||||
rep, target = ctx.reporter, ctx.dest / DLSS_DLL
|
||||
if target.is_file():
|
||||
if file_sha256(target) == DLSS_SHA256:
|
||||
return {'installed': DLSS_VERSION}
|
||||
rep.warning('warn.dlss_kept', path=str(target))
|
||||
return {'kept': True}
|
||||
part = ctx.temp / (DLSS_DLL + '.part')
|
||||
rep.progress('dlss', 'download', 0, DLSS_SIZE, 'bytes', force=True)
|
||||
rep.log(f'dlss: {DLSS_URL} (license {DLSS_LICENSE})')
|
||||
try:
|
||||
part.parent.mkdir(parents=True, exist_ok=True)
|
||||
h, done = hashlib.sha256(), 0
|
||||
with urllib.request.urlopen(DLSS_URL, timeout=60) as r, open(part, 'wb') as f:
|
||||
while block := r.read(1 << 20):
|
||||
f.write(block)
|
||||
h.update(block)
|
||||
done += len(block)
|
||||
rep.progress('dlss', 'download', min(done, DLSS_SIZE), DLSS_SIZE, 'bytes')
|
||||
if done != DLSS_SIZE or h.hexdigest() != DLSS_SHA256:
|
||||
raise OSError(f'{done} bytes, SHA-256 {h.hexdigest()[:16]}, not the expected file')
|
||||
replace_file(part, target)
|
||||
remove(part)
|
||||
# urllib's errors, timeouts and a bad file; a connection cut mid-file raises http.client.IncompleteRead,
|
||||
# which is not an OSError
|
||||
except (OSError, http.client.HTTPException) as e:
|
||||
remove(part)
|
||||
rep.warning('warn.dlss_download_failed', url=DLSS_URL, detail=str(e)[:300])
|
||||
return {'failed': True}
|
||||
return {'installed': DLSS_VERSION}
|
||||
|
||||
|
||||
# ---- 8. clean up
|
||||
|
||||
class Cleanup(Step):
|
||||
id = 'cleanup'
|
||||
@@ -1040,7 +1214,7 @@ class Cleanup(Step):
|
||||
rep.progress('cleanup', 'temp', force=True)
|
||||
for stale in dest.glob('update-merge-*'):
|
||||
remove(stale)
|
||||
for name in ('check', 'shader_list.tsv', 'shared-fonts'): # gen/ and build/ stay for the next rebuild
|
||||
for name in ('check', 'unpacked', 'shader_list.tsv', 'shared-fonts'): # gen/ and build/ stay for the next rebuild
|
||||
remove(ctx.temp / name)
|
||||
romfs_problem(dest / 'romfs', expected_update(ctx))
|
||||
write_rebuild_command(ctx)
|
||||
@@ -1090,4 +1264,4 @@ def romfs_problem(romfs, expected):
|
||||
|
||||
|
||||
def all_steps():
|
||||
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Dlss(), Cleanup()]
|
||||
+16
-7
@@ -5,12 +5,15 @@
|
||||
"step.lift": "Translating the game code to C",
|
||||
"step.compile": "Compiling {game_exe}",
|
||||
"step.prepare": "Preparing the shaders",
|
||||
"step.dlss": "Downloading NVIDIA DLSS",
|
||||
"step.cleanup": "Cleaning up",
|
||||
|
||||
"phase.check.host": "Checking the processor and the graphics card",
|
||||
"phase.check.verify": "Checking the files already installed",
|
||||
"phase.check.verify_romfs": "Checking the installed game data",
|
||||
"phase.check.inputs": "Opening the game files with the keys",
|
||||
"phase.check.unpack_base": "Unpacking the compressed base game",
|
||||
"phase.check.unpack_update": "Unpacking the compressed update",
|
||||
"phase.check.versions": "Checking the game versions",
|
||||
"phase.fonts.extract": "Extracting the shared fonts from the firmware",
|
||||
"phase.fonts.copy": "Copying the shared fonts into the install folder",
|
||||
@@ -30,6 +33,7 @@
|
||||
"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.dlss.download": "Downloading nvngx_dlss.dll from NVIDIA's DLSS SDK",
|
||||
"phase.cleanup.ini": "Writing totk.ini",
|
||||
"phase.cleanup.temp": "Removing temporary files",
|
||||
|
||||
@@ -58,14 +62,15 @@
|
||||
"check.keys_no_header_key": "The keys file has no header_key. Dump prod.keys from your console again.",
|
||||
"check.keys_wrong": "The keys cannot open the {input} file. Use the prod.keys of your console.",
|
||||
"check.keys_too_old": "The keys file is too old for the {input} file (needs key generation {generation}). Dump prod.keys from a console with newer firmware.",
|
||||
"check.not_game_file": "The {input} file is not an XCI or NSP game file.",
|
||||
"check.not_game_file": "The {input} file is not an XCI, NSP, XCZ or NSZ game file.",
|
||||
"check.file_unreadable": "The {input} file cannot be read: {detail}",
|
||||
"check.unpack_failed": "The compressed {input} file could not be unpacked: {detail}",
|
||||
"check.ticket_personalized": "The {input} file carries a personalized ticket; a common ticket is needed.",
|
||||
"check.ticket_missing": "The {input} file lacks the ticket its content needs.",
|
||||
"check.base_not_totk": "The base game file is not The Legend of Zelda: Tears of the Kingdom ({detail}).",
|
||||
"check.base_is_update": "The base game file is an update. Choose the base game NSP or XCI (version 1.0.0) here and this file as the update.",
|
||||
"check.base_combined": "The base game file holds several packages (base game, update or DLC in one file). Choose a base game dump that contains only the game.",
|
||||
"check.update_not_nsp": "The update must be an NSP file.",
|
||||
"check.update_not_nsp": "The update must be an NSP or NSZ file.",
|
||||
"check.update_is_base": "The update file holds a base game. Choose the 1.1.0 or 1.2.1 or 1.4.3 update NSP.",
|
||||
"check.update_not_totk": "The update file is not a Tears of the Kingdom update ({detail}).",
|
||||
"check.exefs_failed": "The game code of the {input} file failed its checks; see {log}.",
|
||||
@@ -98,9 +103,11 @@
|
||||
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
|
||||
"warn.cheats_skipped": "The cheats of the mods folder that change the game's code could not be built; the game installs without them. Details: {log}",
|
||||
"warn.exefs_skipped": "The mods' ExeFS patches (.ips, .pchtxt) could not be built; the game installs without them. Details: {log}",
|
||||
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
|
||||
"warn.xbox_prompts_failed": "The Xbox or PC button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
|
||||
"warn.shaders_skipped": "The graphics driver crashed while building some shaders, so {count} were left out of the preparation. The game builds them when it first needs them, and the driver may crash there too; a driver update may fix it. Details: {log}",
|
||||
"warn.kernel_old": "Linux {version} lacks the write tracking the game relies on (6.7 or later); the game may run very slowly. Updating the system fixes this.",
|
||||
"warn.dlss_kept": "A DLSS library is already in the install folder ({path}); the setup keeps it.",
|
||||
"warn.dlss_download_failed": "NVIDIA DLSS could not be downloaded from {url}: {detail}. The game runs without it; the next setup run tries again.",
|
||||
|
||||
"ui.title": "TotK Setup",
|
||||
"ui.rebuild_title": "Rebuilding the game for your mods",
|
||||
@@ -109,8 +116,8 @@
|
||||
"ui.intro": "Builds a playable Tears of the Kingdom folder from your own game files. This takes a while and runs once.",
|
||||
"ui.notice": "The folder this setup builds contains the game. It is for your own use only; do not share or upload it.",
|
||||
"ui.keys_private": "The keys file is read in place. Its contents are never shown, copied or stored.",
|
||||
"ui.base": "Base game (NSP or XCI)",
|
||||
"ui.update": "Update 1.1.0 or 1.2.1 or 1.4.3 (NSP)",
|
||||
"ui.base": "Base game (NSP, XCI, NSZ or XCZ)",
|
||||
"ui.update": "Update 1.1.0 or 1.2.1 or 1.4.3 (NSP or NSZ)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "Firmware folder (optional)",
|
||||
"ui.game_language": "Game language",
|
||||
@@ -122,8 +129,10 @@
|
||||
"ui.pick_firmware": "Choose the Switch firmware folder (Contents/registered)",
|
||||
"ui.pick_dest": "Choose the install folder",
|
||||
"ui.firmware_help": "For Chinese, Korean and Traditional Chinese text: the firmware's Contents/registered folder, for example from Ryujinx or Yuzu. The setup copies the Switch system fonts from it into the install folder.",
|
||||
"ui.type_game": "Game files (NSP, XCI)",
|
||||
"ui.type_nsp": "NSP files",
|
||||
"ui.dlss": "Download NVIDIA DLSS (nvngx_dlss.dll) for NVIDIA RTX graphics cards",
|
||||
"ui.dlss_help": "From NVIDIA's DLSS SDK on GitHub, under NVIDIA's RTX SDK license (github.com/NVIDIA/DLSS). It makes the dlaa and DLSS settings work; without it they do nothing.",
|
||||
"ui.compressed_help": "Compressed NSZ and XCZ files work as they are: the setup unpacks them by itself (until the game data is extracted, that needs as much free space as the unpacked files take).",
|
||||
"ui.type_game": "Game files (NSP, XCI, NSZ, XCZ)",
|
||||
"ui.type_keys": "Key files (*.keys)",
|
||||
"ui.type_all": "All files",
|
||||
"ui.start": "Start",
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
"step.lift": "Traduzindo o código do jogo para C",
|
||||
"step.compile": "Compilando o {game_exe}",
|
||||
"step.prepare": "Preparando os shaders",
|
||||
"step.dlss": "Baixando o NVIDIA DLSS",
|
||||
"step.cleanup": "Finalizando",
|
||||
|
||||
"phase.check.host": "Verificando o processador e a placa de vídeo",
|
||||
"phase.check.verify": "Verificando os arquivos já instalados",
|
||||
"phase.check.verify_romfs": "Verificando os dados do jogo instalados",
|
||||
"phase.check.inputs": "Abrindo os arquivos do jogo com as chaves",
|
||||
"phase.check.unpack_base": "Descompactando o jogo base compactado",
|
||||
"phase.check.unpack_update": "Descompactando a atualização compactada",
|
||||
"phase.check.versions": "Verificando as versões do jogo",
|
||||
"phase.fonts.extract": "Extraindo as fontes compartilhadas do firmware",
|
||||
"phase.fonts.copy": "Copiando as fontes compartilhadas para a pasta de instalação",
|
||||
@@ -30,6 +33,7 @@
|
||||
"phase.prepare.translate": "Traduzindo os shaders",
|
||||
"phase.prepare.libraries": "Montando os shaders para o driver de vídeo",
|
||||
"phase.prepare.pack": "Juntando o cache de shaders num único arquivo",
|
||||
"phase.dlss.download": "Baixando o nvngx_dlss.dll do DLSS SDK da NVIDIA",
|
||||
"phase.cleanup.ini": "Gravando o totk.ini",
|
||||
"phase.cleanup.temp": "Removendo os arquivos temporários",
|
||||
|
||||
@@ -58,14 +62,15 @@
|
||||
"check.keys_no_header_key": "O arquivo de chaves não tem header_key. Extraia o prod.keys do seu console de novo.",
|
||||
"check.keys_wrong": "As chaves não abrem o arquivo de {input}. Use o prod.keys do seu console.",
|
||||
"check.keys_too_old": "O arquivo de chaves é antigo demais para o arquivo de {input} (precisa da geração de chaves {generation}). Extraia o prod.keys de um console com firmware mais novo.",
|
||||
"check.not_game_file": "O arquivo de {input} não é um arquivo de jogo XCI ou NSP.",
|
||||
"check.not_game_file": "O arquivo de {input} não é um arquivo de jogo XCI, NSP, XCZ ou NSZ.",
|
||||
"check.file_unreadable": "Não foi possível ler o arquivo de {input}: {detail}",
|
||||
"check.unpack_failed": "Não foi possível descompactar o arquivo compactado de {input}: {detail}",
|
||||
"check.ticket_personalized": "O arquivo de {input} tem um ticket personalizado; é preciso um ticket comum.",
|
||||
"check.ticket_missing": "O arquivo de {input} não tem o ticket de que o conteúdo precisa.",
|
||||
"check.base_not_totk": "O arquivo do jogo base não é The Legend of Zelda: Tears of the Kingdom ({detail}).",
|
||||
"check.base_is_update": "O arquivo do jogo base é uma atualização. Escolha aqui o NSP ou XCI do jogo base (versão 1.0.0) e este arquivo como a atualização.",
|
||||
"check.base_combined": "O arquivo do jogo base contém vários pacotes (jogo base, atualização ou DLC em um só arquivo). Escolha um dump do jogo base que contenha só o jogo.",
|
||||
"check.update_not_nsp": "A atualização precisa ser um arquivo NSP.",
|
||||
"check.update_not_nsp": "A atualização precisa ser um arquivo NSP ou NSZ.",
|
||||
"check.update_is_base": "O arquivo de atualização contém um jogo base. Escolha o NSP da atualização 1.1.0, 1.2.1 ou 1.4.3.",
|
||||
"check.update_not_totk": "O arquivo de atualização não é uma atualização de Tears of the Kingdom ({detail}).",
|
||||
"check.exefs_failed": "O código do jogo no arquivo de {input} falhou nas verificações; veja {log}.",
|
||||
@@ -98,9 +103,11 @@
|
||||
"warn.cheats_skipped": "Não foi possível compilar os cheats da pasta de mods que alteram o código do jogo; o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.exefs_skipped": "Não foi possível compilar os patches ExeFS dos mods (.ips, .pchtxt); o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.npdm_differs": "O main.npdm difere do dump de referência da versão {version}; só os módulos de código são usados.",
|
||||
"warn.xbox_prompts_failed": "Não foi possível preparar os ícones de botão do Xbox (código de saída {code}); o jogo mantém os ícones do Switch. Detalhes: {log}",
|
||||
"warn.xbox_prompts_failed": "Não foi possível preparar os ícones de botão do Xbox ou do PC (código de saída {code}); o jogo mantém os ícones do Switch. Detalhes: {log}",
|
||||
"warn.shaders_skipped": "O driver gráfico travou ao compilar alguns shaders, então {count} ficaram fora da preparação. O jogo os compila quando precisar deles pela primeira vez, e o driver pode travar ali também; uma atualização do driver pode resolver. Detalhes: {log}",
|
||||
"warn.kernel_old": "O Linux {version} não tem o rastreamento de escrita de que o jogo depende (6.7 ou mais novo); o jogo pode rodar muito devagar. Atualizar o sistema resolve.",
|
||||
"warn.dlss_kept": "Já existe uma biblioteca DLSS na pasta de instalação ({path}); a instalação a mantém.",
|
||||
"warn.dlss_download_failed": "Não foi possível baixar o NVIDIA DLSS de {url}: {detail}. O jogo funciona sem ele; a próxima execução da instalação tenta de novo.",
|
||||
|
||||
"ui.title": "Instalação do TotK",
|
||||
"ui.rebuild_title": "Recompilando o jogo para os seus mods",
|
||||
@@ -109,8 +116,8 @@
|
||||
"ui.intro": "Monta uma pasta jogável de Tears of the Kingdom a partir dos seus próprios arquivos do jogo. Isso demora um pouco e só precisa ser feito uma vez.",
|
||||
"ui.notice": "A pasta que esta instalação monta contém o jogo. Ela é só para seu uso pessoal; não compartilhe nem envie para a internet.",
|
||||
"ui.keys_private": "O arquivo de chaves é lido no lugar. O conteúdo dele nunca é mostrado, copiado nem guardado.",
|
||||
"ui.base": "Jogo base (NSP ou XCI)",
|
||||
"ui.update": "Atualização 1.1.0, 1.2.1 ou 1.4.3 (NSP)",
|
||||
"ui.base": "Jogo base (NSP, XCI, NSZ ou XCZ)",
|
||||
"ui.update": "Atualização 1.1.0, 1.2.1 ou 1.4.3 (NSP ou NSZ)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "Pasta de firmware (opcional)",
|
||||
"ui.game_language": "Idioma do jogo",
|
||||
@@ -122,8 +129,10 @@
|
||||
"ui.pick_firmware": "Escolha a pasta de firmware do Switch (Contents/registered)",
|
||||
"ui.pick_dest": "Escolha a pasta de instalação",
|
||||
"ui.firmware_help": "Para textos em chinês, coreano e chinês tradicional: a pasta Contents/registered do firmware, por exemplo do Ryujinx ou do Yuzu. A instalação copia dela as fontes do sistema do Switch para a pasta de instalação.",
|
||||
"ui.type_game": "Arquivos de jogo (NSP, XCI)",
|
||||
"ui.type_nsp": "Arquivos NSP",
|
||||
"ui.dlss": "Baixar o NVIDIA DLSS (nvngx_dlss.dll) para placas de vídeo NVIDIA RTX",
|
||||
"ui.dlss_help": "Do DLSS SDK da NVIDIA no GitHub, sob a licença NVIDIA RTX SDK (github.com/NVIDIA/DLSS). Sem ele as opções dlaa e DLSS não fazem nada.",
|
||||
"ui.compressed_help": "Arquivos compactados NSZ e XCZ funcionam do jeito que estão: a instalação os descompacta sozinha (para isso, até os dados do jogo serem extraídos, ela precisa de tanto espaço livre quanto ocupam os arquivos descompactados).",
|
||||
"ui.type_game": "Arquivos de jogo (NSP, XCI, NSZ, XCZ)",
|
||||
"ui.type_keys": "Arquivos de chaves (*.keys)",
|
||||
"ui.type_all": "Todos os arquivos",
|
||||
"ui.start": "Iniciar",
|
||||
|
||||
+16
-7
@@ -5,12 +5,15 @@
|
||||
"step.lift": "Перевод кода игры в C",
|
||||
"step.compile": "Сборка {game_exe}",
|
||||
"step.prepare": "Подготовка шейдеров",
|
||||
"step.dlss": "Загрузка NVIDIA DLSS",
|
||||
"step.cleanup": "Уборка",
|
||||
|
||||
"phase.check.host": "Проверка процессора и видеокарты",
|
||||
"phase.check.verify": "Проверка уже установленных файлов",
|
||||
"phase.check.verify_romfs": "Проверка установленных данных игры",
|
||||
"phase.check.inputs": "Открытие игровых файлов с помощью ключей",
|
||||
"phase.check.unpack_base": "Распаковка сжатой базовой игры",
|
||||
"phase.check.unpack_update": "Распаковка сжатого обновления",
|
||||
"phase.check.versions": "Проверка версий игры",
|
||||
"phase.fonts.extract": "Извлечение общих шрифтов из прошивки",
|
||||
"phase.fonts.copy": "Копирование общих шрифтов в папку установки",
|
||||
@@ -30,6 +33,7 @@
|
||||
"phase.prepare.translate": "Трансляция шейдеров",
|
||||
"phase.prepare.libraries": "Сборка шейдеров для драйвера видеокарты",
|
||||
"phase.prepare.pack": "Упаковка кэша шейдеров в один файл",
|
||||
"phase.dlss.download": "Загрузка nvngx_dlss.dll из DLSS SDK от NVIDIA",
|
||||
"phase.cleanup.ini": "Запись totk.ini",
|
||||
"phase.cleanup.temp": "Удаление временных файлов",
|
||||
|
||||
@@ -58,14 +62,15 @@
|
||||
"check.keys_no_header_key": "В файле ключей нет header_key. Снова получите prod.keys со своей консоли.",
|
||||
"check.keys_wrong": "Эти ключи не открывают файл «{input}». Используйте prod.keys со своей консоли.",
|
||||
"check.keys_too_old": "Файл ключей слишком старый для файла «{input}» (нужно поколение ключей {generation}). Получите prod.keys с консоли с более новой прошивкой.",
|
||||
"check.not_game_file": "Файл «{input}» не является игровым файлом XCI или NSP.",
|
||||
"check.not_game_file": "Файл «{input}» не является игровым файлом XCI, NSP, XCZ или NSZ.",
|
||||
"check.file_unreadable": "Не удаётся прочитать файл «{input}»: {detail}",
|
||||
"check.unpack_failed": "Не удалось распаковать сжатый файл «{input}»: {detail}",
|
||||
"check.ticket_personalized": "В файле «{input}» личный тикет; нужен общий тикет.",
|
||||
"check.ticket_missing": "В файле «{input}» нет тикета, который нужен его содержимому.",
|
||||
"check.base_not_totk": "Файл базовой игры — не The Legend of Zelda: Tears of the Kingdom ({detail}).",
|
||||
"check.base_is_update": "Файл базовой игры — это обновление. Выберите здесь NSP или XCI базовой игры (версия 1.0.0), а этот файл укажите как обновление.",
|
||||
"check.base_combined": "Файл базовой игры содержит несколько пакетов (игра, обновление или DLC в одном файле). Выберите дамп, в котором только базовая игра.",
|
||||
"check.update_not_nsp": "Обновление должно быть файлом NSP.",
|
||||
"check.update_not_nsp": "Обновление должно быть файлом NSP или NSZ.",
|
||||
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.1.0 или 1.2.1 или 1.4.3.",
|
||||
"check.update_not_totk": "Файл обновления — не обновление Tears of the Kingdom ({detail}).",
|
||||
"check.exefs_failed": "Код игры из файла «{input}» не прошёл проверку; подробности в {log}.",
|
||||
@@ -98,9 +103,11 @@
|
||||
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
|
||||
"warn.cheats_skipped": "Не удалось собрать читы из папки модов, меняющие код игры; игра установится без них. Подробности: {log}",
|
||||
"warn.exefs_skipped": "Не удалось собрать ExeFS-патчи модов (.ips, .pchtxt); игра установится без них. Подробности: {log}",
|
||||
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
|
||||
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox или PC (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
|
||||
"warn.shaders_skipped": "Графический драйвер аварийно завершился при сборке части шейдеров, поэтому {count} из них пропущены при подготовке. Игра соберёт их, когда они впервые понадобятся, и драйвер может упасть и там; обновление драйвера может это исправить. Подробности: {log}",
|
||||
"warn.kernel_old": "В Linux {version} нет отслеживания записи, на которое опирается игра (нужна версия 6.7 или новее); игра может работать очень медленно. Обновление системы это исправит.",
|
||||
"warn.dlss_kept": "В папке установки уже есть библиотека DLSS ({path}); установка её оставляет.",
|
||||
"warn.dlss_download_failed": "Не удалось загрузить NVIDIA DLSS с {url}: {detail}. Игра работает и без него; при следующем запуске установка попробует снова.",
|
||||
|
||||
"ui.title": "Установка TotK",
|
||||
"ui.rebuild_title": "Пересборка игры под ваши моды",
|
||||
@@ -109,8 +116,8 @@
|
||||
"ui.intro": "Собирает готовую к запуску папку с Tears of the Kingdom из ваших игровых файлов. Занимает много времени и выполняется один раз.",
|
||||
"ui.notice": "Папка, которую создаёт установка, содержит игру. Она предназначена только для личного использования: не передавайте и не выкладывайте её.",
|
||||
"ui.keys_private": "Файл ключей читается на месте. Его содержимое не показывается, не копируется и не сохраняется.",
|
||||
"ui.base": "Базовая игра (NSP или XCI)",
|
||||
"ui.update": "Обновление 1.1.0 или 1.2.1 или 1.4.3 (NSP)",
|
||||
"ui.base": "Базовая игра (NSP, XCI, NSZ или XCZ)",
|
||||
"ui.update": "Обновление 1.1.0 или 1.2.1 или 1.4.3 (NSP или NSZ)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "Папка прошивки (необязательно)",
|
||||
"ui.game_language": "Язык игры",
|
||||
@@ -122,8 +129,10 @@
|
||||
"ui.pick_firmware": "Выберите папку прошивки Switch (Contents/registered)",
|
||||
"ui.pick_dest": "Выберите папку установки",
|
||||
"ui.firmware_help": "Для текста на китайском и корейском: папка Contents/registered прошивки, например из Ryujinx или Yuzu. Установка скопирует из неё системные шрифты Switch в папку установки.",
|
||||
"ui.type_game": "Файлы игры (NSP, XCI)",
|
||||
"ui.type_nsp": "Файлы NSP",
|
||||
"ui.dlss": "Загрузить NVIDIA DLSS (nvngx_dlss.dll) для видеокарт NVIDIA RTX",
|
||||
"ui.dlss_help": "Из DLSS SDK от NVIDIA на GitHub, по лицензии NVIDIA RTX SDK (github.com/NVIDIA/DLSS). Без него настройки dlaa и DLSS ничего не делают.",
|
||||
"ui.compressed_help": "Сжатые файлы NSZ и XCZ подходят как есть: установка сама их распакует (для этого, пока извлекаются данные игры, нужно столько свободного места, сколько занимают распакованные файлы).",
|
||||
"ui.type_game": "Файлы игры (NSP, XCI, NSZ, XCZ)",
|
||||
"ui.type_keys": "Файлы ключей (*.keys)",
|
||||
"ui.type_all": "Все файлы",
|
||||
"ui.start": "Начать",
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
"step.lift": "将游戏代码转换为 C 代码",
|
||||
"step.compile": "编译 {game_exe}",
|
||||
"step.prepare": "准备着色器",
|
||||
"step.dlss": "下载 NVIDIA DLSS",
|
||||
"step.cleanup": "清理",
|
||||
|
||||
"phase.check.host": "检查处理器和显卡",
|
||||
"phase.check.verify": "检查已安装的文件",
|
||||
"phase.check.verify_romfs": "检查已安装的游戏数据",
|
||||
"phase.check.inputs": "用密钥打开游戏文件",
|
||||
"phase.check.unpack_base": "正在解压压缩的游戏本体",
|
||||
"phase.check.unpack_update": "正在解压压缩的更新",
|
||||
"phase.check.versions": "检查游戏版本",
|
||||
"phase.fonts.extract": "正在从固件中提取共享字体",
|
||||
"phase.fonts.copy": "正在将共享字体复制到安装文件夹",
|
||||
@@ -30,6 +33,7 @@
|
||||
"phase.prepare.translate": "转译着色器",
|
||||
"phase.prepare.libraries": "为显卡驱动构建着色器",
|
||||
"phase.prepare.pack": "将着色器缓存打包成一个文件",
|
||||
"phase.dlss.download": "从 NVIDIA 的 DLSS SDK 下载 nvngx_dlss.dll",
|
||||
"phase.cleanup.ini": "写入 totk.ini",
|
||||
"phase.cleanup.temp": "删除临时文件",
|
||||
|
||||
@@ -58,14 +62,15 @@
|
||||
"check.keys_no_header_key": "密钥文件中没有 header_key。请从你的主机重新导出 prod.keys。",
|
||||
"check.keys_wrong": "这些密钥无法打开{input}文件。请使用你自己主机的 prod.keys。",
|
||||
"check.keys_too_old": "密钥文件对{input}文件来说太旧(需要密钥代数 {generation})。请从固件更新的主机导出 prod.keys。",
|
||||
"check.not_game_file": "{input}文件不是 XCI 或 NSP 游戏文件。",
|
||||
"check.not_game_file": "{input}文件不是 XCI、NSP、XCZ 或 NSZ 游戏文件。",
|
||||
"check.file_unreadable": "无法读取{input}文件:{detail}",
|
||||
"check.unpack_failed": "无法解压压缩的{input}文件:{detail}",
|
||||
"check.ticket_personalized": "{input}文件带有个人化票据(personalized ticket);需要通用票据(common ticket)。",
|
||||
"check.ticket_missing": "{input}文件缺少其内容所需的票据(ticket)。",
|
||||
"check.base_not_totk": "游戏本体文件不是《塞尔达传说 王国之泪》({detail})。",
|
||||
"check.base_is_update": "基础游戏文件是一个更新。请在这里选择基础游戏的 NSP 或 XCI(版本 1.0.0),并把此文件选为更新。",
|
||||
"check.base_combined": "基础游戏文件包含多个内容包(基础游戏、更新或 DLC 合在一个文件中)。请选择只包含游戏本体的基础游戏转储。",
|
||||
"check.update_not_nsp": "更新必须是 NSP 文件。",
|
||||
"check.update_not_nsp": "更新必须是 NSP 或 NSZ 文件。",
|
||||
"check.update_is_base": "更新文件里是游戏本体。请选择 1.1.0、1.2.1 或 1.4.3 的更新 NSP。",
|
||||
"check.update_not_totk": "更新文件不是《王国之泪》的更新({detail})。",
|
||||
"check.exefs_failed": "{input}文件的游戏代码未通过检查;详见 {log}。",
|
||||
@@ -98,9 +103,11 @@
|
||||
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
|
||||
"warn.xbox_prompts_failed": "无法准备 Xbox 按键提示(退出代码 {code});游戏将继续使用 Switch 按键提示。详情:{log}",
|
||||
"warn.xbox_prompts_failed": "无法准备 Xbox 或 PC 按键提示(退出代码 {code});游戏将继续使用 Switch 按键提示。详情:{log}",
|
||||
"warn.shaders_skipped": "显卡驱动在编译部分着色器时崩溃,因此有 {count} 个着色器未被预先准备。游戏会在首次需要时编译它们,驱动在那时也可能崩溃;更新驱动可能会解决。详情:{log}",
|
||||
"warn.kernel_old": "Linux {version} 缺少游戏依赖的写入跟踪功能(需要 6.7 或更新版本);游戏可能运行得很慢。更新系统即可解决。",
|
||||
"warn.dlss_kept": "安装文件夹中已有 DLSS 库({path});安装程序将保留它。",
|
||||
"warn.dlss_download_failed": "无法从 {url} 下载 NVIDIA DLSS:{detail}。游戏无需它也能运行;下次运行安装程序时会重试。",
|
||||
|
||||
"ui.title": "TotK 安装程序",
|
||||
"ui.rebuild_title": "正在为你的模组重新构建游戏",
|
||||
@@ -109,8 +116,8 @@
|
||||
"ui.intro": "用你自己的游戏文件构建一个可以游玩的《王国之泪》文件夹。耗时较长,只需运行一次。",
|
||||
"ui.notice": "此安装程序构建的文件夹包含游戏本身,仅供你个人使用;请勿分享或上传。",
|
||||
"ui.keys_private": "密钥文件只在原位置读取,其内容绝不会被显示、复制或保存。",
|
||||
"ui.base": "游戏本体(NSP 或 XCI)",
|
||||
"ui.update": "1.1.0、1.2.1 或 1.4.3 更新(NSP)",
|
||||
"ui.base": "游戏本体(NSP、XCI、NSZ 或 XCZ)",
|
||||
"ui.update": "1.1.0、1.2.1 或 1.4.3 更新(NSP 或 NSZ)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "固件文件夹(可选)",
|
||||
"ui.game_language": "游戏语言",
|
||||
@@ -122,8 +129,10 @@
|
||||
"ui.pick_firmware": "选择 Switch 固件文件夹(Contents/registered)",
|
||||
"ui.pick_dest": "选择安装文件夹",
|
||||
"ui.firmware_help": "用于中文、韩文和繁体中文文本:固件的 Contents/registered 文件夹,例如来自 Ryujinx 或 Yuzu。安装程序会从中把 Switch 系统字体复制到安装文件夹。",
|
||||
"ui.type_game": "游戏文件(NSP、XCI)",
|
||||
"ui.type_nsp": "NSP 文件",
|
||||
"ui.dlss": "为 NVIDIA RTX 显卡下载 NVIDIA DLSS(nvngx_dlss.dll)",
|
||||
"ui.dlss_help": "来自 GitHub 上 NVIDIA 的 DLSS SDK,遵循 NVIDIA RTX SDK 许可(github.com/NVIDIA/DLSS)。没有它,dlaa 和 DLSS 设置不起作用。",
|
||||
"ui.compressed_help": "压缩的 NSZ 和 XCZ 文件可以直接使用:安装程序会自行解压(在游戏数据提取完成之前,需要与解压后文件大小相同的可用空间)。",
|
||||
"ui.type_game": "游戏文件(NSP、XCI、NSZ、XCZ)",
|
||||
"ui.type_keys": "密钥文件(*.keys)",
|
||||
"ui.type_all": "所有文件",
|
||||
"ui.start": "开始",
|
||||
|
||||
@@ -55,6 +55,16 @@ site cloth main 8222f0 9401d7cf @page:x8 bd400104 1e240842 1e240863 9401d7cf #
|
||||
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
|
||||
site bounce main bda798 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
|
||||
|
||||
# Cursor and aim steps (upstream game_patches.c patch_cursor_speed, 363a6eb). These updates read the game's 30 Hz frame
|
||||
# ratio for their smoothing but leave it out of the step itself, so the map cursor and the vertical Ultrahand aim moved
|
||||
# 2x as fast at 60 fps and 4x at 120 as at the shipping 30 fps (issue #44). Each site multiplies the step by that ratio,
|
||||
# so the speed is the shipping one at any frame rate. x9 and s16 are free: the next read of x9 follows a write, the
|
||||
# function does not touch s16, and x20 keeps the update's frame object for the whole function.
|
||||
set cursor TOTK_HIGHFPS_FIX 1
|
||||
site cursor main ec3d9c bd400361 f94002e9 bd400930 1e300800 bd400361 # map pan speed s0 *= [[x23] + 8], then ldr s1, [x27]
|
||||
site cursor main 1d48d64 1e2a198c 1e2a198c bd400a90 1e30098c # fdiv s12, s12, s10, then s12 *= [x20 + 8]: vertical Ultrahand aim
|
||||
site cursor main 1d484e4 1e200923 1e200923 bd400a90 1e300863 # fmul s3, s9, s0, then s3 *= [x20 + 8]: vertical gyro aim (Recall)
|
||||
|
||||
# Output size (upstream games/totk/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
|
||||
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
|
||||
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
|
||||
@@ -93,6 +103,104 @@ set pooloff TOTK_POOL_OFFSET_PATCH 1
|
||||
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
|
||||
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
|
||||
|
||||
# Direct mouse camera (runtime/mouse_direct.c; the same hooks as patches-v143.txt, mapped by enclosing function to the
|
||||
# 1.2.1 main, each original word checked). The camera input update (main+0xd18088, 1.4.3 0x874c1c) after its dead zone,
|
||||
# clamp and StickExponent keeps x in s11 (1.4.3 s9); the host callback performs the displaced store and adds raw mouse
|
||||
# counts. The free orbit (main+0xae28b8) keeps its final angles at [sp+0x3c] / [sp+0x40], Ultrahand's curve results
|
||||
# (main+0x1768ebc) are s9 and s15. The host functions are at page +0xfd0 .. +0xf80; the page is main+0x472b000.
|
||||
set mouse_direct TOTK_MOUSE_DIRECT_PATCH 1
|
||||
site mouse_direct main d18140 bd04228b 94e84fa4 # camera input: str s11, [x20, #0x420]
|
||||
site mouse_direct main d181fc 6d4123e9 94e84f71 # camera input activity: ldp d9, d8, [sp, #0x10]
|
||||
site mouse_direct main ae28e8 f9400408 94f125b2 # free orbit begin: ldr x8, [x0, #8]
|
||||
site mouse_direct main ae355c bd403fed 94f12291 # free orbit angles: ldr s13, [sp, #0x3c]
|
||||
site mouse_direct main 176949c b9001be9 94bf0abd # Ultrahand input: str w9, [sp, #0x18]
|
||||
# The aiming, scope, ability, horse and Purah Pad cameras: per axis, the stick load and the angle the turn is added to,
|
||||
# each a bl to page +0xf80 told apart by the return address (runtime/mouse_direct.c load_sites_121). Where 1.2.1 adds
|
||||
# the turn in a shared helper (main+0x1bb70e8, 0x1bd0d00: *p = wrap(*p + s0)), the hook is on the turn the helper takes
|
||||
# (Horse) or on the store of the helper's result (Aiming's pitch target). Every enclosing function saves x30 in its
|
||||
# prologue and reads it only to return.
|
||||
site mouse_direct main 1d34338 bd4426c1 94a7df12 # Aiming: ldr s1, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d34374 bd015660 94a7df03 # Aiming: str s0, [x19, #0x154] (pitch target: +0x15c + turn from a helper)
|
||||
site mouse_direct main 1d34584 bd4422c1 94a7de7f # Aiming: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d345d0 bc5b83a1 94a7de6c # Aiming: ldur s1, [x29, #-0x48] (yaw)
|
||||
site mouse_direct main 1d35fd4 bd4422e0 94a7d7eb # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 1d35fe0 bd4426e9 94a7d7e8 # AimingTelescope: ldr s9, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 1d36280 bd416260 94a7d740 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 1d36224 bd405be1 94a7d757 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
|
||||
site mouse_direct main 1d48478 bd4422cf 94a78ec2 # SpecialPower: ldr s15, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d48480 bd4426cc 94a78ec0 # SpecialPower: ldr s12, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d48bb4 bd416262 94a78cf3 # SpecialPower: ldr s2, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 1d48b38 bc5a83a1 94a78d12 # SpecialPower: ldur s1, [x29, #-0x58] (yaw)
|
||||
site mouse_direct main 1d4070c bd442288 94a7ae1d # Horse: ldr s8, [x20, #0x420] (stick x)
|
||||
site mouse_direct main 1d40740 bd401be0 94a7ae10 # Horse: ldr s0, [sp, #0x18] (yaw turn, which a helper adds to +0x68)
|
||||
site mouse_direct main 1d408f8 bd4426a8 94a7ada2 # Horse: ldr s8, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d409ac bd406661 94a7ad75 # Horse: ldr s1, [x19, #0x64] (pitch)
|
||||
site mouse_direct main 1d48ccc bd4422c9 94a78cad # SpecialPower offset mode: ldr s9, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d48cf0 bd41da61 94a78ca4 # SpecialPower offset mode: ldr s1, [x19, #0x1d8] (offset x)
|
||||
site mouse_direct main 1d48cfc bd4426c9 94a78ca1 # SpecialPower offset mode: ldr s9, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d48d18 bd41de61 94a78c9a # SpecialPower offset mode: ldr s1, [x19, #0x1dc] (offset y)
|
||||
site mouse_direct main 1d485f8 bd400400 94a78e62 # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
|
||||
site mouse_direct main 1d37b44 bd4426ad 94a7d10f # CameraFinder: ldr s13, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d37b68 bd405261 94a7d106 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
|
||||
site mouse_direct main 1d37bcc bd4422ad 94a7d0ed # CameraFinder: ldr s13, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 1d37bf4 bd405be1 94a7d0e3 # CameraFinder: ldr s1, [sp, #0x58] (yaw)
|
||||
# The other player cameras. ClimbObj, EventMultiTalk and EventTalkManual load an angle with a pre-indexed load whose
|
||||
# write-back the code after it needs; the mov before it adds the offset instead and the hook loads [base] (no register
|
||||
# between the two words, no branch into them). Wakeboard, ShieldSurfing and Tail read the stick twice, once for its
|
||||
# length and once for its angle: the first pair gives the same values without degrees of its own (REPEAT).
|
||||
site mouse_direct main 1d38e08 bd442301 94a7cc5e # ClimbObj: ldr s1, [x24, #0x420]
|
||||
site mouse_direct main 1d38e10 bd409261 94a7cc5c # ClimbObj: ldr s1, [x19, #0x90] (yaw)
|
||||
site mouse_direct main 1d38dac bd442701 94a7cc75 # ClimbObj: ldr s1, [x24, #0x424]
|
||||
site mouse_direct main 1d38da4 aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
|
||||
site mouse_direct main 1d38db8 bc488ec1 94a7cc72 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
|
||||
site mouse_direct main 1d50738 bd4422c0 94a76e12 # Wakeboard: ldr s0, [x22, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d50740 bd4426c1 94a76e10 # Wakeboard: ldr s1, [x22, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d50768 bd4426c0 94a76e06 # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 1d5076c bd4422c1 94a76e05 # Wakeboard: ldr s1, [x22, #0x420]
|
||||
site mouse_direct main 1d50e5c bd40da60 94a76c49 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
|
||||
site mouse_direct main 1d508bc bd401fa0 94a76db1 # Wakeboard: ldr s0, [x29, #0x1c] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 1d50f20 bd403bec 94a76c18 # Wakeboard: ldr s12, [sp, #0x38] (yaw)
|
||||
site mouse_direct main 1d51e10 bd4422cd 94a7685c # WaterfallClimb: ldr s13, [x22, #0x420]
|
||||
site mouse_direct main 1d51fb8 b9403be8 94a767f2 # WaterfallClimb: ldr w8, [sp, #0x38] (yaw)
|
||||
site mouse_direct main 1d51f48 bd4426c9 94a7680e # WaterfallClimb: ldr s9, [x22, #0x424]
|
||||
site mouse_direct main 1d4a11c bd442280 94a78799 # Tail: ldr s0, [x20, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d4a124 bd442681 94a78797 # Tail: ldr s1, [x20, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d4a14c bd442680 94a7878d # Tail (climbing): ldr s0, [x20, #0x424] (stick y)
|
||||
site mouse_direct main 1d4a150 bd442281 94a7878c # Tail: ldr s1, [x20, #0x420]
|
||||
site mouse_direct main 1d4a4a0 bd40c260 94a786b8 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
|
||||
site mouse_direct main 1d4a514 bd40b661 94a7869b # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
|
||||
site mouse_direct main 1d4c930 bd442320 94a77d94 # LockOn / Target: ldr s0, [x25, #0x420]
|
||||
site mouse_direct main 1d4c958 bd442321 94a77d8a # LockOn / Target: ldr s1, [x25, #0x420] (stick x)
|
||||
site mouse_direct main 1d4c97c bd402be0 94a77d81 # LockOn / Target: ldr s0, [sp, #0x28] (yaw turn)
|
||||
site mouse_direct main 1d4c98c bd402be9 94a77d7d # LockOn / Target: ldr s9, [sp, #0x28] (yaw turn)
|
||||
site mouse_direct main 1d4cd5c bd442721 94a77c89 # LockOn / Target: ldr s1, [x25, #0x424] (stick y)
|
||||
site mouse_direct main 1d4cd80 bd402be0 94a77c80 # LockOn / Target: ldr s0, [sp, #0x28] (pitch turn)
|
||||
site mouse_direct main 1d4cd90 bd402beb 94a77c7c # LockOn / Target: ldr s11, [sp, #0x28] (pitch turn)
|
||||
site mouse_direct main 1d443e8 bd4426a9 94a79ee6 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d44430 bd419a61 94a79ed4 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
|
||||
site mouse_direct main 1d44444 bd4422a9 94a79ecf # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 1d44490 bd419e61 94a79ebc # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
|
||||
site mouse_direct main 1d418dc bd44234b 94a7a9a9 # HorseLockOnEmpty: ldr s11, [x26, #0x420]
|
||||
site mouse_direct main 1d3a484 bd44230b 94a7c6bf # EventMultiTalk: ldr s11, [x24, #0x420]
|
||||
site mouse_direct main 1d3a3f8 bd44270b 94a7c6e2 # EventMultiTalk: ldr s11, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 1d3a418 aa1303f7 91017277 # EventMultiTalk: mov x23, x19 -> add x23, x19, #0x5c
|
||||
site mouse_direct main 1d3a420 bc45cee1 94a7c6d8 # EventMultiTalk: ldr s1, [x23, #0x5c]! (pitch), as ldr s1, [x23]
|
||||
site mouse_direct main 1d3a4dc aa1303f6 91019276 # EventMultiTalk: mov x22, x19 -> add x22, x19, #0x64
|
||||
site mouse_direct main 1d3a4e4 bc464ec1 94a7c6a7 # EventMultiTalk: ldr s1, [x22, #0x64]! (yaw), as ldr s1, [x22]
|
||||
site mouse_direct main 1d3e2b0 bd442729 94a7b734 # EventTalkManual: ldr s9, [x25, #0x424] (stick y)
|
||||
site mouse_direct main 1d3e2d4 aa1303f8 9101d278 # EventTalkManual: mov x24, x19 -> add x24, x19, #0x74
|
||||
site mouse_direct main 1d3e2d8 bc474f01 94a7b72a # EventTalkManual: ldr s1, [x24, #0x74]! (pitch), as ldr s1, [x24]
|
||||
site mouse_direct main 1d3e33c bd442329 94a7b711 # EventTalkManual: ldr s9, [x25, #0x420] (stick x)
|
||||
site mouse_direct main 1d3e360 aa1303f6 9101f276 # EventTalkManual: mov x22, x19 -> add x22, x19, #0x7c
|
||||
site mouse_direct main 1d3e364 bc47cec1 94a7b707 # EventTalkManual: ldr s1, [x22, #0x7c]! (yaw), as ldr s1, [x22]
|
||||
site mouse_direct main 1d46108 bd4422e0 94a7979e # ShieldSurfing: ldr s0, [x23, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d46110 bd4426e1 94a7979c # ShieldSurfing: ldr s1, [x23, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d46138 bd4426e0 94a79792 # ShieldSurfing: ldr s0, [x23, #0x424] (stick y, kept)
|
||||
site mouse_direct main 1d4613c bd4422e1 94a79791 # ShieldSurfing: ldr s1, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 1d46788 bd40de60 94a795fe # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
|
||||
site mouse_direct main 1d46910 bd401fa0 94a7959c # ShieldSurfing: ldr s0, [x29, #0x1c] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 1d467b4 bd403bec 94a795f3 # ShieldSurfing: ldr s12, [sp, #0x38] (yaw)
|
||||
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
|
||||
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
|
||||
spin main ac600 ac620 ac630 # 13 instructions; loads
|
||||
|
||||
@@ -103,6 +103,92 @@ set fov TOTK_FOV_PATCH 0
|
||||
site fov main 628c3c bd413e60 f81f0fea @page:x10 bd413e60 bd403d48 1e280800 f84107ea # ldr s0, [x19, #0x13c] (camera update)
|
||||
site fov main b4342c bd4066c0 f81f0fea @page:x10 bd4066c0 bd403d48 1e280800 f84107ea # ldr s0, [x22, #0x64] (camera setup)
|
||||
|
||||
# The camera input update, after its stick dead zone, unit-radius clamp and StickExponent. The host callback
|
||||
# performs the displaced store and adds raw mouse counts to s9/s10 (runtime/mouse_direct.c camera_input).
|
||||
# Its address is page +0xfd0; the page is main+0x3adf000 on this verified build.
|
||||
set mouse_direct TOTK_MOUSE_DIRECT_PATCH 1
|
||||
site mouse_direct main 874cb4 bd042289 94c9acc7
|
||||
site mouse_direct main 874d7c 6d40a3e9 94c9ac91
|
||||
site mouse_direct main 18e444 f9400408 94e546db
|
||||
site mouse_direct main 18f108 bd4047ec 94e543a6
|
||||
site mouse_direct main 1d59460 b9004be8 94761acc
|
||||
# The aiming, scope, ability, horse and Purah Pad camera cameras: per axis, the float load the stick turn is computed
|
||||
# from (the gamepad's part only) and the load of the angle it is added to (plus the mouse's degrees), each a bl to page
|
||||
# +0xf80, told apart by the return address (runtime/mouse_direct.c camera_load, load_sites). Every enclosing function
|
||||
# saves x30 in its prologue, so the bl's link register write is harmless.
|
||||
site mouse_direct main 23beb94 bd4426c1 945c84fb # Aiming: ldr s1, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 23bebc8 bd415e61 945c84ee # Aiming: ldr s1, [x19, #0x15c] (pitch)
|
||||
site mouse_direct main 23bee2c bd4422c1 945c8455 # Aiming: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 23beeec bc5b83a1 945c8425 # Aiming: ldur s1, [x29, #-0x48] (yaw)
|
||||
site mouse_direct main 23c0a10 bd4422e0 945c7d5c # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23c0a1c bd4426eb 945c7d59 # AimingTelescope: ldr s11, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23c0b00 bd416260 945c7d20 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 23c0e80 bd405be1 945c7c40 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
|
||||
site mouse_direct main 23d2eec bd4422e8 945c3425 # SpecialPower: ldr s8, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23d2ef4 bd4426ee 945c3423 # SpecialPower: ldr s14, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23d3158 bd416260 945c338a # SpecialPower: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 23d34cc bc5b03a1 945c32ad # SpecialPower: ldur s1, [x29, #-0x50] (yaw)
|
||||
site mouse_direct main 23cb274 bd442288 945c5343 # Horse: ldr s8, [x20, #0x420] (stick x)
|
||||
site mouse_direct main 23cb2a0 bd406a61 945c5338 # Horse: ldr s1, [x19, #0x68] (yaw)
|
||||
site mouse_direct main 23cb3f8 bd4426a8 945c52e2 # Horse: ldr s8, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23cb424 bd406661 945c52d7 # Horse: ldr s1, [x19, #0x64] (pitch)
|
||||
site mouse_direct main 23d2f30 bd4422e8 945c3414 # SpecialPower offset mode: ldr s8, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23d2f5c bd41da62 945c3409 # SpecialPower offset mode: ldr s2, [x19, #0x1d8] (offset x)
|
||||
site mouse_direct main 23d2f6c bd4426e8 945c3405 # SpecialPower offset mode: ldr s8, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23d2f98 bd41de62 945c33fa # SpecialPower offset mode: ldr s2, [x19, #0x1dc] (offset y)
|
||||
site mouse_direct main 23d3210 bd400400 945c335c # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
|
||||
site mouse_direct main 23c2890 bd4426ad 945c75bc # CameraFinder: ldr s13, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23c28b4 bd405261 945c75b3 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
|
||||
site mouse_direct main 23c2934 bd4422ad 945c7593 # CameraFinder: ldr s13, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 23c2964 bd404be1 945c7587 # CameraFinder: ldr s1, [sp, #0x48] (yaw)
|
||||
# The other player cameras (runtime/mouse_direct.c load_sites; a ROLE_WATCH load stays the game's and only names its
|
||||
# mode in the log when the mouse was in it):
|
||||
site mouse_direct main 23c3dd0 bd442301 945c706c # ClimbObj: ldr s1, [x24, #0x420]
|
||||
site mouse_direct main 23c3dd8 bd409261 945c706a # ClimbObj: ldr s1, [x19, #0x90] (yaw)
|
||||
site mouse_direct main 23c3d64 bd442701 945c7087 # ClimbObj: ldr s1, [x24, #0x424]
|
||||
# ClimbObj's pitch is a pre-indexed load, ldr s1, [x22, #0x88]!, whose write-back the code after it needs (ldp and
|
||||
# str through x22). A host call cannot write x22: the lifter keeps X19-X29 in locals across a call. So the mov before
|
||||
# it adds the 0x88 instead (x22 = x19 + 0x88) and the hook loads [x22] with no write-back.
|
||||
site mouse_direct main 23c3d5c aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
|
||||
site mouse_direct main 23c3d70 bc488ec1 945c7084 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
|
||||
site mouse_direct main 23da694 bd4422c1 945c163b # Wakeboard: ldr s1, [x22, #0x420]
|
||||
site mouse_direct main 23da698 bd4426c0 945c163a # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 23da840 bd40da60 945c15d0 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
|
||||
site mouse_direct main 23da894 bd4017e0 945c15bb # Wakeboard: ldr s0, [sp, #0x14] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 23daed8 bd403fe9 945c142a # Wakeboard: ldr s9, [sp, #0x3c] (yaw)
|
||||
site mouse_direct main 23dced8 bd4422cd 945c0c2a # WaterfallClimb: ldr s13, [x22, #0x420]
|
||||
site mouse_direct main 23dd070 b85cc3a8 945c0bc4 # WaterfallClimb: ldur w8, [x29, #-0x34] (yaw)
|
||||
site mouse_direct main 23dd008 bd4426c9 945c0bde # WaterfallClimb: ldr s9, [x22, #0x424]
|
||||
site mouse_direct main 23d4b14 bd44228c 945c2d1b # Tail: ldr s12, [x20, #0x420]
|
||||
site mouse_direct main 23d4b1c bd44268d 945c2d19 # Tail (climbing): ldr s13, [x20, #0x424] (stick y)
|
||||
site mouse_direct main 23d4f34 bd40c260 945c2c13 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
|
||||
site mouse_direct main 23d5028 bd40b661 945c2bd6 # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
|
||||
site mouse_direct main 23d756c bd442300 945c2285 # LockOn / Target: ldr s0, [x24, #0x420]
|
||||
site mouse_direct main 23d7594 bd442301 945c227b # LockOn / Target: ldr s1, [x24, #0x420] (stick x)
|
||||
site mouse_direct main 23d75b4 bd4023e0 945c2273 # LockOn / Target: ldr s0, [sp, #0x20] (yaw turn)
|
||||
site mouse_direct main 23d75c4 bd4023e9 945c226f # LockOn / Target: ldr s9, [sp, #0x20] (yaw turn)
|
||||
site mouse_direct main 23d7998 bd442701 945c217a # LockOn / Target: ldr s1, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 23d79b8 bd4023e0 945c2172 # LockOn / Target: ldr s0, [sp, #0x20] (pitch turn)
|
||||
site mouse_direct main 23d79c8 bd4023ea 945c216e # LockOn / Target: ldr s10, [sp, #0x20] (pitch turn)
|
||||
site mouse_direct main 23cecb4 bd4426a9 945c44b3 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23cecf8 bd419a61 945c44a2 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
|
||||
site mouse_direct main 23ced0c bd4422a9 945c449d # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 23ced4c bd419e61 945c448d # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
|
||||
site mouse_direct main 23cc4b4 bd44232b 945c4eb3 # HorseLockOnEmpty: ldr s11, [x25, #0x420]
|
||||
site mouse_direct main 23c54d4 bd4422eb 945c6aab # EventMultiTalk: ldr s11, [x23, #0x420]
|
||||
site mouse_direct main 23c542c bd4426eb 945c6ad5 # EventMultiTalk: ldr s11, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23c546c bd405e61 945c6ac5 # EventMultiTalk: ldr s1, [x19, #0x5c] (pitch)
|
||||
site mouse_direct main 23c5554 bd406661 945c6a8b # EventMultiTalk: ldr s1, [x19, #0x64] (yaw)
|
||||
site mouse_direct main 23c90ac bd442709 945c5bb5 # EventTalkManual: ldr s9, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 23c90d8 bd407661 945c5baa # EventTalkManual: ldr s1, [x19, #0x74] (pitch)
|
||||
site mouse_direct main 23c9140 bd442309 945c5b90 # EventTalkManual: ldr s9, [x24, #0x420] (stick x)
|
||||
site mouse_direct main 23c9170 bd407e61 945c5b84 # EventTalkManual: ldr s1, [x19, #0x7c] (yaw)
|
||||
site mouse_direct main 23d0100 bd4422c1 945c3fa0 # ShieldSurfing: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 23d0104 bd4426c0 945c3f9f # ShieldSurfing: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 23d02ac bd40de60 945c3f35 # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
|
||||
site mouse_direct main 23d0a34 bd4013e0 945c3d53 # ShieldSurfing: ldr s0, [sp, #0x10] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 23d087c bd403fe9 945c3dc1 # ShieldSurfing: ldr s9, [sp, #0x3c] (yaw)
|
||||
|
||||
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
|
||||
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
|
||||
# its wrapper ZSTD_decompress_usingDDict (main+0x3a1e0, which tail-branches to it) goes to a host function on the patch
|
||||
|
||||
+22
-17
@@ -56,6 +56,12 @@ static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The 7-byte UID of an NTAG215 dump: bytes 0..2, then 4..7. Byte 3 is the check byte BCC0.
|
||||
static void copy_uid(uint8_t out[7], const uint8_t *dump) {
|
||||
memcpy(out, dump, 3);
|
||||
memcpy(out + 3, dump + 4, 4);
|
||||
}
|
||||
|
||||
static void parse_dump_info(const char *path) {
|
||||
memset(&g_info, 0, sizeof(g_info));
|
||||
if (path) {
|
||||
@@ -69,15 +75,15 @@ static void parse_dump_info(const char *path) {
|
||||
snprintf(g_info.filename, sizeof(g_info.filename), "%s", name);
|
||||
}
|
||||
|
||||
// UID: bytes 0..6 (7 bytes) or up to 9
|
||||
memcpy(g_info.uuid, g_dump, 7);
|
||||
copy_uid(g_info.uuid, g_dump);
|
||||
g_info.uuid_len = 7;
|
||||
|
||||
// Amiibo ID at offset 0x54 (8 bytes)
|
||||
// Amiibo ID at offset 0x54 (8 bytes, 3dbrew "Amiibo"): character ID and variant, figure type, model number
|
||||
// (big-endian), series, format version 02
|
||||
memcpy(g_info.character_id, g_dump + 0x54, 3);
|
||||
g_info.series_id = g_dump[0x57];
|
||||
g_info.nfp_type = g_dump[0x57];
|
||||
g_info.numbering_id = ((uint16_t)g_dump[0x58] << 8) | g_dump[0x59];
|
||||
g_info.nfp_type = g_dump[0x5A];
|
||||
g_info.series_id = g_dump[0x5A];
|
||||
g_info.version = g_dump[0x5B];
|
||||
}
|
||||
|
||||
@@ -321,7 +327,7 @@ void amiibo_get_tag_info(void *out_tag_info) {
|
||||
uint8_t *out = (uint8_t *)out_tag_info;
|
||||
memset(out, 0, 0x58);
|
||||
// UUID: offset 0x00, 10 bytes max
|
||||
memcpy(out + 0x00, g_dump, 7);
|
||||
copy_uid(out, g_dump);
|
||||
out[0x0A] = 7; // UUID length
|
||||
// Protocol at offset 0x20: TypeA = 1
|
||||
*(uint32_t *)(out + 0x20) = 1;
|
||||
@@ -334,15 +340,14 @@ void amiibo_get_model_info(void *out_model_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_model_info;
|
||||
memset(out, 0, 0x40);
|
||||
// Character ID at 0x00 (3 bytes)
|
||||
memcpy(out + 0x00, g_dump + 0x54, 3);
|
||||
// Series ID at 0x03 (1 byte)
|
||||
out[0x03] = g_dump[0x57];
|
||||
// Numbering ID at 0x04 (2 bytes, big-endian from 0x58)
|
||||
out[0x04] = g_dump[0x58];
|
||||
out[0x05] = g_dump[0x59];
|
||||
// NFP Type at 0x06 (1 byte)
|
||||
out[0x06] = g_dump[0x5A];
|
||||
// nn::nfp::ModelInfo from the amiibo ID at 0x54: character ID and variant in the tag's byte order, series, model
|
||||
// number little-endian (big-endian on the tag), figure type. The game tells figures of one character apart by the
|
||||
// model number.
|
||||
memcpy(out + 0x00, g_dump + 0x54, 3); // character ID, variant
|
||||
out[0x03] = g_dump[0x5A]; // series
|
||||
out[0x04] = g_dump[0x59]; // model number
|
||||
out[0x05] = g_dump[0x58];
|
||||
out[0x06] = g_dump[0x57]; // figure type
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
@@ -351,7 +356,7 @@ void amiibo_get_common_info(void *out_common_info) {
|
||||
uint8_t *out = (uint8_t *)out_common_info;
|
||||
memset(out, 0, 0x40);
|
||||
// Last write date: 0x00..0x03
|
||||
out[0x00] = 0x07; out[0x01] = 0xE7; // 2023
|
||||
out[0x00] = 0xE7; out[0x01] = 0x07; // 2023, u16 little-endian
|
||||
out[0x02] = 0x05; out[0x03] = 0x0C; // May 12
|
||||
// Write counter: 0x04..0x05
|
||||
out[0x04] = 0x00; out[0x05] = 0x01;
|
||||
@@ -369,7 +374,7 @@ void amiibo_get_register_info(void *out_register_info) {
|
||||
// Mii CharInfo at 0x00..0x57 (dummy valid mii)
|
||||
out[0x00] = 0x03; // version 3
|
||||
// First write date at 0x58..0x5B
|
||||
out[0x58] = 0x07; out[0x59] = 0xE7;
|
||||
out[0x58] = 0xE7; out[0x59] = 0x07; // 2023, u16 little-endian
|
||||
out[0x5A] = 0x05; out[0x5B] = 0x0C;
|
||||
// Amiibo Name at 0x5C: copy up to 40 bytes NUL-terminated
|
||||
const char *name = g_info.filename[0] ? g_info.filename : "Amiibo";
|
||||
|
||||
+84
-42
@@ -5,8 +5,9 @@
|
||||
//
|
||||
// Per 5 ms frame the render thread (audren.c, renderer lock held) decodes every started voice (PCM8/16/24/32, float,
|
||||
// DSP-ADPCM) along the sample path the voice clock consumes, resamples it linearly, runs the voice biquads, scales it
|
||||
// into its destination mix, folds the submixes, runs the mix effects and writes the device sink's stereo mix into a
|
||||
// single-producer single-consumer ring. The SDL audio thread drains that ring without taking any lock: an empty ring
|
||||
// into its destination mix, folds the submixes, runs the mix effects and writes the device sink's mix (stereo, or 5.1
|
||||
// with TOTK_AUDIO_CHANNELS, output_channels) into a 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, 60 on Linux) 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
|
||||
@@ -32,7 +33,8 @@
|
||||
#include "ipc.h"
|
||||
|
||||
#define MAX_FRAME 1024
|
||||
#define RING_FRAMES 16384 // stereo frames, about 340 ms at 48 kHz
|
||||
#define RING_FRAMES 16384 // output frames, about 340 ms at 48 kHz
|
||||
#define OUT_MAX 6 // output channels at most: 5.1
|
||||
#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
|
||||
@@ -51,7 +53,8 @@ typedef struct Mixer {
|
||||
int *offset; // first mix buffer of each mix id
|
||||
SplitInfo *split; int nsplit; SplitDest *dest; int ndest;
|
||||
SDL_AudioStream *stream; int device_ok, rate;
|
||||
float ring[RING_FRAMES * 2];
|
||||
int channels; // output channels, 2 or 6 (output_channels)
|
||||
float ring[RING_FRAMES * OUT_MAX];
|
||||
_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)
|
||||
@@ -61,7 +64,7 @@ typedef struct Mixer {
|
||||
EffectState *effect; int *effect_order; unsigned effect_reported;
|
||||
uint64_t *sink_address; uint32_t *sink_size, *sink_offset, *sink_old, *sink_written;
|
||||
unsigned reported; // one-time reports
|
||||
float voice_scratch[6][MAX_FRAME], filter_scratch[MAX_FRAME], output_scratch[MAX_FRAME * 2];
|
||||
float voice_scratch[6][MAX_FRAME], filter_scratch[MAX_FRAME], output_scratch[MAX_FRAME * OUT_MAX];
|
||||
double level_sum, level_peak; uint64_t level_count, level_frames; int first_sound_logged;
|
||||
int prefill; // render blocks
|
||||
} Mixer;
|
||||
@@ -105,8 +108,32 @@ static int prefill_blocks_for(const char *e) {
|
||||
return (int)ceil(ms / 5.0);
|
||||
}
|
||||
|
||||
// Fills `frames` stereo frames from the ring (zeros for what is missing). Lock-free; runs on SDL's audio thread.
|
||||
// TOTK_AUDIO_CHANNELS: the output channels of this run, decided once. stereo gives 2; 5.1 gives 6 (FL FR C LFE RL RR,
|
||||
// the device sink's order and SDL's); auto gives 6 when the default playback device has at least six channels (a 5.1
|
||||
// or 7.1 speaker setup in Windows; SDL places 5.1 on 7.1) and 2 otherwise. IAudioDevice reports the same count to the
|
||||
// game (GetActiveChannelCount), which picks its sink layout from it. TOTK_AUDIO=none: 2, without touching SDL.
|
||||
static int output_channels(void) {
|
||||
static _Atomic int decided;
|
||||
int n = atomic_load(&decided);
|
||||
if (n) return n;
|
||||
const char *want = cfg_value("TOTK_AUDIO_CHANNELS"), *mode = cfg_get("TOTK_AUDIO");
|
||||
if ((mode && !strcmp(mode, "none")) || cfg_get("TOTK_AUDIO_OFF")) n = 2;
|
||||
else if (!strcmp(want, "5.1")) n = 6;
|
||||
else if (!strcmp(want, "stereo")) n = 2;
|
||||
else {
|
||||
SDL_AudioSpec dev = {0};
|
||||
n = SDL_InitSubSystem(SDL_INIT_AUDIO) && SDL_GetAudioDeviceFormat(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &dev, NULL) &&
|
||||
dev.channels >= 6 ? 6 : 2;
|
||||
rt_log("audio: TOTK_AUDIO_CHANNELS=auto: the default device has %d channels, output %s", dev.channels,
|
||||
n == 6 ? "5.1" : "stereo");
|
||||
}
|
||||
int expected = 0;
|
||||
return atomic_compare_exchange_strong(&decided, &expected, n) ? n : expected;
|
||||
}
|
||||
|
||||
// Fills `frames` output frames from the ring (zeros for what is missing). Lock-free; runs on SDL's audio thread.
|
||||
static void ring_pull(Mixer *m, float *out, uint32_t frames) {
|
||||
const int oc = m->channels;
|
||||
atomic_fetch_add(&m->callback_frames, frames);
|
||||
unsigned epoch = atomic_load_explicit(&m->stop_epoch, memory_order_acquire);
|
||||
uint64_t rd = atomic_load(&m->rd), wr = atomic_load_explicit(&m->wr, memory_order_acquire), have = wr - rd;
|
||||
@@ -121,7 +148,7 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
|
||||
m->ready = 0;
|
||||
}
|
||||
if (!atomic_load_explicit(&m->playing, memory_order_acquire)) {
|
||||
memset(out, 0, (size_t)frames * 2 * sizeof(float));
|
||||
memset(out, 0, (size_t)frames * oc * sizeof(float));
|
||||
m->ready = 0;
|
||||
atomic_store_explicit(&m->rd, wr, memory_order_release);
|
||||
return;
|
||||
@@ -131,7 +158,7 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
|
||||
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
|
||||
if (!m->ready) {
|
||||
if (have < prefill) {
|
||||
memset(out, 0, (size_t)frames * 2 * sizeof(float));
|
||||
memset(out, 0, (size_t)frames * oc * sizeof(float));
|
||||
return;
|
||||
}
|
||||
m->ready = 1;
|
||||
@@ -147,13 +174,16 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
|
||||
uint64_t nonzero = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
uint64_t k = (rd + i) % RING_FRAMES;
|
||||
out[2 * i] = m->ring[2 * k];
|
||||
out[2 * i + 1] = m->ring[2 * k + 1];
|
||||
if (out[2 * i] != 0 || out[2 * i + 1] != 0) nonzero++;
|
||||
int sound = 0;
|
||||
for (int c = 0; c < oc; c++) {
|
||||
out[oc * i + c] = m->ring[oc * k + c];
|
||||
sound |= out[oc * i + c] != 0;
|
||||
}
|
||||
nonzero += sound;
|
||||
}
|
||||
atomic_fetch_add(&m->callback_nonzero, nonzero);
|
||||
if (n < frames) {
|
||||
memset(out + 2 * n, 0, (size_t)(frames - n) * 2 * sizeof(float));
|
||||
memset(out + (size_t)oc * n, 0, (size_t)(frames - n) * oc * sizeof(float));
|
||||
atomic_fetch_add(&m->underruns, 1);
|
||||
atomic_fetch_add(&m->missing_frames, frames - n);
|
||||
m->ready = 0;
|
||||
@@ -164,15 +194,16 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
|
||||
static void SDLCALL sdl_pull(void *user, SDL_AudioStream *stream, int additional, int total) {
|
||||
(void)total;
|
||||
Mixer *m = user;
|
||||
float tmp[2 * 1024];
|
||||
float tmp[OUT_MAX * 1024];
|
||||
const uint32_t frame_bytes = (uint32_t)m->channels * sizeof(float);
|
||||
atomic_store_explicit(&m->pulled_ns, kern_now_ns(), memory_order_release);
|
||||
while (additional > 0) {
|
||||
uint32_t frames = (uint32_t)additional / (2 * sizeof(float));
|
||||
uint32_t frames = (uint32_t)additional / frame_bytes;
|
||||
if (!frames) break;
|
||||
if (frames > 1024) frames = 1024;
|
||||
ring_pull(m, tmp, frames);
|
||||
if (!SDL_PutAudioStreamData(stream, tmp, (int)(frames * 2 * sizeof(float)))) break;
|
||||
additional -= (int)(frames * 2 * sizeof(float));
|
||||
if (!SDL_PutAudioStreamData(stream, tmp, (int)(frames * frame_bytes))) break;
|
||||
additional -= (int)(frames * frame_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +217,7 @@ static void output_open(Mixer *m, int rate) {
|
||||
rt_log("audio: SDL audio init failed (%s): mixing without sound", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
SDL_AudioSpec spec = {SDL_AUDIO_F32, 2, rate};
|
||||
SDL_AudioSpec spec = {SDL_AUDIO_F32, m->channels, rate};
|
||||
m->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, sdl_pull, m);
|
||||
if (!m->stream) {
|
||||
rt_log("audio: no output device (%s): mixing without sound", SDL_GetError());
|
||||
@@ -199,7 +230,7 @@ static void output_open(Mixer *m, int rate) {
|
||||
return;
|
||||
}
|
||||
m->device_ok = 1;
|
||||
SDL_AudioSpec dev;
|
||||
SDL_AudioSpec dev = {0};
|
||||
int period = 0;
|
||||
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(m->stream), &dev, &period) || period <= 0) period = m->frame;
|
||||
m->period = period;
|
||||
@@ -208,8 +239,9 @@ static void output_open(Mixer *m, int rate) {
|
||||
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, device period %d frames; the device paces "
|
||||
"rendering at %.1f ms queued", name ? name : "?", rate, m->prefill * 5, period, m->target * 1000.0 / rate);
|
||||
rt_log("audio: SDL device '%s' opened, %d Hz %s float (device %d channels), prefill %d ms, device period %d frames; "
|
||||
"the device paces rendering at %.1f ms queued", name ? name : "?", rate, m->channels == 6 ? "5.1" : "stereo",
|
||||
dev.channels, m->prefill * 5, period, m->target * 1000.0 / rate);
|
||||
}
|
||||
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
|
||||
@@ -224,7 +256,8 @@ int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
|
||||
}
|
||||
|
||||
// Render thread only.
|
||||
static void output_push(Mixer *m, const float *lr, int frames) {
|
||||
static void output_push(Mixer *m, const float *out, int frames) {
|
||||
const int oc = m->channels;
|
||||
uint64_t wr = atomic_load(&m->wr), rd = atomic_load_explicit(&m->rd, memory_order_acquire);
|
||||
if (m->device_ok && wr + (uint64_t)frames - rd > RING_FRAMES) { // device stalled: drop this frame
|
||||
atomic_fetch_add(&m->overruns, 1);
|
||||
@@ -232,8 +265,7 @@ static void output_push(Mixer *m, const float *lr, int frames) {
|
||||
}
|
||||
for (int i = 0; i < frames; i++) {
|
||||
uint64_t k = (wr + (uint64_t)i) % RING_FRAMES;
|
||||
m->ring[2 * k] = lr[2 * i];
|
||||
m->ring[2 * k + 1] = lr[2 * i + 1];
|
||||
memcpy(&m->ring[oc * k], &out[oc * i], (size_t)oc * sizeof(float));
|
||||
}
|
||||
atomic_store_explicit(&m->wr, wr + (uint64_t)frames, memory_order_release);
|
||||
if (!m->device_ok) atomic_store(&m->rd, wr + (uint64_t)frames); // no device: the ring only keeps the latest frames
|
||||
@@ -268,16 +300,16 @@ static void sink_dump(const float *raw, int frames) {
|
||||
}
|
||||
|
||||
// Level meter (TOTK_TRACE=audio): one line per 5 s of output, and the first nonzero frame.
|
||||
static void level_note(AudioRenderer *r, Mixer *m, const float *lr, int frames) {
|
||||
static void level_note(AudioRenderer *r, Mixer *m, const float *out, int frames) {
|
||||
if (!(rt_trace & RT_TRACE_AUDIO)) return;
|
||||
int sound = 0;
|
||||
for (int k = 0; k < 2 * frames; k++) {
|
||||
double x = lr[k];
|
||||
for (int k = 0; k < m->channels * frames; k++) {
|
||||
double x = out[k];
|
||||
m->level_sum += x * x;
|
||||
if (fabs(x) > m->level_peak) m->level_peak = fabs(x);
|
||||
if (x != 0) sound = 1;
|
||||
}
|
||||
m->level_count += 2 * (uint64_t)frames;
|
||||
m->level_count += (uint64_t)m->channels * (uint64_t)frames;
|
||||
m->level_frames += (uint64_t)frames;
|
||||
if (sound && !m->first_sound_logged) {
|
||||
m->first_sound_logged = 1;
|
||||
@@ -328,6 +360,7 @@ void audio_mixer_open(AudioRenderer *r) {
|
||||
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using %d", pe, PREFILL_DEFAULT_MS);
|
||||
m->prefill = PREFILL_DEFAULT_MS / 5;
|
||||
}
|
||||
m->channels = output_channels();
|
||||
r->mixer = m;
|
||||
atomic_store(&m->playing, r->state == 0);
|
||||
output_open(m, r->param.sample_rate);
|
||||
@@ -846,33 +879,42 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
if (r->sinks[i].type == SINK_DEVICE && !sink) sink = &r->sinks[i];
|
||||
else if (r->sinks[i].type == SINK_CIRCULAR){mix_circular_sink(r,i);circular=1;}
|
||||
} else {m->sink_address[i]=0;m->sink_size[i]=m->sink_offset[i]=m->sink_old[i]=m->sink_written[i]=0;}
|
||||
float *lr=m->output_scratch;
|
||||
memset(lr, 0, sizeof(float) * 2 * (size_t)frames);
|
||||
const int oc = m->channels;
|
||||
float *out=m->output_scratch;
|
||||
memset(out, 0, sizeof(float) * (size_t)oc * (size_t)frames);
|
||||
if (sink && sink->input_count >= 1 && sink->input_count <= 6) {
|
||||
if (!(m->reported & (1u << 6))) {
|
||||
m->reported |= 1u << 6;
|
||||
rt_log("audio: device sink has %u inputs, output %s", sink->input_count, oc == 6 ? "5.1" : "stereo");
|
||||
}
|
||||
const float gain = host_volume() * atomic_load(&device_volume) * (r->mixes[0].in_use ? r->mixes[0].volume : 1.0f);
|
||||
const float *ch[6] = {0};
|
||||
float raw[2 * MAX_FRAME];
|
||||
for (uint32_t c = 0; c < sink->input_count; c++) if (sink->inputs[c] >= 0 && sink->inputs[c] < m->nbuf) ch[c] = mixbuf(m, sink->inputs[c]);
|
||||
for (int k = 0; k < frames; k++) {
|
||||
float l, rr;
|
||||
if (sink->input_count >= 6) { /* FL FR C LFE RL RR -> stereo */
|
||||
float s[OUT_MAX] = {0}; /* FL FR C LFE RL RR */
|
||||
if (sink->input_count >= 6 && oc == 6) {
|
||||
for (int c = 0; c < 6; c++) s[c] = ch[c] ? ch[c][k] : 0;
|
||||
} else if (sink->input_count >= 6) { /* 5.1 -> stereo; the LFE is left out, as the Switch's downmix does */
|
||||
float ff = sink->downmix_enabled ? sink->downmix[0] : 1.0f, cf = sink->downmix_enabled ? sink->downmix[1] : 0.7071f, sf = sink->downmix_enabled ? sink->downmix[2] : 0.7071f;
|
||||
float C = ch[2] ? ch[2][k] : 0;
|
||||
l = ff * (ch[0] ? ch[0][k] : 0) + cf * C + sf * (ch[4] ? ch[4][k] : 0);
|
||||
rr = ff * (ch[1] ? ch[1][k] : 0) + cf * C + sf * (ch[5] ? ch[5][k] : 0);
|
||||
} else if (sink->input_count >= 2) { l = ch[0] ? ch[0][k] : 0; rr = ch[1] ? ch[1][k] : 0; }
|
||||
else l = rr = ch[0] ? ch[0][k] : 0;
|
||||
l *= gain; rr *= gain;
|
||||
raw[2 * k] = l; raw[2 * k + 1] = rr;
|
||||
lr[2 * k] = l > 1 ? 1 : l < -1 ? -1 : l; lr[2 * k + 1] = rr > 1 ? 1 : rr < -1 ? -1 : rr;
|
||||
s[0] = ff * (ch[0] ? ch[0][k] : 0) + cf * C + sf * (ch[4] ? ch[4][k] : 0);
|
||||
s[1] = ff * (ch[1] ? ch[1][k] : 0) + cf * C + sf * (ch[5] ? ch[5][k] : 0);
|
||||
} else if (sink->input_count >= 2) { s[0] = ch[0] ? ch[0][k] : 0; s[1] = ch[1] ? ch[1][k] : 0; }
|
||||
else s[0] = s[1] = ch[0] ? ch[0][k] : 0;
|
||||
raw[2 * k] = s[0] * gain; raw[2 * k + 1] = s[1] * gain; /* TOTK_AUDIO_DUMP: the front pair before clipping */
|
||||
for (int c = 0; c < oc; c++) {
|
||||
float x = s[c] * gain;
|
||||
out[oc * k + c] = x > 1 ? 1 : x < -1 ? -1 : x;
|
||||
}
|
||||
}
|
||||
sink_dump(raw, frames);
|
||||
} else if (!sink && !circular) report_once(m, 5, "no device sink in use", r->param.sink_count);
|
||||
level_note(r, m, lr, frames);
|
||||
output_push(m, lr, frames);
|
||||
level_note(r, m, out, frames);
|
||||
output_push(m, out, frames);
|
||||
}
|
||||
|
||||
// ---- IAudioDevice: one stereo output device
|
||||
// ---- IAudioDevice: one output device, stereo or 5.1 (output_channels)
|
||||
static const char device_name[] = "AudioTvOutput";
|
||||
|
||||
static void put_name(const IpcBuffer *b, IpcResponse *rs, int pointer) {
|
||||
@@ -911,7 +953,7 @@ void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs)
|
||||
put_name(&b, rs, pointer);
|
||||
return;
|
||||
}
|
||||
case 5: { int32_t n = 2; IPC_OUT(rs, n); return; } // GetActiveChannelCount
|
||||
case 5: { int32_t n = output_channels(); IPC_OUT(rs, n); return; } // GetActiveChannelCount
|
||||
case 4: case 11: case 12: { // QueryAudioDeviceSystemEvent, ...InputEvent, ...OutputEvent
|
||||
uint32_t ev = __atomic_load_n(&change_event, __ATOMIC_ACQUIRE);
|
||||
if (!ev) {
|
||||
|
||||
+85
-27
@@ -7,6 +7,7 @@
|
||||
// cfg_get handed out stays valid) and cfg_save writes only the changed keys back, keeping the rest of the file.
|
||||
// Readers take no lock: an entry is complete before n_entries counts it, and value pointers are swapped atomically.
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -28,24 +29,29 @@ static PlatMutex set_lock = PLAT_MUTEX_INIT; // cfg_set, cfg_reset, cfg_save
|
||||
static int save_ok = 1; // the last cfg_save_to wrote (or had nothing to write)
|
||||
|
||||
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line, and gets a
|
||||
// description in totk.ini.example or docs/settings.md (tests/settings_doc_check.py checks both).
|
||||
static const char *const known[] = {
|
||||
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_OFF", "AUDIO_PREFILL_MS",
|
||||
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_OFF", "AUDIO_PREFILL_MS",
|
||||
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
||||
"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_SHAKE",
|
||||
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
|
||||
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
|
||||
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLSS", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES",
|
||||
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FRAME_QUEUE",
|
||||
"FREEZE_IDLE", "FREE_CORES",
|
||||
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
|
||||
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
|
||||
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_ASPECT", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
|
||||
"MV_SURVEY",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_BENCH", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
||||
@@ -55,38 +61,39 @@ static const char *const known[] = {
|
||||
"SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
};
|
||||
|
||||
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
|
||||
static const struct { const char *key, *section; } sections[] = {
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"},
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"FRAME_QUEUE", "frame rate"},
|
||||
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
||||
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
||||
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"MOVIE_ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"COLOR_PRESET", "display"},
|
||||
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
|
||||
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
|
||||
{"HDR_UI_NITS", "display"}, {"SHADOW_RES", "display"},
|
||||
{"DLAA", "display"}, {"DLSS", "display"}, {"FXAA", "display"},
|
||||
{"HDR_UI_NITS", "display"}, {"HDR_HUE", "display"}, {"SHADOW_RES", "display"},
|
||||
{"DLAA", "display"}, {"DLSS", "display"}, {"DLSS_PRESET", "display"}, {"DLSS_CHAR_MASK", "display"}, {"FXAA", "display"},
|
||||
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
|
||||
{"DISCORD_RPC", "discord"}, {"DISCORD_APP_ID", "discord"}, {"DISCORD_IMAGE", "discord"},
|
||||
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
|
||||
{"PCSS", "display"},
|
||||
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"},
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
||||
{"STICK_SENS", "input"},
|
||||
{"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"MOUSE_AIM_SCALE", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
|
||||
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
|
||||
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
|
||||
{"BIND_RIGHT", "input"}, {"BIND_DOWN", "input"}, {"BIND_LSTICK_LEFT", "input"}, {"BIND_LSTICK_UP", "input"},
|
||||
{"BIND_LSTICK_RIGHT", "input"}, {"BIND_LSTICK_DOWN", "input"}, {"BIND_RSTICK_LEFT", "input"},
|
||||
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"},
|
||||
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"}, {"BIND_SHAKE", "input"},
|
||||
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"FREE_CORES", "performance"},
|
||||
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
@@ -96,7 +103,8 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_MERGE", "diagnostics"}, {"WB_EXACT", "diagnostics"},
|
||||
{"MV_SURVEY", "diagnostics"},
|
||||
};
|
||||
|
||||
// The settings the menu shows: the value in effect when the key is unset, and what the key accepts. The only place a
|
||||
@@ -111,8 +119,10 @@ 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},
|
||||
{"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance", RESTART}, {"FXAA", "1", "0|1", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
||||
{"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance|ultra_performance", LIVE},
|
||||
{"DLSS_PRESET", "default", "default|k|j", LIVE},
|
||||
{"DLSS_CHAR_MASK", "65", "0|35|65|100", LIVE}, {"FXAA", "1", "0|1", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE}, {"MOVIE_ASPECT", "keep", "keep|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
||||
{"ANISO", "game", "game|2|4|8|16", RESTART}, // vk_texture.c sampler_features
|
||||
// colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses
|
||||
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
|
||||
@@ -120,14 +130,14 @@ static const CfgMeta metas[] = {
|
||||
{"COLOR_GAMMA", "1", "0.8..1.2", LIVE},
|
||||
// HDR output (vk_hdr.c): on at start (the tonemap shader is swapped as shaders load); the nits apply at once
|
||||
{"HDR", "0", "0|1", RESTART}, {"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "80..500", LIVE},
|
||||
{"HDR_UI_NITS", "203", "80..500", LIVE},
|
||||
{"HDR_UI_NITS", "203", "80..500", LIVE}, {"HDR_HUE", "80", "0..100", LIVE},
|
||||
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
|
||||
{"SHADOW_RES", "game", "game|256|512|768|1024|1536|2048", RESTART},
|
||||
{"SHADOW_SCALE", "1", "1..4", RESTART},
|
||||
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
|
||||
{"PCSS", "0", "0..200", RESTART},
|
||||
// Game
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE}, {"FRAME_QUEUE", "0", "0|1", LIVE},
|
||||
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
|
||||
{"MODS_REBUILD", "auto", "auto|off", RESTART},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART},
|
||||
@@ -135,26 +145,31 @@ static const CfgMeta metas[] = {
|
||||
{"OPERATION_MODE", "docked", "docked|handheld", RESTART},
|
||||
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
|
||||
// Sound, Controls, Menu
|
||||
{"VOLUME", "100", "0..100", LIVE},
|
||||
{"VOLUME", "100", "0..100", LIVE}, {"AUDIO_CHANNELS", "stereo", "auto|stereo|5.1", RESTART},
|
||||
{"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},
|
||||
{"PROMPTS", "nintendo", "nintendo|xbox", RESTART},
|
||||
{"PROMPTS", "nintendo", "nintendo|xbox|pc", RESTART},
|
||||
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", RESTART},
|
||||
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
|
||||
{"MOUSE_SENS", "1", "0..10", LIVE},
|
||||
{"MOUSE_MODE", "direct", "stick|direct", LIVE},
|
||||
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MOUSE_AIM_SCALE", "1", "0.1..3", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
|
||||
{"WALK_KEY", "none", NULL, LIVE}, {"WALK_SCALE", "0.51", "0.2..0.9", LIVE},
|
||||
{"MENU_LANG", "", "en|ru|pt-BR|zh-Hans", LIVE},
|
||||
{"AMIIBO_DIR", "", NULL, LIVE},
|
||||
// Discord Rich Presence (discord_rpc.c): off by default, the Menu tab of the settings menu switches it.
|
||||
{"DISCORD_RPC", "0", "0|1", LIVE},
|
||||
{"DISCORD_APP_ID", "1567232599846293504", NULL, LIVE},
|
||||
{"DISCORD_IMAGE", "totk_512x512", NULL, LIVE},
|
||||
// Advanced: scheduling
|
||||
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
|
||||
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
|
||||
// shaders and pipelines
|
||||
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "fullscreen", "strict|any|fullscreen", RESTART},
|
||||
{"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART},
|
||||
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "", "0|1", RESTART},
|
||||
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "1", "0|1", RESTART},
|
||||
// textures and memory
|
||||
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
|
||||
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "0", "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|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},
|
||||
@@ -166,7 +181,7 @@ static const CfgMeta metas[] = {
|
||||
{"VK_DYN_A2C", "", "0|1", RESTART}, {"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}, {"WB_EXACT", "1", "0|1", RESTART},
|
||||
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_MERGE", "1", "0|1", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
|
||||
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
|
||||
{"INPUT_RECORD", "", NULL, RESTART},
|
||||
};
|
||||
@@ -315,9 +330,51 @@ static const char *raw_get(const char *name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ---- the screen's shape (desktop): a primary display of another shape than 16:9 gives output_res a default of that
|
||||
// shape while neither the environment nor totk.ini sets it, so the first start fills the screen instead of a 16:9
|
||||
// picture between black bars. The sizes are the ones the installer lays the HUD out for (installer/steps.py
|
||||
// ASPECT_SIZES, ui/menu.cpp row_output_res): of the display's shape within 1%, the smallest at least 1080 high that fits
|
||||
// the display, else the largest that fits. A 16:9 display, an unknown one or one of another shape keeps 1920x1080.
|
||||
// Xbox or keyboard prompts keep 1920x1080 too: the setup builds their folders for 16:9 only (romfs.c overlay_init), so
|
||||
// a default of another shape would trade the prompts the player chose for the Switch ones.
|
||||
static const unsigned SHAPED[][2] = {
|
||||
{2560, 1080}, {3440, 1440}, {3840, 1600}, {5120, 2160}, // 21:9
|
||||
{3840, 1080}, {5120, 1440}, // 32:9
|
||||
{1280, 800}, {1680, 1050}, {1920, 1200}, {2560, 1600}, // 16:10
|
||||
{1920, 1280}, {2160, 1440}, {2256, 1504}, {2880, 1920}, {3000, 2000}, // 3:2
|
||||
};
|
||||
static char screen_res[16]; // set once by cfg_init, "" for none
|
||||
|
||||
static int switch_prompts(void) {
|
||||
const char *p = raw_get(PREFIX "PROMPTS"), *l = raw_get(PREFIX "PAD_LAYOUT");
|
||||
return (!p || !strcasecmp(p, "nintendo")) && !(l && !strcasecmp(l, "xbox_prompts"));
|
||||
}
|
||||
|
||||
static void screen_default(void) {
|
||||
int sw, sh;
|
||||
if (!plat_screen_size(&sw, &sh)) return;
|
||||
double r = (double)sw / sh;
|
||||
if (fabs(r / (16.0 / 9.0) - 1) <= 0.01) return;
|
||||
int best = -1, fit = -1;
|
||||
for (int i = 0; i < (int)(sizeof SHAPED / sizeof *SHAPED); i++) {
|
||||
unsigned w = SHAPED[i][0], h = SHAPED[i][1];
|
||||
if (fabs((double)w / h / r - 1) > 0.01 || w > (unsigned)sw || h > (unsigned)sh) continue;
|
||||
if (fit < 0 || w > SHAPED[fit][0]) fit = i; // the largest that fits
|
||||
if (h >= 1080 && (best < 0 || w < SHAPED[best][0])) best = i;
|
||||
}
|
||||
if (best < 0) best = fit;
|
||||
if (best < 0) return;
|
||||
snprintf(screen_res, sizeof screen_res, "%ux%u", SHAPED[best][0], SHAPED[best][1]);
|
||||
if (!raw_get(PREFIX "OUTPUT_RES") && switch_prompts())
|
||||
rt_log("cfg: the display is %dx%d: output_res defaults to %s, its shape (totk.ini output_res sets another)", sw,
|
||||
sh, screen_res);
|
||||
}
|
||||
|
||||
const char *cfg_get(const char *name) {
|
||||
const char *v = raw_get(name);
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN) || !profile_of()) return v;
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN)) return v;
|
||||
if (!profile_of())
|
||||
return screen_res[0] && !strcasecmp(name + PREFIX_LEN, "OUTPUT_RES") && switch_prompts() ? screen_res : NULL;
|
||||
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
|
||||
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
|
||||
return NULL;
|
||||
@@ -692,6 +749,7 @@ static void load_ini(void) {
|
||||
|
||||
void cfg_init(void) {
|
||||
load_ini();
|
||||
if (!profile_of()) screen_default();
|
||||
if (profile_of())
|
||||
rt_log("cfg: device profile Steam Deck: defaults for the keys totk.ini does not set (TOTK_DEVICE_PROFILE=desktop "
|
||||
"turns it off)");
|
||||
|
||||
@@ -234,6 +234,10 @@ int probe_cfg(void) {
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_ZR | 1ull << B_PLUS | 1ull << B_MINUS | 1ull << B_STICK_L | 1ull << B_RIGHT),
|
||||
"keys: X ZR, Tab Plus, M Minus, Left Ctrl stick click, 3 D-pad right");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_G] = 1;
|
||||
CHECK(input_map_buttons(dn) == 0 && input_map_held(dn, B_SHAKE) && !input_map_held(dn, B_A),
|
||||
"keys: G shakes and presses no button");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_W] = 1;
|
||||
input_map_stick(dn, 0, st);
|
||||
CHECK(st[0] == 0 && st[1] == 32767, "keys: W is left stick up");
|
||||
@@ -301,6 +305,11 @@ int probe_cfg(void) {
|
||||
CHECK(input_prompts_pick(NULL, "auto") == PROMPTS_NINTENDO && input_prompts_pick("", "xbox") == PROMPTS_NINTENDO &&
|
||||
input_prompts_pick("Xbox", "nintendo") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox") == PROMPTS_NINTENDO,
|
||||
"prompts: the game's own unless set to Xbox");
|
||||
CHECK(input_prompts_pick("PC", "nintendo") == PROMPTS_PC && input_prompts_pick("pc", "xbox_prompts") == PROMPTS_PC &&
|
||||
EQ(input_prompts_folder(PROMPTS_PC, "nintendo"), "romfs-patches-pc") &&
|
||||
EQ(input_prompts_folder(PROMPTS_PC, "xbox"), "romfs-patches-pc") &&
|
||||
EQ(input_prompts_folder(PROMPTS_PC, NULL), "romfs-patches-pc"),
|
||||
"prompts: pc shows the keyboard and mouse whatever the pad layout");
|
||||
CHECK(input_prompts_pick(NULL, "xbox_prompts") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox_prompts") == PROMPTS_NINTENDO,
|
||||
"prompts: 0.1.9's pad_layout = xbox_prompts means Xbox while prompts is unset");
|
||||
CHECK(!input_prompts_folder(PROMPTS_NINTENDO, "nintendo") && !input_prompts_folder(PROMPTS_NINTENDO, "xbox"),
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// Discord Rich Presence: "Playing TotK Recomp" with the elapsed play time in the player's Discord status, through
|
||||
// the local IPC of a running Discord client (platform.h's plat_ipc_*). Nothing leaves the machine but the activity
|
||||
// itself, and nothing is sent while TOTK_DISCORD_RPC is off (the default: the status is information about the
|
||||
// player's session). TOTK_DISCORD_APP_ID names the application whose name and artwork Discord shows — the default
|
||||
// is the project's own application, registered as "TotK Recomp"; a player who wants their own name or
|
||||
// art registers an application at discord.com/developers and puts its id in totk.ini — and TOTK_DISCORD_IMAGE the
|
||||
// key of its uploaded art asset.
|
||||
//
|
||||
// One host thread owns the connection and never holds a virtual core (kernel.c: host work runs without one). It
|
||||
// re-reads the settings when cfg_version changes (a menu change reaches it within half a second), connects to the
|
||||
// first of Discord's discord-ipc-0..9 pipes that answers, and writes the activity again every 60 s: that write is
|
||||
// the keep-alive, and one that fails closes the connection and tries again 15 s later. Discord clears the status by
|
||||
// itself when the process ends and the pipe closes, so there is no shutdown path. Every answer frame is read and
|
||||
// discarded: unread, they would fill the pipe's buffer and block the client's writes.
|
||||
//
|
||||
// The protocol is the documented IPC one (discord-rpc's src/rpc_connection.h): a frame is <opcode u32 little
|
||||
// endian><length u32><length bytes of JSON>, with Handshake 0, Frame 1, Close 2, Ping 3 and Pong 4. The handshake
|
||||
// (opcode 0) carries {"v":1,"client_id":...} and is answered by a Ready frame (opcode 1); every command,
|
||||
// SET_ACTIVITY here, is a frame (opcode 1) answered by one, and a Ping the client sends is echoed as a Pong. A
|
||||
// client that speaks another numbering (an early arRPC did) answers the handshake with a Close. --probe-discord
|
||||
// runs the same exchange once, without the game and without the setting being on.
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
enum { OP_HANDSHAKE = 0, OP_FRAME = 1, OP_CLOSE = 2, OP_PING = 3, OP_PONG = 4 };
|
||||
|
||||
#define APP_ID_CAP 24 // a Discord snowflake: at most 20 digits
|
||||
#define IMAGE_KEY_CAP 64
|
||||
#define ANSWER_CAP 8192 // the Ready frame carries the client's config; a few KiB
|
||||
|
||||
// A settings value that fits the JSON below without escaping: the app id is digits, an asset key letters, digits,
|
||||
// '_' and '-' (what Discord's developer portal accepts for an asset key).
|
||||
static int valid_app_id(const char *s) {
|
||||
size_t n = strlen(s);
|
||||
if (!n || n > 20) return 0;
|
||||
for (const char *p = s; *p; p++)
|
||||
if (*p < '0' || *p > '9') return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int valid_image_key(const char *s) {
|
||||
size_t n = strlen(s);
|
||||
if (!n || n >= IMAGE_KEY_CAP) return 0;
|
||||
for (const char *p = s; *p; p++)
|
||||
if ((*p < 'a' || *p > 'z') && (*p < 'A' || *p > 'Z') && (*p < '0' || *p > '9') && *p != '_' && *p != '-')
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The setting's value, or its default when the value does not fit the protocol (logged: the player sees why their
|
||||
// own app does not show). Called on the presence thread and by the probe, after cfg_version changed or once.
|
||||
static void read_setting(const char *name, char *out, size_t cap, int (*valid)(const char *)) {
|
||||
const char *v = cfg_value(name);
|
||||
if (valid(v)) {
|
||||
snprintf(out, cap, "%s", v);
|
||||
return;
|
||||
}
|
||||
snprintf(out, cap, "%s", cfg_meta(name)->def);
|
||||
rt_log("discord: %s does not fit what the Rich Presence protocol takes; using %s", name + strlen("TOTK_"), out);
|
||||
}
|
||||
|
||||
static void le32(uint8_t *at, uint32_t v) {
|
||||
at[0] = (uint8_t)v, at[1] = (uint8_t)(v >> 8), at[2] = (uint8_t)(v >> 16), at[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
static int write_frame(PlatIpc *p, uint32_t op, const char *json) {
|
||||
uint8_t head[8];
|
||||
le32(head, op);
|
||||
le32(head + 4, (uint32_t)strlen(json));
|
||||
return plat_ipc_write(p, head, sizeof head) && plat_ipc_write(p, json, strlen(json));
|
||||
}
|
||||
|
||||
// One frame off the wire, its 8-byte header in as many reads as the peer made, then its payload; a payload longer
|
||||
// than cap-1 fills buf with its first cap-1 bytes and the rest is still consumed. *op gets the opcode; buf is
|
||||
// NUL-terminated. 1 on a whole frame, 0 when the deadline passed, -1 when the connection broke.
|
||||
static int read_one_frame(PlatIpc *p, uint32_t *op, char *buf, size_t cap, uint64_t deadline) {
|
||||
uint8_t head[8];
|
||||
size_t got = 0;
|
||||
while (got < sizeof head) {
|
||||
uint32_t left = deadline > plat_now_ms() ? (uint32_t)(deadline - plat_now_ms()) : 0;
|
||||
int n = plat_ipc_read(p, head + got, sizeof head - got, left);
|
||||
if (n < 0) return -1;
|
||||
if (!n) {
|
||||
if (plat_now_ms() >= deadline) return 0;
|
||||
continue;
|
||||
}
|
||||
got += (size_t)n;
|
||||
}
|
||||
*op = head[0] | head[1] << 8 | head[2] << 16 | (uint32_t)head[3] << 24;
|
||||
uint32_t len = head[4] | head[5] << 8 | head[6] << 16 | (uint32_t)head[7] << 24;
|
||||
size_t taken = 0;
|
||||
while (taken < len) {
|
||||
size_t want = len - taken > cap - 1 ? cap - 1 : len - taken;
|
||||
uint32_t left = deadline > plat_now_ms() ? (uint32_t)(deadline - plat_now_ms()) : 0;
|
||||
int n = plat_ipc_read(p, buf, want, left);
|
||||
if (n < 0) return -1;
|
||||
if (!n) {
|
||||
if (plat_now_ms() >= deadline) return 0;
|
||||
continue;
|
||||
}
|
||||
taken += (size_t)n;
|
||||
}
|
||||
buf[taken < cap - 1 ? taken : cap - 1] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The next frame that is not a Ping: a Ping the client sent to see we are alive is echoed as a Pong with its own
|
||||
// payload, a Close (the client told us it is done) counts as a broken connection so the caller connects again
|
||||
// later. Same return values as read_one_frame; the deadline covers the whole exchange.
|
||||
static int read_frame(PlatIpc *p, uint32_t *op, char *buf, size_t cap, uint32_t ms) {
|
||||
uint64_t deadline = plat_now_ms() + ms;
|
||||
for (;;) {
|
||||
int r = read_one_frame(p, op, buf, cap, deadline);
|
||||
if (r != 1) return r;
|
||||
if (*op == OP_PING) {
|
||||
uint8_t head[8];
|
||||
size_t n = strlen(buf);
|
||||
le32(head, OP_PONG);
|
||||
le32(head + 4, (uint32_t)n);
|
||||
if (!plat_ipc_write(p, head, sizeof head) || !plat_ipc_write(p, buf, n)) return -1;
|
||||
continue;
|
||||
}
|
||||
if (*op == OP_CLOSE) return -1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The first of Discord's pipes that completes a handshake. NULL when no client is running or none answered in time;
|
||||
// the caller tries again later.
|
||||
static PlatIpc *connect_client(const char *app_id) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
char name[32], json[160];
|
||||
snprintf(name, sizeof name, "discord-ipc-%d", i);
|
||||
PlatIpc *p = plat_ipc_connect(name);
|
||||
if (!p) continue;
|
||||
snprintf(json, sizeof json, "{\"v\":1,\"client_id\":\"%s\"}", app_id);
|
||||
if (!write_frame(p, OP_HANDSHAKE, json)) {
|
||||
plat_ipc_close(p);
|
||||
continue;
|
||||
}
|
||||
uint32_t op = 0;
|
||||
char answer[ANSWER_CAP];
|
||||
if (read_frame(p, &op, answer, sizeof answer, 2000) == 1 && op == OP_FRAME) return p; // the Ready event
|
||||
plat_ipc_close(p); // this pipe answered but not with a Ready frame in 2 s: the next may belong to a client
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static uint32_t next_nonce(void) {
|
||||
static volatile int32_t n;
|
||||
return (uint32_t)plat_inc32(&n);
|
||||
}
|
||||
|
||||
// The activity as one SET_ACTIVITY frame: the game update and the port version as the details line ("Game 1.4.3 ·
|
||||
// Recomp 0.3.7": the port version without its +commit suffix, the art's hover text keeps the full one; the dot goes
|
||||
// as a JSON escape), the process's start as the timestamp Discord counts the elapsed time from, and the application's
|
||||
// art asset. 1 when the client answered (a slow answer that ran out of the 2 s read is fine: the next drain takes
|
||||
// it), 0 when the connection broke.
|
||||
static int send_activity(PlatIpc *p, const char *image) {
|
||||
char json[512];
|
||||
int v = rt_game_version;
|
||||
uint64_t start = (plat_unix_time_ns() - plat_process_uptime_ns()) / 1000000000ull;
|
||||
int port_len = (int)strcspn(rt_port_version, "+");
|
||||
snprintf(json, sizeof json,
|
||||
"{\"cmd\":\"SET_ACTIVITY\",\"nonce\":\"%u\",\"args\":{\"pid\":%lu,\"activity\":{\"details\":\"Game "
|
||||
"%d.%d.%d \\u00b7 Recomp %.*s\",\"timestamps\":{\"start\":%llu},\"assets\":{\"large_image\":\"%s\","
|
||||
"\"large_text\":\"TotK Recomp %s\"}}}}",
|
||||
next_nonce(), (unsigned long)plat_process_id(), v / 100, v / 10 % 10, v % 10, port_len, rt_port_version,
|
||||
(unsigned long long)start, image, rt_port_version);
|
||||
if (!write_frame(p, OP_FRAME, json)) return 0;
|
||||
uint32_t op = 0;
|
||||
char answer[ANSWER_CAP];
|
||||
return read_frame(p, &op, answer, sizeof answer, 2000) >= 0;
|
||||
}
|
||||
|
||||
// ---- the presence thread
|
||||
|
||||
static void discord_main(void *arg) {
|
||||
(void)arg;
|
||||
unsigned seen = 0;
|
||||
char app_id[APP_ID_CAP], image[IMAGE_KEY_CAP];
|
||||
read_setting("TOTK_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
|
||||
read_setting("TOTK_DISCORD_IMAGE", image, sizeof image, valid_image_key);
|
||||
PlatIpc *ipc = NULL;
|
||||
uint64_t retry_at = 0, send_at = 0;
|
||||
int said_no_client = 0;
|
||||
for (;;) {
|
||||
unsigned v = cfg_version();
|
||||
if (v != seen) { // the settings changed (the menu writes them): pick the new ones up at once
|
||||
seen = v;
|
||||
read_setting("TOTK_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
|
||||
read_setting("TOTK_DISCORD_IMAGE", image, sizeof image, valid_image_key);
|
||||
if (ipc) plat_ipc_close(ipc), ipc = NULL; // the new app id needs a new handshake; the image is resent
|
||||
said_no_client = 0;
|
||||
}
|
||||
if (!cfg_bool("TOTK_DISCORD_RPC")) {
|
||||
if (ipc) plat_ipc_close(ipc), ipc = NULL;
|
||||
plat_sleep_ms(500);
|
||||
continue;
|
||||
}
|
||||
if (!ipc && plat_now_ms() >= retry_at) {
|
||||
ipc = connect_client(app_id);
|
||||
if (!ipc) {
|
||||
retry_at = plat_now_ms() + 15000;
|
||||
if (!said_no_client)
|
||||
rt_log("discord: no client is running; trying again every 15 s while the setting is on");
|
||||
said_no_client = 1;
|
||||
} else {
|
||||
rt_log("discord: connected (app %s)", app_id);
|
||||
send_at = 0;
|
||||
said_no_client = 0;
|
||||
}
|
||||
}
|
||||
if (ipc && plat_now_ms() >= send_at) {
|
||||
if (send_activity(ipc, image)) {
|
||||
send_at = plat_now_ms() + 60000;
|
||||
} else {
|
||||
rt_log("discord: the connection broke; reconnecting");
|
||||
plat_ipc_close(ipc);
|
||||
ipc = NULL;
|
||||
retry_at = plat_now_ms() + 15000;
|
||||
}
|
||||
}
|
||||
plat_sleep_ms(500);
|
||||
}
|
||||
}
|
||||
|
||||
void discord_init(void) {
|
||||
if (!plat_thread_spawn(discord_main, NULL, "discord", 0)) rt_log("discord: cannot start the presence thread");
|
||||
}
|
||||
|
||||
// ---- --probe-discord: the exchange above without the game and the setting; needs no game files
|
||||
|
||||
int probe_discord(void) {
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL discord: %s\n", what); } } while (0)
|
||||
char app_id[APP_ID_CAP], image[IMAGE_KEY_CAP];
|
||||
read_setting("TOTK_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
|
||||
read_setting("TOTK_DISCORD_IMAGE", image, sizeof image, valid_image_key);
|
||||
PlatIpc *p = connect_client(app_id);
|
||||
if (!p) {
|
||||
fprintf(stderr, "discord: probe skipped: no client on discord-ipc-0..9\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, "discord: connected (app %s)\n", app_id);
|
||||
CHECK(send_activity(p, image), "SET_ACTIVITY written and answered");
|
||||
fprintf(stderr, "discord: activity set; the status shows it for ten seconds, then the connection closes\n");
|
||||
plat_sleep_ms(10000);
|
||||
plat_ipc_close(p);
|
||||
fprintf(stderr, "discord: probe %s\n", fails ? "FAILED" : "passed");
|
||||
return fails != 0;
|
||||
#undef CHECK
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Generated by tools/make_dlaa_mask.py: DLSS's reactive mask pass (vk_dlaa.c mask_pass). Do not edit.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t dlaa_mask_spv[] = {
|
||||
0x07230203, 0x00010000, 0x00000000, 0x00000055, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000002, 0x6e69616d, 0x00000000, 0x0000001b,
|
||||
0x00060010, 0x00000002, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000001b,
|
||||
0x0000000b, 0x0000001c, 0x00040047, 0x00000018, 0x00000022, 0x00000000, 0x00040047, 0x00000018,
|
||||
0x00000021, 0x00000000, 0x00040047, 0x00000019, 0x00000022, 0x00000000, 0x00040047, 0x00000019,
|
||||
0x00000021, 0x00000001, 0x00040047, 0x0000001a, 0x00000022, 0x00000000, 0x00040047, 0x0000001a,
|
||||
0x00000021, 0x00000002, 0x00030047, 0x0000001a, 0x00000019, 0x00030047, 0x00000015, 0x00000002,
|
||||
0x00050048, 0x00000015, 0x00000000, 0x00000023, 0x00000000, 0x00020013, 0x00000003, 0x00030021,
|
||||
0x00000004, 0x00000003, 0x00020014, 0x00000005, 0x00030016, 0x00000006, 0x00000020, 0x00040015,
|
||||
0x00000007, 0x00000020, 0x00000001, 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x00040017,
|
||||
0x00000009, 0x00000006, 0x00000002, 0x00040017, 0x0000000a, 0x00000006, 0x00000003, 0x00040017,
|
||||
0x0000000b, 0x00000006, 0x00000004, 0x00040017, 0x0000000c, 0x00000007, 0x00000002, 0x00040017,
|
||||
0x0000000d, 0x00000008, 0x00000002, 0x00040017, 0x0000000e, 0x00000008, 0x00000003, 0x00090019,
|
||||
0x0000000f, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000,
|
||||
0x0003001b, 0x00000010, 0x0000000f, 0x00090019, 0x00000011, 0x00000006, 0x00000001, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000002, 0x0000000f, 0x00040020, 0x00000012, 0x00000000, 0x00000010,
|
||||
0x00040020, 0x00000013, 0x00000000, 0x00000011, 0x00040020, 0x00000014, 0x00000001, 0x0000000e,
|
||||
0x0003001e, 0x00000015, 0x0000000b, 0x00040020, 0x00000016, 0x00000009, 0x00000015, 0x00040020,
|
||||
0x00000017, 0x00000009, 0x0000000b, 0x0004002b, 0x00000007, 0x0000001d, 0x00000000, 0x0004002b,
|
||||
0x00000006, 0x0000001e, 0x00000000, 0x0004002b, 0x00000006, 0x0000001f, 0x3f800000, 0x0004002b,
|
||||
0x00000006, 0x00000020, 0x38d1b717, 0x0004003b, 0x00000012, 0x00000018, 0x00000000, 0x0004003b,
|
||||
0x00000012, 0x00000019, 0x00000000, 0x0004003b, 0x00000013, 0x0000001a, 0x00000000, 0x0004003b,
|
||||
0x00000014, 0x0000001b, 0x00000001, 0x0004003b, 0x00000016, 0x0000001c, 0x00000009, 0x00050036,
|
||||
0x00000003, 0x00000002, 0x00000000, 0x00000004, 0x000200f8, 0x00000021, 0x00050041, 0x00000017,
|
||||
0x00000024, 0x0000001c, 0x0000001d, 0x0004003d, 0x0000000b, 0x00000025, 0x00000024, 0x0004003d,
|
||||
0x0000000e, 0x00000026, 0x0000001b, 0x0007004f, 0x0000000d, 0x00000027, 0x00000026, 0x00000026,
|
||||
0x00000000, 0x00000001, 0x00050051, 0x00000008, 0x00000028, 0x00000026, 0x00000000, 0x00040070,
|
||||
0x00000006, 0x00000029, 0x00000028, 0x00050051, 0x00000008, 0x0000002a, 0x00000026, 0x00000001,
|
||||
0x00040070, 0x00000006, 0x0000002b, 0x0000002a, 0x00050051, 0x00000006, 0x0000002c, 0x00000025,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x0000002d, 0x00000025, 0x00000001, 0x000500b8, 0x00000005,
|
||||
0x0000002e, 0x00000029, 0x0000002c, 0x000500b8, 0x00000005, 0x0000002f, 0x0000002b, 0x0000002d,
|
||||
0x000500a7, 0x00000005, 0x00000030, 0x0000002e, 0x0000002f, 0x000300f7, 0x00000023, 0x00000000,
|
||||
0x000400fa, 0x00000030, 0x00000022, 0x00000023, 0x000200f8, 0x00000022, 0x0004007c, 0x0000000c,
|
||||
0x00000031, 0x00000027, 0x0004003d, 0x00000010, 0x00000032, 0x00000018, 0x00040064, 0x0000000f,
|
||||
0x00000033, 0x00000032, 0x0007005f, 0x0000000b, 0x00000034, 0x00000033, 0x00000031, 0x00000002,
|
||||
0x0000001d, 0x0008004f, 0x0000000a, 0x00000035, 0x00000034, 0x00000034, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x0004003d, 0x00000010, 0x00000036, 0x00000019, 0x00040064, 0x0000000f, 0x00000037,
|
||||
0x00000036, 0x0007005f, 0x0000000b, 0x00000038, 0x00000037, 0x00000031, 0x00000002, 0x0000001d,
|
||||
0x0008004f, 0x0000000a, 0x00000039, 0x00000038, 0x00000038, 0x00000000, 0x00000001, 0x00000002,
|
||||
0x00050083, 0x0000000a, 0x0000003a, 0x00000039, 0x00000035, 0x0006000c, 0x0000000a, 0x0000003b,
|
||||
0x00000001, 0x00000004, 0x0000003a, 0x00050051, 0x00000006, 0x0000003c, 0x0000003b, 0x00000000,
|
||||
0x00050051, 0x00000006, 0x0000003d, 0x0000003b, 0x00000001, 0x00050051, 0x00000006, 0x0000003e,
|
||||
0x0000003b, 0x00000002, 0x0007000c, 0x00000006, 0x0000003f, 0x00000001, 0x00000028, 0x0000003c,
|
||||
0x0000003d, 0x0007000c, 0x00000006, 0x00000040, 0x00000001, 0x00000028, 0x0000003f, 0x0000003e,
|
||||
0x00050051, 0x00000006, 0x00000041, 0x00000039, 0x00000000, 0x00050051, 0x00000006, 0x00000042,
|
||||
0x00000039, 0x00000001, 0x00050051, 0x00000006, 0x00000043, 0x00000039, 0x00000002, 0x0007000c,
|
||||
0x00000006, 0x00000044, 0x00000001, 0x00000028, 0x00000041, 0x00000042, 0x0007000c, 0x00000006,
|
||||
0x00000045, 0x00000001, 0x00000028, 0x00000044, 0x00000043, 0x00050051, 0x00000006, 0x00000046,
|
||||
0x00000035, 0x00000000, 0x00050051, 0x00000006, 0x00000047, 0x00000035, 0x00000001, 0x00050051,
|
||||
0x00000006, 0x00000048, 0x00000035, 0x00000002, 0x0007000c, 0x00000006, 0x00000049, 0x00000001,
|
||||
0x00000028, 0x00000046, 0x00000047, 0x0007000c, 0x00000006, 0x0000004a, 0x00000001, 0x00000028,
|
||||
0x00000049, 0x00000048, 0x0007000c, 0x00000006, 0x0000004b, 0x00000001, 0x00000028, 0x00000045,
|
||||
0x0000004a, 0x0007000c, 0x00000006, 0x0000004c, 0x00000001, 0x00000028, 0x0000004b, 0x00000020,
|
||||
0x00050088, 0x00000006, 0x0000004d, 0x00000040, 0x0000004c, 0x00050051, 0x00000006, 0x0000004e,
|
||||
0x00000025, 0x00000002, 0x00050051, 0x00000006, 0x0000004f, 0x00000025, 0x00000003, 0x00050083,
|
||||
0x00000006, 0x00000050, 0x0000004d, 0x0000004e, 0x00050085, 0x00000006, 0x00000051, 0x00000050,
|
||||
0x0000004f, 0x0008000c, 0x00000006, 0x00000052, 0x00000001, 0x0000002b, 0x00000051, 0x0000001e,
|
||||
0x0000001f, 0x00070050, 0x0000000b, 0x00000053, 0x00000052, 0x00000052, 0x00000052, 0x00000052,
|
||||
0x0004003d, 0x00000011, 0x00000054, 0x0000001a, 0x00040063, 0x00000054, 0x00000031, 0x00000053,
|
||||
0x000200f9, 0x00000023, 0x000200f8, 0x00000023, 0x000100fd, 0x00010038
|
||||
};
|
||||
+156
-113
@@ -1,119 +1,162 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/dlaa_mv.comp; do not edit.
|
||||
// source sha256 bfa9b6d9e0f9d5cfd4382fa5080fa46f007a1a4e2b886b8fecd9adba11767686
|
||||
// source sha256 e96a6d2d0cec26c47a76280363077c5982919bedc984f20ae9f1d185dc6ae674
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t dlaa_mv_spv[] = {
|
||||
0x07230203, 0x00010600, 0x0008000b, 0x00000131, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x0000019a, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x000b000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000036,
|
||||
0x00000043, 0x0000005d, 0x00000089, 0x000000a0, 0x000000e7, 0x00060010, 0x00000004, 0x00000011,
|
||||
0x00000008, 0x00000008, 0x00000001, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004,
|
||||
0x6e69616d, 0x00000000, 0x00080005, 0x00000036, 0x475f6c67, 0x61626f6c, 0x766e496c, 0x7461636f,
|
||||
0x496e6f69, 0x00000044, 0x00030005, 0x00000041, 0x00000050, 0x00050006, 0x00000041, 0x00000000,
|
||||
0x657a6973, 0x00000000, 0x00050006, 0x00000041, 0x00000001, 0x7474696a, 0x00007265, 0x00030005,
|
||||
0x00000043, 0x00006370, 0x00050005, 0x0000005d, 0x74706564, 0x65745f68, 0x00000078, 0x00030005,
|
||||
0x00000087, 0x00727543, 0x00040006, 0x00000087, 0x00000000, 0x00000072, 0x00030005, 0x00000089,
|
||||
0x00727563, 0x00040005, 0x0000009e, 0x76657250, 0x00000000, 0x00040006, 0x0000009e, 0x00000000,
|
||||
0x00000072, 0x00040005, 0x000000a0, 0x76657270, 0x00000000, 0x00040005, 0x000000e7, 0x6f5f766d,
|
||||
0x00007475, 0x00040047, 0x00000036, 0x0000000b, 0x0000001c, 0x00030047, 0x00000041, 0x00000002,
|
||||
0x00050048, 0x00000041, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000041, 0x00000001,
|
||||
0x00000023, 0x00000008, 0x00040047, 0x0000005d, 0x00000021, 0x00000000, 0x00040047, 0x0000005d,
|
||||
0x00000022, 0x00000000, 0x00040047, 0x00000086, 0x00000006, 0x00000010, 0x00030047, 0x00000087,
|
||||
0x00000002, 0x00050048, 0x00000087, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x00000089,
|
||||
0x00000021, 0x00000002, 0x00040047, 0x00000089, 0x00000022, 0x00000000, 0x00040047, 0x0000009d,
|
||||
0x00000006, 0x00000010, 0x00030047, 0x0000009e, 0x00000002, 0x00050048, 0x0000009e, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00040047, 0x000000a0, 0x00000021, 0x00000003, 0x00040047, 0x000000a0,
|
||||
0x00000022, 0x00000000, 0x00030047, 0x000000e7, 0x00000019, 0x00040047, 0x000000e7, 0x00000021,
|
||||
0x00000001, 0x00040047, 0x000000e7, 0x00000022, 0x00000000, 0x00020013, 0x00000002, 0x00030021,
|
||||
0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006,
|
||||
0x00000004, 0x00040018, 0x00000009, 0x00000007, 0x00000004, 0x0004002b, 0x00000006, 0x00000015,
|
||||
0x3f800000, 0x0004002b, 0x00000006, 0x00000016, 0x00000000, 0x00040015, 0x0000002f, 0x00000020,
|
||||
0x00000001, 0x00040017, 0x00000030, 0x0000002f, 0x00000002, 0x00040015, 0x00000033, 0x00000020,
|
||||
0x00000000, 0x00040017, 0x00000034, 0x00000033, 0x00000003, 0x00040020, 0x00000035, 0x00000001,
|
||||
0x00000034, 0x0004003b, 0x00000035, 0x00000036, 0x00000001, 0x00040017, 0x00000037, 0x00000033,
|
||||
0x00000002, 0x00020014, 0x0000003b, 0x0004002b, 0x00000033, 0x0000003c, 0x00000000, 0x00040017,
|
||||
0x00000040, 0x00000006, 0x00000002, 0x0004001e, 0x00000041, 0x00000040, 0x00000040, 0x00040020,
|
||||
0x00000042, 0x00000009, 0x00000041, 0x0004003b, 0x00000042, 0x00000043, 0x00000009, 0x0004002b,
|
||||
0x0000002f, 0x00000044, 0x00000000, 0x00040020, 0x00000045, 0x00000009, 0x00000006, 0x0004002b,
|
||||
0x00000033, 0x0000004d, 0x00000001, 0x00090019, 0x0000005a, 0x00000006, 0x00000001, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000005b, 0x0000005a, 0x00040020,
|
||||
0x0000005c, 0x00000000, 0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d, 0x00000000, 0x0004002b,
|
||||
0x00000006, 0x00000067, 0x3f000000, 0x0004002b, 0x0000002f, 0x0000006a, 0x00000001, 0x00040020,
|
||||
0x0000006b, 0x00000009, 0x00000040, 0x0004002b, 0x00000006, 0x00000075, 0x40000000, 0x0004002b,
|
||||
0x00000033, 0x00000085, 0x00000010, 0x0004001c, 0x00000086, 0x00000007, 0x00000085, 0x0003001e,
|
||||
0x00000087, 0x00000086, 0x00040020, 0x00000088, 0x00000002, 0x00000087, 0x0004003b, 0x00000088,
|
||||
0x00000089, 0x00000002, 0x0004002b, 0x0000002f, 0x0000008a, 0x00000003, 0x0004002b, 0x0000002f,
|
||||
0x0000008b, 0x00000004, 0x0004002b, 0x0000002f, 0x0000008c, 0x00000005, 0x0004002b, 0x0000002f,
|
||||
0x0000008d, 0x00000006, 0x00040020, 0x0000008f, 0x00000002, 0x00000007, 0x0004001c, 0x0000009d,
|
||||
0x00000007, 0x00000085, 0x0003001e, 0x0000009e, 0x0000009d, 0x00040020, 0x0000009f, 0x00000002,
|
||||
0x0000009e, 0x0004003b, 0x0000009f, 0x000000a0, 0x00000002, 0x0005002c, 0x00000040, 0x000000b4,
|
||||
0x00000016, 0x00000016, 0x0004002b, 0x00000006, 0x000000b9, 0x2b8cbccc, 0x0004002b, 0x00000006,
|
||||
0x000000c7, 0x358637bd, 0x00090019, 0x000000e5, 0x00000006, 0x00000001, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000002, 0x00000007, 0x00040020, 0x000000e6, 0x00000000, 0x000000e5, 0x0004003b,
|
||||
0x000000e6, 0x000000e7, 0x00000000, 0x0005002c, 0x00000040, 0x00000130, 0x00000067, 0x00000067,
|
||||
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x000300f7,
|
||||
0x000000f0, 0x00000000, 0x000300fb, 0x0000003c, 0x000000f1, 0x000200f8, 0x000000f1, 0x0004003d,
|
||||
0x00000034, 0x00000038, 0x00000036, 0x0007004f, 0x00000037, 0x00000039, 0x00000038, 0x00000038,
|
||||
0x00000000, 0x00000001, 0x0004007c, 0x00000030, 0x0000003a, 0x00000039, 0x00050051, 0x0000002f,
|
||||
0x0000003f, 0x0000003a, 0x00000000, 0x00060041, 0x00000045, 0x00000046, 0x00000043, 0x00000044,
|
||||
0x0000003c, 0x0004003d, 0x00000006, 0x00000047, 0x00000046, 0x0004006e, 0x0000002f, 0x00000048,
|
||||
0x00000047, 0x000500af, 0x0000003b, 0x00000049, 0x0000003f, 0x00000048, 0x000400a8, 0x0000003b,
|
||||
0x0000004a, 0x00000049, 0x000300f7, 0x0000004c, 0x00000000, 0x000400fa, 0x0000004a, 0x0000004b,
|
||||
0x0000004c, 0x000200f8, 0x0000004b, 0x00050051, 0x0000002f, 0x0000004f, 0x0000003a, 0x00000001,
|
||||
0x00060041, 0x00000045, 0x00000050, 0x00000043, 0x00000044, 0x0000004d, 0x0004003d, 0x00000006,
|
||||
0x00000051, 0x00000050, 0x0004006e, 0x0000002f, 0x00000052, 0x00000051, 0x000500af, 0x0000003b,
|
||||
0x00000053, 0x0000004f, 0x00000052, 0x000200f9, 0x0000004c, 0x000200f8, 0x0000004c, 0x000700f5,
|
||||
0x0000003b, 0x00000054, 0x00000049, 0x000000f1, 0x00000053, 0x0000004b, 0x000300f7, 0x00000056,
|
||||
0x00000000, 0x000400fa, 0x00000054, 0x00000055, 0x00000056, 0x000200f8, 0x00000055, 0x000200f9,
|
||||
0x000000f0, 0x000200f8, 0x00000056, 0x0004003d, 0x0000005b, 0x0000005e, 0x0000005d, 0x00040064,
|
||||
0x0000005a, 0x00000060, 0x0000005e, 0x0007005f, 0x00000007, 0x00000061, 0x00000060, 0x0000003a,
|
||||
0x00000002, 0x00000044, 0x00050051, 0x00000006, 0x00000062, 0x00000061, 0x00000000, 0x0004006f,
|
||||
0x00000040, 0x00000066, 0x0000003a, 0x00050081, 0x00000040, 0x00000069, 0x00000066, 0x00000130,
|
||||
0x00050041, 0x0000006b, 0x0000006c, 0x00000043, 0x0000006a, 0x0004003d, 0x00000040, 0x0000006d,
|
||||
0x0000006c, 0x00050083, 0x00000040, 0x0000006e, 0x00000069, 0x0000006d, 0x00050051, 0x00000006,
|
||||
0x00000071, 0x0000006e, 0x00000000, 0x00050088, 0x00000006, 0x00000074, 0x00000071, 0x00000047,
|
||||
0x00050085, 0x00000006, 0x00000076, 0x00000074, 0x00000075, 0x00050083, 0x00000006, 0x00000077,
|
||||
0x00000076, 0x00000015, 0x00050051, 0x00000006, 0x00000079, 0x0000006e, 0x00000001, 0x00060041,
|
||||
0x00000045, 0x0000007a, 0x00000043, 0x00000044, 0x0000004d, 0x0004003d, 0x00000006, 0x0000007b,
|
||||
0x0000007a, 0x00050088, 0x00000006, 0x0000007c, 0x00000079, 0x0000007b, 0x00050085, 0x00000006,
|
||||
0x0000007d, 0x0000007c, 0x00000075, 0x00050083, 0x00000006, 0x0000007e, 0x00000015, 0x0000007d,
|
||||
0x00050085, 0x00000006, 0x00000080, 0x00000062, 0x00000075, 0x00050083, 0x00000006, 0x00000081,
|
||||
0x00000080, 0x00000015, 0x00070050, 0x00000007, 0x00000082, 0x00000077, 0x0000007e, 0x00000081,
|
||||
0x00000015, 0x00060041, 0x0000008f, 0x00000090, 0x00000089, 0x00000044, 0x0000008a, 0x0004003d,
|
||||
0x00000007, 0x00000091, 0x00000090, 0x00060041, 0x0000008f, 0x00000093, 0x00000089, 0x00000044,
|
||||
0x0000008b, 0x0004003d, 0x00000007, 0x00000094, 0x00000093, 0x00060041, 0x0000008f, 0x00000096,
|
||||
0x00000089, 0x00000044, 0x0000008c, 0x0004003d, 0x00000007, 0x00000097, 0x00000096, 0x00060041,
|
||||
0x0000008f, 0x00000099, 0x00000089, 0x00000044, 0x0000008d, 0x0004003d, 0x00000007, 0x0000009a,
|
||||
0x00000099, 0x00070050, 0x00000009, 0x00000110, 0x00000091, 0x00000094, 0x00000097, 0x0000009a,
|
||||
0x00040054, 0x00000009, 0x00000111, 0x00000110, 0x00060041, 0x0000008f, 0x000000a2, 0x000000a0,
|
||||
0x00000044, 0x0000008a, 0x0004003d, 0x00000007, 0x000000a3, 0x000000a2, 0x00060041, 0x0000008f,
|
||||
0x000000a5, 0x000000a0, 0x00000044, 0x0000008b, 0x0004003d, 0x00000007, 0x000000a6, 0x000000a5,
|
||||
0x00060041, 0x0000008f, 0x000000a8, 0x000000a0, 0x00000044, 0x0000008c, 0x0004003d, 0x00000007,
|
||||
0x000000a9, 0x000000a8, 0x00060041, 0x0000008f, 0x000000ab, 0x000000a0, 0x00000044, 0x0000008d,
|
||||
0x0004003d, 0x00000007, 0x000000ac, 0x000000ab, 0x00070050, 0x00000009, 0x0000012c, 0x000000a3,
|
||||
0x000000a6, 0x000000a9, 0x000000ac, 0x00040054, 0x00000009, 0x0000012d, 0x0000012c, 0x0006000c,
|
||||
0x00000009, 0x000000b0, 0x00000001, 0x00000022, 0x00000111, 0x00050091, 0x00000007, 0x000000b2,
|
||||
0x000000b0, 0x00000082, 0x00050051, 0x00000006, 0x000000b7, 0x000000b2, 0x00000003, 0x0006000c,
|
||||
0x00000006, 0x000000b8, 0x00000001, 0x00000004, 0x000000b7, 0x000500ba, 0x0000003b, 0x000000ba,
|
||||
0x000000b8, 0x000000b9, 0x000300f7, 0x000000bc, 0x00000000, 0x000400fa, 0x000000ba, 0x000000bb,
|
||||
0x000000bc, 0x000200f8, 0x000000bb, 0x00070050, 0x00000007, 0x000000c2, 0x000000b7, 0x000000b7,
|
||||
0x000000b7, 0x000000b7, 0x00050088, 0x00000007, 0x000000c3, 0x000000b2, 0x000000c2, 0x00050091,
|
||||
0x00000007, 0x000000c4, 0x0000012d, 0x000000c3, 0x00050051, 0x00000006, 0x000000c6, 0x000000c4,
|
||||
0x00000003, 0x000500ba, 0x0000003b, 0x000000c8, 0x000000c6, 0x000000c7, 0x000300f7, 0x000000ca,
|
||||
0x00000000, 0x000400fa, 0x000000c8, 0x000000c9, 0x000000ca, 0x000200f8, 0x000000c9, 0x0007004f,
|
||||
0x00000040, 0x000000cd, 0x000000c4, 0x000000c4, 0x00000000, 0x00000001, 0x00050050, 0x00000040,
|
||||
0x000000d0, 0x000000c6, 0x000000c6, 0x00050088, 0x00000040, 0x000000d1, 0x000000cd, 0x000000d0,
|
||||
0x00050051, 0x00000006, 0x000000d4, 0x000000d1, 0x00000000, 0x00050085, 0x00000006, 0x000000d5,
|
||||
0x000000d4, 0x00000067, 0x00050081, 0x00000006, 0x000000d6, 0x000000d5, 0x00000067, 0x00050085,
|
||||
0x00000006, 0x000000d9, 0x000000d6, 0x00000047, 0x00050051, 0x00000006, 0x000000db, 0x000000d1,
|
||||
0x00000001, 0x00050085, 0x00000006, 0x000000dc, 0x000000db, 0x00000067, 0x00050083, 0x00000006,
|
||||
0x000000dd, 0x00000067, 0x000000dc, 0x00050085, 0x00000006, 0x000000e0, 0x000000dd, 0x0000007b,
|
||||
0x00050050, 0x00000040, 0x000000e1, 0x000000d9, 0x000000e0, 0x00050083, 0x00000040, 0x000000e4,
|
||||
0x000000e1, 0x0000006e, 0x000200f9, 0x000000ca, 0x000200f8, 0x000000ca, 0x000700f5, 0x00000040,
|
||||
0x0000012f, 0x000000b4, 0x000000bb, 0x000000e4, 0x000000c9, 0x000200f9, 0x000000bc, 0x000200f8,
|
||||
0x000000bc, 0x000700f5, 0x00000040, 0x0000012e, 0x000000b4, 0x00000056, 0x0000012f, 0x000000ca,
|
||||
0x0004003d, 0x000000e5, 0x000000e8, 0x000000e7, 0x00050051, 0x00000006, 0x000000eb, 0x0000012e,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x000000ec, 0x0000012e, 0x00000001, 0x00070050, 0x00000007,
|
||||
0x000000ed, 0x000000eb, 0x000000ec, 0x00000016, 0x00000016, 0x00040063, 0x000000e8, 0x0000003a,
|
||||
0x000000ed, 0x000200f9, 0x000000f0, 0x000200f8, 0x000000f0, 0x000100fd, 0x00010038,
|
||||
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000036,
|
||||
0x00060010, 0x00000004, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x00000036,
|
||||
0x0000000b, 0x0000001c, 0x00030047, 0x00000041, 0x00000002, 0x00050048, 0x00000041, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00050048, 0x00000041, 0x00000001, 0x00000023, 0x00000008, 0x00050048,
|
||||
0x00000041, 0x00000002, 0x00000023, 0x00000010, 0x00050048, 0x00000041, 0x00000003, 0x00000023,
|
||||
0x00000014, 0x00040047, 0x0000005d, 0x00000021, 0x00000000, 0x00040047, 0x0000005d, 0x00000022,
|
||||
0x00000000, 0x00040047, 0x00000086, 0x00000006, 0x00000010, 0x00030047, 0x00000087, 0x00000002,
|
||||
0x00050048, 0x00000087, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x00000089, 0x00000021,
|
||||
0x00000002, 0x00040047, 0x00000089, 0x00000022, 0x00000000, 0x00040047, 0x0000009d, 0x00000006,
|
||||
0x00000010, 0x00030047, 0x0000009e, 0x00000002, 0x00050048, 0x0000009e, 0x00000000, 0x00000023,
|
||||
0x00000000, 0x00040047, 0x000000a0, 0x00000021, 0x00000003, 0x00040047, 0x000000a0, 0x00000022,
|
||||
0x00000000, 0x00030047, 0x000000e7, 0x00000019, 0x00040047, 0x000000e7, 0x00000021, 0x00000001,
|
||||
0x00040047, 0x000000e7, 0x00000022, 0x00000000, 0x00040047, 0x000000f6, 0x00000021, 0x00000004,
|
||||
0x00040047, 0x000000f6, 0x00000022, 0x00000000, 0x00030047, 0x00000101, 0x00000019, 0x00040047,
|
||||
0x00000101, 0x00000021, 0x00000007, 0x00040047, 0x00000101, 0x00000022, 0x00000000, 0x00030047,
|
||||
0x0000012a, 0x00000018, 0x00040047, 0x0000012a, 0x00000021, 0x00000006, 0x00040047, 0x0000012a,
|
||||
0x00000022, 0x00000000, 0x00030047, 0x0000013b, 0x00000019, 0x00040047, 0x0000013b, 0x00000021,
|
||||
0x00000005, 0x00040047, 0x0000013b, 0x00000022, 0x00000000, 0x00040047, 0x00000146, 0x0000000b,
|
||||
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006,
|
||||
0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040018, 0x00000009, 0x00000007,
|
||||
0x00000004, 0x0004002b, 0x00000006, 0x00000015, 0x3f800000, 0x0004002b, 0x00000006, 0x00000016,
|
||||
0x00000000, 0x00040015, 0x0000002f, 0x00000020, 0x00000001, 0x00040017, 0x00000030, 0x0000002f,
|
||||
0x00000002, 0x00040015, 0x00000033, 0x00000020, 0x00000000, 0x00040017, 0x00000034, 0x00000033,
|
||||
0x00000003, 0x00040020, 0x00000035, 0x00000001, 0x00000034, 0x0004003b, 0x00000035, 0x00000036,
|
||||
0x00000001, 0x00040017, 0x00000037, 0x00000033, 0x00000002, 0x00020014, 0x0000003b, 0x0004002b,
|
||||
0x00000033, 0x0000003c, 0x00000000, 0x00040017, 0x00000040, 0x00000006, 0x00000002, 0x0006001e,
|
||||
0x00000041, 0x00000040, 0x00000040, 0x00000006, 0x00000006, 0x00040020, 0x00000042, 0x00000009,
|
||||
0x00000041, 0x0004003b, 0x00000042, 0x00000043, 0x00000009, 0x0004002b, 0x0000002f, 0x00000044,
|
||||
0x00000000, 0x00040020, 0x00000045, 0x00000009, 0x00000006, 0x0004002b, 0x00000033, 0x0000004d,
|
||||
0x00000001, 0x00090019, 0x0000005a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000001, 0x00000000, 0x0003001b, 0x0000005b, 0x0000005a, 0x00040020, 0x0000005c, 0x00000000,
|
||||
0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d, 0x00000000, 0x0004002b, 0x00000006, 0x00000067,
|
||||
0x3f000000, 0x0004002b, 0x0000002f, 0x0000006a, 0x00000001, 0x00040020, 0x0000006b, 0x00000009,
|
||||
0x00000040, 0x0004002b, 0x00000006, 0x00000075, 0x40000000, 0x0004002b, 0x00000033, 0x00000085,
|
||||
0x00000010, 0x0004001c, 0x00000086, 0x00000007, 0x00000085, 0x0003001e, 0x00000087, 0x00000086,
|
||||
0x00040020, 0x00000088, 0x00000002, 0x00000087, 0x0004003b, 0x00000088, 0x00000089, 0x00000002,
|
||||
0x0004002b, 0x0000002f, 0x0000008a, 0x00000003, 0x0004002b, 0x0000002f, 0x0000008b, 0x00000004,
|
||||
0x0004002b, 0x0000002f, 0x0000008c, 0x00000005, 0x0004002b, 0x0000002f, 0x0000008d, 0x00000006,
|
||||
0x00040020, 0x0000008f, 0x00000002, 0x00000007, 0x0004001c, 0x0000009d, 0x00000007, 0x00000085,
|
||||
0x0003001e, 0x0000009e, 0x0000009d, 0x00040020, 0x0000009f, 0x00000002, 0x0000009e, 0x0004003b,
|
||||
0x0000009f, 0x000000a0, 0x00000002, 0x0005002c, 0x00000040, 0x000000b4, 0x00000016, 0x00000016,
|
||||
0x0004002b, 0x00000006, 0x000000b9, 0x2b8cbccc, 0x0004002b, 0x00000006, 0x000000c7, 0x358637bd,
|
||||
0x00090019, 0x000000e5, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
|
||||
0x00000007, 0x00040020, 0x000000e6, 0x00000000, 0x000000e5, 0x0004003b, 0x000000e6, 0x000000e7,
|
||||
0x00000000, 0x0004002b, 0x0000002f, 0x000000ee, 0x00000002, 0x0004003b, 0x0000005c, 0x000000f6,
|
||||
0x00000000, 0x0004002b, 0x00000006, 0x000000fc, 0x3e808081, 0x00090019, 0x000000ff, 0x00000006,
|
||||
0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002, 0x0000000f, 0x00040020, 0x00000100,
|
||||
0x00000000, 0x000000ff, 0x0004003b, 0x00000100, 0x00000101, 0x00000000, 0x0004002b, 0x0000002f,
|
||||
0x00000119, 0xffffffff, 0x0004003b, 0x00000100, 0x0000012a, 0x00000000, 0x0005002c, 0x00000030,
|
||||
0x00000131, 0x00000044, 0x00000044, 0x0004003b, 0x00000100, 0x0000013b, 0x00000000, 0x0004002b,
|
||||
0x00000033, 0x00000145, 0x00000008, 0x0006002c, 0x00000034, 0x00000146, 0x00000145, 0x00000145,
|
||||
0x0000004d, 0x0005002c, 0x00000040, 0x00000197, 0x00000067, 0x00000067, 0x0005002c, 0x00000030,
|
||||
0x00000199, 0x0000006a, 0x0000006a, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x000300f7, 0x00000147, 0x00000000, 0x000300fb, 0x0000003c, 0x00000148,
|
||||
0x000200f8, 0x00000148, 0x0004003d, 0x00000034, 0x00000038, 0x00000036, 0x0007004f, 0x00000037,
|
||||
0x00000039, 0x00000038, 0x00000038, 0x00000000, 0x00000001, 0x0004007c, 0x00000030, 0x0000003a,
|
||||
0x00000039, 0x00050051, 0x0000002f, 0x0000003f, 0x0000003a, 0x00000000, 0x00060041, 0x00000045,
|
||||
0x00000046, 0x00000043, 0x00000044, 0x0000003c, 0x0004003d, 0x00000006, 0x00000047, 0x00000046,
|
||||
0x0004006e, 0x0000002f, 0x00000048, 0x00000047, 0x000500af, 0x0000003b, 0x00000049, 0x0000003f,
|
||||
0x00000048, 0x000400a8, 0x0000003b, 0x0000004a, 0x00000049, 0x000300f7, 0x0000004c, 0x00000000,
|
||||
0x000400fa, 0x0000004a, 0x0000004b, 0x0000004c, 0x000200f8, 0x0000004b, 0x00050051, 0x0000002f,
|
||||
0x0000004f, 0x0000003a, 0x00000001, 0x00060041, 0x00000045, 0x00000050, 0x00000043, 0x00000044,
|
||||
0x0000004d, 0x0004003d, 0x00000006, 0x00000051, 0x00000050, 0x0004006e, 0x0000002f, 0x00000052,
|
||||
0x00000051, 0x000500af, 0x0000003b, 0x00000053, 0x0000004f, 0x00000052, 0x000200f9, 0x0000004c,
|
||||
0x000200f8, 0x0000004c, 0x000700f5, 0x0000003b, 0x00000054, 0x00000049, 0x00000148, 0x00000053,
|
||||
0x0000004b, 0x000300f7, 0x00000056, 0x00000000, 0x000400fa, 0x00000054, 0x00000055, 0x00000056,
|
||||
0x000200f8, 0x00000055, 0x000200f9, 0x00000147, 0x000200f8, 0x00000056, 0x0004003d, 0x0000005b,
|
||||
0x0000005e, 0x0000005d, 0x00040064, 0x0000005a, 0x00000060, 0x0000005e, 0x0007005f, 0x00000007,
|
||||
0x00000061, 0x00000060, 0x0000003a, 0x00000002, 0x00000044, 0x00050051, 0x00000006, 0x00000062,
|
||||
0x00000061, 0x00000000, 0x0004006f, 0x00000040, 0x00000066, 0x0000003a, 0x00050081, 0x00000040,
|
||||
0x00000069, 0x00000066, 0x00000197, 0x00050041, 0x0000006b, 0x0000006c, 0x00000043, 0x0000006a,
|
||||
0x0004003d, 0x00000040, 0x0000006d, 0x0000006c, 0x00050083, 0x00000040, 0x0000006e, 0x00000069,
|
||||
0x0000006d, 0x00050051, 0x00000006, 0x00000071, 0x0000006e, 0x00000000, 0x00050088, 0x00000006,
|
||||
0x00000074, 0x00000071, 0x00000047, 0x00050085, 0x00000006, 0x00000076, 0x00000074, 0x00000075,
|
||||
0x00050083, 0x00000006, 0x00000077, 0x00000076, 0x00000015, 0x00050051, 0x00000006, 0x00000079,
|
||||
0x0000006e, 0x00000001, 0x00060041, 0x00000045, 0x0000007a, 0x00000043, 0x00000044, 0x0000004d,
|
||||
0x0004003d, 0x00000006, 0x0000007b, 0x0000007a, 0x00050088, 0x00000006, 0x0000007c, 0x00000079,
|
||||
0x0000007b, 0x00050085, 0x00000006, 0x0000007d, 0x0000007c, 0x00000075, 0x00050083, 0x00000006,
|
||||
0x0000007e, 0x00000015, 0x0000007d, 0x00050085, 0x00000006, 0x00000080, 0x00000062, 0x00000075,
|
||||
0x00050083, 0x00000006, 0x00000081, 0x00000080, 0x00000015, 0x00070050, 0x00000007, 0x00000082,
|
||||
0x00000077, 0x0000007e, 0x00000081, 0x00000015, 0x00060041, 0x0000008f, 0x00000090, 0x00000089,
|
||||
0x00000044, 0x0000008a, 0x0004003d, 0x00000007, 0x00000091, 0x00000090, 0x00060041, 0x0000008f,
|
||||
0x00000093, 0x00000089, 0x00000044, 0x0000008b, 0x0004003d, 0x00000007, 0x00000094, 0x00000093,
|
||||
0x00060041, 0x0000008f, 0x00000096, 0x00000089, 0x00000044, 0x0000008c, 0x0004003d, 0x00000007,
|
||||
0x00000097, 0x00000096, 0x00060041, 0x0000008f, 0x00000099, 0x00000089, 0x00000044, 0x0000008d,
|
||||
0x0004003d, 0x00000007, 0x0000009a, 0x00000099, 0x00070050, 0x00000009, 0x00000167, 0x00000091,
|
||||
0x00000094, 0x00000097, 0x0000009a, 0x00040054, 0x00000009, 0x00000168, 0x00000167, 0x00060041,
|
||||
0x0000008f, 0x000000a2, 0x000000a0, 0x00000044, 0x0000008a, 0x0004003d, 0x00000007, 0x000000a3,
|
||||
0x000000a2, 0x00060041, 0x0000008f, 0x000000a5, 0x000000a0, 0x00000044, 0x0000008b, 0x0004003d,
|
||||
0x00000007, 0x000000a6, 0x000000a5, 0x00060041, 0x0000008f, 0x000000a8, 0x000000a0, 0x00000044,
|
||||
0x0000008c, 0x0004003d, 0x00000007, 0x000000a9, 0x000000a8, 0x00060041, 0x0000008f, 0x000000ab,
|
||||
0x000000a0, 0x00000044, 0x0000008d, 0x0004003d, 0x00000007, 0x000000ac, 0x000000ab, 0x00070050,
|
||||
0x00000009, 0x00000183, 0x000000a3, 0x000000a6, 0x000000a9, 0x000000ac, 0x00040054, 0x00000009,
|
||||
0x00000184, 0x00000183, 0x0006000c, 0x00000009, 0x000000b0, 0x00000001, 0x00000022, 0x00000168,
|
||||
0x00050091, 0x00000007, 0x000000b2, 0x000000b0, 0x00000082, 0x00050051, 0x00000006, 0x000000b7,
|
||||
0x000000b2, 0x00000003, 0x0006000c, 0x00000006, 0x000000b8, 0x00000001, 0x00000004, 0x000000b7,
|
||||
0x000500ba, 0x0000003b, 0x000000ba, 0x000000b8, 0x000000b9, 0x000300f7, 0x000000bc, 0x00000000,
|
||||
0x000400fa, 0x000000ba, 0x000000bb, 0x000000bc, 0x000200f8, 0x000000bb, 0x00070050, 0x00000007,
|
||||
0x000000c2, 0x000000b7, 0x000000b7, 0x000000b7, 0x000000b7, 0x00050088, 0x00000007, 0x000000c3,
|
||||
0x000000b2, 0x000000c2, 0x00050091, 0x00000007, 0x000000c4, 0x00000184, 0x000000c3, 0x00050051,
|
||||
0x00000006, 0x000000c6, 0x000000c4, 0x00000003, 0x000500ba, 0x0000003b, 0x000000c8, 0x000000c6,
|
||||
0x000000c7, 0x000300f7, 0x000000ca, 0x00000000, 0x000400fa, 0x000000c8, 0x000000c9, 0x000000ca,
|
||||
0x000200f8, 0x000000c9, 0x0007004f, 0x00000040, 0x000000cd, 0x000000c4, 0x000000c4, 0x00000000,
|
||||
0x00000001, 0x00050050, 0x00000040, 0x000000d0, 0x000000c6, 0x000000c6, 0x00050088, 0x00000040,
|
||||
0x000000d1, 0x000000cd, 0x000000d0, 0x00050051, 0x00000006, 0x000000d4, 0x000000d1, 0x00000000,
|
||||
0x00050085, 0x00000006, 0x000000d5, 0x000000d4, 0x00000067, 0x00050081, 0x00000006, 0x000000d6,
|
||||
0x000000d5, 0x00000067, 0x00050085, 0x00000006, 0x000000d9, 0x000000d6, 0x00000047, 0x00050051,
|
||||
0x00000006, 0x000000db, 0x000000d1, 0x00000001, 0x00050085, 0x00000006, 0x000000dc, 0x000000db,
|
||||
0x00000067, 0x00050083, 0x00000006, 0x000000dd, 0x00000067, 0x000000dc, 0x00050085, 0x00000006,
|
||||
0x000000e0, 0x000000dd, 0x0000007b, 0x00050050, 0x00000040, 0x000000e1, 0x000000d9, 0x000000e0,
|
||||
0x00050083, 0x00000040, 0x000000e4, 0x000000e1, 0x0000006e, 0x000200f9, 0x000000ca, 0x000200f8,
|
||||
0x000000ca, 0x000700f5, 0x00000040, 0x00000190, 0x000000b4, 0x000000bb, 0x000000e4, 0x000000c9,
|
||||
0x000200f9, 0x000000bc, 0x000200f8, 0x000000bc, 0x000700f5, 0x00000040, 0x0000018f, 0x000000b4,
|
||||
0x00000056, 0x00000190, 0x000000ca, 0x0004003d, 0x000000e5, 0x000000e8, 0x000000e7, 0x00050051,
|
||||
0x00000006, 0x000000eb, 0x0000018f, 0x00000000, 0x00050051, 0x00000006, 0x000000ec, 0x0000018f,
|
||||
0x00000001, 0x00070050, 0x00000007, 0x000000ed, 0x000000eb, 0x000000ec, 0x00000016, 0x00000016,
|
||||
0x00040063, 0x000000e8, 0x0000003a, 0x000000ed, 0x00050041, 0x00000045, 0x000000ef, 0x00000043,
|
||||
0x000000ee, 0x0004003d, 0x00000006, 0x000000f0, 0x000000ef, 0x000500bc, 0x0000003b, 0x000000f1,
|
||||
0x000000f0, 0x00000016, 0x000300f7, 0x000000f3, 0x00000000, 0x000400fa, 0x000000f1, 0x000000f2,
|
||||
0x000000f3, 0x000200f8, 0x000000f2, 0x000200f9, 0x00000147, 0x000200f8, 0x000000f3, 0x0004003d,
|
||||
0x0000005b, 0x000000f7, 0x000000f6, 0x00040064, 0x0000005a, 0x000000f9, 0x000000f7, 0x0007005f,
|
||||
0x00000007, 0x000000fa, 0x000000f9, 0x0000003a, 0x00000002, 0x00000044, 0x00050051, 0x00000006,
|
||||
0x000000fb, 0x000000fa, 0x00000001, 0x000500ba, 0x0000003b, 0x000000fd, 0x000000fb, 0x000000fc,
|
||||
0x000600a9, 0x00000006, 0x000000fe, 0x000000fd, 0x00000015, 0x00000016, 0x0004003d, 0x000000ff,
|
||||
0x00000102, 0x00000101, 0x00070050, 0x00000007, 0x00000105, 0x000000fe, 0x000000fe, 0x000000fe,
|
||||
0x000000fe, 0x00040063, 0x00000102, 0x0000003a, 0x00000105, 0x00050041, 0x00000045, 0x00000107,
|
||||
0x00000043, 0x0000008a, 0x0004003d, 0x00000006, 0x00000108, 0x00000107, 0x000500ba, 0x0000003b,
|
||||
0x00000109, 0x00000108, 0x00000067, 0x000300f7, 0x0000010b, 0x00000000, 0x000400fa, 0x00000109,
|
||||
0x0000010a, 0x0000010b, 0x000200f8, 0x0000010a, 0x00050081, 0x00000040, 0x0000010f, 0x0000006e,
|
||||
0x0000018f, 0x0006000c, 0x00000040, 0x00000110, 0x00000001, 0x00000008, 0x0000010f, 0x0004006e,
|
||||
0x00000030, 0x00000111, 0x00000110, 0x00050041, 0x0000006b, 0x00000113, 0x00000043, 0x00000044,
|
||||
0x0004003d, 0x00000040, 0x00000114, 0x00000113, 0x0004006e, 0x00000030, 0x00000115, 0x00000114,
|
||||
0x00050082, 0x00000030, 0x00000117, 0x00000115, 0x00000199, 0x000200f9, 0x0000011a, 0x000200f8,
|
||||
0x0000011a, 0x000700f5, 0x00000006, 0x00000193, 0x00000016, 0x0000010a, 0x00000196, 0x0000011d,
|
||||
0x000700f5, 0x0000002f, 0x00000191, 0x00000119, 0x0000010a, 0x0000013a, 0x0000011d, 0x000500b3,
|
||||
0x0000003b, 0x00000120, 0x00000191, 0x0000006a, 0x000400f6, 0x0000011c, 0x0000011d, 0x00000000,
|
||||
0x000400fa, 0x00000120, 0x0000011b, 0x0000011c, 0x000200f8, 0x0000011b, 0x000200f9, 0x00000122,
|
||||
0x000200f8, 0x00000122, 0x000700f5, 0x00000006, 0x00000196, 0x00000193, 0x0000011b, 0x00000136,
|
||||
0x00000123, 0x000700f5, 0x0000002f, 0x00000194, 0x00000119, 0x0000011b, 0x00000138, 0x00000123,
|
||||
0x000500b3, 0x0000003b, 0x00000128, 0x00000194, 0x0000006a, 0x000400f6, 0x00000124, 0x00000123,
|
||||
0x00000000, 0x000400fa, 0x00000128, 0x00000123, 0x00000124, 0x000200f8, 0x00000123, 0x0004003d,
|
||||
0x000000ff, 0x0000012b, 0x0000012a, 0x00050050, 0x00000030, 0x0000012f, 0x00000194, 0x00000191,
|
||||
0x00050080, 0x00000030, 0x00000130, 0x00000111, 0x0000012f, 0x0008000c, 0x00000030, 0x00000133,
|
||||
0x00000001, 0x0000002d, 0x00000130, 0x00000131, 0x00000117, 0x00050062, 0x00000007, 0x00000134,
|
||||
0x0000012b, 0x00000133, 0x00050051, 0x00000006, 0x00000135, 0x00000134, 0x00000000, 0x0007000c,
|
||||
0x00000006, 0x00000136, 0x00000001, 0x00000028, 0x00000196, 0x00000135, 0x00050080, 0x0000002f,
|
||||
0x00000138, 0x00000194, 0x0000006a, 0x000200f9, 0x00000122, 0x000200f8, 0x00000124, 0x000200f9,
|
||||
0x0000011d, 0x000200f8, 0x0000011d, 0x00050080, 0x0000002f, 0x0000013a, 0x00000191, 0x0000006a,
|
||||
0x000200f9, 0x0000011a, 0x000200f8, 0x0000011c, 0x000200f9, 0x0000010b, 0x000200f8, 0x0000010b,
|
||||
0x000700f5, 0x00000006, 0x00000192, 0x00000016, 0x000000f3, 0x00000193, 0x0000011c, 0x0004003d,
|
||||
0x000000ff, 0x0000013c, 0x0000013b, 0x0007000c, 0x00000006, 0x00000140, 0x00000001, 0x00000028,
|
||||
0x000000fe, 0x00000192, 0x00050085, 0x00000006, 0x00000143, 0x00000140, 0x000000f0, 0x00070050,
|
||||
0x00000007, 0x00000144, 0x00000143, 0x00000143, 0x00000143, 0x00000143, 0x00040063, 0x0000013c,
|
||||
0x0000003a, 0x00000144, 0x000200f9, 0x00000147, 0x000200f8, 0x00000147, 0x000100fd, 0x00010038,
|
||||
};
|
||||
+2
-1
@@ -41,7 +41,8 @@ size_t romfs_overlay_count(void);
|
||||
int romfs_patches_loaded(void); // a romfs-patches manifest gave at least one file
|
||||
// The button prompts (input_prompts_folder): the folder loaded at start, NULL for the game's own prompts; whether a
|
||||
// folder had a manifest at start; and why the folder the settings asked for at start was not loaded (ROMFS_PROMPTS_OK
|
||||
// for any other folder: it was not asked for).
|
||||
// for any other folder: it was not asked for). romfs-patches-pc goes by that name also where romfs-patches-pc-<w>x<h>
|
||||
// stands in for it (romfs.c overlay_init).
|
||||
enum { ROMFS_PROMPTS_OK, ROMFS_PROMPTS_MISSING, ROMFS_PROMPTS_FAILED, ROMFS_PROMPTS_SHAPE, ROMFS_PROMPTS_MODS,
|
||||
ROMFS_PROMPTS_PATCHES };
|
||||
const char *romfs_prompts(void);
|
||||
|
||||
+9
-2
@@ -23,6 +23,7 @@ uint64_t gfx_vsync_ticks(void); // ticks since start
|
||||
uint64_t gfx_vsync_time_ns(uint64_t tick); // kern_now_ns() time of a tick (pacing)
|
||||
double gfx_window_refresh_hz(void); // refresh rate of the window's monitor; 0 unknown
|
||||
int gfx_window_focused(void);
|
||||
int gfx_picture_rect(float out[4]); // where the last present put the picture, fractions of the swapchain (x, y, w, h); 0 before one
|
||||
|
||||
// ---- shader service client (gfx_shader.c)
|
||||
enum { GFX_STAGE_VS, GFX_STAGE_TCS, GFX_STAGE_TES, GFX_STAGE_GS, GFX_STAGE_FS, GFX_STAGE_CS };
|
||||
@@ -35,6 +36,8 @@ typedef struct { // the translator's a
|
||||
uint8_t attr_type[32], frag_out_type[8]; // GFX_TYPE_*
|
||||
uint16_t local[3]; uint32_t shared_bytes;
|
||||
uint8_t depth_minus_w;
|
||||
uint8_t mvprev; // VS: also write last frame's position (patch 0015,
|
||||
// vk_mvprev.c); packed as bit 7 of the depth_minus_w byte
|
||||
} GfxShaderAnswers;
|
||||
typedef struct GfxShader GfxShader; // SPIR-V words + parsed reflection; owned by the cache
|
||||
const GfxShader *gfx_shader_get(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
|
||||
@@ -50,7 +53,7 @@ const GfxShader *gfx_shader_await(int stage, const uint8_t *control, const uint8
|
||||
uint64_t gfx_shader_async_done(void); // translations finished by the background worker
|
||||
void gfx_shader_counts(uint64_t *keys, uint64_t *translated); // distinct keys in memory, translations done (TOTK_TRACE=perf)
|
||||
// Translator answers for this device (vk_caps.c): shaderFloat64, storage offset rounding, subgroup size per vs/fs/cs.
|
||||
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits);
|
||||
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits, int zero_locals);
|
||||
const uint8_t *gfx_shader_key(const GfxShader *s); // the 32-byte cache key (stable while the translator is)
|
||||
const uint8_t *gfx_shader_input_id(const GfxShader *s); // the 32-byte input id (stable across translators)
|
||||
// By input id from memory or disk; xlate 1 also translates it from its stored inputs. NULL if absent.
|
||||
@@ -73,6 +76,9 @@ size_t gfx_shader_warm_list(GfxWarmEnt **out); // malloc'd; recorde
|
||||
const GfxShader *gfx_shader_load_private(const uint8_t iid[32], int *owned);
|
||||
void gfx_shader_release_private(const GfxShader *s);
|
||||
const uint32_t *gfx_shader_spirv(const GfxShader *s, uint64_t *words);
|
||||
// A shader of the runtime's own (SPIR-V and a .res.json text as the translator writes them), outside the cache and
|
||||
// never freed; its key is the sha256 of both. NULL when the SPIR-V is malformed.
|
||||
const GfxShader *gfx_shader_own(const uint32_t *spirv, uint64_t words, const char *res_json);
|
||||
// vk_hdr.c: with TOTK_HDR=1, replaces a fragment shader's SPIR-V (malloc'd, *words long) by the HDR tonemap when it is
|
||||
// the game's tonemap; gfx_shader.c calls it for every fragment shader entering the cache.
|
||||
void vk_hdr_shader_swap(uint32_t **spirv, uint64_t *words);
|
||||
@@ -97,7 +103,8 @@ int gfx_shader_stage(const GfxShader *s); // GFX_STAGE_VS, GFX
|
||||
int gfx_shader_resources(const GfxShader *s, int kind, const GfxShaderRes **res); // count of GFX_RES_* kind; array in *res
|
||||
uint32_t gfx_shader_slot_mask(const GfxShader *s); // texture/image slots whose type its translation read
|
||||
uint32_t gfx_shader_inputs(const GfxShader *s, GfxShaderIo io[32]); // Input Locations (VS attributes, FS varyings) as a mask
|
||||
uint32_t gfx_shader_outputs(const GfxShader *s, GfxShaderIo io[32]); // Output Locations (VS varyings, FS colour targets)
|
||||
uint32_t gfx_shader_outputs(const GfxShader *s, GfxShaderIo io[32]);
|
||||
uint32_t gfx_shader_mvprev_banks(const GfxShader *s); // a last-frame variant: the banks (bit n: cn) its position reads // Output Locations (VS varyings, FS colour targets)
|
||||
void gfx_shader_local_size(const GfxShader *s, uint32_t local[3]); // CS: the LocalSize execution mode; 1, 1, 1 otherwise
|
||||
|
||||
// ---- back end (gfx_null.c, vk_*.c). Called by the NVN front end (nvn_api_*.c, nvn_ext.c) on guest threads.
|
||||
|
||||
@@ -100,6 +100,7 @@ static void null_replay(uint64_t handle) {
|
||||
}
|
||||
|
||||
static uint64_t null_submit(const uint64_t *handles, int n) {
|
||||
nvn_pins_sweep(nvn_submit_count(), NULL, 0); // no replay state outlives a submission here
|
||||
for (int i = 0; i < n; i++) null_replay(handles[i]);
|
||||
return __atomic_add_fetch(&fence, 1, __ATOMIC_ACQ_REL);
|
||||
}
|
||||
|
||||
+24
-3
@@ -295,8 +295,7 @@ static void free_capture(void) {
|
||||
g_cap_valid = 0;
|
||||
}
|
||||
|
||||
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held.
|
||||
static int create_swapchain(void) {
|
||||
static int build_swapchain(void) {
|
||||
flip_locked(); // the acquired image goes out before its swapchain is replaced
|
||||
VkCtx *c = vk_ctx();
|
||||
VkSurfaceCapabilitiesKHR caps;
|
||||
@@ -364,10 +363,19 @@ static int create_swapchain(void) {
|
||||
}
|
||||
free_capture();
|
||||
vk_queue_unlock();
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held. The stale mark
|
||||
// goes before the surface is read: a resize during the build keeps it. The switch to fullscreen at start lands there
|
||||
// on the window thread, and the driver does not report the swapchain as out of date.
|
||||
static int create_swapchain(void) {
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
if (build_swapchain()) return 1;
|
||||
plat_xchg32(&g_dirty, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sync_barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags2 s0,
|
||||
VkAccessFlags2 a0, VkPipelineStageFlags2 s1, VkAccessFlags2 a1) {
|
||||
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = s0, .srcAccessMask = a0,
|
||||
@@ -543,6 +551,15 @@ static void save_capture(const char *path) {
|
||||
// TOTK_ASPECT=stretch, else (fit, the default) the largest one of the picture's own aspect ratio, centred (a 16:9 picture
|
||||
// on the Steam Deck's 1280x800 screen gets bars of 40 rows above and below; a 16:9 picture in a 21:9 window, bars at the
|
||||
// sides).
|
||||
// Where the last present put the picture, as fractions of the swapchain (x, y, w, h): menu_cursor.c's pointer
|
||||
static float g_pic[4];
|
||||
static volatile int32_t g_pic_ok;
|
||||
int gfx_picture_rect(float out[4]) {
|
||||
if (!__atomic_load_n(&g_pic_ok, __ATOMIC_ACQUIRE)) return 0;
|
||||
memcpy(out, g_pic, sizeof g_pic);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void gfx_fit_rect(int cw, int ch, int dw, int dh, int out[4]) {
|
||||
out[0] = out[1] = 0, out[2] = dw, out[3] = dh;
|
||||
if (cw <= 0 || ch <= 0 || !strcmp(cfg_value("TOTK_ASPECT"), "stretch")) return;
|
||||
@@ -616,6 +633,10 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
const int rect[4] = {cx, cy, cw, ch};
|
||||
int to[4]; // where the picture goes in the swapchain image: {x, y, w, h}
|
||||
gfx_fit_rect(cw, ch, (int)g_sc_ext.width, (int)g_sc_ext.height, to);
|
||||
if (g_sc_ext.width && g_sc_ext.height) {
|
||||
for (int i = 0; i < 4; i++) g_pic[i] = (float)to[i] / (float)(i & 1 ? g_sc_ext.height : g_sc_ext.width);
|
||||
__atomic_store_n(&g_pic_ok, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
if (to[2] != (int)g_sc_ext.width || to[3] != (int)g_sc_ext.height) { // black bars beside or above and below it
|
||||
VkClearColorValue black = {.float32 = {0, 0, 0, 1}};
|
||||
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
|
||||
+46
-9
@@ -86,6 +86,7 @@ struct GfxShader {
|
||||
uint32_t in_mask, out_mask; // user Locations of Input / Output variables
|
||||
GfxShaderIo in[32], out[32];
|
||||
uint32_t local[3];
|
||||
uint32_t mvprev_banks; // .res.json "mvprev_banks" (patch 0015), 0 none
|
||||
};
|
||||
|
||||
static PlatMutex cache_lock = PLAT_MUTEX_INIT; // memory cache
|
||||
@@ -118,7 +119,7 @@ static struct { // translations in fl
|
||||
static char cache_dir[PATH_CAP];
|
||||
static char xlate_exe[PATH_CAP];
|
||||
static uint8_t xlate_id[32]; // translator identity (see the keys at the top)
|
||||
static int host_f64 = 1, host_set; // gfx_shader_set_host
|
||||
static int host_f64 = 1, host_zero_locals, host_set; // gfx_shader_set_host
|
||||
static uint32_t host_sbuf, host_sg[3] = {32, 32, 32}, host_gather;
|
||||
static PlatOnce paths_once = PLAT_ONCE_INIT;
|
||||
|
||||
@@ -312,18 +313,19 @@ static void init_paths(void *param) {
|
||||
static void paths(void) { plat_once(&paths_once, init_paths, NULL); }
|
||||
|
||||
// Host answers (vk_caps.c, at device creation, before any translation). Answers other than the defaults (Float64,
|
||||
// exact storage offsets, 32 lanes, no gather bias) are mixed into the translator identity, so they get their own cache
|
||||
// keys.
|
||||
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits) {
|
||||
// exact storage offsets, 32 lanes, no gather bias, undefined locals) are mixed into the translator identity, so they
|
||||
// get their own cache keys.
|
||||
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits, int zero_locals) {
|
||||
paths();
|
||||
if (host_set++) return;
|
||||
host_f64 = float64, host_sbuf = sbuf_align, host_gather = gather_bits;
|
||||
host_f64 = float64, host_sbuf = sbuf_align, host_gather = gather_bits, host_zero_locals = zero_locals;
|
||||
memcpy(host_sg, subgroup, sizeof host_sg);
|
||||
if (float64 && !sbuf_align && subgroup[0] == 32 && subgroup[1] == 32 && subgroup[2] == 32 && !gather_bits) return;
|
||||
if (float64 && !sbuf_align && subgroup[0] == 32 && subgroup[1] == 32 && subgroup[2] == 32 && !gather_bits && !zero_locals) return;
|
||||
uint8_t buf[32 + 96];
|
||||
int n = snprintf((char *)buf + 32, 96, "TKHO f64=%d sbuf=%u sg=%u,%u,%u", float64, sbuf_align, subgroup[0], subgroup[1],
|
||||
subgroup[2]);
|
||||
if (gather_bits) n += snprintf((char *)buf + 32 + n, (size_t)(96 - n), " gather=%u", gather_bits);
|
||||
if (zero_locals) n += snprintf((char *)buf + 32 + n, (size_t)(96 - n), " zero_locals");
|
||||
memcpy(buf, xlate_id, 32);
|
||||
rt_sha256(buf, 32 + (size_t)n, xlate_id);
|
||||
rt_log("gfx_shader: host options %s: own shader cache keys", (char *)buf + 37);
|
||||
@@ -593,6 +595,25 @@ static GfxShader *disk_load_from(const uint8_t key[32], const uint8_t *iid, int
|
||||
return s;
|
||||
}
|
||||
|
||||
const GfxShader *gfx_shader_own(const uint32_t *spirv, uint64_t words, const char *res_json) {
|
||||
if (words < 5 || spirv[0] != 0x07230203 || !spirv_complete(spirv, words) ||
|
||||
!reflection_valid(res_json, strlen(res_json)))
|
||||
return NULL;
|
||||
GfxShader *s = calloc(1, sizeof *s);
|
||||
if (!s) return NULL;
|
||||
size_t rn = strlen(res_json);
|
||||
uint8_t *buf = malloc(words * 4 + rn);
|
||||
s->spirv = malloc(words * 4), s->reflection = malloc(rn + 1);
|
||||
if (!buf || !s->spirv || !s->reflection) rt_fatal("gfx_shader: out of memory");
|
||||
memcpy(buf, spirv, words * 4), memcpy(buf + words * 4, res_json, rn);
|
||||
rt_sha256(buf, words * 4 + rn, s->key);
|
||||
free(buf);
|
||||
memcpy(s->spirv, spirv, words * 4), memcpy(s->reflection, res_json, rn + 1);
|
||||
s->words = words, s->reflection_size = rn;
|
||||
reflect(s);
|
||||
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) {
|
||||
@@ -826,6 +847,7 @@ static void reflect(GfxShader *s) {
|
||||
char stage[8];
|
||||
json_str(js, js + strlen(js), "stage", stage, sizeof stage);
|
||||
s->stage = !strcmp(stage, "cs") ? GFX_STAGE_CS : !strcmp(stage, "ps") ? GFX_STAGE_FS : GFX_STAGE_VS;
|
||||
s->mvprev_banks = (uint32_t)json_int(js, js + strlen(js), "mvprev_banks", 0);
|
||||
for (int k = 0; k < GFX_RES_KINDS; k++) {
|
||||
int count = json_objects(js, kinds[k], NULL, s, k);
|
||||
s->res[k] = calloc(count ? count : 1, sizeof *s->res[k]);
|
||||
@@ -1037,7 +1059,7 @@ static size_t pack_answers(uint8_t *o, const GfxShaderAnswers *a) {
|
||||
memcpy(p, a->frag_out_type, 8), p += 8;
|
||||
for (int i = 0; i < 3; i++) *p++ = (uint8_t)a->local[i], *p++ = (uint8_t)(a->local[i] >> 8);
|
||||
put_u32(&p, a->shared_bytes);
|
||||
*p++ = a->depth_minus_w;
|
||||
*p++ = (uint8_t)(a->depth_minus_w | (a->mvprev ? 0x80 : 0));
|
||||
return (size_t)(p - o);
|
||||
}
|
||||
|
||||
@@ -1069,6 +1091,7 @@ static void mask_answers(const GfxShaderAnswers *a, const UseMask *m, GfxShaderA
|
||||
if (m->fouts >> i & 1) o->frag_out_type[i] = a->frag_out_type[i];
|
||||
memcpy(o->local, a->local, sizeof o->local);
|
||||
o->shared_bytes = a->shared_bytes;
|
||||
o->mvprev = a->mvprev; // another translation, not an answer the use mask covers
|
||||
}
|
||||
|
||||
// The cache key of an input id under the current translator.
|
||||
@@ -1148,9 +1171,11 @@ static size_t build_request(char *o0, const char *hex, const char *stage, const
|
||||
if (!strcmp(stage, "ps") && (control[0x718] & 1)) o += sprintf(o, ",\"early_fragment_tests\":true");
|
||||
int si = !strcmp(stage, "vs") ? 0 : !strcmp(stage, "cs") ? 2 : 1; // gfx_shader_set_host answers
|
||||
if (!host_f64) o += sprintf(o, ",\"softfloat64\":true");
|
||||
if (a && a->mvprev && !strcmp(stage, "vs")) o += sprintf(o, ",\"mvprev\":true");
|
||||
if (host_sbuf) o += sprintf(o, ",\"sbuf_align\":%u", host_sbuf);
|
||||
if (host_sg[si] != 32) o += sprintf(o, ",\"subgroup\":%u", host_sg[si]);
|
||||
if (host_gather) o += sprintf(o, ",\"gather_bias\":%u", host_gather);
|
||||
if (host_zero_locals) o += sprintf(o, ",\"zero_locals\":true");
|
||||
o += sprintf(o, "},\"out\":\"");
|
||||
json_esc(&o, cache_dir);
|
||||
o += sprintf(o, "\"}");
|
||||
@@ -1350,8 +1375,18 @@ static const GfxShader *translate_one(const Req *r, int stage, const uint8_t *co
|
||||
if (!req) return plat_file_delete(code_path), fail("out of memory", hex);
|
||||
build_request(req, hex, r->stage_name, code_path, r->c1, r->c1_len, a, control);
|
||||
int ok = svc_request(req, reply, sizeof reply);
|
||||
// TOTK_XLATE_KEEP=1: a last-frame variant's job stays as <key>.job (one request line) with its code, to run the
|
||||
// translator on by hand (totk_xlate --serve < <key>.job)
|
||||
static int keep = -1;
|
||||
if (keep < 0) keep = rt_knob("XLATE_KEEP", "0");
|
||||
int kept = keep && a && a->mvprev;
|
||||
if (kept) {
|
||||
char job_path[PATH_CAP + 80];
|
||||
snprintf(job_path, sizeof job_path, "%s/%s.job", cache_dir, hex);
|
||||
write_file(job_path, req, strlen(req));
|
||||
}
|
||||
free(req);
|
||||
plat_file_delete(code_path);
|
||||
if (!kept) plat_file_delete(code_path);
|
||||
if (!ok) return fail("translator unavailable", hex);
|
||||
if (strncmp(reply, "ok ", 3)) {
|
||||
plat_file_delete(spv_path), plat_file_delete(res_path);
|
||||
@@ -1706,6 +1741,8 @@ uint32_t gfx_shader_inputs(const GfxShader *s, GfxShaderIo io[32]) {
|
||||
return s->in_mask;
|
||||
}
|
||||
|
||||
uint32_t gfx_shader_mvprev_banks(const GfxShader *s) { return s->mvprev_banks; }
|
||||
|
||||
uint32_t gfx_shader_outputs(const GfxShader *s, GfxShaderIo io[32]) {
|
||||
if (io) memcpy(io, s->out, sizeof s->out);
|
||||
return s->out_mask;
|
||||
@@ -1795,7 +1832,7 @@ static void unpack_answers(const uint8_t *p, GfxShaderAnswers *a) {
|
||||
memcpy(a->frag_out_type, p, 8), p += 8;
|
||||
for (int i = 0; i < 3; i++) a->local[i] = (uint16_t)(p[0] | p[1] << 8), p += 2;
|
||||
memcpy(&a->shared_bytes, p, 4), p += 4;
|
||||
a->depth_minus_w = *p;
|
||||
a->depth_minus_w = *p & 0x7f, a->mvprev = *p >> 7;
|
||||
}
|
||||
|
||||
// Reads a record file's body (after its header) into *body; 0 when absent, and a file with another header is removed.
|
||||
|
||||
@@ -279,6 +279,24 @@ static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
|
||||
return DefWindowProcW(h, m, w, l);
|
||||
}
|
||||
|
||||
// Brings the game window to the front. Windows refuses SetForegroundWindow to a process that was not the last to get
|
||||
// input, which is the case for a game started by a frontend; attaching this thread's input queue to the foreground
|
||||
// window's thread for the call is the documented way around it. The window is not left topmost.
|
||||
static void raise_window(HWND h) {
|
||||
HWND fg = GetForegroundWindow();
|
||||
DWORD me = GetCurrentThreadId(), other = fg ? GetWindowThreadProcessId(fg, NULL) : 0;
|
||||
int attached = other && other != me && AttachThreadInput(me, other, TRUE);
|
||||
ShowWindow(h, IsIconic(h) ? SW_RESTORE : SW_SHOW);
|
||||
BringWindowToTop(h);
|
||||
SetForegroundWindow(h);
|
||||
SetActiveWindow(h);
|
||||
if (attached) AttachThreadInput(me, other, FALSE);
|
||||
if (GetForegroundWindow() != h) { // still refused: a topmost toggle puts it above the others without keeping it there
|
||||
SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
||||
SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
||||
}
|
||||
}
|
||||
|
||||
static void window_main(void *arg) {
|
||||
(void)arg;
|
||||
HINSTANCE inst = GetModuleHandleW(NULL);
|
||||
@@ -293,10 +311,10 @@ static void window_main(void *arg) {
|
||||
int cw, ch, ow, oh;
|
||||
cfg_window_size(&cw, &ch);
|
||||
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.
|
||||
// The window opens behind the user's windows first and is then raised (raise_window): a game started by a frontend
|
||||
// has no other way to reach it. The game counts itself in focus until the first WM_ACTIVATEAPP, so loading goes on,
|
||||
// and keyboard input waits for the foreground (input_win.c). If Windows still refuses the foreground, the taskbar
|
||||
// button flashes, and TOTK_FULLSCREEN waits for the 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, ow, oh, NULL,
|
||||
@@ -315,9 +333,12 @@ static void window_main(void *arg) {
|
||||
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
if (!background) {
|
||||
// Pending before the raise: the activation it gets is the first WM_ACTIVATEAPP, which takes the fullscreen
|
||||
// setting; set after it, fullscreen waited for the next activation and the game started in a window
|
||||
g_fs_pending = cfg_fullscreen();
|
||||
raise_window(g_hwnd); // a frontend's player may have no way to reach a window left behind
|
||||
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
|
||||
FlashWindowEx(&fw);
|
||||
g_fs_pending = cfg_fullscreen();
|
||||
}
|
||||
note_applied();
|
||||
refresh_rate_update(g_hwnd, 1);
|
||||
|
||||
+267
-122
@@ -1,12 +1,38 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/hdr_tonemap.frag; do not edit.
|
||||
// source sha256 59af32c0b20e9783c36779cdd379db02b52c0652cc1916ebca1c62a2b38df71e
|
||||
// source sha256 a09ae2b13d844bb6a136d2c3cf38b3ec571191775a4c2f0b43a586bd355381fa
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t hdr_tonemap_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000001bf, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x07230203, 0x00010300, 0x0008000b, 0x0000018a, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000053, 0x0000011c, 0x00030010,
|
||||
0x00000004, 0x00000007, 0x00040047, 0x0000004f, 0x00000021, 0x00000001, 0x00040047, 0x0000004f,
|
||||
0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000053, 0x00000181, 0x00030010,
|
||||
0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d,
|
||||
0x00000000, 0x00050005, 0x0000000a, 0x76727563, 0x31662865, 0x0000003b, 0x00030005, 0x00000009,
|
||||
0x00000078, 0x00050005, 0x00000010, 0x76727563, 0x76283365, 0x003b3366, 0x00030005, 0x0000000f,
|
||||
0x00000078, 0x00050005, 0x00000014, 0x6378616d, 0x33667628, 0x0000003b, 0x00030005, 0x00000013,
|
||||
0x00000063, 0x00030005, 0x00000016, 0x00003079, 0x00040005, 0x0000002e, 0x61726170, 0x0000006d,
|
||||
0x00040005, 0x00000034, 0x61726170, 0x0000006d, 0x00040005, 0x00000039, 0x61726170, 0x0000006d,
|
||||
0x00030005, 0x0000004b, 0x00000078, 0x00050005, 0x0000004f, 0x745f7066, 0x6263745f, 0x0000385f,
|
||||
0x00030005, 0x00000053, 0x00007675, 0x00050005, 0x0000005c, 0x745f7066, 0x6263745f, 0x0000415f,
|
||||
0x00030005, 0x00000064, 0x00000077, 0x00030005, 0x00000067, 0x00003143, 0x00040006, 0x00000067,
|
||||
0x00000000, 0x00000076, 0x00040005, 0x00000069, 0x635f7066, 0x00000031, 0x00030005, 0x0000006f,
|
||||
0x00000067, 0x00030005, 0x00000071, 0x00003343, 0x00040006, 0x00000071, 0x00000000, 0x00000076,
|
||||
0x00040005, 0x00000073, 0x635f7066, 0x00000033, 0x00040005, 0x00000076, 0x656d6167, 0x00000000,
|
||||
0x00060005, 0x00000079, 0x70707553, 0x4274726f, 0x65666675, 0x00000072, 0x00040006, 0x00000079,
|
||||
0x00000000, 0x00646170, 0x00040006, 0x00000079, 0x00000001, 0x00726468, 0x00060005, 0x0000007b,
|
||||
0x70707573, 0x5f74726f, 0x66667562, 0x00007265, 0x00040005, 0x00000089, 0x6b616570, 0x00000000,
|
||||
0x00030005, 0x00000094, 0x006d756c, 0x00030005, 0x000000a6, 0x00726473, 0x00040005, 0x000000ab,
|
||||
0x6c5f7962, 0x00006d75, 0x00040005, 0x000000b2, 0x61726170, 0x0000006d, 0x00030005, 0x000000bb,
|
||||
0x00000063, 0x00040005, 0x000000bd, 0x61726170, 0x0000006d, 0x00030005, 0x000000c7, 0x00000073,
|
||||
0x00040005, 0x000000c8, 0x61726170, 0x0000006d, 0x00030005, 0x000000cc, 0x0000006e, 0x00030005,
|
||||
0x000000d1, 0x0000006d, 0x00040005, 0x000000d2, 0x61726170, 0x0000006d, 0x00030005, 0x000000d5,
|
||||
0x00000064, 0x00040005, 0x000000e4, 0x64617267, 0x00000065, 0x00040005, 0x000000fa, 0x5f726473,
|
||||
0x00000063, 0x00030005, 0x00000115, 0x00006d73, 0x00040005, 0x00000116, 0x61726170, 0x0000006d,
|
||||
0x00030005, 0x00000119, 0x00006473, 0x00030005, 0x00000139, 0x00657568, 0x00030005, 0x0000013d,
|
||||
0x00006379, 0x00030005, 0x00000144, 0x00007379, 0x00040005, 0x00000159, 0x61726170, 0x0000006d,
|
||||
0x00030005, 0x00000160, 0x00706f74, 0x00040005, 0x00000161, 0x61726170, 0x0000006d, 0x00040005,
|
||||
0x00000164, 0x65656e6b, 0x00000000, 0x00030005, 0x0000016c, 0x00000072, 0x00040005, 0x00000181,
|
||||
0x6f6c6f63, 0x00000072, 0x00040047, 0x0000004f, 0x00000021, 0x00000001, 0x00040047, 0x0000004f,
|
||||
0x00000022, 0x00000000, 0x00040047, 0x00000053, 0x0000001e, 0x00000000, 0x00040047, 0x0000005c,
|
||||
0x00000021, 0x00000002, 0x00040047, 0x0000005c, 0x00000022, 0x00000000, 0x00040047, 0x00000066,
|
||||
0x00000006, 0x00000010, 0x00030047, 0x00000067, 0x00000002, 0x00050048, 0x00000067, 0x00000000,
|
||||
@@ -17,129 +43,248 @@ static const uint32_t hdr_tonemap_spv[] = {
|
||||
0x00000006, 0x00000010, 0x00030047, 0x00000079, 0x00000002, 0x00050048, 0x00000079, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00050048, 0x00000079, 0x00000001, 0x00000023, 0x000005a0, 0x00040047,
|
||||
0x0000007b, 0x00000021, 0x00000000, 0x00040047, 0x0000007b, 0x00000022, 0x00000000, 0x00040047,
|
||||
0x0000011c, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
|
||||
0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x0000000c, 0x00000006, 0x00000003, 0x0004002b,
|
||||
0x00000006, 0x00000017, 0x3e28aeed, 0x0004002b, 0x00000006, 0x00000019, 0x3e3851ec, 0x00020014,
|
||||
0x0000001a, 0x0004002b, 0x00000006, 0x0000001f, 0x3f800000, 0x0004002b, 0x00000006, 0x00000026,
|
||||
0x3f55d445, 0x00040015, 0x0000002f, 0x00000020, 0x00000000, 0x0004002b, 0x0000002f, 0x00000030,
|
||||
0x00000000, 0x0004002b, 0x0000002f, 0x00000035, 0x00000001, 0x00090019, 0x0000004c, 0x00000006,
|
||||
0x00000181, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
|
||||
0x00030016, 0x00000006, 0x00000020, 0x00040020, 0x00000007, 0x00000007, 0x00000006, 0x00040021,
|
||||
0x00000008, 0x00000006, 0x00000007, 0x00040017, 0x0000000c, 0x00000006, 0x00000003, 0x00040020,
|
||||
0x0000000d, 0x00000007, 0x0000000c, 0x00040021, 0x0000000e, 0x0000000c, 0x0000000d, 0x00040021,
|
||||
0x00000012, 0x00000006, 0x0000000d, 0x0004002b, 0x00000006, 0x00000017, 0x3e28aeed, 0x0004002b,
|
||||
0x00000006, 0x00000019, 0x3e3851ec, 0x00020014, 0x0000001a, 0x0004002b, 0x00000006, 0x0000001f,
|
||||
0x3f800000, 0x0004002b, 0x00000006, 0x00000026, 0x3f55d445, 0x00040015, 0x0000002f, 0x00000020,
|
||||
0x00000000, 0x0004002b, 0x0000002f, 0x00000030, 0x00000000, 0x0004002b, 0x0000002f, 0x00000035,
|
||||
0x00000001, 0x0004002b, 0x0000002f, 0x0000003a, 0x00000002, 0x00090019, 0x0000004c, 0x00000006,
|
||||
0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000004d,
|
||||
0x0000004c, 0x00040020, 0x0000004e, 0x00000000, 0x0000004d, 0x0004003b, 0x0000004e, 0x0000004f,
|
||||
0x00000000, 0x00040017, 0x00000051, 0x00000006, 0x00000004, 0x00040020, 0x00000052, 0x00000001,
|
||||
0x00000051, 0x0004003b, 0x00000052, 0x00000053, 0x00000001, 0x00040017, 0x00000054, 0x00000006,
|
||||
0x00000002, 0x0004002b, 0x00000006, 0x00000059, 0x461c4000, 0x0006002c, 0x0000000c, 0x0000005a,
|
||||
0x00000059, 0x00000059, 0x00000059, 0x0004003b, 0x0000004e, 0x0000005c, 0x00000000, 0x0004002b,
|
||||
0x0000002f, 0x00000065, 0x00001000, 0x0004001c, 0x00000066, 0x00000051, 0x00000065, 0x0003001e,
|
||||
0x00000067, 0x00000066, 0x00040020, 0x00000068, 0x00000002, 0x00000067, 0x0004003b, 0x00000068,
|
||||
0x00000069, 0x00000002, 0x00040015, 0x0000006a, 0x00000020, 0x00000001, 0x0004002b, 0x0000006a,
|
||||
0x0000006b, 0x00000000, 0x00040020, 0x0000006c, 0x00000002, 0x00000051, 0x0004001c, 0x00000070,
|
||||
0x00000051, 0x00000065, 0x0003001e, 0x00000071, 0x00000070, 0x00040020, 0x00000072, 0x00000002,
|
||||
0x00000071, 0x0004003b, 0x00000072, 0x00000073, 0x00000002, 0x0004002b, 0x0000002f, 0x00000077,
|
||||
0x0000005a, 0x0004001c, 0x00000078, 0x00000051, 0x00000077, 0x0004001e, 0x00000079, 0x00000078,
|
||||
0x00000051, 0x00040020, 0x0000007a, 0x00000002, 0x00000079, 0x0004003b, 0x0000007a, 0x0000007b,
|
||||
0x00000002, 0x0004002b, 0x0000006a, 0x0000007c, 0x00000001, 0x00040020, 0x0000007d, 0x00000002,
|
||||
0x00000006, 0x0004002b, 0x00000006, 0x00000080, 0x00000000, 0x0004002b, 0x00000006, 0x00000097,
|
||||
0x3e99096c, 0x0006002c, 0x0000000c, 0x000000c5, 0x00000080, 0x00000080, 0x00000080, 0x0004002b,
|
||||
0x00000006, 0x000000d8, 0x3f2aaaab, 0x0004002b, 0x00000006, 0x000000ff, 0x3f000000, 0x00040020,
|
||||
0x0000011b, 0x00000003, 0x00000051, 0x0004003b, 0x0000011b, 0x0000011c, 0x00000003, 0x00050036,
|
||||
0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000004d,
|
||||
0x00000050, 0x0000004f, 0x0004003d, 0x00000051, 0x00000055, 0x00000053, 0x0007004f, 0x00000054,
|
||||
0x00000056, 0x00000055, 0x00000055, 0x00000000, 0x00000001, 0x00050057, 0x00000051, 0x00000057,
|
||||
0x00000050, 0x00000056, 0x0008004f, 0x0000000c, 0x00000058, 0x00000057, 0x00000057, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x0007000c, 0x0000000c, 0x0000005b, 0x00000001, 0x00000025, 0x00000058,
|
||||
0x0000005a, 0x0004003d, 0x0000004d, 0x0000005d, 0x0000005c, 0x00050057, 0x00000051, 0x00000060,
|
||||
0x0000005d, 0x00000056, 0x0008004f, 0x0000000c, 0x00000061, 0x00000060, 0x00000060, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x00050081, 0x0000000c, 0x00000062, 0x0000005b, 0x00000061, 0x00060041,
|
||||
0x0000006c, 0x0000006d, 0x00000069, 0x0000006b, 0x0000006b, 0x0004003d, 0x00000051, 0x0000006e,
|
||||
0x0000006d, 0x00060041, 0x0000006c, 0x00000074, 0x00000073, 0x0000006b, 0x0000006b, 0x0004003d,
|
||||
0x00000051, 0x00000075, 0x00000074, 0x00060041, 0x0000007d, 0x0000007e, 0x0000007b, 0x0000007c,
|
||||
0x00000059, 0x00000059, 0x00000059, 0x0004003b, 0x0000004e, 0x0000005c, 0x00000000, 0x00040020,
|
||||
0x00000063, 0x00000007, 0x00000051, 0x0004002b, 0x0000002f, 0x00000065, 0x00001000, 0x0004001c,
|
||||
0x00000066, 0x00000051, 0x00000065, 0x0003001e, 0x00000067, 0x00000066, 0x00040020, 0x00000068,
|
||||
0x00000002, 0x00000067, 0x0004003b, 0x00000068, 0x00000069, 0x00000002, 0x00040015, 0x0000006a,
|
||||
0x00000020, 0x00000001, 0x0004002b, 0x0000006a, 0x0000006b, 0x00000000, 0x00040020, 0x0000006c,
|
||||
0x00000002, 0x00000051, 0x0004001c, 0x00000070, 0x00000051, 0x00000065, 0x0003001e, 0x00000071,
|
||||
0x00000070, 0x00040020, 0x00000072, 0x00000002, 0x00000071, 0x0004003b, 0x00000072, 0x00000073,
|
||||
0x00000002, 0x0004002b, 0x0000002f, 0x00000077, 0x0000005a, 0x0004001c, 0x00000078, 0x00000051,
|
||||
0x00000077, 0x0004001e, 0x00000079, 0x00000078, 0x00000051, 0x00040020, 0x0000007a, 0x00000002,
|
||||
0x00000079, 0x0004003b, 0x0000007a, 0x0000007b, 0x00000002, 0x0004002b, 0x0000006a, 0x0000007c,
|
||||
0x00000001, 0x00040020, 0x0000007d, 0x00000002, 0x00000006, 0x0004002b, 0x00000006, 0x00000080,
|
||||
0x00000000, 0x0004002b, 0x00000006, 0x00000097, 0x3e99096c, 0x0006002c, 0x0000000c, 0x000000c5,
|
||||
0x00000080, 0x00000080, 0x00000080, 0x0004002b, 0x00000006, 0x000000d8, 0x3f2aaaab, 0x0006002c,
|
||||
0x0000000c, 0x00000108, 0x0000001f, 0x0000001f, 0x0000001f, 0x0004002b, 0x00000006, 0x0000013f,
|
||||
0x3e59b3d0, 0x0004002b, 0x00000006, 0x00000140, 0x3f371759, 0x0004002b, 0x00000006, 0x00000141,
|
||||
0x3d93dd98, 0x0006002c, 0x0000000c, 0x00000142, 0x0000013f, 0x00000140, 0x00000141, 0x0004002b,
|
||||
0x00000006, 0x0000014a, 0x3727c5ac, 0x0004002b, 0x00000006, 0x00000157, 0x3f000000, 0x0004002b,
|
||||
0x00000006, 0x00000158, 0x3fc00000, 0x00040020, 0x00000180, 0x00000003, 0x00000051, 0x0004003b,
|
||||
0x00000180, 0x00000181, 0x00000003, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x0004003b, 0x0000000d, 0x0000004b, 0x00000007, 0x0004003b, 0x00000063,
|
||||
0x00000064, 0x00000007, 0x0004003b, 0x00000063, 0x0000006f, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x00000076, 0x00000007, 0x0004003b, 0x00000007, 0x00000082, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x00000089, 0x00000007, 0x0004003b, 0x00000007, 0x0000008d, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x00000094, 0x00000007, 0x0004003b, 0x00000007, 0x000000a6, 0x00000007, 0x0004003b, 0x0000000d,
|
||||
0x000000ab, 0x00000007, 0x0004003b, 0x0000000d, 0x000000ae, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x000000b2, 0x00000007, 0x0004003b, 0x0000000d, 0x000000bb, 0x00000007, 0x0004003b, 0x0000000d,
|
||||
0x000000bd, 0x00000007, 0x0004003b, 0x00000007, 0x000000c7, 0x00000007, 0x0004003b, 0x0000000d,
|
||||
0x000000c8, 0x00000007, 0x0004003b, 0x0000000d, 0x000000cc, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x000000d1, 0x00000007, 0x0004003b, 0x0000000d, 0x000000d2, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x000000d5, 0x00000007, 0x0004003b, 0x00000007, 0x000000e4, 0x00000007, 0x0004003b, 0x0000000d,
|
||||
0x000000fa, 0x00000007, 0x0004003b, 0x0000000d, 0x000000fd, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x00000115, 0x00000007, 0x0004003b, 0x0000000d, 0x00000116, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x00000119, 0x00000007, 0x0004003b, 0x00000007, 0x00000139, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x0000013d, 0x00000007, 0x0004003b, 0x00000007, 0x00000144, 0x00000007, 0x0004003b, 0x0000000d,
|
||||
0x00000159, 0x00000007, 0x0004003b, 0x00000007, 0x00000160, 0x00000007, 0x0004003b, 0x0000000d,
|
||||
0x00000161, 0x00000007, 0x0004003b, 0x00000007, 0x00000164, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x0000016c, 0x00000007, 0x0004003d, 0x0000004d, 0x00000050, 0x0000004f, 0x0004003d, 0x00000051,
|
||||
0x00000055, 0x00000053, 0x0007004f, 0x00000054, 0x00000056, 0x00000055, 0x00000055, 0x00000000,
|
||||
0x00000001, 0x00050057, 0x00000051, 0x00000057, 0x00000050, 0x00000056, 0x0008004f, 0x0000000c,
|
||||
0x00000058, 0x00000057, 0x00000057, 0x00000000, 0x00000001, 0x00000002, 0x0007000c, 0x0000000c,
|
||||
0x0000005b, 0x00000001, 0x00000025, 0x00000058, 0x0000005a, 0x0004003d, 0x0000004d, 0x0000005d,
|
||||
0x0000005c, 0x0004003d, 0x00000051, 0x0000005e, 0x00000053, 0x0007004f, 0x00000054, 0x0000005f,
|
||||
0x0000005e, 0x0000005e, 0x00000000, 0x00000001, 0x00050057, 0x00000051, 0x00000060, 0x0000005d,
|
||||
0x0000005f, 0x0008004f, 0x0000000c, 0x00000061, 0x00000060, 0x00000060, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x00050081, 0x0000000c, 0x00000062, 0x0000005b, 0x00000061, 0x0003003e, 0x0000004b,
|
||||
0x00000062, 0x00060041, 0x0000006c, 0x0000006d, 0x00000069, 0x0000006b, 0x0000006b, 0x0004003d,
|
||||
0x00000051, 0x0000006e, 0x0000006d, 0x0003003e, 0x00000064, 0x0000006e, 0x00060041, 0x0000006c,
|
||||
0x00000074, 0x00000073, 0x0000006b, 0x0000006b, 0x0004003d, 0x00000051, 0x00000075, 0x00000074,
|
||||
0x0003003e, 0x0000006f, 0x00000075, 0x00060041, 0x0000007d, 0x0000007e, 0x0000007b, 0x0000007c,
|
||||
0x00000030, 0x0004003d, 0x00000006, 0x0000007f, 0x0000007e, 0x000500ba, 0x0000001a, 0x00000081,
|
||||
0x0000007f, 0x00000080, 0x000600a9, 0x00000006, 0x000001bd, 0x00000081, 0x0000007f, 0x0000001f,
|
||||
0x00060041, 0x0000007d, 0x0000008a, 0x0000007b, 0x0000007c, 0x00000035, 0x0004003d, 0x00000006,
|
||||
0x0000008b, 0x0000008a, 0x000500ba, 0x0000001a, 0x0000008c, 0x0000008b, 0x0000001f, 0x000600a9,
|
||||
0x00000006, 0x000001be, 0x0000008c, 0x0000008b, 0x0000001f, 0x00050051, 0x00000006, 0x00000096,
|
||||
0x00000062, 0x00000000, 0x00050085, 0x00000006, 0x00000098, 0x00000096, 0x00000097, 0x00050051,
|
||||
0x00000006, 0x0000009a, 0x00000062, 0x00000001, 0x00050051, 0x00000006, 0x0000009c, 0x0000006e,
|
||||
0x00000000, 0x00050085, 0x00000006, 0x0000009d, 0x0000009a, 0x0000009c, 0x00050081, 0x00000006,
|
||||
0x0000009e, 0x00000098, 0x0000009d, 0x00050051, 0x00000006, 0x000000a0, 0x00000062, 0x00000002,
|
||||
0x00050051, 0x00000006, 0x000000a2, 0x0000006e, 0x00000001, 0x00050085, 0x00000006, 0x000000a3,
|
||||
0x0000007f, 0x00000080, 0x000300f7, 0x00000084, 0x00000000, 0x000400fa, 0x00000081, 0x00000083,
|
||||
0x00000087, 0x000200f8, 0x00000083, 0x00060041, 0x0000007d, 0x00000085, 0x0000007b, 0x0000007c,
|
||||
0x00000030, 0x0004003d, 0x00000006, 0x00000086, 0x00000085, 0x0003003e, 0x00000082, 0x00000086,
|
||||
0x000200f9, 0x00000084, 0x000200f8, 0x00000087, 0x0003003e, 0x00000082, 0x0000001f, 0x000200f9,
|
||||
0x00000084, 0x000200f8, 0x00000084, 0x0004003d, 0x00000006, 0x00000088, 0x00000082, 0x0003003e,
|
||||
0x00000076, 0x00000088, 0x00060041, 0x0000007d, 0x0000008a, 0x0000007b, 0x0000007c, 0x00000035,
|
||||
0x0004003d, 0x00000006, 0x0000008b, 0x0000008a, 0x000500ba, 0x0000001a, 0x0000008c, 0x0000008b,
|
||||
0x0000001f, 0x000300f7, 0x0000008f, 0x00000000, 0x000400fa, 0x0000008c, 0x0000008e, 0x00000092,
|
||||
0x000200f8, 0x0000008e, 0x00060041, 0x0000007d, 0x00000090, 0x0000007b, 0x0000007c, 0x00000035,
|
||||
0x0004003d, 0x00000006, 0x00000091, 0x00000090, 0x0003003e, 0x0000008d, 0x00000091, 0x000200f9,
|
||||
0x0000008f, 0x000200f8, 0x00000092, 0x0003003e, 0x0000008d, 0x0000001f, 0x000200f9, 0x0000008f,
|
||||
0x000200f8, 0x0000008f, 0x0004003d, 0x00000006, 0x00000093, 0x0000008d, 0x0003003e, 0x00000089,
|
||||
0x00000093, 0x00050041, 0x00000007, 0x00000095, 0x0000004b, 0x00000030, 0x0004003d, 0x00000006,
|
||||
0x00000096, 0x00000095, 0x00050085, 0x00000006, 0x00000098, 0x00000096, 0x00000097, 0x00050041,
|
||||
0x00000007, 0x00000099, 0x0000004b, 0x00000035, 0x0004003d, 0x00000006, 0x0000009a, 0x00000099,
|
||||
0x00050041, 0x00000007, 0x0000009b, 0x00000064, 0x00000030, 0x0004003d, 0x00000006, 0x0000009c,
|
||||
0x0000009b, 0x00050085, 0x00000006, 0x0000009d, 0x0000009a, 0x0000009c, 0x00050081, 0x00000006,
|
||||
0x0000009e, 0x00000098, 0x0000009d, 0x00050041, 0x00000007, 0x0000009f, 0x0000004b, 0x0000003a,
|
||||
0x0004003d, 0x00000006, 0x000000a0, 0x0000009f, 0x00050041, 0x00000007, 0x000000a1, 0x00000064,
|
||||
0x00000035, 0x0004003d, 0x00000006, 0x000000a2, 0x000000a1, 0x00050085, 0x00000006, 0x000000a3,
|
||||
0x000000a0, 0x000000a2, 0x00050081, 0x00000006, 0x000000a4, 0x0000009e, 0x000000a3, 0x0007000c,
|
||||
0x00000006, 0x000000a5, 0x00000001, 0x00000028, 0x000000a4, 0x00000080, 0x0004007f, 0x00000006,
|
||||
0x000000a8, 0x000000a5, 0x0006000c, 0x00000006, 0x000000a9, 0x00000001, 0x0000001b, 0x000000a8,
|
||||
0x00050083, 0x00000006, 0x000000aa, 0x0000001f, 0x000000a9, 0x000500ba, 0x0000001a, 0x000000ad,
|
||||
0x000000a5, 0x00000080, 0x000300f7, 0x000000b0, 0x00000000, 0x000400fa, 0x000000ad, 0x000000af,
|
||||
0x000000b8, 0x000200f8, 0x000000af, 0x000500bc, 0x0000001a, 0x0000012a, 0x000000a5, 0x00000019,
|
||||
0x000300f7, 0x00000136, 0x00000000, 0x000400fa, 0x0000012a, 0x0000012b, 0x00000130, 0x000200f8,
|
||||
0x0000012b, 0x000200f9, 0x00000136, 0x000200f8, 0x00000130, 0x00050083, 0x00000006, 0x00000133,
|
||||
0x000000a5, 0x00000019, 0x00050085, 0x00000006, 0x00000134, 0x00000026, 0x00000133, 0x00050081,
|
||||
0x00000006, 0x00000135, 0x00000017, 0x00000134, 0x000200f9, 0x00000136, 0x000200f8, 0x00000136,
|
||||
0x000700f5, 0x00000006, 0x000001b7, 0x000000aa, 0x0000012b, 0x00000135, 0x00000130, 0x00050088,
|
||||
0x00000006, 0x000000b6, 0x000001b7, 0x000000a5, 0x0005008e, 0x0000000c, 0x000000b7, 0x00000062,
|
||||
0x000000b6, 0x000200f9, 0x000000b0, 0x000200f8, 0x000000b8, 0x000200f9, 0x000000b0, 0x000200f8,
|
||||
0x000000b0, 0x000700f5, 0x0000000c, 0x000001b8, 0x000000b7, 0x00000136, 0x00000062, 0x000000b8,
|
||||
0x000500bc, 0x0000001a, 0x0000014c, 0x00000096, 0x00000019, 0x000300f7, 0x00000158, 0x00000000,
|
||||
0x000400fa, 0x0000014c, 0x0000014d, 0x00000152, 0x000200f8, 0x0000014d, 0x0004007f, 0x00000006,
|
||||
0x0000014f, 0x00000096, 0x0006000c, 0x00000006, 0x00000150, 0x00000001, 0x0000001b, 0x0000014f,
|
||||
0x00050083, 0x00000006, 0x00000151, 0x0000001f, 0x00000150, 0x000200f9, 0x00000158, 0x000200f8,
|
||||
0x00000152, 0x00050083, 0x00000006, 0x00000155, 0x00000096, 0x00000019, 0x00050085, 0x00000006,
|
||||
0x00000156, 0x00000026, 0x00000155, 0x00050081, 0x00000006, 0x00000157, 0x00000017, 0x00000156,
|
||||
0x000200f9, 0x00000158, 0x000200f8, 0x00000158, 0x000700f5, 0x00000006, 0x000001b9, 0x00000151,
|
||||
0x0000014d, 0x00000157, 0x00000152, 0x000500bc, 0x0000001a, 0x0000015f, 0x0000009a, 0x00000019,
|
||||
0x000300f7, 0x0000016b, 0x00000000, 0x000400fa, 0x0000015f, 0x00000160, 0x00000165, 0x000200f8,
|
||||
0x00000160, 0x0004007f, 0x00000006, 0x00000162, 0x0000009a, 0x0006000c, 0x00000006, 0x00000163,
|
||||
0x00000001, 0x0000001b, 0x00000162, 0x00050083, 0x00000006, 0x00000164, 0x0000001f, 0x00000163,
|
||||
0x000200f9, 0x0000016b, 0x000200f8, 0x00000165, 0x00050083, 0x00000006, 0x00000168, 0x0000009a,
|
||||
0x00000019, 0x00050085, 0x00000006, 0x00000169, 0x00000026, 0x00000168, 0x00050081, 0x00000006,
|
||||
0x0000016a, 0x00000017, 0x00000169, 0x000200f9, 0x0000016b, 0x000200f8, 0x0000016b, 0x000700f5,
|
||||
0x00000006, 0x000001ba, 0x00000164, 0x00000160, 0x0000016a, 0x00000165, 0x000500bc, 0x0000001a,
|
||||
0x00000172, 0x000000a0, 0x00000019, 0x000300f7, 0x0000017e, 0x00000000, 0x000400fa, 0x00000172,
|
||||
0x00000173, 0x00000178, 0x000200f8, 0x00000173, 0x0004007f, 0x00000006, 0x00000175, 0x000000a0,
|
||||
0x0006000c, 0x00000006, 0x00000176, 0x00000001, 0x0000001b, 0x00000175, 0x00050083, 0x00000006,
|
||||
0x00000177, 0x0000001f, 0x00000176, 0x000200f9, 0x0000017e, 0x000200f8, 0x00000178, 0x00050083,
|
||||
0x00000006, 0x0000017b, 0x000000a0, 0x00000019, 0x00050085, 0x00000006, 0x0000017c, 0x00000026,
|
||||
0x0000017b, 0x00050081, 0x00000006, 0x0000017d, 0x00000017, 0x0000017c, 0x000200f9, 0x0000017e,
|
||||
0x000200f8, 0x0000017e, 0x000700f5, 0x00000006, 0x000001bb, 0x00000177, 0x00000173, 0x0000017d,
|
||||
0x00000178, 0x00060050, 0x0000000c, 0x00000146, 0x000001b9, 0x000001ba, 0x000001bb, 0x00050085,
|
||||
0x00000006, 0x000000c2, 0x000000aa, 0x000000aa, 0x00060050, 0x0000000c, 0x000000c3, 0x000000c2,
|
||||
0x000000c2, 0x000000c2, 0x0008000c, 0x0000000c, 0x000000c4, 0x00000001, 0x0000002e, 0x000001b8,
|
||||
0x00000146, 0x000000c3, 0x0007000c, 0x0000000c, 0x000000c6, 0x00000001, 0x00000028, 0x000000c4,
|
||||
0x000000c5, 0x00050051, 0x00000006, 0x00000183, 0x000000c6, 0x00000000, 0x00050051, 0x00000006,
|
||||
0x00000185, 0x000000c6, 0x00000001, 0x00050051, 0x00000006, 0x00000187, 0x000000c6, 0x00000002,
|
||||
0x0007000c, 0x00000006, 0x00000188, 0x00000001, 0x00000028, 0x00000185, 0x00000187, 0x0007000c,
|
||||
0x00000006, 0x00000189, 0x00000001, 0x00000028, 0x00000183, 0x00000188, 0x0007000c, 0x00000006,
|
||||
0x000000cb, 0x00000001, 0x00000028, 0x00000189, 0x0000001f, 0x00060050, 0x0000000c, 0x000000cf,
|
||||
0x000000cb, 0x000000cb, 0x000000cb, 0x00050088, 0x0000000c, 0x000000d0, 0x000000c6, 0x000000cf,
|
||||
0x00050051, 0x00000006, 0x0000018d, 0x000000d0, 0x00000000, 0x00050051, 0x00000006, 0x0000018f,
|
||||
0x000000d0, 0x00000001, 0x00050051, 0x00000006, 0x00000191, 0x000000d0, 0x00000002, 0x0007000c,
|
||||
0x00000006, 0x00000192, 0x00000001, 0x00000028, 0x0000018f, 0x00000191, 0x0007000c, 0x00000006,
|
||||
0x00000193, 0x00000001, 0x00000028, 0x0000018d, 0x00000192, 0x00050085, 0x00000006, 0x000000d9,
|
||||
0x0000018d, 0x000000d8, 0x00050081, 0x00000006, 0x000000de, 0x0000018f, 0x00000191, 0x00050051,
|
||||
0x00000006, 0x000000e0, 0x0000006e, 0x00000002, 0x00050085, 0x00000006, 0x000000e1, 0x000000de,
|
||||
0x000000e0, 0x00050081, 0x00000006, 0x000000e2, 0x000000d9, 0x000000e1, 0x00050083, 0x00000006,
|
||||
0x000000e3, 0x000000e2, 0x0000001f, 0x00050051, 0x00000006, 0x000000e6, 0x00000075, 0x00000001,
|
||||
0x00050085, 0x00000006, 0x000000e9, 0x000000e3, 0x000000e3, 0x00050083, 0x00000006, 0x000000ea,
|
||||
0x0000001f, 0x000000e9, 0x00050085, 0x00000006, 0x000000eb, 0x000000e6, 0x000000ea, 0x00050051,
|
||||
0x00000006, 0x000000ed, 0x00000075, 0x00000000, 0x00050081, 0x00000006, 0x000000ee, 0x000000eb,
|
||||
0x000000ed, 0x00060050, 0x0000000c, 0x000000f2, 0x00000193, 0x00000193, 0x00000193, 0x00050083,
|
||||
0x0000000c, 0x000000f3, 0x000000d0, 0x000000f2, 0x0005008e, 0x0000000c, 0x000000f5, 0x000000f3,
|
||||
0x000000ee, 0x00050081, 0x0000000c, 0x000000f8, 0x000000f5, 0x000000f2, 0x0005008e, 0x0000000c,
|
||||
0x000000f9, 0x000000f8, 0x000000cb, 0x00050051, 0x00000006, 0x00000197, 0x000000f9, 0x00000000,
|
||||
0x00050051, 0x00000006, 0x00000199, 0x000000f9, 0x00000001, 0x00050051, 0x00000006, 0x0000019b,
|
||||
0x000000f9, 0x00000002, 0x0007000c, 0x00000006, 0x0000019c, 0x00000001, 0x00000028, 0x00000199,
|
||||
0x0000019b, 0x0007000c, 0x00000006, 0x0000019d, 0x00000001, 0x00000028, 0x00000197, 0x0000019c,
|
||||
0x00050085, 0x00000006, 0x00000101, 0x000000ff, 0x000001be, 0x000500ba, 0x0000001a, 0x00000104,
|
||||
0x0000019d, 0x00000101, 0x000300f7, 0x00000106, 0x00000000, 0x000400fa, 0x00000104, 0x00000105,
|
||||
0x00000106, 0x000200f8, 0x00000105, 0x00050083, 0x00000006, 0x0000010a, 0x000001be, 0x00000101,
|
||||
0x00050083, 0x00000006, 0x0000010f, 0x0000019d, 0x00000101, 0x0004007f, 0x00000006, 0x00000110,
|
||||
0x0000010f, 0x00050088, 0x00000006, 0x00000112, 0x00000110, 0x0000010a, 0x0006000c, 0x00000006,
|
||||
0x00000113, 0x00000001, 0x0000001b, 0x00000112, 0x00050083, 0x00000006, 0x00000114, 0x0000001f,
|
||||
0x00000113, 0x00050085, 0x00000006, 0x00000115, 0x0000010a, 0x00000114, 0x00050081, 0x00000006,
|
||||
0x00000116, 0x00000101, 0x00000115, 0x00050088, 0x00000006, 0x00000118, 0x00000116, 0x0000019d,
|
||||
0x0005008e, 0x0000000c, 0x0000011a, 0x000000f9, 0x00000118, 0x000200f9, 0x00000106, 0x000200f8,
|
||||
0x00000106, 0x000700f5, 0x0000000c, 0x000001bc, 0x000000f9, 0x0000017e, 0x0000011a, 0x00000105,
|
||||
0x0007000c, 0x0000000c, 0x0000011e, 0x00000001, 0x00000028, 0x000001bc, 0x000000c5, 0x0005008e,
|
||||
0x0000000c, 0x00000120, 0x0000011e, 0x000001bd, 0x00050051, 0x00000006, 0x00000121, 0x00000120,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x00000122, 0x00000120, 0x00000001, 0x00050051, 0x00000006,
|
||||
0x00000123, 0x00000120, 0x00000002, 0x00070050, 0x00000051, 0x00000124, 0x00000121, 0x00000122,
|
||||
0x00000123, 0x00000080, 0x0003003e, 0x0000011c, 0x00000124, 0x000100fd, 0x00010038,
|
||||
0x00000006, 0x000000a5, 0x00000001, 0x00000028, 0x000000a4, 0x00000080, 0x0003003e, 0x00000094,
|
||||
0x000000a5, 0x0004003d, 0x00000006, 0x000000a7, 0x00000094, 0x0004007f, 0x00000006, 0x000000a8,
|
||||
0x000000a7, 0x0006000c, 0x00000006, 0x000000a9, 0x00000001, 0x0000001b, 0x000000a8, 0x00050083,
|
||||
0x00000006, 0x000000aa, 0x0000001f, 0x000000a9, 0x0003003e, 0x000000a6, 0x000000aa, 0x0004003d,
|
||||
0x00000006, 0x000000ac, 0x00000094, 0x000500ba, 0x0000001a, 0x000000ad, 0x000000ac, 0x00000080,
|
||||
0x000300f7, 0x000000b0, 0x00000000, 0x000400fa, 0x000000ad, 0x000000af, 0x000000b8, 0x000200f8,
|
||||
0x000000af, 0x0004003d, 0x0000000c, 0x000000b1, 0x0000004b, 0x0004003d, 0x00000006, 0x000000b3,
|
||||
0x00000094, 0x0003003e, 0x000000b2, 0x000000b3, 0x00050039, 0x00000006, 0x000000b4, 0x0000000a,
|
||||
0x000000b2, 0x0004003d, 0x00000006, 0x000000b5, 0x00000094, 0x00050088, 0x00000006, 0x000000b6,
|
||||
0x000000b4, 0x000000b5, 0x0005008e, 0x0000000c, 0x000000b7, 0x000000b1, 0x000000b6, 0x0003003e,
|
||||
0x000000ae, 0x000000b7, 0x000200f9, 0x000000b0, 0x000200f8, 0x000000b8, 0x0004003d, 0x0000000c,
|
||||
0x000000b9, 0x0000004b, 0x0003003e, 0x000000ae, 0x000000b9, 0x000200f9, 0x000000b0, 0x000200f8,
|
||||
0x000000b0, 0x0004003d, 0x0000000c, 0x000000ba, 0x000000ae, 0x0003003e, 0x000000ab, 0x000000ba,
|
||||
0x0004003d, 0x0000000c, 0x000000bc, 0x000000ab, 0x0004003d, 0x0000000c, 0x000000be, 0x0000004b,
|
||||
0x0003003e, 0x000000bd, 0x000000be, 0x00050039, 0x0000000c, 0x000000bf, 0x00000010, 0x000000bd,
|
||||
0x0004003d, 0x00000006, 0x000000c0, 0x000000a6, 0x0004003d, 0x00000006, 0x000000c1, 0x000000a6,
|
||||
0x00050085, 0x00000006, 0x000000c2, 0x000000c0, 0x000000c1, 0x00060050, 0x0000000c, 0x000000c3,
|
||||
0x000000c2, 0x000000c2, 0x000000c2, 0x0008000c, 0x0000000c, 0x000000c4, 0x00000001, 0x0000002e,
|
||||
0x000000bc, 0x000000bf, 0x000000c3, 0x0007000c, 0x0000000c, 0x000000c6, 0x00000001, 0x00000028,
|
||||
0x000000c4, 0x000000c5, 0x0003003e, 0x000000bb, 0x000000c6, 0x0004003d, 0x0000000c, 0x000000c9,
|
||||
0x000000bb, 0x0003003e, 0x000000c8, 0x000000c9, 0x00050039, 0x00000006, 0x000000ca, 0x00000014,
|
||||
0x000000c8, 0x0007000c, 0x00000006, 0x000000cb, 0x00000001, 0x00000028, 0x000000ca, 0x0000001f,
|
||||
0x0003003e, 0x000000c7, 0x000000cb, 0x0004003d, 0x0000000c, 0x000000cd, 0x000000bb, 0x0004003d,
|
||||
0x00000006, 0x000000ce, 0x000000c7, 0x00060050, 0x0000000c, 0x000000cf, 0x000000ce, 0x000000ce,
|
||||
0x000000ce, 0x00050088, 0x0000000c, 0x000000d0, 0x000000cd, 0x000000cf, 0x0003003e, 0x000000cc,
|
||||
0x000000d0, 0x0004003d, 0x0000000c, 0x000000d3, 0x000000cc, 0x0003003e, 0x000000d2, 0x000000d3,
|
||||
0x00050039, 0x00000006, 0x000000d4, 0x00000014, 0x000000d2, 0x0003003e, 0x000000d1, 0x000000d4,
|
||||
0x00050041, 0x00000007, 0x000000d6, 0x000000cc, 0x00000030, 0x0004003d, 0x00000006, 0x000000d7,
|
||||
0x000000d6, 0x00050085, 0x00000006, 0x000000d9, 0x000000d7, 0x000000d8, 0x00050041, 0x00000007,
|
||||
0x000000da, 0x000000cc, 0x00000035, 0x0004003d, 0x00000006, 0x000000db, 0x000000da, 0x00050041,
|
||||
0x00000007, 0x000000dc, 0x000000cc, 0x0000003a, 0x0004003d, 0x00000006, 0x000000dd, 0x000000dc,
|
||||
0x00050081, 0x00000006, 0x000000de, 0x000000db, 0x000000dd, 0x00050041, 0x00000007, 0x000000df,
|
||||
0x00000064, 0x0000003a, 0x0004003d, 0x00000006, 0x000000e0, 0x000000df, 0x00050085, 0x00000006,
|
||||
0x000000e1, 0x000000de, 0x000000e0, 0x00050081, 0x00000006, 0x000000e2, 0x000000d9, 0x000000e1,
|
||||
0x00050083, 0x00000006, 0x000000e3, 0x000000e2, 0x0000001f, 0x0003003e, 0x000000d5, 0x000000e3,
|
||||
0x00050041, 0x00000007, 0x000000e5, 0x0000006f, 0x00000035, 0x0004003d, 0x00000006, 0x000000e6,
|
||||
0x000000e5, 0x0004003d, 0x00000006, 0x000000e7, 0x000000d5, 0x0004003d, 0x00000006, 0x000000e8,
|
||||
0x000000d5, 0x00050085, 0x00000006, 0x000000e9, 0x000000e7, 0x000000e8, 0x00050083, 0x00000006,
|
||||
0x000000ea, 0x0000001f, 0x000000e9, 0x00050085, 0x00000006, 0x000000eb, 0x000000e6, 0x000000ea,
|
||||
0x00050041, 0x00000007, 0x000000ec, 0x0000006f, 0x00000030, 0x0004003d, 0x00000006, 0x000000ed,
|
||||
0x000000ec, 0x00050081, 0x00000006, 0x000000ee, 0x000000eb, 0x000000ed, 0x0003003e, 0x000000e4,
|
||||
0x000000ee, 0x0004003d, 0x00000006, 0x000000ef, 0x000000c7, 0x0004003d, 0x0000000c, 0x000000f0,
|
||||
0x000000cc, 0x0004003d, 0x00000006, 0x000000f1, 0x000000d1, 0x00060050, 0x0000000c, 0x000000f2,
|
||||
0x000000f1, 0x000000f1, 0x000000f1, 0x00050083, 0x0000000c, 0x000000f3, 0x000000f0, 0x000000f2,
|
||||
0x0004003d, 0x00000006, 0x000000f4, 0x000000e4, 0x0005008e, 0x0000000c, 0x000000f5, 0x000000f3,
|
||||
0x000000f4, 0x0004003d, 0x00000006, 0x000000f6, 0x000000d1, 0x00060050, 0x0000000c, 0x000000f7,
|
||||
0x000000f6, 0x000000f6, 0x000000f6, 0x00050081, 0x0000000c, 0x000000f8, 0x000000f5, 0x000000f7,
|
||||
0x0005008e, 0x0000000c, 0x000000f9, 0x000000f8, 0x000000ef, 0x0003003e, 0x000000bb, 0x000000f9,
|
||||
0x0004003d, 0x00000006, 0x000000fb, 0x00000094, 0x000500ba, 0x0000001a, 0x000000fc, 0x000000fb,
|
||||
0x00000080, 0x000300f7, 0x000000ff, 0x00000000, 0x000400fa, 0x000000fc, 0x000000fe, 0x00000105,
|
||||
0x000200f8, 0x000000fe, 0x0004003d, 0x0000000c, 0x00000100, 0x0000004b, 0x0004003d, 0x00000006,
|
||||
0x00000101, 0x000000a6, 0x0004003d, 0x00000006, 0x00000102, 0x00000094, 0x00050088, 0x00000006,
|
||||
0x00000103, 0x00000101, 0x00000102, 0x0005008e, 0x0000000c, 0x00000104, 0x00000100, 0x00000103,
|
||||
0x0003003e, 0x000000fd, 0x00000104, 0x000200f9, 0x000000ff, 0x000200f8, 0x00000105, 0x0004003d,
|
||||
0x0000000c, 0x00000106, 0x0000004b, 0x0003003e, 0x000000fd, 0x00000106, 0x000200f9, 0x000000ff,
|
||||
0x000200f8, 0x000000ff, 0x0004003d, 0x0000000c, 0x00000107, 0x000000fd, 0x0004003d, 0x0000000c,
|
||||
0x00000109, 0x0000004b, 0x0004007f, 0x0000000c, 0x0000010a, 0x00000109, 0x0006000c, 0x0000000c,
|
||||
0x0000010b, 0x00000001, 0x0000001b, 0x0000010a, 0x00050083, 0x0000000c, 0x0000010c, 0x00000108,
|
||||
0x0000010b, 0x0004003d, 0x00000006, 0x0000010d, 0x000000a6, 0x0004003d, 0x00000006, 0x0000010e,
|
||||
0x000000a6, 0x00050085, 0x00000006, 0x0000010f, 0x0000010d, 0x0000010e, 0x00060050, 0x0000000c,
|
||||
0x00000110, 0x0000010f, 0x0000010f, 0x0000010f, 0x0008000c, 0x0000000c, 0x00000111, 0x00000001,
|
||||
0x0000002e, 0x00000107, 0x0000010c, 0x00000110, 0x00060050, 0x0000000c, 0x00000112, 0x00000080,
|
||||
0x00000080, 0x00000080, 0x00060050, 0x0000000c, 0x00000113, 0x0000001f, 0x0000001f, 0x0000001f,
|
||||
0x0008000c, 0x0000000c, 0x00000114, 0x00000001, 0x0000002b, 0x00000111, 0x00000112, 0x00000113,
|
||||
0x0003003e, 0x000000fa, 0x00000114, 0x0004003d, 0x0000000c, 0x00000117, 0x000000fa, 0x0003003e,
|
||||
0x00000116, 0x00000117, 0x00050039, 0x00000006, 0x00000118, 0x00000014, 0x00000116, 0x0003003e,
|
||||
0x00000115, 0x00000118, 0x00050041, 0x00000007, 0x0000011a, 0x000000fa, 0x00000030, 0x0004003d,
|
||||
0x00000006, 0x0000011b, 0x0000011a, 0x00050085, 0x00000006, 0x0000011c, 0x0000011b, 0x000000d8,
|
||||
0x00050041, 0x00000007, 0x0000011d, 0x000000fa, 0x00000035, 0x0004003d, 0x00000006, 0x0000011e,
|
||||
0x0000011d, 0x00050041, 0x00000007, 0x0000011f, 0x000000fa, 0x0000003a, 0x0004003d, 0x00000006,
|
||||
0x00000120, 0x0000011f, 0x00050081, 0x00000006, 0x00000121, 0x0000011e, 0x00000120, 0x00050041,
|
||||
0x00000007, 0x00000122, 0x00000064, 0x0000003a, 0x0004003d, 0x00000006, 0x00000123, 0x00000122,
|
||||
0x00050085, 0x00000006, 0x00000124, 0x00000121, 0x00000123, 0x00050081, 0x00000006, 0x00000125,
|
||||
0x0000011c, 0x00000124, 0x00050083, 0x00000006, 0x00000126, 0x00000125, 0x0000001f, 0x0003003e,
|
||||
0x00000119, 0x00000126, 0x0004003d, 0x0000000c, 0x00000127, 0x000000fa, 0x0004003d, 0x00000006,
|
||||
0x00000128, 0x00000115, 0x00060050, 0x0000000c, 0x00000129, 0x00000128, 0x00000128, 0x00000128,
|
||||
0x00050083, 0x0000000c, 0x0000012a, 0x00000127, 0x00000129, 0x00050041, 0x00000007, 0x0000012b,
|
||||
0x0000006f, 0x00000035, 0x0004003d, 0x00000006, 0x0000012c, 0x0000012b, 0x0004003d, 0x00000006,
|
||||
0x0000012d, 0x00000119, 0x0004003d, 0x00000006, 0x0000012e, 0x00000119, 0x00050085, 0x00000006,
|
||||
0x0000012f, 0x0000012d, 0x0000012e, 0x00050083, 0x00000006, 0x00000130, 0x0000001f, 0x0000012f,
|
||||
0x00050085, 0x00000006, 0x00000131, 0x0000012c, 0x00000130, 0x00050041, 0x00000007, 0x00000132,
|
||||
0x0000006f, 0x00000030, 0x0004003d, 0x00000006, 0x00000133, 0x00000132, 0x00050081, 0x00000006,
|
||||
0x00000134, 0x00000131, 0x00000133, 0x0005008e, 0x0000000c, 0x00000135, 0x0000012a, 0x00000134,
|
||||
0x0004003d, 0x00000006, 0x00000136, 0x00000115, 0x00060050, 0x0000000c, 0x00000137, 0x00000136,
|
||||
0x00000136, 0x00000136, 0x00050081, 0x0000000c, 0x00000138, 0x00000135, 0x00000137, 0x0003003e,
|
||||
0x000000fa, 0x00000138, 0x00060041, 0x0000007d, 0x0000013a, 0x0000007b, 0x0000007c, 0x0000003a,
|
||||
0x0004003d, 0x00000006, 0x0000013b, 0x0000013a, 0x0008000c, 0x00000006, 0x0000013c, 0x00000001,
|
||||
0x0000002b, 0x0000013b, 0x00000080, 0x0000001f, 0x0003003e, 0x00000139, 0x0000013c, 0x0004003d,
|
||||
0x0000000c, 0x0000013e, 0x000000bb, 0x00050094, 0x00000006, 0x00000143, 0x0000013e, 0x00000142,
|
||||
0x0003003e, 0x0000013d, 0x00000143, 0x0004003d, 0x0000000c, 0x00000145, 0x000000fa, 0x00050094,
|
||||
0x00000006, 0x00000146, 0x00000145, 0x00000142, 0x0003003e, 0x00000144, 0x00000146, 0x0004003d,
|
||||
0x00000006, 0x00000147, 0x00000139, 0x000500ba, 0x0000001a, 0x00000148, 0x00000147, 0x00000080,
|
||||
0x0004003d, 0x00000006, 0x00000149, 0x00000144, 0x000500ba, 0x0000001a, 0x0000014b, 0x00000149,
|
||||
0x0000014a, 0x000500a7, 0x0000001a, 0x0000014c, 0x00000148, 0x0000014b, 0x000300f7, 0x0000014e,
|
||||
0x00000000, 0x000400fa, 0x0000014c, 0x0000014d, 0x0000014e, 0x000200f8, 0x0000014d, 0x0004003d,
|
||||
0x0000000c, 0x0000014f, 0x000000bb, 0x0004003d, 0x0000000c, 0x00000150, 0x000000fa, 0x0007000c,
|
||||
0x0000000c, 0x00000151, 0x00000001, 0x00000028, 0x00000150, 0x000000c5, 0x0004003d, 0x00000006,
|
||||
0x00000152, 0x0000013d, 0x0004003d, 0x00000006, 0x00000153, 0x00000144, 0x00050088, 0x00000006,
|
||||
0x00000154, 0x00000152, 0x00000153, 0x0005008e, 0x0000000c, 0x00000155, 0x00000151, 0x00000154,
|
||||
0x0004003d, 0x00000006, 0x00000156, 0x00000139, 0x0004003d, 0x0000000c, 0x0000015a, 0x000000bb,
|
||||
0x0003003e, 0x00000159, 0x0000015a, 0x00050039, 0x00000006, 0x0000015b, 0x00000014, 0x00000159,
|
||||
0x0008000c, 0x00000006, 0x0000015c, 0x00000001, 0x00000031, 0x00000157, 0x00000158, 0x0000015b,
|
||||
0x00050085, 0x00000006, 0x0000015d, 0x00000156, 0x0000015c, 0x00060050, 0x0000000c, 0x0000015e,
|
||||
0x0000015d, 0x0000015d, 0x0000015d, 0x0008000c, 0x0000000c, 0x0000015f, 0x00000001, 0x0000002e,
|
||||
0x0000014f, 0x00000155, 0x0000015e, 0x0003003e, 0x000000bb, 0x0000015f, 0x000200f9, 0x0000014e,
|
||||
0x000200f8, 0x0000014e, 0x0004003d, 0x0000000c, 0x00000162, 0x000000bb, 0x0003003e, 0x00000161,
|
||||
0x00000162, 0x00050039, 0x00000006, 0x00000163, 0x00000014, 0x00000161, 0x0003003e, 0x00000160,
|
||||
0x00000163, 0x0004003d, 0x00000006, 0x00000165, 0x00000089, 0x00050085, 0x00000006, 0x00000166,
|
||||
0x00000157, 0x00000165, 0x0003003e, 0x00000164, 0x00000166, 0x0004003d, 0x00000006, 0x00000167,
|
||||
0x00000160, 0x0004003d, 0x00000006, 0x00000168, 0x00000164, 0x000500ba, 0x0000001a, 0x00000169,
|
||||
0x00000167, 0x00000168, 0x000300f7, 0x0000016b, 0x00000000, 0x000400fa, 0x00000169, 0x0000016a,
|
||||
0x0000016b, 0x000200f8, 0x0000016a, 0x0004003d, 0x00000006, 0x0000016d, 0x00000089, 0x0004003d,
|
||||
0x00000006, 0x0000016e, 0x00000164, 0x00050083, 0x00000006, 0x0000016f, 0x0000016d, 0x0000016e,
|
||||
0x0003003e, 0x0000016c, 0x0000016f, 0x0004003d, 0x00000006, 0x00000170, 0x00000164, 0x0004003d,
|
||||
0x00000006, 0x00000171, 0x0000016c, 0x0004003d, 0x00000006, 0x00000172, 0x00000160, 0x0004003d,
|
||||
0x00000006, 0x00000173, 0x00000164, 0x00050083, 0x00000006, 0x00000174, 0x00000172, 0x00000173,
|
||||
0x0004007f, 0x00000006, 0x00000175, 0x00000174, 0x0004003d, 0x00000006, 0x00000176, 0x0000016c,
|
||||
0x00050088, 0x00000006, 0x00000177, 0x00000175, 0x00000176, 0x0006000c, 0x00000006, 0x00000178,
|
||||
0x00000001, 0x0000001b, 0x00000177, 0x00050083, 0x00000006, 0x00000179, 0x0000001f, 0x00000178,
|
||||
0x00050085, 0x00000006, 0x0000017a, 0x00000171, 0x00000179, 0x00050081, 0x00000006, 0x0000017b,
|
||||
0x00000170, 0x0000017a, 0x0004003d, 0x00000006, 0x0000017c, 0x00000160, 0x00050088, 0x00000006,
|
||||
0x0000017d, 0x0000017b, 0x0000017c, 0x0004003d, 0x0000000c, 0x0000017e, 0x000000bb, 0x0005008e,
|
||||
0x0000000c, 0x0000017f, 0x0000017e, 0x0000017d, 0x0003003e, 0x000000bb, 0x0000017f, 0x000200f9,
|
||||
0x0000016b, 0x000200f8, 0x0000016b, 0x0004003d, 0x0000000c, 0x00000182, 0x000000bb, 0x0007000c,
|
||||
0x0000000c, 0x00000183, 0x00000001, 0x00000028, 0x00000182, 0x000000c5, 0x0004003d, 0x00000006,
|
||||
0x00000184, 0x00000076, 0x0005008e, 0x0000000c, 0x00000185, 0x00000183, 0x00000184, 0x00050051,
|
||||
0x00000006, 0x00000186, 0x00000185, 0x00000000, 0x00050051, 0x00000006, 0x00000187, 0x00000185,
|
||||
0x00000001, 0x00050051, 0x00000006, 0x00000188, 0x00000185, 0x00000002, 0x00070050, 0x00000051,
|
||||
0x00000189, 0x00000186, 0x00000187, 0x00000188, 0x00000080, 0x0003003e, 0x00000181, 0x00000189,
|
||||
0x000100fd, 0x00010038, 0x00050036, 0x00000006, 0x0000000a, 0x00000000, 0x00000008, 0x00030037,
|
||||
0x00000007, 0x00000009, 0x000200f8, 0x0000000b, 0x0004003b, 0x00000007, 0x00000016, 0x00000007,
|
||||
0x0004003b, 0x00000007, 0x0000001c, 0x00000007, 0x0003003e, 0x00000016, 0x00000017, 0x0004003d,
|
||||
0x00000006, 0x00000018, 0x00000009, 0x000500bc, 0x0000001a, 0x0000001b, 0x00000018, 0x00000019,
|
||||
0x000300f7, 0x0000001e, 0x00000000, 0x000400fa, 0x0000001b, 0x0000001d, 0x00000024, 0x000200f8,
|
||||
0x0000001d, 0x0004003d, 0x00000006, 0x00000020, 0x00000009, 0x0004007f, 0x00000006, 0x00000021,
|
||||
0x00000020, 0x0006000c, 0x00000006, 0x00000022, 0x00000001, 0x0000001b, 0x00000021, 0x00050083,
|
||||
0x00000006, 0x00000023, 0x0000001f, 0x00000022, 0x0003003e, 0x0000001c, 0x00000023, 0x000200f9,
|
||||
0x0000001e, 0x000200f8, 0x00000024, 0x0004003d, 0x00000006, 0x00000025, 0x00000016, 0x0004003d,
|
||||
0x00000006, 0x00000027, 0x00000009, 0x00050083, 0x00000006, 0x00000028, 0x00000027, 0x00000019,
|
||||
0x00050085, 0x00000006, 0x00000029, 0x00000026, 0x00000028, 0x00050081, 0x00000006, 0x0000002a,
|
||||
0x00000025, 0x00000029, 0x0003003e, 0x0000001c, 0x0000002a, 0x000200f9, 0x0000001e, 0x000200f8,
|
||||
0x0000001e, 0x0004003d, 0x00000006, 0x0000002b, 0x0000001c, 0x000200fe, 0x0000002b, 0x00010038,
|
||||
0x00050036, 0x0000000c, 0x00000010, 0x00000000, 0x0000000e, 0x00030037, 0x0000000d, 0x0000000f,
|
||||
0x000200f8, 0x00000011, 0x0004003b, 0x00000007, 0x0000002e, 0x00000007, 0x0004003b, 0x00000007,
|
||||
0x00000034, 0x00000007, 0x0004003b, 0x00000007, 0x00000039, 0x00000007, 0x00050041, 0x00000007,
|
||||
0x00000031, 0x0000000f, 0x00000030, 0x0004003d, 0x00000006, 0x00000032, 0x00000031, 0x0003003e,
|
||||
0x0000002e, 0x00000032, 0x00050039, 0x00000006, 0x00000033, 0x0000000a, 0x0000002e, 0x00050041,
|
||||
0x00000007, 0x00000036, 0x0000000f, 0x00000035, 0x0004003d, 0x00000006, 0x00000037, 0x00000036,
|
||||
0x0003003e, 0x00000034, 0x00000037, 0x00050039, 0x00000006, 0x00000038, 0x0000000a, 0x00000034,
|
||||
0x00050041, 0x00000007, 0x0000003b, 0x0000000f, 0x0000003a, 0x0004003d, 0x00000006, 0x0000003c,
|
||||
0x0000003b, 0x0003003e, 0x00000039, 0x0000003c, 0x00050039, 0x00000006, 0x0000003d, 0x0000000a,
|
||||
0x00000039, 0x00060050, 0x0000000c, 0x0000003e, 0x00000033, 0x00000038, 0x0000003d, 0x000200fe,
|
||||
0x0000003e, 0x00010038, 0x00050036, 0x00000006, 0x00000014, 0x00000000, 0x00000012, 0x00030037,
|
||||
0x0000000d, 0x00000013, 0x000200f8, 0x00000015, 0x00050041, 0x00000007, 0x00000041, 0x00000013,
|
||||
0x00000030, 0x0004003d, 0x00000006, 0x00000042, 0x00000041, 0x00050041, 0x00000007, 0x00000043,
|
||||
0x00000013, 0x00000035, 0x0004003d, 0x00000006, 0x00000044, 0x00000043, 0x00050041, 0x00000007,
|
||||
0x00000045, 0x00000013, 0x0000003a, 0x0004003d, 0x00000006, 0x00000046, 0x00000045, 0x0007000c,
|
||||
0x00000006, 0x00000047, 0x00000001, 0x00000028, 0x00000044, 0x00000046, 0x0007000c, 0x00000006,
|
||||
0x00000048, 0x00000001, 0x00000028, 0x00000042, 0x00000047, 0x000200fe, 0x00000048, 0x00010038,
|
||||
};
|
||||
@@ -167,6 +167,9 @@ void mem_thaw(uint64_t a, uint64_t n); // makes the frozen r
|
||||
void mem_freeze_stats(uint64_t *frozen, uint64_t *freezes, uint64_t *fault_thaws, uint64_t *other_thaws);
|
||||
// regions frozen now; freezes and thaws since the last call
|
||||
void mem_kernel_free(void *p, uint64_t size); // TLR/TLS reuse after thread exit
|
||||
int mem_guest_readable(uint64_t a, uint64_t n); // [a, a+n) can be read without a fault: inside a loaded
|
||||
// module or the committed heap answered from the layout,
|
||||
// anything else by plat_mem_readable (an OS query)
|
||||
|
||||
// ---- modules (loader.c)
|
||||
typedef struct {
|
||||
@@ -262,6 +265,9 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); //
|
||||
void res_heapdef_served(void); // romfs.c serves the grown table
|
||||
void res_init(void); // after rt_patches_init: the patch page and the sets
|
||||
float res_dlss_lod_bias(void); // TOTK_DLSS upscaling: texture LOD bias (<= 0)
|
||||
int res_dlss_mode(const char *mode); // TOTK_DLSS changed while running: new scene size
|
||||
void res_fn_hit(uint64_t addr, uint64_t x0); // main.c fnprof_hit: catches the renderer at start
|
||||
void res_present(Cpu *c); // nvnQueuePresentTexture: applies a pending scene size
|
||||
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)
|
||||
@@ -309,6 +315,8 @@ void rt_boot(void); // boot.c
|
||||
int probe_cfg(void); // cfg_probe.c
|
||||
int probe_dpi(void); // dpi_probe_win.c (Windows only)
|
||||
int probe_mods(void); // mods_probe.c
|
||||
void discord_init(void); // discord_rpc.c: the Rich Presence thread, game runs only
|
||||
int probe_discord(void); // discord_rpc.c
|
||||
int probe_loader(void), probe_kernel(void), probe_ipc(void), probe_services(void), probe_fs(void);
|
||||
int probe_gfx_abi(void); // gfx_abi_probe.c
|
||||
int probe_vkdevice(void); // vk_device.c
|
||||
|
||||
+177
-19
@@ -26,6 +26,13 @@ enum { STICK_MAX = 32767, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 100
|
||||
|
||||
static PlatMutex lock = PLAT_MUTEX_INIT;
|
||||
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
|
||||
static volatile int32_t g_block, g_menu; // text entry / host menu: 0 off, 1 open, 2 waiting for release
|
||||
// The device the player used last (input_kbm_last): 1 keyboard and mouse, 0 the gamepad. A pad counts once a button is
|
||||
// down or a stick is past half way (a drifting stick never switches), the mouse once it moved MOUSE_USED raw counts
|
||||
// within one poll (a slow deliberate move does; a desk bump does not), the keyboard with any key or mouse button.
|
||||
enum { PAD_USED = 16384, MOUSE_USED = 6 };
|
||||
static volatile int32_t g_kbm_last = 1;
|
||||
int input_kbm_last(void) { return __atomic_load_n(&g_kbm_last, __ATOMIC_RELAXED); }
|
||||
static SDL_Gamepad *pad;
|
||||
static uint64_t next_scan;
|
||||
static int pad_rumble; // the open gamepad has rumble motors
|
||||
@@ -66,6 +73,7 @@ static const struct { const char *name; int bit; } script_buttons[] = {
|
||||
static ScriptEv *script_ev;
|
||||
static int script_n, script_state; // 0 not loaded, 1 loaded
|
||||
static int script_mixed; // the script has timed lines: its untimed ones end where the world clock starts
|
||||
static int32_t script_mouse[2]; // one-shot raw counts, consumed by the camera hook; under lock
|
||||
|
||||
static void script_load(void) {
|
||||
script_state = 1;
|
||||
@@ -88,10 +96,11 @@ static void script_load(void) {
|
||||
ScriptEv e = {.at = (uint64_t)(at * unit + 0.5), .hold = timed ? 66667 : 4, .bit = -1, .timed = timed};
|
||||
long a, b;
|
||||
double c;
|
||||
if (!strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
|
||||
if (!strcmp(name, "MOUSE") || !strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
|
||||
int n = sscanf(line + off, "%ld %ld %lf", &a, &b, &c);
|
||||
if (n < 2) { rt_log("input: script line %d: %s needs x y [hold]", ln, name); continue; }
|
||||
e.stick = name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
|
||||
e.stick = name[0] == 'M' ? 3 : name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
|
||||
if (e.stick == 3) e.hold = 1; // a displacement, never a held speed
|
||||
if (n >= 3 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
|
||||
snprintf(e.text, sizeof e.text, "%s %d %d", name, e.x, e.y);
|
||||
} else {
|
||||
@@ -123,12 +132,17 @@ static void script_apply(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
|
||||
if (e->timed ? w < 0 : script_mixed && w >= 0) continue;
|
||||
uint64_t now = e->timed ? (uint64_t)w : tick;
|
||||
if (now < e->at || (now >= e->at + e->hold && (e->logged || !e->timed))) continue;
|
||||
if (e->stick == 3 && e->logged) continue;
|
||||
if (!e->logged) {
|
||||
e->logged = 1;
|
||||
if (e->timed) rt_log("input: script frame %llu at %.1f ms: %s", (unsigned long long)fr, w / 1000.0, e->text);
|
||||
else rt_log("input: script frame %llu tick %llu: %s", (unsigned long long)fr, (unsigned long long)tick, e->text);
|
||||
}
|
||||
if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
|
||||
if (e->stick == 3) {
|
||||
int64_t x = (int64_t)script_mouse[0] + e->x, y = (int64_t)script_mouse[1] + e->y;
|
||||
script_mouse[0] = (int32_t)(x < INT32_MIN ? INT32_MIN : x > INT32_MAX ? INT32_MAX : x);
|
||||
script_mouse[1] = (int32_t)(y < INT32_MIN ? INT32_MIN : y > INT32_MAX ? INT32_MAX : y);
|
||||
} else if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
|
||||
else *b |= 1ull << e->bit;
|
||||
}
|
||||
}
|
||||
@@ -223,6 +237,30 @@ static void rec_apply(uint64_t b, const int32_t sl[2], const int32_t sr[2]) {
|
||||
if (wrote) fflush(rec);
|
||||
}
|
||||
|
||||
void input_direct_mouse_sample(int enabled, int32_t *dx, int32_t *dy) {
|
||||
plat_mutex_lock(&lock);
|
||||
enabled = enabled && !g_block && !g_menu;
|
||||
*dx = script_mouse[0]; *dy = script_mouse[1];
|
||||
script_mouse[0] = script_mouse[1] = 0;
|
||||
if (!enabled) { *dx = *dy = 0; }
|
||||
else if (ikb_captured() && ikb_foreground() && input_cursor_allowed()) {
|
||||
int32_t x, y;
|
||||
ikb_mouse_take(&x, &y);
|
||||
int64_t sx = (int64_t)*dx + x, sy = (int64_t)*dy + y;
|
||||
*dx = (int32_t)(sx < INT32_MIN ? INT32_MIN : sx > INT32_MAX ? INT32_MAX : sx);
|
||||
*dy = (int32_t)(sy < INT32_MIN ? INT32_MIN : sy > INT32_MAX ? INT32_MAX : sy);
|
||||
}
|
||||
if (enabled && (*dx || *dy) && rec_state == 1) {
|
||||
char what[80];
|
||||
snprintf(what, sizeof what, "MOUSE %d %d", *dx, *dy);
|
||||
int64_t w = frame_step_world_us();
|
||||
uint64_t tick = frame_step_ticks();
|
||||
rec_line(tick, w, tick + 1, w, what);
|
||||
fflush(rec);
|
||||
}
|
||||
plat_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
static float mouse_sens = -1; // TOTK_MOUSE_SENS, 0 = mouse camera off; live (cfg_version)
|
||||
static unsigned mouse_sens_seen;
|
||||
|
||||
@@ -282,15 +320,41 @@ static void walk_get(void) {
|
||||
}
|
||||
}
|
||||
|
||||
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`. The sample is divided by
|
||||
// the camera speed factor the game runs with (camera_speed.c, cam_speed_applied), which multiplies every right-stick
|
||||
// turn rate, so TOTK_MOUSE_SENS alone sets the mouse's speed; below 1 the sample stays within the stick's range, so the
|
||||
// mouse's top speed drops with the factor.
|
||||
static void mouse_camera(int32_t out[2]) {
|
||||
static int mouse_selector;
|
||||
int input_direct_mouse_selector_active(void) { return __atomic_load_n(&mouse_selector, __ATOMIC_RELAXED) != 0; }
|
||||
|
||||
// Selector 1 is a radial wheel, 2..4 are distinct horizontal quick lists; only a wheel retains a virtual cursor.
|
||||
// Under `lock`.
|
||||
static void mouse_camera(int32_t out[2], int selector) {
|
||||
static uint64_t frame = ~0ull, t0;
|
||||
static int32_t held[2];
|
||||
static int was_selector;
|
||||
static float cursor[2];
|
||||
static InputMouseRow row;
|
||||
uint64_t now = plat_now_ns();
|
||||
int32_t dx, dy;
|
||||
if (!input_direct_mouse_enabled() || !ikb_captured()) selector = 0;
|
||||
__atomic_store_n(&mouse_selector, selector, __ATOMIC_RELAXED);
|
||||
if (selector) {
|
||||
if (was_selector != selector) cursor[0] = cursor[1] = 0, row = (InputMouseRow){0};
|
||||
was_selector = selector;
|
||||
ikb_mouse_take(&dx, &dy);
|
||||
if (selector == 1) input_direct_mouse_radial(dx, dy, mouse_sens_get(), cursor, out);
|
||||
else input_direct_mouse_row(dx, mouse_sens_get(), now, &row, out);
|
||||
held[0] = held[1] = 0; frame = ~0ull;
|
||||
return;
|
||||
}
|
||||
if (was_selector) {
|
||||
was_selector = 0; cursor[0] = cursor[1] = 0; row = (InputMouseRow){0};
|
||||
ikb_mouse_take(&dx, &dy); // release the wheel without sending its last movement to the camera
|
||||
held[0] = held[1] = out[0] = out[1] = 0; frame = ~0ull;
|
||||
return;
|
||||
}
|
||||
if (input_direct_mouse_active()) {
|
||||
held[0] = held[1] = out[0] = out[1] = 0;
|
||||
frame = ~0ull;
|
||||
return; // the camera update takes the raw counts itself, once, outside the HID stick path
|
||||
}
|
||||
if (!ikb_captured()) {
|
||||
ikb_mouse_take(&dx, &dy);
|
||||
held[0] = held[1] = 0;
|
||||
@@ -459,6 +523,53 @@ static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
|
||||
if (b > a) { *x = kx; *y = ky; }
|
||||
}
|
||||
|
||||
// The shake key (B_SHAKE): the game detaches a part fused to what Ultrahand holds when the right stick swings left and
|
||||
// right a few times. A press swings it for SHAKE_NS, holding keeps it swinging; each side lasts SHAKE_SIDE_NS, two
|
||||
// frames at the game's 30 fps, so every side is read even at 20 fps. Returns the stick x, 0 while not shaking.
|
||||
// Under lock.
|
||||
#define SHAKE_NS 800000000ull
|
||||
#define SHAKE_SIDE_NS 66666667ull
|
||||
static int32_t shake_stick(int held) {
|
||||
static int was;
|
||||
static uint64_t from, until;
|
||||
uint64_t now = plat_now_ns();
|
||||
if (held && !was && now >= until) from = now;
|
||||
if (held && !was) until = now + SHAKE_NS;
|
||||
if (held && until < now) until = now;
|
||||
was = held;
|
||||
if (now >= until && !held) return 0;
|
||||
return (now - from) / SHAKE_SIDE_NS % 2 ? STICK_MAX : -STICK_MAX;
|
||||
}
|
||||
|
||||
// The scroll wheel on the Purah Pad's map, which zooms with the right stick's y (up: in): each notch is a full-tilt
|
||||
// pulse of MAP_ZOOM_PULSE_NS on that stick with MAP_ZOOM_GAP_NS of neutral after it, so a step of the zoom and a
|
||||
// held zoom both see one notch as one push. A fast scroll's notches wait their turn (at most MAP_ZOOM_QUEUE), a turn the
|
||||
// other way drops them. Returns the stick y for this poll, 0 between pulses and off the map. Under lock.
|
||||
#define MAP_ZOOM_PULSE_NS 100000000ull
|
||||
#define MAP_ZOOM_GAP_NS 50000000ull
|
||||
#define MAP_ZOOM_QUEUE 4
|
||||
static int32_t map_zoom_stick(int32_t notches, int on_map) {
|
||||
static int32_t queued, dir;
|
||||
static uint64_t until; // the end of the pulse (dir != 0) or of the neutral after it
|
||||
uint64_t now = plat_now_ns();
|
||||
if (!on_map) { queued = dir = 0, until = 0; return 0; }
|
||||
if (notches) {
|
||||
if (queued && (notches > 0) != (queued > 0)) queued = 0;
|
||||
queued += notches;
|
||||
queued = queued > MAP_ZOOM_QUEUE ? MAP_ZOOM_QUEUE : queued < -MAP_ZOOM_QUEUE ? -MAP_ZOOM_QUEUE : queued;
|
||||
}
|
||||
if (dir) {
|
||||
if (now < until) return dir * STICK_MAX;
|
||||
dir = 0, until = now + MAP_ZOOM_GAP_NS;
|
||||
return 0;
|
||||
}
|
||||
if (now < until || !queued) return 0;
|
||||
dir = queued > 0 ? 1 : -1;
|
||||
queued -= dir;
|
||||
until = now + MAP_ZOOM_PULSE_NS;
|
||||
return dir * STICK_MAX;
|
||||
}
|
||||
|
||||
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
|
||||
static int pad_physical(uint8_t pd[PAD_COUNT]) {
|
||||
if ((!pad || !SDL_GamepadConnected(pad)) && plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
|
||||
@@ -497,6 +608,8 @@ int input_pad_stick_percent(double pct[2]) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
static uint64_t g_buttons_last; // the merged buttons of the last poll (input_buttons_last)
|
||||
|
||||
static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
|
||||
uint64_t b = 0;
|
||||
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
|
||||
@@ -505,6 +618,8 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
if (state > 0) {
|
||||
binds_refresh();
|
||||
if (state == 1) pad_read(&b, sl, sr);
|
||||
int pad_used = b || abs(sl[0]) > PAD_USED || abs(sl[1]) > PAD_USED || abs(sr[0]) > PAD_USED ||
|
||||
abs(sr[1]) > PAD_USED, kbm_used = 0;
|
||||
if (ikb_foreground()) {
|
||||
uint8_t down[KEY_COUNT] = {0};
|
||||
// Mouse buttons count only while the cursor is captured, and a button already held when the capture began
|
||||
@@ -514,6 +629,7 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
int cap = ikb_captured();
|
||||
for (int k = 1; k < KEY_COUNT; k++) {
|
||||
down[k] = ikb_down(k);
|
||||
kbm_used |= down[k];
|
||||
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
|
||||
if (cap && !was_captured) mouse_ignore[k] = down[k];
|
||||
if (!down[k]) mouse_ignore[k] = 0;
|
||||
@@ -530,9 +646,32 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
input_map_stick(down, 1, k);
|
||||
if (walk) k[0] = (int32_t)(k[0] * walk_scale), k[1] = (int32_t)(k[1] * walk_scale);
|
||||
pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
mouse_camera(k);
|
||||
pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
int selector = b & (1ull << B_L) ? 1 : b & (1ull << B_UP) ? 2 :
|
||||
b & (1ull << B_LEFT) ? 3 : b & (1ull << B_RIGHT) ? 4 : 0;
|
||||
// Only while the wheel or a strip is on screen (menu_cursor.c reads the focused screen): L also soothes the
|
||||
// horse and turns menu tabs, the D-pad does other things too, and then the mouse stays on the camera.
|
||||
// Before the screens were read (the menu cursor off) the buttons alone decide, as before.
|
||||
if (selector && menu_cursor_shortcut() == 0) selector = 0;
|
||||
mouse_camera(k, selector);
|
||||
// On the map the left stick moves the cursor and the right one zooms: the mouse moves the cursor there,
|
||||
// the scroll wheel zooms (map_zoom_stick). Elsewhere the wheel is unbound; its notches are dropped.
|
||||
int map = menu_cursor_map();
|
||||
if (map) pick(&sl[0], &sl[1], k[0], k[1]);
|
||||
else pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
int32_t zoom = map_zoom_stick(ikb_wheel_take(), map);
|
||||
if (zoom) pick(&sr[0], &sr[1], 0, zoom);
|
||||
int32_t shake = shake_stick(input_map_held(down, B_SHAKE));
|
||||
if (shake) sr[0] = shake, sr[1] = 0;
|
||||
// A mouse shake (mouse_direct.c) moves the stick with the mouse, side by side, and lets go when it rests
|
||||
else if (input_direct_mouse_shake()) sr[0] = input_direct_mouse_shake() * STICK_MAX, sr[1] = 0;
|
||||
static uint32_t travel_seen;
|
||||
uint32_t travel = ikb_mouse_travel();
|
||||
if (travel - travel_seen >= MOUSE_USED) kbm_used = 1;
|
||||
travel_seen = travel;
|
||||
}
|
||||
// A press or a full-way stick on the pad wins over keys held in the same poll: the player picked it up
|
||||
if (pad_used) __atomic_store_n(&g_kbm_last, 0, __ATOMIC_RELAXED);
|
||||
else if (kbm_used) __atomic_store_n(&g_kbm_last, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
script_apply(&b, sl, sr);
|
||||
rec_apply(b, sl, sr);
|
||||
@@ -548,21 +687,29 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
if (overlay) menu_perf_cycle();
|
||||
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
|
||||
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
|
||||
__atomic_store_n(&g_buttons_last, b, __ATOMIC_RELAXED);
|
||||
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
|
||||
}
|
||||
|
||||
uint64_t input_buttons_last(void) { return __atomic_load_n(&g_buttons_last, __ATOMIC_RELAXED); }
|
||||
|
||||
// Text entry (swkbd_*.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
|
||||
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
|
||||
// g_block: 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
|
||||
static volatile int32_t g_block;
|
||||
static volatile int32_t g_menu; // the in-game menu, the same way (input_menu)
|
||||
// When g_block and g_menu last went to 2 (plat_now_ns). The wait for the release ends after RELEASE_WAIT_NS even with
|
||||
// something held: an input that never reads as released (a button another program holds down, a key whose release
|
||||
// went to another window, a stick that rests outside its dead zone) would otherwise keep the game deaf until restart
|
||||
// (#462, #494). A press that closed the menu is let go well within the wait; one still held after it is meant.
|
||||
static volatile uint64_t g_block_closed, g_menu_closed;
|
||||
#define RELEASE_WAIT_NS 1000000000ull
|
||||
|
||||
int input_cursor_allowed(void) {
|
||||
const char *in = cfg_get("TOTK_INPUT");
|
||||
return g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 && !(in && !strcmp(in, "none"));
|
||||
return g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 && !(in && !strcmp(in, "none")) && !menu_cursor_pointer();
|
||||
}
|
||||
|
||||
void input_block(int on) {
|
||||
if (!on) __atomic_store_n(&g_block_closed, plat_now_ns(), __ATOMIC_RELEASE);
|
||||
plat_xchg32(&g_block, on ? 1 : 2);
|
||||
ikb_cursor_update();
|
||||
}
|
||||
@@ -570,25 +717,36 @@ void input_block(int on) {
|
||||
// The in-game menu (ui/menu.cpp) blocks the game's input the same way: g_menu 0 closed, 1 open, 2 closed, waiting for
|
||||
// the release. The cursor is released while it is open, for the menu's mouse.
|
||||
void input_menu(int open) {
|
||||
if (!open) __atomic_store_n(&g_menu_closed, plat_now_ns(), __ATOMIC_RELEASE);
|
||||
plat_xchg32(&g_menu, open ? 1 : 2);
|
||||
if (!open) ikb_menu_closed(); // the capture before Esc comes back
|
||||
ikb_cursor_update();
|
||||
}
|
||||
|
||||
// 1 while flag (g_block, g_menu) blocks the game's input; state 2 ends at the first poll with nothing held. A stick
|
||||
// axis counts as held as well: with the walk key the keyboard's sticks move part way without setting a direction bit, so
|
||||
// a gate that watched the buttons alone would let that movement through the moment the menu or the text entry closes.
|
||||
static int blocked(volatile int32_t *flag, uint64_t buttons, const int32_t *axes) {
|
||||
// 1 while flag (g_block, g_menu) blocks the game's input; state 2 ends at the first poll with nothing held, or with
|
||||
// something still held RELEASE_WAIT_NS after closed. A stick axis counts as held as well: with the walk key the
|
||||
// keyboard's sticks move part way without setting a direction bit, so a gate that watched the buttons alone would let
|
||||
// that movement through the moment the menu or the text entry closes.
|
||||
static int blocked(volatile int32_t *flag, volatile uint64_t *closed, const char *what, uint64_t buttons,
|
||||
const int32_t *axes) {
|
||||
int32_t k = *flag;
|
||||
if (k != 2) return k != 0;
|
||||
int held = buttons != 0;
|
||||
for (int i = 0; i < 4; i++) held |= axes[i] != 0;
|
||||
return k && !(k == 2 && !held && plat_cas32(flag, 0, 2) == 2);
|
||||
if (held && plat_now_ns() - __atomic_load_n(closed, __ATOMIC_ACQUIRE) < RELEASE_WAIT_NS) return 1;
|
||||
if (plat_cas32(flag, 0, 2) != 2) return *flag != 0; // reopened meanwhile
|
||||
if (held)
|
||||
rt_log("input: the %s closed 1 s ago and input is still held (buttons %#llx, sticks %d %d %d %d); the game "
|
||||
"gets it from now on", what, (unsigned long long)buttons, axes[0], axes[1], axes[2], axes[3]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void input_poll(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
|
||||
poll_merged(buttons, lx, ly, rx, ry);
|
||||
if (!g_block && !g_menu) menu_cursor_poll(buttons);
|
||||
const int32_t axes[4] = {*lx, *ly, *rx, *ry};
|
||||
int text = blocked(&g_block, *buttons, axes), menu = blocked(&g_menu, *buttons, axes);
|
||||
int text = blocked(&g_block, &g_block_closed, "text entry", *buttons, axes),
|
||||
menu = blocked(&g_menu, &g_menu_closed, "menu", *buttons, axes);
|
||||
if (!text && !menu) return;
|
||||
*buttons = 0; *lx = *ly = *rx = *ry = 0;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,35 @@
|
||||
// Down=15) and the stick directions (StickLLeft..Down 16-19, StickRLeft..Down 20-23, derived from the sticks).
|
||||
// Sticks are -32767..32767, up is +y. TOTK_INPUT=none reports no input. Callable from any thread.
|
||||
void input_poll(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry);
|
||||
// 1 when keyboard and mouse are the device the player used last, 0 when the gamepad is: what is shown for pointing
|
||||
// (menu_cursor.c shows the pointer in the game's menus only for the keyboard and mouse)
|
||||
int input_kbm_last(void);
|
||||
// Direct mouse input is consumed by the game's camera-input update, after its stick response. While that update
|
||||
// accepts camera input, the HID right stick contains only the controller and keyboard. Selectors route mouse input
|
||||
// separately: a persistent radial cursor for the ability wheel, discrete steps for horizontal quick lists.
|
||||
int input_direct_mouse_active(void);
|
||||
int input_direct_mouse_enabled(void);
|
||||
// A radial selector uses a persistent virtual cursor while its opening button is held.
|
||||
void input_direct_mouse_radial(int32_t dx, int32_t dy, float sensitivity, float cursor[2], int32_t out[2]);
|
||||
// Horizontal quick selectors receive short, separated steps instead of a latched radial-cursor direction.
|
||||
typedef struct { float distance; uint64_t pulse_until, neutral_until; int direction; } InputMouseRow;
|
||||
void input_direct_mouse_row(int32_t dx, float sensitivity, uint64_t now, InputMouseRow *row, int32_t out[2]);
|
||||
int input_direct_mouse_selector_active(void);
|
||||
// Consume direct mouse counts, including one-shot MOUSE events from an input script. enabled=0 discards scripted
|
||||
// counts while leaving the real mouse for menu input. Live counts require cursor capture and foreground focus.
|
||||
void input_direct_mouse_sample(int enabled, int32_t *dx, int32_t *dy);
|
||||
// The buttons (B_* bits) of the last poll, keys and gamepad merged; any thread (mouse_direct.c: ZR for the bow's scale)
|
||||
uint64_t input_buttons_last(void);
|
||||
// mouse_direct.c: the right stick's side a mouse shake gives while Ultrahand holds an object (-1 / +1), 0 while the
|
||||
// mouse does not shake; input.c puts it on the stick
|
||||
int input_direct_mouse_shake(void);
|
||||
// menu_cursor.c: the L ability wheel or a D-pad strip has the focus: 1 / 0, -1 with the menu cursor off (not read)
|
||||
int menu_cursor_shortcut(void);
|
||||
// menu_cursor.c: the pointer in the game's menus (1.4.3). poll: once per pad read, adds a click's A or B; pointer: a
|
||||
// menu has the pointer, so the mouse camera's capture is released
|
||||
void menu_cursor_poll(uint64_t *buttons);
|
||||
int menu_cursor_pointer(void);
|
||||
int menu_cursor_map(void); // the Purah Pad's map is open: the mouse's stick is the left one (its cursor)
|
||||
// The same for host UI (the text entry): never blocked. input_block(1) while a text entry is open: input_poll then
|
||||
// reports nothing; input_block(0) when it closes: nothing until all buttons and stick directions are released.
|
||||
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry);
|
||||
|
||||
+13
-5
@@ -20,7 +20,7 @@
|
||||
{KEY_SPACE, B_X}, {KEY_LSHIFT, B_B}, {KEY_E, B_A}, {KEY_Q, B_L}, {KEY_R, B_R}, \
|
||||
{KEY_LCTRL, B_STICK_L}, {KEY_C, B_STICK_R}, \
|
||||
{KEY_1, B_LEFT}, {KEY_2, B_UP}, {KEY_3, B_RIGHT}, {KEY_4, B_DOWN}, \
|
||||
{KEY_TAB, B_PLUS}, {KEY_M, B_MINUS}
|
||||
{KEY_TAB, B_PLUS}, {KEY_M, B_MINUS}, {KEY_G, B_SHAKE}
|
||||
|
||||
// By position: south is Switch B, east A, west Y, north X (Nintendo layout); the Xbox layout swaps A with B and X with Y
|
||||
// (south A, as the Xbox labels; cross A on a PlayStation pad). The triggers are ZL and ZR.
|
||||
@@ -31,7 +31,7 @@
|
||||
{PAD_ZR, B_ZR}
|
||||
#define NINTENDO_PADS DEFAULT_PADS(B_B, B_A, B_Y, B_X)
|
||||
#define XBOX_PADS DEFAULT_PADS(B_A, B_B, B_X, B_Y)
|
||||
enum { DEFAULT_KEY_COUNT = 26, DEFAULT_PAD_COUNT = 16 };
|
||||
enum { DEFAULT_KEY_COUNT = 27, DEFAULT_PAD_COUNT = 16 };
|
||||
_Static_assert(sizeof (Binding[]){DEFAULT_KEYS} / sizeof (Binding) == DEFAULT_KEY_COUNT, "DEFAULT_KEY_COUNT");
|
||||
|
||||
static const InputMap defaults = {{DEFAULT_KEYS}, DEFAULT_KEY_COUNT, {{NINTENDO_PADS}, {XBOX_PADS}},
|
||||
@@ -41,7 +41,7 @@ InputMap input_map = {{DEFAULT_KEYS}, DEFAULT_KEY_COUNT, {{NINTENDO_PADS}, {XBOX
|
||||
|
||||
static const char *const action_names[ACTION_COUNT] = {
|
||||
"A", "B", "X", "Y", "LCLICK", "RCLICK", "L", "R", "ZL", "ZR", "PLUS", "MINUS", "LEFT", "UP", "RIGHT", "DOWN",
|
||||
"LSTICK_LEFT", "LSTICK_UP", "LSTICK_RIGHT", "LSTICK_DOWN", "RSTICK_LEFT", "RSTICK_UP", "RSTICK_RIGHT", "RSTICK_DOWN"};
|
||||
"LSTICK_LEFT", "LSTICK_UP", "LSTICK_RIGHT", "LSTICK_DOWN", "RSTICK_LEFT", "RSTICK_UP", "RSTICK_RIGHT", "RSTICK_DOWN", "SHAKE"};
|
||||
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", "lpaddle1", "rpaddle1", "lpaddle2", "rpaddle2"};
|
||||
@@ -143,7 +143,7 @@ int input_map_bind(const char *button, const char *list) {
|
||||
}
|
||||
if (!ok) {
|
||||
bad++;
|
||||
rt_log("input: bind %s: ignoring '%s' (expected pad:<name> or key:<name>; stick directions take keys only)",
|
||||
rt_log("input: bind %s: ignoring '%s' (expected pad:<name> or key:<name>; stick directions and SHAKE take keys only)",
|
||||
button, tok);
|
||||
}
|
||||
}
|
||||
@@ -289,11 +289,13 @@ int input_pad_layout_pick(const char *setting, int east_labelled_a) {
|
||||
}
|
||||
|
||||
int input_prompts_pick(const char *prompts, const char *pad_layout) {
|
||||
if (prompts && *prompts) return !strcasecmp(prompts, "xbox") ? PROMPTS_XBOX : PROMPTS_NINTENDO;
|
||||
if (prompts && *prompts)
|
||||
return !strcasecmp(prompts, "xbox") ? PROMPTS_XBOX : !strcasecmp(prompts, "pc") ? PROMPTS_PC : PROMPTS_NINTENDO;
|
||||
return pad_layout && !strcasecmp(pad_layout, "xbox_prompts") ? PROMPTS_XBOX : PROMPTS_NINTENDO;
|
||||
}
|
||||
|
||||
const char *input_prompts_folder(int prompts, const char *pad_layout) {
|
||||
if (prompts == PROMPTS_PC) return "romfs-patches-pc";
|
||||
if (prompts != PROMPTS_XBOX) return NULL;
|
||||
// The folders are read at start, before a pad is open: auto counts as Xbox, as for a pad without Nintendo labels.
|
||||
// A Nintendo-labelled pad under auto plays by label too (its A is on the button labelled A), so it matches as well.
|
||||
@@ -315,6 +317,12 @@ uint64_t input_map_buttons(const uint8_t down[KEY_COUNT]) {
|
||||
return b;
|
||||
}
|
||||
|
||||
int input_map_held(const uint8_t down[KEY_COUNT], int action) {
|
||||
for (int i = 0; i < input_map.nkeys; i++)
|
||||
if (input_map.keys[i].action == action && down[input_map.keys[i].key]) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void input_map_stick(const uint8_t down[KEY_COUNT], int stick, int32_t out[2]) {
|
||||
int first = stick ? B_SR_LEFT : B_SL_LEFT, dir[4] = {0, 0, 0, 0}; // left, up, right, down
|
||||
for (int i = 0; i < input_map.nkeys; i++) {
|
||||
|
||||
+16
-9
@@ -7,11 +7,13 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// Actions are the bit numbers of libnx HidNpadButton (input.h); 16..23 are the stick directions, which keys drive as
|
||||
// stick deflection.
|
||||
// stick deflection. B_SHAKE is a key of its own, never a button bit: a press shakes the right stick left and right for
|
||||
// the game (input.c shake_stick), which detaches a fused part held with Ultrahand. A pad shakes its own stick.
|
||||
enum {
|
||||
B_A = 0, B_B, B_X, B_Y, B_STICK_L, B_STICK_R, B_L, B_R, B_ZL, B_ZR, B_PLUS, B_MINUS, B_LEFT, B_UP, B_RIGHT, B_DOWN,
|
||||
B_SL_LEFT = 16, B_SL_UP, B_SL_RIGHT, B_SL_DOWN, B_SR_LEFT, B_SR_UP, B_SR_RIGHT, B_SR_DOWN,
|
||||
ACTION_COUNT = 24
|
||||
B_SHAKE = 24,
|
||||
ACTION_COUNT = 25
|
||||
};
|
||||
|
||||
// Keys and mouse buttons. F1, F10, F11 and Esc belong to the menu and the window, F8 to the flight recorder: no ids.
|
||||
@@ -67,14 +69,14 @@ extern InputMap input_map;
|
||||
void input_map_reset(void);
|
||||
// The script and totk.ini names, any case when parsed; NULL for an id without one. Actions: A B X Y LCLICK RCLICK L R ZL
|
||||
// ZR PLUS MINUS LEFT UP RIGHT DOWN, then LSTICK_LEFT LSTICK_UP LSTICK_RIGHT LSTICK_DOWN and the same for RSTICK (the
|
||||
// stick directions the keys drive). Keys: a-z 0-9 space tab lshift ... (key_names in input_map.c). Pads: south east ...
|
||||
// stick directions the keys drive), then SHAKE. Keys: a-z 0-9 space tab lshift ... (key_names in input_map.c). Pads: south east ...
|
||||
const char *input_action_name(int action);
|
||||
const char *input_key_name(int key);
|
||||
const char *input_pad_name(int pad);
|
||||
// Replaces every binding of the action `button` (an action name) with `list`: comma separated "pad:<name>" and
|
||||
// "key:<name>" entries, "key:default" / "pad:default" for that part's defaults, or "none" for no input. A part the list
|
||||
// names neither way is left unbound. Pad names are physical buttons, so pad entries go into the gamepad table of every
|
||||
// layout; pad:default keeps each layout's own button. Stick directions take keys only. Invalid entries are logged and
|
||||
// layout; pad:default keeps each layout's own button. Stick directions and SHAKE take keys only. Invalid entries are logged and
|
||||
// skipped; when none is valid the action keeps its bindings. 1 when applied.
|
||||
int input_map_bind(const char *button, const char *list);
|
||||
// Applies the TOTK_BIND_<action> settings (cfg_get) over the defaults: at start, and again when one of them changes.
|
||||
@@ -87,21 +89,26 @@ int input_pad_layout_pick(const char *setting, int east_labelled_a);
|
||||
|
||||
// Button prompts (TOTK_PROMPTS): the controller whose buttons the game's prompts show. Nintendo: the game's own. Xbox:
|
||||
// LB RB LT RT, Menu, View and a d-pad cross for the other buttons, and the face buttons named by the label of the
|
||||
// button the pad layout presses (runtime/romfs.c overlay_init, tools/make_romfs_patches.py --prompts).
|
||||
enum { PROMPTS_NINTENDO, PROMPTS_XBOX };
|
||||
// The prompts for a TOTK_PROMPTS value ("nintendo" or "xbox", any case). Unset or empty: Xbox while the pad layout
|
||||
// setting is "xbox_prompts", else Nintendo.
|
||||
// button the pad layout presses (runtime/romfs.c overlay_init, tools/make_romfs_patches.py --prompts). PC: the key or
|
||||
// mouse button the keyboard defaults press each button with, the mouse for the right stick.
|
||||
enum { PROMPTS_NINTENDO, PROMPTS_XBOX, PROMPTS_PC };
|
||||
// The prompts for a TOTK_PROMPTS value ("nintendo", "xbox" or "pc", any case). Unset or empty: Xbox while the pad
|
||||
// layout setting is "xbox_prompts", else Nintendo.
|
||||
int input_prompts_pick(const char *prompts, const char *pad_layout);
|
||||
// The folder next to romfs-index.bin that holds those prompts for a TOTK_PAD_LAYOUT value, NULL for the game's own. The
|
||||
// Nintendo layout plays an Xbox pad by position, so its folder names the face buttons the other way round
|
||||
// (romfs-patches-xbox: the right button does A and is labelled B); xbox and auto put every Switch button on the button
|
||||
// labelled with its letter (auto picks the layout per pad by its labels), so they share romfs-patches-xbox-label.
|
||||
// labelled with its letter (auto picks the layout per pad by its labels), so they share romfs-patches-xbox-label. The PC
|
||||
// prompts follow no pad: romfs-patches-pc whatever the layout (romfs.c overlay_init serves romfs-patches-pc-<w>x<h>
|
||||
// under that name at a game resolution of another shape).
|
||||
const char *input_prompts_folder(int prompts, const char *pad_layout);
|
||||
// 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);
|
||||
|
||||
// down[key] != 0 for the keys held. Returns the Switch button bits (0..15) the table presses.
|
||||
uint64_t input_map_buttons(const uint8_t down[KEY_COUNT]);
|
||||
// 1 when a key of action is held.
|
||||
int input_map_held(const uint8_t down[KEY_COUNT], int action);
|
||||
// out = x, y of the stick (0 left, 1 right) from its four direction actions; up is +y; a diagonal is scaled to the
|
||||
// unit circle.
|
||||
void input_map_stick(const uint8_t down[KEY_COUNT], int stick, int32_t out[2]);
|
||||
|
||||
+113
-4
@@ -1,11 +1,13 @@
|
||||
// The SDL3 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): SDL scancodes to
|
||||
// the key ids of input_map.h, the mouse camera's relative motion and the cursor capture (SDL's relative mouse mode).
|
||||
// Key and button state comes from the window's events (window thread) and is read by any thread. Keys and their map:
|
||||
// docs/controls.md.
|
||||
// the key ids of input_map.h, the mouse camera's relative motion, the cursor capture (SDL's relative mouse mode), the
|
||||
// pointer's place in the window (the game's menus, menu_cursor.c) and the mouse travel (the last-device switch,
|
||||
// input_kbm_last). Key and button state comes from the window's events (window thread) and is read by any thread. Keys
|
||||
// and their map: docs/controls.md.
|
||||
//
|
||||
// The capture follows the Win32 back end's rules (input_win.c): captured while the window has the focus, the mouse
|
||||
// camera is on and no text entry or menu is open; Esc releases it until a click in the window (and closing the menu
|
||||
// opened by that Esc gives it back); losing the focus releases it until the window has the focus again.
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
@@ -21,8 +23,18 @@ static SDL_Window *win;
|
||||
static volatile uint8_t key_held[KEY_COUNT];
|
||||
static volatile int32_t esc_held, f8_held, focused;
|
||||
static volatile int32_t mouse_dx, mouse_dy; // relative motion since the last ikb_mouse_take, whole counts
|
||||
static volatile int32_t mouse_wheel; // wheel turned while captured, in 1/120 notches (ikb_wheel_take)
|
||||
static float rem_x, rem_y; // window thread: the fractions SDL reported beyond them
|
||||
static volatile int32_t mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
|
||||
static float travel_rem; // window thread: the fraction of a count beyond mouse_travel
|
||||
static volatile int32_t captured; // relative mouse mode is on
|
||||
// The pointer's place in the window, as fractions of its size packed 32.32 fixed point (x in the high word), written
|
||||
// by the window thread from the mouse events and read by the game thread (menu_cursor.c through ikb_pointer).
|
||||
// ptr_have is 0 whenever the pointer is not over the window, as the Win32 back end's client-rect test.
|
||||
static volatile uint64_t ptr_pos;
|
||||
static volatile int32_t ptr_have;
|
||||
static volatile int32_t warp_pending; // ikb_pointer_set asked the window thread to move the pointer
|
||||
static float warp_nx, warp_ny; // its target, read by the window thread once warp_pending is seen
|
||||
enum { CUR_REL = 1, CUR_ESC = 2 }; // as input_win.c: released by Esc or a click elsewhere; by Esc
|
||||
static volatile int32_t cur_rel, cur_suspend, cur_pending;
|
||||
static uint32_t wake_event; // SDL_RegisterEvents: wakes the window thread for a cursor change
|
||||
@@ -133,6 +145,39 @@ static void cursor_apply(void) {
|
||||
plat_xchg32(&captured, want);
|
||||
}
|
||||
|
||||
// The pointer's place in the window (the window thread): x and y are SDL window units, the same space the mouse
|
||||
// events report, so dividing by the window's size makes fractions of it whatever the pixel density is. At and past
|
||||
// the edges the fraction is clamped to 0..1, where a letterboxed picture covers no item anyway.
|
||||
static void pointer_store(float x, float y) {
|
||||
if (!win) return;
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSize(win, &w, &h);
|
||||
if (w < 1 || h < 1) return;
|
||||
double fx = x / w, fy = y / h;
|
||||
if (!(fx >= 0)) fx = 0; // also NaN
|
||||
else if (fx > 1) fx = 1;
|
||||
if (!(fy >= 0)) fy = 0;
|
||||
else if (fy > 1) fy = 1;
|
||||
__atomic_store_n(&ptr_pos, (uint64_t)(uint32_t)(fx * 4294967295.0) << 32 | (uint32_t)(fy * 4294967295.0),
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&ptr_have, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// A warp asked for by ikb_pointer_set, done here as every window call: on the window thread, and after the cursor
|
||||
// change of the same menu opening has been applied. SDL's warp makes a motion event where it works (X11); where it
|
||||
// does not (some Wayland compositors) the stored place stands, so the item under the pointer is the place asked for
|
||||
// even if the visible cursor stayed behind.
|
||||
static void pointer_apply(void) {
|
||||
if (!plat_xchg32(&warp_pending, 0)) return;
|
||||
if (!win || !focused) return;
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSize(win, &w, &h);
|
||||
if (w < 1 || h < 1) return;
|
||||
float x = warp_nx * w, y = warp_ny * h;
|
||||
SDL_WarpMouseInWindow(win, x, y);
|
||||
pointer_store(x, y);
|
||||
}
|
||||
|
||||
// Any thread: the window thread applies the change at its next event.
|
||||
void ikb_cursor_update(void) {
|
||||
plat_xchg32(&cur_pending, 1);
|
||||
@@ -158,11 +203,13 @@ void input_sdl_window(SDL_Window *w) {
|
||||
win = w;
|
||||
wake_event = SDL_RegisterEvents(1);
|
||||
plat_xchg32(&focused, (SDL_GetWindowFlags(w) & SDL_WINDOW_INPUT_FOCUS) != 0);
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE); // the pointer's place is unknown until it enters or moves
|
||||
cursor_apply();
|
||||
}
|
||||
|
||||
void input_sdl_idle(void) {
|
||||
if (cur_pending) cursor_apply();
|
||||
pointer_apply();
|
||||
}
|
||||
|
||||
void input_sdl_event(const SDL_Event *e) {
|
||||
@@ -186,9 +233,19 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
int k = mouse_key(e->button.button);
|
||||
if (k) key_held[k] = (uint8_t)down;
|
||||
if (down && (cur_rel & CUR_REL)) plat_xchg32(&cur_rel, 0); // a click in the window captures again
|
||||
if (!captured) pointer_store(e->button.x, e->button.y); // a click can come without a motion first
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
case SDL_EVENT_MOUSE_MOTION: {
|
||||
// The travel counts any motion, captured or not, as the Win32 back end's raw input does: it is what makes a
|
||||
// moved mouse the device used last, so the menu pointer appears without a click or a key.
|
||||
travel_rem += fabsf(e->motion.xrel) + fabsf(e->motion.yrel);
|
||||
int32_t t = (int32_t)travel_rem;
|
||||
if (t) {
|
||||
travel_rem -= t;
|
||||
plat_add32(&mouse_travel, t);
|
||||
}
|
||||
if (!captured) pointer_store(e->motion.x, e->motion.y); // in relative mode x and y are not the pointer's
|
||||
if (captured) {
|
||||
rem_x += e->motion.xrel;
|
||||
rem_y += e->motion.yrel;
|
||||
@@ -198,6 +255,22 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
if (dy) plat_add32(&mouse_dy, dy);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_WINDOW_MOUSE_ENTER: { // entering without moving yet: the place is where the enter found the pointer
|
||||
float x, y;
|
||||
SDL_GetMouseState(&x, &y); // the window with mouse focus is this one during its own enter event
|
||||
pointer_store(x, y);
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_WHEEL: // notches, + away from the player (a flipped wheel reports the other sign)
|
||||
if (captured) {
|
||||
float y = e->wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -e->wheel.y : e->wheel.y;
|
||||
plat_add32(&mouse_wheel, (int32_t)(y * 120));
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
plat_xchg32(&focused, 1);
|
||||
break;
|
||||
@@ -207,9 +280,11 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
memset((void *)key_held, 0, sizeof key_held); // the key-ups go elsewhere
|
||||
plat_xchg32(&esc_held, 0);
|
||||
plat_xchg32(&f8_held, 0);
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE); // the pointer left with the focus, leave or not
|
||||
break;
|
||||
}
|
||||
cursor_apply();
|
||||
pointer_apply(); // after cursor_apply: a menu opening uncaptures first, then the pointer moves to its middle
|
||||
}
|
||||
|
||||
// ---- input_wsys.h
|
||||
@@ -218,7 +293,41 @@ int ikb_down(int key) { return key > 0 && key < KEY_COUNT && key_held[key]; }
|
||||
int ikb_escape(void) { return esc_held != 0; }
|
||||
int ikb_captured(void) { return captured != 0; }
|
||||
|
||||
// The pointer's place in the window as fractions of its size (menu_cursor.c maps them onto the picture the present
|
||||
// drew). 0 whenever the pointer is not over this window or the window has not the focus, as the Win32 back end's
|
||||
// GetCursorPos test.
|
||||
int ikb_pointer(float *nx, float *ny) {
|
||||
if (!focused || !__atomic_load_n(&ptr_have, __ATOMIC_ACQUIRE)) return 0;
|
||||
uint64_t p = __atomic_load_n(&ptr_pos, __ATOMIC_ACQUIRE);
|
||||
*nx = (float)(uint32_t)(p >> 32) / 4294967295.0f;
|
||||
*ny = (float)(uint32_t)p / 4294967295.0f;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Moves the pointer there (menu_cursor.c: to the middle when a menu opens). The window thread warps at its next
|
||||
// event, as SDL asks for its video thread; until then the warp target stands as the pointer's place.
|
||||
void ikb_pointer_set(float nx, float ny) {
|
||||
if (!(nx >= 0 && nx <= 1) || !(ny >= 0 && ny <= 1)) return;
|
||||
warp_nx = nx, warp_ny = ny;
|
||||
__atomic_store_n(&warp_pending, 1, __ATOMIC_RELEASE);
|
||||
if (wake_event) {
|
||||
SDL_Event e = {.user = {.type = wake_event}};
|
||||
SDL_PushEvent(&e);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ikb_mouse_travel(void) { return (uint32_t)__atomic_load_n(&mouse_travel, __ATOMIC_RELAXED); }
|
||||
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
|
||||
*dx = plat_xchg32(&mouse_dx, 0);
|
||||
*dy = plat_xchg32(&mouse_dy, 0);
|
||||
}
|
||||
|
||||
int32_t ikb_wheel_take(void) {
|
||||
int32_t o, n;
|
||||
do {
|
||||
o = mouse_wheel;
|
||||
n = o % 120; // the part of a notch stays for the next call
|
||||
} while (!__atomic_compare_exchange_n(&mouse_wheel, &o, n, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST));
|
||||
return o / 120;
|
||||
}
|
||||
+42
-3
@@ -83,6 +83,8 @@ uint32_t input_key_char(int key) {
|
||||
|
||||
// ---- mouse camera: raw motion accumulated by the window thread, one stick sample per presented frame
|
||||
static volatile LONG mouse_dx, mouse_dy; // raw counts since the last frame sample
|
||||
static volatile LONG mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
|
||||
static volatile LONG mouse_wheel; // wheel turned while captured, in WHEEL_DELTA units (ikb_wheel_take)
|
||||
static volatile LONG cur_captured; // cursor hidden and confined to the client area
|
||||
static volatile LONG cur_active; // WM_ACTIVATEAPP: the window's application is active
|
||||
// Bits: CUR_REL, Esc or activation by a click on the title bar or border: until a client click; CUR_ESC, set with CUR_REL
|
||||
@@ -137,11 +139,18 @@ int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
|
||||
case WM_INPUT: {
|
||||
RAWINPUT ri;
|
||||
UINT sz = sizeof ri;
|
||||
if (cur_captured && GetRawInputData((HRAWINPUT)l, RID_INPUT, &ri, &sz, sizeof(RAWINPUTHEADER)) != (UINT)-1 &&
|
||||
if (GetRawInputData((HRAWINPUT)l, RID_INPUT, &ri, &sz, sizeof(RAWINPUTHEADER)) != (UINT)-1 &&
|
||||
ri.header.dwType == RIM_TYPEMOUSE && !(ri.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE)) {
|
||||
InterlockedAdd(&mouse_dx, ri.data.mouse.lLastX);
|
||||
InterlockedAdd(&mouse_dy, ri.data.mouse.lLastY);
|
||||
LONG x = ri.data.mouse.lLastX, y = ri.data.mouse.lLastY;
|
||||
InterlockedAdd(&mouse_travel, (x < 0 ? -x : x) + (y < 0 ? -y : y));
|
||||
if (cur_captured) {
|
||||
InterlockedAdd(&mouse_dx, x);
|
||||
InterlockedAdd(&mouse_dy, y);
|
||||
}
|
||||
}
|
||||
// The wheel comes in the same message (usButtonData, signed, + away from the player)
|
||||
if (cur_captured && ri.header.dwType == RIM_TYPEMOUSE && (ri.data.mouse.usButtonFlags & RI_MOUSE_WHEEL))
|
||||
InterlockedAdd(&mouse_wheel, (SHORT)ri.data.mouse.usButtonData);
|
||||
break; // DefWindowProc must see WM_INPUT for cleanup
|
||||
}
|
||||
case WM_SETCURSOR:
|
||||
@@ -203,11 +212,41 @@ int ikb_escape(void) { return key_down(VK_ESCAPE); }
|
||||
|
||||
int ikb_captured(void) { return cur_captured != 0; }
|
||||
|
||||
int ikb_pointer(float *nx, float *ny) {
|
||||
HWND h = gfx_window_handle();
|
||||
RECT r;
|
||||
POINT p;
|
||||
if (!h || GetForegroundWindow() != h || !GetClientRect(h, &r) || r.right < 1 || r.bottom < 1 || !GetCursorPos(&p) ||
|
||||
!ScreenToClient(h, &p))
|
||||
return 0;
|
||||
*nx = (float)p.x / (float)r.right, *ny = (float)p.y / (float)r.bottom;
|
||||
return p.x >= 0 && p.y >= 0 && p.x < r.right && p.y < r.bottom;
|
||||
}
|
||||
|
||||
void ikb_pointer_set(float nx, float ny) {
|
||||
HWND h = gfx_window_handle();
|
||||
RECT r;
|
||||
if (!h || GetForegroundWindow() != h || !GetClientRect(h, &r)) return;
|
||||
POINT p = {(LONG)(nx * r.right), (LONG)(ny * r.bottom)};
|
||||
if (ClientToScreen(h, &p)) SetCursorPos(p.x, p.y);
|
||||
}
|
||||
|
||||
uint32_t ikb_mouse_travel(void) { return (uint32_t)mouse_travel; }
|
||||
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
|
||||
*dx = InterlockedExchange(&mouse_dx, 0);
|
||||
*dy = InterlockedExchange(&mouse_dy, 0);
|
||||
}
|
||||
|
||||
int32_t ikb_wheel_take(void) {
|
||||
LONG o, n;
|
||||
do {
|
||||
o = mouse_wheel;
|
||||
n = o % WHEEL_DELTA; // the part of a notch stays for the next call
|
||||
} while (InterlockedCompareExchange(&mouse_wheel, n, o) != o);
|
||||
return (int32_t)(o / WHEEL_DELTA);
|
||||
}
|
||||
|
||||
void ikb_cursor_update(void) { cursor_update(gfx_window_handle()); }
|
||||
|
||||
void ikb_menu_closed(void) { cur_rel_update(rel_menu_closed); }
|
||||
@@ -7,8 +7,14 @@
|
||||
int ikb_foreground(void); // the game window is in front: keys count
|
||||
int ikb_down(int key); // key id (input_map.h KEY_*) held, mouse buttons included
|
||||
int ikb_escape(void); // Esc held (no key id: it belongs to the menu)
|
||||
// The pointer in the client area, 0..1 from its top left; 0 when the window is not in front or the pointer is outside
|
||||
int ikb_pointer(float *nx, float *ny);
|
||||
void ikb_pointer_set(float nx, float ny); // moves the pointer there (the window in front only)
|
||||
int ikb_captured(void); // the cursor is captured for the mouse camera
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy); // raw mouse motion since the last call, in counts
|
||||
uint32_t ikb_mouse_travel(void); // raw counts the mouse moved in total (|dx| + |dy|), captured or not
|
||||
int32_t ikb_wheel_take(void); // whole wheel notches turned while captured since the last call (+ away
|
||||
// from the player); a high-resolution wheel's part of a notch is kept
|
||||
void ikb_cursor_update(void); // what input_cursor_allowed says may have changed
|
||||
void ikb_menu_closed(void); // the menu closed: a capture that Esc gave up comes back
|
||||
|
||||
|
||||
+38
-8
@@ -2,9 +2,11 @@
|
||||
//
|
||||
// totk.exe [--exefs DIR] [--romfs DIR] [--profile DIR] <mode>
|
||||
// --selftest layout, dispatch table and host-function registry; needs no game files
|
||||
// --probe-mods the mods folder scan, order and switches (mods.c); needs no game files
|
||||
// --probe-mods the mods folder scan, order and switches (mods.c), the size table entries of the members of
|
||||
// mods' packs (restbl_probe.c); needs no game files
|
||||
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
|
||||
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
|
||||
// --probe-discord the Rich Presence IPC exchange; skips without a Discord client; no game files
|
||||
// --probe-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
|
||||
@@ -153,8 +155,8 @@ static RtConfig cfg;
|
||||
const RtConfig *rt_config(void) { return &cfg; }
|
||||
|
||||
// ---- runtime services the generated code calls (cpu.h)
|
||||
volatile int g_fnprof = 0;
|
||||
void fnprof_hit(uint64_t addr, uint64_t lr, uint64_t x0) { (void)addr, (void)lr, (void)x0; }
|
||||
volatile int g_fnprof = 0; // resolution.c switches it on until the game sets its render size (res_fn_hit)
|
||||
void fnprof_hit(uint64_t addr, uint64_t lr, uint64_t x0) { (void)lr, res_fn_hit(addr, x0); }
|
||||
|
||||
void rt_brk(Cpu *c, uint32_t imm, uint64_t pc) {
|
||||
char at[128], lr[128];
|
||||
@@ -218,16 +220,21 @@ static void crash_handler(const PlatCrash *x) {
|
||||
if (plat_xchg32(&busy, 1)) plat_exit(71);
|
||||
char d[160];
|
||||
uintptr_t rip = x->rip;
|
||||
rt_logf("FATAL: %s (%#x) at host %p (totk+%#llx)", x->what, x->code, (void *)rip, (unsigned long long)x->offset);
|
||||
rt_logf("FATAL: %s (%#x) at host %p (", x->what, x->code, (void *)rip);
|
||||
if (x->module && !*x->module) rt_logf("outside every module)");
|
||||
else rt_logf("%s+%#llx)", x->module ? x->module : "totk", (unsigned long long)x->offset);
|
||||
if (x->access >= 0)
|
||||
rt_logf(", %s address %#llx", x->access == 1 ? "writing" : x->access == 8 ? "executing" : "reading",
|
||||
(unsigned long long)x->addr);
|
||||
rt_log_raw("\n", 1);
|
||||
uint64_t f = lifted_function(rip);
|
||||
uint64_t f = x->module ? 0 : lifted_function(rip);
|
||||
if (f) rt_logf(" in lifted function f_%llx = %s\n", (unsigned long long)f, ldr_describe(f, d, sizeof d));
|
||||
for (int i = 0; i < x->nframes; i++) { // abort and invalid parameter: where the host code called it
|
||||
// abort and invalid parameter: where the host code called it; a fault in another module: the host calls that led
|
||||
// into it
|
||||
uintptr_t exe = plat_exe_base();
|
||||
for (int i = 0; i < x->nframes; i++) {
|
||||
uint64_t g = lifted_function(x->frames[i]);
|
||||
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - (rip - x->offset)));
|
||||
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - exe));
|
||||
if (g) rt_logf(" in lifted function f_%llx", (unsigned long long)g);
|
||||
rt_log_raw("\n", 1);
|
||||
}
|
||||
@@ -439,11 +446,27 @@ static int selftest(void) {
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
// --probe-crash-call and --probe-crash-null crash on purpose, to check the report of a fault outside this executable.
|
||||
// call: strlen in the C runtime library (ucrtbase.dll, libc.so.6) reads address 8, the way a driver faults inside a
|
||||
// call the host made; the report names the library and the host frames that called it. null: a call through a null
|
||||
// function pointer, which leaves rip at 0, outside every module. Both end with exit code 71 and a FATAL line in the
|
||||
// log.
|
||||
static size_t (*volatile probe_strlen)(const char *) = strlen;
|
||||
static void (*volatile probe_null)(void);
|
||||
|
||||
static int probe_crash(int null) {
|
||||
if (null) probe_null();
|
||||
else printf("%zu\n", probe_strlen((const char *)(uintptr_t)8));
|
||||
printf("probe-crash: still running, the crash did not happen\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int usage(const char *argv0) {
|
||||
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
|
||||
"--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-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);
|
||||
"--probe-vkexec | --probe-astc | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-discord | "
|
||||
"--prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -481,6 +504,9 @@ static void portable_paths(RtConfig *c) {
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
mem_init(); // first: claim the guest address space before anything else can land there; reads totk.ini
|
||||
// The build that wrote the log, at its top: an attached log from another install or version shows at a glance.
|
||||
rt_log("version: TotK Recomp %s, game %d.%d.%d", rt_port_version, rt_game_version / 100, rt_game_version / 10 % 10,
|
||||
rt_game_version % 10);
|
||||
// A restart from the settings menu (gfx_present.c gfx_window_closed) starts this process while the one it replaces
|
||||
// is still exiting: its log, RomFS and saves are free once it is gone.
|
||||
if (plat_restart_wait(30000) < 0) rt_log("restart: the previous process still runs after 30 s; starting anyway");
|
||||
@@ -526,16 +552,20 @@ int main(int argc, char **argv) {
|
||||
log_open(!mode);
|
||||
if (!mode) {
|
||||
cfg.boot = cfg.seconds > 0; // --seconds starts the game like --boot, with its checkpoints
|
||||
discord_init(); // the Rich Presence thread (discord_rpc.c); it reads totk.ini itself and idles while off
|
||||
rt_boot();
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(mode, "--selftest")) return selftest();
|
||||
if (!strcmp(mode, "--probe-loader")) return probe_loader();
|
||||
if (!strcmp(mode, "--probe-crash-call")) return probe_crash(0);
|
||||
if (!strcmp(mode, "--probe-crash-null")) return probe_crash(1);
|
||||
if (!strcmp(mode, "--probe-cfg")) return probe_cfg();
|
||||
#ifdef _WIN32
|
||||
if (!strcmp(mode, "--probe-dpi")) return probe_dpi(); // the Win32 DPI awareness of totk.manifest
|
||||
#endif
|
||||
if (!strcmp(mode, "--probe-mods")) return probe_mods();
|
||||
if (!strcmp(mode, "--probe-discord")) return probe_discord();
|
||||
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
|
||||
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
|
||||
if (!strcmp(mode, "--probe-services")) return probe_services();
|
||||
|
||||
@@ -288,6 +288,35 @@ uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
|
||||
return R_OK;
|
||||
}
|
||||
|
||||
// The runtime's readers of game objects (menu_cursor.c, mouse_direct.c) check every pointer before they follow it. An
|
||||
// OS query (plat_mem_readable: VirtualQuery) costs tens of microseconds in this process, and a reader that checks a
|
||||
// few dozen pointers at a time stalls the game thread for milliseconds. The game's objects live in the committed heap
|
||||
// and its globals in the modules' images, which are readable for as long as they are mapped: those answer from the
|
||||
// layout. Any other range asks the OS. The heap only shrinks when the game asks for it (mem_set_heap_size), not while
|
||||
// it runs.
|
||||
int mem_guest_readable(uint64_t a, uint64_t n) {
|
||||
static uint64_t fast, slow;
|
||||
if (a + n < a) return 0;
|
||||
uint64_t hs = __atomic_load_n(&heap_size, __ATOMIC_ACQUIRE);
|
||||
int ok = a >= L.heap.base && a + n <= L.heap.base + hs;
|
||||
for (int i = 0; !ok && i < ldr_count(); i++) {
|
||||
const RtLoaded *m = ldr_module(i);
|
||||
ok = a >= m->base && a + n <= m->base + m->size;
|
||||
}
|
||||
if (ok) {
|
||||
if (__atomic_add_fetch(&fast, 1, __ATOMIC_RELAXED) == 100000)
|
||||
rt_log("memory: 100000 guest reads checked by the layout, %llu by the OS",
|
||||
(unsigned long long)__atomic_load_n(&slow, __ATOMIC_RELAXED));
|
||||
} else {
|
||||
ok = plat_mem_readable(a, n);
|
||||
uint64_t s = __atomic_add_fetch(&slow, 1, __ATOMIC_RELAXED);
|
||||
if (!(s & (s - 1)) && s >= 1024) // 1024, 2048, 4096...: how often the layout could not answer
|
||||
rt_log("memory: guest reads checked by the layout %llu, by the OS %llu",
|
||||
(unsigned long long)__atomic_load_n(&fast, __ATOMIC_RELAXED), (unsigned long long)s);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- module data permissions (0x02, 0x03) and module pages for QueryMemory
|
||||
static int module_index(uint64_t a) {
|
||||
for (int i = 0; i < ldr_count(); i++) {
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
// The mouse pointer in TotK 1.4.3's own menus (the title, the inventory, the System tab, Options, shops, dialogue
|
||||
// choices): while a menu screen has the game's focus box (eui's BoxCursor, the frame the d-pad moves), the Windows
|
||||
// pointer is released from the mouse camera and shown, the item under it takes the focus as if the d-pad had moved
|
||||
// there (highlight, sound and description included), a left click on an item is A on it and a right click is B.
|
||||
// Nothing is drawn and no game code is called: at each pad read (ov_input.c GetNpadStates, the game thread) the open
|
||||
// screens are read and the focus is moved with the three writes eui::BoxCursorMgr::moveBoxCursor makes, so the game's
|
||||
// own BoxCursorControl moves the box at its next update.
|
||||
//
|
||||
// Adapted from Weeping of the Empire's engine/runtime/totk_menu_cursor.cpp @ df0586cdc5702ca19bbebfa59697e0b6980f6237
|
||||
// (https://github.com/daniel-madrid-07/weeping-of-the-empire), Copyright (c) 2026 daniel-madrid-07, MIT License: the
|
||||
// focus read, the hit test, the stale-hit-pane fallback and the focus writes, which it does for 1.0.0.
|
||||
// Placed in 1.4.3 by hand:
|
||||
// - eui::ScreenMgr::sInstance: 1.0.0 main+0x4640010, 1.4.3 main+0x3acf960 (its createInstance main+0xc613d4 and
|
||||
// constructor main+0xc61450 match 1.0.0's main+0x119c5cc / +0x119c648 instruction for instruction, the object
|
||||
// 0x450 bytes with the same field stores, so its layout is 1.0.0's);
|
||||
// - the screen's reserved node (+0x150) and request bits (+0x175, bit 2) and its draw target byte (+0x16c): the same
|
||||
// stores at 1.4.3 main+0x87903c.
|
||||
// The other fields are 1.0.0's; menu_cursor logs what it reads the first time a menu opens (TOTK_MENU_CURSOR_LOG=1:
|
||||
// every screen change), so a field that moved shows as a menu with no nodes or boxes off the layout.
|
||||
//
|
||||
// Only while keyboard and mouse are the device used last (input_kbm_last): a gamepad player sees the menus unchanged.
|
||||
// TOTK_MENU_CURSOR (live, default 1): 0 leaves the menus to the keys and the pad.
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "input_map.h"
|
||||
#include "input_wsys.h"
|
||||
#include "layout.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define SCREEN_MGR_V143 0x3acf960ull // eui::ScreenMgr::sInstance
|
||||
#define MAIN_SIZE 0x3adf000ull // main's image (code, data, bss) in 1.4.3
|
||||
|
||||
// eui::ScreenMgr, BoxCursorMgr, BoxCursorControl
|
||||
enum { SM_BOX_MGR = 0x3f8, BM_CONTROLS = 0x28, CTL_ACTIVE = 0x58, CTL_RESERVED = 0x60 };
|
||||
// eui::Screen
|
||||
enum { SCR_LAYOUT = 0x28, SCR_DRAW_INFO = 0x60, SCR_NODES = 0x80, SCR_RESERVED = 0x150, SCR_STATE = 0x16e,
|
||||
SCR_REQUEST = 0x175, SCR_OPENED = 2 };
|
||||
// eui::BoxCursorNode, AnimButton
|
||||
enum { NODE_BUTTON = 0x18, NODE_SCREEN = 0x20, BTN_LAYOUT = 0x20, BTN_FLAGS = 0x40, BTN_HIT_PANE = 0x58 };
|
||||
#define BTN_USABLE 0x20010u // both set: active and enabled (ButtonGroup::Update's test)
|
||||
// nn::ui2d::DrawInfo: projection (4 x 4) and view (3 x 4); nn::ui2d::Layout: root pane, size, name
|
||||
enum { DI_PROJ = 0x10, DI_VIEW = 0x50, LYT_ROOT = 0x18, LYT_SIZE = 0x28, LYT_NAME = 0x30 };
|
||||
// nn::ui2d::Pane
|
||||
enum { PANE_PARENT = 0x18, PANE_SIZE = 0x50, PANE_FLAG = 0x58, PANE_ALPHA = 0x59, PANE_BASE = 0x5b, PANE_MTX = 0x70 };
|
||||
|
||||
static uint64_t main_lo, main_hi;
|
||||
|
||||
static int ok_at(uint64_t p, uint64_t n) {
|
||||
return p >= 0x10000 && p < (1ull << 47) && (p & 7) == 0 && mem_guest_readable(p, n);
|
||||
}
|
||||
static uint64_t rd64(uint64_t a) { uint64_t v; memcpy(&v, (const void *)(uintptr_t)a, 8); return v; }
|
||||
static uint32_t rd32(uint64_t a) { uint32_t v; memcpy(&v, (const void *)(uintptr_t)a, 4); return v; }
|
||||
static uint8_t rd8(uint64_t a) { return *(const volatile uint8_t *)(uintptr_t)a; }
|
||||
static float rdf(uint64_t a) { float v; memcpy(&v, (const void *)(uintptr_t)a, 4); return v; }
|
||||
// An object of the game: readable, its vtable in main's image
|
||||
static int game_object(uint64_t p, uint64_t n) {
|
||||
if (!ok_at(p, n)) return 0;
|
||||
uint64_t vt = rd64(p);
|
||||
return vt >= main_lo && vt < main_hi;
|
||||
}
|
||||
|
||||
// ---- panes (AnimButton::HitTest's rules: 1.0.0 main+0x222ea40, +0x223b090)
|
||||
static int pane_shown(uint64_t pane) {
|
||||
if (!ok_at(pane, PANE_MTX + 0x20)) return 0;
|
||||
if (!(rd8(pane + PANE_FLAG) & 1) || !rd8(pane + PANE_ALPHA)) return 0;
|
||||
int depth = 0;
|
||||
for (uint64_t p = rd64(pane + PANE_PARENT); p; p = rd64(p + PANE_PARENT)) {
|
||||
if (!ok_at(p, PANE_MTX) || ++depth > 64) return 0;
|
||||
uint8_t f = rd8(p + PANE_FLAG);
|
||||
if (!(f & 1) || (!rd8(p + PANE_ALPHA) && (f & 2))) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
// The pane's box in layout coordinates (origin at the centre, y up): centre and half size
|
||||
static void pane_box(uint64_t pane, float *cx, float *cy, float *hw, float *hh) {
|
||||
float w = rdf(pane + PANE_SIZE), h = rdf(pane + PANE_SIZE + 4);
|
||||
float m00 = rdf(pane + PANE_MTX), m11 = rdf(pane + PANE_MTX + 0x14);
|
||||
uint8_t base = rd8(pane + PANE_BASE), bh = base & 3, bv = base >> 2 & 3;
|
||||
*hw = fabsf(w * m00) * 0.5f, *hh = fabsf(h * m11) * 0.5f;
|
||||
*cx = rdf(pane + PANE_MTX + 0xc), *cy = rdf(pane + PANE_MTX + 0x1c);
|
||||
if (bh == 1) *cx += *hw; else if (bh == 2) *cx -= *hw; // the translation is the left / right edge
|
||||
if (bv == 1) *cy -= *hh; else if (bv == 2) *cy += *hh; // ... the top / bottom edge
|
||||
}
|
||||
|
||||
// The screen's draw transform, for a screen drawn away from its centred layout space (scale and translation only)
|
||||
typedef struct { int ok; float v[12], p[16]; } Xf;
|
||||
static Xf screen_xf(uint64_t scr) {
|
||||
Xf x = {0};
|
||||
uint64_t di = rd64(scr + SCR_DRAW_INFO);
|
||||
if (!ok_at(di, DI_VIEW + sizeof x.v)) return x;
|
||||
memcpy(x.p, (const void *)(uintptr_t)(di + DI_PROJ), sizeof x.p);
|
||||
memcpy(x.v, (const void *)(uintptr_t)(di + DI_VIEW), sizeof x.v);
|
||||
for (int i = 0; i < 16; i++) if (!isfinite(x.p[i])) return x;
|
||||
for (int i = 0; i < 12; i++) if (!isfinite(x.v[i])) return x;
|
||||
x.ok = x.p[0] != 0 && x.p[5] != 0 && fabsf(x.p[1]) < 1e-5f && fabsf(x.p[4]) < 1e-5f && fabsf(x.v[1]) < 1e-4f &&
|
||||
fabsf(x.v[4]) < 1e-4f;
|
||||
return x;
|
||||
}
|
||||
static int to_ndc(const Xf *t, float x, float y, float *nx, float *ny) {
|
||||
float vx = t->v[0] * x + t->v[1] * y + t->v[3], vy = t->v[4] * x + t->v[5] * y + t->v[7],
|
||||
vz = t->v[8] * x + t->v[9] * y + t->v[11];
|
||||
float cx = t->p[0] * vx + t->p[1] * vy + t->p[2] * vz + t->p[3];
|
||||
float cy = t->p[4] * vx + t->p[5] * vy + t->p[6] * vz + t->p[7];
|
||||
float cw = t->p[12] * vx + t->p[13] * vy + t->p[14] * vz + t->p[15];
|
||||
if (!(fabsf(cw) > 1e-6f)) return 0;
|
||||
*nx = cx / cw, *ny = cy / cw;
|
||||
return 1;
|
||||
}
|
||||
// The transform maps the layout as the plain layout test does (centre to 0,0, corner to 1,1): it adds nothing
|
||||
static int xf_plain(const Xf *t, float lw, float lh) {
|
||||
float ax, ay, bx, by;
|
||||
if (!to_ndc(t, 0, 0, &ax, &ay) || !to_ndc(t, lw / 2, lh / 2, &bx, &by)) return 1;
|
||||
return fabsf(ax) < 0.01f && fabsf(ay) < 0.01f && fabsf(bx - 1) < 0.01f && fabsf(by - 1) < 0.01f;
|
||||
}
|
||||
|
||||
// The pointer: layout coordinates (x, y) and device coordinates (nx, ny), and the transform when it is not plain
|
||||
typedef struct { float x, y, nx, ny; const Xf *t; } Pt;
|
||||
static int pane_hit(uint64_t pane, const Pt *p) {
|
||||
if (!pane_shown(pane)) return 0;
|
||||
float cx, cy, hw, hh;
|
||||
pane_box(pane, &cx, &cy, &hw, &hh);
|
||||
if (!p->t) return p->x >= cx - hw && p->x <= cx + hw && p->y >= cy - hh && p->y <= cy + hh;
|
||||
float ax, ay, bx, by;
|
||||
if (!to_ndc(p->t, cx - hw, cy - hh, &ax, &ay) || !to_ndc(p->t, cx + hw, cy + hh, &bx, &by)) return 0;
|
||||
return p->nx >= fminf(ax, bx) && p->nx <= fmaxf(ax, bx) && p->ny >= fminf(ay, by) && p->ny <= fmaxf(ay, by);
|
||||
}
|
||||
|
||||
// ---- screens and nodes
|
||||
typedef struct { uint64_t control, node, screen; } Focus;
|
||||
static uint64_t screen_mgr(void) {
|
||||
uint64_t slot = MOD_MAIN_BASE + SCREEN_MGR_V143, sm = rd64(slot);
|
||||
return game_object(sm, SM_BOX_MGR + 8) ? sm : 0;
|
||||
}
|
||||
// The focused node of an open screen, from the draw targets' box cursors (all 0: none, as in gameplay)
|
||||
static Focus focus_now(void) {
|
||||
Focus none = {0};
|
||||
uint64_t sm = screen_mgr(), bm = sm ? rd64(sm + SM_BOX_MGR) : 0;
|
||||
if (!ok_at(bm, BM_CONTROLS + 16)) return none;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
uint64_t ctl = rd64(bm + BM_CONTROLS + 8ull * i);
|
||||
if (!ok_at(ctl, CTL_RESERVED + 8)) continue;
|
||||
uint64_t node = rd64(ctl + CTL_ACTIVE);
|
||||
if (!ok_at(node, NODE_SCREEN + 8)) continue;
|
||||
uint64_t scr = rd64(node + NODE_SCREEN);
|
||||
if (!game_object(scr, SCR_REQUEST + 1) || rd8(scr + SCR_STATE) != SCR_OPENED) continue;
|
||||
return (Focus){ctl, node, scr};
|
||||
}
|
||||
return none;
|
||||
}
|
||||
static void layout_name(uint64_t scr, char *out, size_t n) {
|
||||
snprintf(out, n, "?");
|
||||
uint64_t lyt = rd64(scr + SCR_LAYOUT), name = ok_at(lyt, LYT_NAME + 8) ? rd64(lyt + LYT_NAME) : 0;
|
||||
if (name < 0x10000 || name >= (1ull << 47) || !mem_guest_readable(name, n)) return;
|
||||
memcpy(out, (const void *)(uintptr_t)name, n - 1);
|
||||
out[n - 1] = 0;
|
||||
for (char *q = out; *q; q++) if (*q < 0x20 || *q > 0x7e) { *q = 0; break; }
|
||||
}
|
||||
// The open screens: eui::ScreenMgr's screen array, count at +0x8 and its pointers at +0x10 (1.0.0's layout, which the
|
||||
// 1.4.3 constructor keeps). The map has no focus box, so it is found here: the Purah Pad's frame (AppMenuOverlay_00)
|
||||
// with AppMap_00 and the menu's background AppMenuBG_00 open. The Scope opens the frame over AppMap_00 without that
|
||||
// background (Weeping of the Empire's engine/runtime/totk_menu_cursor.cpp census tells them apart the same way).
|
||||
// TOTK_MENU_CURSOR_LOG=1 logs the open screens whenever the set changes.
|
||||
enum { SM_COUNT = 0x8, SM_SCREENS = 0x10 };
|
||||
static int map_open(void) {
|
||||
uint64_t sm = screen_mgr();
|
||||
if (!sm) return 0;
|
||||
int32_t n = (int32_t)rd32(sm + SM_COUNT);
|
||||
uint64_t arr = rd64(sm + SM_SCREENS);
|
||||
if (n <= 0 || n > 1024 || !ok_at(arr, 8ull * (uint64_t)n)) return 0;
|
||||
const char *e = cfg_get("TOTK_MENU_CURSOR_LOG");
|
||||
int verbose = e && !strcmp(e, "1"), pad = 0, map = 0, bg = 0;
|
||||
char list[1024];
|
||||
size_t len = 0;
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
uint64_t scr = rd64(arr + 8ull * (uint64_t)i);
|
||||
if (!game_object(scr, SCR_REQUEST + 1) || rd8(scr + SCR_STATE) != SCR_OPENED) continue;
|
||||
char nm[48];
|
||||
layout_name(scr, nm, sizeof nm);
|
||||
pad |= !strcmp(nm, "AppMenuOverlay_00");
|
||||
map |= !strcmp(nm, "AppMap_00");
|
||||
bg |= !strcmp(nm, "AppMenuBG_00");
|
||||
if (!verbose) continue;
|
||||
for (const char *c = nm; *c; c++) h = (h ^ (uint8_t)*c) * 1099511628211ull;
|
||||
if (len < sizeof list - 64) len += (size_t)snprintf(list + len, sizeof list - len, "%s%s", len ? " " : "", nm);
|
||||
}
|
||||
static uint64_t last;
|
||||
static int lines;
|
||||
if (verbose && h != last && lines < 200) last = h, lines++, rt_log("menu_cursor: open screens: %s", list);
|
||||
return pad && map && bg;
|
||||
}
|
||||
static void layout_size(uint64_t scr, float *lw, float *lh) {
|
||||
*lw = 1920, *lh = 1080;
|
||||
uint64_t lyt = rd64(scr + SCR_LAYOUT);
|
||||
if (!ok_at(lyt, LYT_SIZE + 8)) return;
|
||||
float w = rdf(lyt + LYT_SIZE), h = rdf(lyt + LYT_SIZE + 4);
|
||||
if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) *lw = w, *lh = h;
|
||||
}
|
||||
// The screen's focusable nodes: an sead OffsetList, head {prev, next} at +0x80, node-to-link offset at +0x94
|
||||
#define MAX_NODES 512
|
||||
static int nodes_of(uint64_t scr, uint64_t *node, uint64_t *btn) {
|
||||
uint64_t head = scr + SCR_NODES;
|
||||
int32_t off = (int32_t)rd32(head + 0x14), n = 0;
|
||||
if (off < 0 || off > 0x40) return 0;
|
||||
for (uint64_t link = rd64(head + 8); link != head && n < MAX_NODES; link = rd64(link + 8)) {
|
||||
if (!ok_at(link, 16)) break;
|
||||
uint64_t nd = link - (uint64_t)off;
|
||||
if (!ok_at(nd, NODE_SCREEN + 8)) continue;
|
||||
uint64_t b = rd64(nd + NODE_BUTTON);
|
||||
if (!ok_at(b, BTN_HIT_PANE + 8)) continue;
|
||||
node[n] = nd, btn[n] = b, n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
static int usable(uint64_t btn) { return !(~rd32(btn + BTN_FLAGS) & BTN_USABLE); }
|
||||
static uint64_t root_pane(uint64_t btn) {
|
||||
uint64_t blyt = rd64(btn + BTN_LAYOUT);
|
||||
return ok_at(blyt, LYT_ROOT + 8) ? rd64(blyt + LYT_ROOT) : 0;
|
||||
}
|
||||
// Every usable node's shown hit pane lies off the visible layout: the hit panes are stale (TotK never uses touch;
|
||||
// the dialogue choices' sit far from where the choices are drawn), so the buttons' own parts panes stand for them
|
||||
static int stale_hit_panes(int n, const uint64_t *btn, float lw, float lh, const Xf *t) {
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (!usable(btn[i])) continue;
|
||||
uint64_t pane = rd64(btn[i] + BTN_HIT_PANE);
|
||||
if (!ok_at(pane, PANE_MTX + 0x20) || !pane_shown(pane)) continue;
|
||||
float cx, cy, hw, hh, vx, vy;
|
||||
pane_box(pane, &cx, &cy, &hw, &hh);
|
||||
int in = t ? to_ndc(t, cx, cy, &vx, &vy) && fabsf(vx) <= 1 && fabsf(vy) <= 1
|
||||
: fabsf(cx) <= lw / 2 && fabsf(cy) <= lh / 2;
|
||||
seen++;
|
||||
if (in) return 0;
|
||||
}
|
||||
return seen > 0;
|
||||
}
|
||||
// The usable node under the pointer, 0: none
|
||||
static uint64_t node_at(int n, const uint64_t *node, const uint64_t *btn, const Pt *p, float lw, float lh) {
|
||||
if (!stale_hit_panes(n, btn, lw, lh, p->t)) {
|
||||
for (int i = 0; i < n; i++)
|
||||
if (usable(btn[i]) && pane_hit(rd64(btn[i] + BTN_HIT_PANE), p)) return node[i];
|
||||
return 0;
|
||||
}
|
||||
uint64_t found = 0;
|
||||
float best = 1e30f;
|
||||
for (int i = 0; i < n; i++) { // parts panes can be several items wide: the nearest centre
|
||||
uint64_t pane = root_pane(btn[i]);
|
||||
if (!usable(btn[i]) || !pane_hit(pane, p)) continue;
|
||||
float cx, cy, hw, hh, d;
|
||||
pane_box(pane, &cx, &cy, &hw, &hh);
|
||||
d = (cx - p->x) * (cx - p->x) + (cy - p->y) * (cy - p->y);
|
||||
if (d < best) best = d, found = node[i];
|
||||
}
|
||||
return found;
|
||||
}
|
||||
// The focus to node: the writes moveBoxCursor makes. The request bit is a byte eui also writes, so it is raised
|
||||
// again until the focus is there.
|
||||
static void move_focus(const Focus *f, uint64_t node) {
|
||||
memcpy((void *)(uintptr_t)(f->control + CTL_RESERVED), &node, 8);
|
||||
memcpy((void *)(uintptr_t)(f->screen + SCR_RESERVED), &node, 8);
|
||||
__atomic_fetch_or((uint8_t *)(uintptr_t)(f->screen + SCR_REQUEST), (uint8_t)4, __ATOMIC_SEQ_CST);
|
||||
}
|
||||
|
||||
// Screens with a focus box that are gameplay, not menus: the L ability wheel, the D-pad strips, Autobuild's list, the
|
||||
// Purah Pad's camera and the map (the stick moves its own cursor there)
|
||||
static int gameplay_screen(const char *nm) {
|
||||
static const char *const names[] = {"ShortCutMainItem_00", "ShortCut_00", "AutoBuilder_00", "CameraPointer_00",
|
||||
"AppMap_00"};
|
||||
for (size_t i = 0; i < sizeof names / sizeof *names; i++)
|
||||
if (!strcmp(nm, names[i])) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static volatile int32_t g_pointer; // a menu has the pointer: input_cursor_allowed releases the capture
|
||||
static volatile int32_t g_map; // the Purah Pad's map is open (not the Scope): the mouse moves its cursor
|
||||
|
||||
int menu_cursor_pointer(void) { return __atomic_load_n(&g_pointer, __ATOMIC_RELAXED); }
|
||||
int menu_cursor_map(void) { return __atomic_load_n(&g_map, __ATOMIC_RELAXED); }
|
||||
|
||||
static int enabled(void) {
|
||||
if (!RT_V143) return 0;
|
||||
const char *v = cfg_get("TOTK_MENU_CURSOR"), *in = cfg_get("TOTK_INPUT");
|
||||
return !(v && !strcmp(v, "0")) && !(in && !strcmp(in, "none"));
|
||||
}
|
||||
|
||||
static void set_pointer(int on) {
|
||||
if (plat_xchg32(&g_pointer, on) == on) return;
|
||||
ikb_cursor_update();
|
||||
if (on) ikb_pointer_set(0.5f, 0.5f); // the pointer appears in the middle, not where the capture left it
|
||||
}
|
||||
|
||||
// The pointer on the game's picture, 0..1 from its top left (the present's fit, letterbox or pillarbox)
|
||||
static int picture_pos(float *px, float *py) {
|
||||
float nx, ny, r[4];
|
||||
if (!ikb_pointer(&nx, &ny) || !gfx_picture_rect(r) || r[2] <= 0 || r[3] <= 0) return 0;
|
||||
*px = (nx - r[0]) / r[2], *py = (ny - r[1]) / r[3];
|
||||
return *px >= 0 && *px <= 1 && *py >= 0 && *py <= 1;
|
||||
}
|
||||
|
||||
static void log_screen(const Focus *f, const char *nm, int n, float lw, float lh, const Xf *t, int stale) {
|
||||
static uint64_t last;
|
||||
static int lines;
|
||||
const char *e = cfg_get("TOTK_MENU_CURSOR_LOG");
|
||||
int verbose = e && !strcmp(e, "1");
|
||||
if (f->screen == last || (!verbose && lines >= 6)) return;
|
||||
last = f->screen, lines++;
|
||||
rt_log("menu_cursor: screen %s (layout %gx%g, %d nodes, %s hit panes, draw transform %s)", nm, lw, lh, n,
|
||||
stale ? "stale" : "own", t ? "scaled" : "plain");
|
||||
}
|
||||
|
||||
// Once per pad read on the game thread: the pointer's hover and clicks, as pad buttons (A, B) for this read
|
||||
void menu_cursor_poll(uint64_t *buttons) {
|
||||
static int inited, pending_a, held_l, held_r;
|
||||
static uint64_t pending_node, pending_until, hold_until, hold_bit, menu_seen;
|
||||
static float last_px = -1, last_py = -1;
|
||||
if (!inited) {
|
||||
inited = 1;
|
||||
main_lo = MOD_MAIN_BASE, main_hi = MOD_MAIN_BASE + MAIN_SIZE;
|
||||
if (RT_V143) rt_log("menu_cursor: on for 1.4.3 (TOTK_MENU_CURSOR=0 turns it off)");
|
||||
}
|
||||
if (!enabled()) { set_pointer(0); __atomic_store_n(&g_map, 0, __ATOMIC_RELAXED); return; }
|
||||
Focus f = focus_now();
|
||||
char nm[48] = "";
|
||||
if (f.screen) layout_name(f.screen, nm, sizeof nm);
|
||||
// The open screens are scanned every 100 ms, not at every pad read: each screen costs memory queries
|
||||
uint64_t now = plat_now_ms();
|
||||
static uint64_t map_at;
|
||||
static int map, map_logged;
|
||||
if (!map_at || now - map_at >= 100) map_at = now, map = map_open();
|
||||
if (map && !map_logged) map_logged = 1, rt_log("menu_cursor: the map is open: the mouse moves its cursor");
|
||||
__atomic_store_n(&g_map, map, __ATOMIC_RELAXED);
|
||||
// With the gamepad as the last device the menus stay as they are on the console: no pointer, the capture kept.
|
||||
// Moving the mouse (or a key) makes keyboard and mouse the last device and the pointer comes.
|
||||
int menu = f.screen && !gameplay_screen(nm) && ikb_foreground() && input_kbm_last();
|
||||
if (menu) menu_seen = now;
|
||||
// A menu's focus gap (a tab switch, a popup opening: no focus box for a frame or two) keeps the pointer where it
|
||||
// is, for up to 300 ms: releasing it there captured it again and centred it when the next tab took the focus
|
||||
int gap = !menu && !f.screen && menu_seen && now - menu_seen < 300 && ikb_foreground() && input_kbm_last();
|
||||
set_pointer(menu || gap);
|
||||
int l = ikb_down(KEY_MOUSE_L), r = ikb_down(KEY_MOUSE_R);
|
||||
int click_l = l && !held_l, click_r = r && !held_r;
|
||||
held_l = l, held_r = r;
|
||||
if (now < hold_until) { // a click's button, held for 60 ms so the game sees one press whatever its read rate
|
||||
*buttons |= hold_bit;
|
||||
return;
|
||||
}
|
||||
if (gap) return; // a click's focus request waits for the next screen
|
||||
if (!menu) { pending_a = 0; last_px = last_py = -1; return; }
|
||||
if (pending_a) { // the focus asked for by a click: A once it is there
|
||||
if (rd64(f.control + CTL_ACTIVE) == pending_node) {
|
||||
pending_a = 0, hold_until = now + 60, hold_bit = 1ull << B_A;
|
||||
*buttons |= hold_bit;
|
||||
return;
|
||||
}
|
||||
if (now > pending_until) pending_a = 0;
|
||||
else move_focus(&f, pending_node);
|
||||
return;
|
||||
}
|
||||
if (click_r) { hold_until = now + 60, hold_bit = 1ull << B_B; *buttons |= hold_bit; return; }
|
||||
float px, py;
|
||||
if (!picture_pos(&px, &py)) return;
|
||||
int moved = fabsf(px - last_px) > 0.001f || fabsf(py - last_py) > 0.001f;
|
||||
if (!moved && !click_l) return;
|
||||
last_px = px, last_py = py;
|
||||
float lw, lh;
|
||||
layout_size(f.screen, &lw, &lh);
|
||||
Xf xf = screen_xf(f.screen);
|
||||
Pt p = {(px - 0.5f) * lw, (0.5f - py) * lh, px * 2 - 1, 1 - py * 2, NULL};
|
||||
if (xf.ok && !xf_plain(&xf, lw, lh)) p.t = &xf;
|
||||
static uint64_t node[MAX_NODES], btn[MAX_NODES];
|
||||
int n = nodes_of(f.screen, node, btn);
|
||||
log_screen(&f, nm, n, lw, lh, p.t, n && stale_hit_panes(n, btn, lw, lh, p.t));
|
||||
uint64_t at = n ? node_at(n, node, btn, &p, lw, lh) : 0;
|
||||
if (!at) return;
|
||||
if (at != f.node) move_focus(&f, at);
|
||||
if (click_l) {
|
||||
if (at == f.node) { hold_until = now + 60, hold_bit = 1ull << B_A; *buttons |= hold_bit; }
|
||||
else pending_a = 1, pending_node = at, pending_until = now + 300;
|
||||
}
|
||||
}
|
||||
|
||||
// The L ability wheel or a D-pad strip (materials, weapons, shields, bows) has the focus: 1 / 0, -1 with the menu
|
||||
// cursor off (the screens are not read). input.c asks while L or the D-pad is held, before it gives the mouse to their
|
||||
// selectors: L also soothes the horse and turns menu tabs, the D-pad does other things too.
|
||||
int menu_cursor_shortcut(void) {
|
||||
if (!enabled()) return -1;
|
||||
Focus f = focus_now();
|
||||
char nm[48] = "";
|
||||
if (f.screen) layout_name(f.screen, nm, sizeof nm);
|
||||
return f.screen && (!strcmp(nm, "ShortCutMainItem_00") || !strcmp(nm, "ShortCut_00"));
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// --probe-mods: the mods folder scan, its order and switches (mods.c). Builds a mods folder of its own next to totk.exe,
|
||||
// scans it and removes it. Needs no game files.
|
||||
// scans it and removes it. Also the resource size table's entries of the members of the mods' packs (restbl_probe.c).
|
||||
// Needs no game files.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -7,6 +8,7 @@
|
||||
#include "host.h"
|
||||
#include "mods.h"
|
||||
#include "platform/platform.h"
|
||||
#include "restbl.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL mods: %s\n", what); } } while (0)
|
||||
@@ -126,5 +128,6 @@ int probe_mods(void) {
|
||||
}
|
||||
remove_tree(root);
|
||||
printf("mods: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails ? 1 : 0;
|
||||
int table = restbl_probe();
|
||||
return fails || table ? 1 : 0;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+9
-1
@@ -29,10 +29,18 @@ typedef struct {
|
||||
enum { MOVIE_EV_INPUT, MOVIE_EV_OUTPUT, MOVIE_EV_FORMAT, MOVIE_EV_ERROR }; // DecoderEvents: EventType * per event
|
||||
enum { MOVIE_TYPE_VP9 = 3, MOVIE_MODE_BUFFER = 1, MOVIE_FORMAT_NV12 = 4, MOVIE_FLAG_EOS = 4 };
|
||||
|
||||
typedef struct { uint64_t frames, decode_ns, max_ns, errors, dropped, handed; } MovieStats;
|
||||
typedef struct { uint64_t frames, decode_ns, max_ns, errors, dropped, handed, fit_ns; } MovieStats;
|
||||
// fit_ns: the part of decode_ns spent writing the NV12 frame
|
||||
|
||||
void movie_set_guest(const MovieGuest *g); // probe: stand-ins instead of the modules
|
||||
void movie_set_clock(uint64_t (*clock)(void)); // probe: a fake clock (ns) for the pacing; NULL = real
|
||||
void movie_set_pace(int on); // probe: 0 hands every decoded frame over at the next tick
|
||||
int movie_stats(uint64_t obj, MovieStats *out); // counters of the decoder at guest object obj; 0 if none
|
||||
int movie_quiet(uint64_t obj); // 1 when its worker is idle and cannot take more input now
|
||||
|
||||
// Keeping a movie's shape on an output of another shape (ov_movie.c): the picture is shrunk inside the frame, with
|
||||
// black (Y 16, U = V 128) around it. movie_fit_new returns NULL when the shapes agree within 1%: copy the frame.
|
||||
typedef struct MovieFit MovieFit;
|
||||
MovieFit *movie_fit_new(int w, int h, unsigned out_w, unsigned out_h);
|
||||
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst);
|
||||
void movie_fit_free(MovieFit *f);
|
||||
@@ -9,6 +9,7 @@
|
||||
// Without the RomFS (no game data) it prints why and exits 0. TOTK_PROBE_MOVIE_SHOT=<file.ppm> saves the middle frame.
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -267,7 +268,104 @@ static int paced(uint64_t obj, const uint8_t *data, const Track *t, int limit, u
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The shape-keeping resampler (ov_movie.c movie_fit_new / movie_fit_apply) on a synthetic 1920x1080 I420 picture, which
|
||||
// needs no game data. The planes are gradients, which a box filter keeps linear, so every output sample has an expected
|
||||
// value: luma rises left to right and top to bottom, U rises left to right, V falls top to bottom. A 16:9 output must give
|
||||
// no plan; the others must give bars (Y 16, U = V 128) that are symmetric, a picture of the movie's shape once the game
|
||||
// stretches the frame, and samples of the picture where the gradients say. TOTK_PROBE_MOVIE_BENCH=1 also times the frame.
|
||||
#define FIT_CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("probe-movie: FAILED: "); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
rc = 1; \
|
||||
goto done; \
|
||||
} \
|
||||
} while (0)
|
||||
static int check_fit(void) {
|
||||
enum { W = 1920, H = 1080, CW = W / 2, CH = H / 2 };
|
||||
int rc = 0;
|
||||
MovieFit *f = NULL;
|
||||
uint8_t *y = malloc((size_t)W * H), *u = malloc((size_t)CW * CH), *v = malloc((size_t)CW * CH);
|
||||
uint8_t *out = malloc((size_t)W * H * 3 / 2);
|
||||
FIT_CHECK(y && u && v && out, "fit test buffers");
|
||||
for (int j = 0; j < H; j++)
|
||||
for (int i = 0; i < W; i++) y[j * W + i] = (uint8_t)(32 + 150.0 * (i + 0.5) / W + 60.0 * (j + 0.5) / H + 0.5);
|
||||
for (int j = 0; j < CH; j++)
|
||||
for (int i = 0; i < CW; i++) {
|
||||
u[j * CW + i] = (uint8_t)(40 + 150.0 * (i + 0.5) / CW + 0.5);
|
||||
v[j * CW + i] = (uint8_t)(220 - 150.0 * (j + 0.5) / CH + 0.5);
|
||||
}
|
||||
const uint8_t *plane[3] = {y, u, v};
|
||||
const int stride[3] = {W, CW, CW};
|
||||
FIT_CHECK(!movie_fit_new(W, H, 1920, 1080) && !movie_fit_new(W, H, 2560, 1440), "16:9 outputs need no plan");
|
||||
static const unsigned outs[][2] = {{5120, 1440}, {3440, 1440}, {1280, 800}, {1920, 1200}};
|
||||
for (int k = 0; k < 4; k++) {
|
||||
unsigned ow = outs[k][0], oh = outs[k][1];
|
||||
f = movie_fit_new(W, H, ow, oh);
|
||||
FIT_CHECK(f, "%ux%u: no plan", ow, oh);
|
||||
memset(out, 0xAA, (size_t)W * H * 3 / 2); // a byte nothing else produces: every pixel must be written
|
||||
movie_fit_apply(f, plane, stride, out);
|
||||
const uint8_t *uv = out + (size_t)W * H;
|
||||
int l = 0, r = W - 1, t = 0, b = H - 1; // the picture's extent: where luma leaves 16, on the middle row and column
|
||||
while (l < W && out[(H / 2) * W + l] == 16) l++;
|
||||
while (r >= 0 && out[(H / 2) * W + r] == 16) r--;
|
||||
while (t < H && out[t * W + W / 2] == 16) t++;
|
||||
while (b >= 0 && out[b * W + W / 2] == 16) b--;
|
||||
int pw = r - l + 1, ph = b - t + 1;
|
||||
FIT_CHECK(pw > 2 && ph > 2, "%ux%u: no picture found", ow, oh);
|
||||
FIT_CHECK(abs(l - (W - 1 - r)) <= 2 && abs(t - (H - 1 - b)) <= 2,
|
||||
"%ux%u: bars not symmetric: left %d right %d, top %d bottom %d", ow, oh, l, W - 1 - r, t, H - 1 - b);
|
||||
// On screen the picture covers pw/W of the width and ph/H of the height of an ow x oh canvas; 1 = the movie's shape.
|
||||
double shape = ((double)pw / W * ow) / ((double)ph / H * oh) / ((double)W / H);
|
||||
FIT_CHECK(fabs(shape - 1) < 0.01, "%ux%u: picture %dx%d shows at %.3f of the movie's shape", ow, oh, pw, ph, shape);
|
||||
FIT_CHECK(l == 0 || ph == H, "%ux%u: sideways bars with a squeezed height", ow, oh);
|
||||
for (int j = 0; j < H; j++) // everything outside the picture is black in luma and chroma, left and right alike
|
||||
for (int i = 0; i < W; i++)
|
||||
if (j < t || j > b || i < l || i > r)
|
||||
FIT_CHECK(out[j * W + i] == 16, "%ux%u: luma (%d,%d) is %d, not black", ow, oh, i, j, out[j * W + i]);
|
||||
for (int j = 0; j < CH; j++)
|
||||
for (int i = 0; i < CW; i++) {
|
||||
int outside = 2 * j + 1 < t || 2 * j > b || 2 * i + 1 < l || 2 * i > r;
|
||||
const uint8_t *c = uv + (size_t)j * W + 2 * i;
|
||||
if (outside) FIT_CHECK(c[0] == 128 && c[1] == 128, "%ux%u: chroma (%d,%d) is %d/%d, not black", ow, oh, i, j,
|
||||
c[0], c[1]);
|
||||
}
|
||||
static const double fr[] = {0.1, 0.35, 0.5, 0.8, 0.95}; // samples of the picture, as fractions of it
|
||||
for (int a = 0; a < 5; a++)
|
||||
for (int c = 0; c < 5; c++) {
|
||||
int px = (int)(pw * fr[a]), py = (int)(ph * fr[c]); // picture pixel
|
||||
double sx = (px + 0.5) * W / pw, sy = (py + 0.5) * H / ph; // where it comes from in the source
|
||||
double ey = 32 + 150.0 * sx / W + 60.0 * sy / H;
|
||||
int got = out[(t + py) * W + l + px];
|
||||
FIT_CHECK(fabs(got - ey) <= 2, "%ux%u: luma at picture (%d,%d) is %d, want %.1f", ow, oh, px, py, got, ey);
|
||||
int cx = px / 2, cy = py / 2, cpw = pw / 2, cph = ph / 2;
|
||||
double sux = (cx + 0.5) * CW / cpw, svy = (cy + 0.5) * CH / cph;
|
||||
const uint8_t *q = uv + (size_t)(t / 2 + cy) * W + 2 * (l / 2 + cx);
|
||||
double eu = 40 + 150.0 * sux / CW, ev = 220 - 150.0 * svy / CH;
|
||||
FIT_CHECK(fabs(q[0] - eu) <= 3 && fabs(q[1] - ev) <= 3,
|
||||
"%ux%u: chroma at picture (%d,%d) is U %d V %d, want U %.1f V %.1f", ow, oh, cx, cy, q[0], q[1], eu,
|
||||
ev);
|
||||
}
|
||||
if (cfg_get("TOTK_PROBE_MOVIE_BENCH")) {
|
||||
uint64_t t0 = plat_now_ns();
|
||||
for (int i = 0; i < 100; i++) movie_fit_apply(f, plane, stride, out);
|
||||
printf("probe-movie: fit %ux%u: %.2f ms per frame\n", ow, oh, (plat_now_ns() - t0) / 1e8);
|
||||
}
|
||||
printf("probe-movie: fit %ux%u: picture %dx%d at (%d,%d) of the frame, bars Y 16 / UV 128, gradients as expected\n",
|
||||
ow, oh, pw, ph, l, t);
|
||||
movie_fit_free(f);
|
||||
f = NULL;
|
||||
}
|
||||
done:
|
||||
movie_fit_free(f);
|
||||
free(y), free(u), free(v), free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int probe_movie(void) {
|
||||
if (check_fit()) return 1;
|
||||
const char *dir = rt_config()->romfs;
|
||||
char image[PLAT_PATH_MAX];
|
||||
snprintf(image, sizeof image, "%s/romfs.bin", dir);
|
||||
|
||||
+10
-1
@@ -75,7 +75,8 @@ uint64_t nvn_tex_view_offset(const tex_t *t, const view_t *v);
|
||||
// A command handle (what QueueSubmitCommands receives) points at a host copy of one recording: a flat sequence of
|
||||
// 8-byte-aligned records {op_hdr, payload}, OP_BEGIN first and OP_END last. Called recordings are inlined, so a copy
|
||||
// holds no OP_CALL or OP_JUMP. Texture pointers in payloads (p_set_rt, p_copy_b2t, p_copy_t2t, p_clear_tex) are
|
||||
// immutable host copies that live for the run, one per tex_t.serial. Deferred binds (p_bind_handles.src != 0) hold the
|
||||
// immutable host copies ("pins"), one per tex_t.serial, that live while a recording copy names them and a while after
|
||||
// (nvn_pins_sweep). Deferred binds (p_bind_handles.src != 0) hold the
|
||||
// handles read at submit (nvn_submit_prepare). Payloads of the ops nvn_obj.h does not define:
|
||||
typedef struct { volatile uint32_t *val; uint32_t mode, value; } p_signal_event; // OP_SIGNAL_EVENT; mode: NVNeventSignalMode
|
||||
typedef struct { uint64_t type, addr; } p_report_counter; // OP_REPORT_COUNTER: NVNcounterType, NVNcounterData address
|
||||
@@ -99,6 +100,14 @@ int nvn_handle_live(uint64_t handle); // 1 while the handle
|
||||
// not added again)
|
||||
uint64_t nvn_handle_bytes(uint64_t handle); // bytes of the copy, OP_BEGIN to OP_END; 0 if not live
|
||||
uint64_t nvn_submit_count(void); // QueueSubmitCommands calls so far
|
||||
// Submits a texture pin outlives its last tex_pin and the release of the last recording copy naming it: about 20 s at
|
||||
// one submit a frame (MacNativeGames frees after two 600-submit windows).
|
||||
#define NVN_PIN_AGE 1200
|
||||
// Frees the texture pins no recording copy has named for NVN_PIN_AGE submits (nvn_api_cmd.c, "texture pins"), except keep[0..nkeep):
|
||||
// the replay's current render targets, which carry over to the next submission. Called between submissions by the
|
||||
// thread that replays them; now = nvn_submit_count().
|
||||
void nvn_pins_sweep(uint64_t now, const tex_t *const *keep, int nkeep);
|
||||
void nvn_pins_stats(uint64_t *live, uint64_t *freed); // pins that exist, pins freed so far
|
||||
const char *nvn_op_name(uint32_t op); // "DRAW_IDX", ...; "?" past OP_COUNT_
|
||||
// MemoryPoolInvalidateMappedRange: the CPU is about to read pool storage the GPU wrote. Weak no-op in nvn_ext.c; a back
|
||||
// end that renders into host-side copies defines it and writes them back.
|
||||
|
||||
+170
-18
@@ -4,7 +4,7 @@
|
||||
// games/totk/third_party/d2r_gfx/gfx/nvn/hle_nvn.c @ b75be8cf (MIT, Copyright (c) 2026 jhaizhou; origin d2r-mobile),
|
||||
// lines 647-1062, 1261-1286, 1486-1760 and 1975-1990, and the *Deferred and memory-callback entry points of
|
||||
// totk_nvn_ext.c in the same directory. Metal, mach, the translation thread, Apple diagnostics, the per-frame
|
||||
// statistics and the long-session sweeps (copy marks in control memory, texture pin sweep) dropped.
|
||||
// statistics and the copy marks in control memory dropped; the texture pin sweep replaced by counting (texture pins).
|
||||
//
|
||||
// Records are written into a growable host buffer per command buffer, never into the game's command memory: games hand
|
||||
// the same command memory to several command buffers recording at once (budgeted for real NVN encodings), and our
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "nvn_api.h"
|
||||
@@ -235,30 +236,169 @@ static void copy_view(view_t *dst, uint8_t *has, const NVNtextureView *v) {
|
||||
// ---------- texture pins
|
||||
// Recordings refer to textures by pointer, but the game may finalize or reuse an NVNtexture after recording and before
|
||||
// the submit (NVN itself captured only GPU state). Records therefore point at an immutable host copy per texture
|
||||
// initialization (tex_t.serial). The copies are kept for the whole run (MacNativeGames sweeps them for long sessions).
|
||||
static PlatMutex pin_lock = PLAT_MUTEX_INIT;
|
||||
static nvn_hmap pins = {.klen = 8}; // serial -> tex_t copy
|
||||
// initialization (tex_t.serial). The game initializes its transient targets again every frame, about 29 new serials a
|
||||
// frame on 1.4.3, so copies kept for the run grew the heap by about 44 MiB per minute of play, standing still too, until
|
||||
// the commit limit ended the game after a few hours. A copy is freed once nothing can read it any more:
|
||||
// - refs counts the recording copies whose ops name it: live ones, released ones a queued submission still reads, and
|
||||
// per-submission clones (pin_refs, at flatten_recording and wherever such a copy is freed or cloned);
|
||||
// - a recording in progress holds pins no copy counts yet, through its own tex_pin calls and through ops inlined from a
|
||||
// copy that was released since: a pin goes only NVN_PIN_AGE submits after its last tex_pin and after refs fell to 0;
|
||||
// - the replay's current render targets carry over to later submissions and are compared by pointer (vk_exec.c set_rt),
|
||||
// so a freed address reused by a new pin would read as the same target: nvn_pins_sweep runs between submissions on
|
||||
// the thread that replays them and keeps the pins it is given.
|
||||
// MacNativeGames hit the same growth (3.16 million copies, 405 MB after 27 minutes) and marks pins by walking every live
|
||||
// recording each window (hle_nvn.c pin_scan); counting at copy and release walks each copy's ops once more, when it is
|
||||
// freed. TOTK_PIN_SWEEP=0 keeps every copy.
|
||||
typedef struct {
|
||||
tex_t t; // first: ops hold pins as tex_t pointers
|
||||
uint64_t last_use; // nsubmits at the last tex_pin or the last drop of refs
|
||||
uint32_t refs;
|
||||
uint8_t queued; // in pin_q
|
||||
} pin_t;
|
||||
_Static_assert(offsetof(pin_t, t) == 0, "ops hold pins as tex_t pointers");
|
||||
typedef struct { pin_t *p; uint64_t at; } pin_due;
|
||||
static PlatMutex pin_lock = PLAT_MUTEX_INIT; // lock order: copy_lock, then pin_lock
|
||||
static nvn_hmap pins = {.klen = 8}; // serial -> pin_t
|
||||
static pin_due *pin_q; // pins with no counted copy, oldest first: [pin_qh, pin_qn)
|
||||
static size_t pin_qh, pin_qn, pin_qcap;
|
||||
static uint64_t pins_live, pins_freed;
|
||||
static uint64_t nsubmits; // QueueSubmitCommands calls so far
|
||||
|
||||
static void pin_queue(pin_t *p, uint64_t at) { // pin_lock held
|
||||
if (p->queued) return;
|
||||
if (pin_qh && pin_qh * 2 >= pin_qn) {
|
||||
memmove(pin_q, pin_q + pin_qh, sizeof *pin_q * (pin_qn - pin_qh));
|
||||
pin_qn -= pin_qh, pin_qh = 0;
|
||||
}
|
||||
if (pin_qn == pin_qcap) {
|
||||
pin_qcap = pin_qcap ? pin_qcap * 2 : 4096;
|
||||
if (!(pin_q = realloc(pin_q, sizeof *pin_q * pin_qcap))) rt_fatal("nvn: out of memory (texture pin queue)");
|
||||
}
|
||||
pin_q[pin_qn++] = (pin_due){p, at};
|
||||
p->queued = 1;
|
||||
}
|
||||
|
||||
static const tex_t *tex_pin(const NVNtexture *nt) {
|
||||
const tex_t *t = (const tex_t *)nt;
|
||||
if (!t) return NULL;
|
||||
uint64_t serial = t->serial;
|
||||
if (!serial) rt_fatal("nvn: a recording references the uninitialized texture object %p", (const void *)t);
|
||||
plat_mutex_lock_shared(&pin_lock);
|
||||
const tex_t *r = nvn_hm_get(&pins, &serial);
|
||||
uint64_t now = __atomic_load_n(&nsubmits, __ATOMIC_RELAXED);
|
||||
plat_mutex_lock_shared(&pin_lock); // nvn_pins_sweep frees under the exclusive lock
|
||||
pin_t *r = nvn_hm_get(&pins, &serial);
|
||||
if (r) __atomic_store_n(&r->last_use, now, __ATOMIC_RELAXED);
|
||||
plat_mutex_unlock_shared(&pin_lock);
|
||||
if (r) return r;
|
||||
if (r) return &r->t;
|
||||
plat_mutex_lock(&pin_lock);
|
||||
r = nvn_hm_get(&pins, &serial);
|
||||
if (!r) {
|
||||
tex_t *copy = malloc(sizeof *copy);
|
||||
if (!copy) rt_fatal("nvn: texture pin copy");
|
||||
*copy = *t;
|
||||
nvn_hm_put(&pins, &serial, copy);
|
||||
r = copy;
|
||||
if (!(r = malloc(sizeof *r))) rt_fatal("nvn: texture pin copy");
|
||||
*r = (pin_t){.t = *t};
|
||||
nvn_hm_put(&pins, &serial, r);
|
||||
pins_live++;
|
||||
pin_queue(r, now); // freed like any other if no copy ever names it
|
||||
}
|
||||
__atomic_store_n(&r->last_use, now, __ATOMIC_RELAXED);
|
||||
plat_mutex_unlock(&pin_lock);
|
||||
return &r->t;
|
||||
}
|
||||
|
||||
// The pins an op's payload names, into out (at most 9); returns how many.
|
||||
static int op_pins(uint32_t op, const void *a, const tex_t *out[9]) {
|
||||
int k = 0;
|
||||
switch (op) {
|
||||
case OP_SET_RT: {
|
||||
const p_set_rt *r = a;
|
||||
int n = r->n < 0 ? 0 : r->n > 8 ? 8 : r->n;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (r->color[i]) out[k++] = r->color[i];
|
||||
if (r->depth) out[k++] = r->depth;
|
||||
break;
|
||||
}
|
||||
case OP_COPY_B2T: case OP_COPY_T2B:
|
||||
if (((const p_copy_b2t *)a)->tex) out[k++] = ((const p_copy_b2t *)a)->tex;
|
||||
break;
|
||||
case OP_CLEAR_TEX:
|
||||
if (((const p_clear_tex *)a)->tex) out[k++] = ((const p_clear_tex *)a)->tex;
|
||||
break;
|
||||
case OP_COPY_T2T: {
|
||||
const p_copy_t2t *c = a;
|
||||
if (c->src) out[k++] = c->src;
|
||||
if (c->dst) out[k++] = c->dst;
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
static void pin_ref(const tex_t *t, int delta, uint64_t now) {
|
||||
pin_t *p = (pin_t *)(uintptr_t)t;
|
||||
if (delta > 0) {
|
||||
__atomic_add_fetch(&p->refs, 1, __ATOMIC_RELAXED);
|
||||
return;
|
||||
}
|
||||
__atomic_store_n(&p->last_use, now, __ATOMIC_RELAXED);
|
||||
if (__atomic_sub_fetch(&p->refs, 1, __ATOMIC_ACQ_REL)) return;
|
||||
plat_mutex_lock(&pin_lock);
|
||||
pin_queue(p, now);
|
||||
plat_mutex_unlock(&pin_lock);
|
||||
}
|
||||
|
||||
// Counts (delta 1) or uncounts (-1) the pins named by the copy at handle, OP_BEGIN to OP_END; returns how many.
|
||||
static uint32_t pin_refs(const uint8_t *copy, int delta) {
|
||||
uint64_t now = __atomic_load_n(&nsubmits, __ATOMIC_RELAXED);
|
||||
uint32_t n = 0;
|
||||
for (const op_hdr *h = nvn_op_first((uintptr_t)copy); h->op != OP_END; h = nvn_op_next(h)) {
|
||||
const tex_t *p[9];
|
||||
int k = op_pins(h->op, nvn_op_payload(h), p);
|
||||
for (int i = 0; i < k; i++) pin_ref(p[i], delta, now);
|
||||
n += (uint32_t)k;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void nvn_pins_sweep(uint64_t now, const tex_t *const *keep, int nkeep) {
|
||||
if (now < NVN_PIN_AGE) return;
|
||||
static int on = -1;
|
||||
if (on < 0) {
|
||||
const char *s = cfg_get("TOTK_PIN_SWEEP");
|
||||
on = !(s && !strcmp(s, "0"));
|
||||
if (!on) rt_log("nvn: TOTK_PIN_SWEEP=0: texture pin copies are kept for the run (the heap grows in long sessions)");
|
||||
}
|
||||
if (!on) return;
|
||||
static uint64_t logged;
|
||||
plat_mutex_lock(&pin_lock);
|
||||
for (int budget = 1024; pin_qh < pin_qn && budget; budget--) {
|
||||
pin_due d = pin_q[pin_qh];
|
||||
if (d.at > now - NVN_PIN_AGE) break;
|
||||
pin_qh++;
|
||||
pin_t *p = d.p;
|
||||
p->queued = 0;
|
||||
if (__atomic_load_n(&p->refs, __ATOMIC_ACQUIRE)) continue; // queued again when the count falls to 0
|
||||
uint64_t last = __atomic_load_n(&p->last_use, __ATOMIC_RELAXED);
|
||||
int kept = 0;
|
||||
for (int i = 0; i < nkeep; i++) kept |= keep[i] == &p->t;
|
||||
if (kept || last > now - NVN_PIN_AGE) {
|
||||
pin_queue(p, kept ? now : last);
|
||||
continue;
|
||||
}
|
||||
nvn_hm_del(&pins, &p->t.serial);
|
||||
free(p);
|
||||
pins_live--, pins_freed++;
|
||||
}
|
||||
if (now / 7200 != logged) {
|
||||
logged = now / 7200;
|
||||
rt_log("nvn: texture pins at submit %llu: %llu live, %llu waiting, %llu freed so far", (unsigned long long)now,
|
||||
(unsigned long long)pins_live, (unsigned long long)(pin_qn - pin_qh), (unsigned long long)pins_freed);
|
||||
}
|
||||
plat_mutex_unlock(&pin_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
void nvn_pins_stats(uint64_t *live, uint64_t *freed) {
|
||||
plat_mutex_lock_shared(&pin_lock);
|
||||
*live = pins_live, *freed = pins_freed;
|
||||
plat_mutex_unlock_shared(&pin_lock);
|
||||
}
|
||||
|
||||
// ---------- recording copies
|
||||
@@ -266,14 +406,15 @@ static const tex_t *tex_pin(const NVNtexture *nt) {
|
||||
// adding control memory again releases the copies recorded there without walking every live copy. 4 KiB pages cost
|
||||
// about 500 lookups per AddControlMemory under the exclusive lock (1.4.3 field: 53 calls a frame, 2 MiB ranges, 3.7
|
||||
// copies per page), and the recording threads waited on it.
|
||||
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred; } copy_prefix; // deferred: 0 unknown, 1 none, 2 some
|
||||
// deferred: 0 unknown, 1 none, 2 some; pins: texture pins its ops name, counted in their refs (pin_refs)
|
||||
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins; } copy_prefix;
|
||||
_Static_assert(sizeof(copy_prefix) % 8 == 0, "records stay 8-byte aligned");
|
||||
typedef struct { uintptr_t lo, hi; uint8_t *copy; } cmd_copy;
|
||||
typedef struct { cmd_copy *v; uint32_t n, cap; } copy_bucket;
|
||||
static PlatMutex copy_lock = PLAT_MUTEX_INIT; // guards the two maps, the counters and every prefix
|
||||
static nvn_hmap copy_pages = {.klen = 8}; // page -> copy_bucket
|
||||
static nvn_hmap copy_live = {.klen = 8}; // handle -> copy_prefix of a live copy
|
||||
static uint64_t ncopies, copy_bytes, copies_made, nsubmits;
|
||||
static uint64_t ncopies, copy_bytes, copies_made;
|
||||
// Asynchronous replay: copies released by the game, and per-submission clones, that a queued submission still reads.
|
||||
// Freed once gfx_replayed_submits() reaches their used_at. Empty with synchronous replay.
|
||||
static copy_prefix **zombies;
|
||||
@@ -288,10 +429,15 @@ static void zombie_add(copy_prefix *pf) { // copy_lock held
|
||||
zombies[nzombies++] = pf;
|
||||
}
|
||||
|
||||
static void copy_free(copy_prefix *pf) { // copy_lock held
|
||||
if (pf->pins) pin_refs((const uint8_t *)(pf + 1), -1);
|
||||
free(pf);
|
||||
}
|
||||
|
||||
static void zombie_sweep(uint64_t replayed) { // copy_lock held
|
||||
for (uint32_t i = 0; i < nzombies;) {
|
||||
if (zombies[i]->used_at <= replayed) {
|
||||
free(zombies[i]);
|
||||
copy_free(zombies[i]);
|
||||
zombies[i] = zombies[--nzombies];
|
||||
} else {
|
||||
i++;
|
||||
@@ -337,19 +483,24 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
|
||||
uint8_t *out = realloc_wait(NULL, sizeof(copy_prefix) + n, "command copy");
|
||||
if (!out) rt_fatal("nvn: command copy allocation");
|
||||
memcpy(out + sizeof(copy_prefix), p, n);
|
||||
uint64_t pins_named = 0;
|
||||
for (size_t at = 0;;) { // the records stay checked: a corrupt one stops here, not in the replay
|
||||
op_hdr h;
|
||||
memcpy(&h, p + at, sizeof h);
|
||||
if (h.size < 8 || (h.size & 7) || h.op >= OP_COUNT_ || h.op == OP_JUMP || h.op == OP_CALL || at + h.size > n ||
|
||||
(!at && h.op != OP_BEGIN))
|
||||
rt_fatal("nvn: corrupt recording at %p (op %u size %u)", (const void *)(p + at), h.op, h.size);
|
||||
const tex_t *pinned[9];
|
||||
int k = op_pins(h.op, out + sizeof(copy_prefix) + at + 8, pinned);
|
||||
for (int i = 0; i < k; i++) pin_ref(pinned[i], 1, 0);
|
||||
pins_named += (uint64_t)k;
|
||||
at += h.size;
|
||||
if (h.op == OP_END) {
|
||||
if (at != n) rt_fatal("nvn: recording at %p ends at %zu of %zu bytes", (const void *)p, at, n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
*(copy_prefix *)out = (copy_prefix){COPY_MAGIC, n, 0, 0};
|
||||
*(copy_prefix *)out = (copy_prefix){.magic = COPY_MAGIC, .bytes = n, .pins = pins_named};
|
||||
return out + sizeof(copy_prefix);
|
||||
}
|
||||
|
||||
@@ -399,7 +550,7 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
|
||||
copy_bytes -= pf->bytes;
|
||||
pf->magic = 0;
|
||||
if (pf->used_at > replayed) zombie_add(pf); // a queued submission still reads it
|
||||
else free(pf);
|
||||
else copy_free(pf);
|
||||
b->v[i] = b->v[--b->n];
|
||||
}
|
||||
if (!b->n) {
|
||||
@@ -1035,6 +1186,7 @@ void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
|
||||
copy_prefix *cl = malloc(sz);
|
||||
if (!cl) rt_fatal("nvn: out of memory (recording clone, %zu bytes)", sz);
|
||||
memcpy(cl, pf, sz);
|
||||
if (cl->pins) pin_refs((const uint8_t *)(cl + 1), 1);
|
||||
cl->magic = 0; // not a live handle: the game never sees it
|
||||
cl->used_at = now;
|
||||
zombie_add(cl);
|
||||
|
||||
+12
-5
@@ -11,6 +11,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "nvn_api.h"
|
||||
@@ -667,8 +668,12 @@ static void sleep_until(uint64_t deadline) {
|
||||
}
|
||||
|
||||
// NVNwindowAcquireTextureResult nvnWindowAcquireTexture(NVNwindow *w, NVNsync *s, int *index): blocks here, not in the
|
||||
// present, until the image's previous present plus one period has passed (the game often presents with a lock held).
|
||||
// That is the time the next frame goes to the display, so the image is free as soon as that time is reached.
|
||||
// present, until the image is free (the game often presents with a lock held). Without the frame queue that is the
|
||||
// image's previous present plus one period, when the next frame goes to the display, so a frame gets exactly one period
|
||||
// from acquire to present. Its present is a copy already on the queue by its display time, so with the frame queue
|
||||
// (TOTK_FRAME_QUEUE=1) the image is free from that time on: the game draws the next frame while the previous one waits
|
||||
// for its turn, and a frame that takes up to one more period still makes its slot, for up to one frame more latency.
|
||||
// Town at 4K with DLAA at 60 on an RTX 5070: frames over 17.5 ms 1-8 per 30 s with the queue against 25-33 without.
|
||||
NVN_API(nvnWindowAcquireTexture) {
|
||||
win_t *o = arg_p(c, 0);
|
||||
sync_t *s = arg_p(c, 1);
|
||||
@@ -693,12 +698,14 @@ NVN_API(nvnWindowAcquireTexture) {
|
||||
|
||||
// void nvnQueuePresentTexture(NVNqueue *q, NVNwindow *w, int index): queues the copy and returns at once. The display
|
||||
// time comes from the pacer: the back end's present thread holds the host present until then (back-edge pacing, as the
|
||||
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later the
|
||||
// image is free for the next acquire. Added latency: the slack of the frame (period minus its work), as on the Switch.
|
||||
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later (at that
|
||||
// time with the frame queue) the image is free for the next acquire. Added latency: the slack of the frame (period minus
|
||||
// its work), as on the Switch, plus up to a period with the frame queue.
|
||||
NVN_API(nvnQueuePresentTexture) {
|
||||
win_t *o = arg_p(c, 1);
|
||||
int idx = (int)arg_x(c, 2);
|
||||
movie_tick(c); // ov_movie.c: hand over decoded movie frames that are due; needs the core, so before the wait
|
||||
res_present(c); // resolution.c: a DLSS mode change sets the game's render size again, between frames
|
||||
NVN_BLOCKING();
|
||||
if (o->magic != NVN_WIN_MAGIC || idx < 0 || idx >= o->ntex || idx >= 4 || !o->tex[idx])
|
||||
rt_fatal("nvn: QueuePresentTexture of image %d on window %p", idx, (void *)o);
|
||||
@@ -708,7 +715,7 @@ NVN_API(nvnQueuePresentTexture) {
|
||||
uint64_t slot = now + pace(&o->pace, now, period);
|
||||
gfx_backend()->present(t, o->crop, slot);
|
||||
o->ticket[idx] = o->presents;
|
||||
o->avail[idx] = slot + period;
|
||||
o->avail[idx] = cfg_bool("TOTK_FRAME_QUEUE") ? slot : slot + period;
|
||||
frame_step_present(slot, o->interval); // frame_rate.c: the real-time step follows the display times
|
||||
rt_frame_presented(t, o->presents);
|
||||
}
|
||||
@@ -694,6 +694,55 @@ static void parse_all(uint64_t h) {
|
||||
n_ops = OP_COUNT_ - OP_SIGNAL_EVENT;
|
||||
}
|
||||
|
||||
// A recording naming tex3 as its render target; *pin: the texture pin in its SET_RT payload.
|
||||
static uint64_t record_target(uint8_t *ctl, const tex_t **pin) {
|
||||
const NVNtexture *colors[1] = {&tex3};
|
||||
begin(&cmd2, ctl, 0x3a000);
|
||||
CALL(nvnCommandBufferSetRenderTargets, U(&cmd2), 1, U(colors), 0, 0, 0);
|
||||
uint64_t h = CALL(nvnCommandBufferEndRecording, U(&cmd2));
|
||||
*pin = NULL;
|
||||
for (const op_hdr *o = nvn_op_first(h); o->op != OP_END; o = nvn_op_next(o))
|
||||
if (o->op == OP_SET_RT) *pin = ((const p_set_rt *)nvn_op_payload(o))->color[0];
|
||||
return h;
|
||||
}
|
||||
|
||||
// Texture pins are freed NVN_PIN_AGE submits after the last recording copy naming them is released, unless the replay
|
||||
// keeps them as its current render targets; a texture named again gets a new pin.
|
||||
static void probe_pins(void) {
|
||||
uint8_t *ctl = mem_kernel_alloc(0x1000, 16);
|
||||
set_tex(NVN_FORMAT_RGBA8, 16, 16, 1, 0x20000);
|
||||
CHECK((uint8_t)CALL(nvnTextureInitialize, U(&tex3), U(&texb)) == 1, "pin probe texture");
|
||||
uint64_t serial = ((const tex_t *)(const void *)&tex3)->serial;
|
||||
uint64_t live0, freed0, live, freed;
|
||||
nvn_pins_stats(&live0, &freed0);
|
||||
const tex_t *pin;
|
||||
uint64_t h = record_target(ctl, &pin);
|
||||
nvn_pins_stats(&live, &freed);
|
||||
CHECK(pin && pin->serial == serial && live == live0 + 1, "a recording naming a new texture makes one pin");
|
||||
uint64_t s = nvn_submit_count();
|
||||
nvn_pins_sweep(s + 10 * NVN_PIN_AGE, NULL, 0);
|
||||
nvn_pins_stats(&live, &freed);
|
||||
CHECK(live == live0 + 1 && freed == freed0 && nvn_handle_live(h), "a pin a live recording names stays");
|
||||
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctl), 0x1000); // retires h: the pin has no copy left
|
||||
CHECK(!nvn_handle_live(h), "the pin's recording is retired");
|
||||
nvn_pins_sweep(s + NVN_PIN_AGE - 1, NULL, 0);
|
||||
nvn_pins_stats(&live, &freed);
|
||||
CHECK(live == live0 + 1 && freed == freed0, "a pin stays NVN_PIN_AGE submits after its last copy went");
|
||||
nvn_pins_sweep(s + NVN_PIN_AGE, &pin, 1);
|
||||
nvn_pins_stats(&live, &freed);
|
||||
CHECK(live == live0 + 1 && freed == freed0, "the replay's current render target is kept");
|
||||
nvn_pins_sweep(s + 2 * NVN_PIN_AGE, NULL, 0);
|
||||
nvn_pins_stats(&live, &freed);
|
||||
CHECK(live == live0 && freed == freed0 + 1, "an unnamed, unkept pin is freed (live %llu of %llu, freed %llu of %llu)",
|
||||
(unsigned long long)live, (unsigned long long)live0, (unsigned long long)freed, (unsigned long long)freed0 + 1);
|
||||
h = record_target(ctl, &pin);
|
||||
nvn_pins_stats(&live, &freed);
|
||||
CHECK(pin && pin->serial == serial && live == live0 + 1, "naming the texture again makes a new pin");
|
||||
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctl), 0x1000);
|
||||
CALL(nvnCommandBufferFinalize, U(&cmd2));
|
||||
CALL(nvnTextureFinalize, U(&tex3));
|
||||
}
|
||||
|
||||
static void probe_commands(void) {
|
||||
ctlmem1 = mem_kernel_alloc(0x1000, 16);
|
||||
ctlmem2 = mem_kernel_alloc(0x1000, 16);
|
||||
@@ -722,6 +771,7 @@ static void probe_commands(void) {
|
||||
CALL(nvnCommandBufferAddControlMemory, U(&cmd2), U(ctlmem2), 0x1000);
|
||||
CHECK(!nvn_handle_live(h2) && nvn_handle_live(h), "reusing control memory retires only the handles recorded in it");
|
||||
CALL(nvnCommandBufferFinalize, U(&cmd2));
|
||||
probe_pins();
|
||||
n_objects += 5;
|
||||
}
|
||||
|
||||
|
||||
+196
-4
@@ -73,6 +73,7 @@ typedef struct {
|
||||
unsigned owed; // events to signal on the next guest thread (bit per MOVIE_EV_*)
|
||||
uint64_t t0; // host time of the first frame handed over (pacing)
|
||||
int64_t pts0; // its presentation time (us)
|
||||
MovieFit *fit; // picture shrunk inside the frame; NULL = the shapes agree, copy
|
||||
MovieStats st;
|
||||
PlatMutex mu;
|
||||
PlatCond work, idle;
|
||||
@@ -207,6 +208,173 @@ static void to_nv12(const vpx_image_t *img, uint8_t *dst, int w, int h) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Keeping the movie's shape. The game draws a decoded frame over the whole canvas, so on an output that is not 16:9
|
||||
// (output_res in totk.ini) the 1920x1080 movies would be stretched. The frame keeps its size; the picture is shrunk
|
||||
// inside it and the rest is black, so that after the game's stretch the picture has the movie's own shape again: a wider
|
||||
// output gets a squeezed picture with black at the left and right, a taller one (16:10) black above and below.
|
||||
// The shrink is a box filter (each output pixel averages the source pixels it covers, weights in 1/4096), separable, with
|
||||
// the per-column and per-row source spans and weights built once per movie (movie_fit_new), so the frame loop only
|
||||
// multiplies and adds.
|
||||
enum { FIT_ONE = 4096, FIT_Y_BLACK = 16, FIT_UV_BLACK = 128 }; // black of limited-range video: Y 16, U = V 128
|
||||
typedef struct { int n, taps; int *start; uint16_t *wt; } FitAxis; // output i: taps weights from source start[i]
|
||||
struct MovieFit {
|
||||
int w, h, x0, y0, pw, ph; // frame, and the picture's corner and size in it (all even)
|
||||
FitAxis lx, ly, cx, cy; // luma, chroma; columns, rows; n = 0 where the axis is kept
|
||||
uint8_t *tmp; // horizontally shrunk rows, before the vertical pass
|
||||
uint32_t *acc; // one row of the vertical sum
|
||||
};
|
||||
|
||||
// Box filter from src_n to dst_n (dst_n < src_n): output i covers [i, i + 1) * src_n / dst_n of the source.
|
||||
static int axis_build(FitAxis *a, int src_n, int dst_n) {
|
||||
double s = (double)src_n / dst_n;
|
||||
a->n = dst_n;
|
||||
a->taps = (int)s + 2 > src_n ? src_n : (int)s + 2;
|
||||
a->start = malloc((size_t)dst_n * sizeof *a->start);
|
||||
a->wt = malloc((size_t)dst_n * (size_t)a->taps * sizeof *a->wt);
|
||||
if (!a->start || !a->wt) return 0;
|
||||
for (int i = 0; i < dst_n; i++) {
|
||||
double lo = i * s, hi = (i + 1) * s;
|
||||
int st = (int)lo;
|
||||
if (st > src_n - a->taps) st = src_n - a->taps;
|
||||
a->start[i] = st;
|
||||
int sum = 0, big = 0;
|
||||
uint16_t *w = a->wt + (size_t)i * a->taps;
|
||||
for (int t = 0; t < a->taps; t++) {
|
||||
double ov = (st + t + 1 < hi ? st + t + 1 : hi) - (st + t > lo ? st + t : lo);
|
||||
w[t] = ov > 0 ? (uint16_t)(ov / s * FIT_ONE + 0.5) : 0;
|
||||
sum += w[t];
|
||||
if (w[t] > w[big]) big = t;
|
||||
}
|
||||
w[big] = (uint16_t)(w[big] + FIT_ONE - sum); // the weights add up to exactly 1
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static void axis_free(FitAxis *a) {
|
||||
free(a->start);
|
||||
free(a->wt);
|
||||
}
|
||||
|
||||
void movie_fit_free(MovieFit *f) {
|
||||
if (!f) return;
|
||||
axis_free(&f->lx), axis_free(&f->ly), axis_free(&f->cx), axis_free(&f->cy);
|
||||
free(f->tmp);
|
||||
free(f->acc);
|
||||
free(f);
|
||||
}
|
||||
|
||||
// The plan for a w x h movie shown on an ow x oh output; NULL when the shapes agree within 1% (the frame is copied).
|
||||
MovieFit *movie_fit_new(int w, int h, unsigned ow, unsigned oh) {
|
||||
if (w < 16 || h < 16 || (w | h) & 1 || !ow || !oh) return NULL;
|
||||
double movie = (double)w / h, out = (double)ow / oh, r = out / movie;
|
||||
if (r > 0.99 && r < 1.01) return NULL;
|
||||
MovieFit *f = calloc(1, sizeof *f);
|
||||
if (!f) {
|
||||
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
|
||||
return NULL;
|
||||
}
|
||||
f->w = w, f->h = h, f->pw = w, f->ph = h;
|
||||
if (r > 1) f->pw = (int)(w / r / 2 + 0.5) * 2; // wider output: squeeze sideways
|
||||
else f->ph = (int)(h * r / 2 + 0.5) * 2; // taller output: squeeze vertically
|
||||
if (f->pw < 2) f->pw = 2;
|
||||
if (f->ph < 2) f->ph = 2;
|
||||
f->x0 = (w - f->pw) / 2 & ~1;
|
||||
f->y0 = (h - f->ph) / 2 & ~1;
|
||||
int ok = 1;
|
||||
if (f->pw < w) ok = axis_build(&f->lx, w, f->pw) && axis_build(&f->cx, w / 2, f->pw / 2);
|
||||
if (f->ph < h) ok = ok && axis_build(&f->ly, h, f->ph) && axis_build(&f->cy, h / 2, f->ph / 2);
|
||||
f->tmp = malloc((size_t)f->pw * h);
|
||||
f->acc = malloc((size_t)f->pw * sizeof *f->acc);
|
||||
if (!ok || !f->tmp || !f->acc) {
|
||||
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
|
||||
movie_fit_free(f);
|
||||
return NULL;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
// One output row of the horizontal pass: dst gets the shrunk src row (dstep 2 writes every other byte, for U and V).
|
||||
// The tap count is a constant in the 3 and 4 tap copies (what 2x and below squeezes need), so the compiler unrolls the sum.
|
||||
static inline __attribute__((always_inline)) void fit_row_taps(const FitAxis *a, const uint8_t *src, uint8_t *dst,
|
||||
int dstep, int taps) {
|
||||
for (int i = 0; i < a->n; i++) {
|
||||
const uint8_t *p = src + a->start[i];
|
||||
const uint16_t *w = a->wt + (size_t)i * taps;
|
||||
uint32_t v = FIT_ONE / 2;
|
||||
for (int t = 0; t < taps; t++) v += w[t] * p[t];
|
||||
dst[(size_t)i * dstep] = (uint8_t)(v / FIT_ONE);
|
||||
}
|
||||
}
|
||||
static void fit_row_h(const FitAxis *a, const uint8_t *src, uint8_t *dst, int dstep) {
|
||||
if (a->taps == 3) fit_row_taps(a, src, dst, dstep, 3);
|
||||
else if (a->taps == 4) fit_row_taps(a, src, dst, dstep, 4);
|
||||
else fit_row_taps(a, src, dst, dstep, a->taps);
|
||||
}
|
||||
|
||||
// One plane (sw x sh) shrunk into the pw x ph picture whose corner (ox, oy) is in dst; ax/ay NULL = that axis is kept.
|
||||
static void fit_plane(const MovieFit *f, const uint8_t *src, int ss, int sw, int sh, uint8_t *dst, int ds, int dstep,
|
||||
int ox, int oy, const FitAxis *ax, const FitAxis *ay, int pw, int ph) {
|
||||
uint8_t *base = dst + (size_t)oy * ds + (size_t)ox * dstep;
|
||||
if (!ay) { // sideways only: straight into the frame
|
||||
for (int y = 0; y < sh; y++) {
|
||||
const uint8_t *row = src + (size_t)y * ss;
|
||||
uint8_t *o = base + (size_t)y * ds;
|
||||
if (ax) fit_row_h(ax, row, o, dstep);
|
||||
else if (dstep == 1) memcpy(o, row, (size_t)sw);
|
||||
else for (int x = 0; x < sw; x++) o[(size_t)x * dstep] = row[x];
|
||||
}
|
||||
return;
|
||||
}
|
||||
const uint8_t *in = src;
|
||||
int is = ss;
|
||||
if (ax) {
|
||||
for (int y = 0; y < sh; y++) fit_row_h(ax, src + (size_t)y * ss, f->tmp + (size_t)y * pw, 1);
|
||||
in = f->tmp, is = pw;
|
||||
}
|
||||
for (int y = 0; y < ph; y++) {
|
||||
const uint16_t *w = ay->wt + (size_t)y * ay->taps;
|
||||
const uint8_t *r0 = in + (size_t)ay->start[y] * is;
|
||||
uint8_t *o = base + (size_t)y * ds;
|
||||
if (ay->taps == 3) { // the common case, summed in registers
|
||||
const uint8_t *r1 = r0 + is, *r2 = r1 + is;
|
||||
for (int x = 0; x < pw; x++)
|
||||
o[(size_t)x * dstep] = (uint8_t)((FIT_ONE / 2 + w[0] * r0[x] + w[1] * r1[x] + w[2] * r2[x]) / FIT_ONE);
|
||||
continue;
|
||||
}
|
||||
memset(f->acc, 0, (size_t)pw * sizeof *f->acc);
|
||||
for (int t = 0; t < ay->taps; t++) {
|
||||
const uint8_t *row = r0 + (size_t)t * is;
|
||||
uint32_t k = w[t];
|
||||
for (int x = 0; x < pw; x++) f->acc[x] += k * row[x];
|
||||
}
|
||||
for (int x = 0; x < pw; x++) o[(size_t)x * dstep] = (uint8_t)((f->acc[x] + FIT_ONE / 2) / FIT_ONE);
|
||||
}
|
||||
}
|
||||
|
||||
// I420 planes -> packed NV12 with the picture shrunk inside black bars: w x h of Y, then w x h/2 of interleaved U, V.
|
||||
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst) {
|
||||
int w = f->w, h = f->h, cw = w / 2, ch = h / 2;
|
||||
uint8_t *uv = dst + (size_t)w * h;
|
||||
// Black only where the picture does not go: whole rows above and below it, the strips left and right of it.
|
||||
int rx = f->x0 + f->pw, ry = f->y0 + f->ph;
|
||||
memset(dst, FIT_Y_BLACK, (size_t)f->y0 * w);
|
||||
memset(dst + (size_t)ry * w, FIT_Y_BLACK, (size_t)(h - ry) * w);
|
||||
memset(uv, FIT_UV_BLACK, (size_t)(f->y0 / 2) * w);
|
||||
memset(uv + (size_t)(ry / 2) * w, FIT_UV_BLACK, (size_t)(ch - ry / 2) * w);
|
||||
for (int y = f->y0; y < ry; y++) {
|
||||
memset(dst + (size_t)y * w, FIT_Y_BLACK, (size_t)f->x0);
|
||||
memset(dst + (size_t)y * w + rx, FIT_Y_BLACK, (size_t)(w - rx));
|
||||
}
|
||||
for (int y = f->y0 / 2; y < ry / 2; y++) {
|
||||
memset(uv + (size_t)y * w, FIT_UV_BLACK, (size_t)f->x0);
|
||||
memset(uv + (size_t)y * w + rx, FIT_UV_BLACK, (size_t)(w - rx));
|
||||
}
|
||||
fit_plane(f, plane[0], stride[0], w, h, dst, w, 1, f->x0, f->y0, f->lx.n ? &f->lx : NULL, f->ly.n ? &f->ly : NULL,
|
||||
f->pw, f->ph);
|
||||
for (int c = 0; c < 2; c++)
|
||||
fit_plane(f, plane[1 + c], stride[1 + c], cw, ch, uv + c, w, 2, f->x0 / 2, f->y0 / 2,
|
||||
f->cx.n ? &f->cx : NULL, f->cy.n ? &f->cy : NULL, f->pw / 2, f->ph / 2);
|
||||
}
|
||||
|
||||
static int free_out(const Movie *m) {
|
||||
for (int i = 0; i < OUT_BUFS; i++)
|
||||
if (!m->out_held[i]) return i;
|
||||
@@ -250,7 +418,9 @@ static void worker(void *arg) {
|
||||
slot = free_out(m);
|
||||
m->out_held[slot] = 1; // reserved: in no queue, so the game cannot release it
|
||||
}
|
||||
uint64_t gen = m->gen, dt = 0;
|
||||
uint64_t gen = m->gen, dt = 0, fit_ns = 0;
|
||||
// A plan made for another frame size (a start that found the worker busy) is not used: it would overrun dst.
|
||||
const MovieFit *fit = m->fit && m->fit->w == m->w && m->fit->h == m->h ? m->fit : NULL;
|
||||
int got = 0, w = m->w, h = m->h;
|
||||
uint8_t *dst = slot >= 0 ? m->out[slot] : NULL;
|
||||
vpx_codec_err_t e = VPX_CODEC_OK;
|
||||
@@ -265,7 +435,12 @@ static void worker(void *arg) {
|
||||
const vpx_image_t *img;
|
||||
while ((img = vpx_codec_get_frame(&m->vpx, &it)) != NULL) {
|
||||
if (img->fmt != VPX_IMG_FMT_I420 || (int)img->d_w != w || (int)img->d_h != h) bad = "layout";
|
||||
else if (!got) to_nv12(img, dst, w, h);
|
||||
else if (!got) {
|
||||
uint64_t t1 = kern_now_ns();
|
||||
if (fit) movie_fit_apply(fit, (const uint8_t *const *)img->planes, img->stride, dst);
|
||||
else to_nv12(img, dst, w, h);
|
||||
fit_ns = kern_now_ns() - t1;
|
||||
}
|
||||
got++;
|
||||
}
|
||||
}
|
||||
@@ -279,6 +454,7 @@ static void worker(void *arg) {
|
||||
m->owed |= 1u << MOVIE_EV_INPUT;
|
||||
if (p.size > 0) {
|
||||
m->st.decode_ns += dt;
|
||||
m->st.fit_ns += fit_ns;
|
||||
if (dt > m->st.max_ns) m->st.max_ns = dt;
|
||||
if (bad) rt_fatal("movie: VP9 frame layout differs from the configured %dx%d I420", w, h);
|
||||
if (e != VPX_CODEC_OK || !got) {
|
||||
@@ -302,8 +478,10 @@ static void worker(void *arg) {
|
||||
push_decoded(m, slot, 0, p.time, frame_bytes(m));
|
||||
m->st.frames++;
|
||||
if (m->st.frames % 600 == 0)
|
||||
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f)", (unsigned long long)m->st.frames,
|
||||
m->st.decode_ns / 1e6 / (double)m->st.frames, m->st.max_ns / 1e6);
|
||||
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f), of it %.2f ms %s",
|
||||
(unsigned long long)m->st.frames, m->st.decode_ns / 1e6 / (double)m->st.frames,
|
||||
m->st.max_ns / 1e6, m->st.fit_ns / 1e6 / (double)m->st.frames,
|
||||
m->fit ? "fitting the picture" : "converting");
|
||||
}
|
||||
}
|
||||
if (p.flags & MOVIE_FLAG_EOS && !m->eos_in) {
|
||||
@@ -355,6 +533,8 @@ static void release_codec(Movie *m) { // m->mu held, worker quiesced
|
||||
free(m->out[i]);
|
||||
m->out[i] = NULL;
|
||||
}
|
||||
movie_fit_free(m->fit);
|
||||
m->fit = NULL;
|
||||
m->configured = m->started = 0;
|
||||
reset_queues(m);
|
||||
}
|
||||
@@ -597,6 +777,18 @@ static void ov_start(Cpu *c) {
|
||||
plat_mutex_lock(&m->mu);
|
||||
int st = ST_NOT_CONFIGURED;
|
||||
if (m->configured) {
|
||||
if (!m->started && !m->busy) { // the worker is idle: it reads m->fit only when it takes an input
|
||||
movie_fit_free(m->fit);
|
||||
m->fit = NULL;
|
||||
unsigned out[2];
|
||||
res_wanted(out);
|
||||
if (strcmp(cfg_value("TOTK_MOVIE_ASPECT"), "stretch")) {
|
||||
m->fit = movie_fit_new(m->w, m->h, out[0], out[1]);
|
||||
if (m->fit)
|
||||
rt_log("movie: %dx%d picture kept in shape on the %ux%u output (%dx%d inside the frame)", m->w,
|
||||
m->h, out[0], out[1], m->fit->pw, m->fit->ph);
|
||||
}
|
||||
}
|
||||
m->started = 1;
|
||||
m->owed |= 1u << MOVIE_EV_INPUT;
|
||||
plat_cond_signal(&m->work);
|
||||
|
||||
+35
-3
@@ -405,8 +405,22 @@ static void probe_applet(void) {
|
||||
0x33 ^ 0x44 ^ 0x55 ^ 0x66, 0x48, 0x0F, 0xE0, 0xF1, 0x10, 0xFF, 0xEE};
|
||||
uint8_t dump[AMIIBO_DUMP_SIZE] = {0};
|
||||
memcpy(dump, head, sizeof head);
|
||||
// Link (Archer): character 0x0100, variant 0, figure type 0, model number 0x0353, series 9, format 02
|
||||
static const uint8_t id[8] = {0x01, 0x00, 0x00, 0x00, 0x03, 0x53, 0x09, 0x02};
|
||||
memcpy(dump + 0x54, id, sizeof id);
|
||||
const char *dump_path = "probe-amiibo.bin";
|
||||
int amiibo_ok = plat_file_write(dump_path, dump, sizeof dump) && amiibo_load_file(dump_path);
|
||||
// TagInfo carries the 7-byte UID without its check byte; ModelInfo has the series at 3, the model number
|
||||
// little-endian at 4 and the figure type at 6
|
||||
uint8_t ti[0x58], mi[0x40];
|
||||
amiibo_get_tag_info(ti);
|
||||
amiibo_get_model_info(mi);
|
||||
static const uint8_t want_uid[7] = {0x04, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
|
||||
static const uint8_t want_model[7] = {0x01, 0x00, 0x00, 0x09, 0x53, 0x03, 0x00};
|
||||
CHECK(amiibo_ok && !memcmp(ti, want_uid, 7) && ti[0x0A] == 7, "amiibo TagInfo UID %02x %02x %02x %02x %02x %02x %02x",
|
||||
ti[0], ti[1], ti[2], ti[3], ti[4], ti[5], ti[6]);
|
||||
CHECK(amiibo_ok && !memcmp(mi, want_model, 7), "amiibo ModelInfo %02x %02x %02x %02x %02x %02x %02x", mi[0], mi[1],
|
||||
mi[2], mi[3], mi[4], mi[5], mi[6]);
|
||||
dump[0x0C] = 0xE1;
|
||||
amiibo_ok = amiibo_ok && plat_file_write(dump_path, dump, sizeof dump) && !amiibo_load_file(dump_path);
|
||||
plat_fs_unlink(dump_path);
|
||||
@@ -875,15 +889,33 @@ static void probe_menu_text(void) {
|
||||
CHECK(!strcmp(ui_text_in("ru", UI_SWKBD_OK), "ОК") && !strcmp(ui_text_in("de", UI_SWKBD_CANCEL), "Abbrechen") &&
|
||||
!strcmp(ui_text_in("en", UI_SWKBD_OK), "OK"), "keyboard texts changed");
|
||||
// language names: the same string in every row
|
||||
for (int id = UI_MENU_LANGNAME_EN; id <= UI_MENU_LANGNAME_PT_BR; id++)
|
||||
for (int id = UI_MENU_LANGNAME_EN; id <= UI_MENU_LANGNAME_ES_419; id++)
|
||||
for (size_t i = 1; i < sizeof langs / sizeof *langs; i++)
|
||||
CHECK(!strcmp(ui_text_in(langs[i], (UiTextId)id), ui_text_in("en", (UiTextId)id)), "language name %d differs in %s", id, langs[i]);
|
||||
CHECK(!strcmp(ui_text_in("en", UI_MENU_LANGNAME_RU), "Русский"), "Russian is not shown as Russian");
|
||||
// game language list: pt-BR only where the build ships USpt; the Switch variants are entries of their own
|
||||
{
|
||||
UiGameLang gl[16];
|
||||
int n121 = ui_text_game_langs(121, gl, 16), has[16] = {0};
|
||||
for (int i = 0; i < n121; i++) CHECK(strcmp(gl[i].code, "pt-BR"), "1.2.1 game language list offers pt-BR");
|
||||
int n143 = ui_text_game_langs(143, gl, 16), pt = 0;
|
||||
for (int i = 0; i < n143; i++) {
|
||||
pt |= !strcmp(gl[i].code, "pt-BR");
|
||||
for (int j = 0; j < i; j++) CHECK(strcmp(gl[i].code, gl[j].code), "duplicate game language %s", gl[i].code);
|
||||
}
|
||||
CHECK(pt && n143 == n121 + 1, "1.4.3 game language list lacks pt-BR");
|
||||
static const char *const want[] = {"en-GB", "fr-CA", "es-419"};
|
||||
for (int w = 0; w < 3; w++)
|
||||
for (int i = 0; i < n143; i++) has[w] |= !strcmp(gl[i].code, want[w]);
|
||||
CHECK(has[0] && has[1] && has[2], "game language list lacks en-GB, fr-CA or es-419");
|
||||
}
|
||||
// the overlay formats keep their conversions
|
||||
CHECK(strstr(ui_text_in("ru", UI_MENU_OV_GAME_PCT), "%.0f%%") && strstr(ui_text_in("ru", UI_MENU_OV_MAX_MS), "%.1f"),
|
||||
CHECK(strstr(ui_text_in("ru", UI_MENU_OV_GAME_PCT), "%.0f%%") && strstr(ui_text_in("ru", UI_MENU_OV_MAX_MS), "%.1f") &&
|
||||
strstr(ui_text_in("ru", UI_MENU_OV_LOW1), "%.0f") && strstr(ui_text_in("ru", UI_MENU_OV_LOW01), "%.0f"),
|
||||
"overlay format lost its conversion");
|
||||
CHECK(strstr(ui_text_in("pt-BR", UI_MENU_OV_GAME_PCT), "%.0f%%") &&
|
||||
strstr(ui_text_in("pt-BR", UI_MENU_OV_MAX_MS), "%.1f"), "pt-BR overlay format lost its conversion");
|
||||
strstr(ui_text_in("pt-BR", UI_MENU_OV_MAX_MS), "%.1f") &&
|
||||
strstr(ui_text_in("pt-BR", UI_MENU_OV_LOW1), "%.0f") && strstr(ui_text_in("zh-Hans", UI_MENU_OV_LOW01), "%.0f"), "pt-BR overlay format lost its conversion");
|
||||
// a Brazilian game language gets the Brazilian keyboard row, Portugal's Portuguese keeps its own
|
||||
CHECK(!strcmp(ui_text_lang("pt-BR"), "pt-BR") && !strcmp(ui_text_lang("pt"), "pt") &&
|
||||
strcmp(ui_text_in("pt-BR", UI_SWKBD_PROMPT), ui_text_in("pt", UI_SWKBD_PROMPT)), "pt-BR keyboard row not used");
|
||||
|
||||
@@ -32,6 +32,14 @@ static float g_ring[PERF_HISTORY];
|
||||
static uint64_t g_ring_at[PERF_HISTORY]; // when each frame ended
|
||||
static int g_ring_n, g_ring_pos;
|
||||
|
||||
// The lows: a histogram of the frame times (perf.c's 0.25 ms bins) over the last LOW_SLICES seconds, kept as one slice
|
||||
// per second so the oldest second drops out whole. Per frame: one increment. The percentiles are read twice a second.
|
||||
#define LOW_BINS 1024
|
||||
#define LOW_SLICES 10
|
||||
static uint16_t g_low_hist[LOW_SLICES][LOW_BINS];
|
||||
static int g_low_cur;
|
||||
static uint64_t g_low_t0;
|
||||
|
||||
// The per-frame counters, summed since t0 (the overlay's lines are averages over half a second).
|
||||
static struct {
|
||||
uint64_t t0, frames, gpu0;
|
||||
@@ -47,6 +55,14 @@ void perfmon_frame(uint64_t now, uint64_t interval_ns, const uint64_t *wait_ns,
|
||||
g_ring_at[g_ring_pos] = now;
|
||||
g_ring_pos = (g_ring_pos + 1) % PERF_HISTORY;
|
||||
if (g_ring_n < PERF_HISTORY) g_ring_n++;
|
||||
if (now - g_low_t0 >= 1000000000ull) { // the next second: its slice starts empty
|
||||
g_low_cur = (g_low_cur + 1) % LOW_SLICES;
|
||||
memset(g_low_hist[g_low_cur], 0, sizeof g_low_hist[0]);
|
||||
g_low_t0 = now;
|
||||
}
|
||||
uint64_t bin = interval_ns / 250000ull;
|
||||
uint16_t *h = &g_low_hist[g_low_cur][bin < LOW_BINS ? bin : LOW_BINS - 1];
|
||||
if (*h < 65535) (*h)++;
|
||||
}
|
||||
g_win.frames++;
|
||||
g_win.replay += replay_ns / 1e6;
|
||||
@@ -76,9 +92,27 @@ static void frames_summary(PerfStats *s, uint64_t now) { // (lock held)
|
||||
}
|
||||
}
|
||||
|
||||
// The fps of the slowest 1% and 0.1% of the frames in the window (the bin's upper edge, as perf.c's summary does); -1
|
||||
// when the window holds too few frames. (lock held)
|
||||
static void lows_summary(PerfStats *s) {
|
||||
uint32_t total = 0;
|
||||
for (int k = 0; k < LOW_SLICES; k++)
|
||||
for (int b = 0; b < LOW_BINS; b++) total += g_low_hist[k][b];
|
||||
s->low1_fps = s->low01_fps = -1;
|
||||
if (total < 100) return;
|
||||
uint32_t cut1 = (total + 99) / 100, cut01 = (total + 999) / 1000, acc = 0;
|
||||
for (int b = LOW_BINS - 1; b >= 0 && s->low1_fps < 0; b--) {
|
||||
for (int k = 0; k < LOW_SLICES; k++) acc += g_low_hist[k][b];
|
||||
float fps = 1000.0f / ((b + 1) * 0.25f);
|
||||
if (s->low01_fps < 0 && acc >= cut01) s->low01_fps = fps;
|
||||
if (acc >= cut1) s->low1_fps = fps;
|
||||
}
|
||||
}
|
||||
|
||||
// Every half second: the window's sums as per-frame averages into g_stats, and a new window. (lock held)
|
||||
static void counters_summary(uint64_t now) {
|
||||
if (now - g_win.t0 < 500000000ull) return;
|
||||
lows_summary(&g_stats);
|
||||
uint64_t gpu = perf_gpu_busy_total();
|
||||
if (g_win.t0 && g_win.frames) {
|
||||
double n = (double)g_win.frames;
|
||||
@@ -342,6 +376,8 @@ void perfmon_mode(int mode) {
|
||||
}
|
||||
if (!mode) { // the next start begins a new graph and a new counter window
|
||||
g_ring_n = g_ring_pos = 0;
|
||||
memset(g_low_hist, 0, sizeof g_low_hist);
|
||||
g_low_t0 = 0;
|
||||
memset(&g_win, 0, sizeof g_win);
|
||||
}
|
||||
plat_mutex_unlock(&g_lock);
|
||||
|
||||
@@ -17,6 +17,8 @@ extern "C" {
|
||||
typedef struct {
|
||||
// the game's frames (presents), over the last second
|
||||
float fps, frame_ms_avg, frame_ms_max;
|
||||
// fps of the slowest 1% and 0.1% of the frames over the last ten seconds (-1: too few frames yet)
|
||||
float low1_fps, low01_fps;
|
||||
float frame_ms[PERF_HISTORY]; // oldest first
|
||||
// CPU: the whole machine and this process, percent of all logical processors
|
||||
char cpu_name[64];
|
||||
|
||||
@@ -22,9 +22,11 @@
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/random.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
@@ -268,7 +270,8 @@ int plat_intr_resume(PlatIntr *t, uint64_t to) {
|
||||
// ---- fibers: a stack switch of our own. glibc's swapcontext saves and restores the signal mask with a system call
|
||||
// on every switch, and the game switches fibers often. fiber_swap saves the callee-saved registers, the MXCSR and the
|
||||
// x87 control word on the current stack, stores the stack pointer in *save and loads the other one. A new fiber's
|
||||
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which calls fn(arg) (r12 and r13).
|
||||
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which realigns the stack and calls fn(arg)
|
||||
// (r12 and r13).
|
||||
struct PlatFiber {
|
||||
void *sp;
|
||||
void *stack; // mapped stack (NULL for a thread's own context)
|
||||
@@ -310,6 +313,11 @@ __asm__(".text\n"
|
||||
".type plat_fiber_entry, @function\n"
|
||||
".p2align 4\n"
|
||||
"plat_fiber_entry:\n"
|
||||
// fiber_swap's ret lands here with rsp = top - 8, as at the entry of a called function. The call below must
|
||||
// be made with rsp 16-byte aligned so that fn starts with rsp = 8 mod 16, as the System V ABI demands; without
|
||||
// the realignment every 16-byte local of fn is off by 8, which the VEX instructions of a v3 build tolerate
|
||||
// but the SSE ones of a v2 or k10 build (movdqa, pcmpeqb with a memory operand) fault on.
|
||||
" andq $-16, %rsp\n"
|
||||
" movq %r13, %rdi\n"
|
||||
" callq *%r12\n"
|
||||
" ud2\n"
|
||||
@@ -916,8 +924,9 @@ uint64_t plat_mem_written(uint64_t addr, uint64_t n, uint64_t *pages, uint64_t m
|
||||
}
|
||||
|
||||
// One handler for the synchronous faults. A SIGSEGV whose page-fault error code has the write bit set goes to the
|
||||
// write-fault hook first (memory.c thaws a frozen region); a fault in this executable's code then goes to the crash
|
||||
// hook (main.c); anything else gets the default action. Runs on the thread's signal stack.
|
||||
// write-fault hook first (memory.c thaws a frozen region); any other fault, in this executable or in a library (Mesa,
|
||||
// SDL), then goes to the crash hook (main.c), which reports it and ends the process. Without a crash hook the fault
|
||||
// gets the default action. Runs on the thread's signal stack.
|
||||
static int (*fault_hook)(uint64_t addr);
|
||||
static void (*crash_fn)(const PlatCrash *c);
|
||||
|
||||
@@ -927,10 +936,17 @@ static void fault_handler(int sig, siginfo_t *si, void *uc) {
|
||||
if (sig == SIGSEGV && fault_hook && (err & 2) && fault_hook((uint64_t)(uintptr_t)si->si_addr)) return;
|
||||
uintptr_t rip = (uintptr_t)u->uc_mcontext.gregs[REG_RIP];
|
||||
Dl_info at, self;
|
||||
if (crash_fn && dladdr((void *)rip, &at) && dladdr((void *)plat_crash_hook, &self) && at.dli_fbase == self.dli_fbase) {
|
||||
if (crash_fn) {
|
||||
int found = dladdr((void *)rip, &at) && at.dli_fname;
|
||||
PlatCrash c = {sig == SIGSEGV ? "access violation" : sig == SIGBUS ? "bus error"
|
||||
: sig == SIGILL ? "illegal instruction" : "arithmetic exception",
|
||||
(uint32_t)sig, rip, rip - (uintptr_t)self.dli_fbase, -1, 0};
|
||||
(uint32_t)sig, rip, found ? rip - (uintptr_t)at.dli_fbase : rip, -1, 0};
|
||||
if (!found) {
|
||||
c.module = "";
|
||||
} else if (!dladdr((void *)plat_crash_hook, &self) || at.dli_fbase != self.dli_fbase) {
|
||||
const char *n = strrchr(at.dli_fname, '/');
|
||||
c.module = n ? n + 1 : at.dli_fname;
|
||||
}
|
||||
if (sig == SIGSEGV || sig == SIGBUS)
|
||||
c.access = (err & 16) ? 8 : (err & 2) ? 1 : 0, c.addr = (uint64_t)(uintptr_t)si->si_addr;
|
||||
crash_fn(&c);
|
||||
@@ -983,6 +999,12 @@ int plat_mem_read(uint64_t addr, void *out, size_t n) {
|
||||
return process_vm_readv(getpid(), &local, 1, &remote, 1, 0) == (ssize_t)n;
|
||||
}
|
||||
|
||||
// Not known before the SDL window exists; the Steam Deck has its own profile (cfg.c)
|
||||
int plat_screen_size(int *w, int *h) {
|
||||
(void)w, (void)h;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n) {
|
||||
uint8_t b;
|
||||
uint64_t end = addr + (n ? n : 1);
|
||||
@@ -1265,7 +1287,16 @@ int plat_stderr_path(char *out, size_t cap) {
|
||||
|
||||
// ---- process
|
||||
|
||||
#ifdef TOTK_PGO_GEN
|
||||
// compiler-rt's profile runtime (an instrumented build, tools/build_game.py --pgo gen). It writes its counters from an
|
||||
// atexit handler, which _exit skips; __llvm_profile_dump writes them once and makes later writes no-ops.
|
||||
int __llvm_profile_dump(void);
|
||||
#endif
|
||||
|
||||
void plat_exit(int code) {
|
||||
#ifdef TOTK_PGO_GEN
|
||||
__llvm_profile_dump();
|
||||
#endif
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_exit(code);
|
||||
@@ -1656,6 +1687,62 @@ long plat_child_read(PlatChild *c, void *buf, size_t cap) {
|
||||
return (long)k;
|
||||
}
|
||||
|
||||
// ---- local IPC (platform.h): Discord's Rich Presence Unix domain socket
|
||||
struct PlatIpc { int fd; };
|
||||
|
||||
PlatIpc *plat_ipc_connect(const char *name) {
|
||||
const char *dirs[] = {getenv("DISCORD_IPC_PATH"), getenv("XDG_RUNTIME_DIR"), getenv("TMPDIR"), "/tmp"};
|
||||
for (size_t d = 0; d < sizeof dirs / sizeof *dirs; d++) {
|
||||
if (!dirs[d] || !*dirs[d]) continue;
|
||||
char path[sizeof ((struct sockaddr_un *)0)->sun_path];
|
||||
if ((size_t)snprintf(path, sizeof path, "%s/%s", dirs[d], name) >= sizeof path) continue;
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) return NULL;
|
||||
struct sockaddr_un sa = {0};
|
||||
sa.sun_family = AF_UNIX;
|
||||
strcpy(sa.sun_path, path);
|
||||
if (connect(fd, (struct sockaddr *)&sa, sizeof sa) == 0) {
|
||||
PlatIpc *p = malloc(sizeof *p);
|
||||
if (!p) {
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
p->fd = fd;
|
||||
return p;
|
||||
}
|
||||
close(fd); // nothing listens at this path: the next directory may have it
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms) {
|
||||
struct pollfd pf = {.fd = p->fd, .events = POLLIN};
|
||||
int r = poll(&pf, 1, (int)ms);
|
||||
if (r <= 0) return r < 0 && errno != EINTR ? -1 : 0;
|
||||
ssize_t got = recv(p->fd, buf, n, 0);
|
||||
return got > 0 ? (int)got : -1; // 0 bytes: the peer closed the socket
|
||||
}
|
||||
|
||||
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n) {
|
||||
const uint8_t *at = buf;
|
||||
while (n) {
|
||||
ssize_t put = send(p->fd, at, n, MSG_NOSIGNAL); // a closed peer is an error, not a SIG_PIPE
|
||||
if (put <= 0) {
|
||||
if (put < 0 && errno == EINTR) continue;
|
||||
return 0;
|
||||
}
|
||||
at += put, n -= put;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void plat_ipc_close(PlatIpc *p) {
|
||||
if (!p) return;
|
||||
close(p->fd);
|
||||
free(p);
|
||||
}
|
||||
|
||||
|
||||
// ---- restart
|
||||
// TOTK_RESTART_WAIT names the process that called plat_restart, as its id and start time (field 22 of /proc/<pid>/stat,
|
||||
// clock ticks after boot): a process that has only reused the id is not waited for. Waiting keeps the new process off
|
||||
|
||||
@@ -218,22 +218,29 @@ void plat_mem_fault_hook(int (*fn)(uint64_t addr));
|
||||
// replaces this process with it (Linux). Environment changes made before the call are passed on.
|
||||
_Noreturn void plat_reexec(void);
|
||||
|
||||
// A crash in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
|
||||
// overflow) goes to fn, which reports it and must end the process. Faults in other modules (drivers) are left to the
|
||||
// OS. The write-fault hook above runs first. On Windows abort() (a failed assert, std::terminate) and an invalid
|
||||
// parameter to a C runtime function go there too, with the host frames in this executable: the C runtime ends the
|
||||
// process for both with 0xc0000409 and no exception.
|
||||
// A crash goes to fn, which reports it and must end the process. The write-fault hook above runs first.
|
||||
// - A fault in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
|
||||
// overflow) is reported as it happens.
|
||||
// - A fault in another module (a GPU driver, a system library) is reported only when nothing handles it: on Windows
|
||||
// from the unhandled-exception filter, with the return addresses in this executable on the faulting thread's stack,
|
||||
// so the report shows which host call ran into the driver; on Linux from the signal handler, without frames. Drivers
|
||||
// that raise and catch their own exceptions never reach fn.
|
||||
// - On Windows abort() (a failed assert, std::terminate) and an invalid parameter to a C runtime function go there
|
||||
// too, with the host frames in this executable: the C runtime ends the process for both with 0xc0000409 and no
|
||||
// exception.
|
||||
typedef struct {
|
||||
const char *what; // "access violation", "illegal instruction", ...
|
||||
uint32_t code; // the Windows exception code or the signal number
|
||||
uintptr_t rip, offset; // the faulting instruction, and its offset in the executable
|
||||
uintptr_t rip, offset; // the faulting instruction, and its offset in its module
|
||||
int access; // the data access that faulted: 0 read, 1 write, 8 execute; -1 when not an access
|
||||
uint64_t addr; // its address when access >= 0
|
||||
int nframes; // abort and invalid parameter: return addresses in this executable, innermost first
|
||||
uintptr_t frames[16]; // (rip is frames[0]); 0 for a fault, whose rip is exact
|
||||
int nframes; // return addresses in this executable, innermost first: for abort and invalid parameter
|
||||
uintptr_t frames[16]; // (rip is frames[0]) and for a fault in another module; 0 for a fault in this one
|
||||
const char *module; // the module holding rip: NULL for this executable, "" for none (a jump to address 0)
|
||||
} PlatCrash;
|
||||
void plat_crash_hook(void (*fn)(const PlatCrash *c));
|
||||
int plat_mem_read(uint64_t addr, void *out, size_t n); // copies n bytes, 0 instead of faulting on unmapped memory
|
||||
int plat_screen_size(int *w, int *h); // the primary display's current mode in pixels; 0 unknown
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n); // every page of [addr, addr + n) can be read; no fault
|
||||
void *plat_aligned_alloc(size_t size, size_t align); // free with plat_aligned_free; NULL on failure
|
||||
void plat_aligned_free(void *p);
|
||||
@@ -413,6 +420,17 @@ void plat_child_stop(PlatChild *c); // kills the child and
|
||||
int plat_child_write(PlatChild *c, const void *data, size_t n); // 1 when all n bytes were written
|
||||
long plat_child_read(PlatChild *c, void *buf, size_t cap); // bytes read (at least 1); 0 or negative: pipe broke
|
||||
|
||||
// ---- local IPC: a stream connection to another program of this machine, one listen location per name (Discord's
|
||||
// Rich Presence, discord_rpc.c). On Windows the named pipe \\.\pipe\<name>; on Linux the Unix domain socket
|
||||
// <dir>/<name> with <dir> the first of $DISCORD_IPC_PATH, $XDG_RUNTIME_DIR, $TMPDIR and /tmp that has one.
|
||||
typedef struct PlatIpc PlatIpc;
|
||||
PlatIpc *plat_ipc_connect(const char *name); // NULL when nothing listens under that name
|
||||
// Up to n bytes, waiting at most ms for the first of them: bytes read, 0 none in time, -1 the connection broke.
|
||||
// The rest of what the peer wrote may still be in its buffer: a caller that needs a whole frame loops.
|
||||
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms);
|
||||
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n); // 1 when all n bytes were written, 0 the connection broke
|
||||
void plat_ipc_close(PlatIpc *p);
|
||||
|
||||
// ---- restart: this program again, in a new process that starts once this one is gone
|
||||
// Starts this executable again with the same command line, environment and working folder, and no handle of this
|
||||
// process. 1 when it started; the caller then exits.
|
||||
|
||||
@@ -665,6 +665,76 @@ __attribute__((noinline)) static void crash_from_runtime(const char *what, uint3
|
||||
crash_fn(&c);
|
||||
}
|
||||
|
||||
// The name a DLL records for itself in its export directory ("amdvlk64.dll"). Read from its mapped headers, not through
|
||||
// the loader, whose lock another thread may hold when the process crashes.
|
||||
static const char *image_name(const char *base) {
|
||||
const IMAGE_NT_HEADERS64 *nt = (const void *)(base + ((const IMAGE_DOS_HEADER *)base)->e_lfanew);
|
||||
const IMAGE_DATA_DIRECTORY *d = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
|
||||
if (!d->VirtualAddress || !d->Size) return "unnamed module";
|
||||
return base + ((const IMAGE_EXPORT_DIRECTORY *)(base + d->VirtualAddress))->Name;
|
||||
}
|
||||
|
||||
// The return addresses in [lo, lo + size) on the crashed thread's stack, innermost first: unwinds the faulting context
|
||||
// with the x64 unwind tables; a function without a table entry (a leaf, or address 0 after a call through a null
|
||||
// pointer) has its return address on top of the stack. The walk stops at return address 0 (the thread's first frame)
|
||||
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting.
|
||||
static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t *out, int max) {
|
||||
CONTEXT c = *at;
|
||||
ULONG_PTR low, high;
|
||||
GetCurrentThreadStackLimits(&low, &high);
|
||||
int n = 0;
|
||||
for (int i = 0; i < 256 && n < max && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
|
||||
DWORD64 img;
|
||||
RUNTIME_FUNCTION *fe = RtlLookupFunctionEntry(c.Rip, &img, NULL);
|
||||
if (fe) {
|
||||
void *data;
|
||||
DWORD64 frame;
|
||||
RtlVirtualUnwind(UNW_FLAG_NHANDLER, img, c.Rip, fe, &c, &data, &frame, NULL);
|
||||
} else {
|
||||
c.Rip = *(const DWORD64 *)c.Rsp;
|
||||
c.Rsp += 8;
|
||||
}
|
||||
if (c.Rip - lo < size) out[n++] = (uintptr_t)c.Rip;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// The last-chance filter: an exception no handler took, in any module. crash_veh reports faults in this executable as
|
||||
// they happen; this one catches the rest, which used to end the process with no line in the log: a fault in a GPU
|
||||
// driver's own thread or in a driver call (vkQueueSubmit, vkAllocateMemory), a C++ exception a library let escape, a
|
||||
// call through a null pointer (rip 0, outside every module). Windows calls it only after every handler declined, so
|
||||
// the exceptions drivers raise and catch themselves never get here; it costs nothing until a crash.
|
||||
static LONG WINAPI crash_filter(EXCEPTION_POINTERS *ep) {
|
||||
const EXCEPTION_RECORD *er = ep->ExceptionRecord;
|
||||
DWORD code = er->ExceptionCode;
|
||||
uintptr_t rip = (uintptr_t)ep->ContextRecord->Rip;
|
||||
PlatCrash c = {code == EXCEPTION_ACCESS_VIOLATION ? "access violation"
|
||||
: code == EXCEPTION_ILLEGAL_INSTRUCTION ? "illegal instruction"
|
||||
: code == EXCEPTION_INT_DIVIDE_BY_ZERO ? "integer division by zero"
|
||||
: code == EXCEPTION_PRIV_INSTRUCTION ? "privileged instruction"
|
||||
: code == EXCEPTION_STACK_OVERFLOW ? "stack overflow"
|
||||
: code == EXCEPTION_IN_PAGE_ERROR ? "in-page error"
|
||||
: code == EXCEPTION_BREAKPOINT ? "breakpoint"
|
||||
: code == 0xe06d7363u ? "C++ exception"
|
||||
: code == 0xc0000374u ? "heap corruption" // STATUS_HEAP_CORRUPTION
|
||||
: "unhandled exception",
|
||||
(uint32_t)code, rip, 0, -1, 0};
|
||||
if (code == EXCEPTION_ACCESS_VIOLATION && er->NumberParameters >= 2)
|
||||
c.access = (int)er->ExceptionInformation[0], c.addr = er->ExceptionInformation[1];
|
||||
void *base = NULL;
|
||||
RtlPcToFileHeader((void *)rip, &base);
|
||||
MODULEINFO mi;
|
||||
uintptr_t exe = (uintptr_t)GetModuleHandleW(NULL);
|
||||
c.offset = rip - (uintptr_t)base;
|
||||
if (!base) c.module = "";
|
||||
else if ((uintptr_t)base != exe) c.module = image_name(base);
|
||||
if (c.module && GetModuleInformation(GetCurrentProcess(), (HMODULE)exe, &mi, sizeof mi))
|
||||
c.nframes = frames_in(ep->ContextRecord, exe, mi.SizeOfImage, c.frames,
|
||||
(int)(sizeof c.frames / sizeof *c.frames));
|
||||
crash_fn(&c);
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
static void abort_signal(int sig) { crash_from_runtime("abort", (uint32_t)sig); }
|
||||
|
||||
static void invalid_parameter(const wchar_t *expr, const wchar_t *func, const wchar_t *file, unsigned line,
|
||||
@@ -676,6 +746,7 @@ static void invalid_parameter(const wchar_t *expr, const wchar_t *func, const wc
|
||||
void plat_crash_hook(void (*fn)(const PlatCrash *c)) {
|
||||
crash_fn = fn;
|
||||
AddVectoredExceptionHandler(0, crash_veh);
|
||||
SetUnhandledExceptionFilter(crash_filter); // replaces the C runtime's filter; crash_fn never returns
|
||||
signal(SIGABRT, abort_signal);
|
||||
_set_invalid_parameter_handler(invalid_parameter);
|
||||
}
|
||||
@@ -704,6 +775,14 @@ int plat_mem_read(uint64_t addr, void *out, size_t n) {
|
||||
return ReadProcessMemory(GetCurrentProcess(), (const void *)(uintptr_t)addr, out, n, &got) && got == n;
|
||||
}
|
||||
|
||||
// The display mode's own size: physical pixels whatever the process's DPI awareness and the display scale
|
||||
int plat_screen_size(int *w, int *h) {
|
||||
DEVMODEW dm = {.dmSize = sizeof dm};
|
||||
if (!EnumDisplaySettingsW(NULL, ENUM_CURRENT_SETTINGS, &dm) || !dm.dmPelsWidth || !dm.dmPelsHeight) return 0;
|
||||
*w = (int)dm.dmPelsWidth, *h = (int)dm.dmPelsHeight;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n) {
|
||||
for (uint64_t a = addr, end = addr + n; a < end || a == addr;) {
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
@@ -720,7 +799,33 @@ void plat_aligned_free(void *p) { _aligned_free(p); }
|
||||
|
||||
// ---- process
|
||||
|
||||
#ifdef TOTK_PGO_GEN
|
||||
// compiler-rt's profile runtime (an instrumented build, tools/build_game.py --pgo gen). It writes its counters from an
|
||||
// atexit handler, which ExitProcess skips; __llvm_profile_dump writes them once and makes later writes no-ops.
|
||||
int __llvm_profile_dump(void);
|
||||
int __llvm_profile_write_file(void);
|
||||
void __llvm_profile_reset_counters(void);
|
||||
|
||||
// A long played session must not lose its counts to a crash or a reboot: every minute the counters are added to the
|
||||
// .profraw (the file merges, -fprofile-generate's %m) and zeroed. Counts taken between the write and the reset are lost.
|
||||
static DWORD WINAPI pgo_flush_thread(void *arg) {
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
Sleep(60 * 1000);
|
||||
if (__llvm_profile_write_file() == 0) __llvm_profile_reset_counters();
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void pgo_flush_start(void) {
|
||||
HANDLE h = CreateThread(NULL, 0, pgo_flush_thread, NULL, 0, NULL);
|
||||
if (h) CloseHandle(h);
|
||||
}
|
||||
#endif
|
||||
|
||||
void plat_exit(int code) {
|
||||
#ifdef TOTK_PGO_GEN
|
||||
__llvm_profile_dump();
|
||||
#endif
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
ExitProcess((UINT)code);
|
||||
@@ -1553,6 +1658,56 @@ long plat_child_read(PlatChild *c, void *buf, size_t cap) {
|
||||
return (long)got;
|
||||
}
|
||||
|
||||
// ---- local IPC (platform.h): Discord's Rich Presence named pipe
|
||||
struct PlatIpc { HANDLE h; };
|
||||
|
||||
PlatIpc *plat_ipc_connect(const char *name) {
|
||||
wchar_t wname[128], path[152];
|
||||
if (!widen(name, wname, 128)) return NULL;
|
||||
swprintf(path, 152, L"\\\\.\\pipe\\%ls", wname);
|
||||
HANDLE h = CreateFileW(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
|
||||
if (h == INVALID_HANDLE_VALUE) return NULL; // no listener, or the pipe is busy: the caller retries later
|
||||
PlatIpc *p = malloc(sizeof *p);
|
||||
if (!p) {
|
||||
CloseHandle(h);
|
||||
return NULL;
|
||||
}
|
||||
p->h = h;
|
||||
return p;
|
||||
}
|
||||
|
||||
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms) {
|
||||
uint64_t deadline = plat_now_ms() + ms;
|
||||
for (;;) {
|
||||
DWORD have = 0;
|
||||
if (!PeekNamedPipe(p->h, NULL, 0, NULL, &have, NULL)) return -1; // the writer closed the pipe
|
||||
if (have) {
|
||||
DWORD got = 0;
|
||||
if (!ReadFile(p->h, buf, have < n ? have : (DWORD)n, &got, NULL) || !got) return -1;
|
||||
return (int)got;
|
||||
}
|
||||
if (plat_now_ms() >= deadline) return 0;
|
||||
plat_sleep_ms(5);
|
||||
}
|
||||
}
|
||||
|
||||
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n) {
|
||||
const uint8_t *at = buf;
|
||||
while (n) {
|
||||
DWORD put = 0;
|
||||
if (!WriteFile(p->h, at, n > (1u << 30) ? 1u << 30 : (DWORD)n, &put, NULL) || !put) return 0;
|
||||
at += put, n -= put;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void plat_ipc_close(PlatIpc *p) {
|
||||
if (!p) return;
|
||||
CloseHandle(p->h);
|
||||
free(p);
|
||||
}
|
||||
|
||||
|
||||
// ---- restart
|
||||
// TOTK_RESTART_WAIT names the process that called plat_restart, as its id and creation time: a process that has only
|
||||
// reused the id is not waited for. Waiting keeps the new process off the old one's files (the log it moves at start,
|
||||
|
||||
+125
-30
@@ -27,6 +27,9 @@
|
||||
// rewritten table (res_heapdef_build) in place of the original; memory.c adds the amount to the application pool, so
|
||||
// no other heap shrinks. Without the rewritten table (a RomFS that lacks it, a format this code does not know) the
|
||||
// patch sets stay off and the game keeps its own sizes.
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#endif
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -53,12 +56,15 @@ static struct {
|
||||
int heapdef_ok; // romfs.c serves the grown HeapDef
|
||||
int on; // res_init switched the patch sets on
|
||||
unsigned shadow; // fixed sun shadow map size (TOTK_SHADOW_RES), 0: the game's own
|
||||
double dlss_k; // TOTK_DLSS quality/balanced/performance: the scene's share of the output, else 0
|
||||
double dlss_k; // TOTK_DLSS quality/balanced/performance/ultra_performance: the scene's share of the output, else 0
|
||||
double base_k; // the scene's share without a DLSS mode (render_scale, or the game's 5/6)
|
||||
unsigned hw, hh; // the largest scene a DLSS mode change can ask for: the heap is sized for it
|
||||
int live; // 1.4.3 with DLSS able to run: the DLSS mode can change while the game runs
|
||||
} R;
|
||||
|
||||
// The scene size is decided before the device exists, so NGX cannot be asked yet. NGX loads DLSS from nvngx_dlss.dll
|
||||
// next to totk.exe (vk_dlaa.c vk_dlaa_init): without that file DLSS never runs, and a DLSS mode would leave the game's
|
||||
// plain scaling at 50..67 % of the output. Off Windows vk_dlaa.c is built as no-op stubs, so there is no DLSS at all.
|
||||
// next to totk.exe on Windows, libnvidia-ngx-dlss.so.<version> next to totk on Linux (vk_dlaa.c vk_dlaa_init):
|
||||
// without that file DLSS never runs, and a DLSS mode would leave the game's plain scaling at 50..67 % of the output.
|
||||
static int dlss_dll(void) {
|
||||
#ifdef _WIN32
|
||||
char exe[1024], path[1024 + 32];
|
||||
@@ -68,10 +74,52 @@ static int dlss_dll(void) {
|
||||
snprintf(path, sizeof path, "%.*s/nvngx_dlss.dll", (int)(last - exe), exe);
|
||||
return plat_file_exists(path);
|
||||
#else
|
||||
return 0;
|
||||
char exe[1024];
|
||||
if (!plat_exe_path(exe, sizeof exe)) return 1;
|
||||
char *last = plat_last_sep(exe);
|
||||
if (!last) return 1;
|
||||
*last = 0;
|
||||
DIR *d = opendir(exe);
|
||||
if (!d) return 1;
|
||||
int found = 0;
|
||||
for (struct dirent *e; !found && (e = readdir(d));)
|
||||
found = !strncmp(e->d_name, "libnvidia-ngx-dlss.so.", 22);
|
||||
closedir(d);
|
||||
return found;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The scene size for a share k of the output, within SCENE_MAX per side.
|
||||
static void scene_for(double k, unsigned *w, unsigned *h) {
|
||||
double sw = R.ow * k, sh = R.oh * k;
|
||||
double fit = fmin(1.0, fmin(SCENE_MAX / sw, SCENE_MAX / sh));
|
||||
*w = (unsigned)(sw * fit + 0.5);
|
||||
*h = (unsigned)(sh * fit + 0.5);
|
||||
}
|
||||
|
||||
// The scene's share of the output for a TOTK_DLSS mode; 0 for dlaa (the scene keeps base_k), while DLSS/DLAA is off
|
||||
// (the mode the menu still holds would shrink the scene for the plain upscaler) and where DLSS cannot run (dlss_dll).
|
||||
static double dlss_share(const char *m) {
|
||||
static const struct { const char *name; double k; } dlss[] = {
|
||||
{"quality", 2.0 / 3.0}, {"balanced", 0.58}, {"performance", 0.5}, {"ultra_performance", 1.0 / 3.0}};
|
||||
static int dll = -1;
|
||||
if (!cfg_bool("TOTK_DLAA")) return 0;
|
||||
for (int i = 0; i < (int)(sizeof dlss / sizeof dlss[0]); i++)
|
||||
if (!strcmp(m, dlss[i].name)) {
|
||||
if (dll < 0 && !(dll = dlss_dll())) {
|
||||
#ifdef _WIN32
|
||||
rt_log("resolution: DLSS %s needs nvngx_dlss.dll next to totk.exe, which is missing: the scene follows "
|
||||
"render_scale", m);
|
||||
#else
|
||||
rt_log("resolution: DLSS %s needs libnvidia-ngx-dlss.so.<version> next to totk, which is missing: the "
|
||||
"scene follows render_scale", m);
|
||||
#endif
|
||||
}
|
||||
return dll ? dlss[i].k : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void decide(void) {
|
||||
if (R.decided) return;
|
||||
R.decided = 1;
|
||||
@@ -88,26 +136,7 @@ static void decide(void) {
|
||||
}
|
||||
const char *s = cfg_value("TOTK_RENDER_SCALE");
|
||||
double k = 5.0 / 6.0;
|
||||
// a DLSS mode other than dlaa sets the scene size, only while DLSS/DLAA is on: switched off, the mode the menu
|
||||
// still holds would shrink the scene for the plain upscaler
|
||||
const char *m = cfg_value("TOTK_DLSS");
|
||||
static const struct { const char *name; double k; } dlss[] = {
|
||||
{"quality", 2.0 / 3.0}, {"balanced", 0.58}, {"performance", 0.5}};
|
||||
for (int i = 0; cfg_bool("TOTK_DLAA") && i < 3; i++)
|
||||
if (!strcmp(m, dlss[i].name)) R.dlss_k = dlss[i].k;
|
||||
if (R.dlss_k && !dlss_dll()) {
|
||||
#ifdef _WIN32
|
||||
rt_log("resolution: DLSS %s needs nvngx_dlss.dll next to totk.exe, which is missing: the scene follows render_scale", m);
|
||||
#else
|
||||
rt_log("resolution: DLSS %s runs on Windows only: the scene follows render_scale", m);
|
||||
#endif
|
||||
R.dlss_k = 0;
|
||||
}
|
||||
if (R.dlss_k) {
|
||||
k = R.dlss_k;
|
||||
R.scene_set = 1;
|
||||
rt_log("resolution: DLSS %s: the scene at %.0f %% of the output (render_scale ignored)", m, k * 100);
|
||||
} else if (strcmp(s, "game")) {
|
||||
if (strcmp(s, "game")) {
|
||||
char *end;
|
||||
double pct = strtod(s, &end);
|
||||
if (end != s && !*end && pct >= 25 && pct <= 400) {
|
||||
@@ -117,11 +146,20 @@ static void decide(void) {
|
||||
rt_log("resolution: TOTK_RENDER_SCALE=%s is not game or a percentage 25..400; game", s);
|
||||
}
|
||||
}
|
||||
double sw = R.ow * k, sh = R.oh * k;
|
||||
double fit = fmin(1.0, fmin(SCENE_MAX / sw, SCENE_MAX / sh));
|
||||
if (fit < 1.0) R.scene_set = 1;
|
||||
R.sw = (unsigned)(sw * fit + 0.5);
|
||||
R.sh = (unsigned)(sh * fit + 0.5);
|
||||
// A DLSS mode other than dlaa sets the scene size instead. On 1.4.3 with DLSS able to run (DLAA on, the DLL there)
|
||||
// the mode can change while the game runs (res_dlss_mode): the scene patch is then always on and the heap is sized
|
||||
// for the largest scene the modes ask for. Everyone else keeps the game's own scene size and its dynamic resolution
|
||||
// unless render_scale or a DLSS mode asks for another, and a heap sized for that scene alone.
|
||||
R.base_k = k;
|
||||
R.live = RT_V143 && cfg_bool("TOTK_DLAA") && dlss_dll();
|
||||
const char *m = cfg_value("TOTK_DLSS");
|
||||
R.dlss_k = dlss_share(m);
|
||||
if (R.dlss_k) rt_log("resolution: DLSS %s: the scene at %.0f %% of the output (render_scale ignored)", m, R.dlss_k * 100);
|
||||
double sk = R.dlss_k ? R.dlss_k : k;
|
||||
if (R.live || R.dlss_k || R.ow * sk > SCENE_MAX || R.oh * sk > SCENE_MAX) R.scene_set = 1;
|
||||
scene_for(sk, &R.sw, &R.sh);
|
||||
if (R.live) scene_for(fmax(k, 2.0 / 3.0), &R.hw, &R.hh);
|
||||
else R.hw = R.sw, R.hh = R.sh;
|
||||
const char *sr = cfg_value("TOTK_SHADOW_RES");
|
||||
if (strcmp(sr, "game")) {
|
||||
unsigned n = (unsigned)atoi(sr);
|
||||
@@ -144,7 +182,7 @@ static int bigger(void) {
|
||||
uint64_t res_heap_extra_kib(void) {
|
||||
if (!bigger()) return 0;
|
||||
double grow = fmax(0.0, (double)R.ow * R.oh / (BASE_W * BASE_H) - 1.0) +
|
||||
fmax(0.0, (double)R.sw * R.sh / (SCENE_W * SCENE_H) - 1.0);
|
||||
fmax(0.0, (double)R.hw * R.hh / (SCENE_W * SCENE_H) - 1.0);
|
||||
uint64_t kib = grow > 0 ? (uint64_t)ceil(DTA_KIB * (1.0 + grow) / 1024.0) * 1024 - DTA_KIB : 0;
|
||||
return kib + ((shadow_kib() + 1023) & ~1023ull); // whole MiB
|
||||
}
|
||||
@@ -309,6 +347,7 @@ 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;
|
||||
if (R.live) g_fnprof = 1; // until the game sets its render size: res_fn_hit, or res_present gives up
|
||||
// Another shape than 16:9 (more than 1% off): the camera takes the output's ratio (recomp/patches-v121.txt and
|
||||
// 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).
|
||||
@@ -379,6 +418,62 @@ void res_wanted(unsigned out[2]) {
|
||||
out[0] = R.ow, out[1] = R.oh;
|
||||
}
|
||||
|
||||
// A TOTK_DLSS change while the game runs (vk_dlaa.c, at a present): the new scene size goes to the patch page, where
|
||||
// the game reads it when it sets its render size (main+0x585e48). 1.4.3 does that once, at start: its renderer setup
|
||||
// (main+0x7000e67028) calls main+0x585bd0(renderer, force), which picks the console mode's sizes and flags the render
|
||||
// targets for re-creation. The setup call is caught through the function-entry hook (res_fn_hit, on only until then),
|
||||
// and the next nvnQueuePresentTexture on the game's thread calls it again with force set (res_present), between frames
|
||||
// as at start. The heap was sized for the largest scene a mode asks for. 1 when the size changed.
|
||||
#define SET_RENDER_SIZE 0x7000585bd0ull
|
||||
static uint64_t renderer; // x0 of the game's own call of SET_RENDER_SIZE
|
||||
static volatile int size_pending; // the page holds a size the game has not set yet
|
||||
|
||||
void res_fn_hit(uint64_t addr, uint64_t x0) {
|
||||
if (addr != SET_RENDER_SIZE) return;
|
||||
renderer = x0;
|
||||
g_fnprof = 0;
|
||||
rt_log("resolution: renderer at %#llx; the scene size can change while the game runs", (unsigned long long)x0);
|
||||
}
|
||||
|
||||
void res_present(Cpu *c) {
|
||||
// The hook runs on every guest function entry while it waits: should the game set its render size before
|
||||
// res_init or never, it stops after 1800 presents (half a minute at 60 fps) and modes change at the next start.
|
||||
static unsigned waited;
|
||||
if (g_fnprof && !renderer && ++waited == 1800) {
|
||||
g_fnprof = 0;
|
||||
rt_log("resolution: the game's render size setup was not seen; DLSS modes change at the next start");
|
||||
}
|
||||
if (!size_pending || !renderer) return;
|
||||
size_pending = 0;
|
||||
Cpu keep = *c; // the guest's registers as at the present call: the setup runs as if it never happened
|
||||
rt_call(c, SET_RENDER_SIZE, 2, (uint64_t)renderer, (uint64_t)1);
|
||||
memcpy(c->x, keep.x, sizeof c->x);
|
||||
memcpy(c->v, keep.v, sizeof c->v);
|
||||
c->nzcv = keep.nzcv, c->fpsr = keep.fpsr;
|
||||
rt_log("resolution: the game set the scene size %ux%u", R.sw, R.sh);
|
||||
}
|
||||
|
||||
int res_dlss_mode(const char *m) {
|
||||
decide();
|
||||
double k = dlss_share(m);
|
||||
if (k == R.dlss_k || !R.on) return 0;
|
||||
if (!RT_V143 || !renderer) {
|
||||
rt_log("resolution: DLSS %s takes effect at the next start (the game set its render size already)", m);
|
||||
return 0;
|
||||
}
|
||||
R.dlss_k = k;
|
||||
unsigned w, h;
|
||||
scene_for(k ? k : R.base_k, &w, &h);
|
||||
if (w == R.sw && h == R.sh) return 0;
|
||||
double s = RT_V143 ? (double)R.ow / BASE_W : 1.0;
|
||||
float sc[2] = {(float)(w / s), (float)(h / s)};
|
||||
memcpy((uint8_t *)rt_patch_page() + 16, sc, sizeof sc);
|
||||
R.sw = w, R.sh = h;
|
||||
size_pending = 1;
|
||||
rt_log("resolution: DLSS %s: scene %ux%u", m, w, h);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -234,3 +234,102 @@ uint32_t restbl_grow(const char *key, uint32_t entry, uint64_t old, uint64_t new
|
||||
uint64_t e = entry + ((new_size - old) * f + 999) / 1000;
|
||||
return clamp32((e + 31) & ~(uint64_t)31);
|
||||
}
|
||||
|
||||
// ---- packs
|
||||
// SARC: "SARC", u16 header size, u16 byte order mark 0xfeff, u32 archive size, u32 data offset, u16 version, u16 pad;
|
||||
// SFAT: "SFAT", u16 header size, u16 node count, u32 hash key, then per node {u32 name hash, u32 attributes (bit 24: it
|
||||
// has a name, bits 0-23: the name's offset in SFNT / 4), u32 data start, u32 data end} with the data offsets relative
|
||||
// to the archive's data offset; SFNT: "SFNT", u16 header size, u16 pad, then the NUL-terminated names.
|
||||
static uint32_t rd16(const uint8_t *p) { return p[0] | p[1] << 8; }
|
||||
|
||||
int restbl_sarc_each(const uint8_t *p, size_t n, RestblMember *each, void *ctx) {
|
||||
if (n < 0x14 || memcmp(p, "SARC", 4) || rd16(p + 6) != 0xfeff) return 0;
|
||||
size_t head = rd16(p + 4), data = rd32(p + 12);
|
||||
if (head + 12 > n || memcmp(p + head, "SFAT", 4) || data > n) return 0;
|
||||
size_t count = rd16(p + head + 6), nodes = head + rd16(p + head + 4), names = nodes + count * 16;
|
||||
if (names + 8 > n || memcmp(p + names, "SFNT", 4) || rd16(p + names + 4) < 8) return 0;
|
||||
names += rd16(p + names + 4);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const uint8_t *e = p + nodes + i * 16;
|
||||
uint32_t attr = rd32(e + 4), lo = rd32(e + 8), hi = rd32(e + 12);
|
||||
if (!(attr >> 24)) continue; // without a name: nothing the game looks up by path
|
||||
size_t s = names + (size_t)(attr & 0xffffff) * 4;
|
||||
if (s >= n || !memchr(p + s, 0, n - s) || lo > hi || hi > n - data) return 0;
|
||||
each((const char *)p + s, p + data + lo, hi - lo, ctx);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The decompressed size a zstd frame p[0..n) records (Frame_Content_Size), or 4 times n when it records none, as
|
||||
// romfs.c content_size takes a loose file's.
|
||||
static uint64_t frame_content_size(const uint8_t *p, size_t n) {
|
||||
static const uint8_t did_bytes[4] = {0, 1, 2, 4};
|
||||
if (n < 6 || rd32(p) != 0xFD2FB528u) return n;
|
||||
uint8_t fhd = p[4];
|
||||
int single = fhd >> 5 & 1, fcs = fhd >> 6;
|
||||
size_t at = 5 + !single + did_bytes[fhd & 3], len = fcs ? 1u << fcs : single;
|
||||
if (!len || at + len > n) return (uint64_t)n * 4;
|
||||
uint64_t v = 0;
|
||||
for (size_t i = 0; i < len; i++) v |= (uint64_t)p[at + i] << 8 * i;
|
||||
return len == 2 ? v + 256 : v;
|
||||
}
|
||||
|
||||
typedef struct { const char *name; uint64_t size; } Member;
|
||||
typedef struct { Member *m; size_t n, cap; int full; } Members;
|
||||
|
||||
static int ends_with(const char *s, const char *tail) {
|
||||
size_t n = strlen(s), k = strlen(tail);
|
||||
return n >= k && !strcmp(s + n - k, tail);
|
||||
}
|
||||
|
||||
// A member's size as the game unpacks it: a .zs member's content size, else its bytes (a .mc member's compressed ones).
|
||||
static void member_add(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
Members *ms = ctx;
|
||||
if (ms->n == ms->cap) {
|
||||
size_t cap = ms->cap ? ms->cap * 2 : 64;
|
||||
Member *grown = realloc(ms->m, cap * sizeof *grown);
|
||||
if (!grown) {
|
||||
ms->full = 1;
|
||||
return;
|
||||
}
|
||||
ms->m = grown, ms->cap = cap;
|
||||
}
|
||||
ms->m[ms->n++] = (Member){name, ends_with(name, ".zs") ? frame_content_size(data, size) : size};
|
||||
}
|
||||
|
||||
static int by_name(const void *a, const void *b) {
|
||||
return strcmp(((const Member *)a)->name, ((const Member *)b)->name);
|
||||
}
|
||||
|
||||
int restbl_pack_raise(Restbl *t, const Restbl *base, const uint8_t *mod, size_t mod_n, const uint8_t *orig,
|
||||
size_t orig_n, RestblNote *note, void *ctx) {
|
||||
Members m = {0}, o = {0};
|
||||
int raised = -1;
|
||||
if (!restbl_sarc_each(mod, mod_n, member_add, &m) || m.full ||
|
||||
(orig && (!restbl_sarc_each(orig, orig_n, member_add, &o) || o.full)))
|
||||
goto done;
|
||||
if (o.n) qsort(o.m, o.n, sizeof *o.m, by_name);
|
||||
raised = 0;
|
||||
for (size_t i = 0; i < m.n; i++) {
|
||||
char key[1024];
|
||||
uint32_t entry, before = 0, want;
|
||||
restbl_key(m.m[i].name, key, sizeof key);
|
||||
int mc = ends_with(m.m[i].name, ".mc");
|
||||
const Member *was = o.n ? bsearch(&m.m[i], o.m, o.n, sizeof *o.m, by_name) : NULL;
|
||||
if (was) {
|
||||
if (!restbl_get(base, key, &entry)) continue; // the game sizes it without the table (streamed)
|
||||
want = restbl_grow(key, entry, was->size, m.m[i].size, mc);
|
||||
} else { // new to this pack: no copy of the game's to grow from
|
||||
want = restbl_estimate(key, mc ? m.m[i].size * 8 : m.m[i].size);
|
||||
}
|
||||
restbl_get(t, key, &before);
|
||||
if (want && restbl_raise(t, key, want)) {
|
||||
raised++;
|
||||
if (note) note(key, before, want, ctx);
|
||||
}
|
||||
}
|
||||
done:
|
||||
free(m.m);
|
||||
free(o.m);
|
||||
return raised;
|
||||
}
|
||||
@@ -31,3 +31,20 @@ uint32_t restbl_estimate(const char *key, uint64_t size);
|
||||
// times the type's factor, never lowered. scaled: the sizes are compressed ones (a mesh codec .mc file), whose entry
|
||||
// grows in proportion.
|
||||
uint32_t restbl_grow(const char *key, uint32_t entry, uint64_t old, uint64_t new_size, int scaled);
|
||||
|
||||
// The members of the SARC archive p[0..n): each(name, data, size, ctx) for every named member, in the archive's order.
|
||||
// 0 when p is not a SARC archive or a member lies outside it.
|
||||
typedef void RestblMember(const char *name, const uint8_t *data, size_t size, void *ctx);
|
||||
int restbl_sarc_each(const uint8_t *p, size_t n, RestblMember *each, void *ctx);
|
||||
|
||||
// A pack (Pack/**/*.pack.zs, decompressed: a SARC archive) whose members the game loads as resources of their own, each
|
||||
// sized by its own entry. The entries of a mod's pack `mod` raised in t against the game's table `base` and the game's
|
||||
// copy of the pack `orig` (NULL for a pack the game lacks): a member that grew by restbl_grow, a member the game's copy
|
||||
// lacks by restbl_estimate, the others left as they are. note(key, entry before, entry after, ctx) for each raised
|
||||
// entry. The number raised; -1 when either pack is not a SARC archive.
|
||||
typedef void RestblNote(const char *key, uint32_t was, uint32_t now, void *ctx);
|
||||
int restbl_pack_raise(Restbl *t, const Restbl *base, const uint8_t *mod, size_t mod_n, const uint8_t *orig,
|
||||
size_t orig_n, RestblNote *note, void *ctx);
|
||||
|
||||
// restbl_probe.c, part of --probe-mods: the number of failed checks.
|
||||
int restbl_probe(void);
|
||||
@@ -0,0 +1,133 @@
|
||||
// The entries of the members of the mods' packs (restbl.c restbl_pack_raise), on archives and tables of its own; part
|
||||
// of --probe-mods (mods_probe.c). Needs no game files.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "restbl.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL restbl: %s\n", what); } } while (0)
|
||||
|
||||
// A member of a probe archive: its name and size; a .zs member is a zstd frame header recording `content` bytes:
|
||||
// fcs 4, a frame with a window descriptor and a 4-byte field; 2, a single-segment frame with a 2-byte field holding
|
||||
// content - 256.
|
||||
typedef struct { const char *name; size_t size, content; int fcs; } Member;
|
||||
|
||||
static void wr16(uint8_t *p, unsigned v) { p[0] = (uint8_t)v, p[1] = (uint8_t)(v >> 8); }
|
||||
static void wr32(uint8_t *p, uint32_t v) { wr16(p, v & 0xffff), wr16(p + 2, v >> 16); }
|
||||
|
||||
// A SARC archive of the members m[0..n); unnamed: the index of a member without a name, -1 for none. malloc'd.
|
||||
static uint8_t *sarc(const Member *m, int n, int unnamed, size_t *out_n) {
|
||||
size_t names = 0, data = 0;
|
||||
for (int i = 0; i < n; i++) names += (strlen(m[i].name) + 4) & ~(size_t)3, data += (m[i].size + 7) & ~(size_t)7;
|
||||
size_t sfat = 0x14, sfnt = sfat + 12 + 16 * (size_t)n, start = (sfnt + 8 + names + 15) & ~(size_t)15;
|
||||
size_t total = start + data;
|
||||
uint8_t *p = calloc(1, total);
|
||||
if (!p) return NULL;
|
||||
memcpy(p, "SARC", 4), wr16(p + 4, 0x14), wr16(p + 6, 0xfeff);
|
||||
wr32(p + 8, (uint32_t)total), wr32(p + 12, (uint32_t)start);
|
||||
memcpy(p + sfat, "SFAT", 4), wr16(p + sfat + 4, 12), wr16(p + sfat + 6, (unsigned)n), wr32(p + sfat + 8, 0x65);
|
||||
memcpy(p + sfnt, "SFNT", 4), wr16(p + sfnt + 4, 8);
|
||||
size_t name_at = 0, data_at = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint8_t *node = p + sfat + 12 + 16 * (size_t)i, *d = p + start + data_at;
|
||||
wr32(node + 4, i == unnamed ? 0 : 0x01000000u | (uint32_t)(name_at / 4));
|
||||
wr32(node + 8, (uint32_t)data_at), wr32(node + 12, (uint32_t)(data_at + m[i].size));
|
||||
memcpy(p + sfnt + 8 + name_at, m[i].name, strlen(m[i].name));
|
||||
memset(d, 0x5a, m[i].size);
|
||||
if (m[i].fcs == 4) wr32(d, 0xFD2FB528u), d[4] = 0x80, d[5] = 0x58, wr32(d + 6, (uint32_t)m[i].content);
|
||||
if (m[i].fcs == 2) wr32(d, 0xFD2FB528u), d[4] = 0x60, wr16(d + 5, (unsigned)(m[i].content - 256));
|
||||
name_at += (strlen(m[i].name) + 4) & ~(size_t)3;
|
||||
data_at += (m[i].size + 7) & ~(size_t)7;
|
||||
}
|
||||
*out_n = total;
|
||||
return p;
|
||||
}
|
||||
|
||||
// An empty table with the entries keys[i] = sizes[i].
|
||||
static Restbl *table(const char *const *keys, const uint32_t *sizes, int n) {
|
||||
uint8_t head[22] = "RESTBL";
|
||||
wr32(head + 6, 1), wr32(head + 10, 160), wr32(head + 14, 0), wr32(head + 18, 0);
|
||||
Restbl *t = restbl_parse(head, sizeof head);
|
||||
for (int i = 0; t && i < n; i++) restbl_raise(t, keys[i], sizes[i]);
|
||||
return t;
|
||||
}
|
||||
|
||||
static uint32_t get(const Restbl *t, const char *key) {
|
||||
uint32_t v = 0;
|
||||
return restbl_get(t, key, &v) ? v : 0;
|
||||
}
|
||||
|
||||
static int visited;
|
||||
static void count(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
(void)name, (void)data, (void)size, (void)ctx;
|
||||
visited++;
|
||||
}
|
||||
|
||||
static int notes;
|
||||
static void note(const char *key, uint32_t was, uint32_t now, void *ctx) {
|
||||
(void)key, (void)ctx;
|
||||
notes += now > was;
|
||||
}
|
||||
|
||||
int restbl_probe(void) {
|
||||
fails = 0;
|
||||
static const char *const keys[] = {"AI/Grow.root.ainb", "AI/Same.root.ainb", "AI/Shrink.root.ainb",
|
||||
"Component/Shared.bgyml", "Model/Mesh.bfres", "AI/Zs.ainb", "AI/Zs2.ainb"};
|
||||
static const uint32_t sizes[] = {13512, 5000, 6000, 9000, 30000, 4400, 3500};
|
||||
// The game's copy: an .ainb member a mod grows (by 288 bytes, past its entry's slack), one it keeps, one it
|
||||
// shrinks, one the table does not size, a mesh codec member and two compressed ones.
|
||||
static const Member orig[] = {{"AI/Grow.root.ainb", 13088, 0, 0}, {"AI/Same.root.ainb", 4600, 0, 0},
|
||||
{"AI/Shrink.root.ainb", 5600, 0, 0}, {"AI/NotSized.ainb", 100, 0, 0},
|
||||
{"Model/Mesh.bfres.mc", 1000, 0, 0}, {"AI/Zs.ainb.zs", 64, 4000, 4},
|
||||
{"AI/Zs2.ainb.zs", 64, 3900, 2}};
|
||||
// The mod's copy: those, plus members new to the pack (one the game sizes elsewhere, one it lacks, a streamed one).
|
||||
static const Member mod[] = {{"AI/Grow.root.ainb", 13376, 0, 0}, {"AI/Same.root.ainb", 4600, 0, 0},
|
||||
{"AI/Shrink.root.ainb", 5000, 0, 0}, {"AI/NotSized.ainb", 300, 0, 0},
|
||||
{"Component/New/X.bgyml", 1000, 0, 0}, {"Component/Shared.bgyml", 2000, 0, 0},
|
||||
{"Sound/Stream.bwav", 500, 0, 0}, {"Model/Mesh.bfres.mc", 1500, 0, 0},
|
||||
{"AI/Zs.ainb.zs", 64, 4500, 4}, {"AI/Zs2.ainb.zs", 64, 4200, 2}};
|
||||
size_t on = 0, mn = 0;
|
||||
uint8_t *op = sarc(orig, 7, -1, &on), *mp = sarc(mod, 10, 4, &mn); // the mod's New/X has no name: left alone
|
||||
Restbl *base = table(keys, sizes, 7), *t = table(keys, sizes, 7);
|
||||
CHECK(op && mp && base && t, "probe archives and tables");
|
||||
if (op && mp && base && t) {
|
||||
visited = 0;
|
||||
CHECK(restbl_sarc_each(mp, mn, count, NULL) && visited == 9, "SARC members: every named one");
|
||||
int r = restbl_pack_raise(t, base, mp, mn, op, on, note, NULL);
|
||||
CHECK(r == 5 && notes == 5, "five entries raised, each noted");
|
||||
CHECK(get(t, "AI/Grow.root.ainb") == 13824, "a member that grew: its entry plus the growth, 32-byte steps");
|
||||
CHECK(get(t, "AI/Same.root.ainb") == 5000 && get(t, "AI/Shrink.root.ainb") == 6000,
|
||||
"members that kept or lost bytes keep their entries");
|
||||
CHECK(!get(t, "AI/NotSized.ainb"), "a member the game sizes without the table gets no entry");
|
||||
CHECK(!get(t, "Component/New/X.bgyml"), "a member without a name is not looked up");
|
||||
CHECK(get(t, "Component/Shared.bgyml") == 16384, "a member new to the pack: the estimate of its type");
|
||||
CHECK(!get(t, "Sound/Stream.bwav"), "a streamed member new to the pack gets no entry");
|
||||
CHECK(get(t, "Model/Mesh.bfres") == 45000, "a mesh codec member's entry grows in proportion");
|
||||
CHECK(get(t, "AI/Zs.ainb") == 4928 && get(t, "AI/Zs2.ainb") == 3808,
|
||||
"a compressed member: the growth of the size its frame records (4- and 2-byte fields)");
|
||||
notes = 0;
|
||||
CHECK(restbl_pack_raise(t, base, mp, mn, op, on, note, NULL) == 0 && !notes, "a second pass raises nothing");
|
||||
Restbl *shipped = table(keys, (const uint32_t[]){20000, 5000, 6000, 9000, 30000, 4400, 3500}, 7);
|
||||
CHECK(shipped && restbl_pack_raise(shipped, base, mp, mn, op, on, NULL, NULL) == 4 &&
|
||||
get(shipped, "AI/Grow.root.ainb") == 20000,
|
||||
"an entry a mod's own table raised further stays");
|
||||
restbl_free(shipped);
|
||||
Restbl *added = table(keys, sizes, 7);
|
||||
CHECK(added && restbl_pack_raise(added, base, mp, mn, NULL, 0, NULL, NULL) == 7 &&
|
||||
get(added, "AI/Grow.root.ainb") == 24576 && get(added, "AI/NotSized.ainb") == 12288 &&
|
||||
get(added, "AI/Zs2.ainb") == 16384 && get(added, "Model/Mesh.bfres") == 30000,
|
||||
"a pack the game lacks: every member gets the estimate of its type, an entry above it stays");
|
||||
restbl_free(added);
|
||||
CHECK(restbl_pack_raise(t, base, (const uint8_t *)"SARC", 4, NULL, 0, NULL, NULL) == -1 &&
|
||||
restbl_pack_raise(t, base, mp, mn, mp, 0x20, NULL, NULL) == -1,
|
||||
"an archive that is not one, or whose game copy is cut short, raises nothing");
|
||||
}
|
||||
free(op);
|
||||
free(mp);
|
||||
restbl_free(base);
|
||||
restbl_free(t);
|
||||
printf("restbl: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails;
|
||||
}
|
||||
+157
-22
@@ -24,8 +24,8 @@
|
||||
// matches a RomFS path only without case is served under the RomFS spelling.
|
||||
//
|
||||
// The resource size table: with a player mod enabled, the game's table is rebuilt at start with the tables the mods
|
||||
// ship merged in and the entries of the mods' files raised to what they need (restbl_overlay), and served from
|
||||
// <profile>/mods/ in place of the game's or a mod's table.
|
||||
// ship merged in and the entries of the mods' files raised to what they need (restbl_overlay), the members of their
|
||||
// packs too (restbl_packs), and served from <profile>/mods/ in place of the game's or a mod's table.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -621,10 +621,122 @@ static Restbl *restbl_unpack(const void *z, size_t n) {
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---- the members of the mods' packs (restbl_pack_raise). The game loads each member of a pack (Pack/**/*.pack.zs, a
|
||||
// SARC archive compressed with a dictionary of Pack/ZsDic.pack.zs) as a resource of its own, in a heap sized by the
|
||||
// member's own entry, so a mod that changes a member and ships no table for it (or an older one) leaves the game's
|
||||
// entry. A member that outgrew its entry's slack loads, then its heap runs out: TotK 1.4.3 writes through a null
|
||||
// pointer at main+0x21794c when the 0x68-byte object an .ainb member builds after its file no longer fits.
|
||||
#define PACK_MAX (256u << 20) // the largest pack taken, decompressed (AI.Global.Product.140.pack: 24 MiB)
|
||||
typedef struct { ZSTD_DDict *dict[4]; unsigned id[4]; int n; ZSTD_DCtx *dctx; } Packs;
|
||||
|
||||
// The bytes of zstd frame z[0..n), with the dictionary of ps its header names; malloc'd. NULL when it is not such a
|
||||
// frame with a recorded size of at most PACK_MAX.
|
||||
static uint8_t *pack_unpack(Packs *ps, const void *z, size_t n, size_t *out_n) {
|
||||
unsigned long long raw = ZSTD_getFrameContentSize(z, n);
|
||||
unsigned id = ZSTD_getDictID_fromFrame(z, n);
|
||||
ZSTD_DDict *dd = NULL;
|
||||
for (int i = 0; i < ps->n; i++)
|
||||
if (ps->id[i] == id) dd = ps->dict[i];
|
||||
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > PACK_MAX || (id && !dd) || !ps->dctx)
|
||||
return NULL;
|
||||
uint8_t *d = malloc(raw ? (size_t)raw : 1);
|
||||
size_t got = !d ? 0 : dd ? ZSTD_decompress_usingDDict(ps->dctx, d, (size_t)raw, z, n, dd)
|
||||
: ZSTD_decompressDCtx(ps->dctx, d, (size_t)raw, z, n);
|
||||
if (!d || ZSTD_isError(got) || got != raw) {
|
||||
free(d);
|
||||
return NULL;
|
||||
}
|
||||
*out_n = (size_t)raw;
|
||||
return d;
|
||||
}
|
||||
|
||||
static void pack_dict_add(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
Packs *ps = ctx;
|
||||
ZSTD_DDict *dd = ps->n < 4 && ends_with(name, ".zsdic") ? ZSTD_createDDict(data, size) : NULL; // a copy
|
||||
if (!dd) return;
|
||||
ps->dict[ps->n] = dd;
|
||||
ps->id[ps->n++] = ZSTD_getDictID_fromDDict(dd);
|
||||
}
|
||||
|
||||
// The dictionaries of Pack/ZsDic.pack.zs as served (zs.zsdic, pack.zsdic, bcett.byml.zsdic): a SARC compressed
|
||||
// without one. 0 when it holds none.
|
||||
static int packs_open(Packs *ps) {
|
||||
Overlay *base;
|
||||
size_t n = 0, rn = 0;
|
||||
ps->dctx = ZSTD_createDCtx();
|
||||
void *z = read_served("Pack/ZsDic.pack.zs", &base, &n, "the members of the mods' packs keep their entries");
|
||||
uint8_t *raw = z ? pack_unpack(ps, z, n, &rn) : NULL;
|
||||
int ok = raw && restbl_sarc_each(raw, rn, pack_dict_add, ps) && ps->n;
|
||||
free(z);
|
||||
free(raw);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void packs_close(Packs *ps) {
|
||||
for (int i = 0; i < ps->n; i++) ZSTD_freeDDict(ps->dict[i]);
|
||||
ZSTD_freeDCtx(ps->dctx);
|
||||
}
|
||||
|
||||
// Overlay o (a pack) and the game's copy of it, decompressed; NULL when o cannot be read or unpacked. *orig stays NULL
|
||||
// for a pack the game lacks or whose copy cannot be read.
|
||||
static uint8_t *pack_pair(Packs *ps, const Overlay *o, size_t *n, uint8_t **orig, size_t *orig_n) {
|
||||
uint64_t zn = 0;
|
||||
void *z = plat_file_read(o->path, &zn);
|
||||
uint8_t *mod = z ? pack_unpack(ps, z, (size_t)zn, n) : NULL;
|
||||
free(z);
|
||||
*orig = NULL;
|
||||
if (!mod || o->index == ADDED) return mod;
|
||||
Entry e = entry(o->index);
|
||||
void *g = e.size < READ_CHUNK ? malloc(e.size ? (size_t)e.size : 1) : NULL;
|
||||
if (g && read_at(image, g, (uint32_t)e.size, e.offset) == (int64_t)e.size)
|
||||
*orig = pack_unpack(ps, g, (size_t)e.size, orig_n);
|
||||
free(g);
|
||||
return mod;
|
||||
}
|
||||
|
||||
typedef struct { const char *pack, *mod; int lines; } PackNote;
|
||||
|
||||
static void pack_note(const char *key, uint32_t was, uint32_t now, void *ctx) {
|
||||
PackNote *pn = ctx;
|
||||
if (pn->lines++ < 16)
|
||||
rt_log("RomFS: resource size table: %s in %s (%s): entry %u -> %u", key, pn->pack, pn->mod, was, now);
|
||||
}
|
||||
|
||||
// The entries of the members of every pack a mod serves, raised in out against the game's table base; the number
|
||||
// raised, *packs the number of packs with one.
|
||||
static int restbl_packs(Restbl *out, const Restbl *base, int *packs, int *lines) {
|
||||
Packs ps = {0};
|
||||
int raised = 0, opened = 0, bad = 0;
|
||||
*packs = 0;
|
||||
for (size_t k = 0; k < noverlay; k++) {
|
||||
const Overlay *o = &overlays[k];
|
||||
if ((!o->mod && o->prio != 1) || !ends_with(o->name, ".pack.zs")) continue;
|
||||
if (!opened++ && !packs_open(&ps)) {
|
||||
rt_log("RomFS: no zstd dictionary in Pack/ZsDic.pack.zs; the members of mods' packs keep their entries");
|
||||
break;
|
||||
}
|
||||
// A pack whose game copy cannot be read counts as new: each member gets an estimate, which only raises entries.
|
||||
size_t n = 0, orig_n = 0;
|
||||
uint8_t *orig, *mod = pack_pair(&ps, o, &n, &orig, &orig_n);
|
||||
PackNote pn = {o->name, owner_name(o), *lines};
|
||||
int r = mod ? restbl_pack_raise(out, base, mod, n, orig, orig_n, pack_note, &pn) : -1;
|
||||
if (r < 0 && bad++ < 4) rt_log("RomFS: %s (%s) is not a pack this build reads; its members keep their entries",
|
||||
o->name, owner_name(o));
|
||||
*lines = pn.lines;
|
||||
raised += r > 0 ? r : 0;
|
||||
*packs += r > 0;
|
||||
free(mod);
|
||||
free(orig);
|
||||
}
|
||||
packs_close(&ps);
|
||||
return raised;
|
||||
}
|
||||
|
||||
// With a player mod (or the TOTK_ROMFS_MODS manifest) serving files: the game's table with every shipped table of this
|
||||
// version merged in (an entry larger than the game's wins), the entries a mod's table for another version gives for
|
||||
// that mod's files, and each served mod file's entry raised to what its size needs (restbl_grow for a file the game
|
||||
// has, restbl_estimate for an added one). Never lowers the game's entries.
|
||||
// has, restbl_estimate for an added one), the members of the packs mods serve too (restbl_packs). Never lowers the
|
||||
// game's entries.
|
||||
static void restbl_overlay(void) {
|
||||
int player = 0;
|
||||
for (size_t k = 0; k < noverlay; k++) player |= overlays[k].mod || overlays[k].prio == 1;
|
||||
@@ -691,8 +803,10 @@ static void restbl_overlay(void) {
|
||||
}
|
||||
raised += want && restbl_raise(out, key, want);
|
||||
}
|
||||
int packs = 0, lines = 0, members = restbl_packs(out, base, &packs, &lines);
|
||||
if (lines > 16) rt_log("RomFS: resource size table: %d more entries of pack members raised", lines - 16);
|
||||
restbl_free(base);
|
||||
if (!merged && !raised && !served) {
|
||||
if (!merged && !raised && !members && !served) {
|
||||
restbl_free(out);
|
||||
rt_log("RomFS: resource size table: the mods' files fit the game's entries (%.0f ms)", (now_s() - t0) * 1000);
|
||||
return;
|
||||
@@ -711,7 +825,8 @@ static void restbl_overlay(void) {
|
||||
}
|
||||
if (serve_generated(restbl_rel, served, z, zn, "mods", "merged resource sizes", keeps))
|
||||
rt_log("RomFS: resource size table: %d tables merged (%d entries), %d entries raised for mod files (%d "
|
||||
"added files estimated), %.0f ms", tables, merged, raised, estimated, (now_s() - t0) * 1000);
|
||||
"added files estimated), %d for members of %d mod packs, %.0f ms", tables, merged, raised, estimated,
|
||||
members, packs, (now_s() - t0) * 1000);
|
||||
}
|
||||
|
||||
// The HeapDef table a bigger game resolution needs (resolution.c): its texture heap and that heap's parent grow. The
|
||||
@@ -750,14 +865,17 @@ static void camera_overlay(void) {
|
||||
// The button prompts' folders (input_prompts_folder) and whether each had a manifest next to romfs-index.bin at start;
|
||||
// the one the settings asked for at start, the one overlay_init loaded (NULL: the game's own prompts) and why the first
|
||||
// did not load (ROMFS_PROMPTS_*). The menu compares them with the settings in effect (ui/menu.cpp prompts_note).
|
||||
static struct { const char *name; int ready; } prompts_folders[2];
|
||||
// romfs-patches-pc keeps that name, but at a game resolution of another shape than 16:9 its files come from pc_dir,
|
||||
// romfs-patches-pc-<w>x<h> (overlay_init).
|
||||
static struct { const char *name; int ready; } prompts_folders[3];
|
||||
static const char *prompts_wanted, *prompts_loaded;
|
||||
static int prompts_off;
|
||||
static char pc_dir[64];
|
||||
|
||||
const char *romfs_prompts(void) { return prompts_loaded; }
|
||||
|
||||
int romfs_prompts_ready(const char *folder) {
|
||||
for (int i = 0; i < 2; i++)
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (folder && prompts_folders[i].name && !strcmp(prompts_folders[i].name, folder)) return prompts_folders[i].ready;
|
||||
return 0;
|
||||
}
|
||||
@@ -798,6 +916,9 @@ static void overlay_init(void) {
|
||||
// keeps the Switch prompts. So does a RomFS mod (the TOTK_ROMFS_MODS manifest or a player mod) that replaces one of
|
||||
// the prompts' fonts: romfs-patches loads in their place, so the mod's file is not served beside prompts files made
|
||||
// for the game's own. A mod's resource size table is no clash: restbl_overlay merges it with the prompts' one.
|
||||
// prompts = pc takes romfs-patches-pc whatever the pad layout, and at a game resolution of another shape
|
||||
// romfs-patches-pc-<w>x<h> (make_romfs_patches.py --prompts pc --aspect): it carries that shape's UI layout too, so
|
||||
// it replaces romfs-patches-<w>x<h> as romfs-patches-pc replaces romfs-patches.
|
||||
char def[2100], path[2100];
|
||||
const char *patches = cfg_get("TOTK_ROMFS_PATCHES"), *mods = cfg_get("TOTK_ROMFS_MODS");
|
||||
int wide = 0;
|
||||
@@ -813,34 +934,46 @@ static void overlay_init(void) {
|
||||
"(tools/make_romfs_patches.py --aspect %ux%u)", out[0], out[1], out[0], out[1]);
|
||||
}
|
||||
}
|
||||
static const char *const layouts[2] = {"nintendo", "xbox"}; // one of each folder
|
||||
for (int i = 0; i < 2; i++) {
|
||||
prompts_folders[i].name = input_prompts_folder(PROMPTS_XBOX, layouts[i]);
|
||||
static const char *const layouts[2] = {"nintendo", "xbox"}; // one of each Xbox folder, then the PC one
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const char *name = i < 2 ? input_prompts_folder(PROMPTS_XBOX, layouts[i]) : input_prompts_folder(PROMPTS_PC, 0);
|
||||
const char *dir = name;
|
||||
if (i == 2) {
|
||||
if (wide) snprintf(pc_dir, sizeof pc_dir, "%s-%ux%u", name, out[0], out[1]);
|
||||
else snprintf(pc_dir, sizeof pc_dir, "%s", name);
|
||||
dir = pc_dir;
|
||||
}
|
||||
prompts_folders[i].name = name;
|
||||
prompts_folders[i].ready =
|
||||
snprintf(path, sizeof path, "%s/%s/manifest.tsv", index_dir, prompts_folders[i].name) < (int)sizeof path &&
|
||||
plat_file_exists(path);
|
||||
snprintf(path, sizeof path, "%s/%s/manifest.tsv", index_dir, dir) < (int)sizeof path && plat_file_exists(path);
|
||||
}
|
||||
const char *layout = cfg_value("TOTK_PAD_LAYOUT");
|
||||
prompts_wanted = input_prompts_folder(input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout), layout);
|
||||
int pick = input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout), shaped = wide && pick == PROMPTS_PC;
|
||||
const char *style = pick == PROMPTS_PC ? "keyboard and mouse" : "Xbox";
|
||||
prompts_wanted = input_prompts_folder(pick, layout);
|
||||
prompts_off = !prompts_wanted ? ROMFS_PROMPTS_OK
|
||||
: wide ? ROMFS_PROMPTS_SHAPE
|
||||
: patches ? ROMFS_PROMPTS_PATCHES
|
||||
: !romfs_prompts_ready(prompts_wanted) ? ROMFS_PROMPTS_MISSING
|
||||
: wide && !shaped ? ROMFS_PROMPTS_SHAPE
|
||||
: patches && !wide ? ROMFS_PROMPTS_PATCHES
|
||||
: !romfs_prompts_ready(prompts_wanted) ? (shaped ? ROMFS_PROMPTS_SHAPE : ROMFS_PROMPTS_MISSING)
|
||||
: ROMFS_PROMPTS_OK;
|
||||
restbl_find();
|
||||
size_t first = noverlay;
|
||||
if (prompts_wanted && prompts_off == ROMFS_PROMPTS_OK) {
|
||||
snprintf(path, sizeof path, "%s/%s", index_dir, prompts_wanted); // fits: its manifest path did
|
||||
// fits: its manifest path did
|
||||
snprintf(path, sizeof path, "%s/%s", index_dir, shaped ? pc_dir : prompts_wanted);
|
||||
if (overlay_load(path, 1, 0)) {
|
||||
prompts_loaded = prompts_wanted;
|
||||
rt_log("RomFS overlay: Xbox button prompts (%s)", path);
|
||||
rt_log("RomFS overlay: %s button prompts (%s)", style, path);
|
||||
if (shaped) patches = NULL; // in place of romfs-patches-<w>x<h>, whose UI layout it carries
|
||||
} else {
|
||||
prompts_off = ROMFS_PROMPTS_FAILED;
|
||||
rt_log("RomFS overlay: %s failed its check (run the setup again); romfs-patches loads instead", path);
|
||||
}
|
||||
} else if (prompts_wanted) {
|
||||
rt_log("RomFS overlay: prompts = xbox, but %s; Switch prompts stay",
|
||||
prompts_off == ROMFS_PROMPTS_SHAPE ? "the game resolution takes its own UI layout"
|
||||
rt_log("RomFS overlay: prompts = %s, but %s; Switch prompts stay", pick == PROMPTS_PC ? "pc" : "xbox",
|
||||
prompts_off == ROMFS_PROMPTS_SHAPE ? (shaped ? "the game resolution has no romfs-patches-pc-<w>x<h> "
|
||||
"(tools/make_romfs_patches.py --prompts pc --aspect)"
|
||||
: "the game resolution takes its own UI layout")
|
||||
: prompts_off == ROMFS_PROMPTS_PATCHES ? "TOTK_ROMFS_PATCHES is set"
|
||||
: "their folder has no manifest.tsv (run the setup again)");
|
||||
}
|
||||
@@ -856,11 +989,13 @@ static void overlay_init(void) {
|
||||
mods_overlay_init();
|
||||
const char *clash = prompts_loaded ? overlay_clash(first, prompts_end, mods_first, noverlay) : NULL;
|
||||
if (clash) {
|
||||
rt_log("RomFS overlay: a RomFS mod replaces %s, which the Xbox prompts change too; Switch prompts stay", clash);
|
||||
rt_log("RomFS overlay: a RomFS mod replaces %s, which the %s prompts change too; Switch prompts stay", clash,
|
||||
style);
|
||||
overlay_drop(first, prompts_end);
|
||||
prompts_loaded = NULL;
|
||||
prompts_off = ROMFS_PROMPTS_MODS;
|
||||
if (plain_ok) overlay_load(plain, 0, 0);
|
||||
if (shaped) overlay_load(def, 0, 0);
|
||||
else if (plain_ok) overlay_load(plain, 0, 0);
|
||||
}
|
||||
restbl_collect();
|
||||
overlay_contest();
|
||||
|
||||
@@ -6,14 +6,26 @@
|
||||
// The camera block is TotK's scene uniform block (bound as c4 in the scene's vertex shaders): vec4 rows 3-6 are the
|
||||
// view-projection, rows of a row-major 4x4 with OpenGL clip depth (-w..w). The replay's viewports have negative
|
||||
// heights, so window y grows downwards from ndc y = +1, and depth is z_ndc * 0.5 + 0.5.
|
||||
// With pc.bias > 0 the pass also writes DLSS's bias mask (vk_dlaa.c, "characters"): G-buffer target 0 carries an object
|
||||
// class in green, above 64/255 for characters, creatures and what they hold (Link 253, others 125, held items 160;
|
||||
// terrain 0, foliage 32). A pixel gets pc.bias when a character covers it now, or covered the place its surface came
|
||||
// from last frame (the previous class mask, read through the motion vector, 3x3): there the history holds a moving
|
||||
// character that the camera-only vector does not follow, which is the trail. chars_out keeps this frame's mask for the
|
||||
// next; chars_prev is last frame's (pc.prev_ok 0: none).
|
||||
layout(local_size_x = 8, local_size_y = 8) in;
|
||||
layout(binding = 0) uniform sampler2D depth_tex;
|
||||
layout(binding = 1, rg16f) uniform writeonly image2D mv_out;
|
||||
layout(binding = 2, std140) uniform Cur { vec4 r[16]; } cur;
|
||||
layout(binding = 3, std140) uniform Prev { vec4 r[16]; } prev;
|
||||
layout(binding = 4) uniform sampler2D ids_tex;
|
||||
layout(binding = 5, r8) uniform writeonly image2D mask_out;
|
||||
layout(binding = 6, r8) uniform readonly image2D chars_prev;
|
||||
layout(binding = 7, r8) uniform writeonly image2D chars_out;
|
||||
layout(push_constant) uniform P {
|
||||
vec2 size; // render size in pixels
|
||||
vec2 jitter; // this frame's jitter, pixels, as the scene was drawn (window x right, y down)
|
||||
float bias; // the mask value for character pixels; 0: no mask (bindings 4-7 unused)
|
||||
float prev_ok; // 1: chars_prev holds last frame's class mask at this size
|
||||
} pc;
|
||||
|
||||
mat4 vp(vec4 a, vec4 b, vec4 c, vec4 d) { return transpose(mat4(a, b, c, d)); }
|
||||
@@ -37,4 +49,15 @@ void main() {
|
||||
}
|
||||
}
|
||||
imageStore(mv_out, p, vec4(mv, 0.0, 0.0));
|
||||
if (pc.bias <= 0.0) return;
|
||||
float ch = texelFetch(ids_tex, p, 0).g > 64.0 / 255.0 ? 1.0 : 0.0;
|
||||
imageStore(chars_out, p, vec4(ch));
|
||||
float was = 0.0;
|
||||
if (pc.prev_ok > 0.5) {
|
||||
ivec2 q = ivec2(floor(win + mv)), hi = ivec2(pc.size) - 1;
|
||||
for (int dy = -1; dy <= 1; dy++)
|
||||
for (int dx = -1; dx <= 1; dx++)
|
||||
was = max(was, imageLoad(chars_prev, clamp(q + ivec2(dx, dy), ivec2(0), hi)).r);
|
||||
}
|
||||
imageStore(mask_out, p, vec4(max(ch, was) * pc.bias));
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
// the game's up to PIVOT and a straight line with its slope above, so mid-greys look as in SDR and highlights keep
|
||||
// going; the grade works on the colour divided by its largest channel above 1; a roll-off on the largest channel stops
|
||||
// at the display's peak. The output is relative to the HUD's white (1.0), which the present maps to TOTK_HDR_UI_NITS.
|
||||
// Hue correction (hdr.z, TOTK_HDR_HUE): the game's SDR clamp turns bright saturated colours towards white (a torch flame
|
||||
// clips red, then green, and reads yellow-white, the colour its emitted light was tuned to); the extended curve keeps
|
||||
// their ratios, so they stay saturated. From the picture's white upwards the colour moves towards the game's own SDR
|
||||
// result at the HDR luminance, by up to hdr.z (RenoDX's TotK mod has the same control).
|
||||
// Approach after RenoDX's TotK mod by souperman9 and Musa (github.com/clshortfuse/renodx, src/games/totk, MIT): its
|
||||
// "extended vanilla" curve.
|
||||
// Bindings are those of the game's translated shader: vk_hdr.c rewrites them from the original's names.
|
||||
@@ -14,7 +18,7 @@ layout(location = 0) out vec4 color;
|
||||
|
||||
layout(set = 0, binding = 0, std140) uniform SupportBuffer {
|
||||
vec4 pad[90];
|
||||
vec4 hdr; // byte 1440 (vk_exec_draw.c support_fill): game white / HUD white, peak / game white
|
||||
vec4 hdr; // byte 1440 (vk_exec_draw.c support_fill): game white / HUD white, peak / game white, hue correction
|
||||
} support_buffer;
|
||||
layout(set = 0, binding = 1) uniform sampler2D fp_t_tcb_8;
|
||||
layout(set = 0, binding = 2) uniform sampler2D fp_t_tcb_A;
|
||||
@@ -51,6 +55,15 @@ void main() {
|
||||
float grade = g.y * (1.0 - d * d) + g.x;
|
||||
c = s * ((n - m) * grade + m);
|
||||
|
||||
// the game's own SDR result: the same blend clamped to 0..1, graded
|
||||
vec3 sdr_c = clamp(mix(lum > 0.0 ? x * (sdr / lum) : x, vec3(1.0) - exp(-x), sdr * sdr), 0.0, 1.0);
|
||||
float sm = maxc(sdr_c), sd = sdr_c.r * 0.6666667 + (sdr_c.g + sdr_c.b) * w.z - 1.0;
|
||||
sdr_c = (sdr_c - sm) * (g.y * (1.0 - sd * sd) + g.x) + sm;
|
||||
float hue = clamp(support_buffer.hdr.z, 0.0, 1.0);
|
||||
const vec3 Y = vec3(0.2126, 0.7152, 0.0722); // Rec. 709 luminance
|
||||
float yc = dot(c, Y), ys = dot(sdr_c, Y);
|
||||
if (hue > 0.0 && ys > 1e-5) c = mix(c, max(sdr_c, vec3(0.0)) * (yc / ys), hue * smoothstep(0.5, 1.5, maxc(c)));
|
||||
|
||||
// roll-off of the largest channel from half the peak to the peak
|
||||
float top = maxc(c), knee = 0.5 * peak;
|
||||
if (top > knee) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#version 450
|
||||
// Write-back merge of a VRAM mirror (vk_memory.c, wb_merge_record): copies a 32-bit word of what the GPU left in the
|
||||
// mirror into the pool's imported buffer (guest memory) only where it differs from the snapshot taken before the
|
||||
// submission's GPU work, so words the GPU did not change keep what the CPU wrote meanwhile.
|
||||
// One invocation per word of the listed ranges; ranges are sorted by their first invocation (x: word offset in the
|
||||
// mirror and imported bindings, y: word offset in the snapshot binding, z: words, w: first invocation).
|
||||
layout(local_size_x = 64) in;
|
||||
layout(std430, set = 0, binding = 0) readonly buffer Post { uint post[]; };
|
||||
layout(std430, set = 0, binding = 1) readonly buffer Pre { uint pre[]; };
|
||||
layout(std430, set = 0, binding = 2) buffer Dst { uint dst[]; };
|
||||
layout(std430, set = 0, binding = 3) readonly buffer Ranges { uvec4 rng[]; };
|
||||
layout(push_constant) uniform Push { uint nranges, total; } pc;
|
||||
|
||||
void main() {
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
if (i >= pc.total) return;
|
||||
uint lo = 0, hi = pc.nranges;
|
||||
while (hi - lo > 1) {
|
||||
uint m = (lo + hi) >> 1;
|
||||
if (rng[m].w <= i) lo = m;
|
||||
else hi = m;
|
||||
}
|
||||
uvec4 r = rng[lo];
|
||||
uint k = i - r.w;
|
||||
uint v = post[r.x + k];
|
||||
if (v != pre[r.y + k]) dst[r.x + k] = v;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ extern "C" {
|
||||
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_SHAKE - UI_BIND_A == ACTION_COUNT - 1 && B_SHAKE == 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_RPADDLE2 - UI_PAD_SOUTH == PAD_RPADDLE2 - PAD_SOUTH && PAD_RPADDLE2 == PAD_COUNT - 1,
|
||||
@@ -48,6 +48,7 @@ static const char *action_label(int a) { return menu_text((UiTextId)(UI_BIND_A +
|
||||
static UiTextId action_desc(int a) {
|
||||
if (a <= B_MINUS) return (UiTextId)(UI_BIND_DESC_A + a);
|
||||
if (a <= B_DOWN) return UI_BIND_DESC_DPAD;
|
||||
if (a == B_SHAKE) return UI_BIND_DESC_SHAKE;
|
||||
return a <= B_SL_DOWN ? UI_BIND_DESC_MOVE : UI_BIND_DESC_CAMERA;
|
||||
}
|
||||
|
||||
@@ -168,7 +169,7 @@ static void row_action(int a, const InputMap &m, int layout) {
|
||||
a >= B_SL_LEFT ? menu_text(a <= B_SL_DOWN ? UI_PAD_LSTICK : UI_PAD_RSTICK)
|
||||
: np ? pad_label(p[0])
|
||||
: ""};
|
||||
const bool enabled[3] = {true, true, a < B_SL_LEFT}; // the sticks stay sticks
|
||||
const bool enabled[3] = {true, true, a < B_SL_LEFT}; // the sticks stay sticks (and the shake is the right stick's)
|
||||
char key[32];
|
||||
snprintf(key, sizeof key, "BIND_%s", input_action_name(a));
|
||||
const char *note = g_note_action == a && plat_now_ns() < g_note_until ? g_note : nullptr;
|
||||
@@ -236,6 +237,7 @@ void binds_rows(float scale) {
|
||||
for (int a : kMove) row_action(a, m, layout);
|
||||
opt_group(UI_BINDS_GROUP_CAMERA);
|
||||
for (int a : kCamera) row_action(a, m, layout);
|
||||
row_action(B_SHAKE, m, layout); // keys only; its controller cell shows the right stick, which the pad shakes itself
|
||||
ImGui::Dummy(ImVec2(0, 6 * scale));
|
||||
g_reset_asked = opt_button("##binds_reset", UI_BINDS_RESET, UI_BINDS_RESET_DESC);
|
||||
}
|
||||
|
||||
+77
-35
@@ -346,6 +346,12 @@ static int fonts_up(float scale) {
|
||||
ImFontGlyphRangesBuilder b;
|
||||
b.AddText(ui_text_in("en", kCjk[i].name));
|
||||
ui_text_visit(kCjk[i].lang, add_row_glyphs, &b);
|
||||
// Text the menu shows that no catalog row holds: a mod's cheat names and its folder name,
|
||||
// both read from disk at runtime. Their glyphs would otherwise fall back to the missing-glyph box.
|
||||
for (int k = 0; k < cheats_count(); k++)
|
||||
if (const CheatInfo *ci = cheats_info(k); ci && ci->name) b.AddText(ci->name);
|
||||
for (int k = 0; k < mods_count(); k++)
|
||||
if (const ModInfo *mi = mods_at(k); mi) b.AddText(mi->name);
|
||||
b.BuildRanges(&ranges[i]);
|
||||
ImFontConfig mc;
|
||||
mc.MergeMode = true;
|
||||
@@ -485,13 +491,15 @@ static void row_window_size(void) {
|
||||
}
|
||||
|
||||
// The game resolution (resolution.c): 16:9 sizes from its smallest, 1280x720 (for weak GPUs), up, the common 21:9 and
|
||||
// 32:9 monitor sizes and the 16:10 screens of the Steam Deck, handhelds and laptops (the camera and the HUD follow
|
||||
// 32:9 monitor sizes, the 16:10 screens of the Steam Deck, handhelds and laptops and the 3:2 screens of laptops and
|
||||
// tablets (Surface, Framework; the camera and the HUD follow
|
||||
// another shape, resolution.c res_init; the installer lays the HUD out for each, installer/steps.py ASPECT_SIZES), and
|
||||
// the current value when it is another size.
|
||||
static void row_output_res(void) {
|
||||
static const char *const vals[] = {"1280x720", "1600x900", "1920x1080", "2560x1440", "3200x1800", "3840x2160",
|
||||
"2560x1080", "3440x1440", "3840x1600", "5120x2160", "3840x1080", "5120x1440",
|
||||
"1280x800", "1680x1050", "1920x1200", "2560x1600"};
|
||||
"1280x800", "1680x1050", "1920x1200", "2560x1600",
|
||||
"1920x1280", "2160x1440", "2256x1504", "2880x1920", "3000x2000"};
|
||||
enum { N = sizeof vals / sizeof *vals };
|
||||
char shown[N][32];
|
||||
const char *names[N];
|
||||
@@ -551,27 +559,25 @@ static void row_gpu(void) {
|
||||
opt_choice_list("TOTK_GPU_INDEX", UI_MENU_GPU, vals, names, g_gpu_n + 1, UI_MENU_GPU_DESC);
|
||||
}
|
||||
|
||||
// The game language: one entry per language TotK ships (a Switch code that names it), each under its own name. The
|
||||
// Switch's Portuguese is not among them.
|
||||
// The game language: one entry per Mals language file the running game build ships (ui_text_game_langs), each under
|
||||
// its own name. A code that names no entry exactly (older "en", "fr-CA" on a build without it) selects the first entry
|
||||
// of the same catalog language.
|
||||
static void row_game_lang(void) {
|
||||
static const struct { const char *code; UiTextId name; } langs[] = {
|
||||
{"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-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];
|
||||
UiGameLang langs[16];
|
||||
int n = ui_text_game_langs(rt_game_version, langs, 16);
|
||||
const char *vals[17], *names[17];
|
||||
vals[0] = "";
|
||||
names[0] = menu_text(UI_MENU_GAME_LANG_AUTO);
|
||||
const char *now = cfg_value("TOTK_LANG");
|
||||
int cur = *now ? -1 : 0;
|
||||
for (int i = 1; i < N; i++) {
|
||||
for (int i = 1; i <= n; i++) {
|
||||
vals[i] = langs[i - 1].code;
|
||||
names[i] = menu_text(langs[i - 1].name);
|
||||
if (*now && !strcmp(ui_text_lang(now), ui_text_lang(vals[i]))) cur = i; // en-GB is English too
|
||||
if (*now && !strcmp(now, vals[i])) cur = i;
|
||||
}
|
||||
opt_choice_list("TOTK_LANG", UI_MENU_GAME_LANG, vals, names, N, UI_MENU_GAME_LANG_DESC, cur);
|
||||
for (int i = 1; i <= n && *now && cur < 0; i++)
|
||||
if (!strcmp(ui_text_lang(now), ui_text_lang(vals[i]))) cur = i;
|
||||
opt_choice_list("TOTK_LANG", UI_MENU_GAME_LANG, vals, names, n + 1, UI_MENU_GAME_LANG_DESC, cur);
|
||||
}
|
||||
|
||||
// The frame limit (frame_rate.c): unset, the older fps60 = 0 still means 30. The monitor choice shows the rate it gives.
|
||||
@@ -662,6 +668,8 @@ static void tab_screen(float scale) {
|
||||
opt_slider("TOTK_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
|
||||
static const UiTextId aspect[] = {UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH};
|
||||
opt_choice("TOTK_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
|
||||
static const UiTextId movie_aspect[] = {UI_MENU_MOVIE_ASPECT_KEEP, UI_MENU_MOVIE_ASPECT_STRETCH};
|
||||
opt_choice("TOTK_MOVIE_ASPECT", UI_MENU_MOVIE_ASPECT, movie_aspect, 2, UI_MENU_MOVIE_ASPECT_DESC);
|
||||
opt_slider("TOTK_FOV", UI_MENU_FOV, 70, 140, UI_MENU_FOV_DESC, 5, "%.0f %%");
|
||||
static const char *const aniso[] = {"game", "2", "4", "8", "16"};
|
||||
const char *aniso_names[] = {menu_text(UI_MENU_ANISO_GAME), "2x", "4x", "8x", "16x"};
|
||||
@@ -672,8 +680,13 @@ static void tab_screen(float scale) {
|
||||
opt_slider("TOTK_CONTACT_SHADOWS", UI_MENU_CONTACT_SHADOWS, 0, 100, UI_MENU_CONTACT_SHADOWS_DESC, 10, "%.0f %%");
|
||||
opt_toggle("TOTK_DLAA", UI_MENU_DLAA, UI_MENU_DLAA_DESC);
|
||||
static const UiTextId dlss[] = {UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY, UI_MENU_DLSS_BALANCED,
|
||||
UI_MENU_DLSS_PERFORMANCE};
|
||||
opt_choice("TOTK_DLSS", UI_MENU_DLSS, dlss, 4, UI_MENU_DLSS_DESC);
|
||||
UI_MENU_DLSS_PERFORMANCE, UI_MENU_DLSS_ULTRA};
|
||||
opt_choice("TOTK_DLSS", UI_MENU_DLSS, dlss, 5, UI_MENU_DLSS_DESC);
|
||||
static const UiTextId preset[] = {UI_MENU_DLSS_PRESET_DEFAULT, UI_MENU_DLSS_PRESET_K, UI_MENU_DLSS_PRESET_J};
|
||||
opt_choice("TOTK_DLSS_PRESET", UI_MENU_DLSS_PRESET, preset, 3, UI_MENU_DLSS_PRESET_DESC);
|
||||
static const UiTextId chars[] = {UI_MENU_DLSS_CHARS_OFF, UI_MENU_DLSS_CHARS_LIGHT, UI_MENU_DLSS_CHARS_MEDIUM,
|
||||
UI_MENU_DLSS_CHARS_FULL};
|
||||
opt_choice("TOTK_DLSS_CHAR_MASK", UI_MENU_DLSS_CHARS, chars, 4, UI_MENU_DLSS_CHARS_DESC);
|
||||
opt_toggle("TOTK_FXAA", UI_MENU_FXAA, UI_MENU_FXAA_DESC);
|
||||
rows_color();
|
||||
opt_group(UI_MENU_HDR_GROUP);
|
||||
@@ -681,12 +694,14 @@ static void tab_screen(float scale) {
|
||||
opt_slider("TOTK_HDR_PEAK_NITS", UI_MENU_HDR_PEAK, 400, 10000, UI_MENU_HDR_PEAK_DESC, 50, "%.0f");
|
||||
opt_slider("TOTK_HDR_GAME_NITS", UI_MENU_HDR_GAME, 80, 500, UI_MENU_HDR_GAME_DESC, 5, "%.0f");
|
||||
opt_slider("TOTK_HDR_UI_NITS", UI_MENU_HDR_UI, 80, 500, UI_MENU_HDR_UI_DESC, 5, "%.0f");
|
||||
opt_slider("TOTK_HDR_HUE", UI_MENU_HDR_HUE, 0, 100, UI_MENU_HDR_HUE_DESC, 5, "%.0f %%");
|
||||
opt_end();
|
||||
}
|
||||
|
||||
static void tab_game(float scale) {
|
||||
opt_begin("##game", scale);
|
||||
row_fps_limit();
|
||||
opt_toggle("TOTK_FRAME_QUEUE", UI_MENU_FRAME_QUEUE, UI_MENU_FRAME_QUEUE_DESC);
|
||||
row_game_lang();
|
||||
static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD};
|
||||
opt_choice("TOTK_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC);
|
||||
@@ -697,6 +712,8 @@ static void tab_game(float scale) {
|
||||
static void tab_sound(float scale) {
|
||||
opt_begin("##sound", scale);
|
||||
opt_slider("TOTK_VOLUME", UI_MENU_VOLUME, 0, 100, UI_MENU_VOLUME_DESC, 5, "%.0f %%");
|
||||
static const UiTextId channels[] = {UI_MENU_CHANNELS_AUTO, UI_MENU_CHANNELS_STEREO, UI_MENU_CHANNELS_51};
|
||||
opt_choice("TOTK_AUDIO_CHANNELS", UI_MENU_AUDIO_CHANNELS, channels, 3, UI_MENU_AUDIO_CHANNELS_DESC);
|
||||
opt_end();
|
||||
}
|
||||
|
||||
@@ -759,8 +776,11 @@ static const char *prompts_note(bool *restart) {
|
||||
const char *layout = cfg_value("TOTK_PAD_LAYOUT"), *used = romfs_prompts();
|
||||
const char *want = input_prompts_folder(input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout), layout);
|
||||
if (want == used || (want && used && !strcmp(want, used))) return nullptr;
|
||||
if (want && !romfs_prompts_ready(want)) return menu_text(UI_MENU_PROMPTS_MISSING);
|
||||
switch (romfs_prompts_off(want)) {
|
||||
// A game resolution of another shape needs a folder of its own for the PC prompts (romfs-patches-pc-<w>x<h>), which
|
||||
// the setup does not write: the shape is the reason then, not the setup.
|
||||
int off = romfs_prompts_off(want);
|
||||
if (want && !romfs_prompts_ready(want) && off != ROMFS_PROMPTS_SHAPE) return menu_text(UI_MENU_PROMPTS_MISSING);
|
||||
switch (off) {
|
||||
case ROMFS_PROMPTS_FAILED: return menu_text(UI_MENU_PROMPTS_FAILED);
|
||||
case ROMFS_PROMPTS_SHAPE: return menu_text(UI_MENU_PROMPTS_OFF_SHAPE);
|
||||
case ROMFS_PROMPTS_MODS: return menu_text(UI_MENU_PROMPTS_OFF_MODS);
|
||||
@@ -813,16 +833,19 @@ 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 prompts[] = {UI_MENU_PROMPTS_NINTENDO, UI_MENU_PROMPTS_XBOX};
|
||||
static const UiTextId prompts[] = {UI_MENU_PROMPTS_NINTENDO, UI_MENU_PROMPTS_XBOX, UI_MENU_PROMPTS_PC};
|
||||
bool restart;
|
||||
const char *note = prompts_note(&restart);
|
||||
opt_choice("TOTK_PROMPTS", UI_MENU_PROMPTS, prompts, 2, UI_MENU_PROMPTS_DESC, note);
|
||||
opt_choice("TOTK_PROMPTS", UI_MENU_PROMPTS, prompts, 3, UI_MENU_PROMPTS_DESC, note);
|
||||
bool ask = restart && opt_button("##restart", UI_MENU_RESTART, UI_MENU_RESTART_DESC);
|
||||
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);
|
||||
static const UiTextId mouse_modes[] = {UI_MENU_MOUSE_STICK, UI_MENU_MOUSE_DIRECT};
|
||||
opt_choice("TOTK_MOUSE_MODE", UI_MENU_MOUSE_MODE, mouse_modes, 2, UI_MENU_MOUSE_MODE_DESC);
|
||||
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
|
||||
opt_toggle("TOTK_MENU_CURSOR", UI_MENU_MENU_CURSOR, UI_MENU_MENU_CURSOR_DESC);
|
||||
row_walk_key();
|
||||
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
|
||||
// Analog sticks: one dead zone row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With
|
||||
@@ -851,6 +874,7 @@ static void tab_menu(float scale) {
|
||||
static const UiTextId langs[] = {UI_MENU_MENU_LANG_AUTO, UI_MENU_LANGNAME_EN, UI_MENU_LANGNAME_RU,
|
||||
UI_MENU_LANGNAME_PT_BR, UI_MENU_LANGNAME_ZH_HANS};
|
||||
opt_choice("TOTK_MENU_LANG", UI_MENU_MENU_LANG, langs, sizeof langs / sizeof *langs, UI_MENU_MENU_LANG_DESC);
|
||||
opt_toggle("TOTK_DISCORD_RPC", UI_MENU_DISCORD_RPC, UI_MENU_DISCORD_RPC_DESC);
|
||||
opt_end();
|
||||
}
|
||||
|
||||
@@ -936,7 +960,8 @@ static void tab_advanced(float scale) {
|
||||
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);
|
||||
static const UiTextId astc_bc[] = {UI_MENU_AUTO, UI_MENU_OFF, UI_MENU_ON}; // unset, 0, 1
|
||||
opt_choice("TOTK_ASTC_BC", UI_MENU_ASTC_BC, astc_bc, 3, 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);
|
||||
@@ -1373,17 +1398,30 @@ static void perf_full_draw(const MenuTarget *t, float scale) {
|
||||
ImGui::SetWindowFontScale(0.72f);
|
||||
const ImVec4 gpu_c(0.55f, 0.83f, 0.31f, 1), cpu_c(0.18f, 0.59f, 0.80f, 1), ram_c(0.80f, 0.45f, 0.70f, 1),
|
||||
fps_c(0.95f, 0.95f, 0.95f, 1), wait_c(0.93f, 0.76f, 0.33f, 1), pic_c(0.62f, 0.70f, 0.95f, 1);
|
||||
// the port version and the game update first, so a screenshot in a bug report carries both; formatted once per
|
||||
// menu language
|
||||
static char ver[96];
|
||||
static const char *ver_fmt;
|
||||
if (ver_fmt != menu_text(UI_MENU_VERSION)) {
|
||||
ver_fmt = menu_text(UI_MENU_VERSION);
|
||||
char game[16];
|
||||
snprintf(game, sizeof game, "%d.%d.%d", rt_game_version / 100, rt_game_version / 10 % 10, rt_game_version % 10);
|
||||
snprintf(ver, sizeof ver, ver_fmt, rt_port_version, game);
|
||||
}
|
||||
ImGui::TextDisabled("%s", ver);
|
||||
if (ImGui::BeginTable("##perft", 4, ImGuiTableFlags_SizingFixedFit)) {
|
||||
static const UiTextId labels[] = {UI_MENU_OV_GPU, UI_MENU_OV_VRAM, UI_MENU_OV_CPU, UI_MENU_OV_RAM};
|
||||
static const PerfCol c0[] = {{UI_MENU_OV_FPS, 888}};
|
||||
static const PerfCol c1[] = {{UI_MENU_OV_FMT_PCT, 100}, {UI_MENU_OV_FMT_GB, 888.8f}, {UI_MENU_OV_FRAME_MS, 888.8f}};
|
||||
static const PerfCol c2[] = {{UI_MENU_OV_FMT_TEMP, 188}, {UI_MENU_OV_FMT_GB_OF, 888.8f}, {UI_MENU_OV_MAX_MS, 8888.8f}};
|
||||
static const UiTextId labels[] = {UI_MENU_OV_GPU, UI_MENU_OV_VRAM, UI_MENU_OV_CPU, UI_MENU_OV_RAM,
|
||||
UI_MENU_OV_FPS_LABEL, UI_MENU_OV_FRAME_LABEL};
|
||||
static const PerfCol c1[] = {{UI_MENU_OV_FMT_PCT, 100}, {UI_MENU_OV_FMT_GB, 888.8f}, {UI_MENU_OV_FMT_FPS, 888},
|
||||
{UI_MENU_OV_FRAME_MS, 888.8f}};
|
||||
static const PerfCol c2[] = {{UI_MENU_OV_FMT_TEMP, 188}, {UI_MENU_OV_FMT_GB_OF, 888.8f}, {UI_MENU_OV_LOW1, 888},
|
||||
{UI_MENU_OV_MAX_MS, 8888.8f}};
|
||||
static const PerfCol c3[] = {{UI_MENU_OV_FMT_POWER, 888}, {UI_MENU_OV_FMT_MHZ, 8888}, {UI_MENU_OV_GAME_PCT, 100},
|
||||
{UI_MENU_OV_GAME_GB, 888.8f}};
|
||||
perf_column("##c0", c0, 1, labels, 4);
|
||||
perf_column("##c1", c1, 3);
|
||||
perf_column("##c2", c2, 3);
|
||||
perf_column("##c3", c3, 4);
|
||||
{UI_MENU_OV_GAME_GB, 888.8f}, {UI_MENU_OV_LOW01, 888}};
|
||||
perf_column("##c0", nullptr, 0, labels, 6);
|
||||
perf_column("##c1", c1, 4);
|
||||
perf_column("##c2", c2, 4);
|
||||
perf_column("##c3", c3, 5);
|
||||
perf_label(UI_MENU_OV_GPU, gpu_c);
|
||||
perf_cell(UI_MENU_OV_FMT_PCT, s.gpu_pct);
|
||||
perf_cell(UI_MENU_OV_FMT_TEMP, s.gpu_temp_c);
|
||||
@@ -1400,12 +1438,16 @@ static void perf_full_draw(const MenuTarget *t, float scale) {
|
||||
perf_cell(UI_MENU_OV_FMT_GB, perf_gb(s.ram_used_mb));
|
||||
perf_cell(UI_MENU_OV_FMT_GB_OF, perf_gb(s.ram_total_mb));
|
||||
perf_cell(UI_MENU_OV_GAME_GB, perf_gb(s.ram_game_mb));
|
||||
ImGui::TableNextColumn(); // the frame row: no label, the rate in its place
|
||||
char b[64];
|
||||
snprintf(b, sizeof b, menu_text(UI_MENU_OV_FPS), s.fps);
|
||||
ImGui::TextColored(fps_c, "%s", b);
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_None, 6 * scale); // a gap between the machine and the frames
|
||||
// the frames: the rate over the last second with the lows over the last ten seconds, then the frame times
|
||||
perf_label(UI_MENU_OV_FPS_LABEL, fps_c);
|
||||
perf_cell(UI_MENU_OV_FMT_FPS, s.fps);
|
||||
perf_cell(UI_MENU_OV_LOW1, s.low1_fps);
|
||||
perf_cell(UI_MENU_OV_LOW01, s.low01_fps);
|
||||
perf_label(UI_MENU_OV_FRAME_LABEL, fps_c);
|
||||
perf_cell(UI_MENU_OV_FRAME_MS, s.frame_ms_avg);
|
||||
perf_cell(UI_MENU_OV_MAX_MS, s.frame_ms_max);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::EndTable();
|
||||
}
|
||||
float top = s.frame_ms_max > 50 ? s.frame_ms_max : 50;
|
||||
|
||||
+227
-41
@@ -18,13 +18,23 @@ typedef struct { const char *lang; const char *const *text; } Row;
|
||||
[UI_MENU_LANGNAME_DE] = "Deutsch", [UI_MENU_LANGNAME_IT] = "Italiano", [UI_MENU_LANGNAME_ES] = "Español", \
|
||||
[UI_MENU_LANGNAME_NL] = "Nederlands", [UI_MENU_LANGNAME_RU] = "Русский", \
|
||||
[UI_MENU_LANGNAME_ZH_HANS] = "简体中文", [UI_MENU_LANGNAME_ZH_HANT] = "繁體中文", [UI_MENU_LANGNAME_KO] = "한국어", \
|
||||
[UI_MENU_LANGNAME_PT_BR] = "Português (Brasil)"
|
||||
[UI_MENU_LANGNAME_PT_BR] = "Português (Brasil)", [UI_MENU_LANGNAME_EN_GB] = "English (UK)", \
|
||||
[UI_MENU_LANGNAME_FR_CA] = "Français (Canada)", [UI_MENU_LANGNAME_ES_419] = "Español (Latinoamérica)"
|
||||
|
||||
// SWKBD: TITLE, PROMPT, DEFAULT_NAME (at most 9 UTF-16 units, no '@' '%' '\'), OK, CANCEL, PAD_HINT.
|
||||
#define SWKBD(title, prompt, name, ok, cancel, hint) \
|
||||
[UI_SWKBD_TITLE] = title, [UI_SWKBD_PROMPT] = prompt, [UI_SWKBD_DEFAULT_NAME] = name, [UI_SWKBD_OK] = ok, \
|
||||
[UI_SWKBD_CANCEL] = cancel, [UI_SWKBD_PAD_HINT] = hint, LANG_NAMES
|
||||
|
||||
// The DLSS model file and the executable, as the player sees them on this platform.
|
||||
#ifdef _WIN32
|
||||
#define DLSS_FILE "nvngx_dlss.dll"
|
||||
#define EXE_FILE "totk.exe"
|
||||
#else
|
||||
#define DLSS_FILE "libnvidia-ngx-dlss.so"
|
||||
#define EXE_FILE "totk"
|
||||
#endif
|
||||
|
||||
static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
SWKBD("Text entry", "Enter a name", "Horse", "OK", "Cancel", "Controller: A = OK, B = Cancel"),
|
||||
|
||||
@@ -55,7 +65,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[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. A wider size (21:9) widens the camera's view and moves "
|
||||
"the HUD to the screen's corners; a taller one (16:10, Steam Deck) moves it to the top "
|
||||
"the HUD to the screen's corners; a taller one (16:10, 3:2) moves it to the top "
|
||||
"and bottom edges.",
|
||||
[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 "
|
||||
@@ -69,6 +79,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "When the window's shape differs from the game resolution's (a 16:9 picture on a 21:9 "
|
||||
"screen): keep the picture's shape with black bars, or stretch it over the window.",
|
||||
[UI_MENU_ASPECT_FIT] = "Black bars", [UI_MENU_ASPECT_STRETCH] = "Stretch",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Movie shape",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "When the game resolution is not 16:9, keep the cutscene movies' own shape with black "
|
||||
"bars, or stretch them over the whole picture. Applies from the next movie.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Black bars", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Stretch",
|
||||
[UI_MENU_FOV] = "Field of view",
|
||||
[UI_MENU_FOV_DESC] = "How much of the world the camera shows, in percent of the game's own. Game version 1.4.3 only.",
|
||||
[UI_MENU_SHADOW_RES] = "Shadow map size",
|
||||
@@ -109,6 +123,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
"bright room.",
|
||||
[UI_MENU_HDR_UI] = "HUD and menu brightness",
|
||||
[UI_MENU_HDR_UI_DESC] = "Nits of the HUD's and this menu's white.",
|
||||
[UI_MENU_HDR_HUE] = "Highlight hue correction",
|
||||
[UI_MENU_HDR_HUE_DESC] = "Bright colours such as fire and sparks turn towards white as in the game's own SDR picture, "
|
||||
"matching the light they cast; 0 keeps them fully saturated.",
|
||||
[UI_MENU_SHADOW_SCALE] = "Shadow resolution",
|
||||
[UI_MENU_SHADOW_SCALE_DESC] = "Draws the sun's shadow maps this many times larger on each side than the game's 960x960: "
|
||||
"sharper shadow edges and leaf shadows. Takes effect at the next start.",
|
||||
@@ -123,15 +140,28 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
"away. Spreads the shadow map over more ground: raise Shadow resolution with it. "
|
||||
"Takes effect at the next start.",
|
||||
[UI_MENU_DLAA] = "NVIDIA DLSS / DLAA",
|
||||
[UI_MENU_DLAA_DESC] = "NVIDIA DLSS for the 3D scene (NVIDIA RTX GPUs; nvngx_dlss.dll next to totk.exe): smoother edges "
|
||||
"and less shimmer. The HUD is not touched. The mode below sets the resolution it works from. Takes effect "
|
||||
"after a restart.",
|
||||
[UI_MENU_DLAA_DESC] = "NVIDIA DLSS for the 3D scene (NVIDIA RTX GPUs; " DLSS_FILE " next to " EXE_FILE "): smoother "
|
||||
"edges and less shimmer. The HUD is not touched. The mode below sets the resolution it works "
|
||||
"from. Takes effect after a restart.",
|
||||
[UI_MENU_DLSS_PRESET] = "DLSS model",
|
||||
[UI_MENU_DLSS_PRESET_DESC] = "Which DLSS model runs. Automatic follows NVIDIA's advice per mode: K for DLAA, Quality "
|
||||
"and Balanced, M for Performance, L for Ultra Performance. J: slightly less ghosting "
|
||||
"behind moving characters, a little more flicker. K: the sharpest model in every mode. "
|
||||
"Applies at once.",
|
||||
[UI_MENU_DLSS_PRESET_DEFAULT] = "Automatic", [UI_MENU_DLSS_PRESET_K] = "K (sharpest)",
|
||||
[UI_MENU_DLSS_PRESET_J] = "J (less ghosting)",
|
||||
[UI_MENU_DLSS_CHARS] = "DLSS character trails",
|
||||
[UI_MENU_DLSS_CHARS_DESC] = "Reduces the faint trails behind moving characters, animals and what they hold: DLSS takes "
|
||||
"less of the previous frames there. Stronger removes more of the trails and leaves those "
|
||||
"edges a little less smooth. Applies at once.",
|
||||
[UI_MENU_DLSS_CHARS_OFF] = "Off", [UI_MENU_DLSS_CHARS_LIGHT] = "Light", [UI_MENU_DLSS_CHARS_MEDIUM] = "Medium",
|
||||
[UI_MENU_DLSS_CHARS_FULL] = "Full",
|
||||
[UI_MENU_DLSS] = "DLSS mode",
|
||||
[UI_MENU_DLSS_DESC] = "DLAA draws the 3D scene at full resolution. Quality, Balanced and Performance draw it at 67, 58 "
|
||||
"and 50 % and let DLSS scale it up, for more frames per second; they replace the 3D render "
|
||||
"scale. Takes effect after a restart.",
|
||||
[UI_MENU_DLSS_DESC] = "DLAA draws the 3D scene at full resolution. Quality, Balanced, Performance and Ultra "
|
||||
"Performance draw it at 67, 58, 50 and 33 % and let DLSS scale it up, which saves GPU time; "
|
||||
"they replace the 3D render scale. Takes effect at once.",
|
||||
[UI_MENU_DLSS_DLAA] = "DLAA (native)", [UI_MENU_DLSS_QUALITY] = "Quality", [UI_MENU_DLSS_BALANCED] = "Balanced",
|
||||
[UI_MENU_DLSS_PERFORMANCE] = "Performance",
|
||||
[UI_MENU_DLSS_PERFORMANCE] = "Performance", [UI_MENU_DLSS_ULTRA] = "Ultra Performance",
|
||||
[UI_MENU_FXAA] = "Game anti-aliasing (FXAA)",
|
||||
[UI_MENU_FXAA_DESC] = "The game's own FXAA edge smoothing. It blurs fine detail a little; with DLAA on it is not needed.",
|
||||
|
||||
@@ -142,6 +172,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
// the monitor's refresh rate; printf format with the rate in Hz when it is known
|
||||
[UI_MENU_FPS_DISPLAY] = "Monitor rate", [UI_MENU_FPS_DISPLAY_HZ] = "Monitor rate (%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Frame queue",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "The game starts a frame while the previous one waits for its turn on screen: far fewer "
|
||||
"dropped frames at the limit, up to one frame more input delay. Applies at once.",
|
||||
[UI_MENU_REALTIME] = "Game speed follows the clock",
|
||||
[UI_MENU_REALTIME_DESC] = "Steps the game by real time, so a slow frame does not slow the game down.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Cloth on the clock",
|
||||
@@ -191,6 +224,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
"at 60 FPS.",
|
||||
|
||||
[UI_MENU_VOLUME] = "Volume", [UI_MENU_VOLUME_DESC] = "Master volume of the game.",
|
||||
[UI_MENU_AUDIO_CHANNELS] = "Speakers",
|
||||
[UI_MENU_AUDIO_CHANNELS_DESC] = "Stereo or 5.1 surround output. Automatic picks 5.1 when the Windows speaker setup "
|
||||
"has six or more channels. Applies after a restart.",
|
||||
[UI_MENU_CHANNELS_AUTO] = "Automatic", [UI_MENU_CHANNELS_STEREO] = "Stereo", [UI_MENU_CHANNELS_51] = "5.1 surround",
|
||||
|
||||
[UI_MENU_MOTION] = "Motion controls", [UI_MENU_MOTION_DESC] = "Aiming with the controller's gyroscope.",
|
||||
[UI_MENU_RUMBLE_STRENGTH] = "Vibration strength",
|
||||
@@ -211,10 +248,11 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_DESC] = "Which controller's buttons the game's prompts show. Nintendo: the game's own. Xbox: LB, "
|
||||
"RB, LT, RT, Menu, View and a d-pad cross, and A, B, X and Y as labelled on the button "
|
||||
"the layout above presses (with Nintendo (A right) the right button is labelled B, so the "
|
||||
"prompts for A show B). The game reads its prompts at start, so they change after a "
|
||||
"restart. They are part of the game's files, so a button you remap in Controls keeps its "
|
||||
"old prompt on screen.",
|
||||
[UI_MENU_PROMPTS_NINTENDO] = "Nintendo", [UI_MENU_PROMPTS_XBOX] = "Xbox",
|
||||
"prompts for A show B). PC: the keys and mouse buttons of the keyboard's default layout "
|
||||
"(E, Shift, Space, left click...) and the mouse for the camera. The game reads its prompts "
|
||||
"at start, so they change after a restart. They are part of the game's files, so a button "
|
||||
"you remap in Controls keeps its old prompt on screen.",
|
||||
[UI_MENU_PROMPTS_NINTENDO] = "Nintendo", [UI_MENU_PROMPTS_XBOX] = "Xbox", [UI_MENU_PROMPTS_PC] = "PC",
|
||||
[UI_MENU_PROMPTS_MISSING] = "prompt files missing: run the setup again",
|
||||
[UI_MENU_PROMPTS_FAILED] = "prompt files damaged: run the setup again",
|
||||
[UI_MENU_PROMPTS_RESTART] = "prompts change after restart",
|
||||
@@ -231,6 +269,12 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_LSTICK_DEADZONE] = "Left stick dead zone", [UI_MENU_RSTICK_DEADZONE] = "Right 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: let go of the stick, read \"stick now\" and set the dead zone a few percent above it.",
|
||||
[UI_MENU_STICK_DEADZONE_NOW] = "stick now: %.0f %%",
|
||||
[UI_MENU_MOUSE_MODE] = "Mouse camera",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direct: the camera turns by how far the mouse moves, as in a PC game, with no stick dead zone or curve (games 1.4.3 and 1.2.1; 1.1.0 uses stick emulation). Stick emulation: the mouse acts as the right stick.",
|
||||
[UI_MENU_MOUSE_STICK] = "Stick emulation",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Direct",
|
||||
[UI_MENU_MENU_CURSOR] = "Mouse in the game's menus",
|
||||
[UI_MENU_MENU_CURSOR_DESC] = "In the game's own menus (title, inventory, shops, dialogue choices) the pointer shows: the item under it is selected, left click confirms, right click goes back. Only after the mouse or a key was used last, so a gamepad sees the menus unchanged (game 1.4.3).",
|
||||
[UI_MENU_MOUSE_SENS] = "Mouse camera sensitivity",
|
||||
[UI_MENU_MOUSE_SENS_DESC] = "How fast the mouse turns the camera.",
|
||||
[UI_MENU_WALK_KEY] = "Walk key",
|
||||
@@ -272,6 +316,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_BIND_DESC_MOVE] = "Moves Link like the left stick. The controller's left stick always works too.",
|
||||
[UI_BIND_DESC_CAMERA] = "Turns the camera like the right stick. The mouse and the controller's right stick always "
|
||||
"work too.",
|
||||
[UI_BIND_SHAKE] = "Shake the camera stick",
|
||||
[UI_BIND_DESC_SHAKE] = "Swings the right stick left and right for the game: detaches a fused part from what "
|
||||
"Ultrahand holds. Holding the key keeps shaking.",
|
||||
[UI_KEY_SPACE] = "Space", [UI_KEY_TAB] = "Tab", [UI_KEY_LSHIFT] = "Left Shift", [UI_KEY_LCTRL] = "Left Ctrl",
|
||||
[UI_KEY_UP] = "Up arrow", [UI_KEY_DOWN] = "Down arrow", [UI_KEY_LEFT] = "Left arrow",
|
||||
[UI_KEY_RIGHT] = "Right arrow",
|
||||
@@ -322,6 +369,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_AMIIBO_STATUS] = "Status: %s",
|
||||
[UI_MENU_MENU_LANG] = "Menu language", [UI_MENU_MENU_LANG_DESC] = "Language of this menu and the overlay.",
|
||||
[UI_MENU_MENU_LANG_AUTO] = "Automatic (from Windows)",
|
||||
[UI_MENU_DISCORD_RPC] = "Discord Rich Presence",
|
||||
[UI_MENU_DISCORD_RPC_DESC] = "Shows \"Playing TotK Recomp\" with the play time in your Discord status while the "
|
||||
"game runs. discord_app_id and discord_image in totk.ini choose the application.",
|
||||
|
||||
[UI_MENU_ADV_WARNING] = "These settings are for testing and troubleshooting. Change them only when you know why.",
|
||||
[UI_MENU_GROUP_SCHED] = "Scheduling", [UI_MENU_GROUP_SHADERS] = "Shaders and pipelines",
|
||||
@@ -359,7 +409,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[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_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. Textures look slightly blockier. Auto: on for GPUs with 4 GB of video memory or less.",
|
||||
[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.",
|
||||
@@ -404,6 +454,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
|
||||
[UI_MENU_OV_FPS] = "%.0f FPS", [UI_MENU_OV_FRAME_MS] = "%.1f ms", [UI_MENU_OV_GAME_PCT] = "game %.0f%%",
|
||||
[UI_MENU_OV_MAX_MS] = "max %.1f ms",
|
||||
[UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "Frame", [UI_MENU_OV_FMT_FPS] = "%.0f",
|
||||
[UI_MENU_OV_LOW1] = "1%% low %.0f", [UI_MENU_OV_LOW01] = "0.1%% low %.0f",
|
||||
[UI_MENU_OV_GPU] = "GPU", [UI_MENU_OV_CPU] = "CPU", [UI_MENU_OV_RAM] = "RAM", [UI_MENU_OV_VRAM] = "VRAM",
|
||||
[UI_MENU_OV_TEMP] = "Temperature", [UI_MENU_OV_POWER] = "Power", [UI_MENU_OV_CLOCK] = "Clock",
|
||||
[UI_MENU_OV_NONE] = "-",
|
||||
@@ -483,7 +535,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_OUTPUT_RES] = "Разрешение игры",
|
||||
[UI_MENU_OUTPUT_RES_DESC] = "Размер, в котором игра рисует картинку и интерфейс. Выше 1920 × 1080 игре нужно больше "
|
||||
"видеопамяти и времени GPU. Более широкий размер (21:9) расширяет обзор камеры и "
|
||||
"переносит интерфейс в углы экрана, более высокий (16:10, Steam Deck) переносит его к "
|
||||
"переносит интерфейс в углы экрана, более высокий (16:10, 3:2) переносит его к "
|
||||
"верхнему и нижнему краям.",
|
||||
[UI_MENU_RENDER_SCALE] = "Масштаб 3D-отрисовки",
|
||||
[UI_MENU_RENDER_SCALE_DESC] = "Размер 3D-сцены относительно разрешения игры; FSR игры доводит её до этого размера. "
|
||||
@@ -497,6 +549,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "Когда форма окна отличается от разрешения игры (картинка 16:9 на экране 21:9): сохранить "
|
||||
"форму картинки с чёрными полосами или растянуть её на всё окно.",
|
||||
[UI_MENU_ASPECT_FIT] = "Чёрные полосы", [UI_MENU_ASPECT_STRETCH] = "Растянуть",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Форма видеороликов",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "Когда разрешение игры не 16:9: сохранить форму роликов с чёрными полосами или "
|
||||
"растянуть их на всю картинку. Действует со следующего ролика.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Чёрные полосы", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Растянуть",
|
||||
[UI_MENU_FOV] = "Поле зрения",
|
||||
[UI_MENU_FOV_DESC] = "Сколько мира показывает камера, в процентах от игрового. Только для версии игры 1.4.3.",
|
||||
[UI_MENU_SHADOW_RES] = "Размер карты теней",
|
||||
@@ -538,6 +594,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
"поднимите.",
|
||||
[UI_MENU_HDR_UI] = "Яркость интерфейса и меню",
|
||||
[UI_MENU_HDR_UI_DESC] = "Ниты белого цвета интерфейса игры и этого меню.",
|
||||
[UI_MENU_HDR_HUE] = "Коррекция оттенка бликов",
|
||||
[UI_MENU_HDR_HUE_DESC] = "Яркие цвета (огонь, искры) уходят к белому, как в SDR-картинке игры, и совпадают со светом, "
|
||||
"который они дают; 0 оставляет их полностью насыщенными.",
|
||||
[UI_MENU_SHADOW_SCALE] = "Разрешение теней",
|
||||
[UI_MENU_SHADOW_SCALE_DESC] = "Карты теней от солнца рисуются во столько раз больше по каждой стороне, чем 960x960 "
|
||||
"самой игры: края теней и тени листвы чётче. Действует после перезапуска.",
|
||||
@@ -552,15 +611,28 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
"Карта теней растягивается на большую площадь, поэтому вместе с этим поднимайте "
|
||||
"разрешение теней. Действует после перезапуска.",
|
||||
[UI_MENU_DLAA] = "NVIDIA DLSS / DLAA",
|
||||
[UI_MENU_DLAA_DESC] = "NVIDIA DLSS для 3D-сцены (видеокарты NVIDIA RTX; nvngx_dlss.dll рядом с totk.exe): ровнее края и "
|
||||
"меньше мерцания. Интерфейс не затрагивается. Режим ниже задаёт разрешение, с которым он работает. "
|
||||
"Действует после перезапуска.",
|
||||
[UI_MENU_DLAA_DESC] = "NVIDIA DLSS для 3D-сцены (видеокарты NVIDIA RTX; " DLSS_FILE " рядом с " EXE_FILE "): ровнее "
|
||||
"края и меньше мерцания. Интерфейс не затрагивается. Режим ниже задаёт разрешение, с которым он "
|
||||
"работает. Действует после перезапуска.",
|
||||
[UI_MENU_DLSS_PRESET] = "Модель DLSS",
|
||||
[UI_MENU_DLSS_PRESET_DESC] = "Какая модель DLSS работает. «Автоматически» следует совету NVIDIA для каждого режима: K "
|
||||
"для DLAA, «Качества» и «Баланса», M для «Производительности», L для «Макс. "
|
||||
"производительности». J: чуть меньше шлейфов за движущимися персонажами, немного больше "
|
||||
"мерцания. K: самая чёткая модель во всех режимах. Действует сразу.",
|
||||
[UI_MENU_DLSS_PRESET_DEFAULT] = "Автоматически", [UI_MENU_DLSS_PRESET_K] = "K (самая чёткая)",
|
||||
[UI_MENU_DLSS_PRESET_J] = "J (меньше шлейфов)",
|
||||
[UI_MENU_DLSS_CHARS] = "Шлейфы персонажей в DLSS",
|
||||
[UI_MENU_DLSS_CHARS_DESC] = "Убирает слабые шлейфы за движущимися персонажами, животными и тем, что они держат: DLSS "
|
||||
"берёт там меньше от прошлых кадров. Чем сильнее, тем меньше шлейфов и тем чуть менее "
|
||||
"гладкие края. Действует сразу.",
|
||||
[UI_MENU_DLSS_CHARS_OFF] = "Выкл.", [UI_MENU_DLSS_CHARS_LIGHT] = "Слабо", [UI_MENU_DLSS_CHARS_MEDIUM] = "Средне",
|
||||
[UI_MENU_DLSS_CHARS_FULL] = "Полностью",
|
||||
[UI_MENU_DLSS] = "Режим DLSS",
|
||||
[UI_MENU_DLSS_DESC] = "DLAA рисует 3D-сцену в полном разрешении. «Качество», «Баланс» и «Производительность» рисуют её "
|
||||
"в 67, 58 и 50 % и масштабируют через DLSS ради большего числа кадров; они заменяют масштаб "
|
||||
"3D-отрисовки. Действует после перезапуска.",
|
||||
[UI_MENU_DLSS_DESC] = "DLAA рисует 3D-сцену в полном разрешении. «Качество», «Баланс», «Производительность» и "
|
||||
"«Макс. производительность» рисуют её в 67, 58, 50 и 33 % и масштабируют через DLSS, что "
|
||||
"экономит время видеокарты; они заменяют масштаб 3D-отрисовки. Действует сразу.",
|
||||
[UI_MENU_DLSS_DLAA] = "DLAA (родное)", [UI_MENU_DLSS_QUALITY] = "Качество", [UI_MENU_DLSS_BALANCED] = "Баланс",
|
||||
[UI_MENU_DLSS_PERFORMANCE] = "Производительность",
|
||||
[UI_MENU_DLSS_PERFORMANCE] = "Производительность", [UI_MENU_DLSS_ULTRA] = "Макс. производительность",
|
||||
[UI_MENU_FXAA] = "Сглаживание игры (FXAA)",
|
||||
[UI_MENU_FXAA_DESC] = "Собственное сглаживание FXAA. Слегка размывает мелкие детали; с DLAA оно не нужно.",
|
||||
|
||||
@@ -570,6 +642,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS",
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "Как у монитора", [UI_MENU_FPS_DISPLAY_HZ] = "Как у монитора (%.0f Гц)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Очередь кадров",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "Игра начинает кадр, пока предыдущий ждёт показа: намного меньше пропущенных кадров на "
|
||||
"лимите, задержка ввода больше не более чем на кадр. Применяется сразу.",
|
||||
[UI_MENU_REALTIME] = "Скорость игры по часам",
|
||||
[UI_MENU_REALTIME_DESC] = "Игра идёт по реальному времени, медленный кадр её не замедляет.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Ткань по часам",
|
||||
@@ -617,6 +692,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_MODS_QUICKMENU_DESC] = "Исправление порта: чёрный фон меню оружия, щитов и материалов при 60 FPS.",
|
||||
|
||||
[UI_MENU_VOLUME] = "Громкость", [UI_MENU_VOLUME_DESC] = "Общая громкость игры.",
|
||||
[UI_MENU_AUDIO_CHANNELS] = "Динамики",
|
||||
[UI_MENU_AUDIO_CHANNELS_DESC] = "Стерео или объёмный звук 5.1. Автоматически выбирается 5.1, если в Windows "
|
||||
"настроено шесть каналов или больше. Применяется после перезапуска.",
|
||||
[UI_MENU_CHANNELS_AUTO] = "Автоматически", [UI_MENU_CHANNELS_STEREO] = "Стерео", [UI_MENU_CHANNELS_51] = "Объёмный 5.1",
|
||||
|
||||
[UI_MENU_MOTION] = "Управление движением", [UI_MENU_MOTION_DESC] = "Прицеливание гироскопом контроллера.",
|
||||
[UI_MENU_RUMBLE_STRENGTH] = "Сила вибрации",
|
||||
@@ -637,10 +716,11 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_DESC] = "Кнопки какого контроллера показывают подсказки игры. Nintendo: как в самой игре. Xbox: "
|
||||
"LB, RB, LT, RT, Menu, View и крестовина, а A, B, X и Y подписаны так же, как кнопка, "
|
||||
"которую нажимает раскладка выше (в Nintendo (A справа) правая кнопка подписана B, и "
|
||||
"подсказки A показывают B). Игра читает подсказки при запуске, так что они меняются после "
|
||||
"перезапуска. Подсказки записаны в файлах игры, поэтому у кнопки, переназначенной на "
|
||||
"вкладке управления, на экране остаётся прежняя подсказка.",
|
||||
[UI_MENU_PROMPTS_NINTENDO] = "Nintendo", [UI_MENU_PROMPTS_XBOX] = "Xbox",
|
||||
"подсказки A показывают B). PC: клавиши и кнопки мыши стандартной раскладки клавиатуры "
|
||||
"(E, Shift, пробел, левый клик...) и мышь для камеры. Игра читает подсказки при запуске, "
|
||||
"так что они меняются после перезапуска. Подсказки записаны в файлах игры, поэтому у "
|
||||
"кнопки, переназначенной на вкладке управления, на экране остаётся прежняя подсказка.",
|
||||
[UI_MENU_PROMPTS_NINTENDO] = "Nintendo", [UI_MENU_PROMPTS_XBOX] = "Xbox", [UI_MENU_PROMPTS_PC] = "PC",
|
||||
[UI_MENU_PROMPTS_MISSING] = "нет файлов подсказок: запустите установку снова",
|
||||
[UI_MENU_PROMPTS_FAILED] = "файлы подсказок повреждены: запустите установку снова",
|
||||
[UI_MENU_PROMPTS_RESTART] = "подсказки сменятся после перезапуска",
|
||||
@@ -657,6 +737,12 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_LSTICK_DEADZONE] = "Мёртвая зона левого стика", [UI_MENU_RSTICK_DEADZONE] = "Мёртвая зона правого стика",
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно отклонить стик геймпада, чтобы игра это заметила. Увеличьте, если персонаж или камера двигаются сами: отпустите стик, посмотрите «стик сейчас» и поставьте мёртвую зону на несколько процентов выше.",
|
||||
[UI_MENU_STICK_DEADZONE_NOW] = "стик сейчас: %.0f %%",
|
||||
[UI_MENU_MOUSE_MODE] = "Управление мышью",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Прямое: камера поворачивается на столько, на сколько сдвинута мышь, как в играх для ПК, без мёртвой зоны и кривой стика (версии игры 1.4.3 и 1.2.1; на 1.1.0 эмуляция стика). Эмуляция стика: мышь работает как правый стик.",
|
||||
[UI_MENU_MOUSE_STICK] = "Эмуляция стика",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Прямое",
|
||||
[UI_MENU_MENU_CURSOR] = "Мышь в меню игры",
|
||||
[UI_MENU_MENU_CURSOR_DESC] = "В меню самой игры (заставка, инвентарь, магазины, варианты ответа) появляется указатель: пункт под ним выбирается, левый щелчок подтверждает, правый возвращает назад. Только если последними использовались мышь или клавиатура, поэтому с геймпадом меню не меняются (версия игры 1.4.3).",
|
||||
[UI_MENU_MOUSE_SENS] = "Чувствительность мыши",
|
||||
[UI_MENU_MOUSE_SENS_DESC] = "Как быстро мышь поворачивает камеру.",
|
||||
[UI_MENU_WALK_KEY] = "Клавиша шага",
|
||||
@@ -701,6 +787,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_BIND_DESC_MOVE] = "Двигает Линка, как левый стик. Левый стик контроллера тоже всегда работает.",
|
||||
[UI_BIND_DESC_CAMERA] = "Поворачивает камеру, как правый стик. Мышь и правый стик контроллера тоже всегда "
|
||||
"работают.",
|
||||
[UI_BIND_SHAKE] = "Встряхнуть стик камеры",
|
||||
[UI_BIND_DESC_SHAKE] = "Качает правый стик влево-вправо: отцепляет приклеенную деталь от того, что держит "
|
||||
"Ультрарука. Пока клавиша зажата, тряска продолжается.",
|
||||
[UI_KEY_SPACE] = "Пробел", [UI_KEY_TAB] = "Tab", [UI_KEY_LSHIFT] = "Левый Shift", [UI_KEY_LCTRL] = "Левый Ctrl",
|
||||
[UI_KEY_UP] = "Стрелка вверх", [UI_KEY_DOWN] = "Стрелка вниз", [UI_KEY_LEFT] = "Стрелка влево",
|
||||
[UI_KEY_RIGHT] = "Стрелка вправо",
|
||||
@@ -752,6 +841,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_AMIIBO_STATUS] = "Состояние: %s",
|
||||
[UI_MENU_MENU_LANG] = "Язык меню", [UI_MENU_MENU_LANG_DESC] = "Язык этого меню и индикатора.",
|
||||
[UI_MENU_MENU_LANG_AUTO] = "Автоматически (из Windows)",
|
||||
[UI_MENU_DISCORD_RPC] = "Discord Rich Presence",
|
||||
[UI_MENU_DISCORD_RPC_DESC] = "Пока игра запущена, показывает «Играю в TotK Recomp» со временем игры в статусе "
|
||||
"Discord. Приложение выбирают discord_app_id и discord_image в totk.ini.",
|
||||
|
||||
[UI_MENU_ADV_WARNING] = "Эти настройки нужны для тестов и поиска неполадок. Меняйте их, только если знаете зачем.",
|
||||
[UI_MENU_GROUP_SCHED] = "Планирование", [UI_MENU_GROUP_SHADERS] = "Шейдеры и конвейеры",
|
||||
@@ -789,7 +881,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[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_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 (размеры хранилищ, размер запуска, размер точки, базовый экземпляр) вместо чтения нулей.",
|
||||
@@ -836,6 +928,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
|
||||
[UI_MENU_OV_FPS] = "%.0f FPS", [UI_MENU_OV_FRAME_MS] = "%.1f мс", [UI_MENU_OV_GAME_PCT] = "игра %.0f%%",
|
||||
[UI_MENU_OV_MAX_MS] = "макс. %.1f мс",
|
||||
[UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "Кадр", [UI_MENU_OV_FMT_FPS] = "%.0f",
|
||||
[UI_MENU_OV_LOW1] = "1%% худших %.0f", [UI_MENU_OV_LOW01] = "0.1%% худших %.0f",
|
||||
[UI_MENU_OV_GPU] = "GPU", [UI_MENU_OV_CPU] = "ЦП", [UI_MENU_OV_RAM] = "ОЗУ", [UI_MENU_OV_VRAM] = "Видеопамять",
|
||||
[UI_MENU_OV_TEMP] = "Температура", [UI_MENU_OV_POWER] = "Мощность", [UI_MENU_OV_CLOCK] = "Частота",
|
||||
[UI_MENU_OV_NONE] = "-",
|
||||
@@ -945,7 +1039,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_OUTPUT_RES] = "Resolução do jogo",
|
||||
[UI_MENU_OUTPUT_RES_DESC] = "O tamanho em que o jogo desenha a imagem e a interface. Acima de 1920 x 1080 o jogo "
|
||||
"precisa de mais memória de vídeo e tempo de GPU. Um tamanho mais largo (21:9) amplia a "
|
||||
"visão da câmera e leva o HUD para os cantos da tela; um mais alto (16:10, Steam Deck) "
|
||||
"visão da câmera e leva o HUD para os cantos da tela; um mais alto (16:10, 3:2) "
|
||||
"leva-o para as bordas de cima e de baixo.",
|
||||
[UI_MENU_RENDER_SCALE] = "Escala da renderização 3D",
|
||||
[UI_MENU_RENDER_SCALE_DESC] = "Tamanho da cena 3D em relação à resolução do jogo; o FSR do jogo a amplia até esse "
|
||||
@@ -960,6 +1054,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "Quando o formato da janela difere do da resolução do jogo (uma imagem 16:9 numa tela "
|
||||
"21:9): manter o formato da imagem com barras pretas ou esticá-la sobre a janela.",
|
||||
[UI_MENU_ASPECT_FIT] = "Barras pretas", [UI_MENU_ASPECT_STRETCH] = "Esticar",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Formato dos vídeos",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "Quando a resolução do jogo não é 16:9: manter o formato dos vídeos das cenas com "
|
||||
"barras pretas ou esticá-los sobre a imagem toda. Vale a partir do próximo vídeo.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Barras pretas", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Esticar",
|
||||
[UI_MENU_SHARPNESS] = "Nitidez do FSR",
|
||||
[UI_MENU_SHARPNESS_DESC] = "Aumento de nitidez após a ampliação do FSR; 0 desliga.",
|
||||
|
||||
@@ -974,6 +1072,9 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
"a janela perde o foco, como no Switch ao apertar Home; um carregamento interrompido "
|
||||
"assim pode parar de vez.",
|
||||
[UI_MENU_FPS_DISPLAY] = "Taxa do monitor", [UI_MENU_FPS_DISPLAY_HZ] = "Taxa do monitor (%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Fila de quadros",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "O jogo começa um quadro enquanto o anterior espera a vez de aparecer: muito menos quadros "
|
||||
"perdidos no limite, com até um quadro a mais de atraso na entrada. Vale na hora.",
|
||||
[UI_MENU_REALTIME] = "Velocidade do jogo pelo relógio",
|
||||
[UI_MENU_REALTIME_DESC] = "Avança o jogo pelo tempo real, para que um quadro lento não deixe o jogo mais lento.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Tecidos pelo relógio",
|
||||
@@ -1048,6 +1149,9 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
"funciona.",
|
||||
[UI_BIND_DESC_CAMERA] = "Gira a câmera como o analógico direito. O mouse e o analógico direito do controle também "
|
||||
"sempre funcionam.",
|
||||
[UI_BIND_SHAKE] = "Sacudir o analógico da câmera",
|
||||
[UI_BIND_DESC_SHAKE] = "Balança o analógico direito para os lados: solta uma peça fundida do que a Ultramão segura. "
|
||||
"Segurar a tecla continua sacudindo.",
|
||||
[UI_KEY_SPACE] = "Espaço", [UI_KEY_TAB] = "Tab", [UI_KEY_LSHIFT] = "Shift esquerdo",
|
||||
[UI_KEY_LCTRL] = "Ctrl esquerdo",
|
||||
[UI_KEY_UP] = "Seta para cima", [UI_KEY_DOWN] = "Seta para baixo", [UI_KEY_LEFT] = "Seta para a esquerda",
|
||||
@@ -1100,6 +1204,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_AMIIBO_STATUS] = "Estado: %s",
|
||||
[UI_MENU_MENU_LANG] = "Idioma do menu", [UI_MENU_MENU_LANG_DESC] = "Idioma deste menu e do indicador.",
|
||||
[UI_MENU_MENU_LANG_AUTO] = "Automático (do Windows)",
|
||||
[UI_MENU_DISCORD_RPC] = "Discord Rich Presence",
|
||||
[UI_MENU_DISCORD_RPC_DESC] = "Mostra \"Jogando TotK Recomp\" com o tempo de jogo no seu status do Discord "
|
||||
"enquanto o jogo roda. discord_app_id e discord_image no totk.ini escolhem a "
|
||||
"aplicação.",
|
||||
|
||||
[UI_MENU_ADV_WARNING] = "Estas configurações são para testes e solução de problemas. Altere-as só quando souber "
|
||||
"por quê.",
|
||||
@@ -1140,7 +1248,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_MEMORY_VERIFY] = "Verificar",
|
||||
[UI_MENU_TEX_ALIAS] = "Aliasing de texturas", [UI_MENU_TEX_ALIAS_DESC] = "Uma textura nova criada sobre a memória de uma textura viva começa com esse conteúdo em vez de vazia.",
|
||||
[UI_MENU_TEX_VRAM_PRESSURE] = "Limite de memória de texturas", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Porcentagem da memória de vídeo acima da qual as texturas liberadas pelo jogo são descartadas (voltam do armazenamento quando usadas de novo). 0 mantém todas.",
|
||||
[UI_MENU_ASTC_BC] = "Comprimir ASTC em BC3", [UI_MENU_ASTC_BC_DESC] = "Com o decodificador da GPU: recodifica as texturas ASTC decodificadas em BC3, que ocupa um quarto da memória de vídeo. Para GPUs com 4 GB ou menos; as texturas ficam um pouco mais quadriculadas.",
|
||||
[UI_MENU_ASTC_BC] = "Comprimir ASTC em BC3", [UI_MENU_ASTC_BC_DESC] = "Com o decodificador da GPU: recodifica as texturas ASTC decodificadas em BC3, que ocupa um quarto da memória de vídeo. As texturas ficam um pouco mais quadriculadas. Automático: ligado em GPUs com 4 GB de memória de vídeo ou menos.",
|
||||
[UI_MENU_LOD_GUARD] = "Proteção de LOD", [UI_MENU_LOD_GUARD_DESC] = "Evita que objetos pisquem no lugar errado ou que o plano distante fique achatado por um quadro, esperando o jogo terminar de reescrever os dados de nível de detalhe antes que a GPU os leia.",
|
||||
[UI_MENU_FIX_C0] = "Correção do buffer de constantes",
|
||||
[UI_MENU_FIX_C0_DESC] = "Preenche o banco de constantes c0 reservado ao driver (tamanhos de armazenamento, tamanho do dispatch, tamanho do ponto, instância base) em vez de ler zeros.",
|
||||
@@ -1186,6 +1294,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
|
||||
[UI_MENU_OV_FPS] = "%.0f FPS", [UI_MENU_OV_FRAME_MS] = "%.1f ms", [UI_MENU_OV_GAME_PCT] = "jogo %.0f%%",
|
||||
[UI_MENU_OV_MAX_MS] = "máx. %.1f ms",
|
||||
[UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "Quadro", [UI_MENU_OV_FMT_FPS] = "%.0f",
|
||||
[UI_MENU_OV_LOW1] = "1%% piores %.0f", [UI_MENU_OV_LOW01] = "0.1%% piores %.0f",
|
||||
[UI_MENU_OV_GPU] = "GPU", [UI_MENU_OV_CPU] = "CPU", [UI_MENU_OV_RAM] = "RAM", [UI_MENU_OV_VRAM] = "VRAM",
|
||||
[UI_MENU_OV_TEMP] = "Temperatura", [UI_MENU_OV_POWER] = "Consumo", [UI_MENU_OV_CLOCK] = "Frequência",
|
||||
[UI_MENU_OV_NONE] = "-",
|
||||
@@ -1251,11 +1361,20 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_DESC] = "Os botões de qual controle os ícones do jogo mostram. Nintendo: os do próprio jogo. "
|
||||
"Xbox: LB, RB, LT, RT, Menu, Exibir e uma cruz direcional, e A, B, X e Y com a letra do "
|
||||
"botão que o layout acima aperta (com Nintendo (A à direita) o botão da direita tem a "
|
||||
"letra B, então os ícones de A mostram B). O jogo lê os ícones ao iniciar, então eles "
|
||||
"mudam depois de reiniciar. Eles fazem parte dos arquivos do jogo, então um botão "
|
||||
"remapeado em Controles continua com o ícone antigo na tela.",
|
||||
"letra B, então os ícones de A mostram B). PC: as teclas e os botões do mouse do layout "
|
||||
"padrão do teclado (E, Shift, Espaço, clique esquerdo...) e o mouse para a câmera. O jogo "
|
||||
"lê os ícones ao iniciar, então eles mudam depois de reiniciar. Eles fazem parte dos "
|
||||
"arquivos do jogo, então um botão remapeado em Controles continua com o ícone antigo na "
|
||||
"tela.",
|
||||
[UI_MENU_PROMPTS_NINTENDO] = "Nintendo",
|
||||
[UI_MENU_PROMPTS_XBOX] = "Xbox",
|
||||
[UI_MENU_PROMPTS_PC] = "PC",
|
||||
[UI_MENU_MOUSE_MODE] = "Câmera com o mouse",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direta: a câmera gira o quanto o mouse se move, como num jogo de PC, sem zona morta nem curva do analógico (jogos 1.4.3 e 1.2.1; o 1.1.0 usa a emulação do analógico). Emulação do analógico: o mouse age como o analógico direito.",
|
||||
[UI_MENU_MOUSE_STICK] = "Emulação do analógico",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Direta",
|
||||
[UI_MENU_MENU_CURSOR] = "Mouse nos menus do jogo",
|
||||
[UI_MENU_MENU_CURSOR_DESC] = "Nos menus do próprio jogo (título, inventário, lojas, escolhas de diálogo) o ponteiro aparece: o item sob ele fica selecionado, o clique esquerdo confirma e o direito volta. Só depois de usar o mouse ou uma tecla por último, então com um controle os menus ficam como antes (jogo 1.4.3).",
|
||||
[UI_MENU_PROMPTS_MISSING] = "faltam os arquivos dos ícones: execute a instalação de novo",
|
||||
[UI_MENU_PROMPTS_FAILED] = "arquivos dos ícones danificados: execute a instalação de novo",
|
||||
[UI_MENU_PROMPTS_RESTART] = "os ícones mudam após reiniciar",
|
||||
@@ -1280,6 +1399,9 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_WALK_SCALE_DESC] = "A inclinação dos analógicos do teclado enquanto a tecla de andar está segura. Metade "
|
||||
"é andar, a inclinação total é correr.",
|
||||
[UI_MENU_OV_SCALE_DOWN] = "bilinear (o FSR só amplia)",
|
||||
[UI_MENU_HDR_HUE] = "Correção de matiz dos realces",
|
||||
[UI_MENU_HDR_HUE_DESC] = "Cores fortes como fogo e faíscas vão para o branco como na imagem SDR do próprio jogo e "
|
||||
"combinam com a luz que emitem; 0 as mantém totalmente saturadas.",
|
||||
[UI_MENU_SHADOW_SCALE] = "Resolução das sombras",
|
||||
[UI_MENU_SHADOW_SCALE_DESC] = "Desenha os mapas de sombra do sol essa quantidade de vezes maiores em cada lado "
|
||||
"que os 960x960 do jogo: bordas de sombra e sombras de folhas mais nítidas. Vale no "
|
||||
@@ -1300,13 +1422,26 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
"mais suaves e menos cintilação. O HUD não é afetado. O modo abaixo define a resolução de "
|
||||
"onde ele parte. Vale após reiniciar.",
|
||||
[UI_MENU_DLSS] = "Modo do DLSS",
|
||||
[UI_MENU_DLSS_DESC] = "DLAA desenha a cena 3D na resolução cheia. Qualidade, Equilibrado e Desempenho a desenham "
|
||||
"em 67, 58 e 50 % e deixam o DLSS ampliá-la, para mais quadros por segundo; eles substituem "
|
||||
"a escala 3D. Vale após reiniciar.",
|
||||
[UI_MENU_DLSS_DESC] = "DLAA desenha a cena 3D na resolução cheia. Qualidade, Equilibrado, Desempenho e Ultra "
|
||||
"Desempenho a desenham em 67, 58, 50 e 33 % e deixam o DLSS ampliá-la, o que poupa tempo da "
|
||||
"GPU; eles substituem a escala 3D. Vale na hora.",
|
||||
[UI_MENU_DLSS_DLAA] = "DLAA (nativo)",
|
||||
[UI_MENU_DLSS_QUALITY] = "Qualidade",
|
||||
[UI_MENU_DLSS_BALANCED] = "Equilibrado",
|
||||
[UI_MENU_DLSS_PERFORMANCE] = "Desempenho",
|
||||
[UI_MENU_DLSS_PERFORMANCE] = "Desempenho", [UI_MENU_DLSS_ULTRA] = "Ultra Desempenho",
|
||||
[UI_MENU_DLSS_PRESET] = "Modelo do DLSS",
|
||||
[UI_MENU_DLSS_PRESET_DESC] = "Qual modelo do DLSS roda. Automático segue a recomendação da NVIDIA para cada modo: K "
|
||||
"para DLAA, Qualidade e Equilibrado, M para Desempenho, L para Ultra Desempenho. J: um "
|
||||
"pouco menos de rastro atrás de personagens em movimento, um pouco mais de cintilação. K: "
|
||||
"o modelo mais nítido em todos os modos. Vale na hora.",
|
||||
[UI_MENU_DLSS_PRESET_DEFAULT] = "Automático", [UI_MENU_DLSS_PRESET_K] = "K (mais nítido)",
|
||||
[UI_MENU_DLSS_PRESET_J] = "J (menos rastro)",
|
||||
[UI_MENU_DLSS_CHARS] = "Rastros de personagens no DLSS",
|
||||
[UI_MENU_DLSS_CHARS_DESC] = "Reduz os rastros fracos atrás de personagens, animais e do que eles seguram em movimento: "
|
||||
"ali o DLSS usa menos dos quadros anteriores. Mais forte remove mais rastros e deixa essas "
|
||||
"bordas um pouco menos suaves. Vale na hora.",
|
||||
[UI_MENU_DLSS_CHARS_OFF] = "Desligado", [UI_MENU_DLSS_CHARS_LIGHT] = "Leve", [UI_MENU_DLSS_CHARS_MEDIUM] = "Médio",
|
||||
[UI_MENU_DLSS_CHARS_FULL] = "Total",
|
||||
[UI_MENU_FXAA] = "Antisserrilhado do jogo (FXAA)",
|
||||
[UI_MENU_FXAA_DESC] = "A suavização de bordas FXAA do próprio jogo. Borra um pouco os detalhes finos; com o DLAA "
|
||||
"ligado não é necessária.",
|
||||
@@ -1419,7 +1554,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_OUTPUT_RES_DESC] = "游戏绘制画面和界面的尺寸。"
|
||||
"高于 1920 x 1080 会占用更多显存和 GPU 时间。"
|
||||
"更宽的尺寸(21:9)会扩大视野,并把界面移到屏幕四角;"
|
||||
"更高的尺寸(16:10、Steam Deck)则移到上下边缘。",
|
||||
"更高的尺寸(16:10、3:2)则移到上下边缘。",
|
||||
[UI_MENU_RENDER_SCALE] = "3D 渲染比例",
|
||||
[UI_MENU_RENDER_SCALE_DESC] = "3D 场景相对于游戏分辨率的尺寸;"
|
||||
"游戏的 FSR 会将其缩放到该尺寸。"
|
||||
@@ -1435,6 +1570,11 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
"用黑边保持画面比例,或拉伸铺满窗口。",
|
||||
[UI_MENU_ASPECT_FIT] = "黑边",
|
||||
[UI_MENU_ASPECT_STRETCH] = "拉伸",
|
||||
[UI_MENU_MOVIE_ASPECT] = "视频画面比例",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "当游戏分辨率不是 16:9 时:用黑边保持过场视频的原有比例,或拉伸铺满整个画面。"
|
||||
"从下一段视频起生效。",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "黑边",
|
||||
[UI_MENU_MOVIE_ASPECT_STRETCH] = "拉伸",
|
||||
[UI_MENU_FOV] = "视野",
|
||||
[UI_MENU_FOV_DESC] = "镜头能看到多大范围,以游戏自身视野的百分比表示。"
|
||||
"仅限游戏版本 1.4.3。",
|
||||
@@ -1468,6 +1608,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "显示器刷新率",
|
||||
[UI_MENU_FPS_DISPLAY_HZ] = "显示器刷新率(%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "帧队列",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "前一帧等待显示时游戏就开始绘制下一帧:在帧率上限下掉帧大幅减少,输入延迟最多增加一帧。立即生效。",
|
||||
[UI_MENU_REALTIME] = "游戏速度跟随时钟",
|
||||
[UI_MENU_REALTIME_DESC] = "按真实时间推进游戏,帧率低时游戏速度也不会变慢。",
|
||||
[UI_MENU_CLOTH_REALTIME] = "布料跟随时钟",
|
||||
@@ -1573,6 +1715,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_BIND_DESC_DPAD] = "快捷选择。",
|
||||
[UI_BIND_DESC_MOVE] = "像左摇杆一样移动林克。手柄的左摇杆始终可用。",
|
||||
[UI_BIND_DESC_CAMERA] = "像右摇杆一样转动视角。鼠标和手柄的右摇杆始终可用。",
|
||||
[UI_BIND_SHAKE] = "晃动视角摇杆",
|
||||
[UI_BIND_DESC_SHAKE] = "让右摇杆左右晃动:把究极手抓着的物体上粘着的零件拆下。按住该键会一直晃动。",
|
||||
[UI_KEY_SPACE] = "空格",
|
||||
[UI_KEY_TAB] = "Tab",
|
||||
[UI_KEY_LSHIFT] = "左 Shift",
|
||||
@@ -1657,6 +1801,9 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_MENU_LANG] = "菜单语言",
|
||||
[UI_MENU_MENU_LANG_DESC] = "本菜单和性能浮层的语言。",
|
||||
[UI_MENU_MENU_LANG_AUTO] = "自动(跟随 Windows)",
|
||||
[UI_MENU_DISCORD_RPC] = "Discord Rich Presence",
|
||||
[UI_MENU_DISCORD_RPC_DESC] = "游戏运行时在 Discord 状态中显示“正在玩 TotK Recomp”及游戏时长。totk.ini 中的 "
|
||||
"discord_app_id 和 discord_image 选择应用。",
|
||||
[UI_MENU_ADV_WARNING] = "这些设置用于测试和排查问题,请在了解原因后再修改。",
|
||||
[UI_MENU_GROUP_SCHED] = "调度",
|
||||
[UI_MENU_GROUP_SHADERS] = "着色器和管线",
|
||||
@@ -1715,8 +1862,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
"再次用到时从存储重新加载。0 表示不丢弃。",
|
||||
[UI_MENU_ASTC_BC] = "ASTC 压缩为 BC3",
|
||||
[UI_MENU_ASTC_BC_DESC] = "配合 GPU 解码器使用:把解码后的 ASTC 纹理重新编码为 BC3,"
|
||||
"显存占用只有四分之一。适用于 4 GB 及以下的显卡;"
|
||||
"纹理会出现轻微色块。",
|
||||
"显存占用只有四分之一。纹理会出现轻微色块。"
|
||||
"自动:显存 4 GB 及以下的显卡上开启。",
|
||||
[UI_MENU_LOD_GUARD] = "LOD 保护",
|
||||
[UI_MENU_LOD_GUARD_DESC] = "等待游戏改写完细节层次数据后再让 GPU 读取,"
|
||||
"避免物体错位闪现,或远景有一帧变平。",
|
||||
@@ -1775,6 +1922,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_OV_FRAME_MS] = "%.1f ms",
|
||||
[UI_MENU_OV_GAME_PCT] = "游戏 %.0f%%",
|
||||
[UI_MENU_OV_MAX_MS] = "最长 %.1f ms",
|
||||
[UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "帧时间", [UI_MENU_OV_FMT_FPS] = "%.0f",
|
||||
[UI_MENU_OV_LOW1] = "1%% 低 %.0f", [UI_MENU_OV_LOW01] = "0.1%% 低 %.0f",
|
||||
[UI_MENU_OV_GPU] = "GPU",
|
||||
[UI_MENU_OV_CPU] = "CPU",
|
||||
[UI_MENU_OV_RAM] = "内存",
|
||||
@@ -1828,9 +1977,17 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS] = "按键提示",
|
||||
[UI_MENU_PROMPTS_DESC] = "游戏的按键提示显示哪种手柄的按键。Nintendo:游戏原有的提示。Xbox:LB、RB、LT、RT、Menu、View 和十字键,以及上方布局所按下的按键上标注的 "
|
||||
"A、B、X、Y(使用 Nintendo(A 在右)时右侧按键标为 B,因此 A 的提示显示 "
|
||||
"B)。游戏在启动时读取按键提示,因此重启后才会改变。提示属于游戏文件,在“操作”中重新映射的按键在屏幕上仍显示原来的提示。",
|
||||
"B)。PC:键盘默认布局的按键和鼠标按键(E、Shift、空格、左键……),镜头用鼠标。"
|
||||
"游戏在启动时读取按键提示,因此重启后才会改变。提示属于游戏文件,在“操作”中重新映射的按键在屏幕上仍显示原来的提示。",
|
||||
[UI_MENU_PROMPTS_NINTENDO] = "Nintendo",
|
||||
[UI_MENU_PROMPTS_XBOX] = "Xbox",
|
||||
[UI_MENU_PROMPTS_PC] = "PC",
|
||||
[UI_MENU_MOUSE_MODE] = "鼠标视角",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "直接:镜头按鼠标移动的距离转动,与电脑游戏相同,没有摇杆死区和响应曲线(游戏 1.4.3 和 1.2.1;1.1.0 使用摇杆模拟)。摇杆模拟:鼠标相当于右摇杆。",
|
||||
[UI_MENU_MOUSE_STICK] = "摇杆模拟",
|
||||
[UI_MENU_MOUSE_DIRECT] = "直接",
|
||||
[UI_MENU_MENU_CURSOR] = "在游戏菜单中使用鼠标",
|
||||
[UI_MENU_MENU_CURSOR_DESC] = "在游戏自身的菜单中(标题、背包、商店、对话选项)显示指针:指针下的项目被选中,左键确认,右键返回。仅在最后使用的是鼠标或键盘时生效,因此使用手柄时菜单保持不变(游戏 1.4.3)。",
|
||||
[UI_MENU_PROMPTS_MISSING] = "缺少按键提示文件:请重新运行安装程序",
|
||||
[UI_MENU_PROMPTS_FAILED] = "按键提示文件已损坏:请重新运行安装程序",
|
||||
[UI_MENU_PROMPTS_RESTART] = "按键提示重启后生效",
|
||||
@@ -1850,6 +2007,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_WALK_SCALE] = "步行幅度",
|
||||
[UI_MENU_WALK_SCALE_DESC] = "按住步行键时键盘摇杆的推动幅度。一半为走路,推满为奔跑。",
|
||||
[UI_MENU_OV_SCALE_DOWN] = "双线性(FSR 只能放大)",
|
||||
[UI_MENU_HDR_HUE] = "高光色调校正",
|
||||
[UI_MENU_HDR_HUE_DESC] = "火焰、火花等明亮色彩像游戏自身的 SDR 画面一样趋向白色,与它们发出的光一致;0 保持完全饱和。",
|
||||
[UI_MENU_SHADOW_SCALE] = "阴影分辨率",
|
||||
[UI_MENU_SHADOW_SCALE_DESC] = "将太阳阴影贴图的每条边绘制为游戏 960x960 的若干倍:阴影边缘和树叶阴影更清晰。下次启动时生效。",
|
||||
[UI_MENU_SHADOW_RANGE] = "阴影级联范围",
|
||||
@@ -1862,11 +2021,22 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_DLAA_DESC] = "用于 3D 场景的 NVIDIA DLSS(NVIDIA RTX 显卡;nvngx_dlss.dll 放在 totk.exe 旁):边缘更平滑,闪烁更少。HUD "
|
||||
"不受影响。下方的模式决定它的工作分辨率。重启后生效。",
|
||||
[UI_MENU_DLSS] = "DLSS 模式",
|
||||
[UI_MENU_DLSS_DESC] = "DLAA 以全分辨率绘制 3D 场景。质量、平衡和性能分别以 67%、58% 和 50% 绘制,再由 DLSS 放大,以获得更高帧率;它们会取代 3D 渲染比例。重启后生效。",
|
||||
[UI_MENU_DLSS_DESC] = "DLAA 以全分辨率绘制 3D 场景。质量、平衡、性能和超级性能分别以 67%、58%、50% 和 33% 绘制,再由 DLSS 放大,以节省 GPU 时间;它们会取代 3D 渲染比例。立即生效。",
|
||||
[UI_MENU_DLSS_DLAA] = "DLAA(原生)",
|
||||
[UI_MENU_DLSS_QUALITY] = "质量",
|
||||
[UI_MENU_DLSS_BALANCED] = "平衡",
|
||||
[UI_MENU_DLSS_PERFORMANCE] = "性能",
|
||||
[UI_MENU_DLSS_ULTRA] = "超级性能",
|
||||
[UI_MENU_DLSS_PRESET] = "DLSS 模型",
|
||||
[UI_MENU_DLSS_PRESET_DESC] = "运行哪个 DLSS 模型。自动按 NVIDIA 对各模式的建议:DLAA、质量和平衡用 K,性能用 M,超级性能用 L。"
|
||||
"J:移动角色身后的拖影略少,闪烁略多。K:所有模式都用最清晰的模型。立即生效。",
|
||||
[UI_MENU_DLSS_PRESET_DEFAULT] = "自动", [UI_MENU_DLSS_PRESET_K] = "K(最清晰)",
|
||||
[UI_MENU_DLSS_PRESET_J] = "J(拖影更少)",
|
||||
[UI_MENU_DLSS_CHARS] = "DLSS 角色拖影",
|
||||
[UI_MENU_DLSS_CHARS_DESC] = "减少移动中的角色、动物及其手持物品身后的淡淡拖影:DLSS 在这些位置少用之前的帧。越强拖影越少,"
|
||||
"这些边缘也略欠平滑。立即生效。",
|
||||
[UI_MENU_DLSS_CHARS_OFF] = "关", [UI_MENU_DLSS_CHARS_LIGHT] = "弱", [UI_MENU_DLSS_CHARS_MEDIUM] = "中",
|
||||
[UI_MENU_DLSS_CHARS_FULL] = "强",
|
||||
[UI_MENU_FXAA] = "游戏抗锯齿(FXAA)",
|
||||
[UI_MENU_FXAA_DESC] = "游戏自身的 FXAA 边缘平滑。会让细节略微模糊;开启 DLAA 时不需要。",
|
||||
[UI_MENU_TAB_CHEATS] = "金手指",
|
||||
@@ -1921,6 +2091,22 @@ const char *ui_text_lang(const char *code) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// Every game language: the Mals files EUen/USen, USfr/EUfr, USes/EUes ship in both builds; USpt only from 1.4.3.
|
||||
int ui_text_game_langs(int game_version, UiGameLang *out, int max) {
|
||||
static const struct { const char *code; UiTextId name; int since; } all[] = {
|
||||
{"en-US", UI_MENU_LANGNAME_EN, 0}, {"en-GB", UI_MENU_LANGNAME_EN_GB, 0}, {"ja", UI_MENU_LANGNAME_JA, 0},
|
||||
{"fr", UI_MENU_LANGNAME_FR, 0}, {"fr-CA", UI_MENU_LANGNAME_FR_CA, 0}, {"de", UI_MENU_LANGNAME_DE, 0},
|
||||
{"it", UI_MENU_LANGNAME_IT, 0}, {"es", UI_MENU_LANGNAME_ES, 0}, {"es-419", UI_MENU_LANGNAME_ES_419, 0},
|
||||
{"pt-BR", UI_MENU_LANGNAME_PT_BR, 143}, {"nl", UI_MENU_LANGNAME_NL, 0}, {"ru", UI_MENU_LANGNAME_RU, 0},
|
||||
{"zh-Hans", UI_MENU_LANGNAME_ZH_HANS, 0}, {"zh-Hant", UI_MENU_LANGNAME_ZH_HANT, 0},
|
||||
{"ko", UI_MENU_LANGNAME_KO, 0},
|
||||
};
|
||||
int n = 0;
|
||||
for (size_t i = 0; i < sizeof all / sizeof *all && n < max; i++)
|
||||
if (game_version >= all[i].since) out[n++] = (UiGameLang){all[i].code, all[i].name};
|
||||
return n;
|
||||
}
|
||||
|
||||
static const Row *find_row(const char *lang) {
|
||||
for (size_t i = 0; i < sizeof rows / sizeof *rows; i++)
|
||||
if (!strcmp(rows[i].lang, lang)) return &rows[i];
|
||||
|
||||
+24
-7
@@ -37,6 +37,7 @@ typedef enum {
|
||||
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_MOVIE_ASPECT, UI_MENU_MOVIE_ASPECT_DESC, UI_MENU_MOVIE_ASPECT_KEEP, UI_MENU_MOVIE_ASPECT_STRETCH,
|
||||
UI_MENU_FOV, UI_MENU_FOV_DESC,
|
||||
UI_MENU_SHADOW_RES, UI_MENU_SHADOW_RES_DESC, UI_MENU_SHADOW_RES_GAME,
|
||||
UI_MENU_ANISO, UI_MENU_ANISO_DESC, UI_MENU_ANISO_GAME,
|
||||
@@ -48,18 +49,21 @@ typedef enum {
|
||||
UI_MENU_COLOR_SATURATION, UI_MENU_COLOR_SATURATION_DESC, UI_MENU_COLOR_GAMMA, UI_MENU_COLOR_GAMMA_DESC,
|
||||
UI_MENU_HDR_GROUP, // group heading over the HDR rows
|
||||
UI_MENU_HDR, UI_MENU_HDR_DESC, UI_MENU_HDR_PEAK, UI_MENU_HDR_PEAK_DESC, UI_MENU_HDR_GAME, UI_MENU_HDR_GAME_DESC,
|
||||
UI_MENU_HDR_UI, UI_MENU_HDR_UI_DESC,
|
||||
UI_MENU_HDR_UI, UI_MENU_HDR_UI_DESC, UI_MENU_HDR_HUE, UI_MENU_HDR_HUE_DESC,
|
||||
UI_MENU_SHADOW_SCALE, UI_MENU_SHADOW_SCALE_DESC,
|
||||
UI_MENU_SHADOW_RANGE, UI_MENU_SHADOW_RANGE_DESC,
|
||||
UI_MENU_CONTACT_SHADOWS, UI_MENU_CONTACT_SHADOWS_DESC,
|
||||
UI_MENU_PCSS, UI_MENU_PCSS_DESC,
|
||||
UI_MENU_DLAA, UI_MENU_DLAA_DESC, UI_MENU_DLSS, UI_MENU_DLSS_DESC, UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY,
|
||||
UI_MENU_DLSS_BALANCED, UI_MENU_DLSS_PERFORMANCE, UI_MENU_FXAA, UI_MENU_FXAA_DESC,
|
||||
UI_MENU_DLSS_BALANCED, UI_MENU_DLSS_PERFORMANCE, UI_MENU_DLSS_ULTRA, UI_MENU_DLSS_PRESET, UI_MENU_DLSS_PRESET_DESC,
|
||||
UI_MENU_DLSS_PRESET_DEFAULT, UI_MENU_DLSS_PRESET_K, UI_MENU_DLSS_PRESET_J, UI_MENU_DLSS_CHARS, UI_MENU_DLSS_CHARS_DESC,
|
||||
UI_MENU_DLSS_CHARS_OFF, UI_MENU_DLSS_CHARS_LIGHT, UI_MENU_DLSS_CHARS_MEDIUM, UI_MENU_DLSS_CHARS_FULL, UI_MENU_FXAA, UI_MENU_FXAA_DESC,
|
||||
|
||||
// Game
|
||||
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90,
|
||||
UI_MENU_FPS_120, UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_DISPLAY, UI_MENU_FPS_DISPLAY_HZ,
|
||||
UI_MENU_FRAME_QUEUE, UI_MENU_FRAME_QUEUE_DESC,
|
||||
UI_MENU_REALTIME, UI_MENU_REALTIME_DESC,
|
||||
UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC,
|
||||
UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC,
|
||||
@@ -88,6 +92,7 @@ typedef enum {
|
||||
|
||||
// Sound
|
||||
UI_MENU_VOLUME, UI_MENU_VOLUME_DESC,
|
||||
UI_MENU_AUDIO_CHANNELS, UI_MENU_AUDIO_CHANNELS_DESC, UI_MENU_CHANNELS_AUTO, UI_MENU_CHANNELS_STEREO, UI_MENU_CHANNELS_51,
|
||||
|
||||
// Controls
|
||||
UI_MENU_MOTION, UI_MENU_MOTION_DESC,
|
||||
@@ -96,7 +101,7 @@ typedef enum {
|
||||
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_PROMPTS, UI_MENU_PROMPTS_DESC, UI_MENU_PROMPTS_NINTENDO, UI_MENU_PROMPTS_XBOX,
|
||||
UI_MENU_PROMPTS, UI_MENU_PROMPTS_DESC, UI_MENU_PROMPTS_NINTENDO, UI_MENU_PROMPTS_XBOX, UI_MENU_PROMPTS_PC,
|
||||
// the notes under the prompts: their files are missing or damaged, they change after restart, or why they are off
|
||||
UI_MENU_PROMPTS_MISSING, UI_MENU_PROMPTS_FAILED, UI_MENU_PROMPTS_RESTART, UI_MENU_PROMPTS_OFF_SHAPE,
|
||||
UI_MENU_PROMPTS_OFF_MODS, UI_MENU_PROMPTS_OFF_PATCHES,
|
||||
@@ -104,6 +109,8 @@ typedef enum {
|
||||
UI_MENU_RESTART, UI_MENU_RESTART_DESC, UI_MENU_RESTART_CONFIRM, UI_MENU_RESTART_YES, UI_MENU_RESTART_SAVING,
|
||||
UI_MENU_GROUP_STICKS, UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
|
||||
UI_MENU_STICK_DEADZONE_NOW, UI_MENU_STICK_SENS, UI_MENU_STICK_SENS_DESC,
|
||||
UI_MENU_MOUSE_MODE, UI_MENU_MOUSE_MODE_DESC, UI_MENU_MOUSE_STICK, UI_MENU_MOUSE_DIRECT,
|
||||
UI_MENU_MENU_CURSOR, UI_MENU_MENU_CURSOR_DESC,
|
||||
UI_MENU_MOUSE_SENS, UI_MENU_MOUSE_SENS_DESC,
|
||||
UI_MENU_WALK_KEY, UI_MENU_WALK_KEY_DESC, UI_MENU_WALK_KEY_TAKEN, UI_MENU_WALK_SCALE, UI_MENU_WALK_SCALE_DESC,
|
||||
|
||||
@@ -116,15 +123,15 @@ typedef enum {
|
||||
UI_BINDS_TAKEN, // "%s was taken from %s": the key, the action that lost it
|
||||
UI_BINDS_SWAPPED, // "%s swapped with %s": the two actions
|
||||
UI_BINDS_RESET, UI_BINDS_RESET_DESC,
|
||||
// the actions, in action order (input_map.h B_A .. B_SR_DOWN)
|
||||
// the actions, in action order (input_map.h B_A .. B_SHAKE)
|
||||
UI_BIND_A, UI_BIND_B, UI_BIND_X, UI_BIND_Y, UI_BIND_LCLICK, UI_BIND_RCLICK, UI_BIND_L, UI_BIND_R, UI_BIND_ZL,
|
||||
UI_BIND_ZR, UI_BIND_PLUS, UI_BIND_MINUS, UI_BIND_LEFT, UI_BIND_UP, UI_BIND_RIGHT, UI_BIND_DOWN,
|
||||
UI_BIND_LSTICK_LEFT, UI_BIND_LSTICK_UP, UI_BIND_LSTICK_RIGHT, UI_BIND_LSTICK_DOWN,
|
||||
UI_BIND_RSTICK_LEFT, UI_BIND_RSTICK_UP, UI_BIND_RSTICK_RIGHT, UI_BIND_RSTICK_DOWN,
|
||||
UI_BIND_RSTICK_LEFT, UI_BIND_RSTICK_UP, UI_BIND_RSTICK_RIGHT, UI_BIND_RSTICK_DOWN, UI_BIND_SHAKE,
|
||||
// what an action does in the world, in action order up to Minus
|
||||
UI_BIND_DESC_A, UI_BIND_DESC_B, UI_BIND_DESC_X, UI_BIND_DESC_Y, UI_BIND_DESC_LCLICK, UI_BIND_DESC_RCLICK,
|
||||
UI_BIND_DESC_L, UI_BIND_DESC_R, UI_BIND_DESC_ZL, UI_BIND_DESC_ZR, UI_BIND_DESC_PLUS, UI_BIND_DESC_MINUS,
|
||||
UI_BIND_DESC_DPAD, UI_BIND_DESC_MOVE, UI_BIND_DESC_CAMERA,
|
||||
UI_BIND_DESC_DPAD, UI_BIND_DESC_MOVE, UI_BIND_DESC_CAMERA, UI_BIND_DESC_SHAKE,
|
||||
// key names, in key id order KEY_SPACE .. KEY_PAGEDOWN (letters, digits, F keys and punctuation show as themselves)
|
||||
UI_KEY_SPACE, UI_KEY_TAB, UI_KEY_LSHIFT, UI_KEY_LCTRL, UI_KEY_UP, UI_KEY_DOWN, UI_KEY_LEFT, UI_KEY_RIGHT,
|
||||
UI_KEY_MOUSE_L, UI_KEY_MOUSE_R, UI_KEY_MOUSE_M, UI_KEY_MOUSE_X1, UI_KEY_MOUSE_X2,
|
||||
@@ -150,11 +157,13 @@ typedef enum {
|
||||
|
||||
// Menu tab
|
||||
UI_MENU_MENU_LANG, UI_MENU_MENU_LANG_DESC, UI_MENU_MENU_LANG_AUTO,
|
||||
UI_MENU_DISCORD_RPC, UI_MENU_DISCORD_RPC_DESC,
|
||||
|
||||
// Language names, each in its own language and the same in every row (game language and menu language lists)
|
||||
UI_MENU_LANGNAME_EN, UI_MENU_LANGNAME_JA, UI_MENU_LANGNAME_FR, UI_MENU_LANGNAME_DE, UI_MENU_LANGNAME_IT,
|
||||
UI_MENU_LANGNAME_ES, UI_MENU_LANGNAME_NL, UI_MENU_LANGNAME_RU, UI_MENU_LANGNAME_ZH_HANS,
|
||||
UI_MENU_LANGNAME_ZH_HANT, UI_MENU_LANGNAME_KO, UI_MENU_LANGNAME_PT_BR,
|
||||
UI_MENU_LANGNAME_ZH_HANT, UI_MENU_LANGNAME_KO, UI_MENU_LANGNAME_PT_BR, UI_MENU_LANGNAME_EN_GB,
|
||||
UI_MENU_LANGNAME_FR_CA, UI_MENU_LANGNAME_ES_419,
|
||||
|
||||
// Advanced
|
||||
UI_MENU_ADV_WARNING,
|
||||
@@ -203,6 +212,8 @@ typedef enum {
|
||||
|
||||
// Performance overlay
|
||||
UI_MENU_OV_FPS, UI_MENU_OV_FRAME_MS, UI_MENU_OV_GAME_PCT, UI_MENU_OV_MAX_MS,
|
||||
UI_MENU_OV_FPS_LABEL, UI_MENU_OV_FRAME_LABEL, UI_MENU_OV_FMT_FPS, // the frame rows: their labels, the rate
|
||||
UI_MENU_OV_LOW1, UI_MENU_OV_LOW01, // printf: the fps of the slowest 1% / 0.1% of the frames, no unit (the overlay column stays narrow)
|
||||
UI_MENU_OV_GPU, UI_MENU_OV_CPU, UI_MENU_OV_RAM, UI_MENU_OV_VRAM, UI_MENU_OV_TEMP, UI_MENU_OV_POWER,
|
||||
UI_MENU_OV_CLOCK, UI_MENU_OV_NONE,
|
||||
UI_MENU_OV_REPLAY, UI_MENU_OV_GPU_TIME, UI_MENU_OV_PIPE_WAIT, UI_MENU_OV_SHADER_WAIT, UI_MENU_OV_TEX_UPLOADS,
|
||||
@@ -246,6 +257,12 @@ int ui_text_defined(const char *lang, UiTextId id);
|
||||
// The catalog language for a Switch language code ("en-GB" -> "en", "zh-CN" -> "zh-Hans", ...).
|
||||
const char *ui_text_lang(const char *switch_code);
|
||||
|
||||
// The game languages the F1 menu offers for a game build (rt_game_version): one entry per Mals language file the build
|
||||
// ships, each with the Switch code TOTK_LANG takes and the id of its name. 1.4.3 adds Brazilian Portuguese (USpt).
|
||||
// Fills out (room for max entries) and returns the count.
|
||||
typedef struct { const char *code; UiTextId name; } UiGameLang;
|
||||
int ui_text_game_langs(int game_version, UiGameLang *out, int max);
|
||||
|
||||
// Menu text: ui_text_in(menu language, id). The menu language is TOTK_MENU_LANG ("en", "ru", "pt-BR", "zh-Hans");
|
||||
// unset, the system UI language when the catalog has menu text for it (English, Russian, Brazilian Portuguese,
|
||||
// Simplified Chinese), else English. Follows a live change of the setting.
|
||||
|
||||
+19
-1
@@ -57,9 +57,10 @@ typedef struct {
|
||||
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")
|
||||
int mesa; // a Mesa driver the game runs on: RADV, ANV or NVK
|
||||
} VkCaps;
|
||||
extern VkCaps vk_caps;
|
||||
void vk_caps_driver(VkPhysicalDevice p); // first, before vkCreateDevice: vk_caps.amd, amd_sc
|
||||
void vk_caps_driver(VkPhysicalDevice p); // first, before vkCreateDevice: vk_caps.amd, amd_sc, mesa
|
||||
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.
|
||||
@@ -250,14 +251,28 @@ void vk_mem_pick(VkMemSpan *sp);
|
||||
#define VK_MEM_USE(sp) do { if (vk_mirror_on && (sp).pool) vk_mem_use(&(sp)); } while (0)
|
||||
#define VK_MEM_PICK(sp) do { if (vk_mirror_on && (sp).pool) vk_mem_pick(&(sp)); } while (0)
|
||||
void vk_mem_gpu_wrote(const VkMemSpan *sp, uint64_t size);
|
||||
// A shader's storage-buffer write: goes back through the write-back merge (vk_memory.c), which keeps the words the GPU
|
||||
// did not change.
|
||||
void vk_mem_gpu_wrote_shader(const VkMemSpan *sp, uint64_t size);
|
||||
// Uniform blocks as of the submit call (vk_usnap.c): vk_exec_record copies the uniform ranges a submission binds before
|
||||
// it records, ubo bindings take the copy (0: not copied, bind memory), GPU writes of the replay drop the copies they touch.
|
||||
void vk_usnap_begin(const uint64_t *handles, int n, uint64_t fence);
|
||||
void vk_usnap_end(uint64_t fence);
|
||||
int vk_usnap_find(uint64_t addr, uint64_t n, uint64_t align, VkDescriptorBufferInfo *out);
|
||||
const uint8_t *vk_usnap_map(void); // a host copy of the current arena, at the offsets vk_usnap_find gives
|
||||
const uint8_t *vk_usnap_bytes(uint64_t addr, uint64_t n); // the copy of [addr, addr+n) as of the submit, NULL: none
|
||||
void vk_usnap_drop(uint64_t addr, uint64_t n); // the replay makes the GPU write [addr, addr+n) of guest memory
|
||||
uint64_t vk_usnap_gen(void); // changes whenever a binding may resolve differently
|
||||
void vk_usnap_shutdown(void); // vk_exec_draw_shutdown: frees the arenas
|
||||
// vk_mvprev.c: last frame's copy of the scene's object (c7) and bone (c5) blocks, for per-object motion vectors.
|
||||
// vk_mvprev_step: the distance of the game's other copy of a vertex-stage block the snapshot also copies, 0: none.
|
||||
// vk_mvprev_find: last frame's block of that object (prev, its address; how, MVPREV_*), 0: none.
|
||||
enum { MVPREV_NONE, MVPREV_SELF, MVPREV_SIDE, MVPREV_NEARER };
|
||||
uint64_t vk_mvprev_step(int stage, int index, uint64_t size);
|
||||
int vk_mvprev_on(void);
|
||||
void vk_mvprev_want(uint32_t index_mask); // also keep these binding indices' last-frame copies (bit n: index n) // DLAA on and TOTK_DLAA_OBJMV not 0, as of the last present
|
||||
int vk_mvprev_find(int index, uint64_t addr, uint64_t size, uint64_t *prev, int *how);
|
||||
void vk_mvprev_frame(void); // each present, beside vk_dlaa_frame
|
||||
void vk_mem_mirror_begin(void);
|
||||
int vk_mem_mirror_end(VkCommandBuffer cb, uint64_t fence);
|
||||
int vk_mem_mirror_pending(void);
|
||||
@@ -268,6 +283,8 @@ void vk_mem_mirror_prologue(VkCommandBuffer cb);
|
||||
void vk_mem_shadow_sync(void);
|
||||
void vk_mem_gpu_pending(uint64_t addr, uint64_t size);
|
||||
void vk_gpu_written(uint64_t addr, uint64_t size);
|
||||
int vk_mem_flush_add_probe(void); // --probe-vkexec: the flush range list; 0 or the number of the failed check
|
||||
uint64_t vk_mem_mirror_uploaded(void); // --probe-vkexec: bytes copied into mirrors since the last summary line
|
||||
int vk_mem_mirror_probe(uint64_t addr); // --probe-vkexec: the pool at addr gets its mirror as on a first buffer use;
|
||||
// 1 when it has one
|
||||
void vk_mem_class_log(const VkPhysicalDeviceProperties *p, const VkPhysicalDeviceMemoryProperties *m); // once
|
||||
@@ -390,6 +407,7 @@ uint32_t vk_texture_slots_gen(void);
|
||||
// its storage address is created, dropped, released or taken back into use. Unlocked; read it before the lookup it
|
||||
// guards (a lookup may create an entry). 0 when the slot was never registered: nothing is kept under 0.
|
||||
uint64_t vk_texture_slot_token(int id);
|
||||
void vk_sampler_dlss_bias(float bias); // texture LOD bias added for DLSS upscaling (material samplers)
|
||||
uint32_t vk_sampler_gen(void); // changes whenever a sampler slot is registered
|
||||
void vk_texture_gpu_written(const tex_t *t); // rendered or copied into by the GPU: no re-upload from storage
|
||||
void vk_texture_storage_bound_locked(const VkTexImage *img); // bound as a storage image in the recording submission
|
||||
|
||||
+12
-1
@@ -298,9 +298,20 @@ static int create_pipeline(VkCtx *c) {
|
||||
}
|
||||
|
||||
// TOTK_ASTC_BC=1: the BC3 encoder, when the device samples BC3 (every desktop GPU does); 0 leaves ASTC as RGBA8.
|
||||
// Unset: on when the largest device-local heap is under 5 GiB. There RGBA8 ASTC, four times the size of BC3, fills
|
||||
// video memory (a GTX 1650 4 GiB went from 10-23 to 3-12 ms of GPU time a frame, issue #87); BC3 loses some precision
|
||||
// (normal maps), so GPUs with more memory keep RGBA8.
|
||||
static int create_bc_pipeline(VkCtx *c) {
|
||||
const char *e = cfg_get("TOTK_ASTC_BC");
|
||||
if (!e || !*e || *e == '0') return 0;
|
||||
VkDeviceSize vram = 0;
|
||||
for (uint32_t h = 0; h < c->mem_props.memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
|
||||
if (c->mem_props.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT && c->mem_props.memoryHeaps[h].size > vram)
|
||||
vram = c->mem_props.memoryHeaps[h].size;
|
||||
int on = e && *e ? *e != '0' : vram < (5ull << 30);
|
||||
if (!(e && *e))
|
||||
rt_log("vk: TOTK_ASTC_BC unset: BC3 %s (largest device-local heap %llu MiB)", on ? "on" : "off",
|
||||
(unsigned long long)(vram >> 20));
|
||||
if (!on) 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;
|
||||
|
||||
@@ -102,6 +102,8 @@ static void present_image(const tex_t *t, const int crop[4], uint64_t at_ns, uin
|
||||
static volatile int32_t n_skipped;
|
||||
vk_exec_frame_log(t, frame);
|
||||
vk_dlaa_frame();
|
||||
vk_mvsurvey_frame();
|
||||
vk_mvprev_frame();
|
||||
vk_mem_census(frame);
|
||||
if (vk_dump_armed) vk_dump_present(frame);
|
||||
__atomic_store_n(&gfx_counters.validation_errors, (uint64_t)vk_validation_errors_total(), __ATOMIC_RELAXED);
|
||||
@@ -166,7 +168,11 @@ static uint64_t ck_reads, ck_diff, ck_diff_unexplained, ck_diff_idle;
|
||||
int vk_replay_async(void) {
|
||||
static int on = -1;
|
||||
if (on < 0) {
|
||||
on = cfg_bool("TOTK_REPLAY_ASYNC"); // off by default: no measured gain yet, one more frame of latency
|
||||
// Off by default. Where the game waits for the GPU it overlaps the replay with the game threads (a GTX 1650 in
|
||||
// the Depths went from 450-686 frames over 50 ms a minute to 10-13, issue #88), the only place a gain was
|
||||
// measured. While it was the default, a GTX 1660 Ti at the 60 fps cap with the GPU at 26 % reported uneven
|
||||
// motion that 0.3.4 did not have (#659). It adds up to a frame of latency.
|
||||
on = cfg_bool("TOTK_REPLAY_ASYNC");
|
||||
rt_log("vk: replay %s (TOTK_REPLAY_ASYNC=%d)",
|
||||
on ? "on its own thread, at most one frame behind the game" : "on the submitting thread", on);
|
||||
}
|
||||
|
||||
+16
-11
@@ -6,8 +6,7 @@
|
||||
// - 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; the default on AMD's RDNA 1/2 driver):
|
||||
// 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): 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.
|
||||
@@ -44,17 +43,22 @@ void vk_caps_driver(VkPhysicalDevice p) {
|
||||
// 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");
|
||||
vk_caps.mesa = drv.driverID == VK_DRIVER_ID_MESA_RADV || drv.driverID == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA ||
|
||||
drv.driverID == VK_DRIVER_ID_MESA_NVK;
|
||||
}
|
||||
|
||||
// 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.
|
||||
// TOTK_GPL=0 turns graphics pipeline libraries off; they are on wherever the driver has fast linking. AMD's RDNA 1/2
|
||||
// driver branch had them off for a while: its device losses at boot (issue #5) came with a cold pipeline cache, but
|
||||
// their cause was GPU writes past 3D BC images (vk_exec.c, "3D BC render targets"). With that fixed, the RDNA 1/2
|
||||
// driver boots with GPL from a cold cache and fast-links without a failure, while with GPL off every new pipeline is
|
||||
// built whole on the draw's path (frames of 0.5-1.2 s on an RX 6600, issue #381). 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;
|
||||
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);
|
||||
const char *e = cfg_get("TOTK_GPL");
|
||||
vk_caps.gpl = gpl_lib && gpl_fast && !(e && !strcmp(e, "0"));
|
||||
return vk_caps.gpl;
|
||||
}
|
||||
|
||||
@@ -112,7 +116,10 @@ void vk_caps_init(VkCtx *c) {
|
||||
// BotW and TotK on AMD). The translator biases gather coordinates by the host's sub-texel precision; NVIDIA rounds
|
||||
// as Maxwell does. TOTK_GATHER_BIAS=<bits> forces it (0: off).
|
||||
uint32_t gather = env_u32("TOTK_GATHER_BIAS", c->props.vendorID == 0x10de ? 0 : l->subTexelPrecisionBits);
|
||||
gfx_shader_set_host(vk_caps.float64, vk_caps.sbuf_align, told, gather);
|
||||
// A register read before any write is an undefined load in SPIR-V, and Mesa folds undefined values freely, so on
|
||||
// Mesa function-local variables start at 0 (docs/maxwell.md 3.12). TOTK_ZERO_LOCALS=1 forces it, 0 turns it off.
|
||||
uint32_t zero_locals = env_u32("TOTK_ZERO_LOCALS", (uint32_t)vk_caps.mesa);
|
||||
gfx_shader_set_host(vk_caps.float64, vk_caps.sbuf_align, told, gather, zero_locals != 0);
|
||||
if (gather) rt_log("vk caps: gather coordinates biased for %u sub-texel bits (TOTK_GATHER_BIAS)", gather);
|
||||
|
||||
char dv[32];
|
||||
@@ -137,10 +144,8 @@ 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 ? "" : ge && *ge ? ", off by TOTK_GPL=0" : ", off on AMD's RDNA 1/2 driver");
|
||||
gpl_fast ? "yes" : "no", (unsigned)gpl_lib, !gpl_lib || !gpl_fast || vk_caps.gpl ? "" : ", off by TOTK_GPL=0");
|
||||
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]);
|
||||
|
||||
+648
-90
File diff suppressed because it is too large.
Load diff
@@ -2,6 +2,10 @@
|
||||
#pragma once
|
||||
#include "vk.h"
|
||||
|
||||
// The narrowest scene the DLSS pass and the motion passes recognise by its D32F depth: Ultra Performance at a 1080p
|
||||
// output renders 640 wide, smaller windows less.
|
||||
#define DLAA_SCENE_MIN_W 320u
|
||||
|
||||
int vk_dlaa_wanted(void); // the setting is on (the device may still lack DLSS)
|
||||
// vk_device.c: NGX's instance and device extensions join the lists (room for max names); vk_dlaa_init after the
|
||||
// device exists brings NGX up and checks that DLSS is available.
|
||||
@@ -20,6 +24,25 @@ void vk_dlaa_frame(void);
|
||||
// the viewport of the scene's draws by the frame's jitter (vk_dlaa_viewport, when the viewport is recorded).
|
||||
void vk_dlaa_draw(VkExec *ex);
|
||||
void vk_dlaa_viewport(VkExec *ex, VkViewport *v);
|
||||
// vk_mvsurvey.c (TOTK_MV_SURVEY): notes the uniform blocks of each scene draw, beside vk_dlaa_draw; each present
|
||||
// compares them with the previous frames' and logs a summary now and then.
|
||||
void vk_mvsurvey_draw(VkExec *ex);
|
||||
void vk_mvsurvey_frame(void);
|
||||
void vk_mvprev_draw(VkExec *ex); // vk_mvprev.c: notes the scene draw's object and bone blocks
|
||||
// Per-object motion (vk_exec_draw.c): the moving scene draws are listed again during the replay with
|
||||
// their vertex shader's last-frame variant; vk_dlaa.c mv_pass records them over the camera motion into ex->cb (and
|
||||
// leaves every binding of the replay to be set again; draws whose camera block is not cam are left out).
|
||||
// vk_dlaa_prev_camera: last frame's camera block and this frame's jitter, 0 when there is no history.
|
||||
int vk_mvobj_execute(VkExec *ex, uint64_t cam, VkImageView mv, VkImageView depth, VkFormat depth_fmt, uint32_t w,
|
||||
uint32_t h);
|
||||
int vk_dlaa_prev_camera(VkDescriptorBufferInfo *out, float jitter[2]);
|
||||
// vk_mvcheck.c (TOTK_MV_CHECK, test): mv_pass hands it the motion image after the camera pass and, after the object
|
||||
// pass, the motion image again and the scene light target it ran on; it measures how well the vectors fit the picture.
|
||||
void vk_mvcheck_camera(VkCommandBuffer cb, VkImage mv, uint32_t w, uint32_t h, float jx, float jy);
|
||||
void vk_mvcheck_final(VkCommandBuffer cb, uint64_t fence, VkImage mv, VkImage cimg, VkFormat cfmt, uint32_t level,
|
||||
uint32_t layer, uint32_t w, uint32_t h, float jx, float jy);
|
||||
const char *vk_mvobj_frame_masks(void); // vk_exec_draw.c: the last motion list, per position and changed banks
|
||||
void vk_mvcheck_output(VkCommandBuffer cb, VkImage out, uint32_t ow, uint32_t oh); // after DLSS on a check frame
|
||||
// vk_exec_draw.c, once a draw's fragment shader is known: 1 skips the draw (the game's FXAA while TOTK_FXAA is 0, its
|
||||
// scaling draw when DLSS wrote the scaled picture).
|
||||
int vk_dlaa_skip_draw(VkExec *ex, const GfxShader *fs);
|
||||
+17
-3
@@ -491,18 +491,28 @@ static int tfr_recent(const VkExecTarget *t, uint64_t stamp) {
|
||||
return tfr_match(e, t) && (e->last == stamp ? e->prev : e->last) == stamp - 1;
|
||||
}
|
||||
|
||||
// No frame before the current one rendered the target (a slot another subresource took reads the same).
|
||||
static int tfr_first(const VkExecTarget *t, uint64_t stamp) {
|
||||
const TargetFrames *e = tfr_slot(t);
|
||||
return !tfr_match(e, t) || !(e->last == stamp ? e->prev : e->last);
|
||||
}
|
||||
|
||||
// With any, a recent depth target alone does not make the pass per-frame when a colour target is rendered for the
|
||||
// first time: the game's item icon capture (an inventory or Fuse icon) draws into a new colour target with the
|
||||
// per-frame depth buffer, once. Its draws skipped there stay missing from the icon until the item is captured again.
|
||||
int vk_exec_targets_recent(VkExec *ex, int any) {
|
||||
resolve_rt(ex);
|
||||
uint64_t stamp = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1;
|
||||
int n = 0, recent = 0;
|
||||
int n = 0, color = 0, depth = 0, color_first = 0;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
const VkExecTarget *t = i < 8 ? &ex->color[i] : &ex->depth;
|
||||
if (!t->image) continue;
|
||||
n++;
|
||||
if (tfr_recent(t, stamp)) recent++;
|
||||
if (tfr_recent(t, stamp)) *(i < 8 ? &color : &depth) = 1;
|
||||
else if (!any) return 0;
|
||||
else if (i < 8 && tfr_first(t, stamp)) color_first = 1;
|
||||
}
|
||||
return any ? recent > 0 : n > 0;
|
||||
return any ? color || (depth && !color_first) : n > 0;
|
||||
}
|
||||
|
||||
// One line naming each target of the current pass: format, extent, layer and the two frames it was last rendered in
|
||||
@@ -919,6 +929,10 @@ static void walk(VkExec *ex, const op_hdr *h, int depth) {
|
||||
int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t fence) {
|
||||
VkExec *ex = &X;
|
||||
if (!x_ready) state_reset(ex), vk_dump_init(), x_ready = 1;
|
||||
const tex_t *keep[9]; // the render targets carry over to this submission (set_rt compares them by pointer)
|
||||
memcpy(keep, ex->rt.color, sizeof ex->rt.color);
|
||||
keep[8] = ex->rt.depth;
|
||||
nvn_pins_sweep(nvn_submit_count(), keep, 9);
|
||||
ex->cb = cb;
|
||||
ex->fence = fence;
|
||||
ex->recorded = 0;
|
||||
|
||||
+8
-3
@@ -24,7 +24,7 @@ enum {
|
||||
|
||||
// One render target of OP_SET_RT, resolved to its vk_texture.c image for the current command buffer.
|
||||
typedef struct {
|
||||
const tex_t *tex; // payload copy (lives for the run); NULL: no target in this slot
|
||||
const tex_t *tex; // texture pin of ex->rt (nvn_pins_sweep keeps it); NULL: no target in this slot
|
||||
VkImage image; // VK_NULL_HANDLE: no target or not renderable (logged once)
|
||||
VkFormat format; // view format: the view's when it reinterprets a same-size format
|
||||
VkImageViewType view_type; // 2D, or 2D_ARRAY for more than one layer
|
||||
@@ -142,8 +142,9 @@ void vk_hdr_draw(VkExec *ex, const VkPipeInfo *info);
|
||||
// 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
|
||||
// 1: every target of the current pass (any 1: at least one) was rendered in the previous presented frame (per-frame
|
||||
// targets); 0 otherwise, or when the pass has none.
|
||||
// 1: every target of the current pass (any 1: at least one, and not only the depth target while a colour target is
|
||||
// rendered for the first time) was rendered in the previous presented frame (per-frame targets); 0 otherwise, or when
|
||||
// the pass has none.
|
||||
int vk_exec_targets_recent(VkExec *ex, int any);
|
||||
// The current pass's targets with their formats, extents and the frames they were last rendered in (diagnostics).
|
||||
void vk_exec_targets_describe(VkExec *ex, char *buf, size_t cap);
|
||||
@@ -188,6 +189,10 @@ VkImageView vk_exec_target_view(const VkTexImage *img, VkFormat format, VkImageV
|
||||
// vk_exec_xfer.c: OP_COPY_B2B, OP_COPY_B2T, OP_COPY_T2B, OP_COPY_T2T, OP_CLEAR_TEX, OP_SIGNAL_EVENT, OP_REPORT_COUNTER,
|
||||
// OP_RESET_COUNTER, OP_RENDER_ENABLE, OP_RENDER_COND, OP_CLEAR_BUF.
|
||||
void vk_exec_op_xfer(VkExec *ex, uint32_t op, const void *payload);
|
||||
// vk_dlaa.c: once DLSS has scaled the scene this frame, passes larger than the scene's light target vk_dlaa_swap_img
|
||||
// sample DLSS's output vk_dlaa_swap_view in its place, so the game's scaling draw scales DLSS's picture.
|
||||
extern const VkTexImage *vk_dlaa_swap_img;
|
||||
extern VkImageView vk_dlaa_swap_view;
|
||||
// vk_contact.c: a pass about to begin (its targets resolved, no pass open), and a dispatch or copy about to be recorded
|
||||
void vk_contact_pass_begin(VkExec *ex);
|
||||
void vk_contact_flush(VkExec *ex);
|
||||
|
||||
+795
-13
@@ -731,6 +731,8 @@ static void answers_for(VkExec *ex, int st, const uint8_t *ctl, uint32_t mask, G
|
||||
// *present: the program has this stage. Returns NULL when it has not or the translation failed. pending (graphics
|
||||
// stages, vk_pipeline_async_on): a shader not translated yet is queued and NULL returned with *pending = 1. await: a
|
||||
// shader queued earlier is waited for (gfx_shader_await) instead.
|
||||
static int sf_mvprev; // shader_for asks for the vertex shader's last-frame variant (per-object motion)
|
||||
|
||||
static const GfxShader *shader_for(VkExec *ex, int st, int *present, int *pending, int await) {
|
||||
const uint8_t *ctl, *code;
|
||||
uint32_t len;
|
||||
@@ -741,6 +743,7 @@ static const GfxShader *shader_for(VkExec *ex, int st, int *present, int *pendin
|
||||
GfxShaderAnswers a;
|
||||
uint64_t serial = ex->prog->serial;
|
||||
answers_for(ex, st, ctl, slot_mask_of(serial, st), &a);
|
||||
a.mvprev = (uint8_t)(sf_mvprev && st == NVN_STAGE_VS);
|
||||
ShaderEnt *set = NULL;
|
||||
if (serial) {
|
||||
uint64_t h = (serial * 0x9E3779B97F4A7C15ull ^ (uint64_t)st) ^ nvn_hm_hash(&a, sizeof a); // a is zero-padded
|
||||
@@ -758,7 +761,7 @@ static const GfxShader *shader_for(VkExec *ex, int st, int *present, int *pendin
|
||||
}
|
||||
int gs = st == NVN_STAGE_VS ? GFX_STAGE_VS : st == NVN_STAGE_FS ? GFX_STAGE_FS : GFX_STAGE_CS;
|
||||
const GfxShader *s = await ? gfx_shader_await(gs, ctl, code, len, &a)
|
||||
: pending && vk_pipeline_async_on() ? gfx_shader_get_async(gs, ctl, code, len, &a, pending)
|
||||
: pending && (vk_pipeline_async_on() || a.mvprev) ? gfx_shader_get_async(gs, ctl, code, len, &a, pending)
|
||||
: gfx_shader_get(gs, ctl, code, len, NULL, &a);
|
||||
if (pending && *pending) return NULL;
|
||||
if (s) slot_mask_learn(serial, st, s);
|
||||
@@ -807,7 +810,7 @@ static int bind_range(VkExec *ex, uint64_t addr, uint64_t size, uint64_t align,
|
||||
if (sp.offset % align == 0) {
|
||||
VK_MEM_USE(sp);
|
||||
*out = (VkDescriptorBufferInfo){sp.buffer, sp.offset, n};
|
||||
if (written) vk_mem_gpu_wrote(&sp, n), vk_usnap_drop(addr, n);
|
||||
if (written) vk_mem_gpu_wrote_shader(&sp, n), vk_usnap_drop(addr, n);
|
||||
count_use(sp.cls, use, n);
|
||||
VK_MEM_BOUND(sp);
|
||||
return 1;
|
||||
@@ -895,7 +898,14 @@ static void limits_init(void) {
|
||||
__atomic_store_n(&LIM.ubo_max, l->maxUniformBufferRange, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// Per-object motion (section at the end): while it records a draw again, these replace the uniform bindings of the
|
||||
// vertex stage's slots 18-20 (last frame's camera, bones and place) and the fragment stage's slot 3 (its parameters).
|
||||
static int mo_ovr_on;
|
||||
static VkDescriptorBufferInfo mo_ovr[2][32]; // [vertex, fragment][slot]; buffer 0: not replaced
|
||||
|
||||
static VkDescriptorBufferInfo ubo_info(VkExec *ex, int st, int slot) {
|
||||
if (mo_ovr_on && (st == NVN_STAGE_VS || st == NVN_STAGE_FS) && slot >= 0 && slot < 32 && mo_ovr[st][slot].buffer)
|
||||
return mo_ovr[st][slot];
|
||||
if (!__atomic_load_n(&LIM.ubo_max, __ATOMIC_ACQUIRE)) limits_init();
|
||||
uint64_t max = LIM.ubo_max, align = LIM.ubo_align;
|
||||
VkDescriptorBufferInfo bi;
|
||||
@@ -1398,9 +1408,13 @@ static Twin *twin_update(VkExec *ex, const VkTexImage *img) {
|
||||
}
|
||||
|
||||
// The image descriptor of slot h; in a dispatch, a compare-sampled colour image the driver cannot compare is swapped
|
||||
// for its depth twin.
|
||||
// for its depth twin; in a pass larger than it, the scene's light target, once DLSS scaled it this frame, for DLSS's
|
||||
// output (vk_dlaa.c).
|
||||
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 (h && !storage && vk_dlaa_swap_img && ex->in_pass && ex->pass_w > vk_dlaa_swap_img->width + 1 &&
|
||||
vk_texture_by_id_locked((int)(h & 0xfffff), NULL, NULL) == vk_dlaa_swap_img)
|
||||
out->imageView = vk_dlaa_swap_view, out->imageLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
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;
|
||||
@@ -1912,18 +1926,23 @@ static void bind_pipeline(VkExec *ex, VkPipelineBindPoint bp, VkPipeline p) {
|
||||
if (!k) B.st_valid = 0;
|
||||
}
|
||||
|
||||
static VkViewport draw_viewport(VkExec *ex) {
|
||||
float x = 0, y = 0, w = (float)ex->pass_w, h = (float)ex->pass_h;
|
||||
if (ex->has_vp) {
|
||||
float k = ex->pass_scale > 1 ? (float)ex->pass_scale : 1.0f;
|
||||
x = ex->vp[0][0] * k, y = ex->vp[0][1] * k, w = ex->vp[0][2] * k, h = ex->vp[0][3] * k;
|
||||
}
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
VkViewport v = {x, y + h, w, -h, ex->depth_range[0], ex->depth_range[1]}; // the y flip: negative height
|
||||
vk_dlaa_viewport(ex, &v); // the scene's draws move by the DLAA jitter
|
||||
return v;
|
||||
}
|
||||
|
||||
static void record_fixed(VkExec *ex) {
|
||||
VkCommandBuffer cb = ex->cb;
|
||||
if (ex->dirty & VK_EXEC_DIRTY_VIEWPORT) {
|
||||
float x = 0, y = 0, w = (float)ex->pass_w, h = (float)ex->pass_h;
|
||||
if (ex->has_vp) {
|
||||
float k = ex->pass_scale > 1 ? (float)ex->pass_scale : 1.0f;
|
||||
x = ex->vp[0][0] * k, y = ex->vp[0][1] * k, w = ex->vp[0][2] * k, h = ex->vp[0][3] * k;
|
||||
}
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
VkViewport v = {x, y + h, w, -h, ex->depth_range[0], ex->depth_range[1]}; // the y flip: negative height
|
||||
vk_dlaa_viewport(ex, &v); // the scene's draws move by the DLAA jitter
|
||||
VkViewport v = draw_viewport(ex);
|
||||
vkCmdSetViewportWithCount(cb, 1, &v);
|
||||
ex->recorded++;
|
||||
}
|
||||
@@ -1998,7 +2017,8 @@ static void bind_vertex_buffers(VkExec *ex) {
|
||||
// that was not counts as one-shot and waits.
|
||||
// any (1): also draws in a pass with at least one target rendered in the previous frame (a per-frame depth
|
||||
// buffer or g-buffer with a colour target the frame before did not use: half-resolution effect buffers in fights).
|
||||
// Passes with none (volumes, LUTs, shadow layers seen for the first time) still wait.
|
||||
// Passes with none (volumes, LUTs, shadow layers seen for the first time) still wait, and so do passes whose only
|
||||
// recent target is the depth buffer while a colour target is rendered for the first time (item icon captures).
|
||||
// fullscreen (2, default): as any, and full-screen draws into such passes are skipped instead of waiting from their
|
||||
// budget. A full-screen pass whose stage libraries are cold in the driver cache took 86 ms on average (up to 194 ms)
|
||||
// in a fight on an RX 9070: three frames in a row each spent the 50 ms budget, and some of the draws were skipped
|
||||
@@ -2185,6 +2205,8 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
|
||||
if (ex->mark && vk_exec_frame.draws == ex->mark_draws0) ex->mark->first[0] = vs, ex->mark->first[1] = fs;
|
||||
}
|
||||
vk_dlaa_draw(ex);
|
||||
vk_mvsurvey_draw(ex);
|
||||
vk_mvprev_draw(ex);
|
||||
record_fixed(ex);
|
||||
repeat_stats(ex, info);
|
||||
bind_resources(ex, info);
|
||||
@@ -2197,6 +2219,762 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- per-object motion
|
||||
// The camera pass (vk_dlaa.c mv_pass) gives everything the camera's motion, so a character that moves with the camera,
|
||||
// or on its own, gets the wrong one and DLSS throws its history away there. Each solid scene draw whose position reads
|
||||
// an object's own uniform blocks (bones c5, place c7, c6; mvobj_draw_ says which are left out and why) is therefore
|
||||
// drawn a second time (listed during the replay, MQ, and recorded by mv_pass) with its vertex shader's last-frame
|
||||
// variant (translator patch 0015: the position again from last frame's blocks, bound at slot bank + 15, and last
|
||||
// frame's camera, written to a spare output) and the fragment shader of vk_mvobj_fs.h, which writes last frame's window
|
||||
// position minus this frame's. The draw keeps everything else of the original: vertex and index buffers, the other
|
||||
// uniform blocks, the jittered viewport and the scene depth, tested with the game's own test and its equal case
|
||||
// (LEQUAL, GEQUAL) without writes, so only the visible surface writes its vector. Last frame's blocks:
|
||||
// vk_mvprev_find and mo_prev_block; the camera: vk_dlaa_prev_camera. A draw whose variant or pipeline is still
|
||||
// building, or whose last-frame blocks are not in the snapshot, keeps the camera's motion. TOTK_DLAA_OBJMV=0 turns it
|
||||
// off (vk_mvprev.c).
|
||||
#include "vk_mvobj_fs.h"
|
||||
static const char MO_FS_RES[] = "{\"key\":\"mvobj\",\"stage\":\"ps\",\"notes\":[],\"cbufs\":[{\"binding\":1,\"slot\":3}],"
|
||||
"\"sbufs\":[],\"textures\":[],\"images\":[]}";
|
||||
static struct {
|
||||
const GfxShader *fs[16]; // per input Location
|
||||
int failed;
|
||||
uint64_t cand, still, draws, nodata, building, ran, dropped;
|
||||
uint64_t ns_check, ns_rec, n_rec, ns_emit, ns_still;
|
||||
uint64_t ns_part[3], n_part[3], misses, cyc[3], ncyc, big; // checks: filter, data and stillness (memo misses), variant and pipeline // time spent: checks and lookups of every candidate; recording the draws again // cand: draws whose vertex shader reads c5 or c7
|
||||
uint64_t why[5]; // nodata by cause: camera, bones, place, bones copy, place copy
|
||||
uint64_t no_out, storage, no_fs; // variant without the extra output, with storage resources; no fs
|
||||
uint64_t other_cam; // listed draws left out: their camera block is not the scene camera
|
||||
uint64_t lk[14][5]; // last-frame lookups per binding index: none, self, side, nearer, refused (another object)
|
||||
uint64_t e_draws, e_pipe, e_dyn, e_push, e_pushw, e_sets, e_vb, e_setw, e_setreuse; // emitted commands (summary)
|
||||
} MO;
|
||||
|
||||
// The motion draws of the frame, kept as a list while the scene replays and recorded into the replay's own command
|
||||
// buffer by vk_mvobj_execute, through the recorder thread (vk_rec.h) like every other draw: recording them straight
|
||||
// into a secondary command buffer made the driver's work for each one on the replay thread. What a draw shares with
|
||||
// the one before it (pipeline, dynamic state, viewport, buffers, descriptors) is recorded once; a push descriptor set
|
||||
// is pushed again only where a binding changed (pushes update the set in place while its layout stays bound).
|
||||
typedef struct {
|
||||
VkPipeline pipe;
|
||||
const VkPipeInfo *info;
|
||||
uint32_t st, desc, vb; // indices into MQ.st, MQ.desc (the draw's packed descriptors) and MQ.vb
|
||||
VkDescriptorSet set[2]; // allocated sets; VK_NULL_HANDLE for a pushed one
|
||||
VkViewport v;
|
||||
VkRect2D sc;
|
||||
float bias[3];
|
||||
VkBuffer ib;
|
||||
VkDeviceSize ib_off;
|
||||
VkIndexType it;
|
||||
uint32_t count, instances, base_instance;
|
||||
int32_t base_vertex;
|
||||
uint64_t cam; // the draw's camera block (c4), this frame's
|
||||
} MoDraw;
|
||||
typedef struct { VkBuffer b[17]; VkDeviceSize o[17], sz[17], str[17]; } MoVb;
|
||||
static struct {
|
||||
uint64_t frame, fence; // the frame and submission the list belongs to
|
||||
VkFormat depth_fmt;
|
||||
MoDraw *d;
|
||||
VkPipeState *st;
|
||||
VkDescSlot *desc;
|
||||
MoVb *vb;
|
||||
int n, cap, nst, cst, nvb, cvb;
|
||||
uint32_t ndesc, cdesc;
|
||||
NVNbufferRange vin[17]; // the vertex ranges MQ.vb[nvb - 1] was resolved from, under vk_mem_gen vgen
|
||||
uint64_t vgen;
|
||||
float sup_in[3]; // the support buffer last filled for the list and its inputs
|
||||
VkDescriptorBufferInfo sup;
|
||||
float par_in[4]; // the fragment shader's parameters last written and their inputs
|
||||
VkDescriptorBufferInfo par;
|
||||
VkDescriptorSetLayout aset_layout[2]; // the last allocated set per set number, with its descriptors
|
||||
VkDescriptorSet aset[2];
|
||||
uint32_t aset_n[2];
|
||||
VkDescSlot aset_d[2][MAX_DESC];
|
||||
} MQ;
|
||||
|
||||
static void *mo_grow(void *p, int *cap, int need, size_t elem) {
|
||||
if (need <= *cap) return p;
|
||||
int c = *cap ? *cap : 256;
|
||||
while (c < need) c *= 2;
|
||||
if (!(p = realloc(p, elem * (size_t)c))) rt_fatal("vk_exec: out of memory");
|
||||
*cap = c;
|
||||
return p;
|
||||
}
|
||||
|
||||
// The position bank masks of the draws considered (gfx_shader_mvprev_banks), with counts, for the summary.
|
||||
static struct { uint32_t mask; uint64_t n; int ubo, ssbo, tex; } MM[32]; // ubo, ssbo, tex: the first vertex shader's
|
||||
static void mo_mask_note(uint32_t banks, const GfxShader *vs) {
|
||||
for (int i = 0; i < 32; i++)
|
||||
if (MM[i].mask == banks || !MM[i].n) {
|
||||
if (!MM[i].n) {
|
||||
const GfxShaderRes *r;
|
||||
MM[i].ubo = gfx_shader_resources(vs, GFX_RES_UBO, &r), MM[i].ssbo = gfx_shader_resources(vs, GFX_RES_SSBO, &r);
|
||||
MM[i].tex = gfx_shader_resources(vs, GFX_RES_TEX, &r);
|
||||
}
|
||||
MM[i].mask = banks, MM[i].n++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void mo_drop(void) {
|
||||
if (MQ.n) MO.dropped++;
|
||||
MQ.n = MQ.nst = MQ.nvb = 0, MQ.ndesc = 0, MQ.frame = 0;
|
||||
}
|
||||
|
||||
static uint64_t mo_gen = 1; // the list's snapshot (mo_prev_cached), bumped by mo_begin
|
||||
static struct { uint32_t mask, chg, n; } MF[16]; // this list's draws per position banks and changed banks
|
||||
static char fm_last[400]; // the last executed list's, formatted (vk_mvobj_frame_masks)
|
||||
|
||||
// Starts this frame's list (the old one, never executed, is dropped).
|
||||
static void mo_begin(VkExec *ex, uint64_t frame) {
|
||||
mo_drop();
|
||||
mo_gen++;
|
||||
memset(MF, 0, sizeof MF);
|
||||
MQ.frame = frame, MQ.fence = ex->fence, MQ.depth_fmt = ex->depth.format;
|
||||
memset(MQ.vin, 0, sizeof MQ.vin);
|
||||
MQ.vgen = 0, MQ.sup.buffer = MQ.par.buffer = VK_NULL_HANDLE;
|
||||
memset(MQ.aset, 0, sizeof MQ.aset);
|
||||
}
|
||||
|
||||
static const GfxShader *mo_fs(int loc) {
|
||||
if (loc < 0 || loc >= 16) return NULL;
|
||||
if (!MO.fs[loc]) {
|
||||
uint32_t w[sizeof mvobj_fs_spv / 4];
|
||||
memcpy(w, mvobj_fs_spv, sizeof w);
|
||||
w[MVOBJ_FS_LOCATION_WORD] = (uint32_t)loc;
|
||||
MO.fs[loc] = gfx_shader_own(w, sizeof w / 4, MO_FS_RES);
|
||||
if (!MO.fs[loc]) MO.failed = 1, rt_log("vk_exec: the per-object motion fragment shader is malformed: off");
|
||||
}
|
||||
return MO.fs[loc];
|
||||
}
|
||||
|
||||
// The write of binding b from its packed descriptors.
|
||||
static VkWriteDescriptorSet mo_write(const VkPipeBinding *b, const VkDescSlot *d) {
|
||||
VkWriteDescriptorSet w = {.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = b->binding,
|
||||
.descriptorCount = b->count ? b->count : 1, .descriptorType = b->type};
|
||||
if (b->type == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER || b->type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE ||
|
||||
b->type == VK_DESCRIPTOR_TYPE_SAMPLER)
|
||||
w.pImageInfo = &d->image;
|
||||
else
|
||||
w.pBufferInfo = &d->buffer; // VkDescSlot is 24 bytes, as VkDescriptorBufferInfo: arrays line up
|
||||
return w;
|
||||
}
|
||||
|
||||
// The descriptors of a motion draw, packed into MQ.desc at q->desc without the binding caches of the command buffer
|
||||
// being replayed (B, DS, RS), which then stay true for its next draw: uniform buffers (through ubo_info, so with
|
||||
// mo_ovr), the support buffer and sampled textures. An allocated set is written here (or the last one reused when it
|
||||
// would hold the same); a pushed set is pushed by mo_emit. 0 for a pipeline with storage buffers, images or texel
|
||||
// buffers (not drawn again).
|
||||
static int mo_desc(VkExec *ex, const VkPipeInfo *info, MoDraw *q) {
|
||||
if (info->n_bindings > MAX_BINDINGS) return 0;
|
||||
uint32_t total = 0;
|
||||
for (uint32_t i = 0; i < info->n_bindings; i++) total += info->bindings[i].count ? info->bindings[i].count : 1;
|
||||
if (total > MAX_DESC) return 0;
|
||||
int cdesc = (int)MQ.cdesc;
|
||||
MQ.desc = mo_grow(MQ.desc, &cdesc, (int)(MQ.ndesc + total), sizeof *MQ.desc);
|
||||
MQ.cdesc = (uint32_t)cdesc;
|
||||
VkDescSlot *d = MQ.desc + MQ.ndesc;
|
||||
memset(d, 0, total * sizeof *d); // unused bytes zero: equal descriptors compare equal
|
||||
for (uint32_t i = 0, nd = 0; i < info->n_bindings; i++) {
|
||||
const VkPipeBinding *b = &info->bindings[i];
|
||||
const GfxShaderRes *r = b->res;
|
||||
uint32_t cnt = b->count ? b->count : 1;
|
||||
VkDescSlot *o = &d[nd];
|
||||
int st = nvn_stage_of(b->stage);
|
||||
if (b->kind == VK_PIPE_SUPPORT) {
|
||||
float in[3];
|
||||
support_in(ex, in);
|
||||
if (!MQ.sup.buffer || memcmp(in, MQ.sup_in, sizeof in)) {
|
||||
VkBuffer sb;
|
||||
VkDeviceSize so;
|
||||
support_fill(scratch(ex, SUPPORT_BYTES, &sb, &so), in[0], in[1], in[2]);
|
||||
MQ.sup = (VkDescriptorBufferInfo){sb, so, SUPPORT_BYTES};
|
||||
memcpy(MQ.sup_in, in, sizeof in);
|
||||
}
|
||||
o->buffer = MQ.sup;
|
||||
} else if (b->kind == GFX_RES_UBO) {
|
||||
for (uint32_t k = 0; k < cnt; k++) o[k].buffer = ubo_info(ex, st, r->slot + (int)k);
|
||||
} else if (b->kind == GFX_RES_SSBO && !(r->flags & 1)) { // read only: instance data of foliage and such
|
||||
for (uint32_t k = 0; k < cnt; k++) o[k].buffer = ssbo_info(ex, st, r, 0);
|
||||
} else if (b->kind == GFX_RES_TEX && b->type != VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER) {
|
||||
vk_texture_lock();
|
||||
for (uint32_t k = 0; k < cnt; k++) {
|
||||
int slot = r->slot + (int)k;
|
||||
tex_info(ex, r, slot >= 0 && slot < 128 ? ex->tex[st][slot] : 0, 0, r->separate, &o[k].image);
|
||||
}
|
||||
vk_texture_unlock();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
nd += cnt;
|
||||
}
|
||||
uint32_t n_sets = info->n_sets ? info->n_sets : 1;
|
||||
q->set[0] = q->set[1] = VK_NULL_HANDLE;
|
||||
for (uint32_t s = 0, i0 = 0, k0 = 0; s < n_sets && s < 2; s++) {
|
||||
uint32_t i1 = i0, k1 = k0;
|
||||
while (i1 < info->n_bindings && (n_sets == 1 || info->bindings[i1].set == s))
|
||||
k1 += info->bindings[i1].count ? info->bindings[i1].count : 1, i1++;
|
||||
if (i1 > i0 && !(s ? info->push1 : info->push)) {
|
||||
VkDescriptorSetLayout sl = s ? info->set_layout1 : info->set_layout;
|
||||
if (MQ.aset[s] && MQ.aset_layout[s] == sl && MQ.aset_n[s] == k1 - k0 &&
|
||||
!memcmp(MQ.aset_d[s], d + k0, (k1 - k0) * sizeof *d)) {
|
||||
q->set[s] = MQ.aset[s];
|
||||
MO.e_setreuse++;
|
||||
} else {
|
||||
static VkWriteDescriptorSet w[MAX_BINDINGS];
|
||||
VkDescriptorSet set = alloc_set(ex, sl);
|
||||
for (uint32_t i = i0, k = k0; i < i1; k += info->bindings[i].count ? info->bindings[i].count : 1, i++)
|
||||
w[i - i0] = mo_write(&info->bindings[i], d + k), w[i - i0].dstSet = set;
|
||||
vkUpdateDescriptorSets(vk_ctx()->device, i1 - i0, w, 0, NULL);
|
||||
q->set[s] = MQ.aset[s] = set, MQ.aset_layout[s] = sl, MQ.aset_n[s] = k1 - k0;
|
||||
memcpy(MQ.aset_d[s], d + k0, (k1 - k0) * sizeof *d);
|
||||
MO.e_setw++;
|
||||
}
|
||||
}
|
||||
i0 = i1, k0 = k1;
|
||||
}
|
||||
q->desc = MQ.ndesc;
|
||||
MQ.ndesc += total;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The draw's vertex buffers as bind_vertex_buffers resolves them, into MQ.vb; the previous entry again while the
|
||||
// ranges and vk_mem_gen are the same (most motion draws in a row are one model's parts).
|
||||
static uint32_t mo_vertex_buffers(VkExec *ex) {
|
||||
uint64_t gen = vk_mem_gen();
|
||||
if (MQ.nvb && MQ.vgen == gen && !memcmp(MQ.vin, ex->vbuf, sizeof MQ.vin)) {
|
||||
const MoVb *l = &MQ.vb[MQ.nvb - 1];
|
||||
int same = 1;
|
||||
if (!vk_pipeline_vertex_input_dynamic())
|
||||
for (int b = 0; b < 16 && same; b++) same = l->str[b] == ex->pipe.vstream[b].stride;
|
||||
if (same) return (uint32_t)MQ.nvb - 1;
|
||||
}
|
||||
MQ.vb = mo_grow(MQ.vb, &MQ.cvb, MQ.nvb + 1, sizeof *MQ.vb);
|
||||
MoVb *v = &MQ.vb[MQ.nvb];
|
||||
for (int b = 0; b < 17; b++) {
|
||||
v->b[b] = Z.zbuf, v->o[b] = 0, v->sz[b] = ZERO_BYTES, v->str[b] = b < 16 ? ex->pipe.vstream[b].stride : 0;
|
||||
if (b == VK_PIPE_ZERO_BINDING) continue;
|
||||
const NVNbufferRange *r = &ex->vbuf[b];
|
||||
VkMemSpan sp;
|
||||
if (!r->address || !vk_mem_resolve(r->address, 1, &sp)) continue;
|
||||
VK_MEM_USE(sp);
|
||||
v->b[b] = sp.buffer, v->o[b] = sp.offset, v->sz[b] = r->size && r->size < sp.avail ? r->size : sp.avail;
|
||||
}
|
||||
memcpy(MQ.vin, ex->vbuf, sizeof MQ.vin);
|
||||
MQ.vgen = gen;
|
||||
return (uint32_t)MQ.nvb++;
|
||||
}
|
||||
|
||||
// Records the list into cb (the replay's command buffer, inside the motion pass's rendering), skipping what the draw
|
||||
// before already set.
|
||||
static void mo_emit(VkCommandBuffer cb) {
|
||||
int dyn_vi = vk_pipeline_vertex_input_dynamic();
|
||||
VkPipeline pipe = VK_NULL_HANDLE;
|
||||
uint32_t st = UINT32_MAX, vb = UINT32_MAX;
|
||||
VkPipelineLayout lay[2] = {0};
|
||||
const VkPipeInfo *pinfo[2] = {0};
|
||||
int pdesc[2] = {-1, -1}; // the pushed set's descriptors (MQ.desc index) under lay/pinfo, -1 none
|
||||
VkDescriptorSet bound[2] = {0};
|
||||
const MoDraw *p = NULL;
|
||||
for (int n = 0; n < MQ.n; p = &MQ.d[n++]) {
|
||||
const MoDraw *q = &MQ.d[n];
|
||||
const VkPipeInfo *info = q->info;
|
||||
if (q->pipe != pipe) vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, q->pipe), pipe = q->pipe, MO.e_pipe++;
|
||||
if (q->st != st) vk_pipeline_set_dynamic(cb, &MQ.st[q->st]), st = q->st, MO.e_dyn++;
|
||||
if (!p || memcmp(&q->v, &p->v, sizeof q->v)) vkCmdSetViewportWithCount(cb, 1, &q->v);
|
||||
if (!p || memcmp(&q->sc, &p->sc, sizeof q->sc)) vkCmdSetScissorWithCount(cb, 1, &q->sc);
|
||||
if (!p || memcmp(q->bias, p->bias, sizeof q->bias)) vkCmdSetDepthBias(cb, q->bias[0], q->bias[1], q->bias[2]);
|
||||
uint32_t n_sets = info->n_sets ? info->n_sets : 1;
|
||||
const VkDescSlot *d = MQ.desc + q->desc;
|
||||
for (uint32_t s = 0, i0 = 0, k0 = 0; s < n_sets && s < 2; s++) {
|
||||
uint32_t i1 = i0, k1 = k0;
|
||||
while (i1 < info->n_bindings && (n_sets == 1 || info->bindings[i1].set == s))
|
||||
k1 += info->bindings[i1].count ? info->bindings[i1].count : 1, i1++;
|
||||
if (i1 > i0) {
|
||||
if (s ? info->push1 : info->push) {
|
||||
// the bindings that differ from the last push of this layout and pipeline (all after another)
|
||||
int same = lay[s] == info->layout && pinfo[s] == info && pdesc[s] >= 0;
|
||||
const VkDescSlot *pd = same ? MQ.desc + pdesc[s] : NULL;
|
||||
VkWriteDescriptorSet w[MAX_BINDINGS];
|
||||
uint32_t nw = 0;
|
||||
for (uint32_t i = i0, k = k0; i < i1; i++) {
|
||||
uint32_t cnt = info->bindings[i].count ? info->bindings[i].count : 1;
|
||||
if (!same || memcmp(d + k, pd + k, cnt * sizeof *d)) w[nw++] = mo_write(&info->bindings[i], d + k);
|
||||
k += cnt;
|
||||
}
|
||||
if (nw) rec_push(vk_pipeline_push_fn(), cb, VK_PIPELINE_BIND_POINT_GRAPHICS, info->layout, s, nw, w);
|
||||
MO.e_push += nw != 0, MO.e_pushw += nw;
|
||||
lay[s] = info->layout, pinfo[s] = info, pdesc[s] = (int)q->desc, bound[s] = VK_NULL_HANDLE;
|
||||
} else if (q->set[s] != bound[s] || lay[s] != info->layout) {
|
||||
vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, info->layout, s, 1, &q->set[s], 0, NULL);
|
||||
bound[s] = q->set[s], lay[s] = info->layout, pinfo[s] = info, pdesc[s] = -1, MO.e_sets++;
|
||||
}
|
||||
}
|
||||
i0 = i1, k0 = k1;
|
||||
}
|
||||
if (q->vb != vb) {
|
||||
const MoVb *v = &MQ.vb[q->vb];
|
||||
vkCmdBindVertexBuffers2(cb, 0, 17, v->b, v->o, v->sz, dyn_vi ? NULL : v->str);
|
||||
vb = q->vb, MO.e_vb++;
|
||||
}
|
||||
if (!p || q->ib != p->ib || q->ib_off != p->ib_off || q->it != p->it) vkCmdBindIndexBuffer(cb, q->ib, q->ib_off, q->it);
|
||||
vkCmdDrawIndexed(cb, q->count, q->instances, 0, q->base_vertex, q->base_instance);
|
||||
MO.e_draws++;
|
||||
}
|
||||
}
|
||||
|
||||
static void mvobj_draw_(VkExec *ex, const p_draw_idx *d, VkBuffer ib, VkDeviceSize ib_off, uint64_t t0);
|
||||
static void mvobj_draw(VkExec *ex, const p_draw_idx *d, VkBuffer ib, VkDeviceSize ib_off) {
|
||||
if (MO.failed || !vk_mvprev_on() || !B.st_valid) return;
|
||||
static int timed = -1;
|
||||
if (timed < 0) timed = rt_knob("MVOBJ_TIMES", "0"); // TOTK_MVOBJ_TIMES=1: the summary's times (the clock reads cost)
|
||||
if (!timed) return mvobj_draw_(ex, d, ib, ib_off, 0);
|
||||
uint64_t t0 = kern_now_ns();
|
||||
mvobj_draw_(ex, d, ib, ib_off, t0);
|
||||
MO.ns_check += kern_now_ns() - t0;
|
||||
}
|
||||
|
||||
// TOTK_MVOBJ_BANKS=1 (test): for scene draws, the words of each other uniform bank (c3, c6, c8-c16) that changed
|
||||
// since the same shader's draw last frame, logged at frame 3000: where the time that moves wind and water lives
|
||||
static void mvobj_banks(VkExec *ex) {
|
||||
static int on = -1;
|
||||
if (on < 0) on = rt_knob("MVOBJ_BANKS", "0");
|
||||
if (!on || !B.st_valid) return;
|
||||
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
if (frame < 2999 || frame > 3000) return;
|
||||
static struct { const GfxShader *vs; int bank; uint64_t frame; float w[64]; int n; } tab[4096];
|
||||
static int logged;
|
||||
const GfxShaderRes *ur;
|
||||
int nu = gfx_shader_resources(B.st.vs, GFX_RES_UBO, &ur);
|
||||
for (int i = 0; i < nu; i++) {
|
||||
int bank = ur[i].slot;
|
||||
if (bank < 3 || bank == 4 || bank == 5 || bank == 7 || bank > 16) continue;
|
||||
const p_bind_buf *u = &ex->ubo[NVN_STAGE_VS][bank - 3];
|
||||
uint32_t n = u->size < 256 ? (uint32_t)u->size / 4 : 64;
|
||||
const float *now = n ? (const float *)vk_usnap_bytes(u->addr, n * 4) : NULL;
|
||||
if (!now) continue;
|
||||
uint32_t k = (uint32_t)(((uintptr_t)B.st.vs ^ (uint64_t)bank * 0x9E3779B97F4A7C15ull) * 0xff51afd7ed558ccdull >> 52);
|
||||
if (frame == 2999) {
|
||||
tab[k].vs = B.st.vs, tab[k].bank = bank, tab[k].frame = frame, tab[k].n = (int)n;
|
||||
memcpy(tab[k].w, now, n * 4);
|
||||
} else if (tab[k].vs == B.st.vs && tab[k].bank == bank && tab[k].frame == 2999 && logged < 80) {
|
||||
char line[700];
|
||||
int o = 0, nd = 0;
|
||||
for (int j = 0; j < tab[k].n && j < (int)n && o < 600; j++)
|
||||
if (memcmp(&tab[k].w[j], &now[j], 4)) nd++, o += snprintf(line + o, sizeof line - o, " [%d] %g>%g", j, tab[k].w[j], now[j]);
|
||||
if (nd) logged++, rt_log("mvbanks: vs %p c%d size %#llx: %d words%s", (const void *)B.st.vs, bank,
|
||||
(unsigned long long)u->size, nd, line);
|
||||
tab[k].frame = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1: old is cur a little changed or the same (every float within 10 %, at least 1.0 and at most 4.0); *changed: not
|
||||
// the same. The cap: TotK's world positions are in the thousands, and 10 % of them let another object's block a few
|
||||
// units away pass as this one moved; nothing moves 4 units in a frame.
|
||||
static int mo_similar(const uint8_t *cur, const uint8_t *old, uint64_t n, int *changed) {
|
||||
*changed = 0;
|
||||
if (!memcmp(cur, old, n)) return 1;
|
||||
// word by word as integers; float arithmetic only on two normal floats (the blocks hold integers too, and
|
||||
// denormal arithmetic took this loop 1.9 million cycles a block)
|
||||
const uint32_t *x = (const uint32_t *)cur, *y = (const uint32_t *)old;
|
||||
for (uint64_t w = 0; w < n / 4; w++) {
|
||||
if (x[w] == y[w]) continue;
|
||||
uint32_t ex = x[w] >> 23 & 0xff, ey = y[w] >> 23 & 0xff;
|
||||
if (!ex || ex == 0xff || !ey || ey == 0xff) continue; // not both normal floats: an integer or a tiny value
|
||||
float fx, fy;
|
||||
memcpy(&fx, &x[w], 4), memcpy(&fy, &y[w], 4);
|
||||
float dd = fabsf(fx - fy), tol = 0.1f * fabsf(fy);
|
||||
if (dd > (tol < 1.0f ? 1.0f : tol > 4.0f ? 4.0f : tol)) return 0;
|
||||
}
|
||||
*changed = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Last frame's copy of a block, accepted when it is this frame's block a little changed (mo_similar,
|
||||
// which see): a block of another object the address rule found would make motion up. 1 and *out: found (the block itself
|
||||
// when it is unchanged, *changed 0); 0: none, the draw keeps this frame's block there.
|
||||
static int mo_prev_block(int index, const p_bind_buf *u, VkDescriptorBufferInfo *out, int *changed) {
|
||||
uint64_t max = LIM.ubo_max, n = u->size < max ? u->size : max, pa;
|
||||
int how;
|
||||
*changed = 0;
|
||||
// c9 and c11 keep this frame's block. With the copies the address rule finds for c11 and last frame's c9, the
|
||||
// c4 c7 c9 c11 draws (walls, cliffs, arches) got several pixels of motion under a still camera (TOTK_MV_CHECK,
|
||||
// those draws alone: 34 % of the picture moving; with c11 kept, 0.8 %). With c11 kept, their c9 still changes every
|
||||
// frame while nothing moves (the check's per-draw line: about 120 such draws a frame changed in c9 alone), the
|
||||
// object scale the picture shows is 0.6-0.8 of theirs, and with Link stopped against a cliff 40-77 % of the picture
|
||||
// got 1-3 px of motion that flipped direction, three times the warp error of the camera's vectors. With c9 kept too,
|
||||
// the turning run is as good (object warp error 0.46 of the camera's). Why their copies are wrong is not known.
|
||||
// TOTK_MVPREV_SKIP=<hex bank mask> (test) replaces the default a00 (bit n: cn).
|
||||
static long skip = -2;
|
||||
if (skip == -2) {
|
||||
const char *e = cfg_get("TOTK_MVPREV_SKIP");
|
||||
skip = e && *e ? strtol(e, NULL, 16) : 1L << 9 | 1L << 11;
|
||||
}
|
||||
if (skip >> (index + 3) & 1) return 0;
|
||||
uint64_t c0 = __builtin_ia32_rdtsc();
|
||||
int found = vk_mvprev_find(index, u->addr, u->size, &pa, &how);
|
||||
uint64_t c1 = __builtin_ia32_rdtsc();
|
||||
MO.cyc[0] += c1 - c0, MO.ncyc++, MO.big += n;
|
||||
if (index >= 0 && index < 14) MO.lk[index][found ? how : MVPREV_NONE]++;
|
||||
if (!found || !vk_usnap_find(pa, n, LIM.ubo_align, out)) return 0;
|
||||
if (how == MVPREV_SELF) return 1; // bound at the same address last frame: not written again
|
||||
const uint8_t *map = vk_usnap_map(), *cur = vk_usnap_bytes(u->addr, n);
|
||||
uint64_t c2 = __builtin_ia32_rdtsc();
|
||||
MO.cyc[1] += c2 - c1;
|
||||
if (!cur) return 0;
|
||||
const uint8_t *old = map + out->offset;
|
||||
static long trust = -2; // TOTK_MVPREV_TRUST=<hex bank mask> (test): the copy found is taken without the check
|
||||
if (trust == -2) {
|
||||
const char *e = cfg_get("TOTK_MVPREV_TRUST");
|
||||
trust = e && *e ? strtol(e, NULL, 16) : 0;
|
||||
}
|
||||
if (trust >> (index + 3) & 1) {
|
||||
*changed = memcmp(cur, old, n) != 0;
|
||||
return 1;
|
||||
}
|
||||
int r = mo_similar(cur, old, n, changed);
|
||||
MO.cyc[2] += __builtin_ia32_rdtsc() - c2;
|
||||
if (!r && index >= 0 && index < 14) MO.lk[index][4]++;
|
||||
return r;
|
||||
}
|
||||
|
||||
// mo_prev_block once per block and list: many draws share their blocks (the scene's c14, a model's c7 and c9 across
|
||||
// its parts), and the lookup with its comparison was most of the checks' time.
|
||||
static int mo_prev_cached(int index, const p_bind_buf *u, VkDescriptorBufferInfo *out, int *changed) {
|
||||
static struct { uint64_t gen, addr, size; int index, r, changed; VkDescriptorBufferInfo out; } bc[4096];
|
||||
uint32_t i = (uint32_t)((u->addr ^ (uint64_t)index << 56 ^ u->size << 40) * 0x9E3779B97F4A7C15ull >> 52);
|
||||
if (bc[i].gen == mo_gen && bc[i].addr == u->addr && bc[i].size == u->size && bc[i].index == index) {
|
||||
*out = bc[i].out, *changed = bc[i].changed;
|
||||
return bc[i].r;
|
||||
}
|
||||
int r = mo_prev_block(index, u, out, changed);
|
||||
bc[i].gen = mo_gen, bc[i].addr = u->addr, bc[i].size = u->size, bc[i].index = index;
|
||||
bc[i].r = r, bc[i].changed = *changed, bc[i].out = *out;
|
||||
return r;
|
||||
}
|
||||
|
||||
static void mvobj_draw_(VkExec *ex, const p_draw_idx *d, VkBuffer ib, VkDeviceSize ib_off, uint64_t t0) {
|
||||
mvobj_banks(ex);
|
||||
const VkExecTarget *z = &ex->depth;
|
||||
if (!z->image || z->width < DLAA_SCENE_MIN_W || z->format != VK_FORMAT_D32_SFLOAT || B.st.samples > 1 || B.st.raster_discard)
|
||||
return;
|
||||
// solid geometry only, which tests and writes the scene depth: effects, particles and screen quads write none, and
|
||||
// drawn again they would put their motion on the surfaces behind them (DLSS takes those from the surface)
|
||||
if (!B.st.ds.dt || !B.st.ds.dw) return;
|
||||
// a vertex shader that reads a user bank besides the camera's (c4) may move on its own
|
||||
const GfxShaderRes *ur;
|
||||
int nu = gfx_shader_resources(B.st.vs, GFX_RES_UBO, &ur), reads = 0;
|
||||
for (int i = 0; i < nu; i++) reads |= ur[i].slot >= 3 && ur[i].slot <= 16 && ur[i].slot != 4;
|
||||
if (!reads) return;
|
||||
MO.cand++;
|
||||
uint64_t tp = t0 ? kern_now_ns() : 0;
|
||||
if (t0) MO.ns_part[0] += tp - t0, MO.n_part[0]++;
|
||||
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
if (MQ.frame != frame || MQ.fence != ex->fence || MQ.depth_fmt != z->format) mo_begin(ex, frame);
|
||||
if (!__atomic_load_n(&LIM.ubo_max, __ATOMIC_ACQUIRE)) limits_init();
|
||||
// the variant: the translation of the same code and answers, kept per shader once found; it names the banks the
|
||||
// position depends on (0: none, the shader is skipped from then on)
|
||||
static struct { const GfxShader *of, *vs; uint32_t banks; } vmemo[1024];
|
||||
uint32_t vi = (uint32_t)((uintptr_t)B.st.vs * 0x9E3779B97F4A7C15ull >> 54);
|
||||
if (vmemo[vi].of != B.st.vs) {
|
||||
int has, pending = 0;
|
||||
sf_mvprev = 1;
|
||||
const GfxShader *v = shader_for(ex, NVN_STAGE_VS, &has, &pending, 0);
|
||||
sf_mvprev = 0;
|
||||
if (!v || pending) {
|
||||
MO.building++;
|
||||
return;
|
||||
}
|
||||
GfxShaderIo io[32];
|
||||
uint32_t extra = gfx_shader_outputs(v, io) & ~gfx_shader_outputs(B.st.vs, io);
|
||||
uint32_t banks = extra ? gfx_shader_mvprev_banks(v) & 0x1fff8u : 0;
|
||||
vmemo[vi].of = B.st.vs, vmemo[vi].vs = v, vmemo[vi].banks = banks;
|
||||
vk_mvprev_want((banks & ~(1u << 4)) >> 3); // binding index = bank - 3
|
||||
}
|
||||
const GfxShader *vs = vmemo[vi].vs;
|
||||
uint32_t banks = vmemo[vi].banks;
|
||||
if (!banks || !(banks & ~(1u << 4))) {
|
||||
MO.no_out++;
|
||||
return;
|
||||
}
|
||||
// a position without the scene camera (c4) has a view of its own (c8 c9 c10 [c13]: its c8 is at a new address every
|
||||
// frame, last frame's is never found): drawn again it would get this frame's view and no camera motion
|
||||
// (TOTK_MV_CHECK while turning: 6.2 px end-point error against the camera vectors' 1.7)
|
||||
static int noc4 = -1;
|
||||
if (noc4 < 0) noc4 = rt_knob("MVOBJ_NOC4", "0"); // TOTK_MVOBJ_NOC4=1 (test): drawn again all the same
|
||||
if (!(banks >> 4 & 1) && !noc4) {
|
||||
MO.no_out++;
|
||||
return;
|
||||
}
|
||||
// only positions that read an object's own blocks: bones (c5), place (c7) or c6. The rest (terrain and grass: c14
|
||||
// c15 c16, with or without c9) measured no better than the camera's motion (TOTK_MV_CHECK, turning, those draws
|
||||
// alone: object warp error 0.112 against the camera's 0.113, the object part 0.4 of what the picture shows) and
|
||||
// were half the draws checked and listed. TOTK_MVOBJ_ALL=1 (test) takes them again.
|
||||
static int all = -1;
|
||||
if (all < 0) all = rt_knob("MVOBJ_ALL", "0");
|
||||
if (!all && !(banks & (1u << 5 | 1u << 6 | 1u << 7))) {
|
||||
MO.no_out++;
|
||||
return;
|
||||
}
|
||||
// TOTK_MVOBJ_ONLY=<hex bank mask> (test): only draws whose position reads no bank outside the mask (bit n: cn)
|
||||
static long only = -2;
|
||||
if (only == -2) {
|
||||
const char *e = cfg_get("TOTK_MVOBJ_ONLY");
|
||||
only = e && *e ? strtol(e, NULL, 16) : -1;
|
||||
}
|
||||
if (only >= 0 && banks & ~(uint32_t)only & ~(1u << 4)) return;
|
||||
static long exact = -2; // TOTK_MVOBJ_EXACT=<hex bank mask> (test): only draws with exactly these position banks
|
||||
if (exact == -2) {
|
||||
const char *e = cfg_get("TOTK_MVOBJ_EXACT");
|
||||
exact = e && *e ? strtol(e, NULL, 16) : -1;
|
||||
}
|
||||
if (exact >= 0 && banks != (uint32_t)exact) return;
|
||||
mo_mask_note(banks, B.st.vs);
|
||||
// last frame's camera and blocks; held still when none of the position's blocks changed (most of the scenery)
|
||||
VkDescriptorBufferInfo cam, prevb[17];
|
||||
float jit[2];
|
||||
if (!vk_dlaa_prev_camera(&cam, jit)) {
|
||||
MO.nodata++, MO.why[0]++;
|
||||
return;
|
||||
}
|
||||
static struct { uint64_t frame, key; int moving; VkDescriptorBufferInfo b[17]; uint32_t have; } memo[8192];
|
||||
uint64_t key = banks;
|
||||
for (int b = 3; b <= 16; b++)
|
||||
if (banks >> b & 1 && b != 4) key = (key ^ ex->ubo[NVN_STAGE_VS][b - 3].addr) * 0x100000001b3ull;
|
||||
uint32_t mi = (uint32_t)(key * 0xff51afd7ed558ccdull >> 51);
|
||||
uint32_t have = 0;
|
||||
if (memo[mi].frame == frame && memo[mi].key == key) {
|
||||
if (!memo[mi].moving) {
|
||||
if (t0) MO.ns_still += kern_now_ns() - tp;
|
||||
return (void)MO.still++;
|
||||
}
|
||||
memcpy(prevb, memo[mi].b, sizeof prevb), have = memo[mi].have;
|
||||
} else {
|
||||
MO.misses++;
|
||||
int moving = 0;
|
||||
for (int b = 3; b <= 16; b++) {
|
||||
if (!(banks >> b & 1) || b == 4) continue;
|
||||
const p_bind_buf *u = &ex->ubo[NVN_STAGE_VS][b - 3];
|
||||
int changed;
|
||||
if (u->addr && u->size && mo_prev_cached(b - 3, u, &prevb[b], &changed))
|
||||
have |= 1u << b, moving |= changed << b; // the banks whose block changed
|
||||
}
|
||||
memo[mi].frame = frame, memo[mi].key = key, memo[mi].moving = moving;
|
||||
memcpy(memo[mi].b, prevb, sizeof prevb), memo[mi].have = have;
|
||||
if (!moving) {
|
||||
if (t0) MO.ns_still += kern_now_ns() - tp;
|
||||
return (void)MO.still++;
|
||||
}
|
||||
}
|
||||
if (t0) MO.ns_part[1] += kern_now_ns() - tp, MO.n_part[1]++, tp = kern_now_ns();
|
||||
GfxShaderIo io[32];
|
||||
uint32_t extra = gfx_shader_outputs(vs, io) & ~gfx_shader_outputs(B.st.vs, io);
|
||||
const GfxShaderRes *r;
|
||||
if (gfx_shader_resources(vs, GFX_RES_IMG, &r)) {
|
||||
MO.storage++;
|
||||
return;
|
||||
}
|
||||
const GfxShader *fs = mo_fs(31 - __builtin_clz(extra));
|
||||
if (!fs) {
|
||||
MO.no_fs++;
|
||||
return;
|
||||
}
|
||||
VkPipeState ms = B.st;
|
||||
ms.vs = vs, ms.fs = fs, ms.feedback = 0;
|
||||
memset(ms.color_fmt, 0, sizeof ms.color_fmt);
|
||||
ms.color_fmt[0] = NVN_FORMAT_RG16F;
|
||||
// blending off (blend_en below); the factors stay the game's, valid NVN values
|
||||
ms.color.blend_en = 0, ms.color.logic_op = 0;
|
||||
ms.chmask.mask = 0x3; // r and g of target 0
|
||||
ms.ms.a2c = 0;
|
||||
// the game's own test with its equal case (LESS -> LEQUAL, GREATER -> GEQUAL): the surface the draw left in the
|
||||
// scene depth passes, anything nearer hides it (TOTK_MVOBJ_EQUAL=1: EQUAL, for comparison)
|
||||
static int eq = -1;
|
||||
if (eq < 0) eq = rt_knob("MVOBJ_EQUAL", "0");
|
||||
int df = B.st.ds.df;
|
||||
df = eq ? NVN_DEPTH_FUNC_EQUAL : df == NVN_DEPTH_FUNC_LESS ? NVN_DEPTH_FUNC_LEQUAL
|
||||
: df == NVN_DEPTH_FUNC_GREATER ? NVN_DEPTH_FUNC_GEQUAL : df == NVN_DEPTH_FUNC_LEQUAL || df == NVN_DEPTH_FUNC_GEQUAL ? df
|
||||
: NVN_DEPTH_FUNC_EQUAL;
|
||||
ms.ds.dt = 1, ms.ds.dw = 0, ms.ds.df = (uint64_t)df, ms.ds.st = 0;
|
||||
// the pipeline per state, kept: the lookup hashes and locks the whole state
|
||||
static struct { VkPipeState st; VkPipeline p; const VkPipeInfo *info; int used; } pmemo[64];
|
||||
static int plast = -1; // the entry of the last draw: a model's parts in a row share the state, no hash then
|
||||
uint32_t pi;
|
||||
if (plast >= 0 && pmemo[plast].used && !memcmp(&pmemo[plast].st, &ms, sizeof ms)) {
|
||||
pi = (uint32_t)plast;
|
||||
} else {
|
||||
uint64_t ph = 0xcbf29ce484222325ull;
|
||||
for (size_t i = 0; i < sizeof ms; i += 8) {
|
||||
uint64_t w8;
|
||||
memcpy(&w8, (const uint8_t *)&ms + i, sizeof ms - i < 8 ? sizeof ms - i : 8);
|
||||
ph = (ph ^ w8) * 0x100000001b3ull;
|
||||
}
|
||||
pi = (uint32_t)(ph >> 58);
|
||||
}
|
||||
const VkPipeInfo *info = NULL;
|
||||
VkPipeline pipe;
|
||||
int pending = 0;
|
||||
if (pmemo[pi].used && !memcmp(&pmemo[pi].st, &ms, sizeof ms)) {
|
||||
info = pmemo[pi].info;
|
||||
pipe = __atomic_load_n(&info->pipeline, __ATOMIC_ACQUIRE); // its optimised build once swapped in
|
||||
if (!pipe) return;
|
||||
} else {
|
||||
pipe = vk_pipeline_graphics_async(&ms, &info, &pending, 0);
|
||||
if (pending || !pipe || !info) {
|
||||
MO.building += pending != 0;
|
||||
return;
|
||||
}
|
||||
pmemo[pi].st = ms, pmemo[pi].p = pipe, pmemo[pi].info = info, pmemo[pi].used = 1;
|
||||
}
|
||||
plast = (int)pi;
|
||||
uint64_t t1 = kern_now_ns();
|
||||
if (t0) MO.ns_part[2] += t1 - tp, MO.n_part[2]++;
|
||||
if (t0) MO.ns_check -= t1 - t0; // counted as recording below
|
||||
// into the frame's list, past the primary's binding caches (mo_desc); mo_emit records it
|
||||
MQ.d = mo_grow(MQ.d, &MQ.cap, MQ.n + 1, sizeof *MQ.d);
|
||||
MoDraw *q = &MQ.d[MQ.n];
|
||||
float par_in[4] = {(float)z->width, (float)z->height, jit[0], jit[1]};
|
||||
if (!MQ.par.buffer || memcmp(par_in, MQ.par_in, sizeof par_in)) {
|
||||
VkBuffer pbuf;
|
||||
VkDeviceSize poff;
|
||||
memcpy(scratch(ex, 16, &pbuf, &poff), par_in, sizeof par_in);
|
||||
MQ.par = (VkDescriptorBufferInfo){pbuf, poff, 16};
|
||||
memcpy(MQ.par_in, par_in, sizeof par_in);
|
||||
}
|
||||
memset(mo_ovr, 0, sizeof mo_ovr);
|
||||
for (int bk = 3; bk <= 16; bk++) // last frame's block, or this frame's where none was found
|
||||
mo_ovr[NVN_STAGE_VS][bk + 15] = bk == 4 ? cam : have >> bk & 1 ? prevb[bk] : ubo_info(ex, NVN_STAGE_VS, bk);
|
||||
mo_ovr[NVN_STAGE_FS][3] = MQ.par;
|
||||
mo_ovr_on = 1;
|
||||
int bound = mo_desc(ex, info, q);
|
||||
mo_ovr_on = 0;
|
||||
if (!bound) {
|
||||
MO.storage++;
|
||||
return;
|
||||
}
|
||||
q->pipe = pipe, q->info = info;
|
||||
if (!MQ.nst || memcmp(&MQ.st[MQ.nst - 1], &ms, sizeof ms)) {
|
||||
MQ.st = mo_grow(MQ.st, &MQ.cst, MQ.nst + 1, sizeof *MQ.st);
|
||||
MQ.st[MQ.nst++] = ms;
|
||||
}
|
||||
q->st = (uint32_t)MQ.nst - 1;
|
||||
q->v = draw_viewport(ex), q->sc = vk_exec_scissor(ex);
|
||||
q->bias[0] = ex->poly_offset.units, q->bias[1] = ex->poly_offset.clamp, q->bias[2] = ex->poly_offset.factor;
|
||||
q->vb = mo_vertex_buffers(ex);
|
||||
q->ib = ib, q->ib_off = ib_off;
|
||||
q->it = d->itype == NVN_INDEX_TYPE_UNSIGNED_SHORT ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32;
|
||||
q->count = (uint32_t)d->count, q->instances = d->instances > 0 ? (uint32_t)d->instances : 1;
|
||||
q->base_vertex = d->base_vertex, q->base_instance = (uint32_t)d->base_instance;
|
||||
q->cam = ex->ubo[NVN_STAGE_VS][1].addr;
|
||||
MQ.n++;
|
||||
MO.draws++;
|
||||
for (int i = 0; i < 16; i++) // the frame's draws per position banks and changed banks (vk_mvobj_frame_masks)
|
||||
if (!MF[i].n || (MF[i].mask == banks && MF[i].chg == (uint32_t)memo[mi].moving)) {
|
||||
MF[i].mask = banks, MF[i].chg = (uint32_t)memo[mi].moving, MF[i].n++;
|
||||
break;
|
||||
}
|
||||
if (t0) MO.ns_rec += kern_now_ns() - t1, MO.n_rec++;
|
||||
}
|
||||
|
||||
int vk_mvobj_execute(VkExec *ex, uint64_t cam, VkImageView mv, VkImageView depth, VkFormat depth_fmt, uint32_t w,
|
||||
uint32_t h) {
|
||||
static uint64_t frames;
|
||||
if (++frames % 60 == 0)
|
||||
rt_log("vk_exec: per-object motion: %.2f ms a frame checking, %.2f ms listing (%.1f us a draw), %.2f ms recording",
|
||||
MO.ns_check / 1e6 / (frames ? frames : 1), MO.ns_rec / 1e6 / (frames ? frames : 1),
|
||||
MO.n_rec ? MO.ns_rec / 1e3 / MO.n_rec : 0.0, MO.ns_emit / 1e6 / (frames ? frames : 1));
|
||||
if (frames % 600 == 0) {
|
||||
char l[1400];
|
||||
int o = 0;
|
||||
for (int i = 0; i < 32 && MM[i].n && o < 1300; i++)
|
||||
o += snprintf(l + o, sizeof l - o, " %#x:%llu (u%d s%d t%d)", MM[i].mask, (unsigned long long)MM[i].n, MM[i].ubo,
|
||||
MM[i].ssbo, MM[i].tex);
|
||||
if (o) rt_log("vk_exec: per-object motion position banks (bit n: cn) and draws:%s", l);
|
||||
o = 0;
|
||||
for (int i = 0; i < 14 && o < 1300; i++)
|
||||
if (MO.lk[i][0] + MO.lk[i][1] + MO.lk[i][2] + MO.lk[i][3])
|
||||
o += snprintf(l + o, sizeof l - o, " c%d %llu/%llu/%llu/%llu/%llu", i + 3, (unsigned long long)MO.lk[i][0],
|
||||
(unsigned long long)MO.lk[i][1], (unsigned long long)MO.lk[i][2],
|
||||
(unsigned long long)MO.lk[i][3], (unsigned long long)MO.lk[i][4]);
|
||||
if (o) rt_log("vk_exec: per-object motion last-frame lookups (none/self/side/nearer/refused):%s", l);
|
||||
}
|
||||
if (frames % 60 == 0 && MO.ran)
|
||||
rt_log("vk_exec: per-object motion recorded a frame: %.0f draws, %.0f pipeline binds, %.0f dynamic states, %.0f "
|
||||
"pushes of %.1f bindings, %.0f set binds, %.0f vertex buffer binds; sets written %.0f, reused %.0f",
|
||||
(double)MO.e_draws / MO.ran, (double)MO.e_pipe / MO.ran, (double)MO.e_dyn / MO.ran, (double)MO.e_push / MO.ran,
|
||||
MO.e_push ? (double)MO.e_pushw / MO.e_push : 0.0, (double)MO.e_sets / MO.ran, (double)MO.e_vb / MO.ran,
|
||||
(double)MO.e_setw / MO.ran, (double)MO.e_setreuse / MO.ran);
|
||||
if (frames % 60 == 0)
|
||||
rt_log("vk_exec: per-object motion checks: filter %.2f ms (%.0f a frame), data %.2f ms (%.0f), variant and "
|
||||
"pipeline %.2f ms (%.0f); %.0f objects looked up; still draws %.2f ms", MO.ns_part[0] / 1e6 / frames,
|
||||
(double)MO.n_part[0] / frames,
|
||||
MO.ns_part[1] / 1e6 / frames, (double)MO.n_part[1] / frames, MO.ns_part[2] / 1e6 / frames,
|
||||
(double)MO.n_part[2] / frames, (double)MO.misses / frames, MO.ns_still / 1e6 / frames);
|
||||
if (frames % 60 == 0 && MO.ncyc)
|
||||
rt_log("vk_exec: per-object motion lookups: %llu cycles finding last frame's, %llu the copies, %llu comparing, "
|
||||
"a lookup; %llu bytes a lookup", (unsigned long long)(MO.cyc[0] / MO.ncyc),
|
||||
(unsigned long long)(MO.cyc[1] / MO.ncyc), (unsigned long long)(MO.cyc[2] / MO.ncyc),
|
||||
(unsigned long long)(MO.big / MO.ncyc));
|
||||
if (frames % 60 == 0)
|
||||
rt_log("vk_exec: per-object motion: %llu draws read place or bones, %llu of them still, %llu drawn again, %llu without last frame's data, %llu "
|
||||
"waiting for a shader or pipeline; %llu frames ran, %llu dropped (no data: camera %llu, bones %llu, place %llu, "
|
||||
"bones copy %llu, place copy %llu; uniform alignment %llu; variant without its output %llu, with storage %llu, "
|
||||
"no fragment shader %llu; another camera %llu)",
|
||||
(unsigned long long)MO.cand, (unsigned long long)MO.still, (unsigned long long)MO.draws, (unsigned long long)MO.nodata,
|
||||
(unsigned long long)MO.building,
|
||||
(unsigned long long)MO.ran, (unsigned long long)MO.dropped, (unsigned long long)MO.why[0],
|
||||
(unsigned long long)MO.why[1], (unsigned long long)MO.why[2], (unsigned long long)MO.why[3],
|
||||
(unsigned long long)MO.why[4], (unsigned long long)LIM.ubo_align, (unsigned long long)MO.no_out,
|
||||
(unsigned long long)MO.storage, (unsigned long long)MO.no_fs, (unsigned long long)MO.other_cam);
|
||||
if (!MQ.n) return 0;
|
||||
static int exec = -1;
|
||||
if (exec < 0) exec = rt_knob("MVOBJ_EXEC", "1"); // TOTK_MVOBJ_EXEC=0: listed, never recorded (test)
|
||||
if (!exec) {
|
||||
mo_drop();
|
||||
return 0;
|
||||
}
|
||||
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
if (MQ.frame != frame || MQ.fence != ex->fence || MQ.depth_fmt != depth_fmt) {
|
||||
mo_drop();
|
||||
return 0;
|
||||
}
|
||||
int fo = 0;
|
||||
for (int i = 0; i < 16 && MF[i].n && fo < (int)sizeof fm_last - 40; i++)
|
||||
fo += snprintf(fm_last + fo, sizeof fm_last - fo, " %#x/%#x:%u", MF[i].mask, MF[i].chg, MF[i].n);
|
||||
if (!fo) fm_last[0] = 0;
|
||||
// only draws with the scene camera block (vk_dlaa.c mv_pass's): last frame's camera they get is that block's, and
|
||||
// a draw of another view would move by the difference between the two views
|
||||
int kept = 0;
|
||||
for (int i = 0; i < MQ.n; i++)
|
||||
if (MQ.d[i].cam == cam) MQ.d[kept++] = MQ.d[i];
|
||||
else MO.other_cam++;
|
||||
MQ.n = kept;
|
||||
if (!MQ.n) {
|
||||
mo_drop();
|
||||
return 0;
|
||||
}
|
||||
uint64_t t0 = kern_now_ns();
|
||||
VkCommandBuffer cb = ex->cb;
|
||||
VkRenderingAttachmentInfo ca = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = mv,
|
||||
.imageLayout = VK_IMAGE_LAYOUT_GENERAL, .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
|
||||
.storeOp = VK_ATTACHMENT_STORE_OP_STORE};
|
||||
VkRenderingAttachmentInfo da = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = depth,
|
||||
.imageLayout = VK_IMAGE_LAYOUT_GENERAL, .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
|
||||
.storeOp = VK_ATTACHMENT_STORE_OP_STORE};
|
||||
VkRenderingInfo ri = {.sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, {w, h}}, .layerCount = 1,
|
||||
.colorAttachmentCount = 1, .pColorAttachments = &ca, .pDepthAttachment = &da};
|
||||
vkCmdBeginRendering(cb, &ri); // through the recorder (vk_rec.h), in order with the draws and the end
|
||||
mo_emit(cb);
|
||||
vkCmdEndRendering(cb);
|
||||
// the draws left their pipeline, descriptors, buffers and dynamic state bound: the next game draw sets everything
|
||||
vk_exec_draw_new_cb();
|
||||
ex->dirty = VK_EXEC_DIRTY_ALL;
|
||||
ex->recorded += MQ.n;
|
||||
MQ.n = MQ.nst = MQ.nvb = 0, MQ.ndesc = 0, MQ.frame = 0;
|
||||
MO.ran++;
|
||||
MO.ns_emit += kern_now_ns() - t0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void bind_index(VkExec *ex, const void *data, uint64_t bytes, VkIndexType type) {
|
||||
VkBuffer b;
|
||||
VkDeviceSize o;
|
||||
@@ -2288,6 +3066,7 @@ static void op_draw_idx(VkExec *ex, const p_draw_idx *d) {
|
||||
vkCmdDrawIndexed(ex->cb, (uint32_t)d->count, d->instances > 0 ? (uint32_t)d->instances : 1, 0, d->base_vertex,
|
||||
(uint32_t)d->base_instance);
|
||||
ex->recorded++;
|
||||
if (d->itype != NVN_INDEX_TYPE_UNSIGNED_BYTE) mvobj_draw(ex, d, sp.buffer, sp.offset);
|
||||
}
|
||||
|
||||
// DrawElementsIndirect: the arguments are read by the GPU from the pool buffer (they are often written by an earlier
|
||||
@@ -2495,3 +3274,6 @@ void vk_exec_op_draw(VkExec *ex, uint32_t op, const void *payload) {
|
||||
default: rt_fatal("vk: op %s is not a draw op", nvn_op_name(op));
|
||||
}
|
||||
}
|
||||
|
||||
// The last executed list's draws as " <position banks>/<changed banks>:<draws>" (bit n: cn), for TOTK_MV_CHECK's lines.
|
||||
const char *vk_mvobj_frame_masks(void) { return fm_last; }
|
||||
@@ -912,6 +912,25 @@ static int test_mirror(const GfxBackend *b, uint8_t *guest, int *bytes) {
|
||||
CHECK(xf_submit(b, stream, p), "fence_wait of the third mirror submission timed out");
|
||||
for (int i = 0; i < 0x800; i++) CHECK(m[0x4000 + i] == 0x11, "CPU byte %#x beside a fill is %02x", i, m[0x4000 + i]);
|
||||
for (int i = 0; i < 0x800; i++) CHECK(m[0x4800 + i] == 0x77, "fill byte %#x is %02x", i, m[0x4800 + i]);
|
||||
// 4: two small flushes at the ends of the pool copy their own pages, not what lies between them.
|
||||
int fl_bad = vk_mem_flush_add_probe();
|
||||
CHECK(!fl_bad, "flush range list check %d failed", fl_bad);
|
||||
p = put(stream, OP_BEGIN, NULL, 0);
|
||||
const p_copy_b2b c4 = {a, a + 0x3000, 4096};
|
||||
p = put(p, OP_COPY_B2B, &c4, sizeof c4);
|
||||
CHECK(xf_submit(b, stream, p), "fence_wait of the mirror submission before the flushes timed out");
|
||||
uint64_t up0 = vk_mem_mirror_uploaded();
|
||||
nvn_pool_flushed(&pool, 0x6000, 0x1000);
|
||||
nvn_pool_flushed(&pool, 0xF0000, 0x1000);
|
||||
p = put(stream, OP_BEGIN, NULL, 0);
|
||||
p = put(p, OP_COPY_B2B, &c4, sizeof c4);
|
||||
CHECK(xf_submit(b, stream, p), "fence_wait of the mirror submission after the flushes timed out");
|
||||
// A submission's copies reach the counter with the next one, and c4's own write-back adds its destination page.
|
||||
CHECK(xf_submit(b, stream, p), "fence_wait of the mirror submission that counts the flushes timed out");
|
||||
uint64_t up = vk_mem_mirror_uploaded() - up0;
|
||||
if (!rt_knob("FLUSH_SPAN", "0"))
|
||||
CHECK(up >= 0x2000 && up < 0x10000, "two flushed pages copied %#llx bytes, 0xeb000 would be the span",
|
||||
(unsigned long long)up);
|
||||
b->pool_fini(&pool);
|
||||
*bytes = 4096 + 0x1000 + 4096 + 0x1000;
|
||||
return 0;
|
||||
|
||||
+78
-3
@@ -8,7 +8,8 @@
|
||||
// 8-bit sRGB window texture it presents. So:
|
||||
// 1. vk_hdr_shader_swap: the tonemap's fragment shader becomes shaders/hdr_tonemap.frag, which keeps values above 1
|
||||
// (relative to the HUD's white) and rolls off at the peak. It is recognised by its constants and resources, not by
|
||||
// a hash, so another game version or translator still finds it; its SPIR-V is replaced in memory only.
|
||||
// a hash, so another game version or translator still finds it; its SPIR-V is replaced in memory only. The game's
|
||||
// FSR 1 upscale after it would clamp the picture to 1 again; fsr_unclamp drops that clamp in its SPIR-V.
|
||||
// 2. vk_hdr_draw / vk_hdr_present_source: a draw into a presented window texture that samples one float texture of
|
||||
// its size is that copy; the present then reads the float texture instead of the window texture. Any other last
|
||||
// draw into it leaves the window texture, decoded from sRGB: menus and movies stay SDR at the HUD's white.
|
||||
@@ -50,7 +51,7 @@ void vk_hdr_off(void) { g_wanted = 0; }
|
||||
|
||||
// The three nits, read again when the settings change. The replay (every draw's support buffer) and the present
|
||||
// thread read them: an update fills the snapshot not in use and then publishes it.
|
||||
typedef struct { float peak, game, ui; } Nits;
|
||||
typedef struct { float peak, game, ui, hue; } Nits;
|
||||
static struct { unsigned seen; int cur; Nits snap[2]; PlatMutex lock; } N = {.seen = ~0u, .lock = PLAT_MUTEX_INIT};
|
||||
|
||||
static Nits nits(void) {
|
||||
@@ -62,6 +63,7 @@ static Nits nits(void) {
|
||||
n->peak = fmaxf((float)cfg_double("TOTK_HDR_PEAK_NITS"), 80.0f);
|
||||
n->game = fmaxf((float)cfg_double("TOTK_HDR_GAME_NITS"), 80.0f);
|
||||
n->ui = fmaxf((float)cfg_double("TOTK_HDR_UI_NITS"), 80.0f);
|
||||
n->hue = fminf(fmaxf((float)cfg_double("TOTK_HDR_HUE"), 0.0f), 100.0f) / 100.0f;
|
||||
__atomic_store_n(&N.cur, !N.cur, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&N.seen, v, __ATOMIC_RELEASE);
|
||||
}
|
||||
@@ -74,7 +76,8 @@ void vk_hdr_support(float out[4]) {
|
||||
Nits n = nits();
|
||||
out[0] = n.game / n.ui; // the picture's white against the HUD's
|
||||
out[1] = n.peak > n.game ? n.peak / n.game : 1.0f; // the peak against the picture's white
|
||||
out[2] = out[3] = 0;
|
||||
out[2] = n.hue; // hue correction of highlights (hdr_tonemap.frag)
|
||||
out[3] = 0;
|
||||
}
|
||||
|
||||
// ---- 1. the tonemap shader
|
||||
@@ -90,8 +93,80 @@ static int name_is(const uint32_t *a, uint32_t len, const char *s) { // OpName
|
||||
return (len - 2) * 4 >= n + 1 && !memcmp(a + 1, s, n + 1);
|
||||
}
|
||||
|
||||
// The game's FSR 1 pass after the tonemap (some frames take it in place of the plain pass, at 1:1 as well as when it
|
||||
// scales up; what picks it is not known yet) ends with clamp(colour, 0.0, 1.0) per channel, which cuts the HDR
|
||||
// highlights back to the HUD's white for as long as the game takes it. Its own 0..1 clamps of the filter weights each
|
||||
// feed a float bit trick (OpBitcast), so only the clamps whose result is stored to the shader's output change, to
|
||||
// max(x, 0.0): one word shorter, so the words after it move down and the module shrinks. Recognised by FSR's two
|
||||
// reciprocal constants (0x7ef07ebb, 0x7ef19fff) and its one texture fp_t_tcb_8. Returns the clamps changed, -1 for
|
||||
// that shader with no output clamp found (a translator that stores the colour another way), 0 for any other shader.
|
||||
enum { SPV_OP_EXT_INST_IMPORT = 11, SPV_OP_EXT_INST = 12, SPV_OP_VARIABLE = 59, SPV_OP_STORE = 62,
|
||||
SPV_OP_ACCESS_CHAIN = 65, SPV_SC_OUTPUT = 3, GLSL_FMAX = 40, GLSL_FCLAMP = 43 };
|
||||
|
||||
static int fsr_unclamp(uint32_t *w, uint64_t *words) {
|
||||
uint64_t n = *words;
|
||||
uint32_t bound = w[3], glsl = 0, float_type = 0, zero = 0, one = 0;
|
||||
int magic = 0, tex = 0;
|
||||
for (uint64_t i = 5; i < n;) {
|
||||
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
|
||||
if (!len || i + len > n) return 0;
|
||||
const uint32_t *a = w + i + 1;
|
||||
if (op == SPV_OP_EXT_INST_IMPORT && len >= 3 && name_is(a, len, "GLSL.std.450")) glsl = a[0];
|
||||
else if (op == SPV_OP_TYPE_FLOAT && len == 3 && a[1] == 32) float_type = a[0];
|
||||
else if (op == SPV_OP_CONSTANT && len == 4) {
|
||||
magic |= (a[2] == 0x7ef07ebbu) | (a[2] == 0x7ef19fffu) << 1;
|
||||
if (a[0] == float_type && float_type && a[2] == 0) zero = a[1];
|
||||
if (a[0] == float_type && float_type && a[2] == 0x3f800000u) one = a[1];
|
||||
} else if (op == SPV_OP_NAME && len >= 3 && name_is(a, len, "fp_t_tcb_8")) tex = 1;
|
||||
i += len;
|
||||
}
|
||||
if (magic != 3 || !tex) return 0;
|
||||
if (!glsl || !zero || !one || !bound) return -1;
|
||||
uint8_t *out = calloc(bound, 1); // id -> 1: a pointer into an Output variable, 2: a value stored through one
|
||||
if (!out) return 0;
|
||||
for (uint64_t i = 5; i < n; i += w[i] >> 16) {
|
||||
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
|
||||
const uint32_t *a = w + i + 1;
|
||||
if (op == SPV_OP_VARIABLE && len >= 4 && a[2] == SPV_SC_OUTPUT && a[1] < bound) out[a[1]] = 1;
|
||||
else if (op == SPV_OP_ACCESS_CHAIN && len >= 4 && a[2] < bound && out[a[2]] == 1 && a[1] < bound) out[a[1]] = 1;
|
||||
else if (op == SPV_OP_STORE && len >= 3 && a[0] < bound && out[a[0]] == 1 && a[1] < bound) out[a[1]] = 2;
|
||||
}
|
||||
int changed = 0;
|
||||
uint64_t k = 5; // the write position: never past the read position, as instructions only shrink
|
||||
for (uint64_t i = 5; i < n;) {
|
||||
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
|
||||
const uint32_t *a = w + i + 1; // result type, result, set, instruction, x, min, max
|
||||
if (op == SPV_OP_EXT_INST && len == 8 && a[2] == glsl && a[3] == GLSL_FCLAMP && a[1] < bound && out[a[1]] == 2 &&
|
||||
a[5] == zero && a[6] == one) {
|
||||
uint32_t ins[7] = {7u << 16 | SPV_OP_EXT_INST, a[0], a[1], a[2], GLSL_FMAX, a[4], zero};
|
||||
memmove(w + k, ins, sizeof ins);
|
||||
k += 7, changed++;
|
||||
} else {
|
||||
memmove(w + k, w + i, len * sizeof *w);
|
||||
k += len;
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
free(out);
|
||||
*words = k;
|
||||
return changed ? changed : -1;
|
||||
}
|
||||
|
||||
void vk_hdr_shader_swap(uint32_t **spirv, uint64_t *words) {
|
||||
if (!vk_hdr_wanted() || !*spirv || *words < 5) return;
|
||||
int unclamped = fsr_unclamp(*spirv, words);
|
||||
if (unclamped > 0) {
|
||||
static volatile int32_t fsrs;
|
||||
rt_log("hdr: the game's FSR pass keeps values above 1 (%d output clamps; %d so far)", unclamped,
|
||||
plat_inc32(&fsrs));
|
||||
return;
|
||||
}
|
||||
if (unclamped < 0) {
|
||||
static volatile int32_t missed;
|
||||
if (plat_inc32(&missed) == 1)
|
||||
rt_log("hdr: the game's FSR pass was found but no clamp of its output; it still clips HDR highlights");
|
||||
return;
|
||||
}
|
||||
const uint32_t *w = *spirv;
|
||||
uint64_t n = *words;
|
||||
// the game's constants: the BT.601 red weight, -1/ln 2 (e^-x as exp2), 2/3 (the grade's red weight), the bloom clamp
|
||||
|
||||
+379
-21
@@ -21,6 +21,7 @@
|
||||
#include "nvn_pool_index.h"
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
#include "wb_merge_spv.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
// Timestamps for the tracking cost, in ns; qpc_ms converts.
|
||||
@@ -34,6 +35,9 @@ enum { MS_NONE, MS_ON, MS_REFUSED };
|
||||
|
||||
typedef struct { struct PoolRec *phys; uint64_t off; } VPage; // virtual page -> physical pool and offset
|
||||
|
||||
#define FLUSH_RANGES 32 // flush ranges a mirrored pool keeps between two syncs (flush_add)
|
||||
typedef struct { uint64_t lo, hi; } FlushRange;
|
||||
|
||||
typedef struct PoolRec {
|
||||
const pool_t *key;
|
||||
int kind;
|
||||
@@ -59,7 +63,9 @@ typedef struct PoolRec {
|
||||
uint32_t wb_sub; // replay: the last submission that wrote mirror bytes back to guest memory
|
||||
uint32_t mgen; // gen_lock: every page write stamped up to this generation is in the mirror
|
||||
// (or in a copy queued before its next use)
|
||||
uint64_t fl_lo, fl_hi; // flush_lock: nvnMemoryPoolFlushMappedRange bytes not copied yet (fl_hi 0: none)
|
||||
FlushRange fl[FLUSH_RANGES + 1]; // flush_lock: nvnMemoryPoolFlushMappedRange pages not copied yet (flush_add)
|
||||
int fl_n; // flush_lock: ranges in fl; read without the lock to skip an empty list
|
||||
uint64_t fl_calls, fl_sum; // flush_lock: the calls and bytes behind fl, for mirror_sync's statistics
|
||||
// Shadow storage (POOL_IMPORT in shadow mode, "shadow storage" below): buf is a host-visible copy of the guest pages.
|
||||
VmaAllocation salloc;
|
||||
uint8_t *smap; // its mapping; offsets as in buf (from import_lo)
|
||||
@@ -504,7 +510,10 @@ static int dead_pending(void);
|
||||
|
||||
static void vram_log(uint64_t frame);
|
||||
|
||||
static volatile uint64_t mirror_frame; // the frame vk_mem_census last saw, for mirror_sync's log lines
|
||||
|
||||
void vk_mem_census(uint64_t frame) {
|
||||
mirror_frame = frame;
|
||||
static uint64_t window_start;
|
||||
if ((rt_trace & RT_TRACE_PERF) && frame % 600 == 0) vram_log(frame);
|
||||
if ((rt_trace & RT_TRACE_PERF) && !vk_mirror_on) dirty_frame(frame);
|
||||
@@ -1132,7 +1141,10 @@ static void dirty_frame(uint64_t frame) {
|
||||
// - CPU -> mirror: the first access of a submission to a mirror syncs it (mirror_sync): the 2 MiB write-watch
|
||||
// snapshots of its regions for this epoch (snap_regions, shared with the texture checks: the OS bits keep one
|
||||
// consumer, the shared page generations) and every page stamped after the mirror's mgen goes into a copy; so do the
|
||||
// nvnMemoryPoolFlushMappedRange ranges. The first use copies the whole range. The copies run in a prologue command
|
||||
// nvnMemoryPoolFlushMappedRange ranges, each on its own: one span from the lowest to the highest flush of a 252 MiB
|
||||
// pool copied up to the whole pool for a few MiB flushed, 30-50 ms of GPU time over PCIe 3.0 x8 in crowded towns
|
||||
// (TOTK_FLUSH_SPAN=1 restores it). Writes between the ranges need no copy: the write watch sees them. The first use
|
||||
// copies the whole range. The copies run in a prologue command
|
||||
// buffer that vk_sync.c submits right before the part that accessed the mirror first, between full barriers.
|
||||
// vk_mem_mirror_begin snaps the regions of the pools the previous submission accessed in a few merged queries.
|
||||
// - GPU -> guest: what the replay writes into a mirror (storage buffers its shaders write, buffer copies, fills,
|
||||
@@ -1160,10 +1172,13 @@ static int npend, cpend, nwbl, cwbl;
|
||||
static VkBufferCopy *mregions; // replay: vkCmdCopyBuffer regions
|
||||
static int cregions;
|
||||
static PlatMutex flush_lock = PLAT_MUTEX_INIT;
|
||||
static int flush_cap = FLUSH_RANGES; // ranges a pool keeps; 1 with TOTK_FLUSH_SPAN=1
|
||||
|
||||
typedef struct {
|
||||
uint64_t subs, syncs, upload_bytes, upload_copies, first_n, first_bytes, wb_bytes, wb_copies, wb_ranges;
|
||||
uint64_t presnap_queries, sync_queries, presnap_t, sync_t, prologues, presnap_bytes;
|
||||
uint64_t snap_bytes, merge_bytes, merge_dispatches, snap_allocs; // write-back merge
|
||||
uint64_t page_bytes, flush_n, flush_bytes, flush_calls, flush_sum; // what mirror_sync queues, by source
|
||||
} MirrorAcc;
|
||||
static MirrorAcc mrep; // replay: this submission
|
||||
static MirrorAcc macc; // mirror_stat_lock: since the last summary
|
||||
@@ -1188,6 +1203,10 @@ static void mirror_mode(int discrete) {
|
||||
if (mirror_gap < 0) mirror_gap = 0;
|
||||
// Submissions a region stays unwritten before it is frozen. 60 rather than 300: in a fight 100 of the 257 MiB
|
||||
// queried a frame lay in regions idle for 30-299 submissions, and a thaw by a write costs about 60 us.
|
||||
if (rt_knob("FLUSH_SPAN", "0")) {
|
||||
flush_cap = 1;
|
||||
rt_log("mem: a pool's flushes copy one span from the lowest to the highest (TOTK_FLUSH_SPAN=1)");
|
||||
}
|
||||
const char *fi = cfg_get("TOTK_FREEZE_IDLE");
|
||||
freeze_idle = fi && *fi ? atoi(fi) : 60;
|
||||
if (freeze_idle < 0) freeze_idle = 0;
|
||||
@@ -1201,6 +1220,11 @@ static void mirror_mode(int discrete) {
|
||||
sh && *sh ? " (TOTK_SHADOW)" : "");
|
||||
rt_log("mem: mode %s (%s): %s", mode == 2 ? "verify" : mode ? "mirror" : "import", why,
|
||||
mode ? "pools used as buffers get a device-local mirror" : "every CPU-visible pool is imported host memory");
|
||||
// Import is the integrated-GPU mode. On a discrete card every draw reads the pools over PCIe (the comment above
|
||||
// this section has the measurements), which is the difference between playable and 5 fps when it is not intended.
|
||||
if (!mode && discrete)
|
||||
rt_log("mem: warning: import on a discrete GPU: the GPU reads the game's pools over PCIe on every draw, far "
|
||||
"slower than mirror (the auto default on a discrete GPU); use memory = auto unless testing");
|
||||
}
|
||||
|
||||
static void range_add(MRange **a, int *n, int *cap, PoolRec *r, uint64_t off, uint64_t size) {
|
||||
@@ -1385,19 +1409,30 @@ static void mirror_sync(PoolRec *r) {
|
||||
if (!ch) continue;
|
||||
for (uint64_t x = a; x < b; x += 1ull << GEN_PAGE_SHIFT)
|
||||
if (ch[(x >> GEN_PAGE_SHIFT) & (GEN_PER_CHUNK - 1)] > since)
|
||||
range_add(&pend, &npend, &cpend, r, x - lo, 1ull << GEN_PAGE_SHIFT);
|
||||
range_add(&pend, &npend, &cpend, r, x - lo, 1ull << GEN_PAGE_SHIFT), mrep.page_bytes += 1ull << GEN_PAGE_SHIFT;
|
||||
}
|
||||
r->mgen = gen_now;
|
||||
if (excl) plat_mutex_unlock(&gen_lock);
|
||||
else plat_mutex_unlock_shared(&gen_lock);
|
||||
if (__atomic_load_n(&r->fl_hi, __ATOMIC_ACQUIRE)) {
|
||||
if (__atomic_load_n(&r->fl_n, __ATOMIC_ACQUIRE)) {
|
||||
FlushRange fl[FLUSH_RANGES + 1];
|
||||
plat_mutex_lock(&flush_lock);
|
||||
uint64_t a = r->fl_lo, b = r->fl_hi;
|
||||
r->fl_hi = 0;
|
||||
int n = r->fl_n;
|
||||
memcpy(fl, r->fl, (size_t)n * sizeof *fl);
|
||||
uint64_t calls = r->fl_calls, sum = r->fl_sum;
|
||||
r->fl_n = 0, r->fl_calls = r->fl_sum = 0;
|
||||
plat_mutex_unlock(&flush_lock);
|
||||
a = a > lo ? a & ~0xfffull : lo;
|
||||
b = b < hi ? (b + 0xfff) & ~0xfffull : hi;
|
||||
if (a < b) range_add(&pend, &npend, &cpend, r, a - lo, b - a);
|
||||
mrep.flush_calls += calls, mrep.flush_sum += sum;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint64_t a = fl[i].lo > lo ? fl[i].lo : lo, b = fl[i].hi < hi ? fl[i].hi : hi;
|
||||
if (a >= b) continue;
|
||||
range_add(&pend, &npend, &cpend, r, a - lo, b - a);
|
||||
mrep.flush_n++, mrep.flush_bytes += b - a;
|
||||
if ((rt_trace & RT_TRACE_PERF) && b - a >= 16u << 20)
|
||||
rt_log("mem: mirror frame %llu: flush range %.1f MiB at +%.1f MiB of a %.1f MiB pool (%d ranges from %llu "
|
||||
"flushes of %.3f MiB)", (unsigned long long)mirror_frame, (b - a) / 1048576.0, (a - lo) / 1048576.0,
|
||||
(hi - lo) / 1048576.0, n, (unsigned long long)calls, sum / 1048576.0);
|
||||
}
|
||||
}
|
||||
mrep.syncs++;
|
||||
mrep.sync_t += qpc() - t0;
|
||||
@@ -1426,11 +1461,302 @@ int vk_mem_mirror_probe(uint64_t addr) {
|
||||
return ((PoolRec *)sp.pool)->mstate == MS_ON;
|
||||
}
|
||||
|
||||
void vk_mem_gpu_wrote(const VkMemSpan *sp, uint64_t size) {
|
||||
// ---- write-back merge
|
||||
// A storage buffer a shader writes whole records of does not
|
||||
// go back to guest memory as a plain copy. The CPU may write fields of the same records while the GPU runs: TotK's
|
||||
// particle emitters write a new particle's whole record into a free slot of a buffer that the simulation shaders update
|
||||
// (the waterfall splashes and mist, 2026-11-02), and a plain copy puts the mirror's stale copy of those words back over
|
||||
// them. Instead:
|
||||
// - record time (gpu_wrote from vk_mem_gpu_wrote_shader): each distinct range of the submission gets room in a snapshot
|
||||
// chunk (snap_add);
|
||||
// - each part's prologue, after the CPU -> mirror copies: the mirror's words of the ranges the part added are copied
|
||||
// into their snapshots (snap_copies), the state before the part's GPU work. Words of an earlier part's writes that a
|
||||
// later range overlaps are covered by the earlier range's own snapshot;
|
||||
// - vk_mem_mirror_end: shaders/wb_merge.comp writes each 32-bit word that differs between the mirror and the snapshot
|
||||
// into the pool's imported buffer (wb_merge_record), one dispatch per pool and group of ranges.
|
||||
// Words the GPU left unchanged keep the CPU's value; a word both changed takes the GPU's, as a plain copy would.
|
||||
// TOTK_WB_MERGE=0 restores the plain copies; so does a pipeline or allocation failure.
|
||||
#define SNAP_CHUNK (1u << 20)
|
||||
#define MERGE_SPAN (64ull << 20) // the bytes of one pool a merge dispatch binds at most
|
||||
#define MERGE_WORDS (65535u * 64u) // the words one merge dispatch covers at most
|
||||
#define MERGE_MAX (4ull << 20) // larger written ranges stay plain copies
|
||||
typedef struct { PoolRec *r; uint64_t off, size, soff; int chunk; } SnapRange;
|
||||
typedef struct { VkBuffer buf; VmaAllocation alloc; uint64_t size, used; int host; } SnapChunk;
|
||||
typedef struct { int n; SnapChunk c[]; } SnapFree; // a submission's chunks and range lists, pooled at its fence
|
||||
static SnapRange *snl; // replay: this submission's merged ranges; [0, snl_copied) have their snapshot copy
|
||||
static int nsnl, csnl, snl_copied;
|
||||
static SnapChunk *chunks; // replay: this submission's snapshot chunks and merge range lists
|
||||
static int nchunks, cchunks;
|
||||
// Chunks a finished submission used (snap_free) wait here for chunk_new, so a frame with merges creates no buffer once
|
||||
// the pool holds the frames in flight: a VMA allocation that needs a new memory block costs 8-11 ms on the replay
|
||||
// thread. Chunk sizes are powers of two so that one fits the next request; what the pool cannot hold is destroyed.
|
||||
#define SNAP_LIST_MIN (64u << 10)
|
||||
#define SNAP_POOL_N 32
|
||||
#define SNAP_POOL_BYTES (16ull << 20)
|
||||
static PlatMutex snap_pool_lock = PLAT_MUTEX_INIT;
|
||||
static SnapChunk snap_pool[SNAP_POOL_N];
|
||||
static int nsnap_pool;
|
||||
static uint64_t snap_pool_bytes;
|
||||
static int merge_on = -1;
|
||||
static struct {
|
||||
int tried, ok;
|
||||
PFN_vkCmdPushDescriptorSetKHR push;
|
||||
VkDescriptorSetLayout dsl;
|
||||
VkPipelineLayout layout;
|
||||
VkPipeline pipe;
|
||||
} WM;
|
||||
|
||||
static int merge_init(VkCtx *c) {
|
||||
WM.tried = 1;
|
||||
WM.push = (PFN_vkCmdPushDescriptorSetKHR)vkGetDeviceProcAddr(c->device, "vkCmdPushDescriptorSetKHR");
|
||||
VkDescriptorSetLayoutBinding b[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
b[i] = (VkDescriptorSetLayoutBinding){.binding = (uint32_t)i, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT};
|
||||
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
||||
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
|
||||
.bindingCount = 4, .pBindings = b};
|
||||
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, 8};
|
||||
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof wb_merge_spv,
|
||||
.pCode = wb_merge_spv};
|
||||
VkShaderModule m = VK_NULL_HANDLE;
|
||||
if (!WM.push || vkCreateDescriptorSetLayout(c->device, &dci, NULL, &WM.dsl) != VK_SUCCESS) goto fail;
|
||||
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
||||
.pSetLayouts = &WM.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
|
||||
if (vkCreatePipelineLayout(c->device, &li, NULL, &WM.layout) != VK_SUCCESS ||
|
||||
vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS)
|
||||
goto fail;
|
||||
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 = WM.layout};
|
||||
VkResult res = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &WM.pipe);
|
||||
vkDestroyShaderModule(c->device, m, NULL);
|
||||
if (res != VK_SUCCESS) goto fail;
|
||||
return WM.ok = 1;
|
||||
fail:
|
||||
rt_log("mem: write-back merge off: its pipeline could not be created");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int merging(void) {
|
||||
if (merge_on < 0) {
|
||||
merge_on = cfg_bool("TOTK_WB_MERGE");
|
||||
rt_log("mem: write-back merge %s (TOTK_WB_MERGE)", merge_on ? "on" : "off");
|
||||
}
|
||||
if (!merge_on) return 0;
|
||||
if (!WM.tried) merge_init(vk_ctx());
|
||||
return WM.ok;
|
||||
}
|
||||
|
||||
// A chunk of at least size bytes in chunks[]: the smallest fitting one of the pool, else a new buffer; -1 when the
|
||||
// allocation fails.
|
||||
static int chunk_new(uint64_t size, int host) {
|
||||
uint64_t want = host ? SNAP_LIST_MIN : SNAP_CHUNK;
|
||||
while (want < size) want <<= 1;
|
||||
if (nchunks == cchunks) grow((void **)&chunks, &cchunks, sizeof *chunks);
|
||||
SnapChunk *ch = &chunks[nchunks];
|
||||
int best = -1;
|
||||
plat_mutex_lock(&snap_pool_lock);
|
||||
for (int i = 0; i < nsnap_pool; i++)
|
||||
if (snap_pool[i].host == host && snap_pool[i].size >= want && (best < 0 || snap_pool[i].size < snap_pool[best].size))
|
||||
best = i;
|
||||
if (best >= 0) {
|
||||
*ch = snap_pool[best];
|
||||
snap_pool[best] = snap_pool[--nsnap_pool];
|
||||
snap_pool_bytes -= ch->size;
|
||||
}
|
||||
plat_mutex_unlock(&snap_pool_lock);
|
||||
if (best < 0) {
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = want,
|
||||
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE};
|
||||
VmaAllocationCreateInfo ai = {.usage = host ? VMA_MEMORY_USAGE_AUTO_PREFER_HOST : VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE,
|
||||
.flags = host ? VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
|
||||
VMA_ALLOCATION_CREATE_MAPPED_BIT : 0};
|
||||
if (vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &ch->buf, &ch->alloc, NULL) != VK_SUCCESS) return -1;
|
||||
ch->size = want, ch->host = host;
|
||||
mrep.snap_allocs++;
|
||||
}
|
||||
ch->used = 0;
|
||||
return nchunks++;
|
||||
}
|
||||
|
||||
// Record time: room in a snapshot chunk for [off, off+size) of r's mirror, once per submission. 0: plain copy instead.
|
||||
static int snap_add(PoolRec *r, uint64_t off, uint64_t size) {
|
||||
for (int i = nsnl; i-- > 0;)
|
||||
if (snl[i].r == r && snl[i].off == off && snl[i].size == size) return 1;
|
||||
int k = -1;
|
||||
for (int i = nchunks; i-- > 0;) // the newest snapshot chunk, if the range fits
|
||||
if (chunks[i].used != UINT64_MAX && chunks[i].size - chunks[i].used >= size) { k = i; break; }
|
||||
if (k < 0 && (k = chunk_new(size > SNAP_CHUNK ? size : SNAP_CHUNK, 0)) < 0) return 0;
|
||||
if (nsnl == csnl) grow((void **)&snl, &csnl, sizeof *snl);
|
||||
snl[nsnl++] = (SnapRange){r, off, size, chunks[k].used, k};
|
||||
chunks[k].used += (size + 15) & ~15ull;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Prologue: the snapshot copies of the ranges added since the last prologue.
|
||||
static void snap_copies(VkCommandBuffer cb) {
|
||||
for (int i = snl_copied; i < nsnl; i++) {
|
||||
VkBufferCopy cp = {snl[i].off, snl[i].soff, snl[i].size};
|
||||
vkCmdCopyBuffer(cb, snl[i].r->mbuf, chunks[snl[i].chunk].buf, 1, &cp);
|
||||
mrep.snap_bytes += snl[i].size;
|
||||
}
|
||||
snl_copied = nsnl;
|
||||
}
|
||||
|
||||
static int snap_cmp(const void *x, const void *y) {
|
||||
const SnapRange *a = x, *b = y;
|
||||
if (a->r != b->r) return (uintptr_t)a->r < (uintptr_t)b->r ? -1 : 1;
|
||||
if (a->chunk != b->chunk) return a->chunk < b->chunk ? -1 : 1;
|
||||
return a->off < b->off ? -1 : a->off > b->off;
|
||||
}
|
||||
|
||||
// One dispatch: ranges s[0..n) share a pool and a chunk and lie in [base, base + span) of the pool.
|
||||
static void merge_dispatch(VkCommandBuffer cb, const SnapRange *s, int n, uint64_t base, uint64_t span, int list,
|
||||
uint64_t list_off, uint32_t total) {
|
||||
VkDescriptorBufferInfo bi[4] = {{s->r->mbuf, base, span}, {chunks[s->chunk].buf, 0, chunks[s->chunk].size},
|
||||
{s->r->buf, base, span}, {chunks[list].buf, list_off, (uint64_t)n * 16}};
|
||||
VkWriteDescriptorSet w[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
w[i] = (VkWriteDescriptorSet){.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = (uint32_t)i,
|
||||
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.pBufferInfo = &bi[i]};
|
||||
rec_push(WM.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, WM.layout, 0, 4, w); // in call order (vk_rec.h)
|
||||
uint32_t pc[2] = {(uint32_t)n, total};
|
||||
vkCmdPushConstants(cb, WM.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, pc);
|
||||
vkCmdDispatch(cb, (total + 63) / 64, 1, 1);
|
||||
}
|
||||
|
||||
// vk_mem_mirror_end: the merge dispatches of this submission's ranges. Returns the commands recorded; 0 with nsnl
|
||||
// left as it was when the range list cannot be allocated (the caller copies them plainly).
|
||||
static int wb_merge_record(VkCommandBuffer cb) {
|
||||
VkCtx *c = vk_ctx();
|
||||
uint64_t align = c->props.limits.minStorageBufferOffsetAlignment;
|
||||
if (align < 16) align = 16;
|
||||
SnapRange *s = malloc(sizeof *s * (size_t)nsnl);
|
||||
if (!s) rt_fatal("vk: out of memory");
|
||||
memcpy(s, snl, sizeof *s * (size_t)nsnl);
|
||||
qsort(s, (size_t)nsnl, sizeof *s, snap_cmp);
|
||||
// the range lists: at most one 16-byte entry per range, each dispatch's list at an aligned offset
|
||||
int list = chunk_new((uint64_t)nsnl * 16 + (uint64_t)nsnl * align, 1);
|
||||
if (list < 0) { free(s); return 0; }
|
||||
chunks[list].used = UINT64_MAX; // not a snapshot chunk
|
||||
VmaAllocationInfo li;
|
||||
vmaGetAllocationInfo(c->vma, chunks[list].alloc, &li);
|
||||
uint32_t *lm = li.pMappedData;
|
||||
uint64_t lo = 0;
|
||||
int rec = 0;
|
||||
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, WM.pipe);
|
||||
for (int i = 0; i < nsnl;) {
|
||||
uint64_t base = s[i].off & ~(align - 1), end = s[i].off + s[i].size;
|
||||
uint32_t total = 0;
|
||||
int j = i;
|
||||
lo = (lo + align - 1) & ~(align - 1);
|
||||
uint64_t l0 = lo;
|
||||
for (; j < nsnl && s[j].r == s[i].r && s[j].chunk == s[i].chunk; j++) {
|
||||
uint64_t e = s[j].off + s[j].size > end ? s[j].off + s[j].size : end;
|
||||
if (j > i && (e - base > MERGE_SPAN || total + s[j].size / 4 > MERGE_WORDS)) break;
|
||||
end = e;
|
||||
uint32_t *q = lm + lo / 4;
|
||||
q[0] = (uint32_t)((s[j].off - base) / 4), q[1] = (uint32_t)(s[j].soff / 4), q[2] = (uint32_t)(s[j].size / 4);
|
||||
q[3] = total;
|
||||
total += (uint32_t)(s[j].size / 4);
|
||||
lo += 16;
|
||||
mrep.merge_bytes += s[j].size;
|
||||
}
|
||||
merge_dispatch(cb, s + i, j - i, base, end - base, list, l0, total);
|
||||
mrep.merge_dispatches++;
|
||||
rec += 3;
|
||||
i = j;
|
||||
}
|
||||
vmaFlushAllocation(c->vma, chunks[list].alloc, 0, VK_WHOLE_SIZE);
|
||||
for (int i = 0; i < nsnl; i++) snl[i].r->wb_sub = msub;
|
||||
free(s);
|
||||
return rec;
|
||||
}
|
||||
|
||||
static void snap_free(void *arg) { // completion thread: the submission is done with its chunks
|
||||
SnapFree *f = arg;
|
||||
plat_mutex_lock(&snap_pool_lock);
|
||||
for (int i = 0; i < f->n; i++) {
|
||||
SnapChunk *ch = &f->c[i];
|
||||
if (nsnap_pool == SNAP_POOL_N || snap_pool_bytes + ch->size > SNAP_POOL_BYTES) continue;
|
||||
snap_pool[nsnap_pool++] = *ch;
|
||||
snap_pool_bytes += ch->size;
|
||||
ch->buf = VK_NULL_HANDLE;
|
||||
}
|
||||
plat_mutex_unlock(&snap_pool_lock);
|
||||
for (int i = 0; i < f->n; i++)
|
||||
if (f->c[i].buf) vmaDestroyBuffer(vk_ctx()->vma, f->c[i].buf, f->c[i].alloc);
|
||||
free(f);
|
||||
}
|
||||
|
||||
static void gpu_wrote(const VkMemSpan *sp, uint64_t size, int shader) {
|
||||
PoolRec *r = sp->pool;
|
||||
if (!r || r->mstate != MS_ON || sp->buffer != r->mbuf || !size) return;
|
||||
// A binding to the end of its pool (no size) can span hundreds of MiB: those stay copies.
|
||||
if (shader && !(sp->offset & 3) && !(size & 3) && size <= MERGE_MAX && merging() && snap_add(r, sp->offset, size))
|
||||
return;
|
||||
range_add(&wbl, &nwbl, &cwbl, r, sp->offset, size);
|
||||
}
|
||||
void vk_mem_gpu_wrote(const VkMemSpan *sp, uint64_t size) { gpu_wrote(sp, size, 0); }
|
||||
void vk_mem_gpu_wrote_shader(const VkMemSpan *sp, uint64_t size) { gpu_wrote(sp, size, 1); }
|
||||
|
||||
// Adds [a, b) to l, n sorted ranges that do not touch; returns the new count. Above cap ranges (l holds cap + 1) the two
|
||||
// nearest merge, so a full list copies the fewest bytes that were not flushed; cap 1 keeps one span over all flushes.
|
||||
static int flush_add(FlushRange *l, int n, uint64_t a, uint64_t b, int cap) {
|
||||
int i = 0;
|
||||
while (i < n && l[i].hi < a) i++;
|
||||
int j = i;
|
||||
for (; j < n && l[j].lo <= b; j++) {
|
||||
if (l[j].lo < a) a = l[j].lo;
|
||||
if (l[j].hi > b) b = l[j].hi;
|
||||
}
|
||||
memmove(l + i + 1, l + j, (size_t)(n - j) * sizeof *l);
|
||||
l[i] = (FlushRange){a, b};
|
||||
n += 1 - (j - i);
|
||||
if (n > cap) {
|
||||
int k = 0;
|
||||
for (int x = 1; x + 1 < n; x++)
|
||||
if (l[x + 1].lo - l[x].hi < l[k + 1].lo - l[k].hi) k = x;
|
||||
l[k].hi = l[k + 1].hi;
|
||||
memmove(l + k + 1, l + k + 2, (size_t)(n - k - 2) * sizeof *l);
|
||||
n--;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int vk_mem_flush_add_probe(void) {
|
||||
FlushRange l[5];
|
||||
int n = 0;
|
||||
n = flush_add(l, n, 0x8000, 0x9000, 4);
|
||||
n = flush_add(l, n, 0x1000, 0x2000, 4);
|
||||
n = flush_add(l, n, 0x4000, 0x5000, 4);
|
||||
if (n != 3 || l[0].lo != 0x1000 || l[1].lo != 0x4000 || l[2].hi != 0x9000) return 1; // kept apart, sorted
|
||||
n = flush_add(l, n, 0x2000, 0x3000, 4);
|
||||
if (n != 3 || l[0].hi != 0x3000) return 2; // touching ranges join
|
||||
n = flush_add(l, n, 0x800, 0x8800, 4);
|
||||
if (n != 1 || l[0].lo != 0x800 || l[0].hi != 0x9000) return 3; // one covers several
|
||||
n = 0;
|
||||
n = flush_add(l, n, 0x10000, 0x11000, 2);
|
||||
n = flush_add(l, n, 0x40000, 0x41000, 2);
|
||||
n = flush_add(l, n, 0x12000, 0x13000, 2); // over the cap: nearest merge
|
||||
if (n != 2 || l[0].lo != 0x10000 || l[0].hi != 0x13000 || l[1].lo != 0x40000) return 4;
|
||||
n = 0;
|
||||
n = flush_add(l, n, 0x10000, 0x11000, 1);
|
||||
n = flush_add(l, n, 0x40000, 0x41000, 1);
|
||||
if (n != 1 || l[0].lo != 0x10000 || l[0].hi != 0x41000) return 5; // TOTK_FLUSH_SPAN=1
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t vk_mem_mirror_uploaded(void) {
|
||||
plat_mutex_lock(&mirror_stat_lock);
|
||||
uint64_t n = macc.upload_bytes;
|
||||
plat_mutex_unlock(&mirror_stat_lock);
|
||||
return n;
|
||||
}
|
||||
|
||||
void nvn_pool_flushed(const pool_t *p, uint64_t off, uint64_t size) {
|
||||
if (!vk_mirror_on || !size || !p->mem) return;
|
||||
@@ -1440,13 +1766,10 @@ void nvn_pool_flushed(const pool_t *p, uint64_t off, uint64_t size) {
|
||||
const pidx_ent *e = pidx_find(&pool_index, (uintptr_t)p->mem, 0);
|
||||
PoolRec *r = e && e->key == p ? e->buf : NULL;
|
||||
if (r && r->mstate == MS_ON) {
|
||||
uint64_t a = (uintptr_t)p->mem + off, b = a + size;
|
||||
uint64_t a = ((uintptr_t)p->mem + off) & ~0xfffull, b = ((uintptr_t)p->mem + off + size + 0xfff) & ~0xfffull;
|
||||
plat_mutex_lock(&flush_lock);
|
||||
if (!r->fl_hi) r->fl_lo = a, r->fl_hi = b;
|
||||
else {
|
||||
if (a < r->fl_lo) r->fl_lo = a;
|
||||
if (b > r->fl_hi) r->fl_hi = b;
|
||||
}
|
||||
__atomic_store_n(&r->fl_n, flush_add(r->fl, r->fl_n, a, b, flush_cap), __ATOMIC_RELEASE);
|
||||
r->fl_calls++, r->fl_sum += size;
|
||||
plat_mutex_unlock(&flush_lock);
|
||||
}
|
||||
plat_mutex_unlock_shared(&pool_lock);
|
||||
@@ -1474,6 +1797,7 @@ void vk_mem_mirror_begin(void) {
|
||||
return;
|
||||
}
|
||||
uint64_t t0 = qpc();
|
||||
nsnl = snl_copied = nchunks = 0; // the previous submission handed its chunks to snap_free
|
||||
uint32_t prev = msub;
|
||||
msub = msub + 1 ? msub + 1 : 1;
|
||||
__atomic_add_fetch(&mirror_gen, 1, __ATOMIC_RELEASE);
|
||||
@@ -1505,13 +1829,18 @@ void vk_mem_mirror_begin(void) {
|
||||
mrep.presnap_t += qpc() - t0;
|
||||
}
|
||||
|
||||
int vk_mem_mirror_pending(void) { return npend; }
|
||||
int vk_mem_mirror_pending(void) { return npend || snl_copied < nsnl; }
|
||||
|
||||
void vk_mem_mirror_prologue(VkCommandBuffer cb) {
|
||||
if (!npend) return;
|
||||
if (!npend && snl_copied == nsnl) return;
|
||||
mbarrier(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
copy_ranges(cb, pend, npend, 1, &mrep.upload_bytes, &mrep.upload_copies);
|
||||
if (snl_copied < nsnl) { // the snapshots read what the uploads above wrote
|
||||
if (npend) mbarrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_TRANSFER_READ_BIT);
|
||||
snap_copies(cb);
|
||||
}
|
||||
mbarrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
|
||||
npend = 0;
|
||||
@@ -1610,6 +1939,26 @@ static int verify_record(VkCommandBuffer cb, uint64_t fence) {
|
||||
int vk_mem_mirror_end(VkCommandBuffer cb, uint64_t fence) {
|
||||
if (!vk_mirror_on) return 0;
|
||||
int rec = 0;
|
||||
if (nsnl) {
|
||||
// the merge reads the mirror the submission's shaders wrote and writes the imported buffers
|
||||
mbarrier(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT);
|
||||
int m = wb_merge_record(cb);
|
||||
if (!m) { // no range list: copy them plainly
|
||||
for (int i = 0; i < nsnl; i++) range_add(&wbl, &nwbl, &cwbl, snl[i].r, snl[i].off, snl[i].size);
|
||||
}
|
||||
rec += m + 1;
|
||||
mbarrier(cb, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_HOST_BIT | VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_2_HOST_READ_BIT | VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
|
||||
}
|
||||
if (nchunks) { // the last part's prologue still records snapshot copies into them; back to the pool at the fence
|
||||
SnapFree *f = malloc(sizeof *f + sizeof(SnapChunk) * (size_t)nchunks);
|
||||
if (!f) rt_fatal("vk: out of memory");
|
||||
f->n = nchunks;
|
||||
memcpy(f->c, chunks, sizeof(SnapChunk) * (size_t)nchunks);
|
||||
vk_completion_add(fence, snap_free, f);
|
||||
}
|
||||
if (nwbl) {
|
||||
mbarrier(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
|
||||
VK_ACCESS_2_TRANSFER_READ_BIT);
|
||||
@@ -1664,14 +2013,19 @@ static void mirror_log(uint64_t frame) {
|
||||
(long long)plat_xchg64(&v_mismatch, 0));
|
||||
rt_log("mem: mirror frame %llu: pools %lld, %.1f MiB device-local; stay imported: budget %lld (%.1f MiB), untracked "
|
||||
"%lld, overlapping %lld, failed %lld; per frame: submits %.2f, syncs %.1f, upload %.3f MiB in %.1f ranges, "
|
||||
"first use %.2f pools %.3f MiB, write-back %.3f MiB in %.1f ranges, flush calls %.1f (%.3f MiB), queries "
|
||||
"first use %.2f pools %.3f MiB, write-back %.3f MiB in %.1f ranges, merge %.3f MiB in %.1f dispatches "
|
||||
"(snapshots %.3f MiB, new buffers %.2f), flush calls %.1f (%.3f MiB), queries "
|
||||
"%.1f pre-snap (%.1f MiB) + %.1f sync, prologues %.2f, tracking cpu %.3f ms (pre-snap %.3f, syncs %.3f)%s",
|
||||
(unsigned long long)frame, (long long)m_live_n, m_live_bytes / M, (long long)m_refused_n[MR_BUDGET],
|
||||
m_refused_bytes[MR_BUDGET] / M, (long long)m_refused_n[MR_UNTRACKED], (long long)m_refused_n[MR_OVERLAP],
|
||||
(long long)m_refused_n[MR_ALLOC], a.subs / f, a.syncs / f, a.upload_bytes / f / M, a.upload_copies / f,
|
||||
a.first_n / f, a.first_bytes / f / M, a.wb_bytes / f / M, a.wb_copies / f, fc / f, fb / f / M,
|
||||
a.first_n / f, a.first_bytes / f / M, a.wb_bytes / f / M, a.wb_copies / f, a.merge_bytes / f / M,
|
||||
a.merge_dispatches / f, a.snap_bytes / f / M, a.snap_allocs / f, fc / f, fb / f / M,
|
||||
a.presnap_queries / f, a.presnap_bytes / f / M, a.sync_queries / f, a.prologues / f, qpc_ms(a.presnap_t + a.sync_t) / f,
|
||||
qpc_ms(a.presnap_t) / f, qpc_ms(a.sync_t) / f, ver);
|
||||
rt_log("mem: mirror frame %llu: upload sources per frame, overlaps counted twice: written pages %.3f MiB; flush ranges "
|
||||
"%.2f, %.3f MiB, from %.1f flushes of %.3f MiB", (unsigned long long)frame, a.page_bytes / f / M, a.flush_n / f,
|
||||
a.flush_bytes / f / M, a.flush_calls / f, a.flush_sum / f / M);
|
||||
}
|
||||
|
||||
// With TOTK_TRACE=perf every 600 frames: what holds device-local memory, to see what grows over a long session.
|
||||
@@ -2033,7 +2387,11 @@ void vk_mem_shutdown(void) {
|
||||
plat_mutex_lock(&pool_lock);
|
||||
for (int i = 0; i < pool_index.n; i++) destroy_rec(c, pool_index.e[i].buf);
|
||||
pool_index.n = 0;
|
||||
nm = npend = nwbl = 0; // mirrors went with their pools
|
||||
nm = npend = nwbl = nsnl = snl_copied = 0; // mirrors went with their pools
|
||||
plat_mutex_lock(&snap_pool_lock);
|
||||
for (int i = 0; i < nsnap_pool; i++) vmaDestroyBuffer(c->vma, snap_pool[i].buf, snap_pool[i].alloc);
|
||||
nsnap_pool = 0, snap_pool_bytes = 0;
|
||||
plat_mutex_unlock(&snap_pool_lock);
|
||||
plat_mutex_lock(&shadow_lock);
|
||||
nstouch = nspend = 0; // shadows too
|
||||
s_live_n = s_live_bytes = 0;
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
// TOTK_MV_CHECK=<frames> (test): measures how well DLSS's motion vectors explain the picture, without screenshots or
|
||||
// judging by eye. Every <frames> DLAA frames, vk_dlaa.c mv_pass copies to host memory the scene light target of that
|
||||
// frame and of the frame before (before DLSS, so jittered), and the motion image twice: after the camera pass (camera
|
||||
// only) and after the per-object pass (what DLSS gets). Once the GPU is done, the completion thread compares them:
|
||||
// warp error: the frame before, sampled bilinearly at p + mv + d, against this frame at p, as a relative luminance
|
||||
// difference |a - b| / (a + b) averaged over pixels; d = last frame's jitter minus this frame's (the vectors are
|
||||
// between unjittered positions, the pictures are jittered: content at x is drawn at x + jitter). Lower is better,
|
||||
// and the correct vectors give the lowest. Over every pixel: zero motion, the camera's and the final vectors, and
|
||||
// the final ones with d subtracted or left out (the jitter convention). Over the object pixels (final and camera
|
||||
// differ by more than a quarter pixel): the camera's, the final ones and variants of the object part (final -
|
||||
// camera) scaled by k = -1, 0.5, 1.5, 2 or with one axis negated. The variant with the lowest error names the
|
||||
// correction the object vectors need, if any ("best").
|
||||
// flow: on up to FLOW_MAX object pixels with some texture, the offset whose 5x5 luminance patch of the frame before
|
||||
// matches best, searched over a window around both the camera and the final vector and refined to a sub-pixel by
|
||||
// a parabola: the measured motion. The end-point error of the camera and final vectors (+ d) against it, median in
|
||||
// render pixels, and how often the final vector is the nearer.
|
||||
// output (DLSS's picture, which the vectors are for): on about OUT_SAMPLES pixels picked independently of the vectors,
|
||||
// the picture's own motion is measured with a wide search (12 px around the camera vector); where it differs from
|
||||
// the camera's by over 1.5 px the pixel moves on its own. At each sample, DLSS's output (at the same unjittered
|
||||
// place) against this frame's input, as a relative luminance difference: on the moving samples it grows where
|
||||
// DLSS drags old colour along wrong vectors (ghosting, smearing), on the static ones it is the plain difference
|
||||
// anti-aliasing and upscaling make. Their ratio compares runs with the per-object pass on and off (it does not
|
||||
// depend on the pass's own vectors) and modes.
|
||||
// One "mv_check:" line per check. Disocclusion and changing light add error to every candidate alike, so the comparison
|
||||
// between candidates counts, not the absolute values. Copies and analysis cost a few ms on check frames only.
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "nvn_api.h"
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
#include "vk_dlaa.h"
|
||||
|
||||
#define FLOW_MAX 20000
|
||||
#define OBJ_MIN 0.25f // final and camera vectors differ by more than this (render pixels): an object pixel
|
||||
|
||||
#define OUT_SAMPLES 4000
|
||||
enum { PREV, CUR, MV_CAM, MV_FIN, OUT, NBUF };
|
||||
static struct {
|
||||
int period; // 0 off, -1 not read yet
|
||||
VkBuffer buf[NBUF];
|
||||
VmaAllocation mem[NBUF];
|
||||
uint8_t *map[NBUF];
|
||||
uint64_t cap[NBUF];
|
||||
uint32_t w, h, ow, oh; // ow, oh: DLSS's output in OUT, 0 none this check
|
||||
int want_out; // the check frame's output is to be copied (vk_mvcheck_output)
|
||||
char masks[400]; // the check frame's motion draws (vk_mvobj_frame_masks)
|
||||
VkFormat cfmt;
|
||||
uint64_t n; // camera-stage calls (DLAA frames with motion)
|
||||
uint64_t prev_frame; // gfx frame the PREV colour is of, 0 none
|
||||
float prev_u[2], u[2], uprev[2];
|
||||
int check; // this frame is a check frame (camera stage copied MV_CAM)
|
||||
int busy; // analysis pending: the buffers are not touched
|
||||
uint64_t done;
|
||||
} K = {.period = -1};
|
||||
|
||||
static int on(void) {
|
||||
if (K.period < 0) {
|
||||
const char *v = cfg_get("TOTK_MV_CHECK");
|
||||
K.period = v && *v ? atoi(v) : 0;
|
||||
if (K.period < 0) K.period = 0;
|
||||
if (K.period && K.period < 4) K.period = 4;
|
||||
if (K.period) rt_log("mv_check: motion vectors measured every %d frames", K.period);
|
||||
}
|
||||
return K.period > 0;
|
||||
}
|
||||
|
||||
static int cbytes(VkFormat f) {
|
||||
return f == VK_FORMAT_B10G11R11_UFLOAT_PACK32 ? 4 : f == VK_FORMAT_R16G16B16A16_SFLOAT ? 8 : 0;
|
||||
}
|
||||
|
||||
// Host buffers of at least the size asked for; a larger size gets new ones and leaves the old (test only).
|
||||
static int ensure(int i, uint64_t bytes) {
|
||||
if (K.cap[i] >= bytes) return 1;
|
||||
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = bytes,
|
||||
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO,
|
||||
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
|
||||
VmaAllocationInfo info;
|
||||
if (vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &K.buf[i], &K.mem[i], &info) != VK_SUCCESS || !info.pMappedData) {
|
||||
rt_log("mv_check: host buffer of %llu bytes failed: off", (unsigned long long)bytes);
|
||||
K.period = 0;
|
||||
return 0;
|
||||
}
|
||||
K.map[i] = info.pMappedData, K.cap[i] = bytes;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void barrier(VkCommandBuffer cb) {
|
||||
VkMemoryBarrier2 m = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_2_HOST_BIT,
|
||||
.dstAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT |
|
||||
VK_ACCESS_2_HOST_READ_BIT};
|
||||
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &m};
|
||||
vkCmdPipelineBarrier2(cb, &di);
|
||||
}
|
||||
|
||||
static void copy(VkCommandBuffer cb, int i, VkImage img, uint32_t level, uint32_t layer, uint32_t w, uint32_t h) {
|
||||
barrier(cb);
|
||||
VkBufferImageCopy r = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1}, .imageExtent = {w, h, 1}};
|
||||
vkCmdCopyImageToBuffer(cb, img, VK_IMAGE_LAYOUT_GENERAL, K.buf[i], 1, &r);
|
||||
barrier(cb);
|
||||
}
|
||||
|
||||
// ---- decoding
|
||||
|
||||
static float half(uint16_t v) {
|
||||
uint32_t s = (uint32_t)(v >> 15) << 31, e = v >> 10 & 31, m = v & 1023, f;
|
||||
if (!e) return (s ? -1.0f : 1.0f) * ldexpf((float)m, -24);
|
||||
f = e == 31 ? s | 0x7f800000u | m << 13 : s | (e + 112) << 23 | m << 13;
|
||||
float r;
|
||||
memcpy(&r, &f, 4);
|
||||
return r;
|
||||
}
|
||||
|
||||
static float ufloat(uint32_t v, int mbits) { // unsigned 5-bit exponent mini-float
|
||||
uint32_t e = v >> mbits, m = v & ((1u << mbits) - 1);
|
||||
if (e == 31) return 65504.0f;
|
||||
return e ? ldexpf(1.0f + (float)m / (float)(1u << mbits), (int)e - 15) : ldexpf((float)m / (float)(1u << mbits), -14);
|
||||
}
|
||||
|
||||
static void luminance(const uint8_t *src, VkFormat f, uint32_t n, float *out) {
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
float r, g, b;
|
||||
if (f == VK_FORMAT_B10G11R11_UFLOAT_PACK32) {
|
||||
uint32_t v;
|
||||
memcpy(&v, src + 4 * (size_t)i, 4);
|
||||
r = ufloat(v & 0x7ff, 6), g = ufloat(v >> 11 & 0x7ff, 6), b = ufloat(v >> 22, 5);
|
||||
} else {
|
||||
uint16_t c[4];
|
||||
memcpy(c, src + 8 * (size_t)i, 8);
|
||||
r = half(c[0]), g = half(c[1]), b = half(c[2]);
|
||||
}
|
||||
float y = 0.2126f * r + 0.7152f * g + 0.0722f * b;
|
||||
out[i] = isfinite(y) && y > 0 ? y : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- analysis
|
||||
|
||||
typedef struct { const float *cur, *prev; uint32_t w, h; } Pic;
|
||||
|
||||
static int sample(const Pic *p, float x, float y, float *out) { // bilinear, pixel index coordinates
|
||||
if (!(x >= 0 && y >= 0 && x <= (float)(p->w - 1) && y <= (float)(p->h - 1))) return 0;
|
||||
uint32_t x0 = (uint32_t)x, y0 = (uint32_t)y, x1 = x0 + 1 < p->w ? x0 + 1 : x0, y1 = y0 + 1 < p->h ? y0 + 1 : y0;
|
||||
float fx = x - (float)x0, fy = y - (float)y0;
|
||||
const float *a = p->prev + (size_t)y0 * p->w, *b = p->prev + (size_t)y1 * p->w;
|
||||
*out = (a[x0] * (1 - fx) + a[x1] * fx) * (1 - fy) + (b[x0] * (1 - fx) + b[x1] * fx) * fy;
|
||||
return 1;
|
||||
}
|
||||
|
||||
typedef struct { double sum; uint64_t n; } Acc;
|
||||
static void acc(Acc *a, const Pic *p, uint32_t x, uint32_t y, float mx, float my) {
|
||||
float b;
|
||||
if (!sample(p, (float)x + mx, (float)y + my, &b)) return;
|
||||
float c = p->cur[(size_t)y * p->w + x];
|
||||
a->sum += fabsf(c - b) / (c + b + 1e-4f), a->n++;
|
||||
}
|
||||
static double mean(const Acc *a) { return a->n ? a->sum / (double)a->n : NAN; }
|
||||
|
||||
static float sad5(const Pic *p, uint32_t x, uint32_t y, int dx, int dy) {
|
||||
float s = 0;
|
||||
for (int j = -2; j <= 2; j++)
|
||||
for (int i = -2; i <= 2; i++) {
|
||||
int px = (int)x + i + dx, py = (int)y + j + dy;
|
||||
if (px < 0 || py < 0 || px >= (int)p->w || py >= (int)p->h) return INFINITY;
|
||||
float c = p->cur[(size_t)(y + j) * p->w + x + i], b = p->prev[(size_t)py * p->w + px];
|
||||
s += fabsf(c - b) / (c + b + 1e-4f);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static float parabola(float l, float c, float r) { // offset of the minimum of three samples at -1, 0, 1
|
||||
float den = l - 2 * c + r;
|
||||
if (!(den > 1e-9f) || !isfinite(l) || !isfinite(r)) return 0;
|
||||
float o = 0.5f * (l - r) / den;
|
||||
return o < -0.5f ? -0.5f : o > 0.5f ? 0.5f : o; // a flat curve says nothing past the neighbours
|
||||
}
|
||||
|
||||
static int by_float(const void *a, const void *b) {
|
||||
float x = *(const float *)a, y = *(const float *)b;
|
||||
return x < y ? -1 : x > y;
|
||||
}
|
||||
|
||||
// The object pixels' measured motion (sub-pixel), searched over the box around both vectors with a margin of 3;
|
||||
// 0 when the patch is flat or the match is not clearly better than the box's average.
|
||||
static int flow(const Pic *p, uint32_t x, uint32_t y, float ax, float ay, float bx, float by, float out[2]) {
|
||||
float mu = 0, var = 0;
|
||||
for (int j = -2; j <= 2; j++)
|
||||
for (int i = -2; i <= 2; i++) mu += p->cur[(size_t)(y + j) * p->w + x + i];
|
||||
mu /= 25;
|
||||
for (int j = -2; j <= 2; j++)
|
||||
for (int i = -2; i <= 2; i++) {
|
||||
float d = p->cur[(size_t)(y + j) * p->w + x + i] - mu;
|
||||
var += d * d;
|
||||
}
|
||||
if (mu <= 0 || sqrtf(var / 25) < 0.05f * mu) return 0;
|
||||
int x0 = (int)floorf(fminf(ax, bx)) - 3, x1 = (int)ceilf(fmaxf(ax, bx)) + 3;
|
||||
int y0 = (int)floorf(fminf(ay, by)) - 3, y1 = (int)ceilf(fmaxf(ay, by)) + 3;
|
||||
if (x1 - x0 > 40 || y1 - y0 > 40) return 0;
|
||||
float best = INFINITY, total = 0;
|
||||
int bdx = 0, bdy = 0, n = 0;
|
||||
for (int dy = y0; dy <= y1; dy++)
|
||||
for (int dx = x0; dx <= x1; dx++) {
|
||||
float s = sad5(p, x, y, dx, dy);
|
||||
if (!isfinite(s)) continue;
|
||||
total += s, n++;
|
||||
if (s < best) best = s, bdx = dx, bdy = dy;
|
||||
}
|
||||
if (!n || !isfinite(best) || best > 0.5f * total / (float)n) return 0;
|
||||
out[0] = (float)bdx + parabola(sad5(p, x, y, bdx - 1, bdy), best, sad5(p, x, y, bdx + 1, bdy));
|
||||
out[1] = (float)bdy + parabola(sad5(p, x, y, bdx, bdy - 1), best, sad5(p, x, y, bdx, bdy + 1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Totals over every check (logged every TOTAL_EVERY checks): end-point error histograms in HBIN px bins of the object
|
||||
// samples (camera, final) and of the static samples (the measurement's own noise floor), and the object warp errors.
|
||||
#define HBIN 0.05f
|
||||
#define HN 640
|
||||
#define TOTAL_EVERY 20
|
||||
#define KN 120 // histogram of per-sample scale ratios, -2 to 4 in steps of 0.05
|
||||
enum { H_CAM, H_FIN, H_STATIC, NH };
|
||||
static struct { uint64_t h[NH][HN], n[NH], checks, wrong, kh[KN], kn; double obj[2], objn, knum, kden, out[2], outn[2]; uint64_t cal[3]; } T; // cal: checks where k=0.5, k=1 fit the static pixels best; checks
|
||||
|
||||
static void hist_add(int k, float e) {
|
||||
int b = e >= 0 && e < HBIN * HN ? (int)(e / HBIN) : HN - 1; // NaN and huge errors in the last bin
|
||||
T.h[k][b < HN ? b : HN - 1]++, T.n[k]++;
|
||||
}
|
||||
static float hist_q(int k, double q) { // quantile q of histogram k, bin centre
|
||||
uint64_t want = (uint64_t)(q * (double)T.n[k]), s = 0;
|
||||
for (int b = 0; b < HN; b++)
|
||||
if ((s += T.h[k][b]) > want) return ((float)b + 0.5f) * HBIN;
|
||||
return NAN;
|
||||
}
|
||||
|
||||
static float kmedian(void) {
|
||||
uint64_t s = 0;
|
||||
for (int b = 0; b < KN; b++)
|
||||
if ((s += T.kh[b]) * 2 > T.kn) return -2 + ((float)b + 0.5f) * 0.05f;
|
||||
return NAN;
|
||||
}
|
||||
|
||||
static uint32_t mix(uint32_t x, uint32_t y) { // a fixed pseudo-random pick of pixels, spread over the picture
|
||||
uint32_t v = x * 0x9E3779B1u ^ y * 0x85EBCA77u;
|
||||
v ^= v >> 15, v *= 0x2C1B3C6Du, v ^= v >> 12;
|
||||
return v;
|
||||
}
|
||||
|
||||
static void analyse(void *arg) {
|
||||
(void)arg;
|
||||
VmaAllocator vma = vk_ctx()->vma;
|
||||
for (int i = 0; i < NBUF; i++) vmaInvalidateAllocation(vma, K.mem[i], 0, VK_WHOLE_SIZE);
|
||||
uint32_t w = K.w, h = K.h, n = w * h;
|
||||
float *cur = malloc(sizeof(float) * n), *prev = malloc(sizeof(float) * n), *e_cam = NULL, *e_fin = NULL,
|
||||
*e_st = NULL;
|
||||
if (!cur || !prev) goto out;
|
||||
luminance(K.map[CUR], K.cfmt, n, cur);
|
||||
luminance(K.map[PREV], K.cfmt, n, prev);
|
||||
Pic p = {cur, prev, w, h};
|
||||
const uint16_t *A = (const uint16_t *)K.map[MV_CAM], *B = (const uint16_t *)K.map[MV_FIN];
|
||||
float dx = K.uprev[0] - K.u[0], dy = K.uprev[1] - K.u[1];
|
||||
enum { ZERO, CAM, FIN, FIN_NEG_D, FIN_NO_D, NALL };
|
||||
enum { O_CAM, O_FIN, O_KM1, O_K05, O_K15, O_K2, O_NX, O_NY, NOBJ };
|
||||
static const char *const oname[NOBJ] = {"camera", "final", "k=-1", "k=0.5", "k=1.5", "k=2", "-x", "-y"};
|
||||
Acc all[NALL] = {0}, obj[NOBJ] = {0}, cal[3] = {0};
|
||||
// the object pixels first, for the sampling rates: about FLOW_MAX object and FLOW_MAX / 4 static samples
|
||||
uint64_t nobj = 0, huge = 0; // huge: camera vectors over 256 px or not finite
|
||||
double cam_len = 0;
|
||||
for (uint32_t y = 2; y + 2 < h; y++)
|
||||
for (uint32_t x = 2; x + 2 < w; x++) {
|
||||
size_t i = (size_t)y * w + x;
|
||||
float ax = half(A[2 * i]), ay = half(A[2 * i + 1]), bx = half(B[2 * i]), by = half(B[2 * i + 1]);
|
||||
if (isfinite(ax) && isfinite(ay)) cam_len += hypotf(ax, ay);
|
||||
huge += !isfinite(ax) || !isfinite(ay) || hypotf(ax, ay) > 256;
|
||||
nobj += fabsf(bx - ax) > OBJ_MIN || fabsf(by - ay) > OBJ_MIN;
|
||||
}
|
||||
uint32_t rate_obj = nobj > FLOW_MAX ? (uint32_t)(0xffffffffull * FLOW_MAX / nobj) : 0xffffffffu;
|
||||
uint64_t nst = (uint64_t)n - nobj;
|
||||
uint32_t rate_st = nst > FLOW_MAX / 4 ? (uint32_t)(0xffffffffull * (FLOW_MAX / 4) / nst) : 0xffffffffu;
|
||||
e_cam = malloc(sizeof(float) * FLOW_MAX * 2), e_fin = malloc(sizeof(float) * FLOW_MAX * 2),
|
||||
e_st = malloc(sizeof(float) * FLOW_MAX);
|
||||
if (!e_cam || !e_fin || !e_st) goto out;
|
||||
int nflow = 0, nearer = 0, tried = 0, nst_f = 0;
|
||||
double knum = 0, kden = 0;
|
||||
// object samples the final vector misses by more than 1.5 px where the camera's fits better: where and how
|
||||
struct { int n; uint32_t x0, y0, x1, y1; double ox, oy, mx, my; } bad = {0, UINT32_MAX, UINT32_MAX, 0, 0, 0, 0, 0, 0};
|
||||
for (uint32_t y = 2; y + 2 < h; y++)
|
||||
for (uint32_t x = 2; x + 2 < w; x++) {
|
||||
size_t i = (size_t)y * w + x;
|
||||
float ax = half(A[2 * i]), ay = half(A[2 * i + 1]), bx = half(B[2 * i]), by = half(B[2 * i + 1]);
|
||||
if (!isfinite(ax) || !isfinite(ay) || !isfinite(bx) || !isfinite(by)) continue;
|
||||
acc(&all[ZERO], &p, x, y, dx, dy);
|
||||
acc(&all[CAM], &p, x, y, ax + dx, ay + dy);
|
||||
acc(&all[FIN], &p, x, y, bx + dx, by + dy);
|
||||
acc(&all[FIN_NEG_D], &p, x, y, bx - dx, by - dy);
|
||||
acc(&all[FIN_NO_D], &p, x, y, bx, by);
|
||||
float ox = bx - ax, oy = by - ay, f[2];
|
||||
if (fabsf(ox) <= OBJ_MIN && fabsf(oy) <= OBJ_MIN) {
|
||||
// calibration: static pixels with a small camera vector (known right), at k = 0.5, 1, 1.5 of it
|
||||
float al = hypotf(ax, ay);
|
||||
if (al > 0.25f && al < 2) {
|
||||
acc(&cal[0], &p, x, y, 0.5f * ax + dx, 0.5f * ay + dy), acc(&cal[1], &p, x, y, ax + dx, ay + dy);
|
||||
acc(&cal[2], &p, x, y, 1.5f * ax + dx, 1.5f * ay + dy);
|
||||
}
|
||||
if (mix(x, y) > rate_st || nst_f >= FLOW_MAX) continue;
|
||||
if (flow(&p, x, y, ax + dx, ay + dy, ax + dx, ay + dy, f)) e_st[nst_f++] = hypotf(ax + dx - f[0], ay + dy - f[1]);
|
||||
continue;
|
||||
}
|
||||
acc(&obj[O_CAM], &p, x, y, ax + dx, ay + dy);
|
||||
acc(&obj[O_FIN], &p, x, y, bx + dx, by + dy);
|
||||
acc(&obj[O_KM1], &p, x, y, ax - ox + dx, ay - oy + dy);
|
||||
acc(&obj[O_K05], &p, x, y, ax + 0.5f * ox + dx, ay + 0.5f * oy + dy);
|
||||
acc(&obj[O_K15], &p, x, y, ax + 1.5f * ox + dx, ay + 1.5f * oy + dy);
|
||||
acc(&obj[O_K2], &p, x, y, ax + 2 * ox + dx, ay + 2 * oy + dy);
|
||||
acc(&obj[O_NX], &p, x, y, ax - ox + dx, by + dy);
|
||||
acc(&obj[O_NY], &p, x, y, bx + dx, ay - oy + dy);
|
||||
if (mix(x, y) > rate_obj || nflow >= 2 * FLOW_MAX) continue;
|
||||
tried++;
|
||||
if (!flow(&p, x, y, ax + dx, ay + dy, bx + dx, by + dy, f)) continue;
|
||||
float ec = hypotf(ax + dx - f[0], ay + dy - f[1]), ef = hypotf(bx + dx - f[0], by + dy - f[1]);
|
||||
e_cam[nflow] = ec, e_fin[nflow] = ef, nflow++;
|
||||
nearer += ef < ec;
|
||||
if (fabsf(ox) > 0.5f || fabsf(oy) > 0.5f) { // the scale of the object part the measured motion shows
|
||||
float mx = f[0] - ax - dx, my = f[1] - ay - dy, oo = ox * ox + oy * oy, kr = (mx * ox + my * oy) / oo;
|
||||
knum += mx * ox + my * oy, kden += oo;
|
||||
int kb = (int)floorf((kr + 2) / 0.05f);
|
||||
T.kh[kb < 0 ? 0 : kb >= KN ? KN - 1 : kb]++, T.kn++;
|
||||
}
|
||||
if (ef > 1.5f && ef > ec) {
|
||||
bad.n++, bad.ox += ox, bad.oy += oy, bad.mx += f[0] - ax - dx, bad.my += f[1] - ay - dy;
|
||||
if (x < bad.x0) bad.x0 = x;
|
||||
if (y < bad.y0) bad.y0 = y;
|
||||
if (x > bad.x1) bad.x1 = x;
|
||||
if (y > bad.y1) bad.y1 = y;
|
||||
}
|
||||
}
|
||||
int best = 0;
|
||||
for (int k = 1; k < NOBJ; k++)
|
||||
if (obj[k].n && (!obj[best].n || mean(&obj[k]) < mean(&obj[best]))) best = k;
|
||||
char line[512];
|
||||
int o = snprintf(line, sizeof line, "object pixels %.1f%% (%llu):", 100.0 * (double)nobj / (double)n,
|
||||
(unsigned long long)nobj);
|
||||
for (int k = 0; k < NOBJ && o < (int)sizeof line; k++) o += snprintf(line + o, sizeof line - o, " %s %.4f", oname[k], mean(&obj[k]));
|
||||
if (o < (int)sizeof line) snprintf(line + o, sizeof line - o, "; best %s", nobj ? oname[best] : "-");
|
||||
// DLSS's output against this frame's input, on samples moving on their own and static ones (the picture's motion)
|
||||
char outl[200] = "";
|
||||
if (K.ow && K.oh) {
|
||||
float *ol = malloc(sizeof(float) * (size_t)K.ow * K.oh);
|
||||
if (ol) {
|
||||
luminance(K.map[OUT], VK_FORMAT_R16G16B16A16_SFLOAT, K.ow * K.oh, ol);
|
||||
Pic op = {NULL, ol, K.ow, K.oh}; // sample() reads prev
|
||||
float sx = (float)K.ow / (float)w, sy = (float)K.oh / (float)h;
|
||||
uint32_t rate = (uint32_t)(0xffffffffull * OUT_SAMPLES / n);
|
||||
Acc mov = {0}, sta = {0};
|
||||
for (uint32_t y = 2; y + 2 < h; y++)
|
||||
for (uint32_t x = 2; x + 2 < w; x++) {
|
||||
if ((mix(x, y) ^ 0x5bd1e995u) * 0x2C1B3C6Du > rate) continue;
|
||||
size_t i = (size_t)y * w + x;
|
||||
float ax = half(A[2 * i]) + dx, ay = half(A[2 * i + 1]) + dy, f[2], ov;
|
||||
if (!isfinite(ax) || !isfinite(ay) || !flow(&p, x, y, ax - 12, ay - 12, ax + 12, ay + 12, f)) continue;
|
||||
if (!sample(&op, ((float)x + 0.5f - K.u[0]) * sx - 0.5f, ((float)y + 0.5f - K.u[1]) * sy - 0.5f, &ov))
|
||||
continue;
|
||||
float c = cur[i], e = fabsf(ov - c) / (ov + c + 1e-4f);
|
||||
Acc *a = hypotf(f[0] - ax, f[1] - ay) > 1.5f ? &mov : &sta;
|
||||
a->sum += e, a->n++;
|
||||
}
|
||||
snprintf(outl, sizeof outl, "; output against input: moving on their own %.4f (%llu), static %.4f (%llu), "
|
||||
"ratio %.3f", mean(&mov), (unsigned long long)mov.n, mean(&sta), (unsigned long long)sta.n,
|
||||
mean(&mov) / mean(&sta));
|
||||
if (mov.n) T.out[0] += mov.sum, T.outn[0] += (double)mov.n;
|
||||
if (sta.n) T.out[1] += sta.sum, T.outn[1] += (double)sta.n;
|
||||
free(ol);
|
||||
}
|
||||
}
|
||||
float mc = NAN, mf = NAN, ms = NAN;
|
||||
for (int k = 0; k < nflow; k++) hist_add(H_CAM, e_cam[k]), hist_add(H_FIN, e_fin[k]);
|
||||
for (int k = 0; k < nst_f; k++) hist_add(H_STATIC, e_st[k]);
|
||||
if (nflow) {
|
||||
qsort(e_cam, (size_t)nflow, sizeof *e_cam, by_float), qsort(e_fin, (size_t)nflow, sizeof *e_fin, by_float);
|
||||
mc = e_cam[nflow / 2], mf = e_fin[nflow / 2];
|
||||
}
|
||||
if (nst_f) qsort(e_st, (size_t)nst_f, sizeof *e_st, by_float), ms = e_st[nst_f / 2];
|
||||
char badl[200] = "";
|
||||
if (bad.n)
|
||||
snprintf(badl, sizeof badl, "; %d missed (over 1.5 px, camera nearer) in x %u-%u y %u-%u, object part (%.2f %.2f), "
|
||||
"measured (%.2f %.2f)", bad.n, bad.x0, bad.x1, bad.y0, bad.y1, bad.ox / bad.n, bad.oy / bad.n,
|
||||
bad.mx / bad.n, bad.my / bad.n);
|
||||
rt_log("mv_check: #%llu %ux%u, jitter (%.2f %.2f) after (%.2f %.2f), camera motion %.2f px; warp error, every pixel: "
|
||||
"zero %.4f camera %.4f final %.4f (jitter subtracted %.4f, left out %.4f); camera vectors over 256 px %llu; %s; "
|
||||
"flow on %d of %d object "
|
||||
"samples: median error camera %.2f px, final %.2f px, final nearer %.0f%%; static %.2f px (%d); scale %.2f; calibration, static pixels under 2 px of camera motion (%llu): k=0.5 %.4f k=1 %.4f "
|
||||
"k=1.5 %.4f%s%s; motion draws (position banks/changed banks:draws)%s",
|
||||
(unsigned long long)++K.done, w, h, K.u[0], K.u[1], K.uprev[0], K.uprev[1], cam_len / (double)n,
|
||||
mean(&all[ZERO]), mean(&all[CAM]), mean(&all[FIN]), mean(&all[FIN_NEG_D]), mean(&all[FIN_NO_D]), (unsigned long long)huge, line,
|
||||
nflow, tried, mc, mf, nflow ? 100.0 * nearer / nflow : 0.0, ms, nst_f, kden > 0 ? knum / kden : NAN, (unsigned long long)cal[1].n, mean(&cal[0]), mean(&cal[1]), mean(&cal[2]), badl, outl, K.masks);
|
||||
if (cal[1].n > 1000) T.cal[0] += mean(&cal[0]) < mean(&cal[1]) && mean(&cal[0]) < mean(&cal[2]), T.cal[1] += mean(&cal[1]) <= mean(&cal[0]) && mean(&cal[1]) <= mean(&cal[2]), T.cal[2]++;
|
||||
T.knum += knum, T.kden += kden;
|
||||
T.checks++;
|
||||
if (obj[O_FIN].n) {
|
||||
T.obj[0] += obj[O_CAM].sum, T.obj[1] += obj[O_FIN].sum, T.objn += (double)obj[O_FIN].n;
|
||||
T.wrong += nobj * 200 > n && best != O_FIN; // over 0.5 % of the picture and the vectors are not the best fit
|
||||
}
|
||||
if (T.checks % TOTAL_EVERY == 0)
|
||||
rt_log("mv_check total: %llu checks, %llu with over 0.5%% object pixels fit better by a variant; object warp error "
|
||||
"camera %.4f final %.4f; end-point error (median / 90%% / 99%%) object samples camera %.2f / %.2f / %.2f "
|
||||
"px, final %.2f / %.2f / %.2f px (%llu); static samples %.2f / %.2f / %.2f px (%llu); object scale %.3f (median %.2f of %llu); calibration: k=0.5 best in %llu, k=1 in %llu of %llu checks; "
|
||||
"output against input: moving on their own %.4f (%.0f), static %.4f (%.0f), ratio %.3f",
|
||||
(unsigned long long)T.checks, (unsigned long long)T.wrong, T.objn ? T.obj[0] / T.objn : NAN,
|
||||
T.objn ? T.obj[1] / T.objn : NAN, hist_q(H_CAM, 0.5), hist_q(H_CAM, 0.9), hist_q(H_CAM, 0.99),
|
||||
hist_q(H_FIN, 0.5), hist_q(H_FIN, 0.9), hist_q(H_FIN, 0.99), (unsigned long long)T.n[H_FIN],
|
||||
hist_q(H_STATIC, 0.5), hist_q(H_STATIC, 0.9), hist_q(H_STATIC, 0.99), (unsigned long long)T.n[H_STATIC],
|
||||
T.kden > 0 ? T.knum / T.kden : NAN, kmedian(), (unsigned long long)T.kn, (unsigned long long)T.cal[0], (unsigned long long)T.cal[1],
|
||||
(unsigned long long)T.cal[2], T.outn[0] ? T.out[0] / T.outn[0] : NAN, T.outn[0],
|
||||
T.outn[1] ? T.out[1] / T.outn[1] : NAN, T.outn[1],
|
||||
T.outn[0] && T.outn[1] ? (T.out[0] / T.outn[0]) / (T.out[1] / T.outn[1]) : NAN);
|
||||
out:
|
||||
free(cur), free(prev), free(e_cam), free(e_fin), free(e_st);
|
||||
__atomic_store_n(&K.busy, 0, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// ---- capture (vk_dlaa.c mv_pass)
|
||||
|
||||
void vk_mvcheck_camera(VkCommandBuffer cb, VkImage mv, uint32_t w, uint32_t h, float jx, float jy) {
|
||||
K.check = 0;
|
||||
if (!on()) return;
|
||||
K.n++;
|
||||
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
if (K.n % (uint64_t)K.period || __atomic_load_n(&K.busy, __ATOMIC_ACQUIRE) || K.prev_frame + 1 != frame ||
|
||||
K.w != w || K.h != h)
|
||||
return;
|
||||
if (!ensure(MV_CAM, (uint64_t)w * h * 4) || !ensure(MV_FIN, (uint64_t)w * h * 4)) return;
|
||||
copy(cb, MV_CAM, mv, 0, 0, w, h);
|
||||
K.check = 1, K.u[0] = jx, K.u[1] = jy, K.uprev[0] = K.prev_u[0], K.uprev[1] = K.prev_u[1];
|
||||
}
|
||||
|
||||
void vk_mvcheck_final(VkCommandBuffer cb, uint64_t fence, VkImage mv, VkImage cimg, VkFormat cfmt, uint32_t level,
|
||||
uint32_t layer, uint32_t w, uint32_t h, float jx, float jy) {
|
||||
if (!on()) return;
|
||||
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
int b = cbytes(cfmt);
|
||||
int want_prev = (K.n + 1) % (uint64_t)K.period == 0 && !__atomic_load_n(&K.busy, __ATOMIC_ACQUIRE);
|
||||
if (!b) return (void)(K.check = 0, K.prev_frame = 0);
|
||||
if (K.check) {
|
||||
if (!ensure(CUR, (uint64_t)w * h * (uint64_t)b)) return;
|
||||
copy(cb, MV_FIN, mv, 0, 0, w, h);
|
||||
copy(cb, CUR, cimg, level, layer, w, h);
|
||||
K.check = 0, K.prev_frame = 0, K.ow = K.oh = 0, K.want_out = 1;
|
||||
snprintf(K.masks, sizeof K.masks, "%s", vk_mvobj_frame_masks());
|
||||
__atomic_store_n(&K.busy, 1, __ATOMIC_RELEASE);
|
||||
vk_completion_add(fence, analyse, NULL);
|
||||
return;
|
||||
}
|
||||
if (!want_prev) return;
|
||||
if (!ensure(PREV, (uint64_t)w * h * (uint64_t)b)) return;
|
||||
copy(cb, PREV, cimg, level, layer, w, h);
|
||||
K.prev_frame = frame, K.prev_u[0] = jx, K.prev_u[1] = jy, K.w = w, K.h = h, K.cfmt = cfmt;
|
||||
}
|
||||
|
||||
// After DLSS ran on a check frame (vk_dlaa.c evaluate): its output (RGBA16F, ow x oh) for the analysis, which runs once
|
||||
// the submission is done.
|
||||
void vk_mvcheck_output(VkCommandBuffer cb, VkImage out, uint32_t ow, uint32_t oh) {
|
||||
if (!K.want_out) return;
|
||||
K.want_out = 0;
|
||||
if (!ensure(OUT, (uint64_t)ow * oh * 8)) return;
|
||||
copy(cb, OUT, out, 0, 0, ow, oh);
|
||||
K.ow = ow, K.oh = oh;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Generated by tools/make_mvobj_fs.py: the per-object motion fragment shader (vk_mvobj.c). Do not edit.
|
||||
#define MVOBJ_FS_LOCATION_WORD 24 // the input Location literal, patched to the variant's
|
||||
static const uint32_t mvobj_fs_spv[] = {
|
||||
0x07230203, 0x00010000, 0x00000000, 0x00000034, 0x00000000, 0x00020011, 0x00000001, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x0008000f, 0x00000004, 0x00000001, 0x6e69616d, 0x00000000, 0x0000000e,
|
||||
0x0000000f, 0x00000010, 0x00030010, 0x00000001, 0x00000007, 0x00040047, 0x0000000e, 0x0000001e,
|
||||
0x00000000, 0x00040047, 0x0000000f, 0x0000000b, 0x0000000f, 0x00040047, 0x00000010, 0x0000001e,
|
||||
0x00000000, 0x00030047, 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x00000023,
|
||||
0x00000000, 0x00040047, 0x00000011, 0x00000022, 0x00000000, 0x00040047, 0x00000011, 0x00000021,
|
||||
0x00000001, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000004,
|
||||
0x00000020, 0x00040017, 0x00000005, 0x00000004, 0x00000002, 0x00040017, 0x00000006, 0x00000004,
|
||||
0x00000004, 0x00040015, 0x00000007, 0x00000020, 0x00000000, 0x00020014, 0x00000008, 0x00040020,
|
||||
0x00000009, 0x00000001, 0x00000006, 0x00040020, 0x0000000a, 0x00000003, 0x00000006, 0x0003001e,
|
||||
0x0000000b, 0x00000006, 0x00040020, 0x0000000c, 0x00000002, 0x0000000b, 0x00040020, 0x00000017,
|
||||
0x00000002, 0x00000006, 0x0004002b, 0x00000004, 0x00000012, 0x3f000000, 0x0004002b, 0x00000004,
|
||||
0x00000013, 0x00000000, 0x0004002b, 0x00000004, 0x00000014, 0x358637bd, 0x0004002b, 0x00000007,
|
||||
0x00000015, 0x00000000, 0x0007002c, 0x00000006, 0x00000016, 0x00000013, 0x00000013, 0x00000013,
|
||||
0x00000013, 0x0004003b, 0x00000009, 0x0000000e, 0x00000001, 0x0004003b, 0x00000009, 0x0000000f,
|
||||
0x00000001, 0x0004003b, 0x0000000a, 0x00000010, 0x00000003, 0x0004003b, 0x0000000c, 0x00000011,
|
||||
0x00000002, 0x00050036, 0x00000002, 0x00000001, 0x00000000, 0x00000003, 0x000200f8, 0x00000018,
|
||||
0x00050041, 0x00000017, 0x00000019, 0x00000011, 0x00000015, 0x0004003d, 0x00000006, 0x0000001a,
|
||||
0x00000019, 0x0004003d, 0x00000006, 0x0000001b, 0x0000000e, 0x0004003d, 0x00000006, 0x0000001c,
|
||||
0x0000000f, 0x0007004f, 0x00000005, 0x0000001d, 0x0000001c, 0x0000001c, 0x00000000, 0x00000001,
|
||||
0x0007004f, 0x00000005, 0x0000001e, 0x0000001a, 0x0000001a, 0x00000002, 0x00000003, 0x00050083,
|
||||
0x00000005, 0x0000001f, 0x0000001d, 0x0000001e, 0x00050051, 0x00000004, 0x00000020, 0x0000001b,
|
||||
0x00000000, 0x00050051, 0x00000004, 0x00000021, 0x0000001b, 0x00000001, 0x00050051, 0x00000004,
|
||||
0x00000022, 0x0000001b, 0x00000003, 0x00050088, 0x00000004, 0x00000023, 0x00000020, 0x00000022,
|
||||
0x00050088, 0x00000004, 0x00000024, 0x00000021, 0x00000022, 0x00050051, 0x00000004, 0x00000025,
|
||||
0x0000001a, 0x00000000, 0x00050051, 0x00000004, 0x00000026, 0x0000001a, 0x00000001, 0x00050085,
|
||||
0x00000004, 0x00000027, 0x00000023, 0x00000012, 0x00050081, 0x00000004, 0x00000028, 0x00000027,
|
||||
0x00000012, 0x00050085, 0x00000004, 0x00000029, 0x00000028, 0x00000025, 0x00050085, 0x00000004,
|
||||
0x0000002a, 0x00000024, 0x00000012, 0x00050083, 0x00000004, 0x0000002b, 0x00000012, 0x0000002a,
|
||||
0x00050085, 0x00000004, 0x0000002c, 0x0000002b, 0x00000026, 0x00050051, 0x00000004, 0x0000002d,
|
||||
0x0000001f, 0x00000000, 0x00050051, 0x00000004, 0x0000002e, 0x0000001f, 0x00000001, 0x00050083,
|
||||
0x00000004, 0x0000002f, 0x00000029, 0x0000002d, 0x00050083, 0x00000004, 0x00000030, 0x0000002c,
|
||||
0x0000002e, 0x00070050, 0x00000006, 0x00000031, 0x0000002f, 0x00000030, 0x00000013, 0x00000013,
|
||||
0x000500ba, 0x00000008, 0x00000032, 0x00000022, 0x00000014, 0x000600a9, 0x00000006, 0x00000033,
|
||||
0x00000032, 0x00000031, 0x00000016, 0x0003003e, 0x00000010, 0x00000033, 0x000100fd, 0x00010038
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
// Last frame's object and bone blocks, for per-object motion vectors (TOTK_DLAA_OBJMV, on with DLAA). The banks kept
|
||||
// start as c5, c7 and c14 and grow with what the vertex shader variants read (vk_mvprev_want); c7 and c5 are described
|
||||
// here, the others follow the same two-copy rule.
|
||||
//
|
||||
// The scene's vertex shaders read an object's place from bank c7 (gsys_shape: one 3x4 matrix at the start of a
|
||||
// 0x100 block) and a skinned object's bones from c5 (gsys_skeleton: a run of 3x4 matrices), of this frame only. The
|
||||
// game writes both into two copies that take turns, the second right after the first: an object's block is at addr in
|
||||
// one frame and at addr +- step in the next, step its size rounded up to 256 bytes (vk_mvsurvey.c measured it: every
|
||||
// c7 pair 0x100 apart, c5 pairs their rounded size apart, the pair's contents the same object moved a little). The
|
||||
// copy this frame does not write still holds last frame's when the frame is submitted, so the uniform snapshot
|
||||
// (vk_usnap.c) also copies both neighbours of these blocks, and vk_mvprev_find picks last frame's:
|
||||
// self the block was bound at the same address last frame too (an object the game did not write again): itself
|
||||
// side one neighbour was bound in that bank last frame: that one
|
||||
// nearer both were (the copies of adjacent objects interleave): the one with more of its first 16 words equal to
|
||||
// this frame's
|
||||
// none neither: a new object, no motion of its own
|
||||
// The pick is checked against what was snapshotted of that block last frame (intact: the same first CHECK bytes), and
|
||||
// a summary goes to the log every SUMMARY frames:
|
||||
// mv_prev: c<n>: <blocks> blocks: self a %, side b %, nearer c %, none d %; intact e %; moved: still f %, under
|
||||
// one unit g %, more h %
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
#include "vk_dlaa.h"
|
||||
|
||||
#define CHECK 128u
|
||||
#define TABLE (1u << 14)
|
||||
#define SUMMARY 600
|
||||
|
||||
enum { BONES = 2, PLACE = 4, CLOCK = 11, BANKS = 14 }; // NVN binding index: banks c5, c7 and c14; c3..c16
|
||||
|
||||
// 16 bytes, so a table of TABLE entries stays in the cache: every scene draw probes it (the bytes of TOTK_MVPREV_CHECK
|
||||
// live in a table of their own, by entry index, allocated only for the check)
|
||||
typedef struct {
|
||||
uint64_t addr; // 0: empty
|
||||
int index;
|
||||
uint32_t have;
|
||||
} Seen;
|
||||
|
||||
typedef struct { uint64_t blocks, self, side, nearer, none, checked, intact, still, small, big; } Stat;
|
||||
|
||||
static struct {
|
||||
int on; // 1 knob on and DLAA on this frame
|
||||
int knob; // -1 not read
|
||||
Seen *t[2]; // this frame's bound blocks, last frame's
|
||||
uint8_t (*bytes[2])[CHECK]; // TOTK_MVPREV_CHECK: the first CHECK bytes of each entry of t[k], NULL without the check
|
||||
int timed; // TOTK_MVOBJ_TIMES: the summary's times
|
||||
uint32_t frames;
|
||||
Stat st[BANKS]; // per binding index
|
||||
uint64_t last[BANKS]; // the block the last scene draw bound per binding index
|
||||
uint32_t mask; // binding indices kept (vk_mvprev_want): bones, place and clock from the start
|
||||
uint64_t ns_draw, ns_frame; // time in vk_mvprev_draw and vk_mvprev_frame since the last summary
|
||||
} P = {.knob = -1, .mask = 1u << BONES | 1u << PLACE | 1u << CLOCK};
|
||||
|
||||
static uint32_t slot_of(uint64_t addr, int index) {
|
||||
uint64_t h = (addr ^ (uint64_t)index * 0x9E3779B97F4A7C15ull) * 0xff51afd7ed558ccdull;
|
||||
return (uint32_t)(h >> 40) & (TABLE - 1);
|
||||
}
|
||||
|
||||
static Seen *find(Seen *t, uint64_t addr, int index, int add) {
|
||||
for (uint32_t i = slot_of(addr, index), k = 0; k < TABLE; i = (i + 1) & (TABLE - 1), k++) {
|
||||
if (t[i].addr == addr && t[i].index == index) return &t[i];
|
||||
if (!t[i].addr) {
|
||||
if (!add) return NULL;
|
||||
t[i].addr = addr, t[i].index = index;
|
||||
return &t[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int vk_mvprev_on(void) { return P.on > 0; }
|
||||
|
||||
void vk_mvprev_want(uint32_t index_mask) {
|
||||
static int fixed = -1;
|
||||
if (fixed < 0) fixed = !rt_knob("MVPREV_GROW", "1"); // TOTK_MVPREV_GROW=0: c5, c7 and c14 only (test)
|
||||
if (!fixed) P.mask |= index_mask & ((1u << BANKS) - 1);
|
||||
}
|
||||
|
||||
uint64_t vk_mvprev_step(int stage, int index, uint64_t size) {
|
||||
if (P.on <= 0 || stage != 0 || index < 0 || index >= BANKS || !(P.mask >> index & 1)) return 0;
|
||||
return (size + 255) & ~(uint64_t)255;
|
||||
}
|
||||
|
||||
// Translation distance between the first matrices of two blocks (rows' fourth floats).
|
||||
static float moved(const uint8_t *a, const uint8_t *b) {
|
||||
const float *x = (const float *)a, *y = (const float *)b;
|
||||
float dx = x[3] - y[3], dy = x[7] - y[7], dz = x[11] - y[11];
|
||||
return sqrtf(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
int vk_mvprev_find(int index, uint64_t addr, uint64_t size, uint64_t *prev, int *how) {
|
||||
*how = MVPREV_NONE;
|
||||
if (P.on <= 0 || !P.t[1]) return 0;
|
||||
if (find(P.t[1], addr, index, 0)) {
|
||||
*prev = addr, *how = MVPREV_SELF;
|
||||
return 1;
|
||||
}
|
||||
uint64_t step = (size + 255) & ~(uint64_t)255, cand[2];
|
||||
int n = 0;
|
||||
if (find(P.t[1], addr + step, index, 0)) cand[n++] = addr + step;
|
||||
if (addr >= step && find(P.t[1], addr - step, index, 0)) cand[n++] = addr - step;
|
||||
if (!n) return 0;
|
||||
if (n == 2) { // the copies of adjacent objects interleave: the one with more words equal to this frame's
|
||||
const uint32_t *c = (const uint32_t *)vk_usnap_bytes(addr, 64), *a = (const uint32_t *)vk_usnap_bytes(cand[0], 64),
|
||||
*b = (const uint32_t *)vk_usnap_bytes(cand[1], 64);
|
||||
if (!c || !a || !b) return 0;
|
||||
int na = 0, nb = 0;
|
||||
for (int w = 0; w < 16; w++) na += c[w] == a[w], nb += c[w] == b[w];
|
||||
if (nb > na) cand[0] = cand[1];
|
||||
*how = MVPREV_NEARER;
|
||||
} else {
|
||||
*how = MVPREV_SIDE;
|
||||
}
|
||||
*prev = cand[0];
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void mvprev_draw_(VkExec *ex);
|
||||
void vk_mvprev_draw(VkExec *ex) {
|
||||
if (P.on <= 0) return;
|
||||
if (!P.timed) return mvprev_draw_(ex); // the clock reads were 1 % of the render thread
|
||||
uint64_t t0 = kern_now_ns();
|
||||
mvprev_draw_(ex);
|
||||
P.ns_draw += kern_now_ns() - t0;
|
||||
}
|
||||
|
||||
static void mvprev_draw_(VkExec *ex) {
|
||||
const VkExecTarget *z = &ex->depth;
|
||||
if (!z->image || z->width < DLAA_SCENE_MIN_W || z->format != VK_FORMAT_D32_SFLOAT) return;
|
||||
int check = P.bytes[0] != NULL; // TOTK_MVPREV_CHECK=1: the summary's checks (3.6 ms a frame)
|
||||
for (int index = 0; index < BANKS; index++) {
|
||||
if (!(P.mask >> index & 1)) continue;
|
||||
const p_bind_buf *u = &ex->ubo[0][index];
|
||||
if (!u->addr || !u->size || u->addr == P.last[index]) continue; // the next draws keep the block: noted already
|
||||
P.last[index] = u->addr;
|
||||
Seen *k = find(P.t[0], u->addr, index, 1);
|
||||
if (!k || k->have || !check) continue; // else only the address is noted
|
||||
int w = index;
|
||||
uint32_t want = u->size < CHECK ? (uint32_t)u->size : CHECK;
|
||||
const uint8_t *cur = vk_usnap_bytes(u->addr, want);
|
||||
if (!cur) continue;
|
||||
memcpy(P.bytes[0][k - P.t[0]], cur, want), k->have = want;
|
||||
Stat *s = &P.st[w];
|
||||
s->blocks++;
|
||||
uint64_t prev;
|
||||
int how;
|
||||
if (!vk_mvprev_find(index, u->addr, u->size, &prev, &how)) {
|
||||
s->none++;
|
||||
continue;
|
||||
}
|
||||
if (how == MVPREV_SELF) s->self++;
|
||||
else if (how == MVPREV_SIDE) s->side++;
|
||||
else s->nearer++;
|
||||
const Seen *last = find(P.t[1], prev, index, 0);
|
||||
const uint8_t *now = vk_usnap_bytes(prev, want);
|
||||
if (last && now && last->have >= want && want >= 48) {
|
||||
s->checked++;
|
||||
if (!memcmp(now, P.bytes[1][last - P.t[1]], want)) s->intact++;
|
||||
float d = moved(cur, now);
|
||||
if (d < 0.01f) s->still++;
|
||||
else if (d < 1.0f) s->small++;
|
||||
else s->big++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void vk_mvprev_frame(void) {
|
||||
// without DLSS (no NGX, no RTX GPU, dlaa = 0) nothing reads last frame's blocks: P.on stays 0, no tables
|
||||
if (!vk_dlaa_available()) return;
|
||||
if (P.knob < 0) {
|
||||
P.knob = rt_knob("DLAA_OBJMV", "1");
|
||||
if (P.knob) {
|
||||
P.t[0] = calloc(TABLE, sizeof(Seen)), P.t[1] = calloc(TABLE, sizeof(Seen));
|
||||
if (!P.t[0] || !P.t[1]) P.knob = 0;
|
||||
if (P.knob && rt_knob("MVPREV_CHECK", "0")) {
|
||||
P.bytes[0] = calloc(TABLE, CHECK), P.bytes[1] = calloc(TABLE, CHECK);
|
||||
if (!P.bytes[0] || !P.bytes[1]) free(P.bytes[0]), free(P.bytes[1]), P.bytes[0] = P.bytes[1] = NULL;
|
||||
}
|
||||
P.timed = rt_knob("MVOBJ_TIMES", "0");
|
||||
}
|
||||
}
|
||||
int on = P.knob && cfg_bool("TOTK_DLAA");
|
||||
if (on != (P.on > 0)) {
|
||||
P.on = on ? 1 : -1;
|
||||
if (P.t[0]) memset(P.t[0], 0, TABLE * sizeof(Seen)), memset(P.t[1], 0, TABLE * sizeof(Seen));
|
||||
return;
|
||||
}
|
||||
if (P.on <= 0) return;
|
||||
uint64_t t0 = P.timed ? kern_now_ns() : 0;
|
||||
Seen *t = P.t[1];
|
||||
P.t[1] = P.t[0], P.t[0] = t;
|
||||
uint8_t (*b)[CHECK] = P.bytes[1];
|
||||
P.bytes[1] = P.bytes[0], P.bytes[0] = b;
|
||||
memset(P.t[0], 0, TABLE * sizeof(Seen));
|
||||
memset(P.last, 0, sizeof P.last);
|
||||
if (P.timed) P.ns_frame += kern_now_ns() - t0;
|
||||
if (++P.frames % SUMMARY) return;
|
||||
if (P.timed) rt_log("mv_prev: %.3f ms a frame in the draws, %.3f ms at the present", P.ns_draw / 1e6 / SUMMARY,
|
||||
P.ns_frame / 1e6 / SUMMARY);
|
||||
P.ns_draw = P.ns_frame = 0;
|
||||
for (int w = 0; w < BANKS; w++) {
|
||||
Stat *s = &P.st[w];
|
||||
if (!s->blocks) continue;
|
||||
double b = (double)s->blocks, c = s->checked ? (double)s->checked : 1;
|
||||
rt_log("mv_prev: c%d: %llu blocks: self %.1f %%, side %.1f %%, nearer %.1f %%, none %.1f %%; intact %.1f %% of "
|
||||
"%llu; moved: still %.1f %%, under one unit %.1f %%, more %.1f %%",
|
||||
w + 3, (unsigned long long)s->blocks, 100 * s->self / b, 100 * s->side / b, 100 * s->nearer / b,
|
||||
100 * s->none / b, 100 * s->intact / c, (unsigned long long)s->checked, 100 * s->still / c,
|
||||
100 * s->small / c, 100 * s->big / c);
|
||||
}
|
||||
memset(P.st, 0, sizeof P.st);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// Motion-vector survey (TOTK_MV_SURVEY=1, off by default): measures whether per-object motion vectors can be made from
|
||||
// the uniform blocks the scene's draws bind. Per-object vectors need each object's matrices of the previous frame; the
|
||||
// game keeps none (its shaders read gsys_shape, gsys_skeleton and gsys_projection of this frame only), so they would
|
||||
// have to be kept from the previous frame's uniform snapshot (vk_usnap.c). That works when a block's guest address
|
||||
// stands for the same object from one frame to the next, which this survey measures.
|
||||
//
|
||||
// For every scene draw (vertex stage, scene depth bound: D32 at least DLAA_SCENE_MIN_W wide, as vk_dlaa_draw), each user bank
|
||||
// (c3 + index) with a binding is noted: address, size and its first SNAP bytes as of the submit (the snapshot's copy).
|
||||
// At each frame the bank's blocks are compared with the previous frame's and the one before:
|
||||
// repeat1 / repeat2 blocks whose address was bound in that bank one / two frames ago
|
||||
// same / near / far of the blocks repeated from last frame: bytes unchanged / every float within 5 % or 1.0 of
|
||||
// last frame's (the same object, moved a little) / some float further (another object, likely)
|
||||
// The game writes its per-object blocks into two copies that take turns, so a moving object's block is at one address
|
||||
// in even frames and another in odd ones. To find the rule that pairs them, a bank's blocks bound two frames ago but
|
||||
// not last frame (this frame's copy) are sorted by address, and so are last frame's blocks not bound this frame (the
|
||||
// other copy); with equal counts they are paired in order:
|
||||
// pairs, delta the paired blocks and the commonest address differences (this frame's minus last frame's)
|
||||
// psame / pnear / pfar the pairs' contents, as same / near / far above (near: the same object, moved)
|
||||
// unequal frames whose two lists had different lengths (no pairing that frame)
|
||||
// What the bank holds: each block's leading 3x4 matrices (rows of a rotation times a scale: three rows of the same
|
||||
// length, at right angles, finite), counted over the whole block as of the submit:
|
||||
// mats 0 / 1 / 2-7 / 8+ blocks by that count (one: an object's place, gsys_shape; many: bones, gsys_skeleton)
|
||||
// Every SURVEY_FRAMES frames the log gets two lines per bank with the window's totals:
|
||||
// mv_survey: c<n>: <draws> draws, <blocks> blocks (<per frame>), sizes <top three>; repeat1 a %, repeat2 b %;
|
||||
// same c %, near d %, far e %
|
||||
// mv_survey: c<n>: pairs <n> (<unequal> frames unequal), delta <top three>; psame a %, pnear b %, pfar c %;
|
||||
// mats 0 x<n> 1 x<n> 2-7 x<n> 8+ x<n>, at most <m>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
#include "vk_dlaa.h"
|
||||
|
||||
#define BANKS 18
|
||||
#define SNAP 256u
|
||||
#define SURVEY_FRAMES 300
|
||||
#define TABLE (1u << 15) // blocks per frame, all banks; a town frame binds about 10,000 uniform ranges
|
||||
|
||||
typedef struct {
|
||||
uint64_t addr; // 0: empty slot
|
||||
uint32_t size, n;
|
||||
int bank;
|
||||
uint8_t bytes[SNAP];
|
||||
uint32_t have; // bytes of `bytes` that were in the snapshot
|
||||
uint32_t mats; // leading 3x4 matrices of the whole block, 0 when it was not in the snapshot
|
||||
} Block;
|
||||
|
||||
typedef struct {
|
||||
uint64_t draws, blocks, rep1, rep2, same, near, far, unread;
|
||||
uint32_t size[4];
|
||||
uint64_t size_n[4];
|
||||
uint64_t pairs, psame, pnear, pfar, unequal, mats[4];
|
||||
uint32_t mats_max;
|
||||
int64_t delta[4];
|
||||
uint64_t delta_n[4];
|
||||
} Stat;
|
||||
|
||||
static struct {
|
||||
int on;
|
||||
Block *t[3]; // this frame, last frame, the one before
|
||||
const Block **x, **y; // pairing: this frame's copy, last frame's
|
||||
uint32_t frames;
|
||||
Stat st[BANKS];
|
||||
} S;
|
||||
|
||||
static uint32_t slot_of(uint64_t addr, int bank) {
|
||||
uint64_t h = (addr ^ (uint64_t)bank * 0x9E3779B97F4A7C15ull) * 0xff51afd7ed558ccdull;
|
||||
return (uint32_t)(h >> 40) & (TABLE - 1);
|
||||
}
|
||||
|
||||
static Block *find(Block *t, uint64_t addr, int bank, int add) {
|
||||
for (uint32_t i = slot_of(addr, bank), k = 0; k < TABLE; i = (i + 1) & (TABLE - 1), k++) {
|
||||
if (t[i].addr == addr && t[i].bank == bank) return &t[i];
|
||||
if (!t[i].addr) {
|
||||
if (!add) return NULL;
|
||||
t[i].addr = addr, t[i].bank = bank;
|
||||
return &t[i];
|
||||
}
|
||||
}
|
||||
return NULL; // full: the block goes uncounted
|
||||
}
|
||||
|
||||
static void note_size(Stat *s, uint32_t size) {
|
||||
for (int i = 0; i < 4; i++)
|
||||
if (s->size[i] == size || !s->size_n[i]) {
|
||||
s->size[i] = size, s->size_n[i]++;
|
||||
return;
|
||||
}
|
||||
s->size_n[3]++; // a fifth size: counted with the last
|
||||
}
|
||||
|
||||
static void note_delta(Stat *s, int64_t d) {
|
||||
for (int i = 0; i < 4; i++)
|
||||
if (s->delta[i] == d || !s->delta_n[i]) {
|
||||
s->delta[i] = d, s->delta_n[i]++;
|
||||
return;
|
||||
}
|
||||
s->delta_n[3]++;
|
||||
}
|
||||
|
||||
// Leading 3x4 matrices: three rows of about the same length (a uniform scale), at right angles, all finite.
|
||||
static uint32_t lead_mats(const float *f, uint32_t nf) {
|
||||
uint32_t k = 0;
|
||||
for (; (k + 1) * 12 <= nf && k < 4096; k++) {
|
||||
const float *m = f + k * 12;
|
||||
float l[3];
|
||||
for (int r = 0; r < 3; r++) {
|
||||
const float *v = m + r * 4;
|
||||
if (!isfinite(v[0]) || !isfinite(v[1]) || !isfinite(v[2]) || !isfinite(v[3])) return k;
|
||||
l[r] = sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||||
if (l[r] < 1e-3f || l[r] > 1e3f) return k;
|
||||
}
|
||||
if (fabsf(l[1] - l[0]) > 0.02f * l[0] || fabsf(l[2] - l[0]) > 0.02f * l[0]) return k;
|
||||
for (int a = 0; a < 3; a++)
|
||||
for (int b = a + 1; b < 3; b++) {
|
||||
const float *u = m + a * 4, *v = m + b * 4;
|
||||
if (fabsf(u[0] * v[0] + u[1] * v[1] + u[2] * v[2]) > 0.02f * l[a] * l[b]) return k;
|
||||
}
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
static int by_bank_addr(const void *a, const void *b) {
|
||||
const Block *x = *(const Block *const *)a, *y = *(const Block *const *)b;
|
||||
if (x->bank != y->bank) return x->bank < y->bank ? -1 : 1;
|
||||
return x->addr < y->addr ? -1 : x->addr > y->addr;
|
||||
}
|
||||
|
||||
static void survey_init(void) {
|
||||
S.on = rt_knob("MV_SURVEY", "0") ? 1 : -1;
|
||||
if (S.on < 0) return;
|
||||
for (int i = 0; i < 3; i++) S.t[i] = calloc(TABLE, sizeof(Block));
|
||||
S.x = malloc(TABLE * sizeof *S.x), S.y = malloc(TABLE * sizeof *S.y);
|
||||
if (!S.t[0] || !S.t[1] || !S.t[2] || !S.x || !S.y) {
|
||||
S.on = -1;
|
||||
return;
|
||||
}
|
||||
rt_log("mv_survey: on (scene draws' uniform blocks, a summary every %d frames)", SURVEY_FRAMES);
|
||||
}
|
||||
|
||||
void vk_mvsurvey_draw(VkExec *ex) {
|
||||
if (!S.on) survey_init();
|
||||
if (S.on < 0) return;
|
||||
const VkExecTarget *z = &ex->depth;
|
||||
if (!z->image || z->width < DLAA_SCENE_MIN_W || z->format != VK_FORMAT_D32_SFLOAT) return;
|
||||
for (int b = 0; b < BANKS; b++) {
|
||||
const p_bind_buf *u = &ex->ubo[0][b];
|
||||
if (!u->addr || !u->size) continue;
|
||||
S.st[b].draws++;
|
||||
Block *k = find(S.t[0], u->addr, b, 1);
|
||||
if (!k) continue;
|
||||
if (k->n++) continue;
|
||||
k->size = (uint32_t)u->size;
|
||||
uint32_t want = u->size < SNAP ? (uint32_t)u->size : SNAP;
|
||||
VkDescriptorBufferInfo bi;
|
||||
const uint8_t *src = vk_usnap_find(u->addr, want, 1, &bi) ? vk_usnap_map() + bi.offset : NULL;
|
||||
k->have = src ? want : 0;
|
||||
if (src) memcpy(k->bytes, src, want);
|
||||
const uint8_t *all = vk_usnap_find(u->addr, u->size, 1, &bi) ? vk_usnap_map() + bi.offset : NULL;
|
||||
k->mats = all ? lead_mats((const float *)all, (uint32_t)(u->size / 4)) : 0;
|
||||
if (all) {
|
||||
Stat *s = &S.st[b];
|
||||
s->mats[k->mats == 0 ? 0 : k->mats == 1 ? 1 : k->mats < 8 ? 2 : 3]++;
|
||||
if (k->mats > s->mats_max) s->mats_max = k->mats;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Same object moved a little: every float within 5 % or 1.0 of last frame's.
|
||||
static int compare(const Block *a, const Block *b) {
|
||||
uint32_t n = a->have < b->have ? a->have : b->have;
|
||||
if (!n) return -1;
|
||||
if (!memcmp(a->bytes, b->bytes, n)) return 0;
|
||||
const float *x = (const float *)a->bytes, *y = (const float *)b->bytes;
|
||||
for (uint32_t i = 0; i < n / 4; i++) {
|
||||
float d = fabsf(x[i] - y[i]);
|
||||
if (isnan(d) || (d > 1.0f && d > 0.05f * fabsf(y[i]))) return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void vk_mvsurvey_frame(void) {
|
||||
if (S.on <= 0) return;
|
||||
for (uint32_t i = 0; i < TABLE; i++) {
|
||||
const Block *k = &S.t[0][i];
|
||||
if (!k->addr) continue;
|
||||
Stat *s = &S.st[k->bank];
|
||||
s->blocks++;
|
||||
note_size(s, k->size);
|
||||
const Block *p = find(S.t[1], k->addr, k->bank, 0);
|
||||
if (p) {
|
||||
s->rep1++;
|
||||
int c = compare(k, p);
|
||||
if (c == 0) s->same++;
|
||||
else if (c == 1) s->near++;
|
||||
else if (c == 2) s->far++;
|
||||
else s->unread++;
|
||||
}
|
||||
if (find(S.t[2], k->addr, k->bank, 0)) s->rep2++;
|
||||
}
|
||||
// pairing of the two copies, bank by bank
|
||||
uint32_t nx = 0, ny = 0;
|
||||
for (uint32_t i = 0; i < TABLE; i++) {
|
||||
const Block *k = &S.t[0][i];
|
||||
if (k->addr && !find(S.t[1], k->addr, k->bank, 0) && find(S.t[2], k->addr, k->bank, 0)) S.x[nx++] = k;
|
||||
k = &S.t[1][i];
|
||||
if (k->addr && !find(S.t[0], k->addr, k->bank, 0)) S.y[ny++] = k;
|
||||
}
|
||||
qsort(S.x, nx, sizeof *S.x, by_bank_addr);
|
||||
qsort(S.y, ny, sizeof *S.y, by_bank_addr);
|
||||
for (uint32_t i = 0, j = 0; i < nx;) {
|
||||
int b = S.x[i]->bank;
|
||||
uint32_t i1 = i, j0, j1;
|
||||
while (i1 < nx && S.x[i1]->bank == b) i1++;
|
||||
while (j < ny && S.y[j]->bank < b) j++;
|
||||
for (j0 = j1 = j; j1 < ny && S.y[j1]->bank == b;) j1++;
|
||||
Stat *s = &S.st[b];
|
||||
if (i1 - i != j1 - j0) s->unequal++;
|
||||
else
|
||||
for (uint32_t a = i, c = j0; a < i1; a++, c++) {
|
||||
s->pairs++;
|
||||
note_delta(s, (int64_t)(S.x[a]->addr - S.y[c]->addr));
|
||||
int r = compare(S.x[a], S.y[c]);
|
||||
if (r == 0) s->psame++;
|
||||
else if (r == 1) s->pnear++;
|
||||
else if (r == 2) s->pfar++;
|
||||
}
|
||||
i = i1, j = j1;
|
||||
}
|
||||
Block *old = S.t[2];
|
||||
S.t[2] = S.t[1], S.t[1] = S.t[0], S.t[0] = old;
|
||||
memset(S.t[0], 0, TABLE * sizeof(Block));
|
||||
if (++S.frames % SURVEY_FRAMES) return;
|
||||
for (int b = 0; b < BANKS; b++) {
|
||||
Stat *s = &S.st[b];
|
||||
if (!s->blocks) continue;
|
||||
double bl = (double)s->blocks, r1 = s->rep1 ? (double)s->rep1 : 1;
|
||||
rt_log("mv_survey: c%d: %llu draws, %llu blocks (%.0f a frame), sizes %#x x%llu %#x x%llu %#x x%llu; repeat1 %.1f %%, "
|
||||
"repeat2 %.1f %%; same %.1f %%, near %.1f %%, far %.1f %%, unread %.1f %%",
|
||||
b + 3, (unsigned long long)s->draws, (unsigned long long)s->blocks, bl / SURVEY_FRAMES, s->size[0],
|
||||
(unsigned long long)s->size_n[0], s->size[1], (unsigned long long)s->size_n[1], s->size[2],
|
||||
(unsigned long long)s->size_n[2], 100 * s->rep1 / bl, 100 * s->rep2 / bl, 100 * s->same / r1,
|
||||
100 * s->near / r1, 100 * s->far / r1, 100 * s->unread / r1);
|
||||
double pr = s->pairs ? (double)s->pairs : 1;
|
||||
rt_log("mv_survey: c%d: pairs %llu (%llu frames unequal), delta %#llx x%llu %#llx x%llu %#llx x%llu; psame %.1f %%, "
|
||||
"pnear %.1f %%, pfar %.1f %%; mats 0 x%llu 1 x%llu 2-7 x%llu 8+ x%llu, at most %u",
|
||||
b + 3, (unsigned long long)s->pairs, (unsigned long long)s->unequal, (unsigned long long)s->delta[0],
|
||||
(unsigned long long)s->delta_n[0], (unsigned long long)s->delta[1], (unsigned long long)s->delta_n[1],
|
||||
(unsigned long long)s->delta[2], (unsigned long long)s->delta_n[2], 100 * s->psame / pr,
|
||||
100 * s->pnear / pr, 100 * s->pfar / pr, (unsigned long long)s->mats[0], (unsigned long long)s->mats[1],
|
||||
(unsigned long long)s->mats[2], (unsigned long long)s->mats[3], s->mats_max);
|
||||
}
|
||||
memset(S.st, 0, sizeof S.st);
|
||||
}
|
||||
@@ -1913,6 +1913,8 @@ static void rec_fill(KeyRec *r, const VkPipeState *s) {
|
||||
// A pipeline was built for a draw: queue its record for the file. Caller holds P.lock.
|
||||
static void keys_note(const VkPipeState *s) {
|
||||
if (!A.on || !K.path[0]) return;
|
||||
static const uint8_t none[32];
|
||||
if (s->fs && !memcmp(gfx_shader_input_id(s->fs), none, 32)) return; // the runtime's own shader (gfx_shader_own)
|
||||
KeyRec r;
|
||||
rec_fill(&r, s);
|
||||
if (!seen_add(rec_hash(&r))) return;
|
||||
|
||||
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user