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.
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Target branch: dev. Rules: CONTRIBUTING.md, "Pull requests". Write each section in your own words about this diff;
|
||||
delete the rows that do not apply. Never attach game files, keys, extracted data or logs that contain them.
|
||||
-->
|
||||
|
||||
## Problem
|
||||
|
||||
<!-- What the player sees or what is wrong in the code. Issue link if there is one. -->
|
||||
|
||||
## Change
|
||||
|
||||
<!-- What the diff does and why this way. Name the switch (totk.ini setting or TOTK_* variable) that turns it off. -->
|
||||
|
||||
## Where it reproduces
|
||||
|
||||
<!-- OS, CPU, GPU and driver, game update (1.2.1 / 1.4.3), settings. Hardware it cannot appear on, if known. -->
|
||||
|
||||
## Proof
|
||||
|
||||
<!-- Before and after on the same repro: screenshots, log lines, a test that failed and now passes. -->
|
||||
|
||||
## Checks run
|
||||
|
||||
<!-- The rows of CONTRIBUTING.md "Checks by what you changed" that match the diff, each with its result. -->
|
||||
|
||||
- [ ] `python tools/check_content.py --staged`
|
||||
- [ ] `totk.exe --selftest`
|
||||
|
||||
## Measurements
|
||||
|
||||
<!-- Frame path, timing or scheduling only. Per run, switch off and on, at least two each: average fps, 1% low or
|
||||
worst frame, `perf: slow frame` count. Scene, save, seconds, CPU, GPU, driver, resolution. -->
|
||||
|
||||
## Not run
|
||||
|
||||
<!-- Paths you could not run (Linux, HDR, a vendor you do not have) and how they are gated. -->
|
||||
@@ -29,6 +29,15 @@ This repository is public. Game data lives outside it and every derived file sta
|
||||
- Graphics bugs: [docs/maxwell.md](docs/maxwell.md) lists the Maxwell behaviour the Vulkan path must match and the TotK bugs other ports fixed.
|
||||
- Comments say what the code does and why, in words that stand on their own. Cite our code by file plus function (a line number may follow as a hint) and upstream code by project, file and function: `refs/` moves, so its line numbers rot. Commit hashes belong to the attribution of copied code.
|
||||
|
||||
## Running and debugging
|
||||
|
||||
- Running the game, screenshots, frame dumps, timings: [docs/agents/running.md](docs/agents/running.md).
|
||||
- Investigating a bug, glitch or crash: search [docs/agents/symptoms.md](docs/agents/symptoms.md) by the player's words first, then follow [docs/agents/debugging.md](docs/agents/debugging.md). Read the code before running the game.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Preparing a pull request: target `dev`; the checks by changed area, the proof and the performance runs it carries are in [CONTRIBUTING.md](CONTRIBUTING.md), "Pull requests". Reviewing one: [docs/pr-review-kit/](docs/pr-review-kit/).
|
||||
|
||||
## Writing
|
||||
|
||||
Code, comments, docs and commit messages are in English. Commit messages are plain sentences without type prefixes.
|
||||
+86
-7
@@ -29,14 +29,93 @@ Use the issue form. Include the port version and game update from the title line
|
||||
|
||||
## Pull requests
|
||||
|
||||
- One topic per pull request.
|
||||
- Describe the problem, then what changed.
|
||||
- Say how you tested it: which version, which scene, how long.
|
||||
- A change to the lifter (`recomp/`) or the CPU headers passes the differential tests in [docs/building.md](docs/building.md), "Checks".
|
||||
- Commit messages are plain sentences, without `feat:` or other prefixes.
|
||||
- AI-assisted code is welcome. You are responsible for having read and run it.
|
||||
### Branch
|
||||
|
||||
Open pull requests against `dev`. Branch from the current `dev` and rebase on it before you open the pull request. `main` receives `dev` at a release; a pull request against `main` is retargeted to `dev` before review.
|
||||
|
||||
### Before you start
|
||||
|
||||
- Search the open pull requests and issues on the topic. When another pull request already covers it, improve that one or say in yours how they differ.
|
||||
- Fixes are reviewed before features, in order of how many players the bug hits. A feature with a design in [docs/milestones/](docs/milestones/) (mods, cheats, the menu) follows that design. For a feature without one, open an issue first.
|
||||
- Find out where the bug reproduces: OS, GPU vendor and generation, driver, game update, settings. Put that in the description.
|
||||
|
||||
### What the change must keep
|
||||
|
||||
- **The default for everyone.** A fix for one setup (a GPU vendor, a memory size, a display) is gated on that setup. Players without the problem see no change.
|
||||
- **A switch.** New behaviour on the frame path or in rendering sits behind a `totk.ini` setting or a `TOTK_*` variable, so a player can turn it off and a reviewer can compare. A new setting is described in [totk.ini.example](totk.ini.example) and [docs/settings.md](docs/settings.md).
|
||||
- **The fix, not a disabled feature.** A crash that goes away with a feature off points at the bug in that feature. Turning the feature off for a vendor is not a fix.
|
||||
- **The frame time on weak hardware.** Game threads and the render thread do no file I/O, no GPU allocations, no per-draw state copies and no global atomic counters. Memory kept per frame or per object is freed. The full list with measured costs: [docs/pr-review-kit/02-performance.md](docs/pr-review-kit/02-performance.md), "What the hot path must not do".
|
||||
- **Other GPUs.** New Vulkan code queries each feature, extension, format and limit it uses and keeps a working path without it. Nothing is hardcoded from what one GPU reports.
|
||||
- **Text in the catalogs.** Text a player sees gets an id in `runtime/ui_text.h` and an entry in `runtime/ui_text.c` for every menu language (English, Russian, Brazilian Portuguese, Simplified Chinese); installer text goes into `installer/text/*.json`.
|
||||
- **The generator owns generated C.** Change `recomp/`, never `generated/`.
|
||||
|
||||
### Checks by what you changed
|
||||
|
||||
Run every row that matches your diff and list the results in the description. Commands and details: [docs/building.md](docs/building.md), "Checks".
|
||||
|
||||
| You changed | Run |
|
||||
|---|---|
|
||||
| anything | `python tools/check_content.py --staged`; `build\<ver>\totk.exe --selftest` |
|
||||
| `recomp/`, `runtime/cpu.h`, `runtime/simd.h` | the differential tests against Unicorn: `tests\diff\lift.py`, `simd.py`, `atomic.py`, `func.py`, `tests\flags_check.py`, `tests\excl_mt.py`; the same with a `--cpu v2` build |
|
||||
| `recomp/jumptables.py` | `tests\jumptables_check.py`: 0 disagreements on 1.2.1 and 1.4.3 |
|
||||
| `runtime/` | `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex`, `--probe-shader`, plus the probe for the file you touched (list at the top of `runtime/main.c`) |
|
||||
| settings | `totk.exe --probe-cfg`; `python tests\settings_doc_check.py` |
|
||||
| button prompts | `.venv-py314\Scripts\python tests\romfs_prompts_test.py` |
|
||||
| mods, ExeFS patches | `.venv\Scripts\python tests\exefs_patches_test.py`; `totk.exe --probe-mods` |
|
||||
| `installer/` | `python tests\installer_test.py`; `python tests\installer_gui_test.py` |
|
||||
| shader translator | `tools\xlate_smoke.py`; `tools\shader_census.py --version <ver> --translate` with no new failure groups |
|
||||
| rendering | a run in a scene where the change acts, with `TOTK_VK_VALIDATION=1`: no new validation messages; screenshots before and after |
|
||||
| frame path, timing, scheduling | the measurements below |
|
||||
|
||||
Checks that need game files run on your own copy of the game. Results go into the pull request as text: pass or fail, counts, log lines. Never attach logs or dumps that contain game data.
|
||||
|
||||
### Proof
|
||||
|
||||
- **A bug fix** shows the bug before the change and gone after it, on the same repro: a screenshot pair, a log line, a probe or test that fails before and passes after. A scripted repro (`TOTK_INPUT_RECORD`, [docs/controls.md](docs/controls.md), "Scripted input") is the strongest. If the bug cannot be reproduced on your hardware, say so and point at the code that causes it.
|
||||
- **A crash fix** names the crash: the log's crash report, `vk: pass marks` lines for a lost device.
|
||||
- **Parts you could not run** (Linux-only code, HDR, vendor-only paths) are listed as unrun and gated away from everyone else.
|
||||
|
||||
### Performance measurements
|
||||
|
||||
A change on the frame path, in timing or in scheduling carries these numbers, or it waits until someone measures it.
|
||||
|
||||
1. **Ceiling first.** Before the work, estimate the share of the frame the changed code holds on the critical path (`TOTK_CRIT` + `tools/crit_path.py`). A ceiling of a few percent is not worth a hot-path change.
|
||||
2. **Build 1.4.3** with prepared shaders in that build folder (`tools/prepare_shaders.py`, [docs/building.md](docs/building.md) step 6). Caches copied from another build folder do not count.
|
||||
3. **A heavy scene** where the change acts: a fight with a large enemy and a crowd, or a town, with `TOTK_FPS_LIMIT=360`.
|
||||
4. **Whole runs, switch off and on, at least two each**, the same save and the same input script, timed by game seconds:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\run_game.py build\v143 --script <input.txt> --seconds 60 --quiet --env TOTK_TRACE=perf,sched --env TOTK_FPS_LIMIT=360 --env <SWITCH>=0
|
||||
```
|
||||
|
||||
`--quiet` waits until no other game or build runs. Close everything heavy on the machine. Before reading the numbers, check that each run reached the world and showed the same scene.
|
||||
5. **Report per run**: average fps, 1% low or worst frame, the number of `perf: slow frame` lines, VRAM; `tools/perf_summary.py` condenses the log. Add CPU, GPU, driver, RAM, resolution and the settings you changed.
|
||||
|
||||
A switch toggled inside one run on a moving scene gives false results: compare whole runs. Zero new slow frames is a hard bar; an fps gain does not buy back new hitches. Background and the reasons behind each rule: [docs/pr-review-kit/02-performance.md](docs/pr-review-kit/02-performance.md).
|
||||
|
||||
### Description and commits
|
||||
|
||||
Fill in the pull request template: problem, change, where it reproduces, checks, measurements, what stayed unrun. One topic per pull request.
|
||||
|
||||
Commit messages are plain English sentences, without `feat:`, `fix:` or other prefixes. A commit that works on an issue ends its subject with the issue numbers: `Render the handheld scene at the asked size on 1.4.3 (#998, #536)`.
|
||||
|
||||
### Review and merge
|
||||
|
||||
The maintainers review every pull request against [docs/pr-review-kit/](docs/pr-review-kit/): read it to see what gets a pull request rejected, which bug classes are already fixed and which invariants a change must keep. A good pull request that misses the project's conventions is fixed up while merging rather than sent back. Part of a pull request can be taken; the reason for what was left out or rejected is posted on the pull request.
|
||||
|
||||
Accepted work merges into `dev` and reaches players through test builds. `main` moves only at a release.
|
||||
|
||||
## Coding agents
|
||||
|
||||
AI-assisted code is welcome. You are responsible for having read and run it, and the pull request says which checks ran on your machine.
|
||||
|
||||
Point your agent at [AGENTS.md](AGENTS.md) (Claude Code reaches it through `CLAUDE.md`), this file and [docs/pr-review-kit/](docs/pr-review-kit/) before it writes code. [docs/agents/](docs/agents/) holds what it needs to run the game unattended, measure, and debug: the method, the bug classes of a recompiled Switch game, and a symptom catalog. The rules in them bind the agent as they bind you:
|
||||
|
||||
- Key values stay out of prompts, logs, commands and files. The agent passes the keys file path to the tools.
|
||||
- Results in the pull request come from runs that happened. A check the agent did not run is listed as not run.
|
||||
- The agent writes the description in its own words about this diff. A review kit checklist pasted in with every box ticked tells the reviewer nothing.
|
||||
|
||||
## Code and docs
|
||||
|
||||
- Code, comments, commit messages and documentation are in English.
|
||||
- Generated code is never edited by hand: change the generator.
|
||||
- Comments say what the code does and why. Cite our code by file and function, upstream code by project, file and function.
|
||||
@@ -1,46 +1,71 @@
|
||||
# TotK Recomp
|
||||
|
||||
**TotK Recomp** is a static recompilation of *The Legend of Zelda: Tears of the Kingdom* (Nintendo Switch, 2023) into a native Windows x86-64 application ([docs/architecture.md](docs/architecture.md)).
|
||||
**TotK Recomp** is a static recompilation of *The Legend of Zelda: Tears of the Kingdom* (Nintendo Switch, 2023) into a native x86-64 application for Windows and Linux ([docs/architecture.md](docs/architecture.md)).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **No game files are included.** You need your own legally obtained copy of *Tears of the Kingdom*, dumped from your own console: the base game and the 1.2.1 or 1.4.3 update. Instructions for obtaining those files will not be provided.
|
||||
> **No game files are included.** You need your own legally obtained copy of *Tears of the Kingdom*, dumped from your own console: the base game and the 1.1.0, 1.2.1 or 1.4.3 update. Instructions for obtaining those files will not be provided.
|
||||
>
|
||||
> The game is built from your files on your machine, so the result contains the game. It is for your own use. Do not share it.
|
||||
|
||||
## Status
|
||||
|
||||
Early development. The game is playable on Windows; visual bugs and crashes are still expected.
|
||||
Early development. The game is playable on Windows and Linux; visual bugs and crashes are still expected.
|
||||
|
||||
Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## Features
|
||||
|
||||
- Native code, not emulation
|
||||
- Game updates 1.2.1 and 1.4.3, with saves that carry over between them
|
||||
- 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
|
||||
- 16:10 (Steam Deck, handhelds), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
|
||||
- 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), 3:2 (Surface, Framework), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
|
||||
- Anisotropic texture filtering up to 16x, for sharp ground into the distance
|
||||
- In-game settings menu (F1) and performance overlay (F10)
|
||||
- Play time across runs on the Game tab of the settings menu, kept in `playtime.txt` next to the saves
|
||||
- Discord Rich Presence ("Playing TotK Recomp" with the play time), off by default, own application id supported
|
||||
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
|
||||
- Keyboard, mouse and gamepad with remapping, rumble and gyro
|
||||
- Xbox button prompts: a layout that keeps the Switch button positions and shows the Xbox labels
|
||||
- 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
|
||||
mods, TKMM's merged output and ExeFS code patches (`.ips`, `.pchtxt`) work; the game rebuilds itself for code patches
|
||||
([docs/mods.md](docs/mods.md)). HD texture packs work: the port grows the texture heap a pack asks for by itself. Tested
|
||||
with the HDPacks.com 4K Texture Pack v1.0 on 1.4.3 and 1.2.1; other packs come with no guarantee
|
||||
- Atmosphère cheats: put a cheat file into a mod and switch its cheats in the menu ([docs/cheats.md](docs/cheats.md))
|
||||
|
||||
## Planned
|
||||
|
||||
- Linux and the Steam Deck
|
||||
- Mods made for emulators: unpack a mod into a `mods` folder
|
||||
- VRR
|
||||
|
||||
## Installing
|
||||
|
||||
Requirements: Windows 10 or later, an x86-64 CPU with AVX2, a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, about 36 GiB free during setup (20 GiB after).
|
||||
### Windows
|
||||
|
||||
Requirements: Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA machines get a faster build), a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, about 36 GiB free during setup (20 GiB after).
|
||||
|
||||
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.
|
||||
|
||||
To update, run a newer `TotK-Setup.exe` on the same folder; saves and settings are kept. Settings are in the in-game menu and in `totk.ini` ([totk.ini.example](totk.ini.example) lists every key). Controls: [docs/controls.md](docs/controls.md).
|
||||
### Linux
|
||||
|
||||
Requirements: as on Windows, plus Linux 6.7 or later and glibc 2.34 or later (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3).
|
||||
|
||||
1. Download `TotK-Setup-x86_64.AppImage` from [Releases](../../releases/latest), or build it ([docs/installer.md](docs/installer.md), "Linux release").
|
||||
2. Make it executable (`chmod +x TotK-Setup-x86_64.AppImage`) and start it. On a Steam Deck, do this in Desktop Mode.
|
||||
3. Pick your game files, the language and an install folder, and press *Start*.
|
||||
4. Play `totk` from the install folder.
|
||||
|
||||
To update, run a newer setup on the same folder; saves and settings are kept. Settings are in the in-game menu and in `totk.ini` ([totk.ini.example](totk.ini.example) lists every key). Controls: [docs/controls.md](docs/controls.md).
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ 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` |
|
||||
| TOTK-Ultrawide, the community mod for emulators: the list of UI panes the ultrawide layout edits and the distances they move (data only, no code copied; the edit code is our own, and MacNativeGames' `ultrawide_layout.py` in the row above already carries the same values). The mod's repository URL and licence version are not recorded in this repository | TOTK-Ultrawide (community mod; URL not recorded here) | GPL (version not recorded here) | `tools/ultrawide_layout.py` (`KEEP_ROOT`, `edits`, `anim_edits`) |
|
||||
| MacNativeGames TotK virtual-core preemption: asynchronous preemption of an owner in top-level game code, priority inheritance through guest mutexes, the SDK's preemption-disable protocol (TLR+0x100/+0x102, SVC 0x36), the core-wait diagnostics | https://github.com/ly05010419/MacNativeGames @ `33e8fa83052ea1ae09a3583137008e9797e12950` (`games/totk/runtime/threads.c`, commits 8bf0893 and 19730cd), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/kernel.c` (SuspendThread and a register-saving stub replace SIGUSR2), `runtime/hostfn.c`, `runtime/svc.c` (host depth) |
|
||||
| MacNativeGames RomFS index builder (`TOTKRFS1` layout) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/scripts/prepare_romfs_backend.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/romfs_index.py` |
|
||||
@@ -24,7 +27,7 @@ Code copied or adapted from another project is listed here with its source, the
|
||||
| deko3d block-linear image layout, as ported in MacNativeGames' `nvn_tex.c` (Apple parts dropped, layout math unchanged; licence text in `games/totk/third_party/d2r_gfx/LICENSE.deko3d`, reproduced in the file header) | https://github.com/devkitPro/deko3d (`source/dk_image.cpp`), via MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/nvn/nvn_tex.c`, `nvn_tex.h`), Copyright (C) 2018-2020 fincs | zlib | `runtime/nvn_tex.c`, `runtime/nvn_tex.h` |
|
||||
| MacNativeGames texture auto-layout goldens (40 original-SDK size and block-height cases) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/tests/texture_auto_layout/probe.c`), Copyright (c) 2026 jhaizhou | MIT | `runtime/nvn_tex_probe.c`; `runtime/nvn_probe.c` (four of the cases, through the procs) |
|
||||
| MacNativeGames block-linear deswizzler (address function; copy loop split out as `tex_bl_copy`) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/tex/nvn_deswizzle.c`), Copyright (c) 2026 jhaizhou | MIT | `runtime/tex_deswizzle.c`, `runtime/tex.h` |
|
||||
| SDL 3.4.18, dynamic link, `SDL3.dll` copied next to `totk.exe`. Developer builds use the MSYS2 build (`mingw-w64-ucrt-x86_64-sdl3`, which imports `libiconv-2.dll`, LGPL-2.1, unmodified, copied beside it); the installer release ships the upstream release `SDL3-3.4.18-win32-x64.zip`, which imports no libiconv | https://libsdl.org, Copyright (c) 1997-2026 Sam Lantinga | zlib | `runtime/input_win.c` (gamepad input, rumble, gyro and accelerometer) |
|
||||
| SDL 3.4.20, dynamic link, `SDL3.dll` copied next to `totk.exe`. Developer builds use the MSYS2 build (`mingw-w64-ucrt-x86_64-sdl3`, which imports `libiconv-2.dll`, LGPL-2.1, unmodified, copied beside it); the installer release ships the upstream release `SDL3-3.4.20-win32-x64.zip`, which imports no libiconv | https://libsdl.org, Copyright (c) 1997-2026 Sam Lantinga | zlib | `runtime/input_win.c` (gamepad input, rumble, gyro and accelerometer) |
|
||||
| libopus (MSYS2 `mingw-w64-ucrt-x86_64-opus`), dynamic link, `libopus-0.dll` copied next to `totk.exe` | https://opus-codec.org, Copyright (c) 2001-2023 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon | BSD-3-Clause | `runtime/ov_media.c` (nn::codec hardware Opus decoder) |
|
||||
| Dear ImGui 1.91.9 (core and Vulkan back end, unmodified), as vendored by LAS Recomp | https://github.com/ocornut/imgui, via https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f` (`third_party/imgui/`), Copyright (c) 2014-2025 Omar Cornut | MIT | `runtime/third_party/imgui/` (compiled into `totk.exe` for the in-game menu, `runtime/ui/`) |
|
||||
| SavageWindRecomp settings menu: two-pane option rows (row look, left / right stepping, hold speed-up, description pane), the menu style and tab bar | https://github.com/Revivedx/SavageWindRecomp @ `575ad88e02f9a89a9a885d90039d48af33c982f4` (`runtime/ui/option_rows.mm`, `runtime/ui/menu.mm`), Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors, Copyright (c) 2026 Peiikai | MIT | `runtime/ui/option_rows.cpp`, `runtime/ui/option_rows.h`, `runtime/ui/menu.cpp` |
|
||||
@@ -37,6 +40,7 @@ Code copied or adapted from another project is listed here with its source, the
|
||||
| AMD FidelityFX Super Resolution 1.0 (`ffx_a.h`, `ffx_fsr1.h`, unmodified; licence text in `runtime/shaders/ffx/LICENSE.txt`) | https://github.com/GPUOpen-Effects/FidelityFX-FSR @ `a21ffb8f6c13233ba336352bdff293894c706575`, Copyright (c) 2021 Advanced Micro Devices, Inc. | MIT | `runtime/shaders/ffx/`, `runtime/shaders/fsr_easu.comp`, `runtime/shaders/fsr_rcas.comp` (compiled into `runtime/fsr_easu_spv.h`, `runtime/fsr_rcas_spv.h`) |
|
||||
| AMD Display Library (ADL) declarations: `AdapterInfo`, `ADLSingleSensorData`, `ADLPMLogDataOutput`, the `ADL_PMLOG_*` sensor numbers and the `ADL2_*` function signatures | https://github.com/GPUOpen-LibrariesAndSDKs/display-library @ `84c881cb2136f04287eb3af12de00a9e2867aa9e` (`include/adl_structures.h`, `include/adl_defines.h`, `include/adl_sdk.h`), Copyright (c) 2016 - 2022 Advanced Micro Devices, Inc. | MIT | `runtime/perfmon.c` |
|
||||
| Zstandard 1.5.7 (MSYS2 `mingw-w64-ucrt-x86_64-zstd`), static link | https://github.com/facebook/zstd, Copyright (c) Meta Platforms, Inc. and affiliates | BSD-3-Clause | `runtime/resolution.c` (the HeapDef table), `runtime/ov_zstd.c` (the game's zstd frames decoded on the host) |
|
||||
| musl `fma.c`, `fmaf.c` and `scalbn.c`, adapted into one file for the x86-64-v2 build | https://github.com/kraj/musl @ `5a56ad13e1e0790c06122978aad2a14d6b9efc1a` (mirror of https://git.musl-libc.org/cgit/musl), Copyright (c) 2005-2020 Rich Felker and contributors | MIT | `runtime/fma.c` (the correctly rounded fma/fmaf the FP lowering needs on CPUs without FMA3) |
|
||||
|
||||
## Compiler runtime in a source build
|
||||
|
||||
@@ -67,9 +71,9 @@ These tools run on the developer's or player's machine during the build. They ar
|
||||
| Tcl/Tk 9.0.4, rebuilt with Xft for the Linux installer | https://github.com/tcltk/tcl and https://github.com/tcltk/tk, tag `core-9-0-4`; archives pinned by SHA-256 in `tools/make_release_linux.py` | Tcl/Tk licence (BSD-style) | `python/lib/`; licence texts in `licenses/tcl-license.terms` and `licenses/tk-license.terms` |
|
||||
| libXft 2 and libXrender 1, the sniper SDK sysroot's shared libraries, bundled beside the Xft Tk of the Linux installer; their Debian `copyright` files go into `licenses/` | https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20261021.266025/ | HPND (MIT-style) | `python/lib/`; `licenses/libXft-copyright.txt`, `licenses/libXrender-copyright.txt` |
|
||||
| CPython 3.14.9 embeddable package, with `tkinter` and Tcl/Tk 9.0.4 taken from the python.org 3.14.9 installer's component MSI (unpacked, never run); includes `vcruntime140*.dll` (Microsoft, redistributable with Python), OpenSSL, libffi, SQLite, zlib, libtommath | https://www.python.org/downloads/release/python-3149/ | PSF-2.0 (Tcl/Tk: Tcl/Tk licence, BSD-style) | `python/` |
|
||||
| numpy 2.5.4 (with its bundled OpenBLAS), lz4 4.4.5, cryptography 50.0.2 (with its bundled OpenSSL), cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | BSD-3-Clause; lz4 BSD-3-Clause; cryptography Apache-2.0 OR BSD-3-Clause; cffi MIT; pycparser BSD-3-Clause | `python/Lib/site-packages/` |
|
||||
| numpy 2.5.4 (with its bundled OpenBLAS), lz4 4.4.5, cryptography 50.0.2 (with its bundled OpenSSL), fontTools 4.68.0, cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | BSD-3-Clause; lz4 BSD-3-Clause; cryptography Apache-2.0 OR BSD-3-Clause; fontTools MIT; cffi MIT; pycparser BSD-3-Clause | `python/Lib/site-packages/` |
|
||||
| llvm-mingw 20261020 (LLVM/Clang/LLD 23, mingw-w64, libc++, libunwind, compiler-rt), x86_64 subset | https://github.com/mstorsjo/llvm-mingw/releases/tag/20261020 | Apache-2.0 with LLVM exception; mingw-w64 ZPL-2.1/BSD/public domain; libc++ and libunwind Apache-2.0 with LLVM exception and MIT/UIUC; winpthreads MIT | `llvm/` |
|
||||
| SDL 3.4.18 upstream Windows release (`SDL3.dll`) and mingw development package (headers, `libSDL3.dll.a`) | https://github.com/libsdl-org/SDL/releases/tag/release-3.4.18 | zlib | `bin/SDL3.dll`, `runtime/deps/` |
|
||||
| SDL 3.4.20 upstream Windows release (`SDL3.dll`) and mingw development package (headers, `libSDL3.dll.a`) | https://github.com/libsdl-org/SDL/releases/tag/release-3.4.20 | zlib | `bin/SDL3.dll`, `runtime/deps/` |
|
||||
| libopus 1.6.1 (MSYS2 `mingw-w64-ucrt-x86_64-opus` 1.6.1-2; `libopus-0.dll` imports only KERNEL32 and the UCRT api-ms-win-crt DLLs), headers and import library | https://repo.msys2.org/mingw/ucrt64/ | BSD-3-Clause | `bin/libopus-0.dll`, `runtime/deps/` |
|
||||
| libvpx 1.17.0 (MSYS2 `mingw-w64-ucrt-x86_64-libvpx` 1.17.0-1), headers and static library compiled into `totk.exe`; `libvpx-LICENSE.txt` and `libvpx-PATENTS.txt` come from the v1.17.0 source tag | https://github.com/webmproject/libvpx | BSD-3-Clause with the WebM patent grant | `runtime/deps/`, `licenses/` |
|
||||
| Zstandard 1.5.7 (MSYS2 `mingw-w64-ucrt-x86_64-zstd` 1.5.7-2), headers and static library compiled into `totk.exe`; `zstd-LICENSE.txt` from the package | https://github.com/facebook/zstd | BSD-3-Clause | `runtime/deps/`, `licenses/` |
|
||||
@@ -100,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` |
|
||||
@@ -10,6 +10,7 @@ updates = ['C:\Games\TotK\update-1.4.3.nsp']
|
||||
keys = 'C:\Switch\prod.keys'
|
||||
# Optional: a firmware NCA directory (an unpacked firmware, or an emulator's installed registered/ directory).
|
||||
# tools/extract_fonts.py takes the system shared fonts from it; the game needs them only with Chinese or Korean text.
|
||||
# The installer asks for the same folder (--firmware) when the language needs those fonts.
|
||||
# firmware = 'C:\Switch\firmware'
|
||||
|
||||
[work]
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Debugging the port: method for agents
|
||||
|
||||
A game run costs minutes and a full rebuild costs several more, while reading code costs seconds. Order the work so the
|
||||
cheap steps decide as much as they can before the game starts. Each step ends on its stated condition.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Evidence.** Hold one artifact that shows the symptom: the player's screenshot, log or input recording, a
|
||||
flight-recorder dump, a frame dump ([running.md](running.md), "Observing"). Done when you can point at the wrong
|
||||
pixel, line or frame.
|
||||
2. **Where it reproduces.** OS, GPU vendor and generation, driver, game update, settings, mods, the save. Done when you
|
||||
know whether your hardware can show the bug at all. NVIDIA bugs do not reproduce on AMD; RDNA1/2 bugs do not
|
||||
reproduce on RDNA3/4 (another driver branch); a Ryzen 7000 iGPU is RDNA2 and runs that branch. When your hardware
|
||||
cannot show it, say so and work from the code and the reporter's logs; runs that cannot decide anything are skipped.
|
||||
3. **Known already?** Search [symptoms.md](symptoms.md), [pr-review-kit/04-knowledge.md](../pr-review-kit/04-knowledge.md)
|
||||
and [maxwell.md](../maxwell.md) sections 6-7 with the words the player used (black, magenta, flicker, slow motion,
|
||||
missing, stale, streaks, stuck) and the object (sky, grass, shadow, fog, HUD, weapon). Then the upstream notes:
|
||||
`refs/nxnative/docs`, `refs/MacNativeGames/docs` and the SavageWindRecomp issue tracker (`tools/fetch_refs.sh`
|
||||
fetches `refs/`). Done when every hit is either ruled out or is your first hypothesis. A hit on a class we already
|
||||
fixed: confirm that fix still holds before deriving anything new.
|
||||
4. **Read the code.** The game's function in Ghidra ([building.md](../building.md), "Reverse engineering") or in
|
||||
`generated/<ver>/`, and our runtime path in `runtime/`. Done when the code either rules the hypothesis out or names
|
||||
the exact condition a run must show. A run confirms what the code could not rule out.
|
||||
5. **The cheapest falsifying check.** An existing switch (a `totk.ini` setting or a `TOTK_*` variable), a dump, a trace
|
||||
line, a probe. When it refutes the hypothesis, stop: write down what was checked and why it fell, and start again
|
||||
at step 3 with the next hypothesis. No build, no benchmark for a refuted idea.
|
||||
6. **Repro red, then green.** The same repro shows the bug before the fix and its absence after. A fix without a
|
||||
reproduced failure is a guess.
|
||||
7. **Fix the cause.** A crash that goes away with a feature off points at the bug in that feature; fix the feature.
|
||||
A fix that one setup needs is gated on that setup, and everyone else keeps today's behaviour.
|
||||
|
||||
## Traps
|
||||
|
||||
- **One-off failures.** A failure that happens once and not again: rerun before you debug it as a code bug. A crash or
|
||||
a forced shutdown leaves zero-filled cache files, and memory errors on the host flip bits in files and objects.
|
||||
- **Every build fails the same way at once.** Suspect the inputs and caches (game files, the OS file cache, shader and
|
||||
pipeline caches), not the code. Bisecting code is pointless when the old builds fail too.
|
||||
- **Compile errors in generated C** on a line the generator could not have written are a flipped bit. Regenerate and
|
||||
compare hashes; `recomp/` is fine.
|
||||
- **A crash that looks like a race.** Try the save first: some saves crash the same way on a real Switch.
|
||||
- **Theories about locks and threads.** Read the game's own protocol in Ghidra before running anything. Ten runs on
|
||||
lock theories once went where one read of the game's double-buffer code would have narrowed it.
|
||||
- **Numbers from a fast machine.** A change neutral on a fast desktop can matter on a weak laptop or the Steam Deck,
|
||||
in either direction. Say which hardware your numbers come from.
|
||||
|
||||
## Bug classes of a recompiled Switch game
|
||||
|
||||
The port runs the game's own code on another OS and another GPU. Most bugs come from places where the host differs
|
||||
from the console.
|
||||
|
||||
### Variable frame step and epsilon-gated timers
|
||||
|
||||
The console steps the game by whole 30 Hz units, so a countdown started at a whole value reaches exactly 0. With the
|
||||
real-time step (`realtime = 1`, the default) the step is a measured ratio, and the last step can leave a remainder of
|
||||
about 1e-8. Many of the game's updaters step a timer only while it is above `FLT_EPSILON`; a remainder in
|
||||
(0, `FLT_EPSILON`] is never stepped again. A consumer that tests the same field with `> 0` then sees it active
|
||||
forever, until a load reinitialises the object. Issue #18 (weapons locked after a Fuse) was this.
|
||||
|
||||
Symptom words: stuck until a reload, a teleport or a shrine; locked after an action; works sometimes; only above
|
||||
30 fps. To check a report: find the field the stuck behaviour reads, then compare the updater's threshold with the
|
||||
consumer's test. A change to the frame step, the pacer or the fps limit is checked for this class.
|
||||
|
||||
### Thread scheduling: Horizon is not a desktop OS
|
||||
|
||||
- Horizon runs a woken better-priority thread at once on its core, before the thread that woke it goes on. A host
|
||||
runs both in parallel, so state can be read one step early or freed while another thread still uses it.
|
||||
- Threads created with ideal core -2 start pinned to one core on Horizon. The game keys per-core data by
|
||||
`GetCurrentProcessorNumber` without locks and expects that pinning.
|
||||
- The game has hundreds of spin loops without a system call. On the console a spinning thread holds one core of three;
|
||||
on the host it burns a host core and can keep an equal-priority waiter out.
|
||||
- ARMv8 `stlr` followed by `ldar` is ordered (RCsc). C11 release/acquire lets x86 reorder that store and load. A
|
||||
two-thread flag handshake that loses a wake points here; lifter changes to barriers and atomics pass
|
||||
`tests\diff\atomic.py` and `tests\excl_mt.py`.
|
||||
- A scheduling change is judged by how long a ready, better thread waits for a core it may use, plus the slow-frame
|
||||
count. A check that made one bug rarer once added 50-75 ms hitches.
|
||||
|
||||
### Maxwell behaviour versus Vulkan defaults
|
||||
|
||||
The port must do what the Switch GPU does: last-vertex provoking, (0,0,0,1) for unwritten vertex outputs, colour
|
||||
targets outside the shader's output map keep their texels, the G-buffer pass samples its own targets. The full list
|
||||
with sources: [maxwell.md](../maxwell.md), section 7.
|
||||
|
||||
### Memory shared by the CPU and the GPU
|
||||
|
||||
- The game fills textures and buffers with the CPU and samples them on the GPU without the flush a desktop API needs.
|
||||
Missing uploads show as magenta, brown or stale textures.
|
||||
- The game reads GPU-rendered textures with the CPU (grass maps, cut and burn masks). Missing write-backs show as
|
||||
effects that never happen: cut grass stays up, fire does not spread.
|
||||
- Memory only the GPU writes looks idle to write-watch tracking: a store from a shader leaves no fault and no dirty bit.
|
||||
- Anything kept per texture, per frame or per object without a free grows host memory until a commit failure after
|
||||
hours. Watch private memory over a few idle minutes.
|
||||
|
||||
### Device lost and GPU hangs
|
||||
|
||||
- The `vk: pass marks` lines name the pass in flight. Validation names the misuse when there is one.
|
||||
- Submits and presents share one queue under one lock. A present from its own thread on a second queue gave device
|
||||
lost on NVIDIA before Turing in another port.
|
||||
- Caches from another driver or GPU (pipeline caches, translated shaders) gave black windows after driver updates;
|
||||
every cache is validated on load and dropped when vendor, device or `pipelineCacheUUID` change.
|
||||
- A first-time shader stall over 2 s on a slow GPU trips the Windows GPU timeout (display event 4101). Large one-shot
|
||||
submissions (a boot-time decode of every texture) do the same.
|
||||
- Point lists from vertex shaders that write no point size are undefined without `VK_KHR_maintenance5`.
|
||||
|
||||
### Windows and Wine host costs
|
||||
|
||||
What the hot path must not do, with measured costs: [pr-review-kit/02-performance.md](../pr-review-kit/02-performance.md),
|
||||
"What the hot path must not do". For debugging, also:
|
||||
|
||||
- `SuspendThread` returns at once, but `GetThreadContext` waits until the thread leaves the kernel. A sampler or a
|
||||
debugger stalls for as long as the stopped thread's file open or driver call runs.
|
||||
- A thread stopped between its `sub rsp` and its stack probe has its stack pointer inside the guard page. Check
|
||||
`PAGE_GUARD` with `VirtualQuery` before reading another thread's stack.
|
||||
- Paths go through the W APIs with UTF-8 to UTF-16 conversion. An A API call breaks on a path with non-Latin letters,
|
||||
such as a Windows user name in Cyrillic or Chinese.
|
||||
- Wine emulates `GetWriteWatch` with `mprotect` and one fault per first write. Anything relying on cheap OS memory
|
||||
tricks (write-watch, protection changes, fault handlers) costs more under Proton.
|
||||
- `win32u.dll` frames in a host stack are kernel graphics calls: memory allocation or submission.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Running and observing the game as an agent
|
||||
|
||||
How an agent runs a build into gameplay without a person at the controls, captures what the game shows, and reads the
|
||||
results without flooding its context. Setting names and file formats live in [settings.md](../settings.md) and
|
||||
[controls.md](../controls.md); this page says which to use when. How to judge a performance change:
|
||||
[pr-review-kit/02-performance.md](../pr-review-kit/02-performance.md).
|
||||
|
||||
## Before the first run
|
||||
|
||||
- **A build dir per change.** Build into its own folder (`tools/build_game.py --out build/<ver>-<name>`) so a
|
||||
comparison has two exes side by side. After every build check the exe's modification time: a failed link leaves the
|
||||
old `totk.exe` in place and you test old code.
|
||||
- **Prepared shaders in that folder.** `tools/prepare_shaders.py build/<dir> --version <ver>` ([building.md](../building.md)
|
||||
step 6). Without it the game compiles shaders during play, and every timing and every "a draw is missing" shot of the
|
||||
first minutes says nothing about your change. Shader and pipeline caches are not copied between build folders.
|
||||
- **The same `totk.ini` on both sides** of any comparison. A folder without one runs at the defaults; check the log's
|
||||
`resolution:` line of each run before comparing them.
|
||||
- **Saves.** `tools/run_game.py` copies the profile `build/profile`, swaps in a save from `build/save-backup/<name>/`
|
||||
and deletes the copy afterwards, so the real profile is never written. To make a backup, copy the user folder from
|
||||
`build/profile/save/` into `build/save-backup/<name>/`. Pick a save that starts where the change acts.
|
||||
|
||||
## Running
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\run_game.py build\<dir> --save <name> --seconds 30 [--script <input.txt>] [--shot out.png] [--env KEY=VALUE] [--quiet] [--tag <name>]
|
||||
```
|
||||
|
||||
- `--seconds` is game time since the world loaded (the log line `frame step: world clock starts` marks the start), at
|
||||
any frame rate. Measure and script by game time; a frame count at another fps is another scene.
|
||||
- `--script` replays input ([controls.md](../controls.md), "Scripted input"). A new script starts with the presses of
|
||||
`tools/input/load_save.txt`, which load the first save from the title. `TOTK_INPUT_RECORD` turns a person's play into
|
||||
a script; ask the user to record the repro rather than guessing the button sequence.
|
||||
- `--shot` saves the last frame and builds pipelines synchronously: use it for pictures, never for timing.
|
||||
- `--quiet` waits until no other `totk.exe` or `ninja.exe` runs. Every timing you report comes from a `--quiet` run.
|
||||
- The log goes to `<dir>\logs\<tag>.log`; the tool prints `<log>: rc=<code>`.
|
||||
|
||||
Run one game at a time: two instances share the GPU's memory and the CPU, and both runs' numbers are void.
|
||||
|
||||
Running `totk.exe` yourself: set `TOTK_ASSUME_FOCUS=1` (an unfocused window otherwise pauses the game),
|
||||
`TOTK_INPUT=none` (the user's keyboard and pad stay out) and `TOTK_BACKGROUND_WINDOW=1`. Stop only the process you
|
||||
started, by PID: `taskkill /IM totk.exe` also closes the user's game.
|
||||
|
||||
**Check that the run reached the world** before trusting it: the world clock line is in the log and the shot shows the
|
||||
scene you expected. Title menus on a slow boot eat presses, and runs of one script drift apart after 10-15 game seconds
|
||||
(enemy AI, physics, weather). Two runs are comparable only when shots at the same game second show the same scene.
|
||||
|
||||
## Observing
|
||||
|
||||
| You need | Use |
|
||||
|---|---|
|
||||
| pictures over a time span | `TOTK_SHOTS=w<lo>-<hi>:<step>` (world seconds) and `TOTK_SHOTS_DIR`; 480x270 by default |
|
||||
| the frames around a hitch or a one-frame glitch | `TOTK_FLIGHT=1`: keeps the last 600 frames with timings and thumbnails, dumps on F8, on a frame over `TOTK_FLIGHT_SPIKE_MS` or at `TOTK_FLIGHT_AT=<frame>`; `python tools/flight_view.py <dump dir>` lists the worst frames and writes a contact sheet |
|
||||
| every render pass of one frame | `TOTK_DUMP=<frame>` and `TOTK_DUMP_DIR`: a PNG per pass target plus `passes.txt` (draws, bound textures with swizzles, storage buffers, copies, clears) and `images.txt`; `TOTK_DUMP_BUF` adds a guest buffer |
|
||||
| Vulkan misuse | `TOTK_VK_VALIDATION=1` (needs the Vulkan SDK), run into the world; compare the message kinds with a run without your change. The layer caps repeats, so counts are not totals |
|
||||
| which pass the GPU was in at a device lost | the `vk: pass marks` lines; on by default on AMD, `TOTK_PASS_CRUMBS=1` elsewhere |
|
||||
| a crash | the crash report at the end of the log: exception, guest and host addresses, registers |
|
||||
| a run on a large card that acts like a small one | `TOTK_VRAM_LIMIT_MIB=<MiB>` |
|
||||
|
||||
Frame dumps: a draw's argument lines come before its pass header. Pass numbers shift between runs, so find a pass by
|
||||
its draw count or its targets, not by its number. RenderDoc cannot capture this renderer ([controls.md](../controls.md),
|
||||
"RenderDoc capture").
|
||||
|
||||
## Measuring
|
||||
|
||||
| What | How |
|
||||
|---|---|
|
||||
| fps, worst frame, render-thread waits, GPU busy time, VRAM, `bound: gpu\|cpu\|pace` | `--env TOTK_TRACE=perf,sched`, then `python tools\perf_summary.py <log> <lo> <hi>` |
|
||||
| slow frames | count the `perf: slow frame` lines; each names what the render thread was blocked on (pipeline, translation, texture, fence, pacing) |
|
||||
| the frame's critical path, by thread and state | `TOTK_CRIT=w<lo>-<hi>` and `TOTK_CRIT_OUT=<absolute path>` with `sched` tracing, then `python tools\crit_path.py <file>` |
|
||||
| where CPU time goes | `TOTK_PROFILE=<path>` and `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, then `python tools\prof_report.py <profile> <totk.map>`; `TOTK_PROFILE_RENDER=1` samples the render thread with call chains |
|
||||
| a switch on a static scene | a setting set to `ab` with `TOTK_AB=<seconds>` alternates the two sides in blocks of game time; `python tools\ab_summary.py <log> --skip <seconds>`. On a moving scene compare whole runs instead |
|
||||
|
||||
A single run proves little: runs of one script on one build spread by a third in their 1% lows. Compare medians of at
|
||||
least two whole runs per side, on a quiet machine.
|
||||
|
||||
## Probes
|
||||
|
||||
Each probe is a mode of `totk.exe`, run from the build folder; the list with what each covers is at the top of
|
||||
`runtime/main.c`. `--selftest` needs no game files. `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex` and
|
||||
`--probe-shader` cover most runtime changes and run in seconds: run them before any game run.
|
||||
|
||||
## Reading output without filling the context
|
||||
|
||||
- Game logs run to megabytes. `grep` the lines you need, `tail` the end, pipe `perf_summary.py` through `grep SUMMARY`.
|
||||
- Generated C runs to hundreds of thousands of lines per file. `grep -n` the function, then print its line range.
|
||||
- Look at screenshots downscaled: paste several into one 1280x720 grid and read the grid once. Write down what it shows
|
||||
instead of opening it again.
|
||||
- Send long builds and runs to the background with their output in a file; read the last lines when they end.
|
||||
- Keep a notes file in the build folder from the first finding: what is ruled out, the leading hypothesis, the next
|
||||
step, temporary debug edits with file and line. A fresh session continues from it.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Symptoms and their usual causes
|
||||
|
||||
What a player reports, the causes this port and other Switch ports (nxnative, MacNativeGames, SavageWindRecomp,
|
||||
LAS Recomp) found behind it, and the first check. Search by the player's words and by the object. Which of these are
|
||||
already fixed here, and where: [pr-review-kit/04-knowledge.md](../pr-review-kit/04-knowledge.md), "Fixed bug classes".
|
||||
GPU semantics behind the graphics rows: [maxwell.md](../maxwell.md). The method around this table:
|
||||
[debugging.md](debugging.md).
|
||||
|
||||
## Graphics
|
||||
|
||||
| Symptom | Usual causes | First check |
|
||||
|---|---|---|
|
||||
| magenta, brown or stale textures; caves or sky wrong | the CPU filled the texture and the GPU never got the upload (no flush on the console) | frame dump: the texture's upload count and mirror state in `passes.txt` |
|
||||
| effects that never happen: cut grass stays up, fire does not spread | the CPU reads a GPU-rendered texture that was never written back | which texture the game reads after its fence; write-back of that target |
|
||||
| black scene, black dialog background or loading map | deferred binds read at record time; a 3D target rejected; a texture compare ignoring the channel mapping | frame dump: the first pass whose target goes black |
|
||||
| lime silhouettes, rectangles in the sky | a target outside the shader's output map overwritten instead of kept | dump: the target's previous contents versus after the pass |
|
||||
| dark or magenta 16x16 blocks, missing characters | a pass samples its own render targets (G-buffer feedback) | feedback-loop layout and barriers on that pass |
|
||||
| tile rectangles, wrong data in tiles | storage buffers bound in the wrong slot order | `passes.txt`: storage buffers with their c0 slots |
|
||||
| streaks on cliffs, characters stretched into streaks | primitive restart ignored; integer vertex inputs against a float format | validation run; the draw's topology and vertex formats in the dump |
|
||||
| garbage geometry on title or load screens | 8-bit indices widened on the CPU before a copy in the same submission wrote them | whether a copy to the index buffer precedes the draw in one submission |
|
||||
| black tiles, masks not cleared | clears with a partial channel or stencil mask skipped or done in full | `passes.txt` clears and their masks |
|
||||
| garbage in feedback effects at first use | new GPU-only images start undefined; the console starts them at zero | clear on creation and compare |
|
||||
| empty clouds or fog for the whole session | a one-shot draw skipped while its pipeline built, never drawn again | `perf: slow frame` lines naming pipeline waits; the draw's first frame |
|
||||
| fog that never parts, flicker in tile rows | driver constants (c0: dispatch size, point size, base instance) not filled | the c0 contents the translator expects for that stage |
|
||||
| no sun shadows, wrong lighting | unwritten vertex outputs read as zero instead of (0,0,0,1) | the shader's output map |
|
||||
| one flat ambient colour | a 1-layer 2D array sampled as 2D | the view type the shader expects |
|
||||
| upside-down text | lower-left window origin | origin mode in the log |
|
||||
| flicker or a texture swapped with a similar one | the newest texture picked at shared storage instead of the one with the matching view | texture lookup by id in the dump |
|
||||
|
||||
## Timing and frame pacing
|
||||
|
||||
| Symptom | Usual causes | First check |
|
||||
|---|---|---|
|
||||
| stuck until a reload or teleport, locked after an action | a timer remainder below `FLT_EPSILON` with the real-time step ([debugging.md](debugging.md)) | `realtime = 0` in `totk.ini`; if the bug is gone, find the timer field |
|
||||
| slow motion in GPU-heavy scenes, dynamic resolution never drops | the engine's GPU time comes from report timestamps; wrong timestamps make the frame look free | what the engine reads as GPU time and what the report writes |
|
||||
| frame counter wanders 50-60 | two 60 Hz clocks (vsync event and present acquire) drifting apart | whether both follow one display tick |
|
||||
| a 30 Hz thread runs at 27 Hz, slow cutscenes | host sleep overshoot: Windows waitable timers wake 0.3-0.5 ms late | `TOTK_TRACE=sched` sleep asked versus took |
|
||||
| 20 fps after a scheduler change | a yield back-off that starts too early | yield handling against the console's |
|
||||
| climbing or movement wrong only above some fps | game code that assumes a fixed step count per frame | compare at 30, 60 and the failing rate |
|
||||
|
||||
## Threads and crashes
|
||||
|
||||
| Symptom | Usual causes | First check |
|
||||
|---|---|---|
|
||||
| state read one step early, one frame of inconsistent data | a woken better-priority thread does not run before the waker goes on | sched trace around the handoff |
|
||||
| per-core data corrupted, rare crash in a container walk | threads the console pins to one core run on any host core | the thread's core mask from its creator; a run on one core |
|
||||
| rare hang or lost wake | address-arbiter signal values or wake order differ from Horizon | the arbiter path against mesosphere |
|
||||
| a flag handshake loses a wake | `stlr`/`ldar` ordering lost in x86 release/acquire | the guest code's barrier pattern |
|
||||
| a host core at 100% while the game waits | game spin loops without system calls | profile: which guest function spins |
|
||||
| crash on world load | sizes the game reads from the driver (command memory used) differ from the console's | the sizes the game asks for against the console's |
|
||||
| crash with one save only | the save itself; some crash on a real Switch | the same save on another build and, if possible, on hardware |
|
||||
|
||||
## Device lost, hangs, black window
|
||||
|
||||
| Symptom | Usual causes | First check |
|
||||
|---|---|---|
|
||||
| device lost a few frames in, GPU writes to address 0 (older NVIDIA) | present from another thread or queue | every submit and present on one queue under one lock |
|
||||
| device lost or garbage on one AMD generation | views the driver branch mishandles (BC block views of 3D textures) | `vk: pass marks`, validation; the formats in the pass |
|
||||
| device lost on one scene, every run, one vendor | a shader or pass that driver cannot run | pass marks name the pass; skip shaders by hash to bisect |
|
||||
| GPU timeout after 1-2 min, single frames over 2 s | first-time shader stalls or a huge one-shot submission on a slow GPU | prepared shaders; submission sizes at boot |
|
||||
| GPU hang at boot with pipeline libraries | point lists without a point size | the shader writes point size or the device clamps it |
|
||||
| black window after a driver update | caches from another driver | the log line `vk_pipeline: ... written by another device or driver`; delete the caches next to `totk.exe` |
|
||||
|
||||
## Memory
|
||||
|
||||
| Symptom | Usual causes | First check |
|
||||
|---|---|---|
|
||||
| host memory grows in play until a commit failure after hours | state kept per texture init or per frame and never freed | private memory over a few idle minutes |
|
||||
| multi-GB peaks while travelling | upload staging grows inside one submission | staging bytes per submission |
|
||||
| out of memory after minutes | dead textures re-entering tables | live images and allocator bytes over time |
|
||||
| stutter as VRAM fills | new memory blocks allocated on the render thread (8-23 ms each) | profile the render thread over the hitch: `vkAllocateMemory` under the allocator's lock |
|
||||
+35
-6
@@ -54,7 +54,9 @@ MacNativeGames cannot run on x86-64 as is: its CPU path executes ARM64 code nati
|
||||
libzstd on the host (`ov_zstd.c`, `TOTK_ZSTD_NATIVE`); the lifted decoder took almost three times as long.
|
||||
- Settings and menu: every key with its default is in one table (`cfg.c`), read from `totk.ini` and `TOTK_*`
|
||||
variables and written back when the in-game menu changes it. The menu and the performance overlay are Dear ImGui,
|
||||
drawn into the swapchain image (`runtime/ui/`); their text is in `ui_text.c` (English, Russian).
|
||||
drawn into the swapchain image (`runtime/ui/`); their text is in `ui_text.c` (English, Russian, Brazilian Portuguese,
|
||||
Simplified Chinese). The Chinese glyphs come from a system font (Microsoft YaHei, `plat_font_path`) merged into the
|
||||
font atlas.
|
||||
- OS calls the host makes go through `runtime/platform/` (`platform.h`, `win32.c`), so a Linux or macOS host adds one file instead of touching the rest. The split is not finished: `tools/check_platform.py` lists the Win32 uses left in the other files.
|
||||
|
||||
#### Threads and cores
|
||||
@@ -74,19 +76,26 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
- Spin waits. Loops that only another thread can end (857 in 1.4.3, 864 in 1.2.1, found by `tools/spin_waits.py`, the
|
||||
nxnative search) send their back-edges through `rt_spin_gate`, which pauses the core once per pass and hands it to a
|
||||
waiter after 50 us of polling (`TOTK_SPIN`, `TOTK_SPIN_HANDOFF_US`).
|
||||
- `TOTK_FREE_CORES=1` turns the gate off, for experiments.
|
||||
- `free_cores = 1` (`TOTK_FREE_CORES=1`) turns the gate off, for experiments only: the game crashes or hangs, usually
|
||||
at start, because its job workers all report core 0 and share per-core memory (#212).
|
||||
|
||||
### 4. Graphics
|
||||
|
||||
- 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.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. `TOTK_VK_RECORDER=0` makes every call on the submitting thread.
|
||||
- Replay thread. `TOTK_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is off by default. Where the game waits for the GPU it overlaps the replay with the game threads (a GTX 1650 in the Depths, issue #88), a gain measured only there; while it was the default, a GTX 1660 Ti at the 60 fps cap with the GPU at 26 % reported uneven motion (#659). It adds up to a frame of latency; `replay_async = 1` turns it on.
|
||||
- Chunked replay. A large submission is recorded in chunks by several threads (`TOTK_REPLAY_WORKERS`, 0 by default: the sequential replay; 4 is a good value from 8 logical processors) and the chunks go to the queue in order (`vk_exec.c`). A sequential scan keeps a copy of the queue state where each chunk starts and lists what a chunk would otherwise learn from the chunks before it: inline uniform updates, conditions, contact shadow marches, uniform drops. It also makes render targets live in replay order and notes the op, since a chunk can sample a storage before the chunk that renders it got there or after the scan went past a later target there: a chunk's texture slots resolve as of its own op, and the chunks after a texture with no entry yet wait for the chunk that makes it. DLSS's decisions (the G-buffer, the light target, the end of the scene, the scaling pass) are the scan's too, and its copy of the lit scene and its evaluation are records of the chunk that holds the op; a chunk's draws are jittered while they come before the scan's end of the scene. The evaluation waits until the chunks before it are recorded, as it reads what their draws noted (the camera block, the jitter, the per-object motion list, which chunks add to under one lock), and the chunks after it wait for it, as they sample its output. `TOTK_REPLAY_STATE_CHECK=1` compares the state after the last chunk with the scan's.
|
||||
- Frame ahead. `TOTK_SYNC_AHEAD=1` (`sync_ahead`, off by default) makes the one `nvnSyncWait` per frame that waits for the frame presented last wait for the frame before it, so the game builds the next frame while the GPU still runs the previous one (`nvn_api_core.c`, `nvnSyncWait`). The game then writes memory that in-flight work still reads. This is safe only with shadow storage (`vk_memory.c`, the CPU-visible pools are host-visible copies of guest memory, on by default on Linux and Wine, `TOTK_SHADOW=1` on native Windows; without it the switch is ignored): a CPU write reaches the GPU only through the shadow copy made before each submit (`vk_mem_shadow_sync`), and a copy that would overwrite bytes an unfinished submission reads is queued as a GPU copy in the prologue command buffer of the part submitted next (`vk_mem_shadow_prologue`, `TOTK_SHADOW_DEFER=1`, the default with the feature) or, with `=0`, waited for on the replay thread (`shadow_reader_wait`). Only that one wait lags (identified by its call site, known for 1.2.1 and 1.4.3); results the game reads back from the GPU after it can be a frame old.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. It is off by default on AMD GPUs, where the driver loses the device or hangs the GPU at startup with it (`TOTK_VK_RECORDER=1` forces it on, `=0` makes every call on the submitting thread).
|
||||
- Uniform snapshot. The game rewrites per-object uniform blocks on other threads soon after it submits the frame
|
||||
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
|
||||
binds from CPU-written pools is copied, and the draws bind the copy (`vk_usnap.c`, after MacNativeGames'
|
||||
`TOTK_UBO_SNAPSHOT`); without it, late draws read another object's matrix.
|
||||
- Uniform updates. `nvnCommandBufferUpdateUniformBuffer` between the draws of a pass is replayed as an overlay: the
|
||||
bytes go to scratch memory, later bindings of the range bind them, and the copies into the game's buffers are
|
||||
recorded behind one barrier when the pass ends (`vk_exec_draw.c` `op_update_ub`, `TOTK_UB_OVERLAY`). A copy at the
|
||||
update itself would end the pass with a full barrier, which the terrain paid before most of its draws.
|
||||
- Driver data. The NVN driver keeps a per-stage constant bank (c0) that shaders read for things like point size, dispatch size and base instance. The back end fills it the way the driver does.
|
||||
- Shaders: the Ryujinx translator 1.3.2 with MacNativeGames' patches and ours (`tools/xlate/patches/`), built with .NET 10 NativeAOT as `totk_xlate.exe` and run as a separate process. Its SPIR-V goes to Vulkan directly. `tools/prepare_shaders.py` and `totk.exe --prepare-shaders` translate and compile the variants of MacNativeGames' shared list ahead of play (29,587 for 1.4.3); the rest are translated on first use and cached on disk.
|
||||
- Pipelines: `VK_EXT_graphics_pipeline_library` where the driver has it. Shader stages are compiled ahead into libraries; a draw that meets a new state links them at once (fast link), and an optimised pipeline replaces the linked one when a background thread finishes it. Pipeline keys seen in play are saved and built at boot. A draw whose pipeline is not ready is skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise, so one-shot passes are never lost; a full-screen draw is skipped as well rather than waiting (`TOTK_PIPE_SKIP`); the stage libraries a waiting draw needs are built on idle workers in parallel (`TOTK_PIPE_WAIT`). Without the extension, a missing pipeline is built on a worker and the draw waits or is skipped by the same rule.
|
||||
@@ -96,12 +105,32 @@ 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
|
||||
stretched.
|
||||
- HDR (`hdr = 1`, `vk_hdr.c`). The game tonemaps its scene into an r11g11b10f target, upscales it and draws the HUD
|
||||
into another float target, and a last draw copies that into the 8-bit sRGB window texture it presents. With HDR on,
|
||||
the tonemap's fragment shader (recognised by its constants and resource names, not a hash) runs as
|
||||
`shaders/hdr_tonemap.frag`, which keeps highlights up to the display's peak; the present reads the float target the
|
||||
window texture was copied from (any other last draw into it, such as a menu or a movie, leaves the window texture,
|
||||
shown at the HUD's white); a compute pass turns the picture into PQ code values in Rec. 2020 for an HDR10
|
||||
swapchain, which FSR 1 or the blit then scale; the in-game menu is drawn into an RGBA8 image and blended on at the
|
||||
HUD's white. Without an HDR10 surface format (Windows with "Use HDR" off, a compositor without colour management)
|
||||
the swapchain stays SDR. `TOTK_SHOTS` reads the window texture, so its pictures clip highlights in HDR.
|
||||
- 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; 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`).
|
||||
|
||||
+52
-8
@@ -1,7 +1,9 @@
|
||||
# Building from source
|
||||
|
||||
Windows 10 or later, an x86-64 CPU with AVX2, and a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`. Allow about
|
||||
60 GB for the work folder. Extraction wants about 55 GB free while it runs and keeps about 35 GB afterwards.
|
||||
Windows 10 or later, an x86-64 CPU with SSE4.2, and a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`. The
|
||||
default build targets x86-64-v3 (AVX2/FMA); a CPU without them builds with `--cpu v2` instead and runs slower (see
|
||||
"Build"). Allow about 60 GB for the work folder. Extraction wants about 55 GB free while it runs and keeps about 35 GB
|
||||
afterwards.
|
||||
|
||||
Players who only want to play can use the [installer](installer.md) instead: it builds the game on their machine
|
||||
without this toolchain.
|
||||
@@ -52,7 +54,7 @@ hactool reads `-k <prod.keys>`; MSYS2 shells set `HOME`, which hides its default
|
||||
## Build
|
||||
|
||||
From the repository root, in PowerShell. Every step that depends on the game version takes it as `<ver>`: `v143` for
|
||||
1.4.3, `v121` for 1.2.1 or `v111` for 1.1.1. The tools default to `v121` when `--version` is left out. Saves load on any
|
||||
1.4.3, `v121` for 1.2.1 or `v110` for 1.1.0. The tools default to `v121` when `--version` is left out. Saves load on any
|
||||
of them.
|
||||
|
||||
1. Python environments (once). `.venv` runs the lifter and the build; `.venv-py314` runs the extraction scripts, which
|
||||
@@ -69,13 +71,15 @@ of them.
|
||||
keys file. `config.local.toml` is ignored by git. The tools read the keys file themselves.
|
||||
|
||||
3. Extract and verify. This writes `build\extracted\v0` (1.0.0) and `build\extracted\<ver>` for each update in
|
||||
`updates`, identified by its hash; a hash mismatch stops the run:
|
||||
`updates`, identified by its hash; a hash mismatch stops the run. With another `[work] dir` in `config.local.toml`,
|
||||
`extracted` goes there instead, and `build_game.py` reads the ExeFS and RomFS from there:
|
||||
|
||||
```powershell
|
||||
.venv-py314\Scripts\python tools\extract.py
|
||||
```
|
||||
|
||||
4. Shader translator (once; no game files). This publishes `build\xlate\totk_xlate.exe`:
|
||||
4. Shader translator (no game files; again after a change to its sources, and `tools\make_release.py` stops on an older
|
||||
one). This publishes `build\xlate\totk_xlate.exe`:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\build_xlate.py
|
||||
@@ -90,6 +94,14 @@ of them.
|
||||
.venv\Scripts\python tools\build_game.py --version <ver>
|
||||
```
|
||||
|
||||
`--cpu v2` targets x86-64-v2 (SSE4.2, no AVX2/FMA) for older CPUs; the default `v3` needs AVX2. The v2 build links
|
||||
`runtime/fma.c`, a correctly rounded software fma the FP lowering needs, and runs slower. Inline assembly,
|
||||
target attributes and intrinsics are not bound by `-march`, so a v2 build runs `tools/check_isa.py` over the runtime
|
||||
objects before it links: any AVX, BMI or other instruction above x86-64-v2 outside a function listed there as
|
||||
dispatched by a runtime CPU check stops the build.
|
||||
|
||||
`gen.py` sizes its emit pool from the CPU count and the free memory; `--jobs N` sets it, and so does the environment variable `TOTK_LIFT_JOBS=N`, which the installer's lift step honours too (it has no `--jobs`). When a worker runs out of memory, the pool restarts with half the workers (`emit: out of memory with N workers, retrying with M`), down to one; only an out-of-memory error with one worker fails the step.
|
||||
|
||||
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
|
||||
A compiler cache in front of clang shortens it further: with `SCCACHE` set to an sccache executable, `build_game.py`
|
||||
writes it into the ninja file ahead of `cc`, and every lift rewrites the generated chunks with the same contents, so
|
||||
@@ -101,9 +113,17 @@ of them.
|
||||
```
|
||||
`--patches` builds in the frame-rate, resolution and other patch sets, switchable at run time. The link copies
|
||||
`SDL3.dll`, `libiconv-2.dll` and `libopus-0.dll` next to the exe.
|
||||
For Xbox button prompts, run `make_romfs_patches.py` once more with `--xbox-prompts`: it writes
|
||||
`build\<ver>\romfs-patches-xbox` ([controls.md](controls.md), "Xbox button prompts"); `build_game.py` copies it next
|
||||
to `totk.exe` like `romfs-patches`.
|
||||
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`. `--prompts pc` writes
|
||||
`romfs-patches-pc`, the keyboard and mouse prompts ([controls.md](controls.md), "PC button prompts").
|
||||
|
||||
A build from source has no layouts for other shapes than 16:9. At an `output_res` such as 5120x1440 the game then
|
||||
draws the largest 16:9 size inside it (2560x1440) and the present adds bars, instead of a wide camera over a
|
||||
stretched HUD (`runtime/resolution.c`, `shape_fallback`). For the full shape make `romfs-patches-<w>x<h>` once per
|
||||
size: `make_romfs_patches.py --version <ver> --aspect <w>x<h> --out build\<ver>\romfs-patches-<w>x<h>` (the sizes
|
||||
the installer makes are `ASPECT_SIZES` in `installer/steps.py`); `build_game.py` copies every `romfs-patches*`
|
||||
folder next to `totk.exe`.
|
||||
|
||||
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
|
||||
for the shared shader list:
|
||||
@@ -115,6 +135,11 @@ of them.
|
||||
Without this step the first launch compiles shaders and pipelines during play and stutters until the caches fill.
|
||||
The AMD driver keeps its pipeline cache per exe path and holds a limited amount: preparing another build folder can
|
||||
evict this one's. `totk.exe --warm-shaders` rebuilds it from the shader cache.
|
||||
When the driver's shader compiler crashes on a variant (NVIDIA 615.78.08 on an RTX 5080 does, issue #30),
|
||||
`prepare_shaders.py` runs the driver step again with `--warm-shaders`, up to three times, without the variants the
|
||||
driver was building at the crash. `shader-prepare-skip.txt` next to the exe lists them with the GPU and driver
|
||||
version; they build at their first draw instead, another driver version builds them again, and so does deleting the
|
||||
file.
|
||||
|
||||
7. Run. The game keeps running when its window loses the focus (`assume_focus = 1` in `totk.ini`, the default; the
|
||||
Game tab switches it). With 0 it pauses, as it does on the Switch when Home is pressed.
|
||||
@@ -133,6 +158,12 @@ 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 and the runtime at -O2 with an execution
|
||||
profile, 6-12% shorter frames. Which build uses which profile, when to retrain, where and how to train and how to
|
||||
measure a new profile: [pgo.md](pgo.md).
|
||||
|
||||
## Linux (in progress)
|
||||
|
||||
The runtime builds and runs on x86-64 Linux with a window, input and menu on SDL3 (`*_sdl.c`) in place of the Win32
|
||||
@@ -183,6 +214,16 @@ Before a commit:
|
||||
`git config core.hooksPath .githooks` runs it before each local commit.
|
||||
- `build\<ver>\totk.exe --selftest`: layout, dispatch table and host-function registry, without game files.
|
||||
|
||||
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` and `python tests\settings_doc_check.py`, which fails
|
||||
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md). A change to the mods' ExeFS patches
|
||||
(`tools/exefs_patches.py`, `runtime/mods.c`, the `folded.txt` of `recomp/gen.py`) passes
|
||||
`.venv\Scripts\python tests\exefs_patches_test.py`, which makes its own modules and runs `gen.py` on one with a jump
|
||||
table (no game files), and `totk.exe --probe-mods`. A change to the cheat VM or the cheat opcode tables
|
||||
(`runtime/cheats.c`, `tools/cheats.py`) passes `.venv\Scripts\python tests\cheats_vm_check.py`, which runs both on
|
||||
cheats of its own with clang from PATH (`--cc` names another).
|
||||
|
||||
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
|
||||
change passes these, all run with `.venv`:
|
||||
|
||||
@@ -196,6 +237,9 @@ change passes these, all run with `.venv`:
|
||||
| `tests\excl_mt.py` | the exclusive monitor helpers under contention and ABA, on several threads | no |
|
||||
| `tests\jumptables_check.py` | `recomp/jumptables.py` against a Unicorn oracle; must report 0 disagreements, on 1.4.3 too | yes |
|
||||
|
||||
A `--cpu v2` build passes the same differential tests with `--cflags=-march=x86-64-v2`; they compile `runtime/fma.c`
|
||||
too, so they check the software fma the v2 build links rather than the C runtime's.
|
||||
|
||||
Runtime probes, each a mode of `totk.exe` (the list is at the top of `runtime/main.c`): `--probe-kernel`,
|
||||
`--probe-vkexec`, `--probe-nvn-tex` and `--probe-shader` cover most runtime changes. `--probe-shader` and the draw
|
||||
part of `--probe-vkexec` translate shaders of the census below: they look for `build/<ver>/shaders/jobs.json` of the
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Cheats
|
||||
|
||||
The port reads Atmosphère cheat files, the format Switch homebrew and the emulators use. It ships no cheat files: a cheat file goes into a mod in the `mods` folder next to `totk.exe`, like any other mod ([mods.md](mods.md)). Open the menu (F1) and switch the cheats on the Cheats tab. The switches are saved under `[cheats]` in `totk.ini`.
|
||||
|
||||
## Adding a cheat file
|
||||
|
||||
1. Find the cheat file for your update. Cheat sites and collections name it after the first 8 bytes of the game's build id:
|
||||
|
||||
| Update | File |
|
||||
|---|---|
|
||||
| 1.2.1 | `9B4E43650501A4D4.txt` |
|
||||
| 1.4.3 | `277178B7DBA1B6D4.txt` |
|
||||
|
||||
The Cheats tab names the file the installed game reads when no mod holds one, and `logs\totk.log` says `cheats: no enabled mod holds cheats/<id>.txt`.
|
||||
2. Put it in the `cheats` folder of a mod, a folder of its own under `mods`:
|
||||
|
||||
```
|
||||
mods\
|
||||
My cheats\
|
||||
cheats\
|
||||
277178B7DBA1B6D4.txt
|
||||
```
|
||||
|
||||
A download laid out for the Switch's SD card or an emulator works as it is: unpack it into a folder of its own under `mods`, and the port finds `atmosphere\contents\0100F2C0115B6000\cheats`, `contents\0100F2C0115B6000\cheats` or `0100F2C0115B6000\cheats` inside it. The file name's case does not matter.
|
||||
3. Start the game. Cheats that write the game's memory work now.
|
||||
4. Cheats that change the game's code need the game rebuilt with them. When the game starts and finds such cheats it did not build, it opens the setup's rebuild window, which builds them in (a few minutes; the shaders stay prepared) and starts the game again. Until then they are greyed out with "after the rebuild". When the rebuild fails or you cancel it, the game starts without them and does not try again for the same cheats; "Rebuild the game now" on the Cheats tab tries again. `mods_rebuild = off` under `[mods]` in `totk.ini` turns the automatic rebuild off; running `TotK-Setup.exe` on the same folder does the same job by hand. Saves and settings are kept.
|
||||
5. Switch the cheats on the Cheats tab of the menu.
|
||||
|
||||
The Cheats tab lists the cheats grouped by mod, in the order of the Mods tab. A mod switched off on the Mods tab, or one the port cannot use, contributes no cheats; the change applies at the next start. The Mods tab says how many cheats each mod brought.
|
||||
|
||||
## The file format
|
||||
|
||||
Standard Atmosphère: a `[name]` line starts a cheat, `{name}` a master cheat that is always on, and the cheat's opcodes follow as 8-digit hex words. A name with no opcodes is shown as a heading. The file's title line (the one with `TID=`) is not shown.
|
||||
|
||||
```
|
||||
[The Legend of Zelda: Tears of the Kingdom v1.4.3 TID=0100F2C0115B6000 BID=277178B7DBA1B6D4]
|
||||
|
||||
[Player]
|
||||
[A code cheat]
|
||||
040A0000 XXXXXXXX YYYYYYYY
|
||||
|
||||
[A data cheat (Hold ZL+X)]
|
||||
80000104
|
||||
580F0000 XXXXXXXX
|
||||
640F0000 00000000 YYYYYYYY
|
||||
20000000
|
||||
```
|
||||
|
||||
The words above are placeholders: `XXXXXXXX` an offset, `YYYYYYYY` a value.
|
||||
|
||||
Each cheat's switch is `cheat_<hash> = 1`, where the hash is FNV-1a 32 of `<mod folder name>/<cheat name>`. A name repeated in one file gets `#2`, `#3` and so on. The build names each code cheat the same way, so renaming a mod's folder resets its cheats' switches to off and makes the next start rebuild its code cheats. Rename the folder before you switch its cheats on.
|
||||
|
||||
## How cheats run
|
||||
|
||||
The game's code is compiled ahead of time, so a cheat cannot write new instructions into it while the game runs. The port sorts each cheat by what it writes (`tools/cheats.py` at setup, `runtime/cheats.c` in game):
|
||||
|
||||
| Kind | What the cheat does | How the port runs it |
|
||||
|---|---|---|
|
||||
| code | Opcode 0 writes into main's code | Built into the game at setup as a patch set (`recomp/patches.py`); switches at once. |
|
||||
| key | Code writes inside a button condition (`8` mask, writes, `21` else, the original words, `20`) | A patch set that is on while the cheat is on and the buttons are held. |
|
||||
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A and C0 to C3, every presented frame while on. |
|
||||
| master | `{name}` with code writes | A patch set that is always on. |
|
||||
| restore | Writes back only the game's original instructions | Not needed: switching a cheat off restores them. Greyed out. |
|
||||
|
||||
A cheat whose code writes call or jump into a code cave the cheat writes itself (into main's zero padding) gets the cave inlined at the call site. The setup builds the code cheats of every mod in the `mods` folder, switched on or off, so switching a mod on later needs only a restart. A code cheat added or edited after the last build waits for the rebuild; a data cheat runs at the next start.
|
||||
|
||||
Not supported, and shown greyed out with the reason:
|
||||
|
||||
- opcode C4 (extended button condition) and the 0xF extensions (FF0 pause, FF1 resume, FFF debug log);
|
||||
- opcode 5 with a register offset;
|
||||
- code writes mixed with other opcodes, except the button pattern above;
|
||||
- code writes that change the CPU flags differently from the game's instruction, or code caves with branches or pc-relative instructions.
|
||||
|
||||
`build-cache\cheats.status` next to `totk.exe`, written by the setup, records which cheats were built and how; the log line `cheats: ...` sums them up at start. When the code cheats cannot be built, the setup warns and installs the game without them.
|
||||
+138
-15
@@ -7,15 +7,14 @@ Input comes from the first SDL3 gamepad, the keyboard and the mouse, merged into
|
||||
- `nintendo`: by position, as on a Switch (the table below).
|
||||
- `xbox`: south A, east B, west X, north Y, as an Xbox pad labels them; on a PlayStation pad cross is A, circle B, square X, triangle Y.
|
||||
- `auto` (default): `nintendo` for a pad whose east button SDL reports labelled A (Switch Pro Controller, Nintendo-layout pads), `xbox` for the others.
|
||||
- `xbox_prompts`: the buttons as `nintendo`, and the game's prompts name the Xbox labels (see Xbox button prompts below).
|
||||
|
||||
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. With `nintendo`, `xbox` and `auto` the game's button prompts stay Switch letters; `xbox_prompts` changes them.
|
||||
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. Which controller's buttons the game's prompts show is `TOTK_PROMPTS` (see Xbox button prompts below). 0.1.9's `pad_layout = xbox_prompts` is read as `pad_layout = nintendo` with `prompts = xbox`, and the menu writes it that way.
|
||||
|
||||
| Switch | Gamepad (by position, Nintendo layout) | Keyboard and mouse | Action in the world |
|
||||
|---|---|---|---|
|
||||
| Left stick | left stick | W A S D | move |
|
||||
| Right stick | right stick | mouse; arrow keys as a second source | camera |
|
||||
| Y | west | left mouse button | attack |
|
||||
| Y | west | left mouse button, T | attack |
|
||||
| ZL | left trigger (over 50%) | right mouse button, Z | shield, lock on |
|
||||
| ZR | right trigger (over 50%) | F, X | bow |
|
||||
| X | north | Space | jump |
|
||||
@@ -28,21 +27,55 @@ 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. T duplicates Y for the game's menus, where the left mouse button is A while the pointer is shown (sort the inventory with T). The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
|
||||
## Xbox button prompts
|
||||
|
||||
`pad_layout = xbox_prompts` (*Xbox prompts (A right)* on the Controls tab) is for an Xbox pad played by position. The buttons work as with `nintendo`: the right button, labelled B, does Switch A (interact, confirm), and the bottom one, labelled A, does Switch B (run, cancel). The letters the game shows are swapped to match the labels, A with B and X with Y, so each prompt names the button to press. The other layouts leave the prompts alone: with `xbox` the Switch letters already match the labels.
|
||||
`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).
|
||||
|
||||
The swapped letters are part of the game's textures, fonts and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the letter the game draws for it, and the prompt then names the wrong button. Use `xbox_prompts` with the default bindings.
|
||||
- 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.
|
||||
|
||||
The buttons change at once, but the game reads its UI archive and fonts once at boot, so the prompts change at the next start (the menu says *prompts change after restart*). `xbox_prompts` loads `romfs-patches-xbox` next to `romfs-index.bin` in place of `romfs-patches` (`runtime/romfs.c`, `overlay_init`): the same 60 fps patches, with the swap made from the game's own files by `tools/make_romfs_patches.py --xbox-prompts`, which the setup runs next to the plain one:
|
||||
| Switch | `nintendo` layout | `xbox` or `auto` layout |
|
||||
|---|---|---|
|
||||
| A, B, X, Y | B, A, Y, X | A, B, X, Y |
|
||||
| L, R | LB, RB | LB, RB |
|
||||
| ZL, ZR | LT, RT | LT, RT |
|
||||
| Plus, Minus | Menu, View | Menu, View |
|
||||
| D-pad | cross | cross |
|
||||
|
||||
- the action prompt (`DoCommand_00`) draws the letter images `Nt_KeyTexA/B/X/Y_00`, whose image data is swapped, in a diamond that names the other face buttons. The diamond's letters are text the game takes from `LayoutMsg/DoCommand_00.msbt` of each language's `Mals` archive (and the layout's own text panes behind it); both are swapped. The diamond's shape shows the button's position and stays.
|
||||
- every other prompt is text in the button font (`scft/External.bfotf` in each `Font/Font*.Nin_NX_NVN.bfarc.zs`, glyphs U+E040 to U+E043); the A and B, X and Y glyphs are swapped.
|
||||
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters stay and the diamond is laid out as an Xbox pad: A at the bottom, B on the right, X on the left and Y at the top, the button the prompt names lit in its place, its bar reaching left from X and right from the others (`tools/make_romfs_patches.py`, `bflyt_xbox_diamond`: the panes of `blyt/DoCommand_00.bflyt` take their partner's place, A's of B and X's of Y). The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
|
||||
|
||||
Without the folder (an install made by an older setup, or one where the setup could not prepare it and showed a warning) the menu says the files are missing, the log has `RomFS overlay: pad_layout = xbox_prompts, but ...`, and the Switch prompts stay; running the setup again on the install folder writes it. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --xbox-prompts` writes `build\<ver>\romfs-patches-xbox`.
|
||||
The prompts are part of the game's fonts, textures and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the prompt the game draws for it, which then names the wrong button. Use the Xbox prompts with the default bindings.
|
||||
|
||||
The game reads its UI archive and fonts once at boot, so the prompts change at the next start, also after `pad_layout` changes between `nintendo` and the others (the menu says *prompts change after restart*); the buttons change at once. Under that note the menu offers *Restart now*: after a confirmation it writes `totk.ini`, closes the game as *Quit game* does and starts the game again with the same command line, which waits until the old process is gone before it opens the log and the saves (`runtime/gfx_present.c`, `gfx_window_closed`; `plat_restart` and `plat_restart_wait` in `runtime/platform/win32.c` and `linux.c`). Progress since the last save is lost, as with *Quit game*; while the game writes a save, the confirmation waits. With `prompts = xbox` the game loads `romfs-patches-xbox` (`pad_layout = nintendo`) or `romfs-patches-xbox-label` (`xbox`, `auto`) next to `romfs-index.bin` in place of `romfs-patches` (`runtime/romfs.c`, `overlay_init`; `runtime/input_map.c`, `input_prompts_folder`): the same 60 fps patches, with the prompts drawn from the game's own files by `tools/make_romfs_patches.py --prompts xbox` or `--prompts xbox-label` (`tools/button_icons.py`, after MacNativeGames' `make_button_icons.py`), which the setup runs next to the plain one:
|
||||
|
||||
- every prompt but the button guide in play is text in the button font, `scft/External.bfotf` in each `Font/Font*.Nin_NX_NVN.bfarc.zs`: glyphs U+E040 to U+E043 (A B X Y), U+E044 to U+E047 (L R ZL ZR), U+E04B and U+E04C (Plus, Minus), U+E049 and U+E060 to U+E065 (the d-pad). fontTools writes the font again, so the archives grow and the folder also carries the resource size table (`System/Resource/ResourceSizeTable.Product.<n>...rsizetable.zs`) with their entries raised by as much; without that the game stops at start.
|
||||
- the button guide in play draws L and ZL from the images `Nt_KeyTexL_00` and `Nt_KeyTexZL_00` of the Common archive's `timg/__Combined.bntx`, painted with the same icons. The action prompt draws its button from `Nt_KeyTexA/B/X/Y_00`, whose image data is swapped for `nintendo`, and the diamond's letters are text the game takes from `LayoutMsg/DoCommand_00.msbt` of each language's `Mals` archive (and the layout's own text panes behind it), swapped the same way.
|
||||
|
||||
The Switch prompts stay, with a note under *Button prompts* and a `RomFS overlay: ...` line in the log, when the folder is missing (an install made by an older setup, or one where the setup could not prepare it and showed a warning; running the setup again on the install folder writes it), when it fails its check, with a game resolution of another shape than 16:9 that has no folder of its own (below), and when the `romfs_mods` folder replaces one of its files: `romfs-patches` then loads in its place, where two overlays replacing one file would stop the game. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts xbox` (and `--prompts xbox-label`) writes `build\<ver>\romfs-patches-xbox` (`-label`); it needs fontTools from `tools/requirements-py314.txt`. `--xbox-prompts` is the older name of `--prompts xbox`.
|
||||
|
||||
At a game resolution of another shape than 16:9 the game loads `romfs-patches-xbox-<w>x<h>` (or `romfs-patches-xbox-label-<w>x<h>`) in place of `romfs-patches-<w>x<h>`: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts xbox --aspect <w>x<h> --reuse <install>\romfs-patches-xbox --out <install>\romfs-patches-xbox-<w>x<h>`. The setup writes both for each of the fifteen sizes it lays the UI out for, as for the PC prompts below, so on a display of one of those shapes the game resolution follows the display with these prompts too. At another size, without that folder, the Switch prompts stay (*off at this game resolution*).
|
||||
|
||||
## PC button prompts
|
||||
|
||||
`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 (Shift: an arrow over its name); a mouse button is a mouse with that button filled and the others hollow.
|
||||
- The button guide in play draws every button from images of the Common archive (`Nt_KeyTexA/B/X/Y/L/ZL_00`, `Nt_KeyTexLSt_00` and `_01`, the stick tilted and pressed): all of them are painted with the same icons. The letters of the action prompt's diamond are one character of the Normal font each: E, an arrow for Shift, a bar for Space, L and R for the mouse buttons.
|
||||
- Like the Xbox prompts they follow the defaults and not the bindings. `--binds <totk.ini>` draws the first key of each `bind_` line of that file instead, for a folder made by hand.
|
||||
- At a game resolution of another shape than 16:9 the game loads `romfs-patches-pc-<w>x<h>` in place of `romfs-patches-<w>x<h>`: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts pc --aspect <w>x<h> --reuse <install>\romfs-patches-pc --out <install>\romfs-patches-pc-<w>x<h>`. The setup writes one for each of the fifteen sizes it lays the UI out for, so on a display of one of those shapes the game resolution follows the display with these prompts too. At another size, without that folder, the Switch prompts stay (*off at this game resolution*).
|
||||
|
||||
## Window keys
|
||||
|
||||
@@ -56,16 +89,16 @@ 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`
|
||||
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
|
||||
- `none` leaves the button unbound.
|
||||
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above, less any key a line binds: with `bind_X = key:t`, T presses X only, not also Y. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `TOTK_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
|
||||
|
||||
A pad that reports its face buttons swapped (A/B and X/Y, as some GuliKit models do) is fixed with:
|
||||
|
||||
@@ -78,14 +111,98 @@ 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.
|
||||
|
||||
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it.
|
||||
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it. `TOTK_LSTICK_DEADZONE` and `TOTK_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows under *Analog sticks* on the Controls tab) set one stick each and fall back to `TOTK_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.
|
||||
|
||||
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 middle and side mouse buttons keep their bindings (bound to L and R they turn the tabs), and T presses Y (sort). The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
|
||||
|
||||
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`TOTK_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `TOTK_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
|
||||
|
||||
## 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.
|
||||
@@ -99,14 +216,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)
|
||||
|
||||
+118
-34
@@ -9,20 +9,26 @@ 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.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
|
||||
profile/ saves
|
||||
mods/ the player's mods, one folder per mod (created by the menu's "Open mods folder")
|
||||
logs/ totk.log of the last run, totk.1.log to totk.4.log of the runs before it
|
||||
shader and pipeline caches prepared by the setup; shader-cache/shader-cache.pack holds the translations
|
||||
totk-install.json what built this folder: release version, input hashes, finished steps
|
||||
@@ -31,43 +37,89 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
|
||||
## Updating the port
|
||||
|
||||
The player runs a newer installer on the same folder. It reads `totk-install.json`, keeps `romfs/`, `exefs/`,
|
||||
`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.
|
||||
`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. 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.2.1, then 1.4.3), the installer extracts and builds again from it and
|
||||
keeps `profile/`. Saves carry over both ways: 1.4.3 converts a 1.2.1 save on load, and 1.2.1 loads the saves 1.4.3 wrote.
|
||||
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
|
||||
loads the saves 1.4.3 wrote.
|
||||
|
||||
## Setup steps
|
||||
|
||||
1. **Check** before writing anything: the files open with the keys, the update is 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 has
|
||||
AVX2 (`-march=x86-64-v3`), a Vulkan 1.3 GPU exists. Each failure is one clear message.
|
||||
2. **Extract** ExeFS and RomFS into the install folder and verify the hashes.
|
||||
3. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
|
||||
to the installer.
|
||||
4. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
|
||||
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.
|
||||
2. **Fonts** with Chinese, Korean or Traditional Chinese text and a firmware folder: extract the Switch system fonts
|
||||
from the firmware into `shared-fonts/` (MacNativeGames' `extract_shared_fonts.py`, which verifies every NCA layer
|
||||
and BFTTF header). Without a firmware folder the setup warns and goes on; the game then lacks those glyphs.
|
||||
3. **Extract** ExeFS and RomFS into the install folder and verify the hashes.
|
||||
4. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
|
||||
to the installer. The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets first
|
||||
(`tools/cheats.py`, `tools/exefs_patches.py`, status in `build-cache/cheats.status` and `exefs.status`); a file the
|
||||
tools refuse costs those cheats or patches, not the install. The patch files' writes into main's data go to
|
||||
`build-cache/exefs.data`, which the game stores into main at start; after the lift they are checked against the
|
||||
bytes the lifted code read as constants (`<gen>/main/folded.txt`, its jump tables).
|
||||
5. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
|
||||
hashes and the game version's patch page) and link
|
||||
them with the prebuilt runtime library into `totk.exe` (`build_game.py --runtime-lib`; about 3 min on 24 cores).
|
||||
5. **Prepare** the RomFS patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
|
||||
them with the prebuilt runtime library for the host's level (`libtotkrt.a` or `libtotkrt-v2.a`) into `totk.exe`
|
||||
(`build_game.py --runtime-lib`; about 3 min on 24 cores).
|
||||
6. **Prepare** (only when the runtime, the translator or the game version changed, not for a new lift) the RomFS
|
||||
patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
|
||||
`totk.exe --prepare-shaders`) in the install folder, then fold the shader cache's loose files (four per shader, about
|
||||
117,000) into `shader-cache/shader-cache.pack` (`tools/pack_shader_cache.py`). totk.exe reads the pack first and
|
||||
writes the translations it makes in play as loose files; running the tool again with the game closed folds those in.
|
||||
The patches are written twice: `romfs-patches` and `romfs-patches-xbox` (`--xbox-prompts`, the A/B and X/Y prompts
|
||||
swapped for `pad_layout = xbox_prompts`).
|
||||
6. **Clean up** the temp folder (generated C and objects, about 1.3 GB).
|
||||
The patches are written four times: `romfs-patches`, `romfs-patches-xbox` and `romfs-patches-xbox-label`
|
||||
(`--prompts`: Xbox button prompts for `prompts = xbox`, the first for `pad_layout = nintendo`, the second for `xbox`
|
||||
and `auto`), and `romfs-patches-pc` (keyboard and mouse prompts for `prompts = pc`). Each of the three prompt
|
||||
folders is then laid out once more for each game resolution of another shape, `romfs-patches-<style>-<w>x<h>`
|
||||
(`--prompts <style> --aspect`). Those differ from `romfs-patches-<style>` in the UI archive only, and `--reuse`
|
||||
hard-links the other files from it, so the forty-five folders take seconds and a few MiB each. The prompt folders are optional: when one cannot be made, the
|
||||
setup warns and finishes.
|
||||
When the graphics driver crashes while building the shaders, `prepare_shaders.py` runs that part again without the
|
||||
variants it was building ([building.md](building.md), step 6), and the setup finishes with a warning that names how
|
||||
many were left out.
|
||||
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`).
|
||||
|
||||
## Rebuilding for the mods
|
||||
|
||||
The generated C and the objects stay in the temp folder, so the next lift rewrites only the chunks whose C changed and
|
||||
Ninja recompiles only those: a new or edited code cheat costs the lift (under a minute on 24 cores, a few minutes on 4)
|
||||
and a few dozen chunks, not a full compile, and the shaders stay prepared.
|
||||
|
||||
`build-cache/rebuild.txt` holds the command that opens the setup window in its rebuild mode on the install folder
|
||||
(`installer/gui.py --rebuild <install folder>`, started through `python -c` so the release folder is on `sys.path`
|
||||
whatever the working folder), one argument per line. When an enabled mod has code cheats or ExeFS patch files the build
|
||||
lacks, `totk.exe` runs it at start and exits (`runtime/cheats.c` `rebuild_if_needed`); the
|
||||
window runs the setup on the folder at once and starts the game again when it succeeds. A failed or cancelled rebuild
|
||||
offers to start the game with the code it has; `build-cache/rebuild.skip` then keeps the same files from starting it
|
||||
again, until "Rebuild the game now" on the Cheats or Mods tab. `[mods] mods_rebuild = off` turns it off. The rebuild
|
||||
needs the release folder the setup ran from: a moved or deleted release folder means running a setup again.
|
||||
|
||||
## Command line
|
||||
|
||||
`installer/` (stdlib only; `installer/layout.py` maps the release folder and, in a repository, the dev toolchain):
|
||||
|
||||
```
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR] [--temp DIR] [--verify]
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR] [--root DIR] [--temp DIR] [--verify]
|
||||
```
|
||||
|
||||
- On a folder with `totk-install.json` only `--dest` is needed; the game files only when the extraction must run again.
|
||||
- `--firmware DIR` is the folder holding the firmware NCAs: an unpacked firmware, or the emulators' installed
|
||||
`Contents/registered` (the parent `Contents` folder works too). It is read only with Chinese, Korean or Traditional
|
||||
Chinese text, and only when `shared-fonts/` is not already in the install folder.
|
||||
- stdout: one JSON event per line, types `start`, `plan`, `step` (started, done, skipped), `progress` (step, phase,
|
||||
done/total/unit), `space` (bytes needed and free on the install disk and the temp disk), `warning`, `error`, `end` (`installer/events.py`). Messages are ids with parameters;
|
||||
`installer/text/en.json` holds their English text. The human-readable log goes to `<temp>/logs/`.
|
||||
@@ -79,11 +131,18 @@ python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR
|
||||
the merged RomFS (15.9 GiB) before it deletes them. The check computes the exact figure.
|
||||
- Window (`python -m installer.gui`, `installer/gui.py` draws, `installer/guilogic.py` decides): form, step list with
|
||||
progress, space line, collapsible log, Cancel (Ctrl+Break to the CLI's process group, then it waits for the exit),
|
||||
finish screen with Open folder. English or Russian, chosen from the Windows display language, switchable in the
|
||||
window; every sentence is an id in `installer/text/<en|ru>.json`. A folder with `totk-install.json` needs only the
|
||||
folder. `--selftest [--shots DIR]` builds every screen in both languages 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).
|
||||
finish screen with Open folder. English, Russian, Brazilian Portuguese or Simplified Chinese, chosen from the Windows
|
||||
display language (zh-CN and zh-SG give Chinese; the Traditional Chinese regions give English), switchable in the
|
||||
window; every sentence is an id in `installer/text/<en|ru|pt-BR|zh-Hans>.json`. A Chinese window still starts with
|
||||
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. 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`.
|
||||
|
||||
@@ -93,27 +152,42 @@ The unpacked release; `tools/make_release.py` assembles it and `--zip` packs it:
|
||||
|
||||
```
|
||||
TotK-Setup.exe starts python/pythonw.exe -m installer.gui (installer/launcher/launcher.c, no console)
|
||||
installer/ the installer: steps, CLI, window, en/ru text catalog
|
||||
installer/ the installer: steps, CLI, window, en/ru/zh-Hans text catalog
|
||||
python/ Python 3.14.9 embeddable + numpy, lz4, cryptography, cffi, pycparser (+ tkinter for the window)
|
||||
recomp/, tools/ the lifter and the build, extraction and shader scripts the steps run (copied from the repo)
|
||||
llvm/ llvm-mingw, x86_64-only subset (Apache-2.0 with LLVM exception; mingw-w64 parts permissive)
|
||||
runtime/ libtotkrt.a and the headers the generated code and modules.c/romfs.c/patches.c include
|
||||
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 (pgo.md)
|
||||
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
|
||||
|
||||
Simplified Chinese, Traditional Chinese and Korean text needs the Switch system fonts (`0-standard.bfttf` and the other
|
||||
nn::pl shared fonts). The game does not ship them. Without them it exits before its first frame in those languages
|
||||
(`rt_fatal` in `runtime/ov_system.c`). Supply them from your own console, in either way:
|
||||
|
||||
- Give the setup the firmware folder (`--firmware DIR`, or the window's firmware field); the Fonts step extracts them to
|
||||
`shared-fonts/` in the install folder.
|
||||
- Run `python tools/extract_fonts.py --firmware <dir>` with `.venv-py314` and the keys from `config.local.toml`, or
|
||||
point `TOTK_SHARED_FONTS` at a folder that holds the extracted fonts.
|
||||
|
||||
## Linux release
|
||||
|
||||
`tools/make_release_linux.py` builds the same setup for Linux and packs it as `TotK-Setup-x86_64.AppImage` (the static
|
||||
type-2 runtime: no libfuse2 needed on the host). The player makes it executable and starts it; on a Steam Deck, from
|
||||
Desktop Mode. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
|
||||
Desktop Mode. The command-line setup needs the desktop session's display (`DISPLAY` or `WAYLAND_DISPLAY`) because preparing the
|
||||
shaders opens a window; the check step fails at once without it. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
|
||||
Xft against the sniper sysroot so Russian text uses system TrueType fonts. The setup builds the game into the folder they pick, as on Windows, and the game is `totk` there with
|
||||
`libSDL3.so.0` beside it. The folder inside the AppImage:
|
||||
|
||||
@@ -125,8 +199,8 @@ python/ python-build-standalone 3.14 (with tkinter) + the installe
|
||||
tools/ and recomp/ on sys.path
|
||||
llvm/ LLVM 23 (clang, lld, llvm-ar) and llvm/sysroot: the files of the Steam Runtime 3 "sniper" SDK the
|
||||
game's compile and link read (about 130 files)
|
||||
runtime/ libtotkrt.a, headers, the install-side sources, totk.png; deps/ with the Vulkan headers, VMA and a
|
||||
static zstd
|
||||
runtime/ libtotkrt.a, libtotkrt-v2.a, headers, the install-side sources, totk.png; deps/ with the Vulkan
|
||||
headers, VMA and a static zstd
|
||||
bin/ totk_xlate, hactool, ninja, libSDL3.so.0
|
||||
```
|
||||
|
||||
@@ -149,7 +223,8 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
|
||||
|
||||
## Constraints
|
||||
|
||||
- The installer steps contain no Win32 calls: Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The installer steps contain no Win32 calls; only the window's DPI awareness (`installer/dpi.py`) is Windows-only, and
|
||||
it does nothing elsewhere. Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The keys file path is passed to the tools; key values never appear in the UI, logs or files.
|
||||
- The window is a front end over the CLI (`python -m installer --base ... --update ... --keys ... --lang ... --dest
|
||||
...`), which prints one JSON progress event per line. Tests and CI use the CLI.
|
||||
@@ -161,8 +236,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.
|
||||
@@ -68,6 +68,7 @@ Source shorthands:
|
||||
| 2.9 | **LOD**: TSC min/max LOD clamp, LOD bias, `TWO_D_NO_MIPMAP` texture type, a min filter without mips stays on the base level; TIC carries its own max anisotropy and aniso quality fields. | `minLod`/`maxLod`, `mipLodBias` limited by `maxSamplerLodBias`. | yuzu: anisotropy heuristics for mods forcing trilinear (YZ-2306, PR 10744). Ours: non-mip filter → `maxLod = minLod + 0.25`, bias clamped (`vk_texture.c` `make_sampler`). | G80TEX |
|
||||
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | yuzu adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). Ours: the translator biases gather coordinates by the host's `subTexelPrecisionBits` on every non-NVIDIA GPU (`vk_caps.c` → job `gather_bias` → Ryujinx `TexturePass.InsertCoordGatherBias`); TotK on RADV drew dashed dark lines along wall and floor edges in the prologue caves without it. `TOTK_GATHER_BIAS=0` turns it off. | — |
|
||||
| 2.11 | **Gather offsets**: `TLD4.PTP` takes four per-texel offsets, possibly from registers. | `ConstOffsets` must be constant. | Ours: `QueryHostSupportsNonConstantTextureOffset() => false` makes Ryujinx emulate register offsets (`tools/xlate/Program.cs`). | RYU-SH `Translation/Optimizations` (TexturePass) |
|
||||
| 2.13 | **TXD gradient register order**: TotK's terrain shaders fill the gradient registers of a `TXD` per coordinate (du/dx, du/dy, dv/dx, dv/dy). | Vulkan's `Grad` takes dPdx (du/dx, dv/dx) and dPdy (du/dy, dv/dy). | Ryujinx's `EmitTxd` takes the first two registers as dPdx and the last two as dPdy, so the mip level came from the wrong derivatives and was too sharp on terrain faces turned across the screen: patches that followed the faces crawled when the camera moved (1.4.3, the Depths; found by the user from their F9 frame, a gradient sample's coordinate and gradients traced back to `DPdx` and `DPdy` of the interpolants). Ours: `gfx_shader.c` `patch_txd_transpose` swaps the two inner components of every two-component gradient pair in the loaded SPIR-V (the shader cache is untouched). `TOTK_TXD_TRANSPOSE=0` turns it off. Only 2D gradients are handled; 3D and cube ones are left as translated. | — |
|
||||
| 2.12 | **Depth compare** (`TSC` depth compare + `TIC_4_DEPTH_TEXTURE`), min/max reduction filtering. | `compareEnable`; reduction mode needs `samplerFilterMinmax` and is not allowed with compare. | Ours: reduction dropped when compare is on (`vk_texture.c` `make_sampler`). | G80TEX |
|
||||
| 2.13 | **ASTC / BC**: native ASTC; zero ASTC block = error colour on hosts. | AMD and NVIDIA desktop GPUs lack ASTC: decode to RGBA8. yuzu offered ASTC recompression to BC1/BC3 for low VRAM (YZ-2305, PR 10398). | Ours: native upload where the device samples ASTC, else a compute-shader decode (`vk_astc.c`, `TOTK_ASTC`), CPU decode (`tex_astc.c`) as the fallback. | `runtime/vk_astc.c` |
|
||||
| 2.14 | **Linear CPU-written textures** (`FLAGS_LINEAR`, small ramps, glyph caches): CPU writes visible to the GPU directly. | Must re-upload or use linear images over host memory. | NXN Mario: sky black without its 64×1 RGBA16F ramps. | NXN |
|
||||
@@ -85,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
|
||||
|
||||
@@ -142,6 +146,7 @@ Source shorthands:
|
||||
| TotK | "detective mode" Link on AMD | AMD EDS3 colour-blend driver regression | YZ-2306 PR 10946 |
|
||||
| BotW | shadow artifacts at object edges (AMD) | gather rounding | yuzu commit 780240e (2.10) |
|
||||
| TotK | dashed dark lines along wall and floor edges, changing with the camera angle (AMD, RADV) | gather rounding | gather bias (2.10) |
|
||||
| TotK | blocky foliage shadows that jump instead of sliding | not a GPU difference: the engine sizes its sun shadow map to the area it covers, down to about 400 near the ground, and each change of size moves the shadow texels | fixed size, `shadow_res` (the shadow set of `recomp/patches-v121.txt`; the community mods force 1024 the same way) |
|
||||
| BotW | black textures on GCN | HTILE (depth compression) | `AMD_DEBUG=nohyperz` wiki (1.9) |
|
||||
| BotW | 3D scene black | deferred texture handles | NXN |
|
||||
| BotW | albedo scattered squares | duplicate colour attachments | NXN |
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
# Mods
|
||||
|
||||
TotK Recomp loads the mods players make for the Switch emulators (Ryujinx, yuzu and its forks): texture and model
|
||||
packs, gameplay and text changes from GameBanana and similar sites, TKMM's merged output, code patches (`.ips`,
|
||||
`.pchtxt`) and Atmosphère cheat files. No mod or cheat file ships with the port.
|
||||
|
||||
## Install a mod
|
||||
|
||||
1. Open the settings menu (F1, Esc, or Minus and Plus together on a controller), go to the Mods tab and choose
|
||||
**Open mods folder**. That is the `mods` folder next to `totk.exe`; the menu creates it when it is missing.
|
||||
2. Make one folder per mod in it and unpack the mod's archive into that folder. The mod's own folders (`romfs`,
|
||||
`exefs`, `cheats`) must sit directly inside the mod's folder:
|
||||
|
||||
```
|
||||
mods\
|
||||
Better Grass\
|
||||
romfs\
|
||||
...
|
||||
Fast Travel Anywhere\
|
||||
exefs\
|
||||
patch.pchtxt
|
||||
My cheats\
|
||||
cheats\
|
||||
277178B7DBA1B6D4.txt
|
||||
```
|
||||
|
||||
An archive laid out for a Switch SD card or an emulator works as it is. Inside the mod's folder the port also finds
|
||||
`atmosphere\contents\0100F2C0115B6000\romfs`, `contents\0100F2C0115B6000\romfs` and `0100F2C0115B6000\romfs`, the
|
||||
same places for `exefs` and `cheats`, and `atmosphere\exefs_patches\<name>\` for code patches.
|
||||
3. Start the game. A mod that patches the game's code makes the game rebuild itself first (see Code patches below).
|
||||
4. Open the Mods tab to check the mod. Each row says how many game files the mod replaces and adds, how many cheats it
|
||||
brings, and why a part of it is not used.
|
||||
|
||||
## Order and switches
|
||||
|
||||
The Mods tab lists every folder in `mods`. Select a mod and press A or Enter to switch it on or off. The top face button
|
||||
or Ctrl+Up moves it up, the left face button or Ctrl+Down moves it down. When two mods change the same file, the
|
||||
higher one wins; the lower mod's row says how many of its files the higher one covers. Changes apply after a restart.
|
||||
|
||||
The menu saves the order and the switches in `totk.ini`, under `[mods]`, by folder name: `mods_order` and
|
||||
`mods_disabled`. A folder the list does not name yet, such as a new or renamed one, goes below the named ones.
|
||||
|
||||
### Mods that change the same files
|
||||
|
||||
Two mods that edit the same game file do not combine: the higher one wins the whole file. Merge such mods with
|
||||
[TKMM](https://github.com/TKMM-Team/Tkmm) (TotK Mod Manager) and set its export location to an empty folder inside
|
||||
`mods`, for example `mods\TKMM`. The exported folder then counts as one mod here. Mods packaged for TKMM only
|
||||
(`.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, 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)
|
||||
|
||||
A mod with `.ips` or `.pchtxt` files patches the game's code. A patch is for one game update: an `.ips` file is named
|
||||
after the build id of the update's code, a `.pchtxt` file names it on its `@nsobid` line. The game's code is compiled
|
||||
ahead of time, so these patches are built into `totk.exe`:
|
||||
|
||||
1. The first start after such a mod is added closes the game and opens the setup's rebuild window. The rebuild takes
|
||||
a few minutes (the shaders stay prepared), then the game starts again. Saves and settings are kept.
|
||||
2. Until the rebuild succeeds, the whole mod stays off, its other files included.
|
||||
3. After the rebuild the mod switches on and off from the Mods tab like any other, without another rebuild.
|
||||
|
||||
The rebuild needs the setup folder the game was installed from. When that folder was moved or deleted, run
|
||||
`TotK-Setup.exe` again on the same install folder: it does the same job. `mods_rebuild = off` under `[mods]` in
|
||||
`totk.ini` turns the automatic rebuild off.
|
||||
|
||||
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
|
||||
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
|
||||
|
||||
A patch file may also change the game's data (constants and tables in its `.rodata` and `.data`). Those bytes are
|
||||
stored into the loaded game at start, before it runs. When two mods write the same bytes, the higher one wins,
|
||||
as with files.
|
||||
|
||||
## Cheats
|
||||
|
||||
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
|
||||
that change the game's code need the same rebuild as code patches. [cheats.md](cheats.md) explains the file names for
|
||||
each game update, the rebuild and which cheats cannot run.
|
||||
|
||||
## Renaming a mod folder
|
||||
|
||||
The port keys a mod by its folder name. A renamed folder counts as a new mod:
|
||||
|
||||
- it moves below the mods the list names, and comes back switched on;
|
||||
- its cheats come back switched off;
|
||||
- its code patches and code cheats are built again, so the next start runs the rebuild.
|
||||
|
||||
Rename mod folders before you set them up in the menu, not after.
|
||||
|
||||
## HD texture packs
|
||||
|
||||
Texture packs made for the emulators replace the game's textures with larger ones, so the heap the world's textures
|
||||
stream into must grow. Such a pack grows it in its own `System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs`, sized for
|
||||
the emulators' 8 GB memory option. The port reads that file at start: the texture heap and the game's memory pool grow
|
||||
by the pack's amount, and the game gets the port's own heap table with that growth (the log says
|
||||
`resolution: mod <name> ships a HeapDef with TextureHeap ...`). Nothing needs to be set. For a pack without such a file
|
||||
that still loops the panic Blood Moon, set `tex_heap_extra_mb` under `[mods]` in `totk.ini` (0 to 4096 MiB).
|
||||
|
||||
Tested: **HDPacks.com Tears of the Kingdom 4K Texture Pack v1.0** (`4kTextures`, 10,407 textures, 6.5 GB) on game 1.4.3
|
||||
and 1.2.1, RTX 4080 SUPER: it loads into the game from a save and runs at the same frame rate, with about 3.3 GB more of
|
||||
textures in memory (4.5 GB of game memory pools against 1.2 GB without it). Other packs are not tested and come with no
|
||||
guarantee. Such a pack needs a lot of video memory; on a GPU with less than 8 GB, `astc_bc = 1` under `[performance]`
|
||||
keeps its textures at a quarter of the size.
|
||||
|
||||
## What does not work
|
||||
|
||||
| On the Mods tab | Why | What to do |
|
||||
|---|---|---|
|
||||
| This folder has no romfs, exefs or cheats files. | The archive was unpacked one level too deep (`mods\Mod\Mod\romfs`), or it holds no game files. | Move the folders up so `romfs` sits directly in the mod's folder. |
|
||||
| ... files are made for another game version and are left out. | The mod's files carry another update's version in their names (`.Product.<number>.`). | Download the mod's release for your game update. |
|
||||
| Its code patches (exefs) are made for another game version. | The patch files name another update's build id. | Same: get the release for your update. |
|
||||
| This mod brings its own game code (a subsdk plugin or a replaced main). | Mods such as UltraCam ship compiled Switch code. The port cannot load it. | No workaround; the whole mod stays off. |
|
||||
| This mod patches the game code (exefs) in a way the port cannot apply. | The patch writes the game's data, not its code, or changes code in a form the port cannot build. | No workaround; the reason follows on the row. |
|
||||
| The folder name contains \|. | The mod list uses `\|` between names. | Rename the folder. |
|
||||
|
||||
A mod never stops the game from starting. Whatever the port cannot use is left out and shown on the Mods tab.
|
||||
|
||||
## When something looks wrong
|
||||
|
||||
1. Switch the newest mod off on the Mods tab and restart. If the problem goes away, check that the mod is made for your
|
||||
game update and does not fight another mod over the same files.
|
||||
2. `logs\totk.log` next to `totk.exe` names every mod at the start (lines starting with `mods:`), the cheats
|
||||
(`cheats:`) and the resource size table (`RomFS: resource size table:`). Open log folder on the Advanced tab of
|
||||
the menu shows the logs.
|
||||
3. When reporting a problem, attach `totk.log` and list your mods with the links you downloaded them from.
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# Profile-guided build
|
||||
|
||||
A profile-guided build compiles the generated code and the runtime at -O2 with an execution profile: a record of
|
||||
which functions and branches ran while someone played. Hot paths are laid out straight, hot calls are inlined, and code
|
||||
that never ran is compiled for size. On 1.4.3 a trained fight ran 12% faster per frame and an untrained fight about
|
||||
6%; on 1.2.1 the trained fight ran 11% faster.
|
||||
|
||||
Each game version has its own profile, `pgo-v121.profdata` and `pgo-v143.profdata`, trained on that version's code.
|
||||
|
||||
## How a build uses a profile
|
||||
|
||||
- `build_game.py --pgo <file>.profdata` compiles with that profile. Every compile depends on the file, so a new
|
||||
profile rebuilds everything.
|
||||
- `tools/pgo.py merge` writes the profile to `build\pgo\pgo-<ver>.profdata`, next to its record `pgo-<ver>.json`. The
|
||||
record holds the git tree of `recomp/` it was trained on and the digests of the runtime headers the generated C
|
||||
includes (`cpu.h`, `simd.h`, `simd_fp.h`).
|
||||
- `make_release.py` ships every profile in `build\pgo` in the release's `data\` folder. It warns when a record names
|
||||
another `recomp/` tree than the one released, or a header digest differs. The runtime libraries of the release
|
||||
compile with the newest version's profile.
|
||||
- The installer builds a player's game with `data\pgo-<ver>.profdata` when the release has one for that version, and
|
||||
at -O1 without it.
|
||||
|
||||
A profile only fits the code it was trained on. clang matches each function by name and control-flow hash, and a
|
||||
function whose code changed loses its counts without a warning (`build_game.py` passes `-Wno-backend-plugin`, which
|
||||
hides the "hash mismatch" messages). The inline helpers of `cpu.h`, `simd.h` and `simd_fp.h` become part of every
|
||||
lifted function, so a change to them invalidates the profile as surely as a generator change: one change to the
|
||||
floating-point helpers left a profile with a fifth of its weight and 2% of its gain.
|
||||
|
||||
## When to retrain
|
||||
|
||||
- After a change to the lifter or the generator (`recomp/`) that changes the generated C. Patch table changes
|
||||
(`recomp/patches-<ver>.txt`) touch only the patched functions and can wait for the next retrain.
|
||||
- After a change to `runtime/cpu.h`, `runtime/simd.h` or `runtime/simd_fp.h`.
|
||||
- Before a release whose `make_release.py` run warns about a profile.
|
||||
|
||||
Train both versions from the same commit, from a clean `recomp/`.
|
||||
|
||||
## Train
|
||||
|
||||
### 1. Instrumented build
|
||||
|
||||
MSYS2's clang has no profile runtime, so the instrumented build uses llvm-mingw (unpack it under `toolchain\`):
|
||||
|
||||
```powershell
|
||||
$llvm = "toolchain\llvm-mingw-<date>\llvm-mingw-<date>-ucrt-x86_64"
|
||||
.venv\Scripts\python tools\build_game.py --version v143 --out build\v143-pgogen --toolchain $llvm --pgo gen
|
||||
```
|
||||
|
||||
Regenerate `generated\<ver>` first if it is older than the commit you train on. If `totk.exe` stops at start with
|
||||
`0xc000007b`, copy `libiconv-2.dll` from `C:\msys64\ucrt64\bin` next to it.
|
||||
|
||||
`totk.exe` writes `build\v143-pgogen\pgo\default_<id>.profraw` every minute and at exit. Each run of the same exe adds
|
||||
to that file.
|
||||
|
||||
### 2. Shaders
|
||||
|
||||
Copy your `totk.ini` into the build folder and prepare the shaders (`tools\prepare_shaders.py build\v143-pgogen
|
||||
--version v143`). Shader preparation runs `totk.exe` too, so move `pgo\*.profraw` out of the build folder afterwards:
|
||||
its counts describe shader building, not play.
|
||||
|
||||
Keep the default settings, with the GPU command recorder on (`vk_recorder = 1`, the default).
|
||||
|
||||
### 3. Training scenes
|
||||
|
||||
The instrumented build runs slower than a normal one. Count each scene's time by the game clock, not by the wall
|
||||
clock. Spend 90 to 120 seconds on each of the first four scenes, where most frames are spent, and about 60 seconds on
|
||||
each of the others, which cover code the first four do not reach.
|
||||
|
||||
| Place | What to do | Time |
|
||||
| --- | --- | --- |
|
||||
| Lookout Landing | Stand in the middle of the town and turn the camera slowly all the way round, then walk through the crowd. | 120 s |
|
||||
| Kakariko Village | The same: a slow camera turn, then walk between the houses. | 100 s |
|
||||
| A fight with a Battle Talus | Fight it with weapons and the bow. | 110 s |
|
||||
| The Depths, near a Lightroot | Turn the camera over the lit area, throw a Brightbloom Seed. | 90 s |
|
||||
| The Depths, away from any light | Walk and turn the camera in the dark. | 60 s |
|
||||
| The edge of a sky island | Run off the edge, skydive, open the paraglider. | 60 s |
|
||||
| Outdoors in the rain | Walk and look around while it rains. | 60 s |
|
||||
| A cave with Brightbloom plants | Walk in and fight the Moblins and Bokoblins inside. | 60 s |
|
||||
| Inside a shrine | Walk through the first rooms. | 60 s |
|
||||
| A Moblin camp | Fight several Moblins at once. | 60 s |
|
||||
| Next to Yunobo | Use his ability: roll him into something. | 60 s |
|
||||
| Hyrule Field, by the standing stone pillars | Run across the field, also backwards past the pillars. | 60 s |
|
||||
|
||||
Keep two scenes out of the training, so that you can measure on scenes the profile has not seen: Hyrule Field in tall
|
||||
grass, and a fight with Moblins and Zonai constructs at once.
|
||||
|
||||
With a save at a scene, `tools\run_game.py` plays it without you: `--save <backup> --script <input file> --seconds
|
||||
<game time>` ([agents/running.md](agents/running.md)). Without a save, play the scenes by hand.
|
||||
|
||||
### 4. Free play
|
||||
|
||||
Then play for 15 to 25 minutes and touch every kind of thing the game does, so that no common path is left without
|
||||
counts:
|
||||
|
||||
- Run, sprint, climb, swim and glide; ride a horse; jump from a tower and skydive.
|
||||
- Fight with one-handed and two-handed weapons, a spear and the bow; shield parry, flurry rush, fused arrows.
|
||||
- Use every ability: Ultrahand (build a vehicle with a fan and wheels and drive it), Fuse, Ascend, Recall, Autobuild.
|
||||
- Call the sages and use their abilities: Tulin, Riju, Sidon, Yunobo, Mineru.
|
||||
- Open the inventory, the map and the camera; cook at a pot; talk to people in a town.
|
||||
- Travel quickly to a few places, so that areas load; enter and leave a shrine and the Depths.
|
||||
- Play in daytime and at night, in clear weather and in rain or snow.
|
||||
|
||||
### 5. Merge
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\pgo.py merge build\v143-pgogen --version v143
|
||||
```
|
||||
|
||||
writes `build\pgo\pgo-v143.profdata` and its record. Merge right after the training, from the checkout and commit the
|
||||
instrumented build was made in: the record reads the `recomp/` tree from git and the header digests from the build's
|
||||
Ninja deps log.
|
||||
|
||||
Repeat steps 1 to 5 with `v121` for 1.2.1.
|
||||
|
||||
## Measure
|
||||
|
||||
Build the same code three ways in three folders with llvm-mingw, as the installer does: with the new profile, with
|
||||
the old one and with none. Prepare the shaders in each folder. Then run the held-out scenes and one trained scene
|
||||
in the order new, old, none, none, old, new, and compare the mean frame time over the same stretch of game time. One
|
||||
run of a fight can vary by up to 15% from the next, so a profile counts as better only when it wins both pairs.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Rules for taking a PR
|
||||
|
||||
## 1. What to review first
|
||||
|
||||
1. Fixes before features. A PR that fixes a bug, a glitch or a crash goes first; feature PRs wait.
|
||||
2. Order fixes by reach: how many players the bug hits and how badly. Windows and default settings first, rare
|
||||
hardware and unusual setups later.
|
||||
3. Scale the depth of the checks to the reach. A crash many players hit and anything on the frame path get the full
|
||||
checks. A rare case (a CPU without AVX, an unusual setup) gets the minimal fix, one targeted check and a short
|
||||
note. A crash fix on rare hardware is still worth taking: cut the checks around it, not the fix.
|
||||
4. Before testing, find out where the bug reproduces: OS, GPU vendor and generation, game version, settings. If the
|
||||
symptom cannot appear on the hardware at hand (an NVIDIA paging bug on an AMD machine), say so and do not run
|
||||
tests that cannot decide it.
|
||||
5. Check a feature PR against the designs in `docs/milestones/`. A feature with a design (mods, cheats) goes in as
|
||||
part of that design.
|
||||
6. Several PRs on one topic: compare them before taking either. Another PR may already do it right.
|
||||
|
||||
## 2. How to read the diff
|
||||
|
||||
- The author's claim is a hypothesis. Read the whole diff: races, wrong semantics, edge cases, leaks, lock order.
|
||||
- Check the behaviour against evidence: how yuzu, Ryujinx, LAS Recomp, SavageWindRecomp, nxnative and MacNativeGames
|
||||
do the same thing in `refs/` (`tools/fetch_refs.sh` recreates it); the Vulkan spec; hardware docs
|
||||
([docs/maxwell.md](../../docs/maxwell.md)); upstream issues.
|
||||
- Weigh two things for every change: its cost on the hot path, and how it fails (device lost, hang, stale data, weak
|
||||
or non-AVX CPUs, other GPU vendors, Wine/Proton).
|
||||
- Read the code a hypothesis depends on before running the game: the game's functions in Ghidra or
|
||||
`generated/<ver>`, our path in `runtime/`. A run confirms what the code could not rule out. It is one run, not a
|
||||
series of variants.
|
||||
- Order the work so the cheapest check that could refute the PR's idea runs first. Refuted: stop, write down why,
|
||||
no build, no benches.
|
||||
|
||||
## 3. What a PR needs
|
||||
|
||||
- **Proof.** The bug reproduced before the fix, the same repro after, both results side by side: a screenshot, a log
|
||||
line, a probe, a differential test. A fix without a reproduced failure is not proof. Where a repro is impossible
|
||||
on the hardware at hand, say so; the fix is then taken only on strong static evidence in the code. Where a repro is
|
||||
risky (device lost or a TDR can hang the machine), write the recipe and let the owner decide.
|
||||
- **Frame-path changes** get whole runs of the heavy scene with the switch off and on, at least two each
|
||||
([02-performance.md](02-performance.md)). Zero new slow frames is a hard bar.
|
||||
- **A switch.** A change on the hot path or in rendering sits behind an env or `totk.ini` switch, so a player can turn
|
||||
it off and we can compare.
|
||||
- **Nothing changes for players outside the feature.** The DLSS PRs touched shared code; they went in once every
|
||||
path that runs without DLSS was gated on DLSS being on.
|
||||
- **A fix for one setup keeps the default for everyone.** The PRs that raised the low memory block from 6 to 10 GiB for
|
||||
all players were rejected; the right shape is 6 GiB plus the extra where it is needed.
|
||||
- **Parts we cannot run** (Linux-only code, an HDR display, RTX-only paths) are gated so they cannot touch anyone
|
||||
else, or wait for a report from someone with that hardware. Note them as unrun.
|
||||
- **Project tests** from [03-correctness.md](03-correctness.md): lifter differential tests, probes, `--selftest`,
|
||||
`tools/check_content.py --staged`.
|
||||
- **Text the player sees** goes into the catalog: `runtime/ui_text.h` ids and `runtime/ui_text.c` entries in every
|
||||
language the menu has (English, Russian, Brazilian Portuguese, Simplified Chinese), and `installer/text/*.json` for
|
||||
the installer. No hardcoded sentences, no second string table.
|
||||
- **A new setting** is described in `totk.ini.example` and [docs/settings.md](../../docs/settings.md).
|
||||
- **Licences.** MIT, BSD, ISC, zlib and Apache-2.0 code may be copied, with its header and a row in
|
||||
`THIRD_PARTY_NOTICES.md`. yuzu, suyu and other GPL code is read for behaviour only; we write our own.
|
||||
|
||||
## 4. Reasons to reject or rework
|
||||
|
||||
- No test or repro for a change that is not trivial.
|
||||
- Slow: a state copy per draw, a global atomic counter on the hot path, synchronous file I/O or a GPU allocation on a
|
||||
game or render thread, new slow frames.
|
||||
- Changes behaviour for everyone to serve one setup, or is on by default without a run in a scene where it acts.
|
||||
- "Fixes" a crash by turning a feature off for a vendor. A crash that goes away with a feature off points at the bug;
|
||||
the feature gets fixed.
|
||||
- Breaks another feature (a preview that loses anti-aliasing).
|
||||
- Fake features: labelled FSR4 or frame generation, and not that.
|
||||
- Wrong measurements: percentiles over window means, counters over the wrong set.
|
||||
- Generated C edited by hand instead of the generator (`recomp/`).
|
||||
- A disallowed licence or pasted GPL code.
|
||||
|
||||
## 5. Finishing a good PR
|
||||
|
||||
A good PR that misses the project's scheme (hardcoded text, conflicts, layout, comment style) is brought to it while
|
||||
merging, not sent back to the author for another round. When only part of a PR is good, take that part and note what
|
||||
was left out and why.
|
||||
|
||||
A bug found outside the PR under review is not fixed on the spot: search the open PRs on that topic, report the bug
|
||||
with them, and let the owner decide.
|
||||
|
||||
## 6. Merging, branches, commits
|
||||
|
||||
- Each PR or wave of PRs gets its own branch `task/<tag>` from a fresh `dev` and its own build dir. It merges into
|
||||
`dev`.
|
||||
- Once the review is done, the symptom is gone on one paired shot and the probes pass, merge into `dev` and hand the
|
||||
build to players: they test in game faster and wider than our routes. The wider checks (other routes, validation,
|
||||
fps cost) follow as a separate diff. Speed never skips the review of the diff itself.
|
||||
- `main` only with the owner's explicit yes. Push only when told to.
|
||||
- A commit message is a plain English sentence, no `feat:`/`fix:` prefixes.
|
||||
- The version lives in the repo-root `VERSION` file; the owner decides when it moves.
|
||||
- After a merge, record which PRs went in, what was reworked, what was rejected and why. The reason for a rejection
|
||||
goes into the PR as a plain English comment.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Performance: no lost frames on weak hardware
|
||||
|
||||
## Who we tune for
|
||||
|
||||
- The goal is many more fps on modest hardware, and Steam Deck later (Zen 2 4c/8t APU, shared LPDDR5, the exe runs
|
||||
through Proton/Wine).
|
||||
- The developer's machine (Ryzen 9 7900X + RX 9070) is fast and its numbers do not carry over. A laptop with an
|
||||
i7-9750H and a GTX 1050 Ti is a sample of a weak machine, not the target to fit constants to.
|
||||
- What weak hardware measured:
|
||||
- GTX 1050 Ti (4 GiB, 214 MiB BAR, no ReBAR): the GPU read imported host pools over PCIe, 110-120 ms a frame
|
||||
against 4.3 ms on the RX 9070. The VRAM mirror brought it to 10-11 ms; that machine is now CPU-bound at 25-45 fps,
|
||||
replay 25-30 ms a frame.
|
||||
- The same write-watch scan costs about 3x the CPU time there.
|
||||
- NVIDIA and desktop AMD GPUs have no ASTC: textures decode in a compute shader. CPU decodes before that made
|
||||
0.4-0.7 s load frames on the 1050 Ti.
|
||||
- Wine emulates `GetWriteWatch` with `mprotect` and a fault per first write. Anything relying on cheap OS memory
|
||||
tricks (write-watch, `VirtualProtect` freezing, fault handlers) is checked against Wine's implementation.
|
||||
- The port uses about half of even a weak CPU, so the frame's critical path is the limit, not throughput. TotK keys
|
||||
per-core data by `GetCurrentProcessorNumber` without locks, so the game's threads share three virtual cores on any
|
||||
host. In a fight the main thread and five job workers keep 2.6 of the 3 busy: guest code speed sets the fps.
|
||||
- Perf work builds and measures 1.4.3. A 1.2.1 build is for a change about to merge with a large gain.
|
||||
|
||||
## Gain ceiling first
|
||||
|
||||
Before writing or taking a perf change, compute its ceiling: the share of the code it removes times the share of the
|
||||
frame that code holds on the critical path, on weak hardware. A ceiling of a few percent or less is not worth it.
|
||||
|
||||
- Use the critical path (`TOTK_CRIT` + `tools/crit_path.py`, heavy frames only), not one thread's profile. On the weak
|
||||
laptop the submit thread is 55% replay, yet the critical path of heavy frames is 67% guest code, 15%
|
||||
`nvnSyncWait`, 13% core waits and wake latency.
|
||||
- Changes that can move the frame by 10% or more are usually design changes. Polishing a hot function from a flat
|
||||
profile almost always gives zero.
|
||||
|
||||
## A neutral safe change stays
|
||||
|
||||
An optimization that measures neutral on the fast machine stays on by default when:
|
||||
|
||||
- upstream (`refs/`, other ports) reports a gain for it, especially on weak CPUs or GPUs;
|
||||
- the static read leaves no hazard, and the probe or differential test passes;
|
||||
- whole heavy-scene runs off/on x2 show no fps loss and no new slow frames;
|
||||
- a switch turns it off.
|
||||
|
||||
"No gain here" is not a reason to drop a safe change with upstream evidence. A regression or a hazard nobody ruled
|
||||
out is.
|
||||
|
||||
## How to measure
|
||||
|
||||
- **By game time, never by frame counts.** Run length, analysis windows, A/B blocks and input scripts go by the world
|
||||
clock (`--seconds`, `TOTK_CRIT=w<lo>-<hi>`, `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, `TOTK_SHOTS=w<lo>-<hi>:<step>`). The
|
||||
same frame count at another fps is another scene.
|
||||
- **Check a scripted run reached the world.** Title menus on a slow boot eat button presses; confirm the run is in the
|
||||
world before trusting its numbers.
|
||||
- **Whole runs off and on, two each at least.** An in-run A/B on a moving scene lies: the A and B blocks land on the
|
||||
same game seconds every run, so a content difference repeats. One in-run A/B showed "-11% replay"; whole runs gave
|
||||
5.84/5.73 against 5.76/5.73 ms, nothing. In-run A/B only on a static scene (standing still).
|
||||
- **Spread.** One script on near-identical builds gave 1% lows from 31.5 to 50 fps. Compare medians of three runs,
|
||||
never one run against one run.
|
||||
- **A quiet machine.** `tools/run_game.py --quiet` takes the machine-wide lock; no other `totk.exe`, build or test runs
|
||||
meanwhile. One game at a time.
|
||||
- **A warm shader cache, prepared properly.** Shader and pipeline caches are not copied between build dirs:
|
||||
`totk.exe --prepare-shaders` or `--warm-shaders` in that dir. The AMD driver cache is keyed to the exe path and
|
||||
limited in size: preparing another build dir can evict this one's libraries (40-220 ms per new effect in play).
|
||||
- **A heavy scene.** A fight with a large enemy and a crowd, fps limit at the maximum (`TOTK_FPS_LIMIT=360`). Compare
|
||||
one shot with the scene's reference before trusting numbers.
|
||||
- **No base runs by habit.** Run the candidate alone and judge it against the targets: max fps, zero slow frames
|
||||
(`perf: slow frame`), no black or flat frames, no new validation errors. Glitches are found by looking at shots and
|
||||
frame dumps, not by diffing metrics. Run a base build only when a specific question needs a side-by-side.
|
||||
|
||||
| What | How |
|
||||
|---|---|
|
||||
| fps, worst frame, render-thread buckets, GPU busy, VRAM | `--env TOTK_TRACE=perf,sched`, then `python tools/perf_summary.py <log> <lo> <hi>` |
|
||||
| the frame's critical path | `TOTK_CRIT=w<lo>-<hi>` + `TOTK_CRIT_OUT=<path>`, then `python tools/crit_path.py <file>` |
|
||||
| sampling profiler | `TOTK_PROFILE=<path>` + `TOTK_PROFILE_FRAMES=w<lo>-<hi>`, then `tools/prof_report.py <profile> <totk.map>` |
|
||||
| A/B on a static scene | `TOTK_AB=<seconds>`, then `tools/ab_summary.py --skip <seconds>` |
|
||||
| VRAM pressure on a large card | `TOTK_VRAM_LIMIT_MIB=<MiB>` caps the budget the port sees |
|
||||
|
||||
## What the hot path must not do (Windows host)
|
||||
|
||||
- **Files.** The game threads (main, job workers) and the render thread never open, read, write or flush a file, and
|
||||
never wait for a thread that does. Opening a 20-byte file in a 139k-file folder: 0.1 ms idle, 11-40 ms in play while
|
||||
the translator, the pipeline cache and the save wrote to the same volume. A save's `FlushFileBuffers` and
|
||||
`MOVEFILE_WRITE_THROUGH`: 93-135 ms. Saving the pipeline cache under its lock on the present thread froze the replay
|
||||
every 20 s. File I/O goes to a background thread; results come back in memory.
|
||||
- **Waits.** Short waits spin on the counter; waits over 1 ms use the high-resolution timer. A waitable timer wakes
|
||||
0.3-0.5 ms late; the SDK's 20 us sleeps took 460-515 us. A 1 ms timer thread does not help: it gave more slow frames.
|
||||
- **GPU allocations.** VMA's `vkAllocateMemory` of a 256 MiB block under its lock: 8-23 ms. A new block on the render
|
||||
thread is a hitch.
|
||||
- **128-bit division.** `(unsigned __int128)t * num / den` compiles to the library call `__udivti3`; counter
|
||||
conversions that way took a tenth of the game threads' time in a fight. Use the precomputed scale (`platform.h`
|
||||
`PlatScale`).
|
||||
- **Per-draw copies and counters.** A pipeline-state copy per draw, a global atomic counter, a scan of every page each
|
||||
frame: on a weak CPU each is frames.
|
||||
- **Memory kept per frame.** Anything allocated per texture initialization or per frame must be freed. One copy kept
|
||||
per texture init grew host memory by ~44 MiB a game minute until a commit failure after hours (#670). Check private
|
||||
memory over a few idle minutes for a PR that stores per-object state.
|
||||
- **Scheduling.** Any timing or scheduling change is checked for slow frames. A preempt check at every host call made
|
||||
one bug rarer but added 50-75 ms CPU hitches and was reverted.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Correctness: checks that keep behaviour intact
|
||||
|
||||
## Lifter and generated C
|
||||
|
||||
- Generated C is never edited by hand: change the generator in `recomp/`.
|
||||
- `recomp/layout.py` alone decides module bases.
|
||||
- Every change to the lifter or the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) passes the differential tests
|
||||
against Unicorn, all run with `.venv`:
|
||||
|
||||
| Test | Checks |
|
||||
|---|---|
|
||||
| `tests\diff\lift.py` | instructions sampled from the modules; `--coverage` lists the words the lifter rejects |
|
||||
| `tests\diff\simd.py` | the whole FP/SIMD encoding space |
|
||||
| `tests\diff\atomic.py` | exclusive load/store and ARMv8.1 atomics |
|
||||
| `tests\diff\func.py` | whole functions as the generator emits them: loops, jump tables, calls |
|
||||
| `tests\flags_check.py` | the function-local flag helpers against the packed ones |
|
||||
| `tests\excl_mt.py` | the exclusive monitor under contention and ABA, on several threads |
|
||||
| `tests\jumptables_check.py` | `recomp/jumptables.py` against Unicorn: 0 disagreements on 1.2.1 and 1.4.3 |
|
||||
|
||||
- A `--cpu v2` build (no AVX2) passes the same tests with `--cflags=-march=x86-64-v2`. Players without AVX exist.
|
||||
- A compile error in generated C on a line the generator could not have written is a bit flip on disk or in memory.
|
||||
Regenerate and compare hashes; leave `recomp/` alone.
|
||||
|
||||
## Runtime
|
||||
|
||||
- From the build dir: `totk.exe --selftest`, `--probe-kernel`, `--probe-vkexec`, `--probe-nvn-tex`, `--probe-shader`
|
||||
pass.
|
||||
- Settings: `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`.
|
||||
- Button prompts: `.venv-py314\Scripts\python tests\romfs_prompts_test.py`. Build dirs carry only `romfs-patches`; to
|
||||
test PC or Xbox prompts in game, generate the overlay first:
|
||||
`.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts pc --out build\<dir>\romfs-patches-pc`,
|
||||
then restart the game.
|
||||
- Installer: `tests\installer_gui_test.py` (event model, form, catalogs, cancel, display scale).
|
||||
- Shaders: `tools\shader_census.py --version <ver> --translate`, `tools\xlate_smoke.py`. A translator patch
|
||||
(`tools/xlate/patches/`) needs `build/xlate` rebuilt and changes shader hashes: anything keyed to a shader hash
|
||||
breaks with it.
|
||||
|
||||
## GPU portability
|
||||
|
||||
- New Vulkan code queries the feature, extension, format or limit it uses and keeps a working path or a logged
|
||||
fallback without it (provoking vertex and subgroup size pinning in `vk_pipeline.c` are examples).
|
||||
- Never hardcode what one GPU reports: alignments, subgroup size (RDNA4 may run wave64, old AMD Vega is wave64 only),
|
||||
depth formats, `maxPushDescriptors`.
|
||||
- One queue: every submit and present takes `vk_queue_lock`. Presenting from its own thread on a second queue gave
|
||||
device lost on GTX 10-series in another port.
|
||||
- RDNA1/2 bugs do not reproduce on RDNA4 (another driver branch); NVIDIA bugs do not reproduce on AMD. A Ryzen 7000
|
||||
iGPU is RDNA2 and runs the RDNA1/2 driver branch: a cheap testbed for those reports.
|
||||
- Validation: a `--validation` run in the world shows no new messages.
|
||||
|
||||
## Maxwell / NVN behaviour
|
||||
|
||||
The port must do what the Switch GPU does, not what Vulkan does by default. The full list with sources:
|
||||
[docs/maxwell.md](../../docs/maxwell.md), section 7. The traps a PR breaks most easily:
|
||||
|
||||
- Provoking vertex: Vulkan takes the first vertex, NVN the last.
|
||||
- Unwritten vertex outputs read (0, 0, 0, 1).
|
||||
- A colour target outside the fragment shader's output map keeps its texel (write mask 0). AMD writes garbage there.
|
||||
- Feedback loops: the G-buffer pass reads its own targets. It needs the feedback layout and a by-region barrier.
|
||||
- Depth bias argument order, depth mode, depth clamp: checked against deko3d.
|
||||
- Report timestamps: the engine's GPU time comes from them, and dynamic resolution and game speed follow it.
|
||||
|
||||
## Timers and the real-time step
|
||||
|
||||
- With `realtime` set, the game steps by a measured ratio, not whole 30 Hz units. A countdown remainder in
|
||||
(0, FLT_EPSILON] is never stepped again, and a consumer testing `> 0` sees it active forever. That was "weapons
|
||||
cannot be changed after a Fuse until a reload" (#18).
|
||||
- Symptoms of this class: stuck until a reload or teleport, locked after an action, works sometimes.
|
||||
- A PR touching the frame step, the pacer or the fps limit is checked for this class.
|
||||
|
||||
## Threads and scheduling
|
||||
|
||||
- ARMv8 `stlr` then `ldar` is ordered (RCsc); C11 release/acquire lets x86 reorder that store and load. Changes to
|
||||
the lifter's barriers or atomics pass `tests\diff\atomic.py` and `tests\excl_mt.py`.
|
||||
- A scheduler change is judged from one `TOTK_TRACE=sched` run by how long a ready, better thread waits for a core it
|
||||
may use, plus the slow-frame count. Not by statistics over many runs.
|
||||
- A crash that looks like a race: rule out the save first. One reported crash was the save itself; it crashed the
|
||||
same way on a real Switch.
|
||||
|
||||
## Files and caches
|
||||
|
||||
- A crash leaves zero-filled files. Caches are validated on load, not trusted because they exist.
|
||||
- The pipeline cache is dropped when vendor, device or `pipelineCacheUUID` change.
|
||||
- Unicode paths: W APIs with UTF-8 to UTF-16 conversion.
|
||||
- A failure that happens once and does not come back: rerun before debugging it as a code bug. Broken caches and bit
|
||||
flips happen.
|
||||
|
||||
## Game content boundary
|
||||
|
||||
- The repository is public. No game files, and nothing extracted or generated outside `build/` and `generated/`.
|
||||
- Keys never appear in logs, commands, files, issues or comments.
|
||||
- Every file the generator emits carries the `GENERATED_MARKER` defined in `tools/check_content.py`.
|
||||
- `python tools/check_content.py --staged` before every commit. CI runs the same check.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Knowledge base
|
||||
|
||||
Before investigating a symptom, or trusting a PR that "fixes" it, look for it here. Search by what the player sees
|
||||
(black, magenta, flicker, slow motion, missing, stale, streaks) and by the object (sky, grass, shadow, fog, HUD).
|
||||
|
||||
## Invariants of recent changes
|
||||
|
||||
A PR that touches these areas must keep them.
|
||||
|
||||
- **Queue order.** When anything is submitted, every earlier part is already on the queue. Present, uploads, readbacks
|
||||
from guest threads and the `vkDeviceWaitIdle` sites rely on it. The recorder thread now begins, ends and submits
|
||||
the parts it records; the submitting thread still waits for that submission, so the order holds.
|
||||
`TOTK_REC_END=0` restores the old split.
|
||||
- **GPU-written memory is never frozen.** Storage-buffer stores, copies and fills land in imported guest memory
|
||||
without a fault or a write-watch bit, so a region only the GPU writes looks idle. Such regions are never frozen
|
||||
read-only, and a frozen region is thawed before a GPU write. LAS Recomp never changes the protection of imported
|
||||
memory because of NVIDIA. A suspected cause of NVIDIA "write invalid" device faults (#788, #802), not proven.
|
||||
- **VRAM pressure.** Under pressure, released textures whose storage the game has given to another texture go first:
|
||||
30 s after release and once no finalized slot has bound them for 600 submits. Clean mirrored entries wait 5 s.
|
||||
Both stop once use is back under the line. A PR touching texture lifetime is tested with `TOTK_VRAM_LIMIT_MIB`.
|
||||
- **Texture pins are freed.** Each pin counts the recording copies naming it and is freed 1200 submits after its last
|
||||
use unless the replay holds it as a render target. Before that, pins leaked ~44 MiB a game minute (#670).
|
||||
|
||||
## Fixed bug classes
|
||||
|
||||
If a PR touches this code, confirm the fix still holds.
|
||||
|
||||
| Symptom | Cause | Where the fix is |
|
||||
|---|---|---|
|
||||
| lime silhouettes in grass; rectangles in the sky | a target outside the output map must keep its texel; zero-filling it wiped the half-res depth | `vk_pipeline.c` `target_wmask` |
|
||||
| stale grass maps in the far terrain for a second after a hit | write-back of a texture the CPU had rewritten | `vk_texture.c` `write_back` |
|
||||
| cut grass stays standing; fire does not spread (#132) | CPU reads of GPU-rendered textures without an invalidate | `vk_texture.c` `vk_texture_write_back_small`, `nvn_ext.c` `nvn_pool_invalidate` |
|
||||
| cloud and fog volumes empty for the session | a one-shot draw skipped instead of waiting for its pipeline | `vk_exec_draw.c` `prepare_draw` |
|
||||
| tile rectangles | storage buffers bound by Ryujinx first-use order, not the c0 slot | `vk_exec_draw.c` `ssbo_slot` |
|
||||
| black dialog background and loading map | 3D targets rejected, the grading LUT never written | `vk_exec.c` `resolve_target` |
|
||||
| streaks on cliffs | primitive restart ignored | `vk_exec_draw.c` `restart_for` |
|
||||
| streaky map screen | the newest texture picked at shared storage | `vk_texture.c` `vk_texture_by_id` |
|
||||
| dark 16x16 blocks on the ground, sage spirit not drawn | G-buffer feedback loop | `vk_exec_draw.c`, `TOTK_FEEDBACK` |
|
||||
| magenta cave walls, brown sky | CPU-filled textures without a flush | write-watched mirrors, `vk_memory.c`, `vk_texture.c` |
|
||||
| black 3D scene | deferred texture binds | `nvn_api_cmd.c` `Bind*Deferred` |
|
||||
| HUD prompt boxes, black stamina wheel | `nvnTextureCompare` ignored the channel mapping | `nvn_api_obj.c` |
|
||||
| crash on world load | `GetCommandMemoryUsed` did not follow the driver's sizes; the game sizes model heaps from it | `nvn_api_cmd.c` |
|
||||
| 20 fps | a yield back-off starting before 2 ms | `kernel.c` SVC 0x0B: no back-off |
|
||||
| weapons locked after a Fuse (#18) | timer remainder in (0, FLT_EPSILON] with the real-time step | patch under the `realtime` setting |
|
||||
| magenta/blue garbage or device lost on RDNA1/2 (#5) | writes into 3D BC block views | in `dev` |
|
||||
| black window after a driver update | a pipeline cache from another driver | `vk_pipeline.c` drops the cache by UUID |
|
||||
|
||||
## Known open risks
|
||||
|
||||
A PR touching these needs stronger proof.
|
||||
|
||||
- Buffer textures are not replayed and bind a stand-in: light and decal lists may read 0.
|
||||
- New GPU-only colour images start undefined; feedback effects may show garbage.
|
||||
- Clears with a partial channel or stencil mask are skipped (`W_CLEAR_MASK`, `W_STENCIL_MASK`).
|
||||
- 8-bit indices are widened on the CPU at record time; a copy in the same submission gives stale indices.
|
||||
- `UpdateUniformBuffer` is a copy between full barriers and ends the render pass (fps outdoors).
|
||||
- Report timestamps carry host time at fence completion: the engine's GPU time is wrong.
|
||||
- The game's SVC-free spin loops burn a host core (843 in `main`).
|
||||
- Images aliased onto one GPU-only pool allocation do not see each other's data on AMD.
|
||||
|
||||
## Past PR decisions
|
||||
|
||||
| PR | Decision | Why |
|
||||
|---|---|---|
|
||||
| #690 shrine snapshots | rejected | a pass restart per copy, a `VkPipeState` copy per draw, broken `bind_sig` skip; fps loss |
|
||||
| #674 | rejected | a global STXR counter on the hot path |
|
||||
| #660 | rejected | the preview loses anti-aliasing |
|
||||
| #713, #576 | rejected | low memory block 10 GiB for everyone; should be 6 GiB plus the extra where needed |
|
||||
| #573 | rejected | dynamic resolution off by default for everyone, the 1.4.3 site never run |
|
||||
| #568, #569 | rejected | fake FSR4 and frame generation |
|
||||
| #487 | rejected | wrong metrics: percentiles over window means, counters over the wrong set |
|
||||
| #451, #380 | rejected | not painless to take |
|
||||
| #566, #594, #534 DLSS | taken with changes | #594's shared paths gated on DLSS being on; nothing changes without DLSS |
|
||||
| #489 | partly taken | the runtime part checked by a frame dump; the installer's DLL download was the owner's call, its pinned hash re-checked |
|
||||
| #519, #582 amiibo | rewritten by us | took the data-format fixes; left out the features and the Linux event rework |
|
||||
| #483 vs #585 | #585's mapping kept | #483's stick glyph mapping was rewritten unasked while #585 already had it right |
|
||||
| #528 rain | taken after a check | code review fine, merged only after a run on a rain save |
|
||||
| #767 | taken with our fix | the default was skipped with `prompts = xbox|pc` when those folders did not exist |
|
||||
| #625 frame queue | taken, off by default | a new frame queue, one field run with it on |
|
||||
| #743 UB overlay | waiting | +25% in a Depths fight on an RTX 4080; needs review, fight runs x2 and shots on AMD |
|
||||
|
||||
## Our mistakes, not to repeat
|
||||
|
||||
- **"-11%" was an A/B artifact** of a moving scene. Whole runs off/on showed zero.
|
||||
- **Micro-optimizations from a flat profile** (1-3% of samples each) gave 26.2 against 26.4 ms on the weak laptop:
|
||||
nothing. Nobody had computed a ceiling.
|
||||
- **A scheduler patch merged on run statistics** added 50-75 ms hitches. Slow frames are a hard bar.
|
||||
- **Ten game runs on lock theories** where one Ghidra read of the game's code narrowed it at once.
|
||||
- **45 minutes of builds and benches** on an idea its own first thread census had refuted: the game pins 53 of 56
|
||||
threads itself. The cheapest refuting check goes first; when it refutes, stop and write the idea down.
|
||||
- **Tests of a bug that could not appear on the hardware at hand** (NVIDIA paging on an AMD machine): 245k tokens and
|
||||
37 minutes for nothing. Ask first where the bug reproduces.
|
||||
- **Turning the recorder off for a vendor** instead of finding its bug. Stopped: the feature gets fixed, not silenced.
|
||||
- **Bisecting code when every build crashed the same way at once.** The cause was a bad page of a game file in the OS
|
||||
file cache. When all builds fail identically, suspect inputs and caches first.
|
||||
- **A duplicate issue closed with a bare "Same as #N"** lost the logs and links. Move everything useful to the issue
|
||||
that stays open first: hardware, driver, version, when it happens, workarounds, links from comments.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Per-PR checklist
|
||||
|
||||
## Before reading
|
||||
|
||||
- [ ] Fix or feature? Fixes first, ordered by how many players they reach.
|
||||
- [ ] Where does the bug reproduce: OS, GPU vendor and generation, game version, settings? Can the hardware at hand
|
||||
show it?
|
||||
- [ ] Is the symptom in [04-knowledge.md](04-knowledge.md) or `docs/maxwell.md`?
|
||||
- [ ] Another open PR on the same topic? Compare them.
|
||||
- [ ] Feature: is there a design in `docs/milestones/`?
|
||||
|
||||
## Reading the diff
|
||||
|
||||
- [ ] The whole diff read, not the description: races, edge cases, lock order, leaks.
|
||||
- [ ] Behaviour checked against `refs/` (Ryujinx, yuzu, LAS, SavageWind, nxnative, MacNativeGames), the Vulkan spec,
|
||||
`docs/maxwell.md`.
|
||||
- [ ] Hot-path cost: no file I/O, GPU allocations, waits, per-draw copies or global atomics on game or render threads.
|
||||
- [ ] No memory kept per frame or per object without a free.
|
||||
- [ ] Failure modes: device lost, hang, stale data, CPUs without AVX, NVIDIA/Intel/old AMD, Wine/Proton.
|
||||
- [ ] New Vulkan code queries the feature and has a fallback; nothing hardcoded for one GPU.
|
||||
- [ ] Invariants kept: queue order, GPU-written memory never frozen, VRAM pressure path, pins freed.
|
||||
- [ ] Players without the feature see no change. The default for everyone stays for one setup's sake.
|
||||
- [ ] New behaviour behind a switch. A new setting described in `totk.ini.example` and `docs/settings.md`.
|
||||
- [ ] Player-visible text in the catalogs, in every menu language (`runtime/ui_text.c`, `installer/text/*.json`).
|
||||
- [ ] Copied code: allowed licence, header kept, row in `THIRD_PARTY_NOTICES.md`. No GPL code.
|
||||
- [ ] Generated C not edited by hand.
|
||||
- [ ] Parts we cannot run (Linux, HDR, RTX-only) gated away from everyone else and noted as unrun.
|
||||
|
||||
## Proof
|
||||
|
||||
- [ ] Repro before the fix and the same repro after, side by side.
|
||||
- [ ] Lifter or CPU headers: differential tests against Unicorn; `jumptables_check.py` 0 disagreements.
|
||||
- [ ] Runtime: `--selftest` and the four probes pass.
|
||||
- [ ] Settings: `--probe-cfg` and `tests\settings_doc_check.py`. Prompts: `romfs_prompts_test.py`, overlay generated
|
||||
for a game check.
|
||||
- [ ] Frame path: whole heavy-scene runs off/on, two each at least, by game time, quiet machine, warm cache prepared
|
||||
in that build dir. No fps loss, zero new slow frames.
|
||||
- [ ] Rendering: shots or a frame dump in a scene where the change acts; no black or flat frames; no new validation
|
||||
errors.
|
||||
- [ ] Perf PR: gain ceiling computed for weak hardware.
|
||||
|
||||
## Decision
|
||||
|
||||
- [ ] Taken, partly taken or rejected, with the reason written down and posted to the PR.
|
||||
- [ ] What missed the project's scheme brought to it while merging.
|
||||
- [ ] Bugs found outside the PR reported, not fixed on the spot.
|
||||
- [ ] Merged into `dev` from `task/<tag>`. `main` only with the owner's yes.
|
||||
- [ ] `python tools/check_content.py --staged` before the commit.
|
||||
- [ ] Commit message: a plain English sentence, no `feat:`/`fix:`.
|
||||
@@ -0,0 +1,32 @@
|
||||
# PR review kit
|
||||
|
||||
Rules and background for a tester or reviewer who takes community PRs into `dev`. One goal: no PR breaks the
|
||||
game's behaviour or costs frames on weak hardware.
|
||||
|
||||
## The short version
|
||||
|
||||
1. A PR description and "works for me" prove nothing. Read the diff yourself and check it against `refs/`, the
|
||||
Vulkan spec and upstream issues.
|
||||
2. Almost every change needs a test: a repro before and after, a probe, a differential test, or whole runs with the
|
||||
switch off and on.
|
||||
3. The developer's machine is fast; players run weak machines. "Neutral here" says nothing about them, in either
|
||||
direction.
|
||||
4. A player without the problem sees no change: the new path sits behind a switch and the old path stays as it was.
|
||||
5. Nothing goes into `main` without the project owner's explicit yes. Accepted work lives in `dev` first.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| [01-rules.md](01-rules.md) | triage order, what a PR needs, reasons to reject, merging and commits |
|
||||
| [02-performance.md](02-performance.md) | target hardware, gain ceiling, how to measure, what the hot path must not do |
|
||||
| [03-correctness.md](03-correctness.md) | lifter, tests and probes, GPU portability, Maxwell behaviour, timers, threads, game content |
|
||||
| [04-knowledge.md](04-knowledge.md) | invariants of recent changes, fixed bug classes, open risks, past PR decisions, our mistakes |
|
||||
| [05-checklist.md](05-checklist.md) | the per-PR checklist |
|
||||
|
||||
The public docs this kit builds on: [docs/architecture.md](../../docs/architecture.md),
|
||||
[docs/building.md](../../docs/building.md) ("Checks"), [docs/maxwell.md](../../docs/maxwell.md),
|
||||
[docs/settings.md](../../docs/settings.md), [docs/tester-guide.md](../../docs/tester-guide.md), `AGENTS.md`.
|
||||
Working with a coding agent: give it this folder and `AGENTS.md` before the first review.
|
||||
|
||||
State as of 2026-11-06. The code and `docs/` win over this text where they differ.
|
||||
@@ -0,0 +1,215 @@
|
||||
# Settings reference
|
||||
|
||||
`totk.ini.example` describes the settings for players. This page lists the rest: diagnostics, test hooks, paths and experiments. Set one as `name = value` in `totk.ini`, or as the environment variable `TOTK_NAME`, which wins over the file. Remove the line to go back to the default.
|
||||
|
||||
## Safety
|
||||
|
||||
- **Safe**: changes only what the row says, with no effect on stability or saves.
|
||||
- **Caution**: can slow the game, grow files, or change the picture, and undoes cleanly when removed.
|
||||
- **Risky**: can crash or hang the game, or break the picture or game logic, while it is set.
|
||||
- **Save risk**: can damage or lose saves, or point the game at other game or save files.
|
||||
- **Unknown**: the code did not settle it.
|
||||
|
||||
Many rows below are developer tools that only act together with another setting or a command-line mode (for example a trace mode or the DLSS path). The row says so where the code does. The rows come from reading the code, and most effects have not been checked in a run.
|
||||
|
||||
`tests/settings_doc_check.py` fails when a name in `known[]` (`runtime/cfg.c`) is described neither here nor in `totk.ini.example`, or when a default here differs from the code.
|
||||
|
||||
## Diagnostics and logging
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `astc_time` | `1` times the GPU dispatches of each ASTC texture decode and logs the time per block footprint and the largest call. | Adds GPU timestamp queries around every decode; if the queue has no timestamps or no `hostQueryReset`, the log says the decodes are not timed and nothing is measured. | Caution |
|
||||
| `bind_stats` | `1` counts bindings and descriptors per draw of each stage and how much of the bound state changes between draws. | Adds per-draw bookkeeping to the render thread and extra log output; frames get slower. | Caution |
|
||||
| `contact_debug` | `1` draws the contact-shadow factor in grey in place of the picture. | The frame shows a grey debug image while it is set; has no effect when contact shadows are off. | Caution |
|
||||
| `core_diag` | `1` records why ready guest threads wait for a virtual core and for how long, with a histogram every 60 presents. | Turns on host-call and SVC depth tracking and writes many log lines (one per present over 25 ms); frames get slower. | Caution |
|
||||
| `menu_cursor_log` | `1` logs what the menu cursor code reads at every screen change. | Only adds log lines. | Safe |
|
||||
| `perf_slow_ms` (default `50`) | Frames at least this many milliseconds long get a `perf: slow frame` log line with the time blocked per cause. | Changes how many frames are logged; a small value fills the log. | Safe |
|
||||
| `replay_check` | `1` reads every texture read-back a second time after a full drain of the replay queue and compares the two. | Stalls the replay queue at every read-back, so frames get much slower; mismatches are only counted and logged. | Caution |
|
||||
| `resolve_check` | `1` compares every cached pool lookup with the locked lookup and stops on a difference. | Slows every pool lookup; the game stops if the two ever differ. | Caution |
|
||||
| `speed_probe` | `1` adds the player's position and ground speed to the frame-rate line that `TOTK_TRACE=perf` prints every 120 presents. | Only adds log text; has no effect without `TOTK_TRACE=perf`. | Safe |
|
||||
|
||||
## Profiling and measurement
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `ab` | Splits game time into blocks of this many seconds in the order A B B A and lets settings written as `ab` take effect only in the B blocks. | Settings that accept `ab` flip on and off every few seconds, so frame times and the picture change with the block; without such a setting it does nothing. | Caution |
|
||||
| `crit` | `<first>-<last>` frame range (with `TOTK_TRACE=sched`) that records every state change and wake of the guest threads, then writes a file at the last frame. | Recording adds overhead to every thread switch in the range and writes one file; `tools/crit_path.py` reads it. | Caution |
|
||||
| `crit_out` (default `crit.bin`) | Path of the file that `crit` writes. | Only moves the file; nothing is written without `crit`. | Safe |
|
||||
| `profile` | Path of a sampling profile file; the profiler suspends threads to unwind their stacks. | Frames get slower while it samples and the profile file grows; `tools/prof_report.py` reads it. | Caution |
|
||||
| `profile_chains` | `1` keeps call chains of every thread in the profile. | Larger profile file and more work per sample; has no effect without `profile`. | Caution |
|
||||
| `profile_frames` | `<lo>-<hi>` samples only while presents lo to hi-1 are pending; `w<lo>-<hi>` uses world-clock seconds. | Limits the profile to a window; has no effect without `profile`. | Safe |
|
||||
| `profile_main` | `1` samples only the guest main thread. | Narrows the profile; has no effect without `profile`. | Safe |
|
||||
| `profile_render` | `1` samples only the render thread and keeps its call chains. | Narrows the profile and makes it larger; has no effect without `profile`. | Caution |
|
||||
|
||||
## Capture and frame dumps
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `dump` | Frame number to dump: every render pass target and storage image of that frame is copied out as PNG, with `passes.txt` and `images.txt`. | Writes many PNG files and shader hashes to disk and slows the dumped frame; size is capped by the dump code. | Caution |
|
||||
| `dump_buf` | `<addr>:<size>[:<off>:<stride>:<w>:<h>]` snapshots one GPU buffer range after every logged pass of a dump. | Writes `buf_<pass>.bin` files and `bufs.txt`; has no effect without `dump`. | Caution |
|
||||
| `dump_dir` (default `dump`) | Folder that receives the frame dump files. | Only moves the output. | Safe |
|
||||
| `dump_log` | Frame number; logs the frames from there up to the dumped frame into `passes.txt`, with PNGs only of targets up to 64K texels. | Writes a longer `passes.txt` and more PNGs; has no effect without `dump`. | Caution |
|
||||
| `flight_at` | Comma-separated frame numbers that trigger a flight-recorder dump. | Writes a dump folder when each frame is reached; has no effect unless the flight recorder (`TOTK_FLIGHT=1`) is on. | Caution |
|
||||
| `flight_dir` | Folder for flight-recorder dumps; the default is `flight` beside the folder of `totk.exe`. | Only moves the output. | Safe |
|
||||
| `flight_frames` (default `600`) | Number of presents the flight recorder keeps. | A larger ring keeps more history and more thumbnails in memory; has no effect unless `TOTK_FLIGHT=1`. | Caution |
|
||||
| `flight_thumbs` (default `1`) | `0` turns off the 320x180 thumbnail the flight recorder saves for each present. | Dumps hold only `frames.tsv`; thumbnails cost a GPU blit per present when on. | Safe |
|
||||
| `rdoc` | Comma-separated frame numbers to capture with RenderDoc, which is loaded before Vulkan starts. | The game runs under RenderDoc, with its capture options set to keep `VK_EXT_external_memory_host` available and to reference all resources, and captures are written to disk. | Caution |
|
||||
| `rdoc_dir` | Output folder for RenderDoc captures (the default is `build/captures`). | Only moves the output. | Safe |
|
||||
| `rdoc_dll` | Path of the RenderDoc library, for when it is not found by default. | A wrong path logs that the library cannot be loaded and no capture is taken. | Safe |
|
||||
| `shots` | `<lo>-<hi>:<step>` saves every step-th present in the range as `f<n>.png`, scaled to 480x270; `w<lo>-<hi>:<step>` takes them by world-clock seconds. | Writes one PNG per step and adds a readback of the window texture; the pictures clip highlights in HDR. | Caution |
|
||||
| `shots_dir` (default `.`) | Folder for the `shots` pictures; it is created when needed. | Only moves the output. | Safe |
|
||||
| `shots_full` | Set to any value to save `shots` at full size instead of 480x270. | Files get far larger and writing them takes longer; has no effect without `shots`. | Caution |
|
||||
|
||||
## Automated tests and test hooks
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `vram_limit_mib` | Pretends the device-local memory budget is at most this many MiB, for the paths that react to the budget (`tex_vram_pressure`, mirror refusal); the driver knows nothing of it. | Lets a large card drive those paths; a low value drops released textures and refuses mirrors that would have fit. | Caution |
|
||||
| `background_window` | `1` keeps the window behind the user's windows, never fullscreen at start, and F11 keeps its place; SDL windows ignore it. | The window does not come to the front or flash, so it is easy to miss; meant for unattended runs. | Caution |
|
||||
| `gpu` | `vulkan` or `null`: the graphics back end; `null` has no GPU, completes every submit at once and returns zeros for texture reads. | `null` shows no picture; any other value stops the start with an error. | Caution |
|
||||
| `input` | `none` reports no input from the keyboard, mouse or controllers. | The game gets no controller input and the menu cursor stops following the mouse; `input_script` still applies. | Caution |
|
||||
| `input_script` | Path of a file with lines `<frame> <button> [hold]` or `<frame> LSTICK or RSTICK <x> <y> [hold]` that are ORed into the real input. | The game presses those buttons by itself; a file that cannot be opened is logged and ignored. | Caution |
|
||||
| `menu_test` | `<frame>[:<frames per page>]` opens the menu at that frame, shows each tab for 30 frames (or the given count), saves one screenshot per tab and closes the menu. | The menu opens by itself and screenshots are written. | Caution |
|
||||
| `menu_test_dir` | Folder for the `menu_test` screenshots; the default is the working directory. | Only moves the output. | Safe |
|
||||
| `menu_test_set` | `<frame>:<key>=<value>[,...][;...]` changes settings live at that frame, with or without the menu open; `SAVE=1` also writes `totk.ini`. | Settings change during play without input, and `SAVE=1` rewrites the file. | Caution |
|
||||
| `probe_movie` | Text that picks the movie for `totk.exe --probe-movie`: the first file whose path contains it. | Only used by the movie decoder probe; ignored in normal play. | Safe |
|
||||
| `probe_movie_shot` | Path of a `.ppm` file that the movie probe writes the middle frame to. | Only used by the movie decoder probe. | Safe |
|
||||
| `probe_movie_bench` | `1` makes the movie probe also time the resampler that fits a movie to a game resolution of another shape, and print the time per frame. | Only used by the movie decoder probe. | Safe |
|
||||
| `swkbd_answer` | `<text>`, `<cancel>` or `<initial>`: answers the on-screen keyboard (for example a horse name) without opening a window. | The game gets that answer every time it asks for text. | Caution |
|
||||
| `swkbd_test` | `ok` or `cancel` opens the keyboard window and presses OK or Cancel on the first timer tick. | The keyboard window closes by itself. | Caution |
|
||||
| `test_oom` | `<site>:<n>[,...]` fails the first n allocations at a site: `commit` (guest memory), `staging` (upload buffers), `image` (texture images), `chunk` (draw data buffers). | Each forced failure takes the out-of-memory path: a memory release, a wait of 10 ms per retry and log lines; a large n stalls up to 30 s and then ends the game. | Caution |
|
||||
| `test_pad` | `<lx>,<ly>,<rx>,<ry>` attaches a virtual gamepad whose sticks rest at these raw SDL axis values (-32768 to 32767). | The game sees a controller with that drift or deflection; a malformed value is logged and ignored. | Caution |
|
||||
| `test_prepare_shaders` | `1` translates every recorded shader on all usable CPUs during start. | Start takes longer and fills the shader cache folder; the CPU is fully used while it runs. | Caution |
|
||||
|
||||
## Input
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `bind_<button>` | Remaps one Switch button or stick direction to `pad:<name>` or `key:<name>` entries; covers `bind_a`, `bind_b`, `bind_x`, `bind_y`, `bind_l`, `bind_r`, `bind_zl`, `bind_zr`, `bind_plus`, `bind_minus`, `bind_left`, `bind_up`, `bind_right`, `bind_down`, `bind_lclick`, `bind_rclick`, `bind_lstick_left`, `bind_lstick_up`, `bind_lstick_right`, `bind_lstick_down`, `bind_rstick_left`, `bind_rstick_up`, `bind_rstick_right`, `bind_rstick_down` and `bind_shake`. | A line replaces every default of that button; button prompts in the game still show the default layout. The Controls tab of the menu writes these lines; `docs/controls.md` lists the entry names. | Safe |
|
||||
|
||||
## Paths and files
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `amiibo_dir` | Folder the Amiibo tab starts in; the tab saves the folder you last browsed here. | Only changes which folder the tab lists first. | Safe |
|
||||
| `exefs` | Folder of the game's ExeFS (with `main.npdm`); a relative path is relative to `totk.exe`'s folder. | Without it, an `exefs` folder next to `totk.exe` is used, else the path set at build time. The game code is compiled into `totk.exe`, and the loader checks the files here by SHA-256, so files from another build or game version stop the start with a message. | Caution |
|
||||
| `mods_dir` (default `mods`) | Folder that holds one subfolder per player mod, beside `totk.exe`. | Mods found there replace game files; see `docs/mods.md`. | Caution |
|
||||
| `mods_disabled` | Names of mods that are switched off, separated by `\|`. | Listed mods are not applied; the Mods tab writes this line. | Safe |
|
||||
| `mods_order` | Mod names, top first, separated by `\|`; the top mod wins a file that two mods replace. | Changes which mod's file the game gets; unlisted mods go below the listed ones, by name. | Safe |
|
||||
| `pipeline_cache` | Path of the Vulkan pipeline cache file (default `pipeline-cache.bin` beside `totk.exe`). | Moves the cache; a new path starts empty and pipelines are built again. | Caution |
|
||||
| `profile_dir` | Folder of the game profile, which holds the user account id that save data is named after; the default is a `profile` folder beside `totk.exe`, else the build-time path. | Another folder means a different set of saves. | Save risk |
|
||||
| `romfs` | Folder of the game's RomFS (with `romfs.bin`); a relative path is relative to `totk.exe`'s folder. | Without it, a `romfs` folder next to `totk.exe` is used, else the build-time path; a missing image stops the start with a message. | Save risk |
|
||||
| `romfs_index` | Path of `romfs-index.bin`, which `tools/romfs_index.py` wrote; the default is beside `totk.exe`. | The index is checked against the image by SHA-256 and size; a mismatch stops the start. | Caution |
|
||||
| `romfs_mmap` | `1` maps the RomFS image into memory instead of reading it with positional reads. | Changes how the image is read; the log reports when the mapped path is on. | Caution |
|
||||
| `romfs_mods` | Folder of an overlay with a `manifest.tsv` of sizes and SHA-256 hashes; its files are served in place of the originals. | A missing file, size or hash mismatch, or a changed original stops the start; player mods win over this overlay. | Save risk |
|
||||
| `romfs_patches` | Folder of an overlay with a `manifest.tsv`, served in place of the original files; the default is `romfs-patches` beside the index. | Same checks as `romfs_mods`; while it is set the Xbox and PC button prompts are off, and the menu says so. | Save risk |
|
||||
| `romfs_proof_dir` | Folder for the `.romfs-verified-v1` proof file of a full verification; the default is beside the index. | Only moves one small file. | Safe |
|
||||
| `romfs_verify` | `full` hashes the whole RomFS image at start; `force` hashes it again even when a proof file exists. | Start takes many minutes for a 17 GB image; a proof file is written so later starts skip the hash. | Caution |
|
||||
| `shader_cache` | Folder of the translated shader cache (default `shader-cache` beside `totk.exe`). | A new folder starts empty, so shaders are translated again during play. | Caution |
|
||||
| `shared_fonts` | Folder of the extracted system fonts; the default is `shared-fonts` beside `totk.exe`, else beside the RomFS. | Simplified Chinese, Traditional Chinese and Korean need these fonts: without them the game exits before its first frame in those languages. Other languages do not use them. | Caution |
|
||||
|
||||
## Shader translation
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `xlate` | Path of the shader translator program; the default is `totk_xlate` beside `totk.exe`, else under `build/xlate`. | Shaders are translated by that program; if it is missing the log says so and graphics shaders cannot be translated. | Risky |
|
||||
| `xlate_id` | Text or 64 hex digits that identify the translator build in shader cache keys; the default comes from an `.id` file beside the translator, else a hash of the program. | A value that two different builds share lets the cache serve shaders translated by the other build. | Caution |
|
||||
| `xlate_keep` (default `0`) | `1` keeps the translation job of a last-frame variant as `<key>.job` with its code. | Writes extra files to the shader cache folder; meant for running the translator by hand. | Caution |
|
||||
| `xlate_procs` (default `2`) | Number of translator processes allowed during play. | More processes translate new shaders sooner and use more CPU and memory; values below 1 count as 1. | Caution |
|
||||
|
||||
## Graphics experiments
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `block_views_3d` (default `0`) | `1` gives 3D block-compressed images block views too. | On some AMD cards a GPU write past the end of the image's memory loses the Vulkan device, and the game stops. | Risky |
|
||||
| `deswizzle` (default `auto`) | Where block-linear textures are deswizzled: `auto` or `gpu` uses a compute pass where the texture's storage is an imported pool and the CPU elsewhere, `cpu` always uses the CPU, `check` uses the GPU and also deswizzles every upload on the CPU and compares the two. | `cpu` brings back the long frames on entering new areas (the replay thread deswizzles every texture); `check` waits for the GPU at every upload and is for testing only. The log names the mode in use. | Caution |
|
||||
| `dyn_delta` (default `1`) | Records only the pipeline dynamic states (cull mode, depth and stencil tests, vertex input, blend enables, equations and write masks, ...) whose values changed since the last draw in the command buffer, also across pipeline changes; `0` records the full set at every change of pipeline state, as before; `ab` alternates with the `ab` blocks. | Fewer commands per pipeline change on the render and recorder threads; a driver that loses dynamic state across pipeline binds would draw with stale state. `TOTK_TRACE=perf` prints the calls and commands per submission. | Caution |
|
||||
| `feedback` | `0` samples a target that an earlier draw of the same pass wrote with no barrier; `restart` ends and restarts the pass behind a full barrier; otherwise the feedback-loop layout is used when the GPU supports it. | `0` brings back the old behaviour that the barrier fixed, so the picture can break in passes that read a target they draw into; `restart` costs extra passes. | Risky |
|
||||
| `flush_span` (default `0`) | `1` copies one span from the lowest to the highest flushed address of a pool, as an older version did. | Copies far more data to the GPU in crowded towns: an estimated 30-50 ms of GPU time per large copy over PCIe 3.0 x8. | Caution |
|
||||
| `shadow_snap` (default `1`) | Shadow storage (Linux, Wine): `1` syncs the pools from per-submission write-watch snapshots, each 2 MiB region queried at most once per submission and shared with the texture checks; `0` queries each pool at every part of a submission, as before. | `1` cut the replay thread's write tracking from about 4.4 to 1.3 ms a frame on a Steam Deck; `0` is for comparison runs. Under Wine/Proton the default is `0` until it has been measured there; `1` turns it on. No effect where shadow storage is off (Windows by default). | Caution |
|
||||
| `freeze_idle` (default `60`) | Number of submissions a 2 MiB region stays unwritten before it is made read-only; `0` turns the freeze off. | A larger value keeps more regions writable and costs more write tracking; a write to a frozen region costs about 60 microseconds. | Caution |
|
||||
| `gpl_draw_build` (default `1`) | `0` leaves the missing shader libraries of a waiting draw to its background job instead of building them in parallel at once. | Draws whose pipeline is not ready are skipped meanwhile, until the job finishes. | Caution |
|
||||
| `lib_prebuild_keep` (default `0`) | Number of recorded graphics shader library variants that stay in memory after the boot prebuild. | Each kept library costs 140-460 KB of memory; the driver cache already makes a rebuild cheap. | Caution |
|
||||
| `minimal_layout` (default `1`) | `0` skips the compare against one-block tiles when a texture's memory layout is chosen. | The code calls it a negative control for fresh sessions; the comment does not say what the picture does. | Unknown |
|
||||
| `mirror_gap` (default `2`) | Hot pools at most this many 2 MiB regions apart share one write-watch query. | Changes the number of queries per submission; negative values count as 0. | Caution |
|
||||
| `movie_host` (default `1`) | `0` leaves every movie decoder to the game's own library. | The code comment says that library aborts at the first movie, so the game stops at the first cutscene. | Risky |
|
||||
| `movie_pace` (default `1`) | `0` hands every decoded movie frame over at the next present instead of when it is due. | Movies play at the game's frame rate rather than their own, so their speed no longer matches the sound. | Caution |
|
||||
| `pcss_k` (default `0.5`) | Depth-to-penumbra factor of soft shadows at 100 percent, for tuning. | Changes how wide soft shadows blur; has no effect while `pcss` is `0`. | Caution |
|
||||
| `pipe_sync` | Any value other than `0` or empty stops graphics pipelines from being built in the background. | Pipelines are built on the render path, which causes stutter when new effects appear. | Caution |
|
||||
| `push_stage` | `fs` pushes the descriptor set from the fragment stage instead of the vertex stage. | The log names the stage in use; the code does not say what changes in the picture. | Unknown |
|
||||
| `rec_async` (default `1`) | `0` makes the submitting thread wait until the recorder thread has recorded and submitted each part; `ab` alternates with the `ab` blocks. | With `0` the submitting thread waits for every part (about 0.3 ms a frame in a town on a Ryzen 7900X). | Caution |
|
||||
| `rec_end` (default `1`) | `0` lets the submitting thread begin, end and submit a recorded part again, as older versions did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
|
||||
| `rec_spin_us` (default `60`) | Microseconds the recorder thread polls for the next packets before it sleeps; `0` polls 4096 pauses instead; `ab` polls only in the B blocks of `ab`. With fewer than 6 logical processors it always polls 4096 pauses. | A longer poll saves an OS wake-up per packet and takes more CPU time; a shorter one sleeps sooner. | Caution |
|
||||
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which released textures are dropped, 0 to 100; `0` is off. First those the game has overwritten with a later texture and left alone for 30 s, then those that can be uploaded again. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets the card spill into system memory. Also a slider in the menu. | Caution |
|
||||
| `txd_transpose` (default `1`) | `0` turns off the swap of gradient components for gradient texture samples. | Terrain textures turned across the screen show crawling patches again. | Caution |
|
||||
| `ub_overlay` (default `1`) | `0` copies each uniform update inside a pass into its buffer at once, ending the pass with a full barrier; `ab` alternates with the `ab` blocks. | With `0` scenes with many updated blocks (the terrain, the sky islands) take longer on the GPU; `TOTK_TRACE=perf` prints how many updates stayed in their pass. | Caution |
|
||||
| `ub_overlay_out` (default `1`) | `0` copies each uniform update between passes into its buffer at once, behind a full barrier of its own; `ab` alternates with the `ab` blocks. Needs `ub_overlay`. | With `0` a run of updates before a pass costs a full GPU barrier each; `TOTK_TRACE=perf` prints how many updates between passes were overlaid. | Caution |
|
||||
| `ubo_snapshot` (default `1`) | `0` binds live memory for uniform blocks; `ab` alternates with the `ab` blocks. | Without snapshots late draws read another object's matrix, so objects can be drawn in the wrong place. | Risky |
|
||||
| `vk_no_float64` (default `0`) | `1` clears the 64-bit float feature, so the translator lowers FP64 to FP32. | Shaders change, so the shader cache entries for it are made again; precision of FP64 shaders drops. | Caution |
|
||||
| `vk_ssbo_align` (default unset) | Power of two that storage buffer offsets are treated as needing, to test the unaligned path. | The translator and the draw code round storage offsets down to it; shaders are translated again. | Caution |
|
||||
| `vk_subgroup` (default `32`) | Subgroup size to pin where the driver allows it. | A value other than 32 changes what the translator is told about the GPU, and shaders are translated again. | Caution |
|
||||
| `vram_headroom` (default `1`) | `0` turns off the spare half-block of video memory that a background thread keeps per memory type. | Texture allocations on the render thread can wait for a new block again. | Caution |
|
||||
| `vs_outs` | `all` declares all 32 vertex shader output locations; otherwise only the union of the paired fragment inputs. | Larger vertex shaders and new pipelines; unwritten outputs read the same as before. | Caution |
|
||||
| `wb_exact` (default `1`) | `0` decides write-back of GPU-written textures by page only. | A target that shares a page with data the CPU writes can go unwritten, so a name label next to the reticle can blink. | Caution |
|
||||
| `wb_merge` (default `1`) | `0` copies GPU-written ranges back as plain copies instead of writing word by word the words that changed. | In mirror memory mode the GPU's copy can overwrite words the CPU wrote, which breaks effects such as falling rain and waterfall splashes. | Caution |
|
||||
|
||||
## Motion vectors and DLSS
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `dlaa_jitter` (default `1`) | `0` turns off the camera jitter fed to DLSS. | DLSS gets no jitter between frames; a test setting. | Caution |
|
||||
| `dlaa_maskdebug` (default `0`) | `1` shows the reactive mask in place of the picture. | The frame shows a debug image while it is set. | Caution |
|
||||
| `dlaa_mv` (default `1`) | `0` sends zero motion vectors to DLSS. | DLSS sees no motion, so moving content can smear; a test setting. | Caution |
|
||||
| `dlaa_mvdebug` (default `0`) | `1` shows the motion vectors in place of the picture, x in red and y in green. | The frame shows a debug image while it is set. | Caution |
|
||||
| `dlaa_survey` (default `0`) | `1` logs what is drawn into the light target that DLSS reads. | Adds log output and per-draw work. | Caution |
|
||||
| `dlaa_times` (default `0`) | `1` times the DLSS passes on the GPU. | Adds GPU timestamps and log lines. | Caution |
|
||||
| `dlss_reactive` (default `0`) | `1` builds a reactive mask for DLSS where it ran. | Changes how DLSS blends moving areas; a test setting. | Caution |
|
||||
| `mv_check` | Frame interval; every that many DLSS frames copies the light target and motion images to host memory and logs how well the vectors explain the picture. | Costs a GPU-to-host copy and a comparison on the completion thread at each interval. | Caution |
|
||||
| `mv_survey` (default `0`) | `1` notes the uniform blocks of each scene draw and compares them with those of earlier frames. | Adds per-draw work and log output. | Caution |
|
||||
| `mvobj_all` (default `0`) | `1` also lists draws for object motion that read no object blocks (terrain and grass). | More draws are checked and listed, which costs time. | Caution |
|
||||
| `mvobj_banks` (default `0`) | `1` logs which words of the other uniform banks changed for each scene draw. | Adds log output and per-draw work. | Caution |
|
||||
| `mvobj_equal` (default `0`) | `1` uses an EQUAL depth compare for the object motion pass, for comparison. | Changes which pixels get object vectors; the code comment names the default as the one that hides objects behind nearer geometry. | Caution |
|
||||
| `mvobj_exact` | Hex bank mask; only draws whose position uses exactly these banks get object motion. | Fewer draws get object vectors, so their motion falls back to the camera's. | Caution |
|
||||
| `mvobj_exec` (default `1`) | `0` lists object motion draws but never records them. | Objects get camera motion only. | Caution |
|
||||
| `mvobj_noc4` (default `0`) | `1` draws again positions that have no scene camera bank. | Those draws get this frame's view, so they can show wrong motion. | Caution |
|
||||
| `mvobj_only` | Hex bank mask; only draws whose position reads no bank outside the mask get object motion. | Fewer draws get object vectors. | Caution |
|
||||
| `mvobj_times` (default `0`) | `1` adds timings to the object-motion summary. | The clock reads themselves cost frame time. | Caution |
|
||||
| `mvprev_check` (default `0`) | `1` verifies the stored first bytes of each previous-frame table entry. | The code comment measures 3.6 ms per frame. | Caution |
|
||||
| `mvprev_grow` (default `1`) | `0` keeps only three banks in the previous-frame tables. | Object motion that depends on the other banks is no longer tracked. | Caution |
|
||||
| `mvprev_skip` | Hex bank mask of banks left out of the previous-frame copy, replacing the default. | Different banks are tracked, which changes object motion. | Caution |
|
||||
| `mvprev_trust` | Hex bank mask of banks whose found previous-frame copy is taken without the check. | A copy that does not match is used anyway, which can give wrong motion. | Caution |
|
||||
|
||||
## Scheduling and host
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `app_pool` | Size in bytes of the guest application memory pool; accepts decimal or `0x` hex. | The default depends on the resolution settings; `0` stops the start with a message, and too small a pool leaves the game without memory. | Risky |
|
||||
| `audio_off` | Any value mixes audio without opening an output device. | The game runs in silence and reports two channels; same as `audio = none`. | Caution |
|
||||
| `kspin_us` (default `20`) | Microseconds a guest wait or a wait for a virtual core polls before it sleeps; `0` turns the poll off; `ab` polls only in the B blocks of `ab`. Off with fewer than 6 logical processors. | A wake within the poll costs no OS wake-up; a longer poll takes more CPU time from other threads. | Caution |
|
||||
| `mem_reserve_mib` (default `1536`) | MiB of system memory kept free for Windows, other programs and the game's own heap when the renderer sizes what it keeps in device memory, where device memory takes system memory (integrated GPUs, and discrete GPUs whose driver charges it to the process). | A larger value keeps fewer released textures and spare blocks when the system runs short; a smaller one lets the game use more before it holds back. | Caution |
|
||||
| `one_per_core` (default `0`) | `1` keeps the process on the first logical processor of each physical core; `ab` switches it with the `ab` blocks. | Busy threads stop sharing a core with a sibling thread, which can help or hurt on the CPU; it only applies when the CPU has hyperthreads. | Caution |
|
||||
| `pcore` (default `1`) | On a hybrid CPU (performance and efficiency cores), the game's main and render threads and the replay, recorder and present threads prefer the performance cores; `0` turns that off. | With `0` Windows may run those threads on efficiency cores; no effect on a CPU with one kind of core. The log says which cores were found. | Caution |
|
||||
| `reexec` | Counter of address-space retries that the runtime sets itself when it starts the process again. | The counter is cleared when the window is recreated; a value of 20 or more stops the start with a fatal error. | Risky |
|
||||
| `replay_chunks` (default `0`) | Most chunks a submission is cut into when `replay_workers` is set; `0` uses twice the number of recording threads. | Fewer chunks leave threads idle at the end of a submission, more add scan and submit work per chunk; no effect while `replay_workers` is `0`. | Caution |
|
||||
| `replay_chunk_draws` (default `128`) | Least draws a chunk of the chunked replay holds before it may end at a pass. | Small values make many short chunks, large ones leave big submissions on few threads; no effect while `replay_workers` is `0`. | Caution |
|
||||
| `replay_state_check` (default `0`) | `1` compares the replay state after the last chunk of a chunked submission with the state of the scan and logs differences. | Adds a comparison per submission and log lines on a mismatch; no effect while `replay_workers` is `0`. | Caution |
|
||||
| `replay_skew_us` (default `0`) | Debug: the even chunks of a chunked submission wait this many microseconds before they record, so the chunk after each one records first. | Shows a recording that depends on which thread gets to a chunk first in every submission; slows the replay; no effect with fewer than 2 `replay_workers`. | Caution |
|
||||
| `restart_wait` | Set by the runtime when it restarts itself; names the old process by id and creation time so the new process waits for it to exit. | Meant for the runtime's own use; the code does not describe a manual value. | Unknown |
|
||||
| `spin` (default `1`) | `0` makes a guest thread that polls memory keep its virtual core. | Polling threads can starve the thread whose item they wait for, so frames can stall. | Caution |
|
||||
| `spin_handoff_us` (default `50`) | Microseconds a polling wait lasts before it gives its virtual core to a waiting thread. | A shorter value hands cores over sooner; a longer one keeps them longer. | Caution |
|
||||
| `tex_block_mib` (default `64`) | Block size in MiB of the pool that holds the game's textures; images of a quarter block or more get memory of their own. `0` puts textures in the default pools with the buffers. | Larger blocks hold more memory that no texture uses; smaller ones mean more allocations from the driver. | Caution |
|
||||
| `tex_released_mib` (default `256`) | MiB of textures the game released that are kept for reuse; older ones beyond it are dropped. `0` keeps up to 2048 of them, whatever their size. | A larger value keeps more device memory and process commit; a smaller one makes the game create more textures again. | Caution |
|
||||
| `vma_block_mib` (default `64`) | Block size in MiB that the Vulkan memory allocator takes from the driver for buffers and other images on heaps over 1 GiB; larger allocations than half a block get memory of their own. `0` uses the allocator's 256. | Larger blocks hold more memory that nothing uses; smaller ones mean more allocations from the driver. | Caution |
|
||||
|
||||
## Generated-code patches
|
||||
|
||||
These names are code patch sets built into the generated game code (`recomp/patches-*.txt`). `rt_patches_init` reads the environment name at start, then the resolution and mouse code switches some sets itself.
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `aspect_patch` (default `0`) | Patch set that feeds the camera the output's aspect ratio when it is not 16:9. | The resolution code switches this set on or off itself at start, so a value here is replaced; the code does not settle what a manual value does. | Unknown |
|
||||
| `fixed_res_patch` (default `0`) | Patch set that holds the game's dynamic resolution at 100 percent (the scene tier). | The resolution code switches this set from `dynamic_res` (on at `0`); a manual value is replaced. | Unknown |
|
||||
| `fov_patch` (default `0`) | Patch set that scales the camera's vertical view angle by the field-of-view setting. | The resolution code switches this set itself whenever the field of view is not 100 percent; a manual value is replaced. | Unknown |
|
||||
| `mouse_direct_patch` (default `1`) | Patch set of the hooks in the game's camera code behind `mouse_mode = direct` on game 1.4.3. | `0` stops `mouse_mode = direct` from taking effect; other mouse modes are unaffected. | Safe |
|
||||
| `patch_output` (default `0`) | Patch set that makes the game use the configured output size for its swapchain and display configuration. | The resolution code switches this set whenever the output differs from the base size; a manual value is replaced. | Unknown |
|
||||
| `patch_scene` (default `0`) | Patch set that makes the game use the configured scene render size. | The resolution code switches this set from the render size setting; a manual value is replaced. | Unknown |
|
||||
+14
-2
@@ -6,9 +6,10 @@ from it may be shared.
|
||||
|
||||
## What you need
|
||||
|
||||
- Windows 10 or later, an x86-64 CPU with AVX2, about 60 GB free.
|
||||
- Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA recommended: they build the default, faster version), about
|
||||
60 GB free.
|
||||
- A GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, with a current driver.
|
||||
- *Tears of the Kingdom* base game (1.0.0) and the 1.2.1 or 1.4.3 update, dumped from your own Switch as NSP or XCI.
|
||||
- *Tears of the Kingdom* base game (1.0.0) and the 1.1.0, 1.2.1 or 1.4.3 update, dumped from your own Switch as NSP or XCI.
|
||||
- `prod.keys` dumped from your own Switch. Never send it to anyone, and never paste it into a log or an issue.
|
||||
|
||||
## Build
|
||||
@@ -38,6 +39,10 @@ build\<ver>\totk.exe
|
||||
`TOTK_VK_VALIDATION=1` turns on the Vulkan validation layer (it comes with the Vulkan SDK). It makes the game slower;
|
||||
do one run with it and one without (`set TOTK_VK_VALIDATION=`).
|
||||
|
||||
If the game stops with a lost device (the log says `device lost` or `VK_ERROR_DEVICE_LOST`), the lines starting with
|
||||
`vk: pass marks` name the render pass or dispatch the GPU was running. AMD GPUs write them by default. On an NVIDIA GPU,
|
||||
`set TOTK_PASS_CRUMBS=1` (or GPU pass marks: On in the Advanced tab) before the run that reproduces the crash.
|
||||
|
||||
The game keeps running behind other windows (`assume_focus = 0` in `totk.ini` pauses it instead). Shaders the preparation did not cover compile on first use, so
|
||||
the first minutes of play can stutter.
|
||||
Play into the open field (load a save, walk around for a minute, fight something if you can), then close the window.
|
||||
@@ -51,6 +56,13 @@ A laptop with an iGPU and a discrete GPU, or two cards, lists every GPU at the t
|
||||
|
||||
The line `vk: using gpu N: <name>` says which one ran.
|
||||
|
||||
## Mods
|
||||
|
||||
Mods and cheats made for the emulators go into `mods/` next to `totk.exe`, one folder per mod. [mods.md](mods.md) covers
|
||||
installing them, their order, code patches, what does not work and what to check when something looks wrong;
|
||||
[cheats.md](cheats.md) covers cheat files. The log names every mod at the start (`mods: <name>: ...`); when you
|
||||
report a problem with mods, list them with their download links.
|
||||
|
||||
## What to send back
|
||||
|
||||
1. `logs\totk.log` from each run. It contains no keys and no game files. The lines that matter most start with
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""The TotK installer: builds a playable folder from the player's own game files (docs/installer.md).
|
||||
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR] [--temp DIR]
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR] [--root DIR]
|
||||
[--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 six 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.
|
||||
|
||||
+19
-9
@@ -1,10 +1,15 @@
|
||||
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR]
|
||||
[--temp DIR] [--verify]
|
||||
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR]
|
||||
[--root DIR] [--temp DIR] [--verify] [--dlss]
|
||||
|
||||
A first install needs every option. On a folder with totk-install.json (resume after a cancel or failure, or update
|
||||
with a newer 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).
|
||||
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
|
||||
Traditional Chinese. On a folder with totk-install.json (resume after a cancel or failure, or update with a newer
|
||||
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
|
||||
@@ -17,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}
|
||||
|
||||
@@ -45,15 +50,19 @@ 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.1, 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')
|
||||
ap.add_argument('--lang', help='game language, a Switch language code (en-US, en-GB, ja, de, ru, ...)')
|
||||
ap.add_argument('--dest', type=Path, required=True, help='install folder')
|
||||
ap.add_argument('--root', type=Path, help='release folder or repository (default: the folder above installer/)')
|
||||
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
|
||||
@@ -75,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
|
||||
+192
-64
@@ -1,20 +1,33 @@
|
||||
"""The installer window: python -m installer.gui
|
||||
|
||||
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, 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>.json under "ui.*" and "launcher.*" ids.
|
||||
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. 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 both languages and close by itself (PNG per screen with --shots)
|
||||
--lang en|ru window language (default: the Windows display language)
|
||||
--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)
|
||||
--rebuild DIR rebuild the install folder DIR for its mods (totk.exe starts this, build-cache/rebuild.txt):
|
||||
the run starts at once with that folder, and the game starts again when it succeeds
|
||||
"""
|
||||
import argparse
|
||||
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
|
||||
@@ -23,13 +36,28 @@ except ImportError:
|
||||
|
||||
from . import ini
|
||||
from . import layout as layouts
|
||||
from .guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, UI_LANGUAGES, Catalog, Form, Model, Process,
|
||||
amount, default_game_language, describe_event, parse_line, windows_ui_language)
|
||||
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():
|
||||
@@ -42,8 +70,9 @@ def fix_tcl_paths():
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self, root, ui_lang, layout, run_args=None):
|
||||
def __init__(self, root, ui_lang, layout, run_args=None, rebuild=None):
|
||||
self.root, self.layout = root, layout
|
||||
self.rebuild = rebuild # the install folder of --rebuild, or None
|
||||
self.cat = Catalog(ui_lang)
|
||||
self.model = Model()
|
||||
self.process = None
|
||||
@@ -51,8 +80,13 @@ class App:
|
||||
self.run_args = run_args or {} # extra CLI options for tests: temp, verify
|
||||
self.log_entries = [] # events and raw lines of the run, rendered by render_log
|
||||
self.bound = [] # (widget, option, id, params) retranslated on a language switch
|
||||
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'dest')}
|
||||
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
|
||||
@@ -70,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')
|
||||
|
||||
@@ -80,19 +114,34 @@ 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)
|
||||
for code in UI_LANGUAGES:
|
||||
ttk.Radiobutton(sw, text=code.upper(), value=code, variable=self.ui_var, style='Toolbutton',
|
||||
command=self.switch_language).pack(side='left', padx=1)
|
||||
self.ui_var = tk.StringVar(value=self.cat.lang) # the window language code; the box shows its name
|
||||
labels = [UI_LANGUAGE_LABELS[c] for c in UI_LANGUAGES]
|
||||
self.ui_box = ttk.Combobox(sw, values=labels, state='readonly', width=max(map(len, labels)) + 1)
|
||||
self.ui_box.set(UI_LANGUAGE_LABELS[self.cat.lang])
|
||||
self.ui_box.bind('<<ComboboxSelected>>', self.pick_language)
|
||||
self.ui_box.pack(side='left')
|
||||
self.body = ttk.Frame(root, padding=PAD)
|
||||
self.body.pack(fill='both', expand=True)
|
||||
self.frames = {'form': self.build_form(), 'run': self.build_run(), 'finish': self.build_finish()}
|
||||
self.vars['lang'].set(self.lang_label(self.lang_code))
|
||||
self.vars['dest'].trace_add('write', lambda *_: self.refresh_form())
|
||||
self.show('form')
|
||||
if rebuild:
|
||||
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 -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -120,8 +169,13 @@ class App:
|
||||
self.refresh_finish()
|
||||
self.render_log()
|
||||
|
||||
def pick_language(self, _event=None):
|
||||
self.ui_var.set(UI_LANGUAGES[self.ui_box.current()])
|
||||
self.switch_language()
|
||||
|
||||
def switch_language(self):
|
||||
self.cat.set_language(self.ui_var.get())
|
||||
self.ui_box.set(UI_LANGUAGE_LABELS[self.ui_var.get()])
|
||||
code = self.selected_lang()
|
||||
self.retext()
|
||||
self.vars['lang'].set(self.lang_label(code))
|
||||
@@ -170,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)
|
||||
@@ -191,32 +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.game_language').grid(row=i, column=0, sticky='w', pady=4)
|
||||
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=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.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)
|
||||
@@ -225,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))
|
||||
@@ -246,31 +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')
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=620, justify='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, 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(),
|
||||
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'))
|
||||
@@ -281,15 +385,19 @@ 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)
|
||||
if path:
|
||||
self.vars[name].set(os.path.normpath(path))
|
||||
|
||||
def pick_firmware(self):
|
||||
path = filedialog.askdirectory(title=self.cat('ui.pick_firmware'), mustexist=True)
|
||||
if path:
|
||||
self.vars['firmware'].set(os.path.normpath(path))
|
||||
|
||||
def pick_dest(self):
|
||||
path = filedialog.askdirectory(title=self.cat('ui.pick_dest'), mustexist=False)
|
||||
if path:
|
||||
@@ -305,8 +413,12 @@ class App:
|
||||
self.fit()
|
||||
return
|
||||
self.form_error.configure(text='')
|
||||
command = form.command(self.layout.python, root=self.layout.root if self.layout.dev else None,
|
||||
temp=self.run_args.get('temp'), verify=self.run_args.get('verify', False))
|
||||
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
|
||||
command = form.command(python, root=self.layout.root if self.layout.dev else None,
|
||||
temp=self.run_args.get('temp'), verify_hashes=self.run_args.get('verify', False))
|
||||
self.begin(command, self.layout.root)
|
||||
|
||||
def begin(self, command, cwd):
|
||||
@@ -328,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)
|
||||
@@ -352,6 +464,9 @@ class App:
|
||||
return
|
||||
self.model.finish(code)
|
||||
self.process = None
|
||||
if self.rebuild and self.model.result == 'ok' and not self.closing:
|
||||
self.start_game()
|
||||
return
|
||||
self.refresh_run()
|
||||
self.refresh_finish()
|
||||
self.show('finish')
|
||||
@@ -410,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]
|
||||
@@ -482,6 +597,15 @@ class App:
|
||||
else:
|
||||
self.open_button.state(['disabled' if not Path(m.dest or '.').is_dir() else '!disabled'])
|
||||
|
||||
def start_game(self):
|
||||
"""Starts the game of the install folder and closes the window."""
|
||||
dest = Path(self.vars['dest'].get().strip())
|
||||
exe = dest / ('totk.exe' if os.name == 'nt' else 'totk')
|
||||
flags = subprocess.DETACHED_PROCESS if os.name == 'nt' else 0
|
||||
if exe.is_file():
|
||||
subprocess.Popen([str(exe)], cwd=str(dest), creationflags=flags, close_fds=True)
|
||||
self.root.destroy()
|
||||
|
||||
def open_folder(self):
|
||||
dest = self.model.dest
|
||||
if dest and Path(dest).is_dir():
|
||||
@@ -495,10 +619,10 @@ class App:
|
||||
# ---- self test ---------------------------------------------------------------------------------------------------
|
||||
|
||||
SAMPLE_START = {'type': 'start', 'release': 'selftest', 'mode': 'install', 'dest': 'Z:/selftest/TotK', 'log': 'D:\\log.txt',
|
||||
'steps': ['check', 'extract', 'lift', 'compile', 'prepare', 'cleanup']}
|
||||
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup']}
|
||||
SAMPLE_RUN = [
|
||||
SAMPLE_START,
|
||||
{'type': 'plan', 'run': ['check', 'extract'], 'skip': {}},
|
||||
{'type': 'plan', 'run': ['check', 'fonts', 'extract'], 'skip': {}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'started'},
|
||||
{'type': 'progress', 'step': 'check', 'phase': 'inputs'},
|
||||
{'type': 'space', 'dest': 'Z:/selftest/TotK', 'need': 36_400_000_000, 'free': 210_000_000_000, 'temp': 'C:\\t',
|
||||
@@ -511,12 +635,14 @@ SAMPLE_RUN = [
|
||||
]
|
||||
|
||||
|
||||
SAMPLE_OK = [SAMPLE_START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'lift': 'current', 'compile': 'current',
|
||||
'prepare': 'current', 'cleanup': 'not_needed'}},
|
||||
SAMPLE_OK = [SAMPLE_START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'fonts': 'current', 'lift': 'current',
|
||||
'compile': 'current', 'prepare': 'current',
|
||||
'cleanup': 'not_needed'}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'started'},
|
||||
{'type': 'step', 'step': 'check', 'state': 'done', 'seconds': 1.0},
|
||||
{'type': 'step', 'step': 'extract', 'state': 'started'},
|
||||
{'type': 'step', 'step': 'extract', 'state': 'done', 'seconds': 1.0},
|
||||
{'type': 'step', 'step': 'fonts', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'lift', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'compile', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'prepare', 'state': 'skipped', 'reason': 'current'},
|
||||
@@ -534,8 +660,9 @@ def grab(root, path):
|
||||
|
||||
|
||||
def selftest(layout, shots=None):
|
||||
"""Every screen in both languages; the window closes itself. Returns 0 when nothing raised."""
|
||||
"""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)
|
||||
@@ -612,13 +739,14 @@ def main(argv=None):
|
||||
ap.add_argument('--shots', type=Path)
|
||||
ap.add_argument('--lang', choices=UI_LANGUAGES)
|
||||
ap.add_argument('--root', type=Path)
|
||||
ap.add_argument('--rebuild', type=Path)
|
||||
args = ap.parse_args(argv)
|
||||
layout = layouts.find(args.root)
|
||||
if args.selftest:
|
||||
return selftest(layout, args.shots)
|
||||
fix_tcl_paths()
|
||||
root = tk.Tk()
|
||||
App(root, args.lang or windows_ui_language(), layout)
|
||||
App(root, args.lang or windows_ui_language(), layout, rebuild=args.rebuild)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
+38
-9
@@ -24,11 +24,17 @@ from . import state as statefile
|
||||
from .events import render
|
||||
|
||||
TEXT = Path(__file__).resolve().parent / 'text'
|
||||
UI_LANGUAGES = ('en', 'ru')
|
||||
STEP_ORDER = ('check', 'extract', 'lift', 'compile', 'prepare', 'cleanup')
|
||||
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', '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
|
||||
LANG_PT_BR = 0x0416 # Portuguese (Brazil), the whole LANGID: Portugal's Portuguese stays English
|
||||
# Windows LANGIDs that write Simplified Chinese: zh-CN, zh-SG. zh-TW, zh-HK and zh-MO write Traditional Chinese, which
|
||||
# the window does not have.
|
||||
LANGIDS_ZH_HANS = (0x0804, 0x1004)
|
||||
|
||||
|
||||
def load_texts(lang):
|
||||
@@ -76,18 +82,35 @@ def flat(value):
|
||||
|
||||
|
||||
def windows_ui_language():
|
||||
"""'ru' when the display language (Windows; elsewhere LC_ALL, LC_MESSAGES or LANG) is Russian, else 'en'."""
|
||||
"""The window language for the display language: the Windows one (ui_language_for_langid); elsewhere LC_ALL,
|
||||
LC_MESSAGES or LANG ('ru', 'pt_BR', 'zh_CN' or 'zh_SG', else English)."""
|
||||
if os.name != 'nt':
|
||||
loc = os.environ.get('LC_ALL') or os.environ.get('LC_MESSAGES') or os.environ.get('LANG') or ''
|
||||
return 'ru' if loc.lower().startswith('ru') else 'en'
|
||||
loc = (os.environ.get('LC_ALL') or os.environ.get('LC_MESSAGES') or os.environ.get('LANG') or '').lower()
|
||||
return ('ru' if loc.startswith('ru') else 'pt-BR' if loc.startswith(('pt_br', 'pt-br'))
|
||||
else 'zh-Hans' if loc.startswith(('zh_cn', 'zh-cn', 'zh_sg', 'zh-sg', 'zh_hans', 'zh-hans')) else 'en')
|
||||
try:
|
||||
langid = ctypes.windll.kernel32.GetUserDefaultUILanguage()
|
||||
except (AttributeError, OSError):
|
||||
return 'en'
|
||||
return 'ru' if langid & 0x3ff == LANG_RU else 'en'
|
||||
return ui_language_for_langid(langid)
|
||||
|
||||
|
||||
def ui_language_for_langid(langid):
|
||||
"""'ru' for a Russian Windows LANGID, 'pt-BR' for Brazilian Portuguese, 'zh-Hans' for a Simplified Chinese one,
|
||||
else 'en'."""
|
||||
if langid & 0x3ff == LANG_RU:
|
||||
return 'ru'
|
||||
if langid == LANG_PT_BR:
|
||||
return 'pt-BR'
|
||||
if langid in LANGIDS_ZH_HANS:
|
||||
return 'zh-Hans'
|
||||
return 'en'
|
||||
|
||||
|
||||
def default_game_language(ui):
|
||||
"""The game language the form starts with. A Chinese window still starts with English: Chinese game text needs
|
||||
the Switch shared fonts (ini.NEED_SHARED_FONTS), which the setup extracts only from a firmware folder, and the game
|
||||
stops when they are missing; the player can still pick Chinese."""
|
||||
return 'ru' if ui == 'ru' else 'en-US'
|
||||
|
||||
|
||||
@@ -111,8 +134,10 @@ class Form:
|
||||
base: str = ''
|
||||
update: str = ''
|
||||
keys: str = ''
|
||||
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."""
|
||||
@@ -147,20 +172,24 @@ class Form:
|
||||
value = getattr(self, name).strip()
|
||||
if value and not Path(value).is_file():
|
||||
found.append(f'ui.missing_{name}')
|
||||
if self.firmware.strip() and not Path(self.firmware.strip()).is_dir():
|
||||
found.append('ui.missing_firmware')
|
||||
return found
|
||||
|
||||
def command(self, python, root=None, temp=None, verify=False):
|
||||
def command(self, python, root=None, temp=None, verify_hashes=False):
|
||||
cmd = [str(python), '-m', 'installer']
|
||||
for flag, value in (('--base', self.base), ('--update', self.update), ('--keys', self.keys),
|
||||
('--lang', self.lang), ('--dest', self.dest)):
|
||||
('--firmware', self.firmware), ('--lang', self.lang), ('--dest', self.dest)):
|
||||
if value.strip():
|
||||
cmd += [flag, value.strip()]
|
||||
if root:
|
||||
cmd += ['--root', str(root)]
|
||||
if temp:
|
||||
cmd += ['--temp', str(temp)]
|
||||
if verify:
|
||||
if verify_hashes:
|
||||
cmd.append('--verify')
|
||||
if self.dlss:
|
||||
cmd.append('--dlss')
|
||||
return cmd
|
||||
|
||||
|
||||
|
||||
+17
-2
@@ -1,5 +1,5 @@
|
||||
// Host check of the installer's first step, compiled with the bundled clang at install time: does the CPU run
|
||||
// -march=x86-64-v3 code, and does a Vulkan 1.3 device exist. Prints one JSON object on stdout.
|
||||
// -march=x86-64-v3 code (or only x86-64-v2), and does a Vulkan 1.3 device exist. Prints one JSON object on stdout.
|
||||
// The Vulkan loader is opened at run time, so a machine without one gets "loader": false instead of a failed start.
|
||||
#include <cpuid.h>
|
||||
#include <stdio.h>
|
||||
@@ -25,10 +25,24 @@ static void json_string(const char *s) {
|
||||
putchar('"');
|
||||
}
|
||||
|
||||
// The x86-64-v2 level from CPUID: SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, CMPXCHG16B and LAHF/SAHF. Every CPU with
|
||||
// SSE4.2 has the others, but the level is what the v2 build is compiled for, so check them all.
|
||||
static int x86_64_v2(void) {
|
||||
unsigned a, b, c, d;
|
||||
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
|
||||
const unsigned sse3 = 1u << 0, ssse3 = 1u << 9, cx16 = 1u << 13, sse41 = 1u << 19, sse42 = 1u << 20,
|
||||
popcnt = 1u << 23;
|
||||
if ((c & (sse3 | ssse3 | cx16 | sse41 | sse42 | popcnt)) != (sse3 | ssse3 | cx16 | sse41 | sse42 | popcnt))
|
||||
return 0;
|
||||
if (!__get_cpuid(0x80000001, &a, &b, &c, &d)) return 0;
|
||||
return (c & (1u << 0)) != 0; // LAHF/SAHF
|
||||
}
|
||||
|
||||
// The x86-64-v3 level from CPUID: AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT and MOVBE, with the OS saving the YMM state.
|
||||
// Not __builtin_cpu_supports("x86-64-v3"): clang reads that from the C runtime's CPU model, and libgcc before GCC 12
|
||||
// (the Linux release's sysroot has GCC 10) has no level bits, so it answers false on every CPU.
|
||||
static int x86_64_v3(void) {
|
||||
if (!x86_64_v2()) return 0;
|
||||
unsigned a, b, c, d;
|
||||
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
|
||||
const unsigned fma = 1u << 12, movbe = 1u << 22, osxsave = 1u << 27, avx = 1u << 28, f16c = 1u << 29;
|
||||
@@ -44,7 +58,8 @@ static int x86_64_v3(void) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("{\"x86_64_v3\": %s, ", x86_64_v3() ? "true" : "false");
|
||||
printf("{\"x86_64_v2\": %s, \"x86_64_v3\": %s, ", x86_64_v2() ? "true" : "false",
|
||||
x86_64_v3() ? "true" : "false");
|
||||
void *lib = open_lib();
|
||||
PFN_vkGetInstanceProcAddr gipa = lib ? (PFN_vkGetInstanceProcAddr)lib_sym(lib, "vkGetInstanceProcAddr") : NULL;
|
||||
if (!gipa) {
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@ LANGUAGES = ('ja', 'en-US', 'en-GB', 'fr', 'fr-CA', 'de', 'it', 'es', 'es-419',
|
||||
'zh-Hans', 'zh-Hant')
|
||||
# Older installer builds wrote these two codes; they are read as the canonical ones and rewritten on the next install.
|
||||
ALIASES = {'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant'}
|
||||
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the installer does not extract.
|
||||
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the fonts step extracts from the
|
||||
# firmware folder when the player gives one.
|
||||
NEED_SHARED_FONTS = ('ko', 'zh-Hans', 'zh-Hant')
|
||||
|
||||
ACTIVE = re.compile(r'^(\s*)(?:totk_)?lang(\s*=\s*)(.*?)(\s*)$', re.IGNORECASE)
|
||||
|
||||
@@ -3,20 +3,35 @@
|
||||
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>
|
||||
|
||||
#include "launcher_text.h"
|
||||
|
||||
#define LANGID_RUSSIAN 0x19
|
||||
#define LANGID_RUSSIAN 0x19 /* primary language id */
|
||||
#define LANGID_PT_BR 0x0416 /* the whole LANGID: Portugal's Portuguese stays English */
|
||||
#define PATH_CHARS (MAX_PATH * 2)
|
||||
|
||||
/* The language of the window (installer/guilogic.py ui_language_for_langid): Russian, Simplified Chinese (zh-CN and
|
||||
zh-SG; the Traditional Chinese regions get English, as the window has no Traditional Chinese), else English. */
|
||||
static void fail(void)
|
||||
{
|
||||
int russian = (GetUserDefaultUILanguage() & 0x3ff) == LANGID_RUSSIAN;
|
||||
MessageBoxW(NULL, russian ? LAUNCHER_PYTHON_MISSING_RU : LAUNCHER_PYTHON_MISSING_EN,
|
||||
russian ? LAUNCHER_TITLE_RU : LAUNCHER_TITLE_EN, MB_OK | MB_ICONERROR);
|
||||
LANGID ui = GetUserDefaultUILanguage();
|
||||
int russian = (ui & 0x3ff) == LANGID_RUSSIAN, brazilian = ui == LANGID_PT_BR;
|
||||
int chinese = ui == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED) ||
|
||||
ui == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SINGAPORE);
|
||||
MessageBoxW(NULL,
|
||||
russian ? LAUNCHER_PYTHON_MISSING_RU
|
||||
: brazilian ? LAUNCHER_PYTHON_MISSING_PT_BR
|
||||
: chinese ? LAUNCHER_PYTHON_MISSING_ZH_HANS
|
||||
: LAUNCHER_PYTHON_MISSING_EN,
|
||||
russian ? LAUNCHER_TITLE_RU : brazilian ? LAUNCHER_TITLE_PT_BR : chinese ? LAUNCHER_TITLE_ZH_HANS
|
||||
: LAUNCHER_TITLE_EN,
|
||||
MB_OK | MB_ICONERROR);
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, int show)
|
||||
@@ -26,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;
|
||||
|
||||
+39
-10
@@ -7,10 +7,13 @@ Release (docs/installer.md "Release layout"), root = the unpacked folder:
|
||||
recomp/, tools/ the lifter and the scripts; recomp/patches-<v>.txt, tools/data/expected-v<version>.json for
|
||||
each supported update (SUPPORTED)
|
||||
llvm/ llvm-mingw (bin/clang.exe)
|
||||
runtime/ libtotkrt.a, the headers, install-side sources (romfs.c, patches.c, ov_zstd.c) and totk.ico; runtime/deps/ holds
|
||||
runtime/ libtotkrt.a and libtotkrt-v2.a (x86-64-v3 / v2), the headers, install-side sources (romfs.c,
|
||||
patches.c, ov_zstd.c) and totk.ico; runtime/deps/ holds
|
||||
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)
|
||||
@@ -21,9 +24,9 @@ against), runtime/deps/ with include/ and the static Opus, libvpx and zstd, bin/
|
||||
|
||||
Dev mode, root = a repository checkout (no python/python.exe): build/py314-embed/py/python.exe (the embeddable
|
||||
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 (tools/build_game.py --make-runtime-lib --out
|
||||
build/install-rtlib); 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,
|
||||
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/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
|
||||
@@ -36,7 +39,7 @@ from pathlib import Path
|
||||
WIN = os.name == 'nt'
|
||||
EXE = '.exe' if WIN else ''
|
||||
UCRT = Path(r'C:\msys64\ucrt64\bin')
|
||||
SUPPORTED = ('1.2.1', '1.4.3', '1.1.1') # updates the installer builds; the check identifies the player's by hash
|
||||
SUPPORTED = ('1.2.1', '1.4.3', '1.1.0') # updates the installer builds; the check identifies the player's by hash
|
||||
|
||||
|
||||
def version_tag(version):
|
||||
@@ -64,11 +67,27 @@ class Layout:
|
||||
ini_example: Path | None = None
|
||||
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):
|
||||
return self.llvm / 'bin' / ('clang' + EXE)
|
||||
|
||||
def runtime_lib_for(self, cpu):
|
||||
"""The prebuilt runtime library for cpu (v2 or v3). None in dev mode when it was not built: the runtime is
|
||||
then compiled from the sources in runtime/ instead."""
|
||||
lib = self.runtime_lib_v2 if cpu == 'v2' else self.runtime_lib
|
||||
if lib and lib.is_file():
|
||||
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'
|
||||
@@ -77,13 +96,18 @@ class Layout:
|
||||
"""The expected hashes of the update."""
|
||||
return self.tools / 'data' / f'expected-v{version}.json'
|
||||
|
||||
def missing(self):
|
||||
"""Paths the steps need that do not exist."""
|
||||
need = [self.python, self.tools / 'extract.py', self.tools / 'build_game.py', self.tools / 'prepare_shaders.py',
|
||||
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 / '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',
|
||||
self.recomp / 'gen.py',
|
||||
self.clang,
|
||||
*((self.deps / 'include', self.deps / 'lib') if self.deps else ()), self.runtime, self.runtime_lib, self.ninja, self.hactool,
|
||||
*((self.deps / 'include', self.deps / 'lib') if self.deps else ()), self.runtime, *([lib] if lib else ()),
|
||||
self.ninja, self.hactool,
|
||||
self.xlate, *self.dlls, *(f(v) for v in SUPPORTED for f in (self.patches, self.expected))]
|
||||
if self.shader_list:
|
||||
need.append(self.shader_list)
|
||||
@@ -109,10 +133,12 @@ def release_layout(root):
|
||||
tools=root / 'tools', recomp=root / 'recomp',
|
||||
llvm=root / 'llvm', deps=root / 'runtime' / 'deps', runtime=root / 'runtime',
|
||||
runtime_lib=root / 'runtime' / 'libtotkrt.a',
|
||||
runtime_lib_v2=root / 'runtime' / 'libtotkrt-v2.a',
|
||||
ninja=root / 'bin' / ('ninja' + EXE), hactool=root / 'bin' / ('hactool' + EXE),
|
||||
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,
|
||||
@@ -134,10 +160,12 @@ def dev_layout(root):
|
||||
tools=root / 'tools', recomp=root / 'recomp',
|
||||
llvm=trees[-1] if trees else root / 'toolchain' / 'llvm-mingw-missing', deps=deps,
|
||||
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libtotkrt.a',
|
||||
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libtotkrt-v2.a',
|
||||
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):
|
||||
@@ -153,6 +181,7 @@ def dev_layout_posix(root):
|
||||
python=Path(sys.executable), tools=root / 'tools', recomp=root / 'recomp',
|
||||
llvm=Path(clang).parent.parent if clang else root / 'toolchain' / 'clang-missing', deps=None,
|
||||
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libtotkrt.a',
|
||||
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libtotkrt-v2.a',
|
||||
ninja=Path(ninja) if ninja else root / 'toolchain' / 'ninja-missing',
|
||||
hactool=root / 'toolchain' / 'hactool' / 'hactool', xlate=root / 'build' / 'xlate' / 'totk_xlate',
|
||||
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp')
|
||||
|
||||
@@ -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
|
||||
+610
-58
@@ -1,17 +1,29 @@
|
||||
"""The six setup steps of docs/installer.md: check, 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); lift,
|
||||
compile and prepare run the bundled Python on recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and
|
||||
tools/prepare_shaders.py, whose output goes to <temp>/logs/<step>-<tool>.log and is read for progress. No Win32 call
|
||||
here: the host check is a small C program built with the bundled clang (hostcheck.c).
|
||||
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
|
||||
recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and tools/prepare_shaders.py, whose output goes to
|
||||
<temp>/logs/<step>-<tool>.log and is read for progress. No Win32 call here: the host check is a small C program built
|
||||
with the bundled clang (hostcheck.c).
|
||||
|
||||
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, totk.exe and its DLLs, romfs-index.bin,
|
||||
romfs-patches/, romfs-patches-xbox/, totk_xlate.exe, the shader state of totk.exe --prepare-shaders, totk.ini,
|
||||
totk-install.json.
|
||||
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), gen/ (C), build/ (objects),
|
||||
logs/ (kept by the cleanup).
|
||||
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, shared-fonts/ (Chinese and Korean text), totk.exe
|
||||
and its DLLs, nvngx_dlss.dll (with --dlss), romfs-index.bin, romfs-patches/, romfs-patches-xbox/,
|
||||
romfs-patches-xbox-label/, romfs-patches-pc/, romfs-patches-<style>-<w>x<h>/ (each of the three for each shaped size),
|
||||
totk_xlate.exe, the
|
||||
shader state of totk.exe --prepare-shaders, totk.ini, totk-install.json.
|
||||
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), unpacked/ (the NSP of an NSZ
|
||||
or XCZ input until the extraction is done), gen/ (C), build/ (objects), logs/. The cleanup keeps gen/, build/ and
|
||||
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 and exefs.data (tools/exefs_patches.py) and
|
||||
rebuild.txt, the command that starts this setup in its rebuild mode on the install folder (installer/gui.py --rebuild),
|
||||
one argument per line; totk.exe runs it at start when an enabled mod has code cheats the build lacks
|
||||
(runtime/cheats.c).
|
||||
"""
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -21,6 +33,7 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from . import ini
|
||||
@@ -29,6 +42,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)
|
||||
@@ -155,7 +169,8 @@ def run_tool(ctx, step, script, args, parse=None, cwd=None):
|
||||
def free_bytes(path):
|
||||
"""Free bytes on the disk of path (its nearest existing parent) and that parent's device."""
|
||||
p = Path(path)
|
||||
while not p.exists():
|
||||
# An unavailable drive or share has no existing ancestor: let disk_usage report its OS error at the root.
|
||||
while not p.exists() and p.parent != p:
|
||||
p = p.parent
|
||||
return shutil.disk_usage(p).free, os.stat(p).st_dev
|
||||
|
||||
@@ -167,6 +182,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."""
|
||||
@@ -177,6 +198,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')
|
||||
@@ -217,15 +257,100 @@ def size_of(paths):
|
||||
return sum(p.stat().st_size for p in paths if p.is_file())
|
||||
|
||||
|
||||
# The Switch system shared fonts (runtime/ov_system.c): TotK falls back to them for Chinese, Korean and Traditional
|
||||
# Chinese text. tools/vendor/macnativegames/extract_shared_fonts.py writes these names.
|
||||
SHARED_FONTS = 'shared-fonts'
|
||||
SHARED_FONT_FILES = ('0-standard.bfttf', '1-chinese-simplified.bfttf', '2-chinese-simplified-extension.bfttf',
|
||||
'3-chinese-traditional.bfttf', '4-korean.bfttf', '5-nintendo-extension.bfttf')
|
||||
FONTS_NEED = 32 << 20 # the six BFTTF files and their manifest
|
||||
|
||||
|
||||
def requested_lang(ctx):
|
||||
"""The language this run uses: --lang, else the one recorded for the install (Check canonicalizes it)."""
|
||||
return ini.canonical_language(getattr(ctx.options, 'lang', None) or ctx.state.get('lang'))
|
||||
|
||||
|
||||
def firmware_dir(value):
|
||||
"""The firmware NCA folder behind a --firmware path: the folder itself, or its registered/ child (the emulators'
|
||||
layout, Contents/registered)."""
|
||||
if not value:
|
||||
return None
|
||||
path = Path(value)
|
||||
return path / 'registered' if (path / 'registered').is_dir() else path
|
||||
|
||||
|
||||
def firmware_has_ncas(folder):
|
||||
"""Whether folder holds firmware NCAs: .nca files, or the hashed directories with a 00 file (registered/)."""
|
||||
try:
|
||||
entries = list(Path(folder).iterdir())
|
||||
except OSError:
|
||||
return False
|
||||
return any((e.is_file() and e.suffix.lower() == '.nca') or (e.is_dir() and (e / '00').is_file()) for e in entries)
|
||||
|
||||
|
||||
def firmware_stamp(folder):
|
||||
"""A cheap identity of the firmware NCAs: names, sizes and mtimes, not their contents (the folder is ~326 MB)."""
|
||||
try:
|
||||
entries = sorted(Path(folder).iterdir())
|
||||
except OSError:
|
||||
return 'missing'
|
||||
rows = []
|
||||
for entry in entries:
|
||||
if entry.is_dir():
|
||||
file = entry / '00'
|
||||
if not file.is_file():
|
||||
continue
|
||||
elif not entry.is_file() or entry.suffix.lower() != '.nca':
|
||||
continue
|
||||
else:
|
||||
file = entry
|
||||
st = file.stat()
|
||||
rows.append((entry.name, st.st_size, st.st_mtime_ns))
|
||||
return rows
|
||||
|
||||
|
||||
def shared_fonts_ready(dest):
|
||||
"""Whether dest/shared-fonts holds a complete set that the game can load."""
|
||||
folder = Path(dest) / SHARED_FONTS
|
||||
return (folder / 'manifest.json').is_file() and all((folder / name).is_file() for name in SHARED_FONT_FILES)
|
||||
|
||||
|
||||
# ---- 1. check
|
||||
|
||||
UPDATE_TITLE_ID = '0100F2C0115B6800'
|
||||
|
||||
|
||||
def base_misuse(records):
|
||||
"""Name why a file chosen as the base game holds an update or a combined package, or return None.
|
||||
|
||||
The Meta NCA (content type 1) carries the package's own title id: the update's id (...800) for an update NSP, and
|
||||
several Meta NCAs mean base, update and DLC repacked into one file.
|
||||
"""
|
||||
metadata = [r['title_id'] for r, _ in records if r['content_type'] == 1]
|
||||
if len(metadata) > 1:
|
||||
return 'check.base_combined'
|
||||
if UPDATE_TITLE_ID in metadata:
|
||||
return 'check.base_is_update'
|
||||
return None
|
||||
|
||||
|
||||
class Check(Step):
|
||||
id = 'check'
|
||||
always = True
|
||||
|
||||
def key(self, ctx):
|
||||
# The compile step fingerprints the CPU level, and every key is computed before a step runs: probe the host
|
||||
# here, once, so both steps see the same answer (the check step itself is always run). A failed probe is
|
||||
# reported by run(), where the error names this step.
|
||||
try:
|
||||
self.host(ctx)
|
||||
except InstallError:
|
||||
pass
|
||||
return ''
|
||||
|
||||
def run(self, ctx):
|
||||
layout, rep, dest = ctx.layout, ctx.reporter, ctx.dest
|
||||
missing = layout.missing()
|
||||
missing = layout.missing(getattr(ctx, 'cpu', 'v3'))
|
||||
if missing:
|
||||
raise InstallError('check.release_incomplete', paths=missing[:5])
|
||||
if dest.exists() and not dest.is_dir():
|
||||
@@ -233,13 +358,35 @@ class Check(Step):
|
||||
own = (ctx.temp, *ctx.temp.parents) # a temp folder inside the install folder (installer/cli.py) is ours
|
||||
if ctx.mode == 'install' and dest.is_dir() and any(p not in own for p in dest.iterdir()):
|
||||
raise InstallError('check.dest_not_empty', path=str(dest))
|
||||
lang = ini.canonical_language(ctx.options.lang or ctx.state.get('lang'))
|
||||
lang = requested_lang(ctx)
|
||||
if not lang:
|
||||
raise InstallError('check.lang_missing')
|
||||
if lang not in ini.LANGUAGES:
|
||||
raise InstallError('check.lang_unknown', lang=lang, known=', '.join(ini.LANGUAGES))
|
||||
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
|
||||
if firmware is not None:
|
||||
if not firmware.is_dir():
|
||||
raise InstallError('check.firmware_missing', path=str(firmware))
|
||||
if not firmware_has_ncas(firmware):
|
||||
raise InstallError('check.firmware_empty', path=str(firmware))
|
||||
if lang in ini.NEED_SHARED_FONTS:
|
||||
rep.warning('warn.lang_needs_fonts', lang=lang)
|
||||
if firmware is None:
|
||||
rep.warning('warn.lang_needs_fonts', lang=lang)
|
||||
elif not shared_fonts_ready(dest):
|
||||
keys = getattr(ctx.options, 'keys', None)
|
||||
if not keys or not Path(keys).is_file():
|
||||
raise InstallError('check.fonts_keys_needed')
|
||||
try:
|
||||
key_values = load_keys(Path(keys))
|
||||
except (ValueError, OSError, UnicodeDecodeError):
|
||||
raise InstallError('check.keys_unreadable') from None
|
||||
if 'header_key' not in key_values:
|
||||
raise InstallError('check.keys_no_header_key')
|
||||
if (sys.platform == 'linux' and ctx.will_run('prepare')
|
||||
and not (os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY'))):
|
||||
# totk --prepare-shaders opens a window and needs a present-capable queue family; without a display it
|
||||
# fails after lift and compile (~15 min), so stop here
|
||||
raise InstallError('check.no_display')
|
||||
ctx.lang = lang
|
||||
ctx.temp.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -258,6 +405,8 @@ class Check(Step):
|
||||
return {}
|
||||
|
||||
def host(self, ctx):
|
||||
if getattr(ctx, 'host', None) is not None:
|
||||
return
|
||||
layout, work = ctx.layout, ctx.temp / 'check'
|
||||
glibc = linux_glibc()
|
||||
if glibc and glibc < GLIBC_NEEDED and not layout.dev:
|
||||
@@ -282,8 +431,10 @@ class Check(Step):
|
||||
except ValueError:
|
||||
raise InstallError('check.host_probe_failed', code=r.returncode) from None
|
||||
ctx.reporter.log(f'host: {r.stdout.strip()}')
|
||||
if not host['x86_64_v3']:
|
||||
raise InstallError('check.cpu_no_avx2')
|
||||
if not host['x86_64_v2']:
|
||||
raise InstallError('check.cpu_too_old')
|
||||
# v3 is the faster build; v2 runs on CPUs without AVX2/FMA (runtime/fma.c supplies the fused multiply-add).
|
||||
ctx.cpu = 'v3' if host['x86_64_v3'] else 'v2'
|
||||
if not host['loader']:
|
||||
raise InstallError('check.vulkan_missing')
|
||||
devices = host['devices']
|
||||
@@ -314,14 +465,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:
|
||||
@@ -350,6 +560,10 @@ class Check(Step):
|
||||
except ValueError as e:
|
||||
if is_update and 'BKTR' in str(e):
|
||||
raise InstallError('check.update_is_base') from None
|
||||
if not is_update:
|
||||
reason = base_misuse(records)
|
||||
if reason:
|
||||
raise InstallError(reason) from None
|
||||
raise InstallError('check.update_not_totk' if is_update else 'check.base_not_totk', detail=str(e)) from None
|
||||
try:
|
||||
nca_ctr_key(header, keys, tickets)
|
||||
@@ -368,6 +582,8 @@ class Check(Step):
|
||||
romfs = ctx.dest / 'romfs' / 'romfs.bin'
|
||||
if romfs.is_file(): # a resumed extraction replaces it
|
||||
dest_need -= romfs.stat().st_size
|
||||
if ctx.will_run('fonts'):
|
||||
dest_need += FONTS_NEED
|
||||
if ctx.will_run('prepare') or ctx.will_run('extract'):
|
||||
dest_need += DEST_EXTRA
|
||||
if ctx.will_run('lift') or ctx.will_run('compile'):
|
||||
@@ -396,9 +612,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
|
||||
@@ -419,7 +636,72 @@ class Check(Step):
|
||||
ctx.state['version'] = found
|
||||
|
||||
|
||||
# ---- 2. extract
|
||||
# ---- 2. fonts
|
||||
|
||||
class Fonts(Step):
|
||||
"""The Switch system fonts for Chinese, Korean and Traditional Chinese text (tools/extract_fonts.py, which runs the
|
||||
vendored extract_shared_fonts.py). The firmware folder is optional: without it the setup warns and the game runs
|
||||
with those glyphs missing. The extraction verifies the firmware NCAs and writes the six BFTTF files the console
|
||||
puts in shared memory."""
|
||||
|
||||
id = 'fonts'
|
||||
|
||||
def key(self, ctx):
|
||||
lang = requested_lang(ctx)
|
||||
if lang not in ini.NEED_SHARED_FONTS:
|
||||
return fingerprint('fonts-1', 'not-needed')
|
||||
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
|
||||
if firmware is None:
|
||||
return fingerprint('fonts-1', 'no-firmware')
|
||||
return fingerprint('fonts-1', lang, str(firmware), firmware_stamp(firmware))
|
||||
|
||||
def current(self, ctx, record):
|
||||
if requested_lang(ctx) not in ini.NEED_SHARED_FONTS or not getattr(ctx.options, 'firmware', None):
|
||||
return True
|
||||
return shared_fonts_ready(ctx.dest)
|
||||
|
||||
def verify(self, ctx, record):
|
||||
# An update run may omit --firmware; the installed set still has to be there for these languages.
|
||||
if requested_lang(ctx) in ini.NEED_SHARED_FONTS:
|
||||
return shared_fonts_ready(ctx.dest)
|
||||
return True
|
||||
|
||||
def run(self, ctx):
|
||||
lang = getattr(ctx, 'lang', None) or requested_lang(ctx)
|
||||
if lang not in ini.NEED_SHARED_FONTS:
|
||||
return {}
|
||||
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
|
||||
if firmware is None:
|
||||
return {} # Check warned: the game runs, some text may not show
|
||||
keys = getattr(ctx.options, 'keys', None)
|
||||
if not keys:
|
||||
raise InstallError('check.fonts_keys_needed')
|
||||
out = ctx.temp / 'shared-fonts'
|
||||
remove(out)
|
||||
ctx.reporter.progress('fonts', 'extract', force=True)
|
||||
tool = ctx.layout.tools / 'vendor' / 'macnativegames' / 'extract_shared_fonts.py'
|
||||
code, log = run_tool(ctx, 'fonts', tool, ['--keys', keys, '--firmware', firmware, '--output', out])
|
||||
if code:
|
||||
detail = ''
|
||||
try:
|
||||
detail = log.read_text(encoding='utf-8', errors='replace').strip().splitlines()[-1][:300]
|
||||
except (OSError, IndexError):
|
||||
pass
|
||||
raise InstallError('fonts.failed', code=code, detail=detail, log=str(log))
|
||||
missing = [name for name in SHARED_FONT_FILES if not (out / name).is_file()]
|
||||
if missing or not (out / 'manifest.json').is_file():
|
||||
raise InstallError('fonts.incomplete', files=', '.join(missing) or 'manifest.json')
|
||||
ctx.reporter.progress('fonts', 'copy', force=True)
|
||||
try:
|
||||
ctx.dest.mkdir(parents=True, exist_ok=True)
|
||||
remove(ctx.dest / SHARED_FONTS)
|
||||
shutil.move(str(out), str(ctx.dest / SHARED_FONTS))
|
||||
except OSError as e:
|
||||
raise InstallError('fonts.copy_failed', path=str(ctx.dest / SHARED_FONTS), detail=str(e)) from None
|
||||
return {'files': len(SHARED_FONT_FILES)}
|
||||
|
||||
|
||||
# ---- 3. extract
|
||||
|
||||
class Extract(Step):
|
||||
id = 'extract'
|
||||
@@ -452,9 +734,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'):
|
||||
@@ -464,6 +746,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
|
||||
@@ -475,7 +758,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
|
||||
@@ -497,6 +780,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}
|
||||
|
||||
@@ -518,7 +802,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-'
|
||||
@@ -527,18 +812,75 @@ class Extract(Step):
|
||||
watch.stop()
|
||||
|
||||
|
||||
# ---- 3. lift
|
||||
# ---- 4. lift
|
||||
|
||||
EMITTED = re.compile(r'^emitted (\d+)/(\d+) chunks')
|
||||
DISCOVERED = re.compile(r'^(\w+): \d+ functions \(.*discovery')
|
||||
|
||||
|
||||
CHEAT_PLACES = ('cheats', 'atmosphere/contents/0100F2C0115B6000/cheats', 'contents/0100F2C0115B6000/cheats',
|
||||
'0100F2C0115B6000/cheats')
|
||||
|
||||
|
||||
def cheat_files(ctx):
|
||||
"""The Atmosphere cheat files of the mods folder (<dest>/mods/<mod>/cheats/*.txt and the Atmosphere places, as
|
||||
tools/cheats.py finds them), for the lift key. All of them: the ones the build reads go by the build id of the
|
||||
installed main, and the keys are computed before Extract runs (runner plan), when a fresh install has no main and an
|
||||
update may still have another version's."""
|
||||
mods = ctx.dest / 'mods'
|
||||
if not mods.is_dir():
|
||||
return []
|
||||
return sorted(f for mod in mods.iterdir() if mod.is_dir() for where in CHEAT_PLACES
|
||||
for f in ((mod / where).glob('*.txt') if (mod / where).is_dir() else ()))
|
||||
|
||||
|
||||
EXEFS_PLACES = ('exefs', 'atmosphere/contents/0100F2C0115B6000/exefs', 'contents/0100F2C0115B6000/exefs',
|
||||
'0100F2C0115B6000/exefs')
|
||||
|
||||
|
||||
def exefs_files(ctx):
|
||||
"""The ExeFS patch files of the mods folder (.ips and .pchtxt in each mod's exefs places and its
|
||||
atmosphere/exefs_patches/<name>/, as tools/exefs_patches.py finds them), for the lift key."""
|
||||
mods = ctx.dest / 'mods'
|
||||
if not mods.is_dir():
|
||||
return []
|
||||
out = []
|
||||
for mod in (m for m in mods.iterdir() if m.is_dir()):
|
||||
folders = [mod / w for w in EXEFS_PLACES if (mod / w).is_dir()]
|
||||
patches = mod / 'atmosphere' / 'exefs_patches'
|
||||
folders += [d for d in patches.iterdir() if d.is_dir()] if patches.is_dir() else []
|
||||
out += [f for d in folders for f in d.iterdir() if f.is_file() and f.suffix.lower() in ('.ips', '.pchtxt')]
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def exefs_status(ctx):
|
||||
"""The build's ExeFS patch status file (tools/exefs_patches.py --status), which runtime/mods.c reads."""
|
||||
return ctx.dest / 'build-cache' / 'exefs.status'
|
||||
|
||||
|
||||
def exefs_data(ctx):
|
||||
"""The data writes of the mods' ExeFS patch files (tools/exefs_patches.py --data), which runtime/mods.c stores into
|
||||
main before the game runs."""
|
||||
return ctx.dest / 'build-cache' / 'exefs.data'
|
||||
|
||||
|
||||
def cheats_status(ctx):
|
||||
"""The build's cheat status file (tools/cheats.py --status), which runtime/cheats.c reads: in build-cache, not in a
|
||||
mod's own cheats folder, which the player (or TKMM's merge) may empty."""
|
||||
return ctx.dest / 'build-cache' / 'cheats.status'
|
||||
|
||||
|
||||
class Lift(Step):
|
||||
id = 'lift'
|
||||
transient = True
|
||||
|
||||
def key(self, ctx):
|
||||
return fingerprint('lift-1', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)))
|
||||
# fingerprint takes a file's name and content only; the paths under mods/ go in too, because tools/cheats.py
|
||||
# and tools/exefs_patches.py name each patch set after its mod folder, so a renamed folder needs a lift
|
||||
files = cheat_files(ctx) + exefs_files(ctx)
|
||||
return fingerprint('lift-5', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
|
||||
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *files,
|
||||
*(f.relative_to(ctx.dest / 'mods').as_posix() for f in files))
|
||||
|
||||
def current(self, ctx, record):
|
||||
gen = ctx.temp / 'gen'
|
||||
@@ -546,8 +888,10 @@ class Lift(Step):
|
||||
return (gen / 'layout.json').is_file() and stamp.is_file() and stamp.read_text().strip() == ctx.keys['lift']
|
||||
|
||||
def run(self, ctx):
|
||||
# gen/ of the last run stays: gen.py rewrites only the chunks whose C changed (and removes the ones a smaller
|
||||
# partition no longer has), so Ninja recompiles only those
|
||||
layout, rep, gen = ctx.layout, ctx.reporter, ctx.temp / 'gen'
|
||||
remove(gen)
|
||||
(gen / STAMP).unlink(missing_ok=True)
|
||||
rep.progress('lift', 'discover', force=True)
|
||||
|
||||
def parse(line):
|
||||
@@ -555,28 +899,81 @@ class Lift(Step):
|
||||
if m:
|
||||
rep.progress('lift', 'emit', int(m.group(1)), int(m.group(2)), 'chunks')
|
||||
|
||||
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py',
|
||||
['--exefs', ctx.dest / 'exefs', '--out', gen, '--patches', layout.patches(game_version(ctx))], parse)
|
||||
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets and data writes
|
||||
# (tools/cheats.py, tools/exefs_patches.py); their status files tell the runtime what the build has. Both are
|
||||
# the player's downloads, so files the tools refuse cost those cheats or patches, not the install: the game is
|
||||
# built without them and the setup warns.
|
||||
plain = layout.patches(game_version(ctx))
|
||||
# (tool, patch file it writes, its outputs for the runtime: the status first, then the data file, the files it
|
||||
# reads, the warning when it fails)
|
||||
stages = [('cheats.py', 'patches-cheats.txt', [cheats_status(ctx)], cheat_files(ctx), 'warn.cheats_skipped'),
|
||||
('exefs_patches.py', 'patches-exefs.txt', [exefs_status(ctx), exefs_data(ctx)], exefs_files(ctx),
|
||||
'warn.exefs_skipped')]
|
||||
patches, skipped = plain, {}
|
||||
|
||||
def drop(outputs): # no status: the runtime treats every such file as not built
|
||||
for f in outputs:
|
||||
f.unlink(missing_ok=True)
|
||||
|
||||
for tool, out, outputs, inputs, warn in stages:
|
||||
drop(outputs)
|
||||
if not inputs:
|
||||
continue
|
||||
code, log = run_tool(ctx, 'lift', layout.tools / tool,
|
||||
['--exefs', ctx.dest / 'exefs', '--mods', ctx.dest / 'mods', '--patches', patches,
|
||||
'--out', ctx.temp / out, '--status', outputs[0],
|
||||
*(['--data', outputs[1]] if len(outputs) > 1 else [])])
|
||||
if code:
|
||||
drop(outputs)
|
||||
skipped[warn] = log
|
||||
else:
|
||||
patches = ctx.temp / out
|
||||
args = ['--exefs', ctx.dest / 'exefs', '--out', gen]
|
||||
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', patches], parse)
|
||||
if code and patches != plain:
|
||||
for tool, out, outputs, inputs, warn in stages:
|
||||
if inputs and warn not in skipped:
|
||||
drop(outputs)
|
||||
skipped[warn] = log
|
||||
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', plain], parse)
|
||||
if code:
|
||||
raise InstallError('lift.failed', code=code, log=str(log))
|
||||
# The mods' data writes must stay off the bytes the lifted code reads as constants (its jump tables), which only
|
||||
# the lift knows: gen.py lists them in folded.txt, and a file that writes them becomes unsupported
|
||||
if exefs_data(ctx).is_file():
|
||||
code, log = run_tool(ctx, 'lift', layout.tools / 'exefs_patches.py',
|
||||
['--exefs', ctx.dest / 'exefs', '--status', exefs_status(ctx), '--data',
|
||||
exefs_data(ctx), '--folded', gen / 'main' / 'folded.txt'])
|
||||
if code:
|
||||
drop([exefs_status(ctx), exefs_data(ctx)])
|
||||
skipped.setdefault('warn.exefs_skipped', log)
|
||||
for warn, log in skipped.items():
|
||||
rep.warning(warn, log=str(log))
|
||||
(gen / STAMP).write_text(ctx.keys['lift'] + '\n')
|
||||
return {}
|
||||
|
||||
|
||||
# ---- 4. compile
|
||||
# ---- 5. compile
|
||||
|
||||
NINJA_LINE = re.compile(r'^\[(\d+)/(\d+)\] ')
|
||||
|
||||
|
||||
def runtime_key(ctx):
|
||||
"""Fingerprint of what totk.exe is built from besides the lifted code: the runtime, the toolchain and the build
|
||||
tools. Shader preparation depends on these, not on the code cheats a lift adds."""
|
||||
layout, cpu = ctx.layout, getattr(ctx, 'cpu', 'v3')
|
||||
clang = subprocess.run([str(layout.clang), '--version'], capture_output=True, text=True).stdout
|
||||
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py', 'check_isa.py')]
|
||||
return fingerprint('runtime-1', cpu, layout.runtime_lib_for(cpu), layout.runtime, layout.deps or 'system',
|
||||
layout.sysroot or 'host', *tools, clang, *[d.name for d in layout.dlls])
|
||||
|
||||
|
||||
class Compile(Step):
|
||||
id = 'compile'
|
||||
|
||||
def key(self, ctx):
|
||||
layout = ctx.layout
|
||||
clang = subprocess.run([str(layout.clang), '--version'], capture_output=True, text=True).stdout
|
||||
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py')]
|
||||
return fingerprint('compile-1', ctx.keys['lift'], layout.runtime, layout.runtime_lib, layout.deps or 'system',
|
||||
layout.sysroot or 'host', *tools, clang, *[d.name for d in layout.dlls])
|
||||
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)
|
||||
@@ -600,10 +997,14 @@ class Compile(Step):
|
||||
rep.progress('compile', 'link', force=True)
|
||||
|
||||
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,
|
||||
'--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', layout.runtime_lib, '--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)
|
||||
@@ -617,18 +1018,23 @@ class Compile(Step):
|
||||
replace_file(build / 'romfs-index.bin', dest / 'romfs-index.bin')
|
||||
for dll in layout.dlls:
|
||||
replace_file(dll, dest / dll.name)
|
||||
return {'exe_sha256': file_sha256(dest / exe.name)}
|
||||
return {'exe_sha256': file_sha256(dest / exe.name), 'cpu': cpu}
|
||||
|
||||
|
||||
# ---- 5. prepare
|
||||
# ---- 6. prepare
|
||||
|
||||
TRANSLATED = re.compile(r'gfx_shader: prepare: (\d+) of (\d+) pairs')
|
||||
# runtime/vk_pipeline.c lb_warm_prepared: variants the driver crashed on in an earlier run (tools/prepare_shaders.py
|
||||
# runs the driver step again after such a crash)
|
||||
SKIPPED = re.compile(r'prepare-shaders: (\d+) variants skipped: the graphics driver crashed')
|
||||
|
||||
|
||||
# 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):
|
||||
@@ -638,8 +1044,10 @@ class Prepare(Step):
|
||||
layout = ctx.layout
|
||||
xlate_id = layout.xlate.with_suffix('.id')
|
||||
tools = [layout.tools / n for n in ('prepare_shaders.py', 'shader_census.py', 'make_romfs_patches.py',
|
||||
'pack_shader_cache.py', 'ultrawide_layout.py')]
|
||||
return fingerprint('prepare-1', ctx.keys['compile'], layout.xlate, xlate_id, shader_list_text(layout), *tools)
|
||||
'pack_shader_cache.py', 'ultrawide_layout.py', 'button_icons.py')]
|
||||
# not the compile key: a rebuild for the mods' code cheats keeps the prepared shaders
|
||||
return fingerprint('prepare-2', ctx.keys['extract'], runtime_key(ctx), layout.xlate, xlate_id,
|
||||
shader_list_text(layout), *tools)
|
||||
|
||||
def current(self, ctx, record):
|
||||
dest = ctx.dest
|
||||
@@ -654,10 +1062,18 @@ class Prepare(Step):
|
||||
# Another translator: its cache keys differ, so the old translations would only take space.
|
||||
for name in ('shader-cache', 'shader-prepared.bin'):
|
||||
remove(dest / name)
|
||||
replace_file(layout.xlate, xlate)
|
||||
# totk.exe starts totk_xlate.exe as a child while it prepares shaders; a preparation that was stopped from
|
||||
# outside leaves that child holding this file, and Windows then refuses to replace it (issue #174).
|
||||
try:
|
||||
replace_file(layout.xlate, xlate)
|
||||
except PermissionError:
|
||||
raise InstallError('prepare.xlate_in_use', path=str(xlate)) from None
|
||||
xlate_id = layout.xlate.with_suffix('.id')
|
||||
if xlate_id.is_file():
|
||||
replace_file(xlate_id, xlate.with_suffix('.id'))
|
||||
try:
|
||||
replace_file(xlate_id, xlate.with_suffix('.id'))
|
||||
except PermissionError:
|
||||
raise InstallError('prepare.xlate_in_use', path=str(xlate.with_suffix('.id'))) from None
|
||||
else:
|
||||
remove(xlate.with_suffix('.id'))
|
||||
|
||||
@@ -678,26 +1094,57 @@ class Prepare(Step):
|
||||
dest / f'romfs-patches-{size}', '--aspect', size])
|
||||
if code:
|
||||
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
|
||||
# romfs-patches-xbox: the same with the A/B and X/Y prompts swapped, for pad_layout = xbox_prompts (written on
|
||||
# every install, so the menu choice works without another run of the setup). Optional: when it fails, the game
|
||||
# keeps the Switch prompts and the menu says the files are missing, so the setup warns and goes on.
|
||||
remove(dest / 'romfs-patches-xbox')
|
||||
code, log = run_tool(ctx, 'prepare-xbox', layout.tools / 'make_romfs_patches.py',
|
||||
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
|
||||
dest / 'romfs-patches-xbox', '--xbox-prompts'])
|
||||
if code:
|
||||
remove(dest / 'romfs-patches-xbox')
|
||||
rep.warning('warn.xbox_prompts_failed', code=code, log=str(log))
|
||||
# The same with Xbox button icons in the prompts, for prompts = xbox (make_romfs_patches.py --prompts):
|
||||
# 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. romfs-patches-pc likewise, with keyboard and mouse
|
||||
# prompts for prompts = pc.
|
||||
failed = None
|
||||
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',
|
||||
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out', out,
|
||||
'--prompts', style])
|
||||
if code:
|
||||
remove(out)
|
||||
failed = failed or (code, log)
|
||||
# Each prompts' folder once more for each shape of ASPECT_SIZES, romfs-patches-<style>-<w>x<h> with that
|
||||
# shape's UI layout (--prompts <style> --aspect), which runtime/romfs.c overlay_init loads in place of
|
||||
# romfs-patches-<w>x<h>: without it a player with Xbox or PC prompts on a screen that is not 16:9 chose between
|
||||
# the prompts and the screen's shape. Only the Common archive differs from romfs-patches-<style>; --reuse links
|
||||
# the fonts, messages and size table from there instead of compressing them again, so each folder takes
|
||||
# seconds and a few MiB. Optional like romfs-patches-<style>, and skipped without it.
|
||||
for style in ('xbox', 'xbox-label', 'pc'):
|
||||
plain = dest / f'romfs-patches-{style}'
|
||||
for size in ASPECT_SIZES:
|
||||
out = dest / f'romfs-patches-{style}-{size}'
|
||||
remove(out)
|
||||
if not (plain / 'manifest.tsv').is_file():
|
||||
continue
|
||||
code, log = run_tool(ctx, f'prepare-{style}-{size}', layout.tools / 'make_romfs_patches.py',
|
||||
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
|
||||
out, '--prompts', style, '--aspect', size, '--reuse', plain])
|
||||
if code:
|
||||
remove(out)
|
||||
failed = failed or (code, log)
|
||||
if failed:
|
||||
rep.warning('warn.xbox_prompts_failed', code=failed[0], log=str(failed[1]))
|
||||
|
||||
shader_list = layout.shader_list
|
||||
if not shader_list:
|
||||
shader_list = ctx.temp / 'shader_list.tsv'
|
||||
shader_list.write_text(shader_list_text(layout), encoding='utf-8', newline='\n')
|
||||
|
||||
skipped = [0]
|
||||
|
||||
def parse(line):
|
||||
m = TRANSLATED.search(line)
|
||||
if m:
|
||||
rep.progress('prepare', 'translate', int(m.group(1)), int(m.group(2)), 'shaders')
|
||||
elif SKIPPED.search(line):
|
||||
skipped[0] = int(SKIPPED.search(line).group(1))
|
||||
elif 'listed variants' in line:
|
||||
rep.progress('prepare', 'translate', force=True)
|
||||
elif 'gfx_shader: prepare:' in line and 'translated' in line and 'programs' in line:
|
||||
@@ -709,6 +1156,8 @@ class Prepare(Step):
|
||||
dest / 'exefs', '--list', shader_list], parse)
|
||||
if code or not (dest / 'shader-prepared.bin').is_file():
|
||||
raise InstallError('prepare.shaders_failed', code=code, log=str(log))
|
||||
if skipped[0]:
|
||||
rep.warning('warn.shaders_skipped', count=skipped[0], log=str(log))
|
||||
|
||||
# One shader-cache.pack instead of four files per shader (~117,000): totk.exe reads the pack first.
|
||||
rep.progress('prepare', 'pack', force=True)
|
||||
@@ -718,7 +1167,82 @@ class Prepare(Step):
|
||||
return {'xlate_sha256': file_sha256(xlate)}
|
||||
|
||||
|
||||
# ---- 6. 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'
|
||||
@@ -736,12 +1260,40 @@ class Cleanup(Step):
|
||||
rep.progress('cleanup', 'temp', force=True)
|
||||
for stale in dest.glob('update-merge-*'):
|
||||
remove(stale)
|
||||
for name in ('gen', 'build', 'check', 'shader_list.tsv'):
|
||||
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)
|
||||
return {}
|
||||
|
||||
|
||||
# Puts the release folder (the argument after -c) on sys.path and runs installer.gui with the rest. `-m installer.gui`
|
||||
# finds the package only through the release Python's ._pth: the Linux release's Python and the dev-mode embeddable one
|
||||
# lack that entry, and totk.exe starts the command in the install folder.
|
||||
REBUILD_BOOT = ('import runpy, sys; sys.path.insert(0, sys.argv.pop(1)); '
|
||||
"runpy.run_module('installer.gui', run_name='__main__', alter_sys=True)")
|
||||
|
||||
|
||||
def rebuild_command(ctx):
|
||||
"""The command that starts the setup window in its rebuild mode on this install folder (installer/gui.py
|
||||
--rebuild): pythonw beside the setup's Python on Windows, so no console opens. In a repository that is the Python
|
||||
running this setup: the tools' embeddable Python there has no tkinter."""
|
||||
layout = ctx.layout
|
||||
python = Path(sys.executable) if layout.dev else layout.python
|
||||
if os.name == 'nt' and python.with_name('pythonw.exe').is_file():
|
||||
python = python.with_name('pythonw.exe')
|
||||
return [str(python), '-c', REBUILD_BOOT, str(layout.root), '--root', str(layout.root), '--rebuild', str(ctx.dest)]
|
||||
|
||||
|
||||
def write_rebuild_command(ctx):
|
||||
"""build-cache/rebuild.txt in the install folder: rebuild_command, one argument per line, for totk.exe."""
|
||||
cache = ctx.dest / 'build-cache'
|
||||
cache.mkdir(exist_ok=True)
|
||||
tmp = cache / 'rebuild.txt.tmp'
|
||||
tmp.write_text('\n'.join(rebuild_command(ctx)) + '\n', encoding='utf-8', newline='\n')
|
||||
os.replace(tmp, cache / 'rebuild.txt')
|
||||
|
||||
|
||||
def romfs_problem(romfs, expected):
|
||||
"""Raise cleanup.romfs_unreadable unless romfs/ still holds the verified image. The game opens romfs.bin at start;
|
||||
an install whose romfs/ went empty and unreadable after the extraction verified it (a folder Windows was still
|
||||
@@ -758,4 +1310,4 @@ def romfs_problem(romfs, expected):
|
||||
|
||||
|
||||
def all_steps():
|
||||
return [Check(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Dlss(), Cleanup()]
|
||||
+44
-13
@@ -1,16 +1,22 @@
|
||||
{
|
||||
"step.check": "Checking the computer and the game files",
|
||||
"step.fonts": "Extracting the system fonts",
|
||||
"step.extract": "Extracting the game",
|
||||
"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",
|
||||
"phase.extract.hash_base": "Computing the checksum of the base game",
|
||||
"phase.extract.hash_update": "Computing the checksum of the update",
|
||||
"phase.extract.exefs": "Extracting the game code",
|
||||
@@ -27,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",
|
||||
|
||||
@@ -44,36 +51,47 @@
|
||||
"check.toolchain_broken": "The bundled compiler does not work: {detail}",
|
||||
"check.glibc_old": "This system's C library is glibc {version}; the setup's compiler needs {need} or later (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 and newer have it). Updating the system fixes this.",
|
||||
"check.host_probe_failed": "The computer check did not run (exit code {code}).",
|
||||
"check.cpu_no_avx2": "This processor lacks AVX2 (x86-64-v3), which the game needs. Intel Haswell (2013) or AMD Excavator / Zen and newer have it.",
|
||||
"check.cpu_too_old": "This processor lacks x86-64-v2 (SSE4.2, POPCNT and CMPXCHG16B), the oldest level the game is built for. Intel Nehalem (2008) or AMD Bulldozer (2011) and newer have it.",
|
||||
"check.vulkan_missing": "No Vulkan driver is installed. Install the latest driver for your graphics card.",
|
||||
"check.vulkan_no_device": "Vulkan finds no graphics card. Install the latest driver for your graphics card.",
|
||||
"check.vulkan_too_old": "The game needs a graphics card with Vulkan 1.3; found: {devices}. Update the graphics driver.",
|
||||
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.1 or 1.2.1 or 1.4.3 update and prod.keys.",
|
||||
"check.no_display": "Preparing the shaders needs a display. Run the setup from the desktop session, or export DISPLAY=:0 (on a Steam Deck in Game Mode also XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.0 or 1.2.1 or 1.4.3 update and prod.keys.",
|
||||
"check.input_missing": "File not found ({input}): {path}",
|
||||
"check.keys_unreadable": "The keys file cannot be read. Choose a prod.keys file.",
|
||||
"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.update_not_nsp": "The update must be an NSP file.",
|
||||
"check.update_is_base": "The update file holds a base game. Choose the 1.1.1 or 1.2.1 or 1.4.3 update NSP.",
|
||||
"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 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}.",
|
||||
"check.input_damaged": "The {input} file is damaged or incomplete: {path} ({detail}). Copy that file again from its source, or dump it again, then run the setup again.",
|
||||
"check.base_wrong_version": "The base game is not version 1.0.0 ({modules} differ).",
|
||||
"check.update_wrong_version": "The update is version {found}; this release takes {wanted}.",
|
||||
"check.disk_space": "Not enough free space for {path}: {need_gb} GB needed, {free_gb} GB free.",
|
||||
"check.firmware_missing": "The firmware folder {path} does not exist. Choose the firmware's Contents/registered folder.",
|
||||
"check.firmware_empty": "The firmware folder {path} holds no NCA files. Choose the firmware's Contents/registered folder.",
|
||||
"check.fonts_keys_needed": "The shared fonts for this language are not installed yet; choose prod.keys so the setup can extract them from the firmware.",
|
||||
|
||||
"extract.exefs_mismatch": "The extracted game code does not match version {version} ({modules}).",
|
||||
"extract.romfs_merge_failed": "Applying the update to the game data failed: {detail}. Details: {log}",
|
||||
"extract.romfs_mismatch": "The extracted game data does not match version {version} (size {size}, checksum {sha256}...).",
|
||||
"fonts.failed": "Extracting the shared fonts failed (exit code {code}): {detail}. Details: {log}",
|
||||
"fonts.incomplete": "The shared font extraction did not write every file ({files}).",
|
||||
"fonts.copy_failed": "The shared fonts cannot be copied into {path}: {detail}",
|
||||
"lift.failed": "Translating the game code failed (exit code {code}). Details: {log}",
|
||||
"compile.failed": "Compiling {game_exe} failed (exit code {code}). Details: {log}",
|
||||
"compile.exe_in_use": "{path} is in use. Close the game and run the installer again.",
|
||||
"prepare.xlate_in_use": "{path} is in use. Close the game, end any totk_xlate process left from an earlier run, then run the installer again.",
|
||||
"prepare.shader_list_missing": "The shader list is missing.",
|
||||
"prepare.romfs_patches_failed": "Writing the game data patches failed (exit code {code}). Details: {log}",
|
||||
"prepare.shaders_failed": "Preparing the shaders failed (exit code {code}). Details: {log}",
|
||||
@@ -81,28 +99,40 @@
|
||||
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
|
||||
"cleanup.ini_failed": "totk.ini cannot be written: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, which the installer does not extract; some text may not show.",
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, and none were given. The game will not start in this language until they are added. Choose the firmware folder (Contents/registered), or see \"Switch system fonts\" in docs/installer.md.",
|
||||
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
|
||||
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
|
||||
"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 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",
|
||||
"ui.start_game": "Start the game",
|
||||
"ui.version": "version {version}",
|
||||
"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.1 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",
|
||||
"ui.dest": "Install folder",
|
||||
"ui.browse": "Browse...",
|
||||
"ui.pick_game": "Choose the base game file",
|
||||
"ui.pick_update": "Choose the 1.1.1 or 1.2.1 or 1.4.3 update file",
|
||||
"ui.pick_update": "Choose the 1.1.0 or 1.2.1 or 1.4.3 update file",
|
||||
"ui.pick_keys": "Choose prod.keys",
|
||||
"ui.pick_firmware": "Choose the Switch firmware folder (Contents/registered)",
|
||||
"ui.pick_dest": "Choose the install folder",
|
||||
"ui.type_game": "Game files (NSP, XCI)",
|
||||
"ui.type_nsp": "NSP files",
|
||||
"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.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",
|
||||
@@ -116,12 +146,13 @@
|
||||
"ui.update_mode": "This folder holds an earlier installation. The setup continues or updates it; the game files are asked for only if they are needed again.",
|
||||
"ui.need_dest": "Choose the install folder.",
|
||||
"ui.need_base": "Choose the base game file.",
|
||||
"ui.need_update": "Choose the 1.1.1 or 1.2.1 or 1.4.3 update file.",
|
||||
"ui.need_update": "Choose the 1.1.0 or 1.2.1 or 1.4.3 update file.",
|
||||
"ui.need_keys": "Choose the prod.keys file.",
|
||||
"ui.need_lang": "Choose the game language.",
|
||||
"ui.missing_base": "The base game file does not exist.",
|
||||
"ui.missing_update": "The update file does not exist.",
|
||||
"ui.missing_keys": "The keys file does not exist.",
|
||||
"ui.missing_firmware": "The firmware folder does not exist.",
|
||||
"ui.running_install": "Installing",
|
||||
"ui.running_resume": "Continuing the earlier setup",
|
||||
"ui.running_update": "Updating the installation",
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"step.check": "Verificando o computador e os arquivos do jogo",
|
||||
"step.fonts": "Extraindo as fontes do sistema",
|
||||
"step.extract": "Extraindo o jogo",
|
||||
"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",
|
||||
"phase.extract.hash_base": "Calculando o checksum do jogo base",
|
||||
"phase.extract.hash_update": "Calculando o checksum da atualização",
|
||||
"phase.extract.exefs": "Extraindo o código do jogo",
|
||||
"phase.extract.copy": "Copiando os dados do jogo",
|
||||
"phase.extract.merge": "Aplicando a atualização aos dados do jogo",
|
||||
"phase.extract.diagnose": "Procurando o arquivo do jogo danificado",
|
||||
"phase.extract.index": "Verificando os dados do jogo",
|
||||
"phase.lift.discover": "Encontrando as funções",
|
||||
"phase.lift.emit": "Gerando o código C",
|
||||
"phase.compile.compile": "Compilando",
|
||||
"phase.compile.link": "Ligando o {game_exe}",
|
||||
"phase.prepare.romfs_patches": "Gravando os patches de 60 fps nos dados do jogo",
|
||||
"phase.prepare.seed": "Lendo os shaders dos dados do jogo",
|
||||
"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",
|
||||
|
||||
"cli.usage": "Linha de comando inválida: {detail}",
|
||||
"internal": "Erro inesperado ({exception}): {detail}",
|
||||
"state.unreadable": "Não foi possível ler {path}. Escolha outra pasta ou apague esse arquivo.",
|
||||
"state.newer_format": "{path} foi gravado por um instalador mais novo. Use esse instalador ou outra pasta.",
|
||||
"state.output_changed": "Os arquivos instalados da etapa '{step}' mudaram desde que foram criados; essa etapa será executada de novo.",
|
||||
|
||||
"check.release_incomplete": "A pasta do instalador está incompleta; faltam: {paths}. Extraia o pacote de novo.",
|
||||
"check.dest_not_folder": "O local de instalação {path} é um arquivo, não uma pasta.",
|
||||
"check.dest_not_empty": "A pasta de instalação {path} não está vazia e não tem uma instalação anterior. Escolha uma pasta vazia.",
|
||||
"check.lang_missing": "Escolha o idioma do jogo.",
|
||||
"check.lang_unknown": "Idioma desconhecido '{lang}'. Conhecidos: {known}.",
|
||||
"check.toolchain_broken": "O compilador incluído não funciona: {detail}",
|
||||
"check.glibc_old": "A biblioteca C deste sistema é a glibc {version}; o compilador da instalação precisa da {need} ou mais nova (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 e mais novos têm). Atualizar o sistema resolve.",
|
||||
"check.host_probe_failed": "A verificação do computador não rodou (código de saída {code}).",
|
||||
"check.cpu_too_old": "Este processador não tem x86-64-v2 (SSE4.2, POPCNT e CMPXCHG16B), o nível mais antigo para o qual o jogo é compilado. Intel Nehalem (2008) ou AMD Bulldozer (2011) e mais novos têm.",
|
||||
"check.vulkan_missing": "Nenhum driver Vulkan está instalado. Instale o driver mais recente da sua placa de vídeo.",
|
||||
"check.vulkan_no_device": "O Vulkan não encontra nenhuma placa de vídeo. Instale o driver mais recente da sua placa de vídeo.",
|
||||
"check.vulkan_too_old": "O jogo precisa de uma placa de vídeo com Vulkan 1.3; encontrado: {devices}. Atualize o driver de vídeo.",
|
||||
"check.no_display": "Preparar os shaders exige uma tela. Execute a instalação na sessão da área de trabalho ou use export DISPLAY=:0 (no Steam Deck em Game Mode, também XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "Os arquivos do jogo são necessários: escolha o jogo base, a atualização 1.1.0, 1.2.1 ou 1.4.3 e o prod.keys.",
|
||||
"check.input_missing": "Arquivo não encontrado ({input}): {path}",
|
||||
"check.keys_unreadable": "Não foi possível ler o arquivo de chaves. Escolha um arquivo prod.keys.",
|
||||
"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, 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 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}.",
|
||||
"check.input_damaged": "O arquivo de {input} está danificado ou incompleto: {path} ({detail}). Copie esse arquivo de novo da origem, ou extraia-o de novo, e então execute a instalação outra vez.",
|
||||
"check.base_wrong_version": "O jogo base não é a versão 1.0.0 ({modules} diferem).",
|
||||
"check.update_wrong_version": "A atualização é a versão {found}; esta versão do instalador aceita {wanted}.",
|
||||
"check.disk_space": "Espaço livre insuficiente em {path}: são necessários {need_gb} GB, há {free_gb} GB livres.",
|
||||
"check.firmware_missing": "A pasta de firmware {path} não existe. Escolha a pasta Contents/registered do firmware.",
|
||||
"check.firmware_empty": "A pasta de firmware {path} não contém arquivos NCA. Escolha a pasta Contents/registered do firmware.",
|
||||
"check.fonts_keys_needed": "As fontes compartilhadas deste idioma ainda não estão instaladas; escolha o prod.keys para que a instalação possa extraí-las do firmware.",
|
||||
|
||||
"extract.exefs_mismatch": "O código do jogo extraído não corresponde à versão {version} ({modules}).",
|
||||
"extract.romfs_merge_failed": "Falha ao aplicar a atualização aos dados do jogo: {detail}. Detalhes: {log}",
|
||||
"extract.romfs_mismatch": "Os dados do jogo extraídos não correspondem à versão {version} (tamanho {size}, checksum {sha256}...).",
|
||||
"fonts.failed": "A extração das fontes compartilhadas falhou (código de saída {code}): {detail}. Detalhes: {log}",
|
||||
"fonts.incomplete": "A extração das fontes compartilhadas não gravou todos os arquivos ({files}).",
|
||||
"fonts.copy_failed": "Não foi possível copiar as fontes compartilhadas para {path}: {detail}",
|
||||
"lift.failed": "Falha ao traduzir o código do jogo (código de saída {code}). Detalhes: {log}",
|
||||
"compile.failed": "Falha ao compilar o {game_exe} (código de saída {code}). Detalhes: {log}",
|
||||
"compile.exe_in_use": "{path} está em uso. Feche o jogo e execute o instalador de novo.",
|
||||
"prepare.xlate_in_use": "{path} está em uso. Feche o jogo, encerre qualquer processo totk_xlate que tenha sobrado de uma execução anterior e execute o instalador de novo.",
|
||||
"prepare.shader_list_missing": "A lista de shaders está faltando.",
|
||||
"prepare.romfs_patches_failed": "Falha ao gravar os patches nos dados do jogo (código de saída {code}). Detalhes: {log}",
|
||||
"prepare.shaders_failed": "Falha ao preparar os shaders (código de saída {code}). Detalhes: {log}",
|
||||
"cleanup.romfs_unreadable": "Os dados do jogo em {path} estão faltando ou não podem ser lidos depois da instalação. Reinicie o Windows e execute o instalador de novo na mesma pasta: ele extrai os dados do jogo outra vez e mantém o resto.",
|
||||
"prepare.pack_failed": "Falha ao juntar o cache de shaders (código de saída {code}). Detalhes: {log}",
|
||||
"cleanup.ini_failed": "Não foi possível gravar o totk.ini: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Textos em {lang} precisam das fontes de sistema do Switch, e nenhuma foi informada. O jogo não inicia neste idioma até que sejam adicionadas. Escolha a pasta do firmware (Contents/registered) ou veja \"Switch system fonts\" em docs/installer.md.",
|
||||
"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 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",
|
||||
"ui.start_game": "Iniciar o jogo",
|
||||
"ui.version": "versão {version}",
|
||||
"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, 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",
|
||||
"ui.dest": "Pasta de instalação",
|
||||
"ui.browse": "Procurar...",
|
||||
"ui.pick_game": "Escolha o arquivo do jogo base",
|
||||
"ui.pick_update": "Escolha o arquivo da atualização 1.1.0, 1.2.1 ou 1.4.3",
|
||||
"ui.pick_keys": "Escolha o prod.keys",
|
||||
"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.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",
|
||||
"ui.cancel": "Cancelar",
|
||||
"ui.cancelling": "Cancelando...",
|
||||
"ui.show_log": "Mostrar log",
|
||||
"ui.hide_log": "Ocultar log",
|
||||
"ui.close": "Fechar",
|
||||
"ui.back": "Voltar",
|
||||
"ui.open_folder": "Abrir pasta",
|
||||
"ui.update_mode": "Esta pasta tem uma instalação anterior. A instalação vai continuar ou atualizá-la; os arquivos do jogo só são pedidos se forem necessários de novo.",
|
||||
"ui.need_dest": "Escolha a pasta de instalação.",
|
||||
"ui.need_base": "Escolha o arquivo do jogo base.",
|
||||
"ui.need_update": "Escolha o arquivo da atualização 1.1.0, 1.2.1 ou 1.4.3.",
|
||||
"ui.need_keys": "Escolha o arquivo prod.keys.",
|
||||
"ui.need_lang": "Escolha o idioma do jogo.",
|
||||
"ui.missing_base": "O arquivo do jogo base não existe.",
|
||||
"ui.missing_update": "O arquivo de atualização não existe.",
|
||||
"ui.missing_keys": "O arquivo de chaves não existe.",
|
||||
"ui.missing_firmware": "A pasta de firmware não existe.",
|
||||
"ui.running_install": "Instalando",
|
||||
"ui.running_resume": "Continuando a instalação anterior",
|
||||
"ui.running_update": "Atualizando a instalação",
|
||||
"ui.state_started": "iniciada",
|
||||
"ui.state_done": "concluída",
|
||||
"ui.state_skipped": "pulada",
|
||||
"ui.state_pending": "aguardando",
|
||||
"ui.state_running": "em andamento",
|
||||
"ui.state_failed": "falhou",
|
||||
"ui.log_warning": "Aviso",
|
||||
"ui.log_error": "Erro",
|
||||
"ui.log_space": "Espaço necessário: {need} GB, livre: {free} GB",
|
||||
"ui.log_end": "Concluído",
|
||||
"ui.result_ok": "O jogo está pronto",
|
||||
"ui.result_failed": "A instalação falhou",
|
||||
"ui.result_cancelled": "A instalação foi cancelada",
|
||||
"ui.done_text": "Abra o {game_exe} em {dest}. O programa de instalação e os arquivos do jogo que você escolheu não são mais necessários.",
|
||||
"ui.cancelled_text": "As etapas concluídas são mantidas. Inicie a instalação de novo com a mesma pasta para continuar.",
|
||||
"ui.log_file": "Arquivo de log: {path}",
|
||||
"ui.process_died": "A instalação parou de forma inesperada (código de saída {code}).",
|
||||
"ui.confirm_cancel": "A instalação está em andamento. Cancelar e fechar a janela?",
|
||||
"ui.space_one": "Espaço livre necessário em {path}: {need} GB (livre: {free} GB).",
|
||||
"ui.space_two": "Espaço livre necessário em {path}: {need} GB (livre: {free} GB); em {temp}: {temp_need} GB (livre: {temp_free} GB).",
|
||||
"ui.amount_bytes": "{done} / {total} GB",
|
||||
"ui.amount_chunks": "{done} / {total} partes",
|
||||
"ui.amount_objects": "{done} / {total} arquivos",
|
||||
"ui.amount_shaders": "{done} / {total} shaders",
|
||||
"ui.amount_other": "{done} / {total} {unit}",
|
||||
"ui.seconds": "{n} s",
|
||||
"param.input.base": "jogo base",
|
||||
"param.input.update": "atualização",
|
||||
"param.input.keys": "chaves",
|
||||
|
||||
"launcher.title": "Instalação do TotK",
|
||||
"launcher.python_missing": "Os arquivos de instalação estão incompletos: a pasta python ao lado do TotK-Setup.exe não existe. Extraia o arquivo compactado inteiro de novo e abra o TotK-Setup.exe a partir da pasta extraída.",
|
||||
|
||||
"lang.ja": "Japonês",
|
||||
"lang.en-US": "Inglês (EUA)",
|
||||
"lang.en-GB": "Inglês (Reino Unido)",
|
||||
"lang.fr": "Francês",
|
||||
"lang.fr-CA": "Francês (Canadá)",
|
||||
"lang.de": "Alemão",
|
||||
"lang.it": "Italiano",
|
||||
"lang.es": "Espanhol",
|
||||
"lang.es-419": "Espanhol (América Latina)",
|
||||
"lang.nl": "Holandês",
|
||||
"lang.pt": "Português",
|
||||
"lang.pt-BR": "Português (Brasil)",
|
||||
"lang.ru": "Russo",
|
||||
"lang.ko": "Coreano",
|
||||
"lang.zh-Hans": "Chinês (Simplificado)",
|
||||
"lang.zh-Hant": "Chinês (Tradicional)"
|
||||
}
|
||||
+44
-13
@@ -1,16 +1,22 @@
|
||||
{
|
||||
"step.check": "Проверка компьютера и игровых файлов",
|
||||
"step.fonts": "Извлечение системных шрифтов",
|
||||
"step.extract": "Распаковка игры",
|
||||
"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": "Копирование общих шрифтов в папку установки",
|
||||
"phase.extract.hash_base": "Подсчёт контрольной суммы базовой игры",
|
||||
"phase.extract.hash_update": "Подсчёт контрольной суммы обновления",
|
||||
"phase.extract.exefs": "Распаковка кода игры",
|
||||
@@ -27,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": "Удаление временных файлов",
|
||||
|
||||
@@ -44,36 +51,47 @@
|
||||
"check.toolchain_broken": "Встроенный компилятор не работает: {detail}",
|
||||
"check.glibc_old": "Библиотека C этой системы — glibc {version}; компилятору установщика нужна {need} или новее (она есть в Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 и новее). Обновление системы это исправит.",
|
||||
"check.host_probe_failed": "Проверка компьютера не выполнилась (код выхода {code}).",
|
||||
"check.cpu_no_avx2": "Этому процессору не хватает AVX2 (x86-64-v3), который нужен игре. AVX2 есть у Intel Haswell (2013) и новее, у AMD Excavator и Zen и новее.",
|
||||
"check.cpu_too_old": "Этому процессору не хватает x86-64-v2 (SSE4.2, POPCNT и CMPXCHG16B) — минимального уровня, под который собрана игра. Он есть у Intel Nehalem (2008) и AMD Bulldozer (2011) и новее.",
|
||||
"check.vulkan_missing": "Драйвер Vulkan не установлен. Установите свежий драйвер видеокарты.",
|
||||
"check.vulkan_no_device": "Vulkan не находит видеокарту. Установите свежий драйвер видеокарты.",
|
||||
"check.vulkan_too_old": "Игре нужна видеокарта с Vulkan 1.3; найдено: {devices}. Обновите драйвер видеокарты.",
|
||||
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.1 или 1.2.1 или 1.4.3 и prod.keys.",
|
||||
"check.no_display": "Для подготовки шейдеров нужен дисплей. Запустите установку из сеанса рабочего стола или выполните export DISPLAY=:0 (на Steam Deck в игровом режиме ещё XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.0 или 1.2.1 или 1.4.3 и prod.keys.",
|
||||
"check.input_missing": "Файл не найден ({input}): {path}",
|
||||
"check.keys_unreadable": "Не удаётся прочитать файл ключей. Выберите файл prod.keys.",
|
||||
"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.update_not_nsp": "Обновление должно быть файлом NSP.",
|
||||
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.1.1 или 1.2.1 или 1.4.3.",
|
||||
"check.base_is_update": "Файл базовой игры — это обновление. Выберите здесь NSP или XCI базовой игры (версия 1.0.0), а этот файл укажите как обновление.",
|
||||
"check.base_combined": "Файл базовой игры содержит несколько пакетов (игра, обновление или DLC в одном файле). Выберите дамп, в котором только базовая игра.",
|
||||
"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}.",
|
||||
"check.input_damaged": "Файл «{input}» повреждён или скопирован не полностью: {path} ({detail}). Скопируйте этот файл заново из источника или сделайте дамп ещё раз, затем снова запустите установку.",
|
||||
"check.base_wrong_version": "Базовая игра не версии 1.0.0 (отличаются модули: {modules}).",
|
||||
"check.update_wrong_version": "Версия обновления {found}; этот выпуск принимает {wanted}.",
|
||||
"check.disk_space": "Не хватает свободного места для {path}: нужно {need_gb} ГБ, свободно {free_gb} ГБ.",
|
||||
"check.firmware_missing": "Папки прошивки {path} нет. Выберите папку Contents/registered прошивки.",
|
||||
"check.firmware_empty": "В папке прошивки {path} нет файлов NCA. Выберите папку Contents/registered прошивки.",
|
||||
"check.fonts_keys_needed": "Общие шрифты для этого языка ещё не установлены; выберите prod.keys, чтобы установка извлекла их из прошивки.",
|
||||
|
||||
"extract.exefs_mismatch": "Распакованный код игры не соответствует версии {version} ({modules}).",
|
||||
"extract.romfs_merge_failed": "Не удалось применить обновление к данным игры: {detail}. Подробности: {log}",
|
||||
"extract.romfs_mismatch": "Распакованные данные игры не соответствуют версии {version} (размер {size}, контрольная сумма {sha256}...).",
|
||||
"fonts.failed": "Не удалось извлечь общие шрифты (код выхода {code}): {detail}. Подробности: {log}",
|
||||
"fonts.incomplete": "При извлечении общих шрифтов записаны не все файлы ({files}).",
|
||||
"fonts.copy_failed": "Не удаётся скопировать общие шрифты в {path}: {detail}",
|
||||
"lift.failed": "Не удалось перевести код игры (код выхода {code}). Подробности: {log}",
|
||||
"compile.failed": "Не удалось собрать {game_exe} (код выхода {code}). Подробности: {log}",
|
||||
"compile.exe_in_use": "Файл {path} занят. Закройте игру и запустите установку снова.",
|
||||
"prepare.xlate_in_use": "Файл {path} занят. Закройте игру, завершите процесс totk_xlate, оставшийся от прошлого запуска, и запустите установку снова.",
|
||||
"prepare.shader_list_missing": "Нет списка шейдеров.",
|
||||
"prepare.romfs_patches_failed": "Не удалось записать патчи данных игры (код выхода {code}). Подробности: {log}",
|
||||
"prepare.shaders_failed": "Не удалось подготовить шейдеры (код выхода {code}). Подробности: {log}",
|
||||
@@ -81,28 +99,40 @@
|
||||
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
|
||||
"cleanup.ini_failed": "Не удаётся записать totk.ini: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, которые установка не извлекает; часть текста может не отображаться.",
|
||||
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, а они не указаны. Игра не запустится на этом языке, пока их не добавят. Выберите папку прошивки (Contents/registered) или см. раздел «Switch system fonts» в docs/installer.md.",
|
||||
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
|
||||
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
|
||||
"warn.cheats_skipped": "Не удалось собрать читы из папки модов, меняющие код игры; игра установится без них. Подробности: {log}",
|
||||
"warn.exefs_skipped": "Не удалось собрать ExeFS-патчи модов (.ips, .pchtxt); игра установится без них. Подробности: {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": "Пересборка игры под ваши моды",
|
||||
"ui.start_game": "Запустить игру",
|
||||
"ui.version": "версия {version}",
|
||||
"ui.intro": "Собирает готовую к запуску папку с Tears of the Kingdom из ваших игровых файлов. Занимает много времени и выполняется один раз.",
|
||||
"ui.notice": "Папка, которую создаёт установка, содержит игру. Она предназначена только для личного использования: не передавайте и не выкладывайте её.",
|
||||
"ui.keys_private": "Файл ключей читается на месте. Его содержимое не показывается, не копируется и не сохраняется.",
|
||||
"ui.base": "Базовая игра (NSP или XCI)",
|
||||
"ui.update": "Обновление 1.1.1 или 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": "Язык игры",
|
||||
"ui.dest": "Папка установки",
|
||||
"ui.browse": "Обзор...",
|
||||
"ui.pick_game": "Выберите файл базовой игры",
|
||||
"ui.pick_update": "Выберите файл обновления 1.1.1 или 1.2.1 или 1.4.3",
|
||||
"ui.pick_update": "Выберите файл обновления 1.1.0 или 1.2.1 или 1.4.3",
|
||||
"ui.pick_keys": "Выберите prod.keys",
|
||||
"ui.pick_firmware": "Выберите папку прошивки Switch (Contents/registered)",
|
||||
"ui.pick_dest": "Выберите папку установки",
|
||||
"ui.type_game": "Файлы игры (NSP, XCI)",
|
||||
"ui.type_nsp": "Файлы NSP",
|
||||
"ui.firmware_help": "Для текста на китайском и корейском: папка Contents/registered прошивки, например из Ryujinx или Yuzu. Установка скопирует из неё системные шрифты Switch в папку установки.",
|
||||
"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": "Начать",
|
||||
@@ -116,12 +146,13 @@
|
||||
"ui.update_mode": "В этой папке уже есть установка. Программа продолжит или обновит её; файлы игры понадобятся, только если их снова придётся читать.",
|
||||
"ui.need_dest": "Выберите папку установки.",
|
||||
"ui.need_base": "Выберите файл базовой игры.",
|
||||
"ui.need_update": "Выберите файл обновления 1.1.1 или 1.2.1 или 1.4.3.",
|
||||
"ui.need_update": "Выберите файл обновления 1.1.0 или 1.2.1 или 1.4.3.",
|
||||
"ui.need_keys": "Выберите файл prod.keys.",
|
||||
"ui.need_lang": "Выберите язык игры.",
|
||||
"ui.missing_base": "Файл базовой игры не найден.",
|
||||
"ui.missing_update": "Файл обновления не найден.",
|
||||
"ui.missing_keys": "Файл ключей не найден.",
|
||||
"ui.missing_firmware": "Папки прошивки нет.",
|
||||
"ui.running_install": "Установка",
|
||||
"ui.running_resume": "Продолжение прерванной установки",
|
||||
"ui.running_update": "Обновление установки",
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"step.check": "检查电脑和游戏文件",
|
||||
"step.fonts": "正在提取系统字体",
|
||||
"step.extract": "解包游戏",
|
||||
"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": "正在将共享字体复制到安装文件夹",
|
||||
"phase.extract.hash_base": "计算游戏本体的校验和",
|
||||
"phase.extract.hash_update": "计算更新的校验和",
|
||||
"phase.extract.exefs": "解包游戏代码",
|
||||
"phase.extract.copy": "复制游戏数据",
|
||||
"phase.extract.merge": "将更新应用到游戏数据",
|
||||
"phase.extract.diagnose": "查找损坏的游戏文件",
|
||||
"phase.extract.index": "校验游戏数据",
|
||||
"phase.lift.discover": "查找函数",
|
||||
"phase.lift.emit": "生成 C 代码",
|
||||
"phase.compile.compile": "编译",
|
||||
"phase.compile.link": "链接 {game_exe}",
|
||||
"phase.prepare.romfs_patches": "写入 60 帧游戏数据补丁",
|
||||
"phase.prepare.seed": "从游戏数据中读取着色器",
|
||||
"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": "删除临时文件",
|
||||
|
||||
"cli.usage": "命令行无效:{detail}",
|
||||
"internal": "意外错误({exception}):{detail}",
|
||||
"state.unreadable": "无法读取 {path}。请选择其他文件夹,或删除该文件。",
|
||||
"state.newer_format": "{path} 是由更新版本的安装程序写入的。请使用那个安装程序,或选择其他文件夹。",
|
||||
"state.output_changed": "步骤“{step}”生成的已安装文件在生成后被改动过;该步骤将重新运行。",
|
||||
|
||||
"check.release_incomplete": "安装程序文件夹不完整,缺少:{paths}。请重新解压发布包。",
|
||||
"check.dest_not_folder": "安装位置 {path} 是一个文件,而不是文件夹。",
|
||||
"check.dest_not_empty": "安装文件夹 {path} 不是空的,其中也没有之前的安装。请选择一个空文件夹。",
|
||||
"check.lang_missing": "请选择游戏语言。",
|
||||
"check.lang_unknown": "未知语言“{lang}”。可用语言:{known}。",
|
||||
"check.toolchain_broken": "自带的编译器无法工作:{detail}",
|
||||
"check.glibc_old": "此系统的 C 库为 glibc {version};安装程序的编译器需要 {need} 或更新版本(Ubuntu 22.04、Debian 12、Fedora 35、SteamOS 3 及更新版本都满足)。更新系统即可解决。",
|
||||
"check.host_probe_failed": "电脑检查未能运行(退出代码 {code})。",
|
||||
"check.cpu_too_old": "此处理器不支持 x86-64-v2(SSE4.2、POPCNT 和 CMPXCHG16B),这是游戏所需的最低指令集。Intel Nehalem(2008 年)或 AMD Bulldozer(2011 年)及更新的处理器都支持。",
|
||||
"check.vulkan_missing": "未安装 Vulkan 驱动。请为你的显卡安装最新的驱动程序。",
|
||||
"check.vulkan_no_device": "Vulkan 找不到显卡。请为你的显卡安装最新的驱动程序。",
|
||||
"check.vulkan_too_old": "游戏需要支持 Vulkan 1.3 的显卡;检测到:{devices}。请更新显卡驱动程序。",
|
||||
"check.no_display": "准备着色器需要显示器。请在桌面会话中运行安装程序,或执行 export DISPLAY=:0(Steam Deck 游戏模式下还需 XDG_RUNTIME_DIR=/run/user/1000)。",
|
||||
"check.inputs_needed": "需要游戏文件:请选择游戏本体、1.1.0、1.2.1 或 1.4.3 更新以及 prod.keys。",
|
||||
"check.input_missing": "找不到文件({input}):{path}",
|
||||
"check.keys_unreadable": "无法读取密钥文件。请选择一个 prod.keys 文件。",
|
||||
"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、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 或 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}。",
|
||||
"check.input_damaged": "{input}文件已损坏或不完整:{path}({detail})。请从来源重新复制该文件或重新导出,然后再次运行安装程序。",
|
||||
"check.base_wrong_version": "游戏本体不是 1.0.0 版本({modules} 不一致)。",
|
||||
"check.update_wrong_version": "更新的版本是 {found};此发布版需要 {wanted}。",
|
||||
"check.disk_space": "{path} 的可用空间不足:需要 {need_gb} GB,可用 {free_gb} GB。",
|
||||
"check.firmware_missing": "固件文件夹 {path} 不存在。请选择固件的 Contents/registered 文件夹。",
|
||||
"check.firmware_empty": "固件文件夹 {path} 中没有 NCA 文件。请选择固件的 Contents/registered 文件夹。",
|
||||
"check.fonts_keys_needed": "此语言的共享字体尚未安装;请选择 prod.keys,以便安装程序从固件中提取它们。",
|
||||
|
||||
"extract.exefs_mismatch": "解包出的游戏代码与 {version} 版本不符({modules})。",
|
||||
"extract.romfs_merge_failed": "将更新应用到游戏数据失败:{detail}。详情:{log}",
|
||||
"extract.romfs_mismatch": "解包出的游戏数据与 {version} 版本不符(大小 {size},校验和 {sha256}...)。",
|
||||
"fonts.failed": "提取共享字体失败(退出代码 {code}):{detail}。详情:{log}",
|
||||
"fonts.incomplete": "共享字体提取没有写出全部文件({files})。",
|
||||
"fonts.copy_failed": "无法将共享字体复制到 {path}:{detail}",
|
||||
"lift.failed": "游戏代码转换失败(退出代码 {code})。详情:{log}",
|
||||
"compile.failed": "编译 {game_exe} 失败(退出代码 {code})。详情:{log}",
|
||||
"compile.exe_in_use": "{path} 正在使用中。请关闭游戏后重新运行安装程序。",
|
||||
"prepare.xlate_in_use": "{path} 正在被占用。请关闭游戏,结束上次残留的 totk_xlate 进程,再重新运行安装程序。",
|
||||
"prepare.shader_list_missing": "缺少着色器列表。",
|
||||
"prepare.romfs_patches_failed": "写入游戏数据补丁失败(退出代码 {code})。详情:{log}",
|
||||
"prepare.shaders_failed": "准备着色器失败(退出代码 {code})。详情:{log}",
|
||||
"cleanup.romfs_unreadable": "安装完成后,{path} 中的游戏数据缺失或无法读取。请重启 Windows,然后对同一文件夹再次运行安装程序:它会重新提取游戏数据,其余内容保留。",
|
||||
"prepare.pack_failed": "打包着色器缓存失败(退出代码 {code})。详情:{log}",
|
||||
"cleanup.ini_failed": "无法写入 totk.ini:{detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "{lang}文本需要 Switch 系统字体,但未提供。在添加这些字体之前,游戏无法以该语言启动。请选择固件文件夹(Contents/registered),或参阅 docs/installer.md 中的“Switch system fonts”一节。",
|
||||
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
|
||||
"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": "正在为你的模组重新构建游戏",
|
||||
"ui.start_game": "启动游戏",
|
||||
"ui.version": "版本 {version}",
|
||||
"ui.intro": "用你自己的游戏文件构建一个可以游玩的《王国之泪》文件夹。耗时较长,只需运行一次。",
|
||||
"ui.notice": "此安装程序构建的文件夹包含游戏本身,仅供你个人使用;请勿分享或上传。",
|
||||
"ui.keys_private": "密钥文件只在原位置读取,其内容绝不会被显示、复制或保存。",
|
||||
"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": "游戏语言",
|
||||
"ui.dest": "安装文件夹",
|
||||
"ui.browse": "浏览...",
|
||||
"ui.pick_game": "选择游戏本体文件",
|
||||
"ui.pick_update": "选择 1.1.0、1.2.1 或 1.4.3 更新文件",
|
||||
"ui.pick_keys": "选择 prod.keys",
|
||||
"ui.pick_firmware": "选择 Switch 固件文件夹(Contents/registered)",
|
||||
"ui.pick_dest": "选择安装文件夹",
|
||||
"ui.firmware_help": "用于中文、韩文和繁体中文文本:固件的 Contents/registered 文件夹,例如来自 Ryujinx 或 Yuzu。安装程序会从中把 Switch 系统字体复制到安装文件夹。",
|
||||
"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": "开始",
|
||||
"ui.cancel": "取消",
|
||||
"ui.cancelling": "正在取消...",
|
||||
"ui.show_log": "显示日志",
|
||||
"ui.hide_log": "隐藏日志",
|
||||
"ui.close": "关闭",
|
||||
"ui.back": "返回",
|
||||
"ui.open_folder": "打开文件夹",
|
||||
"ui.update_mode": "此文件夹中已有之前的安装。安装程序会继续或更新它;只有在需要时才会再次要求提供游戏文件。",
|
||||
"ui.need_dest": "请选择安装文件夹。",
|
||||
"ui.need_base": "请选择游戏本体文件。",
|
||||
"ui.need_update": "请选择 1.1.0、1.2.1 或 1.4.3 更新文件。",
|
||||
"ui.need_keys": "请选择 prod.keys 文件。",
|
||||
"ui.need_lang": "请选择游戏语言。",
|
||||
"ui.missing_base": "游戏本体文件不存在。",
|
||||
"ui.missing_update": "更新文件不存在。",
|
||||
"ui.missing_keys": "密钥文件不存在。",
|
||||
"ui.missing_firmware": "固件文件夹不存在。",
|
||||
"ui.running_install": "正在安装",
|
||||
"ui.running_resume": "正在继续之前的安装",
|
||||
"ui.running_update": "正在更新安装",
|
||||
"ui.state_started": "已开始",
|
||||
"ui.state_done": "完成",
|
||||
"ui.state_skipped": "已跳过",
|
||||
"ui.state_pending": "等待中",
|
||||
"ui.state_running": "进行中",
|
||||
"ui.state_failed": "失败",
|
||||
"ui.log_warning": "警告",
|
||||
"ui.log_error": "错误",
|
||||
"ui.log_space": "所需空间:{need} GB,可用:{free} GB",
|
||||
"ui.log_end": "已结束",
|
||||
"ui.result_ok": "游戏已准备就绪",
|
||||
"ui.result_failed": "安装失败",
|
||||
"ui.result_cancelled": "安装已取消",
|
||||
"ui.done_text": "从 {dest} 启动 {game_exe}。安装程序和你选择的游戏文件都不再需要了。",
|
||||
"ui.cancelled_text": "已完成的步骤会保留。用同一个文件夹再次运行安装程序即可继续。",
|
||||
"ui.log_file": "日志文件:{path}",
|
||||
"ui.process_died": "安装程序意外停止(退出代码 {code})。",
|
||||
"ui.confirm_cancel": "安装正在进行。要取消安装并关闭窗口吗?",
|
||||
"ui.space_one": "{path} 需要的可用空间:{need} GB(可用:{free} GB)。",
|
||||
"ui.space_two": "{path} 需要的可用空间:{need} GB(可用:{free} GB);{temp} 需要:{temp_need} GB(可用:{temp_free} GB)。",
|
||||
"ui.amount_bytes": "{done} / {total} GB",
|
||||
"ui.amount_chunks": "{done} / {total} 部分",
|
||||
"ui.amount_objects": "{done} / {total} 个文件",
|
||||
"ui.amount_shaders": "{done} / {total} 个着色器",
|
||||
"ui.amount_other": "{done} / {total} {unit}",
|
||||
"ui.seconds": "{n} 秒",
|
||||
"param.input.base": "游戏本体",
|
||||
"param.input.update": "更新",
|
||||
"param.input.keys": "密钥",
|
||||
|
||||
"launcher.title": "TotK 安装程序",
|
||||
"launcher.python_missing": "安装文件不完整:TotK-Setup.exe 旁边缺少 python 文件夹。请重新解压整个压缩包,然后从解压出的文件夹中启动 TotK-Setup.exe。",
|
||||
|
||||
"lang.ja": "日语",
|
||||
"lang.en-US": "英语(美国)",
|
||||
"lang.en-GB": "英语(英国)",
|
||||
"lang.fr": "法语",
|
||||
"lang.fr-CA": "法语(加拿大)",
|
||||
"lang.de": "德语",
|
||||
"lang.it": "意大利语",
|
||||
"lang.es": "西班牙语",
|
||||
"lang.es-419": "西班牙语(拉丁美洲)",
|
||||
"lang.nl": "荷兰语",
|
||||
"lang.pt": "葡萄牙语",
|
||||
"lang.pt-BR": "葡萄牙语(巴西)",
|
||||
"lang.ru": "俄语",
|
||||
"lang.ko": "韩语",
|
||||
"lang.zh-Hans": "中文(简体)",
|
||||
"lang.zh-Hant": "中文(繁体)"
|
||||
}
|
||||
+155
-53
@@ -9,6 +9,9 @@ Output, under --out:
|
||||
<mod>/<mod>_NNNN.c one `void f_<addr>(Cpu *c)` per guest function, about --chunk-bytes of C per file
|
||||
<mod>/<mod>_table.c sorted <mod>_fn_addrs[] / <mod>_fn_ptrs[] / <mod>_fn_count for rt_dispatch
|
||||
<mod>/manifest.json chunk list, function count, statistics and timings
|
||||
<mod>/folded.txt the module's bytes the generated code took as constants: what the jump tables read at
|
||||
lift time, as module offsets [start, end); tools/exefs_patches.py keeps a mod's data
|
||||
writes off them
|
||||
patches.h code patch sets for the runtime (recomp/patches.py; no sets without --patches)
|
||||
|
||||
Function discovery runs in this process, vectorised over the module's .text words. Function starts are the module
|
||||
@@ -211,7 +214,7 @@ class Emitter:
|
||||
def __init__(self, mod, starts, patched=None, reg_locals=regs.LOCALS, callee_saved=True, spins=None):
|
||||
self.mod = mod
|
||||
self.starts = starts
|
||||
self.patched = patched or {} # {address: (set index, [replacement words])} (recomp/patches.py)
|
||||
self.patched = patched or {} # {address: [(set index, [replacement words])]} (recomp/patches.py)
|
||||
self.spins = spins or {} # {spin wait back-edge: its head} (recomp/patches.py `spin`)
|
||||
self.reg_locals = reg_locals # recomp/regs.py: registers per function in C locals
|
||||
self.kept = regs.CALLEE_SAVED if callee_saved else frozenset() # not reloaded after a call
|
||||
@@ -250,33 +253,54 @@ class Emitter:
|
||||
self.unsupported[re.sub(r'\b[0-9a-f]{8}\b', '', msg).strip()] += 1
|
||||
elif k == 'asm':
|
||||
raise ValueError(f'{entry + 4 * i:#x}: asm instruction from the portable lifter')
|
||||
for a in self.patched: # a replacement's branch into the function needs its label
|
||||
if entry <= a < end:
|
||||
for words in self.replacements(a):
|
||||
r = words[-1]
|
||||
if r.kind in ('b', 'bcond') and entry <= r.target < end:
|
||||
labels.add(r.target)
|
||||
return labels, tables
|
||||
|
||||
def replacements(self, a):
|
||||
"""Lifted replacement words of the patched site a."""
|
||||
return [self.mod.lifter.lift(w, a) for w in self.patched[a][1]]
|
||||
"""The lifted words of each alternative of the patched site a, in set order. Only the last word of an
|
||||
alternative may branch (bl, b, b.cond / cbz / tbz, ret)."""
|
||||
out = []
|
||||
for _, words in self.patched[a]:
|
||||
lifted = [self.mod.lifter.lift(w, a) for w in words]
|
||||
for j, r in enumerate(lifted):
|
||||
last = j == len(lifted) - 1
|
||||
if r.kind != 'op' and not (last and r.kind in ('bl', 'b', 'bcond', 'ret')):
|
||||
raise patches.PatchError(f'{a:#x}: replacement word {words[j]:08x} is a {r.kind}')
|
||||
out.append(lifted)
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def flow(insns, entry, end, tables):
|
||||
def flow(self, insns, entry, end, tables):
|
||||
"""Successors of each instruction inside the function [entry, end). Index len(insns) stands for falling
|
||||
through into the next function and has none."""
|
||||
through into the next function and has none. A patched site has the successors of all its alternatives."""
|
||||
def idx(t):
|
||||
return [(t - entry) // 4] if entry <= t < end else []
|
||||
|
||||
succ = []
|
||||
for i, ins in enumerate(insns):
|
||||
def of(ins, i):
|
||||
k = ins.kind
|
||||
if k in ('op', 'bl', 'blr'):
|
||||
succ.append([i + 1])
|
||||
elif k == 'b':
|
||||
succ.append(idx(ins.target))
|
||||
elif k == 'bcond':
|
||||
succ.append(idx(ins.target) + [i + 1])
|
||||
elif k == 'br':
|
||||
return [i + 1]
|
||||
if k == 'b':
|
||||
return idx(ins.target)
|
||||
if k == 'bcond':
|
||||
return idx(ins.target) + [i + 1]
|
||||
if k == 'br':
|
||||
jt = tables.get(entry + 4 * i)
|
||||
succ.append([t for x in jt for t in idx(x)] if jt and jt != 'all' else [])
|
||||
else: # ret, trap
|
||||
succ.append([])
|
||||
return [t for x in jt for t in idx(x)] if jt and jt != 'all' else []
|
||||
return [] # ret, trap
|
||||
|
||||
succ = []
|
||||
for i, ins in enumerate(insns):
|
||||
s = of(ins, i)
|
||||
a = entry + 4 * i
|
||||
if a in self.patched:
|
||||
for words in self.replacements(a):
|
||||
s += [t for t in of(words[-1], i) if t not in s]
|
||||
succ.append(s)
|
||||
succ.append([])
|
||||
return succ
|
||||
|
||||
@@ -294,27 +318,34 @@ class Emitter:
|
||||
exits.add(i) # through rt_dispatch, also a table's default
|
||||
if a in self.patched:
|
||||
effects.append(self.patched_effect(ins, a))
|
||||
if any(r.kind == 'bl' for r in self.replacements(a)):
|
||||
calls.add(i)
|
||||
for words in self.replacements(a):
|
||||
r = words[-1]
|
||||
if r.kind == 'bl':
|
||||
calls.add(i)
|
||||
elif r.kind in ('b', 'bcond') and not entry <= r.target < end:
|
||||
exits.add(i)
|
||||
else:
|
||||
effects.append(flags.effect(ins.cond if k == 'bcond' else ins.code or ''))
|
||||
effects.append((False, None))
|
||||
return flags.plan(effects, succ, calls, exits)
|
||||
|
||||
def patched_effect(self, ins, a):
|
||||
"""Flag effect of a patched site: both alternatives must have the same one, in at most one word each."""
|
||||
"""Flag effect of a patched site: every alternative (the original and each replacement) must write the same
|
||||
flags, in at most one word each; the site reads flags when any alternative does (a b.cond reads them)."""
|
||||
alts = []
|
||||
for words in ([ins], self.replacements(a)):
|
||||
fx = [flags.effect(w.code or '') for w in words if w.kind == 'op']
|
||||
for words in [[ins]] + self.replacements(a):
|
||||
fx = [flags.effect(w.code or '') if w.kind == 'op' else flags.effect(w.cond) for w in words
|
||||
if w.kind in ('op', 'bcond')]
|
||||
fx = [e for e in fx if e[0] or e[1]]
|
||||
if len(fx) > 1:
|
||||
raise patches.PatchError(f'{a:#x}: several flag-using words in one alternative')
|
||||
alts.append(fx[0] if fx else (False, None))
|
||||
if alts[0][1] != alts[1][1]:
|
||||
raise patches.PatchError(f'{a:#x}: the alternatives write different flags ({alts[0][1]}, {alts[1][1]})')
|
||||
if (alts[0][1] or alts[0][0] or alts[1][0]) and ins.kind == 'bl':
|
||||
if any(e[1] != alts[0][1] for e in alts):
|
||||
raise patches.PatchError(f'{a:#x}: the alternatives write different flags ({[e[1] for e in alts]})')
|
||||
reads = any(e[0] for e in alts)
|
||||
if (alts[0][1] or reads) and (ins.kind == 'bl' or any(w[-1].kind == 'bl' for w in self.replacements(a))):
|
||||
raise patches.PatchError(f'{a:#x}: a patched call that uses flags')
|
||||
return alts[0][0] or alts[1][0], alts[0][1]
|
||||
return reads, alts[0][1]
|
||||
|
||||
def function(self, entry, end):
|
||||
"""(lines of C, referenced function starts) for the guest function [entry, end)."""
|
||||
@@ -385,23 +416,30 @@ class Emitter:
|
||||
code = fx(i, code)
|
||||
return f'{spill}{code}{reload}' if regs.ESCAPE.search(code) else code
|
||||
|
||||
def one(i, r, a):
|
||||
"""C of one lifted word of a patched site."""
|
||||
if r.kind == 'op':
|
||||
return op(i, r.code)
|
||||
if r.kind == 'bl':
|
||||
return call(r.target, a)
|
||||
if r.kind == 'b':
|
||||
return jump(r.target, i)
|
||||
if r.kind == 'bcond':
|
||||
return f'if ({fx(i, r.cond)}) {{ {jump(r.target, i)} }}'
|
||||
if r.kind == 'ret':
|
||||
return f'{spill}return;'
|
||||
raise patches.PatchError(f'{a:#x}: patched instruction is a {r.kind}')
|
||||
|
||||
def patch(i, ins, a):
|
||||
"""if (switch) { replacement } else { original } for a patched site (recomp/patches.py)."""
|
||||
k, words = self.patched[a]
|
||||
if ins.kind not in ('op', 'bl'):
|
||||
"""if (switch) { replacement } else if ... else { original } for a patched site (recomp/patches.py)."""
|
||||
if ins.kind not in ('op', 'bl', 'b', 'bcond'):
|
||||
raise patches.PatchError(f'{a:#x}: patched instruction is a {ins.kind}')
|
||||
repl = []
|
||||
for j, (w, r) in enumerate(zip(words, self.replacements(a))):
|
||||
if r.kind == 'op':
|
||||
repl.append(op(i, r.code))
|
||||
elif r.kind == 'bl' and j == len(words) - 1:
|
||||
repl.append(call(r.target, a))
|
||||
else:
|
||||
raise patches.PatchError(f'{a:#x}: replacement word {w:08x} is a {r.kind}')
|
||||
orig = op(i, ins.code) if ins.kind == 'op' else call(ins.target, a)
|
||||
line = ' '
|
||||
for (k, _), words in zip(self.patched[a], self.replacements(a)):
|
||||
line += f' if (rt_patch_on[{k}]) {{ {" ".join(one(i, r, a) for r in words)} }} else'
|
||||
self.uses_patches = True
|
||||
self.stats['patched_sites'] += 1
|
||||
return f' if (rt_patch_on[{k}]) {{ {" ".join(repl)} }} else {{ {orig} }}'
|
||||
return f'{line} {{ {one(i, ins, a)} }}'
|
||||
|
||||
body = [] # (instruction index or None for a label, line); a line belongs to the instruction it lifts
|
||||
|
||||
@@ -445,7 +483,7 @@ class Emitter:
|
||||
else:
|
||||
raise ValueError(f'{a:#x}: unknown instruction kind {k!r}')
|
||||
n = len(insns)
|
||||
if k not in TERMINATORS: # falls through into the next function
|
||||
if k not in TERMINATORS or end - 4 in self.patched: # falls through into the next function
|
||||
if end < mod.hi:
|
||||
emit(n, ' ' + jump(end, n))
|
||||
else:
|
||||
@@ -520,15 +558,18 @@ def _retry(fn, *args):
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ workers
|
||||
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter},
|
||||
# 'reads' {name: [(start, end)]} module bytes read since the last chunk
|
||||
|
||||
|
||||
def _worker_init(mods, starts, table, opts):
|
||||
def _worker_init(mods, starts, table, opts, jobs):
|
||||
_W['jobs'] = jobs
|
||||
_W['mods'] = mods
|
||||
_W['starts'] = starts
|
||||
_W['patches'] = table
|
||||
_W['opts'] = opts # Emitter keyword arguments
|
||||
_W['emitters'] = {}
|
||||
_W['reads'] = {}
|
||||
|
||||
|
||||
def _emitter(name):
|
||||
@@ -536,16 +577,39 @@ def _emitter(name):
|
||||
if e is None:
|
||||
path, base = _W['mods'][name]
|
||||
mod = Module(name, path, base, 'c')
|
||||
# Module.read is how the jump tables (recomp/jumptables.py _eval) read .text and .rodata at lift time: the
|
||||
# bytes it returns become part of the generated code, so <mod>/folded.txt lists them
|
||||
read, reads = mod.read, _W['reads'].setdefault(name, [])
|
||||
|
||||
def recorded(a, n, signed=False):
|
||||
reads.append((a - base, a - base + n))
|
||||
return read(a, n, signed=signed)
|
||||
mod.read = recorded
|
||||
e = _W['emitters'][name] = Emitter(mod, set(_W['starts'][name].tolist()),
|
||||
_W['patches'].for_module(name, base),
|
||||
spins=_W['patches'].spins_for(name, base), **_W['opts'])
|
||||
return e
|
||||
|
||||
|
||||
def merge_ranges(ranges):
|
||||
"""Sorted [start, end) ranges with overlapping and touching ones joined."""
|
||||
out = []
|
||||
for s, e in sorted(ranges):
|
||||
if out and s <= out[-1][1]:
|
||||
out[-1][1] = max(out[-1][1], e)
|
||||
else:
|
||||
out.append([s, e])
|
||||
return out
|
||||
|
||||
|
||||
def _emit_chunk(task):
|
||||
"""Worker: write one chunk. Returns its statistics."""
|
||||
name, number, path, fns = task
|
||||
t0 = time.perf_counter()
|
||||
# Test hook: a pool wider than this raises the MemoryError a busy machine does, so the retry can be exercised.
|
||||
limit = os.environ.get('TOTK_LIFT_TEST_OOM_ABOVE')
|
||||
if limit and _W['jobs'] > int(limit) and number % 2 == 1:
|
||||
raise MemoryError
|
||||
e = _emitter(name)
|
||||
e.stats.clear()
|
||||
e.unsupported.clear()
|
||||
@@ -555,9 +619,12 @@ def _emit_chunk(task):
|
||||
written = write_if_changed(Path(path), text)
|
||||
except OSError as ex:
|
||||
written, error = False, f'{path}: {ex}'
|
||||
reads = _W['reads'].get(name, [])
|
||||
folded = merge_ranges(reads)
|
||||
reads.clear()
|
||||
return {'module': name, 'number': number, 'file': Path(path).name, 'functions': len(fns), 'bytes': len(text),
|
||||
'written': written, 'error': error, 'seconds': time.perf_counter() - t0, 'stats': dict(e.stats),
|
||||
'unsupported': dict(e.unsupported)}
|
||||
'unsupported': dict(e.unsupported), 'folded': folded}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ driver
|
||||
@@ -573,6 +640,14 @@ def table_text(name, addrs):
|
||||
return ''.join(lines)
|
||||
|
||||
|
||||
def folded_text(name, ranges):
|
||||
"""<mod>/folded.txt: the module offsets [start, end) of the bytes the jump tables read at lift time, in hex."""
|
||||
lines = [f'# {GENERATED_MARKER}: {name} bytes the generated code took as constants (jump tables), module offsets '
|
||||
f'[start, end), generated by recomp/gen.py from game code. Do not edit or commit.']
|
||||
lines += [f'{s:x} {e:x}' for s, e in ranges]
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def remove_stale(moddir, name, nchunks, errors):
|
||||
"""Delete chunks an earlier run wrote beyond this run's count, and temporary files an interrupted run left."""
|
||||
pat = re.compile(re.escape(name) + r'_(\d{4})\.c')
|
||||
@@ -597,6 +672,14 @@ def pick_jobs(requested):
|
||||
"""Worker count and the reason for it: --jobs if given, else the CPU count capped by the free memory."""
|
||||
if requested:
|
||||
return requested, '--jobs'
|
||||
env = os.environ.get('TOTK_LIFT_JOBS', '').strip()
|
||||
if env:
|
||||
try:
|
||||
if int(env) >= 1:
|
||||
return int(env), 'TOTK_LIFT_JOBS'
|
||||
except ValueError:
|
||||
pass
|
||||
print(f'ignoring TOTK_LIFT_JOBS={env!r}: not a positive integer', file=sys.stderr, flush=True)
|
||||
cpus = os.cpu_count() or 4
|
||||
free = hostmem.available_ram()
|
||||
if free is None:
|
||||
@@ -666,20 +749,38 @@ def main():
|
||||
# Largest chunks first, so the pool does not end on a long tail.
|
||||
tasks.sort(key=lambda t: -t[0])
|
||||
results = {name: [] for name in info}
|
||||
folded = {name: [] for name in info}
|
||||
done_at, errors = {}, []
|
||||
t_emit = time.perf_counter()
|
||||
ctx = mp.get_context('spawn')
|
||||
jobs, why = pick_jobs(args.jobs)
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts)) as pool:
|
||||
for i, r in enumerate(pool.imap_unordered(_emit_chunk, [t for _, t in tasks], chunksize=1)):
|
||||
results[r['module']].append(r)
|
||||
if r['error']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
if (i + 1) % 200 == 0 or i + 1 == len(tasks):
|
||||
print(f'emitted {i + 1}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s', file=sys.stderr,
|
||||
flush=True)
|
||||
pending = [t for _, t in tasks]
|
||||
emitted = 0
|
||||
while True:
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
try:
|
||||
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
|
||||
# Its folded bytes come with its result: a chunk emitted again reads them again in its new worker.
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
|
||||
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
|
||||
results[r['module']].append(r)
|
||||
folded[r['module']] += r['folded']
|
||||
if r['error']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
emitted += 1
|
||||
if emitted % 200 == 0 or emitted == len(tasks):
|
||||
print(f'emitted {emitted}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s',
|
||||
file=sys.stderr, flush=True)
|
||||
break
|
||||
except MemoryError:
|
||||
if jobs == 1:
|
||||
raise
|
||||
smaller = jobs // 2
|
||||
print(f'emit: out of memory with {jobs} workers, retrying with {smaller}', file=sys.stderr, flush=True)
|
||||
jobs, why = smaller, 'after out of memory'
|
||||
done = {(r['module'], r['number']) for rs in results.values() for r in rs}
|
||||
pending = [t for t in pending if (t[0], t[1]) not in done]
|
||||
|
||||
for name, d in info.items():
|
||||
rs = sorted(results[name], key=lambda r: r['number'])
|
||||
@@ -690,6 +791,7 @@ def main():
|
||||
unsupported.update(r['unsupported'])
|
||||
t = time.perf_counter()
|
||||
table_written = write_if_changed(moddir / f'{name}_table.c', table_text(name, d['emitted']))
|
||||
write_if_changed(moddir / 'folded.txt', folded_text(name, merge_ranges(folded[name])))
|
||||
removed = remove_stale(moddir, name, d['chunks'], errors)
|
||||
manifest = {
|
||||
'module': name,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Code patches for TotK 1.1.1 main (recomp/patches.py). The sites come from MacNativeGames
|
||||
# Code patches for TotK 1.1.0 main (recomp/patches.py). The sites come from MacNativeGames
|
||||
# games/totk/runtime/game_patches.c (MIT, Copyright (c) 2026 MacNativeGames contributors) at upstream ebe621d, through
|
||||
# its 1.2.1 addresses in game_version.h (2db612b); each one was located again in 1.1.1 by instruction-window matching
|
||||
# its 1.2.1 addresses in game_version.h (2db612b); each one was located again in 1.1.0 by instruction-window matching
|
||||
# and its original words checked against this build's main. The patch page holds two floats the runtime writes at every
|
||||
# present (frame_rate.c): +0 the 30 Hz ratio (seconds * 30), +4 the frame step in seconds.
|
||||
|
||||
@@ -35,7 +35,7 @@ site realtime main 90e5e8 1e210800 d503201f # fmul s0, s0, s1 -> nop
|
||||
|
||||
# Hair and cloth (upstream 290ec9f, 8f8022f): the actor animation update builds a second clock {seconds = factor / 30,
|
||||
# ratio = factor} per rendered frame. Three inlined places: at the fmul, seconds = measured seconds * factor; at the
|
||||
# store of the factor into the ratio slot, measured ratio * factor. The call of the shared update (1.1.1
|
||||
# store of the factor into the ratio slot, measured ratio * factor. The call of the shared update (1.1.0
|
||||
# main+0x8ecb40, 1.2.1 main+0x89822c) scales s2 (seconds) and s3 (ratio) by the measured ratio. Upstream's
|
||||
# veneer bodies, inlined at the site.
|
||||
site cloth main 9efc20 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
|
||||
@@ -66,7 +66,7 @@ site scene main cea5e0 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
|
||||
site scene main cea5e8 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
|
||||
site scene main cea5ec 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
|
||||
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v111/exefs --patches recomp/patches-v111.txt: loops that only
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v110/exefs --patches recomp/patches-v110.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 waits found by tools/spin_waits.py on the 1.1.x modules
|
||||
@@ -9,6 +9,7 @@ set cloth TOTK_CLOTH_REALTIME 1
|
||||
set bounce TOTK_HIGHFPS_FIX 1
|
||||
set output TOTK_PATCH_OUTPUT 0
|
||||
set scene TOTK_PATCH_SCENE 0
|
||||
set shadow TOTK_PATCH_SHADOW 0
|
||||
|
||||
# 60 fps: the adaptive present-interval controller (main+0x8f26c8, 1.0.0 0x8f6bec) gets the minimum interval 1.
|
||||
site fps60 main 8f273c 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
|
||||
@@ -54,6 +55,47 @@ site cloth main 8222f0 9401d7cf @page:x8 bd400104 1e240842 1e240863 9401d7cf #
|
||||
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
|
||||
site bounce main bda798 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
|
||||
|
||||
# Climbing (issues #35, #276, #1001): the climb controller's velocity step (main+0x1918220, the same code as 1.4.3
|
||||
# main+0x1f49754) multiplies the requested velocity by a factor that makes up for a movement loss growing with the
|
||||
# frame rate: 1.272 - 0.362 (r - 1) clamped to [0.5, 1.5] (r = VFR +0, the 30 Hz ratio times the time scale). Right at
|
||||
# 30 fps, 78% of the speed at 60 and 30% at 240. Below r = 1 use 1.4.3's factor, 1.272 + 1.189 (1 - r) at most 2
|
||||
# (Nintendo's 60 fps fit), and below r = 0.5 the 1.4.3 set climb's g(r) = 0.66265 / r + 0.4982 + 0.0848 r, at most 8.
|
||||
# Without flags: h = max(min(1.4.3 factor, 2), g - 16 max(r - 0.5, 0)), s0 = max(s0, h), s1 = max(1.5, h), and the
|
||||
# following fmin s1, s0, s1 gives max(min(s0, 1.5), h). For r >= 1 h is at most the shipping value, so 30 fps and
|
||||
# slower keep the factor bit for bit. x8 is dead after the load at 0x19182c0; s5-s7 and s16 are written before any
|
||||
# later read in this function.
|
||||
# g takes u = max(page +0, r): the patch page's 30 Hz frame ratio without the time scale (runtime/frame_rate.c), so
|
||||
# slow motion at 30 or 60 fps keeps the shipping factor and only a real frame rate above 60 raises it; the runtime
|
||||
# switches the set off without the real-time step, where the page is not written.
|
||||
set climb TOTK_CLIMB_FIX 1
|
||||
site climb main 19182cc 1e2f1001 @page:x8 bd400111 1e296a31 1e2e1005 1e3138a5 52862508 72a7e308 1e270106 1e2608a5 1e2528a5 529a1ca8 72a7f448 1e270106 1e2628a5 1e201006 1e2678a5 52946dc8 72a7e528 1e270106 1e3118c6 52828248 72a7dfe8 1e270107 1e2728c6 529573e8 72a7b5a8 1e270107 1f1118e6 1e283a27 2f00e410 1e3068e7 1e261010 1f1098e6 1e241010 1e3078c6 1e2668a5 1e256800 1e2f1001 1e256821 # fmov s1, #1.5 -> s5 = h(r); s0 = max(s0, s5); s1 = max(1.5, s5)
|
||||
|
||||
# Cursor and aim steps (upstream game_patches.c patch_cursor_speed, 363a6eb). These updates read the game's 30 Hz frame
|
||||
# ratio for their smoothing but leave it out of the step itself, so the map cursor and the vertical Ultrahand aim moved
|
||||
# 2x as fast at 60 fps and 4x at 120 as at the shipping 30 fps (issue #44). Each site multiplies the step by that ratio,
|
||||
# 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)
|
||||
|
||||
# Gear rotation check (issues #330, #984). The update of ActorCheckRotateModule (main+0x21350c0; vtable +0x110, called
|
||||
# by the AI node dispatchers main+0x11f5a40 / 0x2adbcf8 with x1 = the update context) reports a gear or water wheel as
|
||||
# turning when the angle it turned since the previous update is above RotateStartDeg / 30, the angle of one 30 Hz
|
||||
# frame. Above 30 fps a gear turns less per update than that, reads as stopped, and the shrine doors that wait on it
|
||||
# (Riogok, Turakamik, the Wind Temple) never open. 1.4.3 (main+0x273d080) divides the angle by the context's +8, the
|
||||
# frame delta in 30 Hz frames (the AI root sets it from its owner's virtual +0x30, main+0x106acc4, and the frame
|
||||
# counter node SelectorGenericFrameCounter, main+0x10b66f8, adds it up per update). 1.2.1 gets the same context and
|
||||
# ignores it: the update overwrites x1 with its first instruction. The first site keeps ctx+8 in s12 (the prologue has
|
||||
# saved d12, the function first writes it at the second site, and callees preserve it); the second multiplies the
|
||||
# threshold by it, which is 1.4.3's test with both sides multiplied by the delta. s4 is free after the fdiv: the next
|
||||
# read follows a write in the callee main+0x11c2014. The context follows the measured step with the set realtime and is
|
||||
# the whole-vsync ratio without it (0.25 at 120 fps, 0.5 at 60 fps measured), so the set works either way.
|
||||
set gears TOTK_HIGHFPS_FIX 1
|
||||
site gears main 21350e0 f9402001 bd40082c f9402001 # ldr x1, [x0, #0x40], after ldr s12, [x1, #8] (frame delta)
|
||||
site gears main 21352d0 1e24180c 1e241804 1e2c088c # fdiv s12, s0, s4 -> fdiv s4, s0, s4; fmul s12, s4, s12
|
||||
|
||||
# Output size (upstream games/totk/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
|
||||
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
|
||||
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
|
||||
@@ -70,6 +112,14 @@ site scene main cc3f68 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
|
||||
site scene main cc3f70 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
|
||||
site scene main cc3f74 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
|
||||
|
||||
# Fixed scene size (MacNativeGames game_patches_v143.c patch_fixed_res, 1.4.3 main+0x5848a8; runtime/resolution.c
|
||||
# res_dynres_live switches the set on unless TOTK_DYNAMIC_RES=1). The game's dynamic resolution controller
|
||||
# (main+0xcc26f4, called once per frame from the renderer at main+0xcc0fa0) steps the scene through tiers of 100, 90 and
|
||||
# 80 % by a cost model that grows with the scene width squared (runtime/resolution.c res_dynres_live), and the renderer stores scene width * tier / output width at +0xccc (main+0xcc1150).
|
||||
# Every path leaves through one epilogue, where the tier's scale moves into the result: 1.0 there makes every tier 100 %.
|
||||
set fixres TOTK_FIXED_RES_PATCH 0
|
||||
site fixres main cc2c00 2a1703e9 52a7f009 # mov w9, w23 -> mov w9, #0x3f800000 (1.0f)
|
||||
|
||||
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches.c, the 1.2.1 sites of aspect_install;
|
||||
# runtime/resolution.c res_init writes the output's width / height to page +56 and switches the set on). The same four
|
||||
# sites as patches-v143.txt, with the same original words: two camera sites (update and setup) load the camera's copy of
|
||||
@@ -81,6 +131,115 @@ site aspect main 12521cc bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0
|
||||
site aspect main 19b36f0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
site aspect main 1ecdf68 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
|
||||
# Texture pool offsets past 2 GiB (MacNativeGames ca311e2d). The texture loader keeps a texture's place in its pool as a
|
||||
# 32-bit offset (main+0xad4294: sub x25, x9, x10, stored as the word w25) and widens it as a signed number for
|
||||
# nvnTextureBuilderSetStorage's offset and for SetPackagedTextureData's source (the pool's CPU address + offset). The
|
||||
# shipped heap table keeps every pool far below 2 GiB; a texture pack's heap table with TOTK_APP_POOL raised can make
|
||||
# one larger (MacNativeGames saw a 3.58 GiB pool), and every texture past 2 GiB then got storage 4 GiB below its pool.
|
||||
# The offset is widened unsigned instead, the same value below 2 GiB, right up to 4 GiB. 1.4.3 needs no patch: its
|
||||
# loaders (main+0x583c34, main+0x27ed214) keep the offset in 64 bits.
|
||||
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
|
||||
@@ -947,3 +1106,12 @@ spin sdk 23a590 23a5d8 # 19 instructions; loads
|
||||
spin sdk 23a658 23a6a0 # 19 instructions; loads
|
||||
spin sdk 23a7fc 23a918 # 19 instructions; loads
|
||||
spin sdk 3d3250 3d3270 # 9 instructions; loads
|
||||
|
||||
# Sun shadow map size (runtime/resolution.c shadow_init switches the set on). Each frame the engine sizes its shadow
|
||||
# cascades (a D16 4-layer array) in main+0xcebdf4: a base size (960 in the field), interpolated down by how close the
|
||||
# cover is, rounded up to 8 at main+0xcec22c and stored as the width (+0x3d8 of the cascade set) and, through the packed
|
||||
# store at +0xe4, the height (+0x3f8). Near the ground it falls to about 400, which draws foliage shadows blocky, and
|
||||
# every change of size moves the shadow texels, so leaf shadows jump instead of sliding. The page's fixed size (+92)
|
||||
# goes in just before the rounding, as the community's shadow resolution mods do (1.1.0: mov w11, #1024 over the
|
||||
# computation). Found by a write watch on the size field (the writer was this function, and no other).
|
||||
site shadow main cec22c 11001d6b @page:x11 b9405d6b 11001d6b # add w11, w11, #7 -> adrp x11, page; ldr w11, [x11, #92]; add w11, w11, #7
|
||||
@@ -10,6 +10,8 @@ set cloth TOTK_CLOTH_REALTIME 1
|
||||
set bounce TOTK_HIGHFPS_FIX 1
|
||||
set output TOTK_PATCH_OUTPUT 0
|
||||
set scene TOTK_PATCH_SCENE 0
|
||||
set shadow TOTK_PATCH_SHADOW 0
|
||||
set cascade TOTK_PATCH_CASCADE 0
|
||||
|
||||
# 60 fps: the adaptive present-interval controller (main+0x2db174, 1.2.1 0x8f26c8) gets the minimum interval 1.
|
||||
site fps60 main 2db204 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
|
||||
@@ -80,6 +82,13 @@ site scene main 585e50 bd0cc660 2d420460 # str s0, [x19, #0xcc4] -> ldp s0, s
|
||||
site scene main 585e54 1e220140 bd0cc660 # scvtf s0, w10 -> str s0, [x19, #0xcc4]
|
||||
site scene main 585e60 bd0cca60 bd0cca61 # str s0, [x19, #0xcc8] -> str s1, [x19, #0xcc8]
|
||||
|
||||
# Fixed scene size (MacNativeGames game_patches_v143.c patch_fixed_res; runtime/resolution.c res_dynres_live switches
|
||||
# the set on unless TOTK_DYNAMIC_RES=1). The dynamic resolution controller (main+0x584300, 1.2.1 0xcc26f4) steps the
|
||||
# scene through tiers of 100, 90 and 80 % by a cost model that grows with the scene width squared; at its one epilogue the tier's scale moves into
|
||||
# the result, and 1.0 there makes every tier 100 %. The original word is MacNativeGames' check against 1.4.3.
|
||||
set fixres TOTK_FIXED_RES_PATCH 0
|
||||
site fixres main 5848a8 2a1703e9 52a7f009 # mov w9, w23 -> mov w9, #0x3f800000 (1.0f)
|
||||
|
||||
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches_v143.c and aspect_patch.h
|
||||
# aspect_install / aspect_stub; runtime/resolution.c res_init writes the output's width / height to page +56 and
|
||||
# switches the set on). Each site leaves the ratio sead's perspective projection (+0xb0) takes in s0: two camera sites
|
||||
@@ -101,6 +110,94 @@ 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
|
||||
# Keep native camera-distance easing when direct mouse orbit bypasses the stick integrators.
|
||||
site mouse_direct main 18ea34 b9418a88 94e5454f # orbit state: ldr w8, [x20, #0x188]
|
||||
site mouse_direct main 1d59460 b9004be8 94761acc
|
||||
# The aiming, scope, ability, horse and Purah Pad camera cameras: per axis, the float load the stick turn is computed
|
||||
# from (the gamepad's part only) and the load of the angle it is added to (plus the mouse's degrees), each a bl to page
|
||||
# +0xf80, told apart by the return address (runtime/mouse_direct.c camera_load, load_sites). Every enclosing function
|
||||
# saves x30 in its prologue, so the bl's link register write is harmless.
|
||||
site mouse_direct main 23beb94 bd4426c1 945c84fb # Aiming: ldr s1, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 23bebc8 bd415e61 945c84ee # Aiming: ldr s1, [x19, #0x15c] (pitch)
|
||||
site mouse_direct main 23bee2c bd4422c1 945c8455 # Aiming: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 23beeec bc5b83a1 945c8425 # Aiming: ldur s1, [x29, #-0x48] (yaw)
|
||||
site mouse_direct main 23c0a10 bd4422e0 945c7d5c # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23c0a1c bd4426eb 945c7d59 # AimingTelescope: ldr s11, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23c0b00 bd416260 945c7d20 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 23c0e80 bd405be1 945c7c40 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
|
||||
site mouse_direct main 23d2eec bd4422e8 945c3425 # SpecialPower: ldr s8, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23d2ef4 bd4426ee 945c3423 # SpecialPower: ldr s14, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23d3158 bd416260 945c338a # SpecialPower: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 23d34cc bc5b03a1 945c32ad # SpecialPower: ldur s1, [x29, #-0x50] (yaw)
|
||||
site mouse_direct main 23cb274 bd442288 945c5343 # Horse: ldr s8, [x20, #0x420] (stick x)
|
||||
site mouse_direct main 23cb2a0 bd406a61 945c5338 # Horse: ldr s1, [x19, #0x68] (yaw)
|
||||
site mouse_direct main 23cb3f8 bd4426a8 945c52e2 # Horse: ldr s8, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23cb424 bd406661 945c52d7 # Horse: ldr s1, [x19, #0x64] (pitch)
|
||||
site mouse_direct main 23d2f30 bd4422e8 945c3414 # SpecialPower offset mode: ldr s8, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 23d2f5c bd41da62 945c3409 # SpecialPower offset mode: ldr s2, [x19, #0x1d8] (offset x)
|
||||
site mouse_direct main 23d2f6c bd4426e8 945c3405 # SpecialPower offset mode: ldr s8, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23d2f98 bd41de62 945c33fa # SpecialPower offset mode: ldr s2, [x19, #0x1dc] (offset y)
|
||||
site mouse_direct main 23d3210 bd400400 945c335c # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
|
||||
site mouse_direct main 23c2890 bd4426ad 945c75bc # CameraFinder: ldr s13, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23c28b4 bd405261 945c75b3 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
|
||||
site mouse_direct main 23c2934 bd4422ad 945c7593 # CameraFinder: ldr s13, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 23c2964 bd404be1 945c7587 # CameraFinder: ldr s1, [sp, #0x48] (yaw)
|
||||
# The other player cameras (runtime/mouse_direct.c load_sites; a ROLE_WATCH load stays the game's and only names its
|
||||
# mode in the log when the mouse was in it):
|
||||
site mouse_direct main 23c3dd0 bd442301 945c706c # ClimbObj: ldr s1, [x24, #0x420]
|
||||
site mouse_direct main 23c3dd8 bd409261 945c706a # ClimbObj: ldr s1, [x19, #0x90] (yaw)
|
||||
site mouse_direct main 23c3d64 bd442701 945c7087 # ClimbObj: ldr s1, [x24, #0x424]
|
||||
# ClimbObj's pitch is a pre-indexed load, ldr s1, [x22, #0x88]!, whose write-back the code after it needs (ldp and
|
||||
# str through x22). A host call cannot write x22: the lifter keeps X19-X29 in locals across a call. So the mov before
|
||||
# it adds the 0x88 instead (x22 = x19 + 0x88) and the hook loads [x22] with no write-back.
|
||||
site mouse_direct main 23c3d5c aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
|
||||
site mouse_direct main 23c3d70 bc488ec1 945c7084 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
|
||||
site mouse_direct main 23da694 bd4422c1 945c163b # Wakeboard: ldr s1, [x22, #0x420]
|
||||
site mouse_direct main 23da698 bd4426c0 945c163a # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 23da840 bd40da60 945c15d0 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
|
||||
site mouse_direct main 23da894 bd4017e0 945c15bb # Wakeboard: ldr s0, [sp, #0x14] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 23daed8 bd403fe9 945c142a # Wakeboard: ldr s9, [sp, #0x3c] (yaw)
|
||||
site mouse_direct main 23dced8 bd4422cd 945c0c2a # WaterfallClimb: ldr s13, [x22, #0x420]
|
||||
site mouse_direct main 23dd070 b85cc3a8 945c0bc4 # WaterfallClimb: ldur w8, [x29, #-0x34] (yaw)
|
||||
site mouse_direct main 23dd008 bd4426c9 945c0bde # WaterfallClimb: ldr s9, [x22, #0x424]
|
||||
site mouse_direct main 23d4b14 bd44228c 945c2d1b # Tail: ldr s12, [x20, #0x420]
|
||||
site mouse_direct main 23d4b1c bd44268d 945c2d19 # Tail (climbing): ldr s13, [x20, #0x424] (stick y)
|
||||
site mouse_direct main 23d4f34 bd40c260 945c2c13 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
|
||||
site mouse_direct main 23d5028 bd40b661 945c2bd6 # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
|
||||
site mouse_direct main 23d756c bd442300 945c2285 # LockOn / Target: ldr s0, [x24, #0x420]
|
||||
site mouse_direct main 23d7594 bd442301 945c227b # LockOn / Target: ldr s1, [x24, #0x420] (stick x)
|
||||
site mouse_direct main 23d75b4 bd4023e0 945c2273 # LockOn / Target: ldr s0, [sp, #0x20] (yaw turn)
|
||||
site mouse_direct main 23d75c4 bd4023e9 945c226f # LockOn / Target: ldr s9, [sp, #0x20] (yaw turn)
|
||||
site mouse_direct main 23d7998 bd442701 945c217a # LockOn / Target: ldr s1, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 23d79b8 bd4023e0 945c2172 # LockOn / Target: ldr s0, [sp, #0x20] (pitch turn)
|
||||
site mouse_direct main 23d79c8 bd4023ea 945c216e # LockOn / Target: ldr s10, [sp, #0x20] (pitch turn)
|
||||
site mouse_direct main 23cecb4 bd4426a9 945c44b3 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 23cecf8 bd419a61 945c44a2 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
|
||||
site mouse_direct main 23ced0c bd4422a9 945c449d # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 23ced4c bd419e61 945c448d # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
|
||||
site mouse_direct main 23cc4b4 bd44232b 945c4eb3 # HorseLockOnEmpty: ldr s11, [x25, #0x420]
|
||||
site mouse_direct main 23c54d4 bd4422eb 945c6aab # EventMultiTalk: ldr s11, [x23, #0x420]
|
||||
site mouse_direct main 23c542c bd4426eb 945c6ad5 # EventMultiTalk: ldr s11, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 23c546c bd405e61 945c6ac5 # EventMultiTalk: ldr s1, [x19, #0x5c] (pitch)
|
||||
site mouse_direct main 23c5554 bd406661 945c6a8b # EventMultiTalk: ldr s1, [x19, #0x64] (yaw)
|
||||
site mouse_direct main 23c90ac bd442709 945c5bb5 # EventTalkManual: ldr s9, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 23c90d8 bd407661 945c5baa # EventTalkManual: ldr s1, [x19, #0x74] (pitch)
|
||||
site mouse_direct main 23c9140 bd442309 945c5b90 # EventTalkManual: ldr s9, [x24, #0x420] (stick x)
|
||||
site mouse_direct main 23c9170 bd407e61 945c5b84 # EventTalkManual: ldr s1, [x19, #0x7c] (yaw)
|
||||
site mouse_direct main 23d0100 bd4422c1 945c3fa0 # ShieldSurfing: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 23d0104 bd4426c0 945c3f9f # ShieldSurfing: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 23d02ac bd40de60 945c3f35 # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
|
||||
site mouse_direct main 23d0a34 bd4013e0 945c3d53 # ShieldSurfing: ldr s0, [sp, #0x10] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 23d087c bd403fe9 945c3dc1 # ShieldSurfing: ldr s9, [sp, #0x3c] (yaw)
|
||||
|
||||
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
|
||||
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
|
||||
# its wrapper ZSTD_decompress_usingDDict (main+0x3a1e0, which tail-branches to it) goes to a host function on the patch
|
||||
@@ -976,3 +1073,32 @@ spin subsdk0 1c4e54 1c4e60 # 4 instructions; loads
|
||||
spin sdk 2d3654 2d3668 # 6 instructions; loads
|
||||
spin sdk 443864 443884 # 9 instructions; loads
|
||||
spin sdk 541e54 541f98 # 32 instructions; loads
|
||||
|
||||
# Sun shadow map size (patches-v121.txt explains the set and the page). 1.4.3 interpolates the size between a minimum
|
||||
# and the base and rounds it up to 8 at main+0x683604; the rounded value is the width (+0x3d8) and, through the packed
|
||||
# store at +0xf4, the height (+0x3f8). The page's size goes in just before the rounding.
|
||||
site shadow main 683604 11001d6b @page:x11 b9405d6b 11001d6b # add w11, w11, #7 -> adrp x11, page; ldr w11, [x11, #92]; add w11, w11, #7
|
||||
|
||||
# Sun shadow cascade distances (runtime/resolution.c res_shadow_range switches the set on). The depth shadow parameter
|
||||
# object (main+0x6fa3c0) registers depth_shadow_near_0_1..3 = 20, 40, 80 m and depth_shadow_far_0 = 160 m as
|
||||
# immediates, and the field's .bgsdw files do not override them; with the set on they come from page +64..+76.
|
||||
site cascade main 6fabb8 52a83408 @page:x8 b9404108 # mov w8, #20.0 -> ldr w8, [page+64]
|
||||
site cascade main 6fac40 52a84408 @page:x8 b9404508 # mov w8, #40.0 -> ldr w8, [page+68]
|
||||
site cascade main 6facc8 52a85408 @page:x8 b9404908 # mov w8, #80.0 -> ldr w8, [page+72]
|
||||
site cascade main 6fad4c 52a86408 @page:x8 b9404d08 # mov w8, #160.0 -> ldr w8, [page+76]
|
||||
|
||||
# Climbing (issues #35, #276, #1001): the climb controller's velocity step (main+0x1f49754, shared by wall and ladder
|
||||
# climbing, climb jumps and climb-overs) multiplies the requested velocity by a factor that makes up for a movement loss
|
||||
# growing with the frame rate. Below ratio 1 the factor is 1.272 + 1.189 (1 - r) clamped to [1, 2] (r = VFR +0, the
|
||||
# 30 Hz ratio times the time scale): Nintendo's fit for 30 and 60 fps, flat at 2 from about 77 fps, so Link climbs at
|
||||
# 63% of the speed at 120 fps and 34% at 240. Below r = 0.5 take g(r) = 0.66265 / r + 0.4982 + 0.0848 r instead,
|
||||
# fitted to the replayed ladder climb at 120 and 240 fps and equal to the shipping 1.8665 at r = 0.5; at most 8. The
|
||||
# site computes g with 16 * max(r - 0.5, 0) subtracted, so g stays under the shipping factor for r >= 0.5, and folds it
|
||||
# into the existing clamp without flags: s0 = max(s0, g) (the value clamped), s1 = max(2, g) (the bound). r >= 0.5
|
||||
# keeps the shipping factor bit for bit. x8 is dead after the constant load at 0x1f497e8, s2-s4 are loaded again at
|
||||
# 0x1f49844 before any read.
|
||||
# g takes u = max(page +0, r): the patch page's 30 Hz frame ratio without the time scale (runtime/frame_rate.c), so
|
||||
# slow motion at 30 or 60 fps keeps the shipping factor and only a real frame rate above 60 raises it; the runtime
|
||||
# switches the set off without the real-time step, where the page is not written.
|
||||
set climb TOTK_CLIMB_FIX 1
|
||||
site climb main 1f497f4 1e201001 @page:x8 bd400104 1e286884 52946dc8 72a7e528 1e270102 1e241842 52828248 72a7dfe8 1e270103 1e232842 529573e8 72a7b5a8 1e270103 1f040862 1e2a3883 2f00e404 1e246863 1e261004 1f048862 1e241004 1e247842 1e226800 1e201001 1e226821 # fmov s1, #2.0 -> s2 = g(u); s0 = max(s0, s2); s1 = max(2.0, s2)
|
||||
+21
-15
@@ -14,14 +14,16 @@ encoded for the site's address. The patch page is the first 4 KiB page after mai
|
||||
of the 2 MiB-aligned module slot free); the runtime commits it and owns its contents. Words in use, by byte offset:
|
||||
+0..15 the frame-rate step (runtime/frame_rate.c), +16..55 the output, swapchain and scene sizes
|
||||
(runtime/resolution.c res_init), +56..59 the camera's aspect ratio (runtime/resolution.c res_init, set aspect), +60..63
|
||||
the field-of-view factor (runtime/resolution.c res_fov_live, set fov). A new
|
||||
set takes words outside these.
|
||||
the field-of-view factor (runtime/resolution.c res_fov_live, set fov), +92..95 the sun shadow map size
|
||||
(runtime/resolution.c shadow_init, set shadow). A new set takes words outside these.
|
||||
|
||||
The replacement words are lifted at the site's address in place of the original instruction, and the generated C reads
|
||||
`if (rt_patch_on[<set>]) { replacement } else { original }`: one byte load and a branch per executed site. Replacement
|
||||
words must not branch except a final `bl` (lifted with the site's return address); the original must not be a branch
|
||||
other than `bl`. So the function's control flow, labels and jump tables are the original's. The generator writes
|
||||
patches.h (set names, switches, the page address) next to layout.h for the runtime.
|
||||
`if (rt_patch_on[<set>]) { replacement } else { original }`: one byte load and a branch per executed site. Several
|
||||
sets may patch one address (cheats that write different values there): `if (a) {..} else if (b) {..} else {..}`, the
|
||||
first set in file order wins. The original may be any instruction but br, blr and ret; a replacement's last word may be
|
||||
a bl (lifted with the site's return address), a b or b.cond / cbz / tbz (its target joins the function's labels when
|
||||
inside it) or a ret; earlier words must not branch. Every alternative of a site writes the same flags. The generator
|
||||
writes patches.h (set names, switches, the page address) next to layout.h for the runtime.
|
||||
|
||||
A `spin` line names a spin wait found by tools/spin_waits.py: a loop with head <head> that only another thread can end,
|
||||
and the direct branches back to it (module offsets). Each back-edge (checked: a b, b.cond, cbz / cbnz or tbz / tbnz to
|
||||
@@ -46,7 +48,8 @@ def adrp(pc, target, rd):
|
||||
|
||||
|
||||
class Table:
|
||||
"""sets: [(name, env, default)]; sites: {module: {offset: (set index, [word | ('page', reg)], original)}}; page."""
|
||||
"""sets: [(name, env, default)]; sites: {module: {offset: [(set index, [word | ('page', reg)], original)]}} (the
|
||||
alternatives of a site in file order); page."""
|
||||
|
||||
def __init__(self, sets=(), sites=None, page=0, spins=None):
|
||||
self.sets = list(sets)
|
||||
@@ -59,11 +62,12 @@ class Table:
|
||||
return {base + b: base + h for b, h in self.spins.get(name, {}).items()}
|
||||
|
||||
def for_module(self, name, base):
|
||||
"""{absolute address: (set index, [words])} with @page resolved."""
|
||||
"""{absolute address: [(set index, [words])]} with @page resolved."""
|
||||
out = {}
|
||||
for off, (k, repl, _) in self.sites.get(name, {}).items():
|
||||
for off, alts in self.sites.get(name, {}).items():
|
||||
a = base + off
|
||||
out[a] = (k, [adrp(a, self.page, w[1]) if isinstance(w, tuple) else w for w in repl])
|
||||
out[a] = [(k, [adrp(a, self.page, w[1]) if isinstance(w, tuple) else w for w in repl])
|
||||
for k, repl, _ in alts]
|
||||
return out
|
||||
|
||||
|
||||
@@ -93,10 +97,12 @@ def load(path, mods):
|
||||
repl.append(('page', int(w[7:])))
|
||||
else:
|
||||
repl.append(int(w, 16))
|
||||
mod = sites.setdefault(f[2], {})
|
||||
if off in mod:
|
||||
raise PatchError(f'{where}: {f[2]}+{off:#x} patched twice')
|
||||
mod[off] = (index[f[1]], repl, orig)
|
||||
alts = sites.setdefault(f[2], {}).setdefault(off, [])
|
||||
if any(k == index[f[1]] for k, _, _ in alts):
|
||||
raise PatchError(f'{where}: {f[2]}+{off:#x} patched twice by set {f[1]}')
|
||||
if alts and alts[0][2] != orig:
|
||||
raise PatchError(f'{where}: {f[2]}+{off:#x} expects {orig:08x}, an earlier line {alts[0][2]:08x}')
|
||||
alts.append((index[f[1]], repl, orig))
|
||||
elif f[0] == 'spin' and len(f) >= 4:
|
||||
if f[1] not in bases:
|
||||
raise PatchError(f'{where}: module {f[1]} not in the layout')
|
||||
@@ -118,7 +124,7 @@ def load(path, mods):
|
||||
|
||||
def check(table, mod, name):
|
||||
"""Stops on a site whose original word differs from the module's."""
|
||||
for off, (_, _, want) in table.sites.get(name, {}).items():
|
||||
for off, [(_, _, want), *_] in table.sites.get(name, {}).items():
|
||||
a = mod.base + off
|
||||
if not mod.in_text(a):
|
||||
raise PatchError(f'{name}+{off:#x}: patch site outside .text')
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
// Amiibo dump loader and Horizon OS NFP state emulation.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "amiibo.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static PlatMutex g_amiibo_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
// 540-byte raw NTAG215 binary dump
|
||||
static uint8_t g_dump[AMIIBO_DUMP_SIZE];
|
||||
static bool g_has_dump = false;
|
||||
static AmiiboInfo g_info;
|
||||
|
||||
static bool g_device_present = true;
|
||||
static bool g_detecting = false;
|
||||
static bool g_tag_present = false;
|
||||
static bool g_mounted = false;
|
||||
static bool g_auto_remove = true;
|
||||
static float g_auto_remove_delay_s = 2.5f;
|
||||
|
||||
static uint64_t g_tap_timestamp_ns = 0;
|
||||
static bool g_timer_active = false;
|
||||
|
||||
static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
|
||||
if (size != AMIIBO_DUMP_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C). An amiibo's is F1 10 FF EE, not the E1 of a plain
|
||||
// NDEF tag (3dbrew, "Amiibo"); the CC is not encrypted, so encrypted dumps carry it too.
|
||||
static const uint8_t amiibo_cc[4] = {0xF1, 0x10, 0xFF, 0xEE};
|
||||
if (memcmp(data + 0x0C, amiibo_cc, sizeof amiibo_cc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// UID sanity: UID (bytes 0..2 and 4..7) cannot be all zeroes or all 0xFF.
|
||||
if ((data[0] == 0 && data[1] == 0 && data[2] == 0 && data[4] == 0 &&
|
||||
data[5] == 0 && data[6] == 0 && data[7] == 0) ||
|
||||
(data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[4] == 0xFF &&
|
||||
data[5] == 0xFF && data[6] == 0xFF && data[7] == 0xFF)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ISO 14443-3 / NTAG215 Bit Check Character (BCC) verification.
|
||||
// Page 0 byte 3 (BCC0) = 0x88 ^ UID[0] ^ UID[1] ^ UID[2]
|
||||
// Page 2 byte 0 (BCC1) = UID[3] ^ UID[4] ^ UID[5] ^ UID[6]
|
||||
uint8_t bcc0 = (uint8_t)(0x88 ^ data[0] ^ data[1] ^ data[2]);
|
||||
uint8_t bcc1 = (uint8_t)(data[4] ^ data[5] ^ data[6] ^ data[7]);
|
||||
if (data[3] != bcc0 || data[8] != bcc1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
snprintf(g_info.path, sizeof(g_info.path), "%s", path);
|
||||
const char *slash = strrchr(path, '\\');
|
||||
const char *fslash = strrchr(path, '/');
|
||||
const char *name = slash ? slash + 1 : (fslash ? fslash + 1 : path);
|
||||
if (slash && fslash) {
|
||||
name = (slash > fslash ? slash : fslash) + 1;
|
||||
}
|
||||
snprintf(g_info.filename, sizeof(g_info.filename), "%s", name);
|
||||
}
|
||||
|
||||
copy_uid(g_info.uuid, g_dump);
|
||||
g_info.uuid_len = 7;
|
||||
|
||||
// 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.nfp_type = g_dump[0x57];
|
||||
g_info.numbering_id = ((uint16_t)g_dump[0x58] << 8) | g_dump[0x59];
|
||||
g_info.series_id = g_dump[0x5A];
|
||||
g_info.version = g_dump[0x5B];
|
||||
}
|
||||
|
||||
void amiibo_init(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_device_present = true;
|
||||
g_detecting = false;
|
||||
g_tag_present = false;
|
||||
g_mounted = false;
|
||||
g_has_dump = false;
|
||||
g_timer_active = false;
|
||||
memset(g_dump, 0, sizeof(g_dump));
|
||||
memset(&g_info, 0, sizeof(g_info));
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_set_device_present(bool present) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_device_present = present;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_is_device_present(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_device_present;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
// While the game waits for a tag it stops and starts detection again several times a second (tens with the menu open),
|
||||
// so the log names the first few and then a count every 200th start.
|
||||
static uint64_t g_detect_starts;
|
||||
static uint64_t g_detect_stop_ns; // the last stop: the menu holds "searching" across the game's restarts
|
||||
|
||||
void amiibo_start_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_detecting = true;
|
||||
uint64_t n = ++g_detect_starts;
|
||||
if (n <= 3) rt_log("amiibo: start detection");
|
||||
else if (n % 200 == 0) rt_log("amiibo: detection started %llu times so far", (unsigned long long)n);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_stop_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_detecting) g_detect_stop_ns = plat_now_ns();
|
||||
g_detecting = false;
|
||||
if (g_detect_starts <= 3) rt_log("amiibo: stop detection");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_is_detecting(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_detecting;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool amiibo_mount(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_device_present || !g_tag_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return false;
|
||||
}
|
||||
g_mounted = true;
|
||||
rt_log("amiibo: mounted tag '%s'", g_info.filename[0] ? g_info.filename : "unknown");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_unmount(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_mounted = false;
|
||||
rt_log("amiibo: unmounted tag");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_is_mounted(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_mounted;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
AmiiboDeviceState amiibo_get_device_state(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_device_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_UNAVAILABLE;
|
||||
}
|
||||
if (g_mounted) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_TAG_MOUNTED;
|
||||
}
|
||||
if (g_tag_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_TAG_FOUND;
|
||||
}
|
||||
if (g_detecting) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_SEARCHING;
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_INITIALIZED;
|
||||
}
|
||||
|
||||
AmiiboDeviceState amiibo_get_reader_state(void) {
|
||||
AmiiboDeviceState state = amiibo_get_device_state();
|
||||
if (state != AMIIBO_DEV_STATE_INITIALIZED) return state;
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool recent = g_detect_stop_ns && plat_now_ns() - g_detect_stop_ns < 1000000000ull;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return recent ? AMIIBO_DEV_STATE_SEARCHING : state;
|
||||
}
|
||||
|
||||
bool amiibo_load_file(const char *path) {
|
||||
if (!path || !*path) return false;
|
||||
uint64_t size = 0;
|
||||
uint8_t *data = plat_file_read(path, &size);
|
||||
if (!data) {
|
||||
rt_log("amiibo: failed to read file: %s", path);
|
||||
return false;
|
||||
}
|
||||
if (size != AMIIBO_DUMP_SIZE) {
|
||||
rt_log("amiibo: invalid dump size (%llu bytes, expected exactly %d): %s",
|
||||
(unsigned long long)size, AMIIBO_DUMP_SIZE, path);
|
||||
free(data);
|
||||
return false;
|
||||
}
|
||||
if (!amiibo_validate_dump(data, size)) {
|
||||
rt_log("amiibo: dump failed NTAG215 structure sanity checks: %s", path);
|
||||
free(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
memcpy(g_dump, data, AMIIBO_DUMP_SIZE);
|
||||
g_has_dump = true;
|
||||
parse_dump_info(path);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
|
||||
free(data);
|
||||
rt_log("amiibo: loaded dump '%s' (char_id: %02x%02x%02x, series: %02x, num: %04x)",
|
||||
g_info.filename, g_info.character_id[0], g_info.character_id[1], g_info.character_id[2],
|
||||
g_info.series_id, g_info.numbering_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_tap_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_has_dump) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return;
|
||||
}
|
||||
g_tag_present = true;
|
||||
g_tap_timestamp_ns = plat_now_ns();
|
||||
if (g_auto_remove) {
|
||||
g_timer_active = true;
|
||||
}
|
||||
rt_log("amiibo: tag tapped on reader ('%s')", g_info.filename);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_scan_and_tap(const char *path) {
|
||||
if (!amiibo_load_file(path)) return false;
|
||||
amiibo_tap_tag();
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_remove_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_tag_present || g_mounted) {
|
||||
g_tag_present = false;
|
||||
g_mounted = false;
|
||||
g_timer_active = false;
|
||||
rt_log("amiibo: tag removed from reader");
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_tick(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_timer_active && g_tag_present) {
|
||||
uint64_t now = plat_now_ns();
|
||||
uint64_t diff = now > g_tap_timestamp_ns ? now - g_tap_timestamp_ns : 0;
|
||||
uint64_t limit_ns = (uint64_t)(g_auto_remove_delay_s * 1000000000.0f);
|
||||
if (diff >= limit_ns) {
|
||||
g_tag_present = false;
|
||||
g_mounted = false;
|
||||
g_timer_active = false;
|
||||
rt_log("amiibo: auto-removed tag after %.1fs timeout", (double)g_auto_remove_delay_s);
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_set_auto_remove(bool enable) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_auto_remove = enable;
|
||||
if (!enable) g_timer_active = false;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_get_auto_remove(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_auto_remove;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
void amiibo_set_auto_remove_delay_s(float seconds) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (seconds >= 0.5f && seconds <= 10.0f) g_auto_remove_delay_s = seconds;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
float amiibo_get_auto_remove_delay_s(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
float res = g_auto_remove_delay_s;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool amiibo_has_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_has_dump && g_tag_present;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
const AmiiboInfo *amiibo_get_current_info(void) {
|
||||
return &g_info;
|
||||
}
|
||||
|
||||
const uint8_t *amiibo_get_raw_data(void) {
|
||||
return g_dump;
|
||||
}
|
||||
|
||||
void amiibo_get_tag_info(void *out_tag_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_tag_info;
|
||||
memset(out, 0, 0x58);
|
||||
// UUID: offset 0x00, 10 bytes max
|
||||
copy_uid(out, g_dump);
|
||||
out[0x0A] = 7; // UUID length
|
||||
// Protocol at offset 0x20: TypeA = 1
|
||||
*(uint32_t *)(out + 0x20) = 1;
|
||||
// TagType at offset 0x24: Type2 / NTAG215 = 2
|
||||
*(uint32_t *)(out + 0x24) = 2;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
void amiibo_get_common_info(void *out_common_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_common_info;
|
||||
memset(out, 0, 0x40);
|
||||
// Last write date: 0x00..0x03
|
||||
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;
|
||||
// Version: 0x06
|
||||
out[0x06] = 0x02;
|
||||
// Application area size: 0x08..0x0B = 0xD8 (216 bytes)
|
||||
*(uint32_t *)(out + 0x08) = 0xD8;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_get_register_info(void *out_register_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_register_info;
|
||||
memset(out, 0, 0x100);
|
||||
// Mii CharInfo at 0x00..0x57 (dummy valid mii)
|
||||
out[0x00] = 0x03; // version 3
|
||||
// First write date at 0x58..0x5B
|
||||
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";
|
||||
snprintf((char *)(out + 0x5C), 0x29, "%s", name);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
int amiibo_get_application_area(void *out_buf, uint32_t max_size) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_has_dump || !out_buf) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return 0;
|
||||
}
|
||||
uint32_t to_copy = max_size < 0xD8 ? max_size : 0xD8;
|
||||
// Decrypted/payload application area starts at offset 0xDC (or 0x0A0..0x178 in raw dump)
|
||||
if (0xDC + to_copy <= AMIIBO_DUMP_SIZE) {
|
||||
memcpy(out_buf, g_dump + 0xDC, to_copy);
|
||||
} else {
|
||||
memset(out_buf, 0, to_copy);
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return (int)to_copy;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Amiibo loader and state management subsystem (540-byte NTAG215 binary dumps).
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define AMIIBO_DUMP_SIZE 540
|
||||
|
||||
// Device state corresponding to nn::nfp / nn::nfc DeviceState
|
||||
typedef enum {
|
||||
AMIIBO_DEV_STATE_INITIALIZED = 0,
|
||||
AMIIBO_DEV_STATE_SEARCHING = 1,
|
||||
AMIIBO_DEV_STATE_TAG_FOUND = 2,
|
||||
AMIIBO_DEV_STATE_TAG_REMOVED = 3,
|
||||
AMIIBO_DEV_STATE_TAG_MOUNTED = 4,
|
||||
AMIIBO_DEV_STATE_UNAVAILABLE = 5
|
||||
} AmiiboDeviceState;
|
||||
|
||||
// Amiibo identification info
|
||||
typedef struct {
|
||||
uint8_t uuid[10];
|
||||
uint8_t uuid_len;
|
||||
uint8_t character_id[3];
|
||||
uint8_t series_id;
|
||||
uint16_t numbering_id;
|
||||
uint8_t nfp_type;
|
||||
uint8_t version;
|
||||
char path[512];
|
||||
char filename[128];
|
||||
} AmiiboInfo;
|
||||
|
||||
// Subsystem lifecycle
|
||||
void amiibo_init(void);
|
||||
|
||||
// Hardware / device emulation status
|
||||
void amiibo_set_device_present(bool present);
|
||||
bool amiibo_is_device_present(void);
|
||||
|
||||
// Tag detection & lifecycle
|
||||
void amiibo_start_detection(void);
|
||||
void amiibo_stop_detection(void);
|
||||
bool amiibo_is_detecting(void);
|
||||
|
||||
// Tag mounting
|
||||
bool amiibo_mount(void);
|
||||
void amiibo_unmount(void);
|
||||
bool amiibo_is_mounted(void);
|
||||
|
||||
// State query
|
||||
AmiiboDeviceState amiibo_get_device_state(void);
|
||||
// The state the menu shows: searching also within a second after a stop, since the game restarts detection many times
|
||||
// a second while it waits for a tag. The game reads amiibo_get_device_state.
|
||||
AmiiboDeviceState amiibo_get_reader_state(void);
|
||||
|
||||
// File loading & triggering actions
|
||||
bool amiibo_load_file(const char *path);
|
||||
bool amiibo_scan_and_tap(const char *path);
|
||||
void amiibo_tap_tag(void);
|
||||
void amiibo_remove_tag(void);
|
||||
|
||||
// Timer tick called during menu / frame updates
|
||||
void amiibo_tick(void);
|
||||
|
||||
// Options
|
||||
void amiibo_set_auto_remove(bool enable);
|
||||
bool amiibo_get_auto_remove(void);
|
||||
void amiibo_set_auto_remove_delay_s(float seconds);
|
||||
float amiibo_get_auto_remove_delay_s(void);
|
||||
|
||||
// Tag inspection
|
||||
bool amiibo_has_tag(void);
|
||||
const AmiiboInfo *amiibo_get_current_info(void);
|
||||
const uint8_t *amiibo_get_raw_data(void);
|
||||
|
||||
// Horizon OS buffers extraction (called by NFP IPC/override services)
|
||||
void amiibo_get_tag_info(void *out_tag_info);
|
||||
void amiibo_get_model_info(void *out_model_info);
|
||||
void amiibo_get_common_info(void *out_common_info);
|
||||
void amiibo_get_register_info(void *out_register_info);
|
||||
int amiibo_get_application_area(void *out_buf, uint32_t max_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -22,8 +22,11 @@ struct AudioEffectDsp {
|
||||
};
|
||||
static int channel_ok(unsigned n) { return n==1 || n==2 || n==4 || n==6; }
|
||||
static unsigned sample_count(unsigned rate, float ms) { return (unsigned)nearbyintf(rate*ms); }
|
||||
/* The reference DelayLine wraps at CurrentSampleCount-1, including when its
|
||||
backing allocation is larger. Preserve this timing for sample comparisons. */
|
||||
/* The ring has `count` slots. The reference DelayLine wraps its cursor at
|
||||
CurrentSampleCount-1 while its taps index modulo CurrentSampleCount, so once
|
||||
per turn every tap reads a slot that is never written (a one-sample dropout
|
||||
per tap, audible as grain). The cursor and the taps share one ring length here;
|
||||
each delay is one sample longer than the reference. */
|
||||
static void line_set(DelayLine *d, unsigned n) { d->count=n<d->max?n:d->max; d->at=0; }
|
||||
static void line_init(DelayLine *d, unsigned rate, float ms) {
|
||||
d->max=sample_count(rate,ms); d->data=calloc((size_t)d->max+1,sizeof(float));
|
||||
@@ -32,12 +35,12 @@ static void line_init(DelayLine *d, unsigned rate, float ms) {
|
||||
static float line_read(const DelayLine *d) { return d->data[d->at]; }
|
||||
static float line_update(DelayLine *d,float v) {
|
||||
float old=line_read(d);d->data[d->at++]=v;
|
||||
if(d->at >= (d->count?d->count-1:0))d->at=0;
|
||||
if(d->at >= d->count)d->at=0;
|
||||
return old;
|
||||
}
|
||||
static float line_tap(const DelayLine *d,unsigned sample,int offset) {
|
||||
int at=(int)d->at-(int)sample-offset;
|
||||
if(at<0)at+=(int)d->count;
|
||||
if(d->count){at%=(int)d->count;if(at<0)at+=(int)d->count;}
|
||||
if(at<0 || (unsigned)at>d->max)rt_fatal("audio effect delay tap bounds");
|
||||
return d->data[at];
|
||||
}
|
||||
|
||||
+144
-46
@@ -5,10 +5,11 @@
|
||||
//
|
||||
// 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) is the queued audio the output waits for after start or underrun.
|
||||
// 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
|
||||
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
|
||||
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart. LASRecomp and
|
||||
@@ -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;
|
||||
@@ -90,16 +93,47 @@ static void report_once(Mixer *m, unsigned bit, const char *msg, int value) {
|
||||
// ---- output: SDL3 audio stream fed from the ring
|
||||
// How much audio the output waits for before it starts, and again after an underrun: TOTK_AUDIO_PREFILL_MS rounded up
|
||||
// to whole 5 ms render blocks. Returns -1 for a value outside 0..500 ms.
|
||||
// The Linux default is higher: at 20 ms PipeWire's queue (max(prefill, two 1024-sample periods)) underran
|
||||
// thousands of times within minutes in busy scenes, and 60 ms removed the underruns.
|
||||
#ifdef __linux__
|
||||
#define PREFILL_DEFAULT_MS 60
|
||||
#else
|
||||
#define PREFILL_DEFAULT_MS 20
|
||||
#endif
|
||||
static int prefill_blocks_for(const char *e) {
|
||||
if (!e || !*e) return 4;
|
||||
if (!e || !*e) return PREFILL_DEFAULT_MS / 5;
|
||||
char *end;
|
||||
double ms = strtod(e, &end);
|
||||
if (end == e || *end || !(ms >= 0) || ms > 500) return -1;
|
||||
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;
|
||||
@@ -114,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;
|
||||
@@ -124,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;
|
||||
@@ -140,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;
|
||||
@@ -157,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,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());
|
||||
@@ -192,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;
|
||||
@@ -201,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) {
|
||||
@@ -217,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);
|
||||
@@ -225,24 +265,51 @@ 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
|
||||
}
|
||||
|
||||
// TOTK_AUDIO_DUMP=<path>: the sink mix before the clamp as raw interleaved float32 stereo, and every 5 s one log line with
|
||||
// how many frames were rendered less than 1 ms after the one before (a burst refill). File writes on the render thread:
|
||||
// for diagnosis only.
|
||||
static void sink_dump(const float *raw, int frames) {
|
||||
static FILE *f;
|
||||
static int opened;
|
||||
static uint64_t last, window_start, total, bursts;
|
||||
if (!opened) {
|
||||
opened = 1;
|
||||
const char *path = cfg_get("TOTK_AUDIO_DUMP");
|
||||
if (path && *path) f = fopen(path, "wb");
|
||||
}
|
||||
if (!f) return;
|
||||
fwrite(raw, sizeof(float), (size_t)frames * 2, f);
|
||||
uint64_t now = kern_now_ns();
|
||||
total++;
|
||||
if (last && now - last < 1000000ull) bursts++;
|
||||
last = now;
|
||||
if (!window_start) window_start = now;
|
||||
if (now - window_start >= 5000000000ull) {
|
||||
rt_log("audio: dump: %llu frames in %.1f s, %llu rendered under 1 ms after the previous", (unsigned long long)total,
|
||||
(now - window_start) / 1e9, (unsigned long long)bursts);
|
||||
total = bursts = 0;
|
||||
window_start = now;
|
||||
fflush(f);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -290,9 +357,10 @@ void audio_mixer_open(AudioRenderer *r) {
|
||||
const char *pe = cfg_get("TOTK_AUDIO_PREFILL_MS");
|
||||
m->prefill = prefill_blocks_for(pe);
|
||||
if (m->prefill < 0) {
|
||||
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using 20", pe);
|
||||
m->prefill = 4;
|
||||
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);
|
||||
@@ -505,8 +573,25 @@ static void walk_step(Walk *w) { /* the same transitions as advance_voice in a
|
||||
walk_skip_empty(w);
|
||||
}
|
||||
/* Decodes `frames` output samples per channel into out[c][k]; the renderer advances the voice afterwards by the same amount. */
|
||||
static double voice_step(const AudioRenderer *r, const ServerVoice *v) {
|
||||
return v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
|
||||
}
|
||||
/* Source samples the voice consumes over `frames` output samples, counted with the exact accumulation voice_render steps by
|
||||
(float rounding makes `fraction + seconds * rate * pitch` differ from it by one sample on some frames, a seam click);
|
||||
leaves the fractional position in v->fraction. */
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames) {
|
||||
const double step = voice_step(r, v);
|
||||
double frac = v->fraction;
|
||||
uint64_t consumed = 0;
|
||||
for (int k = 0; k < frames; k++) {
|
||||
frac += step;
|
||||
while (frac >= 1.0) { frac -= 1.0; consumed++; }
|
||||
}
|
||||
v->fraction = frac;
|
||||
return consumed;
|
||||
}
|
||||
static void voice_render(AudioRenderer *r, ServerVoice *v, float out[6][MAX_FRAME], int frames) {
|
||||
const double step = v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
|
||||
const double step = voice_step(r, v);
|
||||
Walk w = {v, v->index, v->offset, v->loops_done, {0}, 0, v->adpcm.history[0], v->adpcm.history[1], v->adpcm.buffer, v->adpcm.at};
|
||||
memcpy(w.valid, v->valid, sizeof w.valid);
|
||||
walk_skip_empty(&w);
|
||||
@@ -545,6 +630,7 @@ static void mix_aux(AudioRenderer *r, int mix, const AudrenEffectIn *e) {
|
||||
uint8_t *send=ipc_guest(p.send_info,128+(uint64_t)p.capacity*4), *ret=ipc_guest(p.return_info,128+(uint64_t)p.capacity*4);
|
||||
uint32_t *sh=(uint32_t *)(send+64),*rh=(uint32_t *)(ret+64); int32_t *sd=(int32_t *)(send+128),*rd=(int32_t *)(ret+128);
|
||||
uint32_t wr=sh[1],read=rh[0],total=p.channels*(uint32_t)m->frame;
|
||||
if(e->enabled) report_once(m,6,"auxiliary effect in use, channels",(int)p.channels);
|
||||
for(unsigned c=0;c<p.channels;c++) {
|
||||
if(p.input[c]>=r->mixes[mix].buffer_count || p.output[c]>=r->mixes[mix].buffer_count)rt_fatal("auxiliary effect mix buffer");
|
||||
float *input=mixbuf(m,m->offset[mix]+p.input[c]),*output=mixbuf(m,m->offset[mix]+p.output[c]);
|
||||
@@ -793,30 +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;
|
||||
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) {
|
||||
@@ -855,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) {
|
||||
|
||||
@@ -24,8 +24,8 @@ static void delay_set(Delay *d, unsigned n) { if (n > d->cap) rt_fatal("reverb d
|
||||
static void delay_init(Delay *d, unsigned khz, float ms) { d->cap = samples(khz, ms); d->data = calloc(d->cap + 1, sizeof(float)); if (!d->data) rt_fatal("reverb delay allocation"); delay_set(d, d->cap); }
|
||||
static float delay_update(Delay *d, float v) {
|
||||
d->data[d->write++] = v; float out = d->data[d->read++];
|
||||
if (d->read >= d->cap) d->read = 0;
|
||||
if (d->write >= d->cap) d->write = 0;
|
||||
if (d->read > d->cap) d->read = 0;
|
||||
if (d->write > d->cap) d->write = 0;
|
||||
return out;
|
||||
}
|
||||
static float delay_tap(const Delay *d, unsigned at) { int i = (int)d->write - (int)at - 1; if (i < 0) i += (int)d->cap + 1; return d->data[i]; }
|
||||
|
||||
+21
-6
@@ -318,11 +318,9 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
|
||||
|
||||
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
|
||||
// or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls
|
||||
static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds) {
|
||||
static void advance_voice(AudioRenderer *r, ServerVoice *v) {
|
||||
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return;
|
||||
double want = v->fraction + out_seconds * v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0);
|
||||
uint64_t samples = (uint64_t)want;
|
||||
v->fraction = want - (double)samples;
|
||||
uint64_t samples = audio_voice_consumed(r, v, (int)r->param.sample_count);
|
||||
while (samples && v->valid[v->index]) {
|
||||
AudrenWaveBuffer *b = &v->buffers[v->index];
|
||||
uint64_t length = (uint64_t)(b->end - b->start), left = length - v->offset;
|
||||
@@ -354,7 +352,19 @@ static void render_thread(void *p) {
|
||||
plat_thread_priority(PLAT_PRIO_HIGHEST);
|
||||
const uint64_t frame_ns = (uint64_t)r->param.sample_count * 1000000000ull / (uint64_t)r->param.sample_rate;
|
||||
const double frame = (double)r->param.sample_count / r->param.sample_rate;
|
||||
uint64_t next = kern_now_ns();
|
||||
// When the device pulls a period (1024 samples on PipeWire, 4-5 frames) the ring drops below its target
|
||||
// and the frames would otherwise render back to back. On hardware the DSP renders one frame every 5 ms and
|
||||
// the game's audio thread works in between; AUX effects need that time to fill the return ring, or mix_aux
|
||||
// reads stale samples and the frame clicks at its seams. Device-paced frames stay at least 4/5 of a frame
|
||||
// apart, which still refills a period well before the device drains it. Linux only: on Windows SDL pulls
|
||||
// 480-sample periods, and a sink dump (TOTK_AUDIO_DUMP) of the bursts there showed no more seam spikes than
|
||||
// chance, so a wait on this thread buys nothing.
|
||||
#ifdef __linux__
|
||||
const uint64_t gap_ns = frame_ns * 4 / 5;
|
||||
#else
|
||||
const uint64_t gap_ns = 0;
|
||||
#endif
|
||||
uint64_t next = kern_now_ns(), last = 0;
|
||||
while (plat_cas32(&r->running, 0, 0)) {
|
||||
uint64_t now = kern_now_ns();
|
||||
int64_t pace = audio_mixer_pace_ns(r, now);
|
||||
@@ -362,6 +372,10 @@ static void render_thread(void *p) {
|
||||
plat_wait_ns((uint64_t)pace);
|
||||
continue;
|
||||
}
|
||||
if (pace == 0 && last && now - last < gap_ns) {
|
||||
plat_wait_ns(gap_ns - (now - last));
|
||||
continue;
|
||||
}
|
||||
if (pace == 0) {
|
||||
next = now + frame_ns; // where the timer goes on if the device stops pacing
|
||||
} else {
|
||||
@@ -369,11 +383,12 @@ static void render_thread(void *p) {
|
||||
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
|
||||
next += frame_ns;
|
||||
}
|
||||
last = kern_now_ns();
|
||||
plat_mutex_lock(&r->lock);
|
||||
if (r->state == 0) {
|
||||
voices_prepare(r);
|
||||
audio_mixer_render(r, frame);
|
||||
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i], frame);
|
||||
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i]);
|
||||
r->elapsed_frames++;
|
||||
uint64_t given = r->stop_released + r->empty_played + r->flushed;
|
||||
if ((rt_trace & RT_TRACE_IPC) && given != r->logged_released && r->elapsed_frames % 1000 == 0) {
|
||||
|
||||
@@ -184,5 +184,6 @@ void audio_mixer_render(AudioRenderer *r, double seconds); // after audio_voic
|
||||
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
|
||||
// device stalled).
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames); // source samples over one frame; advances v->fraction
|
||||
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
|
||||
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
|
||||
@@ -0,0 +1,111 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/bl_deswizzle.comp; do not edit.
|
||||
// source sha256 bd2065290e315e85d2fcdb609d812c15ba9a109d7506b3105850a5860677b0ef
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t bl_deswizzle_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000000d0, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0007000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000b, 0x0000001f, 0x00060010,
|
||||
0x00000004, 0x00000011, 0x00000040, 0x00000001, 0x00000001, 0x00040047, 0x0000000b, 0x0000000b,
|
||||
0x0000001a, 0x00030047, 0x00000010, 0x00000002, 0x00050048, 0x00000010, 0x00000000, 0x00000023,
|
||||
0x00000000, 0x00050048, 0x00000010, 0x00000001, 0x00000023, 0x00000004, 0x00050048, 0x00000010,
|
||||
0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x00000010, 0x00000003, 0x00000023, 0x0000000c,
|
||||
0x00050048, 0x00000010, 0x00000004, 0x00000023, 0x00000010, 0x00050048, 0x00000010, 0x00000005,
|
||||
0x00000023, 0x00000014, 0x00050048, 0x00000010, 0x00000006, 0x00000023, 0x00000018, 0x00050048,
|
||||
0x00000010, 0x00000007, 0x00000023, 0x0000001c, 0x00050048, 0x00000010, 0x00000008, 0x00000023,
|
||||
0x00000020, 0x00050048, 0x00000010, 0x00000009, 0x00000023, 0x00000024, 0x00050048, 0x00000010,
|
||||
0x0000000a, 0x00000023, 0x00000028, 0x00040047, 0x0000001f, 0x0000000b, 0x0000001b, 0x00040047,
|
||||
0x000000ad, 0x00000006, 0x00000010, 0x00030047, 0x000000ae, 0x00000002, 0x00040048, 0x000000ae,
|
||||
0x00000000, 0x00000019, 0x00050048, 0x000000ae, 0x00000000, 0x00000023, 0x00000000, 0x00030047,
|
||||
0x000000b0, 0x00000019, 0x00040047, 0x000000b0, 0x00000021, 0x00000001, 0x00040047, 0x000000b0,
|
||||
0x00000022, 0x00000000, 0x00040047, 0x000000c1, 0x00000006, 0x00000010, 0x00030047, 0x000000c2,
|
||||
0x00000002, 0x00040048, 0x000000c2, 0x00000000, 0x00000018, 0x00050048, 0x000000c2, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00030047, 0x000000c4, 0x00000018, 0x00040047, 0x000000c4, 0x00000021,
|
||||
0x00000000, 0x00040047, 0x000000c4, 0x00000022, 0x00000000, 0x00040047, 0x000000cd, 0x0000000b,
|
||||
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
|
||||
0x00000020, 0x00000000, 0x00040017, 0x00000009, 0x00000006, 0x00000003, 0x00040020, 0x0000000a,
|
||||
0x00000001, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000001, 0x0004002b, 0x00000006,
|
||||
0x0000000c, 0x00000001, 0x00040020, 0x0000000d, 0x00000001, 0x00000006, 0x000d001e, 0x00000010,
|
||||
0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006,
|
||||
0x00000006, 0x00000006, 0x00000006, 0x00040020, 0x00000011, 0x00000009, 0x00000010, 0x0004003b,
|
||||
0x00000011, 0x00000012, 0x00000009, 0x00040015, 0x00000013, 0x00000020, 0x00000001, 0x0004002b,
|
||||
0x00000013, 0x00000014, 0x0000000a, 0x00040020, 0x00000015, 0x00000009, 0x00000006, 0x0004002b,
|
||||
0x00000006, 0x00000019, 0x00000000, 0x0004002b, 0x00000006, 0x0000001d, 0x00000040, 0x0004003b,
|
||||
0x0000000a, 0x0000001f, 0x00000001, 0x0004002b, 0x00000013, 0x00000024, 0x00000002, 0x00020014,
|
||||
0x00000027, 0x0004002b, 0x00000013, 0x0000002d, 0x00000006, 0x0004002b, 0x00000013, 0x00000032,
|
||||
0x00000007, 0x0004002b, 0x00000006, 0x00000037, 0x00000020, 0x0004002b, 0x00000006, 0x00000048,
|
||||
0x00000005, 0x0004002b, 0x00000006, 0x0000004c, 0x0000001f, 0x0004002b, 0x00000013, 0x0000004f,
|
||||
0x00000008, 0x0004002b, 0x00000013, 0x00000052, 0x00000009, 0x0004002b, 0x00000006, 0x0000006a,
|
||||
0x00000004, 0x0004002b, 0x00000006, 0x0000006f, 0x00000002, 0x0004002b, 0x00000006, 0x00000078,
|
||||
0x00000008, 0x0004002b, 0x00000006, 0x00000083, 0x00000003, 0x0004002b, 0x00000013, 0x00000093,
|
||||
0x00000003, 0x0004002b, 0x00000013, 0x0000009b, 0x00000004, 0x0004002b, 0x00000013, 0x000000a4,
|
||||
0x00000005, 0x00040017, 0x000000ac, 0x00000006, 0x00000004, 0x0003001d, 0x000000ad, 0x000000ac,
|
||||
0x0003001e, 0x000000ae, 0x000000ad, 0x00040020, 0x000000af, 0x0000000c, 0x000000ae, 0x0004003b,
|
||||
0x000000af, 0x000000b0, 0x0000000c, 0x0004002b, 0x00000013, 0x000000b1, 0x00000000, 0x0004002b,
|
||||
0x00000013, 0x000000b2, 0x00000001, 0x0003001d, 0x000000c1, 0x000000ac, 0x0003001e, 0x000000c2,
|
||||
0x000000c1, 0x00040020, 0x000000c3, 0x0000000c, 0x000000c2, 0x0004003b, 0x000000c3, 0x000000c4,
|
||||
0x0000000c, 0x00040020, 0x000000c9, 0x0000000c, 0x000000ac, 0x0006002c, 0x00000009, 0x000000cd,
|
||||
0x0000001d, 0x0000000c, 0x0000000c, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x000300f7, 0x000000ce, 0x00000000, 0x000300fb, 0x00000019, 0x000000cf,
|
||||
0x000200f8, 0x000000cf, 0x00050041, 0x0000000d, 0x0000000e, 0x0000000b, 0x0000000c, 0x0004003d,
|
||||
0x00000006, 0x0000000f, 0x0000000e, 0x00050041, 0x00000015, 0x00000016, 0x00000012, 0x00000014,
|
||||
0x0004003d, 0x00000006, 0x00000017, 0x00000016, 0x00050084, 0x00000006, 0x00000018, 0x0000000f,
|
||||
0x00000017, 0x00050041, 0x0000000d, 0x0000001a, 0x0000000b, 0x00000019, 0x0004003d, 0x00000006,
|
||||
0x0000001b, 0x0000001a, 0x00050080, 0x00000006, 0x0000001c, 0x00000018, 0x0000001b, 0x00050084,
|
||||
0x00000006, 0x0000001e, 0x0000001c, 0x0000001d, 0x00050041, 0x0000000d, 0x00000020, 0x0000001f,
|
||||
0x00000019, 0x0004003d, 0x00000006, 0x00000021, 0x00000020, 0x00050080, 0x00000006, 0x00000022,
|
||||
0x0000001e, 0x00000021, 0x00050041, 0x00000015, 0x00000025, 0x00000012, 0x00000024, 0x0004003d,
|
||||
0x00000006, 0x00000026, 0x00000025, 0x000500ae, 0x00000027, 0x00000028, 0x00000022, 0x00000026,
|
||||
0x000300f7, 0x0000002a, 0x00000000, 0x000400fa, 0x00000028, 0x00000029, 0x0000002a, 0x000200f8,
|
||||
0x00000029, 0x000200f9, 0x000000ce, 0x000200f8, 0x0000002a, 0x00050041, 0x00000015, 0x0000002e,
|
||||
0x00000012, 0x0000002d, 0x0004003d, 0x00000006, 0x0000002f, 0x0000002e, 0x000500c4, 0x00000006,
|
||||
0x00000030, 0x0000000c, 0x0000002f, 0x00050041, 0x00000015, 0x00000033, 0x00000012, 0x00000032,
|
||||
0x0004003d, 0x00000006, 0x00000034, 0x00000033, 0x000500c4, 0x00000006, 0x00000035, 0x0000000c,
|
||||
0x00000034, 0x00050080, 0x00000006, 0x0000003c, 0x0000002f, 0x00000034, 0x000500c4, 0x00000006,
|
||||
0x0000003d, 0x00000037, 0x0000003c, 0x00050086, 0x00000006, 0x00000041, 0x00000022, 0x0000003d,
|
||||
0x00050089, 0x00000006, 0x00000045, 0x00000022, 0x0000003d, 0x000500c2, 0x00000006, 0x00000049,
|
||||
0x00000045, 0x00000048, 0x000500c7, 0x00000006, 0x0000004d, 0x00000045, 0x0000004c, 0x00050041,
|
||||
0x00000015, 0x00000050, 0x00000012, 0x0000004f, 0x0004003d, 0x00000006, 0x00000051, 0x00000050,
|
||||
0x00050041, 0x00000015, 0x00000053, 0x00000012, 0x00000052, 0x0004003d, 0x00000006, 0x00000054,
|
||||
0x00000053, 0x00050084, 0x00000006, 0x00000055, 0x00000051, 0x00000054, 0x00050086, 0x00000006,
|
||||
0x00000059, 0x00000041, 0x00000055, 0x00050089, 0x00000006, 0x0000005d, 0x00000041, 0x00000055,
|
||||
0x00050086, 0x00000006, 0x00000062, 0x0000005d, 0x00000051, 0x00050089, 0x00000006, 0x00000067,
|
||||
0x0000005d, 0x00000051, 0x00050084, 0x00000006, 0x0000006b, 0x00000067, 0x0000006a, 0x000500c2,
|
||||
0x00000006, 0x0000006d, 0x0000004d, 0x0000006a, 0x000500c7, 0x00000006, 0x0000006e, 0x0000006d,
|
||||
0x0000000c, 0x00050084, 0x00000006, 0x00000070, 0x0000006e, 0x0000006f, 0x00050080, 0x00000006,
|
||||
0x00000071, 0x0000006b, 0x00000070, 0x000500c2, 0x00000006, 0x00000073, 0x0000004d, 0x0000000c,
|
||||
0x000500c7, 0x00000006, 0x00000074, 0x00000073, 0x0000000c, 0x00050080, 0x00000006, 0x00000075,
|
||||
0x00000071, 0x00000074, 0x00050084, 0x00000006, 0x00000079, 0x00000062, 0x00000078, 0x00050084,
|
||||
0x00000006, 0x0000007b, 0x00000079, 0x00000030, 0x00050089, 0x00000006, 0x0000007e, 0x00000049,
|
||||
0x00000030, 0x00050084, 0x00000006, 0x0000007f, 0x0000007e, 0x00000078, 0x00050080, 0x00000006,
|
||||
0x00000080, 0x0000007b, 0x0000007f, 0x000500c2, 0x00000006, 0x00000082, 0x0000004d, 0x0000006f,
|
||||
0x000500c7, 0x00000006, 0x00000084, 0x00000082, 0x00000083, 0x00050084, 0x00000006, 0x00000085,
|
||||
0x00000084, 0x0000006f, 0x00050080, 0x00000006, 0x00000086, 0x00000080, 0x00000085, 0x000500c7,
|
||||
0x00000006, 0x00000088, 0x00000045, 0x0000000c, 0x00050080, 0x00000006, 0x00000089, 0x00000086,
|
||||
0x00000088, 0x00050084, 0x00000006, 0x0000008d, 0x00000059, 0x00000035, 0x00050086, 0x00000006,
|
||||
0x00000090, 0x00000049, 0x00000030, 0x00050080, 0x00000006, 0x00000091, 0x0000008d, 0x00000090,
|
||||
0x00050041, 0x00000015, 0x00000094, 0x00000012, 0x00000093, 0x0004003d, 0x00000006, 0x00000095,
|
||||
0x00000094, 0x000500ae, 0x00000027, 0x00000096, 0x00000075, 0x00000095, 0x000400a8, 0x00000027,
|
||||
0x00000097, 0x00000096, 0x000300f7, 0x00000099, 0x00000000, 0x000400fa, 0x00000097, 0x00000098,
|
||||
0x00000099, 0x000200f8, 0x00000098, 0x00050041, 0x00000015, 0x0000009c, 0x00000012, 0x0000009b,
|
||||
0x0004003d, 0x00000006, 0x0000009d, 0x0000009c, 0x000500ae, 0x00000027, 0x0000009e, 0x00000089,
|
||||
0x0000009d, 0x000200f9, 0x00000099, 0x000200f8, 0x00000099, 0x000700f5, 0x00000027, 0x0000009f,
|
||||
0x00000096, 0x0000002a, 0x0000009e, 0x00000098, 0x000400a8, 0x00000027, 0x000000a0, 0x0000009f,
|
||||
0x000300f7, 0x000000a2, 0x00000000, 0x000400fa, 0x000000a0, 0x000000a1, 0x000000a2, 0x000200f8,
|
||||
0x000000a1, 0x00050041, 0x00000015, 0x000000a5, 0x00000012, 0x000000a4, 0x0004003d, 0x00000006,
|
||||
0x000000a6, 0x000000a5, 0x000500ae, 0x00000027, 0x000000a7, 0x00000091, 0x000000a6, 0x000200f9,
|
||||
0x000000a2, 0x000200f8, 0x000000a2, 0x000700f5, 0x00000027, 0x000000a8, 0x0000009f, 0x00000099,
|
||||
0x000000a7, 0x000000a1, 0x000300f7, 0x000000aa, 0x00000000, 0x000400fa, 0x000000a8, 0x000000a9,
|
||||
0x000000aa, 0x000200f8, 0x000000a9, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000aa, 0x00050041,
|
||||
0x00000015, 0x000000b3, 0x00000012, 0x000000b2, 0x0004003d, 0x00000006, 0x000000b4, 0x000000b3,
|
||||
0x00050041, 0x00000015, 0x000000b6, 0x00000012, 0x0000009b, 0x0004003d, 0x00000006, 0x000000b7,
|
||||
0x000000b6, 0x00050084, 0x00000006, 0x000000b8, 0x00000091, 0x000000b7, 0x00050080, 0x00000006,
|
||||
0x000000ba, 0x000000b8, 0x00000089, 0x00050084, 0x00000006, 0x000000bd, 0x000000ba, 0x00000095,
|
||||
0x00050080, 0x00000006, 0x000000be, 0x000000b4, 0x000000bd, 0x00050080, 0x00000006, 0x000000c0,
|
||||
0x000000be, 0x00000075, 0x00050041, 0x00000015, 0x000000c5, 0x00000012, 0x000000b1, 0x0004003d,
|
||||
0x00000006, 0x000000c6, 0x000000c5, 0x00050080, 0x00000006, 0x000000c8, 0x000000c6, 0x00000022,
|
||||
0x00060041, 0x000000c9, 0x000000ca, 0x000000c4, 0x000000b1, 0x000000c8, 0x0004003d, 0x000000ac,
|
||||
0x000000cb, 0x000000ca, 0x00060041, 0x000000c9, 0x000000cc, 0x000000b0, 0x000000b1, 0x000000c0,
|
||||
0x0003003e, 0x000000cc, 0x000000cb, 0x000200f9, 0x000000ce, 0x000200f8, 0x000000ce, 0x000100fd,
|
||||
0x00010038,
|
||||
};
|
||||
+12
-7
@@ -4,7 +4,7 @@
|
||||
// The boot driver: the guest main thread runs the game's own nn::init::Start
|
||||
// with three host callbacks, the second of which runs DT_INIT of main, subsdk0 and sdk; Start then calls nnMain.
|
||||
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call, at the return address of
|
||||
// each in main (1.1.1, 1.2.1, 1.4.3):
|
||||
// each in main (1.1.0, 1.2.1, 1.4.3):
|
||||
// SetFocusHandlingMode main+0xeb8c14 (1.2.1: +0xe7a954, 1.4.3: +0xbdb48c) in nnMain, every module's constructor
|
||||
// flag reads 2
|
||||
// nvnBootstrapLoader main+0xebcdec (1.2.1: +0xe7eb2c, 1.4.3: +0xbe0360) in the graphics bring-up; logs the
|
||||
@@ -15,27 +15,29 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "cheats.h"
|
||||
#include "fs.h"
|
||||
#include "gfx.h"
|
||||
#include "mods.h"
|
||||
#include "nvn_api.h"
|
||||
#include "platform/platform.h"
|
||||
#include "vk.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define SYM_INIT_START "_ZN2nn4init5StartEmmPFvvES2_S2_"
|
||||
// main: return addresses of nnMain's SetFocusHandlingMode call (1.1.1 and 1.2.1 from this port's analysis of main;
|
||||
// main: return addresses of nnMain's SetFocusHandlingMode call (1.1.0 and 1.2.1 from this port's analysis of main;
|
||||
// 1.4.3 from MacNativeGames' scripts/verify_boot.py) and of the first nvnBootstrapLoader call, per game version.
|
||||
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : RT_V111 ? 0xeb8c14ull : 0xe7a954ull)
|
||||
#define NVN_RET (RT_V143 ? 0xbe0360ull : RT_V111 ? 0xebcdecull : 0xe7eb2cull)
|
||||
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : RT_V110 ? 0xeb8c14ull : 0xe7a954ull)
|
||||
#define NVN_RET (RT_V143 ? 0xbe0360ull : RT_V110 ? 0xebcdecull : 0xe7eb2cull)
|
||||
#define CTOR_DONE 2 // a module's constructor flag after its DT_INIT ran the init_array
|
||||
|
||||
// The init_array sizes (1.1.1 and 1.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check
|
||||
// The init_array sizes (1.1.0 and 1.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check
|
||||
// expects the same. The three tables carry the same rows, so cb_module_inits walks them by the size of one.
|
||||
typedef struct { const char *name; uint64_t entries; } InitCount;
|
||||
static const InitCount inits_111[] = {{"main", 10668}, {"subsdk0", 45}, {"sdk", 116}};
|
||||
static const InitCount inits_110[] = {{"main", 10668}, {"subsdk0", 45}, {"sdk", 116}};
|
||||
static const InitCount inits_121[] = {{"main", 10673}, {"subsdk0", 45}, {"sdk", 116}};
|
||||
static const InitCount inits_143[] = {{"main", 10717}, {"subsdk0", 45}, {"sdk", 83}};
|
||||
#define expected_inits (RT_V143 ? inits_143 : RT_V111 ? inits_111 : inits_121)
|
||||
#define expected_inits (RT_V143 ? inits_143 : RT_V110 ? inits_110 : inits_121)
|
||||
|
||||
static volatile int32_t inits_done, checkpoint_seen;
|
||||
uint64_t rt_guest_start_ns;
|
||||
@@ -305,6 +307,7 @@ void rt_frame_presented(const tex_t *t, uint64_t n) {
|
||||
fflush(stderr);
|
||||
fs_save_flush();
|
||||
vk_pipeline_save(); // no-op without the Vulkan back end
|
||||
playtime_save();
|
||||
prof_write();
|
||||
fflush(stderr);
|
||||
plat_exit(0);
|
||||
@@ -342,8 +345,10 @@ void rt_boot(void) {
|
||||
PlatThread *warm = plat_thread_new(prewarm_main, NULL, "prewarm");
|
||||
ldr_load(rt_config()->exefs);
|
||||
rt_patches_init();
|
||||
mods_exefs_switch(); // after the patch sets' settings: an enabled mod's ExeFS patches on
|
||||
frame_step_init();
|
||||
res_init();
|
||||
cheats_init(); // after the patch sets: a cheat's set follows its setting
|
||||
cb_addr[0] = hostfn_register("<rtld loader_ready>", cb_loader_ready);
|
||||
cb_addr[1] = hostfn_register("<rtld module_inits>", cb_module_inits);
|
||||
cb_addr[2] = hostfn_register("<rtld module_finis>", cb_module_finis);
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// Camera speed: TOTK_STICK_SENS (0.1 to 3, default 1, read at start) multiplies the game's own right-stick camera speeds.
|
||||
// The game's Options menu offers five camera sensitivities, Very Low to Very High; their turn rates are the array
|
||||
// StickSensitivity {0.8, 1.2, 1.6, 2.0, 2.4} in Component/PlayerCameraParam/PlayerCamera.game__component__
|
||||
// PlayerCameraParam.bgyml inside Pack/Actor/PlayerCamera.pack.zs. romfs.c serves a copy of that pack with every entry
|
||||
// multiplied (cam_pack_build), so the setting raises the camera's top speed as well as its speed at a partial tilt, at
|
||||
// any of the five sensitivities. The mouse camera reaches the game as a right-stick deflection (input.c, mouse_camera),
|
||||
// which divides it by the same factor (cam_speed_applied), so TOTK_MOUSE_SENS alone sets the mouse's speed.
|
||||
//
|
||||
// The pack is a SARC archive compressed with the dictionary pack.zsdic of Pack/ZsDic.pack.zs. A float in a BYML array
|
||||
// is stored in the array itself, so the multiplied values take the bytes of the old ones and no offset moves.
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
|
||||
#define MEMBER "Component/PlayerCameraParam/PlayerCamera.game__component__PlayerCameraParam.bgyml"
|
||||
#define MAX_RAW (64u << 20)
|
||||
|
||||
static float applied = 1.0f;
|
||||
|
||||
float cam_speed_wanted(void) {
|
||||
double v = cfg_double("TOTK_STICK_SENS");
|
||||
return (float)(v < 0.1 ? 0.1 : v > 3 ? 3 : v);
|
||||
}
|
||||
|
||||
float cam_speed_applied(void) { return applied; }
|
||||
|
||||
static int u32_at(const uint8_t *p, size_t n, size_t off, uint32_t *v) {
|
||||
if (off > n || n - off < 4) return 0;
|
||||
memcpy(v, p + off, 4);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// SARC member `name` of the archive p[0..n): its start and size; 0 when it is not there.
|
||||
static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size) {
|
||||
uint32_t data;
|
||||
uint16_t head, count, bom;
|
||||
if (n < 0x14 || memcmp(p, "SARC", 4)) return 0;
|
||||
memcpy(&head, p + 4, 2);
|
||||
memcpy(&bom, p + 6, 2);
|
||||
if (bom != 0xfeff || !u32_at(p, n, 12, &data) || head + 12u > n || memcmp(p + head, "SFAT", 4)) return 0;
|
||||
memcpy(&count, p + head + 6, 2);
|
||||
size_t names = head + 12 + (size_t)count * 16;
|
||||
if (names + 8 > n || memcmp(p + names, "SFNT", 4)) return 0;
|
||||
names += 8;
|
||||
for (unsigned i = 0; i < count; i++) {
|
||||
uint32_t attr, lo, hi;
|
||||
size_t e = head + 12 + (size_t)i * 16;
|
||||
if (!u32_at(p, n, e + 4, &attr) || !u32_at(p, n, e + 8, &lo) || !u32_at(p, n, e + 12, &hi)) return 0;
|
||||
size_t s = names + (size_t)(attr & 0xffffff) * 4, len = strlen(name);
|
||||
if (!(attr >> 24) || s + len + 1 > n || memcmp(p + s, name, len + 1)) continue;
|
||||
if (lo > hi || data + (size_t)hi > n) return 0;
|
||||
*at = data + lo;
|
||||
*size = hi - lo;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The StickSensitivity floats of the BYML p[0..n), multiplied by k; their count, 0 when the file has no such array.
|
||||
static int scale_sensitivity(uint8_t *p, size_t n, float k, float was[8]) {
|
||||
uint32_t keys, root, head;
|
||||
if (n < 16 || memcmp(p, "YB", 2) || !u32_at(p, n, 4, &keys) || !u32_at(p, n, 12, &root) ||
|
||||
!u32_at(p, n, root, &head) || (head & 0xff) != 0xc1 || !u32_at(p, n, keys, &head) || (head & 0xff) != 0xc2)
|
||||
return 0;
|
||||
uint32_t nkeys = head >> 8;
|
||||
if (!u32_at(p, n, root, &head)) return 0;
|
||||
for (uint32_t i = 0; i < head >> 8; i++) {
|
||||
uint32_t e, at, arr, ah;
|
||||
if (!u32_at(p, n, root + 4 + 8 * (size_t)i, &e) || (e & 0xffffff) >= nkeys ||
|
||||
!u32_at(p, n, keys + 4 + 4 * (size_t)(e & 0xffffff), &at))
|
||||
return 0;
|
||||
const char *name = (const char *)p + keys + at;
|
||||
if (keys + at + sizeof "StickSensitivity" > n || strcmp(name, "StickSensitivity")) continue;
|
||||
if (e >> 24 != 0xc0 || !u32_at(p, n, root + 8 + 8 * (size_t)i, &arr) || !u32_at(p, n, arr, &ah) ||
|
||||
(ah & 0xff) != 0xc0)
|
||||
return 0;
|
||||
uint32_t count = ah >> 8;
|
||||
size_t vals = arr + 4 + ((count + 3) & ~3u);
|
||||
if (!count || count > 8 || vals + 4 * (size_t)count > n) return 0;
|
||||
for (uint32_t j = 0; j < count; j++)
|
||||
if (p[arr + 4 + j] != 0xd2) return 0;
|
||||
for (uint32_t j = 0; j < count; j++) {
|
||||
float v;
|
||||
memcpy(&v, p + vals + 4 * j, 4);
|
||||
was[j] = v;
|
||||
v *= k;
|
||||
memcpy(p + vals + 4 * j, &v, 4);
|
||||
}
|
||||
return (int)count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cam_pack_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, void **out, size_t *out_n) {
|
||||
float k = cam_speed_wanted();
|
||||
*out = NULL;
|
||||
if (k == 1.0f) return 0;
|
||||
int ok = 0;
|
||||
uint8_t *dics = NULL, *raw = NULL;
|
||||
ZSTD_DDict *dd = NULL;
|
||||
ZSTD_CDict *cd = NULL;
|
||||
void *z = NULL;
|
||||
// The dictionaries: Pack/ZsDic.pack.zs is a SARC compressed without one.
|
||||
unsigned long long dn = ZSTD_getFrameContentSize(zsdic, zsdic_n);
|
||||
size_t dic_at, dic_n;
|
||||
if (dn == ZSTD_CONTENTSIZE_UNKNOWN || dn == ZSTD_CONTENTSIZE_ERROR || dn > MAX_RAW || !(dics = malloc((size_t)dn)) ||
|
||||
ZSTD_decompress(dics, (size_t)dn, zsdic, zsdic_n) != dn ||
|
||||
!sarc_member(dics, (size_t)dn, "pack.zsdic", &dic_at, &dic_n)) {
|
||||
rt_log("camera speed: no pack.zsdic in Pack/ZsDic.pack.zs; the camera keeps its own speed");
|
||||
goto done;
|
||||
}
|
||||
unsigned long long rn = ZSTD_getFrameContentSize(in, n);
|
||||
unsigned id = ZSTD_getDictID_fromFrame(in, n);
|
||||
dd = ZSTD_createDDict(dics + dic_at, dic_n);
|
||||
if (!dd || id != ZSTD_getDictID_fromDDict(dd) || rn == ZSTD_CONTENTSIZE_UNKNOWN ||
|
||||
rn == ZSTD_CONTENTSIZE_ERROR || rn > MAX_RAW || !(raw = malloc((size_t)rn))) {
|
||||
rt_log("camera speed: Pack/Actor/PlayerCamera.pack.zs is not a pack.zsdic frame with a size; the camera keeps its "
|
||||
"own speed");
|
||||
goto done;
|
||||
}
|
||||
ZSTD_DCtx *dctx = ZSTD_createDCtx();
|
||||
size_t got = dctx ? ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, in, n, dd) : 0;
|
||||
ZSTD_freeDCtx(dctx);
|
||||
size_t at, size;
|
||||
float was[8];
|
||||
int count = !ZSTD_isError(got) && got == rn && sarc_member(raw, (size_t)rn, MEMBER, &at, &size)
|
||||
? scale_sensitivity(raw + at, size, k, was) : 0;
|
||||
if (!count) {
|
||||
rt_log("camera speed: no StickSensitivity array in %s; the camera keeps its own speed", MEMBER);
|
||||
goto done;
|
||||
}
|
||||
cd = ZSTD_createCDict(dics + dic_at, dic_n, 9);
|
||||
size_t cap = ZSTD_compressBound((size_t)rn), zn = 0;
|
||||
ZSTD_CCtx *cctx = cd ? ZSTD_createCCtx() : NULL;
|
||||
if (cctx && (z = malloc(cap))) zn = ZSTD_compress_usingCDict(cctx, z, cap, raw, (size_t)rn, cd);
|
||||
ZSTD_freeCCtx(cctx);
|
||||
if (!z || !zn || ZSTD_isError(zn)) goto done;
|
||||
*out = z;
|
||||
*out_n = zn;
|
||||
z = NULL;
|
||||
ok = 1;
|
||||
rt_log("camera speed: x%.2f, StickSensitivity %.2f %.2f %.2f %.2f %.2f -> %.2f %.2f %.2f %.2f %.2f", k, was[0],
|
||||
count > 1 ? was[1] : 0, count > 2 ? was[2] : 0, count > 3 ? was[3] : 0, count > 4 ? was[4] : 0, was[0] * k,
|
||||
count > 1 ? was[1] * k : 0, count > 2 ? was[2] * k : 0, count > 3 ? was[3] * k : 0,
|
||||
count > 4 ? was[4] * k : 0);
|
||||
done:
|
||||
free(z);
|
||||
free(raw);
|
||||
free(dics);
|
||||
ZSTD_freeDDict(dd);
|
||||
ZSTD_freeCDict(cd);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void cam_pack_served(void) { applied = cam_speed_wanted(); }
|
||||
+176
-42
@@ -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,61 +29,84 @@ 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", "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", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DEVICE_PROFILE", "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", "GPL", "GPL_DRAW_BUILD", "GPU",
|
||||
"GPU_INDEX", "GPU_NAME", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "MEMORY", "MENU_LANG", "MENU_TEST",
|
||||
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PATCH_OUTPUT", "PATCH_SCENE", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
||||
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
|
||||
"SHADER_MEMO", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR", "SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_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",
|
||||
"WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLIMB_FIX", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
|
||||
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
||||
"DESC_TEMPLATE", "DESWIZZLE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "DYNAMIC_RES", "DYN_DELTA", "EXEFS", "FEEDBACK", "FIXED_RES_PATCH", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FRAME_QUEUE",
|
||||
"FREEZE_IDLE", "FREE_CORES",
|
||||
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
|
||||
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "KSPIN_US", "LANG", "LIB_PREBUILD",
|
||||
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
|
||||
"MEM_RESERVE_MIB", "MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_ASPECT", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
|
||||
"MV_SURVEY",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCORE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_BENCH", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_ASYNC", "REC_END", "REC_SPIN_US", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_CHUNKS", "REPLAY_CHUNK_DRAWS", "REPLAY_SKEW_US", "REPLAY_STATE_CHECK", "REPLAY_WORKERS",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
||||
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
|
||||
"SHADER_MEMO", "SHADOW_DEFER", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHADOW_SNAP", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
|
||||
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
|
||||
"SWKBD_ANSWER", "SYNC_AHEAD",
|
||||
"SWKBD_TEST", "TEST_OOM", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_BLOCK_MIB", "TEX_HEAP_EXTRA_MB", "TEX_MEMO", "TEX_RELEASED_MIB", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UB_OVERLAY", "UB_OVERLAY_OUT", "UPSCALER", "VB_USED", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VMA_BLOCK_MIB", "VOLUME", "VRAM_HEADROOM", "VRAM_LIMIT_MIB", "VS_OUTS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
};
|
||||
|
||||
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
|
||||
static const struct { const char *key, *section; } sections[] = {
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"},
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"FRAME_QUEUE", "frame rate"},
|
||||
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
||||
{"CLIMB_FIX", "frame rate"},
|
||||
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
||||
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"COLOR_PRESET", "display"},
|
||||
{"SHARPNESS", "display"}, {"DYNAMIC_RES", "display"}, {"ASPECT", "display"}, {"MOVIE_ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"COLOR_PRESET", "display"},
|
||||
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
|
||||
{"COLOR_GAMMA", "display"},
|
||||
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "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"},
|
||||
{"TEX_HEAP_EXTRA_MB", "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"}, {"STICK_DEADZONE", "input"}, {"MOUSE_SENS", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
||||
{"STICK_SENS", "input"},
|
||||
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"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"},
|
||||
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"FREE_CORES", "performance"},
|
||||
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"}, {"BIND_SHAKE", "input"},
|
||||
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"REPLAY_WORKERS", "performance"}, {"FREE_CORES", "performance"},
|
||||
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"TEX_RELEASED_MIB", "performance"}, {"TEX_BLOCK_MIB", "performance"}, {"VMA_BLOCK_MIB", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"VB_USED", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"SYNC_AHEAD", "performance"}, {"SHADOW_DEFER", "performance"}, {"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "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
|
||||
@@ -96,39 +120,68 @@ static const CfgMeta metas[] = {
|
||||
{"VK_PRESENT", "mailbox", "mailbox|fifo|immediate", LIVE}, {"GPU_INDEX", "", NULL, RESTART},
|
||||
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
|
||||
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
|
||||
{"DYNAMIC_RES", "1", "0|1", LIVE}, // resolution.c res_dynres_live
|
||||
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
|
||||
{"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},
|
||||
{"COLOR_CONTRAST", "100", "70..150", LIVE}, {"COLOR_SATURATION", "100", "50..200", LIVE},
|
||||
{"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_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},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART},
|
||||
{"MODS_REBUILD", "auto", "auto|off", RESTART},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART}, {"CLIMB_FIX", "1", "0|1", RESTART},
|
||||
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR", RESTART},
|
||||
{"OPERATION_MODE", "docked", "docked|handheld", RESTART},
|
||||
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
|
||||
// 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|xbox_prompts", LIVE},
|
||||
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"MOUSE_SENS", "1", "0..10", LIVE},
|
||||
{"MENU_LANG", "", "en|ru", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox", LIVE},
|
||||
{"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_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},
|
||||
{"REPLAY_WORKERS", "0", "0..16", RESTART}, {"REPLAY_CHUNKS", "0", "0..64", RESTART},
|
||||
{"REPLAY_CHUNK_DRAWS", "128", "1..100000", RESTART}, {"REPLAY_STATE_CHECK", "0", "0|1", RESTART},
|
||||
{"REPLAY_SKEW_US", "0", "0..100000", RESTART},
|
||||
{"SYNC_AHEAD", "0", "0|1", RESTART}, {"SHADOW_DEFER", "1", "0|1", RESTART},
|
||||
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
|
||||
// shaders and pipelines
|
||||
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "fullscreen", "strict|any|fullscreen", RESTART},
|
||||
{"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},
|
||||
{"TEX_RELEASED_MIB", "256", "0..65536", RESTART}, {"TEX_BLOCK_MIB", "64", "0..1024", RESTART}, {"VMA_BLOCK_MIB", "64", "0..1024", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
||||
// render fixes
|
||||
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
|
||||
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
|
||||
{"VB_USED", "1", "0|1", RESTART},
|
||||
{"DESC_TEMPLATE", "1", "0|1", RESTART},
|
||||
{"PUSH_SKIP", "1", "0|1", RESTART}, {"SPLIT_DRAWS", "64", "0..1000", RESTART},
|
||||
{"SPLIT_US", "1000", "0..20000", RESTART},
|
||||
@@ -137,14 +190,15 @@ 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},
|
||||
};
|
||||
|
||||
static int is_known(const char *key) {
|
||||
for (size_t i = 0; i < sizeof known / sizeof *known; i++)
|
||||
if (!strcmp(known[i], key)) return 1;
|
||||
return 0;
|
||||
return !strncmp(key, "CHEAT_", 6); // cheats.c and the cheat patch sets (tools/cheats.py)
|
||||
}
|
||||
|
||||
static void clear(void) {
|
||||
@@ -182,6 +236,24 @@ static void add(char *key, char *value, int line) {
|
||||
entries[n_entries++] = (CfgEntry){strdup(key), strdup(value), 0, 0};
|
||||
}
|
||||
|
||||
// 0.1.9 had the Xbox prompts as a pad layout: pad_layout = xbox_prompts is pad_layout = nintendo with prompts = xbox.
|
||||
// Read as those two, which the menu shows and the next cfg_save writes; a prompts line of the file's own wins. The
|
||||
// value still works from the environment (input_pad_layout_pick, input_prompts_pick).
|
||||
static void migrate(void) {
|
||||
CfgEntry *layout = NULL, *prompts = NULL;
|
||||
for (int i = 0; i < n_entries; i++) {
|
||||
if (!strcmp(entries[i].key, "PAD_LAYOUT")) layout = &entries[i];
|
||||
if (!strcmp(entries[i].key, "PROMPTS")) prompts = &entries[i];
|
||||
}
|
||||
if (!layout || strcasecmp(layout->value, "xbox_prompts")) return;
|
||||
free(layout->value);
|
||||
layout->value = strdup("nintendo");
|
||||
layout->dirty = 1;
|
||||
if (!prompts && n_entries < CFG_MAX_KEYS) entries[n_entries++] = (CfgEntry){strdup("PROMPTS"), strdup("xbox"), 1, 0};
|
||||
rt_log("cfg: totk.ini pad_layout = xbox_prompts is read as pad_layout = nintendo and prompts = %s",
|
||||
prompts ? prompts->value : "xbox");
|
||||
}
|
||||
|
||||
int cfg_load(const char *path) {
|
||||
clear();
|
||||
uint64_t size = 0;
|
||||
@@ -205,6 +277,7 @@ int cfg_load(const char *path) {
|
||||
p = eol + 1;
|
||||
}
|
||||
free(data);
|
||||
migrate();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -266,9 +339,68 @@ static const char *raw_get(const char *name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ---- the screen's shape (desktop): a primary display of another shape than 16:9 gives output_res a default of that
|
||||
// shape while neither the environment nor totk.ini sets it, so the first start fills the screen instead of a 16:9
|
||||
// picture between black bars. The sizes are the ones the installer lays the HUD out for (installer/steps.py
|
||||
// ASPECT_SIZES, ui/menu.cpp row_output_res): of the display's shape within 1%, the smallest at least 1080 high that fits
|
||||
// the display, else the largest that fits. A 16:9 display, an unknown one or one of another shape keeps 1920x1080.
|
||||
// Xbox and keyboard prompts have a folder for each of these sizes (romfs-patches-xbox-<w>x<h>, -xbox-label-<w>x<h>,
|
||||
// -pc-<w>x<h>, installer/steps.py Prepare): they keep 1920x1080 only when the one for the display's size is missing,
|
||||
// as in a source build without make_romfs_patches.py --prompts <style> --aspect or an install by an older setup,
|
||||
// where a default of another shape would trade the prompts the player chose for the Switch ones.
|
||||
static const unsigned SHAPED[][2] = {
|
||||
{2560, 1080}, {3440, 1440}, {3840, 1600}, {5120, 2160}, // 21:9
|
||||
{3840, 1080}, {5120, 1440}, // 32:9
|
||||
{1280, 800}, {1680, 1050}, {1920, 1200}, {2560, 1600}, // 16:10
|
||||
{1920, 1280}, {2160, 1440}, {2256, 1504}, {2880, 1920}, {3000, 2000}, // 3:2
|
||||
};
|
||||
static char screen_res[16]; // set once by cfg_init, "" for none
|
||||
// romfs-patches-xbox-, romfs-patches-xbox-label- and romfs-patches-pc-<screen_res> have a manifest (set once by
|
||||
// cfg_init)
|
||||
static const char *const screen_folders[3] = {"romfs-patches-xbox", "romfs-patches-xbox-label", "romfs-patches-pc"};
|
||||
static int screen_prompts[3];
|
||||
|
||||
// Whether the prompts in effect have their folder at screen_res. The same picks as input_map.c input_prompts_pick and
|
||||
// input_prompts_folder: an unknown or empty value is the Nintendo prompts, an unset one Xbox while the pad layout is
|
||||
// "xbox_prompts"; the Xbox prompts take romfs-patches-xbox with the Nintendo pad layout (or "xbox_prompts") and
|
||||
// romfs-patches-xbox-label with any other.
|
||||
static int shape_keeps_prompts(void) {
|
||||
const char *p = raw_get(PREFIX "PROMPTS"), *l = raw_get(PREFIX "PAD_LAYOUT");
|
||||
if (p && *p && !strcasecmp(p, "pc")) return screen_prompts[2];
|
||||
int xbox = p && *p ? !strcasecmp(p, "xbox") : l && !strcasecmp(l, "xbox_prompts");
|
||||
if (!xbox) return 1;
|
||||
int by_position = l && (!strcasecmp(l, "nintendo") || !strcasecmp(l, "xbox_prompts"));
|
||||
return screen_prompts[by_position ? 0 : 1];
|
||||
}
|
||||
|
||||
static void screen_default(void) {
|
||||
int sw, sh;
|
||||
if (!plat_screen_size(&sw, &sh)) return;
|
||||
double r = (double)sw / sh;
|
||||
if (fabs(r / (16.0 / 9.0) - 1) <= 0.01) return;
|
||||
int best = -1, fit = -1;
|
||||
for (int i = 0; i < (int)(sizeof SHAPED / sizeof *SHAPED); i++) {
|
||||
unsigned w = SHAPED[i][0], h = SHAPED[i][1];
|
||||
if (fabs((double)w / h / r - 1) > 0.01 || w > (unsigned)sw || h > (unsigned)sh) continue;
|
||||
if (fit < 0 || w > SHAPED[fit][0]) fit = i; // the largest that fits
|
||||
if (h >= 1080 && (best < 0 || w < SHAPED[best][0])) best = i;
|
||||
}
|
||||
if (best < 0) best = fit;
|
||||
if (best < 0) return;
|
||||
snprintf(screen_res, sizeof screen_res, "%ux%u", SHAPED[best][0], SHAPED[best][1]);
|
||||
for (int i = 0; i < 3; i++)
|
||||
screen_prompts[i] = romfs_shaped_prompts_ready(screen_folders[i], SHAPED[best][0], SHAPED[best][1]);
|
||||
if (!raw_get(PREFIX "OUTPUT_RES") && shape_keeps_prompts())
|
||||
rt_log("cfg: the display is %dx%d: output_res defaults to %s, its shape (totk.ini output_res sets another)", sw,
|
||||
sh, screen_res);
|
||||
}
|
||||
|
||||
const char *cfg_get(const char *name) {
|
||||
const char *v = raw_get(name);
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN) || !profile_of()) return v;
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN)) return v;
|
||||
if (!profile_of())
|
||||
return screen_res[0] && !strcasecmp(name + PREFIX_LEN, "OUTPUT_RES") && shape_keeps_prompts() ? screen_res
|
||||
: NULL;
|
||||
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
|
||||
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
|
||||
return NULL;
|
||||
@@ -463,6 +595,7 @@ static void puts_(Out *o, const char *s) { put(o, s, strlen(s)); }
|
||||
static const char *section_of(const char *key) {
|
||||
for (size_t i = 0; i < sizeof sections / sizeof *sections; i++)
|
||||
if (!strcmp(sections[i].key, key)) return sections[i].section;
|
||||
if (!strncmp(key, "CHEAT_", 6)) return "cheats"; // cheats.c: one key per cheat of the cheat file
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -642,6 +775,7 @@ static void load_ini(void) {
|
||||
|
||||
void cfg_init(void) {
|
||||
load_ini();
|
||||
if (!profile_of()) screen_default();
|
||||
if (profile_of())
|
||||
rt_log("cfg: device profile Steam Deck: defaults for the keys totk.ini does not set (TOTK_DEVICE_PROFILE=desktop "
|
||||
"turns it off)");
|
||||
|
||||
+86
-3
@@ -1,4 +1,5 @@
|
||||
// --probe-cfg: totk.ini parsing, the environment override and a missing file (cfg.c). Needs no game files.
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -229,10 +230,24 @@ int probe_cfg(void) {
|
||||
dn[KEY_MOUSE_R] = dn[KEY_Z] = 1;
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_Y | 1ull << B_ZL), "keys: right mouse button and Z both press ZL");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_T] = 1;
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_Y, "keys: T is Y too (the left button is A in the menus)");
|
||||
plat_setenv("TOTK_BIND_X", "key:t");
|
||||
input_map_configure();
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_X, "bind: bind_X = key:t takes T from Y's defaults");
|
||||
dn[KEY_T] = 0, dn[KEY_MOUSE_L] = 1;
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_Y, "bind: Y keeps the left mouse button");
|
||||
plat_setenv("TOTK_BIND_X", NULL);
|
||||
input_map_reset();
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_X] = dn[KEY_TAB] = dn[KEY_M] = dn[KEY_LCTRL] = dn[KEY_3] = 1;
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_ZR | 1ull << B_PLUS | 1ull << B_MINUS | 1ull << B_STICK_L | 1ull << B_RIGHT),
|
||||
"keys: X ZR, Tab Plus, M Minus, Left Ctrl stick click, 3 D-pad right");
|
||||
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");
|
||||
@@ -272,6 +287,22 @@ int probe_cfg(void) {
|
||||
"pad stick: a square gate's corner stays on the unit circle");
|
||||
input_pad_stick(0, 32767 / 2 + 1, 0.2f, st);
|
||||
CHECK(abs(st[1] - 32767 * 3 / 8) <= 3, "pad stick: half deflection past a 20% zone is (0.5 - 0.2) / 0.8 of full");
|
||||
plat_setenv("TOTK_STICK_DEADZONE", "12");
|
||||
plat_setenv("TOTK_LSTICK_DEADZONE", NULL);
|
||||
plat_setenv("TOTK_RSTICK_DEADZONE", NULL);
|
||||
CHECK(fabsf(input_pad_deadzone(0) - 0.12f) < 1e-6f && fabsf(input_pad_deadzone(1) - 0.12f) < 1e-6f,
|
||||
"pad dead zone: both sticks follow stick_deadzone while lstick/rstick are unset");
|
||||
plat_setenv("TOTK_RSTICK_DEADZONE", "20");
|
||||
CHECK(fabsf(input_pad_deadzone(0) - 0.12f) < 1e-6f && fabsf(input_pad_deadzone(1) - 0.20f) < 1e-6f,
|
||||
"pad dead zone: rstick_deadzone sets the right stick alone");
|
||||
plat_setenv("TOTK_LSTICK_DEADZONE", "80");
|
||||
CHECK(fabsf(input_pad_deadzone(0) - 0.5f) < 1e-6f, "pad dead zone: clamped to 50%");
|
||||
plat_setenv("TOTK_STICK_DEADZONE", NULL);
|
||||
plat_setenv("TOTK_LSTICK_DEADZONE", NULL);
|
||||
plat_setenv("TOTK_RSTICK_DEADZONE", NULL);
|
||||
CHECK(input_stick_percent(32767, 0) > 99.99 && input_stick_percent(32767, 0) < 100.01 &&
|
||||
input_stick_percent(0, 0) == 0 && fabs(input_stick_percent(3000, -4000) - 5000 * 100.0 / 32767) < 1e-9,
|
||||
"stick percent: an axis's end is 100%, the centre 0%, round in between");
|
||||
// Gamepad layouts (input_map.c)
|
||||
CHECK(input_pad_layout_pick("auto", 1) == PAD_LAYOUT_NINTENDO && input_pad_layout_pick(NULL, 0) == PAD_LAYOUT_XBOX,
|
||||
"pad layout: automatic is Nintendo for an east button labelled A, Xbox otherwise");
|
||||
@@ -279,7 +310,35 @@ int probe_cfg(void) {
|
||||
"pad layout: a set layout wins over the labels");
|
||||
CHECK(input_pad_layout_pick("xbox_prompts", 0) == PAD_LAYOUT_NINTENDO &&
|
||||
input_pad_layout_pick("Xbox_Prompts", 1) == PAD_LAYOUT_NINTENDO,
|
||||
"pad layout: Xbox prompts keep the Nintendo positions (only the prompts change, romfs.c overlay_init)");
|
||||
"pad layout: 0.1.9's xbox_prompts keeps the Nintendo positions (only the prompts change, romfs.c overlay_init)");
|
||||
// Button prompts (input_map.c)
|
||||
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"),
|
||||
"prompts: the game's own need no folder");
|
||||
CHECK(EQ(input_prompts_folder(PROMPTS_XBOX, "nintendo"), "romfs-patches-xbox") &&
|
||||
EQ(input_prompts_folder(PROMPTS_XBOX, "xbox_prompts"), "romfs-patches-xbox") &&
|
||||
EQ(input_prompts_folder(PROMPTS_XBOX, "xbox"), "romfs-patches-xbox-label") &&
|
||||
EQ(input_prompts_folder(PROMPTS_XBOX, "auto"), "romfs-patches-xbox-label") &&
|
||||
EQ(input_prompts_folder(PROMPTS_XBOX, NULL), "romfs-patches-xbox-label"),
|
||||
"prompts: the face buttons by position for the Nintendo layout, by label for xbox and auto");
|
||||
// romfs-patches-xbox shows A as B (tests/romfs_prompts_test.py checks the folders' contents). It is right exactly
|
||||
// where the layout puts A on the east button, which an Xbox pad labels B.
|
||||
input_map_reset();
|
||||
static const char *const layouts[] = {"nintendo", "xbox", "auto", "xbox_prompts"};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int a_east = input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_EAST] = 1}, input_pad_layout_pick(layouts[i], 0)) == 1ull << B_A;
|
||||
CHECK(a_east == EQ(input_prompts_folder(PROMPTS_XBOX, layouts[i]), "romfs-patches-xbox"),
|
||||
"prompts: the folder names A as B exactly when the layout puts A on the button labelled B");
|
||||
}
|
||||
uint8_t pd[PAD_COUNT] = {0};
|
||||
pd[PAD_SOUTH] = pd[PAD_NORTH] = 1;
|
||||
CHECK(input_map_pad_buttons(pd, PAD_LAYOUT_NINTENDO) == (1ull << B_B | 1ull << B_X) &&
|
||||
@@ -370,6 +429,24 @@ int probe_cfg(void) {
|
||||
plat_setenv("TOTK_FREE_CORES", "1");
|
||||
CHECK(cfg_bool("TOTK_FREE_CORES") == 1, "free_cores 1: on");
|
||||
plat_setenv("TOTK_FREE_CORES", NULL);
|
||||
// 0.1.9's pad_layout = xbox_prompts (cfg.c migrate): read as the Nintendo layout with the Xbox prompts, saved so
|
||||
plat_setenv("TOTK_PAD_LAYOUT", NULL);
|
||||
plat_setenv("TOTK_PROMPTS", NULL);
|
||||
static const char old_layout[] = "[input]\r\npad_layout = xbox_prompts\r\n";
|
||||
CHECK(plat_file_write(path, old_layout, sizeof old_layout - 1) && cfg_load(path) == 1 &&
|
||||
EQ(cfg_get("TOTK_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("TOTK_PROMPTS"), "xbox"),
|
||||
"0.1.9's pad_layout = xbox_prompts reads as nintendo with prompts = xbox");
|
||||
CHECK(cfg_save_to(path) == 1, "the read pad layout and prompts save");
|
||||
uint64_t saved_n = 0;
|
||||
char *saved = (char *)plat_file_read(path, &saved_n);
|
||||
CHECK(saved && strstr(saved, "pad_layout = nintendo") && strstr(saved, "prompts = xbox") && !strstr(saved, "xbox_prompts"),
|
||||
"the next save writes the pad layout and the prompts");
|
||||
free(saved);
|
||||
static const char old_layout_own[] = "[input]\r\npad_layout = xbox_prompts\r\nprompts = nintendo\r\n";
|
||||
CHECK(plat_file_write(path, old_layout_own, sizeof old_layout_own - 1) && cfg_load(path) == 1 &&
|
||||
EQ(cfg_get("TOTK_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("TOTK_PROMPTS"), "nintendo"),
|
||||
"a prompts line of the file's own wins over 0.1.9's pad_layout = xbox_prompts");
|
||||
plat_file_delete(path);
|
||||
cfg_load("cfg-probe-missing.ini");
|
||||
int listed = 0;
|
||||
for (int i = 0; cfg_meta_at(i); i++) {
|
||||
@@ -383,9 +460,10 @@ int probe_cfg(void) {
|
||||
int ok = 0;
|
||||
if (!m->values) ok = 1;
|
||||
else if (strstr(m->values, "..")) {
|
||||
long lo = strtol(m->values, NULL, 10), hi = strtol(strstr(m->values, "..") + 2, NULL, 10);
|
||||
// strtod: ranges such as 0.2..0.9 have fractional ends
|
||||
double lo = strtod(m->values, NULL), hi = strtod(strstr(m->values, "..") + 2, NULL);
|
||||
double d = strtod(m->def, NULL);
|
||||
ok = d >= (double)lo && d <= (double)hi;
|
||||
ok = d >= lo && d <= hi;
|
||||
} else {
|
||||
size_t dl = strlen(m->def);
|
||||
for (const char *v = m->values; *v;) {
|
||||
@@ -397,6 +475,11 @@ int probe_cfg(void) {
|
||||
if (!ok) fprintf(stderr, "FAIL cfg: default '%s' of %s is not one of its values '%s'\n", m->def, m->key, m->values), fails++;
|
||||
}
|
||||
CHECK(listed >= 30, "the defaults table was walked");
|
||||
// cfg_meta answers with the first row of a key: a second row would be dead, and Reset would visit the key twice
|
||||
for (int i = 0; cfg_meta_at(i); i++)
|
||||
for (int j = i + 1; cfg_meta_at(j); j++)
|
||||
if (!strcmp(cfg_meta_at(i)->key, cfg_meta_at(j)->key))
|
||||
fprintf(stderr, "FAIL cfg: %s has two rows in the defaults table\n", cfg_meta_at(i)->key), fails++;
|
||||
printf("probe-cfg: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,741 @@
|
||||
// Cheats of the mods' Atmosphere cheat files (cheats.h). Each enabled mod the port can use (mods.h) may hold
|
||||
// <build id>.txt in its cheats folder, the build id the first 8 bytes of main's NSO build id, as Atmosphere and the
|
||||
// emulators name it; the file name's case does not matter. Each cheat's switch is the setting TOTK_CHEAT_<hash>
|
||||
// (cheat_<hash> under [cheats] in totk.ini), the hash FNV-1a 32 of "<mod folder name>/<cheat name>", so two mods'
|
||||
// cheats of one name keep their own switches.
|
||||
//
|
||||
// Code cheats (opcode 0 writes into main's code) cannot be applied here: the game's code is lifted C. tools/cheats.py
|
||||
// turns the code cheats of every mod into patch sets cheat_<hash> when the game is built and writes
|
||||
// build-cache\cheats.status next to totk.exe, one line per cheat: name hash, content hash, kind, buttons, note
|
||||
// (tools/cheats.py has the format). A cheat whose status line is missing or has another content hash was added or
|
||||
// edited after the build: a data cheat runs anyway, a code cheat waits for the setup to run again.
|
||||
//
|
||||
// Data cheats run in a VM after Atmosphere's dmnt cheat VM (opcodes 0-A and C0-C3; C4 and the 0xF.. extensions are not
|
||||
// supported), every presented frame while their switch is on, with registers and saved registers cleared before each
|
||||
// cheat; the static registers (C3) stay, shared by all cheats, as dmnt keeps them. Memory is the
|
||||
// guest's, mapped at its own addresses: main's image is written with its pages made writable for the write (read-only
|
||||
// data included, as dmnt writes through the debugger), everything else after mem_query says the guest may read or write
|
||||
// it. Buttons are input_poll's HidNpadButton bits, the same as the cheat format's key masks; they read 0 while the menu
|
||||
// is open.
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "cheats.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "mods.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define SEP "\\"
|
||||
#else
|
||||
#define SEP "/"
|
||||
#endif
|
||||
|
||||
enum { STATUS_MAX = 160, OPS_PER_RUN = 1 << 20 };
|
||||
|
||||
typedef struct {
|
||||
CheatInfo info;
|
||||
uint32_t *words;
|
||||
int n;
|
||||
int dup; // 1 for the first cheat of its name in its file, 2 for the next ...
|
||||
uint32_t content; // FNV-1a 32 of its words as tools/cheats.py writes them
|
||||
int set; // patch set, -1 none
|
||||
volatile int32_t on;
|
||||
char note[STATUS_MAX];
|
||||
} Cheat;
|
||||
|
||||
static Cheat *g_cheats;
|
||||
static int g_n, g_cap, g_vm; // g_vm: some cheat needs cheats_frame
|
||||
static char g_file[64]; // "<build id>.txt"
|
||||
static char g_dir[PLAT_PATH_MAX]; // the folder of totk.exe, with a trailing separator
|
||||
static int g_files; // mods whose file was read
|
||||
static uint64_t g_static[256]; // C3's static registers: 0x00-0x7F loaded by cheats, 0x80-0xFF stored by them
|
||||
static uint64_t g_main, g_main_size, g_code_end; // main's image; its code with the padding up to .rodata
|
||||
|
||||
static uint32_t fnv(const char *s, size_t n) {
|
||||
uint32_t h = 0x811C9DC5u;
|
||||
for (size_t i = 0; i < n; i++) h = (h ^ (uint8_t)s[i]) * 0x01000193u;
|
||||
return h;
|
||||
}
|
||||
|
||||
// The folder of the executable, with a trailing separator.
|
||||
static int exe_dir(char *out, size_t n) {
|
||||
if (!plat_exe_path(out, n)) return 0;
|
||||
char *cut = plat_last_sep(out);
|
||||
if (!cut) return 0;
|
||||
cut[1] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int build_id(char out[17]) {
|
||||
char path[PLAT_PATH_MAX];
|
||||
snprintf(path, sizeof path, "%s/main", rt_config()->exefs);
|
||||
PlatFile *f = plat_file_open(path);
|
||||
if (!f) return 0;
|
||||
uint8_t head[0x48];
|
||||
int ok = plat_file_pread(f, head, sizeof head, 0) == sizeof head && !memcmp(head, "NSO0", 4);
|
||||
plat_file_close(f);
|
||||
for (int i = 0; ok && i < 8; i++) snprintf(out + 2 * i, 3, "%02X", head[0x40 + i]);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- opcodes
|
||||
|
||||
// Words of the opcode at w (left words remain), 0 when it is cut short.
|
||||
static int op_len(const uint32_t *w, int left) {
|
||||
uint32_t f = w[0];
|
||||
int t = f >> 28, width = (f >> 24) & 0xF, n;
|
||||
switch (t) {
|
||||
case 0: case 1: n = width == 8 ? 4 : 3; break;
|
||||
case 2: case 8: n = 1; break;
|
||||
case 3: n = width == 0 ? 2 : 1; break;
|
||||
case 4: case 6: n = 3; break;
|
||||
case 5: case 7: n = 2; break;
|
||||
case 9: n = 1 + (((f >> 8) & 0xF) ? (width == 8 ? 2 : 1) : 0); break;
|
||||
case 0xA: n = ((f >> 8) & 0xF) == 2 || ((f >> 8) & 0xF) == 4 || ((f >> 8) & 0xF) == 5 ? 2 : 1; break;
|
||||
case 0xC: // C0TcSX..: an address word for operand types 0 and 2, the value (two words at width 8) for 4
|
||||
if (((f >> 24) & 0xF) != 0) n = 1;
|
||||
else if (((f >> 8) & 0xF) == 0 || ((f >> 8) & 0xF) == 2) n = 2;
|
||||
else n = ((f >> 8) & 0xF) == 4 ? (((f >> 20) & 0xF) == 8 ? 3 : 2) : 1;
|
||||
break;
|
||||
default: n = 1; break;
|
||||
}
|
||||
return n <= left ? n : 0;
|
||||
}
|
||||
|
||||
// NULL when the VM runs the opcode, else why not (tools/cheats.py vm_ok).
|
||||
static const char *vm_unsupported(const uint32_t *w, char *buf, int n) {
|
||||
uint32_t f = w[0];
|
||||
int t = f >> 28, width = (f >> 24) & 0xF;
|
||||
int ext = f >> 24; // 0xC0-0xC3 run; C0's width is at bits 20-23 and its operand type (at most 5) at bits 8-11
|
||||
if (ext == 0xC0) t = -1, width = (f >> 20) & 0xF;
|
||||
if ((t > 0xA && (ext < 0xC1 || ext > 0xC3)) || (t == -1 && ((f >> 8) & 0xF) > 5) ||
|
||||
(t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
|
||||
snprintf(buf, n, "opcode %08X is not supported", f);
|
||||
return buf;
|
||||
}
|
||||
if ((t <= 1 || t == 5 || t == 6 || t == 7 || t == 9 || t == 0xA) && width != 1 && width != 2 && width != 4 &&
|
||||
width != 8) { // C0 too (t -1)
|
||||
snprintf(buf, n, "opcode %08X has a bad width", f);
|
||||
return buf;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The kind of a cheat the build did not see: heading, data, rebuild (it writes main's code) or unsupported.
|
||||
static CheatKind classify(Cheat *c) {
|
||||
int ops = 0, ends = 0;
|
||||
for (int i = 0; i < c->n;) {
|
||||
int len = op_len(c->words + i, c->n - i);
|
||||
if (!len) {
|
||||
snprintf(c->note, sizeof c->note, "opcode %08X is cut short", c->words[i]);
|
||||
return CHEAT_UNSUPPORTED;
|
||||
}
|
||||
uint32_t f = c->words[i];
|
||||
ops++;
|
||||
ends += f >> 28 == 2;
|
||||
if (f >> 28 == 0 && ((f >> 20) & 0xF) == 0) {
|
||||
uint64_t off = ((uint64_t)(f & 0xFF) << 32) | c->words[i + 1];
|
||||
if (off < g_code_end - g_main) return CHEAT_REBUILD;
|
||||
}
|
||||
if (vm_unsupported(c->words + i, c->note, sizeof c->note)) return CHEAT_UNSUPPORTED;
|
||||
i += len;
|
||||
}
|
||||
return ops == ends && ops <= 1 ? CHEAT_HEADING : CHEAT_DATA;
|
||||
}
|
||||
|
||||
// ---- guest memory
|
||||
|
||||
static uint64_t region(int m) {
|
||||
const RtGuestLayout *l = mem_layout();
|
||||
switch (m) {
|
||||
case 0: return g_main;
|
||||
case 1: return l->heap.base;
|
||||
case 2: return l->alias.base;
|
||||
case 3: return l->aslr.base;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int in_main(uint64_t a, unsigned n) { return a >= g_main && a + n <= g_main + g_main_size; }
|
||||
|
||||
static int guest_ok(uint64_t a, unsigned n, uint32_t perm) {
|
||||
if (in_main(a, n)) return 1;
|
||||
RtMemoryInfo mi;
|
||||
if (mem_query(a, &mi) || !mi.state || (mi.perm & perm) != perm) return 0;
|
||||
return a + n <= mi.base + mi.size;
|
||||
}
|
||||
|
||||
static int rd(uint64_t a, int width, uint64_t *v) {
|
||||
if (!a || !guest_ok(a, (unsigned)width, 1)) return 0;
|
||||
uint64_t x = 0;
|
||||
memcpy(&x, (const void *)(uintptr_t)a, (size_t)width);
|
||||
*v = x;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Writes n bytes into main's image a page at a time: a read-only page is writable for its part of the copy only.
|
||||
static void wr_main(uint64_t a, const uint8_t *from, int n) {
|
||||
while (n > 0) {
|
||||
uint64_t page = a & ~0xFFFull, end;
|
||||
int k = (int)(page + 0x1000 - a);
|
||||
if (k > n) k = n;
|
||||
int prot = plat_mem_state(page, page + 0x1000, &end);
|
||||
if (prot != PLAT_PROT_READ && prot != PLAT_PROT_RW) return;
|
||||
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return;
|
||||
memcpy((void *)(uintptr_t)a, from, (size_t)k);
|
||||
if (prot == PLAT_PROT_READ) plat_mem_protect(page, 0x1000, PLAT_PROT_READ);
|
||||
a += (uint64_t)k;
|
||||
from += k;
|
||||
n -= k;
|
||||
}
|
||||
}
|
||||
|
||||
static void wr(uint64_t a, int width, uint64_t v) {
|
||||
uint64_t now;
|
||||
if (!rd(a, width, &now) || !memcmp(&now, &v, (size_t)width)) return; // unchanged: no write, no protection change
|
||||
void *p = (void *)(uintptr_t)a;
|
||||
if (in_main(a, (unsigned)width)) {
|
||||
wr_main(a, (const uint8_t *)&v, width);
|
||||
} else if (guest_ok(a, (unsigned)width, 2)) {
|
||||
memcpy(p, &v, (size_t)width); // a frozen region thaws on the write (memory.c)
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t masked(int width, uint64_t v) { return width >= 8 ? v : v & ((1ull << (8 * width)) - 1); }
|
||||
|
||||
static uint64_t arith(int op, uint64_t a, uint64_t b) {
|
||||
switch (op) {
|
||||
case 0: return a + b;
|
||||
case 1: return a - b;
|
||||
case 2: return a * b;
|
||||
case 3: return b < 64 ? a << b : 0;
|
||||
case 4: return b < 64 ? a >> b : 0;
|
||||
case 5: return a & b;
|
||||
case 6: return a | b;
|
||||
case 7: return ~a;
|
||||
case 8: return a ^ b;
|
||||
default: return a; // 9: move
|
||||
}
|
||||
}
|
||||
|
||||
static int compare(int cond, uint64_t a, uint64_t b) {
|
||||
switch (cond) {
|
||||
case 1: return a > b;
|
||||
case 2: return a >= b;
|
||||
case 3: return a < b;
|
||||
case 4: return a <= b;
|
||||
case 5: return a == b;
|
||||
case 6: return a != b;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// From i (after a failed condition, or an else reached while running), the opcode to continue at: after the
|
||||
// matching else (else_ok) or end. Conditions 1, 8 and C0 open a block, as in Atmosphere's dmnt
|
||||
// (stratosphere/dmnt/source/cheat/impl/dmnt_cheat_vm.cpp, CheatVirtualMachine::SkipConditionalBlock), which steps
|
||||
// over whole opcodes so that an operand word reading 20000000 ends nothing.
|
||||
static int skip(const uint32_t *w, int n, int i, int else_ok) {
|
||||
int depth = 0;
|
||||
while (i < n) {
|
||||
int len = op_len(w + i, n - i);
|
||||
if (!len) return n;
|
||||
uint32_t f = w[i];
|
||||
int t = f >> 28;
|
||||
i += len;
|
||||
if (t == 1 || t == 8 || f >> 24 == 0xC0) depth++;
|
||||
else if (t == 2 && ((f >> 24) & 0xF) == 1) { // else; any other 2x000000 ends, as dmnt decodes is_else
|
||||
if (depth == 0 && else_ok) return i;
|
||||
} else if (t == 2 && depth-- == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static uint64_t imm64(const uint32_t *w) { return (uint64_t)w[0] << 32 | w[1]; }
|
||||
|
||||
// C0TcSX..: if register S <cond c> operand X, all cut to width T; the operand as Atmosphere's dmnt reads it
|
||||
// (stratosphere/dmnt/source/cheat/impl/dmnt_cheat_vm.cpp, CheatVirtualMachine::DecodeNextOpcode and Execute,
|
||||
// BeginRegisterConditionalBlock). X: 0 region M + a, 1 region M + register r, 2 register R + a, 3 register R +
|
||||
// register r, 4 the value V, 5 register R. M, R at bits 4-7, r and the top bits of a at bits 0-3. A read the guest
|
||||
// cannot make compares as 0: dmnt ignores the read's result and compares the zero it started with.
|
||||
static int reg_cond(const uint32_t *w, const uint64_t *r) {
|
||||
uint32_t f = w[0];
|
||||
int width = (f >> 20) & 0xF, cond = (f >> 16) & 0xF, a4 = (f >> 4) & 0xF, a0 = f & 0xF;
|
||||
uint64_t src = masked(width, r[(f >> 12) & 0xF]), b = 0, addr;
|
||||
switch ((f >> 8) & 0xF) { // w[1] exists for types 0, 2 and 4 only (op_len)
|
||||
case 0: addr = region(a4) + (((uint64_t)a0 << 32) | w[1]); break;
|
||||
case 1: addr = region(a4) + r[a0]; break;
|
||||
case 2: addr = r[a4] + (((uint64_t)a0 << 32) | w[1]); break;
|
||||
case 3: addr = r[a4] + r[a0]; break;
|
||||
case 4: return compare(cond, src, width == 8 ? imm64(w + 1) : masked(width, w[1]));
|
||||
default: return compare(cond, src, masked(width, r[a4]));
|
||||
}
|
||||
if (!rd(addr, width, &b)) b = 0;
|
||||
return compare(cond, src, b);
|
||||
}
|
||||
|
||||
static void run(const Cheat *c, uint64_t keys) {
|
||||
uint64_t r[16] = {0}, saved[16] = {0};
|
||||
int loop[16] = {0};
|
||||
const uint32_t *w = c->words;
|
||||
int n = c->n, budget = OPS_PER_RUN;
|
||||
for (int i = 0; i < n && budget-- > 0;) {
|
||||
int len = op_len(w + i, n - i);
|
||||
if (!len) return;
|
||||
uint32_t f = w[i];
|
||||
int t = f >> 28, width = (f >> 24) & 0xF, x = (f >> 20) & 0xF, y = (f >> 16) & 0xF, z = (f >> 12) & 0xF;
|
||||
uint64_t rel = ((uint64_t)(f & 0xFF) << 32) | (len > 1 ? w[i + 1] : 0), v;
|
||||
switch (t) {
|
||||
case 0: // 0TMR00AA AAAAAAAA V..: store a value at region M + A + register R
|
||||
wr(region(x) + rel + r[y], width, width == 8 ? imm64(w + i + 2) : w[i + 2]);
|
||||
break;
|
||||
case 1: // 1TMC00AA AAAAAAAA V..: if (region M + A) <cond C> V
|
||||
if (!rd(region(x) + rel, width, &v) || !compare(y, v, width == 8 ? imm64(w + i + 2) : w[i + 2])) {
|
||||
i = skip(w, n, i + len, 1);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2: // 20000000 end, 21000000 else: an else reached while running skips to the end
|
||||
if (width == 1) {
|
||||
i = skip(w, n, i + len, 0);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 3: // 300R0000 VVVVVVVV loop start, 310R0000 loop end
|
||||
if (width == 0) {
|
||||
r[y] = w[i + 1];
|
||||
loop[y] = i + len;
|
||||
} else if (r[y] && --r[y]) {
|
||||
i = loop[y];
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 4: // 400R0000 V V: register R = V
|
||||
r[y] = imm64(w + i + 1);
|
||||
break;
|
||||
case 5: // 5TMRI0AA AAAAAAAA: R = (I ? R : region M) + A, read
|
||||
if (!rd(z ? r[y] + rel : region(x) + rel, width, &v)) return; // a broken pointer chain: nothing more
|
||||
r[y] = v;
|
||||
break;
|
||||
case 6: { // 6T0RIor0 V V: store V at R (+ register r); I: R += width
|
||||
uint64_t a = r[y] + (((f >> 8) & 0xF) ? r[(f >> 4) & 0xF] : 0);
|
||||
wr(a, width, imm64(w + i + 1));
|
||||
if (z) r[y] += (uint64_t)width;
|
||||
break;
|
||||
}
|
||||
case 7: // 7T0RC000 VVVVVVVV: R = R <op C> V
|
||||
r[y] = masked(width, arith(z, r[y], w[i + 1]));
|
||||
break;
|
||||
case 8: // 8kkkkkkk: if the buttons are held
|
||||
if ((keys & (f & 0x0FFFFFFF)) != (f & 0x0FFFFFFF)) {
|
||||
i = skip(w, n, i + len, 1);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 9: { // 9TCRS0s0 / 9TCRS100 V..: R = S <op C> (s or V)
|
||||
uint64_t b = (f >> 8) & 0xF ? (width == 8 ? imm64(w + i + 1) : w[i + 1]) : r[(f >> 4) & 0xF];
|
||||
r[y] = masked(width, arith(x, r[z], b));
|
||||
break;
|
||||
}
|
||||
case 0xA: { // ATSRIOxa (aaaaaaaa): store register S at an address of type O
|
||||
int o = (f >> 8) & 0xF, m = (f >> 4) & 0xF;
|
||||
uint64_t imm = ((uint64_t)(f & 0xF) << 32) | (len > 1 ? w[i + 1] : 0), a;
|
||||
switch (o) {
|
||||
case 0: a = r[y]; break;
|
||||
case 1: a = r[y] + r[m]; break;
|
||||
case 2: a = r[y] + imm; break;
|
||||
case 3: a = region(m) + r[y]; break;
|
||||
case 4: a = region(m) + imm; break;
|
||||
default: a = region(m) + r[y] + imm; break;
|
||||
}
|
||||
wr(a, width, r[x]);
|
||||
if (z) r[y] += (uint64_t)width;
|
||||
break;
|
||||
}
|
||||
case 0xC: // C1-C3 as dmnt's Execute runs SaveRestoreRegister(Mask) and ReadWriteStaticRegister
|
||||
switch (f >> 24) {
|
||||
case 0xC0:
|
||||
if (!reg_cond(w + i, r)) {
|
||||
i = skip(w, n, i + len, 1);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 0xC1: { // C10D0Sx0: x 0 register D = saved S, 1 saved D = register S, 2 saved D = 0,
|
||||
// 3 register D = 0
|
||||
int d = (f >> 16) & 0xF, s = (f >> 8) & 0xF, op = (f >> 4) & 0xF;
|
||||
if (op == 1) saved[d] = r[s];
|
||||
else if (op == 2) saved[d] = 0;
|
||||
else if (op == 3) r[d] = 0;
|
||||
else r[d] = saved[s]; // dmnt restores for any other x too
|
||||
break;
|
||||
}
|
||||
case 0xC2: { // C2x0XXXX: for each register in the mask X, x as C1 with D = S = that register
|
||||
int op = (f >> 20) & 0xF;
|
||||
for (int k = 0; k < 16; k++) {
|
||||
if (!(f >> k & 1)) continue;
|
||||
if (op == 1) saved[k] = r[k];
|
||||
else if (op == 2) saved[k] = 0;
|
||||
else if (op == 3) r[k] = 0;
|
||||
else r[k] = saved[k];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xC3: { // C3000XXx: static register XX < 0x80 into register x, else register x into it
|
||||
int s = (f >> 4) & 0xFF;
|
||||
if (s < 0x80) r[f & 0xF] = g_static[s];
|
||||
else g_static[s] = r[f & 0xF];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the file
|
||||
|
||||
static char *read_text(const char *path) {
|
||||
uint64_t n = 0;
|
||||
char *s = (char *)plat_file_read(path, &n);
|
||||
if (s && n >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) memmove(s, s + 3, (size_t)n - 2); // a UTF-8 BOM
|
||||
return s;
|
||||
}
|
||||
|
||||
static void add_word(Cheat *c, uint32_t v, int *cap) {
|
||||
if (c->n == *cap) {
|
||||
*cap = *cap ? *cap * 2 : 16;
|
||||
c->words = (uint32_t *)realloc(c->words, sizeof *c->words * (size_t)*cap);
|
||||
if (!c->words) rt_fatal("cheats: out of memory");
|
||||
}
|
||||
c->words[c->n++] = v;
|
||||
}
|
||||
|
||||
// Appends the cheats of one mod's file; mod: the mod's folder name (kept).
|
||||
static void parse(char *s, const char *mod) {
|
||||
int first = g_n, wcap = 0;
|
||||
for (char *line = s; line && *line;) {
|
||||
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
||||
if (end) *end = 0;
|
||||
while (*line == ' ' || *line == '\t') line++;
|
||||
size_t len = strlen(line);
|
||||
while (len && (line[len - 1] == '\r' || line[len - 1] == ' ' || line[len - 1] == '\t')) line[--len] = 0;
|
||||
if (len >= 2 && (line[0] == '[' || line[0] == '{') && (line[len - 1] == ']' || line[len - 1] == '}')) {
|
||||
if (g_n == g_cap) {
|
||||
g_cap = g_cap ? g_cap * 2 : 32;
|
||||
g_cheats = (Cheat *)realloc(g_cheats, sizeof *g_cheats * (size_t)g_cap);
|
||||
if (!g_cheats) rt_fatal("cheats: out of memory");
|
||||
}
|
||||
Cheat *c = &g_cheats[g_n++];
|
||||
memset(c, 0, sizeof *c);
|
||||
line[len - 1] = 0;
|
||||
char *name = line + 1;
|
||||
while (*name == ' ' || *name == '\t') name++;
|
||||
size_t k = strlen(name);
|
||||
while (k && (name[k - 1] == ' ' || name[k - 1] == '\t')) name[--k] = 0;
|
||||
c->info.name = strdup(name);
|
||||
c->info.mod = mod;
|
||||
c->info.kind = line[0] == '{' ? CHEAT_MASTER : CHEAT_HEADING;
|
||||
c->set = -1;
|
||||
c->dup = 1; // a repeated name gets "#2", "#3" ... (tools/cheats.py parse)
|
||||
for (int j = first; j < g_n - 1; j++) c->dup += !strcmp(g_cheats[j].info.name, c->info.name);
|
||||
wcap = 0;
|
||||
} else if (g_n > first) {
|
||||
for (char *tok = strtok(line, " \t"); tok; tok = strtok(NULL, " \t")) {
|
||||
char *e;
|
||||
unsigned long v = strtoul(tok, &e, 16);
|
||||
if (e - tok == 8 && !*e) add_word(&g_cheats[g_n - 1], (uint32_t)v, &wcap);
|
||||
}
|
||||
}
|
||||
line = next;
|
||||
}
|
||||
}
|
||||
|
||||
// The file g_file in folder, its name in any case, into out; 0 when there is none.
|
||||
typedef struct {
|
||||
const char *dir;
|
||||
char *out;
|
||||
size_t cap;
|
||||
int found;
|
||||
} FindFile;
|
||||
static int find_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
FindFile *f = (FindFile *)ctx;
|
||||
if (kind == PLAT_FS_FILE && !strcasecmp(name, g_file) &&
|
||||
snprintf(f->out, f->cap, "%s/%s", f->dir, name) < (int)f->cap)
|
||||
f->found = 1;
|
||||
return !f->found;
|
||||
}
|
||||
static int find_file(const char *folder, char *out, size_t cap) {
|
||||
FindFile f = {folder, out, cap, 0};
|
||||
plat_fs_list(folder, find_cb, &f);
|
||||
return f.found;
|
||||
}
|
||||
|
||||
// The status line of key, its fields split in place; 0 when none.
|
||||
static int status_of(char *status, const char *key, char **fields) {
|
||||
for (char *line = status; line && *line;) {
|
||||
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
||||
if (!strncmp(line, key, 8) && line[8] == '\t') {
|
||||
static char copy[512];
|
||||
size_t n = end ? (size_t)(end - line) : strlen(line);
|
||||
if (n >= sizeof copy) n = sizeof copy - 1;
|
||||
memcpy(copy, line, n);
|
||||
copy[n] = 0;
|
||||
if (n && copy[n - 1] == '\r') copy[n - 1] = 0;
|
||||
int k = 0;
|
||||
fields[k++] = copy;
|
||||
for (char *p = copy; *p && k < 5; p++)
|
||||
if (*p == '\t') *p = 0, fields[k++] = p + 1;
|
||||
while (k < 5) fields[k++] = (char *)"";
|
||||
return 1;
|
||||
}
|
||||
line = next;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The menu's font has no numero sign (U+2116), which cheat names often start with: they show "No" instead.
|
||||
static void plain_name(char *s) {
|
||||
for (char *p; (p = strstr(s, "\xE2\x84\x96")) != NULL;) {
|
||||
p[0] = 'N', p[1] = 'o';
|
||||
memmove(p + 2, p + 3, strlen(p + 3) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- rebuild: code cheats and ExeFS patches the build lacks
|
||||
// The setup writes build-cache\rebuild.txt (installer/steps.py write_rebuild_command), the command that starts its
|
||||
// window in rebuild mode on this install folder, one argument per line. When an enabled mod has code cheats this build
|
||||
// lacks (CHEAT_REBUILD) or ExeFS patch files it lacks (mods_exefs_pending), the game runs it and exits; the setup lifts
|
||||
// and compiles them in and starts the game again.
|
||||
// build-cache\rebuild.skip holds the key of the cheats a rebuild was started for: a rebuild that failed or was
|
||||
// cancelled leaves them unbuilt, and the same set does not start it again. TOTK_MODS_REBUILD=off never starts it.
|
||||
// Scripted runs (--seconds, --boot) never start it.
|
||||
static void rebuild_if_needed(const char *dir) {
|
||||
uint32_t key = 0x811C9DC5u;
|
||||
int need = 0;
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
const Cheat *c = &g_cheats[i];
|
||||
if (c->info.kind != CHEAT_REBUILD) continue;
|
||||
need++;
|
||||
key = (key ^ fnv(c->info.key, strlen(c->info.key))) * 0x01000193u;
|
||||
key = (key ^ c->content) * 0x01000193u;
|
||||
}
|
||||
int patches = mods_exefs_pending(&key);
|
||||
if (!need && !patches) return;
|
||||
char cmd_path[PLAT_PATH_MAX + 64], skip_path[PLAT_PATH_MAX + 64], skip[16] = "", want[16];
|
||||
snprintf(cmd_path, sizeof cmd_path, "%sbuild-cache" SEP "rebuild.txt", dir);
|
||||
snprintf(skip_path, sizeof skip_path, "%sbuild-cache" SEP "rebuild.skip", dir);
|
||||
snprintf(want, sizeof want, "%08X", key);
|
||||
char *old = read_text(skip_path);
|
||||
if (old) snprintf(skip, sizeof skip, "%.8s", old);
|
||||
free(old);
|
||||
const char *why = !strcmp(cfg_value("TOTK_MODS_REBUILD"), "off") ? "mods_rebuild = off"
|
||||
: rt_config()->seconds || rt_config()->boot ? "a scripted run"
|
||||
: !strcmp(skip, want) ? "the last rebuild for them failed or was cancelled"
|
||||
: NULL;
|
||||
char *cmd = why ? NULL : read_text(cmd_path);
|
||||
if (!why && !cmd) why = "no build-cache/rebuild.txt (a build without the setup)";
|
||||
if (why) {
|
||||
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; no rebuild: %s", need,
|
||||
patches, why);
|
||||
free(cmd);
|
||||
return;
|
||||
}
|
||||
const char *argv[32];
|
||||
int argc = 0;
|
||||
for (char *line = cmd; line && *line && argc < 31;) {
|
||||
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
||||
if (end) *end = 0;
|
||||
size_t n = strlen(line);
|
||||
if (n && line[n - 1] == '\r') line[n - 1] = 0;
|
||||
if (*line) argv[argc++] = line;
|
||||
line = next;
|
||||
}
|
||||
argv[argc] = NULL;
|
||||
if (argc < 2 || !plat_file_write(skip_path, want, strlen(want))) {
|
||||
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; cannot start the "
|
||||
"rebuild (%s)", need, patches, cmd_path);
|
||||
free(cmd);
|
||||
return;
|
||||
}
|
||||
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; starting the rebuild: %s",
|
||||
need, patches, argv[0]);
|
||||
int ok = plat_spawn(argv, dir);
|
||||
free(cmd);
|
||||
if (!ok) {
|
||||
rt_log("cheats: the rebuild did not start; the game starts with the code it has");
|
||||
return;
|
||||
}
|
||||
prof_write(); // plat_exit runs no atexit handlers
|
||||
plat_exit(0);
|
||||
}
|
||||
|
||||
void cheats_init(void) {
|
||||
const RtLoaded *m = ldr_count() ? ldr_module(0) : NULL;
|
||||
char dir[PLAT_PATH_MAX], bid[17], path[2400]; // path: a mod's cheats folder (ModInfo.cheats) + the file
|
||||
if (!m || !exe_dir(dir, sizeof dir) || !build_id(bid)) return;
|
||||
snprintf(g_dir, sizeof g_dir, "%s", dir);
|
||||
g_main = m->base;
|
||||
g_main_size = m->size;
|
||||
g_code_end = m->base + ((m->text_end + 0xFFF) & ~0xFFFull);
|
||||
snprintf(g_file, sizeof g_file, "%s.txt", bid);
|
||||
mods_scan();
|
||||
for (int k = 0; k < mods_count(); k++) { // the Mods tab's order, top first
|
||||
const ModInfo *mod = mods_at(k);
|
||||
if (!mod->enabled || mod->problem != MOD_OK || !mod->cheats[0] || !find_file(mod->cheats, path, sizeof path))
|
||||
continue;
|
||||
char *text = read_text(path);
|
||||
if (!text) {
|
||||
rt_log("cheats: %s: cannot read %s", mod->name, path);
|
||||
continue;
|
||||
}
|
||||
parse(text, strdup(mod->name));
|
||||
free(text);
|
||||
g_files++;
|
||||
}
|
||||
if (!g_files) {
|
||||
rt_log("cheats: no enabled mod holds cheats/%s", g_file);
|
||||
rebuild_if_needed(dir); // an enabled mod's ExeFS patches may still wait for it
|
||||
return;
|
||||
}
|
||||
snprintf(path, sizeof path, "%sbuild-cache" SEP "cheats.status", dir);
|
||||
char *status = read_text(path);
|
||||
int counts[CHEAT_UNSUPPORTED + 1] = {0}, seen_dups = 0;
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
Cheat *c = &g_cheats[i];
|
||||
char named[900];
|
||||
snprintf(named, sizeof named, c->dup > 1 ? "%s/%s#%d" : "%s/%s", c->info.mod, c->info.name, c->dup);
|
||||
seen_dups += c->dup > 1;
|
||||
char key[9], content[9];
|
||||
snprintf(key, sizeof key, "%08X", fnv(named, strlen(named)));
|
||||
size_t tn = (size_t)c->n * 9 + 1, tl = 0;
|
||||
char *t = (char *)malloc(tn);
|
||||
if (!t) rt_fatal("cheats: out of memory");
|
||||
t[0] = 0;
|
||||
for (int k = 0; k < c->n; k++) tl += (size_t)snprintf(t + tl, tn - tl, k ? " %08X" : "%08X", c->words[k]);
|
||||
c->content = fnv(t, tl);
|
||||
snprintf(content, sizeof content, "%08X", c->content);
|
||||
free(t);
|
||||
snprintf(c->info.key, sizeof c->info.key, "TOTK_CHEAT_%s", key);
|
||||
int master = c->info.kind == CHEAT_MASTER;
|
||||
char *f[5];
|
||||
CheatKind kind;
|
||||
if (status && status_of(status, key, f) && !strcmp(f[1], content)) {
|
||||
const char *k = f[2];
|
||||
kind = !strcmp(k, "code") ? CHEAT_CODE : !strcmp(k, "key") ? CHEAT_KEY : !strcmp(k, "master") ? CHEAT_MASTER
|
||||
: !strcmp(k, "restore") ? CHEAT_RESTORE : !strcmp(k, "heading") ? CHEAT_HEADING
|
||||
: !strcmp(k, "data") ? CHEAT_DATA : CHEAT_UNSUPPORTED;
|
||||
c->info.buttons = (uint32_t)strtoul(f[3], NULL, 16);
|
||||
snprintf(c->note, sizeof c->note, "%s", f[4]);
|
||||
if (kind == CHEAT_CODE || kind == CHEAT_KEY || kind == CHEAT_MASTER) {
|
||||
char set[32];
|
||||
snprintf(set, sizeof set, "cheat_%08x", fnv(named, strlen(named)));
|
||||
c->set = rt_patch_set(set);
|
||||
if (c->set < 0 && kind != CHEAT_MASTER) kind = CHEAT_REBUILD;
|
||||
}
|
||||
} else {
|
||||
kind = classify(c);
|
||||
}
|
||||
if (master && kind == CHEAT_DATA) kind = CHEAT_MASTER; // a data master code: the VM, always on
|
||||
c->info.kind = kind;
|
||||
c->info.note = c->note;
|
||||
const char *v = cfg_get(c->info.key);
|
||||
c->on = kind == CHEAT_MASTER || (v && *v && strcmp(v, "0") != 0);
|
||||
if (kind == CHEAT_KEY && c->set >= 0) rt_patch_switch(c->set, 0); // on while the buttons are held
|
||||
if (kind == CHEAT_KEY || kind == CHEAT_DATA || (kind == CHEAT_MASTER && c->set < 0)) g_vm = 1;
|
||||
counts[kind]++;
|
||||
}
|
||||
free(status);
|
||||
rebuild_if_needed(dir);
|
||||
for (int i = 0; i < g_n; i++) plain_name((char *)g_cheats[i].info.name); // the hashes above read the file's bytes
|
||||
int on = 0;
|
||||
for (int i = 0; i < g_n; i++) on += g_cheats[i].on && g_cheats[i].info.kind != CHEAT_HEADING;
|
||||
rt_log("cheats: %s in %d mods: %d cheats, %d on; %d code, %d while buttons are held, %d data, %d master, "
|
||||
"%d restore, %d headings, %d wait for a rebuild, %d not supported%s",
|
||||
g_file, g_files, g_n, on, counts[CHEAT_CODE], counts[CHEAT_KEY], counts[CHEAT_DATA], counts[CHEAT_MASTER],
|
||||
counts[CHEAT_RESTORE], counts[CHEAT_HEADING], counts[CHEAT_REBUILD], counts[CHEAT_UNSUPPORTED],
|
||||
seen_dups ? " (repeated names numbered)" : "");
|
||||
}
|
||||
|
||||
int cheats_rebuild_pending(void) {
|
||||
char path[PLAT_PATH_MAX + 64];
|
||||
snprintf(path, sizeof path, "%sbuild-cache" SEP "rebuild.txt", g_dir);
|
||||
uint32_t key = 0;
|
||||
int need = mods_exefs_pending(&key);
|
||||
for (int i = 0; i < g_n && !need; i++) need = g_cheats[i].info.kind == CHEAT_REBUILD;
|
||||
return need && g_dir[0] && plat_file_exists(path);
|
||||
}
|
||||
|
||||
void cheats_rebuild_allow(void) {
|
||||
char path[PLAT_PATH_MAX + 64];
|
||||
snprintf(path, sizeof path, "%sbuild-cache" SEP "rebuild.skip", g_dir);
|
||||
plat_fs_unlink(path);
|
||||
}
|
||||
|
||||
int cheats_count(void) { return g_n; }
|
||||
|
||||
const CheatInfo *cheats_info(int i) { return i >= 0 && i < g_n ? &g_cheats[i].info : NULL; }
|
||||
|
||||
int cheats_on(int i) { return i >= 0 && i < g_n && g_cheats[i].on; }
|
||||
|
||||
const char *cheats_file(void) { return g_file; }
|
||||
|
||||
void cheats_set(int i, int on) {
|
||||
if (i < 0 || i >= g_n) return;
|
||||
Cheat *c = &g_cheats[i];
|
||||
if (c->info.kind != CHEAT_CODE && c->info.kind != CHEAT_KEY && c->info.kind != CHEAT_DATA) return;
|
||||
plat_xchg32(&c->on, on != 0);
|
||||
if (c->info.kind == CHEAT_CODE) rt_patch_switch(c->set, on); // CHEAT_KEY: cheats_frame follows the buttons
|
||||
cfg_set(c->info.key, on ? "1" : "0");
|
||||
rt_log("cheats: %s/%s %s", c->info.mod, c->info.name, on ? "on" : "off");
|
||||
}
|
||||
|
||||
void cheats_frame(uint64_t frame) {
|
||||
if (!g_vm || frame < 60) return; // the game polls its input (and starts SDL) before this
|
||||
uint64_t keys = 0;
|
||||
int32_t lx, ly, rx, ry;
|
||||
input_poll(&keys, &lx, &ly, &rx, &ry); // 0 while the menu is open
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
Cheat *c = &g_cheats[i];
|
||||
switch (c->info.kind) {
|
||||
case CHEAT_KEY:
|
||||
if (c->set >= 0) {
|
||||
int want = c->on && (keys & c->info.buttons) == c->info.buttons;
|
||||
if (rt_patch_enabled(c->set) != want) rt_patch_switch(c->set, want);
|
||||
}
|
||||
break;
|
||||
case CHEAT_DATA:
|
||||
if (c->on) run(c, keys);
|
||||
break;
|
||||
case CHEAT_MASTER:
|
||||
if (c->set < 0) run(c, keys);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char *cheats_buttons(uint32_t mask, char *buf, int n) {
|
||||
static const char *const names[] = {"A", "B", "X", "Y", "Left stick press", "Right stick press", "L", "R", "ZL",
|
||||
"ZR", "+", "-", "D-pad left", "D-pad up", "D-pad right", "D-pad down",
|
||||
"Left stick left", "Left stick up", "Left stick right", "Left stick down",
|
||||
"Right stick left", "Right stick up", "Right stick right", "Right stick down"};
|
||||
int k = 0;
|
||||
buf[0] = 0;
|
||||
for (int b = 0; b < 24; b++)
|
||||
if (mask & (1u << b)) k += snprintf(buf + k, (size_t)(n > k ? n - k : 0), "%s%s", k ? " + " : "", names[b]);
|
||||
return buf;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Cheats of the mods' Atmosphere cheat files (cheats.c): mods\<mod>\cheats\<build id>.txt of each enabled mod,
|
||||
// switched from the menu's Cheats tab. A cheat that writes the game's instructions is a code patch set that tools/cheats.py built into the game
|
||||
// (switched with rt_patch_switch); one that writes data runs in the cheat VM every presented frame.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
CHEAT_HEADING, // no opcodes: a title or a group label
|
||||
CHEAT_CODE, // a patch set
|
||||
CHEAT_KEY, // a patch set on while the buttons are held
|
||||
CHEAT_DATA, // the cheat VM
|
||||
CHEAT_MASTER, // {master}: always on
|
||||
CHEAT_RESTORE, // writes the original instructions back: nothing to do
|
||||
CHEAT_REBUILD, // writes instructions, but the game was built without this version of it
|
||||
CHEAT_UNSUPPORTED, // note says why
|
||||
} CheatKind;
|
||||
|
||||
typedef struct {
|
||||
const char *name; // UTF-8, as in the file
|
||||
const char *mod; // the mod's folder name; cheats come grouped by mod, in the Mods tab's order
|
||||
const char *note; // why it is unsupported ("" otherwise)
|
||||
CheatKind kind;
|
||||
uint32_t buttons; // CHEAT_KEY: HidNpadButton mask
|
||||
char key[24]; // its setting, TOTK_CHEAT_<hash of "<mod>/<name>">
|
||||
} CheatInfo;
|
||||
|
||||
void cheats_init(void); // after rt_patches_init: reads the mods' files and their build status
|
||||
int cheats_count(void);
|
||||
// 1 when a cheat of an enabled mod waits for a rebuild (CHEAT_REBUILD) and the setup left its rebuild command.
|
||||
int cheats_rebuild_pending(void);
|
||||
// The next start rebuilds again for cheats whose last rebuild failed or was cancelled (the menu restarts after it).
|
||||
void cheats_rebuild_allow(void);
|
||||
const CheatInfo *cheats_info(int i);
|
||||
int cheats_on(int i); // the player's switch (1 for a master cheat)
|
||||
void cheats_set(int i, int on); // switches at once and sets the cheat's setting (the menu saves totk.ini)
|
||||
const char *cheats_file(void); // "<build id>.txt", the name the files have for this game version
|
||||
// Every presented frame (the present path): the data cheats' VM and the button state of CHEAT_KEY cheats.
|
||||
void cheats_frame(uint64_t frame);
|
||||
// The button names of a mask ("ZL + Right stick left"), into buf.
|
||||
const char *cheats_buttons(uint32_t mask, char *buf, int n);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,156 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/contact_shadow.comp; do not edit.
|
||||
// source sha256 b452a1920e59c4d83b2c994b0c6573cc1e479f7032a1310a7432d11c5c2e9908
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t contact_shadow_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000001b1, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000069,
|
||||
0x00060010, 0x00000004, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00030047, 0x00000027,
|
||||
0x00000002, 0x00050048, 0x00000027, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000027,
|
||||
0x00000001, 0x00000023, 0x00000010, 0x00050048, 0x00000027, 0x00000002, 0x00000023, 0x00000020,
|
||||
0x00050048, 0x00000027, 0x00000003, 0x00000023, 0x00000030, 0x00040047, 0x00000069, 0x0000000b,
|
||||
0x0000001c, 0x00040047, 0x00000088, 0x00000021, 0x00000000, 0x00040047, 0x00000088, 0x00000022,
|
||||
0x00000000, 0x00040047, 0x0000013f, 0x00000021, 0x00000001, 0x00040047, 0x0000013f, 0x00000022,
|
||||
0x00000000, 0x00040047, 0x0000015c, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021,
|
||||
0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006,
|
||||
0x00000002, 0x00040017, 0x0000000a, 0x00000006, 0x00000003, 0x0004002b, 0x00000006, 0x0000001b,
|
||||
0x40000000, 0x0004002b, 0x00000006, 0x0000001d, 0x3f800000, 0x00040015, 0x00000020, 0x00000020,
|
||||
0x00000000, 0x0004002b, 0x00000020, 0x00000021, 0x00000000, 0x00040017, 0x00000026, 0x00000006,
|
||||
0x00000004, 0x0006001e, 0x00000027, 0x00000026, 0x00000026, 0x00000026, 0x00000026, 0x00040020,
|
||||
0x00000028, 0x00000009, 0x00000027, 0x0004003b, 0x00000028, 0x00000029, 0x00000009, 0x00040015,
|
||||
0x0000002a, 0x00000020, 0x00000001, 0x0004002b, 0x0000002a, 0x0000002b, 0x00000000, 0x00040020,
|
||||
0x0000002c, 0x00000009, 0x00000006, 0x0004002b, 0x00000020, 0x00000030, 0x00000001, 0x0004002b,
|
||||
0x00000020, 0x0000003f, 0x00000002, 0x0004002b, 0x00000006, 0x00000043, 0x3f000000, 0x0004002b,
|
||||
0x00000006, 0x00000059, 0x4253ee82, 0x0004002b, 0x00000006, 0x0000005b, 0x3d897143, 0x0004002b,
|
||||
0x00000006, 0x0000005c, 0x3bbf4590, 0x0005002c, 0x00000007, 0x0000005d, 0x0000005b, 0x0000005c,
|
||||
0x00040017, 0x00000064, 0x0000002a, 0x00000002, 0x00040017, 0x00000067, 0x00000020, 0x00000003,
|
||||
0x00040020, 0x00000068, 0x00000001, 0x00000067, 0x0004003b, 0x00000068, 0x00000069, 0x00000001,
|
||||
0x00040017, 0x0000006a, 0x00000020, 0x00000002, 0x00020014, 0x0000006e, 0x0004002b, 0x0000002a,
|
||||
0x00000072, 0x00000002, 0x00090019, 0x00000085, 0x00000006, 0x00000001, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x00000086, 0x00000085, 0x00040020, 0x00000087,
|
||||
0x00000000, 0x00000086, 0x0004003b, 0x00000087, 0x00000088, 0x00000000, 0x0004002b, 0x00000006,
|
||||
0x0000008f, 0x00000000, 0x0004002b, 0x00000006, 0x00000092, 0x3f7fbe77, 0x0004002b, 0x00000006,
|
||||
0x0000009e, 0x42a00000, 0x00040020, 0x000000a8, 0x00000009, 0x00000026, 0x0004002b, 0x0000002a,
|
||||
0x000000b4, 0x00000001, 0x0004002b, 0x00000020, 0x000000b5, 0x00000003, 0x0004002b, 0x00000006,
|
||||
0x000000bb, 0x3ecccccd, 0x0004002b, 0x00000006, 0x000000bc, 0x40400000, 0x0004002b, 0x00000006,
|
||||
0x000000c0, 0x3e19999a, 0x0004002b, 0x00000006, 0x000000c2, 0x3c23d70a, 0x0004002b, 0x00000006,
|
||||
0x000000cb, 0x3e800000, 0x0004002b, 0x0000002a, 0x000000da, 0x00000010, 0x0004002b, 0x00000006,
|
||||
0x000000e6, 0xbd4ccccd, 0x0005002c, 0x00000007, 0x000000f0, 0x0000008f, 0x0000008f, 0x00040017,
|
||||
0x000000f1, 0x0000006e, 0x00000002, 0x0005002c, 0x00000007, 0x000000f8, 0x0000001d, 0x0000001d,
|
||||
0x0004002b, 0x00000006, 0x00000114, 0x3ca3d70a, 0x0004002b, 0x00000006, 0x00000116, 0x3b03126f,
|
||||
0x0004002b, 0x00000006, 0x0000012b, 0x42200000, 0x0004002b, 0x0000002a, 0x00000137, 0x00000003,
|
||||
0x00090019, 0x0000013d, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
|
||||
0x00000008, 0x00040020, 0x0000013e, 0x00000000, 0x0000013d, 0x0004003b, 0x0000013e, 0x0000013f,
|
||||
0x00000000, 0x0004002b, 0x00000020, 0x0000015b, 0x00000008, 0x0006002c, 0x00000067, 0x0000015c,
|
||||
0x0000015b, 0x0000015b, 0x00000030, 0x0005002c, 0x00000007, 0x000001ad, 0x00000043, 0x00000043,
|
||||
0x0004002b, 0x00000006, 0x000001af, 0x3dcccccd, 0x0004002b, 0x00000006, 0x000001b0, 0x3d800000,
|
||||
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x000300f7,
|
||||
0x0000015d, 0x00000000, 0x000300fb, 0x00000021, 0x0000015e, 0x000200f8, 0x0000015e, 0x0004003d,
|
||||
0x00000067, 0x0000006b, 0x00000069, 0x0007004f, 0x0000006a, 0x0000006c, 0x0000006b, 0x0000006b,
|
||||
0x00000000, 0x00000001, 0x0004007c, 0x00000064, 0x0000006d, 0x0000006c, 0x00050051, 0x0000002a,
|
||||
0x00000071, 0x0000006d, 0x00000000, 0x00060041, 0x0000002c, 0x00000073, 0x00000029, 0x00000072,
|
||||
0x00000021, 0x0004003d, 0x00000006, 0x00000074, 0x00000073, 0x0004006e, 0x0000002a, 0x00000075,
|
||||
0x00000074, 0x000500af, 0x0000006e, 0x00000076, 0x00000071, 0x00000075, 0x000400a8, 0x0000006e,
|
||||
0x00000077, 0x00000076, 0x000300f7, 0x00000079, 0x00000000, 0x000400fa, 0x00000077, 0x00000078,
|
||||
0x00000079, 0x000200f8, 0x00000078, 0x00050051, 0x0000002a, 0x0000007b, 0x0000006d, 0x00000001,
|
||||
0x00060041, 0x0000002c, 0x0000007c, 0x00000029, 0x00000072, 0x00000030, 0x0004003d, 0x00000006,
|
||||
0x0000007d, 0x0000007c, 0x0004006e, 0x0000002a, 0x0000007e, 0x0000007d, 0x000500af, 0x0000006e,
|
||||
0x0000007f, 0x0000007b, 0x0000007e, 0x000200f9, 0x00000079, 0x000200f8, 0x00000079, 0x000700f5,
|
||||
0x0000006e, 0x00000080, 0x00000076, 0x0000015e, 0x0000007f, 0x00000078, 0x000300f7, 0x00000082,
|
||||
0x00000000, 0x000400fa, 0x00000080, 0x00000081, 0x00000082, 0x000200f8, 0x00000081, 0x000200f9,
|
||||
0x0000015d, 0x000200f8, 0x00000082, 0x0004003d, 0x00000086, 0x00000089, 0x00000088, 0x00040064,
|
||||
0x00000085, 0x0000008b, 0x00000089, 0x0007005f, 0x00000026, 0x0000008c, 0x0000008b, 0x0000006d,
|
||||
0x00000002, 0x0000002b, 0x00050051, 0x00000006, 0x0000008d, 0x0000008c, 0x00000000, 0x000500bc,
|
||||
0x0000006e, 0x00000090, 0x0000008d, 0x0000008f, 0x000500be, 0x0000006e, 0x00000093, 0x0000008d,
|
||||
0x00000092, 0x000500a6, 0x0000006e, 0x00000094, 0x00000090, 0x00000093, 0x000300f7, 0x00000096,
|
||||
0x00000000, 0x000400fa, 0x00000094, 0x00000095, 0x00000096, 0x000200f8, 0x00000095, 0x000200f9,
|
||||
0x0000015d, 0x000200f8, 0x00000096, 0x00060041, 0x0000002c, 0x0000009a, 0x00000029, 0x0000002b,
|
||||
0x0000003f, 0x0004003d, 0x00000006, 0x0000009b, 0x0000009a, 0x00050085, 0x00000006, 0x0000009c,
|
||||
0x0000008d, 0x0000009b, 0x000500ba, 0x0000006e, 0x0000009f, 0x0000009c, 0x0000009e, 0x000300f7,
|
||||
0x000000a1, 0x00000000, 0x000400fa, 0x0000009f, 0x000000a0, 0x000000a1, 0x000200f8, 0x000000a0,
|
||||
0x000200f9, 0x0000015d, 0x000200f8, 0x000000a1, 0x0004006f, 0x00000007, 0x000000a5, 0x0000006d,
|
||||
0x00050081, 0x00000007, 0x000000a7, 0x000000a5, 0x000001ad, 0x00050041, 0x000000a8, 0x000000a9,
|
||||
0x00000029, 0x00000072, 0x0004003d, 0x00000026, 0x000000aa, 0x000000a9, 0x0007004f, 0x00000007,
|
||||
0x000000ab, 0x000000aa, 0x000000aa, 0x00000002, 0x00000003, 0x00050085, 0x00000007, 0x000000ac,
|
||||
0x000000a7, 0x000000ab, 0x0005008e, 0x00000007, 0x00000167, 0x000000ac, 0x0000001b, 0x00050083,
|
||||
0x00000007, 0x00000169, 0x00000167, 0x000000f8, 0x00050051, 0x00000006, 0x0000016b, 0x00000169,
|
||||
0x00000000, 0x00050085, 0x00000006, 0x0000016d, 0x0000016b, 0x0000009c, 0x00060041, 0x0000002c,
|
||||
0x0000016e, 0x00000029, 0x0000002b, 0x00000021, 0x0004003d, 0x00000006, 0x0000016f, 0x0000016e,
|
||||
0x00050088, 0x00000006, 0x00000170, 0x0000016d, 0x0000016f, 0x00050051, 0x00000006, 0x00000172,
|
||||
0x00000169, 0x00000001, 0x0004007f, 0x00000006, 0x00000173, 0x00000172, 0x00050085, 0x00000006,
|
||||
0x00000175, 0x00000173, 0x0000009c, 0x00060041, 0x0000002c, 0x00000176, 0x00000029, 0x0000002b,
|
||||
0x00000030, 0x0004003d, 0x00000006, 0x00000177, 0x00000176, 0x00050088, 0x00000006, 0x00000178,
|
||||
0x00000175, 0x00000177, 0x0004007f, 0x00000006, 0x0000017a, 0x0000009c, 0x00060050, 0x0000000a,
|
||||
0x0000017b, 0x00000170, 0x00000178, 0x0000017a, 0x00060041, 0x0000002c, 0x000000b6, 0x00000029,
|
||||
0x000000b4, 0x000000b5, 0x0004003d, 0x00000006, 0x000000b7, 0x000000b6, 0x00050085, 0x00000006,
|
||||
0x000000ba, 0x0000009c, 0x000001af, 0x0008000c, 0x00000006, 0x000000bd, 0x00000001, 0x0000002b,
|
||||
0x000000ba, 0x000000bb, 0x000000bc, 0x00050085, 0x00000006, 0x000000be, 0x000000b7, 0x000000bd,
|
||||
0x00050085, 0x00000006, 0x000000c3, 0x0000009c, 0x000000c2, 0x00050081, 0x00000006, 0x000000c4,
|
||||
0x000000c0, 0x000000c3, 0x00050085, 0x00000006, 0x000000c8, 0x000000be, 0x000001b0, 0x00050094,
|
||||
0x00000006, 0x0000017f, 0x000000a5, 0x0000005d, 0x0006000c, 0x00000006, 0x00000180, 0x00000001,
|
||||
0x0000000a, 0x0000017f, 0x00050085, 0x00000006, 0x00000181, 0x00000059, 0x00000180, 0x0006000c,
|
||||
0x00000006, 0x00000182, 0x00000001, 0x0000000a, 0x00000181, 0x00050081, 0x00000006, 0x000000d0,
|
||||
0x000000cb, 0x00000182, 0x00050085, 0x00000006, 0x000000d1, 0x000000c8, 0x000000d0, 0x000200f9,
|
||||
0x000000d4, 0x000200f8, 0x000000d4, 0x000700f5, 0x00000006, 0x000001a7, 0x000000d1, 0x000000a1,
|
||||
0x00000129, 0x000000d7, 0x000700f5, 0x0000002a, 0x000001a6, 0x0000002b, 0x000000a1, 0x00000126,
|
||||
0x000000d7, 0x000500b1, 0x0000006e, 0x000000db, 0x000001a6, 0x000000da, 0x000400f6, 0x000000d6,
|
||||
0x000000d7, 0x00000000, 0x000400fa, 0x000000db, 0x000000d5, 0x000000d6, 0x000200f8, 0x000000d5,
|
||||
0x00050041, 0x000000a8, 0x000000de, 0x00000029, 0x000000b4, 0x0004003d, 0x00000026, 0x000000df,
|
||||
0x000000de, 0x0008004f, 0x0000000a, 0x000000e0, 0x000000df, 0x000000df, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x0005008e, 0x0000000a, 0x000000e2, 0x000000e0, 0x000001a7, 0x00050081, 0x0000000a,
|
||||
0x000000e3, 0x0000017b, 0x000000e2, 0x00050051, 0x00000006, 0x000000e5, 0x000000e3, 0x00000002,
|
||||
0x000500be, 0x0000006e, 0x000000e7, 0x000000e5, 0x000000e6, 0x000300f7, 0x000000e9, 0x00000000,
|
||||
0x000400fa, 0x000000e7, 0x000000e8, 0x000000e9, 0x000200f8, 0x000000e8, 0x000200f9, 0x000000d6,
|
||||
0x000200f8, 0x000000e9, 0x0004007f, 0x00000006, 0x00000188, 0x000000e5, 0x00050051, 0x00000006,
|
||||
0x0000018a, 0x000000e3, 0x00000000, 0x00050085, 0x00000006, 0x0000018b, 0x00000043, 0x0000018a,
|
||||
0x00050085, 0x00000006, 0x0000018e, 0x0000018b, 0x0000016f, 0x00050088, 0x00000006, 0x00000190,
|
||||
0x0000018e, 0x00000188, 0x00050081, 0x00000006, 0x00000191, 0x00000043, 0x00000190, 0x00050051,
|
||||
0x00000006, 0x00000193, 0x000000e3, 0x00000001, 0x00050085, 0x00000006, 0x00000194, 0x00000043,
|
||||
0x00000193, 0x00050085, 0x00000006, 0x00000197, 0x00000194, 0x00000177, 0x00050088, 0x00000006,
|
||||
0x00000199, 0x00000197, 0x00000188, 0x00050083, 0x00000006, 0x0000019a, 0x00000043, 0x00000199,
|
||||
0x00050050, 0x00000007, 0x0000019b, 0x00000191, 0x0000019a, 0x000500b8, 0x000000f1, 0x000000f2,
|
||||
0x0000019b, 0x000000f0, 0x0004009a, 0x0000006e, 0x000000f3, 0x000000f2, 0x000400a8, 0x0000006e,
|
||||
0x000000f4, 0x000000f3, 0x000300f7, 0x000000f6, 0x00000000, 0x000400fa, 0x000000f4, 0x000000f5,
|
||||
0x000000f6, 0x000200f8, 0x000000f5, 0x000500ba, 0x000000f1, 0x000000f9, 0x0000019b, 0x000000f8,
|
||||
0x0004009a, 0x0000006e, 0x000000fa, 0x000000f9, 0x000200f9, 0x000000f6, 0x000200f8, 0x000000f6,
|
||||
0x000700f5, 0x0000006e, 0x000000fb, 0x000000f3, 0x000000e9, 0x000000fa, 0x000000f5, 0x000300f7,
|
||||
0x000000fd, 0x00000000, 0x000400fa, 0x000000fb, 0x000000fc, 0x000000fd, 0x000200f8, 0x000000fc,
|
||||
0x000200f9, 0x000000d6, 0x000200f8, 0x000000fd, 0x0004003d, 0x00000086, 0x00000100, 0x00000088,
|
||||
0x0007004f, 0x00000007, 0x00000104, 0x000000aa, 0x000000aa, 0x00000000, 0x00000001, 0x00050085,
|
||||
0x00000007, 0x00000105, 0x0000019b, 0x00000104, 0x0004006e, 0x00000064, 0x00000106, 0x00000105,
|
||||
0x00040064, 0x00000085, 0x00000107, 0x00000100, 0x0007005f, 0x00000026, 0x00000108, 0x00000107,
|
||||
0x00000106, 0x00000002, 0x0000002b, 0x00050051, 0x00000006, 0x00000109, 0x00000108, 0x00000000,
|
||||
0x00050085, 0x00000006, 0x0000010c, 0x00000109, 0x0000009b, 0x00050083, 0x00000006, 0x00000112,
|
||||
0x00000188, 0x0000010c, 0x00050085, 0x00000006, 0x00000117, 0x0000009c, 0x00000116, 0x00050081,
|
||||
0x00000006, 0x00000118, 0x00000114, 0x00000117, 0x000500ba, 0x0000006e, 0x00000119, 0x00000112,
|
||||
0x00000118, 0x000500b8, 0x0000006e, 0x0000011c, 0x00000112, 0x000000c4, 0x000500a7, 0x0000006e,
|
||||
0x0000011d, 0x00000119, 0x0000011c, 0x000300f7, 0x0000011f, 0x00000000, 0x000400fa, 0x0000011d,
|
||||
0x0000011e, 0x0000011f, 0x000200f8, 0x0000011e, 0x0004006f, 0x00000006, 0x00000121, 0x000001a6,
|
||||
0x00050085, 0x00000006, 0x00000122, 0x00000121, 0x000001b0, 0x00050083, 0x00000006, 0x00000123,
|
||||
0x0000001d, 0x00000122, 0x000200f9, 0x000000d6, 0x000200f8, 0x0000011f, 0x000200f9, 0x000000d7,
|
||||
0x000200f8, 0x000000d7, 0x00050080, 0x0000002a, 0x00000126, 0x000001a6, 0x000000b4, 0x00050081,
|
||||
0x00000006, 0x00000129, 0x000001a7, 0x000000c8, 0x000200f9, 0x000000d4, 0x000200f8, 0x000000d6,
|
||||
0x000b00f5, 0x00000006, 0x000001aa, 0x0000008f, 0x000000d4, 0x0000008f, 0x000000e8, 0x0000008f,
|
||||
0x000000fc, 0x00000123, 0x0000011e, 0x0008000c, 0x00000006, 0x0000012d, 0x00000001, 0x00000031,
|
||||
0x0000012b, 0x0000009e, 0x0000009c, 0x00050083, 0x00000006, 0x0000012e, 0x0000001d, 0x0000012d,
|
||||
0x00060041, 0x0000002c, 0x00000130, 0x00000029, 0x0000002b, 0x000000b5, 0x0004003d, 0x00000006,
|
||||
0x00000131, 0x00000130, 0x00050085, 0x00000006, 0x00000133, 0x00000131, 0x000001aa, 0x00050085,
|
||||
0x00000006, 0x00000135, 0x00000133, 0x0000012e, 0x00050083, 0x00000006, 0x00000136, 0x0000001d,
|
||||
0x00000135, 0x00060041, 0x0000002c, 0x00000138, 0x00000029, 0x00000137, 0x00000021, 0x0004003d,
|
||||
0x00000006, 0x00000139, 0x00000138, 0x000500ba, 0x0000006e, 0x0000013a, 0x00000139, 0x00000043,
|
||||
0x000300f7, 0x0000013c, 0x00000000, 0x000400fa, 0x0000013a, 0x0000013b, 0x0000013c, 0x000200f8,
|
||||
0x0000013b, 0x0004003d, 0x0000013d, 0x00000140, 0x0000013f, 0x00070050, 0x00000026, 0x00000143,
|
||||
0x00000136, 0x00000136, 0x00000136, 0x0000001d, 0x00040063, 0x00000140, 0x0000006d, 0x00000143,
|
||||
0x000200f9, 0x0000015d, 0x000200f8, 0x0000013c, 0x000500be, 0x0000006e, 0x00000146, 0x00000136,
|
||||
0x00000092, 0x000300f7, 0x00000148, 0x00000000, 0x000400fa, 0x00000146, 0x00000147, 0x00000148,
|
||||
0x000200f8, 0x00000147, 0x000200f9, 0x0000015d, 0x000200f8, 0x00000148, 0x0004003d, 0x0000013d,
|
||||
0x0000014c, 0x0000013f, 0x00050062, 0x00000026, 0x0000014e, 0x0000014c, 0x0000006d, 0x0004003d,
|
||||
0x0000013d, 0x0000014f, 0x0000013f, 0x0008004f, 0x0000000a, 0x00000152, 0x0000014e, 0x0000014e,
|
||||
0x00000000, 0x00000001, 0x00000002, 0x0005008e, 0x0000000a, 0x00000154, 0x00000152, 0x00000136,
|
||||
0x00050051, 0x00000006, 0x00000156, 0x0000014e, 0x00000003, 0x00050051, 0x00000006, 0x00000157,
|
||||
0x00000154, 0x00000000, 0x00050051, 0x00000006, 0x00000158, 0x00000154, 0x00000001, 0x00050051,
|
||||
0x00000006, 0x00000159, 0x00000154, 0x00000002, 0x00070050, 0x00000026, 0x0000015a, 0x00000157,
|
||||
0x00000158, 0x00000159, 0x00000156, 0x00040063, 0x0000014f, 0x0000006d, 0x0000015a, 0x000200f9,
|
||||
0x0000015d, 0x000200f8, 0x0000015d, 0x000100fd, 0x00010038,
|
||||
};
|
||||
+1
-1
@@ -109,7 +109,7 @@ EXCL(16, uint16_t)
|
||||
EXCL(32, uint32_t)
|
||||
EXCL(64, uint64_t)
|
||||
|
||||
// Exclusive pair of two X registers: one 16-byte-aligned 128-bit access (cmpxchg16b; x86-64-v3 has CX16).
|
||||
// Exclusive pair of two X registers: one 16-byte-aligned 128-bit access (cmpxchg16b; x86-64-v2 has CX16).
|
||||
// A pair of two W registers is a 64-bit exclusive and uses ldx64/stx64.
|
||||
static inline __uint128_t ldxp128(Cpu *c, uint64_t a) {
|
||||
__uint128_t v = __atomic_load_n((__uint128_t *)(uintptr_t)a, __ATOMIC_ACQUIRE);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -52,6 +52,7 @@ void rt_fatal(const char *fmt, ...) {
|
||||
double proc, used, limit;
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
rt_log("fatal: memory commit: this process %.0f MB, the system %.0f of %.0f MB", proc, used, limit);
|
||||
kern_free_cores_note();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_Exit(70);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/dlaa_mv.comp; do not edit.
|
||||
// source sha256 e96a6d2d0cec26c47a76280363077c5982919bedc984f20ae9f1d185dc6ae674
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t dlaa_mv_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x0000019a, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
|
||||
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,
|
||||
};
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
// Correctly rounded fma/fmaf for x86-64 builds without FMA3: the -march=x86-64-v2 build (docs/building.md).
|
||||
//
|
||||
// runtime/simd.h lowers AArch64 FMADD/FMSUB and the RECPS/RSQRTS helpers to __builtin_elementwise_fma, which clang
|
||||
// turns into a call to fma/fmaf when the target has no FMA instruction. The C runtime is not a substitute: on CPUs
|
||||
// without FMA3 the UCRT's software fallback is documented as rounding incorrectly by up to half an ulp, while the FP
|
||||
// lowering needs the single rounding the ARM instruction gives. This file supplies it.
|
||||
//
|
||||
// Adapted from musl src/math/fma.c and src/math/fmaf.c (MIT, Copyright (c) 2005-2020 Rich Felker and contributors).
|
||||
// The result is rounded in the MXCSR rounding mode the caller set (simd.h simd_rm_enter), and MXCSR FTZ and DAZ are
|
||||
// honoured as the hardware FMA would, so runtime/simd.h's fast paths keep their assumptions: denormal inputs read as
|
||||
// zero and denormal results as zero under FPCR.FZ, without a trip through the slow path.
|
||||
//
|
||||
// The definitions exist only where the compiler would emit the calls. A -march=x86-64-v3 build defines __FMA__ and
|
||||
// emits vfmadd instead, so this file contributes nothing there.
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(__x86_64__) && !defined(__FMA__)
|
||||
|
||||
#include <float.h>
|
||||
#include <immintrin.h>
|
||||
|
||||
#define MXCSR_DAZ 0x0040u
|
||||
#define MXCSR_FTZ 0x8000u
|
||||
|
||||
static double fma_flush_d(double x) {
|
||||
union { double f; uint64_t i; } u = {x};
|
||||
if (!(u.i & 0x7ff0000000000000ull) && (u.i & 0x000fffffffffffffull)) u.i &= 0x8000000000000000ull;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static float fma_flush_f(float x) {
|
||||
union { float f; uint32_t i; } u = {x};
|
||||
if (!(u.i & 0x7f800000u) && (u.i & 0x007fffffu)) u.i &= 0x80000000u;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
// ---- float: the exact product fits in a double, so one extra term and a round-to-odd step decide the float
|
||||
// rounding. Halfway and tiny results are the only ones where rounding the double first can change the float result.
|
||||
float fmaf(float x, float y, float z) {
|
||||
unsigned csr = _mm_getcsr();
|
||||
if (csr & MXCSR_DAZ) x = fma_flush_f(x), y = fma_flush_f(y), z = fma_flush_f(z);
|
||||
double xy = (double)x * y; // exact
|
||||
union { double r; uint64_t i; } u = {xy + z};
|
||||
int e = u.i >> 52 & 0x7ff;
|
||||
int halfway = (u.i & 0x1fffffff) == 0x10000000;
|
||||
int tiny = e <= 0x3ff - 126 && e >= 0x3ff - 149;
|
||||
if (halfway || tiny) {
|
||||
if (e != 0x7ff) {
|
||||
// r+t is the exact sum in nearest rounding; otherwise t keeps its sign and whether it is zero, which
|
||||
// is all the round-to-odd step needs. An odd sticky bit makes the final conversion round once, in
|
||||
// whichever mode MXCSR asks for.
|
||||
int s = u.i >> 63;
|
||||
double t = s == (xy < z) ? xy - u.r + z : z - u.r + xy;
|
||||
if (t) {
|
||||
u.i -= s ^ (t < 0);
|
||||
u.i |= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
float r = (float)u.r;
|
||||
if (csr & MXCSR_FTZ) r = fma_flush_f(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- double: the significands are multiplied and aligned as integers, then converted back with one rounding.
|
||||
#define ASUINT64(x) ((union { double f; uint64_t i; }){x}).i
|
||||
#define ZEROINFNAN (0x7ff - 0x3ff - 52 - 1)
|
||||
|
||||
struct fma_num { uint64_t m; int e; int sign; };
|
||||
|
||||
static struct fma_num fma_normalize(double x) {
|
||||
uint64_t ix = ASUINT64(x);
|
||||
int e = ix >> 52;
|
||||
int sign = e & 0x800;
|
||||
e &= 0x7ff;
|
||||
if (!e) {
|
||||
ix = ASUINT64(x * 0x1p63);
|
||||
e = ix >> 52 & 0x7ff;
|
||||
e = e ? e - 63 : 0x800;
|
||||
}
|
||||
ix &= (1ull << 52) - 1;
|
||||
ix |= 1ull << 52;
|
||||
ix <<= 1;
|
||||
e -= 0x3ff + 52 + 1;
|
||||
return (struct fma_num){ix, e, sign};
|
||||
}
|
||||
|
||||
static void fma_mul(uint64_t *hi, uint64_t *lo, uint64_t x, uint64_t y) {
|
||||
uint64_t xlo = (uint32_t)x, xhi = x >> 32;
|
||||
uint64_t ylo = (uint32_t)y, yhi = y >> 32;
|
||||
uint64_t t1 = xlo * ylo;
|
||||
uint64_t t2 = xlo * yhi + xhi * ylo;
|
||||
uint64_t t3 = xhi * yhi;
|
||||
*lo = t1 + (t2 << 32);
|
||||
*hi = t3 + (t2 >> 32) + (t1 > *lo);
|
||||
}
|
||||
|
||||
// scalbn from musl src/math/scalbn.c (MIT): the fma's final scaling, kept here so the result does not depend on the
|
||||
// C runtime's version. Its multiplications honour MXCSR like any other host FP operation.
|
||||
static double fma_scalbn(double x, int n) {
|
||||
union { double f; uint64_t i; } u;
|
||||
double y = x;
|
||||
if (n > 1023) {
|
||||
y *= 0x1p1023;
|
||||
n -= 1023;
|
||||
if (n > 1023) {
|
||||
y *= 0x1p1023;
|
||||
n -= 1023;
|
||||
if (n > 1023) n = 1023;
|
||||
}
|
||||
} else if (n < -1022) {
|
||||
// Make sure the final n < -53, so the subnormal range rounds only once.
|
||||
y *= 0x1p-1022 * 0x1p53;
|
||||
n += 1022 - 53;
|
||||
if (n < -1022) {
|
||||
y *= 0x1p-1022 * 0x1p53;
|
||||
n += 1022 - 53;
|
||||
if (n < -1022) n = -1022;
|
||||
}
|
||||
}
|
||||
u.i = (uint64_t)(0x3ff + n) << 52;
|
||||
return y * u.f;
|
||||
}
|
||||
|
||||
double fma(double x, double y, double z) {
|
||||
#pragma STDC FENV_ACCESS ON
|
||||
unsigned csr = _mm_getcsr();
|
||||
if (csr & MXCSR_DAZ) x = fma_flush_d(x), y = fma_flush_d(y), z = fma_flush_d(z);
|
||||
|
||||
struct fma_num nx = fma_normalize(x), ny = fma_normalize(y), nz = fma_normalize(z);
|
||||
if (nx.e >= ZEROINFNAN || ny.e >= ZEROINFNAN) return x * y + z;
|
||||
if (nz.e >= ZEROINFNAN) {
|
||||
if (nz.e > ZEROINFNAN) return x * y; // z == 0
|
||||
return z;
|
||||
}
|
||||
|
||||
uint64_t rhi, rlo, zhi, zlo;
|
||||
fma_mul(&rhi, &rlo, nx.m, ny.m);
|
||||
|
||||
// Align the exponents: shift z left by kz and r right by kr with kz + kr == d.
|
||||
int e = nx.e + ny.e;
|
||||
int d = nz.e - e;
|
||||
if (d > 0) {
|
||||
if (d < 64) {
|
||||
zlo = nz.m << d;
|
||||
zhi = nz.m >> (64 - d);
|
||||
} else {
|
||||
zlo = 0;
|
||||
zhi = nz.m;
|
||||
e = nz.e - 64;
|
||||
d -= 64;
|
||||
if (d == 0) {
|
||||
} else if (d < 64) {
|
||||
rlo = rhi << (64 - d) | rlo >> d | !!(rlo << (64 - d));
|
||||
rhi = rhi >> d;
|
||||
} else {
|
||||
rlo = 1;
|
||||
rhi = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
zhi = 0;
|
||||
d = -d;
|
||||
if (d == 0) {
|
||||
zlo = nz.m;
|
||||
} else if (d < 64) {
|
||||
zlo = nz.m >> d | !!(nz.m << (64 - d));
|
||||
} else {
|
||||
zlo = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int sign = nx.sign ^ ny.sign;
|
||||
int samesign = !(sign ^ nz.sign);
|
||||
int nonzero = 1;
|
||||
if (samesign) {
|
||||
rlo += zlo;
|
||||
rhi += zhi + (rlo < zlo);
|
||||
} else {
|
||||
uint64_t t = rlo;
|
||||
rlo -= zlo;
|
||||
rhi = rhi - zhi - (t < rlo);
|
||||
if (rhi >> 63) {
|
||||
rlo = -rlo;
|
||||
rhi = -rhi - !!rlo;
|
||||
sign = !sign;
|
||||
}
|
||||
nonzero = !!rhi;
|
||||
}
|
||||
|
||||
// Keep the top 63 bits in rhi; the last bit is sticky.
|
||||
if (nonzero) {
|
||||
e += 64;
|
||||
d = __builtin_clzll(rhi) - 1;
|
||||
rhi = rhi << d | rlo >> (64 - d) | !!(rlo << d);
|
||||
} else if (rlo) {
|
||||
d = __builtin_clzll(rlo) - 1;
|
||||
if (d < 0)
|
||||
rhi = rlo >> 1 | (rlo & 1);
|
||||
else
|
||||
rhi = rlo << d;
|
||||
} else {
|
||||
return x * y + z; // exact +-0
|
||||
}
|
||||
e -= d;
|
||||
|
||||
int64_t i = rhi;
|
||||
if (sign) i = -i;
|
||||
double r = i; // |r| is in [0x1p62, 0x1p63]
|
||||
|
||||
if (e < -1022 - 62) {
|
||||
// The result is subnormal before rounding.
|
||||
if (e == -1022 - 63) {
|
||||
double c = 0x1p63;
|
||||
if (sign) c = -c;
|
||||
if (r == c) {
|
||||
// The smallest normal after rounding: whether it underflows depends on the architecture. A double
|
||||
// to float conversion imitates the before-rounding rule the guest has.
|
||||
float fltmin = 0x0.ffffff8p-63 * FLT_MIN * r;
|
||||
r = DBL_MIN / FLT_MIN * fltmin;
|
||||
return (csr & MXCSR_FTZ) ? fma_flush_d(r) : r;
|
||||
}
|
||||
// One bit is lost when scaled: add another top bit so the conversion rounds only once if inexact.
|
||||
if (rhi << 53) {
|
||||
i = rhi >> 1 | (rhi & 1) | 1ull << 62;
|
||||
if (sign) i = -i;
|
||||
r = i;
|
||||
r = 2 * r - c;
|
||||
// Raise underflow portably, so the compiler cannot optimize the expression away.
|
||||
double tiny = DBL_MIN / FLT_MIN * r;
|
||||
r += (double)(tiny * tiny) * (r - r);
|
||||
}
|
||||
} else {
|
||||
// Only round once when scaled.
|
||||
d = 10;
|
||||
i = (rhi >> d | !!(rhi << (64 - d))) << d;
|
||||
if (sign) i = -i;
|
||||
r = i;
|
||||
}
|
||||
}
|
||||
r = fma_scalbn(r, e);
|
||||
if (csr & MXCSR_FTZ) r = fma_flush_d(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
// The host's own folder dialog, for menu pages that point at a folder (the Amiibo tab): folder_pick_win.cpp with Win32,
|
||||
// folder_pick_sdl.c with SDL. The menu has no text entry, so a folder is chosen here or by walking the page's list.
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// done gets the chosen folder (UTF-8) or NULL when the player cancelled or the host has no dialog.
|
||||
typedef void (*FolderPickDone)(const char *path, void *ctx);
|
||||
|
||||
// Opens the dialog at start (may be relative or missing) and returns at once; done runs later on another thread, once.
|
||||
// The caller keeps one dialog open at a time.
|
||||
void folder_pick(const char *start, FolderPickDone done, void *ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
// The SDL front end of folder_pick.h: SDL_ShowOpenFolderDialog (the desktop portal, else zenity), started on the window
|
||||
// thread, which runs SDL's event loop that delivers the portal's answer.
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "folder_pick.h"
|
||||
#include "host.h"
|
||||
#include "input_sdl.h"
|
||||
|
||||
typedef struct {
|
||||
char start[PATH_MAX]; // absolute, or empty: the dialog's own default
|
||||
FolderPickDone done;
|
||||
void *ctx;
|
||||
} PickReq;
|
||||
|
||||
static void SDLCALL on_pick(void *userdata, const char *const *list, int filter) {
|
||||
(void)filter;
|
||||
PickReq *r = userdata;
|
||||
if (!list) rt_log("folder_pick: the folder dialog failed (%s)", SDL_GetError());
|
||||
r->done(list && list[0] ? list[0] : NULL, r->ctx);
|
||||
free(r);
|
||||
}
|
||||
|
||||
static void show(void *arg) {
|
||||
PickReq *r = arg;
|
||||
SDL_ShowOpenFolderDialog(on_pick, r, gfx_window_sdl(), r->start[0] ? r->start : NULL, false);
|
||||
}
|
||||
|
||||
void folder_pick(const char *start, FolderPickDone done, void *ctx) {
|
||||
PickReq *r = calloc(1, sizeof *r);
|
||||
if (!r) {
|
||||
done(NULL, ctx);
|
||||
return;
|
||||
}
|
||||
r->done = done;
|
||||
r->ctx = ctx;
|
||||
if (start && *start && !realpath(start, r->start)) r->start[0] = 0;
|
||||
gfx_window_sdl_run(show, r);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// The Win32 front end of folder_pick.h: the shell's folder dialog (IFileOpenDialog with FOS_PICKFOLDERS), owned by the
|
||||
// game window, on a thread of its own so the present thread that draws the menu keeps running.
|
||||
#include <windows.h>
|
||||
#include <shobjidl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
|
||||
extern "C" {
|
||||
#include "folder_pick.h"
|
||||
#include "host.h"
|
||||
#include "input_win.h"
|
||||
#include "platform/platform.h"
|
||||
}
|
||||
|
||||
struct PickReq {
|
||||
std::string start;
|
||||
FolderPickDone done;
|
||||
void *ctx;
|
||||
};
|
||||
|
||||
static std::wstring wide(const std::string &utf8) {
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
|
||||
std::wstring w(n > 0 ? n : 1, L'\0');
|
||||
if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], n);
|
||||
w.resize(wcslen(w.c_str()));
|
||||
return w;
|
||||
}
|
||||
|
||||
static std::string utf8(const wchar_t *w) {
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
|
||||
std::string s(n > 0 ? n : 1, '\0');
|
||||
if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w, -1, &s[0], n, NULL, NULL);
|
||||
s.resize(strlen(s.c_str()));
|
||||
return s;
|
||||
}
|
||||
|
||||
// The dialog wants an absolute folder that exists; anything else leaves it at the shell's last folder.
|
||||
static void set_start(IFileOpenDialog *d, const std::string &start) {
|
||||
wchar_t full[MAX_PATH * 2];
|
||||
if (start.empty() || !GetFullPathNameW(wide(start).c_str(), MAX_PATH * 2, full, NULL)) return;
|
||||
IShellItem *item = NULL;
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(full, NULL, IID_PPV_ARGS(&item)))) {
|
||||
d->SetFolder(item);
|
||||
item->Release();
|
||||
}
|
||||
}
|
||||
|
||||
static void pick_main(void *arg) {
|
||||
PickReq *r = (PickReq *)arg;
|
||||
std::string chosen;
|
||||
HRESULT co = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||
IFileOpenDialog *d = NULL;
|
||||
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d));
|
||||
if (SUCCEEDED(hr)) {
|
||||
FILEOPENDIALOGOPTIONS opt = 0;
|
||||
d->GetOptions(&opt);
|
||||
d->SetOptions(opt | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
||||
set_start(d, r->start);
|
||||
hr = d->Show(gfx_window_handle());
|
||||
IShellItem *item = NULL;
|
||||
if (SUCCEEDED(hr) && SUCCEEDED(hr = d->GetResult(&item))) {
|
||||
PWSTR path = NULL;
|
||||
if (SUCCEEDED(hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
|
||||
chosen = utf8(path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
item->Release();
|
||||
}
|
||||
d->Release();
|
||||
}
|
||||
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_CANCELLED)) rt_log("folder_pick: the folder dialog failed (0x%08lx)", (unsigned long)hr);
|
||||
r->done(chosen.empty() ? NULL : chosen.c_str(), r->ctx);
|
||||
if (SUCCEEDED(co)) CoUninitialize();
|
||||
delete r;
|
||||
}
|
||||
|
||||
extern "C" void folder_pick(const char *start, FolderPickDone done, void *ctx) {
|
||||
PickReq *r = new PickReq{start ? start : "", done, ctx};
|
||||
if (!plat_thread_spawn(pick_main, r, "folder dialog", 0)) {
|
||||
rt_log("folder_pick: cannot start the dialog thread");
|
||||
done(NULL, ctx);
|
||||
delete r;
|
||||
}
|
||||
}
|
||||
+36
-11
@@ -11,8 +11,20 @@
|
||||
// game speed is right only when frames take exactly their interval. With the set realtime both read the patch page
|
||||
// instead: +0 ratio = seconds * 30, +4 seconds (1.4.3: +8 interval = seconds * 60, the producer's nominal interval
|
||||
// before its half-step coefficient), the moving average of the last 5 display intervals, at most 1/15 s
|
||||
// (slower frames slow the game down, as on the console), no lower bound, so a higher frame-rate cap only changes the
|
||||
// pacer (frame_step_cap_ns, TOTK_FPS_LIMIT). The set cloth gives hair and cloth the same clock and needs realtime.
|
||||
// (slower frames slow the game down, as on the console) and at least 1/240 s, so a higher frame-rate cap only changes
|
||||
// the pacer (frame_step_cap_ns, TOTK_FPS_LIMIT). The set cloth gives hair and cloth the same clock and needs realtime.
|
||||
//
|
||||
// The floor: with a shorter step (ratio below 0.125) 1.4.3 aborts within seconds of some save loads (issue #241, a
|
||||
// 280 Hz monitor): Havok's hkDefaultMemoryImpl reports "Out of memory" for the buffer the physics pre-solve (profiler
|
||||
// scope "TtPreSolve") resizes in main+0x1261630. Forced steps on the Depths save: 1/240 s plays, 1/244 s and shorter
|
||||
// abort; the field, sky island and fight saves play at 1/280 s, the town save aborts after about 800 frames. The
|
||||
// same short steps also crash a physics worker reading that buffer's contacts: an access violation in main+0x1292560
|
||||
// or its caller main+0x1286870 (narrowphase contact setup, reached from the grid task main+0x12f1ea0), reading a
|
||||
// Havok body's shape at body+0x78 as null or garbage (issues #29 and #92, 240 Hz monitors with fps_limit = display).
|
||||
// Steps drawn from 1/252..1/230 s crash the Depths load there in seconds; 1/240..1/230 s plays. The frame limit
|
||||
// stops at 240 too (LIMIT_MAX_HZ), so the floor only catches the average dipping under 1/240 s at that limit: a
|
||||
// present late by less than a period keeps the pacer's phase (nvn_api_obj.c, pace), so the next display interval is
|
||||
// shorter than the period. No limit makes the game run slower than the clock.
|
||||
//
|
||||
// The sample is the display time the pacer gives each present (nvnQueuePresentTexture), so the step follows the
|
||||
// frames as shown, pacing waits included. With the step on, the pacer ignores the game's interval and holds a frame
|
||||
@@ -47,6 +59,7 @@
|
||||
#define STEP_MAX_S (1.0 / 15.0) // longest step; a longer frame slows the game down
|
||||
#define STEP_RESET_S 0.25 // a gap this long (loading, a debugger) restarts the average
|
||||
#define STEP_FIRST_S (1.0 / 60.0)
|
||||
#define STEP_MIN_S (1.0 / 240.0) // shortest step; a shorter one makes the game's physics run out of memory
|
||||
|
||||
static _Atomic uint64_t *step_word; // patch page +0: ratio (low float), seconds (high float)
|
||||
static double step_now = STEP_FIRST_S; // the step the patch page holds: what the game steps each frame
|
||||
@@ -78,6 +91,11 @@ void frame_step_init(void) {
|
||||
rt_patch_disable(cloth);
|
||||
rt_log("frame rate: cloth clock off, it needs the real-time step (TOTK_REALTIME=0)");
|
||||
}
|
||||
int climb = rt_patch_set("climb"); // reads the page's frame ratio, written only with the real-time step
|
||||
if (climb >= 0 && !rt_patch_enabled(realtime) && rt_patch_enabled(climb)) {
|
||||
rt_patch_disable(climb);
|
||||
rt_log("frame rate: climb speed fix off, it needs the real-time step (TOTK_REALTIME=0)");
|
||||
}
|
||||
if (rt_patch_enabled(realtime)) {
|
||||
step_word = (_Atomic uint64_t *)rt_patch_page();
|
||||
step_interval = (_Atomic uint32_t *)((uint8_t *)rt_patch_page() + 8);
|
||||
@@ -90,14 +108,15 @@ void frame_step_init(void) {
|
||||
|
||||
int frame_step_realtime(void) { return step_word != NULL; }
|
||||
|
||||
// The frame limit (menu, TOTK_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 360, or display (the
|
||||
// refresh rate of the window's monitor). Unset, the older TOTK_FPS60=0 still means 30. A limit of 30 switches the set
|
||||
// The frame limit (menu, TOTK_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 240, or display (the
|
||||
// refresh rate of the window's monitor, at most 240). A higher number gives 240, the rate of the shortest step the
|
||||
// game survives (STEP_MIN_S). Unset, the older TOTK_FPS60=0 still means 30. A limit of 30 switches the set
|
||||
// fps60 off (the shipping minimum interval 2), every other limit switches it on; the controller reads its minimum
|
||||
// interval from the patched sites each frame. With the real-time step the pacer holds frames to the limit; above 60
|
||||
// only the pacer changes, as the step has no lower bound. Without it the game steps whole 60 Hz vsyncs and the pacer
|
||||
// only the pacer changes, as the step's floor is the limit's ceiling. Without it the game steps whole 60 Hz vsyncs and the pacer
|
||||
// follows the game's interval, so the limit stops at 60.
|
||||
#define LIMIT_MIN_HZ 30
|
||||
#define LIMIT_MAX_HZ 360
|
||||
#define LIMIT_MAX_HZ 240 // 1 / STEP_MIN_S
|
||||
static int fps60_set = -2;
|
||||
static unsigned limit_seen;
|
||||
static int limit_read;
|
||||
@@ -109,8 +128,13 @@ static int limit_parse(void) {
|
||||
if (!strcmp(e, "display")) return 0;
|
||||
char *end;
|
||||
long n = strtol(e, &end, 10);
|
||||
if (!*end && n >= LIMIT_MIN_HZ && n <= LIMIT_MAX_HZ) return (int)n;
|
||||
rt_log("frame rate: TOTK_FPS_LIMIT=%s is not 30..360 or display; 60", e);
|
||||
if (!*end && n > LIMIT_MAX_HZ) {
|
||||
rt_log("frame rate: TOTK_FPS_LIMIT=%s is above %d, where the game's physics runs out of memory; %d", e,
|
||||
LIMIT_MAX_HZ, LIMIT_MAX_HZ);
|
||||
return LIMIT_MAX_HZ;
|
||||
}
|
||||
if (!*end && n >= LIMIT_MIN_HZ) return (int)n;
|
||||
rt_log("frame rate: TOTK_FPS_LIMIT=%s is not %d..%d or display; 60", e, LIMIT_MIN_HZ, LIMIT_MAX_HZ);
|
||||
return 60;
|
||||
}
|
||||
|
||||
@@ -161,13 +185,13 @@ static uint64_t read_ptr(uint64_t a) {
|
||||
uint64_t p = 0;
|
||||
return read_guest(a, &p, 8) ? p : 0;
|
||||
}
|
||||
// 1.2.1: [main+0x4728538] (1.1.1: [main+0x4630a68], 1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13 lists the
|
||||
// 1.2.1: [main+0x4728538] (1.1.0: [main+0x4630a68], 1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13 lists the
|
||||
// resident actors; the one named "Player" has its position at +0x2b4 (MacNativeGames world_survey.c, game_version.h
|
||||
// TOTK_GAME_V121/V143). The 1.1.1 slot was measured from its own dereference chain: 181 scene accesses in that build
|
||||
// TOTK_GAME_V121/V143). The 1.1.0 slot was measured from its own dereference chain: 181 scene accesses in that build
|
||||
// read it, and one of them reproduces this walk (the 14-component check, component 13, the 0x70 stride and the name
|
||||
// compare) instruction for instruction.
|
||||
static int player_pos(float xyz[3]) {
|
||||
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : RT_V111 ? 0x4630a68 : 0x4728538)),
|
||||
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : RT_V110 ? 0x4630a68 : 0x4728538)),
|
||||
scene = module ? read_ptr(module + 0x1e8) : 0;
|
||||
int32_t count = 0;
|
||||
if (!scene || !read_guest(scene + 0x50, &count, 4) || count < 14 || count > 256) return 0;
|
||||
@@ -258,6 +282,7 @@ void frame_step_present(uint64_t display_ns, int interval) {
|
||||
double sum = 0;
|
||||
for (unsigned i = 0; i < sample_count; i++) sum += samples[i];
|
||||
step = sum / sample_count;
|
||||
if (step < STEP_MIN_S) step = STEP_MIN_S;
|
||||
step_store((float)step);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -38,8 +38,16 @@ uint64_t romfs_file_count(void); // files in the index
|
||||
const char *romfs_entry(uint64_t i, uint64_t *offset, uint64_t *size); // path, image offset and size of index row i
|
||||
const char *romfs_image_path(void); // the romfs.bin path fs_init opened
|
||||
size_t romfs_overlay_count(void);
|
||||
int romfs_xbox_prompts_ready(void); // romfs-patches-xbox was there at start
|
||||
int romfs_xbox_prompts_active(void); // and was loaded (pad_layout = xbox_prompts at start)
|
||||
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). 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);
|
||||
int romfs_prompts_ready(const char *folder);
|
||||
int romfs_prompts_off(const char *folder);
|
||||
const char *romfs_overlay(size_t k, const char **file, uint64_t *size, int *is_added); // RomFS path of overlay k
|
||||
void fs_trace(const char *what, const char *path); // TOTK_TRACE=fs: one line per open
|
||||
|
||||
|
||||
+48
-5
@@ -125,12 +125,30 @@ static void make_dirs(const char *p) {
|
||||
if (!make_dir(buf)) rt_fatal("fs: cannot create directory %s", buf);
|
||||
}
|
||||
|
||||
// Remove a directory tree; a link (reparse point, symbolic link) is removed as a link, never followed. 0 also when
|
||||
// The first path remove_tree could not delete and the OS error code, for the message of a fatal.
|
||||
static char remove_failed[1100];
|
||||
static int remove_error;
|
||||
static void remove_note(const char *path) {
|
||||
if (*remove_failed) return;
|
||||
snprintf(remove_failed, sizeof remove_failed, "%s", path);
|
||||
remove_error = plat_fs_os_error();
|
||||
}
|
||||
|
||||
// Remove a directory tree; a link (symbolic link, junction) is removed as a link, never followed. 0 also when
|
||||
// nothing is there.
|
||||
static int remove_tree(const char *path) {
|
||||
int kind = plat_fs_kind(path);
|
||||
if (kind == PLAT_FS_MISSING) return 0;
|
||||
if (kind == PLAT_FS_FILE) return plat_fs_unlink(path) ? -1 : 0;
|
||||
if (kind == PLAT_FS_FILE) {
|
||||
if (!plat_fs_unlink(path)) return 0;
|
||||
remove_note(path);
|
||||
return -1;
|
||||
}
|
||||
if (kind == PLAT_FS_LINK) { // a link to a file goes by unlink, a link to a directory by rmdir
|
||||
if (!plat_fs_unlink(path) || !plat_fs_rmdir(path)) return 0;
|
||||
remove_note(path);
|
||||
return -1;
|
||||
}
|
||||
int bad = 0;
|
||||
if (kind == PLAT_FS_DIR) {
|
||||
Listing l;
|
||||
@@ -142,7 +160,23 @@ static int remove_tree(const char *path) {
|
||||
}
|
||||
listing_free(l.e, l.n);
|
||||
}
|
||||
return bad | (plat_fs_rmdir(path) ? -1 : 0);
|
||||
if (plat_fs_rmdir(path)) {
|
||||
remove_note(path);
|
||||
bad |= -1;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
// remove_tree, tried again every 50 ms for up to 2 s while another process (the previous run still exiting, a virus
|
||||
// scanner, a sync client) holds a file in the tree. The failing path and its OS error stay in remove_failed.
|
||||
#define MOVE_TRIES 40 // 2 s, also move_dir's
|
||||
static int remove_tree_retry(const char *path) {
|
||||
for (int i = 0;; i++) {
|
||||
*remove_failed = 0;
|
||||
if (!remove_tree(path)) return 0;
|
||||
if (i == MOVE_TRIES) return -1;
|
||||
plat_sleep_ms(50);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- save transactions
|
||||
@@ -190,7 +224,6 @@ static int exists(const char *p) { return plat_fs_kind(p) != PLAT_FS_MISSING; }
|
||||
|
||||
// Rename a directory, durably; retried every 50 ms, tries times in all, while another process (a virus scanner, the
|
||||
// indexer) holds a file in it. 0 on success.
|
||||
#define MOVE_TRIES 40 // 2 s
|
||||
static int move_dir(const char *from, const char *to, int tries) {
|
||||
for (int i = 0; i < tries; i++) {
|
||||
int e = plat_fs_rename(from, to, 1);
|
||||
@@ -446,7 +479,8 @@ static void save_recover(void) {
|
||||
did = swap_in() ? "a finished commit still waits in save-tx; the game loads it from there"
|
||||
: "completed a finished commit";
|
||||
if (exists(work_root)) {
|
||||
if (remove_tree(work_root)) rt_fatal("fs: cannot delete the old save working copy");
|
||||
if (remove_tree_retry(work_root))
|
||||
rt_fatal("fs: cannot delete the old save working copy: %s (OS error %d)", remove_failed, remove_error);
|
||||
if (!strcmp(did, "nothing to recover"))
|
||||
did = "deleted the last run's working copy (changes after its last Commit)";
|
||||
}
|
||||
@@ -1575,6 +1609,15 @@ static void probe_save_tx(const uint64_t *uid) {
|
||||
CALL(S_UNMOUNT, U("ro"));
|
||||
CHECK(RES(S_DELETE_FILE, U(a)) == 0 && RES(S_DELETE_FILE, U(b)) == 0 && tx_commit() == 0 &&
|
||||
tx_read_committed("tx_a.sav") == -1, "Commit of deletions");
|
||||
// a run that left read-only folders and files in the working copy (attributes another emulator's save carried):
|
||||
// the next start deletes them and names the failing path if it cannot
|
||||
char *ro_dir = join(work_root, "ro_dir"), *ro_file = join(ro_dir, "ro.sav");
|
||||
CHECK(!plat_fs_mkdir(ro_dir) && plat_file_write(ro_file, "x", 1) && plat_file_set_readonly(ro_file, 1) &&
|
||||
plat_file_set_readonly(ro_dir, 1), "read-only leftovers in the working copy");
|
||||
probe_restart(uid);
|
||||
CHECK(!exists(ro_dir), "the next start deleted a working copy with read-only folders");
|
||||
free(ro_dir);
|
||||
free(ro_file);
|
||||
printf("save tx: uncommitted writes visible to the game and dropped by a restart and by Unmount, commits kept; a "
|
||||
"crash at each of %d commit steps recovers to the old save %d times, the new one %d times, mixed %d "
|
||||
"times; %d backups\n", steps, olds, news, mixed, backups);
|
||||
|
||||
+12
-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,12 @@ 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);
|
||||
uint64_t gfx_shader_failures(void); // failed translations; feeds GfxStats.shader_failures
|
||||
// Reflection: parsed once per shader from the translator's .res.json and the SPIR-V before it enters the cache.
|
||||
enum { GFX_RES_UBO, GFX_RES_SSBO, GFX_RES_TEX, GFX_RES_IMG, GFX_RES_KINDS };
|
||||
@@ -94,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);
|
||||
}
|
||||
|
||||
+93
-21
@@ -23,9 +23,9 @@
|
||||
#include "gfx.h"
|
||||
#include "gfx_wsys.h"
|
||||
#include "nvn_api.h"
|
||||
#include "platform/platform.h"
|
||||
#include "ui/menu.h"
|
||||
#include "vk.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define FRAMES 2
|
||||
#define MAX_IMAGES 16
|
||||
@@ -33,13 +33,25 @@
|
||||
#define VSYNC_HZ 60
|
||||
|
||||
// ---- queue lock
|
||||
// The recorder queues the parts it records and submits them later (vk_rec.c rec_submit): the first lock of a thread
|
||||
// waits until the parts queued before it are submitted, so whatever it puts on the queue comes after them, as when
|
||||
// the submitting thread submitted them itself. Not inside a held lock: the recorder takes this lock to submit.
|
||||
static PlatRMutex g_ql;
|
||||
void vk_queue_lock(void) { plat_rmutex_lock(&g_ql); }
|
||||
void vk_queue_unlock(void) { plat_rmutex_unlock(&g_ql); }
|
||||
static _Thread_local int ql_depth;
|
||||
void vk_queue_lock(void) {
|
||||
if (!ql_depth) rec_queue_wait();
|
||||
plat_rmutex_lock(&g_ql);
|
||||
ql_depth++;
|
||||
}
|
||||
void vk_queue_unlock(void) {
|
||||
ql_depth--;
|
||||
plat_rmutex_unlock(&g_ql);
|
||||
}
|
||||
|
||||
// ---- window state shared with the back end
|
||||
static VkSurfaceKHR g_surface;
|
||||
static volatile int32_t g_dirty; // the window was resized; swapchain stale
|
||||
static volatile int32_t g_restart; // gfx_window_request_restart: closing the window starts the game again
|
||||
static volatile int32_t g_focused = 1;
|
||||
static volatile int32_t g_focus_msgs;
|
||||
static PlatRMutex g_start_lock; // gfx_window_start / vsync start
|
||||
@@ -47,25 +59,44 @@ static int g_started;
|
||||
|
||||
void gfx_window_mark_dirty(void) { plat_xchg32(&g_dirty, 1); }
|
||||
|
||||
// The window gained or lost the focus: the game hears it as nn::oe's focus state and a notification message.
|
||||
// The window gained or lost the focus: the game hears it as nn::oe's focus state and a notification message. The line
|
||||
// carries the local wall time, the only one in the log: when a GPU hang takes the whole system down, the log ends
|
||||
// without an error, and this time set against the driver's entry in the Windows event log (nvlddmkm 153, Display 4101)
|
||||
// tells whether an alt-tab or an overlay came before the hang or followed it.
|
||||
void gfx_window_focus_changed(int in) {
|
||||
plat_xchg32(&g_focused, in);
|
||||
oe_set_focus(in); // pinned to 1 by TOTK_ASSUME_FOCUS inside
|
||||
oe_push_message(OE_MSG_FOCUS_STATE_CHANGED);
|
||||
plat_inc32(&g_focus_msgs);
|
||||
rt_log("gfx: window %s the focus", in ? "gained" : "lost");
|
||||
struct tm t;
|
||||
plat_local_time(&t);
|
||||
rt_log("gfx: window %s the focus at %04d-%02d-%02d %02d:%02d:%02d local time", in ? "gained" : "lost",
|
||||
t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
|
||||
}
|
||||
|
||||
// The window was closed: the process ends with it, as in MNG, once a save Commit still running in the background is
|
||||
// done.
|
||||
// done. After gfx_window_request_restart the executable then starts again with the same command line and environment
|
||||
// (plat_restart), and the new process waits until this one is gone (main.c) before it moves the log and opens the
|
||||
// RomFS and the saves.
|
||||
void gfx_window_closed(void) {
|
||||
rt_log("gfx: window closed; exiting");
|
||||
int restart = plat_cas32(&g_restart, 0, 0) != 0;
|
||||
rt_log("gfx: window closed; %s", restart ? "restarting" : "exiting");
|
||||
fs_save_flush();
|
||||
vk_pipeline_save();
|
||||
playtime_save();
|
||||
prof_write(); // plat_exit runs no atexit handlers
|
||||
if (restart) {
|
||||
plat_setenv("TOTK_REEXEC", NULL); // the new process counts its own address-space retries (memory.c)
|
||||
if (!plat_restart()) rt_log("gfx: the restart could not start the game again");
|
||||
}
|
||||
plat_exit(0);
|
||||
}
|
||||
|
||||
void gfx_window_request_restart(void) {
|
||||
plat_xchg32(&g_restart, 1);
|
||||
gfx_window_request_close();
|
||||
}
|
||||
|
||||
// TOTK_FULLSCREEN=1: borderless window over the monitor. TOTK_WINDOW_SIZE=<w>x<h>: client area of the window (the
|
||||
// swapchain follows the window; the game image is scaled to it).
|
||||
int gfx_cfg_fullscreen(void) { return cfg_bool("TOTK_FULLSCREEN"); }
|
||||
@@ -93,6 +124,7 @@ double gfx_window_refresh_hz(void) { return plat_cas32(&g_rate_mhz, 0, 0) / 1000
|
||||
static PlatRMutex g_pl;
|
||||
static VkSwapchainKHR g_sc;
|
||||
static VkFormat g_sc_fmt;
|
||||
static int g_sc_hdr; // HDR10 (TOTK_HDR, vk_hdr.c): PQ code values in Rec. 2020
|
||||
static VkExtent2D g_sc_ext;
|
||||
static int g_scaler; // the last present: 0 the game image 1:1, 1 scaled by the blit, 2 by FSR
|
||||
static uint32_t g_sc_n;
|
||||
@@ -215,6 +247,7 @@ static void window_settings_live(void) {
|
||||
if (started && v == seen) return;
|
||||
seen = v;
|
||||
res_fov_live();
|
||||
res_dynres_live();
|
||||
const char *e = cfg_value("TOTK_VK_PRESENT");
|
||||
if (!started || strncmp(e, present, sizeof present - 1)) {
|
||||
snprintf(present, sizeof present, "%s", e);
|
||||
@@ -275,8 +308,7 @@ static void free_capture(void) {
|
||||
g_cap_valid = 0;
|
||||
}
|
||||
|
||||
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held.
|
||||
static int create_swapchain(void) {
|
||||
static int build_swapchain(void) {
|
||||
flip_locked(); // the acquired image goes out before its swapchain is replaced
|
||||
VkCtx *c = vk_ctx();
|
||||
VkSurfaceCapabilitiesKHR caps;
|
||||
@@ -292,9 +324,9 @@ static int create_swapchain(void) {
|
||||
if (nf > 64) nf = 64;
|
||||
vkGetPhysicalDeviceSurfaceFormatsKHR(c->phys, g_surface, &nf, fmts);
|
||||
if (!nf) return 0;
|
||||
// UNORM: the blit writes the bytes the game wrote (no sRGB encode).
|
||||
// UNORM: the blit writes the bytes the game wrote (no sRGB encode). TOTK_HDR: HDR10 when the surface offers it.
|
||||
VkSurfaceFormatKHR sf = fmts[0];
|
||||
int found = 0;
|
||||
int hdr = vk_hdr_surface_format(fmts, nf, &sf), found = hdr;
|
||||
for (uint32_t i = 0; i < nf && !found; i++)
|
||||
if (fmts[i].format == VK_FORMAT_B8G8R8A8_UNORM && fmts[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { sf = fmts[i]; found = 1; }
|
||||
for (uint32_t i = 0; i < nf && !found; i++)
|
||||
@@ -328,6 +360,9 @@ static int create_swapchain(void) {
|
||||
}
|
||||
if (old) vkDestroySwapchainKHR(c->device, old, NULL);
|
||||
g_sc_fmt = sf.format;
|
||||
g_sc_hdr = hdr;
|
||||
vk_hdr_set_active(hdr);
|
||||
if (hdr) vk_hdr_metadata(g_sc, 1);
|
||||
g_sc_ext = ext;
|
||||
g_sc_n = MAX_IMAGES;
|
||||
vkGetSwapchainImagesKHR(c->device, g_sc, &g_sc_n, g_sc_img);
|
||||
@@ -341,10 +376,19 @@ static int create_swapchain(void) {
|
||||
}
|
||||
free_capture();
|
||||
vk_queue_unlock();
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held. The stale mark
|
||||
// goes before the surface is read: a resize during the build keeps it. The switch to fullscreen at start lands there
|
||||
// on the window thread, and the driver does not report the swapchain as out of date.
|
||||
static int create_swapchain(void) {
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
if (build_swapchain()) return 1;
|
||||
plat_xchg32(&g_dirty, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sync_barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags2 s0,
|
||||
VkAccessFlags2 a0, VkPipelineStageFlags2 s1, VkAccessFlags2 a1) {
|
||||
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = s0, .srcAccessMask = a0,
|
||||
@@ -411,12 +455,14 @@ static int ensure_thumbs(void) {
|
||||
}
|
||||
g_th_ptr = info.pMappedData;
|
||||
}
|
||||
if (g_th_img && g_th_fmt != g_sc_fmt) {
|
||||
// an HDR10 swapchain blits into RGBA8: its PQ codes, flat-looking but readable as bytes
|
||||
VkFormat tf = g_sc_hdr ? VK_FORMAT_R8G8B8A8_UNORM : g_sc_fmt;
|
||||
if (g_th_img && g_th_fmt != tf) {
|
||||
vmaDestroyImage(c->vma, g_th_img, g_th_img_alloc); // the previous present's fence was waited for: idle
|
||||
g_th_img = VK_NULL_HANDLE;
|
||||
}
|
||||
if (!g_th_img) {
|
||||
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = g_sc_fmt,
|
||||
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = tf,
|
||||
.extent = {FLIGHT_THUMB_W, FLIGHT_THUMB_H, 1}, .mipLevels = 1, .arrayLayers = 1,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
@@ -428,7 +474,7 @@ static int ensure_thumbs(void) {
|
||||
rt_log("gfx: flight thumbnails off: no %ux%u image", FLIGHT_THUMB_W, FLIGHT_THUMB_H);
|
||||
return 0;
|
||||
}
|
||||
g_th_fmt = g_sc_fmt;
|
||||
g_th_fmt = tf;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -502,7 +548,8 @@ static void save_capture(const char *path) {
|
||||
uint8_t *px = malloc(n);
|
||||
if (!px) return;
|
||||
int bgra = g_sc_fmt == VK_FORMAT_B8G8R8A8_UNORM || g_sc_fmt == VK_FORMAT_B8G8R8A8_SRGB;
|
||||
for (size_t i = 0; i < n; i += 4) {
|
||||
if (g_sc_hdr) vk_hdr_to_srgb8(g_cap_ptr, px, n / 4, g_sc_fmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32);
|
||||
else for (size_t i = 0; i < n; i += 4) {
|
||||
px[i] = g_cap_ptr[i + (bgra ? 2 : 0)];
|
||||
px[i + 1] = g_cap_ptr[i + 1];
|
||||
px[i + 2] = g_cap_ptr[i + (bgra ? 0 : 2)];
|
||||
@@ -517,6 +564,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;
|
||||
@@ -554,6 +610,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) goto out; // lost, not ready: skip the frame
|
||||
if (r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1);
|
||||
vkResetFences(c->device, 1, &g_fence[slot]);
|
||||
if (g_sc_hdr) vk_hdr_metadata(g_sc, 0); // a changed peak or white goes to the display
|
||||
if (g_th_pending[slot]) { // that fence has signalled: the thumbnail its submission wrote is complete
|
||||
g_th_tag[g_th_pending[slot] % (uint64_t)g_th_n] = g_th_pending[slot];
|
||||
g_th_pending[slot] = 0;
|
||||
@@ -568,7 +625,8 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
window_settings_live();
|
||||
// The menu overlay: laid out before recording (its first frame starts the ImGui back end and uploads the font)
|
||||
const char *menu_shot = NULL;
|
||||
MenuTarget mt = {c->instance, c->phys, c->device, c->gfx_family, c->gfx_queue, g_sc_fmt, g_sc_ext.width,
|
||||
MenuTarget mt = {c->instance, c->phys, c->device, c->gfx_family, c->gfx_queue,
|
||||
g_sc_hdr ? VK_FORMAT_R8G8B8A8_UNORM : g_sc_fmt, g_sc_ext.width,
|
||||
g_sc_ext.height, cw, ch, g_scaler};
|
||||
int overlay = g_sc_view[idx] &&
|
||||
menu_overlay_frame(__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), &mt, &menu_shot);
|
||||
@@ -588,6 +646,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};
|
||||
@@ -596,8 +658,10 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
}
|
||||
// Colour adjustment (TOTK_COLOR_*, vk_color.c): an adjusted copy of the crop takes the game image's place below
|
||||
VkImage graded = general ? vk_color_record(cb, slot, src, fmt, rect) : VK_NULL_HANDLE;
|
||||
// Colour adjustment (TOTK_COLOR_*, vk_color.c), or on an HDR10 swapchain the PQ encode (vk_hdr.c; the adjustment is
|
||||
// for SDR): a copy of the crop takes the game image's place below
|
||||
VkImage graded = !general ? VK_NULL_HANDLE : g_sc_hdr ? vk_hdr_encode(cb, slot, src, fmt, rect)
|
||||
: vk_color_record(cb, slot, src, fmt, rect);
|
||||
const int grect[4] = {0, 0, cw, ch};
|
||||
VkImage psrc = graded ? graded : src;
|
||||
VkFormat pfmt = graded ? VK_FORMAT_R16G16B16A16_SFLOAT : fmt;
|
||||
@@ -623,18 +687,24 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
VkImageLayout last = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
VkPipelineStageFlags2 last_stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
|
||||
VkAccessFlags2 last_write = VK_ACCESS_2_TRANSFER_WRITE_BIT;
|
||||
// HDR10: the menu goes into vk_hdr.c's RGBA8 image, which is then blended onto the swapchain image at the HUD's white
|
||||
VkImageView menu_view = overlay && g_sc_hdr ? vk_hdr_ui_target(cb, g_sc_ext.width, g_sc_ext.height) : VK_NULL_HANDLE;
|
||||
if (overlay && g_sc_hdr && !menu_view) overlay = 0;
|
||||
if (overlay) {
|
||||
sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, last_stage, last_write,
|
||||
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT);
|
||||
VkRenderingAttachmentInfo ca = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = g_sc_view[idx],
|
||||
VkRenderingAttachmentInfo ca = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
|
||||
.imageView = menu_view ? menu_view : g_sc_view[idx],
|
||||
.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
||||
.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE};
|
||||
.loadOp = menu_view ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD,
|
||||
.storeOp = VK_ATTACHMENT_STORE_OP_STORE};
|
||||
VkRenderingInfo ri = {.sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, g_sc_ext}, .layerCount = 1,
|
||||
.colorAttachmentCount = 1, .pColorAttachments = &ca};
|
||||
vkCmdBeginRendering(cb, &ri);
|
||||
menu_overlay_draw(cb);
|
||||
vkCmdEndRendering(cb);
|
||||
if (menu_view) vk_hdr_ui_compose(cb, g_sc_view[idx], g_sc_fmt, g_sc_ext.width, g_sc_ext.height);
|
||||
last = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
last_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
last_write = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
@@ -728,6 +798,8 @@ void gfx_window_release_vk(void) {
|
||||
menu_vk_release();
|
||||
vk_fsr_release();
|
||||
vk_color_release();
|
||||
vk_hdr_release();
|
||||
vk_contact_release();
|
||||
free_capture();
|
||||
free_thumbs();
|
||||
for (int i = 0; i < FRAMES; i++) {
|
||||
|
||||
+109
-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);
|
||||
@@ -589,6 +591,26 @@ static GfxShader *disk_load_from(const uint8_t key[32], const uint8_t *iid, int
|
||||
s->reflection = (char *)res;
|
||||
s->reflection_size = res_size;
|
||||
reflect(s);
|
||||
if (s->stage == GFX_STAGE_FS) vk_hdr_shader_swap(&s->spirv, &s->words); // the in-memory copy; the cache keeps its own
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -758,18 +780,81 @@ out:
|
||||
free(loc), free(packed), free(ttype), free(tcomps), free(pointee);
|
||||
}
|
||||
|
||||
// Every gradient sample (OpImageSampleExplicitLod with Grad) whose two gradients are each built from two components gets
|
||||
// them transposed, dx = (a, c) and dy = (b, d) from dx = (a, b) and dy = (c, d), by two new OpCompositeConstruct before
|
||||
// it. TotK's shaders fill a TXD's gradient registers per coordinate (du/dx, du/dy, dv/dx, dv/dy), and the translator
|
||||
// (Ryujinx, InstEmitTexture EmitTxd) builds dx = (first two registers), dy = (last two) = (du/dx, du/dy) and
|
||||
// (dv/dx, dv/dy): a mip level chosen from the wrong derivatives, too sharp on terrain faces turned across the screen,
|
||||
// which crawled in patches that followed the faces (1.4.3, the Depths). TOTK_TXD_TRANSPOSE=0 turns it off.
|
||||
// Applied to the SPIR-V after it is loaded, so the shader cache is untouched.
|
||||
enum { OP_COMPOSITE_CONSTRUCT = 80, OP_SAMPLE_EXPLICIT = 88, IO_GRAD = 4 };
|
||||
|
||||
static void patch_txd_transpose(GfxShader *s) {
|
||||
static int on = -1;
|
||||
static volatile int32_t n_logged;
|
||||
if (on < 0) {
|
||||
const char *e = cfg_get("TOTK_TXD_TRANSPOSE");
|
||||
on = !(e && *e == '0');
|
||||
}
|
||||
uint32_t *w = s->spirv;
|
||||
uint64_t n = s->words;
|
||||
if (!on || n < 5) return;
|
||||
uint32_t bound = w[3] < (1u << 20) ? w[3] : (1u << 20);
|
||||
uint64_t *cc = calloc(bound, sizeof *cc); // word index of a 2-constituent OpCompositeConstruct defining the id
|
||||
int found = 0;
|
||||
if (!cc) return;
|
||||
for (uint64_t i = 5; i < n;) {
|
||||
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
|
||||
if (!len || i + len > n) goto out;
|
||||
if (op == OP_COMPOSITE_CONSTRUCT && len == 5 && w[i + 2] < bound) cc[w[i + 2]] = i;
|
||||
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
|
||||
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1])
|
||||
found++;
|
||||
i += len;
|
||||
}
|
||||
if (!found) goto out;
|
||||
uint32_t *o = malloc((n + (uint64_t)found * 10) * sizeof *o);
|
||||
if (!o) goto out;
|
||||
uint64_t k = 0;
|
||||
uint32_t next = w[3];
|
||||
for (uint64_t i = 0; i < 5; i++) o[k++] = w[i];
|
||||
for (uint64_t i = 5; i < n;) {
|
||||
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
|
||||
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
|
||||
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1]) {
|
||||
const uint32_t *dx = w + cc[w[i + 6]], *dy = w + cc[w[i + 7]]; // [hdr, type, id, a, b], [hdr, type, id, c, d]
|
||||
uint32_t ndx = next++, ndy = next++;
|
||||
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndx, o[k++] = dx[3], o[k++] = dy[3];
|
||||
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndy, o[k++] = dx[4], o[k++] = dy[4];
|
||||
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
|
||||
o[k - len + 6] = ndx, o[k - len + 7] = ndy;
|
||||
} else {
|
||||
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
o[3] = next;
|
||||
free(s->spirv);
|
||||
s->spirv = o, s->words = k;
|
||||
if (plat_inc32(&n_logged) <= 4) rt_log("gfx_shader: %d gradient samples now read their derivatives per coordinate", found);
|
||||
out:
|
||||
free(cc);
|
||||
}
|
||||
|
||||
static void reflect(GfxShader *s) {
|
||||
static const char *const kinds[GFX_RES_KINDS] = {"cbufs", "sbufs", "textures", "images"};
|
||||
const char *js = s->reflection;
|
||||
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]);
|
||||
json_objects(js, kinds[k], add_res, s, k);
|
||||
}
|
||||
reflect_spirv(s);
|
||||
patch_txd_transpose(s);
|
||||
}
|
||||
|
||||
// ---- use masks (see the cache key at the top)
|
||||
@@ -974,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);
|
||||
}
|
||||
|
||||
@@ -1006,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.
|
||||
@@ -1085,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, "\"}");
|
||||
@@ -1287,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);
|
||||
@@ -1643,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;
|
||||
@@ -1732,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.
|
||||
|
||||
@@ -53,9 +53,9 @@ static int hexval(char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; }
|
||||
// parent of totk.exe that has one with a jobs.json, the running game version's first, then the others' (any census
|
||||
// serves a probe). 1 with that folder in dir.
|
||||
int probe_census_dir(char *dir, size_t n) {
|
||||
const char *const versions[3] = {RT_V111 ? "v111" : RT_V143 ? "v143" : "v121",
|
||||
RT_V111 ? "v121" : RT_V143 ? "v121" : "v143",
|
||||
RT_V111 ? "v143" : RT_V143 ? "v111" : "v111"};
|
||||
const char *const versions[3] = {RT_V110 ? "v110" : RT_V143 ? "v143" : "v121",
|
||||
RT_V110 ? "v121" : RT_V143 ? "v121" : "v143",
|
||||
RT_V110 ? "v143" : RT_V143 ? "v110" : "v110"};
|
||||
for (int v = 0; v < 3; v++) {
|
||||
char exe[PLAT_PATH_MAX * 2];
|
||||
int len = plat_exe_path(exe, sizeof exe);
|
||||
|
||||
@@ -131,6 +131,7 @@ static int present_and_check(Source *s, int first, int i, int *w_out, int *h_out
|
||||
}
|
||||
|
||||
int probe_window(void) {
|
||||
vk_hdr_off(); // the checks read 8-bit swapchain pixels
|
||||
if (!menu_font_check()) return fail("the menu's font check takes a damaged font or refuses a good one");
|
||||
gfx_window_start();
|
||||
VkCtx *c = vk_ctx();
|
||||
|
||||
@@ -64,6 +64,8 @@ double gfx_window_sdl_density(void) { return __atomic_load_n(&g_density_milli, _
|
||||
|
||||
SDL_Window *gfx_window_sdl(void) { return g_win; }
|
||||
|
||||
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg) { run_on_window(fn, arg); }
|
||||
|
||||
// ---- settings: size and fullscreen
|
||||
static char g_applied_size[32];
|
||||
static int g_applied_fs;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/hdr_encode.comp; do not edit.
|
||||
// source sha256 08e1d94f9041b32d4183286db39941eec9cde41412babb197c0303fd64bb07a7
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t hdr_encode_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000000dd, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000004c, 0x00060010, 0x00000004,
|
||||
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000004c, 0x0000000b, 0x0000001c,
|
||||
0x00030047, 0x00000053, 0x00000002, 0x00050048, 0x00000053, 0x00000000, 0x00000023, 0x00000000,
|
||||
0x00050048, 0x00000053, 0x00000001, 0x00000023, 0x00000008, 0x00050048, 0x00000053, 0x00000002,
|
||||
0x00000023, 0x00000010, 0x00040047, 0x00000064, 0x00000021, 0x00000000, 0x00040047, 0x00000064,
|
||||
0x00000022, 0x00000000, 0x00030047, 0x0000009c, 0x00000019, 0x00040047, 0x0000009c, 0x00000021,
|
||||
0x00000001, 0x00040047, 0x0000009c, 0x00000022, 0x00000000, 0x00040047, 0x000000a7, 0x0000000b,
|
||||
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006,
|
||||
0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000003, 0x0004002b, 0x00000006, 0x00000015,
|
||||
0x3d6147ae, 0x0004002b, 0x00000006, 0x0000001b, 0x4019999a, 0x0006002c, 0x00000007, 0x0000001c,
|
||||
0x0000001b, 0x0000001b, 0x0000001b, 0x0004002b, 0x00000006, 0x0000001f, 0x3d25aee6, 0x0006002c,
|
||||
0x00000007, 0x00000020, 0x0000001f, 0x0000001f, 0x0000001f, 0x00020014, 0x00000021, 0x00040017,
|
||||
0x00000022, 0x00000021, 0x00000003, 0x0004002b, 0x00000006, 0x0000002c, 0x00000000, 0x0004002b,
|
||||
0x00000006, 0x0000002d, 0x3f800000, 0x0004002b, 0x00000006, 0x00000031, 0x3e232000, 0x0006002c,
|
||||
0x00000007, 0x00000032, 0x00000031, 0x00000031, 0x00000031, 0x0004002b, 0x00000006, 0x00000034,
|
||||
0x3f560000, 0x0004002b, 0x00000006, 0x00000035, 0x4196d000, 0x0004002b, 0x00000006, 0x0000003a,
|
||||
0x41958000, 0x0004002b, 0x00000006, 0x00000040, 0x429db000, 0x0006002c, 0x00000007, 0x00000041,
|
||||
0x00000040, 0x00000040, 0x00000040, 0x00040015, 0x00000045, 0x00000020, 0x00000001, 0x00040017,
|
||||
0x00000046, 0x00000045, 0x00000002, 0x00040015, 0x00000049, 0x00000020, 0x00000000, 0x00040017,
|
||||
0x0000004a, 0x00000049, 0x00000003, 0x00040020, 0x0000004b, 0x00000001, 0x0000004a, 0x0004003b,
|
||||
0x0000004b, 0x0000004c, 0x00000001, 0x00040017, 0x0000004d, 0x00000049, 0x00000002, 0x00040017,
|
||||
0x00000052, 0x00000006, 0x00000004, 0x0005001e, 0x00000053, 0x00000046, 0x00000046, 0x00000052,
|
||||
0x00040020, 0x00000054, 0x00000009, 0x00000053, 0x0004003b, 0x00000054, 0x00000055, 0x00000009,
|
||||
0x0004002b, 0x00000045, 0x00000056, 0x00000001, 0x00040020, 0x00000057, 0x00000009, 0x00000046,
|
||||
0x00040017, 0x0000005a, 0x00000021, 0x00000002, 0x00090019, 0x00000061, 0x00000006, 0x00000001,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x00000062, 0x00000061,
|
||||
0x00040020, 0x00000063, 0x00000000, 0x00000062, 0x0004003b, 0x00000063, 0x00000064, 0x00000000,
|
||||
0x0004002b, 0x00000045, 0x00000067, 0x00000000, 0x0006002c, 0x00000007, 0x0000006e, 0x0000002c,
|
||||
0x0000002c, 0x0000002c, 0x0004002b, 0x00000045, 0x00000070, 0x00000002, 0x0004002b, 0x00000049,
|
||||
0x00000071, 0x00000002, 0x00040020, 0x00000072, 0x00000009, 0x00000006, 0x0004002b, 0x00000006,
|
||||
0x00000075, 0x3f000000, 0x00040018, 0x00000080, 0x00000007, 0x00000003, 0x0004002b, 0x00000006,
|
||||
0x00000081, 0x3f209d49, 0x0004002b, 0x00000006, 0x00000082, 0x3d8d844d, 0x0004002b, 0x00000006,
|
||||
0x00000083, 0x3c86594b, 0x0006002c, 0x00000007, 0x00000084, 0x00000081, 0x00000082, 0x00000083,
|
||||
0x0004002b, 0x00000006, 0x00000085, 0x3ea89a02, 0x0004002b, 0x00000006, 0x00000086, 0x3f6b645a,
|
||||
0x0004002b, 0x00000006, 0x00000087, 0x3db43958, 0x0006002c, 0x00000007, 0x00000088, 0x00000085,
|
||||
0x00000086, 0x00000087, 0x0004002b, 0x00000006, 0x00000089, 0x3d315b57, 0x0004002b, 0x00000006,
|
||||
0x0000008a, 0x3c3ac711, 0x0004002b, 0x00000006, 0x0000008b, 0x3f65460b, 0x0006002c, 0x00000007,
|
||||
0x0000008c, 0x00000089, 0x0000008a, 0x0000008b, 0x0006002c, 0x00000080, 0x0000008d, 0x00000084,
|
||||
0x00000088, 0x0000008c, 0x0004002b, 0x00000049, 0x00000091, 0x00000000, 0x0004002b, 0x00000049,
|
||||
0x00000095, 0x00000001, 0x00090019, 0x0000009a, 0x00000006, 0x00000001, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000002, 0x00000002, 0x00040020, 0x0000009b, 0x00000000, 0x0000009a, 0x0004003b,
|
||||
0x0000009b, 0x0000009c, 0x00000000, 0x0004002b, 0x00000049, 0x000000a6, 0x00000008, 0x0006002c,
|
||||
0x0000004a, 0x000000a7, 0x000000a6, 0x000000a6, 0x00000095, 0x0006002c, 0x00000007, 0x000000d1,
|
||||
0x0000002d, 0x0000002d, 0x0000002d, 0x0006002c, 0x00000007, 0x000000d3, 0x00000015, 0x00000015,
|
||||
0x00000015, 0x0006002c, 0x00000007, 0x000000d6, 0x00000034, 0x00000034, 0x00000034, 0x0004002b,
|
||||
0x00000006, 0x000000d7, 0x3d9e8391, 0x0006002c, 0x00000007, 0x000000d8, 0x000000d7, 0x000000d7,
|
||||
0x000000d7, 0x0004002b, 0x00000006, 0x000000d9, 0x3f72a76f, 0x0006002c, 0x00000007, 0x000000da,
|
||||
0x000000d9, 0x000000d9, 0x000000d9, 0x0004002b, 0x00000006, 0x000000db, 0x38d1b717, 0x0006002c,
|
||||
0x00000007, 0x000000dc, 0x000000db, 0x000000db, 0x000000db, 0x00050036, 0x00000002, 0x00000004,
|
||||
0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x000300f7, 0x000000a8, 0x00000000, 0x000300fb,
|
||||
0x00000091, 0x000000a9, 0x000200f8, 0x000000a9, 0x0004003d, 0x0000004a, 0x0000004e, 0x0000004c,
|
||||
0x0007004f, 0x0000004d, 0x0000004f, 0x0000004e, 0x0000004e, 0x00000000, 0x00000001, 0x0004007c,
|
||||
0x00000046, 0x00000050, 0x0000004f, 0x00050041, 0x00000057, 0x00000058, 0x00000055, 0x00000056,
|
||||
0x0004003d, 0x00000046, 0x00000059, 0x00000058, 0x000500af, 0x0000005a, 0x0000005b, 0x00000050,
|
||||
0x00000059, 0x0004009a, 0x00000021, 0x0000005c, 0x0000005b, 0x000300f7, 0x0000005e, 0x00000000,
|
||||
0x000400fa, 0x0000005c, 0x0000005d, 0x0000005e, 0x000200f8, 0x0000005d, 0x000200f9, 0x000000a8,
|
||||
0x000200f8, 0x0000005e, 0x0004003d, 0x00000062, 0x00000065, 0x00000064, 0x00050041, 0x00000057,
|
||||
0x00000068, 0x00000055, 0x00000067, 0x0004003d, 0x00000046, 0x00000069, 0x00000068, 0x00050080,
|
||||
0x00000046, 0x0000006a, 0x00000050, 0x00000069, 0x00040064, 0x00000061, 0x0000006b, 0x00000065,
|
||||
0x0007005f, 0x00000052, 0x0000006c, 0x0000006b, 0x0000006a, 0x00000002, 0x00000067, 0x0008004f,
|
||||
0x00000007, 0x0000006d, 0x0000006c, 0x0000006c, 0x00000000, 0x00000001, 0x00000002, 0x0007000c,
|
||||
0x00000007, 0x0000006f, 0x00000001, 0x00000028, 0x0000006d, 0x0000006e, 0x00060041, 0x00000072,
|
||||
0x00000073, 0x00000055, 0x00000070, 0x00000071, 0x0004003d, 0x00000006, 0x00000074, 0x00000073,
|
||||
0x000500ba, 0x00000021, 0x00000076, 0x00000074, 0x00000075, 0x000300f7, 0x00000078, 0x00000000,
|
||||
0x000400fa, 0x00000076, 0x00000077, 0x00000078, 0x000200f8, 0x00000077, 0x0008000c, 0x00000007,
|
||||
0x0000007c, 0x00000001, 0x0000002b, 0x0000006f, 0x0000006e, 0x000000d1, 0x00050085, 0x00000007,
|
||||
0x000000b2, 0x0000007c, 0x000000d8, 0x00050081, 0x00000007, 0x000000b5, 0x0000007c, 0x000000d3,
|
||||
0x00050085, 0x00000007, 0x000000b7, 0x000000b5, 0x000000da, 0x0007000c, 0x00000007, 0x000000b8,
|
||||
0x00000001, 0x0000001a, 0x000000b7, 0x0000001c, 0x000500ba, 0x00000022, 0x000000ba, 0x0000007c,
|
||||
0x00000020, 0x000600a9, 0x00000007, 0x000000bb, 0x000000ba, 0x000000b8, 0x000000b2, 0x000200f9,
|
||||
0x00000078, 0x000200f8, 0x00000078, 0x000700f5, 0x00000007, 0x000000d0, 0x0000006f, 0x0000005e,
|
||||
0x000000bb, 0x00000077, 0x00050091, 0x00000007, 0x0000008f, 0x0000008d, 0x000000d0, 0x0007000c,
|
||||
0x00000007, 0x00000090, 0x00000001, 0x00000028, 0x0000008f, 0x0000006e, 0x00060041, 0x00000072,
|
||||
0x00000092, 0x00000055, 0x00000070, 0x00000091, 0x0004003d, 0x00000006, 0x00000093, 0x00000092,
|
||||
0x0005008e, 0x00000007, 0x00000094, 0x00000090, 0x00000093, 0x00060041, 0x00000072, 0x00000096,
|
||||
0x00000055, 0x00000070, 0x00000095, 0x0004003d, 0x00000006, 0x00000097, 0x00000096, 0x00060050,
|
||||
0x00000007, 0x00000098, 0x00000097, 0x00000097, 0x00000097, 0x0007000c, 0x00000007, 0x00000099,
|
||||
0x00000001, 0x00000025, 0x00000094, 0x00000098, 0x0004003d, 0x0000009a, 0x0000009d, 0x0000009c,
|
||||
0x00050085, 0x00000007, 0x000000c1, 0x00000099, 0x000000dc, 0x0008000c, 0x00000007, 0x000000c4,
|
||||
0x00000001, 0x0000002b, 0x000000c1, 0x0000006e, 0x000000d1, 0x0007000c, 0x00000007, 0x000000c5,
|
||||
0x00000001, 0x0000001a, 0x000000c4, 0x00000032, 0x0005008e, 0x00000007, 0x000000c7, 0x000000c5,
|
||||
0x00000035, 0x00050081, 0x00000007, 0x000000c9, 0x000000d6, 0x000000c7, 0x0005008e, 0x00000007,
|
||||
0x000000cb, 0x000000c5, 0x0000003a, 0x00050081, 0x00000007, 0x000000cd, 0x000000d1, 0x000000cb,
|
||||
0x00050088, 0x00000007, 0x000000ce, 0x000000c9, 0x000000cd, 0x0007000c, 0x00000007, 0x000000cf,
|
||||
0x00000001, 0x0000001a, 0x000000ce, 0x00000041, 0x00050051, 0x00000006, 0x000000a2, 0x000000cf,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x000000a3, 0x000000cf, 0x00000001, 0x00050051, 0x00000006,
|
||||
0x000000a4, 0x000000cf, 0x00000002, 0x00070050, 0x00000052, 0x000000a5, 0x000000a2, 0x000000a3,
|
||||
0x000000a4, 0x0000002d, 0x00040063, 0x0000009d, 0x00000050, 0x000000a5, 0x000200f9, 0x000000a8,
|
||||
0x000200f8, 0x000000a8, 0x000100fd, 0x00010038,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/hdr_fullscreen.vert; do not edit.
|
||||
// source sha256 3474de7753260cdbc5df61bbd96074c566f98c9be15be4791ae7cbea32542b03
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t hdr_fullscreen_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x0000002c, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0008000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x0000001d,
|
||||
0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000c, 0x0000000b, 0x0000002a,
|
||||
0x00030047, 0x0000001b, 0x00000002, 0x00050048, 0x0000001b, 0x00000000, 0x0000000b, 0x00000000,
|
||||
0x00050048, 0x0000001b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000001b, 0x00000002,
|
||||
0x0000000b, 0x00000003, 0x00050048, 0x0000001b, 0x00000003, 0x0000000b, 0x00000004, 0x00020013,
|
||||
0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017,
|
||||
0x00000007, 0x00000006, 0x00000002, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b,
|
||||
0x00000008, 0x00000009, 0x00000003, 0x00040015, 0x0000000a, 0x00000020, 0x00000001, 0x00040020,
|
||||
0x0000000b, 0x00000001, 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000001, 0x0004002b,
|
||||
0x0000000a, 0x0000000e, 0x00000001, 0x0004002b, 0x0000000a, 0x00000010, 0x00000002, 0x00040017,
|
||||
0x00000017, 0x00000006, 0x00000004, 0x00040015, 0x00000018, 0x00000020, 0x00000000, 0x0004002b,
|
||||
0x00000018, 0x00000019, 0x00000001, 0x0004001c, 0x0000001a, 0x00000006, 0x00000019, 0x0006001e,
|
||||
0x0000001b, 0x00000017, 0x00000006, 0x0000001a, 0x0000001a, 0x00040020, 0x0000001c, 0x00000003,
|
||||
0x0000001b, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000003, 0x0004002b, 0x0000000a, 0x0000001e,
|
||||
0x00000000, 0x0004002b, 0x00000006, 0x00000020, 0x40000000, 0x0004002b, 0x00000006, 0x00000022,
|
||||
0x3f800000, 0x0004002b, 0x00000006, 0x00000025, 0x00000000, 0x00040020, 0x00000029, 0x00000003,
|
||||
0x00000017, 0x0005002c, 0x00000007, 0x0000002b, 0x00000022, 0x00000022, 0x00050036, 0x00000002,
|
||||
0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000a, 0x0000000d,
|
||||
0x0000000c, 0x000500c4, 0x0000000a, 0x0000000f, 0x0000000d, 0x0000000e, 0x000500c7, 0x0000000a,
|
||||
0x00000011, 0x0000000f, 0x00000010, 0x0004006f, 0x00000006, 0x00000012, 0x00000011, 0x000500c7,
|
||||
0x0000000a, 0x00000014, 0x0000000d, 0x00000010, 0x0004006f, 0x00000006, 0x00000015, 0x00000014,
|
||||
0x00050050, 0x00000007, 0x00000016, 0x00000012, 0x00000015, 0x0003003e, 0x00000009, 0x00000016,
|
||||
0x0004003d, 0x00000007, 0x0000001f, 0x00000009, 0x0005008e, 0x00000007, 0x00000021, 0x0000001f,
|
||||
0x00000020, 0x00050083, 0x00000007, 0x00000024, 0x00000021, 0x0000002b, 0x00050051, 0x00000006,
|
||||
0x00000026, 0x00000024, 0x00000000, 0x00050051, 0x00000006, 0x00000027, 0x00000024, 0x00000001,
|
||||
0x00070050, 0x00000017, 0x00000028, 0x00000026, 0x00000027, 0x00000025, 0x00000022, 0x00050041,
|
||||
0x00000029, 0x0000002a, 0x0000001d, 0x0000001e, 0x0003003e, 0x0000002a, 0x00000028, 0x000100fd,
|
||||
0x00010038,
|
||||
};
|
||||
@@ -0,0 +1,290 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/hdr_tonemap.frag; do not edit.
|
||||
// source sha256 a09ae2b13d844bb6a136d2c3cf38b3ec571191775a4c2f0b43a586bd355381fa
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t hdr_tonemap_spv[] = {
|
||||
0x07230203, 0x00010300, 0x0008000b, 0x0000018a, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
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,
|
||||
0x00000023, 0x00000000, 0x00040047, 0x00000069, 0x00000021, 0x00000003, 0x00040047, 0x00000069,
|
||||
0x00000022, 0x00000000, 0x00040047, 0x00000070, 0x00000006, 0x00000010, 0x00030047, 0x00000071,
|
||||
0x00000002, 0x00050048, 0x00000071, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x00000073,
|
||||
0x00000021, 0x00000004, 0x00040047, 0x00000073, 0x00000022, 0x00000000, 0x00040047, 0x00000078,
|
||||
0x00000006, 0x00000010, 0x00030047, 0x00000079, 0x00000002, 0x00050048, 0x00000079, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00050048, 0x00000079, 0x00000001, 0x00000023, 0x000005a0, 0x00040047,
|
||||
0x0000007b, 0x00000021, 0x00000000, 0x00040047, 0x0000007b, 0x00000022, 0x00000000, 0x00040047,
|
||||
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, 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, 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, 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,
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/hdr_ui.frag; do not edit.
|
||||
// source sha256 59cfbb1ab5393fdb743016cbf75a085ee8a05929c7ea67a69b4ab77f26a14303
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t hdr_ui_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000000be, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x0000004f, 0x00000068, 0x00030010,
|
||||
0x00000004, 0x00000007, 0x00040047, 0x0000004b, 0x00000021, 0x00000000, 0x00040047, 0x0000004b,
|
||||
0x00000022, 0x00000000, 0x00040047, 0x0000004f, 0x0000001e, 0x00000000, 0x00040047, 0x00000068,
|
||||
0x0000001e, 0x00000000, 0x00030047, 0x0000007b, 0x00000002, 0x00050048, 0x0000007b, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016,
|
||||
0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000003, 0x0004002b, 0x00000006,
|
||||
0x00000015, 0x3d6147ae, 0x0004002b, 0x00000006, 0x0000001b, 0x4019999a, 0x0006002c, 0x00000007,
|
||||
0x0000001c, 0x0000001b, 0x0000001b, 0x0000001b, 0x0004002b, 0x00000006, 0x0000001f, 0x3d25aee6,
|
||||
0x0006002c, 0x00000007, 0x00000020, 0x0000001f, 0x0000001f, 0x0000001f, 0x00020014, 0x00000021,
|
||||
0x00040017, 0x00000022, 0x00000021, 0x00000003, 0x0004002b, 0x00000006, 0x0000002c, 0x00000000,
|
||||
0x0004002b, 0x00000006, 0x0000002d, 0x3f800000, 0x0004002b, 0x00000006, 0x00000031, 0x3e232000,
|
||||
0x0006002c, 0x00000007, 0x00000032, 0x00000031, 0x00000031, 0x00000031, 0x0004002b, 0x00000006,
|
||||
0x00000034, 0x3f560000, 0x0004002b, 0x00000006, 0x00000035, 0x4196d000, 0x0004002b, 0x00000006,
|
||||
0x0000003a, 0x41958000, 0x0004002b, 0x00000006, 0x00000040, 0x429db000, 0x0006002c, 0x00000007,
|
||||
0x00000041, 0x00000040, 0x00000040, 0x00000040, 0x00040017, 0x00000045, 0x00000006, 0x00000004,
|
||||
0x00090019, 0x00000048, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
|
||||
0x00000000, 0x0003001b, 0x00000049, 0x00000048, 0x00040020, 0x0000004a, 0x00000000, 0x00000049,
|
||||
0x0004003b, 0x0000004a, 0x0000004b, 0x00000000, 0x00040017, 0x0000004d, 0x00000006, 0x00000002,
|
||||
0x00040020, 0x0000004e, 0x00000001, 0x0000004d, 0x0004003b, 0x0000004e, 0x0000004f, 0x00000001,
|
||||
0x00040015, 0x00000052, 0x00000020, 0x00000000, 0x00040020, 0x00000067, 0x00000003, 0x00000045,
|
||||
0x0004003b, 0x00000067, 0x00000068, 0x00000003, 0x00040018, 0x00000069, 0x00000007, 0x00000003,
|
||||
0x0004002b, 0x00000006, 0x0000006a, 0x3f209d49, 0x0004002b, 0x00000006, 0x0000006b, 0x3d8d844d,
|
||||
0x0004002b, 0x00000006, 0x0000006c, 0x3c86594b, 0x0006002c, 0x00000007, 0x0000006d, 0x0000006a,
|
||||
0x0000006b, 0x0000006c, 0x0004002b, 0x00000006, 0x0000006e, 0x3ea89a02, 0x0004002b, 0x00000006,
|
||||
0x0000006f, 0x3f6b645a, 0x0004002b, 0x00000006, 0x00000070, 0x3db43958, 0x0006002c, 0x00000007,
|
||||
0x00000071, 0x0000006e, 0x0000006f, 0x00000070, 0x0004002b, 0x00000006, 0x00000072, 0x3d315b57,
|
||||
0x0004002b, 0x00000006, 0x00000073, 0x3c3ac711, 0x0004002b, 0x00000006, 0x00000074, 0x3f65460b,
|
||||
0x0006002c, 0x00000007, 0x00000075, 0x00000072, 0x00000073, 0x00000074, 0x0006002c, 0x00000069,
|
||||
0x00000076, 0x0000006d, 0x00000071, 0x00000075, 0x0006002c, 0x00000007, 0x00000079, 0x0000002c,
|
||||
0x0000002c, 0x0000002c, 0x0003001e, 0x0000007b, 0x00000045, 0x00040020, 0x0000007c, 0x00000009,
|
||||
0x0000007b, 0x0004003b, 0x0000007c, 0x0000007d, 0x00000009, 0x00040015, 0x0000007e, 0x00000020,
|
||||
0x00000001, 0x0004002b, 0x0000007e, 0x0000007f, 0x00000000, 0x0004002b, 0x00000052, 0x00000080,
|
||||
0x00000000, 0x00040020, 0x00000081, 0x00000009, 0x00000006, 0x0006002c, 0x00000007, 0x000000b2,
|
||||
0x0000002d, 0x0000002d, 0x0000002d, 0x0006002c, 0x00000007, 0x000000b4, 0x00000015, 0x00000015,
|
||||
0x00000015, 0x0006002c, 0x00000007, 0x000000b7, 0x00000034, 0x00000034, 0x00000034, 0x0004002b,
|
||||
0x00000006, 0x000000b8, 0x3d9e8391, 0x0006002c, 0x00000007, 0x000000b9, 0x000000b8, 0x000000b8,
|
||||
0x000000b8, 0x0004002b, 0x00000006, 0x000000ba, 0x3f72a76f, 0x0006002c, 0x00000007, 0x000000bb,
|
||||
0x000000ba, 0x000000ba, 0x000000ba, 0x0004002b, 0x00000006, 0x000000bc, 0x38d1b717, 0x0006002c,
|
||||
0x00000007, 0x000000bd, 0x000000bc, 0x000000bc, 0x000000bc, 0x00050036, 0x00000002, 0x00000004,
|
||||
0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x00000049, 0x0000004c, 0x0000004b,
|
||||
0x0004003d, 0x0000004d, 0x00000050, 0x0000004f, 0x00050057, 0x00000045, 0x00000051, 0x0000004c,
|
||||
0x00000050, 0x00050051, 0x00000006, 0x00000056, 0x00000051, 0x00000003, 0x000500bc, 0x00000021,
|
||||
0x00000057, 0x00000056, 0x0000002c, 0x000300f7, 0x00000059, 0x00000000, 0x000400fa, 0x00000057,
|
||||
0x00000058, 0x00000059, 0x000200f8, 0x00000058, 0x000100fc, 0x000200f8, 0x00000059, 0x0008004f,
|
||||
0x00000007, 0x0000005d, 0x00000051, 0x00000051, 0x00000000, 0x00000001, 0x00000002, 0x00060050,
|
||||
0x00000007, 0x00000060, 0x00000056, 0x00000056, 0x00000056, 0x00050088, 0x00000007, 0x00000061,
|
||||
0x0000005d, 0x00000060, 0x0008000c, 0x00000007, 0x00000064, 0x00000001, 0x0000002b, 0x00000061,
|
||||
0x00000079, 0x000000b2, 0x00050085, 0x00000007, 0x00000091, 0x00000064, 0x000000b9, 0x00050081,
|
||||
0x00000007, 0x00000094, 0x00000064, 0x000000b4, 0x00050085, 0x00000007, 0x00000096, 0x00000094,
|
||||
0x000000bb, 0x0007000c, 0x00000007, 0x00000097, 0x00000001, 0x0000001a, 0x00000096, 0x0000001c,
|
||||
0x000500ba, 0x00000022, 0x00000099, 0x00000064, 0x00000020, 0x000600a9, 0x00000007, 0x0000009a,
|
||||
0x00000099, 0x00000097, 0x00000091, 0x00050091, 0x00000007, 0x00000078, 0x00000076, 0x0000009a,
|
||||
0x0007000c, 0x00000007, 0x0000007a, 0x00000001, 0x00000028, 0x00000078, 0x00000079, 0x00060041,
|
||||
0x00000081, 0x00000082, 0x0000007d, 0x0000007f, 0x00000080, 0x0004003d, 0x00000006, 0x00000083,
|
||||
0x00000082, 0x0005008e, 0x00000007, 0x00000084, 0x0000007a, 0x00000083, 0x00050085, 0x00000007,
|
||||
0x000000a0, 0x00000084, 0x000000bd, 0x0008000c, 0x00000007, 0x000000a3, 0x00000001, 0x0000002b,
|
||||
0x000000a0, 0x00000079, 0x000000b2, 0x0007000c, 0x00000007, 0x000000a4, 0x00000001, 0x0000001a,
|
||||
0x000000a3, 0x00000032, 0x0005008e, 0x00000007, 0x000000a6, 0x000000a4, 0x00000035, 0x00050081,
|
||||
0x00000007, 0x000000a8, 0x000000b7, 0x000000a6, 0x0005008e, 0x00000007, 0x000000aa, 0x000000a4,
|
||||
0x0000003a, 0x00050081, 0x00000007, 0x000000ac, 0x000000b2, 0x000000aa, 0x00050088, 0x00000007,
|
||||
0x000000ad, 0x000000a8, 0x000000ac, 0x0007000c, 0x00000007, 0x000000ae, 0x00000001, 0x0000001a,
|
||||
0x000000ad, 0x00000041, 0x00050051, 0x00000006, 0x00000089, 0x000000ae, 0x00000000, 0x00050051,
|
||||
0x00000006, 0x0000008a, 0x000000ae, 0x00000001, 0x00050051, 0x00000006, 0x0000008b, 0x000000ae,
|
||||
0x00000002, 0x00070050, 0x00000045, 0x0000008c, 0x00000089, 0x0000008a, 0x0000008b, 0x00000056,
|
||||
0x0003003e, 0x00000068, 0x0000008c, 0x000100fd, 0x00010038,
|
||||
};
|
||||
+60
-2
@@ -36,6 +36,10 @@ void perf_end(int kind, uint64_t t0); // counts render-thread time onl
|
||||
void perf_replay(uint64_t t0); // replay (NVN -> Vulkan recording) time on any thread, waits inside it included
|
||||
void perf_replay_thread(void); // the calling thread replays for the render thread: perf_end counts its waits
|
||||
void perf_main_thread(void); // the calling thread is the guest main thread (TOTK_PROFILE_MAIN)
|
||||
// The calling thread is one every frame goes through: on a hybrid CPU it prefers the P-cores (main.c, TOTK_PCORE);
|
||||
// the first thread of each role logs it.
|
||||
enum { PCORE_MAIN, PCORE_RENDER, PCORE_REPLAY, PCORE_RECORDER, PCORE_PRESENT, PCORE_N };
|
||||
void perf_core_thread(int role);
|
||||
// Events per 60-frame window: graphics pipelines finished by the background workers, draws skipped while pending,
|
||||
// first texture uploads and re-uploads after CPU writes, draws skipped while a shader translates in the background,
|
||||
// draws that waited for their pending pipeline and got it, draws into a target the previous frame did not render that
|
||||
@@ -57,6 +61,9 @@ typedef struct {
|
||||
int have_gpu, have_vram, vram_exact;
|
||||
uint64_t gpu_ns, gpu_parts, gpu_missed; // busy time, submission parts timed, parts left untimed
|
||||
uint64_t vram_used, vram_budget;
|
||||
int have_idle; // calibrated timestamps: the idle split below is filled
|
||||
uint64_t idle_ns, starved_ns, queued_ns; // GPU idle between timed parts; of it, before the part was submitted; submit to start
|
||||
uint64_t idle_gaps, max_gap_ns; // gaps over 0.5 ms; the longest
|
||||
} PerfGpu;
|
||||
extern void (*perf_gpu_stats)(PerfGpu *out);
|
||||
// The performance overlay (perfmon.c): mode 0 off, 1 fps, 2 full. While it is on, perf_present hands every frame to
|
||||
@@ -164,6 +171,26 @@ void mem_thaw(uint64_t a, uint64_t n); // makes the frozen r
|
||||
void mem_freeze_stats(uint64_t *frozen, uint64_t *freezes, uint64_t *fault_thaws, uint64_t *other_thaws);
|
||||
// regions frozen now; freezes and thaws since the last call
|
||||
void mem_kernel_free(void *p, uint64_t size); // TLR/TLS reuse after thread exit
|
||||
int mem_guest_readable(uint64_t a, uint64_t n); // [a, a+n) can be read without a fault: inside a loaded
|
||||
// module or the committed heap answered from the layout,
|
||||
// anything else by plat_mem_readable (an OS query)
|
||||
|
||||
// ---- short out-of-memory spells (oom.c)
|
||||
enum { RT_OOM_COMMIT, RT_OOM_STAGING, RT_OOM_IMAGE, RT_OOM_CHUNK, RT_OOM_SITES };
|
||||
extern volatile int32_t rt_oom_req; // set by rt_oom_request; vk_texture_new_submit takes it
|
||||
void rt_oom_request(void); // asks the Vulkan back end to give memory back at its next
|
||||
// submit (released textures, headroom blocks); any thread,
|
||||
// any lock held
|
||||
int rt_oom_wait(const char *what, uint64_t bytes, int (*retry)(void *arg), void *arg);
|
||||
// after a failed allocation: requests a release, then calls
|
||||
// retry every 10 ms for up to 30 s; 1 once retry returned 1,
|
||||
// 0 after the wait (the caller ends the game). Logs the
|
||||
// first 8 with the commit numbers
|
||||
void *rt_realloc_wait(void *old, size_t n, const char *what); // realloc, NULL only after rt_oom_wait gave up
|
||||
int rt_oom_forced(int site); // 1: fail this attempt at site (TOTK_TEST_OOM=<site>:<n>,
|
||||
// sites commit, staging, image, chunk; or rt_oom_force)
|
||||
void rt_oom_force(int site, int n); // probes: fail the next n attempts at site
|
||||
int rt_oom_forced_left(int site); // forced failures still to come at site
|
||||
|
||||
// ---- modules (loader.c)
|
||||
typedef struct {
|
||||
@@ -184,14 +211,16 @@ typedef struct { const char *file, *sha256; } RtFileHash;
|
||||
extern const RtFileHash rt_exefs_sha256[]; // tools/data/expected-<version>.json "exefs_sha256"
|
||||
extern const int rt_exefs_sha256_count;
|
||||
extern const char rt_default_exefs[], rt_default_romfs[], rt_default_profile[];
|
||||
extern const int rt_game_version; // the game build: 111 (1.1.1), 121 (1.2.1) or 143 (1.4.3)
|
||||
extern const int rt_game_version; // the game build: 110 (1.1.0), 121 (1.2.1) or 143 (1.4.3)
|
||||
extern const char rt_port_version[]; // VERSION at the build's root: 0.1.0, 0.1.0+973c626 in a release
|
||||
#define RT_V111 (rt_game_version == 111) // 1.1.1: its own boot constants and data slots
|
||||
#define RT_V110 (rt_game_version == 110) // 1.1.0: its own boot constants and data slots
|
||||
#define RT_V143 (rt_game_version >= 143)
|
||||
|
||||
// ---- kernel (kernel.c)
|
||||
typedef struct RtThread RtThread;
|
||||
Cpu *kern_cpu(void); // current guest thread's Cpu, NULL on host threads
|
||||
void kern_free_cores_note(void); // for crash reports: logs that free_cores = 1 lifted the
|
||||
// virtual core gate, nothing when the gate is on
|
||||
uint32_t kern_main_thread(Cpu **cpu); // main thread object and Cpu for the calling host thread
|
||||
// (main.npdm: 1 MiB stack at the top of the stack region,
|
||||
// priority 44, core 0); on the main thread: its handle
|
||||
@@ -242,19 +271,41 @@ void account_uid(uint64_t out[2]); // the local user's n
|
||||
// ---- movies (ov_movie.c)
|
||||
void movie_tick(Cpu *c); // at every present, on the presenting guest thread: hands
|
||||
// decoded movie frames over when due, signals their events
|
||||
// ---- camera speed (camera_speed.c): TOTK_STICK_SENS
|
||||
float cam_speed_wanted(void); // the factor TOTK_STICK_SENS asks for, 0.1 to 3
|
||||
float cam_speed_applied(void); // the factor in effect: 1 until romfs.c serves the pack
|
||||
int cam_pack_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, void **out, size_t *out_n);
|
||||
// Pack/Actor/PlayerCamera.pack.zs with the camera speeds
|
||||
// multiplied, malloc'd; 0 = factor 1 or cannot (logged)
|
||||
void cam_pack_served(void); // romfs.c serves the multiplied pack
|
||||
// ---- game resolution (resolution.c): TOTK_OUTPUT_RES, TOTK_RENDER_SCALE
|
||||
uint64_t res_heap_extra_bytes(void); // added to the application pool (mem_init), 2 MiB steps
|
||||
uint64_t res_heap_extra_kib(void); // added to the HeapDef texture heap and its parent; 0 = none
|
||||
uint64_t res_tex_heap_extra_bytes(void); // TOTK_TEX_HEAP_EXTRA_MB: added to the application pool and
|
||||
// the heap cap (mem_init), 2 MiB steps; 0 = none
|
||||
int res_heapdef_tex_grown(const void *in, size_t n); // the HeapDef table (.byml.zs) grows TextureHeap over the
|
||||
// shipped size (an HD texture pack's)
|
||||
int res_heapdef_plain(const void *in, size_t n); // a readable HeapDef table whose TextureHeap is not
|
||||
// grown (a content mod's)
|
||||
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); // the HeapDef table (.byml.zs) with
|
||||
// the grown heaps, malloc'd; 0 = cannot (logged)
|
||||
void res_heapdef_served(void); // romfs.c serves the grown table
|
||||
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)
|
||||
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
|
||||
int romfs_shaped_ready(unsigned w, unsigned h); // romfs-patches-<w>x<h> has a manifest (romfs.c; works
|
||||
// before fs_init)
|
||||
int romfs_shaped_prompts_ready(const char *folder, unsigned w, unsigned h); // the same for <folder>-<w>x<h>, a
|
||||
// prompts' folder (romfs-patches-xbox, -xbox-label, -pc)
|
||||
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
|
||||
void res_fov_live(void); // TOTK_FOV to the camera (set fov); on a settings change
|
||||
void res_dynres_live(void); // TOTK_DYNAMIC_RES to the game (set fixres); on a settings change
|
||||
// ---- code patches (patches.c) and the frame step (frame_rate.c)
|
||||
void rt_patches_init(void); // after ldr_load: switches from the environment, the patch
|
||||
// page committed; logs the sets
|
||||
@@ -295,6 +346,13 @@ void rt_boot(void); // boot.c
|
||||
// process exit code.
|
||||
int probe_cfg(void); // cfg_probe.c
|
||||
int probe_dpi(void); // dpi_probe_win.c (Windows only)
|
||||
int probe_mods(void); // mods_probe.c
|
||||
void discord_init(void); // discord_rpc.c: the Rich Presence thread, game runs only
|
||||
void playtime_init(void); // playtime.c: the play time thread, game runs only
|
||||
void playtime_save(void); // playtime.c: writes <profile>/playtime.txt; at exit
|
||||
int playtime_get(uint64_t *total_s, uint64_t *session_s); // playtime.c: all runs and this one; 0 without a profile
|
||||
int probe_playtime(void); // playtime.c: the file, the ticks and the save
|
||||
int probe_discord(void); // discord_rpc.c
|
||||
int probe_loader(void), probe_kernel(void), probe_ipc(void), probe_services(void), probe_fs(void);
|
||||
int probe_gfx_abi(void); // gfx_abi_probe.c
|
||||
int probe_vkdevice(void); // vk_device.c
|
||||
|
||||
+25
-6
@@ -35,7 +35,7 @@ typedef struct {
|
||||
const char *name;
|
||||
GuestFn fn;
|
||||
uint64_t original; // observers: the guest definition rt_original calls
|
||||
uint64_t calls;
|
||||
uint64_t calls; // with TOTK_TRACE=host only (hostfn_report)
|
||||
int blocking; // runs with the virtual core given up (hostfn_blocking)
|
||||
} Slot;
|
||||
static Slot slots[MAX_HOSTFN];
|
||||
@@ -44,6 +44,21 @@ static RtHostEntry added[MAX_ADDED];
|
||||
static int nadded;
|
||||
static int sealed;
|
||||
|
||||
// The call count (hostfn_calls): one counter per calling thread, each on its own cache line, written only by that
|
||||
// thread. A locked add on a per-function counter beside the read-mostly slot fields made the game threads share and
|
||||
// bounce those lines on every host call (1.4.3 town: about 1% of the game threads' time in hostfn_entry).
|
||||
#define MAX_COUNTERS 4096
|
||||
typedef struct { _Alignas(64) uint64_t n; } Counter;
|
||||
static Counter counters[MAX_COUNTERS];
|
||||
static uint32_t ncounters;
|
||||
static uint64_t shared_calls; // the threads past MAX_COUNTERS, counted with an atomic add
|
||||
static __thread uint64_t *thread_calls;
|
||||
|
||||
__attribute__((noinline)) static uint64_t *counter_new(void) {
|
||||
uint32_t i = __atomic_fetch_add(&ncounters, 1, __ATOMIC_RELAXED);
|
||||
return thread_calls = i < MAX_COUNTERS ? &counters[i].n : &shared_calls;
|
||||
}
|
||||
|
||||
static GuestFn table_find(const RtHostEntry *start, const RtHostEntry *stop, const char *name) {
|
||||
__asm__("" : "+r"(start), "+r"(stop)); // separate objects: keep the compiler from reasoning about them
|
||||
for (const RtHostEntry *p = start; p < stop; p++)
|
||||
@@ -121,7 +136,7 @@ __attribute__((noinline, noreturn, cold)) static void bad_entry(Cpu *c) {
|
||||
}
|
||||
|
||||
// kern_host_track: the call counted in the thread's host depth. Not a tail call, so the host stack holds this frame
|
||||
// while fn runs; longjmp out of fn (ExitThread) leaves the depth raised on a thread that ends.
|
||||
// while fn runs; the jump out of fn (ExitThread) leaves the depth raised on a thread that ends.
|
||||
__attribute__((noinline)) static void hostfn_tracked(Cpu *c, GuestFn fn) {
|
||||
void *t = c->thread;
|
||||
int *d = kern_host_depth(t);
|
||||
@@ -146,8 +161,11 @@ static void hostfn_entry(Cpu *c) {
|
||||
uint64_t i = (c->pc - RT_HOSTFN_BASE) >> 4;
|
||||
if (__builtin_expect((c->pc & 15) || i >= (uint64_t)nslots, 0)) bad_entry(c);
|
||||
Slot *s = &slots[i];
|
||||
uint64_t n = __atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED);
|
||||
if (__builtin_expect(!n && (rt_trace & RT_TRACE_HOSTCALL), 0)) {
|
||||
uint64_t *k = thread_calls;
|
||||
if (__builtin_expect(!k, 0)) k = counter_new();
|
||||
if (__builtin_expect(k == &shared_calls, 0)) __atomic_fetch_add(k, 1, __ATOMIC_RELAXED);
|
||||
else __atomic_store_n(k, *k + 1, __ATOMIC_RELAXED);
|
||||
if (__builtin_expect(rt_trace & RT_TRACE_HOSTCALL, 0) && !__atomic_fetch_add(&s->calls, 1, __ATOMIC_RELAXED)) {
|
||||
char lr[96];
|
||||
rt_log("host call %s from %s", s->name, ldr_describe(c->x[30] - 4, lr, sizeof lr));
|
||||
}
|
||||
@@ -178,8 +196,9 @@ void hostfn_seal(void) {
|
||||
}
|
||||
|
||||
uint64_t hostfn_calls(void) {
|
||||
uint64_t n = 0;
|
||||
for (int i = 0; i < nslots; i++) n += __atomic_load_n(&slots[i].calls, __ATOMIC_RELAXED);
|
||||
uint32_t m = __atomic_load_n(&ncounters, __ATOMIC_RELAXED);
|
||||
uint64_t n = __atomic_load_n(&shared_calls, __ATOMIC_RELAXED);
|
||||
for (uint32_t i = 0; i < m && i < MAX_COUNTERS; i++) n += __atomic_load_n(&counters[i].n, __ATOMIC_RELAXED);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
+266
-28
@@ -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;
|
||||
|
||||
@@ -236,12 +274,87 @@ static float mouse_sens_get(void) {
|
||||
}
|
||||
|
||||
|
||||
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`.
|
||||
static void mouse_camera(int32_t out[2]) {
|
||||
// ---- walk key: the keyboard's stick samples scaled down while a key is held (TOTK_WALK_KEY, TOTK_WALK_SCALE)
|
||||
// A keyboard key is all or nothing, so every keyboard-driven stick sample is +/-32767 and the game only ever sees a
|
||||
// full tilt. Holding this key scales those two samples, which gives the half tilt a gamepad gets from its analog range,
|
||||
// so the keyboard can walk as well as run. yuzu and eden do the same with their modifier and modifier_scale settings.
|
||||
// Only the keyboard is scaled: the gamepad's axes and the mouse camera sample their own values.
|
||||
static int walk_key = KEY_NONE; // TOTK_WALK_KEY as a KEY_*, KEY_NONE = off; live (cfg_version)
|
||||
static float walk_scale = 0.51f; // TOTK_WALK_SCALE, 0.2 to 0.9; live (cfg_version)
|
||||
static unsigned walk_seen = ~0u;
|
||||
|
||||
// The ini takes the key names the Controls tab writes; a hand-written line may spell them in any case.
|
||||
static int name_matches(const char *name, const char *key) {
|
||||
for (; *name && *key; name++, key++) {
|
||||
unsigned char a = (unsigned char)*name, b = (unsigned char)*key;
|
||||
if (a >= 'A' && a <= 'Z') a = (unsigned char)(a - 'A' + 'a');
|
||||
if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a');
|
||||
if (a != b) return 0;
|
||||
}
|
||||
return *name == *key;
|
||||
}
|
||||
|
||||
static void walk_get(void) {
|
||||
unsigned ver = cfg_version();
|
||||
if (ver == walk_seen) return;
|
||||
walk_seen = ver;
|
||||
const char *name = cfg_value("TOTK_WALK_KEY"); // "none" (the default) turns the key off
|
||||
int key = KEY_NONE;
|
||||
if (*name && !name_matches(name, "none")) {
|
||||
for (int k = KEY_A; k < KEY_COUNT && key == KEY_NONE; k++)
|
||||
if (input_key_name(k) && name_matches(name, input_key_name(k))) key = k;
|
||||
if (key == KEY_NONE) rt_log("input: walk_key: unknown key '%s', the keyboard sticks keep their full tilt", name);
|
||||
}
|
||||
// 0.51 rather than 0.5: a keyboard sample is 32767, and 32767 * 0.5 = 16383 falls one short of stick_bits'
|
||||
// STICK_DIR (16384), which would leave the game's stick-direction bits clear while the walk key is held. The value
|
||||
// in the settings table carries that default; this code only rejects what the table's range does not allow.
|
||||
const char *sv = cfg_value("TOTK_WALK_SCALE");
|
||||
char *end;
|
||||
double d = strtod(sv, &end);
|
||||
double s = end == sv || *end || !(d >= 0.2 && d <= 0.9) ? strtod(cfg_meta("TOTK_WALK_SCALE")->def, NULL) : d;
|
||||
if (key != walk_key || (float)s != walk_scale) {
|
||||
walk_key = key;
|
||||
walk_scale = (float)s;
|
||||
rt_log("input: walk key %s, keyboard sticks at %.0f%% of their tilt (mouse and gamepad unchanged)",
|
||||
key != KEY_NONE ? input_key_name(key) : "off", s * 100);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -259,20 +372,23 @@ static void mouse_camera(int32_t out[2]) {
|
||||
held[0] = held[1] = 0; // frames stopped: do not hold a deflection
|
||||
}
|
||||
}
|
||||
out[0] = held[0];
|
||||
out[1] = held[1];
|
||||
double k = cam_speed_applied(), x = held[0] / k, y = held[1] / k, len = sqrt(x * x + y * y);
|
||||
if (len > STICK_MAX) x *= STICK_MAX / len, y *= STICK_MAX / len;
|
||||
out[0] = (int32_t)lround(x);
|
||||
out[1] = (int32_t)lround(y);
|
||||
}
|
||||
|
||||
static float stick_dead = -1; // TOTK_STICK_DEADZONE as a fraction of full deflection; live (cfg_version)
|
||||
static float stick_dead[2] = {-1, -1}; // input_pad_deadzone of the left and right stick; live (cfg_version)
|
||||
static unsigned stick_dead_seen;
|
||||
|
||||
static float stick_dead_get(void) {
|
||||
static float stick_dead_get(int s) {
|
||||
unsigned ver = cfg_version();
|
||||
if (stick_dead >= 0 && ver == stick_dead_seen) return stick_dead;
|
||||
stick_dead_seen = ver;
|
||||
double v = cfg_double("TOTK_STICK_DEADZONE");
|
||||
stick_dead = (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
|
||||
return stick_dead;
|
||||
if (stick_dead[0] < 0 || ver != stick_dead_seen) {
|
||||
stick_dead_seen = ver;
|
||||
stick_dead[0] = input_pad_deadzone(0);
|
||||
stick_dead[1] = input_pad_deadzone(1);
|
||||
}
|
||||
return stick_dead[s];
|
||||
}
|
||||
|
||||
// One gamepad stick through the dead zone; SDL's Y grows downwards, the Switch's upwards.
|
||||
@@ -407,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(); }
|
||||
@@ -425,12 +588,28 @@ static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
|
||||
uint8_t pd[PAD_COUNT] = {0};
|
||||
if (!pad_physical(pd)) return 0;
|
||||
*b |= input_map_pad_buttons(pd, pad_layout_get());
|
||||
float dead = stick_dead_get();
|
||||
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, dead, sl);
|
||||
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, dead, sr);
|
||||
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, stick_dead_get(0), sl);
|
||||
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, stick_dead_get(1), sr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int input_pad_stick_percent(double pct[2]) {
|
||||
int ok = 0;
|
||||
pct[0] = pct[1] = 0;
|
||||
plat_mutex_lock(&lock);
|
||||
if (state == 1 && pad && SDL_GamepadConnected(pad)) { // the game's poll keeps the state fresh (SDL_UpdateGamepads)
|
||||
pct[0] = input_stick_percent(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX),
|
||||
SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY));
|
||||
pct[1] = input_stick_percent(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTX),
|
||||
SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTY));
|
||||
ok = 1;
|
||||
}
|
||||
plat_mutex_unlock(&lock);
|
||||
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};
|
||||
@@ -439,30 +618,65 @@ 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
|
||||
// (the click that refocused the window) is ignored until it is released.
|
||||
// (the click that refocused the window) is ignored until it is released. While a game menu has the pointer
|
||||
// (menu_cursor.c) and it is over the window, the left and right buttons are that menu's A and B, and the
|
||||
// middle and side buttons keep their bindings: bound to L and R they turn the menu's tabs.
|
||||
static int was_captured;
|
||||
static uint8_t mouse_ignore[KEY_COUNT];
|
||||
int cap = ikb_captured();
|
||||
float pointer_x, pointer_y;
|
||||
int menu_pointer = !cap && menu_cursor_pointer() && ikb_pointer(&pointer_x, &pointer_y);
|
||||
for (int k = 1; k < KEY_COUNT; k++) {
|
||||
down[k] = ikb_down(k);
|
||||
kbm_used |= down[k];
|
||||
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
|
||||
if (cap && !was_captured) mouse_ignore[k] = down[k];
|
||||
if (!down[k]) mouse_ignore[k] = 0;
|
||||
if (!cap || mouse_ignore[k]) down[k] = 0;
|
||||
int counts = cap || (menu_pointer && k != KEY_MOUSE_L && k != KEY_MOUSE_R);
|
||||
if (!counts || mouse_ignore[k]) down[k] = 0;
|
||||
}
|
||||
was_captured = cap;
|
||||
b |= input_map_buttons(down);
|
||||
int32_t k[2];
|
||||
walk_get();
|
||||
int walk = walk_key != KEY_NONE && down[walk_key];
|
||||
input_map_stick(down, 0, k);
|
||||
if (walk) k[0] = (int32_t)(k[0] * walk_scale), k[1] = (int32_t)(k[1] * walk_scale);
|
||||
pick(&sl[0], &sl[1], k[0], k[1]);
|
||||
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);
|
||||
@@ -478,21 +692,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();
|
||||
}
|
||||
@@ -500,20 +722,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.
|
||||
static int blocked(volatile int32_t *flag, uint64_t buttons) {
|
||||
// 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;
|
||||
return k && !(k == 2 && !buttons && plat_cas32(flag, 0, 2) == 2);
|
||||
if (k != 2) return k != 0;
|
||||
int held = buttons != 0;
|
||||
for (int i = 0; i < 4; i++) held |= axes[i] != 0;
|
||||
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);
|
||||
int text = blocked(&g_block, *buttons), menu = blocked(&g_menu, *buttons);
|
||||
if (!g_block && !g_menu) menu_cursor_poll(buttons);
|
||||
const int32_t axes[4] = {*lx, *ly, *rx, *ry};
|
||||
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);
|
||||
@@ -21,6 +50,10 @@ int input_overlay_gate(int *held, uint64_t *buttons); // L3+R3: 1 in the poll
|
||||
int input_pad_read(uint64_t *buttons, int32_t *lx, int32_t *ly);
|
||||
// The gamepad layout in effect (PAD_LAYOUT_*, input_map.h; TOTK_PAD_LAYOUT, live); -1 without a gamepad. Any thread.
|
||||
int input_pad_layout(void);
|
||||
// How far the open gamepad's left (pct[0]) and right (pct[1]) sticks are from the centre now, in percent of full
|
||||
// deflection, before the dead zone: the menu shows it on the dead zone rows, so a drifting stick's resting value can be
|
||||
// read off. 0 (and both 0) without a connected gamepad. Any thread.
|
||||
int input_pad_stick_percent(double pct[2]);
|
||||
// The label on a face button of the open gamepad (input_map.h PAD_SOUTH..PAD_NORTH, as SDL reports it); PAD_LABEL_NONE for another
|
||||
// button, an unknown label or no gamepad. Any thread.
|
||||
enum { PAD_LABEL_NONE, PAD_LABEL_A, PAD_LABEL_B, PAD_LABEL_X, PAD_LABEL_Y, PAD_LABEL_CROSS, PAD_LABEL_CIRCLE,
|
||||
|
||||
+52
-6
@@ -16,11 +16,11 @@
|
||||
#define DEFAULT_KEYS \
|
||||
{KEY_W, B_SL_UP}, {KEY_A, B_SL_LEFT}, {KEY_S, B_SL_DOWN}, {KEY_D, B_SL_RIGHT}, /* move */ \
|
||||
{KEY_UP, B_SR_UP}, {KEY_LEFT, B_SR_LEFT}, {KEY_DOWN, B_SR_DOWN}, {KEY_RIGHT, B_SR_RIGHT}, /* camera, second source */ \
|
||||
{KEY_MOUSE_L, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
|
||||
{KEY_MOUSE_L, B_Y}, {KEY_T, B_Y}, {KEY_MOUSE_R, B_ZL}, {KEY_Z, B_ZL}, {KEY_F, B_ZR}, {KEY_X, B_ZR}, \
|
||||
{KEY_SPACE, B_X}, {KEY_LSHIFT, B_B}, {KEY_E, B_A}, {KEY_Q, B_L}, {KEY_R, B_R}, \
|
||||
{KEY_LCTRL, B_STICK_L}, {KEY_C, B_STICK_R}, \
|
||||
{KEY_1, B_LEFT}, {KEY_2, B_UP}, {KEY_3, B_RIGHT}, {KEY_4, B_DOWN}, \
|
||||
{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 = 28, DEFAULT_PAD_COUNT = 16 };
|
||||
_Static_assert(sizeof (Binding[]){DEFAULT_KEYS} / sizeof (Binding) == DEFAULT_KEY_COUNT, "DEFAULT_KEY_COUNT");
|
||||
|
||||
static const InputMap defaults = {{DEFAULT_KEYS}, DEFAULT_KEY_COUNT, {{NINTENDO_PADS}, {XBOX_PADS}},
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -166,11 +166,27 @@ int input_map_bind(const char *button, const char *list) {
|
||||
|
||||
void input_map_configure(void) {
|
||||
input_map_reset();
|
||||
uint8_t lined[ACTION_COUNT] = {0};
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
char var[32];
|
||||
snprintf(var, sizeof var, "TOTK_BIND_%s", action_names[a]);
|
||||
const char *v = cfg_get(var);
|
||||
if (v && *v && input_map_bind(action_names[a], v)) rt_log("input: %s = %s", var + 5, v);
|
||||
if (v && *v && input_map_bind(action_names[a], v)) lined[a] = 1, rt_log("input: %s = %s", var + 5, v);
|
||||
}
|
||||
// A key a line binds leaves the defaults of the actions without a line, as the remapping rows move a key: with
|
||||
// bind_X = key:t, T presses X only, not also Y, whose defaults hold it.
|
||||
uint8_t bound[KEY_COUNT] = {0}, list[INPUT_BINDING_MAX];
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
if (!lined[a]) continue;
|
||||
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX);
|
||||
for (int i = 0; i < n; i++) bound[list[i]] = 1;
|
||||
}
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
if (lined[a]) continue;
|
||||
int n = input_map_keys_of(&input_map, a, list, INPUT_BINDING_MAX), w = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!bound[list[i]]) list[w++] = list[i];
|
||||
if (w < n) keys_put(&input_map, a, list, w);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +304,20 @@ int input_pad_layout_pick(const char *setting, int east_labelled_a) {
|
||||
return east_labelled_a ? PAD_LAYOUT_NINTENDO : PAD_LAYOUT_XBOX;
|
||||
}
|
||||
|
||||
int input_prompts_pick(const char *prompts, const char *pad_layout) {
|
||||
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.
|
||||
return input_pad_layout_pick(pad_layout, 0) == PAD_LAYOUT_NINTENDO ? "romfs-patches-xbox" : "romfs-patches-xbox-label";
|
||||
}
|
||||
|
||||
uint64_t input_map_pad_buttons(const uint8_t down[PAD_COUNT], int layout) {
|
||||
const PadBinding *t = input_map.pads[layout];
|
||||
uint64_t b = 0;
|
||||
@@ -303,6 +333,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++) {
|
||||
@@ -334,6 +370,16 @@ void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]) {
|
||||
out[1] = (int32_t)lrint(fmax(-1, fmin(1, fy * k)) * STICK_MAX);
|
||||
}
|
||||
|
||||
float input_pad_deadzone(int stick) {
|
||||
const char *own = stick ? "TOTK_RSTICK_DEADZONE" : "TOTK_LSTICK_DEADZONE", *set = cfg_get(own);
|
||||
double v = cfg_double(set && *set ? own : "TOTK_STICK_DEADZONE");
|
||||
return (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
|
||||
}
|
||||
|
||||
double input_stick_percent(int32_t x, int32_t y) {
|
||||
return sqrt((double)x * x + (double)y * y) * 100.0 / STICK_MAX;
|
||||
}
|
||||
|
||||
// Minus and Plus held together open or close the menu. *held: latched from the poll where both go down until neither is
|
||||
// down (the caller keeps it). Every poll of the latch reports neither to the game, so releasing one button first does not
|
||||
// leak the other; Minus alone before Plus reaches the game unchanged. With the menu open the game sees a neutral pad.
|
||||
|
||||
+34
-8
@@ -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,29 +69,47 @@ 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.
|
||||
// A key a line binds leaves the defaults of the actions that have no line.
|
||||
void input_map_configure(void);
|
||||
|
||||
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts" is the Nintendo layout with
|
||||
// the A/B and X/Y prompts swapped (runtime/romfs.c overlay_init), so the buttons map as "nintendo". Anything else
|
||||
// ("auto", NULL) picks Nintendo when the pad's east button is labelled A (a Switch Pro Controller, a GuliKit) and Xbox
|
||||
// otherwise.
|
||||
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts", 0.1.9's name for the Nintendo
|
||||
// layout with the Xbox prompts, maps the buttons as "nintendo". Anything else ("auto", NULL) picks Nintendo when the
|
||||
// pad's east button is labelled A (a Switch Pro Controller, a GuliKit) and Xbox otherwise.
|
||||
int input_pad_layout_pick(const char *setting, int east_labelled_a);
|
||||
|
||||
// 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). 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. 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]);
|
||||
@@ -100,6 +120,12 @@ void input_mouse_stick(int32_t dx, int32_t dy, double dt_s, float sens, int32_t
|
||||
// 0..0.5): inside the circle the stick reads 0, and the rest of the travel is scaled to start at 0 at the edge, so a
|
||||
// drifting stick rests at 0 and full deflection still reaches full. The result stays inside the unit square.
|
||||
void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]);
|
||||
// The dead zone of gamepad stick 0 (left) or 1 (right) for input_pad_stick, as a fraction of full deflection:
|
||||
// TOTK_LSTICK_DEADZONE or TOTK_RSTICK_DEADZONE when set, else TOTK_STICK_DEADZONE (both sticks), percent clamped to 0..50.
|
||||
float input_pad_deadzone(int stick);
|
||||
// How far a stick (x, y in -32768..32767) is from the centre, in percent of full deflection (100 at an axis's end; a
|
||||
// square gate's corner reads more).
|
||||
double input_stick_percent(int32_t x, int32_t y);
|
||||
|
||||
// ---- the remapping rows' edits, on a copy of input_map. Pure.
|
||||
// action's keys (table order) / its gamepad buttons in layout, at most max; returns how many.
|
||||
|
||||
+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;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ extern "C" {
|
||||
// gfx_window_sdl.c
|
||||
SDL_Window *gfx_window_sdl(void); // the game window, NULL before gfx_window_start; any thread
|
||||
double gfx_window_sdl_density(void); // pixels per window unit (mouse positions are in units); any thread
|
||||
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg); // runs fn on the window thread and waits; any thread
|
||||
|
||||
// input_sdl.c
|
||||
void input_sdl_window(SDL_Window *w); // the game window was created
|
||||
|
||||
+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); }
|
||||
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user