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
@@ -1,46 +1,62 @@
|
||||
# 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
|
||||
- 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
|
||||
- HDR10 output with peak, game and HUD brightness settings
|
||||
- 16:10 (Steam Deck, handhelds), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
|
||||
- Anisotropic texture filtering up to 16x, for sharp ground into the distance
|
||||
- In-game settings menu (F1) and performance overlay (F10)
|
||||
- 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
|
||||
- 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/tester-guide.md](docs/tester-guide.md), "Mods")
|
||||
- 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.
|
||||
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
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ Code copied or adapted from another project is listed here with its source, the
|
||||
| MacNativeGames TotK frame-rate patches: 60 fps present-interval controller, real-time frame step and its present sampling, hair and cloth clock, trampoline slow-motion check (instruction words and 1.2.1 sites), player position chain; quick-menu capture background RomFS overlay | https://github.com/ly05010419/MacNativeGames @ `ebe621de99bfc48f3c42f2d2aa73e04fbcc6eae2` (`games/totk/runtime/game_patches.c`, `game_version.h`, `world_survey.c`, `games/totk/scripts/make_romfs_patches.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/frame_rate.c`, `recomp/patches-v121.txt`, `tools/make_romfs_patches.py` |
|
||||
| MacNativeGames TotK 1.4.3 fade fix: the four distance / LOD and pre-actor fade sites, their instruction words and the floor of the fade step at 1 | https://github.com/ly05010419/MacNativeGames @ `3e9e7f67fdfdd3404134dcfa0f24969a9425a33f` (`games/totk/runtime/game_patches_v143.c` patch_fade_increment), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set fade; the floor without flags, clz >> 5, as in pull request #78) |
|
||||
| MacNativeGames TotK aspect ratio: the camera and viewport sites that hand the perspective projection its aspect ratio, and the UI layout edits for a canvas wider than 16:9 | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/runtime/aspect_patch.h`, `aspect_math.h`, `game_patches_v143.c`, `games/totk/scripts/ultrawide_layout.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set aspect), `tools/ultrawide_layout.py`, `tools/make_romfs_patches.py` (--aspect) |
|
||||
| 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` |
|
||||
@@ -37,6 +39,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,7 +70,7 @@ 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/` |
|
||||
| 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/` |
|
||||
|
||||
@@ -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]
|
||||
|
||||
+20
-3
@@ -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,7 +76,8 @@ 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
|
||||
|
||||
@@ -82,7 +85,7 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
- Vulkan 1.3 back end in place of MacNativeGames' Metal one. At `QueueSubmitCommands` the recorded NVN commands are replayed into Vulkan command buffers (`vk_exec*.c`).
|
||||
- Memory pools. Apple's unified memory let MacNativeGames use the game's pools zero-copy. The back end imports the pools as host memory (`VK_EXT_external_memory_host`). On a discrete GPU, pools bound as buffers are mirrored in device-local memory and the pages the CPU wrote (write watch, above) are uploaded before a replay; idle pool regions are made read-only so the write-watch queries skip them (`vk_memory.c`). GPU-rendered textures are written back when the CPU reads them: on a pool invalidate, on a CPU copy, and for small targets once the CPU waits for a fence (`vk_texture.c`).
|
||||
- Replay thread. `TOTK_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is off by default because no timing has shown a gain yet.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; 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.
|
||||
- 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'
|
||||
@@ -102,6 +105,20 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
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; motion vectors are camera-only, reprojected from the scene depth through this and last frame's
|
||||
view-projection of the scene camera block (constant bank c4). `fxaa` switches the game's own FXAA pass.
|
||||
- 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`).
|
||||
|
||||
+27
-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,9 @@ 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.
|
||||
|
||||
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 +108,9 @@ 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`.
|
||||
|
||||
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 +122,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.
|
||||
@@ -183,6 +195,10 @@ 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`.
|
||||
|
||||
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 +212,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,65 @@
|
||||
# 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 ([tester-guide.md](tester-guide.md), "Mods"). 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: `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. Renaming a mod's folder resets its cheats' switches to off.
|
||||
|
||||
## 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, 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:
|
||||
|
||||
- the 0xC and 0xF opcode extensions;
|
||||
- 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.
|
||||
+21
-9
@@ -7,9 +7,8 @@ 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 |
|
||||
|---|---|---|---|
|
||||
@@ -33,16 +32,29 @@ Esc is unbound in the game; it opens the settings menu and releases the mouse cu
|
||||
|
||||
## 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.
|
||||
|
||||
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 are right but the diamond keeps the Switch positions: A shows on its right, while the button labelled A is at the bottom. The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
|
||||
|
||||
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 (its own UI layout wins; 16:10 and 21:9), and when the `romfs_mods` folder replaces one of its files: `romfs-patches` then loads in its place, where two overlays replacing one file would stop the game. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts xbox` (and `--prompts xbox-label`) writes `build\<ver>\romfs-patches-xbox` (`-label`); it needs fontTools from `tools/requirements-py314.txt`. `--xbox-prompts` is the older name of `--prompts xbox`.
|
||||
|
||||
## Window keys
|
||||
|
||||
@@ -82,7 +94,7 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
|
||||
|
||||
`TOTK_MOUSE_SENS` (0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `TOTK_INPUT=none` disables the capture too.
|
||||
|
||||
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it.
|
||||
`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 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`.
|
||||
|
||||
|
||||
+65
-25
@@ -9,8 +9,10 @@ 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);
|
||||
- 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.
|
||||
@@ -22,7 +24,9 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
|
||||
totk.exe, SDL3.dll, libopus-0.dll, totk_xlate.exe
|
||||
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 +35,74 @@ 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.
|
||||
|
||||
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: 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.
|
||||
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 three times: `romfs-patches`, and `romfs-patches-xbox` and `romfs-patches-xbox-label`
|
||||
(`--prompts`: Xbox button prompts for `prompts = xbox`, the first for `pad_layout = nintendo`, the second for `xbox`
|
||||
and `auto`). The two Xbox folders are optional: when one cannot be made, the setup warns and finishes.
|
||||
When the graphics driver crashes while building the shaders, `prepare_shaders.py` runs that part again without the
|
||||
variants it was building ([building.md](building.md), step 6), and the setup finishes with a warning that names how
|
||||
many were left out.
|
||||
7. **Clean up** the temp folder, except the generated C and the objects (about 1.3 GB), and write
|
||||
`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,9 +114,13 @@ 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.
|
||||
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.
|
||||
- `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).
|
||||
- `tests/installer_test.py`: bookkeeping with fake steps. `tests/installer_e2e.py`: a dev-mode run with the files of
|
||||
@@ -93,11 +132,12 @@ 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)
|
||||
THIRD_PARTY_NOTICES.md
|
||||
@@ -125,8 +165,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
|
||||
```
|
||||
|
||||
|
||||
@@ -142,6 +142,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 |
|
||||
|
||||
+34
-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
|
||||
@@ -51,6 +52,37 @@ 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 made for emulators go into `mods/` next to `totk.exe`, one folder per mod, unpacked as they come:
|
||||
`mods/<name>/romfs/...` (the Atmosphere layout `mods/<name>/atmosphere/contents/0100F2C0115B6000/romfs` works
|
||||
too). The game loads them at the next start. The Mods tab of the in-game menu (F1) lists them: switch a mod off, or move
|
||||
it up or down. When two mods change the same file, the higher one wins.
|
||||
|
||||
- RomFS mods (textures, models, text, gameplay files) work.
|
||||
- Mods packaged for TKMM (`.tkcl`, "TKMM only") and mods that change the same files: merge them in TKMM and set an
|
||||
export location to an empty folder inside `mods/` (for example `mods/TKMM`). That folder is then one mod here.
|
||||
- Files made for another game version (names with `.Product.<number>.` that this version does not have) are left out;
|
||||
the Mods tab counts them. Look for the mod's release for your game version.
|
||||
- The resource size table (`System/Resource/ResourceSizeTable.Product.<n>...rsizetable.zs`), which tells the game how
|
||||
much memory each file needs, is rebuilt at every start with mods enabled: the tables the mods ship are merged and
|
||||
the entries of files a mod made larger or added are raised. The log line `RomFS: resource size table: ...` says
|
||||
how many. A mod's table for another game version still counts for that mod's own files.
|
||||
- Mods that patch the game code (`exefs/*.ips`, `exefs/*.pchtxt`, or `atmosphere/exefs_patches/<name>/*.ips`) work
|
||||
when the patches are for this game version and change only code: the game rebuilds itself with them at the next
|
||||
start (the setup's rebuild window, a few minutes) and switches them with the mod. Patches for another version are
|
||||
counted and left out; a file that writes the game's data, or that the port cannot express, keeps the whole mod off
|
||||
and the Mods tab says why. A patch over one of the port's own patches turns that built-in patch off while the mod
|
||||
is on (the Mods tab names it).
|
||||
- Mods with their own code (`exefs/subsdk*` or a replaced `exefs/main`, such as UltraCam) never load, their RomFS files
|
||||
included.
|
||||
- Cheats: an Atmosphère cheat file goes into a mod's `cheats` folder (`mods/<name>/cheats/<build id>.txt`, the
|
||||
build id `277178B7DBA1B6D4` for 1.4.3, `9B4E43650501A4D4` for 1.2.1). The Cheats tab of the menu switches them.
|
||||
Cheats that write memory work at the next start; for cheats that change the game's code the game opens the setup's
|
||||
rebuild window at start and comes back after it (a few minutes). The port ships no cheat files. Details: [cheats.md](cheats.md).
|
||||
|
||||
The log names every mod at the start (`mods: <name>: ...`) and how many of its files it serves.
|
||||
|
||||
## 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 seven 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.
|
||||
|
||||
+11
-7
@@ -1,10 +1,12 @@
|
||||
"""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]
|
||||
|
||||
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).
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
@@ -46,8 +48,10 @@ 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('--update', type=Path, help='the update NSP: 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/)')
|
||||
|
||||
+67
-22
@@ -1,11 +1,14 @@
|
||||
"""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.
|
||||
|
||||
--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
|
||||
@@ -23,8 +26,8 @@ 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 .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'}
|
||||
@@ -42,8 +45,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,7 +55,7 @@ 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.lang_code = default_game_language(ui_lang)
|
||||
self.screen = None
|
||||
@@ -83,16 +87,22 @@ class App:
|
||||
version.pack(side='left', padx=(10, 0), pady=(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)
|
||||
|
||||
# ---- text -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -120,8 +130,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))
|
||||
@@ -197,6 +212,10 @@ class App:
|
||||
b.grid(row=i, column=2, padx=(8, 0))
|
||||
self.form_rows[name] = lab
|
||||
i = len(rows)
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(8, 0))
|
||||
i += 1
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=4)
|
||||
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)
|
||||
@@ -206,6 +225,7 @@ class App:
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(8, 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(8, 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=620, justify='left')
|
||||
@@ -252,6 +272,8 @@ class App:
|
||||
self.open_button.pack(side='right', padx=(0, 8))
|
||||
self.back_button = self.button(bar, 'ui.back', lambda: self.show('form'))
|
||||
self.back_button.pack(side='left')
|
||||
if self.rebuild: # a failed or cancelled rebuild: the game starts with the code it has
|
||||
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, 8))
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=620, justify='left')
|
||||
self.finish_title.pack(anchor='w')
|
||||
self.finish_text = ttk.Label(f, wraplength=620, justify='left')
|
||||
@@ -266,7 +288,7 @@ class App:
|
||||
|
||||
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())
|
||||
|
||||
def refresh_form(self):
|
||||
form = self.form()
|
||||
@@ -290,6 +312,11 @@ class App:
|
||||
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 +332,11 @@ 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))
|
||||
# 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):
|
||||
@@ -352,6 +382,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')
|
||||
@@ -482,6 +515,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 +537,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', '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 +553,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,7 +578,7 @@ 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()
|
||||
root = tk.Tk()
|
||||
root.geometry('+60+40')
|
||||
@@ -612,13 +656,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
|
||||
|
||||
|
||||
+35
-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', '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,6 +134,7 @@ class Form:
|
||||
base: str = ''
|
||||
update: str = ''
|
||||
keys: str = ''
|
||||
firmware: str = ''
|
||||
lang: str = ''
|
||||
dest: str = ''
|
||||
|
||||
@@ -147,19 +171,21 @@ 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')
|
||||
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)
|
||||
|
||||
@@ -9,14 +9,26 @@
|
||||
|
||||
#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)
|
||||
|
||||
+24
-7
@@ -7,7 +7,8 @@ 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
|
||||
@@ -21,8 +22,8 @@ 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
|
||||
runtime/ as the sources with build/install-rtlib/libtotkrt.a and libtotkrt-v2.a (tools/build_game.py --make-runtime-lib
|
||||
--out build/install-rtlib, once per --cpu); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/totk_xlate.exe; the shader list from
|
||||
refs/MacNativeGames; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
|
||||
dev mode takes clang, lld and ninja from PATH and the system's libraries (no deps folder, no SDL3 copy).
|
||||
"""
|
||||
@@ -36,7 +37,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 +65,20 @@ 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)
|
||||
|
||||
@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 patches(self, version):
|
||||
"""The code patch table for recomp/gen.py."""
|
||||
return self.recomp / f'patches-{version_tag(version)}.txt'
|
||||
@@ -77,13 +87,17 @@ 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."""
|
||||
def missing(self, cpu='v3'):
|
||||
"""Paths the steps need that do not exist. cpu picks the runtime library the host will use."""
|
||||
lib = self.runtime_lib_for(cpu)
|
||||
need = [self.python, self.tools / 'extract.py', self.tools / 'build_game.py', self.tools / 'prepare_shaders.py',
|
||||
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,6 +123,7 @@ 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',),
|
||||
@@ -134,6 +149,7 @@ 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'))),
|
||||
@@ -153,6 +169,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')
|
||||
|
||||
+371
-47
@@ -1,15 +1,22 @@
|
||||
"""The six setup steps of docs/installer.md: check, extract, lift, compile, prepare, cleanup.
|
||||
"""The seven setup steps of docs/installer.md: check, fonts, extract, lift, compile, prepare, 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.
|
||||
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, shared-fonts/ (Chinese and Korean text), totk.exe
|
||||
and its DLLs, romfs-index.bin, romfs-patches/, romfs-patches-xbox/, romfs-patches-xbox-label/, totk_xlate.exe, the
|
||||
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).
|
||||
logs/. The cleanup keeps gen/, build/ and logs/: the next run lifts into the same gen/, where recomp/gen.py rewrites
|
||||
only the chunks whose C changed, and Ninja in build/ recompiles only those. A change of the mods' code cheats then
|
||||
costs the lift (about a minute) and the chunks it touches, not a full compile.
|
||||
|
||||
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status (tools/exefs_patches.py) and rebuild.txt, the command that starts this setup in
|
||||
its rebuild mode on the install folder (installer/gui.py --rebuild), one argument per line; totk.exe runs it at start
|
||||
when an enabled mod has code cheats the build lacks (runtime/cheats.c).
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
@@ -217,15 +224,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 +325,30 @@ 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')
|
||||
ctx.lang = lang
|
||||
ctx.temp.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -258,6 +367,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 +393,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']
|
||||
@@ -350,6 +463,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 +485,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'):
|
||||
@@ -419,7 +538,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'
|
||||
@@ -527,18 +711,66 @@ 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 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)))
|
||||
return fingerprint('lift-4', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
|
||||
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *cheat_files(ctx),
|
||||
*exefs_files(ctx))
|
||||
|
||||
def current(self, ctx, record):
|
||||
gen = ctx.temp / 'gen'
|
||||
@@ -546,8 +778,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 +789,62 @@ 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 (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))
|
||||
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_files(ctx), 'warn.exefs_skipped')]
|
||||
patches, skipped = plain, {}
|
||||
for tool, out, status, inputs, warn in stages:
|
||||
status.unlink(missing_ok=True) # no status: the runtime treats every such file as not built
|
||||
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', status])
|
||||
if code:
|
||||
status.unlink(missing_ok=True)
|
||||
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, status, inputs, warn in stages:
|
||||
if inputs and warn not in skipped:
|
||||
status.unlink(missing_ok=True)
|
||||
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))
|
||||
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')]
|
||||
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))
|
||||
|
||||
def current(self, ctx, record):
|
||||
exe = ctx.dest / ('totk' + EXE)
|
||||
@@ -600,10 +868,12 @@ 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)
|
||||
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 ()), '--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,12 +887,15 @@ 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,
|
||||
@@ -638,8 +911,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 +929,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 +961,37 @@ 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.
|
||||
failed = None
|
||||
for style in ('xbox', 'xbox-label'):
|
||||
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)
|
||||
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 +1003,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 +1014,7 @@ class Prepare(Step):
|
||||
return {'xlate_sha256': file_sha256(xlate)}
|
||||
|
||||
|
||||
# ---- 6. clean up
|
||||
# ---- 7. clean up
|
||||
|
||||
class Cleanup(Step):
|
||||
id = 'cleanup'
|
||||
@@ -736,12 +1032,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', '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 +1082,4 @@ def romfs_problem(romfs, expected):
|
||||
|
||||
|
||||
def all_steps():
|
||||
return [Check(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
+28
-7
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"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}",
|
||||
@@ -11,6 +12,8 @@
|
||||
"phase.check.verify_romfs": "Checking the installed game data",
|
||||
"phase.check.inputs": "Opening the game files with the keys",
|
||||
"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",
|
||||
@@ -44,11 +47,11 @@
|
||||
"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.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.",
|
||||
@@ -59,21 +62,30 @@
|
||||
"check.ticket_personalized": "The {input} file carries a personalized ticket; a common ticket is needed.",
|
||||
"check.ticket_missing": "The {input} file lacks the ticket its content needs.",
|
||||
"check.base_not_totk": "The base game file is not The Legend of Zelda: Tears of the Kingdom ({detail}).",
|
||||
"check.base_is_update": "The base game file is an update. Choose the base game NSP or XCI (version 1.0.0) here and this file as the update.",
|
||||
"check.base_combined": "The base game file holds several packages (base game, update or DLC in one file). Choose a base game dump that contains only the game.",
|
||||
"check.update_not_nsp": "The update must be an NSP file.",
|
||||
"check.update_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.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,26 +93,34 @@
|
||||
"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. Choose the firmware folder (Contents/registered) to install them; without it some text may not show.",
|
||||
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
|
||||
"warn.cheats_skipped": "The cheats of the mods folder that change the game's code could not be built; the game installs without them. Details: {log}",
|
||||
"warn.exefs_skipped": "The mods' ExeFS patches (.ips, .pchtxt) could not be built; the game installs without them. Details: {log}",
|
||||
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
|
||||
"warn.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.",
|
||||
|
||||
"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.update": "Update 1.1.0 or 1.2.1 or 1.4.3 (NSP)",
|
||||
"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.firmware_help": "For Chinese, Korean and Traditional Chinese text: the firmware's Contents/registered folder, for example from Ryujinx or Yuzu. The setup copies the Switch system fonts from it into the install folder.",
|
||||
"ui.type_game": "Game files (NSP, XCI)",
|
||||
"ui.type_nsp": "NSP files",
|
||||
"ui.type_keys": "Key files (*.keys)",
|
||||
@@ -116,12 +136,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,198 @@
|
||||
{
|
||||
"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.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.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.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.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 ou NSP.",
|
||||
"check.file_unreadable": "Não foi possível ler o arquivo de {input}: {detail}",
|
||||
"check.ticket_personalized": "O arquivo de {input} tem um ticket personalizado; é preciso um ticket comum.",
|
||||
"check.ticket_missing": "O arquivo de {input} não tem o ticket de que o conteúdo precisa.",
|
||||
"check.base_not_totk": "O arquivo do jogo base não é The Legend of Zelda: Tears of the Kingdom ({detail}).",
|
||||
"check.base_is_update": "O arquivo do jogo base é uma atualização. Escolha aqui o NSP ou XCI do jogo base (versão 1.0.0) e este arquivo como a atualização.",
|
||||
"check.base_combined": "O arquivo do jogo base contém vários pacotes (jogo base, atualização ou DLC em um só arquivo). Escolha um dump do jogo base que contenha só o jogo.",
|
||||
"check.update_not_nsp": "A atualização precisa ser um arquivo NSP.",
|
||||
"check.update_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, que o instalador não extrai; parte do texto pode não aparecer.",
|
||||
"warn.cheats_skipped": "Não foi possível compilar os cheats da pasta de mods que alteram o código do jogo; o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.exefs_skipped": "Não foi possível compilar os patches ExeFS dos mods (.ips, .pchtxt); o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.npdm_differs": "O main.npdm difere do dump de referência da versão {version}; só os módulos de código são usados.",
|
||||
"warn.xbox_prompts_failed": "Não foi possível preparar os ícones de botão do Xbox (código de saída {code}); o jogo mantém os ícones do Switch. Detalhes: {log}",
|
||||
"warn.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.",
|
||||
|
||||
"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 ou XCI)",
|
||||
"ui.update": "Atualização 1.1.0, 1.2.1 ou 1.4.3 (NSP)",
|
||||
"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.type_game": "Arquivos de jogo (NSP, XCI)",
|
||||
"ui.type_nsp": "Arquivos NSP",
|
||||
"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)"
|
||||
}
|
||||
+28
-7
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"step.check": "Проверка компьютера и игровых файлов",
|
||||
"step.fonts": "Извлечение системных шрифтов",
|
||||
"step.extract": "Распаковка игры",
|
||||
"step.lift": "Перевод кода игры в C",
|
||||
"step.compile": "Сборка {game_exe}",
|
||||
@@ -11,6 +12,8 @@
|
||||
"phase.check.verify_romfs": "Проверка установленных данных игры",
|
||||
"phase.check.inputs": "Открытие игровых файлов с помощью ключей",
|
||||
"phase.check.versions": "Проверка версий игры",
|
||||
"phase.fonts.extract": "Извлечение общих шрифтов из прошивки",
|
||||
"phase.fonts.copy": "Копирование общих шрифтов в папку установки",
|
||||
"phase.extract.hash_base": "Подсчёт контрольной суммы базовой игры",
|
||||
"phase.extract.hash_update": "Подсчёт контрольной суммы обновления",
|
||||
"phase.extract.exefs": "Распаковка кода игры",
|
||||
@@ -44,11 +47,11 @@
|
||||
"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.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 со своей консоли.",
|
||||
@@ -59,21 +62,30 @@
|
||||
"check.ticket_personalized": "В файле «{input}» личный тикет; нужен общий тикет.",
|
||||
"check.ticket_missing": "В файле «{input}» нет тикета, который нужен его содержимому.",
|
||||
"check.base_not_totk": "Файл базовой игры — не The Legend of Zelda: Tears of the Kingdom ({detail}).",
|
||||
"check.base_is_update": "Файл базовой игры — это обновление. Выберите здесь NSP или XCI базовой игры (версия 1.0.0), а этот файл укажите как обновление.",
|
||||
"check.base_combined": "Файл базовой игры содержит несколько пакетов (игра, обновление или DLC в одном файле). Выберите дамп, в котором только базовая игра.",
|
||||
"check.update_not_nsp": "Обновление должно быть файлом NSP.",
|
||||
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.1.1 или 1.2.1 или 1.4.3.",
|
||||
"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,26 +93,34 @@
|
||||
"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), чтобы установить их; без неё часть текста может не отображаться.",
|
||||
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
|
||||
"warn.cheats_skipped": "Не удалось собрать читы из папки модов, меняющие код игры; игра установится без них. Подробности: {log}",
|
||||
"warn.exefs_skipped": "Не удалось собрать ExeFS-патчи модов (.ips, .pchtxt); игра установится без них. Подробности: {log}",
|
||||
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
|
||||
"warn.shaders_skipped": "Графический драйвер аварийно завершился при сборке части шейдеров, поэтому {count} из них пропущены при подготовке. Игра соберёт их, когда они впервые понадобятся, и драйвер может упасть и там; обновление драйвера может это исправить. Подробности: {log}",
|
||||
"warn.kernel_old": "В Linux {version} нет отслеживания записи, на которое опирается игра (нужна версия 6.7 или новее); игра может работать очень медленно. Обновление системы это исправит.",
|
||||
|
||||
"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.update": "Обновление 1.1.0 или 1.2.1 или 1.4.3 (NSP)",
|
||||
"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.firmware_help": "Для текста на китайском и корейском: папка Contents/registered прошивки, например из Ryujinx или Yuzu. Установка скопирует из неё системные шрифты Switch в папку установки.",
|
||||
"ui.type_game": "Файлы игры (NSP, XCI)",
|
||||
"ui.type_nsp": "Файлы NSP",
|
||||
"ui.type_keys": "Файлы ключей (*.keys)",
|
||||
@@ -116,12 +136,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,198 @@
|
||||
{
|
||||
"step.check": "检查电脑和游戏文件",
|
||||
"step.fonts": "正在提取系统字体",
|
||||
"step.extract": "解包游戏",
|
||||
"step.lift": "将游戏代码转换为 C 代码",
|
||||
"step.compile": "编译 {game_exe}",
|
||||
"step.prepare": "准备着色器",
|
||||
"step.cleanup": "清理",
|
||||
|
||||
"phase.check.host": "检查处理器和显卡",
|
||||
"phase.check.verify": "检查已安装的文件",
|
||||
"phase.check.verify_romfs": "检查已安装的游戏数据",
|
||||
"phase.check.inputs": "用密钥打开游戏文件",
|
||||
"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.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.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.file_unreadable": "无法读取{input}文件:{detail}",
|
||||
"check.ticket_personalized": "{input}文件带有个人化票据(personalized ticket);需要通用票据(common ticket)。",
|
||||
"check.ticket_missing": "{input}文件缺少其内容所需的票据(ticket)。",
|
||||
"check.base_not_totk": "游戏本体文件不是《塞尔达传说 王国之泪》({detail})。",
|
||||
"check.base_is_update": "基础游戏文件是一个更新。请在这里选择基础游戏的 NSP 或 XCI(版本 1.0.0),并把此文件选为更新。",
|
||||
"check.base_combined": "基础游戏文件包含多个内容包(基础游戏、更新或 DLC 合在一个文件中)。请选择只包含游戏本体的基础游戏转储。",
|
||||
"check.update_not_nsp": "更新必须是 NSP 文件。",
|
||||
"check.update_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 系统字体,而安装程序不会提取这些字体;部分文字可能无法显示。",
|
||||
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
|
||||
"warn.xbox_prompts_failed": "无法准备 Xbox 按键提示(退出代码 {code});游戏将继续使用 Switch 按键提示。详情:{log}",
|
||||
"warn.shaders_skipped": "显卡驱动在编译部分着色器时崩溃,因此有 {count} 个着色器未被预先准备。游戏会在首次需要时编译它们,驱动在那时也可能崩溃;更新驱动可能会解决。详情:{log}",
|
||||
"warn.kernel_old": "Linux {version} 缺少游戏依赖的写入跟踪功能(需要 6.7 或更新版本);游戏可能运行得很慢。更新系统即可解决。",
|
||||
|
||||
"ui.title": "TotK 安装程序",
|
||||
"ui.rebuild_title": "正在为你的模组重新构建游戏",
|
||||
"ui.start_game": "启动游戏",
|
||||
"ui.version": "版本 {version}",
|
||||
"ui.intro": "用你自己的游戏文件构建一个可以游玩的《王国之泪》文件夹。耗时较长,只需运行一次。",
|
||||
"ui.notice": "此安装程序构建的文件夹包含游戏本身,仅供你个人使用;请勿分享或上传。",
|
||||
"ui.keys_private": "密钥文件只在原位置读取,其内容绝不会被显示、复制或保存。",
|
||||
"ui.base": "游戏本体(NSP 或 XCI)",
|
||||
"ui.update": "1.1.0、1.2.1 或 1.4.3 更新(NSP)",
|
||||
"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.type_game": "游戏文件(NSP、XCI)",
|
||||
"ui.type_nsp": "NSP 文件",
|
||||
"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": "中文(繁体)"
|
||||
}
|
||||
+75
-40
@@ -211,7 +211,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 +250,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 +315,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 +413,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 +480,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:
|
||||
|
||||
@@ -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)
|
||||
@@ -947,3 +948,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)
|
||||
@@ -976,3 +978,16 @@ 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]
|
||||
+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,377 @@
|
||||
// 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).
|
||||
// Byte 0x0C must be 0xE1 (NDEF Magic / Type 2 Tag CC container byte).
|
||||
if (data[0x0C] != 0xE1) {
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// UID: bytes 0..6 (7 bytes) or up to 9
|
||||
memcpy(g_info.uuid, g_dump, 7);
|
||||
g_info.uuid_len = 7;
|
||||
|
||||
// Amiibo ID at offset 0x54 (8 bytes)
|
||||
memcpy(g_info.character_id, g_dump + 0x54, 3);
|
||||
g_info.series_id = g_dump[0x57];
|
||||
g_info.numbering_id = ((uint16_t)g_dump[0x58] << 8) | g_dump[0x59];
|
||||
g_info.nfp_type = g_dump[0x5A];
|
||||
g_info.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;
|
||||
}
|
||||
|
||||
void amiibo_start_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_detecting = true;
|
||||
rt_log("amiibo: start detection");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_stop_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_detecting = false;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
memcpy(out + 0x00, g_dump, 7);
|
||||
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);
|
||||
// Character ID at 0x00 (3 bytes)
|
||||
memcpy(out + 0x00, g_dump + 0x54, 3);
|
||||
// Series ID at 0x03 (1 byte)
|
||||
out[0x03] = g_dump[0x57];
|
||||
// Numbering ID at 0x04 (2 bytes, big-endian from 0x58)
|
||||
out[0x04] = g_dump[0x58];
|
||||
out[0x05] = g_dump[0x59];
|
||||
// NFP Type at 0x06 (1 byte)
|
||||
out[0x06] = g_dump[0x5A];
|
||||
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] = 0x07; out[0x01] = 0xE7; // 2023
|
||||
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] = 0x07; out[0x59] = 0xE7;
|
||||
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,84 @@
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
+11
-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;
|
||||
@@ -342,8 +344,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(); }
|
||||
+69
-20
@@ -30,27 +30,33 @@ static int save_ok = 1; // the last cfg_save_to wrote (o
|
||||
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||
static const char *const known[] = {
|
||||
"AB", "APP_POOL", "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_OFF", "AUDIO_PREFILL_MS",
|
||||
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
||||
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
|
||||
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
|
||||
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
|
||||
"BIND_ZL", "BIND_ZR", "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",
|
||||
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
|
||||
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLSS", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES",
|
||||
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS",
|
||||
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
|
||||
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "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",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
||||
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
|
||||
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
|
||||
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
|
||||
"SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_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",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
};
|
||||
|
||||
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
|
||||
@@ -59,14 +65,22 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
||||
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
||||
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"COLOR_PRESET", "display"},
|
||||
{"SHARPNESS", "display"}, {"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"}, {"SHADOW_RES", "display"},
|
||||
{"DLAA", "display"}, {"DLSS", "display"}, {"FXAA", "display"},
|
||||
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
|
||||
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
|
||||
{"PCSS", "display"},
|
||||
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"},
|
||||
{"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"},
|
||||
{"MOUSE_SENS", "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"},
|
||||
@@ -97,15 +111,25 @@ static const CfgMeta metas[] = {
|
||||
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
|
||||
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
|
||||
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
|
||||
{"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance", RESTART}, {"FXAA", "1", "0|1", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
||||
{"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},
|
||||
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
|
||||
{"SHADOW_RES", "game", "game|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},
|
||||
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
|
||||
{"MODS_REBUILD", "auto", "auto|off", RESTART},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART},
|
||||
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR", RESTART},
|
||||
{"OPERATION_MODE", "docked", "docked|handheld", RESTART},
|
||||
@@ -113,9 +137,14 @@ static const CfgMeta metas[] = {
|
||||
// Sound, Controls, Menu
|
||||
{"VOLUME", "100", "0..100", LIVE},
|
||||
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox|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", RESTART},
|
||||
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", RESTART},
|
||||
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
|
||||
{"MOUSE_SENS", "1", "0..10", LIVE},
|
||||
{"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},
|
||||
// Advanced: scheduling
|
||||
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
|
||||
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
|
||||
@@ -144,7 +173,7 @@ static const CfgMeta metas[] = {
|
||||
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 +211,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 +252,7 @@ int cfg_load(const char *path) {
|
||||
p = eol + 1;
|
||||
}
|
||||
free(data);
|
||||
migrate();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -463,6 +511,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;
|
||||
}
|
||||
|
||||
|
||||
+67
-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>
|
||||
@@ -272,6 +273,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 +296,30 @@ 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(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 +410,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 +441,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 +456,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,667 @@
|
||||
// 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; the 0xC.. and 0xF.. extensions are not
|
||||
// supported), every presented frame while their switch is on, with registers cleared before each cheat. Memory is the
|
||||
// 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_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;
|
||||
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;
|
||||
if (t > 0xA || (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) {
|
||||
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.
|
||||
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) depth++;
|
||||
else if (t == 2 && ((f >> 24) & 0xF) == 0 && depth-- == 0) return i;
|
||||
else if (t == 2 && depth == 0 && else_ok) return i;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static uint64_t imm64(const uint32_t *w) { return (uint64_t)w[0] << 32 | w[1]; }
|
||||
|
||||
static void run(const Cheat *c, uint64_t keys) {
|
||||
uint64_t r[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;
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -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,119 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/dlaa_mv.comp; do not edit.
|
||||
// source sha256 bfa9b6d9e0f9d5cfd4382fa5080fa46f007a1a4e2b886b8fecd9adba11767686
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t dlaa_mv_spv[] = {
|
||||
0x07230203, 0x00010600, 0x0008000b, 0x00000131, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x000b000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000036,
|
||||
0x00000043, 0x0000005d, 0x00000089, 0x000000a0, 0x000000e7, 0x00060010, 0x00000004, 0x00000011,
|
||||
0x00000008, 0x00000008, 0x00000001, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004,
|
||||
0x6e69616d, 0x00000000, 0x00080005, 0x00000036, 0x475f6c67, 0x61626f6c, 0x766e496c, 0x7461636f,
|
||||
0x496e6f69, 0x00000044, 0x00030005, 0x00000041, 0x00000050, 0x00050006, 0x00000041, 0x00000000,
|
||||
0x657a6973, 0x00000000, 0x00050006, 0x00000041, 0x00000001, 0x7474696a, 0x00007265, 0x00030005,
|
||||
0x00000043, 0x00006370, 0x00050005, 0x0000005d, 0x74706564, 0x65745f68, 0x00000078, 0x00030005,
|
||||
0x00000087, 0x00727543, 0x00040006, 0x00000087, 0x00000000, 0x00000072, 0x00030005, 0x00000089,
|
||||
0x00727563, 0x00040005, 0x0000009e, 0x76657250, 0x00000000, 0x00040006, 0x0000009e, 0x00000000,
|
||||
0x00000072, 0x00040005, 0x000000a0, 0x76657270, 0x00000000, 0x00040005, 0x000000e7, 0x6f5f766d,
|
||||
0x00007475, 0x00040047, 0x00000036, 0x0000000b, 0x0000001c, 0x00030047, 0x00000041, 0x00000002,
|
||||
0x00050048, 0x00000041, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000041, 0x00000001,
|
||||
0x00000023, 0x00000008, 0x00040047, 0x0000005d, 0x00000021, 0x00000000, 0x00040047, 0x0000005d,
|
||||
0x00000022, 0x00000000, 0x00040047, 0x00000086, 0x00000006, 0x00000010, 0x00030047, 0x00000087,
|
||||
0x00000002, 0x00050048, 0x00000087, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x00000089,
|
||||
0x00000021, 0x00000002, 0x00040047, 0x00000089, 0x00000022, 0x00000000, 0x00040047, 0x0000009d,
|
||||
0x00000006, 0x00000010, 0x00030047, 0x0000009e, 0x00000002, 0x00050048, 0x0000009e, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00040047, 0x000000a0, 0x00000021, 0x00000003, 0x00040047, 0x000000a0,
|
||||
0x00000022, 0x00000000, 0x00030047, 0x000000e7, 0x00000019, 0x00040047, 0x000000e7, 0x00000021,
|
||||
0x00000001, 0x00040047, 0x000000e7, 0x00000022, 0x00000000, 0x00020013, 0x00000002, 0x00030021,
|
||||
0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006,
|
||||
0x00000004, 0x00040018, 0x00000009, 0x00000007, 0x00000004, 0x0004002b, 0x00000006, 0x00000015,
|
||||
0x3f800000, 0x0004002b, 0x00000006, 0x00000016, 0x00000000, 0x00040015, 0x0000002f, 0x00000020,
|
||||
0x00000001, 0x00040017, 0x00000030, 0x0000002f, 0x00000002, 0x00040015, 0x00000033, 0x00000020,
|
||||
0x00000000, 0x00040017, 0x00000034, 0x00000033, 0x00000003, 0x00040020, 0x00000035, 0x00000001,
|
||||
0x00000034, 0x0004003b, 0x00000035, 0x00000036, 0x00000001, 0x00040017, 0x00000037, 0x00000033,
|
||||
0x00000002, 0x00020014, 0x0000003b, 0x0004002b, 0x00000033, 0x0000003c, 0x00000000, 0x00040017,
|
||||
0x00000040, 0x00000006, 0x00000002, 0x0004001e, 0x00000041, 0x00000040, 0x00000040, 0x00040020,
|
||||
0x00000042, 0x00000009, 0x00000041, 0x0004003b, 0x00000042, 0x00000043, 0x00000009, 0x0004002b,
|
||||
0x0000002f, 0x00000044, 0x00000000, 0x00040020, 0x00000045, 0x00000009, 0x00000006, 0x0004002b,
|
||||
0x00000033, 0x0000004d, 0x00000001, 0x00090019, 0x0000005a, 0x00000006, 0x00000001, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000005b, 0x0000005a, 0x00040020,
|
||||
0x0000005c, 0x00000000, 0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d, 0x00000000, 0x0004002b,
|
||||
0x00000006, 0x00000067, 0x3f000000, 0x0004002b, 0x0000002f, 0x0000006a, 0x00000001, 0x00040020,
|
||||
0x0000006b, 0x00000009, 0x00000040, 0x0004002b, 0x00000006, 0x00000075, 0x40000000, 0x0004002b,
|
||||
0x00000033, 0x00000085, 0x00000010, 0x0004001c, 0x00000086, 0x00000007, 0x00000085, 0x0003001e,
|
||||
0x00000087, 0x00000086, 0x00040020, 0x00000088, 0x00000002, 0x00000087, 0x0004003b, 0x00000088,
|
||||
0x00000089, 0x00000002, 0x0004002b, 0x0000002f, 0x0000008a, 0x00000003, 0x0004002b, 0x0000002f,
|
||||
0x0000008b, 0x00000004, 0x0004002b, 0x0000002f, 0x0000008c, 0x00000005, 0x0004002b, 0x0000002f,
|
||||
0x0000008d, 0x00000006, 0x00040020, 0x0000008f, 0x00000002, 0x00000007, 0x0004001c, 0x0000009d,
|
||||
0x00000007, 0x00000085, 0x0003001e, 0x0000009e, 0x0000009d, 0x00040020, 0x0000009f, 0x00000002,
|
||||
0x0000009e, 0x0004003b, 0x0000009f, 0x000000a0, 0x00000002, 0x0005002c, 0x00000040, 0x000000b4,
|
||||
0x00000016, 0x00000016, 0x0004002b, 0x00000006, 0x000000b9, 0x2b8cbccc, 0x0004002b, 0x00000006,
|
||||
0x000000c7, 0x358637bd, 0x00090019, 0x000000e5, 0x00000006, 0x00000001, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000002, 0x00000007, 0x00040020, 0x000000e6, 0x00000000, 0x000000e5, 0x0004003b,
|
||||
0x000000e6, 0x000000e7, 0x00000000, 0x0005002c, 0x00000040, 0x00000130, 0x00000067, 0x00000067,
|
||||
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x000300f7,
|
||||
0x000000f0, 0x00000000, 0x000300fb, 0x0000003c, 0x000000f1, 0x000200f8, 0x000000f1, 0x0004003d,
|
||||
0x00000034, 0x00000038, 0x00000036, 0x0007004f, 0x00000037, 0x00000039, 0x00000038, 0x00000038,
|
||||
0x00000000, 0x00000001, 0x0004007c, 0x00000030, 0x0000003a, 0x00000039, 0x00050051, 0x0000002f,
|
||||
0x0000003f, 0x0000003a, 0x00000000, 0x00060041, 0x00000045, 0x00000046, 0x00000043, 0x00000044,
|
||||
0x0000003c, 0x0004003d, 0x00000006, 0x00000047, 0x00000046, 0x0004006e, 0x0000002f, 0x00000048,
|
||||
0x00000047, 0x000500af, 0x0000003b, 0x00000049, 0x0000003f, 0x00000048, 0x000400a8, 0x0000003b,
|
||||
0x0000004a, 0x00000049, 0x000300f7, 0x0000004c, 0x00000000, 0x000400fa, 0x0000004a, 0x0000004b,
|
||||
0x0000004c, 0x000200f8, 0x0000004b, 0x00050051, 0x0000002f, 0x0000004f, 0x0000003a, 0x00000001,
|
||||
0x00060041, 0x00000045, 0x00000050, 0x00000043, 0x00000044, 0x0000004d, 0x0004003d, 0x00000006,
|
||||
0x00000051, 0x00000050, 0x0004006e, 0x0000002f, 0x00000052, 0x00000051, 0x000500af, 0x0000003b,
|
||||
0x00000053, 0x0000004f, 0x00000052, 0x000200f9, 0x0000004c, 0x000200f8, 0x0000004c, 0x000700f5,
|
||||
0x0000003b, 0x00000054, 0x00000049, 0x000000f1, 0x00000053, 0x0000004b, 0x000300f7, 0x00000056,
|
||||
0x00000000, 0x000400fa, 0x00000054, 0x00000055, 0x00000056, 0x000200f8, 0x00000055, 0x000200f9,
|
||||
0x000000f0, 0x000200f8, 0x00000056, 0x0004003d, 0x0000005b, 0x0000005e, 0x0000005d, 0x00040064,
|
||||
0x0000005a, 0x00000060, 0x0000005e, 0x0007005f, 0x00000007, 0x00000061, 0x00000060, 0x0000003a,
|
||||
0x00000002, 0x00000044, 0x00050051, 0x00000006, 0x00000062, 0x00000061, 0x00000000, 0x0004006f,
|
||||
0x00000040, 0x00000066, 0x0000003a, 0x00050081, 0x00000040, 0x00000069, 0x00000066, 0x00000130,
|
||||
0x00050041, 0x0000006b, 0x0000006c, 0x00000043, 0x0000006a, 0x0004003d, 0x00000040, 0x0000006d,
|
||||
0x0000006c, 0x00050083, 0x00000040, 0x0000006e, 0x00000069, 0x0000006d, 0x00050051, 0x00000006,
|
||||
0x00000071, 0x0000006e, 0x00000000, 0x00050088, 0x00000006, 0x00000074, 0x00000071, 0x00000047,
|
||||
0x00050085, 0x00000006, 0x00000076, 0x00000074, 0x00000075, 0x00050083, 0x00000006, 0x00000077,
|
||||
0x00000076, 0x00000015, 0x00050051, 0x00000006, 0x00000079, 0x0000006e, 0x00000001, 0x00060041,
|
||||
0x00000045, 0x0000007a, 0x00000043, 0x00000044, 0x0000004d, 0x0004003d, 0x00000006, 0x0000007b,
|
||||
0x0000007a, 0x00050088, 0x00000006, 0x0000007c, 0x00000079, 0x0000007b, 0x00050085, 0x00000006,
|
||||
0x0000007d, 0x0000007c, 0x00000075, 0x00050083, 0x00000006, 0x0000007e, 0x00000015, 0x0000007d,
|
||||
0x00050085, 0x00000006, 0x00000080, 0x00000062, 0x00000075, 0x00050083, 0x00000006, 0x00000081,
|
||||
0x00000080, 0x00000015, 0x00070050, 0x00000007, 0x00000082, 0x00000077, 0x0000007e, 0x00000081,
|
||||
0x00000015, 0x00060041, 0x0000008f, 0x00000090, 0x00000089, 0x00000044, 0x0000008a, 0x0004003d,
|
||||
0x00000007, 0x00000091, 0x00000090, 0x00060041, 0x0000008f, 0x00000093, 0x00000089, 0x00000044,
|
||||
0x0000008b, 0x0004003d, 0x00000007, 0x00000094, 0x00000093, 0x00060041, 0x0000008f, 0x00000096,
|
||||
0x00000089, 0x00000044, 0x0000008c, 0x0004003d, 0x00000007, 0x00000097, 0x00000096, 0x00060041,
|
||||
0x0000008f, 0x00000099, 0x00000089, 0x00000044, 0x0000008d, 0x0004003d, 0x00000007, 0x0000009a,
|
||||
0x00000099, 0x00070050, 0x00000009, 0x00000110, 0x00000091, 0x00000094, 0x00000097, 0x0000009a,
|
||||
0x00040054, 0x00000009, 0x00000111, 0x00000110, 0x00060041, 0x0000008f, 0x000000a2, 0x000000a0,
|
||||
0x00000044, 0x0000008a, 0x0004003d, 0x00000007, 0x000000a3, 0x000000a2, 0x00060041, 0x0000008f,
|
||||
0x000000a5, 0x000000a0, 0x00000044, 0x0000008b, 0x0004003d, 0x00000007, 0x000000a6, 0x000000a5,
|
||||
0x00060041, 0x0000008f, 0x000000a8, 0x000000a0, 0x00000044, 0x0000008c, 0x0004003d, 0x00000007,
|
||||
0x000000a9, 0x000000a8, 0x00060041, 0x0000008f, 0x000000ab, 0x000000a0, 0x00000044, 0x0000008d,
|
||||
0x0004003d, 0x00000007, 0x000000ac, 0x000000ab, 0x00070050, 0x00000009, 0x0000012c, 0x000000a3,
|
||||
0x000000a6, 0x000000a9, 0x000000ac, 0x00040054, 0x00000009, 0x0000012d, 0x0000012c, 0x0006000c,
|
||||
0x00000009, 0x000000b0, 0x00000001, 0x00000022, 0x00000111, 0x00050091, 0x00000007, 0x000000b2,
|
||||
0x000000b0, 0x00000082, 0x00050051, 0x00000006, 0x000000b7, 0x000000b2, 0x00000003, 0x0006000c,
|
||||
0x00000006, 0x000000b8, 0x00000001, 0x00000004, 0x000000b7, 0x000500ba, 0x0000003b, 0x000000ba,
|
||||
0x000000b8, 0x000000b9, 0x000300f7, 0x000000bc, 0x00000000, 0x000400fa, 0x000000ba, 0x000000bb,
|
||||
0x000000bc, 0x000200f8, 0x000000bb, 0x00070050, 0x00000007, 0x000000c2, 0x000000b7, 0x000000b7,
|
||||
0x000000b7, 0x000000b7, 0x00050088, 0x00000007, 0x000000c3, 0x000000b2, 0x000000c2, 0x00050091,
|
||||
0x00000007, 0x000000c4, 0x0000012d, 0x000000c3, 0x00050051, 0x00000006, 0x000000c6, 0x000000c4,
|
||||
0x00000003, 0x000500ba, 0x0000003b, 0x000000c8, 0x000000c6, 0x000000c7, 0x000300f7, 0x000000ca,
|
||||
0x00000000, 0x000400fa, 0x000000c8, 0x000000c9, 0x000000ca, 0x000200f8, 0x000000c9, 0x0007004f,
|
||||
0x00000040, 0x000000cd, 0x000000c4, 0x000000c4, 0x00000000, 0x00000001, 0x00050050, 0x00000040,
|
||||
0x000000d0, 0x000000c6, 0x000000c6, 0x00050088, 0x00000040, 0x000000d1, 0x000000cd, 0x000000d0,
|
||||
0x00050051, 0x00000006, 0x000000d4, 0x000000d1, 0x00000000, 0x00050085, 0x00000006, 0x000000d5,
|
||||
0x000000d4, 0x00000067, 0x00050081, 0x00000006, 0x000000d6, 0x000000d5, 0x00000067, 0x00050085,
|
||||
0x00000006, 0x000000d9, 0x000000d6, 0x00000047, 0x00050051, 0x00000006, 0x000000db, 0x000000d1,
|
||||
0x00000001, 0x00050085, 0x00000006, 0x000000dc, 0x000000db, 0x00000067, 0x00050083, 0x00000006,
|
||||
0x000000dd, 0x00000067, 0x000000dc, 0x00050085, 0x00000006, 0x000000e0, 0x000000dd, 0x0000007b,
|
||||
0x00050050, 0x00000040, 0x000000e1, 0x000000d9, 0x000000e0, 0x00050083, 0x00000040, 0x000000e4,
|
||||
0x000000e1, 0x0000006e, 0x000200f9, 0x000000ca, 0x000200f8, 0x000000ca, 0x000700f5, 0x00000040,
|
||||
0x0000012f, 0x000000b4, 0x000000bb, 0x000000e4, 0x000000c9, 0x000200f9, 0x000000bc, 0x000200f8,
|
||||
0x000000bc, 0x000700f5, 0x00000040, 0x0000012e, 0x000000b4, 0x00000056, 0x0000012f, 0x000000ca,
|
||||
0x0004003d, 0x000000e5, 0x000000e8, 0x000000e7, 0x00050051, 0x00000006, 0x000000eb, 0x0000012e,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x000000ec, 0x0000012e, 0x00000001, 0x00070050, 0x00000007,
|
||||
0x000000ed, 0x000000eb, 0x000000ec, 0x00000016, 0x00000016, 0x00040063, 0x000000e8, 0x0000003a,
|
||||
0x000000ed, 0x000200f9, 0x000000f0, 0x000200f8, 0x000000f0, 0x000100fd, 0x00010038,
|
||||
};
|
||||
+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
|
||||
+31
-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
|
||||
@@ -90,14 +103,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 +123,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 +180,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 +277,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);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -38,8 +38,15 @@ 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).
|
||||
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
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ size_t gfx_shader_warm_list(GfxWarmEnt **out); // malloc'd; recorde
|
||||
const GfxShader *gfx_shader_load_private(const uint8_t iid[32], int *owned);
|
||||
void gfx_shader_release_private(const GfxShader *s);
|
||||
const uint32_t *gfx_shader_spirv(const GfxShader *s, uint64_t *words);
|
||||
// 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 };
|
||||
|
||||
+54
-16
@@ -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
|
||||
@@ -40,6 +40,7 @@ void vk_queue_unlock(void) { 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 +48,43 @@ 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();
|
||||
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 +112,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;
|
||||
@@ -292,9 +312,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 +348,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);
|
||||
@@ -411,12 +434,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 +453,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 +527,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)];
|
||||
@@ -554,6 +580,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 +595,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);
|
||||
@@ -596,8 +624,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 +653,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 +764,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++) {
|
||||
|
||||
@@ -589,6 +589,7 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,145 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/hdr_tonemap.frag; do not edit.
|
||||
// source sha256 59af32c0b20e9783c36779cdd379db02b52c0652cc1916ebca1c62a2b38df71e
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t hdr_tonemap_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000001bf, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000053, 0x0000011c, 0x00030010,
|
||||
0x00000004, 0x00000007, 0x00040047, 0x0000004f, 0x00000021, 0x00000001, 0x00040047, 0x0000004f,
|
||||
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,
|
||||
0x0000011c, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
|
||||
0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x0000000c, 0x00000006, 0x00000003, 0x0004002b,
|
||||
0x00000006, 0x00000017, 0x3e28aeed, 0x0004002b, 0x00000006, 0x00000019, 0x3e3851ec, 0x00020014,
|
||||
0x0000001a, 0x0004002b, 0x00000006, 0x0000001f, 0x3f800000, 0x0004002b, 0x00000006, 0x00000026,
|
||||
0x3f55d445, 0x00040015, 0x0000002f, 0x00000020, 0x00000000, 0x0004002b, 0x0000002f, 0x00000030,
|
||||
0x00000000, 0x0004002b, 0x0000002f, 0x00000035, 0x00000001, 0x00090019, 0x0000004c, 0x00000006,
|
||||
0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000004d,
|
||||
0x0000004c, 0x00040020, 0x0000004e, 0x00000000, 0x0000004d, 0x0004003b, 0x0000004e, 0x0000004f,
|
||||
0x00000000, 0x00040017, 0x00000051, 0x00000006, 0x00000004, 0x00040020, 0x00000052, 0x00000001,
|
||||
0x00000051, 0x0004003b, 0x00000052, 0x00000053, 0x00000001, 0x00040017, 0x00000054, 0x00000006,
|
||||
0x00000002, 0x0004002b, 0x00000006, 0x00000059, 0x461c4000, 0x0006002c, 0x0000000c, 0x0000005a,
|
||||
0x00000059, 0x00000059, 0x00000059, 0x0004003b, 0x0000004e, 0x0000005c, 0x00000000, 0x0004002b,
|
||||
0x0000002f, 0x00000065, 0x00001000, 0x0004001c, 0x00000066, 0x00000051, 0x00000065, 0x0003001e,
|
||||
0x00000067, 0x00000066, 0x00040020, 0x00000068, 0x00000002, 0x00000067, 0x0004003b, 0x00000068,
|
||||
0x00000069, 0x00000002, 0x00040015, 0x0000006a, 0x00000020, 0x00000001, 0x0004002b, 0x0000006a,
|
||||
0x0000006b, 0x00000000, 0x00040020, 0x0000006c, 0x00000002, 0x00000051, 0x0004001c, 0x00000070,
|
||||
0x00000051, 0x00000065, 0x0003001e, 0x00000071, 0x00000070, 0x00040020, 0x00000072, 0x00000002,
|
||||
0x00000071, 0x0004003b, 0x00000072, 0x00000073, 0x00000002, 0x0004002b, 0x0000002f, 0x00000077,
|
||||
0x0000005a, 0x0004001c, 0x00000078, 0x00000051, 0x00000077, 0x0004001e, 0x00000079, 0x00000078,
|
||||
0x00000051, 0x00040020, 0x0000007a, 0x00000002, 0x00000079, 0x0004003b, 0x0000007a, 0x0000007b,
|
||||
0x00000002, 0x0004002b, 0x0000006a, 0x0000007c, 0x00000001, 0x00040020, 0x0000007d, 0x00000002,
|
||||
0x00000006, 0x0004002b, 0x00000006, 0x00000080, 0x00000000, 0x0004002b, 0x00000006, 0x00000097,
|
||||
0x3e99096c, 0x0006002c, 0x0000000c, 0x000000c5, 0x00000080, 0x00000080, 0x00000080, 0x0004002b,
|
||||
0x00000006, 0x000000d8, 0x3f2aaaab, 0x0004002b, 0x00000006, 0x000000ff, 0x3f000000, 0x00040020,
|
||||
0x0000011b, 0x00000003, 0x00000051, 0x0004003b, 0x0000011b, 0x0000011c, 0x00000003, 0x00050036,
|
||||
0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000004d,
|
||||
0x00000050, 0x0000004f, 0x0004003d, 0x00000051, 0x00000055, 0x00000053, 0x0007004f, 0x00000054,
|
||||
0x00000056, 0x00000055, 0x00000055, 0x00000000, 0x00000001, 0x00050057, 0x00000051, 0x00000057,
|
||||
0x00000050, 0x00000056, 0x0008004f, 0x0000000c, 0x00000058, 0x00000057, 0x00000057, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x0007000c, 0x0000000c, 0x0000005b, 0x00000001, 0x00000025, 0x00000058,
|
||||
0x0000005a, 0x0004003d, 0x0000004d, 0x0000005d, 0x0000005c, 0x00050057, 0x00000051, 0x00000060,
|
||||
0x0000005d, 0x00000056, 0x0008004f, 0x0000000c, 0x00000061, 0x00000060, 0x00000060, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x00050081, 0x0000000c, 0x00000062, 0x0000005b, 0x00000061, 0x00060041,
|
||||
0x0000006c, 0x0000006d, 0x00000069, 0x0000006b, 0x0000006b, 0x0004003d, 0x00000051, 0x0000006e,
|
||||
0x0000006d, 0x00060041, 0x0000006c, 0x00000074, 0x00000073, 0x0000006b, 0x0000006b, 0x0004003d,
|
||||
0x00000051, 0x00000075, 0x00000074, 0x00060041, 0x0000007d, 0x0000007e, 0x0000007b, 0x0000007c,
|
||||
0x00000030, 0x0004003d, 0x00000006, 0x0000007f, 0x0000007e, 0x000500ba, 0x0000001a, 0x00000081,
|
||||
0x0000007f, 0x00000080, 0x000600a9, 0x00000006, 0x000001bd, 0x00000081, 0x0000007f, 0x0000001f,
|
||||
0x00060041, 0x0000007d, 0x0000008a, 0x0000007b, 0x0000007c, 0x00000035, 0x0004003d, 0x00000006,
|
||||
0x0000008b, 0x0000008a, 0x000500ba, 0x0000001a, 0x0000008c, 0x0000008b, 0x0000001f, 0x000600a9,
|
||||
0x00000006, 0x000001be, 0x0000008c, 0x0000008b, 0x0000001f, 0x00050051, 0x00000006, 0x00000096,
|
||||
0x00000062, 0x00000000, 0x00050085, 0x00000006, 0x00000098, 0x00000096, 0x00000097, 0x00050051,
|
||||
0x00000006, 0x0000009a, 0x00000062, 0x00000001, 0x00050051, 0x00000006, 0x0000009c, 0x0000006e,
|
||||
0x00000000, 0x00050085, 0x00000006, 0x0000009d, 0x0000009a, 0x0000009c, 0x00050081, 0x00000006,
|
||||
0x0000009e, 0x00000098, 0x0000009d, 0x00050051, 0x00000006, 0x000000a0, 0x00000062, 0x00000002,
|
||||
0x00050051, 0x00000006, 0x000000a2, 0x0000006e, 0x00000001, 0x00050085, 0x00000006, 0x000000a3,
|
||||
0x000000a0, 0x000000a2, 0x00050081, 0x00000006, 0x000000a4, 0x0000009e, 0x000000a3, 0x0007000c,
|
||||
0x00000006, 0x000000a5, 0x00000001, 0x00000028, 0x000000a4, 0x00000080, 0x0004007f, 0x00000006,
|
||||
0x000000a8, 0x000000a5, 0x0006000c, 0x00000006, 0x000000a9, 0x00000001, 0x0000001b, 0x000000a8,
|
||||
0x00050083, 0x00000006, 0x000000aa, 0x0000001f, 0x000000a9, 0x000500ba, 0x0000001a, 0x000000ad,
|
||||
0x000000a5, 0x00000080, 0x000300f7, 0x000000b0, 0x00000000, 0x000400fa, 0x000000ad, 0x000000af,
|
||||
0x000000b8, 0x000200f8, 0x000000af, 0x000500bc, 0x0000001a, 0x0000012a, 0x000000a5, 0x00000019,
|
||||
0x000300f7, 0x00000136, 0x00000000, 0x000400fa, 0x0000012a, 0x0000012b, 0x00000130, 0x000200f8,
|
||||
0x0000012b, 0x000200f9, 0x00000136, 0x000200f8, 0x00000130, 0x00050083, 0x00000006, 0x00000133,
|
||||
0x000000a5, 0x00000019, 0x00050085, 0x00000006, 0x00000134, 0x00000026, 0x00000133, 0x00050081,
|
||||
0x00000006, 0x00000135, 0x00000017, 0x00000134, 0x000200f9, 0x00000136, 0x000200f8, 0x00000136,
|
||||
0x000700f5, 0x00000006, 0x000001b7, 0x000000aa, 0x0000012b, 0x00000135, 0x00000130, 0x00050088,
|
||||
0x00000006, 0x000000b6, 0x000001b7, 0x000000a5, 0x0005008e, 0x0000000c, 0x000000b7, 0x00000062,
|
||||
0x000000b6, 0x000200f9, 0x000000b0, 0x000200f8, 0x000000b8, 0x000200f9, 0x000000b0, 0x000200f8,
|
||||
0x000000b0, 0x000700f5, 0x0000000c, 0x000001b8, 0x000000b7, 0x00000136, 0x00000062, 0x000000b8,
|
||||
0x000500bc, 0x0000001a, 0x0000014c, 0x00000096, 0x00000019, 0x000300f7, 0x00000158, 0x00000000,
|
||||
0x000400fa, 0x0000014c, 0x0000014d, 0x00000152, 0x000200f8, 0x0000014d, 0x0004007f, 0x00000006,
|
||||
0x0000014f, 0x00000096, 0x0006000c, 0x00000006, 0x00000150, 0x00000001, 0x0000001b, 0x0000014f,
|
||||
0x00050083, 0x00000006, 0x00000151, 0x0000001f, 0x00000150, 0x000200f9, 0x00000158, 0x000200f8,
|
||||
0x00000152, 0x00050083, 0x00000006, 0x00000155, 0x00000096, 0x00000019, 0x00050085, 0x00000006,
|
||||
0x00000156, 0x00000026, 0x00000155, 0x00050081, 0x00000006, 0x00000157, 0x00000017, 0x00000156,
|
||||
0x000200f9, 0x00000158, 0x000200f8, 0x00000158, 0x000700f5, 0x00000006, 0x000001b9, 0x00000151,
|
||||
0x0000014d, 0x00000157, 0x00000152, 0x000500bc, 0x0000001a, 0x0000015f, 0x0000009a, 0x00000019,
|
||||
0x000300f7, 0x0000016b, 0x00000000, 0x000400fa, 0x0000015f, 0x00000160, 0x00000165, 0x000200f8,
|
||||
0x00000160, 0x0004007f, 0x00000006, 0x00000162, 0x0000009a, 0x0006000c, 0x00000006, 0x00000163,
|
||||
0x00000001, 0x0000001b, 0x00000162, 0x00050083, 0x00000006, 0x00000164, 0x0000001f, 0x00000163,
|
||||
0x000200f9, 0x0000016b, 0x000200f8, 0x00000165, 0x00050083, 0x00000006, 0x00000168, 0x0000009a,
|
||||
0x00000019, 0x00050085, 0x00000006, 0x00000169, 0x00000026, 0x00000168, 0x00050081, 0x00000006,
|
||||
0x0000016a, 0x00000017, 0x00000169, 0x000200f9, 0x0000016b, 0x000200f8, 0x0000016b, 0x000700f5,
|
||||
0x00000006, 0x000001ba, 0x00000164, 0x00000160, 0x0000016a, 0x00000165, 0x000500bc, 0x0000001a,
|
||||
0x00000172, 0x000000a0, 0x00000019, 0x000300f7, 0x0000017e, 0x00000000, 0x000400fa, 0x00000172,
|
||||
0x00000173, 0x00000178, 0x000200f8, 0x00000173, 0x0004007f, 0x00000006, 0x00000175, 0x000000a0,
|
||||
0x0006000c, 0x00000006, 0x00000176, 0x00000001, 0x0000001b, 0x00000175, 0x00050083, 0x00000006,
|
||||
0x00000177, 0x0000001f, 0x00000176, 0x000200f9, 0x0000017e, 0x000200f8, 0x00000178, 0x00050083,
|
||||
0x00000006, 0x0000017b, 0x000000a0, 0x00000019, 0x00050085, 0x00000006, 0x0000017c, 0x00000026,
|
||||
0x0000017b, 0x00050081, 0x00000006, 0x0000017d, 0x00000017, 0x0000017c, 0x000200f9, 0x0000017e,
|
||||
0x000200f8, 0x0000017e, 0x000700f5, 0x00000006, 0x000001bb, 0x00000177, 0x00000173, 0x0000017d,
|
||||
0x00000178, 0x00060050, 0x0000000c, 0x00000146, 0x000001b9, 0x000001ba, 0x000001bb, 0x00050085,
|
||||
0x00000006, 0x000000c2, 0x000000aa, 0x000000aa, 0x00060050, 0x0000000c, 0x000000c3, 0x000000c2,
|
||||
0x000000c2, 0x000000c2, 0x0008000c, 0x0000000c, 0x000000c4, 0x00000001, 0x0000002e, 0x000001b8,
|
||||
0x00000146, 0x000000c3, 0x0007000c, 0x0000000c, 0x000000c6, 0x00000001, 0x00000028, 0x000000c4,
|
||||
0x000000c5, 0x00050051, 0x00000006, 0x00000183, 0x000000c6, 0x00000000, 0x00050051, 0x00000006,
|
||||
0x00000185, 0x000000c6, 0x00000001, 0x00050051, 0x00000006, 0x00000187, 0x000000c6, 0x00000002,
|
||||
0x0007000c, 0x00000006, 0x00000188, 0x00000001, 0x00000028, 0x00000185, 0x00000187, 0x0007000c,
|
||||
0x00000006, 0x00000189, 0x00000001, 0x00000028, 0x00000183, 0x00000188, 0x0007000c, 0x00000006,
|
||||
0x000000cb, 0x00000001, 0x00000028, 0x00000189, 0x0000001f, 0x00060050, 0x0000000c, 0x000000cf,
|
||||
0x000000cb, 0x000000cb, 0x000000cb, 0x00050088, 0x0000000c, 0x000000d0, 0x000000c6, 0x000000cf,
|
||||
0x00050051, 0x00000006, 0x0000018d, 0x000000d0, 0x00000000, 0x00050051, 0x00000006, 0x0000018f,
|
||||
0x000000d0, 0x00000001, 0x00050051, 0x00000006, 0x00000191, 0x000000d0, 0x00000002, 0x0007000c,
|
||||
0x00000006, 0x00000192, 0x00000001, 0x00000028, 0x0000018f, 0x00000191, 0x0007000c, 0x00000006,
|
||||
0x00000193, 0x00000001, 0x00000028, 0x0000018d, 0x00000192, 0x00050085, 0x00000006, 0x000000d9,
|
||||
0x0000018d, 0x000000d8, 0x00050081, 0x00000006, 0x000000de, 0x0000018f, 0x00000191, 0x00050051,
|
||||
0x00000006, 0x000000e0, 0x0000006e, 0x00000002, 0x00050085, 0x00000006, 0x000000e1, 0x000000de,
|
||||
0x000000e0, 0x00050081, 0x00000006, 0x000000e2, 0x000000d9, 0x000000e1, 0x00050083, 0x00000006,
|
||||
0x000000e3, 0x000000e2, 0x0000001f, 0x00050051, 0x00000006, 0x000000e6, 0x00000075, 0x00000001,
|
||||
0x00050085, 0x00000006, 0x000000e9, 0x000000e3, 0x000000e3, 0x00050083, 0x00000006, 0x000000ea,
|
||||
0x0000001f, 0x000000e9, 0x00050085, 0x00000006, 0x000000eb, 0x000000e6, 0x000000ea, 0x00050051,
|
||||
0x00000006, 0x000000ed, 0x00000075, 0x00000000, 0x00050081, 0x00000006, 0x000000ee, 0x000000eb,
|
||||
0x000000ed, 0x00060050, 0x0000000c, 0x000000f2, 0x00000193, 0x00000193, 0x00000193, 0x00050083,
|
||||
0x0000000c, 0x000000f3, 0x000000d0, 0x000000f2, 0x0005008e, 0x0000000c, 0x000000f5, 0x000000f3,
|
||||
0x000000ee, 0x00050081, 0x0000000c, 0x000000f8, 0x000000f5, 0x000000f2, 0x0005008e, 0x0000000c,
|
||||
0x000000f9, 0x000000f8, 0x000000cb, 0x00050051, 0x00000006, 0x00000197, 0x000000f9, 0x00000000,
|
||||
0x00050051, 0x00000006, 0x00000199, 0x000000f9, 0x00000001, 0x00050051, 0x00000006, 0x0000019b,
|
||||
0x000000f9, 0x00000002, 0x0007000c, 0x00000006, 0x0000019c, 0x00000001, 0x00000028, 0x00000199,
|
||||
0x0000019b, 0x0007000c, 0x00000006, 0x0000019d, 0x00000001, 0x00000028, 0x00000197, 0x0000019c,
|
||||
0x00050085, 0x00000006, 0x00000101, 0x000000ff, 0x000001be, 0x000500ba, 0x0000001a, 0x00000104,
|
||||
0x0000019d, 0x00000101, 0x000300f7, 0x00000106, 0x00000000, 0x000400fa, 0x00000104, 0x00000105,
|
||||
0x00000106, 0x000200f8, 0x00000105, 0x00050083, 0x00000006, 0x0000010a, 0x000001be, 0x00000101,
|
||||
0x00050083, 0x00000006, 0x0000010f, 0x0000019d, 0x00000101, 0x0004007f, 0x00000006, 0x00000110,
|
||||
0x0000010f, 0x00050088, 0x00000006, 0x00000112, 0x00000110, 0x0000010a, 0x0006000c, 0x00000006,
|
||||
0x00000113, 0x00000001, 0x0000001b, 0x00000112, 0x00050083, 0x00000006, 0x00000114, 0x0000001f,
|
||||
0x00000113, 0x00050085, 0x00000006, 0x00000115, 0x0000010a, 0x00000114, 0x00050081, 0x00000006,
|
||||
0x00000116, 0x00000101, 0x00000115, 0x00050088, 0x00000006, 0x00000118, 0x00000116, 0x0000019d,
|
||||
0x0005008e, 0x0000000c, 0x0000011a, 0x000000f9, 0x00000118, 0x000200f9, 0x00000106, 0x000200f8,
|
||||
0x00000106, 0x000700f5, 0x0000000c, 0x000001bc, 0x000000f9, 0x0000017e, 0x0000011a, 0x00000105,
|
||||
0x0007000c, 0x0000000c, 0x0000011e, 0x00000001, 0x00000028, 0x000001bc, 0x000000c5, 0x0005008e,
|
||||
0x0000000c, 0x00000120, 0x0000011e, 0x000001bd, 0x00050051, 0x00000006, 0x00000121, 0x00000120,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x00000122, 0x00000120, 0x00000001, 0x00050051, 0x00000006,
|
||||
0x00000123, 0x00000120, 0x00000002, 0x00070050, 0x00000051, 0x00000124, 0x00000121, 0x00000122,
|
||||
0x00000123, 0x00000080, 0x0003003e, 0x0000011c, 0x00000124, 0x000100fd, 0x00010038,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
+16
-2
@@ -57,6 +57,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
|
||||
@@ -184,14 +187,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,6 +247,13 @@ 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
|
||||
@@ -249,6 +261,7 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); //
|
||||
// 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)
|
||||
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)
|
||||
@@ -295,6 +308,7 @@ 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
|
||||
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
|
||||
|
||||
+1
-1
@@ -121,7 +121,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);
|
||||
|
||||
+92
-17
@@ -236,7 +236,56 @@ static float mouse_sens_get(void) {
|
||||
}
|
||||
|
||||
|
||||
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`.
|
||||
// ---- 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);
|
||||
}
|
||||
}
|
||||
|
||||
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`. The sample is divided by
|
||||
// the camera speed factor the game runs with (camera_speed.c, cam_speed_applied), which multiplies every right-stick
|
||||
// turn rate, so TOTK_MOUSE_SENS alone sets the mouse's speed; below 1 the sample stays within the stick's range, so the
|
||||
// mouse's top speed drops with the factor.
|
||||
static void mouse_camera(int32_t out[2]) {
|
||||
static uint64_t frame = ~0ull, t0;
|
||||
static int32_t held[2];
|
||||
@@ -259,20 +308,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.
|
||||
@@ -425,12 +477,26 @@ 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 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};
|
||||
@@ -456,9 +522,13 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
|
||||
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]);
|
||||
@@ -505,15 +575,20 @@ void input_menu(int open) {
|
||||
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. A stick
|
||||
// axis counts as held as well: with the walk key the keyboard's sticks move part way without setting a direction bit, so
|
||||
// a gate that watched the buttons alone would let that movement through the moment the menu or the text entry closes.
|
||||
static int blocked(volatile int32_t *flag, uint64_t buttons, const int32_t *axes) {
|
||||
int32_t k = *flag;
|
||||
return k && !(k == 2 && !buttons && plat_cas32(flag, 0, 2) == 2);
|
||||
int held = buttons != 0;
|
||||
for (int i = 0; i < 4; i++) held |= axes[i] != 0;
|
||||
return k && !(k == 2 && !held && plat_cas32(flag, 0, 2) == 2);
|
||||
}
|
||||
|
||||
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);
|
||||
const int32_t axes[4] = {*lx, *ly, *rx, *ry};
|
||||
int text = blocked(&g_block, *buttons, axes), menu = blocked(&g_menu, *buttons, axes);
|
||||
if (!text && !menu) return;
|
||||
*buttons = 0; *lx = *ly = *rx = *ry = 0;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,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,
|
||||
|
||||
@@ -288,6 +288,18 @@ 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 : 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_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;
|
||||
@@ -334,6 +346,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.
|
||||
|
||||
+22
-4
@@ -80,11 +80,23 @@ int input_map_bind(const char *button, const char *list);
|
||||
// Applies the TOTK_BIND_<action> settings (cfg_get) over the defaults: at start, and again when one of them changes.
|
||||
void input_map_configure(void);
|
||||
|
||||
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "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).
|
||||
enum { PROMPTS_NINTENDO, PROMPTS_XBOX };
|
||||
// The prompts for a TOTK_PROMPTS value ("nintendo" or "xbox", any case). Unset or empty: Xbox while the pad layout
|
||||
// setting is "xbox_prompts", else Nintendo.
|
||||
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.
|
||||
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);
|
||||
|
||||
@@ -100,6 +112,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.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "audren.h"
|
||||
#include "host.h"
|
||||
#include "ipc.h"
|
||||
#include "nfp.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define CMIF_IN_MAGIC 0x49434653u // "SFCI"
|
||||
@@ -25,6 +26,7 @@ enum { T_CLOSE = 2, T_REQUEST = 4, T_CONTROL = 5, T_REQUEST_CTX = 6, T_CONTROL_C
|
||||
// Services sm: hands out. Anything else is a named fatal.
|
||||
static const struct { const char *name; IpcHandler handler; } services[] = {
|
||||
{"audren:u", audren_manager_handler},
|
||||
{"nfp:user", nfp_user_manager_handler},
|
||||
};
|
||||
|
||||
static volatile int32_t g_live;
|
||||
|
||||
+23
-14
@@ -8,10 +8,6 @@
|
||||
// Cpu. The SDK's nn::os runs as lifted code; only the kernel under it is the host. Every wait hangs on one global lock
|
||||
// G with one condition variable per thread; wake order is the kernel's: lower priority value first, then first come.
|
||||
// Semantics follow the public Horizon kernel (KConditionVariable, KAddressArbiter, KSynchronization).
|
||||
#ifdef _WIN32
|
||||
#define __USE_MINGW_SETJMP_NON_SEH // setjmp(b) = _setjmp(b, NULL): longjmp restores registers, no SEH unwinding
|
||||
#endif
|
||||
#include <setjmp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -77,7 +73,7 @@ struct RtThread {
|
||||
uint64_t cseq;
|
||||
RtThread *cnext;
|
||||
uint8_t *tlr, *tls;
|
||||
jmp_buf exit_jmp; // the thread start; ExitThread longjmps to it
|
||||
PlatJmp exit_jmp; // the thread start; ExitThread jumps back to it
|
||||
// fibers
|
||||
PlatFiber *base_fiber; // the host thread as a host fiber, after the first switch
|
||||
Fiber *fiber; // current guest fiber, NULL on the thread's own context
|
||||
@@ -296,7 +292,13 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
|
||||
// and gives it up only where Horizon would switch threads (waits, sleep, yield, exit, blocking host calls); a free
|
||||
// core goes to the best waiting thread. A waiter better than a holder asks one holder to give its core up (core_kick),
|
||||
// which it does at its next SVC or indirect call (kern_checkpoint, rt_dispatch), as Horizon runs the better thread at
|
||||
// once; guest code without either keeps the core until it reaches one. TOTK_FREE_CORES=1 turns the gate off.
|
||||
// once; guest code without either keeps the core until it reaches one.
|
||||
//
|
||||
// free_cores = 1 (TOTK_FREE_CORES) turns the gate off, for experiments only: the game does not run that way. A thread
|
||||
// that holds no core reports its ideal core to GetCurrentProcessorNumber, so the game's job workers all report core 0
|
||||
// and run its per-core code at once: on 1.2.1 two of them add the same per-core NVN control memory, which retires the
|
||||
// command handle the other just recorded, and CallCommands finds no recording at start (#212); 1.4.3 corrupts a heap
|
||||
// in main+0x82e00 or hangs at the title menu. The start log and every crash report say so (kern_free_cores_note).
|
||||
static RtThread *g_core_owner[NCORE], *g_core_wait;
|
||||
static int g_cores_on = -1, g_held_max;
|
||||
|
||||
@@ -307,14 +309,21 @@ static int cores_enabled(void) {
|
||||
if (g_cores_on < 0) {
|
||||
g_cores_on = !cfg_bool("TOTK_FREE_CORES"); // "0" keeps the gate, like unset
|
||||
if (!g_cores_on)
|
||||
rt_log("kernel: virtual core gate off (TOTK_FREE_CORES): guest threads run without the limit of one per "
|
||||
"core that the game's per-core data needs; for experiments only, the game can fail");
|
||||
rt_log("kernel: WARNING: free_cores = 1 (TOTK_FREE_CORES) turns the virtual core gate off. This is an "
|
||||
"experimental mode and the game crashes or hangs in it, usually at start. Set free_cores = 0 in "
|
||||
"totk.ini to play");
|
||||
diag_init();
|
||||
ap_init();
|
||||
}
|
||||
return g_cores_on;
|
||||
}
|
||||
|
||||
void kern_free_cores_note(void) {
|
||||
if (!g_cores_on)
|
||||
rt_log("fatal: free_cores = 1 was on: the game crashes in this experimental mode; set free_cores = 0 in "
|
||||
"totk.ini before reporting this crash");
|
||||
}
|
||||
|
||||
static int core_allowed(const RtThread *t, int c) { return (int)((t->mask >> c) & 1); }
|
||||
|
||||
// ---- asynchronous preemption (TOTK_ASYNC_PREEMPT, on unless 0)
|
||||
@@ -1435,10 +1444,10 @@ static void thread_finish(RtThread *t) {
|
||||
unlock();
|
||||
}
|
||||
|
||||
// ExitThread: back to the thread start with a non-unwinding longjmp; no SEH unwinding through lifted frames.
|
||||
// (__builtin_setjmp is not usable: on Win64 LLVM saves the frame address, which is not rbp when the frame register
|
||||
// is offset, and the landing code then reads its locals through the wrong base.)
|
||||
static __attribute__((noreturn)) void exit_now(RtThread *t) { longjmp(t->exit_jmp, 1); }
|
||||
// ExitThread: back to the thread start with plat_jmp_go, which unwinds nothing: no SEH unwinding through lifted
|
||||
// frames. (__builtin_setjmp is not usable: on Win64 LLVM saves the frame address, which is not rbp when the frame
|
||||
// register is offset, and the landing code then reads its locals through the wrong base.)
|
||||
static __attribute__((noreturn)) void exit_now(RtThread *t) { plat_jmp_go(&t->exit_jmp); }
|
||||
|
||||
// The calling host thread, for ap_try (TOTK_ASYNC_PREEMPT) and dg_sample (TOTK_CORE_DIAG).
|
||||
static PlatIntr *ap_open_self(void) {
|
||||
@@ -1460,7 +1469,7 @@ static void thread_main(void *p) {
|
||||
unlock();
|
||||
Cpu *c = t->cpu;
|
||||
c->sp = t->stack_top & ~15ull;
|
||||
if (!setjmp(t->exit_jmp)) {
|
||||
if (!plat_jmp_save(&t->exit_jmp)) {
|
||||
rt_call(c, t->entry, 1, t->arg);
|
||||
char d[128];
|
||||
rt_fatal("guest thread %#x: entry %s returned (Horizon threads end with ExitThread)", t->handle,
|
||||
@@ -1520,7 +1529,7 @@ static void guest_main(void *p) {
|
||||
void (*fn)(Cpu *) = (void (*)(Cpu *))p;
|
||||
Cpu *c;
|
||||
kern_main_thread(&c);
|
||||
if (!setjmp(tcur->exit_jmp)) fn(c);
|
||||
if (!plat_jmp_save(&tcur->exit_jmp)) fn(c);
|
||||
thread_finish(tcur);
|
||||
return;
|
||||
}
|
||||
|
||||
+4
-4
@@ -552,8 +552,8 @@ uint64_t ldr_plt_stub(const char *module, const char *name) {
|
||||
}
|
||||
|
||||
// ---- --probe-loader
|
||||
// Expected values, measured on the 1.1.1, 1.2.1 and 1.4.3 modules; the 1.4.3 relocation totals match the ones
|
||||
// MacNativeGames' port notes give for that version. 1.1.1 shares subsdk0 and sdk with 1.2.1 down to every count.
|
||||
// Expected values, measured on the 1.1.0, 1.2.1 and 1.4.3 modules; the 1.4.3 relocation totals match the ones
|
||||
// MacNativeGames' port notes give for that version. 1.1.0 shares subsdk0 and sdk with 1.2.1 down to every count.
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint64_t kinds[K_COUNT], total, sym_import, sym_own;
|
||||
@@ -564,7 +564,7 @@ static const Expect expect_121[] = {
|
||||
{"subsdk0", {1623, 12820, 1030, 3924}, 19397, 6576, 11198, 667, {9, 0, 654}, 4, 0x30, 45},
|
||||
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
|
||||
};
|
||||
static const Expect expect_111[] = {
|
||||
static const Expect expect_110[] = {
|
||||
{"main", {508604, 1412, 32, 639}, 510687, 2083, 0, 673, {0, 16, 645}, 12, 0x30, 10668},
|
||||
{"subsdk0", {1623, 12820, 1030, 3924}, 19397, 6576, 11198, 667, {9, 0, 654}, 4, 0x30, 45},
|
||||
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
|
||||
@@ -574,7 +574,7 @@ static const Expect expect_143[] = {
|
||||
{"subsdk0", {3424, 10927, 905, 3817}, 19073, 6557, 9092, 663, {9, 0, 654}, 0, 0x20, 45},
|
||||
{"sdk", {15013, 10135, 1079, 7832}, 34059, 10, 19036, 8, {8, 0, 0}, 0, 0x400, 83},
|
||||
};
|
||||
#define expect (RT_V143 ? expect_143 : RT_V111 ? expect_111 : expect_121)
|
||||
#define expect (RT_V143 ? expect_143 : RT_V110 ? expect_110 : expect_121)
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
|
||||
|
||||
+18
-1
@@ -2,6 +2,7 @@
|
||||
//
|
||||
// totk.exe [--exefs DIR] [--romfs DIR] [--profile DIR] <mode>
|
||||
// --selftest layout, dispatch table and host-function registry; needs no game files
|
||||
// --probe-mods the mods folder scan, order and switches (mods.c); needs no game files
|
||||
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
|
||||
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
|
||||
// --probe-dpi the process DPI awareness (per-monitor v2) and the primary monitor's size; no game files
|
||||
@@ -246,6 +247,7 @@ static void crash_handler(const PlatCrash *x) {
|
||||
} else {
|
||||
rt_logf(" not on a guest thread\n");
|
||||
}
|
||||
kern_free_cores_note();
|
||||
fflush(stderr);
|
||||
plat_exit(71);
|
||||
}
|
||||
@@ -259,11 +261,22 @@ static double now_s(void) { return (double)plat_now_ns() / 1e9; }
|
||||
static volatile int host_probe_hits;
|
||||
static void host_probe(Cpu *c) { host_probe_hits++; c->x[0] = 42; }
|
||||
|
||||
// The instruction set build_game.py --cpu compiled for: v3 (AVX2/FMA/BMI2, the default) or v2 (SSE4.2/POPCNT).
|
||||
#ifndef RT_CPU_LEVEL
|
||||
#define RT_CPU_LEVEL 3
|
||||
#endif
|
||||
|
||||
static void check_cpu(void) {
|
||||
__builtin_cpu_init();
|
||||
#if RT_CPU_LEVEL >= 3
|
||||
int ok = __builtin_cpu_supports("avx2") && __builtin_cpu_supports("fma") && __builtin_cpu_supports("bmi2");
|
||||
printf("cpu: x86-64-v3 (AVX2, FMA, BMI2) %s\n", ok ? "present" : "MISSING");
|
||||
CHECK(ok, "the lifted code is built for x86-64-v3");
|
||||
#else
|
||||
int ok = __builtin_cpu_supports("sse4.2") && __builtin_cpu_supports("popcnt");
|
||||
printf("cpu: x86-64-v2 (SSE4.2, POPCNT) %s\n", ok ? "present" : "MISSING");
|
||||
CHECK(ok, "the lifted code is built for x86-64-v2");
|
||||
#endif
|
||||
}
|
||||
|
||||
// Layout: ascending, no overlap, below 2^39, and inside the window mem_init reserved.
|
||||
@@ -428,7 +441,7 @@ static int selftest(void) {
|
||||
|
||||
static int usage(const char *argv0) {
|
||||
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
|
||||
"--probe-cfg | --probe-dpi | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
|
||||
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
|
||||
"--probe-vkexec | --probe-astc | --probe-movie | --prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
|
||||
return 2;
|
||||
@@ -468,6 +481,9 @@ static void portable_paths(RtConfig *c) {
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
mem_init(); // first: claim the guest address space before anything else can land there; reads totk.ini
|
||||
// A restart from the settings menu (gfx_present.c gfx_window_closed) starts this process while the one it replaces
|
||||
// is still exiting: its log, RomFS and saves are free once it is gone.
|
||||
if (plat_restart_wait(30000) < 0) rt_log("restart: the previous process still runs after 30 s; starting anyway");
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
plat_crash_hook(crash_handler);
|
||||
@@ -519,6 +535,7 @@ int main(int argc, char **argv) {
|
||||
#ifdef _WIN32
|
||||
if (!strcmp(mode, "--probe-dpi")) return probe_dpi(); // the Win32 DPI awareness of totk.manifest
|
||||
#endif
|
||||
if (!strcmp(mode, "--probe-mods")) return probe_mods();
|
||||
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
|
||||
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
|
||||
if (!strcmp(mode, "--probe-services")) return probe_services();
|
||||
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
// Player mods: the scan of the mods folder, its order and switches (mods.h).
|
||||
//
|
||||
// One subfolder per mod, as emulators take them. The RomFS root is the first of <mod>/romfs, <mod>/romfslite (TKMM's
|
||||
// name for it when it bypasses Atmosphere's RomFS build), <mod>/atmosphere/contents/<title>/romfs,
|
||||
// <mod>/contents/<title>/romfs and <mod>/<title>/romfs that exists; the ExeFS and cheats folders likewise. A mod is
|
||||
// listed whatever it holds; what the port cannot use is its problem, shown in the menu and logged, and never stops the
|
||||
// start. romfs.c serves the RomFS files of the enabled mods, the top one winning a path.
|
||||
//
|
||||
// The order and the switches are totk.ini keys (cfg.c): TOTK_MODS_ORDER lists names top first, separated by '|' (a
|
||||
// Windows file name cannot hold '|'; a folder whose name does on Linux is listed as unusable); a folder it does not name
|
||||
// goes below the named ones, by name. TOTK_MODS_DISABLED lists the switched-off names. The menu changes the list in
|
||||
// place and writes both keys; the scan's state is kept to tell the menu a restart is due.
|
||||
//
|
||||
// ExeFS patch files: an .ips or .pchtxt file in the exefs folder or in atmosphere/exefs_patches/<name>/ is for the game
|
||||
// when its build id is main's (an IPS file's name, a pchtxt file's @nsobid line); others are another version's and
|
||||
// counted. The setup builds every mod's files into patch sets and writes build-cache/exefs.status next to totk.exe
|
||||
// (tools/exefs_patches.py has the format): a file whose line is missing or has another content hash waits for the
|
||||
// rebuild at start (cheats.c), and its mod stays off until then.
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "mods.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define TITLE "0100F2C0115B6000"
|
||||
#define MAX_MODS 512
|
||||
|
||||
static ModInfo *mods;
|
||||
static int nmods;
|
||||
static char dir[2048];
|
||||
static char start_order[MAX_MODS][256]; // names in the scan's order and their switches, for mods_changed
|
||||
static int start_enabled[MAX_MODS];
|
||||
|
||||
// 1 when a file lies anywhere under path.
|
||||
static int has_file_cb(const char *name, int kind, uint64_t size, void *ctx);
|
||||
static int has_file(const char *path) {
|
||||
struct { const char *path; int found; } c = {path, 0};
|
||||
plat_fs_list(path, has_file_cb, &c);
|
||||
return c.found;
|
||||
}
|
||||
static int has_file_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
struct { const char *path; int found; } *c = ctx;
|
||||
if (kind == PLAT_FS_FILE) {
|
||||
c->found = 1;
|
||||
} else if (kind == PLAT_FS_DIR) {
|
||||
char sub[2100];
|
||||
if (snprintf(sub, sizeof sub, "%s/%s", c->path, name) < (int)sizeof sub) c->found = has_file(sub);
|
||||
}
|
||||
return !c->found;
|
||||
}
|
||||
|
||||
// The first of the emulator and Atmosphere places of <mod>/<part> that is a folder, into out ("" none).
|
||||
static void find_root(const char *mod, const char *part, char *out, size_t cap) {
|
||||
static const char *const forms[] = {"%s/%s", "%s/atmosphere/contents/" TITLE "/%s", "%s/contents/" TITLE "/%s",
|
||||
"%s/" TITLE "/%s"};
|
||||
for (size_t i = 0; i < sizeof forms / sizeof *forms; i++) {
|
||||
if (snprintf(out, cap, forms[i], mod, part) < (int)cap && plat_dir_exists(out)) return;
|
||||
}
|
||||
out[0] = 0;
|
||||
}
|
||||
|
||||
static int ends_with_ci(const char *s, const char *tail) {
|
||||
size_t n = strlen(s), t = strlen(tail);
|
||||
return n >= t && !strcasecmp(s + n - t, tail);
|
||||
}
|
||||
|
||||
// What the ExeFS folder holds: MOD_EXEFS for patches (.ips, .pchtxt), MOD_SUBSDK for code modules (subsdk* plugins, a
|
||||
// replaced main). main.npdm alone is the process's metadata, not code: it counts as neither.
|
||||
static int exefs_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
int *k = ctx;
|
||||
if (kind != PLAT_FS_FILE) return 1;
|
||||
if (ends_with_ci(name, ".ips") || ends_with_ci(name, ".pchtxt")) *k |= MOD_EXEFS;
|
||||
else if (!strncasecmp(name, "subsdk", 6) || !strcasecmp(name, "main")) *k |= MOD_SUBSDK;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// MOD_CHEATS when the cheats folder holds an Atmosphere cheat file (<build id>.txt).
|
||||
static int cheats_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
if (kind == PLAT_FS_FILE && ends_with_ci(name, ".txt")) *(int *)ctx |= MOD_CHEATS;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 1 when name is one of the '|'-separated names of list.
|
||||
static int listed(const char *list, const char *name) {
|
||||
size_t n = strlen(name);
|
||||
for (const char *p = list; p && *p;) {
|
||||
const char *bar = strchr(p, '|');
|
||||
size_t len = bar ? (size_t)(bar - p) : strlen(p);
|
||||
if (len == n && !memcmp(p, name, n)) return 1;
|
||||
p = bar ? bar + 1 : NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The position of name in the '|'-separated list, or -1.
|
||||
static int list_pos(const char *list, const char *name) {
|
||||
size_t n = strlen(name);
|
||||
int k = 0;
|
||||
for (const char *p = list; p && *p; k++) {
|
||||
const char *bar = strchr(p, '|');
|
||||
size_t len = bar ? (size_t)(bar - p) : strlen(p);
|
||||
if (len == n && !memcmp(p, name, n)) return k;
|
||||
p = bar ? bar + 1 : NULL;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static const char *order_list; // TOTK_MODS_ORDER while sorting
|
||||
static int by_order(const void *a, const void *b) {
|
||||
const ModInfo *x = a, *y = b;
|
||||
int px = list_pos(order_list, x->name), py = list_pos(order_list, y->name);
|
||||
if (px >= 0 && py >= 0) return px - py;
|
||||
if (px >= 0 || py >= 0) return px >= 0 ? -1 : 1;
|
||||
return strcmp(x->name, y->name);
|
||||
}
|
||||
|
||||
static const char *problem_name(ModProblem p) {
|
||||
switch (p) {
|
||||
case MOD_EMPTY: return "empty";
|
||||
case MOD_PLUGIN: return "has a code module (subsdk or main), not supported; the whole mod stays off";
|
||||
case MOD_EXEFS_LATER: return "ExeFS patches wait for a rebuild of the game code; the whole mod stays off";
|
||||
case MOD_EXEFS_UNSUPPORTED: return "an ExeFS patch file cannot be applied; the whole mod stays off";
|
||||
case MOD_EXEFS_OTHER: return "its ExeFS patches are for another game version";
|
||||
case MOD_BAD_NAME: return "its folder name holds '|', which the order list uses; rename the folder";
|
||||
default: return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ExeFS patch files
|
||||
|
||||
static uint32_t fnv(const void *p, size_t n) {
|
||||
uint32_t h = 0x811C9DC5u;
|
||||
for (size_t i = 0; i < n; i++) h = (h ^ ((const uint8_t *)p)[i]) * 0x01000193u;
|
||||
return h;
|
||||
}
|
||||
|
||||
const char *mods_main_build_id(void) {
|
||||
static char bid[65];
|
||||
static int read;
|
||||
if (read) return bid;
|
||||
read = 1;
|
||||
char path[2100];
|
||||
snprintf(path, sizeof path, "%s/main", rt_config()->exefs);
|
||||
PlatFile *f = plat_file_open(path);
|
||||
uint8_t head[0x60];
|
||||
int ok = f && plat_file_pread(f, head, sizeof head, 0) == sizeof head && !memcmp(head, "NSO0", 4);
|
||||
if (f) plat_file_close(f);
|
||||
for (int i = 0; ok && i < 32; i++) snprintf(bid + 2 * i, 3, "%02x", head[0x40 + i]);
|
||||
return bid;
|
||||
}
|
||||
|
||||
// 1 when hex (any case; trailing zeros left out or not) names main's build id; also when main's is unknown.
|
||||
static int same_build(const char *hex, size_t n) {
|
||||
const char *bid = mods_main_build_id();
|
||||
if (!*bid) return 1;
|
||||
if (!n || n > 64) return 0;
|
||||
for (size_t i = 0; i < 64; i++) {
|
||||
char c = i < n ? hex[i] : '0';
|
||||
if (c >= 'A' && c <= 'F') c = (char)(c - 'A' + 'a');
|
||||
if (c != bid[i]) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// build-cache/exefs.status next to totk.exe, read once; "" when there is none.
|
||||
static const char *exefs_status(void) {
|
||||
static char *text;
|
||||
if (text) return text;
|
||||
char path[2100];
|
||||
uint64_t n = 0;
|
||||
if (plat_exe_path(path, sizeof path)) {
|
||||
char *cut = strrchr(path, '/'), *back = strrchr(path, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (cut) snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/exefs.status");
|
||||
text = (char *)plat_file_read(path, &n);
|
||||
}
|
||||
if (!text) text = calloc(1, 1);
|
||||
return text;
|
||||
}
|
||||
|
||||
// The status line of key: its kind, built-in sets and note, NUL-terminated copies; 0 when none.
|
||||
static int status_line(const char *key, const char *content, char *kind, char *over, char *note) {
|
||||
char want[20];
|
||||
snprintf(want, sizeof want, "%s\t%s\t", key, content);
|
||||
for (const char *line = exefs_status(); *line;) {
|
||||
const char *end = strchr(line, '\n');
|
||||
size_t len = end ? (size_t)(end - line) : strlen(line);
|
||||
if (len > 18 && !memcmp(line, want, 18)) {
|
||||
char copy[600];
|
||||
snprintf(copy, sizeof copy, "%.*s", (int)(len < sizeof copy ? len : sizeof copy - 1), line + 18);
|
||||
char *t1 = strchr(copy, '\t'), *t2 = t1 ? strchr(t1 + 1, '\t') : NULL;
|
||||
if (!t1 || !t2) return 0;
|
||||
*t1 = *t2 = 0;
|
||||
char *r = strchr(t2 + 1, '\r');
|
||||
if (r) *r = 0;
|
||||
snprintf(kind, 16, "%s", copy);
|
||||
snprintf(over, 256, "%s", strcmp(t1 + 1, "-") ? t1 + 1 : "");
|
||||
snprintf(note, 256, "%s", t2 + 1);
|
||||
return 1;
|
||||
}
|
||||
line = end ? end + 1 : line + len;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
ModInfo *m;
|
||||
const char *folder, *rel; // the folder listed and its path in the mod
|
||||
int unsupported;
|
||||
} ExefsWalk;
|
||||
|
||||
// One file of an exefs folder: its build, then its status line.
|
||||
static int exefs_file_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
ExefsWalk *w = ctx;
|
||||
ModInfo *m = w->m;
|
||||
int ips = ends_with_ci(leaf, ".ips");
|
||||
if (kind != PLAT_FS_FILE || (!ips && !ends_with_ci(leaf, ".pchtxt"))) return 1;
|
||||
m->kind |= MOD_EXEFS;
|
||||
char path[4300], rel[600];
|
||||
snprintf(path, sizeof path, "%s/%s", w->folder, leaf);
|
||||
snprintf(rel, sizeof rel, "%s/%s/%s", m->name, w->rel, leaf);
|
||||
uint64_t n = 0;
|
||||
char *data = (char *)plat_file_read(path, &n);
|
||||
if (!data) {
|
||||
rt_log("mods: %s: cannot read %s", m->name, path);
|
||||
return 1;
|
||||
}
|
||||
int for_main;
|
||||
if (ips) {
|
||||
for_main = same_build(leaf, strlen(leaf) - 4);
|
||||
} else {
|
||||
char *id = NULL; // after "@nsobid-" at the start of a line
|
||||
for (char *p = data; !id && p && data + n - p > 8;) {
|
||||
if (!memcmp(p, "@nsobid-", 8)) id = p + 8;
|
||||
p = memchr(p, '\n', (size_t)(data + n - p));
|
||||
if (p) p++;
|
||||
}
|
||||
size_t len = 0;
|
||||
while (id && id + len < data + n && isxdigit((unsigned char)id[len])) len++;
|
||||
for_main = id && same_build(id, len);
|
||||
}
|
||||
char key[9], content[9], kind_s[16], over[256], note[256];
|
||||
snprintf(key, sizeof key, "%08X", fnv(rel, strlen(rel)));
|
||||
snprintf(content, sizeof content, "%08X", fnv(data, (size_t)n));
|
||||
free(data);
|
||||
if (!for_main) {
|
||||
m->other_version++;
|
||||
return 1;
|
||||
}
|
||||
if (!status_line(key, content, kind_s, over, note)) {
|
||||
m->pending++;
|
||||
m->pending_key = (m->pending_key ^ fnv(key, 8)) * 0x01000193u;
|
||||
m->pending_key = (m->pending_key ^ fnv(content, 8)) * 0x01000193u;
|
||||
return 1;
|
||||
}
|
||||
if (!strcmp(kind_s, "unsupported")) {
|
||||
if (!w->unsupported) snprintf(m->note, sizeof m->note, "%s: %s", leaf, note);
|
||||
w->unsupported = 1;
|
||||
} else if (!strcmp(kind_s, "code") && m->nsets < (int)(sizeof m->sets / sizeof *m->sets)) {
|
||||
m->sets[m->nsets++] = (uint32_t)strtoul(key, NULL, 16);
|
||||
size_t k = strlen(m->overrides);
|
||||
if (*over) snprintf(m->overrides + k, sizeof m->overrides - k, "%s%s", k ? "," : "", over);
|
||||
} else if (!strcmp(kind_s, "other")) {
|
||||
m->other_version++;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// atmosphere/exefs_patches/<name>/ of a mod: each <name> folder's files.
|
||||
static int exefs_patches_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
ExefsWalk *w = ctx;
|
||||
if (kind != PLAT_FS_DIR) return 1;
|
||||
char folder[4300], rel[600];
|
||||
snprintf(folder, sizeof folder, "%s/%s", w->folder, leaf);
|
||||
snprintf(rel, sizeof rel, "atmosphere/exefs_patches/%s", leaf);
|
||||
ExefsWalk sub = {w->m, folder, rel, 0};
|
||||
plat_fs_list(folder, exefs_file_cb, &sub);
|
||||
w->unsupported |= sub.unsupported;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The ExeFS patch files of mod m (folder path): 1 when one cannot be applied.
|
||||
static int exefs_check(ModInfo *m, const char *path) {
|
||||
ExefsWalk w = {m, m->exefs, m->exefs + strlen(path) + 1, 0};
|
||||
if (m->exefs[0]) plat_fs_list(m->exefs, exefs_file_cb, &w);
|
||||
char patches[2200];
|
||||
snprintf(patches, sizeof patches, "%s/atmosphere/exefs_patches", path);
|
||||
ExefsWalk at = {m, patches, "", 0};
|
||||
if (plat_dir_exists(patches)) plat_fs_list(patches, exefs_patches_cb, &at);
|
||||
return w.unsupported || at.unsupported;
|
||||
}
|
||||
|
||||
// One mod folder of the scan.
|
||||
static int scan_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
const char *disabled = ctx;
|
||||
if (kind != PLAT_FS_DIR) return 1;
|
||||
if (nmods == MAX_MODS) {
|
||||
rt_log("mods: more than %d folders in %s; the rest are left out", MAX_MODS, dir);
|
||||
return 0;
|
||||
}
|
||||
ModInfo *m = &mods[nmods];
|
||||
memset(m, 0, sizeof *m);
|
||||
char path[2100];
|
||||
if (snprintf(m->name, sizeof m->name, "%s", name) >= (int)sizeof m->name ||
|
||||
snprintf(path, sizeof path, "%s/%s", dir, name) >= (int)sizeof path) {
|
||||
rt_log("mods: %s: name too long, left out", name);
|
||||
return 1;
|
||||
}
|
||||
find_root(path, "romfs", m->romfs, sizeof m->romfs);
|
||||
if (!m->romfs[0]) find_root(path, "romfslite", m->romfs, sizeof m->romfs);
|
||||
find_root(path, "exefs", m->exefs, sizeof m->exefs);
|
||||
find_root(path, "cheats", m->cheats, sizeof m->cheats);
|
||||
if (m->romfs[0] && has_file(m->romfs)) m->kind |= MOD_ROMFS;
|
||||
if (m->exefs[0]) plat_fs_list(m->exefs, exefs_cb, &m->kind);
|
||||
if (m->cheats[0]) plat_fs_list(m->cheats, cheats_cb, &m->kind);
|
||||
int unsupported = exefs_check(m, path);
|
||||
// a mod whose code part the port cannot use is off as a whole: its RomFS files may expect that code
|
||||
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
|
||||
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
|
||||
: unsupported ? MOD_EXEFS_UNSUPPORTED
|
||||
: m->pending ? MOD_EXEFS_LATER
|
||||
: m->nsets || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
|
||||
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
|
||||
: MOD_EMPTY;
|
||||
m->enabled = !listed(disabled, m->name);
|
||||
nmods++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mods_scan(void) {
|
||||
static int done;
|
||||
if (done) return;
|
||||
done = 1;
|
||||
const char *d = cfg_get("TOTK_MODS_DIR");
|
||||
if (d && *d) {
|
||||
snprintf(dir, sizeof dir, "%s", d);
|
||||
} else {
|
||||
char exe[2048];
|
||||
if (!plat_exe_path(exe, sizeof exe)) return;
|
||||
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (!cut) return;
|
||||
*cut = 0;
|
||||
snprintf(dir, sizeof dir, "%s/mods", exe);
|
||||
}
|
||||
if (!plat_dir_exists(dir)) {
|
||||
rt_log("mods: no folder %s", dir);
|
||||
return;
|
||||
}
|
||||
mods = calloc(MAX_MODS, sizeof *mods);
|
||||
if (!mods) return;
|
||||
plat_fs_list(dir, scan_cb, (void *)cfg_get("TOTK_MODS_DISABLED"));
|
||||
order_list = cfg_get("TOTK_MODS_ORDER");
|
||||
qsort(mods, nmods, sizeof *mods, by_order);
|
||||
order_list = NULL;
|
||||
for (int i = 0; i < nmods; i++) {
|
||||
snprintf(start_order[i], sizeof start_order[i], "%s", mods[i].name);
|
||||
start_enabled[i] = mods[i].enabled;
|
||||
const ModInfo *m = &mods[i];
|
||||
rt_log("mods: %s: %s,%s%s%s%s%s, %s%s%s", m->name, m->enabled ? "on" : "off", m->kind & MOD_ROMFS ? " romfs" : "",
|
||||
m->kind & MOD_EXEFS ? " exefs" : "", m->kind & MOD_SUBSDK ? " subsdk" : "",
|
||||
m->kind & MOD_CHEATS ? " cheats" : "", m->kind ? "" : " no files", problem_name(m->problem),
|
||||
m->note[0] ? ": " : "", m->note);
|
||||
}
|
||||
rt_log("mods: %d folders in %s", nmods, dir);
|
||||
}
|
||||
|
||||
void mods_exefs_switch(void) {
|
||||
mods_scan();
|
||||
for (int i = 0; i < nmods; i++) {
|
||||
const ModInfo *m = &mods[i];
|
||||
if (!start_enabled[i] || m->problem != MOD_OK || !m->nsets) continue;
|
||||
for (int k = 0; k < m->nsets; k++) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof name, "mod_%08x", m->sets[k]);
|
||||
int set = rt_patch_set(name);
|
||||
if (set >= 0) rt_patch_switch(set, 1);
|
||||
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
|
||||
}
|
||||
char over[256];
|
||||
snprintf(over, sizeof over, "%s", m->overrides);
|
||||
for (char *s = strtok(over, ","); s; s = strtok(NULL, ",")) {
|
||||
int set = rt_patch_set(s);
|
||||
if (set >= 0) rt_patch_switch(set, 0);
|
||||
rt_log("mods: %s replaces the port's patch set %s; it stays off", m->name, s);
|
||||
}
|
||||
rt_log("mods: %s: %d ExeFS patch files on", m->name, m->nsets);
|
||||
}
|
||||
}
|
||||
|
||||
int mods_exefs_pending(uint32_t *key) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < nmods; i++) {
|
||||
if (!start_enabled[i] || mods[i].problem != MOD_EXEFS_LATER) continue;
|
||||
n += (int)mods[i].pending;
|
||||
*key = (*key ^ mods[i].pending_key) * 0x01000193u;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
const char *mods_dir(void) { return dir; }
|
||||
int mods_count(void) { return nmods; }
|
||||
ModInfo *mods_at(int i) { return i >= 0 && i < nmods ? &mods[i] : NULL; }
|
||||
|
||||
// TOTK_MODS_ORDER as every listed name top first; TOTK_MODS_DISABLED as the switched-off names. A name holding '|'
|
||||
// would split into two; it is left out of both (its mod is unusable anyway).
|
||||
static void write_keys(void) {
|
||||
size_t cap = (size_t)nmods * 257 + 1;
|
||||
char *order = calloc(cap, 1), *off = calloc(cap, 1);
|
||||
if (order && off) {
|
||||
for (int i = 0; i < nmods; i++) {
|
||||
if (mods[i].problem == MOD_BAD_NAME) continue;
|
||||
if (*order) strcat(order, "|");
|
||||
strcat(order, mods[i].name);
|
||||
if (!mods[i].enabled) {
|
||||
if (*off) strcat(off, "|");
|
||||
strcat(off, mods[i].name);
|
||||
}
|
||||
}
|
||||
cfg_set("TOTK_MODS_ORDER", order);
|
||||
cfg_set("TOTK_MODS_DISABLED", off);
|
||||
}
|
||||
free(order);
|
||||
free(off);
|
||||
}
|
||||
|
||||
void mods_set_enabled(int i, int on) {
|
||||
if (i < 0 || i >= nmods) return;
|
||||
mods[i].enabled = on != 0;
|
||||
write_keys();
|
||||
}
|
||||
|
||||
void mods_move(int i, int delta) {
|
||||
int j = i + delta;
|
||||
if (i < 0 || i >= nmods || j < 0 || j >= nmods || i == j) return;
|
||||
ModInfo t = mods[i];
|
||||
mods[i] = mods[j];
|
||||
mods[j] = t;
|
||||
write_keys();
|
||||
}
|
||||
|
||||
int mods_changed(void) {
|
||||
for (int i = 0; i < nmods; i++)
|
||||
if (strcmp(mods[i].name, start_order[i]) || mods[i].enabled != start_enabled[i]) return 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Player mods (mods.c): one folder per mod under mods/ next to totk.exe (TOTK_MODS_DIR), unpacked as emulators take
|
||||
// them: <mod>/romfs, <mod>/exefs and <mod>/cheats, the Atmosphere layout (atmosphere/contents/<title id>/romfs,
|
||||
// atmosphere/exefs_patches/<name>/*.ips) or TKMM's merged output (romfs or romfslite, exefs, cheats). ExeFS patch
|
||||
// files (.ips, .pchtxt) for the game's main are patch sets the setup builds into the game (tools/exefs_patches.py),
|
||||
// switched with their mod (mods_exefs_switch). The list is scanned once at start (romfs.c overlay_init);
|
||||
// its order and switches live in totk.ini [mods] (TOTK_MODS_ORDER: names top first, separated by '|';
|
||||
// TOTK_MODS_DISABLED: names switched off). The top mod wins a file two mods replace.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
enum { MOD_ROMFS = 1, MOD_EXEFS = 2, MOD_SUBSDK = 4, MOD_CHEATS = 8 }; // ModInfo.kind bits
|
||||
typedef enum {
|
||||
MOD_OK,
|
||||
MOD_EMPTY, // no RomFS file, no ExeFS patch, no cheat file
|
||||
MOD_PLUGIN, // has code modules (exefs/subsdk*, a replaced exefs/main): the port has no module loader, so the
|
||||
// whole mod stays off (its RomFS files may expect that code)
|
||||
MOD_EXEFS_LATER, // has ExeFS patches for this game's main that the game was built without: the rebuild at the
|
||||
// next start builds them in; the whole mod stays off until then
|
||||
MOD_EXEFS_UNSUPPORTED, // an ExeFS patch file the port cannot apply (note says why): the whole mod stays off
|
||||
MOD_EXEFS_OTHER, // only ExeFS patches, all for another game version
|
||||
MOD_BAD_NAME, // its folder name holds '|', the separator of the order keys
|
||||
} ModProblem;
|
||||
|
||||
typedef struct {
|
||||
char name[256]; // folder name, UTF-8
|
||||
char romfs[2048]; // UTF-8 path of its RomFS root; "" none
|
||||
char exefs[2048]; // UTF-8 path of its ExeFS folder; "" none
|
||||
char cheats[2048]; // UTF-8 path of its cheats folder; "" none
|
||||
int kind, enabled;
|
||||
ModProblem problem;
|
||||
uint32_t replaced, added, overridden; // romfs.c: files served (replacing / added), files a higher mod serves
|
||||
uint32_t other_version; // files for another game version (RomFS *.Product.<n>.*, ExeFS patches
|
||||
// for another build), not used
|
||||
char overridden_by[256]; // the first higher mod that took one of its files
|
||||
char note[256]; // MOD_EXEFS_UNSUPPORTED: why
|
||||
uint32_t pending, pending_key; // ExeFS patch files the build lacks; FNV-1a of their names and contents
|
||||
int nsets;
|
||||
uint32_t sets[16]; // the built patch sets mod_<hash> of its ExeFS patch files
|
||||
char overrides[256]; // the port's own patch sets its patches replace, comma separated
|
||||
} ModInfo;
|
||||
|
||||
void mods_scan(void); // once; later calls return at once
|
||||
const char *mods_dir(void); // the folder scanned (default: <dir of totk.exe>/mods)
|
||||
int mods_count(void);
|
||||
ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
|
||||
// Menu edits (present path). They change the list mods_at shows and write TOTK_MODS_ORDER / TOTK_MODS_DISABLED with
|
||||
// cfg_set; romfs.c used the start state, so they apply at the next start.
|
||||
void mods_set_enabled(int i, int on);
|
||||
void mods_move(int i, int delta); // -1 up, +1 down; nothing at the ends
|
||||
int mods_changed(void); // 1 when the order or a switch differs from the start
|
||||
// After rt_patches_init: the patch sets of the enabled mods' ExeFS patches on, the port's own sets they replace off.
|
||||
void mods_exefs_switch(void);
|
||||
// The ExeFS patch files of the enabled mods that the build lacks, their FNV-1a mixed into *key (cheats.c starts the
|
||||
// rebuild for them).
|
||||
int mods_exefs_pending(uint32_t *key);
|
||||
// The hex of the game main's build id (64 digits), "" when exefs/main cannot be read.
|
||||
const char *mods_main_build_id(void);
|
||||
@@ -0,0 +1,130 @@
|
||||
// --probe-mods: the mods folder scan, its order and switches (mods.c). Builds a mods folder of its own next to totk.exe,
|
||||
// scans it and removes it. Needs no game files.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "mods.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL mods: %s\n", what); } } while (0)
|
||||
#define EQ(a, b) ((a) && !strcmp((a), (b)))
|
||||
|
||||
// root/rel as a file of a few bytes (rel ends in '/': a folder), with every folder above it.
|
||||
static void put(const char *root, const char *rel) {
|
||||
char p[2048];
|
||||
snprintf(p, sizeof p, "%s/%s", root, rel);
|
||||
char *slash = strrchr(p, '/');
|
||||
*slash = 0;
|
||||
plat_file_mkdir_p(p);
|
||||
*slash = '/';
|
||||
if (slash[1]) plat_file_write(p, "probe", 5);
|
||||
}
|
||||
|
||||
static int remove_cb(const char *name, int kind, uint64_t size, void *ctx);
|
||||
static void remove_tree(const char *dir) {
|
||||
plat_fs_list(dir, remove_cb, (void *)dir);
|
||||
plat_fs_rmdir(dir);
|
||||
}
|
||||
static int remove_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
char sub[2048];
|
||||
snprintf(sub, sizeof sub, "%s/%s", (const char *)ctx, name);
|
||||
if (kind == PLAT_FS_DIR) remove_tree(sub);
|
||||
else plat_fs_unlink(sub);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// s ends in tail, either path separator matching either.
|
||||
static int ends_with(const char *s, const char *tail) {
|
||||
size_t n = strlen(s), t = strlen(tail);
|
||||
if (n < t) return 0;
|
||||
for (size_t i = 0; i < t; i++) {
|
||||
char a = s[n - t + i], b = tail[i];
|
||||
if (a == '\\') a = '/';
|
||||
if (b == '\\') b = '/';
|
||||
if (a != b) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int probe_mods(void) {
|
||||
char root[1024];
|
||||
if (!plat_exe_path(root, sizeof root)) return 1;
|
||||
char *cut = strrchr(root, '/'), *back = strrchr(root, '\\');
|
||||
if (back > cut) cut = back;
|
||||
if (!cut) return 1;
|
||||
strcpy(cut + 1, "mods-probe");
|
||||
remove_tree(root);
|
||||
plat_file_mkdir_p(root);
|
||||
static const char *const files[] = {
|
||||
"A/romfs/x.bin", "B/atmosphere/contents/0100F2C0115B6000/romfs/x.bin",
|
||||
"C/contents/0100F2C0115B6000/romfs/y.bin", "D/exefs/main.pchtxt", "E/exefs/subsdk9", "F/romfs/",
|
||||
"G/romfs/z.bin", "G/exefs/1234.ips", "Ultra \xD0\x96/romfs/u.bin",
|
||||
"H/romfs/h.bin", "H/exefs/subsdk9", "I/romfs/i.bin", "I/exefs/main", "J/cheats/0123456789ABCDEF.txt",
|
||||
"K/romfslite/k.bin", "L/romfs/l.bin", "L/cheats/0123456789ABCDEF.txt", "L/exefs/main.npdm",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof files / sizeof *files; i++) put(root, files[i]);
|
||||
// an IPS file for this game's main (any file counts as one when main cannot be read), in Atmosphere's layout
|
||||
char ips[160];
|
||||
snprintf(ips, sizeof ips, "M/atmosphere/exefs_patches/x/%s.ips", *mods_main_build_id() ? mods_main_build_id() : "0");
|
||||
put(root, ips);
|
||||
|
||||
cfg_set("TOTK_MODS_DIR", root);
|
||||
cfg_set("TOTK_MODS_ORDER", "B|A|Gone");
|
||||
cfg_set("TOTK_MODS_DISABLED", "C");
|
||||
mods_scan();
|
||||
|
||||
static const char *const order[] = {"B", "A", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
||||
"Ultra \xD0\x96"};
|
||||
enum { N = sizeof order / sizeof *order };
|
||||
CHECK(mods_count() == N, "fourteen mod folders");
|
||||
int in_order = mods_count() == N;
|
||||
for (int i = 0; in_order && i < N; i++) in_order = EQ(mods_at(i)->name, order[i]);
|
||||
CHECK(in_order, "order: mods_order first, the rest by name");
|
||||
if (mods_count() == N && in_order) {
|
||||
ModInfo *a = mods_at(1), *b = mods_at(0), *c = mods_at(2), *d = mods_at(3), *e = mods_at(4), *f = mods_at(5),
|
||||
*g = mods_at(6), *hm = mods_at(7), *im = mods_at(8), *jm = mods_at(9), *km = mods_at(10),
|
||||
*lm = mods_at(11), *mm = mods_at(12), *u = mods_at(13);
|
||||
CHECK(hm->kind == (MOD_ROMFS | MOD_SUBSDK) && hm->problem == MOD_PLUGIN, "RomFS + subsdk plugin: unusable");
|
||||
CHECK(im->kind == (MOD_ROMFS | MOD_SUBSDK) && im->problem == MOD_PLUGIN, "RomFS + replaced exefs/main");
|
||||
CHECK(ends_with(b->romfs, "B/atmosphere/contents/0100F2C0115B6000/romfs"), "Atmosphere RomFS root");
|
||||
CHECK(ends_with(c->romfs, "C/contents/0100F2C0115B6000/romfs"), "contents RomFS root");
|
||||
CHECK(ends_with(a->romfs, "A/romfs") && a->kind == MOD_ROMFS && a->problem == MOD_OK, "plain RomFS mod");
|
||||
CHECK(!c->enabled && a->enabled && b->enabled && g->enabled && u->enabled, "mods_disabled switches C off");
|
||||
CHECK(d->kind == MOD_EXEFS && d->problem == MOD_EXEFS_OTHER && d->other_version == 1,
|
||||
"ExeFS patch mod without a build id: another version's");
|
||||
CHECK(e->problem == MOD_PLUGIN, "subsdk-only mod is a plugin mod");
|
||||
CHECK(f->problem == MOD_EMPTY, "empty RomFS folder");
|
||||
CHECK(g->kind == (MOD_ROMFS | MOD_EXEFS) && g->problem == MOD_OK && g->other_version == 1,
|
||||
"RomFS + another version's ExeFS patch: the RomFS is used");
|
||||
CHECK(mm->kind == MOD_EXEFS && mm->problem == MOD_EXEFS_LATER && mm->pending == 1,
|
||||
"an ExeFS patch for main the build lacks waits for the rebuild");
|
||||
CHECK(jm->kind == MOD_CHEATS && jm->problem == MOD_OK && ends_with(jm->cheats, "J/cheats"),
|
||||
"cheats-only mod");
|
||||
CHECK(km->kind == MOD_ROMFS && km->problem == MOD_OK && ends_with(km->romfs, "K/romfslite"), "TKMM romfslite");
|
||||
CHECK(lm->kind == (MOD_ROMFS | MOD_CHEATS) && lm->problem == MOD_OK, "RomFS + cheats, main.npdm is no code");
|
||||
CHECK(u->kind == MOD_ROMFS && ends_with(u->romfs, "Ultra \xD0\x96/romfs"), "Unicode folder name");
|
||||
CHECK(!mods_changed(), "nothing changed after the scan");
|
||||
|
||||
mods_move(1, -1);
|
||||
CHECK(EQ(mods_at(0)->name, "A") && EQ(mods_at(1)->name, "B"), "move up");
|
||||
CHECK(EQ(cfg_get("TOTK_MODS_ORDER"), "A|B|C|D|E|F|G|H|I|J|K|L|M|Ultra \xD0\x96"),
|
||||
"order written without the gone name");
|
||||
mods_move(0, -1);
|
||||
CHECK(EQ(mods_at(0)->name, "A"), "move up at the top does nothing");
|
||||
mods_move(N - 1, 1);
|
||||
CHECK(EQ(mods_at(N - 1)->name, "Ultra \xD0\x96"), "move down at the bottom does nothing");
|
||||
mods_set_enabled(2, 1);
|
||||
const char *dis = cfg_get("TOTK_MODS_DISABLED");
|
||||
CHECK(mods_at(2)->enabled && (!dis || !*dis), "switching C on empties mods_disabled");
|
||||
mods_set_enabled(0, 0);
|
||||
CHECK(EQ(cfg_get("TOTK_MODS_DISABLED"), "A"), "switching A off writes it");
|
||||
CHECK(mods_changed(), "changes seen");
|
||||
}
|
||||
remove_tree(root);
|
||||
printf("mods: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
// Horizon OS nfp:user IPC service implementation (nn::nfp::detail::IUserManager & IUser).
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "amiibo.h"
|
||||
#include "host.h"
|
||||
#include "ipc.h"
|
||||
#include "kernel.h"
|
||||
#include "nfp.h"
|
||||
|
||||
// Simulated NFC device handle: ID 1
|
||||
static const uint8_t g_device_handle[8] = { 1, 0, 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
typedef struct {
|
||||
uint32_t activate_event; // writable handle
|
||||
uint32_t deactivate_event; // writable handle
|
||||
uint32_t avail_event; // writable handle
|
||||
} NfpUserState;
|
||||
|
||||
static void nfp_user_close(void *p) {
|
||||
NfpUserState *st = p;
|
||||
if (st) {
|
||||
if (st->activate_event) kern_handle_close(st->activate_event);
|
||||
if (st->deactivate_event) kern_handle_close(st->deactivate_event);
|
||||
if (st->avail_event) kern_handle_close(st->avail_event);
|
||||
free(st);
|
||||
}
|
||||
rt_log("nfp: IUser session closed");
|
||||
}
|
||||
|
||||
// Handler for nn::nfp::detail::IUser
|
||||
static void nfp_user_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
||||
NfpUserState *st = (NfpUserState *)s->state;
|
||||
|
||||
switch (rq->cmd) {
|
||||
case 0: { // InitializeNfp(u64 aruid, u64 m_pid, pid, buffer<unknown, 5>)
|
||||
rt_log("nfp: Initialize");
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 1: { // FinalizeNfp()
|
||||
rt_log("nfp: Finalize");
|
||||
amiibo_stop_detection();
|
||||
amiibo_unmount();
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 2: { // ListDevicesNfp() -> (s32 total_out, buffer<DeviceHandle, 0xa>)
|
||||
int32_t count = amiibo_is_device_present() ? 1 : 0;
|
||||
IPC_OUT(rs, count);
|
||||
if (count > 0 && rq->nb > 0 && rq->b[0].size >= sizeof(g_device_handle)) {
|
||||
void *dst = ipc_guest(rq->b[0].addr, sizeof(g_device_handle));
|
||||
memcpy(dst, g_device_handle, sizeof(g_device_handle));
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 3: { // StartDetectionNfp(DeviceHandle)
|
||||
amiibo_start_detection();
|
||||
if (amiibo_has_tag() && st && st->activate_event) {
|
||||
kern_event_signal(st->activate_event);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 4: { // StopDetectionNfp(DeviceHandle)
|
||||
amiibo_stop_detection();
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 5: { // MountNfp(DeviceHandle, u32 ModelType, u32 MountTarget)
|
||||
if (!amiibo_has_tag()) {
|
||||
rs->result = 0x8073u; // Device/Tag not found
|
||||
return;
|
||||
}
|
||||
amiibo_mount();
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 6: { // UnmountNfp(DeviceHandle)
|
||||
amiibo_unmount();
|
||||
if (st && st->deactivate_event) {
|
||||
kern_event_signal(st->deactivate_event);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 7: { // OpenApplicationAreaNfp(DeviceHandle, u32 access_id)
|
||||
if (!amiibo_is_mounted()) {
|
||||
rs->result = 0x927Fu; // not mounted
|
||||
return;
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 8: { // GetApplicationAreaNfp(DeviceHandle) -> (u32 size, buffer<data, 0x6>)
|
||||
if (!amiibo_is_mounted()) {
|
||||
rs->result = 0x927Fu;
|
||||
return;
|
||||
}
|
||||
uint32_t area_size = 0xD8;
|
||||
if (rq->nb > 0 && rq->b[0].size > 0) {
|
||||
void *dst = ipc_guest(rq->b[0].addr, rq->b[0].size);
|
||||
area_size = (uint32_t)amiibo_get_application_area(dst, (uint32_t)rq->b[0].size);
|
||||
}
|
||||
IPC_OUT(rs, area_size);
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 13: { // GetTagInfoNfp(DeviceHandle) -> buffer<TagInfo, 0x1a>
|
||||
if (!amiibo_has_tag() && !amiibo_is_mounted()) {
|
||||
rs->result = 0x8073u;
|
||||
return;
|
||||
}
|
||||
if (rq->nb > 0 && rq->b[0].size >= 0x58) {
|
||||
void *dst = ipc_guest(rq->b[0].addr, 0x58);
|
||||
amiibo_get_tag_info(dst);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 14: { // GetRegisterInfoNfp(DeviceHandle) -> buffer<RegisterInfo, 0x1a>
|
||||
if (!amiibo_is_mounted()) {
|
||||
rs->result = 0x927Fu;
|
||||
return;
|
||||
}
|
||||
if (rq->nb > 0 && rq->b[0].size >= 0x100) {
|
||||
void *dst = ipc_guest(rq->b[0].addr, 0x100);
|
||||
amiibo_get_register_info(dst);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 15: { // GetCommonInfoNfp(DeviceHandle) -> buffer<CommonInfo, 0x1a>
|
||||
if (!amiibo_is_mounted()) {
|
||||
rs->result = 0x927Fu;
|
||||
return;
|
||||
}
|
||||
if (rq->nb > 0 && rq->b[0].size >= 0x40) {
|
||||
void *dst = ipc_guest(rq->b[0].addr, 0x40);
|
||||
amiibo_get_common_info(dst);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 16: { // GetModelInfoNfp(DeviceHandle) -> buffer<ModelInfo, 0x1a>
|
||||
if (!amiibo_is_mounted()) {
|
||||
rs->result = 0x927Fu;
|
||||
return;
|
||||
}
|
||||
if (rq->nb > 0 && rq->b[0].size >= 0x40) {
|
||||
void *dst = ipc_guest(rq->b[0].addr, 0x40);
|
||||
amiibo_get_model_info(dst);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 17: { // GetActivateEventHandleNfp(DeviceHandle) -> handle<copy>
|
||||
if (st && st->activate_event) {
|
||||
rs->copy[rs->ncopy++] = kern_event_readable(st->activate_event);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 18: { // GetDeactivateEventHandleNfp(DeviceHandle) -> handle<copy>
|
||||
if (st && st->deactivate_event) {
|
||||
rs->copy[rs->ncopy++] = kern_event_readable(st->deactivate_event);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 19: { // GetStateNfp() -> u32 state
|
||||
uint32_t state = 1; // Initialized
|
||||
IPC_OUT(rs, state);
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 20: { // GetDeviceStateNfp(DeviceHandle) -> u32 device_state
|
||||
uint32_t dev_state = (uint32_t)amiibo_get_device_state();
|
||||
IPC_OUT(rs, dev_state);
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 21: { // GetNpadIdNfp(DeviceHandle) -> u32 npad_id
|
||||
uint32_t npad_id = 0; // Handheld / Player 1
|
||||
IPC_OUT(rs, npad_id);
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 22: { // GetApplicationAreaSizeNfp(DeviceHandle) -> u32 size
|
||||
uint32_t size = 0xD8;
|
||||
IPC_OUT(rs, size);
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
case 23: { // GetAvailabilityChangeEventHandleNfp() -> handle<copy>
|
||||
if (st && st->avail_event) {
|
||||
rs->copy[rs->ncopy++] = kern_event_readable(st->avail_event);
|
||||
}
|
||||
rs->result = 0;
|
||||
return;
|
||||
}
|
||||
default:
|
||||
ipc_unimplemented(s, rq);
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for nn::nfp::detail::IUserManager (nfp:user)
|
||||
void nfp_user_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
||||
switch (rq->cmd) {
|
||||
case 0: { // CreateUserInterface() -> move handle to IUser
|
||||
NfpUserState *st = calloc(1, sizeof(*st));
|
||||
if (!st) rt_fatal("nfp: failed to allocate user state");
|
||||
st->activate_event = kern_event_new();
|
||||
st->deactivate_event = kern_event_new();
|
||||
st->avail_event = kern_event_new();
|
||||
// Signal availability immediately so game detects reader
|
||||
kern_event_signal(st->avail_event);
|
||||
|
||||
uint32_t user_session = ipc_session_new("nfp:IUser", nfp_user_handler, st, nfp_user_close);
|
||||
rs->move[rs->nmove++] = user_session;
|
||||
rs->result = 0;
|
||||
rt_log("nfp: created IUser session handle %#x", user_session);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
ipc_unimplemented(s, rq);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Nintendo Figurine Platform (nfp:user) Horizon OS IPC service emulation.
|
||||
#pragma once
|
||||
#include "ipc.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// sm: handler for "nfp:user" (nn::nfp::detail::IUserManager)
|
||||
void nfp_user_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+104
-5
@@ -75,15 +75,114 @@ static void put_hdr(uint8_t *p, uint32_t op, uint32_t size) {
|
||||
memcpy(p, &h, sizeof h);
|
||||
}
|
||||
|
||||
// 1.4.3 initializes and finalizes command buffers about 2000 times a second while a scene loads and several times a
|
||||
// frame in play, and its two largest per-frame recordings take 1-2 MiB. Freeing the recording buffer at Finalize made
|
||||
// every new recording grow again from 64 KiB through up to five reallocations and copies, the last ones fresh 1-2 MiB
|
||||
// blocks the heap takes from the system: new commit and zero-filled pages twice a frame. Those growths were most of the
|
||||
// process's new commit requests, so when the system ran short for a moment (a Depths load at render scale 200 on 16 GB)
|
||||
// a 1 MiB growth was the allocation that failed. Finalized buffers therefore wait in a small pool for the next
|
||||
// recording, and a failed allocation waits for memory instead of ending the game (realloc_wait).
|
||||
#define POOL_SLOTS 16
|
||||
#define POOL_MAX_BYTES ((size_t)4 << 20) // a larger buffer (a recording that inlined a big one) goes back to the heap
|
||||
static PlatMutex pool_lock = PLAT_MUTEX_INIT;
|
||||
static struct { uint8_t *p; size_t size; } pool[POOL_SLOTS];
|
||||
static uint32_t npool;
|
||||
|
||||
// The largest pooled buffer and its size, or NULL: the largest, so the big per-frame recordings stop growing once the
|
||||
// pool holds enough buffers of their size.
|
||||
static uint8_t *pool_take(size_t *size) {
|
||||
uint8_t *p = NULL;
|
||||
plat_mutex_lock(&pool_lock);
|
||||
if (npool) {
|
||||
uint32_t best = 0;
|
||||
for (uint32_t i = 1; i < npool; i++)
|
||||
if (pool[i].size > pool[best].size) best = i;
|
||||
p = pool[best].p;
|
||||
*size = pool[best].size;
|
||||
pool[best] = pool[--npool];
|
||||
}
|
||||
plat_mutex_unlock(&pool_lock);
|
||||
return p;
|
||||
}
|
||||
|
||||
// Keeps a finalized buffer when the pool has room or holds a smaller one (which is freed instead).
|
||||
static void pool_put(uint8_t *p, size_t size) {
|
||||
if (!p) return;
|
||||
if (size <= POOL_MAX_BYTES) {
|
||||
plat_mutex_lock(&pool_lock);
|
||||
if (npool < POOL_SLOTS) {
|
||||
pool[npool].p = p;
|
||||
pool[npool++].size = size;
|
||||
p = NULL;
|
||||
} else {
|
||||
uint32_t small = 0;
|
||||
for (uint32_t i = 1; i < npool; i++)
|
||||
if (pool[i].size < pool[small].size) small = i;
|
||||
if (pool[small].size < size) {
|
||||
uint8_t *drop = pool[small].p;
|
||||
pool[small].p = p;
|
||||
pool[small].size = size;
|
||||
p = drop;
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&pool_lock);
|
||||
}
|
||||
free(p);
|
||||
}
|
||||
|
||||
static void pool_drain(void) {
|
||||
plat_mutex_lock(&pool_lock);
|
||||
while (npool) free(pool[--npool].p);
|
||||
plat_mutex_unlock(&pool_lock);
|
||||
}
|
||||
|
||||
// realloc that waits out a short commit shortage: Windows refuses commit beyond the current limit while it grows a
|
||||
// system-managed page file, so the same request succeeds a moment later, and a stall of some frames is better than
|
||||
// ending the game. Frees the pooled buffers first. NULL only after about 30 s without memory; the caller ends the game.
|
||||
// Never called with a lock held.
|
||||
static void *realloc_wait(void *old, size_t n, const char *what) {
|
||||
void *p = realloc(old, n);
|
||||
if (p) return p;
|
||||
pool_drain();
|
||||
static uint32_t reports;
|
||||
int report = __atomic_fetch_add(&reports, 1, __ATOMIC_RELAXED) < 8;
|
||||
if (report) {
|
||||
double proc, used, limit;
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
rt_log("nvn: %s of %zu bytes failed (memory commit: this process %.0f MB, the system %.0f of %.0f MB); waiting "
|
||||
"for memory", what, n, proc, used, limit);
|
||||
}
|
||||
uint64_t t0 = plat_now_ms();
|
||||
while (plat_now_ms() - t0 < 30000) {
|
||||
plat_sleep_ms(10);
|
||||
if ((p = realloc(old, n))) {
|
||||
if (report)
|
||||
rt_log("nvn: %s of %zu bytes succeeded after %llu ms", what, n,
|
||||
(unsigned long long)(plat_now_ms() - t0));
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void cmd_reserve(cmd_t *o, size_t need) {
|
||||
if (o->seg && o->cur + need + OP_RESERVE <= o->end) return;
|
||||
if (!o->seg) { // the first record since Initialize: a pooled buffer when one waits
|
||||
size_t size = 0;
|
||||
uint8_t *p = pool_take(&size);
|
||||
if (p) {
|
||||
o->seg = o->cur = p;
|
||||
o->end = p + size;
|
||||
if (need + OP_RESERVE <= size) return;
|
||||
}
|
||||
}
|
||||
size_t used = o->seg ? (size_t)(o->cur - o->seg) : 0;
|
||||
size_t cap = o->seg ? (size_t)(o->end - o->seg) : 0;
|
||||
if (!cap) cap = 64 << 10;
|
||||
while (used + need + OP_RESERVE > cap) cap *= 2;
|
||||
uint8_t *b = realloc(o->seg, cap);
|
||||
// A block this size fails only without commit (the fatal report states it) or when seg is no block of ours: a
|
||||
// command buffer object the game overwrote or copied shows a wrong magic or more used than its old size.
|
||||
uint8_t *b = realloc_wait(o->seg, cap, "command recording buffer");
|
||||
// Still failing after the wait: the system has no commit left (the fatal report states it), or seg is no block of
|
||||
// ours: a command buffer object the game overwrote or copied shows a wrong magic or more used than its old size.
|
||||
if (!b)
|
||||
rt_fatal("nvn: command recording buffer of %zu bytes (command buffer %p, magic %#x, buffer %p of %zu bytes, %zu "
|
||||
"used, %zu asked)", cap, (void *)o, o->magic, (void *)o->seg, o->seg ? (size_t)(o->end - o->seg) : 0,
|
||||
@@ -235,7 +334,7 @@ static uint64_t copy_touch(uint64_t handle, uint64_t now, const char *what) {
|
||||
// into one host block of its exact size. One allocation and one copy: growing the copy record by record cost the
|
||||
// game's main thread about 1 ms a frame in reallocations and small copies (1.4.3, field).
|
||||
static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
|
||||
uint8_t *out = malloc(sizeof(copy_prefix) + n);
|
||||
uint8_t *out = realloc_wait(NULL, sizeof(copy_prefix) + n, "command copy");
|
||||
if (!out) rt_fatal("nvn: command copy allocation");
|
||||
memcpy(out + sizeof(copy_prefix), p, n);
|
||||
for (size_t at = 0;;) { // the records stay checked: a corrupt one stops here, not in the replay
|
||||
@@ -334,7 +433,7 @@ NVN_API(nvnCommandBufferInitialize) {
|
||||
// void nvnCommandBufferFinalize(NVNcommandBuffer *cmd): handles outlive their recorder
|
||||
NVN_API(nvnCommandBufferFinalize) {
|
||||
cmd_t *o = cmd_of(c);
|
||||
free(o->seg);
|
||||
pool_put(o->seg, (size_t)(o->end - o->seg));
|
||||
o->seg = o->cur = o->end = NULL;
|
||||
o->magic = 0;
|
||||
cb_set_disabled(o, CB_NO_COMMAND | CB_NO_CONTROL, 0);
|
||||
|
||||
@@ -323,8 +323,11 @@ NVN_API(nvnTextureReadTexelsStrided) { read_texels(c, 1); }
|
||||
NVN_API(nvnTextureWriteTexels) { write_texels(c, 0); }
|
||||
NVN_API(nvnTextureWriteTexelsStrided) { write_texels(c, 1); }
|
||||
// void nvnTexture{Flush,Invalidate}Texels(const NVNtexture *t, const NVNtextureView *v, const NVNcopyRegion *r): texel
|
||||
// access is coherent
|
||||
NVN_API(nvnTextureFlushTexels) { (void)c; }
|
||||
// access is coherent; flush marks the storage as written for re-upload
|
||||
NVN_API(nvnTextureFlushTexels) {
|
||||
const tex_t *t = arg_p(c, 0);
|
||||
if (t && t->mem && !nvn_gpu_only((uintptr_t)t->mem)) nvn_guest_written((uintptr_t)t->mem, t->L.size);
|
||||
}
|
||||
NVN_API(nvnTextureInvalidateTexels) { (void)c; }
|
||||
|
||||
// ---------- Samplers
|
||||
@@ -633,7 +636,7 @@ NVN_API(nvnWindowSetCrop) {
|
||||
// ---------- Present pacing
|
||||
// The vsync thread ticks at a fixed 60 Hz whatever the monitor does, so one game frame lasts `interval` ticks. With
|
||||
// the real-time frame step (frame_rate.c) game speed does not depend on the interval, so a frame is shown when it is
|
||||
// ready, at most at the frame limit (TOTK_FPS_LIMIT: 30 to 360 per second or the monitor's rate): holding it to the
|
||||
// ready, at most at the frame limit (TOTK_FPS_LIMIT: 30 to 240 per second or the monitor's rate): holding it to the
|
||||
// 2nd or 3rd tick would only add latency and 50 ms frames.
|
||||
#define VSYNC_HZ 60ull
|
||||
static uint64_t present_period(const win_t *o) {
|
||||
|
||||
+49
-20
@@ -348,6 +348,13 @@ static int event_works(uint64_t ev, int autoclear) {
|
||||
return !before && after && again == !autoclear;
|
||||
}
|
||||
|
||||
// A manual-clear system event an override signalled when binding it: set, and still set on a second wait.
|
||||
static int event_set(uint64_t ev) {
|
||||
static uint64_t try_wait;
|
||||
if (!try_wait && !(try_wait = ldr_symbol(TRY_WAIT_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", TRY_WAIT_SYSTEM_EVENT);
|
||||
return (uint8_t)rt_call(gc, try_wait, 1, ev) && (uint8_t)rt_call(gc, try_wait, 1, ev);
|
||||
}
|
||||
|
||||
static void probe_applet(void) {
|
||||
uint8_t *report = guest_alloc(0x100);
|
||||
uint64_t uid[2] = {1, 2};
|
||||
@@ -357,21 +364,28 @@ static void probe_applet(void) {
|
||||
"AddInternetConnectionStatus");
|
||||
CHECK(CALL("nfp.Initialize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 1, "nfp initialize");
|
||||
uint8_t handles[64];
|
||||
int32_t count = 7;
|
||||
CHECK((uint32_t)CALL("nfp.ListDevices", U(handles), U(&count), 4) == 0x8073 && count == 0, "nfp ListDevices");
|
||||
int32_t count = 0;
|
||||
CHECK((uint32_t)CALL("nfp.ListDevices", U(handles), U(&count), 4) == 0 && count == 1, "nfp ListDevices");
|
||||
uint64_t ev = U(guest_alloc(0x100));
|
||||
CHECK(CALL("nfp.AttachAvailabilityChangeEvent", ev) == 0, "AttachAvailabilityChangeEvent");
|
||||
int nfp_event = event_works(ev, 0);
|
||||
CHECK(nfp_event, "the nfp availability event does not work as a manual-clear system event");
|
||||
// The emulated reader is there from the start (amiibo.c), so the availability event is set when it is attached.
|
||||
int nfp_event = event_set(ev);
|
||||
CHECK(nfp_event, "the nfp availability event is not set for the emulated reader, or clears itself");
|
||||
CHECK((uint32_t)CALL("nfp.GetDeviceState", U(handles)) == 0, "nfp device state");
|
||||
static const char *const none[] = {"nfp.Mount", "nfp.GetNpadId", "nfp.AttachActivateEvent", "nfp.AttachDeactivateEvent",
|
||||
"nfp.StartDetection", "nfp.StopDetection", "nfp.GetTagInfo", "nfp.GetModelInfo"};
|
||||
static const char *const none[] = {"nfp.Mount", "nfp.GetTagInfo", "nfp.GetModelInfo"};
|
||||
uint8_t out[0x100];
|
||||
for (size_t i = 0; i < sizeof none / sizeof *none; i++) {
|
||||
int first_is_handle = !strcmp(none[i], "nfp.Mount") || strstr(none[i], "Detection");
|
||||
int first_is_handle = !strcmp(none[i], "nfp.Mount");
|
||||
uint64_t r = first_is_handle ? CALL(none[i], U(handles), 0, 0) : CALL(none[i], U(out), U(handles));
|
||||
CHECK((uint32_t)r == 0x8073, "%s: %#x, not device-not-found", none[i], (uint32_t)r);
|
||||
}
|
||||
CHECK((uint32_t)CALL("nfp.GetNpadId", U(out), U(handles)) == 0, "nfp GetNpadId");
|
||||
uint64_t activate = U(guest_alloc(0x100)), deactivate = U(guest_alloc(0x100));
|
||||
CHECK(CALL("nfp.AttachActivateEvent", activate, U(handles)) == 0, "nfp AttachActivateEvent");
|
||||
CHECK(CALL("nfp.AttachDeactivateEvent", deactivate, U(handles)) == 0, "nfp AttachDeactivateEvent");
|
||||
CHECK(event_works(activate, 0), "the nfp activate event is set without an amiibo on the reader");
|
||||
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection");
|
||||
CHECK((uint32_t)CALL("nfp.StopDetection", U(handles)) == 0, "nfp StopDetection");
|
||||
CHECK(CALL("nfp.Finalize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 0, "nfp finalize");
|
||||
CALL("err.ShowError", 0x2a8);
|
||||
CALL("nndiagStartup", 0);
|
||||
@@ -380,8 +394,8 @@ static void probe_applet(void) {
|
||||
CALL("err.InitializeContextWriterApi", 0);
|
||||
CALL("pctl.Initialize", 0);
|
||||
CALL("diag.InitializeApplicationAbortObserver", 0);
|
||||
printf("applet/prepo/nfp/err: launch parameter none, volume balance, play reports local, no NFC device, "
|
||||
"availability event (bound through the SDK's CreateSystemEvent) %s\n", nfp_event ? "ok" : "FAILED");
|
||||
printf("applet/prepo/nfp/err: launch parameter none, volume balance, play reports local, an NFC reader without "
|
||||
"an amiibo, availability event (bound through the SDK's CreateSystemEvent) %s\n", nfp_event ? "ok" : "FAILED");
|
||||
}
|
||||
|
||||
static void probe_account(void) {
|
||||
@@ -811,17 +825,23 @@ static void probe_fonts(void) {
|
||||
(unsigned long long)size, path);
|
||||
}
|
||||
|
||||
// The host catalog (ui_text.c): every menu id has its own English and Russian text, the software keyboard keeps its
|
||||
// game-language rows, and menu_text follows TOTK_MENU_LANG.
|
||||
// The host catalog (ui_text.c): every menu id has its own English, Russian, Brazilian Portuguese and Simplified
|
||||
// Chinese text, the software keyboard keeps its game-language rows, and menu_text follows TOTK_MENU_LANG.
|
||||
static void probe_menu_text(void) {
|
||||
static const char *const langs[] = {"en", "ja", "fr", "de", "it", "es", "nl", "pt", "ru", "zh-Hans", "zh-Hant", "ko"};
|
||||
static const char *const langs[] = {"en", "ja", "fr", "de", "it", "es", "nl", "pt", "ru", "zh-Hans", "zh-Hant", "ko",
|
||||
"pt-BR"};
|
||||
for (int id = UI_MENU_FIRST; id <= UI_MENU_LAST_ID; id++) {
|
||||
CHECK(ui_text_defined("en", (UiTextId)id) && ui_text_defined("ru", (UiTextId)id), "menu text id %d lacks en or ru", id);
|
||||
CHECK(*ui_text_in("en", (UiTextId)id) && *ui_text_in("ru", (UiTextId)id), "menu text id %d is empty", id);
|
||||
CHECK(ui_text_defined("en", (UiTextId)id) && ui_text_defined("ru", (UiTextId)id) &&
|
||||
ui_text_defined("pt-BR", (UiTextId)id), "menu text id %d lacks en, ru or pt-BR", id);
|
||||
CHECK(*ui_text_in("en", (UiTextId)id) && *ui_text_in("ru", (UiTextId)id) && *ui_text_in("pt-BR", (UiTextId)id),
|
||||
"menu text id %d is empty", id);
|
||||
// the description pane keeps the whole text and a note after it (option_rows.cpp desc_set)
|
||||
CHECK(strlen(ui_text_in("en", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
|
||||
strlen(ui_text_in("ru", (UiTextId)id)) + 256 < UI_DESC_BYTES, "menu text id %d is too long for the "
|
||||
"description pane", id);
|
||||
strlen(ui_text_in("ru", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
|
||||
strlen(ui_text_in("pt-BR", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
|
||||
strlen(ui_text_in("zh-Hans", (UiTextId)id)) + 256 < UI_DESC_BYTES,
|
||||
"menu text id %d is too long for the description pane", id);
|
||||
CHECK(ui_text_defined("zh-Hans", (UiTextId)id), "menu text id %d lacks Simplified Chinese", id);
|
||||
}
|
||||
CHECK(!ui_text_defined("en", UI_TEXT_COUNT) && !*ui_text_in("ru", UI_TEXT_COUNT), "an id past the catalog gives text");
|
||||
// other languages fall back to English for the menu, and keep their own keyboard texts
|
||||
@@ -829,22 +849,31 @@ static void probe_menu_text(void) {
|
||||
CHECK(!strcmp(ui_text_in("ru", UI_SWKBD_OK), "ОК") && !strcmp(ui_text_in("de", UI_SWKBD_CANCEL), "Abbrechen") &&
|
||||
!strcmp(ui_text_in("en", UI_SWKBD_OK), "OK"), "keyboard texts changed");
|
||||
// language names: the same string in every row
|
||||
for (int id = UI_MENU_LANGNAME_EN; id <= UI_MENU_LANGNAME_KO; id++)
|
||||
for (int id = UI_MENU_LANGNAME_EN; id <= UI_MENU_LANGNAME_PT_BR; id++)
|
||||
for (size_t i = 1; i < sizeof langs / sizeof *langs; i++)
|
||||
CHECK(!strcmp(ui_text_in(langs[i], (UiTextId)id), ui_text_in("en", (UiTextId)id)), "language name %d differs in %s", id, langs[i]);
|
||||
CHECK(!strcmp(ui_text_in("en", UI_MENU_LANGNAME_RU), "Русский"), "Russian is not shown as Russian");
|
||||
// the overlay formats keep their conversions
|
||||
CHECK(strstr(ui_text_in("ru", UI_MENU_OV_GAME_PCT), "%.0f%%") && strstr(ui_text_in("ru", UI_MENU_OV_MAX_MS), "%.1f"),
|
||||
"overlay format lost its conversion");
|
||||
CHECK(strstr(ui_text_in("pt-BR", UI_MENU_OV_GAME_PCT), "%.0f%%") &&
|
||||
strstr(ui_text_in("pt-BR", UI_MENU_OV_MAX_MS), "%.1f"), "pt-BR overlay format lost its conversion");
|
||||
// a Brazilian game language gets the Brazilian keyboard row, Portugal's Portuguese keeps its own
|
||||
CHECK(!strcmp(ui_text_lang("pt-BR"), "pt-BR") && !strcmp(ui_text_lang("pt"), "pt") &&
|
||||
strcmp(ui_text_in("pt-BR", UI_SWKBD_PROMPT), ui_text_in("pt", UI_SWKBD_PROMPT)), "pt-BR keyboard row not used");
|
||||
// menu_text follows the setting live
|
||||
cfg_set("TOTK_MENU_LANG", "ru");
|
||||
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("ru", UI_MENU_CONTINUE), "menu_text ignores MENU_LANG=ru");
|
||||
cfg_set("TOTK_MENU_LANG", "pt-BR");
|
||||
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("pt-BR", UI_MENU_CONTINUE), "menu_text ignores MENU_LANG=pt-BR");
|
||||
cfg_set("TOTK_MENU_LANG", "en");
|
||||
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("en", UI_MENU_CONTINUE), "menu_text did not follow MENU_LANG=en");
|
||||
cfg_set("TOTK_MENU_LANG", "zh-Hans");
|
||||
CHECK(menu_text(UI_MENU_TITLE) == ui_text_in("zh-Hans", UI_MENU_TITLE), "menu_text ignores MENU_LANG=zh-Hans");
|
||||
cfg_set("TOTK_MENU_LANG", "");
|
||||
const char *sys = plat_ui_language();
|
||||
int ru = !strncmp(sys, "ru", 2);
|
||||
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(ru ? "ru" : "en", UI_MENU_CONTINUE), "menu_text auto ignores the system language");
|
||||
const char *sys_lang = ui_text_lang(plat_ui_language());
|
||||
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang : "en";
|
||||
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(auto_lang, UI_MENU_CONTINUE), "menu_text auto ignores the system language");
|
||||
}
|
||||
|
||||
static void probe_main(Cpu *c) {
|
||||
|
||||
+112
-13
@@ -19,6 +19,7 @@
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "amiibo.h"
|
||||
#include "cfg.h"
|
||||
#include "fs.h"
|
||||
#include "host.h"
|
||||
@@ -266,26 +267,124 @@ static void ov_report_save(Cpu *c) { // Result PlayReport::Save() and Save(con
|
||||
}
|
||||
static void ov_result_ok(Cpu *c) { ret_x(c, 0); } // RequestImmediateTransmission, AddInternetConnectionStatus
|
||||
|
||||
// nn::nfp: no NFC reader
|
||||
// nn::nfp: NFC reader & amiibo support
|
||||
static volatile int32_t nfp_state;
|
||||
static void ov_nfp_initialize(Cpu *c) { plat_xchg32(&nfp_state, 1); ret_x(c, 0); }
|
||||
static void ov_nfp_finalize(Cpu *c) { plat_xchg32(&nfp_state, 0); ret_x(c, 0); }
|
||||
static void ov_nfp_finalize(Cpu *c) {
|
||||
plat_xchg32(&nfp_state, 0);
|
||||
amiibo_stop_detection();
|
||||
amiibo_unmount();
|
||||
ret_x(c, 0);
|
||||
}
|
||||
static void ov_nfp_state(Cpu *c) { ret_x(c, (uint64_t)nfp_state); }
|
||||
|
||||
// Result ListDevices(DeviceHandle *out, int *count, int max)
|
||||
static void ov_nfp_list(Cpu *c) {
|
||||
void *out = arg_p(c, 0);
|
||||
int32_t *count = arg_p(c, 1);
|
||||
if (count) *count = 0;
|
||||
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
|
||||
int32_t max = (int32_t)arg_x(c, 2);
|
||||
if (!amiibo_is_device_present()) {
|
||||
if (count) *count = 0;
|
||||
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
if (out && max > 0) {
|
||||
static const uint8_t dev[8] = { 1, 0, 0, 0, 0, 0, 0, 0 };
|
||||
memcpy(out, dev, sizeof dev);
|
||||
}
|
||||
if (count) *count = 1;
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// Result AttachAvailabilityChangeEvent(os::SystemEventType *event)
|
||||
static void ov_nfp_availability(Cpu *c) {
|
||||
uint64_t ev = arg_x(c, 0);
|
||||
if (!ev) rt_fatal("nn::nfp::AttachAvailabilityChangeEvent: null event");
|
||||
kern_bind_system_event(c, ev, 0);
|
||||
kern_signal_system_event(ev);
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// DeviceState GetDeviceState(const DeviceHandle &)
|
||||
static void ov_nfp_device_state(Cpu *c) {
|
||||
(void)c;
|
||||
ret_x(c, (uint64_t)amiibo_get_device_state());
|
||||
}
|
||||
|
||||
// Result Mount(const DeviceHandle &, ModelType, MountTarget)
|
||||
static void ov_nfp_mount(Cpu *c) {
|
||||
(void)c;
|
||||
if (!amiibo_has_tag()) {
|
||||
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
amiibo_mount();
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// Result GetNpadId(u32 *npad_id, const DeviceHandle &)
|
||||
static void ov_nfp_get_npad_id(Cpu *c) {
|
||||
uint32_t *npad = arg_p(c, 0);
|
||||
if (npad) *npad = 0; // Handheld / Player 1
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// Result AttachActivateEvent(os::SystemEventType *event, const DeviceHandle &)
|
||||
static void ov_nfp_attach_activate(Cpu *c) {
|
||||
uint64_t ev = arg_x(c, 0);
|
||||
if (ev) {
|
||||
kern_bind_system_event(c, ev, 0);
|
||||
if (amiibo_has_tag()) kern_signal_system_event(ev);
|
||||
}
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// Result AttachDeactivateEvent(os::SystemEventType *event, const DeviceHandle &)
|
||||
static void ov_nfp_attach_deactivate(Cpu *c) {
|
||||
uint64_t ev = arg_x(c, 0);
|
||||
if (ev) {
|
||||
kern_bind_system_event(c, ev, 0);
|
||||
}
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// Result StartDetection(const DeviceHandle &)
|
||||
static void ov_nfp_start_detection(Cpu *c) {
|
||||
(void)c;
|
||||
amiibo_start_detection();
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// Result StopDetection(const DeviceHandle &)
|
||||
static void ov_nfp_stop_detection(Cpu *c) {
|
||||
(void)c;
|
||||
amiibo_stop_detection();
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// Result GetTagInfo(TagInfo *out, const DeviceHandle &)
|
||||
static void ov_nfp_get_tag_info(Cpu *c) {
|
||||
void *out = arg_p(c, 0);
|
||||
if (!amiibo_has_tag() && !amiibo_is_mounted()) {
|
||||
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
if (out) amiibo_get_tag_info(out);
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
// Result GetModelInfo(ModelInfo *out, const DeviceHandle &)
|
||||
static void ov_nfp_get_model_info(Cpu *c) {
|
||||
void *out = arg_p(c, 0);
|
||||
if (!amiibo_is_mounted() && !amiibo_has_tag()) {
|
||||
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
if (out) amiibo_get_model_info(out);
|
||||
ret_x(c, 0);
|
||||
}
|
||||
|
||||
static void ov_nfp_no_device(Cpu *c) { ret_x(c, NFP_R_DEVICE_NOT_FOUND); }
|
||||
static void ov_nfp_device_state(Cpu *c) { ret_x(c, 0); } // DeviceState GetDeviceState(const DeviceHandle &): none
|
||||
|
||||
// void err::ShowError(Result): the game's error dialog, logged
|
||||
static void ov_show_error(Cpu *c) {
|
||||
@@ -795,14 +894,14 @@ HOST_OVERRIDE("_ZN2nn3nfp8GetStateEv", ov_nfp_state);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp11ListDevicesEPNS0_12DeviceHandleEPii", ov_nfp_list);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp29AttachAvailabilityChangeEventEPNS_2os15SystemEventTypeE", ov_nfp_availability);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp14GetDeviceStateERKNS0_12DeviceHandleE", ov_nfp_device_state);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp5MountERKNS0_12DeviceHandleENS0_9ModelTypeENS0_11MountTargetE", ov_nfp_no_device);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp9GetNpadIdEPjRKNS0_12DeviceHandleE", ov_nfp_no_device);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp19AttachActivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", ov_nfp_no_device);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp21AttachDeactivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", ov_nfp_no_device);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp14StartDetectionERKNS0_12DeviceHandleE", ov_nfp_no_device);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp13StopDetectionERKNS0_12DeviceHandleE", ov_nfp_no_device);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp10GetTagInfoEPNS0_7TagInfoERKNS0_12DeviceHandleE", ov_nfp_no_device);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp12GetModelInfoEPNS0_9ModelInfoERKNS0_12DeviceHandleE", ov_nfp_no_device);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp5MountERKNS0_12DeviceHandleENS0_9ModelTypeENS0_11MountTargetE", ov_nfp_mount);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp9GetNpadIdEPjRKNS0_12DeviceHandleE", ov_nfp_get_npad_id);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp19AttachActivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", ov_nfp_attach_activate);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp21AttachDeactivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", ov_nfp_attach_deactivate);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp14StartDetectionERKNS0_12DeviceHandleE", ov_nfp_start_detection);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp13StopDetectionERKNS0_12DeviceHandleE", ov_nfp_stop_detection);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp10GetTagInfoEPNS0_7TagInfoERKNS0_12DeviceHandleE", ov_nfp_get_tag_info);
|
||||
HOST_OVERRIDE("_ZN2nn3nfp12GetModelInfoEPNS0_9ModelInfoERKNS0_12DeviceHandleE", ov_nfp_get_model_info);
|
||||
HOST_OVERRIDE("_ZN2nn3err9ShowErrorENS_6ResultE", ov_show_error);
|
||||
|
||||
HOST_OVERRIDE("nndiagStartup", ov_diag_startup);
|
||||
|
||||
@@ -183,6 +183,9 @@ static uint64_t ft_frame(uint64_t frame, uint64_t now, uint64_t d) {
|
||||
// process memory, and which side holds the frame back. pace: the frame waited for pacing (vsync, the frame limiter) at
|
||||
// least 15% of the window; gpu: the GPU was busy for 80% of the frame interval or more (without timestamps: threads
|
||||
// waited for fences for half the interval or more); cpu otherwise.
|
||||
// With calibrated timestamps a third line splits the GPU's idle time between timed parts: the part was submitted only
|
||||
// after the previous one had ended (the GPU waited for the CPU's replay or the game), or it was already queued (untimed
|
||||
// work such as uploads and presents ran there, or a wait held it).
|
||||
static void perf_gpu_line(uint64_t frame, double interval_ms, double pace_share, uint64_t n) {
|
||||
PerfGpu g;
|
||||
memset(&g, 0, sizeof g);
|
||||
@@ -205,6 +208,11 @@ static void perf_gpu_line(uint64_t frame, double interval_ms, double pace_share,
|
||||
rt_log("perf: frame %llu: %s; fence wait per frame: render thread %.2f ms, other threads %.2f ms; %s; ram working set "
|
||||
"%.0f MiB, private %.0f MiB; bound: %s",
|
||||
(unsigned long long)frame, gs, rw, ow, vs, ws, priv, bound);
|
||||
if (g.have_gpu && g.have_idle)
|
||||
rt_log("perf: frame %llu: gpu idle per frame %.2f ms between timed parts: %.2f ms waiting for the next submission, "
|
||||
"%.2f ms with it queued (untimed work or waits); %.1f gaps over 0.5 ms, longest %.2f ms; submit to start avg %.2f ms",
|
||||
(unsigned long long)frame, g.idle_ns / 1e6 / n, g.starved_ns / 1e6 / n, (g.idle_ns - g.starved_ns) / 1e6 / n,
|
||||
(double)g.idle_gaps / n, g.max_gap_ns / 1e6, g.gpu_parts ? g.queued_ns / 1e6 / g.gpu_parts : 0.0);
|
||||
}
|
||||
|
||||
int rt_ab(void) {
|
||||
|
||||
+117
-2
@@ -9,6 +9,7 @@
|
||||
#include <linux/fs.h>
|
||||
#include <linux/futex.h>
|
||||
#include <linux/userfaultfd.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <signal.h>
|
||||
@@ -1180,8 +1181,10 @@ size_t plat_getenv(const char *name, char *out, size_t cap) {
|
||||
|
||||
// Cached per thread: plat_rmutex_lock asks on every acquire, hundreds of thousands of times a second in play, and
|
||||
// gettid is a full syscall on Linux (Windows reads GetCurrentThreadId from the TEB). A thread's id never changes, and
|
||||
// the runtime starts child processes with posix_spawn only, so no forked copy inherits a stale id.
|
||||
uint32_t plat_thread_id(void) {
|
||||
// the runtime starts child processes with posix_spawn only, so no forked copy inherits a stale id. noinline: a fiber
|
||||
// may resume on another thread (platform.h), and a caller that inlined this could keep the address of tid it computed
|
||||
// before a plat_fiber_switch; as a call it reads the slot of the thread it runs on.
|
||||
__attribute__((noinline)) uint32_t plat_thread_id(void) {
|
||||
static __thread uint32_t tid;
|
||||
if (!tid) tid = (uint32_t)syscall(SYS_gettid);
|
||||
return tid;
|
||||
@@ -1247,6 +1250,8 @@ int plat_os_version(char *out, size_t cap) {
|
||||
return snprintf(out, cap, "%s (Linux %s)", name, u.release) < (int)cap;
|
||||
}
|
||||
|
||||
int plat_is_wine(void) { return 0; }
|
||||
|
||||
int plat_stderr_path(char *out, size_t cap) {
|
||||
struct stat st;
|
||||
if (fstat(2, &st) || !S_ISREG(st.st_mode)) return 0;
|
||||
@@ -1648,3 +1653,113 @@ long plat_child_read(PlatChild *c, void *buf, size_t cap) {
|
||||
while (k < 0 && errno == EINTR);
|
||||
return (long)k;
|
||||
}
|
||||
|
||||
// ---- restart
|
||||
// TOTK_RESTART_WAIT names the process that called plat_restart, as its id and start time (field 22 of /proc/<pid>/stat,
|
||||
// clock ticks after boot): a process that has only reused the id is not waited for. Waiting keeps the new process off
|
||||
// the old one's files (the log it moves at start, the saves) until the old one is gone. The new process gets no
|
||||
// descriptor past stderr (glibc 2.34's closefrom file action), and the default signal mask and dispositions, whatever
|
||||
// the calling thread had.
|
||||
#define RESTART_WAIT "TOTK_RESTART_WAIT"
|
||||
|
||||
// The start time of process pid (field 22 of its /proc stat line), 0 when it is gone.
|
||||
static unsigned long long process_start(int pid) {
|
||||
char path[64], buf[1024];
|
||||
snprintf(path, sizeof path, "/proc/%d/stat", pid);
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return 0;
|
||||
size_t n = fread(buf, 1, sizeof buf - 1, f);
|
||||
fclose(f);
|
||||
buf[n] = 0;
|
||||
char *p = strrchr(buf, ')'); // the command name may hold spaces
|
||||
unsigned long long start = 0;
|
||||
if (!p || sscanf(p + 2, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu",
|
||||
&start) != 1)
|
||||
return 0;
|
||||
return start;
|
||||
}
|
||||
|
||||
// glibc 2.34 added posix_spawn_file_actions_addclosefrom_np, and the release links against the Steam Runtime's 2.31: a
|
||||
// weak reference, used when the running glibc has it. Without it the child inherits the open descriptors.
|
||||
extern int posix_spawn_file_actions_addclosefrom_np(posix_spawn_file_actions_t *fa, int from) __attribute__((weak));
|
||||
|
||||
static void spawn_close_from(posix_spawn_file_actions_t *fa, int from) {
|
||||
if (posix_spawn_file_actions_addclosefrom_np) posix_spawn_file_actions_addclosefrom_np(fa, from);
|
||||
}
|
||||
|
||||
int plat_restart(void) {
|
||||
// the arguments, NUL-separated, from /proc/self/cmdline
|
||||
static char args[16384];
|
||||
char *argv[256];
|
||||
int argc = 0;
|
||||
FILE *f = fopen("/proc/self/cmdline", "rb");
|
||||
size_t n = f ? fread(args, 1, sizeof args - 1, f) : 0;
|
||||
if (f) fclose(f);
|
||||
args[n] = 0;
|
||||
for (size_t i = 0; i < n && argc < 255; i += strlen(args + i) + 1) argv[argc++] = args + i;
|
||||
argv[argc] = NULL;
|
||||
char exe[PATH_MAX], wait[64];
|
||||
if (!argc || !plat_exe_path(exe, sizeof exe)) return 0;
|
||||
snprintf(wait, sizeof wait, "%d.%llu", (int)getpid(), process_start((int)getpid()));
|
||||
posix_spawn_file_actions_t fa;
|
||||
posix_spawn_file_actions_init(&fa);
|
||||
spawn_close_from(&fa, 3);
|
||||
posix_spawnattr_t at;
|
||||
posix_spawnattr_init(&at);
|
||||
sigset_t none, all;
|
||||
sigemptyset(&none);
|
||||
sigfillset(&all);
|
||||
posix_spawnattr_setsigmask(&at, &none);
|
||||
posix_spawnattr_setsigdefault(&at, &all);
|
||||
posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF);
|
||||
setenv(RESTART_WAIT, wait, 1);
|
||||
pid_t pid;
|
||||
int e = posix_spawn(&pid, exe, &fa, &at, argv, environ);
|
||||
unsetenv(RESTART_WAIT);
|
||||
posix_spawnattr_destroy(&at);
|
||||
posix_spawn_file_actions_destroy(&fa);
|
||||
return e == 0;
|
||||
}
|
||||
|
||||
int plat_spawn(const char *const *argv, const char *cwd) {
|
||||
posix_spawn_file_actions_t fa;
|
||||
posix_spawn_file_actions_init(&fa);
|
||||
spawn_close_from(&fa, 3);
|
||||
posix_spawn_file_actions_addchdir_np(&fa, cwd);
|
||||
posix_spawnattr_t at;
|
||||
posix_spawnattr_init(&at);
|
||||
sigset_t none, all;
|
||||
sigemptyset(&none);
|
||||
sigfillset(&all);
|
||||
posix_spawnattr_setsigmask(&at, &none);
|
||||
posix_spawnattr_setsigdefault(&at, &all);
|
||||
posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSID);
|
||||
pid_t pid;
|
||||
int e = posix_spawn(&pid, argv[0], &fa, &at, (char *const *)argv, environ);
|
||||
posix_spawnattr_destroy(&at);
|
||||
posix_spawn_file_actions_destroy(&fa);
|
||||
return e == 0;
|
||||
}
|
||||
|
||||
int plat_restart_wait(uint32_t timeout_ms) {
|
||||
const char *v = getenv(RESTART_WAIT);
|
||||
if (!v) return 0;
|
||||
int pid = 0;
|
||||
unsigned long long start = 0;
|
||||
int ok = sscanf(v, "%d.%llu", &pid, &start) == 2 && pid > 0 && start;
|
||||
unsetenv(RESTART_WAIT); // the processes this one starts do not wait again
|
||||
if (!ok) return 0;
|
||||
// The descriptor is taken first: a start time that still matches then proves it is that process, not a new one with
|
||||
// the id. It reads as ready once the process has exited, reaped or not.
|
||||
int fd = (int)syscall(SYS_pidfd_open, pid, 0);
|
||||
if (fd < 0) return 0; // already gone
|
||||
int r = 0;
|
||||
if (process_start(pid) == start) {
|
||||
struct pollfd p = {fd, POLLIN, 0};
|
||||
do r = poll(&p, 1, (int)timeout_ms);
|
||||
while (r < 0 && errno == EINTR);
|
||||
r = r > 0 ? 1 : -1;
|
||||
}
|
||||
close(fd);
|
||||
return r;
|
||||
}
|
||||
@@ -61,6 +61,23 @@ void plat_fiber_unself(void); // the calling thread stops being a fi
|
||||
PlatFiber *plat_fiber_new(size_t stack, void (*fn)(void *arg), void *arg);
|
||||
void plat_fiber_switch(PlatFiber *to); // from the fiber the calling thread runs
|
||||
void plat_fiber_free(PlatFiber *f); // a fiber nobody runs
|
||||
|
||||
// A jump back to a point saved earlier on the same thread, without unwinding the frames in between (no SEH, no C++
|
||||
// cleanups): the kernel's ExitThread leaves the lifted frames this way. plat_jmp_save returns 0 when it saves and 1
|
||||
// when plat_jmp_go comes back to it; the function that called it must still be running. It keeps the callee-saved
|
||||
// registers and the floating-point control state. On Windows this is not the C runtime's setjmp: a non-unwinding
|
||||
// setjmp leaves the buffer's frame 0, and the C runtime's longjmp ends the process with 0xc0000409 (fast fail 13) on
|
||||
// such a buffer when Control Flow Guard is on for the process, which Exploit protection can force on any program.
|
||||
#ifdef _WIN32
|
||||
typedef struct { uint64_t gpr[10]; uint32_t mxcsr, fpcw; uint64_t pad; uint8_t xmm[160]; } PlatJmp;
|
||||
int plat_jmp_save(PlatJmp *j) __attribute__((returns_twice, nothrow));
|
||||
_Noreturn void plat_jmp_go(PlatJmp *j);
|
||||
#else
|
||||
#include <setjmp.h>
|
||||
typedef struct { jmp_buf b; } PlatJmp;
|
||||
#define plat_jmp_save(j) setjmp((j)->b)
|
||||
#define plat_jmp_go(j) longjmp((j)->b, 1)
|
||||
#endif
|
||||
// Scheduling priority, Windows' seven levels: of the calling thread, or of t. On Linux a thread lowered without the
|
||||
// right to raise its priority again (CAP_SYS_NICE, or an RLIMIT_NICE of 20) could never return to normal, and the
|
||||
// runtime lowers threads that later take shared locks at normal priority; without that right every change is skipped
|
||||
@@ -350,6 +367,7 @@ uint32_t plat_thread_id(void); // id of the calling t
|
||||
int plat_font_path(const char *lang, char *out, size_t cap);
|
||||
// The operating system and its build ("Windows 10.0.28000") into out; 0 when unknown.
|
||||
int plat_os_version(char *out, size_t cap);
|
||||
int plat_is_wine(void); // 1 when the Windows build runs under Wine; 0 elsewhere
|
||||
// The file the process's stderr (the log) goes to, as a UTF-8 path; 0 when stderr is not a file (a console, a pipe).
|
||||
int plat_stderr_path(char *out, size_t cap);
|
||||
|
||||
@@ -393,3 +411,14 @@ PlatChild *plat_child_start(const char *exe, const char *arg, uint32_t *err);
|
||||
void plat_child_stop(PlatChild *c); // kills the child and frees c
|
||||
int plat_child_write(PlatChild *c, const void *data, size_t n); // 1 when all n bytes were written
|
||||
long plat_child_read(PlatChild *c, void *buf, size_t cap); // bytes read (at least 1); 0 or negative: pipe broke
|
||||
|
||||
// ---- restart: this program again, in a new process that starts once this one is gone
|
||||
// Starts this executable again with the same command line, environment and working folder, and no handle of this
|
||||
// process. 1 when it started; the caller then exits.
|
||||
int plat_restart(void);
|
||||
// Starts argv[0] with the arguments argv[1..] (NULL-terminated, UTF-8) in folder cwd, detached from this process: no
|
||||
// console, no handle of this one. 1 when it started (the setup window that rebuilds the game, runtime/cheats.c).
|
||||
int plat_spawn(const char *const *argv, const char *cwd);
|
||||
// In a process plat_restart started: waits, at most timeout_ms, until the process that started it has exited. 1 after
|
||||
// the wait, -1 on timeout, 0 when there is nothing to wait for (not started by plat_restart, or that process is gone).
|
||||
int plat_restart_wait(uint32_t timeout_ms);
|
||||
@@ -199,6 +199,69 @@ void plat_fiber_free(PlatFiber *f) {
|
||||
free(f);
|
||||
}
|
||||
|
||||
// plat_jmp_save / plat_jmp_go: PlatJmp holds rbx, rbp, rdi, rsi, r12-r15, the caller's rsp and the return address at
|
||||
// 0..72, MXCSR at 80, the x87 control word at 84 and xmm6-xmm15 from 96, the Win64 callee-saved state. Both are leaf
|
||||
// code that does not move rsp before it leaves, so they need no unwind data.
|
||||
_Static_assert(sizeof(PlatJmp) == 256 && __builtin_offsetof(PlatJmp, xmm) == 96, "plat_jmp_save layout");
|
||||
__asm__(".text\n"
|
||||
".globl plat_jmp_save\n"
|
||||
".def plat_jmp_save; .scl 2; .type 32; .endef\n"
|
||||
".p2align 4\n"
|
||||
"plat_jmp_save:\n"
|
||||
" movq %rbx, 0(%rcx)\n"
|
||||
" movq %rbp, 8(%rcx)\n"
|
||||
" movq %rdi, 16(%rcx)\n"
|
||||
" movq %rsi, 24(%rcx)\n"
|
||||
" movq %r12, 32(%rcx)\n"
|
||||
" movq %r13, 40(%rcx)\n"
|
||||
" movq %r14, 48(%rcx)\n"
|
||||
" movq %r15, 56(%rcx)\n"
|
||||
" leaq 8(%rsp), %rdx\n" // the caller's rsp after this call returns
|
||||
" movq %rdx, 64(%rcx)\n"
|
||||
" movq (%rsp), %rdx\n"
|
||||
" movq %rdx, 72(%rcx)\n"
|
||||
" stmxcsr 80(%rcx)\n"
|
||||
" fnstcw 84(%rcx)\n"
|
||||
" movdqu %xmm6, 96(%rcx)\n"
|
||||
" movdqu %xmm7, 112(%rcx)\n"
|
||||
" movdqu %xmm8, 128(%rcx)\n"
|
||||
" movdqu %xmm9, 144(%rcx)\n"
|
||||
" movdqu %xmm10, 160(%rcx)\n"
|
||||
" movdqu %xmm11, 176(%rcx)\n"
|
||||
" movdqu %xmm12, 192(%rcx)\n"
|
||||
" movdqu %xmm13, 208(%rcx)\n"
|
||||
" movdqu %xmm14, 224(%rcx)\n"
|
||||
" movdqu %xmm15, 240(%rcx)\n"
|
||||
" xorl %eax, %eax\n"
|
||||
" ret\n"
|
||||
".globl plat_jmp_go\n"
|
||||
".def plat_jmp_go; .scl 2; .type 32; .endef\n"
|
||||
".p2align 4\n"
|
||||
"plat_jmp_go:\n"
|
||||
" movq 0(%rcx), %rbx\n"
|
||||
" movq 8(%rcx), %rbp\n"
|
||||
" movq 16(%rcx), %rdi\n"
|
||||
" movq 24(%rcx), %rsi\n"
|
||||
" movq 32(%rcx), %r12\n"
|
||||
" movq 40(%rcx), %r13\n"
|
||||
" movq 48(%rcx), %r14\n"
|
||||
" movq 56(%rcx), %r15\n"
|
||||
" ldmxcsr 80(%rcx)\n"
|
||||
" fldcw 84(%rcx)\n"
|
||||
" movdqu 96(%rcx), %xmm6\n"
|
||||
" movdqu 112(%rcx), %xmm7\n"
|
||||
" movdqu 128(%rcx), %xmm8\n"
|
||||
" movdqu 144(%rcx), %xmm9\n"
|
||||
" movdqu 160(%rcx), %xmm10\n"
|
||||
" movdqu 176(%rcx), %xmm11\n"
|
||||
" movdqu 192(%rcx), %xmm12\n"
|
||||
" movdqu 208(%rcx), %xmm13\n"
|
||||
" movdqu 224(%rcx), %xmm14\n"
|
||||
" movdqu 240(%rcx), %xmm15\n"
|
||||
" movq 64(%rcx), %rsp\n"
|
||||
" movl $1, %eax\n"
|
||||
" jmpq *72(%rcx)\n");
|
||||
|
||||
int plat_thread_spawn(PlatThreadFn fn, void *arg, const char *name, size_t stack) {
|
||||
PlatThread *t = plat_thread_new_ex(fn, arg, name, stack);
|
||||
if (t) plat_thread_detach(t);
|
||||
@@ -1128,6 +1191,11 @@ int plat_os_version(char *out, size_t cap) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int plat_is_wine(void) {
|
||||
HMODULE nt = GetModuleHandleW(L"ntdll.dll");
|
||||
return nt && GetProcAddress(nt, "wine_get_version") != NULL;
|
||||
}
|
||||
|
||||
int plat_stderr_path(char *out, size_t cap) {
|
||||
HANDLE h = GetStdHandle(STD_ERROR_HANDLE);
|
||||
wchar_t w[WPATH];
|
||||
@@ -1458,3 +1526,89 @@ long plat_child_read(PlatChild *c, void *buf, size_t cap) {
|
||||
if (!ReadFile(c->out, buf, (DWORD)cap, &got, NULL)) return -1;
|
||||
return (long)got;
|
||||
}
|
||||
|
||||
// ---- restart
|
||||
// TOTK_RESTART_WAIT names the process that called plat_restart, as its id and creation time: a process that has only
|
||||
// reused the id is not waited for. Waiting keeps the new process off the old one's files (the log it moves at start,
|
||||
// the saves) until the old one is gone; no handle is inherited, so none of them stays open in the new one either.
|
||||
#define RESTART_WAIT "TOTK_RESTART_WAIT"
|
||||
|
||||
static uint64_t process_created(HANDLE h) {
|
||||
FILETIME c, e, k, u;
|
||||
return GetProcessTimes(h, &c, &e, &k, &u) ? ft64(c) : 0;
|
||||
}
|
||||
|
||||
int plat_restart(void) {
|
||||
char wait[48];
|
||||
snprintf(wait, sizeof wait, "%lu.%llu", GetCurrentProcessId(), (unsigned long long)process_created(GetCurrentProcess()));
|
||||
plat_setenv(RESTART_WAIT, wait);
|
||||
STARTUPINFOW si = {.cb = sizeof si};
|
||||
PROCESS_INFORMATION pi;
|
||||
wchar_t *cmd = _wcsdup(GetCommandLineW()); // CreateProcessW may write its command line
|
||||
BOOL ok = cmd && CreateProcessW(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
|
||||
free(cmd);
|
||||
plat_setenv(RESTART_WAIT, NULL);
|
||||
if (!ok) return 0;
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// One argument of a command line as CommandLineToArgvW reads it back: in quotes, a backslash run doubled before a quote
|
||||
// and at the end, a quote escaped.
|
||||
static size_t quote_arg(const wchar_t *a, wchar_t *out, size_t cap, size_t at) {
|
||||
#define PUT(c) do { if (at + 1 < cap) out[at] = (c); at++; } while (0)
|
||||
PUT(L'"');
|
||||
for (const wchar_t *p = a;; p++) {
|
||||
size_t slashes = 0;
|
||||
while (*p == L'\\') p++, slashes++;
|
||||
size_t times = !*p || *p == L'"' ? 2 : 1;
|
||||
for (size_t k = 0; k < slashes * times; k++) PUT(L'\\');
|
||||
if (!*p) break;
|
||||
if (*p == L'"') PUT(L'\\');
|
||||
PUT(*p);
|
||||
}
|
||||
PUT(L'"');
|
||||
PUT(L' ');
|
||||
#undef PUT
|
||||
return at;
|
||||
}
|
||||
|
||||
int plat_spawn(const char *const *argv, const char *cwd) {
|
||||
enum { CMD = 32768 };
|
||||
wchar_t *cmd = calloc(CMD, sizeof *cmd), *arg = malloc(WPATH * sizeof *arg), *dir = malloc(WPATH * sizeof *dir);
|
||||
int ok = cmd && arg && dir && argv[0] && widen(cwd, dir, WPATH);
|
||||
size_t at = 0;
|
||||
for (int i = 0; ok && argv[i]; i++) {
|
||||
ok = widen(argv[i], arg, WPATH);
|
||||
if (ok) at = quote_arg(arg, cmd, CMD, at);
|
||||
}
|
||||
ok = ok && at < CMD && at > 0;
|
||||
if (ok) cmd[at - 1] = 0; // the last separator
|
||||
STARTUPINFOW si = {.cb = sizeof si};
|
||||
PROCESS_INFORMATION pi;
|
||||
ok = ok && CreateProcessW(NULL, cmd, NULL, NULL, FALSE, DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, NULL, dir, &si,
|
||||
&pi);
|
||||
if (ok) {
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
free(cmd);
|
||||
free(arg);
|
||||
free(dir);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int plat_restart_wait(uint32_t timeout_ms) {
|
||||
char v[48];
|
||||
if (!plat_getenv(RESTART_WAIT, v, sizeof v)) return 0;
|
||||
plat_setenv(RESTART_WAIT, NULL); // the processes this one starts do not wait again
|
||||
unsigned long pid = 0;
|
||||
unsigned long long created = 0;
|
||||
if (sscanf(v, "%lu.%llu", &pid, &created) != 2) return 0;
|
||||
HANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||
if (!h) return 0; // already gone
|
||||
int r = process_created(h) != created ? 0 : WaitForSingleObject(h, timeout_ms) == WAIT_OBJECT_0 ? 1 : -1;
|
||||
CloseHandle(h);
|
||||
return r;
|
||||
}
|
||||
+74
-4
@@ -43,6 +43,7 @@
|
||||
#define SCENE_H 900
|
||||
#define DTA_KIB 113664u // DynamicTextureAllocatorMem, shipped
|
||||
#define SCENE_MAX 4096 // per side, as MacNativeGames' TOTK_RENDER_RES
|
||||
#define SHADOW_BASE 960 // the sun shadow map's base size (its cascades: a D16 array of 4 layers)
|
||||
|
||||
static struct {
|
||||
int decided;
|
||||
@@ -50,6 +51,8 @@ static struct {
|
||||
int scene_set; // the scene differs from the game's 5/6 of the output
|
||||
int heapdef_ok; // romfs.c serves the grown HeapDef
|
||||
int on; // res_init switched the patch sets on
|
||||
unsigned shadow; // fixed sun shadow map size (TOTK_SHADOW_RES), 0: the game's own
|
||||
double dlss_k; // TOTK_DLSS quality/balanced/performance: the scene's share of the output, else 0
|
||||
} R;
|
||||
|
||||
static void decide(void) {
|
||||
@@ -68,7 +71,16 @@ static void decide(void) {
|
||||
}
|
||||
const char *s = cfg_value("TOTK_RENDER_SCALE");
|
||||
double k = 5.0 / 6.0;
|
||||
if (strcmp(s, "game")) {
|
||||
const char *m = cfg_value("TOTK_DLSS"); // a DLSS mode other than dlaa sets the scene size
|
||||
static const struct { const char *name; double k; } dlss[] = {
|
||||
{"quality", 2.0 / 3.0}, {"balanced", 0.58}, {"performance", 0.5}};
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (!strcmp(m, dlss[i].name)) R.dlss_k = dlss[i].k;
|
||||
if (R.dlss_k) {
|
||||
k = R.dlss_k;
|
||||
R.scene_set = 1;
|
||||
rt_log("resolution: DLSS %s: the scene at %.0f %% of the output (render_scale ignored)", m, k * 100);
|
||||
} else if (strcmp(s, "game")) {
|
||||
char *end;
|
||||
double pct = strtod(s, &end);
|
||||
if (end != s && !*end && pct >= 25 && pct <= 400) {
|
||||
@@ -83,19 +95,31 @@ static void decide(void) {
|
||||
if (fit < 1.0) R.scene_set = 1;
|
||||
R.sw = (unsigned)(sw * fit + 0.5);
|
||||
R.sh = (unsigned)(sh * fit + 0.5);
|
||||
const char *sr = cfg_value("TOTK_SHADOW_RES");
|
||||
if (strcmp(sr, "game")) {
|
||||
unsigned n = (unsigned)atoi(sr);
|
||||
if (n >= 256 && n <= 4096 && !(n & 7)) R.shadow = n;
|
||||
else rt_log("resolution: TOTK_SHADOW_RES=%s is not game or a size 256..4096 (a multiple of 8); game", sr);
|
||||
}
|
||||
}
|
||||
|
||||
// Texture heap for a sun shadow map above the base size: the four layers of 2 bytes, three maps in flight (the engine
|
||||
// makes the array anew each frame).
|
||||
static uint64_t shadow_kib(void) {
|
||||
return R.shadow > SHADOW_BASE ? ((uint64_t)R.shadow * R.shadow - SHADOW_BASE * SHADOW_BASE) * 8 * 3 / 1024 : 0;
|
||||
}
|
||||
|
||||
static int bigger(void) {
|
||||
decide();
|
||||
return R.ow != BASE_W || R.oh != BASE_H || R.scene_set;
|
||||
return R.ow != BASE_W || R.oh != BASE_H || R.scene_set || shadow_kib();
|
||||
}
|
||||
|
||||
uint64_t res_heap_extra_kib(void) {
|
||||
if (!bigger()) return 0;
|
||||
double grow = fmax(0.0, (double)R.ow * R.oh / (BASE_W * BASE_H) - 1.0) +
|
||||
fmax(0.0, (double)R.sw * R.sh / (SCENE_W * SCENE_H) - 1.0);
|
||||
if (grow <= 0) return 0;
|
||||
return (uint64_t)ceil(DTA_KIB * (1.0 + grow) / 1024.0) * 1024 - DTA_KIB;
|
||||
uint64_t kib = grow > 0 ? (uint64_t)ceil(DTA_KIB * (1.0 + grow) / 1024.0) * 1024 - DTA_KIB : 0;
|
||||
return kib + ((shadow_kib() + 1023) & ~1023ull); // whole MiB
|
||||
}
|
||||
|
||||
uint64_t res_heap_extra_bytes(void) { return ((res_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
|
||||
@@ -190,12 +214,50 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
||||
|
||||
void res_heapdef_served(void) { R.heapdef_ok = 1; }
|
||||
|
||||
// ---- sun shadow map size: each frame the engine sizes its shadow cascades to the area they cover, down to about 400 near
|
||||
// the ground, which draws foliage shadows blocky, and each change of size moves the shadow texels, so leaf shadows jump.
|
||||
// A fixed size (the shadow set, recomp/patches-v121.txt) keeps them still and sharper. Page +92.
|
||||
static void shadow_init(uint8_t *page) {
|
||||
if (!R.shadow) {
|
||||
rt_log("resolution: sun shadow map sized by the game (TOTK_SHADOW_RES=game)");
|
||||
return;
|
||||
}
|
||||
int set = rt_patch_set("shadow");
|
||||
if (set < 0) {
|
||||
rt_log("resolution: the generated C has no shadow patch set: the game sizes its shadow map");
|
||||
return;
|
||||
}
|
||||
int32_t n = (int32_t)R.shadow;
|
||||
memcpy(page + 92, &n, 4);
|
||||
rt_patch_switch(set, 1);
|
||||
rt_log("resolution: sun shadow map %ux%u (TOTK_SHADOW_RES)", R.shadow, R.shadow);
|
||||
}
|
||||
|
||||
// ---- start: the patch page and the sets
|
||||
static uint64_t pack(unsigned w, unsigned h) { return (uint64_t)h << 32 | w; }
|
||||
|
||||
// TOTK_SHADOW_RANGE (percent, 50 to 400; 100 = the game's): the sun's shadow cascades end at 20, 40, 80 and 160 m.
|
||||
// Other values multiply those distances through the cascade patch set (recomp/patches-v143.txt), so the sharp first
|
||||
// cascade reaches further and the visible steps between cascades move away from the player, at the cost of shadow
|
||||
// texels per metre (TOTK_SHADOW_SCALE pays that back).
|
||||
static void res_shadow_range(void) {
|
||||
int pct = cfg_int("TOTK_SHADOW_RANGE"), set = rt_patch_set("cascade");
|
||||
if (pct == 100) return;
|
||||
if (set < 0) {
|
||||
rt_log("resolution: the generated C has no cascade patch set; shadow_range stays 100");
|
||||
return;
|
||||
}
|
||||
float k = (float)(pct < 50 ? 50 : pct > 400 ? 400 : pct) / 100.0f, d[4] = {20 * k, 40 * k, 80 * k, 160 * k};
|
||||
memcpy((uint8_t *)rt_patch_page() + 64, d, sizeof d);
|
||||
rt_patch_switch(set, 1);
|
||||
rt_log("resolution: shadow cascades end at %g, %g, %g and %g m", d[0], d[1], d[2], d[3]);
|
||||
}
|
||||
|
||||
void res_init(void) {
|
||||
res_shadow_range();
|
||||
if (!bigger()) {
|
||||
rt_log("resolution: the game's own (output 1920x1080, scene 1600x900)");
|
||||
shadow_init(rt_patch_page());
|
||||
return;
|
||||
}
|
||||
int output = rt_patch_set("output"), scene = rt_patch_set("scene");
|
||||
@@ -238,10 +300,18 @@ void res_init(void) {
|
||||
} else {
|
||||
rt_patch_switch(aspect, 0); // the page holds no ratio
|
||||
}
|
||||
shadow_init(page);
|
||||
rt_log("resolution: output %ux%u, scene %ux%u%s; texture heap +%llu KiB", R.ow, R.oh, R.sw, R.sh,
|
||||
scene_on ? "" : " (the game's own 5/6 of the output)", (unsigned long long)res_heap_extra_kib());
|
||||
}
|
||||
|
||||
// DLSS upscaling: textures are sampled for the output's resolution, not the scene's (NVIDIA's DLSS guide: a mip bias
|
||||
// of log2(render / output)), so their detail survives the scaling. 0 without a DLSS mode.
|
||||
float res_dlss_lod_bias(void) {
|
||||
decide();
|
||||
return R.dlss_k ? (float)log2(R.dlss_k) : 0.0f;
|
||||
}
|
||||
|
||||
// 16:9 within 1% (1366x768 counts): the shape the game's own layouts and camera are made for.
|
||||
int res_is_16x9(unsigned w, unsigned h) {
|
||||
double rel = ((double)w / h) / ((double)BASE_W / BASE_H);
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
// The resource size table (restbl.h). Layout of a decompressed RESTBL, little-endian:
|
||||
//
|
||||
// "RESTBL", u32 version (1), u32 name length (160), u32 hashed count, u32 named count
|
||||
// hashed entries {u32 crc32 of the key, u32 size}, sorted by the hash
|
||||
// named entries {char key[name length], u32 size}: the keys whose hash collides with another key's
|
||||
//
|
||||
// A key is a RomFS path without .zs or .mc. The estimates come from the 1.4.3 table against its own files: most types
|
||||
// need the decompressed size plus at most 8 KiB (headers, alignment); a few need a multiple of it (bfres 1.5, bkres
|
||||
// 2.05, bgyml 3.85, asb 1.4, bstar 1.2, bphsc 1.1). A type the table does not list takes 1.5 times its size plus 8 KiB.
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "restbl.h"
|
||||
|
||||
typedef struct { uint32_t hash, size; } Hashed;
|
||||
|
||||
struct Restbl {
|
||||
uint32_t version, name_len;
|
||||
Hashed *hashed;
|
||||
size_t nhashed, hashed_cap;
|
||||
uint8_t *named; // named entries as in the file: key (name_len bytes, NUL padded) then u32 size
|
||||
size_t nnamed;
|
||||
};
|
||||
|
||||
static uint32_t rd32(const uint8_t *p) { return p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24; }
|
||||
static void wr32(uint8_t *p, uint32_t v) {
|
||||
for (int i = 0; i < 4; i++) p[i] = (uint8_t)(v >> 8 * i);
|
||||
}
|
||||
|
||||
static uint32_t crc32_of(const char *s) {
|
||||
uint32_t c = ~0u;
|
||||
for (; *s; s++) {
|
||||
c ^= (uint8_t)*s;
|
||||
for (int k = 0; k < 8; k++) c = c >> 1 ^ (0xEDB88320u & (0u - (c & 1)));
|
||||
}
|
||||
return ~c;
|
||||
}
|
||||
|
||||
Restbl *restbl_parse(const uint8_t *data, size_t n) {
|
||||
if (n < 22 || memcmp(data, "RESTBL", 6)) return NULL;
|
||||
uint32_t version = rd32(data + 6), name_len = rd32(data + 10), nh = rd32(data + 14), nn = rd32(data + 18);
|
||||
if (!name_len || name_len > 4096 || (uint64_t)nh * 8 + (uint64_t)nn * (name_len + 4) != n - 22) return NULL;
|
||||
Restbl *t = calloc(1, sizeof *t);
|
||||
if (!t) return NULL;
|
||||
t->version = version, t->name_len = name_len;
|
||||
t->hashed = malloc(((size_t)nh + 1) * sizeof *t->hashed);
|
||||
t->named = malloc((size_t)nn * (name_len + 4) + 1);
|
||||
if (!t->hashed || !t->named) {
|
||||
restbl_free(t);
|
||||
return NULL;
|
||||
}
|
||||
for (uint32_t i = 0; i < nh; i++) t->hashed[i] = (Hashed){rd32(data + 22 + i * 8), rd32(data + 26 + i * 8)};
|
||||
t->nhashed = t->hashed_cap = nh;
|
||||
if (!t->hashed_cap) t->hashed_cap = 1;
|
||||
memcpy(t->named, data + 22 + (size_t)nh * 8, (size_t)nn * (name_len + 4));
|
||||
t->nnamed = nn;
|
||||
return t;
|
||||
}
|
||||
|
||||
void restbl_free(Restbl *t) {
|
||||
if (!t) return;
|
||||
free(t->hashed);
|
||||
free(t->named);
|
||||
free(t);
|
||||
}
|
||||
|
||||
const char *restbl_key(const char *path, char *buf, size_t n) {
|
||||
size_t len = strlen(path);
|
||||
if (len >= 3 && (!strcmp(path + len - 3, ".zs") || !strcmp(path + len - 3, ".mc"))) len -= 3;
|
||||
if (len >= n) len = n - 1;
|
||||
memcpy(buf, path, len);
|
||||
buf[len] = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// The named entry of key, or NULL.
|
||||
static uint8_t *named_of(const Restbl *t, const char *key) {
|
||||
size_t len = strlen(key);
|
||||
if (len >= t->name_len) return NULL;
|
||||
for (size_t i = 0; i < t->nnamed; i++) {
|
||||
uint8_t *e = t->named + i * (t->name_len + 4);
|
||||
if (!memcmp(e, key, len + 1)) return e;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The position of hash in the hashed entries, or where it would go.
|
||||
static size_t hashed_at(const Restbl *t, uint32_t hash) {
|
||||
size_t lo = 0, hi = t->nhashed;
|
||||
while (lo < hi) {
|
||||
size_t m = lo + (hi - lo) / 2;
|
||||
if (t->hashed[m].hash < hash) lo = m + 1;
|
||||
else hi = m;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
int restbl_get(const Restbl *t, const char *key, uint32_t *size) {
|
||||
const uint8_t *e = named_of(t, key);
|
||||
if (e) {
|
||||
*size = rd32(e + t->name_len);
|
||||
return 1;
|
||||
}
|
||||
uint32_t h = crc32_of(key);
|
||||
size_t i = hashed_at(t, h);
|
||||
if (i < t->nhashed && t->hashed[i].hash == h) {
|
||||
*size = t->hashed[i].size;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The hashed entry of hash raised to size (added when missing); 1 when the table changed.
|
||||
static int raise_hash(Restbl *t, uint32_t h, uint32_t size) {
|
||||
size_t i = hashed_at(t, h);
|
||||
if (i < t->nhashed && t->hashed[i].hash == h) {
|
||||
if (t->hashed[i].size >= size) return 0;
|
||||
t->hashed[i].size = size;
|
||||
return 1;
|
||||
}
|
||||
if (t->nhashed == t->hashed_cap) {
|
||||
size_t cap = t->hashed_cap * 2;
|
||||
Hashed *grown = realloc(t->hashed, cap * sizeof *grown);
|
||||
if (!grown) return 0;
|
||||
t->hashed = grown, t->hashed_cap = cap;
|
||||
}
|
||||
memmove(t->hashed + i + 1, t->hashed + i, (t->nhashed - i) * sizeof *t->hashed);
|
||||
t->hashed[i] = (Hashed){h, size};
|
||||
t->nhashed++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int restbl_raise(Restbl *t, const char *key, uint32_t size) {
|
||||
uint8_t *e = named_of(t, key);
|
||||
if (!e) return raise_hash(t, crc32_of(key), size);
|
||||
if (rd32(e + t->name_len) >= size) return 0;
|
||||
wr32(e + t->name_len, size);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int restbl_merge(Restbl *t, const Restbl *base, const Restbl *shipped) {
|
||||
int raised = 0;
|
||||
for (size_t i = 0; i < shipped->nhashed; i++) {
|
||||
Hashed s = shipped->hashed[i];
|
||||
size_t k = hashed_at(base, s.hash);
|
||||
if (k < base->nhashed && base->hashed[k].hash == s.hash && base->hashed[k].size >= s.size) continue;
|
||||
raised += raise_hash(t, s.hash, s.size);
|
||||
}
|
||||
if (shipped->name_len != t->name_len) return raised;
|
||||
for (size_t i = 0; i < shipped->nnamed; i++) {
|
||||
const uint8_t *e = shipped->named + i * (shipped->name_len + 4);
|
||||
char key[4097];
|
||||
memcpy(key, e, shipped->name_len);
|
||||
key[shipped->name_len] = 0;
|
||||
uint32_t size = rd32(e + shipped->name_len), have;
|
||||
if (restbl_get(base, key, &have) && have >= size) continue;
|
||||
uint8_t *mine = named_of(t, key);
|
||||
if (mine) {
|
||||
if (rd32(mine + t->name_len) < size) wr32(mine + t->name_len, size), raised++;
|
||||
continue;
|
||||
}
|
||||
uint8_t *grown = realloc(t->named, (t->nnamed + 1) * (t->name_len + 4));
|
||||
if (!grown) return raised;
|
||||
t->named = grown;
|
||||
memcpy(t->named + t->nnamed * (t->name_len + 4), e, t->name_len + 4);
|
||||
t->nnamed++;
|
||||
raised++;
|
||||
}
|
||||
return raised;
|
||||
}
|
||||
|
||||
int restbl_take(Restbl *t, const Restbl *from, const char *const *keys, size_t n) {
|
||||
int raised = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint32_t size;
|
||||
if (restbl_get(from, keys[i], &size)) raised += restbl_raise(t, keys[i], size);
|
||||
}
|
||||
return raised;
|
||||
}
|
||||
|
||||
uint8_t *restbl_write(const Restbl *t, size_t *n) {
|
||||
size_t size = 22 + t->nhashed * 8 + t->nnamed * (t->name_len + 4);
|
||||
uint8_t *out = malloc(size);
|
||||
if (!out) return NULL;
|
||||
memcpy(out, "RESTBL", 6);
|
||||
wr32(out + 6, t->version);
|
||||
wr32(out + 10, t->name_len);
|
||||
wr32(out + 14, (uint32_t)t->nhashed);
|
||||
wr32(out + 18, (uint32_t)t->nnamed);
|
||||
for (size_t i = 0; i < t->nhashed; i++) {
|
||||
wr32(out + 22 + i * 8, t->hashed[i].hash);
|
||||
wr32(out + 26 + i * 8, t->hashed[i].size);
|
||||
}
|
||||
memcpy(out + 22 + t->nhashed * 8, t->named, t->nnamed * (t->name_len + 4));
|
||||
*n = size;
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- estimates
|
||||
|
||||
// The factor a type's entry needs over its decompressed size (1000 = 1.0); 0 for a type the table never lists.
|
||||
static unsigned type_factor(const char *key) {
|
||||
static const struct { const char *ext; unsigned f; } types[] = {
|
||||
{"bfres", 1500}, {"bkres", 2050}, {"bgyml", 3850}, {"asb", 1400}, {"bstar", 1200}, {"bphsc", 1100},
|
||||
{"byml", 1000}, {"txtg", 1000}, {"pack", 1000}, {"bntx", 1000}, {"bphsh", 1000}, {"vsts", 1000},
|
||||
{"ainb", 1000}, {"bphnm", 1000}, {"bars", 1000}, {"chunk", 1000}, {"bfevfl", 1000}, {"quad", 1000},
|
||||
{"baev", 1000}, {"bcul", 1000}, {"crbin", 1000}, {"wbr", 1000}, {"bhtmp", 1000}, {"dpi", 1000},
|
||||
{"cai", 1000}, {"png", 1000}, {"cutinfo", 1000}, {"blwp", 1000}, {"sarc", 1000}, {"tscb", 1000},
|
||||
{"bfsha", 1000}, {"jpg", 1000}, {"beco", 1000}, {"bfarc", 1000}, {"blarc", 1000}, {"belnk", 1000},
|
||||
{"genvb", 1000}, {"bnsh", 1000}, {"bslnk", 1000}, {"baatarc", 1000}, {"bagst", 1000},
|
||||
{"bwav", 0}, {"webm", 0},
|
||||
};
|
||||
const char *leaf = strrchr(key, '/') ? strrchr(key, '/') + 1 : key, *dot = strrchr(leaf, '.');
|
||||
if (!dot) return 1500;
|
||||
for (size_t i = 0; i < sizeof types / sizeof *types; i++)
|
||||
if (!strcmp(dot + 1, types[i].ext)) return types[i].f;
|
||||
return 1500;
|
||||
}
|
||||
|
||||
static uint32_t clamp32(uint64_t v) { return v > 0xFFFFFFF0u ? 0xFFFFFFF0u : (uint32_t)v; }
|
||||
|
||||
uint32_t restbl_estimate(const char *key, uint64_t size) {
|
||||
unsigned f = type_factor(key);
|
||||
if (!f) return 0;
|
||||
uint64_t e = (size * f + 999) / 1000 + 8192;
|
||||
return clamp32((e + 4095) & ~(uint64_t)4095);
|
||||
}
|
||||
|
||||
uint32_t restbl_grow(const char *key, uint32_t entry, uint64_t old, uint64_t new_size, int scaled) {
|
||||
if (new_size <= old) return entry;
|
||||
if (scaled) return clamp32(old ? ((uint64_t)entry * new_size + old - 1) / old : restbl_estimate(key, new_size * 8));
|
||||
unsigned f = type_factor(key);
|
||||
if (!f) return entry;
|
||||
uint64_t e = entry + ((new_size - old) * f + 999) / 1000;
|
||||
return clamp32((e + 31) & ~(uint64_t)31);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// The game's resource size table (restbl.c): System/Resource/ResourceSizeTable.Product.<n>.Nin_NX_NVN.rsizetable.zs, a
|
||||
// zstd-compressed RESTBL. The game sizes the buffer it loads a file into from the file's entry, so a mod file larger
|
||||
// than the original needs a larger entry, and a file a mod adds needs one of its own. romfs.c merges the tables the
|
||||
// mods ship and raises the entries of the files they serve (romfs.c restbl_overlay).
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct Restbl Restbl;
|
||||
|
||||
// The table of a decompressed RESTBL (copied); NULL when it is not one.
|
||||
Restbl *restbl_parse(const uint8_t *data, size_t n);
|
||||
void restbl_free(Restbl *t);
|
||||
// The table's key of a RomFS path: the path without a trailing .zs or .mc, into buf.
|
||||
const char *restbl_key(const char *path, char *buf, size_t n);
|
||||
// 1 with *size when key has an entry.
|
||||
int restbl_get(const Restbl *t, const char *key, uint32_t *size);
|
||||
// The entry of key raised to size (added when missing); 1 when the table changed.
|
||||
int restbl_raise(Restbl *t, const char *key, uint32_t size);
|
||||
// Every entry of shipped larger than base's (or missing from base) raised in t; the number of entries raised.
|
||||
int restbl_merge(Restbl *t, const Restbl *base, const Restbl *shipped);
|
||||
// Each entry of from whose key one of keys[0..n) names, raised in t; the number raised.
|
||||
int restbl_take(Restbl *t, const Restbl *from, const char *const *keys, size_t n);
|
||||
// The table as a decompressed RESTBL (malloc'd); NULL when out of memory.
|
||||
uint8_t *restbl_write(const Restbl *t, size_t *n);
|
||||
|
||||
// The entry a file of the key's type and decompressed size needs when the game has none for it; 0 for a type the
|
||||
// table never lists (streamed sound and video).
|
||||
uint32_t restbl_estimate(const char *key, uint64_t size);
|
||||
// The entry of a file the game lists with `entry` for its own copy of old bytes, now new bytes: raised by the growth
|
||||
// times the type's factor, never lowered. scaled: the sizes are compressed ones (a mesh codec .mc file), whose entry
|
||||
// grows in proportion.
|
||||
uint32_t restbl_grow(const char *key, uint32_t entry, uint64_t old, uint64_t new_size, int scaled);
|
||||
+514
-71
@@ -17,18 +17,33 @@
|
||||
// TOTK_ROMFS_MODS; unset or "0" = none. A missing path, a size or hash mismatch or a changed original stops the start.
|
||||
// An overlay file is opened when the game opens it; its identity (volume, file id, size, last write) is kept from the
|
||||
// start check, and an open that finds another file there stops the game.
|
||||
//
|
||||
// Player mods (mods.c): the files under each enabled mod's RomFS root are overlays too, checked by identity only (no
|
||||
// hash, no manifest). A path several overlays give is served by one: a player mod higher in the order beats a lower
|
||||
// one, any player mod beats the TOTK_ROMFS_MODS manifest, which beats romfs-patches (overlay_contest). A mod path that
|
||||
// matches a RomFS path only without case is served under the RomFS spelling.
|
||||
//
|
||||
// The resource size table: with a player mod enabled, the game's table is rebuilt at start with the tables the mods
|
||||
// ship merged in and the entries of the mods' files raised to what they need (restbl_overlay), and served from
|
||||
// <profile>/mods/ in place of the game's or a mod's table.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "fs.h"
|
||||
#include "host.h"
|
||||
#include "input_map.h"
|
||||
#include "mods.h"
|
||||
#include "romfs_config.h"
|
||||
#include "romfs_path.h"
|
||||
#include "platform/platform.h"
|
||||
#include "restbl.h"
|
||||
#include "sha256.h"
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#define NH 65536 // RomFS handle slots
|
||||
#define READ_CHUNK (64u << 20) // largest single read
|
||||
#define HASH_CHUNK (8u << 20)
|
||||
@@ -38,7 +53,9 @@ typedef struct { uint64_t offset, size; uint32_t name, len; } Entry;
|
||||
_Static_assert(sizeof(Entry) == 24, "index entry layout");
|
||||
|
||||
typedef struct { uint32_t volume, pad; uint64_t file_id, size, write_time, create_time; } Identity; // no padding
|
||||
typedef struct { uint64_t index, size; char *path, *name; Identity id; } Overlay;
|
||||
// prio: which overlay serves a path two of them give (the higher); romfs-patches 0, the TOTK_ROMFS_MODS manifest 1, a
|
||||
// player mod 2 + its distance from the bottom of the order. mod: its index in mods_at + 1, 0 for any other overlay.
|
||||
typedef struct { uint64_t index, size; char *path, *name; Identity id; int prio, mod; } Overlay;
|
||||
|
||||
static uint8_t *index_bytes;
|
||||
static size_t index_size;
|
||||
@@ -56,6 +73,7 @@ static Overlay *overlays; // sorted by RomFS index after overlay_init
|
||||
static size_t noverlay, overlay_cap;
|
||||
static char **added; // RomFS paths of the added files, sorted; added[k] is index file_count + k
|
||||
static size_t nadded;
|
||||
static int patches_loaded; // a romfs-patches manifest gave a file (romfs_patches_loaded)
|
||||
|
||||
static void need(int c, const char *m) {
|
||||
if (!c) rt_fatal("RomFS: %s", m);
|
||||
@@ -204,8 +222,8 @@ static uint32_t overlay_slot(uint64_t i) {
|
||||
// ---- overlays
|
||||
// Loads and checks one overlay folder. A failed check stops the game, except with optional set (the Xbox prompts
|
||||
// folder): the folder is dropped, the reason logged, and 0 returned. Returns 1 when the folder is loaded or has no
|
||||
// manifest.
|
||||
static int overlay_load(const char *dir, int optional) {
|
||||
// manifest. prio: the Overlay field (0 for the port's own folders, 1 for the TOTK_ROMFS_MODS manifest).
|
||||
static int overlay_load(const char *dir, int optional, int prio) {
|
||||
#define BAD(...) do { if (!optional) rt_fatal(__VA_ARGS__); rt_log(__VA_ARGS__); goto bad; } while (0)
|
||||
char path[2100];
|
||||
need(snprintf(path, sizeof path, "%s/manifest.tsv", dir) < (int)sizeof path, "overlay path");
|
||||
@@ -262,7 +280,8 @@ static int overlay_load(const char *dir, int optional) {
|
||||
}
|
||||
char *copy = strdup(file), *rel = strdup(name);
|
||||
need(copy && rel, "overlay path copy");
|
||||
overlays[noverlay++] = (Overlay){i, size, copy, rel, id};
|
||||
overlays[noverlay++] = (Overlay){i, size, copy, rel, id, prio, 0};
|
||||
if (!prio) patches_loaded = 1;
|
||||
if (noverlay - first <= 16)
|
||||
rt_log("RomFS overlay: %s (%llu bytes) %s", name, size, add ? "is added" : "replaces the original");
|
||||
}
|
||||
@@ -283,6 +302,207 @@ bad:
|
||||
#undef BAD
|
||||
}
|
||||
|
||||
// ---- player mods (mods.c): every file under an enabled mod's RomFS root, by identity (no hash: a texture pack is
|
||||
// gigabytes and the player changes it at will; romfs_open still stops on a file that changed after the start)
|
||||
static uint32_t *folded; // RomFS indices sorted by name ignoring ASCII case; built on the first mod path that misses
|
||||
|
||||
static int folded_order(const void *a, const void *b) {
|
||||
return strcasecmp(entry_name(entry(*(const uint32_t *)a)), entry_name(entry(*(const uint32_t *)b)));
|
||||
}
|
||||
|
||||
// The RomFS index of name, or of the RomFS path equal to it but for case (a mod made on Windows, where an emulator's
|
||||
// folder lookup ignores case); UINT64_MAX for a path the RomFS lacks.
|
||||
static uint64_t index_any_case(const char *name) {
|
||||
uint64_t i = lower(name);
|
||||
if (i < file_count && !strcmp(entry_name(entry(i)), name)) return i;
|
||||
if (!folded) {
|
||||
folded = malloc(file_count * sizeof *folded);
|
||||
need(folded != NULL, "case-folded index");
|
||||
for (uint64_t k = 0; k < file_count; k++) folded[k] = (uint32_t)k;
|
||||
qsort(folded, file_count, sizeof *folded, folded_order);
|
||||
}
|
||||
uint64_t lo = 0, hi = file_count;
|
||||
while (lo < hi) {
|
||||
uint64_t m = lo + (hi - lo) / 2;
|
||||
if (strcasecmp(entry_name(entry(folded[m])), name) < 0) lo = m + 1;
|
||||
else hi = m;
|
||||
}
|
||||
return lo < file_count && !strcasecmp(entry_name(entry(folded[lo])), name) ? folded[lo] : UINT64_MAX;
|
||||
}
|
||||
|
||||
static void overlay_push(Overlay o) {
|
||||
if (noverlay == overlay_cap) {
|
||||
overlay_cap = overlay_cap ? overlay_cap * 2 : 16;
|
||||
overlays = realloc(overlays, overlay_cap * sizeof *overlays);
|
||||
need(overlays != NULL, "overlay table");
|
||||
}
|
||||
overlays[noverlay++] = o;
|
||||
}
|
||||
|
||||
// 1 when path names a file of one game version only (Product.<n> in a name: the resource size table, the actor
|
||||
// database and other version-bound files); the RomFS lacking it means it is another version's.
|
||||
static int other_version(const char *path) {
|
||||
for (const char *p = strstr(path, ".Product."); p; p = strstr(p + 1, ".Product.")) {
|
||||
const char *d = p + strlen(".Product.");
|
||||
if (*d >= '0' && *d <= '9') return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- the resource size table's inputs: the tables the overlays carry, before overlay_contest keeps one of them
|
||||
static char restbl_rel[256]; // the RomFS path of the game's table, "" when it has none
|
||||
typedef struct {
|
||||
char *file;
|
||||
int mod, other; // mod: ModInfo index + 1, 0 for the port's folders; other: made for another game version
|
||||
} Shipped;
|
||||
static Shipped *shipped;
|
||||
static size_t nshipped;
|
||||
|
||||
static int ends_with(const char *s, const char *tail) {
|
||||
size_t n = strlen(s), k = strlen(tail);
|
||||
return n >= k && !strcmp(s + n - k, tail);
|
||||
}
|
||||
|
||||
// 1 when rel is a resource size table of any game version.
|
||||
static int is_restbl(const char *rel) {
|
||||
return !strncmp(rel, "System/Resource/ResourceSizeTable.", 34) && ends_with(rel, ".rsizetable.zs");
|
||||
}
|
||||
|
||||
static void shipped_add(const char *file, int mod, int other) {
|
||||
Shipped *grown = realloc(shipped, (nshipped + 1) * sizeof *shipped);
|
||||
char *copy = strdup(file);
|
||||
need(grown && copy, "resource size table list");
|
||||
shipped = grown;
|
||||
shipped[nshipped++] = (Shipped){copy, mod, other};
|
||||
}
|
||||
|
||||
// The game's table: the one System/Resource/ResourceSizeTable.*.rsizetable.zs of the RomFS.
|
||||
static void restbl_find(void) {
|
||||
for (uint64_t i = lower("System/Resource/ResourceSizeTable."); i < file_count; i++) {
|
||||
const char *n = entry_name(entry(i));
|
||||
if (strncmp(n, "System/Resource/ResourceSizeTable.", 34)) break;
|
||||
if (is_restbl(n) && strlen(n) < sizeof restbl_rel) {
|
||||
snprintf(restbl_rel, sizeof restbl_rel, "%s", n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The tables the overlays carry for this game version, every one of them (overlay_contest keeps one).
|
||||
static void restbl_collect(void) {
|
||||
for (size_t k = 0; k < noverlay; k++)
|
||||
if (restbl_rel[0] && !strcmp(overlays[k].name, restbl_rel)) shipped_add(overlays[k].path, overlays[k].mod, 0);
|
||||
}
|
||||
|
||||
typedef struct { int k, prio; const char *dir, *rel; } ModWalk;
|
||||
|
||||
static void mod_walk(int k, int prio, const char *dir, const char *rel);
|
||||
|
||||
// One entry of a mod's RomFS folder (mod_walk).
|
||||
static int mod_walk_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
const ModWalk *w = ctx;
|
||||
ModInfo *m = mods_at(w->k);
|
||||
char file[4200], path[4200];
|
||||
if (snprintf(file, sizeof file, "%s/%s", w->dir, leaf) >= (int)sizeof file ||
|
||||
snprintf(path, sizeof path, "%s%s%s", w->rel, *w->rel ? "/" : "", leaf) >= (int)sizeof path) {
|
||||
rt_log("mods: %s: path too long under %s; left out", m->name, w->dir);
|
||||
return 1;
|
||||
}
|
||||
if (kind == PLAT_FS_DIR) {
|
||||
mod_walk(w->k, w->prio, file, path);
|
||||
return 1;
|
||||
}
|
||||
if (kind != PLAT_FS_FILE) return 1;
|
||||
if (!romfs_path_valid_relative(path)) {
|
||||
rt_log("mods: %s: %s is not a RomFS path; left out", m->name, path);
|
||||
return 1;
|
||||
}
|
||||
uint64_t i = index_any_case(path);
|
||||
if (i == UINT64_MAX && other_version(path)) {
|
||||
if (is_restbl(path)) shipped_add(file, w->k + 1, 1); // its entries for this mod's files still apply
|
||||
m->other_version++;
|
||||
return 1;
|
||||
}
|
||||
PlatFile *f = open_read(file);
|
||||
if (!f) {
|
||||
rt_log("mods: %s: cannot open %s; left out", m->name, file);
|
||||
return 1;
|
||||
}
|
||||
Identity id = identity_of(f);
|
||||
plat_file_close(f);
|
||||
char *copy = strdup(file), *rp = strdup(i == UINT64_MAX ? path : entry_name(entry(i)));
|
||||
need(copy && rp, "overlay path copy");
|
||||
overlay_push((Overlay){i == UINT64_MAX ? ADDED : i, id.size, copy, rp, id, w->prio, w->k + 1});
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The files under dir (a mod's RomFS root; rel: the RomFS path of dir, "" at the root) as overlays of mod k.
|
||||
static void mod_walk(int k, int prio, const char *dir, const char *rel) {
|
||||
ModWalk w = {k, prio, dir, rel};
|
||||
plat_fs_list(dir, mod_walk_cb, &w);
|
||||
}
|
||||
|
||||
// What overlays compete on: the RomFS index, an added file's path; the higher prio first.
|
||||
static int contest_order(const void *a, const void *b) {
|
||||
const Overlay *x = a, *y = b;
|
||||
if (x->index != y->index) return (x->index > y->index) - (x->index < y->index);
|
||||
int c = x->index == ADDED ? strcmp(x->name, y->name) : 0;
|
||||
return c ? c : y->prio - x->prio;
|
||||
}
|
||||
|
||||
static const char *owner_name(const Overlay *o) {
|
||||
return o->mod ? mods_at(o->mod - 1)->name : o->prio ? "romfs_mods" : "the port's patches";
|
||||
}
|
||||
|
||||
// One overlay per path: the highest prio serves it; the others are dropped and counted on their mod.
|
||||
static void overlay_contest(void) {
|
||||
if (!noverlay) return;
|
||||
qsort(overlays, noverlay, sizeof *overlays, contest_order);
|
||||
size_t kept = 0;
|
||||
for (size_t k = 0; k < noverlay; k++) {
|
||||
Overlay *o = &overlays[k], *w = kept ? &overlays[kept - 1] : NULL;
|
||||
if (w && w->index == o->index && (o->index != ADDED || !strcmp(w->name, o->name))) {
|
||||
if (o->mod) {
|
||||
ModInfo *m = mods_at(o->mod - 1);
|
||||
m->overridden++;
|
||||
if (!m->overridden_by[0]) snprintf(m->overridden_by, sizeof m->overridden_by, "%s", owner_name(w));
|
||||
} else if (w->mod) {
|
||||
rt_log("RomFS overlay: %s from mod %s replaces the one of %s", o->name, owner_name(w), owner_name(o));
|
||||
}
|
||||
free(o->path);
|
||||
free(o->name);
|
||||
continue;
|
||||
}
|
||||
if (o->mod) {
|
||||
if (o->index == ADDED) mods_at(o->mod - 1)->added++;
|
||||
else mods_at(o->mod - 1)->replaced++;
|
||||
}
|
||||
overlays[kept++] = *o;
|
||||
}
|
||||
noverlay = kept;
|
||||
}
|
||||
|
||||
static void mods_overlay_init(void) {
|
||||
mods_scan();
|
||||
int n = mods_count();
|
||||
for (int k = 0; k < n; k++) {
|
||||
const ModInfo *m = mods_at(k);
|
||||
// a mod with ExeFS patches waits for the rebuild that applies them: half of a mod is worse than none
|
||||
if (m->enabled && (m->kind & MOD_ROMFS) && m->problem == MOD_OK) mod_walk(k, 2 + (n - k), m->romfs, "");
|
||||
}
|
||||
}
|
||||
|
||||
static void mods_overlay_log(void) {
|
||||
for (int k = 0; k < mods_count(); k++) {
|
||||
const ModInfo *m = mods_at(k);
|
||||
if (!m->enabled || !(m->kind & MOD_ROMFS) || m->problem != MOD_OK) continue;
|
||||
rt_log("mods: %s: %u files served (%u replaced, %u added), %u overridden%s%s, %u for another game version",
|
||||
m->name, m->replaced + m->added, m->replaced, m->added, m->overridden, m->overridden ? " by " : "",
|
||||
m->overridden_by, m->other_version);
|
||||
}
|
||||
}
|
||||
|
||||
static int overlay_order(const void *a, const void *b) {
|
||||
uint64_t x = ((const Overlay *)a)->index, y = ((const Overlay *)b)->index;
|
||||
return (x > y) - (x < y);
|
||||
@@ -292,7 +512,7 @@ static int added_order(const void *a, const void *b) {
|
||||
return strcmp((*(Overlay *const *)a)->name, (*(Overlay *const *)b)->name);
|
||||
}
|
||||
|
||||
// Number the added files in path order after the RomFS's own (file_count + k); two overlays adding one path stop.
|
||||
// Number the added files in path order after the RomFS's own (file_count + k); overlay_contest left one per path.
|
||||
static void added_init(void) {
|
||||
Overlay **list = NULL;
|
||||
for (size_t k = 0; k < noverlay; k++)
|
||||
@@ -306,8 +526,6 @@ static void added_init(void) {
|
||||
added = malloc(nadded * sizeof *added);
|
||||
need(added != NULL, "added files");
|
||||
for (size_t k = 0; k < nadded; k++) {
|
||||
if (k && !strcmp(list[k]->name, list[k - 1]->name))
|
||||
rt_fatal("RomFS overlay: %s and %s both add %s", list[k - 1]->path, list[k]->path, list[k]->name);
|
||||
list[k]->index = file_count + k;
|
||||
added[k] = list[k]->name;
|
||||
}
|
||||
@@ -315,40 +533,46 @@ static void added_init(void) {
|
||||
rt_log("RomFS overlay: %zu files added to the RomFS", nadded);
|
||||
}
|
||||
|
||||
// The HeapDef table a bigger game resolution needs (resolution.c): its texture heap and that heap's parent grow. The
|
||||
// base is an overlay that replaces the table (a mod's heap sizes stay), else the RomFS original; the grown table is
|
||||
// written to <profile>/resolution/ and served in the base's place.
|
||||
#define HEAPDEF "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
|
||||
static void heapdef_overlay(void) {
|
||||
if (!res_heap_extra_kib()) return;
|
||||
uint64_t i = lower(HEAPDEF);
|
||||
if (i >= file_count || strcmp(entry_name(entry(i)), HEAPDEF)) {
|
||||
rt_log("RomFS: %s missing; the game keeps its own resolution", HEAPDEF);
|
||||
return;
|
||||
// RomFS file `rel` as the game would read it: from an overlay that replaces it (*base), else from the image. malloc'd;
|
||||
// NULL when it is missing, unreadable or 16 MiB or more (logged with `keeps`, what the game does without it).
|
||||
static void *read_served(const char *rel, Overlay **base, size_t *n, const char *keeps) {
|
||||
uint64_t i = lower(rel);
|
||||
*base = NULL;
|
||||
if (i >= file_count || strcmp(entry_name(entry(i)), rel)) {
|
||||
rt_log("RomFS: %s missing; %s", rel, keeps);
|
||||
return NULL;
|
||||
}
|
||||
Overlay *base = NULL;
|
||||
for (size_t k = 0; k < noverlay; k++)
|
||||
if (overlays[k].index == i) base = &overlays[k];
|
||||
PlatFile *f = base ? open_read(base->path) : image;
|
||||
uint64_t off = base ? 0 : entry(i).offset, n = base ? base->size : entry(i).size;
|
||||
void *in = n && n < (16u << 20) && f ? malloc((size_t)n) : NULL;
|
||||
int ok = in && read_at(f, in, (uint32_t)n, off) == (int64_t)n;
|
||||
if (base && f) plat_file_close(f);
|
||||
void *out = NULL;
|
||||
size_t out_n = 0;
|
||||
ok = ok && res_heapdef_build(in, (size_t)n, &out, &out_n);
|
||||
free(in);
|
||||
if (!ok) return;
|
||||
if (overlays[k].index == i) *base = &overlays[k];
|
||||
PlatFile *f = *base ? open_read((*base)->path) : image;
|
||||
uint64_t off = *base ? 0 : entry(i).offset, size = *base ? (*base)->size : entry(i).size;
|
||||
void *in = size && size < (16u << 20) && f ? malloc((size_t)size) : NULL;
|
||||
int ok = in && read_at(f, in, (uint32_t)size, off) == (int64_t)size;
|
||||
if (*base && f) plat_file_close(f);
|
||||
if (!ok) {
|
||||
free(in);
|
||||
rt_log("RomFS: cannot read %s; %s", rel, keeps);
|
||||
return NULL;
|
||||
}
|
||||
*n = (size_t)size;
|
||||
return in;
|
||||
}
|
||||
|
||||
// Serves out[0..out_n) (malloc'd, freed here) in place of RomFS file `rel` and of an overlay that replaces it (base):
|
||||
// written to <profile>/<sub>/<file name of rel>. `what` names the change in the log. 0 when it cannot be written.
|
||||
static int serve_generated(const char *rel, Overlay *base, void *out, size_t out_n, const char *sub, const char *what,
|
||||
const char *keeps) {
|
||||
char dir[2100], file[2200];
|
||||
need(snprintf(dir, sizeof dir, "%s/resolution", rt_config()->profile) < (int)sizeof dir, "resolution path");
|
||||
need(snprintf(file, sizeof file, "%s/Product.Nin_NX_NVN.HeapDef.byml.zs", dir) < (int)sizeof file, "resolution path");
|
||||
const char *leaf = strrchr(rel, '/') ? strrchr(rel, '/') + 1 : rel;
|
||||
need(snprintf(dir, sizeof dir, "%s/%s", rt_config()->profile, sub) < (int)sizeof dir, "generated overlay path");
|
||||
need(snprintf(file, sizeof file, "%s/%s", dir, leaf) < (int)sizeof file, "generated overlay path");
|
||||
plat_file_mkdir_p(dir);
|
||||
ok = plat_file_write(file, out, out_n);
|
||||
int ok = plat_file_write(file, out, out_n);
|
||||
free(out);
|
||||
PlatFile *r = ok ? open_read(file) : NULL;
|
||||
if (!r) {
|
||||
rt_log("RomFS: cannot write %s; the game keeps its own resolution", file);
|
||||
return;
|
||||
rt_log("RomFS: cannot write %s; %s", file, keeps);
|
||||
return 0;
|
||||
}
|
||||
Identity id = identity_of(r);
|
||||
plat_file_close(r);
|
||||
@@ -365,31 +589,217 @@ static void heapdef_overlay(void) {
|
||||
overlays = realloc(overlays, overlay_cap * sizeof *overlays);
|
||||
need(overlays != NULL, "overlay table");
|
||||
}
|
||||
char *rel = strdup(HEAPDEF);
|
||||
need(rel != NULL, "overlay path copy");
|
||||
overlays[noverlay++] = (Overlay){i, out_n, copy, rel, id};
|
||||
char *name = strdup(rel);
|
||||
need(name != NULL, "overlay path copy");
|
||||
overlays[noverlay++] = (Overlay){lower(rel), out_n, copy, name, id};
|
||||
}
|
||||
rt_log("RomFS overlay: %s (%zu bytes, grown heaps) replaces %s", file, out_n, base ? "the mod's table" : "the original");
|
||||
res_heapdef_served();
|
||||
rt_log("RomFS overlay: %s (%zu bytes, %s) replaces %s", file, out_n, what, base ? "the mod's file" : "the original");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int xbox_prompts_ready; // romfs-patches-xbox/manifest.tsv was next to romfs-index.bin at start
|
||||
static int xbox_prompts_active; // and overlay_init loaded it
|
||||
// ---- the resource size table (restbl.c)
|
||||
|
||||
int romfs_xbox_prompts_ready(void) { return xbox_prompts_ready; }
|
||||
int romfs_xbox_prompts_active(void) { return xbox_prompts_active; }
|
||||
// The decompressed size of a file's bytes as the game unpacks them: the content size its zstd frame records (.zs;
|
||||
// the game's files and zstd's own tools write it), four times the bytes for a frame without one, else its size.
|
||||
static uint64_t content_size(PlatFile *f, uint64_t off, uint64_t size, const char *rel) {
|
||||
if (!ends_with(rel, ".zs") || !f) return size;
|
||||
uint8_t head[18];
|
||||
uint32_t n = size < sizeof head ? (uint32_t)size : (uint32_t)sizeof head;
|
||||
if (read_at(f, head, n, off) != n) return size;
|
||||
unsigned long long c = ZSTD_getFrameContentSize(head, n);
|
||||
return c == ZSTD_CONTENTSIZE_ERROR ? size : c == ZSTD_CONTENTSIZE_UNKNOWN ? size * 4 : c;
|
||||
}
|
||||
|
||||
// A table from its zstd bytes; NULL when they are not one.
|
||||
static Restbl *restbl_unpack(const void *z, size_t n) {
|
||||
unsigned long long raw = ZSTD_getFrameContentSize(z, n);
|
||||
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > (64u << 20)) return NULL;
|
||||
uint8_t *d = malloc((size_t)raw ? (size_t)raw : 1);
|
||||
size_t got = d ? ZSTD_decompress(d, (size_t)raw, z, n) : 0;
|
||||
Restbl *t = d && !ZSTD_isError(got) && got == raw ? restbl_parse(d, (size_t)raw) : NULL;
|
||||
free(d);
|
||||
return t;
|
||||
}
|
||||
|
||||
// With a player mod (or the TOTK_ROMFS_MODS manifest) serving files: the game's table with every shipped table of this
|
||||
// version merged in (an entry larger than the game's wins), the entries a mod's table for another version gives for
|
||||
// that mod's files, and each served mod file's entry raised to what its size needs (restbl_grow for a file the game
|
||||
// has, restbl_estimate for an added one). Never lowers the game's entries.
|
||||
static void restbl_overlay(void) {
|
||||
int player = 0;
|
||||
for (size_t k = 0; k < noverlay; k++) player |= overlays[k].mod || overlays[k].prio == 1;
|
||||
if (!player || !restbl_rel[0]) return;
|
||||
static const char keeps[] = "mod files that grew may fail to load";
|
||||
double t0 = now_s();
|
||||
uint64_t gi = lower(restbl_rel);
|
||||
Entry ge = entry(gi);
|
||||
void *gz = ge.size < (64u << 20) ? malloc((size_t)ge.size) : NULL;
|
||||
Restbl *base = NULL, *out = NULL;
|
||||
if (gz && read_at(image, gz, (uint32_t)ge.size, ge.offset) == (int64_t)ge.size) {
|
||||
base = restbl_unpack(gz, (size_t)ge.size);
|
||||
out = restbl_unpack(gz, (size_t)ge.size);
|
||||
}
|
||||
free(gz);
|
||||
if (!base || !out) {
|
||||
rt_log("RomFS: cannot read the resource size table %s; %s", restbl_rel, keeps);
|
||||
restbl_free(base);
|
||||
restbl_free(out);
|
||||
return;
|
||||
}
|
||||
int tables = 0, merged = 0, raised = 0, estimated = 0;
|
||||
for (size_t s = 0; s < nshipped; s++) {
|
||||
uint64_t n = 0;
|
||||
void *z = plat_file_read(shipped[s].file, &n);
|
||||
Restbl *t = z ? restbl_unpack(z, (size_t)n) : NULL;
|
||||
free(z);
|
||||
if (!t) {
|
||||
rt_log("RomFS: %s is not a resource size table; left out", shipped[s].file);
|
||||
continue;
|
||||
}
|
||||
tables++;
|
||||
if (!shipped[s].other) {
|
||||
merged += restbl_merge(out, base, t);
|
||||
} else { // another version's table: only the entries of the files its mod serves
|
||||
for (size_t k = 0; k < noverlay; k++) {
|
||||
char key[4200];
|
||||
const char *kp = restbl_key(overlays[k].name, key, sizeof key);
|
||||
if (overlays[k].mod == shipped[s].mod) merged += restbl_take(out, t, &kp, 1);
|
||||
}
|
||||
}
|
||||
restbl_free(t);
|
||||
}
|
||||
Overlay *served = NULL;
|
||||
for (size_t k = 0; k < noverlay; k++) {
|
||||
Overlay *o = &overlays[k];
|
||||
if (o->index == gi) served = o;
|
||||
if ((!o->mod && o->prio != 1) || o->index == gi) continue;
|
||||
char key[4200];
|
||||
restbl_key(o->name, key, sizeof key);
|
||||
int mc = ends_with(o->name, ".mc");
|
||||
PlatFile *f = open_read(o->path);
|
||||
uint64_t now = mc ? o->size : content_size(f, 0, o->size, o->name);
|
||||
if (f) plat_file_close(f);
|
||||
uint32_t entry_size, want;
|
||||
if (o->index != ADDED) {
|
||||
if (!restbl_get(base, key, &entry_size)) continue; // the game sizes it without the table (streamed)
|
||||
Entry e = entry(o->index);
|
||||
uint64_t was = mc ? e.size : content_size(image, e.offset, e.size, o->name);
|
||||
want = restbl_grow(key, entry_size, was, now, mc);
|
||||
} else {
|
||||
want = restbl_estimate(key, mc ? now * 8 : now); // a mesh codec file unpacks to several times its size
|
||||
estimated += want != 0;
|
||||
}
|
||||
raised += want && restbl_raise(out, key, want);
|
||||
}
|
||||
restbl_free(base);
|
||||
if (!merged && !raised && !served) {
|
||||
restbl_free(out);
|
||||
rt_log("RomFS: resource size table: the mods' files fit the game's entries (%.0f ms)", (now_s() - t0) * 1000);
|
||||
return;
|
||||
}
|
||||
size_t rn = 0, zn = 0;
|
||||
uint8_t *raw = restbl_write(out, &rn);
|
||||
restbl_free(out);
|
||||
size_t cap = raw ? ZSTD_compressBound(rn) : 0;
|
||||
void *z = raw ? malloc(cap) : NULL;
|
||||
if (z) zn = ZSTD_compress(z, cap, raw, rn, 3);
|
||||
free(raw);
|
||||
if (!z || ZSTD_isError(zn)) {
|
||||
free(z);
|
||||
rt_log("RomFS: cannot pack the merged resource size table; %s", keeps);
|
||||
return;
|
||||
}
|
||||
if (serve_generated(restbl_rel, served, z, zn, "mods", "merged resource sizes", keeps))
|
||||
rt_log("RomFS: resource size table: %d tables merged (%d entries), %d entries raised for mod files (%d "
|
||||
"added files estimated), %.0f ms", tables, merged, raised, estimated, (now_s() - t0) * 1000);
|
||||
}
|
||||
|
||||
// The HeapDef table a bigger game resolution needs (resolution.c): its texture heap and that heap's parent grow. The
|
||||
// base is an overlay that replaces the table (a mod's heap sizes stay), else the RomFS original; the grown table is
|
||||
// written to <profile>/resolution/ and served in the base's place.
|
||||
#define HEAPDEF "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
|
||||
static void heapdef_overlay(void) {
|
||||
if (!res_heap_extra_kib()) return;
|
||||
static const char keeps[] = "the game keeps its own resolution";
|
||||
Overlay *base;
|
||||
size_t n;
|
||||
void *in = read_served(HEAPDEF, &base, &n, keeps), *out = NULL;
|
||||
size_t out_n = 0;
|
||||
int ok = in && res_heapdef_build(in, n, &out, &out_n);
|
||||
free(in);
|
||||
if (ok && serve_generated(HEAPDEF, base, out, out_n, "resolution", "grown heaps", keeps)) res_heapdef_served();
|
||||
}
|
||||
|
||||
// The player camera's pack with its stick speeds multiplied by TOTK_STICK_SENS (camera_speed.c). The base is an
|
||||
// overlay that replaces the pack (a mod's camera stays), else the RomFS original; the dictionary is the RomFS's own.
|
||||
#define CAMPACK "Pack/Actor/PlayerCamera.pack.zs"
|
||||
static void camera_overlay(void) {
|
||||
if (cam_speed_wanted() == 1.0f) return;
|
||||
static const char keeps[] = "the camera keeps its own speed";
|
||||
Overlay *base, *dic_base;
|
||||
size_t n, dn;
|
||||
void *in = read_served(CAMPACK, &base, &n, keeps), *out = NULL;
|
||||
void *dic = in ? read_served("Pack/ZsDic.pack.zs", &dic_base, &dn, keeps) : NULL;
|
||||
size_t out_n = 0;
|
||||
int ok = in && dic && cam_pack_build(in, n, dic, dn, &out, &out_n);
|
||||
free(in);
|
||||
free(dic);
|
||||
if (ok && serve_generated(CAMPACK, base, out, out_n, "camera", "camera speed", keeps)) cam_pack_served();
|
||||
}
|
||||
|
||||
// The button prompts' folders (input_prompts_folder) and whether each had a manifest next to romfs-index.bin at start;
|
||||
// the one the settings asked for at start, the one overlay_init loaded (NULL: the game's own prompts) and why the first
|
||||
// did not load (ROMFS_PROMPTS_*). The menu compares them with the settings in effect (ui/menu.cpp prompts_note).
|
||||
static struct { const char *name; int ready; } prompts_folders[2];
|
||||
static const char *prompts_wanted, *prompts_loaded;
|
||||
static int prompts_off;
|
||||
|
||||
const char *romfs_prompts(void) { return prompts_loaded; }
|
||||
|
||||
int romfs_prompts_ready(const char *folder) {
|
||||
for (int i = 0; i < 2; i++)
|
||||
if (folder && prompts_folders[i].name && !strcmp(prompts_folders[i].name, folder)) return prompts_folders[i].ready;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int romfs_prompts_off(const char *folder) {
|
||||
return folder && prompts_wanted && !strcmp(folder, prompts_wanted) ? prompts_off : ROMFS_PROMPTS_OK;
|
||||
}
|
||||
|
||||
// The first RomFS file that the overlays [a, b) and [c, d) both replace, or NULL.
|
||||
static const char *overlay_clash(size_t a, size_t b, size_t c, size_t d) {
|
||||
for (size_t i = a; i < b; i++)
|
||||
for (size_t j = c; j < d; j++)
|
||||
if (overlays[i].index != ADDED && overlays[i].index == overlays[j].index &&
|
||||
strcmp(overlays[i].name, restbl_rel)) // restbl_overlay merges both tables
|
||||
return overlays[i].name;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Drops the overlays [a, b), the ones after them moving down.
|
||||
static void overlay_drop(size_t a, size_t b) {
|
||||
for (size_t k = a; k < b; k++) {
|
||||
free((void *)overlays[k].path);
|
||||
free((void *)overlays[k].name);
|
||||
}
|
||||
memmove(overlays + a, overlays + b, (noverlay - b) * sizeof *overlays);
|
||||
noverlay -= b - a;
|
||||
}
|
||||
|
||||
static void overlay_init(void) {
|
||||
// TOTK_ROMFS_PATCHES unset: romfs-patches next to romfs-index.bin if it has a manifest (tools/make_romfs_patches.py).
|
||||
// A game resolution that is not 16:9 takes romfs-patches-<w>x<h> instead when it is there: the same overlay with
|
||||
// the UI laid out for that shape (make_romfs_patches.py --aspect), when the game version has the camera aspect patch
|
||||
// the layout goes with (res_wants_wide_overlay).
|
||||
// With pad_layout = xbox_prompts, romfs-patches-xbox in its place: the same patches with the A/B and X/Y prompts
|
||||
// swapped (make_romfs_patches.py --xbox-prompts). The game reads the archives once at boot, so the prompts stay
|
||||
// until restart even though the layout itself changes live. Each folder replaces romfs-patches as a whole and the
|
||||
// setup builds no wide folder with Xbox prompts, so a wide game resolution keeps the Switch prompts.
|
||||
char def[2100], xbox[2100];
|
||||
const char *patches = cfg_get("TOTK_ROMFS_PATCHES");
|
||||
// With prompts = xbox, the Xbox prompts' folder for the pad layout in its place (input_prompts_folder): the same
|
||||
// patches with Xbox button icons (make_romfs_patches.py --prompts). The game reads the archives once at boot, so
|
||||
// the prompts stay until restart even though the layout itself changes live. Each folder replaces romfs-patches as
|
||||
// a whole and the setup builds no folder of another shape with Xbox prompts, so a game resolution that is not 16:9
|
||||
// keeps the Switch prompts. So does a RomFS mod (the TOTK_ROMFS_MODS manifest or a player mod) that replaces one of
|
||||
// the prompts' fonts: romfs-patches loads in their place, so the mod's file is not served beside prompts files made
|
||||
// for the game's own. A mod's resource size table is no clash: restbl_overlay merges it with the prompts' one.
|
||||
char def[2100], path[2100];
|
||||
const char *patches = cfg_get("TOTK_ROMFS_PATCHES"), *mods = cfg_get("TOTK_ROMFS_MODS");
|
||||
int wide = 0;
|
||||
unsigned out[2];
|
||||
res_wanted(out);
|
||||
@@ -403,33 +813,64 @@ static void overlay_init(void) {
|
||||
"(tools/make_romfs_patches.py --aspect %ux%u)", out[0], out[1], out[0], out[1]);
|
||||
}
|
||||
}
|
||||
xbox_prompts_ready =
|
||||
snprintf(xbox, sizeof xbox, "%s/romfs-patches-xbox/manifest.tsv", index_dir) < (int)sizeof xbox &&
|
||||
plat_file_exists(xbox);
|
||||
int want_xbox = !strcasecmp(cfg_value("TOTK_PAD_LAYOUT"), "xbox_prompts");
|
||||
if (!patches && want_xbox && xbox_prompts_ready) {
|
||||
xbox[strlen(xbox) - strlen("/manifest.tsv")] = 0;
|
||||
xbox_prompts_active = overlay_load(xbox, 1);
|
||||
if (xbox_prompts_active) rt_log("RomFS overlay: Xbox button prompts (%s)", xbox);
|
||||
else rt_log("RomFS overlay: %s failed its check (run the setup again); romfs-patches loads instead", xbox);
|
||||
} else if (want_xbox) {
|
||||
rt_log("RomFS overlay: pad_layout = xbox_prompts, but %s; Switch prompts stay",
|
||||
wide ? "the game resolution takes the wide UI layout"
|
||||
: patches ? "TOTK_ROMFS_PATCHES is set"
|
||||
: "romfs-patches-xbox has no manifest.tsv (run the setup again)");
|
||||
static const char *const layouts[2] = {"nintendo", "xbox"}; // one of each folder
|
||||
for (int i = 0; i < 2; i++) {
|
||||
prompts_folders[i].name = input_prompts_folder(PROMPTS_XBOX, layouts[i]);
|
||||
prompts_folders[i].ready =
|
||||
snprintf(path, sizeof path, "%s/%s/manifest.tsv", index_dir, prompts_folders[i].name) < (int)sizeof path &&
|
||||
plat_file_exists(path);
|
||||
}
|
||||
if (!patches && !xbox_prompts_active && snprintf(def, sizeof def, "%s/romfs-patches/manifest.tsv", index_dir) < (int)sizeof def &&
|
||||
plat_file_exists(def)) {
|
||||
def[strlen(def) - strlen("/manifest.tsv")] = 0;
|
||||
patches = def;
|
||||
const char *layout = cfg_value("TOTK_PAD_LAYOUT");
|
||||
prompts_wanted = input_prompts_folder(input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout), layout);
|
||||
prompts_off = !prompts_wanted ? ROMFS_PROMPTS_OK
|
||||
: wide ? ROMFS_PROMPTS_SHAPE
|
||||
: patches ? ROMFS_PROMPTS_PATCHES
|
||||
: !romfs_prompts_ready(prompts_wanted) ? ROMFS_PROMPTS_MISSING
|
||||
: ROMFS_PROMPTS_OK;
|
||||
restbl_find();
|
||||
size_t first = noverlay;
|
||||
if (prompts_wanted && prompts_off == ROMFS_PROMPTS_OK) {
|
||||
snprintf(path, sizeof path, "%s/%s", index_dir, prompts_wanted); // fits: its manifest path did
|
||||
if (overlay_load(path, 1, 0)) {
|
||||
prompts_loaded = prompts_wanted;
|
||||
rt_log("RomFS overlay: Xbox button prompts (%s)", path);
|
||||
} else {
|
||||
prompts_off = ROMFS_PROMPTS_FAILED;
|
||||
rt_log("RomFS overlay: %s failed its check (run the setup again); romfs-patches loads instead", path);
|
||||
}
|
||||
} else if (prompts_wanted) {
|
||||
rt_log("RomFS overlay: prompts = xbox, but %s; Switch prompts stay",
|
||||
prompts_off == ROMFS_PROMPTS_SHAPE ? "the game resolution takes its own UI layout"
|
||||
: prompts_off == ROMFS_PROMPTS_PATCHES ? "TOTK_ROMFS_PATCHES is set"
|
||||
: "their folder has no manifest.tsv (run the setup again)");
|
||||
}
|
||||
const char *dirs[2] = {patches, cfg_get("TOTK_ROMFS_MODS")};
|
||||
for (int i = 0; i < 2; i++)
|
||||
if (dirs[i] && *dirs[i] && strcmp(dirs[i], "0")) overlay_load(dirs[i], 0);
|
||||
size_t prompts_end = noverlay;
|
||||
char plain[2100]; // romfs-patches, when it has a manifest
|
||||
int plain_ok = snprintf(plain, sizeof plain, "%s/romfs-patches/manifest.tsv", index_dir) < (int)sizeof plain &&
|
||||
plat_file_exists(plain);
|
||||
if (plain_ok) plain[strlen(plain) - strlen("/manifest.tsv")] = 0;
|
||||
if (!patches && !prompts_loaded && plain_ok) patches = plain;
|
||||
if (patches && *patches && strcmp(patches, "0")) overlay_load(patches, 0, 0);
|
||||
size_t mods_first = noverlay;
|
||||
if (mods && *mods && strcmp(mods, "0")) overlay_load(mods, 0, 1);
|
||||
mods_overlay_init();
|
||||
const char *clash = prompts_loaded ? overlay_clash(first, prompts_end, mods_first, noverlay) : NULL;
|
||||
if (clash) {
|
||||
rt_log("RomFS overlay: a RomFS mod replaces %s, which the Xbox prompts change too; Switch prompts stay", clash);
|
||||
overlay_drop(first, prompts_end);
|
||||
prompts_loaded = NULL;
|
||||
prompts_off = ROMFS_PROMPTS_MODS;
|
||||
if (plain_ok) overlay_load(plain, 0, 0);
|
||||
}
|
||||
restbl_collect();
|
||||
overlay_contest();
|
||||
mods_overlay_log();
|
||||
restbl_overlay();
|
||||
heapdef_overlay();
|
||||
camera_overlay();
|
||||
added_init();
|
||||
if (noverlay) qsort(overlays, noverlay, sizeof *overlays, overlay_order);
|
||||
for (size_t k = 1; k < noverlay; k++)
|
||||
for (size_t k = 1; k < noverlay; k++) // overlay_contest left one per file; the generated ones replace in place
|
||||
if (overlays[k].index == overlays[k - 1].index)
|
||||
rt_fatal("RomFS overlay: %s and %s replace the same file %s", overlays[k - 1].path, overlays[k].path,
|
||||
overlays[k].name);
|
||||
@@ -551,6 +992,8 @@ const char *romfs_image_path(void) { return image_file; }
|
||||
|
||||
size_t romfs_overlay_count(void) { return noverlay; }
|
||||
|
||||
int romfs_patches_loaded(void) { return patches_loaded; }
|
||||
|
||||
const char *romfs_overlay(size_t k, const char **file, uint64_t *size, int *is_added) {
|
||||
if (k >= noverlay) return NULL;
|
||||
*file = overlays[k].path;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#version 450
|
||||
// Screen-space contact shadows (runtime/vk_contact.c). Each pixel of the game's lit scene colour marches a short way
|
||||
// toward the sun through the linear depth target; when the ray passes behind something close in front of it, the pixel
|
||||
// is in a contact shadow the shadow maps are too coarse to show, and its colour is darkened.
|
||||
layout(local_size_x = 8, local_size_y = 8) in;
|
||||
|
||||
layout(binding = 0) uniform sampler2D lin_depth; // the game's linear depth: view distance / far
|
||||
layout(binding = 1, r11f_g11f_b10f) uniform image2D scene; // the lit scene colour, darkened in place
|
||||
|
||||
layout(push_constant) uniform Params {
|
||||
vec4 proj; // projection x scale, y scale, far plane, strength (0..1)
|
||||
vec4 light; // view-space direction toward the sun (xyz), march length in metres at 10 m (w)
|
||||
vec4 size; // width, height, 1/width, 1/height
|
||||
vec4 opt; // x: debug (1: write the shadow factor as grey)
|
||||
} pc;
|
||||
|
||||
vec3 view_pos(vec2 uv, float z) { // z: distance in front of the camera
|
||||
vec2 ndc = uv * 2.0 - 1.0;
|
||||
return vec3(ndc.x * z / pc.proj.x, -ndc.y * z / pc.proj.y, -z);
|
||||
}
|
||||
|
||||
vec2 to_uv(vec3 p) {
|
||||
float z = -p.z;
|
||||
return vec2(0.5 + 0.5 * p.x * pc.proj.x / z, 0.5 - 0.5 * p.y * pc.proj.y / z);
|
||||
}
|
||||
|
||||
float ign(vec2 px) { // interleaved gradient noise: a per-pixel start offset that hides the step pattern
|
||||
return fract(52.9829189 * fract(dot(px, vec2(0.06711056, 0.00583715))));
|
||||
}
|
||||
|
||||
void main() {
|
||||
ivec2 px = ivec2(gl_GlobalInvocationID.xy);
|
||||
if (px.x >= int(pc.size.x) || px.y >= int(pc.size.y)) return;
|
||||
float d = texelFetch(lin_depth, px, 0).x;
|
||||
if (d <= 0.0 || d >= 0.999) return; // nothing drawn, or the sky
|
||||
float z = d * pc.proj.z;
|
||||
if (z > 80.0) return;
|
||||
vec2 uv = (vec2(px) + 0.5) * pc.size.zw;
|
||||
vec3 p = view_pos(uv, z);
|
||||
// the march grows with distance, as one pixel covers more ground; thickness keeps thin blades from hiding the sky
|
||||
float len = pc.light.w * clamp(z / 10.0, 0.4, 3.0);
|
||||
float thick = 0.15 + z * 0.01;
|
||||
const int STEPS = 16;
|
||||
float step_len = len / float(STEPS), t = step_len * (0.25 + ign(vec2(px)));
|
||||
float occ = 0.0;
|
||||
for (int i = 0; i < STEPS; i++, t += step_len) {
|
||||
vec3 q = p + pc.light.xyz * t;
|
||||
if (q.z >= -0.05) break;
|
||||
vec2 quv = to_uv(q);
|
||||
if (any(lessThan(quv, vec2(0.0))) || any(greaterThan(quv, vec2(1.0)))) break;
|
||||
float sz = texelFetch(lin_depth, ivec2(quv * pc.size.xy), 0).x * pc.proj.z;
|
||||
float delta = -q.z - sz; // how far the scene at that pixel is in front of the ray point
|
||||
if (delta > 0.02 + z * 0.002 && delta < thick) {
|
||||
occ = 1.0 - float(i) / float(STEPS); // a hit close to the pixel shadows more than one at the end
|
||||
break;
|
||||
}
|
||||
}
|
||||
float fade = 1.0 - smoothstep(40.0, 80.0, z);
|
||||
float k = 1.0 - pc.proj.w * occ * fade;
|
||||
if (pc.opt.x > 0.5) {
|
||||
imageStore(scene, px, vec4(k, k, k, 1.0));
|
||||
return;
|
||||
}
|
||||
if (k >= 0.999) return;
|
||||
vec4 c = imageLoad(scene, px);
|
||||
imageStore(scene, px, vec4(c.rgb * k, c.a));
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#version 450
|
||||
// Camera motion vectors for DLAA (vk_dlaa.c mv_pass): each pixel's depth is taken back to the world through this
|
||||
// frame's view-projection and forward through last frame's; the vector is last frame's window position minus this
|
||||
// frame's, in render pixels, without the jitter (DLSS's default convention). Objects that move on their own get only
|
||||
// the camera's motion.
|
||||
// The camera block is TotK's scene uniform block (bound as c4 in the scene's vertex shaders): vec4 rows 3-6 are the
|
||||
// view-projection, rows of a row-major 4x4 with OpenGL clip depth (-w..w). The replay's viewports have negative
|
||||
// heights, so window y grows downwards from ndc y = +1, and depth is z_ndc * 0.5 + 0.5.
|
||||
layout(local_size_x = 8, local_size_y = 8) in;
|
||||
layout(binding = 0) uniform sampler2D depth_tex;
|
||||
layout(binding = 1, rg16f) uniform writeonly image2D mv_out;
|
||||
layout(binding = 2, std140) uniform Cur { vec4 r[16]; } cur;
|
||||
layout(binding = 3, std140) uniform Prev { vec4 r[16]; } prev;
|
||||
layout(push_constant) uniform P {
|
||||
vec2 size; // render size in pixels
|
||||
vec2 jitter; // this frame's jitter, pixels, as the scene was drawn (window x right, y down)
|
||||
} pc;
|
||||
|
||||
mat4 vp(vec4 a, vec4 b, vec4 c, vec4 d) { return transpose(mat4(a, b, c, d)); }
|
||||
|
||||
void main() {
|
||||
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
|
||||
if (p.x >= int(pc.size.x) || p.y >= int(pc.size.y)) return;
|
||||
float d = texelFetch(depth_tex, p, 0).r;
|
||||
vec2 win = vec2(p) + 0.5 - pc.jitter; // where this pixel's surface sits without the jitter
|
||||
vec4 ndc = vec4(win.x / pc.size.x * 2.0 - 1.0, 1.0 - win.y / pc.size.y * 2.0, d * 2.0 - 1.0, 1.0);
|
||||
mat4 c = vp(cur.r[3], cur.r[4], cur.r[5], cur.r[6]);
|
||||
mat4 q = vp(prev.r[3], prev.r[4], prev.r[5], prev.r[6]);
|
||||
vec4 world = inverse(c) * ndc;
|
||||
vec2 mv = vec2(0.0);
|
||||
if (abs(world.w) > 1e-12) {
|
||||
vec4 clip = q * (world / world.w);
|
||||
if (clip.w > 1e-6) {
|
||||
vec2 pn = clip.xy / clip.w;
|
||||
vec2 pwin = vec2((pn.x * 0.5 + 0.5) * pc.size.x, (0.5 - pn.y * 0.5) * pc.size.y);
|
||||
mv = pwin - win;
|
||||
}
|
||||
}
|
||||
imageStore(mv_out, p, vec4(mv, 0.0, 0.0));
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#version 450
|
||||
// HDR10 encode on the present (runtime/vk_hdr.c vk_hdr_encode): the game's picture, crop rectangle at offset, linear
|
||||
// Rec. 709 with the HUD's white at 1.0 (an sRGB view decodes the 8-bit window texture), into an RGBA16F image of PQ
|
||||
// code values in Rec. 2020 that FSR 1 or the blit then scale into the HDR10 swapchain. p.x: nits of 1.0, p.y: the
|
||||
// display's peak in nits, p.z: 1 when the source holds sRGB-encoded values in a UNORM format.
|
||||
// PQ: SMPTE ST 2084; the Rec. 709 to Rec. 2020 matrix: ITU-R BT.2087.
|
||||
layout(local_size_x = 8, local_size_y = 8) in;
|
||||
layout(binding = 0) uniform sampler2D src;
|
||||
layout(binding = 1, rgba16f) uniform writeonly image2D dst;
|
||||
layout(push_constant) uniform P {
|
||||
ivec2 offset;
|
||||
ivec2 size;
|
||||
vec4 p;
|
||||
} pc;
|
||||
|
||||
vec3 srgb_decode(vec3 c) {
|
||||
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), greaterThan(c, vec3(0.04045)));
|
||||
}
|
||||
|
||||
vec3 pq(vec3 nits) {
|
||||
const float m1 = 0.1593017578125, m2 = 78.84375, c1 = 0.8359375, c2 = 18.8515625, c3 = 18.6875;
|
||||
vec3 y = pow(clamp(nits / 10000.0, 0.0, 1.0), vec3(m1));
|
||||
return pow((c1 + c2 * y) / (1.0 + c3 * y), vec3(m2));
|
||||
}
|
||||
|
||||
void main() {
|
||||
ivec2 at = ivec2(gl_GlobalInvocationID.xy);
|
||||
if (any(greaterThanEqual(at, pc.size))) return;
|
||||
vec3 c = max(texelFetch(src, at + pc.offset, 0).rgb, vec3(0.0));
|
||||
if (pc.p.z > 0.5) c = srgb_decode(clamp(c, 0.0, 1.0));
|
||||
const mat3 to2020 = mat3(0.6274, 0.0691, 0.0164, 0.3293, 0.9195, 0.0880, 0.0433, 0.0114, 0.8956);
|
||||
vec3 nits = min(max(to2020 * c, vec3(0.0)) * pc.p.x, vec3(pc.p.y));
|
||||
imageStore(dst, at, vec4(pq(nits), 1.0));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#version 450
|
||||
// One triangle over the whole target, for hdr_ui.frag (runtime/vk_hdr.c vk_hdr_ui_compose).
|
||||
layout(location = 0) out vec2 uv;
|
||||
|
||||
void main() {
|
||||
uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||
gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#version 450
|
||||
// The game's tonemap pass for HDR output (runtime/vk_hdr.c vk_hdr_shader_swap puts it in place of the translated
|
||||
// shader when TOTK_HDR=1). Same inputs as the game's: fp_t_tcb_8 (bloom, clamped to 10000 as the game does) plus
|
||||
// fp_t_tcb_A (the scene), weights in c1[0] and the saturation grade in c3[0]. The game maps luminance and each channel
|
||||
// with 1 - e^-x, blends the two by the luminance curve squared, clamps to 0..1 and grades saturation. Here the curve is
|
||||
// the game's up to PIVOT and a straight line with its slope above, so mid-greys look as in SDR and highlights keep
|
||||
// going; the grade works on the colour divided by its largest channel above 1; a roll-off on the largest channel stops
|
||||
// at the display's peak. The output is relative to the HUD's white (1.0), which the present maps to TOTK_HDR_UI_NITS.
|
||||
// Approach after RenoDX's TotK mod by souperman9 and Musa (github.com/clshortfuse/renodx, src/games/totk, MIT): its
|
||||
// "extended vanilla" curve.
|
||||
// Bindings are those of the game's translated shader: vk_hdr.c rewrites them from the original's names.
|
||||
layout(location = 0) in vec4 uv;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
layout(set = 0, binding = 0, std140) uniform SupportBuffer {
|
||||
vec4 pad[90];
|
||||
vec4 hdr; // byte 1440 (vk_exec_draw.c support_fill): game white / HUD white, peak / game white
|
||||
} support_buffer;
|
||||
layout(set = 0, binding = 1) uniform sampler2D fp_t_tcb_8;
|
||||
layout(set = 0, binding = 2) uniform sampler2D fp_t_tcb_A;
|
||||
layout(set = 0, binding = 3, std140) uniform C1 { vec4 v[4096]; } fp_c1;
|
||||
layout(set = 0, binding = 4, std140) uniform C3 { vec4 v[4096]; } fp_c3;
|
||||
|
||||
const float PIVOT = 0.18;
|
||||
|
||||
float curve(float x) { // the game's 1 - e^-x below PIVOT, its tangent there above
|
||||
float y0 = 1.0 - exp(-PIVOT);
|
||||
return x <= PIVOT ? 1.0 - exp(-x) : y0 + exp(-PIVOT) * (x - PIVOT);
|
||||
}
|
||||
|
||||
vec3 curve3(vec3 x) { return vec3(curve(x.r), curve(x.g), curve(x.b)); }
|
||||
|
||||
float maxc(vec3 c) { return max(c.r, max(c.g, c.b)); }
|
||||
|
||||
void main() {
|
||||
vec3 x = min(texture(fp_t_tcb_8, uv.xy).rgb, vec3(10000.0)) + texture(fp_t_tcb_A, uv.xy).rgb;
|
||||
vec4 w = fp_c1.v[0], g = fp_c3.v[0];
|
||||
float game = support_buffer.hdr.x > 0.0 ? support_buffer.hdr.x : 1.0;
|
||||
float peak = support_buffer.hdr.y > 1.0 ? support_buffer.hdr.y : 1.0;
|
||||
|
||||
float lum = max(x.r * 0.2989 + x.g * w.x + x.b * w.y, 0.0);
|
||||
float sdr = 1.0 - exp(-lum);
|
||||
vec3 by_lum = lum > 0.0 ? x * (curve(lum) / lum) : x;
|
||||
vec3 c = max(mix(by_lum, curve3(x), sdr * sdr), vec3(0.0));
|
||||
|
||||
// the game's saturation grade, on the colour scaled into 0..1 by its largest channel
|
||||
float s = max(maxc(c), 1.0);
|
||||
vec3 n = c / s;
|
||||
float m = maxc(n);
|
||||
float d = n.r * 0.6666667 + (n.g + n.b) * w.z - 1.0;
|
||||
float grade = g.y * (1.0 - d * d) + g.x;
|
||||
c = s * ((n - m) * grade + m);
|
||||
|
||||
// roll-off of the largest channel from half the peak to the peak
|
||||
float top = maxc(c), knee = 0.5 * peak;
|
||||
if (top > knee) {
|
||||
float r = peak - knee;
|
||||
c *= (knee + r * (1.0 - exp(-(top - knee) / r))) / top;
|
||||
}
|
||||
color = vec4(max(c, vec3(0.0)) * game, 0.0);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#version 450
|
||||
// The in-game menu on an HDR10 swapchain (runtime/vk_hdr.c vk_hdr_ui_compose): ImGui drew it into an RGBA8 image,
|
||||
// premultiplied sRGB colour over a clear 0; this turns each pixel back into its colour, puts its white at p.x nits and
|
||||
// writes the PQ code with its coverage, which the pipeline blends over the picture (in PQ code values, as the menu
|
||||
// blends in sRGB code values on an SDR swapchain). PQ: SMPTE ST 2084; Rec. 709 to Rec. 2020: ITU-R BT.2087.
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 0) out vec4 color;
|
||||
layout(binding = 0) uniform sampler2D ui;
|
||||
layout(push_constant) uniform P { vec4 p; } pc;
|
||||
|
||||
vec3 srgb_decode(vec3 c) {
|
||||
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), greaterThan(c, vec3(0.04045)));
|
||||
}
|
||||
|
||||
vec3 pq(vec3 nits) {
|
||||
const float m1 = 0.1593017578125, m2 = 78.84375, c1 = 0.8359375, c2 = 18.8515625, c3 = 18.6875;
|
||||
vec3 y = pow(clamp(nits / 10000.0, 0.0, 1.0), vec3(m1));
|
||||
return pow((c1 + c2 * y) / (1.0 + c3 * y), vec3(m2));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 u = texture(ui, uv);
|
||||
if (u.a <= 0.0) discard;
|
||||
vec3 c = srgb_decode(clamp(u.rgb / u.a, 0.0, 1.0));
|
||||
const mat3 to2020 = mat3(0.6274, 0.0691, 0.0164, 0.3293, 0.9195, 0.0880, 0.0433, 0.0114, 0.8956);
|
||||
color = vec4(pq(max(to2020 * c, vec3(0.0)) * pc.p.x), u.a);
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
// tables. Not modelled: FPSR cumulative exception bits and QC, FPCR trap enables, FP16 arithmetic (ARMv8.2).
|
||||
// FPCR.AHP is honoured by the half-precision conversions. Host assumptions: IEEE binary32/64, MXCSR in
|
||||
// round-to-nearest outside the slow paths, with FTZ|DAZ equal to FPCR.FZ (cpu.h rt_fpcr_sync keeps it so); fma must be
|
||||
// correctly rounded (build x86 with -mfma).
|
||||
// correctly rounded: -march=x86-64-v3 has the FMA instruction, and the v2 build links runtime/fma.c for it.
|
||||
#pragma once
|
||||
#include <fenv.h>
|
||||
#include <immintrin.h>
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// Amiibo / NFC menu page (Dear ImGui): browser and scan controls.
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <filesystem>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "ui/amiibo_page.h"
|
||||
#include "ui/option_rows.h"
|
||||
|
||||
extern "C" {
|
||||
#include "amiibo.h"
|
||||
#include "cfg.h"
|
||||
#include "platform/platform.h"
|
||||
#include "ui_text.h"
|
||||
}
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct FileEntry {
|
||||
std::string name;
|
||||
bool is_dir;
|
||||
};
|
||||
|
||||
static const char kDefaultAmiiboDir[] = "./amiibo";
|
||||
static std::string g_current_dir = kDefaultAmiiboDir;
|
||||
static char g_dir_input[512] = "./amiibo";
|
||||
static std::vector<FileEntry> g_entries;
|
||||
static int g_selected_entry = -1;
|
||||
// The last action: a catalog id and the file or folder it names, formatted when drawn (the menu language can change)
|
||||
static UiTextId g_status_id = UI_MENU_AMIIBO_READY;
|
||||
static std::string g_status_arg;
|
||||
|
||||
static void set_status(UiTextId id, const std::string &arg = std::string()) {
|
||||
g_status_id = id;
|
||||
g_status_arg = arg;
|
||||
}
|
||||
|
||||
// Loads a dump and puts it on the reader; the status names the file.
|
||||
static void tap(const std::string &dir, const std::string &name) {
|
||||
std::string full_path = (fs::path(dir) / name).string();
|
||||
set_status(amiibo_scan_and_tap(full_path.c_str()) ? UI_MENU_AMIIBO_TAPPED : UI_MENU_AMIIBO_LOAD_FAILED, name);
|
||||
}
|
||||
|
||||
static bool iequals(std::string_view a, std::string_view b) {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (size_t i = 0; i < a.size(); ++i) {
|
||||
if (std::tolower(static_cast<unsigned char>(a[i])) !=
|
||||
std::tolower(static_cast<unsigned char>(b[i]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void set_current_dir(const std::string &path) {
|
||||
g_current_dir = path;
|
||||
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
|
||||
cfg_set("TOTK_AMIIBO_DIR", g_current_dir.c_str());
|
||||
}
|
||||
|
||||
static void refresh_dir(void) {
|
||||
g_entries.clear();
|
||||
g_selected_entry = -1;
|
||||
|
||||
std::error_code ec;
|
||||
fs::path dir_path(g_current_dir);
|
||||
|
||||
// A missing folder lists nothing and says so; it is not replaced by another one, and opening the tab writes no
|
||||
// setting (the folder is saved only when the player sets one).
|
||||
if (!fs::exists(dir_path, ec) || !fs::is_directory(dir_path, ec)) {
|
||||
set_status(UI_MENU_AMIIBO_NO_DIR, g_current_dir);
|
||||
return;
|
||||
}
|
||||
if (g_status_id == UI_MENU_AMIIBO_NO_DIR) set_status(UI_MENU_AMIIBO_READY);
|
||||
|
||||
// Add parent navigation if not at root
|
||||
if (dir_path.has_parent_path() && dir_path != dir_path.parent_path()) {
|
||||
g_entries.push_back({"..", true});
|
||||
}
|
||||
|
||||
for (const auto &entry : fs::directory_iterator(dir_path, fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (ec) break;
|
||||
std::error_code status_ec;
|
||||
bool is_dir = entry.is_directory(status_ec);
|
||||
if (status_ec) continue;
|
||||
|
||||
std::string filename = entry.path().filename().string();
|
||||
if (filename.empty() || filename == "." || filename == "..") continue;
|
||||
|
||||
if (!is_dir) {
|
||||
std::string ext = entry.path().extension().string();
|
||||
if (!iequals(ext, ".bin") && !iequals(ext, ".nfc")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
g_entries.push_back({std::move(filename), is_dir});
|
||||
}
|
||||
|
||||
std::sort(g_entries.begin(), g_entries.end(), [](const FileEntry &a, const FileEntry &b) {
|
||||
if (a.name == "..") return true;
|
||||
if (b.name == "..") return false;
|
||||
if (a.is_dir != b.is_dir) return a.is_dir;
|
||||
return a.name < b.name;
|
||||
});
|
||||
}
|
||||
|
||||
void tab_amiibo(float scale) {
|
||||
(void)scale; // the rows take ImGui's own sizes, already scaled with the font
|
||||
static bool s_inited = false;
|
||||
if (!s_inited) {
|
||||
const char *cfg_dir = cfg_get("TOTK_AMIIBO_DIR");
|
||||
if (cfg_dir && *cfg_dir) {
|
||||
g_current_dir = cfg_dir;
|
||||
} else {
|
||||
g_current_dir = kDefaultAmiiboDir;
|
||||
}
|
||||
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
|
||||
refresh_dir();
|
||||
s_inited = true;
|
||||
}
|
||||
|
||||
// Two panes: Left (Directory Browser & Tag List), Right (Amiibo Tag Details & Controls)
|
||||
const float content_h = ImGui::GetContentRegionAvail().y - opt_bottom(); // Continue and Quit stay visible
|
||||
const float left_w = ImGui::GetContentRegionAvail().x * 0.58f;
|
||||
const float right_w = ImGui::GetContentRegionAvail().x - left_w - ImGui::GetStyle().ItemSpacing.x;
|
||||
|
||||
// ---- Left Pane: Explorer & List ----
|
||||
ImGui::BeginChild("##amiibo_explorer", ImVec2(left_w, content_h), true);
|
||||
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_DIR));
|
||||
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
if (ImGui::InputText("##amiibo_dir_input", g_dir_input, sizeof(g_dir_input), ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
set_current_dir(g_dir_input);
|
||||
refresh_dir();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_OPEN))) {
|
||||
set_current_dir(g_dir_input);
|
||||
refresh_dir();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_DEFAULT_DIR))) {
|
||||
set_current_dir(kDefaultAmiiboDir);
|
||||
refresh_dir();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_REFRESH))) {
|
||||
refresh_dir();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("%s", menu_text(UI_MENU_AMIIBO_PICK));
|
||||
|
||||
ImGui::BeginChild("##file_list", ImVec2(0, 0), true);
|
||||
for (size_t i = 0; i < g_entries.size(); i++) {
|
||||
const auto &e = g_entries[i];
|
||||
std::string label = e.is_dir ? e.name + "/" : e.name;
|
||||
bool selected = ((int)i == g_selected_entry);
|
||||
|
||||
if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
g_selected_entry = (int)i;
|
||||
if (e.is_dir) {
|
||||
if (ImGui::IsMouseDoubleClicked(0) || ImGui::IsKeyPressed(ImGuiKey_GamepadFaceDown)) {
|
||||
if (e.name == "..") {
|
||||
fs::path cur(g_current_dir);
|
||||
if (cur.has_parent_path() && cur != cur.parent_path()) {
|
||||
set_current_dir(cur.parent_path().string());
|
||||
} else {
|
||||
set_current_dir(".");
|
||||
}
|
||||
} else {
|
||||
set_current_dir((fs::path(g_current_dir) / e.name).string());
|
||||
}
|
||||
refresh_dir();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (ImGui::IsMouseDoubleClicked(0)) tap(g_current_dir, e.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// ---- Right Pane: Controls & Status ----
|
||||
ImGui::BeginChild("##amiibo_controls", ImVec2(right_w, content_h), true);
|
||||
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_READER));
|
||||
ImGui::Separator();
|
||||
|
||||
AmiiboDeviceState state = amiibo_get_device_state();
|
||||
UiTextId state_id = UI_MENU_AMIIBO_STATE_IDLE;
|
||||
ImVec4 state_col = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
|
||||
switch (state) {
|
||||
case AMIIBO_DEV_STATE_INITIALIZED: state_id = UI_MENU_AMIIBO_STATE_IDLE; break;
|
||||
case AMIIBO_DEV_STATE_SEARCHING: state_id = UI_MENU_AMIIBO_STATE_SEARCHING; state_col = ImVec4(1.0f, 0.8f, 0.2f, 1.0f); break;
|
||||
case AMIIBO_DEV_STATE_TAG_FOUND: state_id = UI_MENU_AMIIBO_STATE_FOUND; state_col = ImVec4(0.3f, 0.9f, 0.3f, 1.0f); break;
|
||||
case AMIIBO_DEV_STATE_TAG_MOUNTED: state_id = UI_MENU_AMIIBO_STATE_MOUNTED; state_col = ImVec4(0.2f, 0.9f, 1.0f, 1.0f); break;
|
||||
case AMIIBO_DEV_STATE_UNAVAILABLE: state_id = UI_MENU_AMIIBO_STATE_OFF; state_col = ImVec4(0.9f, 0.3f, 0.3f, 1.0f); break;
|
||||
default: break;
|
||||
}
|
||||
ImGui::TextUnformatted(menu_text(UI_MENU_AMIIBO_STATE));
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(state_col, "%s", menu_text(state_id));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted(menu_text(UI_MENU_AMIIBO_FILE));
|
||||
if (g_selected_entry >= 0 && g_selected_entry < (int)g_entries.size() && !g_entries[g_selected_entry].is_dir) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.4f, 1.0f), "%s", g_entries[g_selected_entry].name.c_str());
|
||||
|
||||
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_TAP), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
||||
tap(g_current_dir, g_entries[g_selected_entry].name);
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", menu_text(UI_MENU_AMIIBO_NO_FILE));
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::Button(menu_text(UI_MENU_AMIIBO_TAP), ImVec2(ImGui::GetContentRegionAvail().x, 0));
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_REMOVE), ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
|
||||
amiibo_remove_tag();
|
||||
set_status(UI_MENU_AMIIBO_REMOVED);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
bool auto_remove = amiibo_get_auto_remove();
|
||||
if (ImGui::Checkbox(menu_text(UI_MENU_AMIIBO_AUTO_REMOVE), &auto_remove)) {
|
||||
amiibo_set_auto_remove(auto_remove);
|
||||
}
|
||||
if (auto_remove) {
|
||||
float delay = amiibo_get_auto_remove_delay_s();
|
||||
ImGui::TextUnformatted(menu_text(UI_MENU_AMIIBO_HOLD)); // above the slider: beside it the text is cut off
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (ImGui::SliderFloat("##amiibo_hold", &delay, 1.0f, 6.0f, menu_text(UI_MENU_AMIIBO_SECONDS))) {
|
||||
amiibo_set_auto_remove_delay_s(delay);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted(menu_text(UI_MENU_AMIIBO_TAG));
|
||||
if (amiibo_has_tag()) {
|
||||
const AmiiboInfo *info = amiibo_get_current_info();
|
||||
char character[8], uid[24];
|
||||
snprintf(character, sizeof character, "%02x%02x%02x", info->character_id[0], info->character_id[1],
|
||||
info->character_id[2]);
|
||||
snprintf(uid, sizeof uid, "%02x:%02x:%02x:%02x:%02x:%02x:%02x", info->uuid[0], info->uuid[1], info->uuid[2],
|
||||
info->uuid[3], info->uuid[4], info->uuid[5], info->uuid[6]);
|
||||
ImGui::Text(menu_text(UI_MENU_AMIIBO_TAG_FILE), info->filename);
|
||||
ImGui::Text(menu_text(UI_MENU_AMIIBO_TAG_ID), character, info->series_id, info->numbering_id, info->nfp_type);
|
||||
ImGui::Text(menu_text(UI_MENU_AMIIBO_TAG_UID), uid);
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", menu_text(UI_MENU_AMIIBO_NO_TAG));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
char status[512];
|
||||
snprintf(status, sizeof status, menu_text(g_status_id), g_status_arg.c_str());
|
||||
ImGui::TextDisabled(menu_text(UI_MENU_AMIIBO_STATUS), status);
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Amiibo / NFC menu page (ui/amiibo_page.cpp).
|
||||
#pragma once
|
||||
|
||||
void tab_amiibo(float scale);
|
||||
@@ -0,0 +1,66 @@
|
||||
// The Cheats tab of the in-game menu: the cheats of the mods' cheat files (cheats.c), a heading per mod in the Mods
|
||||
// tab's order, then a row per cheat in file order and a line per group label of the file; a cheat that cannot be
|
||||
// switched here is greyed out with the reason. While code cheats wait for a rebuild, a row on top restarts the game
|
||||
// into it.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "ui/option_rows.h"
|
||||
|
||||
extern "C" {
|
||||
#include "cheats.h"
|
||||
#include "ui_text.h"
|
||||
}
|
||||
|
||||
// Returns true when "Rebuild the game now" was chosen: the menu then asks to restart (ui/menu.cpp restart_confirm).
|
||||
extern "C" bool tab_cheats(float scale) {
|
||||
opt_begin("##cheats", scale);
|
||||
static char line[512], desc[768], buttons[192];
|
||||
int n = cheats_count();
|
||||
if (!n) {
|
||||
snprintf(line, sizeof line, menu_text(UI_MENU_CHEATS_NONE), cheats_file()[0] ? cheats_file() : "<build id>.txt");
|
||||
opt_text_str(line);
|
||||
opt_end();
|
||||
return false;
|
||||
}
|
||||
bool rebuild =
|
||||
cheats_rebuild_pending() && opt_button("##restart", UI_MENU_CHEATS_REBUILD, UI_MENU_CHEATS_REBUILD_DESC);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const CheatInfo *c = cheats_info(i);
|
||||
if (!i || strcmp(c->mod, cheats_info(i - 1)->mod)) {
|
||||
int listed = 0;
|
||||
for (int j = i; j < n && !strcmp(cheats_info(j)->mod, c->mod); j++)
|
||||
listed += cheats_info(j)->kind != CHEAT_HEADING;
|
||||
snprintf(line, sizeof line, menu_text(UI_MENU_CHEATS_MOD), c->mod, listed);
|
||||
opt_group_text(line);
|
||||
}
|
||||
UiTextId text = UI_MENU_CHEAT_CODE_DESC;
|
||||
switch (c->kind) {
|
||||
case CHEAT_HEADING:
|
||||
if (!strstr(c->name, "TID=")) opt_text_str(c->name); // the file's title line only names the game
|
||||
continue;
|
||||
case CHEAT_CODE: break;
|
||||
case CHEAT_KEY: text = UI_MENU_CHEAT_KEY_DESC; break;
|
||||
case CHEAT_DATA: text = UI_MENU_CHEAT_DATA_DESC; break;
|
||||
case CHEAT_MASTER: text = UI_MENU_CHEAT_MASTER_DESC, opt_disable_next(UI_MENU_CHEAT_MASTER); break;
|
||||
case CHEAT_RESTORE: text = UI_MENU_CHEAT_RESTORE_DESC, opt_disable_next(UI_MENU_CHEAT_RESTORE); break;
|
||||
case CHEAT_REBUILD: text = UI_MENU_CHEAT_REBUILD_DESC, opt_disable_next(UI_MENU_CHEAT_REBUILD); break;
|
||||
case CHEAT_UNSUPPORTED:
|
||||
text = UI_MENU_CHEAT_UNSUPPORTED_DESC, opt_disable_next(UI_MENU_CHEAT_UNSUPPORTED);
|
||||
break;
|
||||
}
|
||||
if (c->kind == CHEAT_KEY)
|
||||
snprintf(desc, sizeof desc, menu_text(text), cheats_buttons(c->buttons, buttons, sizeof buttons));
|
||||
else if (c->kind == CHEAT_UNSUPPORTED)
|
||||
snprintf(desc, sizeof desc, "%s %s", menu_text(text), c->note);
|
||||
else
|
||||
snprintf(desc, sizeof desc, "%s", menu_text(text));
|
||||
if (opt_toggle_text(c->key, c->name, desc, cheats_on(i) != 0)) {
|
||||
cheats_set(i, !cheats_on(i));
|
||||
opt_save_soon();
|
||||
}
|
||||
}
|
||||
opt_end();
|
||||
return rebuild;
|
||||
}
|
||||
+300
-26
@@ -30,13 +30,18 @@ static int g_font_bad;
|
||||
#pragma clang diagnostic ignored "-Wunused-function"
|
||||
#include "imstb_truetype.h"
|
||||
#pragma clang diagnostic pop
|
||||
#include "ui/amiibo_page.h"
|
||||
#include "ui/binds_page.h"
|
||||
#include "ui/option_rows.h"
|
||||
|
||||
extern "C" {
|
||||
#include "amiibo.h"
|
||||
#include "cheats.h"
|
||||
#include "cfg.h"
|
||||
#include "fs.h"
|
||||
#include "input.h"
|
||||
#include "input_map.h"
|
||||
#include "mods.h"
|
||||
#include "perfmon.h"
|
||||
#include "platform/platform.h"
|
||||
#include "sysinfo.h"
|
||||
@@ -49,10 +54,9 @@ void vk_queue_unlock(void);
|
||||
void res_sizes(unsigned out[2], unsigned scene[2]);
|
||||
double frame_step_limit_hz(void);
|
||||
extern const int rt_game_version;
|
||||
// fs.h (romfs.c): romfs-patches-xbox was next to romfs-index.bin at start, and was loaded.
|
||||
int romfs_xbox_prompts_ready(void);
|
||||
int romfs_xbox_prompts_active(void);
|
||||
extern const char rt_port_version[];
|
||||
void cheats_frame(uint64_t frame); // cheats.c
|
||||
bool tab_cheats(float scale); // ui/cheats_tab.cpp; true: restart into the rebuild
|
||||
}
|
||||
|
||||
// Roboto Medium (Apache-2.0, ui/fonts/LICENSE-Roboto.txt) with Latin and Cyrillic glyphs.
|
||||
@@ -266,7 +270,9 @@ static int vk_up(const MenuTarget *t) {
|
||||
|
||||
// Roboto Medium (Latin, Cyrillic, dashes and quotes) at 26 px per 1080 lines, with the glyphs of the Japanese, Korean
|
||||
// and Chinese language names merged in from a system font when there is one (each language is named in its own script).
|
||||
// Made again when the window height changes; the old texture goes after the queue is idle (CreateFontsTexture).
|
||||
// A catalog row also holds menu text (Simplified Chinese), so every string of the row goes into the builder: the merged
|
||||
// font must cover the whole menu of that language, not only its name. Made again when the window height changes; the old
|
||||
// texture goes after the queue is idle (CreateFontsTexture).
|
||||
static const ImWchar kRanges[] = {0x0020, 0x00FF, 0x0400, 0x052F, 0x2010, 0x2027, 0};
|
||||
static const struct { const char *lang; UiTextId name; } kCjk[] = {
|
||||
{"ja", UI_MENU_LANGNAME_JA}, {"ko", UI_MENU_LANGNAME_KO}, {"zh-Hans", UI_MENU_LANGNAME_ZH_HANS},
|
||||
@@ -310,6 +316,11 @@ extern "C" int menu_font_check(void) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The glyphs of every string of a catalog row (ui_text_visit), for the font that draws that language.
|
||||
static void add_row_glyphs(void *ctx, const char *text) {
|
||||
static_cast<ImFontGlyphRangesBuilder *>(ctx)->AddText(text);
|
||||
}
|
||||
|
||||
static int fonts_up(float scale) {
|
||||
enum { N = sizeof kCjk / sizeof *kCjk };
|
||||
ImFontAtlas *fa = ImGui::GetIO().Fonts;
|
||||
@@ -334,6 +345,7 @@ static int fonts_up(float scale) {
|
||||
merged++;
|
||||
ImFontGlyphRangesBuilder b;
|
||||
b.AddText(ui_text_in("en", kCjk[i].name));
|
||||
ui_text_visit(kCjk[i].lang, add_row_glyphs, &b);
|
||||
b.BuildRanges(&ranges[i]);
|
||||
ImFontConfig mc;
|
||||
mc.MergeMode = true;
|
||||
@@ -362,7 +374,7 @@ extern "C" void menu_vk_release(void) {
|
||||
// TOTK_MENU_TEST_SET=<frame>:<key>=<value>[,<key>=<value>...][;<frame>:...]: cfg_set of each key (TOTK_ prefix
|
||||
// optional) at that presented frame; changes live settings with no input, menu open or not. totk.ini is written only by
|
||||
// the pseudo-key SAVE=1 (cfg_save, as a row change does; a failed write shows the menu's save note).
|
||||
enum { TAB_SCREEN, TAB_GAME, TAB_SOUND, TAB_CONTROLS, TAB_MENU, TAB_ADVANCED, TAB_COUNT };
|
||||
enum { TAB_SCREEN, TAB_GAME, TAB_SOUND, TAB_CONTROLS, TAB_AMIIBO, TAB_MENU, TAB_ADVANCED, TAB_MODS, TAB_CHEATS, TAB_COUNT };
|
||||
enum { TEST_PAGE_FRAMES = 30, TEST_SETS = 16 };
|
||||
struct TestSet { uint64_t frame; char key[64], value[64]; };
|
||||
static struct {
|
||||
@@ -505,6 +517,27 @@ static void row_render_scale(void) {
|
||||
opt_choice_list("TOTK_RENDER_SCALE", UI_MENU_RENDER_SCALE, vals, names, N, UI_MENU_RENDER_SCALE_DESC);
|
||||
}
|
||||
|
||||
static void row_shadow_res(void) {
|
||||
static const char *const vals[] = {"game", "512", "768", "1024", "1536", "2048"};
|
||||
enum { N = sizeof vals / sizeof *vals };
|
||||
const char *names[N];
|
||||
names[0] = menu_text(UI_MENU_SHADOW_RES_GAME);
|
||||
for (int i = 1; i < N; i++) names[i] = vals[i];
|
||||
opt_choice_list("TOTK_SHADOW_RES", UI_MENU_SHADOW_RES, vals, names, N, UI_MENU_SHADOW_RES_DESC);
|
||||
}
|
||||
|
||||
static void row_shadow_scale(void) {
|
||||
static const char *const vals[] = {"1", "2", "3", "4"};
|
||||
static const char *const names[] = {"960 (1x)", "1920 (2x)", "2880 (3x)", "3840 (4x)"};
|
||||
opt_choice_list("TOTK_SHADOW_SCALE", UI_MENU_SHADOW_SCALE, vals, names, 4, UI_MENU_SHADOW_SCALE_DESC);
|
||||
}
|
||||
|
||||
static void row_shadow_range(void) {
|
||||
static const char *const vals[] = {"100", "150", "200", "300"};
|
||||
static const char *const names[] = {"160 m (1x)", "240 m (1.5x)", "320 m (2x)", "480 m (3x)"};
|
||||
opt_choice_list("TOTK_SHADOW_RANGE", UI_MENU_SHADOW_RANGE, vals, names, 4, UI_MENU_SHADOW_RANGE_DESC);
|
||||
}
|
||||
|
||||
static void row_gpu(void) {
|
||||
const char *vals[MAX_GPUS + 1], *names[MAX_GPUS + 1];
|
||||
char idx[MAX_GPUS][4];
|
||||
@@ -623,22 +656,37 @@ static void tab_screen(float scale) {
|
||||
opt_group(UI_MENU_GRAPHICS);
|
||||
row_output_res();
|
||||
row_render_scale();
|
||||
row_shadow_res();
|
||||
static const UiTextId up[] = {UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR};
|
||||
opt_choice("TOTK_UPSCALER", UI_MENU_UPSCALER, up, 2, UI_MENU_UPSCALER_DESC);
|
||||
opt_slider("TOTK_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
|
||||
static const UiTextId aspect[] = {UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH};
|
||||
opt_choice("TOTK_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
|
||||
opt_slider("TOTK_FOV", UI_MENU_FOV, 70, 140, UI_MENU_FOV_DESC, 5, "%.0f %%");
|
||||
static const char *const aniso[] = {"game", "2", "4", "8", "16"};
|
||||
const char *aniso_names[] = {menu_text(UI_MENU_ANISO_GAME), "2x", "4x", "8x", "16x"};
|
||||
opt_choice_list("TOTK_ANISO", UI_MENU_ANISO, aniso, aniso_names, 5, UI_MENU_ANISO_DESC);
|
||||
row_shadow_scale();
|
||||
row_shadow_range();
|
||||
opt_slider("TOTK_PCSS", UI_MENU_PCSS, 0, 200, UI_MENU_PCSS_DESC, 10, "%.0f %%");
|
||||
opt_slider("TOTK_CONTACT_SHADOWS", UI_MENU_CONTACT_SHADOWS, 0, 100, UI_MENU_CONTACT_SHADOWS_DESC, 10, "%.0f %%");
|
||||
opt_toggle("TOTK_DLAA", UI_MENU_DLAA, UI_MENU_DLAA_DESC);
|
||||
static const UiTextId dlss[] = {UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY, UI_MENU_DLSS_BALANCED,
|
||||
UI_MENU_DLSS_PERFORMANCE};
|
||||
opt_choice("TOTK_DLSS", UI_MENU_DLSS, dlss, 4, UI_MENU_DLSS_DESC);
|
||||
opt_toggle("TOTK_FXAA", UI_MENU_FXAA, UI_MENU_FXAA_DESC);
|
||||
rows_color();
|
||||
opt_group(UI_MENU_HDR_GROUP);
|
||||
opt_toggle("TOTK_HDR", UI_MENU_HDR, UI_MENU_HDR_DESC);
|
||||
opt_slider("TOTK_HDR_PEAK_NITS", UI_MENU_HDR_PEAK, 400, 10000, UI_MENU_HDR_PEAK_DESC, 50, "%.0f");
|
||||
opt_slider("TOTK_HDR_GAME_NITS", UI_MENU_HDR_GAME, 80, 500, UI_MENU_HDR_GAME_DESC, 5, "%.0f");
|
||||
opt_slider("TOTK_HDR_UI_NITS", UI_MENU_HDR_UI, 80, 500, UI_MENU_HDR_UI_DESC, 5, "%.0f");
|
||||
opt_end();
|
||||
}
|
||||
|
||||
static void tab_game(float scale) {
|
||||
opt_begin("##game", scale);
|
||||
row_fps_limit();
|
||||
opt_toggle("TOTK_REALTIME", UI_MENU_REALTIME, UI_MENU_REALTIME_DESC);
|
||||
opt_toggle("TOTK_CLOTH_REALTIME", UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC);
|
||||
opt_toggle("TOTK_HIGHFPS_FIX", UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC);
|
||||
row_game_lang();
|
||||
static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD};
|
||||
opt_choice("TOTK_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC);
|
||||
@@ -652,14 +700,11 @@ static void tab_sound(float scale) {
|
||||
opt_end();
|
||||
}
|
||||
|
||||
// The note under the layout. Xbox prompts (the Nintendo layout with the A/B and X/Y prompts swapped, romfs.c
|
||||
// overlay_init) change the buttons at once but the prompts only at the next start: "files missing" without
|
||||
// romfs-patches-xbox, "prompts change after restart" while the prompts in use differ from the choice. Automatic with a
|
||||
// gamepad connected: "in use: Xbox". NULL otherwise.
|
||||
// A confirmation is open (Controls: restart, Advanced: reset): B and Esc close it, not the menu; L / R stay on the page.
|
||||
static bool g_modal;
|
||||
|
||||
// The note under the layout: Automatic with a gamepad connected, the layout it picked ("in use: Xbox"). NULL otherwise.
|
||||
static const char *pad_layout_note(void) {
|
||||
bool want = !strcasecmp(cfg_value("TOTK_PAD_LAYOUT"), "xbox_prompts");
|
||||
if (want && !romfs_xbox_prompts_ready()) return menu_text(UI_MENU_PAD_LAYOUT_PROMPTS_MISSING);
|
||||
if (want != (romfs_xbox_prompts_active() != 0)) return menu_text(UI_MENU_PAD_LAYOUT_PROMPTS_RESTART);
|
||||
int got = input_pad_layout();
|
||||
if (got < 0 || strcmp(cfg_value("TOTK_PAD_LAYOUT"), "auto")) return nullptr;
|
||||
static char note[96];
|
||||
@@ -668,31 +713,146 @@ static const char *pad_layout_note(void) {
|
||||
return note;
|
||||
}
|
||||
|
||||
// The action a key presses, or -1: the walk key is a key of its own, but Shift is B in the default bindings, and a
|
||||
// walk key that also presses something walks and does that something at the same time. The bindings come from a
|
||||
// snapshot, and every key of an action is looked at: binds_refresh rebuilds the live table on the polling thread while
|
||||
// the menu draws, and an action may hold more than the two keys its cells show.
|
||||
static int key_action(const InputMap *m, int key) {
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
uint8_t keys[INPUT_BINDING_MAX];
|
||||
int n = input_map_keys_of(m, a, keys, INPUT_BINDING_MAX);
|
||||
for (int i = 0; i < n; i++)
|
||||
if (keys[i] == key) return a;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The walk key as the handful of keys a player is likely to pick. "none" rather than an empty value closes it: an
|
||||
// unset key falls back to the default in the settings table, the same as a fresh installation. totk.ini takes any name
|
||||
// input_map.c knows (walks of the keyboard sticks, not of the gamepad). A key the current bindings also use says which
|
||||
// action it presses.
|
||||
static void row_walk_key(void) {
|
||||
static const char *const keys[] = {"none", "lshift", "lalt", "lctrl", "tab", "capslock"};
|
||||
const char *names[sizeof keys / sizeof *keys] = {menu_text(UI_MENU_OFF), "Shift", "Alt", "Ctrl", "Tab", "Caps Lock"};
|
||||
const char *cur = cfg_value("TOTK_WALK_KEY");
|
||||
static char note[64];
|
||||
const char *n = nullptr;
|
||||
InputMap m;
|
||||
input_map_snapshot(&m);
|
||||
for (int k = KEY_A; k < KEY_COUNT && !n; k++) {
|
||||
if (!input_key_name(k) || strcasecmp(input_key_name(k), cur)) continue;
|
||||
int a = key_action(&m, k);
|
||||
if (a >= 0 && input_action_name(a)) snprintf(note, sizeof note, menu_text(UI_MENU_WALK_KEY_TAKEN), input_action_name(a)), n = note;
|
||||
}
|
||||
opt_choice_list("TOTK_WALK_KEY", UI_MENU_WALK_KEY, keys, names, sizeof keys / sizeof *keys, UI_MENU_WALK_KEY_DESC,
|
||||
-1, n);
|
||||
}
|
||||
|
||||
// The note under the prompts. The settings name a folder of prompts (input_prompts_folder: the prompts and the pad
|
||||
// layout, as a layout that puts A on the right button needs the face buttons named the other way round), but the game
|
||||
// reads its fonts and UI archive once at start (romfs.c overlay_init). While the folder in use differs: "files
|
||||
// missing" when the setup did not write the one asked for, why it was left out at start (damaged, a game resolution
|
||||
// that is not 16:9, a RomFS mod with the same files, TOTK_ROMFS_PATCHES), else "prompts change after restart", and only then
|
||||
// *restart is set: a restart would bring the prompts asked for. NULL when they match.
|
||||
static const char *prompts_note(bool *restart) {
|
||||
*restart = false;
|
||||
const char *layout = cfg_value("TOTK_PAD_LAYOUT"), *used = romfs_prompts();
|
||||
const char *want = input_prompts_folder(input_prompts_pick(cfg_get("TOTK_PROMPTS"), layout), layout);
|
||||
if (want == used || (want && used && !strcmp(want, used))) return nullptr;
|
||||
if (want && !romfs_prompts_ready(want)) return menu_text(UI_MENU_PROMPTS_MISSING);
|
||||
switch (romfs_prompts_off(want)) {
|
||||
case ROMFS_PROMPTS_FAILED: return menu_text(UI_MENU_PROMPTS_FAILED);
|
||||
case ROMFS_PROMPTS_SHAPE: return menu_text(UI_MENU_PROMPTS_OFF_SHAPE);
|
||||
case ROMFS_PROMPTS_MODS: return menu_text(UI_MENU_PROMPTS_OFF_MODS);
|
||||
case ROMFS_PROMPTS_PATCHES: return menu_text(UI_MENU_PROMPTS_OFF_PATCHES);
|
||||
default: *restart = true; return menu_text(UI_MENU_PROMPTS_RESTART);
|
||||
}
|
||||
}
|
||||
|
||||
// The confirmation of "Restart now", over the menu; Cancel has the cursor first. Restart writes totk.ini, then closes
|
||||
// the game as Quit does and starts it again (gfx_window_request_restart). It waits while the game writes a save, and
|
||||
// the game stays when totk.ini cannot be written, as the new prompts would not come back. From the Cheats tab
|
||||
// (restart_rebuild) Restart also lets the next start rebuild for cheats whose last rebuild failed (cheats_rebuild_allow).
|
||||
static bool restart_rebuild;
|
||||
static void restart_confirm(void) {
|
||||
g_modal = false;
|
||||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
if (!ImGui::BeginPopupModal("##restart_confirm", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize))
|
||||
return;
|
||||
g_modal = true;
|
||||
ImGui::TextUnformatted(menu_text(UI_MENU_RESTART_CONFIRM));
|
||||
bool saving = fs_save_busy() != 0;
|
||||
if (saving || !cfg_last_save_ok())
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.45f, 0.35f, 1), "%s",
|
||||
menu_text(saving ? UI_MENU_RESTART_SAVING : UI_MENU_SAVE_FAILED));
|
||||
ImGui::Spacing();
|
||||
const char *yes = menu_text(UI_MENU_RESTART_YES), *no = menu_text(UI_MENU_RESET_NO);
|
||||
const ImGuiStyle &st = ImGui::GetStyle();
|
||||
float bw = fmaxf(ImGui::CalcTextSize(yes).x, ImGui::CalcTextSize(no).x) + st.FramePadding.x * 4;
|
||||
bool close = ImGui::Button(no, ImVec2(bw, 0));
|
||||
if (ImGui::IsWindowAppearing()) ImGui::SetItemDefaultFocus();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(saving);
|
||||
if (ImGui::Button(yes, ImVec2(bw, 0)) && cfg_save()) {
|
||||
if (restart_rebuild) cheats_rebuild_allow();
|
||||
gfx_window_request_restart();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false))
|
||||
close = true;
|
||||
if (close) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
restart_rebuild = false;
|
||||
opt_focus_key("##restart"); // back on the row it came from
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
static void tab_controls(float scale) {
|
||||
opt_begin("##controls", scale);
|
||||
static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX,
|
||||
UI_MENU_PAD_LAYOUT_XBOX_PROMPTS};
|
||||
opt_choice("TOTK_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 4, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());
|
||||
static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX};
|
||||
opt_choice("TOTK_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 3, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());
|
||||
static const UiTextId prompts[] = {UI_MENU_PROMPTS_NINTENDO, UI_MENU_PROMPTS_XBOX};
|
||||
bool restart;
|
||||
const char *note = prompts_note(&restart);
|
||||
opt_choice("TOTK_PROMPTS", UI_MENU_PROMPTS, prompts, 2, UI_MENU_PROMPTS_DESC, note);
|
||||
bool ask = restart && opt_button("##restart", UI_MENU_RESTART, UI_MENU_RESTART_DESC);
|
||||
opt_toggle("TOTK_MOTION", UI_MENU_MOTION, UI_MENU_MOTION_DESC);
|
||||
opt_slider("TOTK_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%");
|
||||
opt_slider("TOTK_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f");
|
||||
opt_toggle("TOTK_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC);
|
||||
opt_slider("TOTK_STICK_DEADZONE", UI_MENU_STICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%");
|
||||
// One row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With a gamepad connected each
|
||||
// row shows its stick's deflection now, before the dead zone: a drifting stick's resting value, to set it above.
|
||||
double now[2];
|
||||
bool pad = input_pad_stick_percent(now) != 0;
|
||||
char stick_note[2][64];
|
||||
for (int s = 0; s < 2; s++)
|
||||
snprintf(stick_note[s], sizeof stick_note[s], menu_text(UI_MENU_STICK_DEADZONE_NOW), now[s]);
|
||||
opt_slider("TOTK_LSTICK_DEADZONE", UI_MENU_LSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%",
|
||||
pad ? stick_note[0] : nullptr, "TOTK_STICK_DEADZONE");
|
||||
opt_slider("TOTK_RSTICK_DEADZONE", UI_MENU_RSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%",
|
||||
pad ? stick_note[1] : nullptr, "TOTK_STICK_DEADZONE");
|
||||
opt_slider("TOTK_STICK_SENS", UI_MENU_STICK_SENS, 0.1f, 3, UI_MENU_STICK_SENS_DESC, 0.05f, "%.2f");
|
||||
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
|
||||
row_walk_key();
|
||||
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
|
||||
binds_rows(scale);
|
||||
opt_end();
|
||||
binds_after_page();
|
||||
if (ask) ImGui::OpenPopup("##restart_confirm"); // at the level BeginPopupModal looks it up
|
||||
restart_confirm();
|
||||
}
|
||||
|
||||
static void tab_menu(float scale) {
|
||||
opt_begin("##menu", scale);
|
||||
static const UiTextId langs[] = {UI_MENU_MENU_LANG_AUTO, UI_MENU_LANGNAME_EN, UI_MENU_LANGNAME_RU};
|
||||
opt_choice("TOTK_MENU_LANG", UI_MENU_MENU_LANG, langs, 3, UI_MENU_MENU_LANG_DESC);
|
||||
static const UiTextId langs[] = {UI_MENU_MENU_LANG_AUTO, UI_MENU_LANGNAME_EN, UI_MENU_LANGNAME_RU,
|
||||
UI_MENU_LANGNAME_PT_BR, UI_MENU_LANGNAME_ZH_HANS};
|
||||
opt_choice("TOTK_MENU_LANG", UI_MENU_MENU_LANG, langs, sizeof langs / sizeof *langs, UI_MENU_MENU_LANG_DESC);
|
||||
opt_end();
|
||||
}
|
||||
|
||||
// ---- Advanced: the testing and troubleshooting keys, the bug report helpers and the reset of every setting
|
||||
static bool g_modal; // the reset confirmation is open: B and Esc close it, not the menu; L / R stay on the page
|
||||
|
||||
// A thread count with no fixed default: Automatic (unset), then the common counts.
|
||||
static void row_threads(const char *key, UiTextId label, UiTextId desc) {
|
||||
@@ -849,12 +1009,111 @@ static void tab_advanced(float scale) {
|
||||
}
|
||||
}
|
||||
|
||||
static const UiTextId kTabNames[TAB_COUNT] = {UI_MENU_TAB_SCREEN, UI_MENU_TAB_GAME, UI_MENU_TAB_SOUND,
|
||||
UI_MENU_TAB_CONTROLS, UI_MENU_TAB_MENU, UI_MENU_TAB_ADVANCED};
|
||||
static const UiTextId kTabNames[TAB_COUNT] = {UI_MENU_TAB_SCREEN, UI_MENU_TAB_GAME, UI_MENU_TAB_SOUND,
|
||||
UI_MENU_TAB_CONTROLS, UI_MENU_TAB_AMIIBO, UI_MENU_TAB_MENU,
|
||||
UI_MENU_TAB_ADVANCED, UI_MENU_TAB_MODS, UI_MENU_TAB_CHEATS};
|
||||
static int g_tab; // the tab shown
|
||||
static int g_tab_request = -1; // select this tab at the next layout; -1 none
|
||||
static int g_focus_page = -1; // this tab's first row takes the cursor once it is shown; -1 none
|
||||
|
||||
// A row of another tab: that tab opens with the row under the cursor.
|
||||
static void go_to(int tab, const char *key) {
|
||||
g_tab_request = tab;
|
||||
opt_focus_key(key);
|
||||
}
|
||||
|
||||
// ---- Mods: the mods folder (mods.c) with switches and the load order, then the port's own patches. The folder was
|
||||
// read at start; the rows change the order and switches for the next start.
|
||||
static void mods_builtin_rows(void) {
|
||||
opt_group(UI_MENU_MODS_BUILTIN);
|
||||
char fps[64], res[64];
|
||||
const char *lim = cfg_value("TOTK_FPS_LIMIT");
|
||||
if (!strcmp(lim, "display")) snprintf(fps, sizeof fps, "%s", menu_text(UI_MENU_FPS_DISPLAY));
|
||||
else snprintf(fps, sizeof fps, "%s FPS", lim);
|
||||
if (opt_button("##mods_fps60", UI_MENU_MODS_FPS60, UI_MENU_MODS_FPS60_DESC, fps)) go_to(TAB_GAME, "TOTK_FPS_LIMIT");
|
||||
int w = 0, h = 0;
|
||||
sscanf(cfg_value("TOTK_OUTPUT_RES"), "%dx%d", &w, &h);
|
||||
snprintf(res, sizeof res, "%d × %d", w, h); // U+00D7, as row_output_res shows it
|
||||
if (opt_button("##mods_res", UI_MENU_MODS_RES, UI_MENU_MODS_RES_DESC, res)) go_to(TAB_SCREEN, "TOTK_OUTPUT_RES");
|
||||
opt_toggle("TOTK_REALTIME", UI_MENU_REALTIME, UI_MENU_REALTIME_DESC);
|
||||
opt_toggle("TOTK_CLOTH_REALTIME", UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC);
|
||||
opt_toggle("TOTK_HIGHFPS_FIX", UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC);
|
||||
opt_button("##mods_quickmenu", UI_MENU_MODS_QUICKMENU, UI_MENU_MODS_QUICKMENU_DESC,
|
||||
menu_text(romfs_patches_loaded() ? UI_MENU_ON : UI_MENU_OFF));
|
||||
}
|
||||
|
||||
static void tab_mods(float scale) {
|
||||
opt_begin("##mods", scale);
|
||||
opt_group(UI_MENU_MODS_INSTALLED);
|
||||
int n = mods_count();
|
||||
if (!n) opt_text(UI_MENU_MODS_NONE);
|
||||
const char *after = mods_changed() ? menu_text(UI_MENU_AFTER_RESTART) : nullptr;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const ModInfo *m = mods_at(i);
|
||||
char id[32], desc[1536];
|
||||
snprintf(id, sizeof id, "##mod%d", i);
|
||||
char over[512] = "", other[512] = "";
|
||||
if (m->overridden) snprintf(over, sizeof over, menu_text(UI_MENU_MODS_OVERRIDDEN), m->overridden, m->overridden_by);
|
||||
if (m->other_version) snprintf(other, sizeof other, menu_text(UI_MENU_MODS_OTHER_VERSION), m->other_version);
|
||||
char cheat[256] = "";
|
||||
if (m->kind & MOD_CHEATS) { // its cheats as read at start, or the file they come from
|
||||
int count = 0;
|
||||
for (int j = 0; j < cheats_count(); j++)
|
||||
count += !strcmp(cheats_info(j)->mod, m->name) && cheats_info(j)->kind != CHEAT_HEADING;
|
||||
if (count) snprintf(cheat, sizeof cheat, menu_text(UI_MENU_MODS_CHEATS), count);
|
||||
else snprintf(cheat, sizeof cheat, menu_text(UI_MENU_MODS_CHEATS_FILE), cheats_file());
|
||||
}
|
||||
char code[512] = "", problem[512] = "";
|
||||
if (m->nsets && m->overrides[0])
|
||||
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_REPLACES_BUILTIN), m->nsets, m->overrides);
|
||||
else if (m->nsets)
|
||||
snprintf(code, sizeof code, menu_text(UI_MENU_MODS_EXEFS_ON), m->nsets);
|
||||
switch (m->problem) {
|
||||
case MOD_EMPTY: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EMPTY)); break;
|
||||
case MOD_PLUGIN: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_SUBSDK)); break;
|
||||
case MOD_EXEFS_LATER: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EXEFS_LATER)); break;
|
||||
case MOD_EXEFS_UNSUPPORTED:
|
||||
snprintf(problem, sizeof problem, menu_text(UI_MENU_MODS_EXEFS_UNSUPPORTED), m->note);
|
||||
break;
|
||||
case MOD_EXEFS_OTHER: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_EXEFS_OTHER)); break;
|
||||
case MOD_BAD_NAME: snprintf(problem, sizeof problem, "%s", menu_text(UI_MENU_MODS_BAD_NAME)); break;
|
||||
default: break;
|
||||
}
|
||||
int k = snprintf(desc, sizeof desc, menu_text(UI_MENU_MODS_ROW_DESC), m->replaced, m->added);
|
||||
const char *parts[] = {over, other, cheat, code, problem, menu_text(UI_MENU_MODS_MOVE_HINT)};
|
||||
for (const char *part : parts)
|
||||
if (*part && k >= 0 && k < (int)sizeof desc) k += snprintf(desc + k, sizeof desc - k, "\n\n%s", part);
|
||||
const char *note = m->enabled && m->problem != MOD_OK ? menu_text(UI_MENU_MODS_UNUSED) : after;
|
||||
int s = opt_text_row(id, m->name, menu_text(m->enabled ? UI_MENU_ON : UI_MENU_OFF), note, desc, true);
|
||||
if (s == 2 || s == -1 || s == 1) {
|
||||
mods_set_enabled(i, !m->enabled);
|
||||
opt_save_soon();
|
||||
} else if ((s == 3 && i > 0) || (s == 4 && i + 1 < n)) {
|
||||
int to = s == 3 ? i - 1 : i + 1;
|
||||
mods_move(i, to - i);
|
||||
snprintf(id, sizeof id, "##mod%d", to);
|
||||
opt_focus_key(id); // the cursor follows the mod
|
||||
opt_save_soon();
|
||||
}
|
||||
}
|
||||
bool rebuild =
|
||||
cheats_rebuild_pending() && opt_button("##restart", UI_MENU_CHEATS_REBUILD, UI_MENU_CHEATS_REBUILD_DESC);
|
||||
static uint64_t open_failed; // plat_now_ns until which the button says it failed
|
||||
if (opt_button("##mods_open", UI_MENU_MODS_OPEN, UI_MENU_MODS_OPEN_DESC,
|
||||
plat_now_ns() < open_failed ? menu_text(UI_MENU_ACTION_FAILED) : "")) {
|
||||
bool ok = mods_dir()[0] != 0;
|
||||
if (ok) plat_file_mkdir_p(mods_dir());
|
||||
if (!ok || !plat_open_folder(mods_dir())) open_failed = plat_now_ns() + 3000000000ull;
|
||||
}
|
||||
mods_builtin_rows();
|
||||
opt_end();
|
||||
if (rebuild) {
|
||||
restart_rebuild = true;
|
||||
ImGui::OpenPopup("##restart_confirm");
|
||||
}
|
||||
restart_confirm();
|
||||
}
|
||||
|
||||
// ---- the window: title, tabs (L / R or Q / E switch), the page, and the bottom row with "Continue" and "Quit game".
|
||||
// B on the gamepad and Esc close it.
|
||||
static void layout_menu(uint32_t w, uint32_t h, float scale) {
|
||||
@@ -899,9 +1158,18 @@ static void layout_menu(uint32_t w, uint32_t h, float scale) {
|
||||
switch (k) {
|
||||
case TAB_SCREEN: tab_screen(scale); break;
|
||||
case TAB_GAME: tab_game(scale); break;
|
||||
case TAB_MODS: tab_mods(scale); break;
|
||||
case TAB_SOUND: tab_sound(scale); break;
|
||||
case TAB_CONTROLS: tab_controls(scale); break;
|
||||
case TAB_AMIIBO: tab_amiibo(scale); break;
|
||||
case TAB_MENU: tab_menu(scale); break;
|
||||
case TAB_CHEATS:
|
||||
if (tab_cheats(scale)) {
|
||||
restart_rebuild = true;
|
||||
ImGui::OpenPopup("##restart_confirm");
|
||||
}
|
||||
restart_confirm();
|
||||
break;
|
||||
case TAB_ADVANCED: tab_advanced(scale); break;
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
@@ -924,7 +1192,7 @@ static void layout_menu(uint32_t w, uint32_t h, float scale) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(quit, ImVec2(bw, 0))) gfx_window_request_close();
|
||||
bool was_modal = g_modal || binds_modal();
|
||||
if (g_tab != TAB_ADVANCED) g_modal = false;
|
||||
if (g_tab != TAB_ADVANCED && g_tab != TAB_CONTROLS) g_modal = false;
|
||||
if (!was_modal && (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false)))
|
||||
set_open(0);
|
||||
ImGui::End();
|
||||
@@ -1061,7 +1329,11 @@ static void perf_picture(const MenuTarget *t, const ImVec4 &c) {
|
||||
int s = cfg_int("TOTK_SHARPNESS");
|
||||
snprintf(v[3], sizeof v[3], menu_text(UI_MENU_OV_FMT_FSR), s < 0 ? 0 : s > 100 ? 100 : s);
|
||||
} else {
|
||||
snprintf(v[3], sizeof v[3], "%s", menu_text(t->scaler ? UI_MENU_OV_SCALE_BILINEAR : UI_MENU_OV_SCALE_NONE));
|
||||
// FSR enlarges only (vk_fsr_record): an image larger than the window is shrunk by the blit even with FSR selected
|
||||
int down = t->scaler == 1 && !strcmp(cfg_value("TOTK_UPSCALER"), "fsr") &&
|
||||
(t->image_w > (int)t->width || t->image_h > (int)t->height);
|
||||
snprintf(v[3], sizeof v[3], "%s",
|
||||
menu_text(down ? UI_MENU_OV_SCALE_DOWN : t->scaler ? UI_MENU_OV_SCALE_BILINEAR : UI_MENU_OV_SCALE_NONE));
|
||||
}
|
||||
double hz = frame_step_limit_hz();
|
||||
snprintf(v[4], sizeof v[4], "%s", menu_text(UI_MENU_OV_NONE));
|
||||
@@ -1178,10 +1450,12 @@ static void perf_draw(const MenuTarget *t, float scale) {
|
||||
|
||||
extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const char **shot) {
|
||||
*shot = NULL;
|
||||
cheats_frame(frame); // the data cheats and the buttons of the cheats that need them held
|
||||
if (!g_test.parsed) test_parse();
|
||||
if (g_test.next_set < g_test.n_sets && frame >= g_test.sets[g_test.next_set].frame) test_apply_sets(frame);
|
||||
if (g_perf_save.exchange(0)) cfg_save();
|
||||
perf_tick(plat_now_ns(), t);
|
||||
amiibo_tick();
|
||||
bool open = g_open.load(std::memory_order_relaxed);
|
||||
if (!open) {
|
||||
if (g_save_pending) g_save_pending = opt_save_tick(1); // closed: the last change goes to totk.ini now
|
||||
|
||||
+4
-2
@@ -5,8 +5,9 @@
|
||||
//
|
||||
// F1 or Esc on the keyboard and Minus and Plus held together (consumed in poll_merged, input_menu_gate in input.h) open it;
|
||||
// F1, Esc, B or "Continue" close it. While it is open the game gets no input (input_menu, input.h) and keeps running; the
|
||||
// cursor is released from the mouse camera. Tabs Screen, Game, Sound, Controls and Menu (L / R or Q / E switch them) hold
|
||||
// option rows (ui/option_rows.h) over the cfg settings; the bottom row has "Continue" and "Quit game".
|
||||
// cursor is released from the mouse camera. Tabs Screen, Game, Mods, Sound, Controls and Menu (L / R or Q / E switch
|
||||
// them) hold option rows (ui/option_rows.h) over the cfg settings and the mods folder (mods.h); the bottom row has
|
||||
// "Continue" and "Quit game".
|
||||
// Test hooks (ui/menu.cpp): TOTK_MENU_TEST=<frame>[:<frames per page>] opens the menu at that presented frame and saves
|
||||
// one swapchain shot per tab into TOTK_MENU_TEST_DIR; TOTK_MENU_TEST_SET=<frame>:<key>=<value>[,...][;...] sets settings
|
||||
// at given frames without input (SAVE=1 writes totk.ini).
|
||||
@@ -60,6 +61,7 @@ int menu_font_check(void);
|
||||
// ---- what the menu asks of the platform side (gfx_window_win.c, gfx_window_sdl.c)
|
||||
void gfx_window_apply_settings(void); // TOTK_FULLSCREEN or TOTK_WINDOW_SIZE changed: the window follows
|
||||
void gfx_window_request_close(void); // as the window's close button: the process ends
|
||||
void gfx_window_request_restart(void); // the same, then totk.exe starts again with the same command line
|
||||
int gfx_window_monitor_size(int *w, int *h); // pixels of the monitor the window is on; 0 when unknown
|
||||
double gfx_window_refresh_hz(void); // its refresh rate (TOTK_FPS_LIMIT=display); 0 when unknown
|
||||
unsigned gfx_window_dpi(void); // the window's DPI (96 at 100% display scale); any thread
|
||||
|
||||
+75
-18
@@ -33,15 +33,15 @@ static char g_focus_key[64]; // opt_focus_key: the row that takes th
|
||||
static bool g_held; // the last row's left / right has been held a while (steppers go faster)
|
||||
static float g_bottom; // opt_reserve_bottom: kept free under the panes
|
||||
static bool g_disable_next; // opt_disable_next: the next row is greyed out with g_disable_note
|
||||
static UiTextId g_disable_note;
|
||||
static const char *g_disable_note;
|
||||
|
||||
// The description pane: this frame's hovered and selected rows, the page's first row, and the last one shown.
|
||||
struct Desc { char text[UI_DESC_BYTES]; };
|
||||
static Desc g_hover, g_focus, g_first, g_shown;
|
||||
|
||||
// disabled: the note of a row opt_disable_next greyed out, else NULL.
|
||||
static void desc_set(Desc &d, UiTextId desc, const char *key, const CfgMeta *m, const char *disabled) {
|
||||
int n = snprintf(d.text, sizeof d.text, "%s", menu_text(desc));
|
||||
static void desc_set(Desc &d, const char *desc, const char *key, const CfgMeta *m, const char *disabled) {
|
||||
int n = snprintf(d.text, sizeof d.text, "%s", desc);
|
||||
if (n < 0 || n >= (int)sizeof d.text) return;
|
||||
if (disabled)
|
||||
snprintf(d.text + n, sizeof d.text - n, "\n\n%s", disabled);
|
||||
@@ -56,8 +56,11 @@ void opt_focus_first(void) { g_focus_req = true; }
|
||||
void opt_focus_key(const char *key) { snprintf(g_focus_key, sizeof g_focus_key, "%s", key); }
|
||||
|
||||
void opt_reserve_bottom(float h) { g_bottom = h; }
|
||||
float opt_bottom(void) { return g_bottom; }
|
||||
|
||||
void opt_disable_next(UiTextId note) {
|
||||
void opt_disable_next(UiTextId note) { opt_disable_next_text(menu_text(note)); }
|
||||
|
||||
void opt_disable_next_text(const char *note) {
|
||||
g_disable_next = true;
|
||||
g_disable_note = note;
|
||||
}
|
||||
@@ -107,11 +110,12 @@ struct RowView {
|
||||
const char *note; // under the value instead of the "default" mark; NULL for none
|
||||
};
|
||||
|
||||
// One row: draws it, handles A / click and left / right while selected. Returns 2 (A / click), -1 / +1 (left / right) or
|
||||
// 0; always 0 for a row the environment fixes (greyed out, still selectable so its description shows).
|
||||
static int row(const char *key, UiTextId label, UiTextId desc, const RowView &v) {
|
||||
// One row: draws it, handles A / click and left / right while selected. Returns 2 (A / click), -1 / +1 (left / right),
|
||||
// 3 / 4 (move up / down, movable rows only) or 0; always 0 for a row the environment fixes (greyed out, still selectable
|
||||
// so its description shows). movable: the top / left face buttons, Ctrl+Up / Ctrl+Down and two drawn arrows move it.
|
||||
static int row_text(const char *key, const char *label, const char *desc, const RowView &v, bool movable) {
|
||||
const CfgMeta *m = cfg_meta(key);
|
||||
const char *disabled = g_disable_next ? menu_text(g_disable_note) : NULL;
|
||||
const char *disabled = g_disable_next ? g_disable_note : NULL;
|
||||
g_disable_next = false;
|
||||
bool off = disabled || cfg_from_env(key) != 0;
|
||||
ImGui::PushID(key);
|
||||
@@ -145,6 +149,13 @@ static int row(const char *key, UiTextId label, UiTextId desc, const RowView &v)
|
||||
// left and right belong to the row (navigation would otherwise move sideways into the description pane)
|
||||
ImGui::SetKeyOwner(k, ImGui::GetItemID());
|
||||
}
|
||||
if (movable) {
|
||||
bool ctrl = ImGui::GetIO().KeyCtrl;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_GamepadFaceUp, false) || (ctrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow)))
|
||||
step = 3;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_GamepadFaceLeft, false) || (ctrl && ImGui::IsKeyPressed(ImGuiKey_DownArrow)))
|
||||
step = 4;
|
||||
}
|
||||
}
|
||||
if (foc) desc_set(g_focus, desc, key, m, disabled);
|
||||
if (hov) desc_set(g_hover, desc, key, m, disabled);
|
||||
@@ -174,11 +185,25 @@ static int row(const char *key, UiTextId label, UiTextId desc, const RowView &v)
|
||||
right = rx - gap;
|
||||
value_left = lx;
|
||||
}
|
||||
if (movable) { // up and down arrows left of the value; a click on one moves the row
|
||||
// a fixed value column (On / Off differ in width) keeps the arrows of the rows in one line
|
||||
float aw = fs * 0.42f, gap = fs * 0.55f;
|
||||
float cx = right - fmaxf(ImGui::CalcTextSize(v.value).x, fs * 2.8f) - gap * 2 - aw;
|
||||
float uy = ty + fs * 0.12f, dy = ty + fs * 0.92f;
|
||||
ImU32 ac = off ? kGrey : hov || foc ? kGold : IM_COL32(170, 170, 160, 255);
|
||||
dl->AddTriangleFilled(ImVec2(cx, uy + aw * 0.7f), ImVec2(cx + aw, uy + aw * 0.7f), ImVec2(cx + aw * 0.5f, uy), ac);
|
||||
dl->AddTriangleFilled(ImVec2(cx, dy - aw * 0.7f), ImVec2(cx + aw, dy - aw * 0.7f), ImVec2(cx + aw * 0.5f, dy), ac);
|
||||
if (clicked && !off) {
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
if (mp.x >= cx - gap * 0.5f && mp.x <= cx + aw + gap * 0.5f) step = mp.y < ty + fs * 0.52f ? 3 : 4;
|
||||
}
|
||||
value_left = cx - gap;
|
||||
}
|
||||
if (v.value[0]) {
|
||||
float vx = right - ImGui::CalcTextSize(v.value).x;
|
||||
dl->AddText(ImVec2(vx, ty), off ? kGrey : v.is_default ? IM_COL32(205, 205, 195, 255) : IM_COL32(255, 255, 255, 255),
|
||||
v.value);
|
||||
if (!v.arrows) value_left = vx;
|
||||
if (!v.arrows && !movable) value_left = vx;
|
||||
}
|
||||
if (v.note || v.is_default) { // under the value, right-aligned with it
|
||||
const char *t = v.note ? v.note : menu_text(UI_MENU_DEFAULT_MARK);
|
||||
@@ -187,8 +212,7 @@ static int row(const char *key, UiTextId label, UiTextId desc, const RowView &v)
|
||||
}
|
||||
// the label and its note, clipped before the value
|
||||
ImVec4 clip(p.x, p.y, value_left - fs * 0.6f, p.y + h);
|
||||
dl->AddText(font, fs, ImVec2(p.x + pad, ty), off ? kGrey : IM_COL32(240, 236, 224, 255), menu_text(label), NULL, 0,
|
||||
&clip);
|
||||
dl->AddText(font, fs, ImVec2(p.x + pad, ty), off ? kGrey : IM_COL32(240, 236, 224, 255), label, NULL, 0, &clip);
|
||||
const char *note = disabled ? disabled
|
||||
: off ? menu_text(UI_MENU_SET_BY_ENV)
|
||||
: m && !m->live ? menu_text(UI_MENU_AFTER_RESTART)
|
||||
@@ -198,6 +222,16 @@ static int row(const char *key, UiTextId label, UiTextId desc, const RowView &v)
|
||||
return off ? 0 : step;
|
||||
}
|
||||
|
||||
static int row(const char *key, UiTextId label, UiTextId desc, const RowView &v) {
|
||||
return row_text(key, menu_text(label), menu_text(desc), v, false);
|
||||
}
|
||||
|
||||
int opt_text_row(const char *id, const char *label, const char *value, const char *note, const char *desc,
|
||||
bool movable) {
|
||||
RowView v = {value ? value : "", false, false, note};
|
||||
return row_text(id, label, desc, v, movable);
|
||||
}
|
||||
|
||||
// ---- writing a setting: at once in memory (live settings follow), to totk.ini a moment later (opt_save_tick)
|
||||
static uint64_t g_save_at; // plat_now_ns deadline of the pending write; 0 none
|
||||
static bool g_save_logged;
|
||||
@@ -241,6 +275,26 @@ bool opt_button(const char *id, UiTextId label, UiTextId desc, const char *value
|
||||
return row(id, label, desc, v) == 2; // A or a click; left and right do nothing
|
||||
}
|
||||
|
||||
bool opt_toggle_text(const char *id, const char *label, const char *desc, bool on, const char *value) {
|
||||
// off is the plain state: its value drawn dim, without the "default" mark
|
||||
RowView v = {value ? value : menu_text(on ? UI_MENU_ON : UI_MENU_OFF), !on, false, on ? NULL : ""};
|
||||
return row_text(id, label, desc, v, false) != 0; // any step flips it
|
||||
}
|
||||
|
||||
void opt_group_text(const char *title) {
|
||||
ImGui::Dummy(ImVec2(0, 6 * g_scale));
|
||||
ImGui::PushTextWrapPos(0);
|
||||
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1), "%s", title);
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
void opt_text_str(const char *text, bool warn) {
|
||||
ImGui::PushTextWrapPos(0);
|
||||
if (warn) ImGui::TextColored(ImVec4(0.95f, 0.66f, 0.40f, 1), "%s", text);
|
||||
else ImGui::TextUnformatted(text);
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
void opt_group(UiTextId title) {
|
||||
ImGui::Dummy(ImVec2(0, 6 * g_scale));
|
||||
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1), "%s", menu_text(title));
|
||||
@@ -294,12 +348,15 @@ bool opt_choice(const char *key, UiTextId label, const UiTextId *names, int n, U
|
||||
return opt_choice_list(key, label, values, shown, n, desc, -1, note);
|
||||
}
|
||||
|
||||
bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId desc, float step, const char *fmt) {
|
||||
const CfgMeta *m = cfg_meta(key);
|
||||
double v = cfg_double(key), def = m ? strtod(m->def, NULL) : lo;
|
||||
bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId desc, float step, const char *fmt,
|
||||
const char *note, const char *fallback) {
|
||||
const char *set = cfg_get(key);
|
||||
const char *src = fallback && !(set && *set) ? fallback : key; // unset: show the value in effect, the fallback's
|
||||
const CfgMeta *m = cfg_meta(src);
|
||||
double v = cfg_double(src), def = m ? strtod(m->def, NULL) : lo;
|
||||
char shown[48];
|
||||
snprintf(shown, sizeof shown, fmt, v);
|
||||
RowView rv = {shown, fabs(v - def) < step * 0.01, true, NULL};
|
||||
RowView rv = {shown, fabs(v - def) < step * 0.01, true, note};
|
||||
int s = row(key, label, desc, rv);
|
||||
if (!s) return false;
|
||||
int steps = g_held && (hi - lo) > 40 * step ? 10 : 1; // held: ten steps at a time over a long range
|
||||
@@ -400,9 +457,9 @@ int opt_bind_row(const char *key, const char *label, UiTextId desc, const char *
|
||||
}
|
||||
char k[64];
|
||||
snprintf(k, sizeof k, "TOTK_%s", key);
|
||||
if (row_foc) desc_set(g_focus, desc, k, NULL, NULL);
|
||||
if (row_hov) desc_set(g_hover, desc, k, NULL, NULL);
|
||||
if (!g_first.text[0]) desc_set(g_first, desc, k, NULL, NULL);
|
||||
if (row_foc) desc_set(g_focus, menu_text(desc), k, NULL, NULL);
|
||||
if (row_hov) desc_set(g_hover, menu_text(desc), k, NULL, NULL);
|
||||
if (!g_first.text[0]) desc_set(g_first, menu_text(desc), k, NULL, NULL);
|
||||
if (note && (row_foc || row_hov)) { // e.g. what moved here, after the description
|
||||
Desc &d = row_hov ? g_hover : g_focus;
|
||||
size_t n = strlen(d.text);
|
||||
|
||||
@@ -19,12 +19,15 @@ void opt_begin(const char *id, float scale);
|
||||
void opt_end(void);
|
||||
// The height the next pages leave free under their panes (the menu's bottom row).
|
||||
void opt_reserve_bottom(float h);
|
||||
// The height opt_reserve_bottom keeps free, for a tab that lays out its own panes (ui/amiibo_page.cpp).
|
||||
float opt_bottom(void);
|
||||
// The next page's first row takes the controller's cursor (opening the menu, switching tabs).
|
||||
void opt_focus_first(void);
|
||||
// The row of key (or button id) takes the cursor, scrolled into view, when it is next drawn.
|
||||
void opt_focus_key(const char *key);
|
||||
// The next row is greyed out like one the environment fixes, with note under its label and in its description.
|
||||
void opt_disable_next(UiTextId note);
|
||||
void opt_disable_next_text(const char *note); // note must outlive the frame
|
||||
|
||||
// Page text that is not a row: a group heading in the accent colour, a wrapped paragraph (warn: the warning colour).
|
||||
void opt_group(UiTextId title);
|
||||
@@ -45,9 +48,22 @@ bool opt_choice_list(const char *key, UiTextId label, const char *const *values,
|
||||
// A row that runs an action: true on A or a click. id: unique on the page and not a setting ("##copy_info"); value:
|
||||
// shown on the right ("" none).
|
||||
bool opt_button(const char *id, UiTextId label, UiTextId desc, const char *value = "");
|
||||
// A row whose label, value, note (under the value) and description are text, not catalog ids: a mod's folder name.
|
||||
// Returns 2 (A / click), -1 / +1 (left / right), 3 / 4 (move up / down: the top / left face buttons, Ctrl+Up /
|
||||
// Ctrl+Down, the drawn arrows; movable rows only) or 0.
|
||||
int opt_text_row(const char *id, const char *label, const char *value, const char *note, const char *desc,
|
||||
bool movable);
|
||||
// Rows and text that are not settings of the cfg table: a label and description of the caller's own (the cheats of
|
||||
// the cheat files). opt_toggle_text shows on / off (value: shown instead, NULL for On / Off) and returns true on A, a
|
||||
// click, left or right; the caller changes its state. id: unique on the page, the environment check uses it.
|
||||
bool opt_toggle_text(const char *id, const char *label, const char *desc, bool on, const char *value = nullptr);
|
||||
void opt_group_text(const char *title);
|
||||
void opt_text_str(const char *text, bool warn = false);
|
||||
// lo..hi in steps of step (left and right held for a second go ten times as fast); fmt formats the value ("%.0f %%").
|
||||
// note: a short line under the value in place of the "default" mark (NULL: none). fallback: the setting that applies
|
||||
// while key is unset; the row shows its value then, and a change sets key.
|
||||
bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId desc, float step = 1,
|
||||
const char *fmt = "%g");
|
||||
const char *fmt = "%g", const char *note = nullptr, const char *fallback = nullptr);
|
||||
|
||||
// Binding rows (remapping on the Controls tab): a label and three cells, each selectable on its own (left / right move between them).
|
||||
// opt_bind_header: the column titles above them. opt_bind_row: key is the setting's name without TOTK_ (BIND_A; the
|
||||
|
||||
+1380
-33
File diff suppressed because it is too large.
Load diff
+73
-11
@@ -11,11 +11,12 @@ typedef enum {
|
||||
UI_SWKBD_CANCEL,
|
||||
UI_SWKBD_PAD_HINT, // how a controller answers
|
||||
|
||||
// Menu: window, tabs, bottom row. UI_MENU_FIRST..UI_MENU_LAST_ID carry English and Russian texts.
|
||||
// Menu: window, tabs, bottom row. UI_MENU_FIRST..UI_MENU_LAST_ID carry English, Russian, Brazilian Portuguese and
|
||||
// Simplified Chinese texts.
|
||||
UI_MENU_FIRST,
|
||||
UI_MENU_TITLE = UI_MENU_FIRST,
|
||||
UI_MENU_TAB_SCREEN, UI_MENU_TAB_GAME, UI_MENU_TAB_SOUND, UI_MENU_TAB_CONTROLS, UI_MENU_TAB_MENU,
|
||||
UI_MENU_TAB_ADVANCED,
|
||||
UI_MENU_TAB_SCREEN, UI_MENU_TAB_GAME, UI_MENU_TAB_MODS, UI_MENU_TAB_SOUND, UI_MENU_TAB_CONTROLS, UI_MENU_TAB_AMIIBO,
|
||||
UI_MENU_TAB_MENU, UI_MENU_TAB_ADVANCED,
|
||||
UI_MENU_CONTINUE, UI_MENU_QUIT,
|
||||
UI_MENU_ON, UI_MENU_OFF, UI_MENU_AUTO, UI_MENU_DEFAULT_MARK, // "default" beside the default value
|
||||
UI_MENU_AFTER_RESTART, // note on a setting that is read at start
|
||||
@@ -37,12 +38,23 @@ typedef enum {
|
||||
UI_MENU_UPSCALER, UI_MENU_UPSCALER_DESC, UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR,
|
||||
UI_MENU_ASPECT, UI_MENU_ASPECT_DESC, UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH,
|
||||
UI_MENU_FOV, UI_MENU_FOV_DESC,
|
||||
UI_MENU_SHADOW_RES, UI_MENU_SHADOW_RES_DESC, UI_MENU_SHADOW_RES_GAME,
|
||||
UI_MENU_ANISO, UI_MENU_ANISO_DESC, UI_MENU_ANISO_GAME,
|
||||
UI_MENU_SHARPNESS, UI_MENU_SHARPNESS_DESC,
|
||||
UI_MENU_COLOR, // group heading over the colour rows
|
||||
UI_MENU_COLOR_PRESET, UI_MENU_COLOR_PRESET_DESC, UI_MENU_COLOR_DEFAULT, UI_MENU_COLOR_VIVID, UI_MENU_COLOR_CLARITY,
|
||||
UI_MENU_COLOR_CUSTOM,
|
||||
UI_MENU_COLOR_BRIGHTNESS, UI_MENU_COLOR_BRIGHTNESS_DESC, UI_MENU_COLOR_CONTRAST, UI_MENU_COLOR_CONTRAST_DESC,
|
||||
UI_MENU_COLOR_SATURATION, UI_MENU_COLOR_SATURATION_DESC, UI_MENU_COLOR_GAMMA, UI_MENU_COLOR_GAMMA_DESC,
|
||||
UI_MENU_HDR_GROUP, // group heading over the HDR rows
|
||||
UI_MENU_HDR, UI_MENU_HDR_DESC, UI_MENU_HDR_PEAK, UI_MENU_HDR_PEAK_DESC, UI_MENU_HDR_GAME, UI_MENU_HDR_GAME_DESC,
|
||||
UI_MENU_HDR_UI, UI_MENU_HDR_UI_DESC,
|
||||
UI_MENU_SHADOW_SCALE, UI_MENU_SHADOW_SCALE_DESC,
|
||||
UI_MENU_SHADOW_RANGE, UI_MENU_SHADOW_RANGE_DESC,
|
||||
UI_MENU_CONTACT_SHADOWS, UI_MENU_CONTACT_SHADOWS_DESC,
|
||||
UI_MENU_PCSS, UI_MENU_PCSS_DESC,
|
||||
UI_MENU_DLAA, UI_MENU_DLAA_DESC, UI_MENU_DLSS, UI_MENU_DLSS_DESC, UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY,
|
||||
UI_MENU_DLSS_BALANCED, UI_MENU_DLSS_PERFORMANCE, UI_MENU_FXAA, UI_MENU_FXAA_DESC,
|
||||
|
||||
// Game
|
||||
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90,
|
||||
@@ -55,6 +67,25 @@ typedef enum {
|
||||
UI_MENU_OPERATION_MODE, UI_MENU_OPERATION_MODE_DESC, UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD,
|
||||
UI_MENU_ASSUME_FOCUS, UI_MENU_ASSUME_FOCUS_DESC,
|
||||
|
||||
// Mods: the port's own patches and the player's mods folder (mods.c)
|
||||
UI_MENU_MODS_BUILTIN, UI_MENU_MODS_INSTALLED,
|
||||
UI_MENU_MODS_NONE, // no mod folder yet: where to unpack one
|
||||
UI_MENU_MODS_OPEN, UI_MENU_MODS_OPEN_DESC,
|
||||
UI_MENU_MODS_ROW_DESC, // printf: files it replaces, files it adds (as of this start)
|
||||
UI_MENU_MODS_OVERRIDDEN, // printf: files a higher mod serves instead, that mod's name
|
||||
UI_MENU_MODS_MOVE_HINT,
|
||||
UI_MENU_MODS_EMPTY, UI_MENU_MODS_SUBSDK, UI_MENU_MODS_EXEFS_LATER, UI_MENU_MODS_BAD_NAME,
|
||||
UI_MENU_MODS_EXEFS_UNSUPPORTED, // printf: the file and why
|
||||
UI_MENU_MODS_EXEFS_OTHER,
|
||||
UI_MENU_MODS_EXEFS_ON, // printf: its ExeFS patch files in use
|
||||
UI_MENU_MODS_REPLACES_BUILTIN, // printf: its ExeFS patch files in use, the port's own patch sets they turn off
|
||||
UI_MENU_MODS_OTHER_VERSION, // printf: files made for another game version, left out
|
||||
UI_MENU_MODS_UNUSED, // under the switch of a mod the port cannot use
|
||||
UI_MENU_MODS_CHEATS, // printf: its cheats as read at start
|
||||
UI_MENU_MODS_CHEATS_FILE, // printf: <build id>.txt, when none were read
|
||||
UI_MENU_MODS_FPS60, UI_MENU_MODS_FPS60_DESC, UI_MENU_MODS_RES, UI_MENU_MODS_RES_DESC,
|
||||
UI_MENU_MODS_QUICKMENU, UI_MENU_MODS_QUICKMENU_DESC,
|
||||
|
||||
// Sound
|
||||
UI_MENU_VOLUME, UI_MENU_VOLUME_DESC,
|
||||
|
||||
@@ -64,10 +95,17 @@ typedef enum {
|
||||
UI_MENU_RUMBLE_CURVE, UI_MENU_RUMBLE_CURVE_DESC,
|
||||
UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC,
|
||||
UI_MENU_PAD_LAYOUT, UI_MENU_PAD_LAYOUT_DESC, UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO,
|
||||
UI_MENU_PAD_LAYOUT_XBOX, UI_MENU_PAD_LAYOUT_XBOX_PROMPTS, UI_MENU_PAD_LAYOUT_IN_USE,
|
||||
UI_MENU_PAD_LAYOUT_PROMPTS_MISSING, UI_MENU_PAD_LAYOUT_PROMPTS_RESTART,
|
||||
UI_MENU_STICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
|
||||
UI_MENU_PAD_LAYOUT_XBOX, UI_MENU_PAD_LAYOUT_IN_USE,
|
||||
UI_MENU_PROMPTS, UI_MENU_PROMPTS_DESC, UI_MENU_PROMPTS_NINTENDO, UI_MENU_PROMPTS_XBOX,
|
||||
// the notes under the prompts: their files are missing or damaged, they change after restart, or why they are off
|
||||
UI_MENU_PROMPTS_MISSING, UI_MENU_PROMPTS_FAILED, UI_MENU_PROMPTS_RESTART, UI_MENU_PROMPTS_OFF_SHAPE,
|
||||
UI_MENU_PROMPTS_OFF_MODS, UI_MENU_PROMPTS_OFF_PATCHES,
|
||||
// "Restart now" under the prompts while they change after restart, and its confirmation (Cancel: UI_MENU_RESET_NO)
|
||||
UI_MENU_RESTART, UI_MENU_RESTART_DESC, UI_MENU_RESTART_CONFIRM, UI_MENU_RESTART_YES, UI_MENU_RESTART_SAVING,
|
||||
UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
|
||||
UI_MENU_STICK_DEADZONE_NOW, UI_MENU_STICK_SENS, UI_MENU_STICK_SENS_DESC,
|
||||
UI_MENU_MOUSE_SENS, UI_MENU_MOUSE_SENS_DESC,
|
||||
UI_MENU_WALK_KEY, UI_MENU_WALK_KEY_DESC, UI_MENU_WALK_KEY_TAKEN, UI_MENU_WALK_SCALE, UI_MENU_WALK_SCALE_DESC,
|
||||
|
||||
// Controls tab: the remapping rows (ui/binds_page.cpp)
|
||||
UI_BINDS_HELP, UI_BINDS_COL_KEY1, UI_BINDS_COL_KEY2, UI_BINDS_COL_PAD,
|
||||
@@ -99,13 +137,24 @@ typedef enum {
|
||||
UI_PAD_ZR, UI_PAD_LPADDLE1, UI_PAD_RPADDLE1, UI_PAD_LPADDLE2, UI_PAD_RPADDLE2,
|
||||
UI_PAD_CROSS, UI_PAD_CIRCLE, UI_PAD_SQUARE, UI_PAD_TRIANGLE,
|
||||
|
||||
// Amiibo tab (ui/amiibo_page.cpp): the dump folder and its files, the emulated NFC reader and its state, the
|
||||
// amiibo on it and the last action
|
||||
UI_MENU_AMIIBO_DIR, UI_MENU_AMIIBO_OPEN, UI_MENU_AMIIBO_DEFAULT_DIR, UI_MENU_AMIIBO_REFRESH, UI_MENU_AMIIBO_PICK,
|
||||
UI_MENU_AMIIBO_NO_DIR, UI_MENU_AMIIBO_READER, UI_MENU_AMIIBO_STATE, UI_MENU_AMIIBO_STATE_IDLE,
|
||||
UI_MENU_AMIIBO_STATE_SEARCHING, UI_MENU_AMIIBO_STATE_FOUND, UI_MENU_AMIIBO_STATE_MOUNTED,
|
||||
UI_MENU_AMIIBO_STATE_OFF, UI_MENU_AMIIBO_FILE, UI_MENU_AMIIBO_NO_FILE, UI_MENU_AMIIBO_TAP, UI_MENU_AMIIBO_REMOVE,
|
||||
UI_MENU_AMIIBO_AUTO_REMOVE, UI_MENU_AMIIBO_HOLD, UI_MENU_AMIIBO_SECONDS, UI_MENU_AMIIBO_TAG,
|
||||
UI_MENU_AMIIBO_NO_TAG, UI_MENU_AMIIBO_TAG_FILE, UI_MENU_AMIIBO_TAG_ID, UI_MENU_AMIIBO_TAG_UID,
|
||||
UI_MENU_AMIIBO_READY, UI_MENU_AMIIBO_TAPPED, UI_MENU_AMIIBO_LOAD_FAILED, UI_MENU_AMIIBO_REMOVED,
|
||||
UI_MENU_AMIIBO_STATUS,
|
||||
|
||||
// Menu tab
|
||||
UI_MENU_MENU_LANG, UI_MENU_MENU_LANG_DESC, UI_MENU_MENU_LANG_AUTO,
|
||||
|
||||
// Language names, each in its own language and the same in every row (game language and menu language lists)
|
||||
UI_MENU_LANGNAME_EN, UI_MENU_LANGNAME_JA, UI_MENU_LANGNAME_FR, UI_MENU_LANGNAME_DE, UI_MENU_LANGNAME_IT,
|
||||
UI_MENU_LANGNAME_ES, UI_MENU_LANGNAME_NL, UI_MENU_LANGNAME_RU, UI_MENU_LANGNAME_ZH_HANS,
|
||||
UI_MENU_LANGNAME_ZH_HANT, UI_MENU_LANGNAME_KO,
|
||||
UI_MENU_LANGNAME_ZH_HANT, UI_MENU_LANGNAME_KO, UI_MENU_LANGNAME_PT_BR,
|
||||
|
||||
// Advanced
|
||||
UI_MENU_ADV_WARNING,
|
||||
@@ -165,7 +214,16 @@ typedef enum {
|
||||
UI_MENU_OV_FMT_SCENE, // "%ux%u, %.0f%%": the scene and its share of the game image's width
|
||||
UI_MENU_OV_FMT_FSR, // "AMD FSR 1, sharpness %d%%"
|
||||
UI_MENU_OV_SCALE_NONE, UI_MENU_OV_SCALE_BILINEAR,
|
||||
UI_MENU_LAST_ID = UI_MENU_OV_SCALE_BILINEAR,
|
||||
UI_MENU_OV_SCALE_DOWN, // FSR is selected but the image is larger than the window: FSR only enlarges
|
||||
// the Cheats tab (cheats.c): the file, the kinds of cheat and why one cannot be switched
|
||||
UI_MENU_TAB_CHEATS,
|
||||
UI_MENU_CHEATS_NONE, // "%s": <build id>.txt
|
||||
UI_MENU_CHEATS_MOD, // "%s": the mod, "%d": its cheats
|
||||
UI_MENU_CHEATS_REBUILD, UI_MENU_CHEATS_REBUILD_DESC, // restart into the rebuild for unbuilt code cheats
|
||||
UI_MENU_CHEAT_CODE_DESC, UI_MENU_CHEAT_KEY_DESC /* "%s": the buttons */, UI_MENU_CHEAT_DATA_DESC,
|
||||
UI_MENU_CHEAT_MASTER, UI_MENU_CHEAT_MASTER_DESC, UI_MENU_CHEAT_RESTORE, UI_MENU_CHEAT_RESTORE_DESC,
|
||||
UI_MENU_CHEAT_REBUILD, UI_MENU_CHEAT_REBUILD_DESC, UI_MENU_CHEAT_UNSUPPORTED, UI_MENU_CHEAT_UNSUPPORTED_DESC,
|
||||
UI_MENU_LAST_ID = UI_MENU_CHEAT_UNSUPPORTED_DESC,
|
||||
UI_TEXT_COUNT
|
||||
} UiTextId;
|
||||
|
||||
@@ -177,13 +235,17 @@ enum { UI_DESC_BYTES = 2048 };
|
||||
// UTF-8 text for id in the language of ov_language(); English when the catalog has no row for that language.
|
||||
const char *ui_text(UiTextId id);
|
||||
// The same for an explicit catalog language ("en", "ja", "fr", "de", "it", "es", "nl", "pt", "ru", "zh-Hans",
|
||||
// "zh-Hant", "ko"); an empty entry gives the English text.
|
||||
// "zh-Hant", "ko", "pt-BR"); an empty entry gives the English text.
|
||||
const char *ui_text_in(const char *lang, UiTextId id);
|
||||
// Calls visit(ctx, text) for every string the row of lang has its own text for (an id that falls back to English is not
|
||||
// visited): the font builder covers the glyphs of the text a language shows (ui/menu.cpp).
|
||||
void ui_text_visit(const char *lang, void (*visit)(void *ctx, const char *text), void *ctx);
|
||||
// 1 when the row of lang has its own text for id (no fallback to English), for the probe.
|
||||
int ui_text_defined(const char *lang, UiTextId id);
|
||||
// The catalog language for a Switch language code ("en-GB" -> "en", "zh-CN" -> "zh-Hans", ...).
|
||||
const char *ui_text_lang(const char *switch_code);
|
||||
|
||||
// Menu text: ui_text_in(menu language, id). The menu language is TOTK_MENU_LANG ("en" / "ru"); unset, Russian when the
|
||||
// system UI language is Russian, else English. Follows a live change of the setting.
|
||||
// Menu text: ui_text_in(menu language, id). The menu language is TOTK_MENU_LANG ("en", "ru", "pt-BR", "zh-Hans");
|
||||
// unset, the system UI language when the catalog has menu text for it (English, Russian, Brazilian Portuguese,
|
||||
// Simplified Chinese), else English. Follows a live change of the setting.
|
||||
const char *menu_text(UiTextId id);
|
||||
+30
-1
@@ -33,7 +33,9 @@ typedef struct VkCtx {
|
||||
int has_custom_border_color, has_eds, has_eds2, has_eds3, has_vertex_input_dynamic_state,
|
||||
has_conditional_rendering, has_transform_feedback, has_provoking_vertex, has_device_fault, has_memory_budget,
|
||||
has_robustness2, has_buffer_device_address, has_fragment_shader_interlock, has_buffer_marker,
|
||||
has_astc_ldr; // textureCompressionASTC_LDR (enabled when reported)
|
||||
has_astc_ldr, // textureCompressionASTC_LDR (enabled when reported)
|
||||
has_calibrated, // VK_KHR/EXT_calibrated_timestamps (GPU idle split in the perf line)
|
||||
has_hdr_metadata; // VK_EXT_hdr_metadata (vk_hdr.c vk_hdr_metadata)
|
||||
// Feedback loops (maxwell.md 1.9, vk_exec_draw.c feedback_prepare): a draw samples a target of its open pass that
|
||||
// an earlier draw of the pass wrote. TOTK_FEEDBACK=0: VK_FEEDBACK_OFF (sampled in GENERAL without a barrier, the
|
||||
// old behaviour); VK_FEEDBACK_LAYOUT with VK_EXT_attachment_feedback_loop_layout and dynamicRenderingLocalRead
|
||||
@@ -107,6 +109,26 @@ void vk_fsr_release(void); // before the device goes away
|
||||
// scales that one instead, crop {0, 0, w, h}); VK_NULL_HANDLE = nothing recorded. src is in GENERAL, fmt its format.
|
||||
VkImage vk_color_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, const int crop[4]);
|
||||
void vk_color_release(void); // before the device goes away
|
||||
// HDR output (vk_hdr.c). vk_hdr_wanted: TOTK_HDR, read once. vk_hdr_surface_format: the HDR10 surface format when wanted
|
||||
// and offered (1). vk_hdr_set_active: gfx_present.c says whether the swapchain is HDR10 (vk_hdr_active). vk_hdr_encode:
|
||||
// like vk_color_record, src's crop as PQ code values (RGBA16F, crop {0, 0, w, h}). vk_hdr_ui_target / vk_hdr_ui_compose:
|
||||
// the in-game menu's RGBA8 target, then blended onto the swapchain image. vk_hdr_to_srgb8: 10-bit PQ pixels to sRGB bytes.
|
||||
int vk_hdr_wanted(void);
|
||||
void vk_hdr_off(void); // probes that read back 8-bit swapchain pixels: SDR whatever the setting
|
||||
int vk_hdr_surface_format(const VkSurfaceFormatKHR *f, uint32_t n, VkSurfaceFormatKHR *out);
|
||||
void vk_hdr_metadata(VkSwapchainKHR sc, int new_sc);
|
||||
void vk_hdr_set_active(int on);
|
||||
int vk_hdr_active(void);
|
||||
VkImage vk_hdr_encode(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, const int crop[4]);
|
||||
VkImageView vk_hdr_ui_target(VkCommandBuffer cb, uint32_t w, uint32_t h);
|
||||
void vk_hdr_ui_compose(VkCommandBuffer cb, VkImageView dst_view, VkFormat fmt, uint32_t w, uint32_t h);
|
||||
void vk_hdr_to_srgb8(const uint8_t *in, uint8_t *out, size_t px, int a2r);
|
||||
void vk_hdr_release(void); // before the device goes away
|
||||
// The support buffer's HDR parameters (vk_exec_draw.c support_fill, read by shaders/hdr_tonemap.frag).
|
||||
void vk_hdr_support(float out[4]);
|
||||
// vk_contact.c: screen-space contact shadows on the game's composed scene colour (TOTK_CONTACT_SHADOWS)
|
||||
int vk_contact_strength(void); // percent, 0 = off
|
||||
void vk_contact_release(void); // before the device goes away
|
||||
void vk_present_flip(void);
|
||||
// Every submission to ctx->gfx_queue (and xfer_queue when shared) takes this lock; vk_present_image and the swapchain
|
||||
// rebuild do. Recursive.
|
||||
@@ -302,7 +324,14 @@ typedef struct {
|
||||
uint8_t fb_usage; // created with ATTACHMENT_FEEDBACK_LOOP usage (VK_FEEDBACK_LAYOUT, attachable images)
|
||||
uint8_t fb_learned; // a draw sampled it while it was a written attachment of the open pass: its passes
|
||||
// attach it in ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT (vk_exec.c vk_exec_pass_begin)
|
||||
uint8_t scale; // 1, or TOTK_SHADOW_SCALE for a shadow map made larger than the game's (vk_texture.c
|
||||
// shadow_scale_of): width and height above are the larger ones, the game's regions
|
||||
// are multiplied by it
|
||||
} VkTexImage;
|
||||
// The window texture the backend presents (vk_backend.c): *image and *format become the float picture the game copied
|
||||
// into it, when the last draw into it was that copy and the swapchain is HDR10.
|
||||
void vk_hdr_present_source(const VkTexImage *win, VkImage *image, VkFormat *format);
|
||||
void vk_hdr_image_gone(VkImage image);
|
||||
const VkTexImage *vk_texture_get(const tex_t *t); // created and made current; NULL for BUFFER or unmapped formats
|
||||
// A texture slot; a slot whose texture was finalized follows the live texture at the same storage (hle_nvn.c).
|
||||
const VkTexImage *vk_texture_by_id(int id, view_t *view, int *has_view);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
#include "platform/platform.h"
|
||||
#include "vk_dlaa.h"
|
||||
|
||||
#define WEAK __attribute__((weak))
|
||||
|
||||
@@ -100,6 +101,7 @@ static void present_image(const tex_t *t, const int crop[4], uint64_t at_ns, uin
|
||||
uint64_t value) {
|
||||
static volatile int32_t n_skipped;
|
||||
vk_exec_frame_log(t, frame);
|
||||
vk_dlaa_frame();
|
||||
vk_mem_census(frame);
|
||||
if (vk_dump_armed) vk_dump_present(frame);
|
||||
__atomic_store_n(&gfx_counters.validation_errors, (uint64_t)vk_validation_errors_total(), __ATOMIC_RELAXED);
|
||||
@@ -116,6 +118,7 @@ static void present_image(const tex_t *t, const int crop[4], uint64_t at_ns, uin
|
||||
}
|
||||
PresentJob j = {.image = img->image, .format = img->format, .w = (int)img->width, .h = (int)img->height, .frame = frame,
|
||||
.at = at_ns, .queued = queued, .value = value};
|
||||
vk_hdr_present_source(img, &j.image, &j.format); // TOTK_HDR: the float picture the game copied into it, same size
|
||||
if (crop) {
|
||||
j.has_crop = 1;
|
||||
for (int i = 0; i < 4; i++) j.crop[i] = crop[i];
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Screen-space contact shadows (TOTK_CONTACT_SHADOWS, percent strength, 0 = off). TotK fills a full-resolution
|
||||
// G-buffer whose targets include a B10G11R11 scene colour, an R32F linear depth (view distance / far) and a D32F depth
|
||||
// (is_gbuffer), lights it in half resolution, and then composites the lighting into that scene colour in one pass
|
||||
// whose only target is the scene colour, with a D16 depth that sorts pixels into material classes (is_composite).
|
||||
// When the pass after the composite begins, or a dispatch or copy comes first, runtime/shaders/contact_shadow.comp
|
||||
// marches each pixel a short way toward the sun through the linear depth and darkens the scene colour where the ray
|
||||
// passes behind nearby geometry: the small shadows at feet, under stones and in grass that the shadow maps are too
|
||||
// coarse for. Transparent surfaces, the sky and post-processing come after, untouched.
|
||||
//
|
||||
// Inputs from the game, read from the fragment stage's uniform block c4 bound in the composite pass (guest memory,
|
||||
// so it may be one frame newer than the replay): the projection scales c4[7].x and c4[8].y, the far plane c4[14].y,
|
||||
// and the first sun cascade's depth row c4[44].xyz, whose negated direction points from a view-space position toward
|
||||
// the sun (the G-buffer shaders compare dot(c4[44].xyz, pos) + c4[44].w with the cascade map). The scene colour images
|
||||
// get STORAGE usage when the setting is on (vk_texture.c create_tex).
|
||||
#include "vk.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "contact_shadow_spv.h"
|
||||
#include "host.h"
|
||||
#include "vk_exec.h"
|
||||
|
||||
static struct {
|
||||
int tried, ok, strength, debug, logged;
|
||||
PFN_vkCmdPushDescriptorSetKHR push;
|
||||
VkDescriptorSetLayout dsl;
|
||||
VkPipelineLayout layout;
|
||||
VkPipeline pipe;
|
||||
VkSampler sampler;
|
||||
const VkTexImage *scene, *depth; // the last G-buffer's scene colour and linear depth
|
||||
uint32_t w, h;
|
||||
int pending; // the composite into scene ran; the march waits for the next pass
|
||||
uint64_t c4;
|
||||
} C;
|
||||
|
||||
int vk_contact_strength(void) {
|
||||
if (!C.tried) {
|
||||
int s = cfg_int("TOTK_CONTACT_SHADOWS");
|
||||
C.strength = s < 0 ? 0 : s > 100 ? 100 : s;
|
||||
}
|
||||
return C.strength;
|
||||
}
|
||||
|
||||
static int init(VkCtx *c) {
|
||||
C.tried = 1;
|
||||
vk_contact_strength();
|
||||
if (!C.strength) return 0;
|
||||
const char *e = cfg_get("TOTK_CONTACT_DEBUG"); // 1: the frame shows the contact shadow factor in grey
|
||||
C.debug = e && *e && strcmp(e, "0");
|
||||
C.push = (PFN_vkCmdPushDescriptorSetKHR)vkGetDeviceProcAddr(c->device, "vkCmdPushDescriptorSetKHR");
|
||||
VkDescriptorSetLayoutBinding b[2] = {
|
||||
{.binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1,
|
||||
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT},
|
||||
{.binding = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 1,
|
||||
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT}};
|
||||
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
||||
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
|
||||
.bindingCount = 2, .pBindings = b};
|
||||
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, 64};
|
||||
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
||||
.pSetLayouts = &C.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
|
||||
VkSamplerCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = VK_FILTER_NEAREST,
|
||||
.minFilter = VK_FILTER_NEAREST, .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .maxLod = 0};
|
||||
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
|
||||
.codeSize = sizeof contact_shadow_spv, .pCode = contact_shadow_spv};
|
||||
VkShaderModule m = VK_NULL_HANDLE;
|
||||
if (!C.push || vkCreateDescriptorSetLayout(c->device, &dci, NULL, &C.dsl) != VK_SUCCESS ||
|
||||
vkCreatePipelineLayout(c->device, &li, NULL, &C.layout) != VK_SUCCESS ||
|
||||
vkCreateSampler(c->device, &si, NULL, &C.sampler) != VK_SUCCESS ||
|
||||
vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS) {
|
||||
rt_log("vk: contact shadows off: their pipeline could not be created");
|
||||
return 0;
|
||||
}
|
||||
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
||||
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
|
||||
.layout = C.layout};
|
||||
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &C.pipe);
|
||||
vkDestroyShaderModule(c->device, m, NULL);
|
||||
if (r != VK_SUCCESS) {
|
||||
rt_log("vk: contact shadows off: their pipeline could not be created");
|
||||
return 0;
|
||||
}
|
||||
rt_log("vk: contact shadows on, strength %d%%%s", C.strength, C.debug ? " (debug view)" : "");
|
||||
return C.ok = 1;
|
||||
}
|
||||
|
||||
// A G-buffer pass: a B10G11R11 scene colour, an R32F linear depth and a D32F depth, all one size, at least 640 wide.
|
||||
static int is_gbuffer(const VkExec *ex, const VkTexImage **scene, const VkTexImage **depth) {
|
||||
*scene = *depth = NULL;
|
||||
if (ex->pass_noatt || ex->pass_w < 640 || ex->depth.format != VK_FORMAT_D32_SFLOAT || ex->depth.level) return 0;
|
||||
for (uint32_t i = 0; i < ex->pass_ncolor && i < 8; i++) {
|
||||
const VkExecTarget *t = &ex->color[i];
|
||||
if (!t->image || t->level || t->width != ex->pass_w || t->height != ex->pass_h) continue;
|
||||
if (t->format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && !*scene) *scene = t->img;
|
||||
if (t->format == VK_FORMAT_R32_SFLOAT && !*depth) *depth = t->img;
|
||||
}
|
||||
return *scene && *depth && ((*scene)->usage & VK_IMAGE_USAGE_STORAGE_BIT);
|
||||
}
|
||||
|
||||
static void mem_barrier(VkCommandBuffer cb) {
|
||||
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT};
|
||||
VkDependencyInfo d = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
|
||||
vkCmdPipelineBarrier2(cb, &d);
|
||||
}
|
||||
|
||||
void vk_contact_flush(VkExec *ex) {
|
||||
if (!C.pending) return;
|
||||
C.pending = 0;
|
||||
if (ex->in_pass) return;
|
||||
const float *u = (const float *)(uintptr_t)C.c4;
|
||||
float lx = -u[44 * 4 + 0], ly = -u[44 * 4 + 1], lz = -u[44 * 4 + 2], ll = sqrtf(lx * lx + ly * ly + lz * lz);
|
||||
float px = u[7 * 4 + 0], py = u[8 * 4 + 1], zfar = u[14 * 4 + 1];
|
||||
if (!(ll > 1e-6f) || !(px > 0) || !(py > 0) || !(zfar > 1)) return;
|
||||
VkImageView in = vk_exec_target_view(C.depth, VK_FORMAT_R32_SFLOAT, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
0, 0, 1, 0);
|
||||
VkImageView out = vk_exec_target_view(C.scene, VK_FORMAT_B10G11R11_UFLOAT_PACK32, VK_IMAGE_VIEW_TYPE_2D,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1, 0);
|
||||
if (!in || !out) return;
|
||||
VkCommandBuffer cb = ex->cb;
|
||||
float pcv[16] = {px, py, zfar, (float)C.strength / 100.0f,
|
||||
lx / ll, ly / ll, lz / ll, 0.6f,
|
||||
(float)C.w, (float)C.h, 1.0f / (float)C.w, 1.0f / (float)C.h,
|
||||
(float)C.debug, 0, 0, 0};
|
||||
if (!C.logged) {
|
||||
C.logged = 1;
|
||||
rt_log("vk: contact shadows on a %ux%u scene, sun toward %.3f %.3f %.3f (view space), far %g", C.w, C.h,
|
||||
pcv[4], pcv[5], pcv[6], zfar);
|
||||
}
|
||||
mem_barrier(cb); // the compose passes' writes, before the march reads and writes them
|
||||
VkDescriptorImageInfo ii = {C.sampler, in, VK_IMAGE_LAYOUT_GENERAL}, oi = {VK_NULL_HANDLE, out, VK_IMAGE_LAYOUT_GENERAL};
|
||||
VkWriteDescriptorSet w[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 0, .descriptorCount = 1,
|
||||
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &ii},
|
||||
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 1, .descriptorCount = 1,
|
||||
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &oi}};
|
||||
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, C.pipe);
|
||||
rec_push(C.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, C.layout, 0, 2, w);
|
||||
vkCmdPushConstants(cb, C.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pcv, pcv);
|
||||
vkCmdDispatch(cb, (C.w + 7) / 8, (C.h + 7) / 8, 1);
|
||||
mem_barrier(cb); // and what follows sees the darkened colour
|
||||
ex->recorded += 6;
|
||||
vk_exec_draw_new_cb(); // the game's bound compute pipeline and descriptors were replaced: bind them again
|
||||
}
|
||||
|
||||
// The lighting composite: the remembered scene colour as the only target, with a D16 depth of its size.
|
||||
static int is_composite(const VkExec *ex) {
|
||||
return C.scene && ex->pass_ncolor >= 1 && ex->color[0].img == C.scene && !ex->color[0].level &&
|
||||
ex->depth.format == VK_FORMAT_D16_UNORM && ex->pass_w == C.w && ex->pass_h == C.h;
|
||||
}
|
||||
|
||||
void vk_contact_pass_begin(VkExec *ex) {
|
||||
if (!C.tried && !init(vk_ctx())) return;
|
||||
if (!C.ok) return;
|
||||
const VkTexImage *scene, *depth;
|
||||
// The camera block: c4 of the fragment stage, 0x940 bytes. The composite binds it only after its pass begins, so
|
||||
// the newest one seen at any pass start serves.
|
||||
const p_bind_buf *b = &ex->ubo[1][1];
|
||||
if (b->addr && b->size == 0x940) C.c4 = b->addr;
|
||||
if (is_gbuffer(ex, &scene, &depth)) {
|
||||
C.pending = 0;
|
||||
C.scene = scene, C.depth = depth, C.w = ex->pass_w, C.h = ex->pass_h;
|
||||
return;
|
||||
}
|
||||
if (is_composite(ex)) {
|
||||
C.pending = C.c4 != 0;
|
||||
return;
|
||||
}
|
||||
vk_contact_flush(ex);
|
||||
}
|
||||
|
||||
void vk_contact_release(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!c || !C.tried) return;
|
||||
if (C.pipe) vkDestroyPipeline(c->device, C.pipe, NULL);
|
||||
if (C.sampler) vkDestroySampler(c->device, C.sampler, NULL);
|
||||
if (C.layout) vkDestroyPipelineLayout(c->device, C.layout, NULL);
|
||||
if (C.dsl) vkDestroyDescriptorSetLayout(c->device, C.dsl, NULL);
|
||||
memset(&C, 0, sizeof C);
|
||||
}
|
||||
+26
-3
@@ -10,6 +10,7 @@
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
#include "platform/platform.h"
|
||||
#include "vk_dlaa.h"
|
||||
|
||||
static VkCtx g_vk;
|
||||
static int g_ready;
|
||||
@@ -41,6 +42,17 @@ static int layer_present(const char *name) {
|
||||
return found;
|
||||
}
|
||||
|
||||
static int instance_ext_present(const char *name) {
|
||||
uint32_t n = 0;
|
||||
vkEnumerateInstanceExtensionProperties(NULL, &n, NULL);
|
||||
VkExtensionProperties *e = calloc(n ? n : 1, sizeof *e);
|
||||
vkEnumerateInstanceExtensionProperties(NULL, &n, e);
|
||||
int found = 0;
|
||||
for (uint32_t i = 0; i < n; i++) if (!strcmp(e[i].extensionName, name)) found = 1;
|
||||
free(e);
|
||||
return found;
|
||||
}
|
||||
|
||||
// Mesa's drivers (RADV on Linux and the Steam Deck) keep a shader cache of their own beside our pipeline cache, by
|
||||
// default in ~/.cache and up to 1 GB: about 35 MB in a minute of the title screen. A Deck's /home is small and the game
|
||||
// usually sits on the SD card, so unless MESA_SHADER_CACHE_DIR names a folder, it goes to mesa-cache beside the exe.
|
||||
@@ -56,7 +68,7 @@ static void mesa_cache_beside_exe(void) {
|
||||
|
||||
static int create_instance(void) {
|
||||
mesa_cache_beside_exe();
|
||||
const char *ext[8];
|
||||
const char *ext[12];
|
||||
uint32_t n_ext = (uint32_t)gfx_window_vk_extensions(ext, 7);
|
||||
const char *layers[] = {"VK_LAYER_KHRONOS_validation"};
|
||||
int validate = cfg_bool("TOTK_VK_VALIDATION");
|
||||
@@ -65,6 +77,9 @@ static int create_instance(void) {
|
||||
validate = 0;
|
||||
}
|
||||
if (validate) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
|
||||
if (vk_hdr_wanted() && instance_ext_present(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) // HDR10 surface formats
|
||||
ext[n_ext++] = VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME;
|
||||
vk_dlaa_instance_exts(ext, &n_ext, 12);
|
||||
VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "TotK Recomp",
|
||||
.pEngineName = "TotK Recomp", .apiVersion = VK_API_VERSION_1_3};
|
||||
VkInstanceCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app,
|
||||
@@ -209,7 +224,7 @@ static int create_device(void) {
|
||||
vkEnumerateDeviceExtensionProperties(g_vk.phys, NULL, &ne, NULL);
|
||||
VkExtensionProperties *ep = calloc(ne ? ne : 1, sizeof *ep);
|
||||
vkEnumerateDeviceExtensionProperties(g_vk.phys, NULL, &ne, ep);
|
||||
const char *ext[28];
|
||||
const char *ext[48];
|
||||
uint32_t n_ext = 0;
|
||||
for (size_t i = 0; i < sizeof k_required_ext / sizeof *k_required_ext; i++) ext[n_ext++] = k_required_ext[i];
|
||||
|
||||
@@ -246,7 +261,15 @@ static int create_device(void) {
|
||||
CHAIN(fbl, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_FEATURES_EXT);
|
||||
CHAIN(lr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR);
|
||||
}
|
||||
vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the ones below
|
||||
if (has_ext(ep, ne, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { g_vk.has_memory_budget = 1; ext[n_ext++] = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }
|
||||
// GPU timestamps on the CPU clock (vk_sync.c): the perf line splits GPU idle time into waiting for a submission or not.
|
||||
if (has_ext(ep, ne, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 2; ext[n_ext++] = VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
|
||||
else if (has_ext(ep, ne, VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 1; ext[n_ext++] = VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
|
||||
if (vk_hdr_wanted() && has_ext(ep, ne, VK_EXT_HDR_METADATA_EXTENSION_NAME)) { // the display's peak (vk_hdr_metadata)
|
||||
g_vk.has_hdr_metadata = 1;
|
||||
ext[n_ext++] = VK_EXT_HDR_METADATA_EXTENSION_NAME;
|
||||
}
|
||||
// GPU breadcrumbs for a lost device (vk_crumbs.c): AMD's drivers and RADV report it.
|
||||
if (has_ext(ep, ne, VK_AMD_BUFFER_MARKER_EXTENSION_NAME)) { g_vk.has_buffer_marker = 1; ext[n_ext++] = VK_AMD_BUFFER_MARKER_EXTENSION_NAME; }
|
||||
// Graphics pipeline library: chained last so it can be dropped again when vk_caps_gpl declines it.
|
||||
@@ -368,7 +391,7 @@ int vk_init(void) {
|
||||
plat_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
memset(&g_vk, 0, sizeof g_vk);
|
||||
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_headroom_start();
|
||||
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_headroom_start(), vk_dlaa_init();
|
||||
else destroy_all();
|
||||
}
|
||||
int ok = g_ready;
|
||||
|
||||
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user