mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 09:51:02 +00:00
Compare commits
No files matched your search
@@ -0,0 +1,65 @@
|
||||
name: Crash
|
||||
description: The game closes, freezes or shows a FATAL message.
|
||||
labels: [bug, crash]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first: they cover the known crashes and their workarounds.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this crash.
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: GPU, driver and Windows version
|
||||
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: when
|
||||
attributes:
|
||||
label: When does it crash?
|
||||
options:
|
||||
- At startup, before the Nintendo logo
|
||||
- Between the Nintendo logo and the title screen
|
||||
- While loading a save
|
||||
- In gameplay
|
||||
- In a cutscene
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Can you keep playing?
|
||||
options:
|
||||
- "Yes, it does not happen again after a restart, or I can avoid it"
|
||||
- "No, it happens every time at the same point"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What you did before the crash
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Attach logs\totk.log from the folder of totk.exe. Copy it before you start the game again, because the next start overwrites it.
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Graphics glitch
|
||||
description: Artifacts, missing or broken textures, flicker, wrong colours or shadows.
|
||||
labels: [bug, graphics]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first: they cover the known glitches on each GPU.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this glitch.
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: GPU, driver and Windows version
|
||||
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshot
|
||||
attributes:
|
||||
label: Screenshot or video
|
||||
description: Drag the file into this field. Say where in the game it was taken.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: What looks wrong and when
|
||||
description: Does it show all the time, in one place, or after some time in play? Which settings did you change from the defaults?
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Can you keep playing?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No, I cannot see enough to play"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Attach logs\totk.log from the folder of totk.exe.
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Other bug
|
||||
description: Sound, controls, performance, the settings menu, the installer, or game logic.
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this bug.
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
options:
|
||||
- Audio
|
||||
- Controls and input
|
||||
- Performance and stutter
|
||||
- Settings menu and overlay
|
||||
- Installer
|
||||
- Game logic and physics
|
||||
- Something else
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: GPU, driver and Windows version
|
||||
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Can you keep playing?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No, it stops the game or a quest"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What happened and how to reproduce it
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Attach logs\totk.log from the folder of totk.exe.
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Feature request
|
||||
description: A setting, option or support for something the port does not do yet.
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) and the Planned section of the README for this request.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: request
|
||||
attributes:
|
||||
label: What you want and why
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Bug report
|
||||
description: Something breaks, crashes or looks wrong.
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: GPU, driver and Windows version
|
||||
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What happened and how to reproduce it
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Attach logs\totk.log from the folder of totk.exe. After a crash, copy it before you start the game again.
|
||||
@@ -1 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question or help with setup
|
||||
url: https://github.com/valentderah/totk-pc-recomp/discussions
|
||||
about: Installing, updating and configuring the port. Ask in Discussions.
|
||||
@@ -42,6 +42,9 @@ __pycache__/
|
||||
*.o
|
||||
*.lib
|
||||
*.a
|
||||
*.so
|
||||
*.so.*
|
||||
*.AppImage
|
||||
|
||||
# .NET build output of the shader translator (tools/build_xlate.py)
|
||||
/tools/xlate/bin/
|
||||
|
||||
@@ -1,45 +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: LB, RB, LT, RT, Menu and View, and A, B, X and Y as labelled on the button to press, with either button layout
|
||||
- Shaders prepared at install instead of during play
|
||||
- Mods made for emulators: unpack a mod into the `mods` folder next to `totk.exe`; switch and order them in the menu. RomFS
|
||||
mods, TKMM's merged output and ExeFS code patches (`.ips`, `.pchtxt`) work; the game rebuilds itself for code patches
|
||||
([docs/mods.md](docs/mods.md))
|
||||
- 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
|
||||
- Custom aspect ratios, including ultrawide
|
||||
- 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
|
||||
|
||||
|
||||
+31
-2
@@ -9,6 +9,11 @@ Code copied or adapted from another project is listed here with its source, the
|
||||
| LAS Recomp host runtime: NSO loader, host-function table, guest-space reservation and re-execution, guest thread start | https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f`, Copyright (c) 2026 Peiikai | MIT | `runtime/loader.c`, `runtime/hostfn.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/boot.c`; `runtime/vk_device.c`, `runtime/gfx_window.c`, `runtime/tex_astc.c` (`runtime/nvn/astc.c`; its decode ported to GLSL in `runtime/shaders/astc_decode.comp`, compiled into `runtime/astc_decode_spv.h`, and its tables copied into `runtime/vk_astc.c`), `runtime/vk_formats.c` (format table of `runtime/nvn/vulkan/textures.c`), `runtime/vk_memory.c` (page write generations of `runtime/nvn/vulkan/exec.c`, deferred destruction of `textures.c`), `runtime/vk_texture.c` (image creation, uploads and samplers of `runtime/nvn/vulkan/textures.c`), `runtime/vk_sync.c` (timeline submissions and waits of `runtime/nvn/vulkan/exec.c`), `runtime/vk_pipeline.c` (blend, compare and stencil mappings, vertex format table with its 3-component fallback and pipeline cache file of `runtime/nvn/vulkan/exec.c` and `device.c`), `runtime/vk_exec.c` (pass begin and end, scissor clip and attachment clears of `runtime/nvn/vulkan/exec.c`), `runtime/vk_exec_xfer.c` (copies of `runtime/nvn/vulkan/exec.c`), `runtime/vk_exec_draw.c` (uniform buffer spans, texture and sampler descriptors and dispatch flow of `runtime/nvn/vulkan/exec.c`); in-game menu: `runtime/ui/menu.cpp` (Dear ImGui Vulkan set-up of `runtime/nvn/vulkan/imgui_glue.cpp`) |
|
||||
| MacNativeGames TotK host runtime: dynamic linking, identity check, module permissions, guest memory and SVCs, threads, IPC, audio renderer, system services, RomFS and save storage, boot sequence, the NVN proc name list | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/runtime/`, `games/totk/scripts/prepare_runtime.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/loader.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/svc.c`, `runtime/ipc.c`, `runtime/ipc.h`, `runtime/audren.c`, `runtime/audren.h`, `runtime/audio_mixer.c`, `runtime/audio_effects.c`, `runtime/audio_effects.h`, `runtime/audio_reverb.c`, `runtime/audio_reverb.h` (SDL3 output replaces CoreAudio), `runtime/ov_system.c` (also `shared_font.c`: the BFTTF layout behind nn::pl), `runtime/ov_input.c` (also `hid_feedback.c`: vibration and six-axis handles and calls), `runtime/motion_fusion.c` (six-axis fusion of `motion_fusion.c`), `runtime/ov_media.c` (hardware Opus decoder over libopus), `runtime/swkbd_win.c` (`swkbd_host.c`: ShowKeyboardArg offsets, UTF-8/UTF-16 conversion, disabled-key filter, result codes; Win32 window replaces the macOS text bar), `runtime/ov_gfx.c`, `runtime/romfs.c`, `runtime/romfs_path.h`, `runtime/fs_host.c`, `runtime/fs.h`, `runtime/boot.c`; `runtime/gfx_window.c`; `tools/gen_nvn_procs.py`, `runtime/nvn_procs.inc` (`NVN/names.txt` and the `NVN_EXTRA` names of `totk_nvn_ext.c`) |
|
||||
| MacNativeGames TotK frame-rate patches: 60 fps present-interval controller, real-time frame step and its present sampling, hair and cloth clock, trampoline slow-motion check (instruction words and 1.2.1 sites), player position chain; quick-menu capture background RomFS overlay | https://github.com/ly05010419/MacNativeGames @ `ebe621de99bfc48f3c42f2d2aa73e04fbcc6eae2` (`games/totk/runtime/game_patches.c`, `game_version.h`, `world_survey.c`, `games/totk/scripts/make_romfs_patches.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/frame_rate.c`, `recomp/patches-v121.txt`, `tools/make_romfs_patches.py` |
|
||||
| MacNativeGames TotK 1.4.3 fade fix: the four distance / LOD and pre-actor fade sites, their instruction words and the floor of the fade step at 1 | https://github.com/ly05010419/MacNativeGames @ `3e9e7f67fdfdd3404134dcfa0f24969a9425a33f` (`games/totk/runtime/game_patches_v143.c` patch_fade_increment), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set fade; the floor without flags, clz >> 5, as in pull request #78) |
|
||||
| MacNativeGames TotK aspect ratio: the camera and viewport sites that hand the perspective projection its aspect ratio, and the UI layout edits for a canvas wider than 16:9 | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/runtime/aspect_patch.h`, `aspect_math.h`, `game_patches_v143.c`, `games/totk/scripts/ultrawide_layout.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set aspect), `tools/ultrawide_layout.py`, `tools/make_romfs_patches.py` (--aspect) |
|
||||
| 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` |
|
||||
| d2r-mobile host thread environment (TLR, TLS, thread object), the origin of MacNativeGames' thread layer; d2r-mobile NVN host object layouts, command record format and NVN front end, as adapted for TotK in MacNativeGames' `d2r_gfx` copy | https://github.com/jhaizhou-ops/d2r-mobile @ `53e7c5bb9ca3ea53e2160005be8fbe920cd9cb53` (`dm/host/thread.c`, `dm/host/hle_thread.c`); the NVN layouts and front end via https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/nvn/nvn_obj.h`, the two types of `nvn_tex.h`, `hle_nvn.c`, `totk_nvn_ext.c`, `nvn_impl.h`, `nvn_hmap.h`, `nvn_pool_index.h`, the request checks and mapping semantics of `totk_vpool.c`, the generation stamps of `nvn_dirty.c`, texture mirroring and texel copies of `nvn_mtl.m`; licence in `games/totk/third_party/d2r_gfx/LICENSE`), Copyright (c) 2026 jhaizhou | MIT | `runtime/kernel.c`; `runtime/nvn_obj.h`; `runtime/nvn_api.h`, `runtime/nvn_api_core.c`, `runtime/nvn_hmap.h`, `runtime/nvn_pool_index.h`; `runtime/nvn_api_obj.c` (also `nvn_pace.h`); `runtime/nvn_api_cmd.c`, `runtime/nvn_ext.c`; `runtime/vk_memory.c`, `runtime/vk_texture.c`; `runtime/vk_sync.c` (side effects written after GPU completion, `hle_nvn.c`); `runtime/vk_pipeline.c` (pipeline key and state mapping of `nvn_mtl.m` `pso_key_fill` / `pso_desc`, its `vfmt` vertex formats and the missing-varying fix-up of `msl_varyings`); `runtime/vk_exec.c`, `runtime/vk_exec.h` (op loop, pure-state ops and state defaults of `nvn_mtl.m` `mtl_exec` / `state_op` / `state_reset`); `runtime/vk_exec_xfer.c` (copy, clear, event, counter and conditional-render semantics of `nvn_mtl.m` `copy_t2b` / `copy_b2t` / `copy_t2t` / `clear_tex` / `cond_eval`); `runtime/vk_exec_draw.c` (bind ops, draw and dispatch flow, vertex streams of `nvn_mtl.m` `state_op` / `bind_vertex_streams` / `draw_inner` / `dispatch`) |
|
||||
@@ -34,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
|
||||
|
||||
@@ -61,8 +67,10 @@ These tools run on the developer's or player's machine during the build. They ar
|
||||
|
||||
| Component | Source | License | Where in the release |
|
||||
|---|---|---|---|
|
||||
| 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/` |
|
||||
@@ -71,6 +79,27 @@ These tools run on the developer's or player's machine during the build. They ar
|
||||
| Vulkan-Headers and Vulkan-Loader 1.4.363.0 (MSYS2 packages; headers and `libvulkan-1.dll.a` only, no loader DLL) | https://github.com/KhronosGroup | Apache-2.0 OR MIT | `runtime/deps/` |
|
||||
| Vulkan Memory Allocator 3.4.0 (MSYS2 package), header | https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator | MIT | `runtime/deps/` |
|
||||
| Ninja 1.13.2 (`ninja-win.zip`) | https://github.com/ninja-build/ninja/releases/tag/v1.13.2 | Apache-2.0 | `bin/ninja.exe` |
|
||||
| hactool (SciresM @ `1d64a83450e025622f3468c28fc4164dad2c5ef6` plus `tools/hactool/portable.patch`, from MacNativeGames' `build_hactool()`: `MNG_TITLE_KEYS` and relaxed BKTR layout checks; our `tools/hactool/unicode.patch`: an embedded manifest that makes UTF-8 the process code page), built by `tools/build_hactool.py` with MSYS2 UCRT64 gcc; imports only KERNEL32 and the UCRT | https://github.com/SciresM/hactool | ISC | `bin/hactool.exe` |
|
||||
| hactool (SciresM @ `1d64a83450e025622f3468c28fc4164dad2c5ef6` plus `tools/hactool/portable.patch`, from MacNativeGames' `build_hactool()`: `MNG_TITLE_KEYS` and relaxed BKTR layout checks; our `tools/hactool/unicode.patch`: an embedded manifest that makes UTF-8 the process code page; our `tools/hactool/mbedtls-asm-clobbers.patch`: the clobbers the bundled mbedtls's x86-64 multiply asm lacks), built by `tools/build_hactool.py` with MSYS2 UCRT64 gcc; imports only KERNEL32 and the UCRT | https://github.com/SciresM/hactool | ISC | `bin/hactool.exe` |
|
||||
| `totk_xlate.exe` (Ryujinx shader translator, NativeAOT build of `tools/xlate/`, see the Ryujinx row above) | built by `tools/build_xlate.py` | MIT | `bin/totk_xlate.exe` |
|
||||
| MacNativeGames shader variant list | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/assets/shader_list.tsv`), Copyright (c) 2026 MacNativeGames contributors | MIT | `data/shader_list.tsv` |
|
||||
|
||||
## Distributed in the Linux release (AppImage)
|
||||
|
||||
`tools/make_release_linux.py` assembles these into the folder of `docs/installer.md` ("Linux release") and packs it as
|
||||
`TotK-Setup-x86_64.AppImage`. Versions and sha256 of every download are pinned in that script; licence texts are in the
|
||||
folder's `licenses/` and in each component's own folder.
|
||||
|
||||
| Component | Source | License | Where in the release |
|
||||
|---|---|---|---|
|
||||
| CPython 3.14.10, python-build-standalone 20261026 `install_only` build, with `tkinter` and the libraries it bundles (OpenSSL, libffi, SQLite, zlib, Tcl/Tk and others; their licences ship in the folder) | https://github.com/astral-sh/python-build-standalone/releases/tag/20261026 | PSF-2.0; bundled libraries under their own licences | `python/` |
|
||||
| numpy 2.5.4 (with the libraries its manylinux wheel bundles), lz4 4.4.5, cryptography 50.0.2, cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | as in the installer release row above | `python/lib/python3.14/site-packages/` |
|
||||
| LLVM 23.1.4 official Linux X64 release: `clang`, `lld`, `llvm-ar`, `llvm-ranlib`, `llvm-objcopy`, `llvm-strip` and the clang resource headers; `llvm-LICENSE.txt` from the `llvmorg-23.1.4` source tag | https://github.com/llvm/llvm-project/releases/tag/llvmorg-23.1.4 | Apache-2.0 with LLVM exception | `llvm/` |
|
||||
| Steam Runtime 3 "sniper" SDK sysroot 3.0.20261021.266025 (Debian 11 based), only the headers, libraries and objects a game build reads: glibc 2.31 (headers, `crt*.o`, `libc_nonshared.a`, link libraries), GCC 10 `libstdc++.a` and `libgcc`, SDL 3.4.18, libopus, libvpx 1.9 (static), the Vulkan loader's link library; their Debian `copyright` files go into `licenses/` | https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20261021.266025/ | glibc LGPL-2.1+; libstdc++ and libgcc GPL-3.0 with the GCC Runtime Library Exception; SDL zlib; libopus BSD-3-Clause; libvpx BSD-3-Clause with the WebM patent grant (`libvpx-PATENTS.txt` from the v1.17.0 source tag); Vulkan loader Apache-2.0 | `llvm/sysroot/`, `bin/libSDL3.so.0` (the sysroot's, copied next to the game by the setup), `licenses/` |
|
||||
| Vulkan-Headers 1.4.363 (source tag) | https://github.com/KhronosGroup/Vulkan-Headers/tree/v1.4.363 | Apache-2.0 OR MIT | `runtime/deps/include/` |
|
||||
| Vulkan Memory Allocator 3.4.0, header (the licence text is its header comment) | https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/tree/v3.4.0 | MIT | `runtime/deps/include/vma/` |
|
||||
| Zstandard 1.5.7, built from the release tarball into a static `libzstd.a` with the folder's clang and sysroot | https://github.com/facebook/zstd/releases/tag/v1.5.7 | BSD-3-Clause | `runtime/deps/` |
|
||||
| Ninja 1.13.2 (`ninja-linux.zip`) | https://github.com/ninja-build/ninja/releases/tag/v1.13.2 | Apache-2.0 | `bin/ninja` |
|
||||
| hactool (as in the installer release row above), built by `tools/build_hactool.py --cc` with the folder's clang and sysroot; includes its bundled mbedtls | https://github.com/SciresM/hactool | ISC; mbedtls Apache-2.0 | `bin/hactool` |
|
||||
| `totk_xlate` (NativeAOT linux-x64 build of `tools/xlate/`, linked with the folder's clang, lld and sysroot) | built by `tools/build_xlate.py --llvm --sysroot` | MIT | `bin/totk_xlate` |
|
||||
| AppImage type-2 runtime (static; includes libfuse and squashfuse), the AppImage's first part; appimagetool packs the folder and is not distributed | https://github.com/AppImage/type2-runtime | MIT; libfuse LGPL-2.1; squashfuse BSD-2-Clause | the AppImage header |
|
||||
| MacNativeGames shader variant list | as in the installer release row above | MIT | `data/shader_list.tsv` |
|
||||
@@ -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]
|
||||
|
||||
+29
-4
@@ -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, and the submit waits until the ring is empty. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. `TOTK_VK_RECORDER=0` makes every call on the submitting thread.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. 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'
|
||||
@@ -93,7 +96,29 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
|
||||
- Resolution. Patch sets make the game read its output, swapchain and scene sizes from the patch page
|
||||
(MacNativeGames' sites); `resolution.c` sets them from `output_res` and `render_scale` and grows the texture heap
|
||||
and the application pool to match. When the window is larger than the output, `vk_fsr.c` runs AMD FSR 1 (EASU,
|
||||
RCAS) on the present.
|
||||
RCAS) on the present. An `output_res` that is not 16:9 (more than 1% off, `res_is_16x9`) also switches on the
|
||||
`aspect` patch set, which hands the output's ratio to the camera, and `romfs.c` `overlay_init` loads the
|
||||
`romfs-patches-<w>x<h>` overlay for it when it exists (`tools/make_romfs_patches.py --aspect`, laid out by
|
||||
`tools/ultrawide_layout.py`; the installer builds ten sizes: 21:9 2560x1080, 3440x1440, 3840x1600, 5120x2160, 32:9
|
||||
3840x1080, 5120x1440 and 16:10 1280x800, 1680x1050, 1920x1200, 2560x1600). A wider layout moves the HUD to the
|
||||
screen's corners, a narrower one to its top and bottom edges. Other sizes get the camera of their shape and the 16:9
|
||||
HUD stretched over it. The `aspect` setting (`fit`, the default; `stretch`) decides what the present does
|
||||
when the window's shape differs from the output's: `fit` letterboxes, where a 16:9 game in a wider window used to be
|
||||
stretched.
|
||||
- HDR (`hdr = 1`, `vk_hdr.c`). The game tonemaps its scene into an r11g11b10f target, upscales it and draws the HUD
|
||||
into another float target, and a last draw copies that into the 8-bit sRGB window texture it presents. With HDR on,
|
||||
the tonemap's fragment shader (recognised by its constants and resource names, not a hash) runs as
|
||||
`shaders/hdr_tonemap.frag`, which keeps highlights up to the display's peak; the present reads the float target the
|
||||
window texture was copied from (any other last draw into it, such as a menu or a movie, leaves the window texture,
|
||||
shown at the HUD's white); a compute pass turns the picture into PQ code values in Rec. 2020 for an HDR10
|
||||
swapchain, which FSR 1 or the blit then scale; the in-game menu is drawn into an RGBA8 image and blended on at the
|
||||
HUD's white. Without an HDR10 surface format (Windows with "Use HDR" off, a compositor without colour management)
|
||||
the swapchain stays SDR. `TOTK_SHOTS` reads the window texture, so its pictures clip highlights in HDR.
|
||||
- DLAA (`vk_dlaa.c`, `dlaa`, off by default). NVIDIA DLSS at the scene's own size, through the driver's NGX core loaded
|
||||
at run time (no SDK code compiled in). It runs on the R11G11B10F light target right before the bloom chain's first
|
||||
pass, so post effects and the HUD read the anti-aliased picture. The scene's draws get a sub-pixel Halton viewport
|
||||
jitter; 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`).
|
||||
|
||||
+94
-10
@@ -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.
|
||||
@@ -37,20 +39,23 @@ PATH:
|
||||
|
||||
### hactool
|
||||
|
||||
`tools/build_hactool.py` fetches SciresM/hactool at `1d64a83` into `toolchain\src\hactool`, applies the two patches in
|
||||
`tools/build_hactool.py` fetches SciresM/hactool at `1d64a83` into `toolchain\src\hactool`, applies the three patches in
|
||||
`tools/hactool/` and builds a static `toolchain\hactool\hactool.exe` with MSYS2 UCRT64 gcc (a few seconds):
|
||||
|
||||
- `portable.patch`: the parts of MacNativeGames' `build_hactool()` that run on Windows, the `MNG_TITLE_KEYS` variable and
|
||||
the two BKTR fixes in `nca.c` for the subsection table padding and the subsection layout of newer cumulative updates;
|
||||
- `unicode.patch`: a manifest with `<activeCodePage>UTF-8</activeCodePage>`, so paths under a non-ASCII user profile
|
||||
reach it intact.
|
||||
reach it intact;
|
||||
- `mbedtls-asm-clobbers.patch`: the `memory` and `cc` clobbers the bundled mbedtls 2.6.1's x86-64 multiply asm lacks.
|
||||
Without them a clang build (the Linux release) fails every RSA signature check and the update merge stops.
|
||||
|
||||
hactool reads `-k <prod.keys>`; MSYS2 shells set `HOME`, which hides its default key location, so pass `-k` there.
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root, in PowerShell. Every step that depends on the game version takes it as `<ver>`: `v143` for
|
||||
1.4.3 or `v121` for 1.2.1. The tools default to `v121` when `--version` is left out. Saves load on either version.
|
||||
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
|
||||
need the stdlib `compression.zstd` of Python 3.14:
|
||||
@@ -66,19 +71,22 @@ From the repository root, in PowerShell. Every step that depends on the game ver
|
||||
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
|
||||
```
|
||||
|
||||
5. Lift the modules to C, write the RomFS half of the frame-rate patches, then compile and link `build\<ver>\totk.exe`:
|
||||
5. Lift the modules to C, write the RomFS overlay (the frame-rate fix), then compile and link
|
||||
`build\<ver>\totk.exe`:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python recomp\gen.py --exefs build\extracted\<ver>\exefs --out generated\<ver> --patches recomp\patches-<ver>.txt
|
||||
@@ -86,9 +94,28 @@ From the repository root, in PowerShell. Every step that depends on the game ver
|
||||
.venv\Scripts\python tools\build_game.py --version <ver>
|
||||
```
|
||||
|
||||
`--cpu v2` targets x86-64-v2 (SSE4.2, no AVX2/FMA) for older CPUs; the default `v3` needs AVX2. The v2 build links
|
||||
`runtime/fma.c`, a correctly rounded software fma the FP lowering needs, and runs slower. Inline assembly,
|
||||
target attributes and intrinsics are not bound by `-march`, so a v2 build runs `tools/check_isa.py` over the runtime
|
||||
objects before it links: any AVX, BMI or other instruction above x86-64-v2 outside a function listed there as
|
||||
dispatched by a runtime CPU check stops the build.
|
||||
|
||||
`gen.py` sizes its emit pool from the CPU count and the free memory; `--jobs N` sets it, and so does the environment variable `TOTK_LIFT_JOBS=N`, which the installer's lift step honours too (it has no `--jobs`). When a worker runs out of memory, the pool restarts with half the workers (`emit: out of memory with N workers, retrying with M`), down to one; only an out-of-memory error with one worker fails the step.
|
||||
|
||||
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
|
||||
A compiler cache in front of clang shortens it further: with `SCCACHE` set to an sccache executable, `build_game.py`
|
||||
writes it into the ninja file ahead of `cc`, and every lift rewrites the generated chunks with the same contents, so
|
||||
a second build of the same version hits all of their objects:
|
||||
|
||||
```powershell
|
||||
$env:SCCACHE = "path\to\sccache.exe"
|
||||
.venv\Scripts\python tools\build_game.py --version <ver>
|
||||
```
|
||||
`--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` 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:
|
||||
@@ -100,8 +127,14 @@ From the repository root, in PowerShell. Every step that depends on the game ver
|
||||
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. Keep the window in the foreground; an unfocused window stalls the game.
|
||||
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.
|
||||
|
||||
```powershell
|
||||
build\<ver>\totk.exe
|
||||
@@ -117,6 +150,48 @@ your machines without MSYS2, Python or .NET. `romfs.bin` stays out unless you pa
|
||||
`romfs\` next to the exe or point `TOTK_ROMFS` at its folder. Run `totk.exe --prepare-shaders` once in the copied
|
||||
folder. The folder holds code and data derived from your game: for your own machines only.
|
||||
|
||||
## Linux (in progress)
|
||||
|
||||
The runtime builds and runs on x86-64 Linux with a window, input and menu on SDL3 (`*_sdl.c`) in place of the Win32
|
||||
files (`*_win.c`); `runtime/platform/linux.c` implements `platform.h`. Status: the selftest and the probes pass; 1.2.1
|
||||
plays at 60 fps on RADV (RX 7800 XT) and NVIDIA (RTX 3060); 1.4.3 builds, probes and plays on RADV (RX 6600, and the
|
||||
Vega 7 of a Ryzen 5 4600G at about 34 fps). Long play sessions and the Steam Deck itself are not verified yet.
|
||||
|
||||
Requirements:
|
||||
|
||||
- Linux 6.7 or later: the write watch uses userfaultfd write protection with `PAGEMAP_SCAN`. SteamOS 3.8 qualifies.
|
||||
- A Vulkan 1.3 driver with `VK_EXT_external_memory_host` (RADV, NVIDIA).
|
||||
- Packages on Arch:
|
||||
|
||||
```sh
|
||||
pacman -S --needed clang lld ninja make sdl3 opus libvpx zstd vulkan-headers vulkan-icd-loader \
|
||||
vulkan-validation-layers shaderc spirv-tools dotnet-sdk uv git
|
||||
```
|
||||
|
||||
Vulkan Memory Allocator is a single header (AUR `vulkan-memory-allocator`). Without it, `build_game.py` fetches the
|
||||
version the Windows release pins (3.4.0) into `toolchain/vma/` on first use and checks its hash.
|
||||
|
||||
The steps are the Windows ones with POSIX paths: `.venv/bin/python` in place of `.venv\Scripts\python`, and `totk` in
|
||||
place of `totk.exe`. `build_game.py` takes clang, lld and Ninja from PATH (or the ones in `<dir>/bin` with
|
||||
`--toolchain <dir>`) and links against the system libraries, with `libstdc++` static and an `$ORIGIN` rpath;
|
||||
`build_hactool.py` builds `hactool` with make. `tools/prepare_shaders.py` reads the shader list from
|
||||
`refs/MacNativeGames` at `origin/HEAD`, which must be the pinned commit with its history (the list format changed later).
|
||||
|
||||
The probes open a window, so from an SSH session they need the desktop session's display, or they stop at `gpu 0
|
||||
skipped: no graphics+compute+present queue family`: on Wayland `XDG_RUNTIME_DIR=/run/user/$(id -u)
|
||||
WAYLAND_DISPLAY=wayland-1 SDL_VIDEODRIVER=wayland` (the socket name is in `$XDG_RUNTIME_DIR`), on X11 `DISPLAY=:0`.
|
||||
|
||||
Memory: on Windows the GPU imports guest memory and `GetWriteWatch` tracks the CPU's writes to it. On Linux,
|
||||
re-protecting imported memory invalidates the driver's userptr buffer on every submit (about 1.3 ms per 16 MiB on
|
||||
amdgpu), and importing pins the pages, which drops their write protection. Each CPU-visible pool therefore gets a
|
||||
host-visible copy (shadow storage, `runtime/vk_memory.c`): the pages the CPU wrote go in before each submit, and the GPU's
|
||||
writes come back at its fence. In play that costs about 0.2 ms a frame and doubles the memory of those pools (about
|
||||
1.1 GB). `TOTK_SHADOW=1` turns it on under Windows too. The Steam Deck's RADV has `VK_EXT_external_memory_host` as
|
||||
well: the shadow copies are chosen for the measured cost above, not for a missing extension.
|
||||
|
||||
The Deck's GPU (Van Gogh) has no native ASTC, so its textures go through the compute decoder (`runtime/vk_astc.c`,
|
||||
`TOTK_ASTC`) as on desktop AMD GPUs without it; frame times measured there include it.
|
||||
|
||||
## Checks
|
||||
|
||||
Before a commit:
|
||||
@@ -125,6 +200,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`:
|
||||
|
||||
@@ -138,8 +217,13 @@ 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-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
|
||||
exe's own version first, then of the other one.
|
||||
|
||||
Shaders: `.venv-py314\Scripts\python tools\shader_census.py --version <ver> --translate` extracts every Maxwell shader
|
||||
to `build\<ver>\shaders\` and runs it through the translator and `spirv-val`; failure groups go to `failures.txt`.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Cheats
|
||||
|
||||
The port reads Atmosphère cheat files, the format Switch homebrew and the emulators use. It ships no cheat files: a cheat file goes into a mod in the `mods` folder next to `totk.exe`, like any other mod ([mods.md](mods.md)). Open the menu (F1) and switch the cheats on the Cheats tab. The switches are saved under `[cheats]` in `totk.ini`.
|
||||
|
||||
## Adding a cheat file
|
||||
|
||||
1. Find the cheat file for your update. Cheat sites and collections name it after the first 8 bytes of the game's build id:
|
||||
|
||||
| Update | File |
|
||||
|---|---|
|
||||
| 1.2.1 | `9B4E43650501A4D4.txt` |
|
||||
| 1.4.3 | `277178B7DBA1B6D4.txt` |
|
||||
|
||||
The Cheats tab names the file the installed game reads when no mod holds one, and `logs\totk.log` says `cheats: no enabled mod holds cheats/<id>.txt`.
|
||||
2. Put it in the `cheats` folder of a mod, a folder of its own under `mods`:
|
||||
|
||||
```
|
||||
mods\
|
||||
My cheats\
|
||||
cheats\
|
||||
277178B7DBA1B6D4.txt
|
||||
```
|
||||
|
||||
A download laid out for the Switch's SD card or an emulator works as it is: unpack it into a folder of its own under `mods`, and the port finds `atmosphere\contents\0100F2C0115B6000\cheats`, `contents\0100F2C0115B6000\cheats` or `0100F2C0115B6000\cheats` inside it. The file name's case does not matter.
|
||||
3. Start the game. Cheats that write the game's memory work now.
|
||||
4. Cheats that change the game's code need the game rebuilt with them. When the game starts and finds such cheats it did not build, it opens the setup's rebuild window, which builds them in (a few minutes; the shaders stay prepared) and starts the game again. Until then they are greyed out with "after the rebuild". When the rebuild fails or you cancel it, the game starts without them and does not try again for the same cheats; "Rebuild the game now" on the Cheats tab tries again. `mods_rebuild = off` under `[mods]` in `totk.ini` turns the automatic rebuild off; running `TotK-Setup.exe` on the same folder does the same job by hand. Saves and settings are kept.
|
||||
5. Switch the cheats on the Cheats tab of the menu.
|
||||
|
||||
The Cheats tab lists the cheats grouped by mod, in the order of the Mods tab. A mod switched off on the Mods tab, or one the port cannot use, contributes no cheats; the change applies at the next start. The Mods tab says how many cheats each mod brought.
|
||||
|
||||
## The file format
|
||||
|
||||
Standard Atmosphère: a `[name]` line starts a cheat, `{name}` a master cheat that is always on, and the cheat's opcodes follow as 8-digit hex words. A name with no opcodes is shown as a heading. The file's title line (the one with `TID=`) is not shown.
|
||||
|
||||
```
|
||||
[The Legend of Zelda: Tears of the Kingdom v1.4.3 TID=0100F2C0115B6000 BID=277178B7DBA1B6D4]
|
||||
|
||||
[Player]
|
||||
[A code cheat]
|
||||
040A0000 XXXXXXXX YYYYYYYY
|
||||
|
||||
[A data cheat (Hold ZL+X)]
|
||||
80000104
|
||||
580F0000 XXXXXXXX
|
||||
640F0000 00000000 YYYYYYYY
|
||||
20000000
|
||||
```
|
||||
|
||||
The words above are placeholders: `XXXXXXXX` an offset, `YYYYYYYY` a value.
|
||||
|
||||
Each cheat's switch is `cheat_<hash> = 1`, where the hash is FNV-1a 32 of `<mod folder name>/<cheat name>`. A name repeated in one file gets `#2`, `#3` and so on. The build names each code cheat the same way, so renaming a mod's folder resets its cheats' switches to off and makes the next start rebuild its code cheats. Rename the folder before you switch its cheats on.
|
||||
|
||||
## How cheats run
|
||||
|
||||
The game's code is compiled ahead of time, so a cheat cannot write new instructions into it while the game runs. The port sorts each cheat by what it writes (`tools/cheats.py` at setup, `runtime/cheats.c` in game):
|
||||
|
||||
| Kind | What the cheat does | How the port runs it |
|
||||
|---|---|---|
|
||||
| code | Opcode 0 writes into main's code | Built into the game at setup as a patch set (`recomp/patches.py`); switches at once. |
|
||||
| key | Code writes inside a button condition (`8` mask, writes, `21` else, the original words, `20`) | A patch set that is on while the cheat is on and the buttons are held. |
|
||||
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A, 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.
|
||||
+39
-3
@@ -8,7 +8,7 @@ Input comes from the first SDL3 gamepad, the keyboard and the mouse, merged into
|
||||
- `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.
|
||||
|
||||
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. The game's button prompts stay Switch letters either way.
|
||||
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. 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 |
|
||||
|---|---|---|---|
|
||||
@@ -30,12 +30,38 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|
||||
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
|
||||
## Xbox button prompts
|
||||
|
||||
`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.
|
||||
|
||||
- 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.
|
||||
|
||||
| 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`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters are right but the diamond keeps the Switch positions: A shows on its right, while the button labelled A is at the bottom. The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
|
||||
|
||||
The 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
|
||||
|
||||
| Key | Gamepad | Action |
|
||||
|---|---|---|
|
||||
| F1 or Esc | Minus and Plus together | open or close the settings menu; the game gets no input while it is open |
|
||||
| F10 | | performance overlay: off, frame rate, full panel |
|
||||
| F10 | L3 + R3 (both stick clicks) | performance overlay: off, frame rate, full panel |
|
||||
| F11 | | borderless fullscreen on or off, saved to `totk.ini` |
|
||||
|
||||
The Minus+Plus combo never reaches the game. In the menu the arrow keys, the D-pad or the left stick move, Enter or A selects, Q and E or L and R switch tabs, Esc or B goes back (Switch button names, as in the table above).
|
||||
@@ -46,7 +72,7 @@ The *Controls* tab of the settings menu lists, under the button layout, every Sw
|
||||
|
||||
The tab writes `totk.ini` lines, one per Switch button or stick direction: `bind_<button> = <entry>, <entry>, ...`. The button is `A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK`, or a stick direction `LSTICK_LEFT LSTICK_UP LSTICK_RIGHT LSTICK_DOWN` (movement, W A S D by default) and `RSTICK_...` (camera, the arrow keys). An entry is `pad:<name>` or `key:<name>`:
|
||||
|
||||
- pad names: `south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down` (`zl`, `zr` are the triggers)
|
||||
- pad names: `south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down lpaddle1 rpaddle1 lpaddle2 rpaddle2` (`zl`, `zr` are the triggers; the paddles are the back buttons of an Xbox Elite, DualSense Edge or Steam Controller, upper pair 1, lower pair 2, and no Switch button has them by default)
|
||||
- key names: `a` to `z`, `0` to `9`, `f2` to `f7`, `f9`, `f12`, `num0` to `num9`, `space tab enter backspace lshift rshift lctrl rctrl lalt capslock insert delete home end pageup pagedown up down left right`, `num_add num_sub num_mul num_div num_decimal`, `minus equals lbracket rbracket semicolon apostrophe comma period slash backslash grave`, `mouse_l mouse_r mouse_m mouse_x1 mouse_x2`
|
||||
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
|
||||
- `none` leaves the button unbound.
|
||||
@@ -68,8 +94,16 @@ 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_LSTICK_DEADZONE` and `TOTK_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows under *Analog sticks* on the Controls tab) set one stick each and fall back to `TOTK_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.
|
||||
|
||||
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
|
||||
|
||||
## Nintendo Switch 2 controllers
|
||||
|
||||
Windows does not present the Switch 2 Pro Controller as a gamepad SDL can open (the log says `input: no gamepad`), so it needs a bridge program that wakes it and creates a virtual gamepad. The DS4Windows fork by hbashton ([github.com/hbashton/DS4Windows](https://github.com/hbashton/DS4Windows), tested with release VIIPERRC4.5.1, a pre-release) with the output set to an emulated DualSense gives buttons, rumble, gyro and accelerometer, and reads the sticks through the controller's calibration (they rest at 0 to 2%). The game reads its virtual DualSense directly; the log then shows `input: gamepad "DualSense Wireless Controller" (rumble, gyro and accelerometer, ...)` and `input: motion sensors on`. Its release notes also list Joy-Con 2 support, which is untested here.
|
||||
|
||||
Bridges that output a ViGEm DualShock 4 (ProCon2Windows, connectyourpro as tested) give buttons and rumble but no motion: their virtual DualShock 4 reports a gyro and an accelerometer that never change, so motion aiming and Ultrahand rotation do nothing. Run one bridge at a time; Steam Input can also claim the controller, so close Steam if the bridge does not find it.
|
||||
|
||||
## Scripted input
|
||||
|
||||
`TOTK_INPUT_SCRIPT=<file>` replays presses. It is ORed with real input and still applies under `TOTK_INPUT=none`. One event per line, `#` starts a comment:
|
||||
@@ -85,6 +119,8 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
|
||||
|
||||
`TOTK_INPUT_RECORD=<file>` (`input_record` in `totk.ini`, or Input recording in the menu) writes the merged input in the same format, one line per button press and per stick segment: events before the world clock starts by tick, the rest timed; a button or stick held when the world clock starts is cut there and goes on as a timed event. The file replays as a script, which makes it the repro to attach to a bug report.
|
||||
|
||||
`TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry>` attaches an SDL virtual gamepad at start whose sticks rest at these raw SDL axis values (-32768 to 32767, down is +y), so a drifting or held stick can be tested without a controller: the values go through the gamepad mapping and `TOTK_STICK_DEADZONE` like a real pad's, and `TOTK_INPUT_RECORD` shows what reached the game.
|
||||
|
||||
## RenderDoc capture (blocked)
|
||||
|
||||
`TOTK_RDOC=<frame>[,...]` (`runtime/rdoc_win.c`; `TOTK_RDOC_DIR`, `TOTK_RDOC_DLL`) loads RenderDoc before `vkCreateInstance`. RenderDoc hides `VK_EXT_external_memory_host`, which the back end requires for guest pool imports (`runtime/vk_memory.c`), so device creation fails under it. A capture needs a copy-based pool fallback first.
|
||||
+119
-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,48 +24,85 @@ 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 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
|
||||
```
|
||||
|
||||
## 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 another update file 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`,
|
||||
`totk.exe --prepare-shaders`) in the install folder.
|
||||
6. **Clean up** the temp folder (generated C and objects, about 1.3 GB).
|
||||
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 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/`.
|
||||
@@ -75,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
|
||||
@@ -89,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
|
||||
@@ -105,9 +149,59 @@ totk.ini.example, VERSION the default ini; the release string (the repository's
|
||||
- The release contains nothing game-derived; `make_release.py` runs `tools/check_content.py` over the folder before it
|
||||
finishes.
|
||||
|
||||
## Switch system fonts
|
||||
|
||||
Simplified Chinese, Traditional Chinese and Korean text needs the Switch system fonts (`0-standard.bfttf` and the other
|
||||
nn::pl shared fonts). The game does not ship them. Without them it exits before its first frame in those languages
|
||||
(`rt_fatal` in `runtime/ov_system.c`). Supply them from your own console, in either way:
|
||||
|
||||
- Give the setup the firmware folder (`--firmware DIR`, or the window's firmware field); the Fonts step extracts them to
|
||||
`shared-fonts/` in the install folder.
|
||||
- Run `python tools/extract_fonts.py --firmware <dir>` with `.venv-py314` and the keys from `config.local.toml`, or
|
||||
point `TOTK_SHARED_FONTS` at a folder that holds the extracted fonts.
|
||||
|
||||
## Linux release
|
||||
|
||||
`tools/make_release_linux.py` builds the same setup for Linux and packs it as `TotK-Setup-x86_64.AppImage` (the static
|
||||
type-2 runtime: no libfuse2 needed on the host). The player makes it executable and starts it; on a Steam Deck, from
|
||||
Desktop Mode. The command-line setup needs the desktop session's display (`DISPLAY` or `WAYLAND_DISPLAY`) because preparing the
|
||||
shaders opens a window; the check step fails at once without it. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
|
||||
Xft against the sniper sysroot so Russian text uses system TrueType fonts. The setup builds the game into the folder they pick, as on Windows, and the game is `totk` there with
|
||||
`libSDL3.so.0` beside it. The folder inside the AppImage:
|
||||
|
||||
```
|
||||
AppRun starts python/bin/python3 -m installer.gui (no bytecode written into the read-only mount)
|
||||
totk-setup.desktop, totk-setup.png
|
||||
installer/, recomp/, tools/, data/, licenses/, THIRD_PARTY_NOTICES.md, totk.ini.example, VERSION as on Windows
|
||||
python/ python-build-standalone 3.14 (with tkinter) + the installer's packages; a .pth puts the folder,
|
||||
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, 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
|
||||
```
|
||||
|
||||
- What we compile is built against the sniper sysroot (glibc 2.31): the game, hostcheck, hactool and the translator
|
||||
need no newer glibc. The game links Opus, libvpx, zstd and libstdc++ statically and needs only glibc, the system's
|
||||
Vulkan loader and the SDL3 beside it. The setup itself needs glibc 2.34 (Ubuntu 22.04, Debian 12, Fedora 35,
|
||||
SteamOS 3): the official LLVM binaries and the `cryptography` wheel are built against it. `make_release_linux.py`
|
||||
checks every ELF file in the folder against that floor (`check_glibc`), and the setup's check step stops with one
|
||||
message on an older system.
|
||||
- The setup's temp folder is `.setup-temp` inside the install folder (`--temp` changes it): the AppImage is mounted
|
||||
read-only, and a Steam Deck's `/home` is often too full for the generated C while the game goes to the SD card.
|
||||
- The game keeps Mesa's shader cache in `mesa-cache` beside `totk` unless `MESA_SHADER_CACHE_DIR` is set, for the
|
||||
same reason: RADV fills its default folder in `~/.cache` up to 1 GB.
|
||||
- The window follows `LANG` for English or Russian, cancels through SIGTERM to the CLI's process group (the CLI handles
|
||||
it as Ctrl+Break on Windows) and opens the finished folder with `xdg-open`.
|
||||
- Requirements beyond Windows': Linux 6.7 or later (the write watch, `docs/building.md`) and glibc 2.34.
|
||||
- Building the release needs one game version extracted and lifted (`tools/extract.py`, `recomp/gen.py`, either
|
||||
version): the runtime library includes the generated `layout.h`, whose shared part (`MOD_COUNT`, `MOD_MAIN_BASE`)
|
||||
is the same for both. `tools/make_release.py` takes `generated/v121`, else `generated/v143`.
|
||||
|
||||
## Constraints
|
||||
|
||||
- The installer steps contain no Win32 calls: Linux follows with its own compiler and the same steps.
|
||||
- The installer steps contain no Win32 calls: Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The keys file path is passed to the tools; key values never appear in the UI, logs or files.
|
||||
- The window is a front end over the CLI (`python -m installer --base ... --update ... --keys ... --lang ... --dest
|
||||
...`), which prints one JSON progress event per line. Tests and CI use the CLI.
|
||||
@@ -123,4 +217,4 @@ totk.ini.example, VERSION the default ini; the release string (the repository's
|
||||
## Later
|
||||
|
||||
- Re-warm the driver cache on start after a move, GPU or driver change.
|
||||
- NSZ and XCZ input. Linux release.
|
||||
- NSZ and XCZ input.
|
||||
+4
-1
@@ -66,8 +66,9 @@ Source shorthands:
|
||||
| 2.7 | **Swizzles** per texture and per view (`TextureBuilderSetSwizzle`, `TextureViewSetSwizzle`); BotW layout captures rebuild a texture on the same storage with (1, 1, 1, R) or alpha 1. Default per-format mapping R8 → (R, 0, 0, 1). | `VkComponentMapping` on the view. | NXN: font atlas white boxes, HUD dark boxes without it. Ours: mapped (`vk_exec_draw.c` `get_view`). | NXN |
|
||||
| 2.8 | **Sample dimension comes from the instruction**: a non-array sample of an array texture reads layer 0; 1,512 BotW shaders sample a cube as a cube array. | View type must match the declared type. | NXN: 2D array bound as 2D gave one pale colour (washed-out scene); cubes created as 6-layer arrays with cube / cube-array views. Ours: views per resource type (`get_view`). | NXN |
|
||||
| 2.9 | **LOD**: TSC min/max LOD clamp, LOD bias, `TWO_D_NO_MIPMAP` texture type, a min filter without mips stays on the base level; TIC carries its own max anisotropy and aniso quality fields. | `minLod`/`maxLod`, `mipLodBias` limited by `maxSamplerLodBias`. | yuzu: anisotropy heuristics for mods forcing trilinear (YZ-2306, PR 10744). Ours: non-mip filter → `maxLod = minLod + 0.25`, bias clamped (`vk_texture.c` `make_sampler`). | G80TEX |
|
||||
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | yuzu adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). | — |
|
||||
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | yuzu adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). Ours: the translator biases gather coordinates by the host's `subTexelPrecisionBits` on every non-NVIDIA GPU (`vk_caps.c` → job `gather_bias` → Ryujinx `TexturePass.InsertCoordGatherBias`); TotK on RADV drew dashed dark lines along wall and floor edges in the prologue caves without it. `TOTK_GATHER_BIAS=0` turns it off. | — |
|
||||
| 2.11 | **Gather offsets**: `TLD4.PTP` takes four per-texel offsets, possibly from registers. | `ConstOffsets` must be constant. | Ours: `QueryHostSupportsNonConstantTextureOffset() => false` makes Ryujinx emulate register offsets (`tools/xlate/Program.cs`). | RYU-SH `Translation/Optimizations` (TexturePass) |
|
||||
| 2.13 | **TXD gradient register order**: TotK's terrain shaders fill the gradient registers of a `TXD` per coordinate (du/dx, du/dy, dv/dx, dv/dy). | Vulkan's `Grad` takes dPdx (du/dx, dv/dx) and dPdy (du/dy, dv/dy). | Ryujinx's `EmitTxd` takes the first two registers as dPdx and the last two as dPdy, so the mip level came from the wrong derivatives and was too sharp on terrain faces turned across the screen: patches that followed the faces crawled when the camera moved (1.4.3, the Depths; found by the user from their F9 frame, a gradient sample's coordinate and gradients traced back to `DPdx` and `DPdy` of the interpolants). Ours: `gfx_shader.c` `patch_txd_transpose` swaps the two inner components of every two-component gradient pair in the loaded SPIR-V (the shader cache is untouched). `TOTK_TXD_TRANSPOSE=0` turns it off. Only 2D gradients are handled; 3D and cube ones are left as translated. | — |
|
||||
| 2.12 | **Depth compare** (`TSC` depth compare + `TIC_4_DEPTH_TEXTURE`), min/max reduction filtering. | `compareEnable`; reduction mode needs `samplerFilterMinmax` and is not allowed with compare. | Ours: reduction dropped when compare is on (`vk_texture.c` `make_sampler`). | G80TEX |
|
||||
| 2.13 | **ASTC / BC**: native ASTC; zero ASTC block = error colour on hosts. | AMD and NVIDIA desktop GPUs lack ASTC: decode to RGBA8. yuzu offered ASTC recompression to BC1/BC3 for low VRAM (YZ-2305, PR 10398). | Ours: native upload where the device samples ASTC, else a compute-shader decode (`vk_astc.c`, `TOTK_ASTC`), CPU decode (`tex_astc.c`) as the fallback. | `runtime/vk_astc.c` |
|
||||
| 2.14 | **Linear CPU-written textures** (`FLAGS_LINEAR`, small ramps, glyph caches): CPU writes visible to the GPU directly. | Must re-upload or use linear images over host memory. | NXN Mario: sky black without its 64×1 RGBA16F ramps. | NXN |
|
||||
@@ -141,6 +142,8 @@ Source shorthands:
|
||||
| TotK | AO broken above 1008p mods | rescale thresholds | YZ-2306 PR 10675 |
|
||||
| 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 |
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# Mods
|
||||
|
||||
TotK Recomp loads the mods players make for the Switch emulators (Ryujinx, yuzu and its forks): texture and model
|
||||
packs, gameplay and text changes from GameBanana and similar sites, TKMM's merged output, code patches (`.ips`,
|
||||
`.pchtxt`) and Atmosphère cheat files. No mod or cheat file ships with the port.
|
||||
|
||||
## Install a mod
|
||||
|
||||
1. Open the settings menu (F1, Esc, or Minus and Plus together on a controller), go to the Mods tab and choose
|
||||
**Open mods folder**. That is the `mods` folder next to `totk.exe`; the menu creates it when it is missing.
|
||||
2. Make one folder per mod in it and unpack the mod's archive into that folder. The mod's own folders (`romfs`,
|
||||
`exefs`, `cheats`) must sit directly inside the mod's folder:
|
||||
|
||||
```
|
||||
mods\
|
||||
Better Grass\
|
||||
romfs\
|
||||
...
|
||||
Fast Travel Anywhere\
|
||||
exefs\
|
||||
patch.pchtxt
|
||||
My cheats\
|
||||
cheats\
|
||||
277178B7DBA1B6D4.txt
|
||||
```
|
||||
|
||||
An archive laid out for a Switch SD card or an emulator works as it is. Inside the mod's folder the port also finds
|
||||
`atmosphere\contents\0100F2C0115B6000\romfs`, `contents\0100F2C0115B6000\romfs` and `0100F2C0115B6000\romfs`, the
|
||||
same places for `exefs` and `cheats`, and `atmosphere\exefs_patches\<name>\` for code patches.
|
||||
3. Start the game. A mod that patches the game's code makes the game rebuild itself first (see Code patches below).
|
||||
4. Open the Mods tab to check the mod. Each row says how many game files the mod replaces and adds, how many cheats it
|
||||
brings, and why a part of it is not used.
|
||||
|
||||
## Order and switches
|
||||
|
||||
The Mods tab lists every folder in `mods`. Select a mod and press A or Enter to switch it on or off. The top face button
|
||||
or Ctrl+Up moves it up, the left face button or Ctrl+Down moves it down. When two mods change the same file, the
|
||||
higher one wins; the lower mod's row says how many of its files the higher one covers. Changes apply after a restart.
|
||||
|
||||
The menu saves the order and the switches in `totk.ini`, under `[mods]`, by folder name: `mods_order` and
|
||||
`mods_disabled`. A folder the list does not name yet, such as a new or renamed one, goes below the named ones.
|
||||
|
||||
### Mods that change the same files
|
||||
|
||||
Two mods that edit the same game file do not combine: the higher one wins the whole file. Merge such mods with
|
||||
[TKMM](https://github.com/TKMM-Team/Tkmm) (TotK Mod Manager) and set its export location to an empty folder inside
|
||||
`mods`, for example `mods\TKMM`. The exported folder then counts as one mod here. Mods packaged for TKMM only
|
||||
(`.tkcl` files) go the same way.
|
||||
|
||||
The port merges the resource size table on its own: at every start with mods on, it combines the tables the mods ship
|
||||
and raises the entries for files a mod made larger or added. A separate RESTBL mod is not needed.
|
||||
|
||||
## Code patches (ExeFS)
|
||||
|
||||
A mod with `.ips` or `.pchtxt` files patches the game's code. A patch is for one game update: an `.ips` file is named
|
||||
after the build id of the update's code, a `.pchtxt` file names it on its `@nsobid` line. The game's code is compiled
|
||||
ahead of time, so these patches are built into `totk.exe`:
|
||||
|
||||
1. The first start after such a mod is added closes the game and opens the setup's rebuild window. The rebuild takes
|
||||
a few minutes (the shaders stay prepared), then the game starts again. Saves and settings are kept.
|
||||
2. Until the rebuild succeeds, the whole mod stays off, its other files included.
|
||||
3. After the rebuild the mod switches on and off from the Mods tab like any other, without another rebuild.
|
||||
|
||||
The rebuild needs the setup folder the game was installed from. When that folder was moved or deleted, run
|
||||
`TotK-Setup.exe` again on the same install folder: it does the same job. `mods_rebuild = off` under `[mods]` in
|
||||
`totk.ini` turns the automatic rebuild off.
|
||||
|
||||
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
|
||||
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
|
||||
|
||||
## Cheats
|
||||
|
||||
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
|
||||
that change the game's code need the same rebuild as code patches. [cheats.md](cheats.md) explains the file names for
|
||||
each game update, the rebuild and which cheats cannot run.
|
||||
|
||||
## Renaming a mod folder
|
||||
|
||||
The port keys a mod by its folder name. A renamed folder counts as a new mod:
|
||||
|
||||
- it moves below the mods the list names, and comes back switched on;
|
||||
- its cheats come back switched off;
|
||||
- its code patches and code cheats are built again, so the next start runs the rebuild.
|
||||
|
||||
Rename mod folders before you set them up in the menu, not after.
|
||||
|
||||
## What does not work
|
||||
|
||||
| On the Mods tab | Why | What to do |
|
||||
|---|---|---|
|
||||
| This folder has no romfs, exefs or cheats files. | The archive was unpacked one level too deep (`mods\Mod\Mod\romfs`), or it holds no game files. | Move the folders up so `romfs` sits directly in the mod's folder. |
|
||||
| ... files are made for another game version and are left out. | The mod's files carry another update's version in their names (`.Product.<number>.`). | Download the mod's release for your game update. |
|
||||
| Its code patches (exefs) are made for another game version. | The patch files name another update's build id. | Same: get the release for your update. |
|
||||
| This mod brings its own game code (a subsdk plugin or a replaced main). | Mods such as UltraCam ship compiled Switch code. The port cannot load it. | No workaround; the whole mod stays off. |
|
||||
| This mod patches the game code (exefs) in a way the port cannot apply. | The patch writes the game's data, not its code, or changes code in a form the port cannot build. | No workaround; the reason follows on the row. |
|
||||
| The folder name contains \|. | The mod list uses `\|` between names. | Rename the folder. |
|
||||
|
||||
A mod never stops the game from starting. Whatever the port cannot use is left out and shown on the Mods tab.
|
||||
|
||||
## When something looks wrong
|
||||
|
||||
1. Switch the newest mod off on the Mods tab and restart. If the problem goes away, check that the mod is made for your
|
||||
game update and does not fight another mod over the same files.
|
||||
2. `logs\totk.log` next to `totk.exe` names every mod at the start (lines starting with `mods:`), the cheats
|
||||
(`cheats:`) and the resource size table (`RomFS: resource size table:`). Open log folder on the Advanced tab of
|
||||
the menu shows the logs.
|
||||
3. When reporting a problem, attach `totk.log` and list your mods with the links you downloaded them from.
|
||||
+36
-3
@@ -6,9 +6,10 @@ from it may be shared.
|
||||
|
||||
## What you need
|
||||
|
||||
- Windows 10 or later, an x86-64 CPU with AVX2, about 60 GB free.
|
||||
- Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA recommended: they build the default, faster version), about
|
||||
60 GB free.
|
||||
- A GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, with a current driver.
|
||||
- *Tears of the Kingdom* base game (1.0.0) and the 1.2.1 or 1.4.3 update, dumped from your own Switch as NSP or XCI.
|
||||
- *Tears of the Kingdom* base game (1.0.0) and the 1.1.0, 1.2.1 or 1.4.3 update, dumped from your own Switch as NSP or XCI.
|
||||
- `prod.keys` dumped from your own Switch. Never send it to anyone, and never paste it into a log or an issue.
|
||||
|
||||
## Build
|
||||
@@ -38,7 +39,11 @@ build\<ver>\totk.exe
|
||||
`TOTK_VK_VALIDATION=1` turns on the Vulkan validation layer (it comes with the Vulkan SDK). It makes the game slower;
|
||||
do one run with it and one without (`set TOTK_VK_VALIDATION=`).
|
||||
|
||||
Keep the window in the foreground: an unfocused window stalls the game. Shaders the preparation did not cover compile on first use, so
|
||||
If the game stops with a lost device (the log says `device lost` or `VK_ERROR_DEVICE_LOST`), the lines starting with
|
||||
`vk: pass marks` name the render pass or dispatch the GPU was running. AMD GPUs write them by default. On an NVIDIA GPU,
|
||||
`set TOTK_PASS_CRUMBS=1` (or GPU pass marks: On in the Advanced tab) before the run that reproduces the crash.
|
||||
|
||||
The game keeps running behind other windows (`assume_focus = 0` in `totk.ini` pauses it instead). Shaders the preparation did not cover compile on first use, so
|
||||
the first minutes of play can stutter.
|
||||
Play into the open field (load a save, walk around for a minute, fight something if you can), then close the window.
|
||||
|
||||
@@ -51,6 +56,13 @@ A laptop with an iGPU and a discrete GPU, or two cards, lists every GPU at the t
|
||||
|
||||
The line `vk: using gpu N: <name>` says which one ran.
|
||||
|
||||
## Mods
|
||||
|
||||
Mods and cheats made for the emulators go into `mods/` next to `totk.exe`, one folder per mod. [mods.md](mods.md) covers
|
||||
installing them, their order, code patches, what does not work and what to check when something looks wrong;
|
||||
[cheats.md](cheats.md) covers cheat files. The log names every mod at the start (`mods: <name>: ...`); when you
|
||||
report a problem with mods, list them with their download links.
|
||||
|
||||
## What to send back
|
||||
|
||||
1. `logs\totk.log` from each run. It contains no keys and no game files. The lines that matter most start with
|
||||
@@ -63,6 +75,27 @@ The line `vk: using gpu N: <name>` says which one ran.
|
||||
5. For a bug you can trigger again: a recording of your input (`set TOTK_INPUT_RECORD=input.txt`, see
|
||||
[controls.md](controls.md), "Scripted input") and the save you started from.
|
||||
|
||||
## Steam Deck
|
||||
|
||||
Build or install on Linux ([installer.md](installer.md), "Linux release"), in Desktop Mode. The game then runs in either
|
||||
mode; for Game Mode, add `totk` from the install folder to Steam as a non-Steam game.
|
||||
|
||||
The game recognises the Deck (LCD and OLED) and uses its device profile for every setting `totk.ini` does not set:
|
||||
fullscreen, the screen's own 1280x800 (16:10: the camera and interface take that shape), the game's own 3D scale,
|
||||
vsync and a 40 fps limit. These are starting points, not measurements. What helps most:
|
||||
|
||||
1. The performance overlay (F10, or both stick clicks L3 + R3) in full mode: the frame rate, mean and longest frame
|
||||
time over a minute in Lookout Landing, in the Depths and in the Sky, with the settings named.
|
||||
2. The same with `fps_limit` 30, 45 and 60 and `render_scale` 50, 67 and game, so the defaults can be picked from
|
||||
measurements. Steam's own performance overlay (the ... button) adds power draw and temperatures.
|
||||
3. LCD or OLED, SteamOS version (`uname -r` in Desktop Mode's terminal), and the game update.
|
||||
4. Anything that looks stretched, squeezed or cut off, with a screenshot. At 16:10 the menus keep their 16:9
|
||||
proportions in the middle of the taller screen, the HUD sits at the top and bottom edges, and full-screen
|
||||
backgrounds reach them; pre-rendered cutscenes play stretched.
|
||||
|
||||
`device_profile = desktop` in `totk.ini` turns the profile off; `deck` turns it on elsewhere, to try the Deck's
|
||||
settings on a PC.
|
||||
|
||||
## Switches for the developer's questions
|
||||
|
||||
Only set these when asked; each forces a fallback the build picks by itself on GPUs that need it.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+20
-10
@@ -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
|
||||
@@ -28,6 +30,11 @@ class Parser(argparse.ArgumentParser):
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# The temp folder inside the install folder when the layout names none (the Linux release); the check step does not
|
||||
# count it when it requires an empty install folder
|
||||
TEMP_IN_DEST = '.setup-temp'
|
||||
|
||||
|
||||
def temp_folder(base, dest):
|
||||
"""One temp folder per install folder, so two installs never share generated files."""
|
||||
tag = hashlib.sha256(str(dest).lower().encode()).hexdigest()[:8]
|
||||
@@ -41,13 +48,15 @@ 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='1.2.1 or 1.4.3 update NSP')
|
||||
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/)')
|
||||
ap.add_argument('--temp', type=Path, help='temp folder (default: temp/ of the release, build/install-temp in a '
|
||||
'repository)')
|
||||
ap.add_argument('--temp', type=Path, help='temp folder (default: temp/ of the release on Windows, .setup-temp in '
|
||||
'the install folder on Linux, build/install-temp in a repository)')
|
||||
ap.add_argument('--verify', action='store_true', help='hash every kept file, also those this release made')
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
@@ -58,7 +67,8 @@ def main(argv=None):
|
||||
signal.signal(getattr(signal, name), cancel)
|
||||
layout = layouts.find(args.root)
|
||||
dest = args.dest.resolve()
|
||||
temp = temp_folder((args.temp or layout.temp).resolve(), dest)
|
||||
base = args.temp or layout.temp
|
||||
temp = temp_folder(base.resolve(), dest) if base else dest / TEMP_IN_DEST
|
||||
log = temp / 'logs' / time.strftime('install-%Y%m%d-%H%M%S.log')
|
||||
rep = Reporter(log_path=log)
|
||||
t0 = time.monotonic()
|
||||
|
||||
+5
-1
@@ -15,6 +15,7 @@ Text is never the payload: ids name a step ("step.<id>"), a phase ("phase.<step>
|
||||
and installer/text/en.json holds their English text with {param} placeholders. The log renders events with it.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
@@ -28,11 +29,14 @@ def load_catalog(path=CATALOG):
|
||||
return json.loads(path.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
GAME_EXE = 'totk.exe' if os.name == 'nt' else 'totk' # {game_exe} in the catalogs
|
||||
|
||||
|
||||
def render(catalog, id, params=None):
|
||||
"""The English text of id, or the id itself when the catalog lacks it."""
|
||||
text = catalog.get(id, id)
|
||||
try:
|
||||
return text.format(**(params or {}))
|
||||
return text.format(**{'game_exe': GAME_EXE, **(params or {})})
|
||||
except (KeyError, IndexError, ValueError):
|
||||
return f'{text} {params}'
|
||||
|
||||
|
||||
+83
-27
@@ -1,15 +1,19 @@
|
||||
"""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
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
@@ -22,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'}
|
||||
@@ -41,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
|
||||
@@ -50,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
|
||||
@@ -58,11 +63,17 @@ class App:
|
||||
|
||||
root.title(self.cat('ui.title'))
|
||||
icon = layout.runtime / 'totk.ico'
|
||||
png = layout.runtime / 'totk.png' # Tk loads no .ico off Windows; the Linux release converts it
|
||||
if os.name == 'nt' and icon.is_file():
|
||||
root.iconbitmap(default=str(icon))
|
||||
elif png.is_file():
|
||||
self.icon = tk.PhotoImage(file=str(png))
|
||||
root.iconphoto(True, self.icon)
|
||||
root.protocol('WM_DELETE_WINDOW', self.on_close)
|
||||
self.ui_font = 'Segoe UI' if os.name == 'nt' else 'DejaVu Sans'
|
||||
self.mono_font = 'Consolas' if os.name == 'nt' else 'DejaVu Sans Mono'
|
||||
style = ttk.Style(root)
|
||||
style.configure('Title.TLabel', font=('Segoe UI', 15, 'bold'))
|
||||
style.configure('Title.TLabel', font=(self.ui_font, 15, 'bold'))
|
||||
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=8)
|
||||
style.configure('Err.TLabel', foreground='#c01c28')
|
||||
style.configure('Version.TLabel', foreground='#808080')
|
||||
@@ -76,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 -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -113,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))
|
||||
@@ -190,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)
|
||||
@@ -199,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')
|
||||
@@ -214,7 +241,7 @@ class App:
|
||||
self.cancel_button.pack(side='right')
|
||||
self.log_toggle = self.button(bar, 'ui.show_log', self.toggle_log)
|
||||
self.log_toggle.pack(side='left')
|
||||
self.run_header = ttk.Label(f, font=('Segoe UI', 11, 'bold'))
|
||||
self.run_header = ttk.Label(f, font=(self.ui_font, 11, 'bold'))
|
||||
self.run_header.pack(anchor='w')
|
||||
self.step_rows = {}
|
||||
self.steps_box = ttk.Frame(f)
|
||||
@@ -228,7 +255,7 @@ class App:
|
||||
self.warn_label = ttk.Label(f, wraplength=620, justify='left', foreground='#9a6700')
|
||||
self.warn_label.pack(anchor='w')
|
||||
self.log_frame = ttk.Frame(f)
|
||||
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=('Consolas', 9))
|
||||
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=(self.mono_font, 9))
|
||||
scroll = ttk.Scrollbar(self.log_frame, command=self.log.yview)
|
||||
self.log.configure(yscrollcommand=scroll.set)
|
||||
scroll.pack(side='right', fill='y')
|
||||
@@ -245,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')
|
||||
@@ -259,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()
|
||||
@@ -283,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:
|
||||
@@ -298,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):
|
||||
@@ -345,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')
|
||||
@@ -373,7 +413,7 @@ class App:
|
||||
glyph.configure(text=GLYPH[state], foreground=COLOR[state])
|
||||
name.configure(text=self.cat('step.' + s) + (f' ({self.cat("ui.state_" + state)})'
|
||||
if state in (SKIPPED, FAILED) else ''),
|
||||
font=('Segoe UI', 10, 'bold') if state == RUNNING else ('Segoe UI', 10))
|
||||
font=(self.ui_font, 10, 'bold') if state == RUNNING else (self.ui_font, 10))
|
||||
mode = m.mode or 'install'
|
||||
self.run_header.configure(text=self.cat('ui.running_' + mode if mode in ('install', 'resume', 'update')
|
||||
else 'ui.running_install'))
|
||||
@@ -475,19 +515,32 @@ 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():
|
||||
os.startfile(dest)
|
||||
if os.name == 'nt':
|
||||
os.startfile(dest)
|
||||
else:
|
||||
subprocess.Popen(['xdg-open', dest], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
|
||||
|
||||
# ---- 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',
|
||||
@@ -500,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'},
|
||||
@@ -523,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')
|
||||
@@ -601,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
|
||||
|
||||
|
||||
+51
-10
@@ -11,6 +11,7 @@ import ctypes
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@@ -23,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):
|
||||
@@ -75,15 +82,35 @@ def flat(value):
|
||||
|
||||
|
||||
def windows_ui_language():
|
||||
"""'ru' when the Windows display language 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 '').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'
|
||||
|
||||
|
||||
@@ -107,6 +134,7 @@ class Form:
|
||||
base: str = ''
|
||||
update: str = ''
|
||||
keys: str = ''
|
||||
firmware: str = ''
|
||||
lang: str = ''
|
||||
dest: str = ''
|
||||
|
||||
@@ -143,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
|
||||
|
||||
@@ -316,7 +346,14 @@ else:
|
||||
|
||||
def send_break(proc, python=None):
|
||||
"""Ctrl+Break to the process group of proc (started with CREATE_NEW_PROCESS_GROUP). The window itself may have no
|
||||
console (pythonw), so a short helper process attaches to the child's console and sends it."""
|
||||
console (pythonw), so a short helper process attaches to the child's console and sends it. Elsewhere SIGTERM to
|
||||
proc's session (Process starts it in one of its own), which installer/cli.py handles as Ctrl+Break."""
|
||||
if os.name != 'nt':
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGTERM)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
helper = [str(python or sys.executable), '-c', _BREAK, str(proc.pid)]
|
||||
flags = CREATE_NO_WINDOW if os.name == 'nt' else 0
|
||||
r = subprocess.run(helper, creationflags=flags, capture_output=True)
|
||||
@@ -332,10 +369,14 @@ class Process:
|
||||
self.command = command
|
||||
self.helper_python = helper_python
|
||||
self.lines = queue.Queue()
|
||||
flags = (CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) if creationflags is None else creationflags
|
||||
env = dict(os.environ, PYTHONIOENCODING='utf-8', PYTHONUTF8='1', PYTHONUNBUFFERED='1')
|
||||
if os.name == 'nt':
|
||||
flags = (CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) if creationflags is None else creationflags
|
||||
group = {'creationflags': flags}
|
||||
else:
|
||||
group = {'start_new_session': True}
|
||||
self.proc = subprocess.Popen(command, cwd=cwd, env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, creationflags=flags)
|
||||
stderr=subprocess.STDOUT, **group)
|
||||
self.cancelled_at = None
|
||||
self.reader = threading.Thread(target=self.read, daemon=True)
|
||||
self.reader.start()
|
||||
|
||||
+36
-3
@@ -1,6 +1,7 @@
|
||||
// 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>
|
||||
#include <string.h>
|
||||
#include <vulkan/vulkan.h>
|
||||
@@ -24,9 +25,41 @@ 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;
|
||||
if ((c & (fma | movbe | osxsave | avx | f16c)) != (fma | movbe | osxsave | avx | f16c)) return 0;
|
||||
unsigned lo, hi;
|
||||
__asm__("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
|
||||
if ((lo & 6) != 6) return 0; // XMM and YMM state enabled by the OS
|
||||
if (!__get_cpuid_count(7, 0, &a, &b, &c, &d)) return 0;
|
||||
const unsigned bmi1 = 1u << 3, avx2 = 1u << 5, bmi2 = 1u << 8;
|
||||
if ((b & (bmi1 | avx2 | bmi2)) != (bmi1 | avx2 | bmi2)) return 0;
|
||||
if (!__get_cpuid(0x80000001, &a, &b, &c, &d)) return 0;
|
||||
return (c & (1u << 5)) != 0; // LZCNT (ABM)
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
__builtin_cpu_init();
|
||||
printf("{\"x86_64_v3\": %s, ", __builtin_cpu_supports("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)
|
||||
|
||||
+73
-16
@@ -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
|
||||
@@ -15,21 +16,28 @@ Release (docs/installer.md "Release layout"), root = the unpacked folder:
|
||||
VERSION the release version, one line
|
||||
temp/ the default temp folder (created)
|
||||
|
||||
Linux release (the AppImage's folder, docs/installer.md "Linux release"): python/bin/python3 (python-build-standalone,
|
||||
with tkinter), llvm/ with clang, lld and llvm-ar plus llvm/sysroot (the glibc headers and libraries the game links
|
||||
against), runtime/deps/ with include/ and the static Opus, libvpx and zstd, bin/ without .exe and with libSDL3.so.0.
|
||||
|
||||
Dev mode, root = a repository checkout (no python/python.exe): build/py314-embed/py/python.exe (the embeddable
|
||||
Python the release ships, with the same packages), else this Python; toolchain/llvm-mingw-*/ and toolchain/deps-mingw;
|
||||
runtime/ as the sources with build/install-rtlib/libtotkrt.a (tools/build_game.py --make-runtime-lib --out
|
||||
build/install-rtlib); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/totk_xlate.exe; the shader list from
|
||||
refs/MacNativeGames; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626.
|
||||
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).
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
EXE = '.exe' if os.name == 'nt' else ''
|
||||
WIN = os.name == 'nt'
|
||||
EXE = '.exe' if WIN else ''
|
||||
UCRT = Path(r'C:\msys64\ucrt64\bin')
|
||||
SUPPORTED = ('1.2.1', '1.4.3') # 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):
|
||||
@@ -45,8 +53,8 @@ class Layout:
|
||||
python: Path # runs the tools
|
||||
tools: Path
|
||||
recomp: Path
|
||||
llvm: Path # llvm-mingw tree
|
||||
deps: Path # include/ and lib/ for the link
|
||||
llvm: Path # llvm-mingw tree; on Linux the tree with bin/clang (a release's, or the system's /usr)
|
||||
deps: Path | None # include/ and lib/ for the link; None: the system's (Linux dev mode)
|
||||
runtime: Path # runtime headers and install-side sources
|
||||
runtime_lib: Path # libtotkrt.a
|
||||
ninja: Path
|
||||
@@ -56,11 +64,21 @@ class Layout:
|
||||
shader_list: Path | None = None # None: dev, read from refs/MacNativeGames through git
|
||||
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'
|
||||
@@ -69,19 +87,31 @@ 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 / 'make_romfs_patches.py', self.recomp / 'gen.py', self.clang,
|
||||
self.deps / 'include', self.deps / 'lib', self.runtime, self.runtime_lib, self.ninja, self.hactool,
|
||||
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, *([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)
|
||||
if self.sysroot:
|
||||
need.append(self.sysroot)
|
||||
return [str(p) for p in need if not p.exists()]
|
||||
|
||||
|
||||
def release_python(root):
|
||||
return root / 'python' / ('python.exe' if WIN else 'bin/python3')
|
||||
|
||||
|
||||
def is_release(root):
|
||||
return (root / 'python' / ('python' + EXE)).is_file()
|
||||
return release_python(root).is_file()
|
||||
|
||||
|
||||
def release_layout(root):
|
||||
@@ -89,17 +119,24 @@ def release_layout(root):
|
||||
return Layout(
|
||||
root=root, dev=False,
|
||||
release=version.read_text(encoding='utf-8').strip() if version.is_file() else 'unknown',
|
||||
python=root / 'python' / ('python' + EXE),
|
||||
python=release_python(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'),
|
||||
shader_list=root / 'data' / 'shader_list.tsv', ini_example=root / 'totk.ini.example', temp=root / 'temp')
|
||||
dlls=(root / 'bin' / 'SDL3.dll', root / 'bin' / 'libopus-0.dll') if WIN else (root / 'bin' / 'libSDL3.so.0',),
|
||||
shader_list=root / 'data' / 'shader_list.tsv', ini_example=root / 'totk.ini.example',
|
||||
# Linux: None, the install folder's .setup-temp (installer/cli.py): the AppImage is mounted read-only, and on
|
||||
# a Steam Deck /home is often too full for the generated C while the install folder is on the SD card
|
||||
temp=root / 'temp' if WIN else None,
|
||||
sysroot=None if WIN else root / 'llvm' / 'sysroot')
|
||||
|
||||
|
||||
def dev_layout(root):
|
||||
if not WIN:
|
||||
return dev_layout_posix(root)
|
||||
commit = subprocess.run(['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'], capture_output=True,
|
||||
text=True).stdout.strip()
|
||||
embed = root / 'build' / 'py314-embed' / 'py' / ('python' + EXE)
|
||||
@@ -112,12 +149,32 @@ def dev_layout(root):
|
||||
tools=root / 'tools', recomp=root / 'recomp',
|
||||
llvm=trees[-1] if trees else root / 'toolchain' / 'llvm-mingw-missing', deps=deps,
|
||||
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libtotkrt.a',
|
||||
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libtotkrt-v2.a',
|
||||
ninja=UCRT / ('ninja' + EXE), hactool=root / 'toolchain' / 'hactool' / ('hactool' + EXE),
|
||||
xlate=root / 'build' / 'xlate' / ('totk_xlate' + EXE),
|
||||
dlls=tuple(sorted((deps / 'bin').glob('*.dll'))),
|
||||
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp')
|
||||
|
||||
|
||||
def dev_commit(root):
|
||||
return subprocess.run(['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'], capture_output=True,
|
||||
text=True).stdout.strip()
|
||||
|
||||
|
||||
def dev_layout_posix(root):
|
||||
clang, ninja = shutil.which('clang'), shutil.which('ninja')
|
||||
semver = (root / 'VERSION').read_text(encoding='utf-8').strip()
|
||||
return Layout(
|
||||
root=root, dev=True, release=f'{semver}-dev+{dev_commit(root) or "unknown"}',
|
||||
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')
|
||||
|
||||
|
||||
def find(root=None):
|
||||
"""The layout of root (default: the folder above this package)."""
|
||||
root = Path(root or Path(__file__).resolve().parent.parent).resolve()
|
||||
|
||||
+468
-43
@@ -1,20 +1,29 @@
|
||||
"""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/, 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
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@@ -94,10 +103,36 @@ def kill_tree(proc):
|
||||
return
|
||||
if os.name == 'nt':
|
||||
subprocess.run(['taskkill', '/T', '/F', '/PID', str(proc.pid)], capture_output=True)
|
||||
else:
|
||||
return
|
||||
try: # run_tool starts each tool in a session of its own: the group holds ninja, clang and the game
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
proc.kill()
|
||||
|
||||
|
||||
def linux_glibc():
|
||||
"""(major, minor) of the C library this Linux process runs on, None elsewhere or when it is not glibc."""
|
||||
try:
|
||||
name = os.confstr('CS_GNU_LIBC_VERSION') if sys.platform.startswith('linux') else None
|
||||
except (ValueError, OSError):
|
||||
return None
|
||||
m = re.match(r'glibc (\d+)\.(\d+)', name or '')
|
||||
return (int(m.group(1)), int(m.group(2))) if m else None
|
||||
|
||||
|
||||
# The bundled clang, lld and the cryptography wheel are built against glibc 2.34 (tools/make_release_linux.py
|
||||
# GLIBC_FLOOR); on an older one they cannot start
|
||||
GLIBC_NEEDED = (2, 34)
|
||||
|
||||
|
||||
def linux_kernel():
|
||||
"""(major, minor) of the running Linux kernel, None elsewhere."""
|
||||
if not sys.platform.startswith('linux'):
|
||||
return None
|
||||
m = re.match(r'(\d+)\.(\d+)', os.uname().release)
|
||||
return (int(m.group(1)), int(m.group(2))) if m else None
|
||||
|
||||
|
||||
def run_tool(ctx, step, script, args, parse=None, cwd=None):
|
||||
"""Run a tool script with the bundled Python. Its output goes to <temp>/logs/<step>-<script>.log and, line by line,
|
||||
to parse(line). Returns (exit code, log path)."""
|
||||
@@ -109,7 +144,8 @@ def run_tool(ctx, step, script, args, parse=None, cwd=None):
|
||||
env = dict(os.environ, PYTHONUNBUFFERED='1', PYTHONIOENCODING='utf-8')
|
||||
with open(log, 'w', encoding='utf-8', newline='\n') as out:
|
||||
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd or ctx.layout.root,
|
||||
env=env, text=True, encoding='utf-8', errors='replace', bufsize=1)
|
||||
env=env, text=True, encoding='utf-8', errors='replace', bufsize=1,
|
||||
start_new_session=os.name != 'nt')
|
||||
try:
|
||||
for line in proc.stdout:
|
||||
out.write(line)
|
||||
@@ -139,9 +175,9 @@ def raise_if_damaged(error, name, path):
|
||||
|
||||
|
||||
def game_version(ctx):
|
||||
"""The update this install is built from: what the check identified, else the recorded one (an install made before
|
||||
1.4.3 support records none: 1.2.1)."""
|
||||
return ctx.state.get('version', SUPPORTED[0])
|
||||
"""The update this install is built from: what the check identified, else the recorded one. An install made before
|
||||
1.4.3 support records no version, and that install is 1.2.1: name it here rather than leaning on SUPPORTED's order."""
|
||||
return ctx.state.get('version', '1.2.1')
|
||||
|
||||
|
||||
def expected_update(ctx):
|
||||
@@ -188,28 +224,136 @@ 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():
|
||||
raise InstallError('check.dest_not_folder', path=str(dest))
|
||||
if ctx.mode == 'install' and dest.is_dir() and any(dest.iterdir()):
|
||||
own = (ctx.temp, *ctx.temp.parents) # a temp folder inside the install folder (installer/cli.py) is ours
|
||||
if ctx.mode == 'install' and dest.is_dir() and any(p not in own for p in dest.iterdir()):
|
||||
raise InstallError('check.dest_not_empty', path=str(dest))
|
||||
lang = ini.canonical_language(ctx.options.lang or ctx.state.get('lang'))
|
||||
lang = requested_lang(ctx)
|
||||
if not lang:
|
||||
raise InstallError('check.lang_missing')
|
||||
if lang not in ini.LANGUAGES:
|
||||
raise InstallError('check.lang_unknown', lang=lang, known=', '.join(ini.LANGUAGES))
|
||||
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
|
||||
if firmware is not None:
|
||||
if not firmware.is_dir():
|
||||
raise InstallError('check.firmware_missing', path=str(firmware))
|
||||
if not firmware_has_ncas(firmware):
|
||||
raise InstallError('check.firmware_empty', path=str(firmware))
|
||||
if lang in ini.NEED_SHARED_FONTS:
|
||||
rep.warning('warn.lang_needs_fonts', lang=lang)
|
||||
if firmware is None:
|
||||
rep.warning('warn.lang_needs_fonts', lang=lang)
|
||||
elif not shared_fonts_ready(dest):
|
||||
keys = getattr(ctx.options, 'keys', None)
|
||||
if not keys or not Path(keys).is_file():
|
||||
raise InstallError('check.fonts_keys_needed')
|
||||
try:
|
||||
key_values = load_keys(Path(keys))
|
||||
except (ValueError, OSError, UnicodeDecodeError):
|
||||
raise InstallError('check.keys_unreadable') from None
|
||||
if 'header_key' not in key_values:
|
||||
raise InstallError('check.keys_no_header_key')
|
||||
if (sys.platform == 'linux' and ctx.will_run('prepare')
|
||||
and not (os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY'))):
|
||||
# totk --prepare-shaders opens a window and needs a present-capable queue family; without a display it
|
||||
# fails after lift and compile (~15 min), so stop here
|
||||
raise InstallError('check.no_display')
|
||||
ctx.lang = lang
|
||||
ctx.temp.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -228,10 +372,20 @@ 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:
|
||||
raise InstallError('check.glibc_old', version='.'.join(map(str, glibc)),
|
||||
need='.'.join(map(str, GLIBC_NEEDED)))
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
exe = work / ('hostcheck' + EXE)
|
||||
command = [str(layout.clang), '-O1', str(HOSTCHECK), '-I', str(layout.deps / 'include'), '-o', str(exe)]
|
||||
command = [str(layout.clang), '-O1', str(HOSTCHECK), '-o', str(exe)]
|
||||
if layout.deps:
|
||||
command += ['-I', str(layout.deps / 'include')]
|
||||
if layout.sysroot:
|
||||
command += [f'--sysroot={layout.sysroot}', '-fuse-ld=lld']
|
||||
if os.name != 'nt':
|
||||
command.append('-ldl')
|
||||
r = subprocess.run(command, capture_output=True, text=True, errors='replace')
|
||||
@@ -244,8 +398,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']
|
||||
@@ -254,6 +410,11 @@ class Check(Step):
|
||||
if not any(tuple(d['api'][:2]) >= (1, 3) for d in devices):
|
||||
raise InstallError('check.vulkan_too_old',
|
||||
devices=', '.join(f'{d["name"]} ({d["api"][0]}.{d["api"][1]})' for d in devices))
|
||||
kernel = linux_kernel()
|
||||
if kernel and kernel < (6, 7):
|
||||
# Without userfaultfd's asynchronous write protection nothing tracks the CPU's writes: the game copies whole
|
||||
# pools to the GPU at each submit and its texture checks see every page as changed (docs/building.md, Linux)
|
||||
ctx.reporter.warning('warn.kernel_old', version='.'.join(map(str, kernel)))
|
||||
ctx.host = host
|
||||
|
||||
def inputs(self, ctx):
|
||||
@@ -307,6 +468,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)
|
||||
@@ -325,6 +490,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'):
|
||||
@@ -376,7 +543,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'
|
||||
@@ -484,18 +716,69 @@ 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)))
|
||||
# fingerprint takes a file's name and content only; the paths under mods/ go in too, because tools/cheats.py
|
||||
# and tools/exefs_patches.py name each patch set after its mod folder, so a renamed folder needs a lift
|
||||
files = cheat_files(ctx) + exefs_files(ctx)
|
||||
return fingerprint('lift-4', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
|
||||
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *files,
|
||||
*(f.relative_to(ctx.dest / 'mods').as_posix() for f in files))
|
||||
|
||||
def current(self, ctx, record):
|
||||
gen = ctx.temp / 'gen'
|
||||
@@ -503,8 +786,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):
|
||||
@@ -512,28 +797,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', 'check_isa.py')]
|
||||
return fingerprint('runtime-1', cpu, layout.runtime_lib_for(cpu), layout.runtime, layout.deps or 'system',
|
||||
layout.sysroot or 'host', *tools, clang, *[d.name for d in layout.dlls])
|
||||
|
||||
|
||||
class Compile(Step):
|
||||
id = 'compile'
|
||||
|
||||
def key(self, ctx):
|
||||
layout = ctx.layout
|
||||
clang = subprocess.run([str(layout.clang), '--version'], capture_output=True, text=True).stdout
|
||||
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py')]
|
||||
return fingerprint('compile-1', ctx.keys['lift'], layout.runtime, layout.runtime_lib, layout.deps, *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)
|
||||
@@ -557,9 +876,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, '--deps', layout.deps,
|
||||
'--runtime', layout.runtime, '--runtime-lib', layout.runtime_lib, '--ninja', layout.ninja,
|
||||
'--gen', ctx.temp / 'gen', '--out', build, '--toolchain', layout.llvm, '--cpu', cpu,
|
||||
*(['--deps', layout.deps] if layout.deps else []), *(['--sysroot', layout.sysroot] if layout.sysroot else []),
|
||||
'--runtime', layout.runtime, *(['--runtime-lib', lib] if lib else ()), '--ninja', layout.ninja,
|
||||
'--version', version_tag(game_version(ctx)), '--expected', layout.expected(game_version(ctx)),
|
||||
'--exefs', dest / 'exefs', '--romfs', dest / 'romfs',
|
||||
'--profile', dest / 'profile'], parse)
|
||||
@@ -573,12 +895,21 @@ class Compile(Step):
|
||||
replace_file(build / 'romfs-index.bin', dest / 'romfs-index.bin')
|
||||
for dll in layout.dlls:
|
||||
replace_file(dll, dest / dll.name)
|
||||
return {'exe_sha256': file_sha256(dest / exe.name)}
|
||||
return {'exe_sha256': file_sha256(dest / exe.name), 'cpu': cpu}
|
||||
|
||||
|
||||
# ---- 5. prepare
|
||||
# ---- 6. prepare
|
||||
|
||||
TRANSLATED = re.compile(r'gfx_shader: prepare: (\d+) of (\d+) pairs')
|
||||
# runtime/vk_pipeline.c lb_warm_prepared: variants the driver crashed on in an earlier run (tools/prepare_shaders.py
|
||||
# runs the driver step again after such a crash)
|
||||
SKIPPED = re.compile(r'prepare-shaders: (\d+) variants skipped: the graphics driver crashed')
|
||||
|
||||
|
||||
# The game resolutions of another shape than 16:9 that runtime/ui/menu.cpp row_output_res lists: 21:9 and 32:9 monitors,
|
||||
# and 16:10 screens (the Steam Deck's 1280x800, handhelds and laptops)
|
||||
ASPECT_SIZES = ('2560x1080', '3440x1440', '3840x1600', '5120x2160', '3840x1080', '5120x1440',
|
||||
'1280x800', '1680x1050', '1920x1200', '2560x1600')
|
||||
|
||||
|
||||
class Prepare(Step):
|
||||
@@ -587,8 +918,11 @@ class Prepare(Step):
|
||||
def key(self, ctx):
|
||||
layout = ctx.layout
|
||||
xlate_id = layout.xlate.with_suffix('.id')
|
||||
tools = [layout.tools / n for n in ('prepare_shaders.py', 'shader_census.py', 'make_romfs_patches.py')]
|
||||
return fingerprint('prepare-1', ctx.keys['compile'], layout.xlate, xlate_id, shader_list_text(layout), *tools)
|
||||
tools = [layout.tools / n for n in ('prepare_shaders.py', 'shader_census.py', 'make_romfs_patches.py',
|
||||
'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
|
||||
@@ -603,10 +937,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'))
|
||||
|
||||
@@ -617,16 +959,47 @@ class Prepare(Step):
|
||||
dest / 'romfs-patches'])
|
||||
if code:
|
||||
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
|
||||
# The same overlay with the UI laid out for each game resolution of another shape the settings menu offers
|
||||
# (runtime/romfs.c overlay_init picks the one that matches; the camera's side is the patch set aspect,
|
||||
# recomp/patches-v<ver>.txt).
|
||||
for size in ASPECT_SIZES:
|
||||
remove(dest / f'romfs-patches-{size}')
|
||||
code, log = run_tool(ctx, 'prepare', layout.tools / 'make_romfs_patches.py',
|
||||
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
|
||||
dest / f'romfs-patches-{size}', '--aspect', size])
|
||||
if code:
|
||||
raise InstallError('prepare.romfs_patches_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:
|
||||
@@ -638,10 +1011,18 @@ 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)
|
||||
code, log = run_tool(ctx, 'prepare', layout.tools / 'pack_shader_cache.py', [dest / 'shader-cache'])
|
||||
if code:
|
||||
raise InstallError('prepare.pack_failed', code=code, log=str(log))
|
||||
return {'xlate_sha256': file_sha256(xlate)}
|
||||
|
||||
|
||||
# ---- 6. clean up
|
||||
# ---- 7. clean up
|
||||
|
||||
class Cleanup(Step):
|
||||
id = 'cleanup'
|
||||
@@ -659,10 +1040,54 @@ 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
|
||||
deleting, #107) reported success and the game stopped at its first file read."""
|
||||
try:
|
||||
ok = extract.romfs_verified(romfs, expected) and (romfs / 'romfs.bin').stat().st_size == expected['romfs']['size']
|
||||
if ok:
|
||||
with open(romfs / 'romfs.bin', 'rb') as f:
|
||||
f.read(1)
|
||||
except (OSError, ValueError):
|
||||
ok = False
|
||||
if not ok:
|
||||
raise InstallError('cleanup.romfs_unreadable', path=str(romfs))
|
||||
|
||||
|
||||
def all_steps():
|
||||
return [Check(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
+39
-11
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"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 totk.exe",
|
||||
"step.compile": "Compiling {game_exe}",
|
||||
"step.prepare": "Preparing the shaders",
|
||||
"step.cleanup": "Cleaning up",
|
||||
|
||||
@@ -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",
|
||||
@@ -21,11 +24,12 @@
|
||||
"phase.lift.discover": "Finding the functions",
|
||||
"phase.lift.emit": "Writing C code",
|
||||
"phase.compile.compile": "Compiling",
|
||||
"phase.compile.link": "Linking totk.exe",
|
||||
"phase.compile.link": "Linking {game_exe}",
|
||||
"phase.prepare.romfs_patches": "Writing the 60 fps game data patches",
|
||||
"phase.prepare.seed": "Reading the shaders from the game data",
|
||||
"phase.prepare.translate": "Translating the shaders",
|
||||
"phase.prepare.libraries": "Building the shaders for the graphics driver",
|
||||
"phase.prepare.pack": "Packing the shader cache into one file",
|
||||
"phase.cleanup.ini": "Writing totk.ini",
|
||||
"phase.cleanup.temp": "Removing temporary files",
|
||||
|
||||
@@ -41,12 +45,14 @@
|
||||
"check.lang_missing": "Choose the game language.",
|
||||
"check.lang_unknown": "Unknown language '{lang}'. Known: {known}.",
|
||||
"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.2.1 or 1.4.3 update and prod.keys.",
|
||||
"check.no_display": "Preparing the shaders needs a display. Run the setup from the desktop session, or export DISPLAY=:0 (on a Steam Deck in Game Mode also XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.0 or 1.2.1 or 1.4.3 update and prod.keys.",
|
||||
"check.input_missing": "File not found ({input}): {path}",
|
||||
"check.keys_unreadable": "The keys file cannot be read. Choose a prod.keys file.",
|
||||
"check.keys_no_header_key": "The keys file has no header_key. Dump prod.keys from your console again.",
|
||||
@@ -57,44 +63,65 @@
|
||||
"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.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 totk.exe 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}",
|
||||
"cleanup.romfs_unreadable": "The game data in {path} is missing or cannot be read after the install. Restart Windows, then run the installer again on the same folder: it extracts the game data again and keeps the rest.",
|
||||
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
|
||||
"cleanup.ini_failed": "totk.ini cannot be written: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, which the installer does not extract; some text may not show.",
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, and none were given. The game will not start in this language until they are added. Choose the firmware folder (Contents/registered), or see \"Switch system fonts\" in docs/installer.md.",
|
||||
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
|
||||
"warn.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.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.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)",
|
||||
@@ -110,12 +137,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.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",
|
||||
@@ -132,7 +160,7 @@
|
||||
"ui.result_ok": "The game is ready",
|
||||
"ui.result_failed": "The setup failed",
|
||||
"ui.result_cancelled": "The setup was cancelled",
|
||||
"ui.done_text": "Start totk.exe from {dest}. The setup program and the game files you chose are no longer needed.",
|
||||
"ui.done_text": "Start {game_exe} from {dest}. The setup program and the game files you chose are no longer needed.",
|
||||
"ui.cancelled_text": "The finished steps are kept. Start the setup again with the same folder to continue.",
|
||||
"ui.log_file": "Log file: {path}",
|
||||
"ui.process_died": "The setup stopped unexpectedly (exit code {code}).",
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"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.no_display": "Preparar os shaders exige uma tela. Execute a instalação na sessão da área de trabalho ou use export DISPLAY=:0 (no Steam Deck em Game Mode, também XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "Os arquivos do jogo são necessários: escolha o jogo base, a atualização 1.1.0, 1.2.1 ou 1.4.3 e o prod.keys.",
|
||||
"check.input_missing": "Arquivo não encontrado ({input}): {path}",
|
||||
"check.keys_unreadable": "Não foi possível ler o arquivo de chaves. Escolha um arquivo prod.keys.",
|
||||
"check.keys_no_header_key": "O arquivo de chaves não tem header_key. Extraia o prod.keys do seu console de novo.",
|
||||
"check.keys_wrong": "As chaves não abrem o arquivo de {input}. Use o prod.keys do seu console.",
|
||||
"check.keys_too_old": "O arquivo de chaves é antigo demais para o arquivo de {input} (precisa da geração de chaves {generation}). Extraia o prod.keys de um console com firmware mais novo.",
|
||||
"check.not_game_file": "O arquivo de {input} não é um arquivo de jogo XCI 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, e nenhuma foi informada. O jogo não inicia neste idioma até que sejam adicionadas. Escolha a pasta do firmware (Contents/registered) ou veja \"Switch system fonts\" em docs/installer.md.",
|
||||
"warn.cheats_skipped": "Não foi possível compilar os cheats da pasta de mods que alteram o código do jogo; o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.exefs_skipped": "Não foi possível compilar os patches ExeFS dos mods (.ips, .pchtxt); o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.npdm_differs": "O main.npdm difere do dump de referência da versão {version}; só os módulos de código são usados.",
|
||||
"warn.xbox_prompts_failed": "Não foi possível preparar os ícones de botão do Xbox (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)"
|
||||
}
|
||||
+39
-11
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"step.check": "Проверка компьютера и игровых файлов",
|
||||
"step.fonts": "Извлечение системных шрифтов",
|
||||
"step.extract": "Распаковка игры",
|
||||
"step.lift": "Перевод кода игры в C",
|
||||
"step.compile": "Сборка totk.exe",
|
||||
"step.compile": "Сборка {game_exe}",
|
||||
"step.prepare": "Подготовка шейдеров",
|
||||
"step.cleanup": "Уборка",
|
||||
|
||||
@@ -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": "Распаковка кода игры",
|
||||
@@ -21,11 +24,12 @@
|
||||
"phase.lift.discover": "Поиск функций",
|
||||
"phase.lift.emit": "Запись кода на C",
|
||||
"phase.compile.compile": "Компиляция",
|
||||
"phase.compile.link": "Компоновка totk.exe",
|
||||
"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": "Удаление временных файлов",
|
||||
|
||||
@@ -41,12 +45,14 @@
|
||||
"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_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.2.1 или 1.4.3 и prod.keys.",
|
||||
"check.no_display": "Для подготовки шейдеров нужен дисплей. Запустите установку из сеанса рабочего стола или выполните export DISPLAY=:0 (на Steam Deck в игровом режиме ещё XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.0 или 1.2.1 или 1.4.3 и prod.keys.",
|
||||
"check.input_missing": "Файл не найден ({input}): {path}",
|
||||
"check.keys_unreadable": "Не удаётся прочитать файл ключей. Выберите файл prod.keys.",
|
||||
"check.keys_no_header_key": "В файле ключей нет header_key. Снова получите prod.keys со своей консоли.",
|
||||
@@ -57,44 +63,65 @@
|
||||
"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.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": "Не удалось собрать totk.exe (код выхода {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.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, а они не указаны. Игра не запустится на этом языке, пока их не добавят. Выберите папку прошивки (Contents/registered) или см. раздел «Switch system fonts» в docs/installer.md.",
|
||||
"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.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.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)",
|
||||
@@ -110,12 +137,13 @@
|
||||
"ui.update_mode": "В этой папке уже есть установка. Программа продолжит или обновит её; файлы игры понадобятся, только если их снова придётся читать.",
|
||||
"ui.need_dest": "Выберите папку установки.",
|
||||
"ui.need_base": "Выберите файл базовой игры.",
|
||||
"ui.need_update": "Выберите файл обновления 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": "Обновление установки",
|
||||
@@ -132,7 +160,7 @@
|
||||
"ui.result_ok": "Игра готова",
|
||||
"ui.result_failed": "Установка не удалась",
|
||||
"ui.result_cancelled": "Установка отменена",
|
||||
"ui.done_text": "Запустите totk.exe из папки {dest}. Программа установки и выбранные игровые файлы больше не нужны.",
|
||||
"ui.done_text": "Запустите {game_exe} из папки {dest}. Программа установки и выбранные игровые файлы больше не нужны.",
|
||||
"ui.cancelled_text": "Завершённые шаги сохранены. Чтобы продолжить, снова запустите установку с той же папкой.",
|
||||
"ui.log_file": "Файл журнала: {path}",
|
||||
"ui.process_died": "Установка неожиданно остановилась (код выхода {code}).",
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"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.no_display": "准备着色器需要显示器。请在桌面会话中运行安装程序,或执行 export DISPLAY=:0(Steam Deck 游戏模式下还需 XDG_RUNTIME_DIR=/run/user/1000)。",
|
||||
"check.inputs_needed": "需要游戏文件:请选择游戏本体、1.1.0、1.2.1 或 1.4.3 更新以及 prod.keys。",
|
||||
"check.input_missing": "找不到文件({input}):{path}",
|
||||
"check.keys_unreadable": "无法读取密钥文件。请选择一个 prod.keys 文件。",
|
||||
"check.keys_no_header_key": "密钥文件中没有 header_key。请从你的主机重新导出 prod.keys。",
|
||||
"check.keys_wrong": "这些密钥无法打开{input}文件。请使用你自己主机的 prod.keys。",
|
||||
"check.keys_too_old": "密钥文件对{input}文件来说太旧(需要密钥代数 {generation})。请从固件更新的主机导出 prod.keys。",
|
||||
"check.not_game_file": "{input}文件不是 XCI 或 NSP 游戏文件。",
|
||||
"check.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 系统字体,但未提供。在添加这些字体之前,游戏无法以该语言启动。请选择固件文件夹(Contents/registered),或参阅 docs/installer.md 中的“Switch system fonts”一节。",
|
||||
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
|
||||
"warn.xbox_prompts_failed": "无法准备 Xbox 按键提示(退出代码 {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": "中文(繁体)"
|
||||
}
|
||||
+114
-51
@@ -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:
|
||||
@@ -523,7 +558,8 @@ def _retry(fn, *args):
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
|
||||
|
||||
|
||||
def _worker_init(mods, starts, table, opts):
|
||||
def _worker_init(mods, starts, table, opts, jobs):
|
||||
_W['jobs'] = jobs
|
||||
_W['mods'] = mods
|
||||
_W['starts'] = starts
|
||||
_W['patches'] = table
|
||||
@@ -546,6 +582,10 @@ def _emit_chunk(task):
|
||||
"""Worker: write one chunk. Returns its statistics."""
|
||||
name, number, path, fns = task
|
||||
t0 = time.perf_counter()
|
||||
# Test hook: a pool wider than this raises the MemoryError a busy machine does, so the retry can be exercised.
|
||||
limit = os.environ.get('TOTK_LIFT_TEST_OOM_ABOVE')
|
||||
if limit and _W['jobs'] > int(limit) and number % 2 == 1:
|
||||
raise MemoryError
|
||||
e = _emitter(name)
|
||||
e.stats.clear()
|
||||
e.unsupported.clear()
|
||||
@@ -597,6 +637,14 @@ def pick_jobs(requested):
|
||||
"""Worker count and the reason for it: --jobs if given, else the CPU count capped by the free memory."""
|
||||
if requested:
|
||||
return requested, '--jobs'
|
||||
env = os.environ.get('TOTK_LIFT_JOBS', '').strip()
|
||||
if env:
|
||||
try:
|
||||
if int(env) >= 1:
|
||||
return int(env), 'TOTK_LIFT_JOBS'
|
||||
except ValueError:
|
||||
pass
|
||||
print(f'ignoring TOTK_LIFT_JOBS={env!r}: not a positive integer', file=sys.stderr, flush=True)
|
||||
cpus = os.cpu_count() or 4
|
||||
free = hostmem.available_ram()
|
||||
if free is None:
|
||||
@@ -670,16 +718,31 @@ def main():
|
||||
t_emit = time.perf_counter()
|
||||
ctx = mp.get_context('spawn')
|
||||
jobs, why = pick_jobs(args.jobs)
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts)) as pool:
|
||||
for i, r in enumerate(pool.imap_unordered(_emit_chunk, [t for _, t in tasks], chunksize=1)):
|
||||
results[r['module']].append(r)
|
||||
if r['error']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
if (i + 1) % 200 == 0 or i + 1 == len(tasks):
|
||||
print(f'emitted {i + 1}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s', file=sys.stderr,
|
||||
flush=True)
|
||||
pending = [t for _, t in tasks]
|
||||
emitted = 0
|
||||
while True:
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
try:
|
||||
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
|
||||
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
|
||||
results[r['module']].append(r)
|
||||
if r['error']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
emitted += 1
|
||||
if emitted % 200 == 0 or emitted == len(tasks):
|
||||
print(f'emitted {emitted}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s',
|
||||
file=sys.stderr, flush=True)
|
||||
break
|
||||
except MemoryError:
|
||||
if jobs == 1:
|
||||
raise
|
||||
smaller = jobs // 2
|
||||
print(f'emit: out of memory with {jobs} workers, retrying with {smaller}', file=sys.stderr, flush=True)
|
||||
jobs, why = smaller, 'after out of memory'
|
||||
done = {(r['module'], r['number']) for rs in results.values() for r in rs}
|
||||
pending = [t for t in pending if (t[0], t[1]) not in done]
|
||||
|
||||
for name, d in info.items():
|
||||
rs = sorted(results[name], key=lambda r: r['number'])
|
||||
|
||||
@@ -0,0 +1,957 @@
|
||||
# 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.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.
|
||||
|
||||
set fps60 TOTK_FPS60 1
|
||||
set realtime TOTK_REALTIME 1
|
||||
set cloth TOTK_CLOTH_REALTIME 1
|
||||
set bounce TOTK_HIGHFPS_FIX 1
|
||||
set output TOTK_PATCH_OUTPUT 0
|
||||
set scene TOTK_PATCH_SCENE 0
|
||||
|
||||
# 60 fps: the adaptive present-interval controller (main+0x8f26c8, 1.0.0 0x8f6bec) gets the minimum interval 1.
|
||||
site fps60 main 90e978 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
|
||||
site fps60 main 90e97c 1a89b10a 1a9fa52a # clamp: csinc w10, w9, wzr, ge (desired = max(desired, 1))
|
||||
site fps60 main 90e9a0 b9401a8a d503201f # lower trigger: nop the stored-candidate load
|
||||
site fps60 main 90ea30 6b08015f 7100055f # raise path: cmp w10, #1
|
||||
site fps60 main 90ea40 1a88c1ac 1a9fc5ac # raise path: csinc w12, w13, wzr, gt (next lower candidate floors at 1)
|
||||
site fps60 main 90ea94 b9400288 52800028 # lower path: mov w8, #1 (minimum interval 1)
|
||||
site fps60 main 90eab0 2a0a03f5 1a9fa555 # lower path: csinc w21, w10, wzr, ge (result never below 1)
|
||||
|
||||
# Real-time step: the VFR step (main+0x7ede6c, 1.0.0 0x7a83e4) and the global ratio after the controller call
|
||||
# (main+0x8f2394, 1.0.0 0x8f68b8) read ratio and seconds from the patch page instead of interval * 0.5 and interval / 60.
|
||||
site realtime main 833e4c 1e220020 @page:x9 # scvtf s0, w1 -> adrp x9, page
|
||||
site realtime main 833e50 52a84e08 bd400120 # mov w8, #60.0 -> ldr s0, [x9] (ratio)
|
||||
site realtime main 833e54 1e270101 bd400521 # fmov s1, w8 -> ldr s1, [x9, #4] (seconds)
|
||||
site realtime main 833e58 1e2c1002 d503201f # fmov s2, #0.5 -> nop
|
||||
site realtime main 833e64 1e211801 d503201f # fdiv s1, s0, s1 -> nop
|
||||
site realtime main 833e68 1e220800 d503201f # fmul s0, s0, s2 -> nop
|
||||
site realtime main 90e5d0 bd425e60 @page:x9 # ldr s0, [x19, #0x25c] -> adrp x9, page
|
||||
site realtime main 90e5d4 1e2c1001 bd400120 # fmov s1, #0.5 -> ldr s0, [x9] (global ratio)
|
||||
site realtime main 90e5e4 5e21d800 d503201f # scvtf s0, s0 -> nop
|
||||
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.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
|
||||
site cloth main 9efc1c bd0117e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0137ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x114]
|
||||
site cloth main 9f0bcc 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
|
||||
site cloth main 9f0ba8 bd0117e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0137ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x114]
|
||||
site cloth main a05a04 1e210801 f81f0fe9 @page:x9 bd400521 1e210801 f84107e9 # fmul s1, s0, s1
|
||||
site cloth main a05a08 bd0137e0 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f081f bd0157ff 3dc007ff f94003e9 910083ff # str s0, [sp, #0x134]
|
||||
site cloth main 9f044c 97fbf1bd @page:x8 bd400104 1e240842 1e240863 97fbf1bd # bl 0x8ecb40
|
||||
|
||||
# Trampolines (upstream 27c3b43): the slow-motion check compares time object +0x28 (30 / (ratio * time scale)) with
|
||||
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
|
||||
site bounce main bee7e0 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
|
||||
|
||||
# Output size (upstream games/totk/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
|
||||
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
|
||||
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
|
||||
# through main+0xcc4160) from page +32; the renderer itself goes to page +40.
|
||||
site output main eb8cfc d280f008 @page:x8 # mov x8, #0x780 -> adrp x8, page
|
||||
site output main eb8d00 f2c08708 f9401908 # movk x8, #0x438, lsl #32 -> ldr x8, [x8, #48]
|
||||
site output main cea5c8 aa1403e0 @page:x21 # mov x0, x20 -> adrp x21, page
|
||||
site output main cea5cc 94000081 f90016b3 # bl 0xcc4160 -> str x19, [x21, #40]
|
||||
site output main cea5d0 f9400015 f94012b5 # ldr x21, [x0] -> ldr x21, [x21, #32]
|
||||
|
||||
# Scene render size (upstream patch_render_res): the renderer's +0xcc4 / +0xcc8, which size every scene render target,
|
||||
# take two floats from page +16 instead of the graphics parameters' docked 1600x900.
|
||||
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/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
|
||||
# main: 869 spin waits
|
||||
spin main ac600 ac620 ac630 # 13 instructions; loads
|
||||
spin main ac680 ac6a8 ac6b8 # 15 instructions; loads
|
||||
spin main ac6f0 ac718 ac728 # 15 instructions; loads
|
||||
spin main e8544 e854c # 3 instructions; loads
|
||||
spin main 2457f0 245804 # 6 instructions; loads
|
||||
spin main 644bbc 644bd0 # 6 instructions; loads
|
||||
spin main 6456ac 6456c0 # 6 instructions; loads
|
||||
spin main 6459d4 6459e8 # 6 instructions; loads
|
||||
spin main 645d58 645d6c # 6 instructions; loads
|
||||
spin main 6460c4 6460d8 # 6 instructions; loads
|
||||
spin main 64649c 6464b0 # 6 instructions; loads
|
||||
spin main 646740 646754 # 6 instructions; loads
|
||||
spin main 6467d4 6467e8 # 6 instructions; loads
|
||||
spin main 646e6c 646e80 # 6 instructions; loads
|
||||
spin main 647080 647094 # 6 instructions; loads
|
||||
spin main 6484f4 648510 # 8 instructions; loads
|
||||
spin main 6485a4 6485c0 # 8 instructions; loads
|
||||
spin main 649fb0 649fc4 # 6 instructions; loads
|
||||
spin main 64a064 64a078 # 6 instructions; loads
|
||||
spin main 64a3d0 64a3e4 # 6 instructions; loads
|
||||
spin main 64afac 64afc8 # 8 instructions; loads
|
||||
spin main 64b404 64b418 # 6 instructions; loads
|
||||
spin main 64b980 64b994 # 6 instructions; loads
|
||||
spin main 64c68c 64c6a0 # 6 instructions; loads
|
||||
spin main 64c978 64c98c # 6 instructions; loads
|
||||
spin main 64ca28 64ca3c # 6 instructions; loads
|
||||
spin main 64d378 64d38c # 6 instructions; loads
|
||||
spin main 64d8d8 64d8ec # 6 instructions; loads
|
||||
spin main 64db90 64dba4 # 6 instructions; loads
|
||||
spin main 64de18 64de2c # 6 instructions; loads
|
||||
spin main 64de40 64de54 # 6 instructions; loads
|
||||
spin main 64de68 64de7c # 6 instructions; loads
|
||||
spin main 64e6c8 64e6dc # 6 instructions; loads
|
||||
spin main 64e6f0 64e704 # 6 instructions; loads
|
||||
spin main 64e8ac 64e8c0 # 6 instructions; loads
|
||||
spin main 64e9a4 64e9b8 # 6 instructions; loads
|
||||
spin main 64fb50 64fb64 # 6 instructions; loads
|
||||
spin main 652eac 652ec0 # 6 instructions; loads
|
||||
spin main 652ed4 652ee8 # 6 instructions; loads
|
||||
spin main 652efc 652f10 # 6 instructions; loads
|
||||
spin main 654590 6545a4 # 6 instructions; loads
|
||||
spin main 654c5c 654c70 # 6 instructions; loads
|
||||
spin main 654cfc 654d10 # 6 instructions; loads
|
||||
spin main 655124 655138 # 6 instructions; loads
|
||||
spin main 655c14 655c28 # 6 instructions; loads
|
||||
spin main 656f80 656f94 # 6 instructions; loads
|
||||
spin main 658e60 658e74 # 6 instructions; loads
|
||||
spin main 658ebc 658ed0 # 6 instructions; loads
|
||||
spin main 659b28 659b3c # 6 instructions; loads
|
||||
spin main 659b50 659b64 # 6 instructions; loads
|
||||
spin main 65c9e0 65c9f4 # 6 instructions; loads
|
||||
spin main 65ca08 65ca1c # 6 instructions; loads
|
||||
spin main 65ca30 65ca44 # 6 instructions; loads
|
||||
spin main 65ca58 65ca6c # 6 instructions; loads
|
||||
spin main 65ca80 65ca94 # 6 instructions; loads
|
||||
spin main 65caa8 65cabc # 6 instructions; loads
|
||||
spin main 65cad0 65cae4 # 6 instructions; loads
|
||||
spin main 65caf8 65cb0c # 6 instructions; loads
|
||||
spin main 65cb20 65cb34 # 6 instructions; loads
|
||||
spin main 65cb48 65cb5c # 6 instructions; loads
|
||||
spin main 65cb70 65cb84 # 6 instructions; loads
|
||||
spin main 65cb98 65cbac # 6 instructions; loads
|
||||
spin main 65cbc0 65cbd4 # 6 instructions; loads
|
||||
spin main 65d4b0 65d4c4 # 6 instructions; loads
|
||||
spin main 65d4d8 65d4ec # 6 instructions; loads
|
||||
spin main 65d500 65d514 # 6 instructions; loads
|
||||
spin main 65d528 65d53c # 6 instructions; loads
|
||||
spin main 65da54 65da68 # 6 instructions; loads
|
||||
spin main 65de00 65de14 # 6 instructions; loads
|
||||
spin main 65e200 65e214 # 6 instructions; loads
|
||||
spin main 65e6ec 65e700 # 6 instructions; loads
|
||||
spin main 65ecbc 65ecd0 # 6 instructions; loads
|
||||
spin main 65ece4 65ecf8 # 6 instructions; loads
|
||||
spin main 663588 66359c # 6 instructions; loads
|
||||
spin main 663b0c 663b20 # 6 instructions; loads
|
||||
spin main 663da0 663db4 # 6 instructions; loads
|
||||
spin main 663dec 663e00 # 6 instructions; loads
|
||||
spin main 6645a4 6645b8 # 6 instructions; loads
|
||||
spin main 6645cc 6645e0 # 6 instructions; loads
|
||||
spin main 666530 666544 # 6 instructions; loads
|
||||
spin main 666570 666584 # 6 instructions; loads
|
||||
spin main 6671d4 6671e8 # 6 instructions; loads
|
||||
spin main 6671fc 667210 # 6 instructions; loads
|
||||
spin main 6676c0 6676d4 # 6 instructions; loads
|
||||
spin main 667cfc 667d10 # 6 instructions; loads
|
||||
spin main 66f34c 66f360 # 6 instructions; loads
|
||||
spin main 6708f8 67090c # 6 instructions; loads
|
||||
spin main 670920 670934 # 6 instructions; loads
|
||||
spin main 670948 67095c # 6 instructions; loads
|
||||
spin main 673f34 673f48 # 6 instructions; loads
|
||||
spin main 67556c 675580 # 6 instructions; loads
|
||||
spin main 676954 676968 # 6 instructions; loads
|
||||
spin main 676994 6769a8 # 6 instructions; loads
|
||||
spin main 676e6c 676e80 # 6 instructions; loads
|
||||
spin main 677148 67715c # 6 instructions; loads
|
||||
spin main 6771c8 6771dc # 6 instructions; loads
|
||||
spin main 677484 677498 # 6 instructions; loads
|
||||
spin main 677df0 677e04 # 6 instructions; loads
|
||||
spin main 6792a8 6792bc # 6 instructions; loads
|
||||
spin main 67a984 67a998 # 6 instructions; loads
|
||||
spin main 698ef8 698f0c # 6 instructions; loads
|
||||
spin main 6b6d8c 6b6da0 # 6 instructions; loads
|
||||
spin main 6b922c 6b9240 # 6 instructions; loads
|
||||
spin main 6c5408 6c541c # 6 instructions; loads
|
||||
spin main 6c5614 6c5628 # 6 instructions; loads
|
||||
spin main 6ccce0 6cccf4 # 6 instructions; loads
|
||||
spin main 6ccd98 6ccdac # 6 instructions; loads
|
||||
spin main 6d3c9c 6d3cb0 # 6 instructions; loads
|
||||
spin main 6d3ff8 6d400c # 6 instructions; loads
|
||||
spin main 6d61cc 6d61e0 # 6 instructions; loads
|
||||
spin main 6d6210 6d6224 # 6 instructions; loads
|
||||
spin main 6d6254 6d6268 # 6 instructions; loads
|
||||
spin main 6d6298 6d62ac # 6 instructions; loads
|
||||
spin main 6d75c4 6d75d8 # 6 instructions; loads
|
||||
spin main 6d79dc 6d79f0 # 6 instructions; loads
|
||||
spin main 6d9398 6d93ac # 6 instructions; loads
|
||||
spin main 6dd8d0 6dd8e4 # 6 instructions; loads
|
||||
spin main 6de150 6de164 # 6 instructions; loads
|
||||
spin main 6e073c 6e0750 # 6 instructions; loads
|
||||
spin main 6e0a94 6e0aa8 # 6 instructions; loads
|
||||
spin main 6e4eb8 6e4ecc # 6 instructions; loads
|
||||
spin main 6eb590 6eb5a4 # 6 instructions; loads
|
||||
spin main 6eccac 6eccc0 # 6 instructions; loads
|
||||
spin main 6ed9b4 6ed9c8 # 6 instructions; loads
|
||||
spin main 6ee69c 6ee6b0 # 6 instructions; loads
|
||||
spin main 6f94c0 6f94d4 # 6 instructions; loads
|
||||
spin main 6f9738 6f974c # 6 instructions; loads
|
||||
spin main 6fa324 6fa338 # 6 instructions; loads
|
||||
spin main 70d844 70d858 # 6 instructions; loads
|
||||
spin main 70dc74 70dc88 # 6 instructions; loads
|
||||
spin main 710a70 710a84 # 6 instructions; loads
|
||||
spin main 711848 71185c # 6 instructions; loads
|
||||
spin main 7125a4 7125b8 # 6 instructions; loads
|
||||
spin main 716474 716488 # 6 instructions; loads
|
||||
spin main 719a30 719a44 # 6 instructions; loads
|
||||
spin main 72cd78 72cd8c # 6 instructions; loads
|
||||
spin main 72f66c 72f680 # 6 instructions; loads
|
||||
spin main 737684 737698 # 6 instructions; loads
|
||||
spin main 73e0dc 73e0f0 # 6 instructions; loads
|
||||
spin main 73ed14 73ed28 # 6 instructions; loads
|
||||
spin main 783520 783534 # 6 instructions; loads
|
||||
spin main 797924 797938 # 6 instructions; loads
|
||||
spin main 79794c 797960 # 6 instructions; loads
|
||||
spin main 797c08 797c1c # 6 instructions; loads
|
||||
spin main 797c64 797c78 # 6 instructions; loads
|
||||
spin main 798ff4 799008 # 6 instructions; loads
|
||||
spin main 79cbec 79cc00 # 6 instructions; loads
|
||||
spin main 7ae134 7ae148 # 6 instructions; loads
|
||||
spin main 7ae388 7ae39c # 6 instructions; loads
|
||||
spin main 7c292c 7c2940 # 6 instructions; loads
|
||||
spin main 7d618c 7d61a0 # 6 instructions; loads
|
||||
spin main 7d904c 7d9060 # 6 instructions; loads
|
||||
spin main 7e11c0 7e11d4 # 6 instructions; loads
|
||||
spin main 7e1c9c 7e1cb0 # 6 instructions; loads
|
||||
spin main 7e24a0 7e24b4 # 6 instructions; loads
|
||||
spin main 7ed89c 7ed8b0 # 6 instructions; loads
|
||||
spin main 7eef28 7eef3c # 6 instructions; loads
|
||||
spin main 7f515c 7f5170 # 6 instructions; loads
|
||||
spin main 801468 80147c # 6 instructions; loads
|
||||
spin main 801490 8014a4 # 6 instructions; loads
|
||||
spin main 8014b8 8014cc # 6 instructions; loads
|
||||
spin main 80a0a4 80a0b8 # 6 instructions; loads
|
||||
spin main 811500 811514 # 6 instructions; loads
|
||||
spin main 816c40 816c54 # 6 instructions; loads
|
||||
spin main 819450 819464 # 6 instructions; loads
|
||||
spin main 85df6c 85e068 # 4 instructions; loads
|
||||
spin main 863220 863234 # 6 instructions; loads
|
||||
spin main 86352c 863540 # 6 instructions; loads
|
||||
spin main 863a80 863a94 # 6 instructions; loads
|
||||
spin main 863b94 863ba8 # 6 instructions; loads
|
||||
spin main 86543c 865450 # 6 instructions; loads
|
||||
spin main 865464 865478 # 6 instructions; loads
|
||||
spin main 87e564 87e578 # 6 instructions; loads
|
||||
spin main 87e58c 87e5a0 # 6 instructions; loads
|
||||
spin main 880d44 880d58 # 6 instructions; loads
|
||||
spin main 8810f4 881108 # 6 instructions; loads
|
||||
spin main 8814c8 8814dc # 6 instructions; loads
|
||||
spin main 88189c 8818b0 # 6 instructions; loads
|
||||
spin main 887ce8 887cfc # 6 instructions; loads
|
||||
spin main 887f50 887f64 # 6 instructions; loads
|
||||
spin main 8884ec 888500 # 6 instructions; loads
|
||||
spin main 888618 88862c # 6 instructions; loads
|
||||
spin main 888748 88875c # 6 instructions; loads
|
||||
spin main 8888e0 8888f4 # 6 instructions; loads
|
||||
spin main 89183c 891850 # 6 instructions; loads
|
||||
spin main 892ccc 892cec # 9 instructions; loads
|
||||
spin main 89540c 89541c # 5 instructions; loads
|
||||
spin main 895668 895694 # 12 instructions; loads
|
||||
spin main 8956a8 8956b8 # 5 instructions; loads
|
||||
spin main 8da7dc 8da7e4 # 3 instructions; loads
|
||||
spin main 8da7ec 8da7f0 # 2 instructions; loads
|
||||
spin main 8da858 8da860 # 3 instructions; loads
|
||||
spin main 8fb190 8fb1a4 # 6 instructions; loads
|
||||
spin main 9045bc 9045d0 # 6 instructions; loads
|
||||
spin main 9045e4 9045f8 # 6 instructions; loads
|
||||
spin main 90460c 904620 # 6 instructions; loads
|
||||
spin main 904634 904648 # 6 instructions; loads
|
||||
spin main 905b20 905b34 # 6 instructions; loads
|
||||
spin main 9062f0 906304 # 6 instructions; loads
|
||||
spin main 91019c 9101b0 # 6 instructions; loads
|
||||
spin main 910720 910734 # 6 instructions; loads
|
||||
spin main 9363cc 9363e0 # 6 instructions; loads
|
||||
spin main 93655c 936570 # 6 instructions; loads
|
||||
spin main 93819c 9381b0 # 6 instructions; loads
|
||||
spin main 940060 94007c # 8 instructions; loads
|
||||
spin main 94012c 940148 # 8 instructions; loads
|
||||
spin main 97864c 978660 # 6 instructions; loads
|
||||
spin main 99aa8c 99aabc # 13 instructions; loads
|
||||
spin main 99aad0 99aae0 # 5 instructions; loads
|
||||
spin main 9a5fd4 9a6004 # 13 instructions; loads
|
||||
spin main 9a6018 9a6028 # 5 instructions; loads
|
||||
spin main 9b2700 9b2710 # 5 instructions; loads
|
||||
spin main 9f395c 9f3970 # 6 instructions; loads
|
||||
spin main 9f39e0 9f39f4 # 6 instructions; loads
|
||||
spin main 9f3b2c 9f3b40 # 6 instructions; loads
|
||||
spin main 9f3bf8 9f3c0c # 6 instructions; loads
|
||||
spin main 9f6f5c 9f6f70 # 6 instructions; loads
|
||||
spin main a11908 a1191c # 6 instructions; loads
|
||||
spin main a1257c a12590 # 6 instructions; loads
|
||||
spin main a14d6c a14d80 # 6 instructions; loads
|
||||
spin main a1b0a4 a1b0b8 # 6 instructions; loads
|
||||
spin main a1b0ec a1b100 # 6 instructions; loads
|
||||
spin main a1c3dc a1c3f0 # 6 instructions; loads
|
||||
spin main a2b268 a2b27c # 6 instructions; loads
|
||||
spin main a2cab8 a2cacc # 6 instructions; loads
|
||||
spin main a2f228 a2f23c # 6 instructions; loads
|
||||
spin main a8e920 a8e934 # 6 instructions; loads
|
||||
spin main aa2c20 aa2c34 # 6 instructions; loads
|
||||
spin main aa5108 aa511c # 6 instructions; loads
|
||||
spin main aa9f94 aa9fa8 # 6 instructions; loads
|
||||
spin main aaa674 aaa688 # 6 instructions; loads
|
||||
spin main aad564 aad578 # 6 instructions; loads
|
||||
spin main ab245c ab24b4 # 7 instructions; loads
|
||||
spin main ac8874 ac88fc # 35 instructions; loads
|
||||
spin main af4040 af4054 # 6 instructions; loads
|
||||
spin main af4068 af407c # 6 instructions; loads
|
||||
spin main af4090 af40a4 # 6 instructions; loads
|
||||
spin main af40cc af40e0 # 6 instructions; loads
|
||||
spin main af40f4 af4108 # 6 instructions; loads
|
||||
spin main af411c af4130 # 6 instructions; loads
|
||||
spin main b47a74 b47a88 # 6 instructions; loads
|
||||
spin main b4e080 b4e094 # 6 instructions; loads
|
||||
spin main b529f4 b52a08 # 6 instructions; loads
|
||||
spin main b52a7c b52a90 # 6 instructions; loads
|
||||
spin main b52cfc b52d10 # 6 instructions; loads
|
||||
spin main b52dd0 b52de4 # 6 instructions; loads
|
||||
spin main b53458 b5346c # 6 instructions; loads
|
||||
spin main b535a8 b535bc # 6 instructions; loads
|
||||
spin main b54320 b54334 # 6 instructions; loads
|
||||
spin main b545fc b54610 # 6 instructions; loads
|
||||
spin main b5662c b56640 # 6 instructions; loads
|
||||
spin main b56654 b56668 # 6 instructions; loads
|
||||
spin main b71800 b71814 # 6 instructions; loads
|
||||
spin main b71828 b7183c # 6 instructions; loads
|
||||
spin main b7b82c b7b840 # 6 instructions; loads
|
||||
spin main b80970 b80984 # 6 instructions; loads
|
||||
spin main b86fe8 b86ffc # 6 instructions; loads
|
||||
spin main b8702c b87040 # 6 instructions; loads
|
||||
spin main b87070 b87084 # 6 instructions; loads
|
||||
spin main b8775c b87770 # 6 instructions; loads
|
||||
spin main b97d6c b97d80 # 6 instructions; loads
|
||||
spin main b97ed4 b97ee8 # 6 instructions; loads
|
||||
spin main b9c628 b9c63c # 6 instructions; loads
|
||||
spin main b9c6c8 b9c6dc # 6 instructions; loads
|
||||
spin main b9c6f0 b9c704 # 6 instructions; loads
|
||||
spin main b9c7b8 b9c7cc # 6 instructions; loads
|
||||
spin main ba1db4 ba1dc8 # 6 instructions; loads
|
||||
spin main ba72e8 ba72fc # 6 instructions; loads
|
||||
spin main bb1070 bb1084 # 6 instructions; loads
|
||||
spin main bb1098 bb10ac # 6 instructions; loads
|
||||
spin main bb6c38 bb6c4c # 6 instructions; loads
|
||||
spin main bb7fa0 bb7fb4 # 6 instructions; loads
|
||||
spin main bd27c8 bd27d8 # 5 instructions; loads
|
||||
spin main bdd678 bdd68c # 6 instructions; loads
|
||||
spin main bf2ccc bf2ce0 # 6 instructions; loads
|
||||
spin main bf2e24 bf2e38 # 6 instructions; loads
|
||||
spin main bf32a0 bf32b4 # 6 instructions; loads
|
||||
spin main bf3730 bf3744 # 6 instructions; loads
|
||||
spin main c0a554 c0a568 # 6 instructions; loads
|
||||
spin main c0a6b4 c0a6c8 # 6 instructions; loads
|
||||
spin main c0a850 c0a864 # 6 instructions; loads
|
||||
spin main c0d758 c0d76c # 6 instructions; loads
|
||||
spin main c0d8b0 c0d8c4 # 6 instructions; loads
|
||||
spin main c0dd3c c0dd50 # 6 instructions; loads
|
||||
spin main c15ffc c16010 # 6 instructions; loads
|
||||
spin main c1665c c16670 # 6 instructions; loads
|
||||
spin main c2315c c23170 # 6 instructions; loads
|
||||
spin main c2374c c23760 # 6 instructions; loads
|
||||
spin main c23998 c239ac # 6 instructions; loads
|
||||
spin main c2d128 c2d13c # 6 instructions; loads
|
||||
spin main c3a9cc c3a9e0 # 6 instructions; loads
|
||||
spin main c5ab4c c5ab60 # 6 instructions; loads
|
||||
spin main c70388 c7039c # 6 instructions; loads
|
||||
spin main c703b0 c703c4 # 6 instructions; loads
|
||||
spin main c94f70 c94f84 # 6 instructions; loads
|
||||
spin main c95a00 c95a14 # 6 instructions; loads
|
||||
spin main c96764 c96778 # 6 instructions; loads
|
||||
spin main c96948 c9695c # 6 instructions; loads
|
||||
spin main c96ce4 c96cf8 # 6 instructions; loads
|
||||
spin main c96d54 c96d68 # 6 instructions; loads
|
||||
spin main c97400 c97414 # 6 instructions; loads
|
||||
spin main c97ca0 c97cb4 # 6 instructions; loads
|
||||
spin main c99ba0 c99bb4 # 6 instructions; loads
|
||||
spin main c99c90 c99ca4 # 6 instructions; loads
|
||||
spin main c9abac c9abc0 # 6 instructions; loads
|
||||
spin main c9ac8c c9aca0 # 6 instructions; loads
|
||||
spin main c9b2e0 c9b2f4 # 6 instructions; loads
|
||||
spin main c9d9f4 c9da08 # 6 instructions; loads
|
||||
spin main c9db3c c9db50 # 6 instructions; loads
|
||||
spin main c9df14 c9df30 # 8 instructions; loads
|
||||
spin main ccaaa8 ccaabc # 6 instructions; loads
|
||||
spin main cd38a4 cd38b8 # 6 instructions; loads
|
||||
spin main cda7c4 cda7d8 # 6 instructions; loads
|
||||
spin main cdb388 cdb39c # 6 instructions; loads
|
||||
spin main cdb3cc cdb3e0 # 6 instructions; loads
|
||||
spin main cdbab4 cdbac8 # 6 instructions; loads
|
||||
spin main cdbaf8 cdbb0c # 6 instructions; loads
|
||||
spin main cdbce4 cdbcf8 # 6 instructions; loads
|
||||
spin main cdce0c cdce20 # 6 instructions; loads
|
||||
spin main cecedc cecef0 # 6 instructions; loads
|
||||
spin main d0fbb4 d0fbc8 # 6 instructions; loads
|
||||
spin main d1acd0 d1ace4 # 6 instructions; loads
|
||||
spin main d1dc54 d1dc60 d1dcac d1dcc4 # 29 instructions; loads
|
||||
spin main d2273c d22750 # 6 instructions; loads
|
||||
spin main d231bc d231d0 # 6 instructions; loads
|
||||
spin main d23398 d233ac # 6 instructions; loads
|
||||
spin main d2e810 d2e824 # 6 instructions; loads
|
||||
spin main d3d8dc d3d8f0 # 6 instructions; loads
|
||||
spin main d4c3a8 d4c3bc # 6 instructions; loads
|
||||
spin main d4ffc4 d4ffd8 # 6 instructions; loads
|
||||
spin main d50a78 d50a8c # 6 instructions; loads
|
||||
spin main d50b04 d50b18 # 6 instructions; loads
|
||||
spin main d50bfc d50c10 # 6 instructions; loads
|
||||
spin main d50e60 d50e74 # 6 instructions; loads
|
||||
spin main d50e88 d50e9c # 6 instructions; loads
|
||||
spin main d50ebc d50ed0 # 6 instructions; loads
|
||||
spin main d510b8 d510cc # 6 instructions; loads
|
||||
spin main d51afc d51b18 # 8 instructions; loads
|
||||
spin main d52478 d5248c # 6 instructions; loads
|
||||
spin main d527f4 d52808 # 6 instructions; loads
|
||||
spin main d5283c d52850 # 6 instructions; loads
|
||||
spin main d52864 d52878 # 6 instructions; loads
|
||||
spin main d5288c d528a0 # 6 instructions; loads
|
||||
spin main d528f8 d5290c # 6 instructions; loads
|
||||
spin main d52920 d52934 # 6 instructions; loads
|
||||
spin main d529a8 d529bc # 6 instructions; loads
|
||||
spin main d52ab0 d52ac4 # 6 instructions; loads
|
||||
spin main d91344 d91358 # 6 instructions; loads
|
||||
spin main d91970 d91984 # 6 instructions; loads
|
||||
spin main d91cc0 d91cd4 # 6 instructions; loads
|
||||
spin main d91dc0 d91dd4 # 6 instructions; loads
|
||||
spin main d92280 d92294 # 6 instructions; loads
|
||||
spin main d923cc d923e0 # 6 instructions; loads
|
||||
spin main d93600 d93614 # 6 instructions; loads
|
||||
spin main d94100 d94114 # 6 instructions; loads
|
||||
spin main d9412c d94140 # 6 instructions; loads
|
||||
spin main d99098 d990ac # 6 instructions; loads
|
||||
spin main d9c260 d9c298 # 8 instructions; loads
|
||||
spin main d9c2c8 d9c300 # 8 instructions; loads
|
||||
spin main d9fe34 d9fe48 # 6 instructions; loads
|
||||
spin main da1eb4 da1ec8 # 6 instructions; loads
|
||||
spin main da1edc da1ef0 # 6 instructions; loads
|
||||
spin main da7010 da7024 # 6 instructions; loads
|
||||
spin main da70f8 da710c # 6 instructions; loads
|
||||
spin main dade1c dade30 # 6 instructions; loads
|
||||
spin main db58dc db58f0 # 6 instructions; loads
|
||||
spin main dbb0e4 dbb0f8 # 6 instructions; loads
|
||||
spin main dbca30 dbca4c # 8 instructions; loads
|
||||
spin main dbfc20 dbfc34 # 6 instructions; loads
|
||||
spin main dbff00 dbff14 # 6 instructions; loads
|
||||
spin main dc78fc dc7910 # 6 instructions; loads
|
||||
spin main dcaa88 dcaa9c # 6 instructions; loads
|
||||
spin main dcb2ac dcb2c0 # 6 instructions; loads
|
||||
spin main dcb9c8 dcb9dc # 6 instructions; loads
|
||||
spin main dd4a28 dd4a3c # 6 instructions; loads
|
||||
spin main dd6414 dd6428 # 6 instructions; loads
|
||||
spin main dd7498 dd74ac # 6 instructions; loads
|
||||
spin main dd78b4 dd78c8 # 6 instructions; loads
|
||||
spin main ddf338 ddf34c # 6 instructions; loads
|
||||
spin main de7324 de7338 # 6 instructions; loads
|
||||
spin main deb218 deb22c # 6 instructions; loads
|
||||
spin main df249c df24b0 # 6 instructions; loads
|
||||
spin main df2930 df2944 # 6 instructions; loads
|
||||
spin main dfab0c dfab20 # 6 instructions; loads
|
||||
spin main e1a248 e1a25c # 6 instructions; loads
|
||||
spin main e27b10 e27b24 # 6 instructions; loads
|
||||
spin main e28428 e2843c # 6 instructions; loads
|
||||
spin main e38cac e38cc0 # 6 instructions; loads
|
||||
spin main e38f90 e38fa4 # 6 instructions; loads
|
||||
spin main e39274 e39288 # 6 instructions; loads
|
||||
spin main e39558 e3956c # 6 instructions; loads
|
||||
spin main e40b1c e40b30 # 6 instructions; loads
|
||||
spin main e41478 e4148c # 6 instructions; loads
|
||||
spin main e450bc e450d0 # 6 instructions; loads
|
||||
spin main e45400 e45414 # 6 instructions; loads
|
||||
spin main e51aa0 e51ab4 # 6 instructions; loads
|
||||
spin main e51c08 e51c1c # 6 instructions; loads
|
||||
spin main e51cf4 e51d08 # 6 instructions; loads
|
||||
spin main e8e42c e8e440 # 6 instructions; loads
|
||||
spin main e90628 e9063c # 6 instructions; loads
|
||||
spin main e930d8 e930ec # 6 instructions; loads
|
||||
spin main e94c64 e94c78 # 6 instructions; loads
|
||||
spin main e9557c e95590 # 6 instructions; loads
|
||||
spin main e966e4 e966f8 # 6 instructions; loads
|
||||
spin main e9711c e97130 # 6 instructions; loads
|
||||
spin main e97164 e97178 # 6 instructions; loads
|
||||
spin main e9c134 e9c148 # 6 instructions; loads
|
||||
spin main ea4c28 ea4c3c # 6 instructions; loads
|
||||
spin main ea4c78 ea4c8c # 6 instructions; loads
|
||||
spin main ea4cc4 ea4cd8 # 6 instructions; loads
|
||||
spin main ea4d10 ea4d24 # 6 instructions; loads
|
||||
spin main eb07cc eb07d4 # 3 instructions; loads
|
||||
spin main eb07dc eb07e0 # 2 instructions; loads
|
||||
spin main eb0808 eb0810 # 3 instructions; loads
|
||||
spin main eb1548 eb155c # 6 instructions; loads
|
||||
spin main eb4240 eb4254 # 6 instructions; loads
|
||||
spin main eb5aa8 eb5abc # 6 instructions; loads
|
||||
spin main eb637c eb6390 # 6 instructions; loads
|
||||
spin main eb7954 eb7968 # 6 instructions; loads
|
||||
spin main ebf63c ebf650 # 6 instructions; loads
|
||||
spin main ebfb5c ebfb70 # 6 instructions; loads
|
||||
spin main ebfb98 ebfbac # 6 instructions; loads
|
||||
spin main ebfd50 ebfd64 # 6 instructions; loads
|
||||
spin main ebffb4 ebffc8 # 6 instructions; loads
|
||||
spin main eca534 eca548 # 6 instructions; loads
|
||||
spin main eca6f8 eca70c # 6 instructions; loads
|
||||
spin main ecb088 ecb09c # 6 instructions; loads
|
||||
spin main ed26e8 ed26fc # 6 instructions; loads
|
||||
spin main ed293c ed2950 # 6 instructions; loads
|
||||
spin main ed2ab0 ed2ac4 # 6 instructions; loads
|
||||
spin main ed2c24 ed2c38 # 6 instructions; loads
|
||||
spin main ed2ed0 ed2ee4 # 6 instructions; loads
|
||||
spin main ed3348 ed335c # 6 instructions; loads
|
||||
spin main ed351c ed3530 # 6 instructions; loads
|
||||
spin main ed3cf4 ed3d08 # 6 instructions; loads
|
||||
spin main ed42e0 ed42f4 # 6 instructions; loads
|
||||
spin main ed430c ed4320 # 6 instructions; loads
|
||||
spin main ed5294 ed52a8 # 6 instructions; loads
|
||||
spin main ed5658 ed566c # 6 instructions; loads
|
||||
spin main ed5680 ed5694 # 6 instructions; loads
|
||||
spin main ed79e8 ed79fc # 6 instructions; loads
|
||||
spin main ed7a28 ed7a3c # 6 instructions; loads
|
||||
spin main ed7a50 ed7a64 # 6 instructions; loads
|
||||
spin main ed7a78 ed7a8c # 6 instructions; loads
|
||||
spin main ed80ac ed80c0 # 6 instructions; loads
|
||||
spin main ed80d4 ed80e8 # 6 instructions; loads
|
||||
spin main ed934c ed9360 # 6 instructions; loads
|
||||
spin main ed94a0 ed94b4 # 6 instructions; loads
|
||||
spin main ed9a50 ed9a64 # 6 instructions; loads
|
||||
spin main ede7fc ede810 # 6 instructions; loads
|
||||
spin main ee0ce4 ee0cf8 # 6 instructions; loads
|
||||
spin main eebd28 eebd3c # 6 instructions; loads
|
||||
spin main ef9ed4 ef9ee8 # 6 instructions; loads
|
||||
spin main efa7a4 efa7b8 # 6 instructions; loads
|
||||
spin main efa7d4 efa7e8 # 6 instructions; loads
|
||||
spin main efad94 efada8 # 6 instructions; loads
|
||||
spin main efadc4 efadd8 # 6 instructions; loads
|
||||
spin main efdac8 efdadc # 6 instructions; loads
|
||||
spin main f01fa4 f01fb8 # 6 instructions; loads
|
||||
spin main f022e4 f022f8 # 6 instructions; loads
|
||||
spin main f02394 f023a8 # 6 instructions; loads
|
||||
spin main f02434 f02448 # 6 instructions; loads
|
||||
spin main f024e4 f024f8 # 6 instructions; loads
|
||||
spin main f3c87c f3c890 # 6 instructions; loads
|
||||
spin main f3d54c f3d560 # 6 instructions; loads
|
||||
spin main f3dba8 f3dbbc # 6 instructions; loads
|
||||
spin main f3dbd4 f3dbe8 # 6 instructions; loads
|
||||
spin main f547a4 f547b8 # 6 instructions; loads
|
||||
spin main f54d60 f54d74 # 6 instructions; loads
|
||||
spin main f57494 f574a8 # 6 instructions; loads
|
||||
spin main f66ef8 f66f0c # 6 instructions; loads
|
||||
spin main f66f20 f66f34 # 6 instructions; loads
|
||||
spin main f67170 f67184 # 6 instructions; loads
|
||||
spin main f671fc f67210 # 6 instructions; loads
|
||||
spin main f67224 f67238 # 6 instructions; loads
|
||||
spin main f67254 f67268 # 6 instructions; loads
|
||||
spin main f684f0 f68504 # 6 instructions; loads
|
||||
spin main f6a1ec f6a200 # 6 instructions; loads
|
||||
spin main f6a3f8 f6a40c # 6 instructions; loads
|
||||
spin main f6afc4 f6afd8 # 6 instructions; loads
|
||||
spin main f712a4 f712b8 # 6 instructions; loads
|
||||
spin main f71400 f71414 # 6 instructions; loads
|
||||
spin main f7155c f71570 # 6 instructions; loads
|
||||
spin main f716b8 f716cc # 6 instructions; loads
|
||||
spin main f729dc f729f0 # 6 instructions; loads
|
||||
spin main f73084 f73098 # 6 instructions; loads
|
||||
spin main f73148 f7315c # 6 instructions; loads
|
||||
spin main f76574 f76588 # 6 instructions; loads
|
||||
spin main f7c8ec f7c900 # 6 instructions; loads
|
||||
spin main f83bd0 f83be4 # 6 instructions; loads
|
||||
spin main f870f0 f87104 # 6 instructions; loads
|
||||
spin main f94eac f94ec0 # 6 instructions; loads
|
||||
spin main fa6580 fa6594 # 6 instructions; loads
|
||||
spin main fa65a8 fa65bc # 6 instructions; loads
|
||||
spin main fabb50 fabb64 # 6 instructions; loads
|
||||
spin main fabe74 fabe88 # 6 instructions; loads
|
||||
spin main fac8a0 fac8b4 # 6 instructions; loads
|
||||
spin main fac8c4 fac8d8 # 6 instructions; loads
|
||||
spin main face58 face6c # 6 instructions; loads
|
||||
spin main fc0a28 fc0a3c # 6 instructions; loads
|
||||
spin main fc0c68 fc0c7c # 6 instructions; loads
|
||||
spin main fc0e8c fc0ea0 # 6 instructions; loads
|
||||
spin main fc1054 fc1068 # 6 instructions; loads
|
||||
spin main fc1178 fc118c # 6 instructions; loads
|
||||
spin main fcb18c fcb1a8 # 8 instructions; loads
|
||||
spin main fccfe8 fccffc # 6 instructions; loads
|
||||
spin main fcd554 fcd640 # 4 instructions; loads
|
||||
spin main fcdc5c fcdce4 # 4 instructions; loads
|
||||
spin main fd2d30 fd2d44 # 6 instructions; loads
|
||||
spin main fd6250 fd6264 # 6 instructions; loads
|
||||
spin main fdf818 fdf82c # 6 instructions; loads
|
||||
spin main fdfb30 fdfb44 # 6 instructions; loads
|
||||
spin main fe0fa8 fe0fbc # 6 instructions; loads
|
||||
spin main fe1ad8 fe1aec # 6 instructions; loads
|
||||
spin main fea3a0 fea3b4 # 6 instructions; loads
|
||||
spin main fead08 fead1c # 6 instructions; loads
|
||||
spin main feb168 feb17c # 6 instructions; loads
|
||||
spin main feb1d0 feb1e4 # 6 instructions; loads
|
||||
spin main ff2af8 ff2b0c # 6 instructions; loads
|
||||
spin main ff5680 ff5694 # 6 instructions; loads
|
||||
spin main ff58e0 ff58f4 # 6 instructions; loads
|
||||
spin main ff9660 ff9674 # 6 instructions; loads
|
||||
spin main ffacec ffad00 # 6 instructions; loads
|
||||
spin main 1001500 1001514 # 6 instructions; loads
|
||||
spin main 1002c28 1002c3c # 6 instructions; loads
|
||||
spin main 1010b3c 1010b50 # 6 instructions; loads
|
||||
spin main 1010ef4 1010f08 # 6 instructions; loads
|
||||
spin main 1011244 1011258 # 6 instructions; loads
|
||||
spin main 1011544 1011558 # 6 instructions; loads
|
||||
spin main 1011a9c 1011ab0 # 6 instructions; loads
|
||||
spin main 1011e34 1011e48 # 6 instructions; loads
|
||||
spin main 10121b4 10121c8 # 6 instructions; loads
|
||||
spin main 1012228 101223c # 6 instructions; loads
|
||||
spin main 1013150 1013164 # 6 instructions; loads
|
||||
spin main 101349c 10134b0 # 6 instructions; loads
|
||||
spin main 1013920 1013934 # 6 instructions; loads
|
||||
spin main 1013b6c 1013b80 # 6 instructions; loads
|
||||
spin main 1014950 1014964 # 6 instructions; loads
|
||||
spin main 10149e8 10149fc # 6 instructions; loads
|
||||
spin main 1015bac 1015bc0 # 6 instructions; loads
|
||||
spin main 10160c0 10160d4 # 6 instructions; loads
|
||||
spin main 10166f8 101670c # 6 instructions; loads
|
||||
spin main 10167b4 10167c8 # 6 instructions; loads
|
||||
spin main 1016870 1016884 # 6 instructions; loads
|
||||
spin main 101692c 1016940 # 6 instructions; loads
|
||||
spin main 10170f0 1017104 # 6 instructions; loads
|
||||
spin main 10191dc 10191f0 # 6 instructions; loads
|
||||
spin main 101948c 10194a0 # 6 instructions; loads
|
||||
spin main 1019778 101978c # 6 instructions; loads
|
||||
spin main 1019a2c 1019a40 # 6 instructions; loads
|
||||
spin main 1019cf0 1019d04 # 6 instructions; loads
|
||||
spin main 1019ff4 101a008 # 6 instructions; loads
|
||||
spin main 101a054 101a068 # 6 instructions; loads
|
||||
spin main 101b594 101b5b0 # 8 instructions; loads
|
||||
spin main 101c7b4 101c7c8 # 6 instructions; loads
|
||||
spin main 101c7fc 101c810 # 6 instructions; loads
|
||||
spin main 101c844 101c858 # 6 instructions; loads
|
||||
spin main 101c88c 101c8a0 # 6 instructions; loads
|
||||
spin main 101c8d4 101c8e8 # 6 instructions; loads
|
||||
spin main 1023190 10231a4 # 6 instructions; loads
|
||||
spin main 10461ac 10461c0 # 6 instructions; loads
|
||||
spin main 1047278 104728c # 6 instructions; loads
|
||||
spin main 1050a8c 1050aa0 # 6 instructions; loads
|
||||
spin main 1055074 10550bc # 3 instructions; loads
|
||||
spin main 1056bd4 1056be8 # 6 instructions; loads
|
||||
spin main 106037c 1060390 # 6 instructions; loads
|
||||
spin main 1066bf4 1066c08 # 6 instructions; loads
|
||||
spin main 106bc38 106bc4c # 6 instructions; loads
|
||||
spin main 106be80 106be94 # 6 instructions; loads
|
||||
spin main 1070220 1070234 # 6 instructions; loads
|
||||
spin main 1071264 1071278 # 6 instructions; loads
|
||||
spin main 1071788 107179c # 6 instructions; loads
|
||||
spin main 1073910 1073924 # 6 instructions; loads
|
||||
spin main 10815e0 10815f4 # 6 instructions; loads
|
||||
spin main 108cb28 108cb3c # 6 instructions; loads
|
||||
spin main 1095678 109568c # 6 instructions; loads
|
||||
spin main 10956c0 10956d4 # 6 instructions; loads
|
||||
spin main 109f134 109f148 # 6 instructions; loads
|
||||
spin main 10bc820 10bc834 # 6 instructions; loads
|
||||
spin main 10be4c8 10be4dc # 6 instructions; loads
|
||||
spin main 10be4f0 10be504 # 6 instructions; loads
|
||||
spin main 10c2d4c 10c2d60 # 6 instructions; loads
|
||||
spin main 10c3748 10c375c # 6 instructions; loads
|
||||
spin main 10d5ef8 10d5f0c # 6 instructions; loads
|
||||
spin main 10da208 10da21c # 6 instructions; loads
|
||||
spin main 10e3edc 10e3ef0 # 6 instructions; loads
|
||||
spin main 10e5ee4 10e5ef8 # 6 instructions; loads
|
||||
spin main 10ec860 10ec874 # 6 instructions; loads
|
||||
spin main 10ec8dc 10ec8f0 # 6 instructions; loads
|
||||
spin main 10f1db4 10f1dc8 # 6 instructions; loads
|
||||
spin main 10ff3b4 10ff3c8 # 6 instructions; loads
|
||||
spin main 10ff788 10ff79c # 6 instructions; loads
|
||||
spin main 10ffef0 10fff04 # 6 instructions; loads
|
||||
spin main 1100500 1100514 # 6 instructions; loads
|
||||
spin main 1103e80 1103e94 # 6 instructions; loads
|
||||
spin main 110ca34 110cb68 # 4 instructions; loads
|
||||
spin main 110cab8 110cb4c # 4 instructions; loads
|
||||
spin main 1117420 1117434 # 6 instructions; loads
|
||||
spin main 1117448 111745c # 6 instructions; loads
|
||||
spin main 111bf5c 111bf70 # 6 instructions; loads
|
||||
spin main 111c1bc 111c1d0 # 6 instructions; loads
|
||||
spin main 111c918 111c92c # 6 instructions; loads
|
||||
spin main 112aef0 112af04 # 6 instructions; loads
|
||||
spin main 1143e28 1143e3c # 6 instructions; loads
|
||||
spin main 1145110 1145124 # 6 instructions; loads
|
||||
spin main 1145138 114514c # 6 instructions; loads
|
||||
spin main 1149244 1149258 # 6 instructions; loads
|
||||
spin main 114b868 114b87c # 6 instructions; loads
|
||||
spin main 114f850 114f864 # 6 instructions; loads
|
||||
spin main 114fcdc 114fcf0 # 6 instructions; loads
|
||||
spin main 1150cbc 1150cd0 # 6 instructions; loads
|
||||
spin main 115aa38 115aa4c # 6 instructions; loads
|
||||
spin main 117d7c8 117d7dc # 6 instructions; loads
|
||||
spin main 118a000 118a014 # 6 instructions; loads
|
||||
spin main 118a3d8 118a3ec # 6 instructions; loads
|
||||
spin main 118f65c 118f670 # 6 instructions; loads
|
||||
spin main 1193d30 1193d44 # 6 instructions; loads
|
||||
spin main 1198130 1198144 # 6 instructions; loads
|
||||
spin main 1198158 119816c # 6 instructions; loads
|
||||
spin main 1198e48 1198e5c # 6 instructions; loads
|
||||
spin main 119c414 119c428 # 6 instructions; loads
|
||||
spin main 11a0778 11a078c # 6 instructions; loads
|
||||
spin main 11a0fcc 11a0fe0 # 6 instructions; loads
|
||||
spin main 11a5884 11a5898 # 6 instructions; loads
|
||||
spin main 11bbd80 11bbd94 # 6 instructions; loads
|
||||
spin main 11bd154 11bd168 # 6 instructions; loads
|
||||
spin main 11bd17c 11bd190 # 6 instructions; loads
|
||||
spin main 11e5fc8 11e5fdc # 6 instructions; loads
|
||||
spin main 11e5ff0 11e6004 # 6 instructions; loads
|
||||
spin main 11ea5b8 11ea5cc # 6 instructions; loads
|
||||
spin main 11eccf8 11ecd0c # 6 instructions; loads
|
||||
spin main 11f50d8 11f50ec # 6 instructions; loads
|
||||
spin main 11f5624 11f5638 # 6 instructions; loads
|
||||
spin main 12010e8 12010fc # 6 instructions; loads
|
||||
spin main 120ecc0 120ecd4 # 6 instructions; loads
|
||||
spin main 12171d8 12171ec # 6 instructions; loads
|
||||
spin main 1217cd8 1217cec # 6 instructions; loads
|
||||
spin main 1219124 1219138 # 6 instructions; loads
|
||||
spin main 1231ca8 1231cbc # 6 instructions; loads
|
||||
spin main 1231e58 1231e6c # 6 instructions; loads
|
||||
spin main 1234784 1234798 # 6 instructions; loads
|
||||
spin main 1234d14 1234d28 # 6 instructions; loads
|
||||
spin main 1236974 1236988 # 6 instructions; loads
|
||||
spin main 1236b1c 1236b30 # 6 instructions; loads
|
||||
spin main 12479cc 12479e0 # 6 instructions; loads
|
||||
spin main 1261af8 1261b0c # 6 instructions; loads
|
||||
spin main 1261f20 1261f34 # 6 instructions; loads
|
||||
spin main 1262200 1262214 # 6 instructions; loads
|
||||
spin main 1268448 126845c # 6 instructions; loads
|
||||
spin main 1283a80 1283a94 # 6 instructions; loads
|
||||
spin main 128ba28 128ba3c # 6 instructions; loads
|
||||
spin main 128ba90 128baa4 # 6 instructions; loads
|
||||
spin main 128bc40 128bc54 # 6 instructions; loads
|
||||
spin main 128bc6c 128bc80 # 6 instructions; loads
|
||||
spin main 128bc98 128bcac # 6 instructions; loads
|
||||
spin main 128bcc4 128bcd8 # 6 instructions; loads
|
||||
spin main 128c468 128c47c # 6 instructions; loads
|
||||
spin main 128c490 128c4a4 # 6 instructions; loads
|
||||
spin main 12924ec 1292500 # 6 instructions; loads
|
||||
spin main 1292e4c 1292e60 # 6 instructions; loads
|
||||
spin main 1292eb4 1292ec8 # 6 instructions; loads
|
||||
spin main 129458c 12945a0 # 6 instructions; loads
|
||||
spin main 12966e0 12966f4 # 6 instructions; loads
|
||||
spin main 129e62c 129e640 # 6 instructions; loads
|
||||
spin main 129f954 129f968 # 6 instructions; loads
|
||||
spin main 12a61b0 12a61c4 # 6 instructions; loads
|
||||
spin main 12ab4cc 12ab4e0 # 6 instructions; loads
|
||||
spin main 12ab4f4 12ab508 # 6 instructions; loads
|
||||
spin main 12c1228 12c123c # 6 instructions; loads
|
||||
spin main 12c1250 12c1264 # 6 instructions; loads
|
||||
spin main 12ca068 12ca07c # 6 instructions; loads
|
||||
spin main 12d040c 12d0420 # 6 instructions; loads
|
||||
spin main 12e3820 12e3834 # 6 instructions; loads
|
||||
spin main 12e8e80 12e8e94 # 6 instructions; loads
|
||||
spin main 12e9640 12e9654 # 6 instructions; loads
|
||||
spin main 1309bc8 1309bdc # 6 instructions; loads
|
||||
spin main 1314ed8 1314eec # 6 instructions; loads
|
||||
spin main 131553c 1315550 # 6 instructions; loads
|
||||
spin main 1322ccc 1322dac # 11 instructions; loads
|
||||
spin main 13277f4 1327808 # 6 instructions; loads
|
||||
spin main 1327ab8 1327acc # 6 instructions; loads
|
||||
spin main 132a1e0 132a1f4 # 6 instructions; loads
|
||||
spin main 132b0a8 132b0bc # 6 instructions; loads
|
||||
spin main 132b87c 132b890 # 6 instructions; loads
|
||||
spin main 1332fc0 1332fd4 # 6 instructions; loads
|
||||
spin main 1363450 1363464 # 6 instructions; loads
|
||||
spin main 13feca8 13fecbc # 6 instructions; loads
|
||||
spin main 13fee40 13fee54 # 6 instructions; loads
|
||||
spin main 157c12c 157c140 # 6 instructions; loads
|
||||
spin main 157dfe0 157dff4 # 6 instructions; loads
|
||||
spin main 1585a20 1585a34 # 6 instructions; loads
|
||||
spin main 1585ee0 1585ef4 # 6 instructions; loads
|
||||
spin main 158c4c8 158c4dc # 6 instructions; loads
|
||||
spin main 1599a14 1599a28 # 6 instructions; loads
|
||||
spin main 159aed0 159aee4 # 6 instructions; loads
|
||||
spin main 159aef8 159af0c # 6 instructions; loads
|
||||
spin main 15a2a20 15a2a34 # 6 instructions; loads
|
||||
spin main 15a3228 15a323c # 6 instructions; loads
|
||||
spin main 15a34e8 15a34fc # 6 instructions; loads
|
||||
spin main 15a3510 15a3524 # 6 instructions; loads
|
||||
spin main 15a3f44 15a3f58 # 6 instructions; loads
|
||||
spin main 15ae678 15ae68c # 6 instructions; loads
|
||||
spin main 15b8d78 15b8d8c # 6 instructions; loads
|
||||
spin main 15b9218 15b922c # 6 instructions; loads
|
||||
spin main 15b9320 15b9334 # 6 instructions; loads
|
||||
spin main 15b9638 15b964c # 6 instructions; loads
|
||||
spin main 15c6c9c 15c6cb0 # 6 instructions; loads
|
||||
spin main 15d5380 15d5394 # 6 instructions; loads
|
||||
spin main 15e8a0c 15e8a20 # 6 instructions; loads
|
||||
spin main 15e9ad4 15e9ae8 # 6 instructions; loads
|
||||
spin main 15e9b7c 15e9b90 # 6 instructions; loads
|
||||
spin main 15ef1c4 15ef1d8 # 6 instructions; loads
|
||||
spin main 15ef304 15ef318 # 6 instructions; loads
|
||||
spin main 15ef32c 15ef340 # 6 instructions; loads
|
||||
spin main 1607238 160724c # 6 instructions; loads
|
||||
spin main 160779c 16077b0 # 6 instructions; loads
|
||||
spin main 16173e0 16173f4 # 6 instructions; loads
|
||||
spin main 1623748 162375c # 6 instructions; loads
|
||||
spin main 1627bdc 1627bf0 # 6 instructions; loads
|
||||
spin main 1627c2c 1627c40 # 6 instructions; loads
|
||||
spin main 1628eb8 1628ecc # 6 instructions; loads
|
||||
spin main 16298a8 16298bc # 6 instructions; loads
|
||||
spin main 16340f8 163410c # 6 instructions; loads
|
||||
spin main 1636748 163675c # 6 instructions; loads
|
||||
spin main 1636ed4 1636ee8 # 6 instructions; loads
|
||||
spin main 163b7f0 163b804 # 6 instructions; loads
|
||||
spin main 1648354 1648368 # 6 instructions; loads
|
||||
spin main 1651478 165148c # 6 instructions; loads
|
||||
spin main 1653730 1653744 # 6 instructions; loads
|
||||
spin main 1656c5c 1656c70 # 6 instructions; loads
|
||||
spin main 16596b0 16596b8 # 3 instructions; loads
|
||||
spin main 16596c0 16596c4 # 2 instructions; loads
|
||||
spin main 16596f8 1659700 # 3 instructions; loads
|
||||
spin main 165cb68 165cb7c # 6 instructions; loads
|
||||
spin main 1677368 167737c # 6 instructions; loads
|
||||
spin main 16dc224 16dc238 # 6 instructions; loads
|
||||
spin main 173d61c 173d630 # 6 instructions; loads
|
||||
spin main 1747354 1747368 # 6 instructions; loads
|
||||
spin main 1747918 174792c # 6 instructions; loads
|
||||
spin main 17a0a20 17a0a34 # 6 instructions; loads
|
||||
spin main 1932774 1932788 # 6 instructions; loads
|
||||
spin main 1933ea8 1933ebc # 6 instructions; loads
|
||||
spin main 1962e88 1962e90 # 3 instructions; loads
|
||||
spin main 1962e98 1962e9c # 2 instructions; loads
|
||||
spin main 1962f20 1962f28 # 3 instructions; loads
|
||||
spin main 196300c 1963014 # 3 instructions; loads
|
||||
spin main 196301c 1963020 # 2 instructions; loads
|
||||
spin main 19630c8 19630d0 # 3 instructions; loads
|
||||
spin main 1975850 1975858 # 3 instructions; loads
|
||||
spin main 1975860 1975864 # 2 instructions; loads
|
||||
spin main 1975888 1975890 # 3 instructions; loads
|
||||
spin main 19758c0 19758c8 # 3 instructions; loads
|
||||
spin main 19758d0 19758d4 # 2 instructions; loads
|
||||
spin main 19758fc 1975904 # 3 instructions; loads
|
||||
spin main 19e5048 19e505c # 6 instructions; loads
|
||||
spin main 1a8b8b4 1a8b8c4 # 5 instructions; loads
|
||||
spin main 1b65f9c 1b65fb0 # 6 instructions; loads
|
||||
spin main 1b66058 1b6606c # 6 instructions; loads
|
||||
spin main 1b66114 1b66128 # 6 instructions; loads
|
||||
spin main 1b66714 1b66728 # 6 instructions; loads
|
||||
spin main 1b667d0 1b667e4 # 6 instructions; loads
|
||||
spin main 1b6688c 1b668a0 # 6 instructions; loads
|
||||
spin main 1dff370 1dff37c # 4 instructions; loads
|
||||
spin main 21e6508 21e6530 # 4 instructions; loads
|
||||
spin main 222a3e0 222a3fc # 8 instructions; loads
|
||||
spin main 22322d0 22322d8 # 3 instructions; loads
|
||||
spin main 2232358 2232360 # 3 instructions; loads
|
||||
spin main 22323e8 2232410 # 3 instructions; loads
|
||||
spin main 223931c 223932c # 5 instructions; loads
|
||||
spin main 225dc10 225dc18 # 3 instructions; loads
|
||||
spin main 225dc20 225dc24 # 2 instructions; loads
|
||||
spin main 225dcac 225dcb4 # 3 instructions; loads
|
||||
spin main 225dd38 225dd40 # 3 instructions; loads
|
||||
spin main 225dd48 225dd4c # 2 instructions; loads
|
||||
spin main 225dd64 225dd6c # 3 instructions; loads
|
||||
spin main 22b72dc 22b72f0 # 6 instructions; loads
|
||||
spin main 22b7440 22b7454 # 6 instructions; loads
|
||||
spin main 22c60ac 22c60d0 # 4 instructions; loads
|
||||
spin main 22c619c 22c61a8 # 4 instructions; loads
|
||||
spin main 233ce40 233ce54 # 6 instructions; loads
|
||||
spin main 240ccb0 240ccb8 # 3 instructions; loads
|
||||
spin main 240ccc0 240ccc4 # 2 instructions; loads
|
||||
spin main 240cd40 240cd48 # 3 instructions; loads
|
||||
spin main 240d57c 240d584 # 3 instructions; loads
|
||||
spin main 240d58c 240d590 # 2 instructions; loads
|
||||
spin main 240d5fc 240d604 # 3 instructions; loads
|
||||
spin main 240da9c 240daa4 # 3 instructions; loads
|
||||
spin main 240daac 240dab0 # 2 instructions; loads
|
||||
spin main 240db3c 240db44 # 3 instructions; loads
|
||||
spin main 240dc80 240dc88 # 3 instructions; loads
|
||||
spin main 240dc90 240dc94 # 2 instructions; loads
|
||||
spin main 240dce8 240dcf0 # 3 instructions; loads
|
||||
spin main 240dd34 240dd3c # 3 instructions; loads
|
||||
spin main 240dd44 240dd48 # 2 instructions; loads
|
||||
spin main 240ddfc 240de04 # 3 instructions; loads
|
||||
spin main 240de0c 240de10 # 2 instructions; loads
|
||||
spin main 240de50 240de58 # 3 instructions; loads
|
||||
spin main 240de80 240de88 # 3 instructions; loads
|
||||
spin main 240df74 240df7c # 3 instructions; loads
|
||||
spin main 240df84 240df88 # 2 instructions; loads
|
||||
spin main 240e068 240e070 # 3 instructions; loads
|
||||
spin main 240e0fc 240e104 # 3 instructions; loads
|
||||
spin main 240e10c 240e110 # 2 instructions; loads
|
||||
spin main 240e1c4 240e1cc # 3 instructions; loads
|
||||
spin main 240e1e4 240e1e8 # 2 instructions; loads
|
||||
spin main 240e288 240e290 # 3 instructions; loads
|
||||
spin main 240e2b8 240e2c0 # 3 instructions; loads
|
||||
spin main 240e300 240e308 # 3 instructions; loads
|
||||
spin main 240e310 240e314 # 2 instructions; loads
|
||||
spin main 240e338 240e340 # 3 instructions; loads
|
||||
spin main 240e384 240e38c # 3 instructions; loads
|
||||
spin main 240e394 240e398 # 2 instructions; loads
|
||||
spin main 240e3c0 240e3c8 # 3 instructions; loads
|
||||
spin main 240e91c 240e924 # 3 instructions; loads
|
||||
spin main 240e92c 240e930 # 2 instructions; loads
|
||||
spin main 240e954 240e95c # 3 instructions; loads
|
||||
spin main 240f088 240f090 # 3 instructions; loads
|
||||
spin main 240f098 240f09c # 2 instructions; loads
|
||||
spin main 240f0c0 240f0c8 # 3 instructions; loads
|
||||
spin main 2410984 241098c # 3 instructions; loads
|
||||
spin main 2410994 2410998 # 2 instructions; loads
|
||||
spin main 2410a18 2410a20 # 3 instructions; loads
|
||||
spin main 2410a40 2410a48 # 3 instructions; loads
|
||||
spin main 2410a50 2410a54 # 2 instructions; loads
|
||||
spin main 2410ad4 2410adc # 3 instructions; loads
|
||||
spin main 2410e70 2410e78 # 3 instructions; loads
|
||||
spin main 2410e80 2410e84 # 2 instructions; loads
|
||||
spin main 2410ed4 2410edc # 3 instructions; loads
|
||||
spin main 2410f1c 2410f24 # 3 instructions; loads
|
||||
spin main 2410f2c 2410f30 # 2 instructions; loads
|
||||
spin main 2410fcc 2410fd4 # 3 instructions; loads
|
||||
spin main 241102c 2411034 # 3 instructions; loads
|
||||
spin main 241103c 2411040 # 2 instructions; loads
|
||||
spin main 24110c4 24110cc # 3 instructions; loads
|
||||
spin main 2413d28 2413d30 # 3 instructions; loads
|
||||
spin main 2413d38 2413d3c # 2 instructions; loads
|
||||
spin main 2413dc8 2413dd0 # 3 instructions; loads
|
||||
spin main 2413e04 2413e0c # 3 instructions; loads
|
||||
spin main 2413e14 2413e18 # 2 instructions; loads
|
||||
spin main 2413e84 2413e8c # 3 instructions; loads
|
||||
spin main 2416358 2416360 # 3 instructions; loads
|
||||
spin main 2416368 241636c # 2 instructions; loads
|
||||
spin main 2416388 2416390 # 3 instructions; loads
|
||||
spin main 2462470 2462484 # 6 instructions; loads
|
||||
spin main 2462498 24624ac # 6 instructions; loads
|
||||
spin main 24651f4 2465208 # 6 instructions; loads
|
||||
spin main 2465688 246569c # 6 instructions; loads
|
||||
spin main 2474d20 2474d34 # 6 instructions; loads
|
||||
spin main 2475154 2475168 # 6 instructions; loads
|
||||
spin main 2479134 2479148 # 6 instructions; loads
|
||||
spin main 2479b7c 2479b90 # 6 instructions; loads
|
||||
spin main 247a378 247a38c # 6 instructions; loads
|
||||
spin main 2493050 2493064 # 6 instructions; loads
|
||||
spin main 2886500 2886514 # 6 instructions; loads
|
||||
spin main 28865bc 28865d0 # 6 instructions; loads
|
||||
spin main 2886678 288668c # 6 instructions; loads
|
||||
spin main 2887484 2887498 # 6 instructions; loads
|
||||
spin main 2887540 2887554 # 6 instructions; loads
|
||||
spin main 28875fc 2887610 # 6 instructions; loads
|
||||
spin main 28876b8 28876cc # 6 instructions; loads
|
||||
spin main 2887774 2887788 # 6 instructions; loads
|
||||
spin main 2887830 2887844 # 6 instructions; loads
|
||||
spin main 28878ec 2887900 # 6 instructions; loads
|
||||
spin main 28879a8 28879bc # 6 instructions; loads
|
||||
spin main 2887e70 2887e84 # 6 instructions; loads
|
||||
spin main 2887f2c 2887f40 # 6 instructions; loads
|
||||
spin main 2a3e540 2a3e548 # 3 instructions; loads
|
||||
spin main 2a3e550 2a3e554 # 2 instructions; loads
|
||||
spin main 2a3e58c 2a3e594 # 3 instructions; loads
|
||||
# subsdk0: 8 spin waits
|
||||
spin subsdk0 2090 20a4 # 6 instructions; loads
|
||||
spin subsdk0 278b4 278c8 # 6 instructions; loads
|
||||
spin subsdk0 bcf28 bcfd0 bcfd8 # 14 instructions; loads
|
||||
spin subsdk0 bd0d0 bd178 bd180 # 14 instructions; loads
|
||||
spin subsdk0 c03a0 c03b4 # 6 instructions; loads
|
||||
spin subsdk0 1bbefc 1bbf08 # 4 instructions; loads
|
||||
spin subsdk0 1bd838 1bd84c # 6 instructions; loads
|
||||
spin subsdk0 1c0c68 1c0c74 # 4 instructions; loads
|
||||
# sdk: 5 spin waits
|
||||
spin sdk f5218 f5350 # 14 instructions; loads
|
||||
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
|
||||
@@ -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)
|
||||
@@ -32,6 +33,12 @@ site realtime main 8f2398 1e2c1001 bd400120 # fmov s1, #0.5 -> ldr s0, [x9] (g
|
||||
site realtime main 8f23a8 5e21d800 d503201f # scvtf s0, s0 -> nop
|
||||
site realtime main 8f23ac 1e210800 d503201f # fmul s0, s0, s1 -> nop
|
||||
|
||||
# Equipment-switch cooldown (issue #18, found by JieML1100 on 1.4.3): the player update (main+0x852d24) steps the
|
||||
# countdown at player state +0x109c through the timer step (main+0xec98cc) only while it is above FLT_EPSILON, but the
|
||||
# equipment gate (main+0xcae6f8) blocks weapon, shield and bow changes while it is above 0. A measured ratio can leave
|
||||
# a remainder in (0, FLT_EPSILON] that is never stepped again; stepping while above 0 takes it below 0.
|
||||
site realtime main 852d28 1e270101 1e2703e1 # fmov s1, w8 (FLT_EPSILON) -> fmov s1, wzr
|
||||
|
||||
# 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 (main+0x89822c)
|
||||
@@ -64,6 +71,28 @@ site scene main cc3f68 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
|
||||
site scene main cc3f70 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
|
||||
site scene main cc3f74 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
|
||||
|
||||
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches.c, the 1.2.1 sites of aspect_install;
|
||||
# runtime/resolution.c res_init writes the output's width / height to page +56 and switches the set on). The same four
|
||||
# sites as patches-v143.txt, with the same original words: two camera sites (update and setup) load the camera's copy of
|
||||
# DefaultAspect 1.777778, two viewport routines divide width by height. The replacement loads the ratio from the page
|
||||
# instead (Hor+). x10 is kept on the stack, so no register liveness is assumed.
|
||||
set aspect TOTK_ASPECT_PATCH 0
|
||||
site aspect main cb4b10 bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera update)
|
||||
site aspect main 12521cc bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera setup)
|
||||
site aspect main 19b36f0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
site aspect main 1ecdf68 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
|
||||
# Texture pool offsets past 2 GiB (MacNativeGames ca311e2d). The texture loader keeps a texture's place in its pool as a
|
||||
# 32-bit offset (main+0xad4294: sub x25, x9, x10, stored as the word w25) and widens it as a signed number for
|
||||
# nvnTextureBuilderSetStorage's offset and for SetPackagedTextureData's source (the pool's CPU address + offset). The
|
||||
# shipped heap table keeps every pool far below 2 GiB; a texture pack's heap table with TOTK_APP_POOL raised can make
|
||||
# one larger (MacNativeGames saw a 3.58 GiB pool), and every texture past 2 GiB then got storage 4 GiB below its pool.
|
||||
# The offset is widened unsigned instead, the same value below 2 GiB, right up to 4 GiB. 1.4.3 needs no patch: its
|
||||
# loaders (main+0x583c34, main+0x27ed214) keep the offset in 64 bits.
|
||||
set pooloff TOTK_POOL_OFFSET_PATCH 1
|
||||
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
|
||||
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
|
||||
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
|
||||
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
|
||||
spin main ac600 ac620 ac630 # 13 instructions; loads
|
||||
@@ -930,3 +959,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)
|
||||
@@ -32,6 +34,13 @@ site realtime main 2daa70 1e201820 d503201f # fdiv s0, s1, s0 -> nop
|
||||
site realtime main 2daa74 1e2c1001 d503201f # fmov s1, #0.5 -> nop
|
||||
site realtime main 2daa78 1e210800 d503201f # fmul s0, s0, s1 -> nop
|
||||
|
||||
# Equipment-switch cooldown (issue #18, found by JieML1100): the player update (main+0x2b0368) steps the countdown at
|
||||
# player state +0x109c through the timer step (main+0x1b0834, value += ratio * rate) only while it is above FLT_EPSILON,
|
||||
# but the equipment gate (main+0x31cc7c) blocks weapon, shield and bow changes while it is above 0. Whole 30 Hz steps
|
||||
# reach exactly 0; a measured ratio leaves a rounding remainder, and one in (0, FLT_EPSILON] is never stepped again and
|
||||
# keeps equipment locked until a load. Stepping while above 0 takes the remainder below 0 like any other last step.
|
||||
site realtime main 2b036c 0f016681 0f000401 # movi v1.2s, #0x34, lsl #24 (FLT_EPSILON) -> movi v1.2s, #0
|
||||
|
||||
# Hair and cloth: the actor animation update builds a second clock {seconds = factor / 30, ratio = factor} per
|
||||
# rendered frame. 1.4.3 inlines it in two actor paths, where the factor was just stored to the ratio slot: the
|
||||
# replacement stores measured ratio * factor there and computes measured seconds * factor. Two other callers share the
|
||||
@@ -73,6 +82,27 @@ site scene main 585e50 bd0cc660 2d420460 # str s0, [x19, #0xcc4] -> ldp s0, s
|
||||
site scene main 585e54 1e220140 bd0cc660 # scvtf s0, w10 -> str s0, [x19, #0xcc4]
|
||||
site scene main 585e60 bd0cca60 bd0cca61 # str s0, [x19, #0xcc8] -> str s1, [x19, #0xcc8]
|
||||
|
||||
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches_v143.c and aspect_patch.h
|
||||
# aspect_install / aspect_stub; runtime/resolution.c res_init writes the output's width / height to page +56 and
|
||||
# switches the set on). Each site leaves the ratio sead's perspective projection (+0xb0) takes in s0: two camera sites
|
||||
# (update and setup) load the camera's copy of DefaultAspect 1.777778, two viewport routines divide width by height.
|
||||
# The replacement loads the ratio from the page instead, so a 21:9 output shows more of the world at the sides with
|
||||
# the vertical view kept (Hor+). x10 is kept on the stack, so no register liveness is assumed.
|
||||
set aspect TOTK_ASPECT_PATCH 0
|
||||
site aspect main 628c74 bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera update)
|
||||
site aspect main b43460 bd406ac0 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x22, #0x68] (camera setup)
|
||||
site aspect main 1ff04f4 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
site aspect main 253fbb0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
|
||||
# Field of view (runtime/resolution.c res_fov_live writes TOTK_FOV / 100 to page +60 and switches the set). The same two
|
||||
# camera routines as the aspect set load the camera's vertical view angle (radians) a few words before the ratio and
|
||||
# hand it to sead's perspective projection (+0xa0, then sin / cos / tan of half of it at +0xa4..+0xac). The replacement
|
||||
# loads the angle as before and multiplies it by the factor on the page; s8 holds the factor because the next
|
||||
# instruction (fmul s8, s0, s1) overwrites it. Zooms the game makes itself (the bow, the scope) scale with it.
|
||||
set fov TOTK_FOV_PATCH 0
|
||||
site fov main 628c3c bd413e60 f81f0fea @page:x10 bd413e60 bd403d48 1e280800 f84107ea # ldr s0, [x19, #0x13c] (camera update)
|
||||
site fov main b4342c bd4066c0 f81f0fea @page:x10 bd4066c0 bd403d48 1e280800 f84107ea # ldr s0, [x22, #0x64] (camera setup)
|
||||
|
||||
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
|
||||
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
|
||||
# its wrapper ZSTD_decompress_usingDDict (main+0x3a1e0, which tail-branches to it) goes to a host function on the patch
|
||||
@@ -948,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]
|
||||
+24
-14
@@ -11,13 +11,19 @@ Lifted code cannot be byte-patched when the game runs, so a patch is applied whe
|
||||
<address> is a module offset (hex), <original> the instruction word the module has there (checked; a mismatch stops
|
||||
the generator), each <replacement> an instruction word (hex) or `@page:x<n>`, which is `adrp x<n>, <patch page>`
|
||||
encoded for the site's address. The patch page is the first 4 KiB page after main's image (layout.py leaves the rest
|
||||
of the 2 MiB-aligned module slot free); the runtime commits it and owns its contents.
|
||||
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), +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
|
||||
@@ -42,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)
|
||||
@@ -55,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
|
||||
|
||||
|
||||
@@ -89,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')
|
||||
@@ -114,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,395 @@
|
||||
// Amiibo dump loader and Horizon OS NFP state emulation.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "amiibo.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static PlatMutex g_amiibo_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
// 540-byte raw NTAG215 binary dump
|
||||
static uint8_t g_dump[AMIIBO_DUMP_SIZE];
|
||||
static bool g_has_dump = false;
|
||||
static AmiiboInfo g_info;
|
||||
|
||||
static bool g_device_present = true;
|
||||
static bool g_detecting = false;
|
||||
static bool g_tag_present = false;
|
||||
static bool g_mounted = false;
|
||||
static bool g_auto_remove = true;
|
||||
static float g_auto_remove_delay_s = 2.5f;
|
||||
|
||||
static uint64_t g_tap_timestamp_ns = 0;
|
||||
static bool g_timer_active = false;
|
||||
|
||||
static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
|
||||
if (size != AMIIBO_DUMP_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C). An amiibo's is F1 10 FF EE, not the E1 of a plain
|
||||
// NDEF tag (3dbrew, "Amiibo"); the CC is not encrypted, so encrypted dumps carry it too.
|
||||
static const uint8_t amiibo_cc[4] = {0xF1, 0x10, 0xFF, 0xEE};
|
||||
if (memcmp(data + 0x0C, amiibo_cc, sizeof amiibo_cc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// UID sanity: UID (bytes 0..2 and 4..7) cannot be all zeroes or all 0xFF.
|
||||
if ((data[0] == 0 && data[1] == 0 && data[2] == 0 && data[4] == 0 &&
|
||||
data[5] == 0 && data[6] == 0 && data[7] == 0) ||
|
||||
(data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[4] == 0xFF &&
|
||||
data[5] == 0xFF && data[6] == 0xFF && data[7] == 0xFF)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ISO 14443-3 / NTAG215 Bit Check Character (BCC) verification.
|
||||
// Page 0 byte 3 (BCC0) = 0x88 ^ UID[0] ^ UID[1] ^ UID[2]
|
||||
// Page 2 byte 0 (BCC1) = UID[3] ^ UID[4] ^ UID[5] ^ UID[6]
|
||||
uint8_t bcc0 = (uint8_t)(0x88 ^ data[0] ^ data[1] ^ data[2]);
|
||||
uint8_t bcc1 = (uint8_t)(data[4] ^ data[5] ^ data[6] ^ data[7]);
|
||||
if (data[3] != bcc0 || data[8] != bcc1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// While the game waits for a tag it stops and starts detection again several times a second (tens with the menu open),
|
||||
// so the log names the first few and then a count every 200th start.
|
||||
static uint64_t g_detect_starts;
|
||||
static uint64_t g_detect_stop_ns; // the last stop: the menu holds "searching" across the game's restarts
|
||||
|
||||
void amiibo_start_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_detecting = true;
|
||||
uint64_t n = ++g_detect_starts;
|
||||
if (n <= 3) rt_log("amiibo: start detection");
|
||||
else if (n % 200 == 0) rt_log("amiibo: detection started %llu times so far", (unsigned long long)n);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_stop_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_detecting) g_detect_stop_ns = plat_now_ns();
|
||||
g_detecting = false;
|
||||
if (g_detect_starts <= 3) rt_log("amiibo: stop detection");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_is_detecting(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_detecting;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool amiibo_mount(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_device_present || !g_tag_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return false;
|
||||
}
|
||||
g_mounted = true;
|
||||
rt_log("amiibo: mounted tag '%s'", g_info.filename[0] ? g_info.filename : "unknown");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_unmount(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_mounted = false;
|
||||
rt_log("amiibo: unmounted tag");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_is_mounted(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_mounted;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
AmiiboDeviceState amiibo_get_device_state(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_device_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_UNAVAILABLE;
|
||||
}
|
||||
if (g_mounted) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_TAG_MOUNTED;
|
||||
}
|
||||
if (g_tag_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_TAG_FOUND;
|
||||
}
|
||||
if (g_detecting) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_SEARCHING;
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_INITIALIZED;
|
||||
}
|
||||
|
||||
AmiiboDeviceState amiibo_get_reader_state(void) {
|
||||
AmiiboDeviceState state = amiibo_get_device_state();
|
||||
if (state != AMIIBO_DEV_STATE_INITIALIZED) return state;
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool recent = g_detect_stop_ns && plat_now_ns() - g_detect_stop_ns < 1000000000ull;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return recent ? AMIIBO_DEV_STATE_SEARCHING : state;
|
||||
}
|
||||
|
||||
bool amiibo_load_file(const char *path) {
|
||||
if (!path || !*path) return false;
|
||||
uint64_t size = 0;
|
||||
uint8_t *data = plat_file_read(path, &size);
|
||||
if (!data) {
|
||||
rt_log("amiibo: failed to read file: %s", path);
|
||||
return false;
|
||||
}
|
||||
if (size != AMIIBO_DUMP_SIZE) {
|
||||
rt_log("amiibo: invalid dump size (%llu bytes, expected exactly %d): %s",
|
||||
(unsigned long long)size, AMIIBO_DUMP_SIZE, path);
|
||||
free(data);
|
||||
return false;
|
||||
}
|
||||
if (!amiibo_validate_dump(data, size)) {
|
||||
rt_log("amiibo: dump failed NTAG215 structure sanity checks: %s", path);
|
||||
free(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
memcpy(g_dump, data, AMIIBO_DUMP_SIZE);
|
||||
g_has_dump = true;
|
||||
parse_dump_info(path);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
|
||||
free(data);
|
||||
rt_log("amiibo: loaded dump '%s' (char_id: %02x%02x%02x, series: %02x, num: %04x)",
|
||||
g_info.filename, g_info.character_id[0], g_info.character_id[1], g_info.character_id[2],
|
||||
g_info.series_id, g_info.numbering_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_tap_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_has_dump) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return;
|
||||
}
|
||||
g_tag_present = true;
|
||||
g_tap_timestamp_ns = plat_now_ns();
|
||||
if (g_auto_remove) {
|
||||
g_timer_active = true;
|
||||
}
|
||||
rt_log("amiibo: tag tapped on reader ('%s')", g_info.filename);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_scan_and_tap(const char *path) {
|
||||
if (!amiibo_load_file(path)) return false;
|
||||
amiibo_tap_tag();
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_remove_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_tag_present || g_mounted) {
|
||||
g_tag_present = false;
|
||||
g_mounted = false;
|
||||
g_timer_active = false;
|
||||
rt_log("amiibo: tag removed from reader");
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_tick(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_timer_active && g_tag_present) {
|
||||
uint64_t now = plat_now_ns();
|
||||
uint64_t diff = now > g_tap_timestamp_ns ? now - g_tap_timestamp_ns : 0;
|
||||
uint64_t limit_ns = (uint64_t)(g_auto_remove_delay_s * 1000000000.0f);
|
||||
if (diff >= limit_ns) {
|
||||
g_tag_present = false;
|
||||
g_mounted = false;
|
||||
g_timer_active = false;
|
||||
rt_log("amiibo: auto-removed tag after %.1fs timeout", (double)g_auto_remove_delay_s);
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_set_auto_remove(bool enable) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_auto_remove = enable;
|
||||
if (!enable) g_timer_active = false;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_get_auto_remove(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_auto_remove;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
void amiibo_set_auto_remove_delay_s(float seconds) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (seconds >= 0.5f && seconds <= 10.0f) g_auto_remove_delay_s = seconds;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
float amiibo_get_auto_remove_delay_s(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
float res = g_auto_remove_delay_s;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool amiibo_has_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_has_dump && g_tag_present;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
const AmiiboInfo *amiibo_get_current_info(void) {
|
||||
return &g_info;
|
||||
}
|
||||
|
||||
const uint8_t *amiibo_get_raw_data(void) {
|
||||
return g_dump;
|
||||
}
|
||||
|
||||
void amiibo_get_tag_info(void *out_tag_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_tag_info;
|
||||
memset(out, 0, 0x58);
|
||||
// UUID: offset 0x00, 10 bytes max
|
||||
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,87 @@
|
||||
// Amiibo loader and state management subsystem (540-byte NTAG215 binary dumps).
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define AMIIBO_DUMP_SIZE 540
|
||||
|
||||
// Device state corresponding to nn::nfp / nn::nfc DeviceState
|
||||
typedef enum {
|
||||
AMIIBO_DEV_STATE_INITIALIZED = 0,
|
||||
AMIIBO_DEV_STATE_SEARCHING = 1,
|
||||
AMIIBO_DEV_STATE_TAG_FOUND = 2,
|
||||
AMIIBO_DEV_STATE_TAG_REMOVED = 3,
|
||||
AMIIBO_DEV_STATE_TAG_MOUNTED = 4,
|
||||
AMIIBO_DEV_STATE_UNAVAILABLE = 5
|
||||
} AmiiboDeviceState;
|
||||
|
||||
// Amiibo identification info
|
||||
typedef struct {
|
||||
uint8_t uuid[10];
|
||||
uint8_t uuid_len;
|
||||
uint8_t character_id[3];
|
||||
uint8_t series_id;
|
||||
uint16_t numbering_id;
|
||||
uint8_t nfp_type;
|
||||
uint8_t version;
|
||||
char path[512];
|
||||
char filename[128];
|
||||
} AmiiboInfo;
|
||||
|
||||
// Subsystem lifecycle
|
||||
void amiibo_init(void);
|
||||
|
||||
// Hardware / device emulation status
|
||||
void amiibo_set_device_present(bool present);
|
||||
bool amiibo_is_device_present(void);
|
||||
|
||||
// Tag detection & lifecycle
|
||||
void amiibo_start_detection(void);
|
||||
void amiibo_stop_detection(void);
|
||||
bool amiibo_is_detecting(void);
|
||||
|
||||
// Tag mounting
|
||||
bool amiibo_mount(void);
|
||||
void amiibo_unmount(void);
|
||||
bool amiibo_is_mounted(void);
|
||||
|
||||
// State query
|
||||
AmiiboDeviceState amiibo_get_device_state(void);
|
||||
// The state the menu shows: searching also within a second after a stop, since the game restarts detection many times
|
||||
// a second while it waits for a tag. The game reads amiibo_get_device_state.
|
||||
AmiiboDeviceState amiibo_get_reader_state(void);
|
||||
|
||||
// File loading & triggering actions
|
||||
bool amiibo_load_file(const char *path);
|
||||
bool amiibo_scan_and_tap(const char *path);
|
||||
void amiibo_tap_tag(void);
|
||||
void amiibo_remove_tag(void);
|
||||
|
||||
// Timer tick called during menu / frame updates
|
||||
void amiibo_tick(void);
|
||||
|
||||
// Options
|
||||
void amiibo_set_auto_remove(bool enable);
|
||||
bool amiibo_get_auto_remove(void);
|
||||
void amiibo_set_auto_remove_delay_s(float seconds);
|
||||
float amiibo_get_auto_remove_delay_s(void);
|
||||
|
||||
// Tag inspection
|
||||
bool amiibo_has_tag(void);
|
||||
const AmiiboInfo *amiibo_get_current_info(void);
|
||||
const uint8_t *amiibo_get_raw_data(void);
|
||||
|
||||
// Horizon OS buffers extraction (called by NFP IPC/override services)
|
||||
void amiibo_get_tag_info(void *out_tag_info);
|
||||
void amiibo_get_model_info(void *out_model_info);
|
||||
void amiibo_get_common_info(void *out_common_info);
|
||||
void amiibo_get_register_info(void *out_register_info);
|
||||
int amiibo_get_application_area(void *out_buf, uint32_t max_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+149
-13
@@ -3,13 +3,15 @@
|
||||
// HDR void extents, random blocks kept by what they exercise (1-4 partitions, dual plane with 1-3 partitions, every
|
||||
// weight grid and quantization the random modes reach), then plain random blocks (mostly error blocks, HDR endpoint
|
||||
// modes among them). Each case decodes two regions into two mip levels of an RGBA8 image with cropped edges, and
|
||||
// compares both the decoder's texels (readback) and what the image holds afterwards. Also times one 2048x2048 texture
|
||||
// on the CPU (astc_decode_ldr_mt) and on the GPU (wall time of the upload batch).
|
||||
// compares both the decoder's texels (readback) and what the image holds afterwards. The same regions also go through
|
||||
// the BC3 encoder (TOTK_ASTC_BC) into a BC3 image, whose blocks are decoded here and held to error bounds (bc_check).
|
||||
// Also times one 2048x2048 texture on the CPU (astc_decode_ldr_mt) and on the GPU (wall time of the upload batch).
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "platform/platform.h"
|
||||
#include "host.h"
|
||||
#include "tex.h"
|
||||
#include "vk.h"
|
||||
@@ -26,7 +28,8 @@ static const uint8_t dims[14][2] = {{4, 4}, {5, 4}, {5, 5}, {6, 5}, {6, 6}, {8,
|
||||
// Buckets of valid blocks: partitions 1-4 single plane, then dual plane with 1-3 partitions.
|
||||
enum { NB = 7, PER_BUCKET = 48 };
|
||||
static const char *const bucket_name[NB] = {"1p", "2p", "3p", "4p", "dual 1p", "dual 2p", "dual 3p"};
|
||||
static uint64_t g_bucket[NB], g_void, g_err, g_blocks, g_texels;
|
||||
static uint64_t g_bucket[NB], g_void, g_err, g_blocks, g_texels, g_bc_cases;
|
||||
static double g_bc_db = 99; // the lowest BC3 PSNR of all cases
|
||||
|
||||
static int dual_bit(uint32_t m) {
|
||||
if (!(m & 3) && (m >> 7 & 3) == 2) return 0; // the 6..10 x 6..10 modes have no dual plane
|
||||
@@ -77,9 +80,9 @@ typedef struct {
|
||||
VmaAllocation alloc;
|
||||
} Img;
|
||||
|
||||
static int make_image(VkCtx *c, uint32_t w, uint32_t h, uint32_t levels, int srgb, Img *o) {
|
||||
static int make_image(VkCtx *c, uint32_t w, uint32_t h, uint32_t levels, VkFormat format, Img *o) {
|
||||
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, .extent = {w, h, 1},
|
||||
.format = format, .extent = {w, h, 1},
|
||||
.mipLevels = levels, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||
@@ -109,18 +112,14 @@ static void to_general(VkCommandBuffer cb, VkImage img) {
|
||||
vkCmdPipelineBarrier2(cb, &di);
|
||||
}
|
||||
|
||||
static double now_ms(void) {
|
||||
LARGE_INTEGER t, f;
|
||||
QueryPerformanceCounter(&t), QueryPerformanceFrequency(&f);
|
||||
return (double)t.QuadPart * 1e3 / (double)f.QuadPart;
|
||||
}
|
||||
static double now_ms(void) { return (double)plat_now_ns() / 1e6; }
|
||||
|
||||
// Decodes regions (w0 x h0 at level 0, w1 x h1 at level 1) of blk on the GPU; out_rb: the decoder's texels, out_img:
|
||||
// the image's two levels read back. 0 on a Vulkan failure.
|
||||
static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32_t w0, uint32_t h0, uint32_t w1,
|
||||
uint32_t h1, uint32_t bw, uint32_t bh, int srgb, uint8_t *out_rb, uint8_t *out_img, double *ms) {
|
||||
Img im;
|
||||
if (!make_image(c, w0, h0, 2, srgb, &im)) return 0;
|
||||
if (!make_image(c, w0, h0, 2, srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, &im)) return 0;
|
||||
size_t t0 = (size_t)w0 * h0 * 4, t1 = (size_t)w1 * h1 * 4;
|
||||
VkCommandBuffer cb = vk_upload_begin();
|
||||
VkBuffer sb, rb, ib;
|
||||
@@ -132,7 +131,7 @@ static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32
|
||||
VkAstcRegion ar[2] = {{.src = 0, .w = w0, .h = h0, .slices = 1, .level = 0},
|
||||
{.src = n0 * 16, .w = w1, .h = h1, .slices = 1, .level = 1}};
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, im.img, VK_IMAGE_ASPECT_COLOR_BIT, rb);
|
||||
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, 0, im.img, VK_IMAGE_ASPECT_COLOR_BIT, rb);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
|
||||
VkBufferImageCopy cp[2] = {{.bufferOffset = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w0, h0, 1}},
|
||||
{.bufferOffset = t0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 1, 0, 1}, .imageExtent = {w1, h1, 1}}};
|
||||
@@ -152,6 +151,123 @@ static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The same two regions decoded and encoded to BC3 (TOTK_ASTC_BC) into a BC3 image; out: its blocks read back, level 0
|
||||
// then level 1, rows of 4x4 blocks. 0 on a Vulkan failure or when nothing was recorded.
|
||||
static int gpu_bc(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32_t w0, uint32_t h0, uint32_t w1, uint32_t h1,
|
||||
uint32_t bw, uint32_t bh, int srgb, uint8_t *out) {
|
||||
Img im;
|
||||
if (!make_image(c, w0, h0, 2, srgb ? VK_FORMAT_BC3_SRGB_BLOCK : VK_FORMAT_BC3_UNORM_BLOCK, &im)) return 0;
|
||||
size_t b0 = (size_t)((w0 + 3) / 4) * ((h0 + 3) / 4) * 16, b1 = (size_t)((w1 + 3) / 4) * ((h1 + 3) / 4) * 16;
|
||||
VkCommandBuffer cb = vk_upload_begin();
|
||||
VkBuffer sb, ib;
|
||||
VmaAllocation ia;
|
||||
uint8_t *stg = vk_staging((n0 + n1) * 16, &sb, NULL), *imap = vk_staging(b0 + b1, &ib, &ia);
|
||||
memcpy(stg, blk, (n0 + n1) * 16);
|
||||
to_general(cb, im.img);
|
||||
VkAstcRegion ar[2] = {{.src = 0, .w = w0, .h = h0, .slices = 1, .level = 0},
|
||||
{.src = n0 * 16, .w = w1, .h = h1, .slices = 1, .level = 1}};
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, 1, im.img, VK_IMAGE_ASPECT_COLOR_BIT, VK_NULL_HANDLE);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
|
||||
VkBufferImageCopy cp[2] = {{.bufferOffset = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w0, h0, 1}},
|
||||
{.bufferOffset = b0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 1, 0, 1}, .imageExtent = {w1, h1, 1}}};
|
||||
vkCmdCopyImageToBuffer(cb, im.img, VK_IMAGE_LAYOUT_GENERAL, ib, 2, cp);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT);
|
||||
vk_upload_end();
|
||||
vk_upload_flush(1);
|
||||
vmaInvalidateAllocation(c->vma, ia, 0, VK_WHOLE_SIZE);
|
||||
memcpy(out, imap, b0 + b1);
|
||||
vmaDestroyBuffer(c->vma, ib, ia);
|
||||
vk_defer_image(im.img, im.alloc);
|
||||
vk_upload_flush(1);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// One BC3 block to 16 RGBA8 texels, as the format defines it (565 endpoints widened by bit replication; the colour
|
||||
// block always in four-colour mode). pos: each texel's place on the line from endpoint 0 (0) to endpoint 1 (3).
|
||||
static void bc3_decode(const uint8_t b[16], uint8_t t[16][4], uint8_t ep[2][3], int pos[16]) {
|
||||
uint8_t a[8] = {b[0], b[1]}, p[4][3];
|
||||
if (a[0] > a[1])
|
||||
for (int i = 1; i < 7; i++) a[i + 1] = (uint8_t)(((7 - i) * a[0] + i * a[1] + 3) / 7);
|
||||
else {
|
||||
for (int i = 1; i < 5; i++) a[i + 1] = (uint8_t)(((5 - i) * a[0] + i * a[1] + 2) / 5);
|
||||
a[6] = 0, a[7] = 255;
|
||||
}
|
||||
uint64_t ai = 0;
|
||||
for (int i = 0; i < 6; i++) ai |= (uint64_t)b[2 + i] << 8 * i;
|
||||
for (int k = 0; k < 2; k++) {
|
||||
unsigned v = b[8 + 2 * k] | b[9 + 2 * k] << 8, r = v >> 11, g = v >> 5 & 63, bl = v & 31;
|
||||
p[k][0] = (uint8_t)(r << 3 | r >> 2), p[k][1] = (uint8_t)(g << 2 | g >> 4), p[k][2] = (uint8_t)(bl << 3 | bl >> 2);
|
||||
}
|
||||
for (int ch = 0; ch < 3; ch++)
|
||||
p[2][ch] = (uint8_t)((2 * p[0][ch] + p[1][ch] + 1) / 3), p[3][ch] = (uint8_t)((p[0][ch] + 2 * p[1][ch] + 1) / 3);
|
||||
uint32_t ci = b[12] | b[13] << 8 | b[14] << 16 | (uint32_t)b[15] << 24;
|
||||
static const int code_pos[4] = {0, 3, 1, 2};
|
||||
for (int i = 0; i < 16; i++) {
|
||||
memcpy(t[i], p[ci >> 2 * i & 3], 3);
|
||||
t[i][3] = a[ai >> 3 * i & 7];
|
||||
pos[i] = code_pos[ci >> 2 * i & 3];
|
||||
}
|
||||
memcpy(ep, p, 6);
|
||||
}
|
||||
|
||||
// BC3 is lossy, so these are bounds, not bit-exactness. In every block: alpha within a fourteenth of the block's alpha
|
||||
// range (the eight-value mode's half step); each texel on the palette entry nearest its projection onto the endpoint
|
||||
// line (within rounding, when the endpoints are apart); a block of one colour within the 565 rounding, alpha exact.
|
||||
// Per level, a PSNR floor against astc_decode_ldr for gross damage: the random blocks are noise, so it is low (19 dB
|
||||
// at worst for 5x5 level 1 on an RX 9070). *db: the level's PSNR.
|
||||
enum { BC_MIN_DB = 15 };
|
||||
static int bc_check(const uint8_t *want, const uint8_t *blocks, uint32_t w, uint32_t h, const char *what, double *db) {
|
||||
uint32_t bx = (w + 3) / 4, by = (h + 3) / 4;
|
||||
double se = 0;
|
||||
for (uint32_t y4 = 0; y4 < by; y4++)
|
||||
for (uint32_t x4 = 0; x4 < bx; x4++) {
|
||||
uint8_t t[16][4], ep[2][3], lo[4] = {255, 255, 255, 255}, hi[4] = {0};
|
||||
int pos[16];
|
||||
bc3_decode(blocks + 16 * ((size_t)y4 * bx + x4), t, ep, pos);
|
||||
double axis[3], len2 = 0;
|
||||
for (int ch = 0; ch < 3; ch++) axis[ch] = (double)ep[1][ch] - ep[0][ch], len2 += axis[ch] * axis[ch];
|
||||
for (int pass = 0; pass < 2; pass++)
|
||||
for (int i = 0; i < 16; i++) {
|
||||
uint32_t x = x4 * 4 + (i & 3), y = y4 * 4 + (i >> 2);
|
||||
if (x >= w || y >= h) continue;
|
||||
const uint8_t *s = want + 4 * ((size_t)y * w + x);
|
||||
if (!pass) {
|
||||
for (int ch = 0; ch < 4; ch++) lo[ch] = s[ch] < lo[ch] ? s[ch] : lo[ch], hi[ch] = s[ch] > hi[ch] ? s[ch] : hi[ch];
|
||||
continue;
|
||||
}
|
||||
int flat = !memcmp(lo, hi, 4), lim[4] = {5, 3, 5, 0};
|
||||
if (len2 >= 64) { // shorter lines: rounding alone can move a texel to the next entry
|
||||
double proj = 0;
|
||||
for (int ch = 0; ch < 3; ch++) proj += ((double)s[ch] - ep[0][ch]) * axis[ch];
|
||||
proj = proj / len2 * 3;
|
||||
proj = proj < 0 ? 0 : proj > 3 ? 3 : proj;
|
||||
if (fabs(pos[i] - proj) > 0.75) { // a wrong code mapping is a whole place off
|
||||
printf("probe-astc: FAIL BC3 %s: texel (%u,%u) on palette place %d, its projection is %.2f\n",
|
||||
what, x, y, pos[i], proj);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
for (int ch = 0; ch < 4; ch++) {
|
||||
int d = abs((int)t[i][ch] - s[ch]);
|
||||
se += (double)d * d;
|
||||
if ((ch == 3 && d > (hi[3] - lo[3]) / 14 + 1) || (flat && d > lim[ch])) {
|
||||
printf("probe-astc: FAIL BC3 %s: texel (%u,%u) channel %d: astc %02x%02x%02x%02x bc3 %02x%02x%02x%02x\n",
|
||||
what, x, y, ch, s[0], s[1], s[2], s[3], t[i][0], t[i][1], t[i][2], t[i][3]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
double mse = se / ((double)w * h * 4);
|
||||
*db = mse > 0 ? 10 * log10(255.0 * 255.0 / mse) : 99;
|
||||
if (*db < BC_MIN_DB) {
|
||||
printf("probe-astc: FAIL BC3 %s: PSNR %.1f dB, below %d\n", what, *db, BC_MIN_DB);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int first_diff(const uint8_t *a, const uint8_t *b, size_t texels, size_t *at) {
|
||||
for (size_t i = 0; i < texels; i++)
|
||||
if (memcmp(a + 4 * i, b + 4 * i, 4)) { *at = i; return 1; }
|
||||
@@ -189,6 +305,22 @@ static int test_case(VkCtx *c, uint32_t bw, uint32_t bh, int srgb) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bad && vk_astc_bc()) {
|
||||
size_t b0 = (size_t)((w0 + 3) / 4) * ((h0 + 3) / 4) * 16, b1 = (size_t)((w1 + 3) / 4) * ((h1 + 3) / 4) * 16;
|
||||
uint8_t *bc = malloc(b0 + b1);
|
||||
char what[2][48];
|
||||
double db[2] = {0, 0};
|
||||
for (int lv = 0; lv < 2; lv++) snprintf(what[lv], sizeof what[lv], "%ux%u %s level %d", bw, bh, srgb ? "sRGB" : "linear", lv);
|
||||
if (!gpu_bc(c, blk, n0, n1, w0, h0, w1, h1, bw, bh, srgb, bc)) {
|
||||
printf("probe-astc: FAIL BC3 %s: the encode was not recorded\n", what[0]);
|
||||
bad = 1;
|
||||
}
|
||||
bad = bad || bc_check(want, bc, w0, h0, what[0], &db[0]) || bc_check(want + t0 * 4, bc + b0, w1, h1, what[1], &db[1]);
|
||||
for (int lv = 0; lv < 2 && !bad; lv++)
|
||||
if (db[lv] < g_bc_db) g_bc_db = db[lv];
|
||||
g_bc_cases += !bad;
|
||||
free(bc);
|
||||
}
|
||||
g_blocks += n0 + n1, g_texels += t0 + t1;
|
||||
free(blk), free(want), free(rb), free(im);
|
||||
return bad;
|
||||
@@ -223,6 +355,7 @@ static void timing(VkCtx *c, uint32_t bw, uint32_t bh) {
|
||||
}
|
||||
|
||||
int probe_astc(void) {
|
||||
plat_setenv("TOTK_ASTC_BC", "1"); // also builds the BC3 encoder; the RGBA8 cases pass bc 0 and do not use it
|
||||
if (!vk_init()) { printf("probe-astc: FAIL vk_init\n"); return 1; }
|
||||
VkCtx *c = vk_ctx();
|
||||
printf("probe-astc: GPU %s, textureCompressionASTC_LDR %d, mode %d\n", c->props.deviceName, c->has_astc_ldr,
|
||||
@@ -236,6 +369,9 @@ int probe_astc(void) {
|
||||
for (int s = 0; s < 2 && !bad; s++) bad = test_case(c, dims[i][0], dims[i][1], s);
|
||||
for (int b = 0; b < NB && !bad; b++)
|
||||
if (!g_bucket[b]) { printf("probe-astc: FAIL no valid %s block was generated\n", bucket_name[b]); bad = 1; }
|
||||
if (!bad && !vk_astc_bc()) printf("probe-astc: BC3 encoder not available on this device: not checked\n");
|
||||
else if (!bad) printf("probe-astc: BC3 encode: %llu cases within the bounds, lowest PSNR %.1f dB (floor %d)\n",
|
||||
(unsigned long long)g_bc_cases, g_bc_db, BC_MIN_DB);
|
||||
if (!bad) {
|
||||
timing(c, 4, 4);
|
||||
timing(c, 6, 6);
|
||||
|
||||
@@ -22,8 +22,11 @@ struct AudioEffectDsp {
|
||||
};
|
||||
static int channel_ok(unsigned n) { return n==1 || n==2 || n==4 || n==6; }
|
||||
static unsigned sample_count(unsigned rate, float ms) { return (unsigned)nearbyintf(rate*ms); }
|
||||
/* The reference DelayLine wraps at CurrentSampleCount-1, including when its
|
||||
backing allocation is larger. Preserve this timing for sample comparisons. */
|
||||
/* The ring has `count` slots. The reference DelayLine wraps its cursor at
|
||||
CurrentSampleCount-1 while its taps index modulo CurrentSampleCount, so once
|
||||
per turn every tap reads a slot that is never written (a one-sample dropout
|
||||
per tap, audible as grain). The cursor and the taps share one ring length here;
|
||||
each delay is one sample longer than the reference. */
|
||||
static void line_set(DelayLine *d, unsigned n) { d->count=n<d->max?n:d->max; d->at=0; }
|
||||
static void line_init(DelayLine *d, unsigned rate, float ms) {
|
||||
d->max=sample_count(rate,ms); d->data=calloc((size_t)d->max+1,sizeof(float));
|
||||
@@ -32,12 +35,12 @@ static void line_init(DelayLine *d, unsigned rate, float ms) {
|
||||
static float line_read(const DelayLine *d) { return d->data[d->at]; }
|
||||
static float line_update(DelayLine *d,float v) {
|
||||
float old=line_read(d);d->data[d->at++]=v;
|
||||
if(d->at >= (d->count?d->count-1:0))d->at=0;
|
||||
if(d->at >= d->count)d->at=0;
|
||||
return old;
|
||||
}
|
||||
static float line_tap(const DelayLine *d,unsigned sample,int offset) {
|
||||
int at=(int)d->at-(int)sample-offset;
|
||||
if(at<0)at+=(int)d->count;
|
||||
if(d->count){at%=(int)d->count;if(at<0)at+=(int)d->count;}
|
||||
if(at<0 || (unsigned)at>d->max)rt_fatal("audio effect delay tap bounds");
|
||||
return d->data[at];
|
||||
}
|
||||
|
||||
+111
-13
@@ -8,7 +8,14 @@
|
||||
// into its destination mix, folds the submixes, runs the mix effects and writes the device sink's stereo mix into a
|
||||
// single-producer single-consumer ring. The SDL audio thread drains that ring without taking any lock: an empty ring
|
||||
// plays silence and never blocks the render thread or the guest. TOTK_AUDIO=none (or TOTK_AUDIO_OFF) mixes without a
|
||||
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20) is the queued audio the output waits for after start or underrun.
|
||||
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20, 60 on Linux) is the queued audio the output waits for after start or underrun.
|
||||
// The device's clock paces the render thread, as the audio hardware paces the Switch's DSP (audio_mixer_pace_ns): a
|
||||
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
|
||||
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart. LASRecomp and
|
||||
// SavageWindRecomp pace their renderers by the SDL stream the same way. Without a device, while the renderer is
|
||||
// stopped and while the device takes nothing (a stall), the render thread keeps real time on its own timer; what piled
|
||||
// up meanwhile is skipped on the SDL thread once the device resumes (more than QUEUE_SLACK_MS above the target plus the
|
||||
// device's request), as MacNativeGames' audio_mixer.c trims its ring after a device stall.
|
||||
#include <math.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
@@ -26,6 +33,8 @@
|
||||
|
||||
#define MAX_FRAME 1024
|
||||
#define RING_FRAMES 16384 // stereo frames, about 340 ms at 48 kHz
|
||||
#define QUEUE_SLACK_MS 40 // queued audio above the target plus the device's request that ring_pull skips
|
||||
#define STALL_MS 50 // the device took nothing this long (at least 4 periods): the timer paces
|
||||
#define SPLITTER_UNUSED 0xffffffffu
|
||||
#define dm_guest_write_prepare(p, n) ((void)(p), (void)(n)) // guest memory is plain host memory on Windows
|
||||
|
||||
@@ -43,8 +52,11 @@ typedef struct Mixer {
|
||||
SplitInfo *split; int nsplit; SplitDest *dest; int ndest;
|
||||
SDL_AudioStream *stream; int device_ok, rate;
|
||||
float ring[RING_FRAMES * 2];
|
||||
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero;
|
||||
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero, skipped_frames;
|
||||
_Atomic int playing; int ready; // ready: the callback's own state
|
||||
int target, period; // output target and device period, frames (output_open)
|
||||
uint64_t stall_ns;
|
||||
_Atomic uint64_t pulled_ns; // kern_now_ns of the device's last request, 0 before the first
|
||||
_Atomic uint64_t flush_until; _Atomic unsigned stop_epoch; unsigned callback_epoch;
|
||||
EffectState *effect; int *effect_order; unsigned effect_reported;
|
||||
uint64_t *sink_address; uint32_t *sink_size, *sink_offset, *sink_old, *sink_written;
|
||||
@@ -78,8 +90,15 @@ static void report_once(Mixer *m, unsigned bit, const char *msg, int value) {
|
||||
// ---- output: SDL3 audio stream fed from the ring
|
||||
// How much audio the output waits for before it starts, and again after an underrun: TOTK_AUDIO_PREFILL_MS rounded up
|
||||
// to whole 5 ms render blocks. Returns -1 for a value outside 0..500 ms.
|
||||
// The Linux default is higher: at 20 ms PipeWire's queue (max(prefill, two 1024-sample periods)) underran
|
||||
// thousands of times within minutes in busy scenes, and 60 ms removed the underruns.
|
||||
#ifdef __linux__
|
||||
#define PREFILL_DEFAULT_MS 60
|
||||
#else
|
||||
#define PREFILL_DEFAULT_MS 20
|
||||
#endif
|
||||
static int prefill_blocks_for(const char *e) {
|
||||
if (!e || !*e) return 4;
|
||||
if (!e || !*e) return PREFILL_DEFAULT_MS / 5;
|
||||
char *end;
|
||||
double ms = strtod(e, &end);
|
||||
if (end == e || *end || !(ms >= 0) || ms > 500) return -1;
|
||||
@@ -107,16 +126,23 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
|
||||
atomic_store_explicit(&m->rd, wr, memory_order_release);
|
||||
return;
|
||||
}
|
||||
uint64_t prefill = (uint64_t)m->frame * (uint64_t)m->prefill;
|
||||
if (prefill < frames) prefill = frames;
|
||||
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
|
||||
if (!m->ready) {
|
||||
uint64_t prefill = (uint64_t)m->frame * (uint64_t)m->prefill;
|
||||
if (prefill < frames) prefill = frames;
|
||||
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
|
||||
if (have < prefill) {
|
||||
memset(out, 0, (size_t)frames * 2 * sizeof(float));
|
||||
return;
|
||||
}
|
||||
m->ready = 1;
|
||||
}
|
||||
uint64_t keep = (prefill > (uint64_t)m->target ? prefill : (uint64_t)m->target) + frames;
|
||||
uint64_t slack = (uint64_t)m->rate * QUEUE_SLACK_MS / 1000;
|
||||
if (have > keep + slack) { // piled up while the device stalled: skip the oldest, keep target + request
|
||||
atomic_fetch_add(&m->skipped_frames, have - keep);
|
||||
rd = wr - keep;
|
||||
have = keep;
|
||||
}
|
||||
uint32_t n = frames < have ? frames : (uint32_t)have;
|
||||
uint64_t nonzero = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
@@ -139,6 +165,7 @@ static void SDLCALL sdl_pull(void *user, SDL_AudioStream *stream, int additional
|
||||
(void)total;
|
||||
Mixer *m = user;
|
||||
float tmp[2 * 1024];
|
||||
atomic_store_explicit(&m->pulled_ns, kern_now_ns(), memory_order_release);
|
||||
while (additional > 0) {
|
||||
uint32_t frames = (uint32_t)additional / (2 * sizeof(float));
|
||||
if (!frames) break;
|
||||
@@ -172,8 +199,28 @@ static void output_open(Mixer *m, int rate) {
|
||||
return;
|
||||
}
|
||||
m->device_ok = 1;
|
||||
SDL_AudioSpec dev;
|
||||
int period = 0;
|
||||
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(m->stream), &dev, &period) || period <= 0) period = m->frame;
|
||||
m->period = period;
|
||||
m->target = m->frame * m->prefill > 2 * period ? m->frame * m->prefill : 2 * period;
|
||||
if (m->target > RING_FRAMES / 2) m->target = RING_FRAMES / 2;
|
||||
uint64_t period_ns = (uint64_t)period * 1000000000ull / (uint64_t)rate;
|
||||
m->stall_ns = 4 * period_ns > STALL_MS * 1000000ull ? 4 * period_ns : STALL_MS * 1000000ull;
|
||||
const char *name = SDL_GetAudioDeviceName(SDL_GetAudioStreamDevice(m->stream));
|
||||
rt_log("audio: SDL device '%s' opened, %d Hz stereo float, prefill %d ms", name ? name : "?", rate, m->prefill * 5);
|
||||
rt_log("audio: SDL device '%s' opened, %d Hz stereo float, prefill %d ms, device period %d frames; the device paces "
|
||||
"rendering at %.1f ms queued", name ? name : "?", rate, m->prefill * 5, period, m->target * 1000.0 / rate);
|
||||
}
|
||||
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
|
||||
Mixer *m = r->mixer;
|
||||
if (!m || !m->device_ok || !atomic_load_explicit(&m->playing, memory_order_acquire)) return -1;
|
||||
uint64_t pulled = atomic_load_explicit(&m->pulled_ns, memory_order_acquire);
|
||||
if (!pulled || (now > pulled && now - pulled > m->stall_ns)) return -1;
|
||||
uint64_t have = atomic_load(&m->wr) - atomic_load_explicit(&m->rd, memory_order_acquire);
|
||||
if (have < (uint64_t)m->target) return 0;
|
||||
int64_t ns = (int64_t)((have - (uint64_t)m->target + 1) * 1000000000ull / (uint64_t)m->rate);
|
||||
return ns < 500000 ? 500000 : ns; // until the device has taken it below the target
|
||||
}
|
||||
|
||||
// Render thread only.
|
||||
@@ -192,6 +239,34 @@ static void output_push(Mixer *m, const float *lr, int frames) {
|
||||
if (!m->device_ok) atomic_store(&m->rd, wr + (uint64_t)frames); // no device: the ring only keeps the latest frames
|
||||
}
|
||||
|
||||
// TOTK_AUDIO_DUMP=<path>: the sink mix before the clamp as raw interleaved float32 stereo, and every 5 s one log line with
|
||||
// how many frames were rendered less than 1 ms after the one before (a burst refill). File writes on the render thread:
|
||||
// for diagnosis only.
|
||||
static void sink_dump(const float *raw, int frames) {
|
||||
static FILE *f;
|
||||
static int opened;
|
||||
static uint64_t last, window_start, total, bursts;
|
||||
if (!opened) {
|
||||
opened = 1;
|
||||
const char *path = cfg_get("TOTK_AUDIO_DUMP");
|
||||
if (path && *path) f = fopen(path, "wb");
|
||||
}
|
||||
if (!f) return;
|
||||
fwrite(raw, sizeof(float), (size_t)frames * 2, f);
|
||||
uint64_t now = kern_now_ns();
|
||||
total++;
|
||||
if (last && now - last < 1000000ull) bursts++;
|
||||
last = now;
|
||||
if (!window_start) window_start = now;
|
||||
if (now - window_start >= 5000000000ull) {
|
||||
rt_log("audio: dump: %llu frames in %.1f s, %llu rendered under 1 ms after the previous", (unsigned long long)total,
|
||||
(now - window_start) / 1e9, (unsigned long long)bursts);
|
||||
total = bursts = 0;
|
||||
window_start = now;
|
||||
fflush(f);
|
||||
}
|
||||
}
|
||||
|
||||
// Level meter (TOTK_TRACE=audio): one line per 5 s of output, and the first nonzero frame.
|
||||
static void level_note(AudioRenderer *r, Mixer *m, const float *lr, int frames) {
|
||||
if (!(rt_trace & RT_TRACE_AUDIO)) return;
|
||||
@@ -209,12 +284,14 @@ static void level_note(AudioRenderer *r, Mixer *m, const float *lr, int frames)
|
||||
rt_log("audio: first nonzero output after %llu frames", (unsigned long long)r->elapsed_frames);
|
||||
}
|
||||
if (m->level_frames >= (uint64_t)r->param.sample_rate * 5) {
|
||||
uint64_t wr = atomic_load(&m->wr), queued = wr - atomic_load(&m->rd);
|
||||
rt_log("audio: level rms %.4f peak %.4f over %.1f s; written %llu frames, underruns %llu, overruns %llu, "
|
||||
"missing frames %llu, callback frames %llu (nonzero %llu)",
|
||||
"missing frames %llu, callback frames %llu (nonzero %llu), queued %.1f ms, skipped %llu frames",
|
||||
sqrt(m->level_sum / (double)m->level_count), m->level_peak, m->level_frames / (double)r->param.sample_rate,
|
||||
(unsigned long long)atomic_load(&m->wr), (unsigned long long)atomic_load(&m->underruns),
|
||||
(unsigned long long)wr, (unsigned long long)atomic_load(&m->underruns),
|
||||
(unsigned long long)atomic_load(&m->overruns), (unsigned long long)atomic_load(&m->missing_frames),
|
||||
(unsigned long long)atomic_load(&m->callback_frames), (unsigned long long)atomic_load(&m->callback_nonzero));
|
||||
(unsigned long long)atomic_load(&m->callback_frames), (unsigned long long)atomic_load(&m->callback_nonzero),
|
||||
queued * 1000.0 / r->param.sample_rate, (unsigned long long)atomic_load(&m->skipped_frames));
|
||||
m->level_sum = m->level_peak = 0;
|
||||
m->level_count = m->level_frames = 0;
|
||||
}
|
||||
@@ -248,8 +325,8 @@ void audio_mixer_open(AudioRenderer *r) {
|
||||
const char *pe = cfg_get("TOTK_AUDIO_PREFILL_MS");
|
||||
m->prefill = prefill_blocks_for(pe);
|
||||
if (m->prefill < 0) {
|
||||
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using 20", pe);
|
||||
m->prefill = 4;
|
||||
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using %d", pe, PREFILL_DEFAULT_MS);
|
||||
m->prefill = PREFILL_DEFAULT_MS / 5;
|
||||
}
|
||||
r->mixer = m;
|
||||
atomic_store(&m->playing, r->state == 0);
|
||||
@@ -463,8 +540,25 @@ static void walk_step(Walk *w) { /* the same transitions as advance_voice in a
|
||||
walk_skip_empty(w);
|
||||
}
|
||||
/* Decodes `frames` output samples per channel into out[c][k]; the renderer advances the voice afterwards by the same amount. */
|
||||
static double voice_step(const AudioRenderer *r, const ServerVoice *v) {
|
||||
return v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
|
||||
}
|
||||
/* Source samples the voice consumes over `frames` output samples, counted with the exact accumulation voice_render steps by
|
||||
(float rounding makes `fraction + seconds * rate * pitch` differ from it by one sample on some frames, a seam click);
|
||||
leaves the fractional position in v->fraction. */
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames) {
|
||||
const double step = voice_step(r, v);
|
||||
double frac = v->fraction;
|
||||
uint64_t consumed = 0;
|
||||
for (int k = 0; k < frames; k++) {
|
||||
frac += step;
|
||||
while (frac >= 1.0) { frac -= 1.0; consumed++; }
|
||||
}
|
||||
v->fraction = frac;
|
||||
return consumed;
|
||||
}
|
||||
static void voice_render(AudioRenderer *r, ServerVoice *v, float out[6][MAX_FRAME], int frames) {
|
||||
const double step = v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
|
||||
const double step = voice_step(r, v);
|
||||
Walk w = {v, v->index, v->offset, v->loops_done, {0}, 0, v->adpcm.history[0], v->adpcm.history[1], v->adpcm.buffer, v->adpcm.at};
|
||||
memcpy(w.valid, v->valid, sizeof w.valid);
|
||||
walk_skip_empty(&w);
|
||||
@@ -503,6 +597,7 @@ static void mix_aux(AudioRenderer *r, int mix, const AudrenEffectIn *e) {
|
||||
uint8_t *send=ipc_guest(p.send_info,128+(uint64_t)p.capacity*4), *ret=ipc_guest(p.return_info,128+(uint64_t)p.capacity*4);
|
||||
uint32_t *sh=(uint32_t *)(send+64),*rh=(uint32_t *)(ret+64); int32_t *sd=(int32_t *)(send+128),*rd=(int32_t *)(ret+128);
|
||||
uint32_t wr=sh[1],read=rh[0],total=p.channels*(uint32_t)m->frame;
|
||||
if(e->enabled) report_once(m,6,"auxiliary effect in use, channels",(int)p.channels);
|
||||
for(unsigned c=0;c<p.channels;c++) {
|
||||
if(p.input[c]>=r->mixes[mix].buffer_count || p.output[c]>=r->mixes[mix].buffer_count)rt_fatal("auxiliary effect mix buffer");
|
||||
float *input=mixbuf(m,m->offset[mix]+p.input[c]),*output=mixbuf(m,m->offset[mix]+p.output[c]);
|
||||
@@ -756,6 +851,7 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
if (sink && sink->input_count >= 1 && sink->input_count <= 6) {
|
||||
const float gain = host_volume() * atomic_load(&device_volume) * (r->mixes[0].in_use ? r->mixes[0].volume : 1.0f);
|
||||
const float *ch[6] = {0};
|
||||
float raw[2 * MAX_FRAME];
|
||||
for (uint32_t c = 0; c < sink->input_count; c++) if (sink->inputs[c] >= 0 && sink->inputs[c] < m->nbuf) ch[c] = mixbuf(m, sink->inputs[c]);
|
||||
for (int k = 0; k < frames; k++) {
|
||||
float l, rr;
|
||||
@@ -767,8 +863,10 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
} else if (sink->input_count >= 2) { l = ch[0] ? ch[0][k] : 0; rr = ch[1] ? ch[1][k] : 0; }
|
||||
else l = rr = ch[0] ? ch[0][k] : 0;
|
||||
l *= gain; rr *= gain;
|
||||
raw[2 * k] = l; raw[2 * k + 1] = rr;
|
||||
lr[2 * k] = l > 1 ? 1 : l < -1 ? -1 : l; lr[2 * k + 1] = rr > 1 ? 1 : rr < -1 ? -1 : rr;
|
||||
}
|
||||
sink_dump(raw, frames);
|
||||
} else if (!sink && !circular) report_once(m, 5, "no device sink in use", r->param.sink_count);
|
||||
level_note(r, m, lr, frames);
|
||||
output_push(m, lr, frames);
|
||||
|
||||
@@ -24,8 +24,8 @@ static void delay_set(Delay *d, unsigned n) { if (n > d->cap) rt_fatal("reverb d
|
||||
static void delay_init(Delay *d, unsigned khz, float ms) { d->cap = samples(khz, ms); d->data = calloc(d->cap + 1, sizeof(float)); if (!d->data) rt_fatal("reverb delay allocation"); delay_set(d, d->cap); }
|
||||
static float delay_update(Delay *d, float v) {
|
||||
d->data[d->write++] = v; float out = d->data[d->read++];
|
||||
if (d->read >= d->cap) d->read = 0;
|
||||
if (d->write >= d->cap) d->write = 0;
|
||||
if (d->read > d->cap) d->read = 0;
|
||||
if (d->write > d->cap) d->write = 0;
|
||||
return out;
|
||||
}
|
||||
static float delay_tap(const Delay *d, unsigned at) { int i = (int)d->write - (int)at - 1; if (i < 0) i += (int)d->cap + 1; return d->data[i]; }
|
||||
|
||||
+57
-43
@@ -5,7 +5,6 @@
|
||||
// service validates every section against the header, keeps server-side voice state, advances it in real time (one
|
||||
// frame of sample_count / sample_rate seconds per render tick, signalling the renderer's event) and writes the output
|
||||
// status the client expects. The mixer behind it is audio_mixer.c.
|
||||
#include <windows.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -14,10 +13,11 @@
|
||||
#include "audren.h"
|
||||
#include "host.h"
|
||||
#include "ipc.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static volatile long g_live;
|
||||
static volatile int32_t g_live;
|
||||
|
||||
long audren_renderers_live(void) { return InterlockedCompareExchange(&g_live, 0, 0); }
|
||||
long audren_renderers_live(void) { return plat_cas32(&g_live, 0, 0); }
|
||||
|
||||
static int revision_number(uint32_t rev) { return (int)(rev >> 24) - '0'; }
|
||||
|
||||
@@ -44,7 +44,7 @@ static AudioRenderer *renderer_new(const AudrenParameter *p) {
|
||||
r->effects = calloc((size_t)(p->effect_count ? p->effect_count : 1), sizeof *r->effects);
|
||||
r->channels = calloc((size_t)p->voice_count * 6, sizeof *r->channels);
|
||||
need(r->voices && r->pools && r->mixes && r->sinks && r->effects && r->channels, "renderer tables");
|
||||
InitializeSRWLock(&r->lock);
|
||||
plat_mutex_init(&r->lock);
|
||||
r->event = kern_event_new();
|
||||
r->state = 1; // stopped
|
||||
return r;
|
||||
@@ -195,7 +195,7 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
|
||||
memcpy(&h, input, sizeof h);
|
||||
need(h.total <= in_size, "update total size");
|
||||
Reader in = {input + sizeof h, input + h.total};
|
||||
AcquireSRWLockExclusive(&r->lock);
|
||||
plat_mutex_lock(&r->lock);
|
||||
r->revision = h.revision;
|
||||
const int vc = r->param.voice_count, ec = r->param.effect_count, sc = r->param.sink_count,
|
||||
mc = r->param.submix_count + 1;
|
||||
@@ -313,15 +313,14 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
|
||||
w += 0x10; // performance: nothing written
|
||||
w += AUDREN_BEHAVIOR_OUT; // behavior: no errors
|
||||
memcpy(w, &r->elapsed_frames, 8);
|
||||
ReleaseSRWLockExclusive(&r->lock);
|
||||
plat_mutex_unlock(&r->lock);
|
||||
}
|
||||
|
||||
// ---- render tick: one frame every sample_count / sample_rate seconds, on a host thread
|
||||
static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds) {
|
||||
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
|
||||
// or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls
|
||||
static void advance_voice(AudioRenderer *r, ServerVoice *v) {
|
||||
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return;
|
||||
double want = v->fraction + out_seconds * v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0);
|
||||
uint64_t samples = (uint64_t)want;
|
||||
v->fraction = want - (double)samples;
|
||||
uint64_t samples = audio_voice_consumed(r, v, (int)r->param.sample_count);
|
||||
while (samples && v->valid[v->index]) {
|
||||
AudrenWaveBuffer *b = &v->buffers[v->index];
|
||||
uint64_t length = (uint64_t)(b->end - b->start), left = length - v->offset;
|
||||
@@ -348,21 +347,48 @@ static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds)
|
||||
}
|
||||
}
|
||||
|
||||
static DWORD WINAPI render_thread(void *p) {
|
||||
static void render_thread(void *p) {
|
||||
AudioRenderer *r = p;
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
||||
HANDLE timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
if (!timer) timer = CreateWaitableTimerExW(NULL, NULL, 0, TIMER_ALL_ACCESS);
|
||||
if (!timer) rt_fatal("audren: CreateWaitableTimerExW failed (error %lu)", GetLastError());
|
||||
plat_thread_priority(PLAT_PRIO_HIGHEST);
|
||||
const uint64_t frame_ns = (uint64_t)r->param.sample_count * 1000000000ull / (uint64_t)r->param.sample_rate;
|
||||
const double frame = (double)r->param.sample_count / r->param.sample_rate;
|
||||
uint64_t next = kern_now_ns();
|
||||
while (InterlockedCompareExchange(&r->running, 0, 0)) {
|
||||
AcquireSRWLockExclusive(&r->lock);
|
||||
// When the device pulls a period (1024 samples on PipeWire, 4-5 frames) the ring drops below its target
|
||||
// and the frames would otherwise render back to back. On hardware the DSP renders one frame every 5 ms and
|
||||
// the game's audio thread works in between; AUX effects need that time to fill the return ring, or mix_aux
|
||||
// reads stale samples and the frame clicks at its seams. Device-paced frames stay at least 4/5 of a frame
|
||||
// apart, which still refills a period well before the device drains it. Linux only: on Windows SDL pulls
|
||||
// 480-sample periods, and a sink dump (TOTK_AUDIO_DUMP) of the bursts there showed no more seam spikes than
|
||||
// chance, so a wait on this thread buys nothing.
|
||||
#ifdef __linux__
|
||||
const uint64_t gap_ns = frame_ns * 4 / 5;
|
||||
#else
|
||||
const uint64_t gap_ns = 0;
|
||||
#endif
|
||||
uint64_t next = kern_now_ns(), last = 0;
|
||||
while (plat_cas32(&r->running, 0, 0)) {
|
||||
uint64_t now = kern_now_ns();
|
||||
int64_t pace = audio_mixer_pace_ns(r, now);
|
||||
if (pace > 0) {
|
||||
plat_wait_ns((uint64_t)pace);
|
||||
continue;
|
||||
}
|
||||
if (pace == 0 && last && now - last < gap_ns) {
|
||||
plat_wait_ns(gap_ns - (now - last));
|
||||
continue;
|
||||
}
|
||||
if (pace == 0) {
|
||||
next = now + frame_ns; // where the timer goes on if the device stops pacing
|
||||
} else {
|
||||
if (next > now) plat_wait_ns(next - now);
|
||||
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
|
||||
next += frame_ns;
|
||||
}
|
||||
last = kern_now_ns();
|
||||
plat_mutex_lock(&r->lock);
|
||||
if (r->state == 0) {
|
||||
voices_prepare(r);
|
||||
audio_mixer_render(r, frame);
|
||||
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i], frame);
|
||||
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i]);
|
||||
r->elapsed_frames++;
|
||||
uint64_t given = r->stop_released + r->empty_played + r->flushed;
|
||||
if ((rt_trace & RT_TRACE_IPC) && given != r->logged_released && r->elapsed_frames % 1000 == 0) {
|
||||
@@ -372,20 +398,9 @@ static DWORD WINAPI render_thread(void *p) {
|
||||
(unsigned long long)r->flushed);
|
||||
}
|
||||
}
|
||||
ReleaseSRWLockExclusive(&r->lock);
|
||||
plat_mutex_unlock(&r->lock);
|
||||
kern_event_signal(r->event);
|
||||
next += frame_ns;
|
||||
uint64_t now = kern_now_ns();
|
||||
if (next > now) {
|
||||
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)((next - now + 99) / 100)}; // relative, 100 ns units
|
||||
if (!SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE))
|
||||
rt_fatal("audren: SetWaitableTimer failed (error %lu)", GetLastError());
|
||||
WaitForSingleObject(timer, INFINITE);
|
||||
} else if (now - next > 100000000ull)
|
||||
next = now; // fell far behind (debugger, sleep): resynchronise
|
||||
}
|
||||
CloseHandle(timer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- IAudioRenderer
|
||||
@@ -417,16 +432,16 @@ static void renderer_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *r
|
||||
return;
|
||||
}
|
||||
case 5: // Start
|
||||
AcquireSRWLockExclusive(&r->lock);
|
||||
plat_mutex_lock(&r->lock);
|
||||
r->state = 0;
|
||||
audio_mixer_set_running(r, 1);
|
||||
ReleaseSRWLockExclusive(&r->lock);
|
||||
plat_mutex_unlock(&r->lock);
|
||||
return;
|
||||
case 6: // Stop
|
||||
AcquireSRWLockExclusive(&r->lock);
|
||||
plat_mutex_lock(&r->lock);
|
||||
r->state = 1;
|
||||
audio_mixer_set_running(r, 0);
|
||||
ReleaseSRWLockExclusive(&r->lock);
|
||||
plat_mutex_unlock(&r->lock);
|
||||
return;
|
||||
case 7: rs->copy[rs->ncopy++] = kern_event_readable(r->event); return; // QuerySystemEvent
|
||||
case 8: r->time_limit = IPC_ARG(rq, uint32_t, 0); return; // SetRenderingTimeLimit
|
||||
@@ -437,12 +452,11 @@ static void renderer_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *r
|
||||
|
||||
static void renderer_close(void *p) {
|
||||
AudioRenderer *r = p;
|
||||
InterlockedExchange(&r->running, 0);
|
||||
plat_xchg32(&r->running, 0);
|
||||
int away = kern_core_away();
|
||||
WaitForSingleObject(r->thread, INFINITE);
|
||||
plat_thread_join(r->thread);
|
||||
kern_core_back(&away);
|
||||
CloseHandle(r->thread);
|
||||
InterlockedDecrement(&g_live);
|
||||
plat_dec32(&g_live);
|
||||
audio_mixer_close(r);
|
||||
kern_handle_close(r->event);
|
||||
rt_log("audren: renderer closed after %llu frames", (unsigned long long)r->elapsed_frames);
|
||||
@@ -461,9 +475,9 @@ void audren_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs
|
||||
AudioRenderer *r = renderer_new(&p);
|
||||
r->running = 1;
|
||||
audio_mixer_open(r);
|
||||
r->thread = CreateThread(NULL, 0, render_thread, r, 0, NULL);
|
||||
if (!r->thread) rt_fatal("audren: render thread CreateThread failed (error %lu)", GetLastError());
|
||||
InterlockedIncrement(&g_live);
|
||||
r->thread = plat_thread_new(render_thread, r, "audren");
|
||||
if (!r->thread) rt_fatal("audren: cannot start the render thread");
|
||||
plat_inc32(&g_live);
|
||||
rt_log("audren: renderer open %d Hz x%d, mix buffers %d, voices %d, effects %d, submixes %d, sinks %d, "
|
||||
"splitters %d/%d, REV%d, work buffer %#llx+%#llx",
|
||||
p.sample_rate, p.sample_count, p.mix_buffer_count, p.voice_count, p.effect_count, p.submix_count,
|
||||
|
||||
+9
-4
@@ -3,10 +3,10 @@
|
||||
// audren:u, the audio renderer service: RequestUpdate wire formats, the server-side
|
||||
// renderer state, and the mixer interface (audio_mixer.c: the real mixer with SDL3 output).
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ipc.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
typedef struct {
|
||||
int32_t sample_rate, sample_count, mix_buffer_count, submix_count, voice_count, sink_count, effect_count,
|
||||
@@ -150,9 +150,9 @@ enum { SV_STOPPED, SV_STARTED, SV_PAUSED, SV_STOPPING };
|
||||
|
||||
typedef struct AudioRenderer {
|
||||
AudrenParameter param;
|
||||
SRWLOCK lock;
|
||||
HANDLE thread;
|
||||
volatile long running;
|
||||
PlatMutex lock;
|
||||
PlatThread *thread;
|
||||
volatile int32_t running;
|
||||
uint32_t state, revision, time_limit, event; // state 0 started, 1 stopped; event: writable handle
|
||||
int logged;
|
||||
uint64_t behavior_flags, elapsed_frames;
|
||||
@@ -180,5 +180,10 @@ void audio_mixer_update_effects(AudioRenderer *r);
|
||||
void audio_mixer_effect_result(AudioRenderer *r, int id, void *output, unsigned size);
|
||||
uint32_t audio_mixer_sink_written(AudioRenderer *r, int id);
|
||||
void audio_mixer_render(AudioRenderer *r, double seconds); // after audio_voices_prepare, before voices advance
|
||||
// Device pacing for the render thread at kern_now_ns `now`: 0 renders the next frame now, a positive value waits that
|
||||
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
|
||||
// device stalled).
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames); // source samples over one frame; advances v->fraction
|
||||
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
|
||||
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
|
||||
@@ -0,0 +1,253 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/bc3_encode.comp; do not edit.
|
||||
// source sha256 ac318d358136b39a113a18474a815f447223667657c90f9e0356a392639d8541
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t bc3_encode_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x00000314, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000007f, 0x00060010, 0x00000004,
|
||||
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00030047, 0x0000001b, 0x00000002, 0x00050048,
|
||||
0x0000001b, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000001b, 0x00000001, 0x00000023,
|
||||
0x00000004, 0x00050048, 0x0000001b, 0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x0000001b,
|
||||
0x00000003, 0x00000023, 0x0000000c, 0x00050048, 0x0000001b, 0x00000004, 0x00000023, 0x00000010,
|
||||
0x00050048, 0x0000001b, 0x00000005, 0x00000023, 0x00000014, 0x00040047, 0x0000002c, 0x00000006,
|
||||
0x00000004, 0x00030047, 0x0000002d, 0x00000002, 0x00040048, 0x0000002d, 0x00000000, 0x00000018,
|
||||
0x00050048, 0x0000002d, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x0000002f, 0x00000018,
|
||||
0x00040047, 0x0000002f, 0x00000021, 0x00000000, 0x00040047, 0x0000002f, 0x00000022, 0x00000000,
|
||||
0x00040047, 0x0000007f, 0x0000000b, 0x0000001c, 0x00040047, 0x000001e9, 0x00000006, 0x00000010,
|
||||
0x00030047, 0x000001ea, 0x00000002, 0x00040048, 0x000001ea, 0x00000000, 0x00000019, 0x00050048,
|
||||
0x000001ea, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x000001ec, 0x00000019, 0x00040047,
|
||||
0x000001ec, 0x00000021, 0x00000001, 0x00040047, 0x000001ec, 0x00000022, 0x00000000, 0x00040047,
|
||||
0x0000020c, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
|
||||
0x00040015, 0x00000006, 0x00000020, 0x00000000, 0x00030016, 0x00000008, 0x00000020, 0x00040017,
|
||||
0x00000009, 0x00000008, 0x00000004, 0x00040017, 0x00000010, 0x00000008, 0x00000003, 0x0008001e,
|
||||
0x0000001b, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00040020,
|
||||
0x0000001c, 0x00000009, 0x0000001b, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000009, 0x00040015,
|
||||
0x0000001e, 0x00000020, 0x00000001, 0x0004002b, 0x0000001e, 0x0000001f, 0x00000002, 0x00040020,
|
||||
0x00000020, 0x00000009, 0x00000006, 0x0004002b, 0x00000006, 0x00000023, 0x00000001, 0x0004002b,
|
||||
0x0000001e, 0x00000027, 0x00000003, 0x0003001d, 0x0000002c, 0x00000006, 0x0003001e, 0x0000002d,
|
||||
0x0000002c, 0x00040020, 0x0000002e, 0x0000000c, 0x0000002d, 0x0004003b, 0x0000002e, 0x0000002f,
|
||||
0x0000000c, 0x0004002b, 0x0000001e, 0x00000030, 0x00000000, 0x00040020, 0x0000003f, 0x0000000c,
|
||||
0x00000006, 0x0004002b, 0x00000008, 0x00000043, 0x437f0000, 0x00040017, 0x00000047, 0x00000006,
|
||||
0x00000003, 0x0004002b, 0x00000008, 0x0000004b, 0x00000000, 0x0004002b, 0x00000006, 0x00000057,
|
||||
0x00000000, 0x0004002b, 0x0000001e, 0x0000005a, 0x0000000b, 0x0004002b, 0x0000001e, 0x0000005e,
|
||||
0x00000005, 0x0004002b, 0x00000006, 0x00000061, 0x00000002, 0x0004002b, 0x00000006, 0x00000069,
|
||||
0x0000001f, 0x0004002b, 0x00000006, 0x00000070, 0x0000003f, 0x00040020, 0x0000007e, 0x00000001,
|
||||
0x00000047, 0x0004003b, 0x0000007e, 0x0000007f, 0x00000001, 0x0004002b, 0x00000006, 0x00000084,
|
||||
0x00000003, 0x0004002b, 0x00000006, 0x00000086, 0x00000004, 0x00020014, 0x00000088, 0x0004002b,
|
||||
0x0000001e, 0x0000008b, 0x00000004, 0x00040020, 0x0000009a, 0x00000007, 0x00000009, 0x0007002c,
|
||||
0x00000009, 0x0000009c, 0x00000043, 0x00000043, 0x00000043, 0x00000043, 0x0007002c, 0x00000009,
|
||||
0x0000009e, 0x0000004b, 0x0000004b, 0x0000004b, 0x0000004b, 0x0004002b, 0x00000006, 0x000000a7,
|
||||
0x00000010, 0x0004001c, 0x000000a9, 0x00000009, 0x000000a7, 0x00040020, 0x000000aa, 0x00000007,
|
||||
0x000000a9, 0x0004002b, 0x0000001e, 0x000000d0, 0x00000001, 0x00040020, 0x000000d7, 0x00000007,
|
||||
0x00000008, 0x00040017, 0x000000df, 0x00000006, 0x00000002, 0x0004002b, 0x0000001e, 0x000000e4,
|
||||
0x00000008, 0x0004002b, 0x00000008, 0x00000101, 0x40e00000, 0x0004002b, 0x00000006, 0x00000110,
|
||||
0x00000007, 0x0004002b, 0x00000006, 0x0000011f, 0x00000020, 0x0004002b, 0x00000008, 0x000001b5,
|
||||
0x358637bd, 0x0004002b, 0x00000008, 0x000001ca, 0x40400000, 0x00040017, 0x000001e8, 0x00000006,
|
||||
0x00000004, 0x0003001d, 0x000001e9, 0x000001e8, 0x0003001e, 0x000001ea, 0x000001e9, 0x00040020,
|
||||
0x000001eb, 0x0000000c, 0x000001ea, 0x0004003b, 0x000001eb, 0x000001ec, 0x0000000c, 0x0004002b,
|
||||
0x0000001e, 0x00000204, 0x00000010, 0x00040020, 0x00000209, 0x0000000c, 0x000001e8, 0x0004002b,
|
||||
0x00000006, 0x0000020b, 0x00000008, 0x0006002c, 0x00000047, 0x0000020c, 0x0000020b, 0x0000020b,
|
||||
0x00000023, 0x0006002c, 0x00000010, 0x00000302, 0x0000004b, 0x0000004b, 0x0000004b, 0x0006002c,
|
||||
0x00000010, 0x00000303, 0x00000043, 0x00000043, 0x00000043, 0x0004002b, 0x00000008, 0x00000304,
|
||||
0x3d800000, 0x0007002c, 0x00000009, 0x00000305, 0x00000304, 0x00000304, 0x00000304, 0x00000304,
|
||||
0x0006002c, 0x00000010, 0x00000306, 0x00000304, 0x00000304, 0x00000304, 0x0004002b, 0x00000008,
|
||||
0x00000309, 0x3df8f8fa, 0x0004002b, 0x00000008, 0x0000030a, 0x3e7cfcfe, 0x0006002c, 0x00000010,
|
||||
0x0000030b, 0x00000309, 0x0000030a, 0x00000309, 0x0004002b, 0x00000008, 0x0000030d, 0x41039ce7,
|
||||
0x0004002b, 0x00000008, 0x0000030f, 0x40818619, 0x0004002b, 0x00000006, 0x00000310, 0x00000013,
|
||||
0x0004002b, 0x00000006, 0x00000311, 0xfffffff0, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
|
||||
0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x000000aa, 0x000000ab, 0x00000007, 0x000300f7,
|
||||
0x0000020d, 0x00000000, 0x000300fb, 0x00000057, 0x0000020e, 0x000200f8, 0x0000020e, 0x0004003d,
|
||||
0x00000047, 0x00000080, 0x0000007f, 0x00050041, 0x00000020, 0x00000082, 0x0000001d, 0x00000027,
|
||||
0x0004003d, 0x00000006, 0x00000083, 0x00000082, 0x00050080, 0x00000006, 0x00000085, 0x00000083,
|
||||
0x00000084, 0x00050086, 0x00000006, 0x00000087, 0x00000085, 0x00000086, 0x00050051, 0x00000006,
|
||||
0x0000008a, 0x00000080, 0x00000000, 0x00050041, 0x00000020, 0x0000008c, 0x0000001d, 0x0000008b,
|
||||
0x0004003d, 0x00000006, 0x0000008d, 0x0000008c, 0x000500ae, 0x00000088, 0x0000008e, 0x0000008a,
|
||||
0x0000008d, 0x000400a8, 0x00000088, 0x0000008f, 0x0000008e, 0x000300f7, 0x00000091, 0x00000000,
|
||||
0x000400fa, 0x0000008f, 0x00000090, 0x00000091, 0x000200f8, 0x00000090, 0x00050051, 0x00000006,
|
||||
0x00000093, 0x00000080, 0x00000001, 0x000500ae, 0x00000088, 0x00000095, 0x00000093, 0x00000087,
|
||||
0x000200f9, 0x00000091, 0x000200f8, 0x00000091, 0x000700f5, 0x00000088, 0x00000096, 0x0000008e,
|
||||
0x0000020e, 0x00000095, 0x00000090, 0x000300f7, 0x00000098, 0x00000000, 0x000400fa, 0x00000096,
|
||||
0x00000097, 0x00000098, 0x000200f8, 0x00000097, 0x000200f9, 0x0000020d, 0x000200f8, 0x00000098,
|
||||
0x000200f9, 0x000000a1, 0x000200f8, 0x000000a1, 0x000700f5, 0x00000009, 0x000002b0, 0x0000009c,
|
||||
0x00000098, 0x000000c4, 0x000000a2, 0x000700f5, 0x00000009, 0x000002af, 0x0000009e, 0x00000098,
|
||||
0x000000c9, 0x000000a2, 0x000700f5, 0x00000009, 0x000002ae, 0x0000009e, 0x00000098, 0x000000ce,
|
||||
0x000000a2, 0x000700f5, 0x00000006, 0x000002ad, 0x00000057, 0x00000098, 0x000000d1, 0x000000a2,
|
||||
0x000500b0, 0x00000088, 0x000000a8, 0x000002ad, 0x000000a7, 0x000400f6, 0x000000a3, 0x000000a2,
|
||||
0x00000000, 0x000400fa, 0x000000a8, 0x000000a2, 0x000000a3, 0x000200f8, 0x000000a2, 0x00050084,
|
||||
0x00000006, 0x000000af, 0x0000008a, 0x00000086, 0x000500c7, 0x00000006, 0x000000b1, 0x000002ad,
|
||||
0x00000084, 0x00050080, 0x00000006, 0x000000b2, 0x000000af, 0x000000b1, 0x00050051, 0x00000006,
|
||||
0x000000b4, 0x00000080, 0x00000001, 0x00050084, 0x00000006, 0x000000b5, 0x000000b4, 0x00000086,
|
||||
0x000500c2, 0x00000006, 0x000000b7, 0x000002ad, 0x0000001f, 0x00050080, 0x00000006, 0x000000b8,
|
||||
0x000000b5, 0x000000b7, 0x00050051, 0x00000006, 0x000000bd, 0x00000080, 0x00000002, 0x00050041,
|
||||
0x00000020, 0x00000216, 0x0000001d, 0x0000001f, 0x0004003d, 0x00000006, 0x00000217, 0x00000216,
|
||||
0x00050082, 0x00000006, 0x00000218, 0x00000217, 0x00000023, 0x0007000c, 0x00000006, 0x00000219,
|
||||
0x00000001, 0x00000026, 0x000000b2, 0x00000218, 0x00050082, 0x00000006, 0x0000021d, 0x00000083,
|
||||
0x00000023, 0x0007000c, 0x00000006, 0x0000021e, 0x00000001, 0x00000026, 0x000000b8, 0x0000021d,
|
||||
0x00050041, 0x00000020, 0x0000021f, 0x0000001d, 0x00000030, 0x0004003d, 0x00000006, 0x00000220,
|
||||
0x0000021f, 0x00050084, 0x00000006, 0x00000224, 0x000000bd, 0x00000083, 0x00050080, 0x00000006,
|
||||
0x00000226, 0x00000224, 0x0000021e, 0x00050084, 0x00000006, 0x00000229, 0x00000226, 0x00000217,
|
||||
0x00050080, 0x00000006, 0x0000022a, 0x00000220, 0x00000229, 0x00050080, 0x00000006, 0x0000022c,
|
||||
0x0000022a, 0x00000219, 0x00060041, 0x0000003f, 0x0000022d, 0x0000002f, 0x00000030, 0x0000022c,
|
||||
0x0004003d, 0x00000006, 0x0000022e, 0x0000022d, 0x0006000c, 0x00000009, 0x0000022f, 0x00000001,
|
||||
0x00000040, 0x0000022e, 0x0005008e, 0x00000009, 0x00000230, 0x0000022f, 0x00000043, 0x00050041,
|
||||
0x0000009a, 0x000000bf, 0x000000ab, 0x000002ad, 0x0003003e, 0x000000bf, 0x00000230, 0x0004003d,
|
||||
0x00000009, 0x000000c3, 0x000000bf, 0x0007000c, 0x00000009, 0x000000c4, 0x00000001, 0x00000025,
|
||||
0x000002b0, 0x000000c3, 0x0004003d, 0x00000009, 0x000000c8, 0x000000bf, 0x0007000c, 0x00000009,
|
||||
0x000000c9, 0x00000001, 0x00000028, 0x000002af, 0x000000c8, 0x0004003d, 0x00000009, 0x000000cc,
|
||||
0x000000bf, 0x00050081, 0x00000009, 0x000000ce, 0x000002ae, 0x000000cc, 0x00050080, 0x00000006,
|
||||
0x000000d1, 0x000002ad, 0x000000d0, 0x000200f9, 0x000000a1, 0x000200f8, 0x000000a3, 0x00050085,
|
||||
0x00000009, 0x000000d5, 0x000002ae, 0x00000305, 0x00050051, 0x00000008, 0x000000d9, 0x000002af,
|
||||
0x00000003, 0x0004006d, 0x00000006, 0x000000da, 0x000000d9, 0x00050051, 0x00000008, 0x000000dd,
|
||||
0x000002b0, 0x00000003, 0x0004006d, 0x00000006, 0x000000de, 0x000000dd, 0x000500c4, 0x00000006,
|
||||
0x000000e5, 0x000000de, 0x000000e4, 0x000500c5, 0x00000006, 0x000000e6, 0x000000da, 0x000000e5,
|
||||
0x00050050, 0x000000df, 0x000000e7, 0x000000e6, 0x00000057, 0x000500ac, 0x00000088, 0x000000ea,
|
||||
0x000000da, 0x000000de, 0x000300f7, 0x000000ec, 0x00000000, 0x000400fa, 0x000000ea, 0x000000eb,
|
||||
0x000000ec, 0x000200f8, 0x000000eb, 0x00050082, 0x00000006, 0x000000f0, 0x000000da, 0x000000de,
|
||||
0x00040070, 0x00000008, 0x000000f1, 0x000000f0, 0x000200f9, 0x000000f3, 0x000200f8, 0x000000f3,
|
||||
0x000700f5, 0x000000df, 0x000002c8, 0x000000e7, 0x000000eb, 0x000002f1, 0x000000f6, 0x000700f5,
|
||||
0x00000006, 0x000002b1, 0x00000057, 0x000000eb, 0x00000143, 0x000000f6, 0x000500b0, 0x00000088,
|
||||
0x000000f9, 0x000002b1, 0x000000a7, 0x000400f6, 0x000000f5, 0x000000f6, 0x00000000, 0x000400fa,
|
||||
0x000000f9, 0x000000f4, 0x000000f5, 0x000200f8, 0x000000f4, 0x00040070, 0x00000008, 0x000000fc,
|
||||
0x000000da, 0x00060041, 0x000000d7, 0x000000fe, 0x000000ab, 0x000002b1, 0x00000084, 0x0004003d,
|
||||
0x00000008, 0x000000ff, 0x000000fe, 0x00050083, 0x00000008, 0x00000100, 0x000000fc, 0x000000ff,
|
||||
0x00050085, 0x00000008, 0x00000102, 0x00000100, 0x00000101, 0x00050088, 0x00000008, 0x00000104,
|
||||
0x00000102, 0x000000f1, 0x0006000c, 0x00000008, 0x00000105, 0x00000001, 0x00000001, 0x00000104,
|
||||
0x0008000c, 0x00000008, 0x00000106, 0x00000001, 0x0000002b, 0x00000105, 0x0000004b, 0x00000101,
|
||||
0x0004006d, 0x00000006, 0x00000107, 0x00000106, 0x000500aa, 0x00000088, 0x0000010a, 0x00000107,
|
||||
0x00000057, 0x000300f7, 0x0000010d, 0x00000000, 0x000400fa, 0x0000010a, 0x0000010c, 0x0000010e,
|
||||
0x000200f8, 0x0000010c, 0x000200f9, 0x0000010d, 0x000200f8, 0x0000010e, 0x000500aa, 0x00000088,
|
||||
0x00000111, 0x00000107, 0x00000110, 0x000300f7, 0x00000114, 0x00000000, 0x000400fa, 0x00000111,
|
||||
0x00000113, 0x00000115, 0x000200f8, 0x00000113, 0x000200f9, 0x00000114, 0x000200f8, 0x00000115,
|
||||
0x00050080, 0x00000006, 0x00000117, 0x00000107, 0x00000023, 0x000200f9, 0x00000114, 0x000200f8,
|
||||
0x00000114, 0x000700f5, 0x00000006, 0x000002db, 0x00000023, 0x00000113, 0x00000117, 0x00000115,
|
||||
0x000200f9, 0x0000010d, 0x000200f8, 0x0000010d, 0x000700f5, 0x00000006, 0x000002dc, 0x00000057,
|
||||
0x0000010c, 0x000002db, 0x00000114, 0x00050084, 0x00000006, 0x0000011c, 0x00000084, 0x000002b1,
|
||||
0x00050080, 0x00000006, 0x0000011d, 0x000000a7, 0x0000011c, 0x000500b0, 0x00000088, 0x00000120,
|
||||
0x0000011d, 0x0000011f, 0x000300f7, 0x00000122, 0x00000000, 0x000400fa, 0x00000120, 0x00000121,
|
||||
0x00000122, 0x000200f8, 0x00000121, 0x000500c4, 0x00000006, 0x00000125, 0x000002dc, 0x0000011d,
|
||||
0x00050051, 0x00000006, 0x00000127, 0x000002c8, 0x00000000, 0x000500c5, 0x00000006, 0x00000128,
|
||||
0x00000127, 0x00000125, 0x00060052, 0x000000df, 0x0000028e, 0x00000128, 0x000002c8, 0x00000000,
|
||||
0x000200f9, 0x00000122, 0x000200f8, 0x00000122, 0x000700f5, 0x000000df, 0x000002e3, 0x000002c8,
|
||||
0x0000010d, 0x0000028e, 0x00000121, 0x00050080, 0x00000006, 0x0000012b, 0x0000011c, 0x00000310,
|
||||
0x000500ac, 0x00000088, 0x0000012c, 0x0000012b, 0x0000011f, 0x000300f7, 0x0000012e, 0x00000000,
|
||||
0x000400fa, 0x0000012c, 0x0000012d, 0x0000012e, 0x000200f8, 0x0000012d, 0x0007000c, 0x00000006,
|
||||
0x00000131, 0x00000001, 0x00000026, 0x0000011d, 0x0000011f, 0x00050082, 0x00000006, 0x00000132,
|
||||
0x0000011f, 0x00000131, 0x000500c2, 0x00000006, 0x00000133, 0x000002dc, 0x00000132, 0x000500ac,
|
||||
0x00000088, 0x00000135, 0x0000011d, 0x0000011f, 0x000300f7, 0x00000138, 0x00000000, 0x000400fa,
|
||||
0x00000135, 0x00000137, 0x0000013b, 0x000200f8, 0x00000137, 0x00050080, 0x00000006, 0x0000013a,
|
||||
0x0000011c, 0x00000311, 0x000200f9, 0x00000138, 0x000200f8, 0x0000013b, 0x000200f9, 0x00000138,
|
||||
0x000200f8, 0x00000138, 0x000700f5, 0x00000006, 0x000002e1, 0x0000013a, 0x00000137, 0x00000057,
|
||||
0x0000013b, 0x000500c4, 0x00000006, 0x0000013d, 0x00000133, 0x000002e1, 0x00050051, 0x00000006,
|
||||
0x0000013f, 0x000002e3, 0x00000001, 0x000500c5, 0x00000006, 0x00000140, 0x0000013f, 0x0000013d,
|
||||
0x00060052, 0x000000df, 0x00000291, 0x00000140, 0x000002e3, 0x00000001, 0x000200f9, 0x0000012e,
|
||||
0x000200f8, 0x0000012e, 0x000700f5, 0x000000df, 0x000002f1, 0x000002e3, 0x00000122, 0x00000291,
|
||||
0x00000138, 0x000200f9, 0x000000f6, 0x000200f8, 0x000000f6, 0x00050080, 0x00000006, 0x00000143,
|
||||
0x000002b1, 0x000000d0, 0x000200f9, 0x000000f3, 0x000200f8, 0x000000f5, 0x000200f9, 0x000000ec,
|
||||
0x000200f8, 0x000000ec, 0x000700f5, 0x000000df, 0x000002c7, 0x000000e7, 0x000000a3, 0x000002c8,
|
||||
0x000000f5, 0x0008004f, 0x00000010, 0x00000146, 0x000002b0, 0x000002b0, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x0008004f, 0x00000010, 0x00000149, 0x000002af, 0x000002af, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x000200f9, 0x0000014d, 0x000200f8, 0x0000014d, 0x000700f5, 0x00000008, 0x000002bb,
|
||||
0x0000004b, 0x000000ec, 0x00000169, 0x0000014e, 0x000700f5, 0x00000008, 0x000002b7, 0x0000004b,
|
||||
0x000000ec, 0x00000162, 0x0000014e, 0x000700f5, 0x00000006, 0x000002b6, 0x00000057, 0x000000ec,
|
||||
0x0000016b, 0x0000014e, 0x000500b0, 0x00000088, 0x00000153, 0x000002b6, 0x000000a7, 0x000400f6,
|
||||
0x0000014f, 0x0000014e, 0x00000000, 0x000400fa, 0x00000153, 0x0000014e, 0x0000014f, 0x000200f8,
|
||||
0x0000014e, 0x00050041, 0x0000009a, 0x00000156, 0x000000ab, 0x000002b6, 0x0004003d, 0x00000009,
|
||||
0x00000157, 0x00000156, 0x0008004f, 0x00000010, 0x00000158, 0x00000157, 0x00000157, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x0008004f, 0x00000010, 0x0000015a, 0x000000d5, 0x000000d5, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x00050083, 0x00000010, 0x0000015b, 0x00000158, 0x0000015a, 0x00050051,
|
||||
0x00000008, 0x0000015d, 0x0000015b, 0x00000000, 0x00050051, 0x00000008, 0x0000015f, 0x0000015b,
|
||||
0x00000001, 0x00050085, 0x00000008, 0x00000160, 0x0000015d, 0x0000015f, 0x00050081, 0x00000008,
|
||||
0x00000162, 0x000002b7, 0x00000160, 0x00050051, 0x00000008, 0x00000164, 0x0000015b, 0x00000002,
|
||||
0x00050085, 0x00000008, 0x00000167, 0x00000164, 0x0000015f, 0x00050081, 0x00000008, 0x00000169,
|
||||
0x000002bb, 0x00000167, 0x00050080, 0x00000006, 0x0000016b, 0x000002b6, 0x000000d0, 0x000200f9,
|
||||
0x0000014d, 0x000200f8, 0x0000014f, 0x000500b8, 0x00000088, 0x0000016d, 0x000002b7, 0x0000004b,
|
||||
0x000300f7, 0x0000016f, 0x00000000, 0x000400fa, 0x0000016d, 0x0000016e, 0x0000016f, 0x000200f8,
|
||||
0x0000016e, 0x00050051, 0x00000008, 0x00000172, 0x000002b0, 0x00000000, 0x00050051, 0x00000008,
|
||||
0x00000174, 0x000002af, 0x00000000, 0x00060052, 0x00000010, 0x00000299, 0x00000174, 0x00000146,
|
||||
0x00000000, 0x00060052, 0x00000010, 0x0000029b, 0x00000172, 0x00000149, 0x00000000, 0x000200f9,
|
||||
0x0000016f, 0x000200f8, 0x0000016f, 0x000700f5, 0x00000010, 0x000002bd, 0x00000149, 0x0000014f,
|
||||
0x0000029b, 0x0000016e, 0x000700f5, 0x00000010, 0x000002bc, 0x00000146, 0x0000014f, 0x00000299,
|
||||
0x0000016e, 0x000500b8, 0x00000088, 0x00000179, 0x000002bb, 0x0000004b, 0x000300f7, 0x0000017b,
|
||||
0x00000000, 0x000400fa, 0x00000179, 0x0000017a, 0x0000017b, 0x000200f8, 0x0000017a, 0x00050051,
|
||||
0x00000008, 0x0000017e, 0x000002bc, 0x00000002, 0x00050051, 0x00000008, 0x00000180, 0x000002bd,
|
||||
0x00000002, 0x00060052, 0x00000010, 0x0000029f, 0x00000180, 0x000002bc, 0x00000002, 0x00060052,
|
||||
0x00000010, 0x000002a1, 0x0000017e, 0x000002bd, 0x00000002, 0x000200f9, 0x0000017b, 0x000200f8,
|
||||
0x0000017b, 0x000700f5, 0x00000010, 0x000002bf, 0x000002bc, 0x0000016f, 0x0000029f, 0x0000017a,
|
||||
0x000700f5, 0x00000010, 0x000002be, 0x000002bd, 0x0000016f, 0x000002a1, 0x0000017a, 0x00050083,
|
||||
0x00000010, 0x00000187, 0x000002be, 0x000002bf, 0x00050085, 0x00000010, 0x00000189, 0x00000187,
|
||||
0x00000306, 0x00050083, 0x00000010, 0x0000018d, 0x000002be, 0x00000189, 0x0008000c, 0x00000010,
|
||||
0x00000237, 0x00000001, 0x0000002b, 0x0000018d, 0x00000302, 0x00000303, 0x00050085, 0x00000010,
|
||||
0x0000023a, 0x00000237, 0x0000030b, 0x0006000c, 0x00000010, 0x0000023b, 0x00000001, 0x00000001,
|
||||
0x0000023a, 0x0004006d, 0x00000047, 0x0000023c, 0x0000023b, 0x00050051, 0x00000006, 0x0000023e,
|
||||
0x0000023c, 0x00000000, 0x000500c4, 0x00000006, 0x0000023f, 0x0000023e, 0x0000005a, 0x00050051,
|
||||
0x00000006, 0x00000241, 0x0000023c, 0x00000001, 0x000500c4, 0x00000006, 0x00000242, 0x00000241,
|
||||
0x0000005e, 0x000500c5, 0x00000006, 0x00000243, 0x0000023f, 0x00000242, 0x00050051, 0x00000006,
|
||||
0x00000245, 0x0000023c, 0x00000002, 0x000500c5, 0x00000006, 0x00000246, 0x00000243, 0x00000245,
|
||||
0x00050081, 0x00000010, 0x00000193, 0x000002bf, 0x00000189, 0x0008000c, 0x00000010, 0x0000024d,
|
||||
0x00000001, 0x0000002b, 0x00000193, 0x00000302, 0x00000303, 0x00050085, 0x00000010, 0x00000250,
|
||||
0x0000024d, 0x0000030b, 0x0006000c, 0x00000010, 0x00000251, 0x00000001, 0x00000001, 0x00000250,
|
||||
0x0004006d, 0x00000047, 0x00000252, 0x00000251, 0x00050051, 0x00000006, 0x00000254, 0x00000252,
|
||||
0x00000000, 0x000500c4, 0x00000006, 0x00000255, 0x00000254, 0x0000005a, 0x00050051, 0x00000006,
|
||||
0x00000257, 0x00000252, 0x00000001, 0x000500c4, 0x00000006, 0x00000258, 0x00000257, 0x0000005e,
|
||||
0x000500c5, 0x00000006, 0x00000259, 0x00000255, 0x00000258, 0x00050051, 0x00000006, 0x0000025b,
|
||||
0x00000252, 0x00000002, 0x000500c5, 0x00000006, 0x0000025c, 0x00000259, 0x0000025b, 0x000500ab,
|
||||
0x00000088, 0x00000199, 0x00000246, 0x0000025c, 0x000300f7, 0x0000019b, 0x00000000, 0x000400fa,
|
||||
0x00000199, 0x0000019a, 0x0000019b, 0x000200f8, 0x0000019a, 0x000500b0, 0x00000088, 0x0000019e,
|
||||
0x00000246, 0x0000025c, 0x000600a9, 0x00000006, 0x00000312, 0x0000019e, 0x00000246, 0x0000025c,
|
||||
0x000600a9, 0x00000006, 0x00000313, 0x0000019e, 0x0000025c, 0x00000246, 0x000500c2, 0x00000006,
|
||||
0x00000260, 0x00000313, 0x0000005a, 0x000500c7, 0x00000006, 0x00000261, 0x00000260, 0x00000069,
|
||||
0x00040070, 0x00000008, 0x00000262, 0x00000261, 0x00050085, 0x00000008, 0x00000264, 0x00000262,
|
||||
0x0000030d, 0x000500c2, 0x00000006, 0x00000266, 0x00000313, 0x0000005e, 0x000500c7, 0x00000006,
|
||||
0x00000267, 0x00000266, 0x00000070, 0x00040070, 0x00000008, 0x00000268, 0x00000267, 0x00050085,
|
||||
0x00000008, 0x0000026a, 0x00000268, 0x0000030f, 0x000500c7, 0x00000006, 0x0000026c, 0x00000313,
|
||||
0x00000069, 0x00040070, 0x00000008, 0x0000026d, 0x0000026c, 0x00050085, 0x00000008, 0x0000026f,
|
||||
0x0000026d, 0x0000030d, 0x00060050, 0x00000010, 0x00000270, 0x00000264, 0x0000026a, 0x0000026f,
|
||||
0x000500c2, 0x00000006, 0x00000274, 0x00000312, 0x0000005a, 0x000500c7, 0x00000006, 0x00000275,
|
||||
0x00000274, 0x00000069, 0x00040070, 0x00000008, 0x00000276, 0x00000275, 0x00050085, 0x00000008,
|
||||
0x00000278, 0x00000276, 0x0000030d, 0x000500c2, 0x00000006, 0x0000027a, 0x00000312, 0x0000005e,
|
||||
0x000500c7, 0x00000006, 0x0000027b, 0x0000027a, 0x00000070, 0x00040070, 0x00000008, 0x0000027c,
|
||||
0x0000027b, 0x00050085, 0x00000008, 0x0000027e, 0x0000027c, 0x0000030f, 0x000500c7, 0x00000006,
|
||||
0x00000280, 0x00000312, 0x00000069, 0x00040070, 0x00000008, 0x00000281, 0x00000280, 0x00050085,
|
||||
0x00000008, 0x00000283, 0x00000281, 0x0000030d, 0x00060050, 0x00000010, 0x00000284, 0x00000278,
|
||||
0x0000027e, 0x00000283, 0x00050083, 0x00000010, 0x000001b0, 0x00000284, 0x00000270, 0x00050094,
|
||||
0x00000008, 0x000001b4, 0x000001b0, 0x000001b0, 0x0007000c, 0x00000008, 0x000001b6, 0x00000001,
|
||||
0x00000028, 0x000001b4, 0x000001b5, 0x000200f9, 0x000001b8, 0x000200f8, 0x000001b8, 0x000700f5,
|
||||
0x00000006, 0x000002d0, 0x00000057, 0x0000019a, 0x000001e5, 0x000001bb, 0x000700f5, 0x00000006,
|
||||
0x000002c2, 0x00000057, 0x0000019a, 0x000001e7, 0x000001bb, 0x000500b0, 0x00000088, 0x000001be,
|
||||
0x000002c2, 0x000000a7, 0x000400f6, 0x000001ba, 0x000001bb, 0x00000000, 0x000400fa, 0x000001be,
|
||||
0x000001b9, 0x000001ba, 0x000200f8, 0x000001b9, 0x00050041, 0x0000009a, 0x000001c1, 0x000000ab,
|
||||
0x000002c2, 0x0004003d, 0x00000009, 0x000001c2, 0x000001c1, 0x0008004f, 0x00000010, 0x000001c3,
|
||||
0x000001c2, 0x000001c2, 0x00000000, 0x00000001, 0x00000002, 0x00050083, 0x00000010, 0x000001c5,
|
||||
0x000001c3, 0x00000270, 0x00050094, 0x00000008, 0x000001c7, 0x000001c5, 0x000001b0, 0x00050088,
|
||||
0x00000008, 0x000001c9, 0x000001c7, 0x000001b6, 0x00050085, 0x00000008, 0x000001cb, 0x000001c9,
|
||||
0x000001ca, 0x0006000c, 0x00000008, 0x000001cc, 0x00000001, 0x00000001, 0x000001cb, 0x0008000c,
|
||||
0x00000008, 0x000001cd, 0x00000001, 0x0000002b, 0x000001cc, 0x0000004b, 0x000001ca, 0x0004006d,
|
||||
0x00000006, 0x000001ce, 0x000001cd, 0x000500aa, 0x00000088, 0x000001d1, 0x000001ce, 0x00000057,
|
||||
0x000300f7, 0x000001d4, 0x00000000, 0x000400fa, 0x000001d1, 0x000001d3, 0x000001d5, 0x000200f8,
|
||||
0x000001d3, 0x000200f9, 0x000001d4, 0x000200f8, 0x000001d5, 0x000500aa, 0x00000088, 0x000001d7,
|
||||
0x000001ce, 0x00000084, 0x000300f7, 0x000001da, 0x00000000, 0x000400fa, 0x000001d7, 0x000001d9,
|
||||
0x000001db, 0x000200f8, 0x000001d9, 0x000200f9, 0x000001da, 0x000200f8, 0x000001db, 0x00050080,
|
||||
0x00000006, 0x000001dd, 0x000001ce, 0x00000023, 0x000200f9, 0x000001da, 0x000200f8, 0x000001da,
|
||||
0x000700f5, 0x00000006, 0x000002d2, 0x00000023, 0x000001d9, 0x000001dd, 0x000001db, 0x000200f9,
|
||||
0x000001d4, 0x000200f8, 0x000001d4, 0x000700f5, 0x00000006, 0x000002d3, 0x00000057, 0x000001d3,
|
||||
0x000002d2, 0x000001da, 0x00050084, 0x00000006, 0x000001e2, 0x00000061, 0x000002c2, 0x000500c4,
|
||||
0x00000006, 0x000001e3, 0x000002d3, 0x000001e2, 0x000500c5, 0x00000006, 0x000001e5, 0x000002d0,
|
||||
0x000001e3, 0x000200f9, 0x000001bb, 0x000200f8, 0x000001bb, 0x00050080, 0x00000006, 0x000001e7,
|
||||
0x000002c2, 0x000000d0, 0x000200f9, 0x000001b8, 0x000200f8, 0x000001ba, 0x000200f9, 0x0000019b,
|
||||
0x000200f8, 0x0000019b, 0x000700f5, 0x00000006, 0x000002cf, 0x00000057, 0x0000017b, 0x000002d0,
|
||||
0x000001ba, 0x000700f5, 0x00000006, 0x000002cd, 0x0000025c, 0x0000017b, 0x00000312, 0x000001ba,
|
||||
0x000700f5, 0x00000006, 0x000002cb, 0x00000246, 0x0000017b, 0x00000313, 0x000001ba, 0x00050041,
|
||||
0x00000020, 0x000001ed, 0x0000001d, 0x000000d0, 0x0004003d, 0x00000006, 0x000001ee, 0x000001ed,
|
||||
0x00050051, 0x00000006, 0x000001f0, 0x00000080, 0x00000002, 0x00050041, 0x00000020, 0x000001f1,
|
||||
0x0000001d, 0x0000005e, 0x0004003d, 0x00000006, 0x000001f2, 0x000001f1, 0x00050084, 0x00000006,
|
||||
0x000001f3, 0x000001f0, 0x000001f2, 0x00050080, 0x00000006, 0x000001f4, 0x000001ee, 0x000001f3,
|
||||
0x00050051, 0x00000006, 0x000001f6, 0x00000080, 0x00000001, 0x00050084, 0x00000006, 0x000001f9,
|
||||
0x000001f6, 0x0000008d, 0x00050080, 0x00000006, 0x000001fa, 0x000001f4, 0x000001f9, 0x00050080,
|
||||
0x00000006, 0x000001fd, 0x000001fa, 0x0000008a, 0x00050051, 0x00000006, 0x000001ff, 0x000002c7,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x00000201, 0x000002c7, 0x00000001, 0x000500c4, 0x00000006,
|
||||
0x00000205, 0x000002cd, 0x00000204, 0x000500c5, 0x00000006, 0x00000206, 0x000002cb, 0x00000205,
|
||||
0x00070050, 0x000001e8, 0x00000208, 0x000001ff, 0x00000201, 0x00000206, 0x000002cf, 0x00060041,
|
||||
0x00000209, 0x0000020a, 0x000001ec, 0x00000030, 0x000001fd, 0x0003003e, 0x0000020a, 0x00000208,
|
||||
0x000200f9, 0x0000020d, 0x000200f8, 0x0000020d, 0x000100fd, 0x00010038,
|
||||
};
|
||||
+38
-36
@@ -3,38 +3,43 @@
|
||||
// da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
||||
// 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:
|
||||
// SetFocusHandlingMode called from main+0xe7a954 (1.4.3: +0xbdb48c) in nnMain, every module's constructor flag
|
||||
// reads 2
|
||||
// nvnBootstrapLoader called from main+0xe7eb2c (1.4.3: +0xbe0360) in the graphics bring-up; logs the run
|
||||
// statistics, exits 0
|
||||
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call, at the return address of
|
||||
// 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
|
||||
// run statistics, exits 0
|
||||
// With --seconds S the run continues past that call; rt_frame_presented ends it once the world clock passed S.
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#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.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 : 0xe7a954ull)
|
||||
#define NVN_RET (RT_V143 ? 0xbe0360ull : 0xe7eb2cull)
|
||||
// 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_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.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check expects
|
||||
// the same.
|
||||
// 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_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 : inits_121)
|
||||
#define expected_inits (RT_V143 ? inits_143 : RT_V110 ? inits_110 : inits_121)
|
||||
|
||||
static volatile LONG inits_done, checkpoint_seen;
|
||||
static volatile int32_t inits_done, checkpoint_seen;
|
||||
uint64_t rt_guest_start_ns;
|
||||
|
||||
static const RtLoaded *module_named(const char *name) {
|
||||
@@ -71,7 +76,7 @@ static void cb_loader_ready(Cpu *c) {
|
||||
// DT_INIT registers the module with musl (__nnmusl_init_dso) and runs its init_array; the constructor flag reads 2
|
||||
// afterwards (MacNativeGames loader.c:458-462).
|
||||
static void cb_module_inits(Cpu *c) {
|
||||
if (InterlockedCompareExchange(&inits_done, 0, 0)) rt_fatal("boot: module constructors requested twice");
|
||||
if (plat_cas32(&inits_done, 0, 0)) rt_fatal("boot: module constructors requested twice");
|
||||
for (unsigned k = 0; k < sizeof inits_121 / sizeof *inits_121; k++) {
|
||||
const RtLoaded *m = module_named(expected_inits[k].name);
|
||||
uint64_t entries = m->init_array_size / 8;
|
||||
@@ -85,7 +90,7 @@ static void cb_module_inits(Cpu *c) {
|
||||
rt_fatal("boot: %s has %llu init_array entries, this game version has %llu", m->name, (unsigned long long)entries,
|
||||
(unsigned long long)expected_inits[k].entries);
|
||||
}
|
||||
InterlockedExchange(&inits_done, 1);
|
||||
plat_xchg32(&inits_done, 1);
|
||||
}
|
||||
|
||||
static void cb_module_finis(Cpu *c) {
|
||||
@@ -109,12 +114,12 @@ void rt_boot_event(const char *what, Cpu *c, const char *detail) {
|
||||
if (ret != main_mod->base + CHECKPOINT_RET)
|
||||
rt_fatal("boot: checkpoint %s reached from %s, expected main+%#llx", detail, at,
|
||||
(unsigned long long)CHECKPOINT_RET);
|
||||
if (!InterlockedCompareExchange(&inits_done, 0, 0)) rt_fatal("boot: checkpoint %s before DT_INIT", detail);
|
||||
if (!plat_cas32(&inits_done, 0, 0)) rt_fatal("boot: checkpoint %s before DT_INIT", detail);
|
||||
for (int i = 0; i < ldr_count(); i++)
|
||||
if (ctor_state(ldr_module(i)) != CTOR_DONE)
|
||||
rt_fatal("boot: checkpoint %s: %s constructor flag reads %u", detail, ldr_module(i)->name,
|
||||
ctor_state(ldr_module(i)));
|
||||
InterlockedExchange(&checkpoint_seen, 1);
|
||||
plat_xchg32(&checkpoint_seen, 1);
|
||||
rt_logf("boot: checkpoint %s from main+%#llx\n", detail, (unsigned long long)(ret - main_mod->base));
|
||||
return;
|
||||
}
|
||||
@@ -122,7 +127,7 @@ void rt_boot_event(const char *what, Cpu *c, const char *detail) {
|
||||
if (ret != main_mod->base + NVN_RET)
|
||||
rt_fatal("boot: first NVN call %s(\"%s\") from %s, expected main+%#llx", "nvnBootstrapLoader", detail, at,
|
||||
(unsigned long long)NVN_RET);
|
||||
if (!InterlockedCompareExchange(&checkpoint_seen, 0, 0))
|
||||
if (!plat_cas32(&checkpoint_seen, 0, 0))
|
||||
rt_fatal("boot: first NVN call before the SetFocusHandlingMode checkpoint");
|
||||
rt_logf("boot: first NVN call nvnBootstrapLoader(\"%s\") from main+%#llx\n", detail,
|
||||
(unsigned long long)(ret - main_mod->base));
|
||||
@@ -131,7 +136,7 @@ void rt_boot_event(const char *what, Cpu *c, const char *detail) {
|
||||
stats_line();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_exit(0); // other guest threads are still running; nothing of theirs needs an orderly end
|
||||
plat_exit(0); // other guest threads are still running; nothing of theirs needs an orderly end
|
||||
}
|
||||
rt_fatal("boot: unknown boot event %s (%s) from %s", what, detail ? detail : "", at);
|
||||
}
|
||||
@@ -241,7 +246,7 @@ static void series_shot(const tex_t *t, uint64_t n) {
|
||||
step = (unsigned long long)(ws * 1e6), next = lo;
|
||||
else if (!e || sscanf(e, "%llu-%llu:%llu", &lo, &hi, &step) != 3 || !step) hi = 0;
|
||||
dir = cfg_get("TOTK_SHOTS_DIR") ? cfg_get("TOTK_SHOTS_DIR") : ".";
|
||||
if (hi) CreateDirectoryA(dir, NULL);
|
||||
if (hi) plat_file_mkdir_p(dir);
|
||||
parsed = 1;
|
||||
}
|
||||
if (world) { // the first present at or after each step of the world clock
|
||||
@@ -275,7 +280,7 @@ static void series_shot(const tex_t *t, uint64_t n) {
|
||||
for (int c = 0; c < 4; c++) o[c] = (uint8_t)(sum[bgra && c != 3 ? 2 - c : c] / k);
|
||||
o[3] = 255;
|
||||
}
|
||||
char path[MAX_PATH];
|
||||
char path[PLAT_PATH_MAX];
|
||||
snprintf(path, sizeof path, "%s/f%05llu.png", dir, (unsigned long long)n);
|
||||
rt_write_png(path, out, ow, oh);
|
||||
free(out);
|
||||
@@ -304,7 +309,7 @@ void rt_frame_presented(const tex_t *t, uint64_t n) {
|
||||
vk_pipeline_save(); // no-op without the Vulkan back end
|
||||
prof_write();
|
||||
fflush(stderr);
|
||||
_exit(0);
|
||||
plat_exit(0);
|
||||
}
|
||||
|
||||
// ---- steps 9-14
|
||||
@@ -321,41 +326,38 @@ static void guest_main(Cpu *c) {
|
||||
rt_fatal("boot: nn::init::Start returned");
|
||||
}
|
||||
|
||||
static DWORD WINAPI prewarm_main(void *arg) {
|
||||
static void prewarm_main(void *arg) {
|
||||
(void)arg;
|
||||
vk_pipeline_prewarm();
|
||||
const char *prep = cfg_get("TOTK_TEST_PREPARE_SHADERS"); // Task 2.3 test hook; Task 2.6 builds the preparation
|
||||
if (prep && !strcmp(prep, "1")) {
|
||||
int cpus = (int)GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
|
||||
int cpus = (int)plat_cpu_usable();
|
||||
gfx_shader_prepare_recorded(cpus, cpus / 2 > 16 ? 16 : cpus / 2 < 1 ? 1 : cpus / 2);
|
||||
}
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
|
||||
void rt_boot(void) {
|
||||
check_npdm();
|
||||
if (!rt_config()->shot) vk_pipeline_async_enable(); // a --shot image must not depend on build timing
|
||||
// Shaders and pipelines recorded in earlier runs are made ready while the modules load; the guest waits for them.
|
||||
HANDLE warm = CreateThread(NULL, 0, prewarm_main, NULL, 0, NULL);
|
||||
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);
|
||||
hostfn_seal();
|
||||
ULONGLONG held = GetTickCount64();
|
||||
if (warm) WaitForSingleObject(warm, INFINITE), CloseHandle(warm);
|
||||
uint64_t held = plat_now_ms();
|
||||
if (warm) plat_thread_join(warm);
|
||||
else vk_pipeline_prewarm();
|
||||
held = GetTickCount64() - held;
|
||||
FILETIME created, x0, x1, x2, now;
|
||||
GetProcessTimes(GetCurrentProcess(), &created, &x0, &x1, &x2);
|
||||
GetSystemTimeAsFileTime(&now);
|
||||
ULARGE_INTEGER c = {.LowPart = created.dwLowDateTime, .HighPart = created.dwHighDateTime},
|
||||
n = {.LowPart = now.dwLowDateTime, .HighPart = now.dwHighDateTime};
|
||||
held = plat_now_ms() - held;
|
||||
rt_log("boot: guest starts %llu ms after process start (%llu ms of it waiting for the prewarm)",
|
||||
(unsigned long long)((n.QuadPart - c.QuadPart) / 10000), (unsigned long long)held);
|
||||
(unsigned long long)(plat_process_uptime_ns() / 1000000), (unsigned long long)held);
|
||||
rt_guest_start_ns = kern_now_ns();
|
||||
kern_run_on_guest_main(guest_main);
|
||||
rt_fatal("boot: the guest main thread ended");
|
||||
|
||||
@@ -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(); }
|
||||
+162
-35
@@ -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", "ASSUME_FOCUS", "ASTC", "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", "CLOTH_REALTIME", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
|
||||
"EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "FLIGHT_SPIKE_MS",
|
||||
"FLIGHT_THUMBS", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "GPL", "GPL_DRAW_BUILD", "GPU",
|
||||
"GPU_INDEX", "GPU_NAME", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
"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", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "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", "SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"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", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"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,11 +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"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"COLOR_PRESET", "display"},
|
||||
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
|
||||
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
|
||||
{"HDR_UI_NITS", "display"}, {"SHADOW_RES", "display"},
|
||||
{"DLAA", "display"}, {"DLSS", "display"}, {"FXAA", "display"},
|
||||
{"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"}, {"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"},
|
||||
@@ -74,12 +91,12 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"VK_DYN_VERTEX", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"},
|
||||
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
|
||||
};
|
||||
|
||||
// The settings the menu shows: the value in effect when the key is unset, and what the key accepts. The only place a
|
||||
@@ -94,18 +111,40 @@ 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|256|512|768|1024|1536|2048", RESTART},
|
||||
{"SHADOW_SCALE", "1", "1..4", RESTART},
|
||||
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
|
||||
{"PCSS", "0", "0..200", RESTART},
|
||||
// Game
|
||||
{"FPS_LIMIT", "60", "30|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"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},
|
||||
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
|
||||
// Sound, Controls, Menu
|
||||
{"VOLUME", "100", "0..100", LIVE},
|
||||
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox", LIVE},
|
||||
{"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},
|
||||
{"MENU_LANG", "", "en|ru", 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},
|
||||
@@ -115,7 +154,7 @@ static const CfgMeta metas[] = {
|
||||
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "", "0|1", RESTART},
|
||||
// textures and memory
|
||||
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
|
||||
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
||||
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "0", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
||||
// render fixes
|
||||
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
|
||||
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
|
||||
@@ -124,17 +163,18 @@ static const CfgMeta metas[] = {
|
||||
{"SPLIT_US", "1000", "0..20000", RESTART},
|
||||
// Vulkan
|
||||
{"VK_VALIDATION", "0", "0|1", RESTART}, {"VK_DYN_VERTEX", "", "0|1", RESTART},
|
||||
{"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
|
||||
{"VK_DYN_A2C", "", "0|1", RESTART}, {"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
|
||||
// diagnostics
|
||||
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
|
||||
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART},
|
||||
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
|
||||
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
|
||||
{"INPUT_RECORD", "", NULL, RESTART},
|
||||
};
|
||||
|
||||
static int is_known(const char *key) {
|
||||
for (size_t i = 0; i < sizeof known / sizeof *known; i++)
|
||||
if (!strcmp(known[i], key)) return 1;
|
||||
return 0;
|
||||
return !strncmp(key, "CHEAT_", 6); // cheats.c and the cheat patch sets (tools/cheats.py)
|
||||
}
|
||||
|
||||
static void clear(void) {
|
||||
@@ -165,11 +205,29 @@ static void add(char *key, char *value, int line) {
|
||||
for (int i = 0; i < n_entries; i++)
|
||||
if (!strcmp(entries[i].key, key)) { // a repeated key: the later line wins
|
||||
free(entries[i].value);
|
||||
entries[i].value = _strdup(value);
|
||||
entries[i].value = strdup(value);
|
||||
return;
|
||||
}
|
||||
if (n_entries == CFG_MAX_KEYS) return;
|
||||
entries[n_entries++] = (CfgEntry){_strdup(key), _strdup(value), 0, 0};
|
||||
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) {
|
||||
@@ -195,18 +253,79 @@ int cfg_load(const char *path) {
|
||||
p = eol + 1;
|
||||
}
|
||||
free(data);
|
||||
migrate();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *cfg_get(const char *name) {
|
||||
// ---- device profiles: defaults for a known machine, below the environment and totk.ini and above the table, so any
|
||||
// key the player sets wins and resetting a key in the menu returns to the profile's value. TOTK_DEVICE_PROFILE = auto
|
||||
// (default: the Steam Deck when the firmware says so), desktop (none) or deck.
|
||||
//
|
||||
// The Steam Deck (LCD Jupiter, OLED Galileo; 1280x800, 16:10, 4 Zen 2 cores, RDNA 2 with 8 CUs sharing 16 GB): the game
|
||||
// at the screen's 1280x800 with its own 5/6 scene (the aspect patch gives the camera its 16:10, resolution.c, and the
|
||||
// romfs-patches-1280x800 overlay the HUD's);
|
||||
// fullscreen and vsync, as gamescope composes Game Mode; 40 fps, which divides 40 Hz (and 120 Hz on the OLED).
|
||||
// Starting points to replace with measured ones (docs/tester-guide.md, Steam Deck).
|
||||
static const struct { const char *key, *value; } DECK[] = {
|
||||
{"FULLSCREEN", "1"}, {"WINDOW_SIZE", "1280x800"}, {"OUTPUT_RES", "1280x800"}, {"RENDER_SCALE", "game"},
|
||||
{"VK_PRESENT", "fifo"}, {"FPS_LIMIT", "40"},
|
||||
};
|
||||
static int profile = -1; // 0 none, 1 Steam Deck
|
||||
|
||||
static int read_line(const char *path, char *out, size_t n) {
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return 0;
|
||||
int ok = fgets(out, (int)n, f) != NULL;
|
||||
fclose(f);
|
||||
out[ok ? strcspn(out, "\r\n") : 0] = 0;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The Steam Deck: Valve's board, product Jupiter or Galileo (/sys/class/dmi/id); SteamDeck=1, which Steam sets on it.
|
||||
static int on_steam_deck(void) {
|
||||
const char *env = getenv("SteamDeck");
|
||||
if (env && !strcmp(env, "1")) return 1;
|
||||
char vendor[64], product[64];
|
||||
return read_line("/sys/class/dmi/id/board_vendor", vendor, sizeof vendor) && !strcmp(vendor, "Valve") &&
|
||||
read_line("/sys/class/dmi/id/product_name", product, sizeof product) &&
|
||||
(!strcmp(product, "Jupiter") || !strcmp(product, "Galileo"));
|
||||
}
|
||||
|
||||
static const char *raw_get(const char *name);
|
||||
|
||||
static int profile_of(void) {
|
||||
int p = __atomic_load_n(&profile, __ATOMIC_ACQUIRE);
|
||||
if (p >= 0) return p;
|
||||
const char *set = raw_get(PREFIX "DEVICE_PROFILE");
|
||||
if (set && !strcmp(set, "deck")) p = 1;
|
||||
else if (set && *set && strcmp(set, "auto")) p = 0;
|
||||
else p = on_steam_deck();
|
||||
__atomic_store_n(&profile, p, __ATOMIC_RELEASE);
|
||||
return p;
|
||||
}
|
||||
|
||||
const char *cfg_device_profile(void) { return profile_of() ? "deck" : "desktop"; }
|
||||
|
||||
static const char *raw_get(const char *name) {
|
||||
const char *e = getenv(name);
|
||||
if (e || strncmp(name, PREFIX, PREFIX_LEN)) return e;
|
||||
int n = __atomic_load_n(&n_entries, __ATOMIC_ACQUIRE);
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!_stricmp(entries[i].key, name + PREFIX_LEN)) return __atomic_load_n(&entries[i].value, __ATOMIC_ACQUIRE);
|
||||
if (!strcasecmp(entries[i].key, name + PREFIX_LEN)) return __atomic_load_n(&entries[i].value, __ATOMIC_ACQUIRE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *cfg_get(const char *name) {
|
||||
const char *v = raw_get(name);
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN) || !profile_of()) return v;
|
||||
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
|
||||
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Whether name's value comes from the player (the environment or totk.ini), not a profile or the table.
|
||||
int cfg_is_set(const char *name) { return raw_get(name) != NULL; }
|
||||
|
||||
unsigned cfg_version(void) { return __atomic_load_n(&version, __ATOMIC_ACQUIRE); }
|
||||
|
||||
int cfg_from_env(const char *name) { return getenv(name) != NULL; }
|
||||
@@ -221,12 +340,12 @@ void cfg_set(const char *name, const char *value) {
|
||||
while (i < n && strcmp(entries[i].key, key)) i++;
|
||||
if (i < n) {
|
||||
if (!entries[i].value || strcmp(entries[i].value, value)) {
|
||||
__atomic_store_n(&entries[i].value, _strdup(value), __ATOMIC_RELEASE); // the old string stays for readers
|
||||
__atomic_store_n(&entries[i].value, strdup(value), __ATOMIC_RELEASE); // the old string stays for readers
|
||||
entries[i].dirty = 1;
|
||||
}
|
||||
entries[i].removed = 0; // a value set after cfg_reset replaces the line instead of deleting it
|
||||
} else if (n < CFG_MAX_KEYS) {
|
||||
entries[n] = (CfgEntry){_strdup(key), _strdup(value), 1, 0};
|
||||
entries[n] = (CfgEntry){strdup(key), strdup(value), 1, 0};
|
||||
__atomic_store_n(&n_entries, n + 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
__atomic_add_fetch(&version, 1, __ATOMIC_RELEASE);
|
||||
@@ -246,7 +365,7 @@ int cfg_reset(const char *const *keys, int n) {
|
||||
while (i < n_entries && strcmp(entries[i].key, name)) i++;
|
||||
if (i == n_entries) { // no value in memory ("key =" in the file): the line still has to go
|
||||
if (i == CFG_MAX_KEYS) continue;
|
||||
entries[i] = (CfgEntry){_strdup(name), NULL, 0, 1};
|
||||
entries[i] = (CfgEntry){strdup(name), NULL, 0, 1};
|
||||
__atomic_store_n(&n_entries, i + 1, __ATOMIC_RELEASE);
|
||||
} else if (entries[i].value) {
|
||||
__atomic_store_n(&entries[i].value, NULL, __ATOMIC_RELEASE); // the old string stays for readers
|
||||
@@ -393,6 +512,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;
|
||||
}
|
||||
|
||||
@@ -542,7 +662,7 @@ static int find_ini(void) {
|
||||
if (ini_path[0]) return 1;
|
||||
char path[1024];
|
||||
if (!plat_exe_path(path, sizeof path)) return 0;
|
||||
char *cut = strrchr(path, '\\');
|
||||
char *cut = plat_last_sep(path);
|
||||
if (!cut) cut = strrchr(path, '/');
|
||||
if (!cut || cut + 10 >= path + sizeof path) return 0;
|
||||
strcpy(cut + 1, "totk.ini");
|
||||
@@ -556,7 +676,7 @@ int cfg_save(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cfg_init(void) {
|
||||
static void load_ini(void) {
|
||||
if (!find_ini()) return;
|
||||
if (!cfg_load(ini_path)) {
|
||||
rt_log("cfg: no settings file at %s", ini_path);
|
||||
@@ -569,3 +689,10 @@ void cfg_init(void) {
|
||||
if (getenv(var)) rt_log("cfg: %s is set in the environment and overrides totk.ini", var);
|
||||
}
|
||||
}
|
||||
|
||||
void cfg_init(void) {
|
||||
load_ini();
|
||||
if (profile_of())
|
||||
rt_log("cfg: device profile Steam Deck: defaults for the keys totk.ini does not set (TOTK_DEVICE_PROFILE=desktop "
|
||||
"turns it off)");
|
||||
}
|
||||
+5
-1
@@ -2,7 +2,7 @@
|
||||
#pragma once
|
||||
|
||||
// getenv for a setting: the environment variable when set, else the totk.ini key (the name without "TOTK_", any
|
||||
// case), else NULL. Same string semantics as getenv; the pointer stays valid for the process (a value cfg_set replaces
|
||||
// case), else the device profile's default (cfg.c: the Steam Deck), else NULL. Same string semantics as getenv; the pointer stays valid for the process (a value cfg_set replaces
|
||||
// is kept). A name without the TOTK_ prefix goes to getenv unchanged. Safe from any thread once cfg_init has returned.
|
||||
const char *cfg_get(const char *name);
|
||||
|
||||
@@ -21,6 +21,10 @@ void cfg_set(const char *name, const char *value);
|
||||
unsigned cfg_version(void);
|
||||
// 1 when the environment sets name: cfg_set cannot change what cfg_get returns (the menu greys the setting out).
|
||||
int cfg_from_env(const char *name);
|
||||
// 1 when the player set name (the environment or totk.ini), 0 when cfg_get gives a device profile's default or NULL.
|
||||
int cfg_is_set(const char *name);
|
||||
// The device profile in use, "deck" or "desktop" (TOTK_DEVICE_PROFILE, else what the machine is).
|
||||
const char *cfg_device_profile(void);
|
||||
// Writes the keys cfg_set changed since the last save into totk.ini next to totk.exe (cfg_save_to: the file at path).
|
||||
// Only those lines change: the last line of a changed key gets the new value, a missing key goes under its section
|
||||
// (after the commented example line of the key when the section has one; the section is added at the end of the file
|
||||
|
||||
+111
-26
@@ -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>
|
||||
@@ -17,7 +18,7 @@ static int fails;
|
||||
int probe_cfg(void) {
|
||||
char path[1024];
|
||||
if (!plat_exe_path(path, sizeof path)) return 1;
|
||||
char *cut = strrchr(path, '\\');
|
||||
char *cut = plat_last_sep(path);
|
||||
if (!cut) return 1;
|
||||
strcpy(cut + 1, "cfg-probe.ini");
|
||||
static const char ini[] =
|
||||
@@ -35,8 +36,8 @@ int probe_cfg(void) {
|
||||
"no_such_setting = 1\r\n"
|
||||
"no equals sign\r\n"
|
||||
"last = no newline at the end";
|
||||
_putenv_s("TOTK_LOCALE", ""); // an empty variable is how _putenv_s spells "remove" on Windows
|
||||
_putenv_s("TOTK_FPS60", "");
|
||||
plat_setenv("TOTK_LOCALE", NULL);
|
||||
plat_setenv("TOTK_FPS60", NULL);
|
||||
CHECK(plat_file_write(path, ini, sizeof ini - 1), "write the probe ini");
|
||||
CHECK(cfg_load(path) == 1, "load the probe ini");
|
||||
CHECK(EQ(cfg_get("TOTK_FPS60"), "0"), "value read, whitespace trimmed, lower-case key");
|
||||
@@ -48,9 +49,9 @@ int probe_cfg(void) {
|
||||
CHECK(cfg_get("TOTK_CLOTH_REALTIME") == NULL, "absent key is NULL");
|
||||
CHECK(cfg_get("TOTK_comment") == NULL, "comment is not a key");
|
||||
CHECK(EQ(cfg_get("TOTK_NO_SUCH_SETTING"), "1"), "unknown key is still readable");
|
||||
_putenv_s("TOTK_FPS60", "1");
|
||||
plat_setenv("TOTK_FPS60", "1");
|
||||
CHECK(EQ(cfg_get("TOTK_FPS60"), "1"), "environment overrides the file");
|
||||
_putenv_s("TOTK_FPS60", "");
|
||||
plat_setenv("TOTK_FPS60", NULL);
|
||||
CHECK(EQ(cfg_get("TOTK_FPS60"), "0"), "file value returns when the variable is removed");
|
||||
plat_file_delete(path);
|
||||
CHECK(cfg_load(path) == 0, "missing file is not an error");
|
||||
@@ -90,9 +91,9 @@ int probe_cfg(void) {
|
||||
"\r\n"
|
||||
"[audio]\r\n"
|
||||
"volume = 40\r\n";
|
||||
_putenv_s("TOTK_MOTION", "");
|
||||
_putenv_s("TOTK_VOLUME", "");
|
||||
_putenv_s("TOTK_RUMBLE_STRENGTH", "");
|
||||
plat_setenv("TOTK_MOTION", NULL);
|
||||
plat_setenv("TOTK_VOLUME", NULL);
|
||||
plat_setenv("TOTK_RUMBLE_STRENGTH", NULL);
|
||||
CHECK(plat_file_write(path, before, sizeof before - 1), "write the save ini");
|
||||
CHECK(cfg_load(path) == 1, "load the save ini");
|
||||
unsigned v0 = cfg_version();
|
||||
@@ -117,11 +118,11 @@ int probe_cfg(void) {
|
||||
EQ(cfg_get("TOTK_RUMBLE_STRENGTH"), "80") && EQ(cfg_get("TOTK_VOLUME"), "40") &&
|
||||
EQ(cfg_get("TOTK_ROMFS_MODS"), "C:\\mods"), "the saved file reads back");
|
||||
const char *held = cfg_get("TOTK_VOLUME");
|
||||
_putenv_s("TOTK_VOLUME", "70");
|
||||
plat_setenv("TOTK_VOLUME", "70");
|
||||
cfg_set("TOTK_VOLUME", "90");
|
||||
CHECK(cfg_from_env("TOTK_VOLUME") && EQ(cfg_get("TOTK_VOLUME"), "70"), "environment wins over cfg_set");
|
||||
CHECK(EQ(held, "40"), "a pointer cfg_get handed out stays valid after cfg_set");
|
||||
_putenv_s("TOTK_VOLUME", "");
|
||||
plat_setenv("TOTK_VOLUME", NULL);
|
||||
CHECK(!cfg_from_env("TOTK_VOLUME") && EQ(cfg_get("TOTK_VOLUME"), "90"), "cfg_set value returns without the variable");
|
||||
plat_file_delete(path);
|
||||
CHECK(cfg_save_to(path) == 1, "cfg_save_to creates a missing file");
|
||||
@@ -147,7 +148,7 @@ int probe_cfg(void) {
|
||||
// cfg_reset: the key's active lines leave totk.ini, the commented example stays
|
||||
static const char rbase[] = "[audio]\n# volume = 100\n[input]\nmotion = 0\n";
|
||||
static const char *const vol[] = {"VOLUME"};
|
||||
_putenv_s("TOTK_VOLUME", "");
|
||||
plat_setenv("TOTK_VOLUME", NULL);
|
||||
CHECK(plat_file_write(path, rbase, sizeof rbase - 1), "write the reset ini");
|
||||
CHECK(cfg_load(path) == 1, "load the reset ini");
|
||||
cfg_set("TOTK_VOLUME", "40");
|
||||
@@ -186,9 +187,9 @@ int probe_cfg(void) {
|
||||
|
||||
// a key the environment sets is skipped and keeps its file line
|
||||
CHECK(cfg_load(path) == 1 && EQ(cfg_get("TOTK_VOLUME"), "60"), "reset: reload");
|
||||
_putenv_s("TOTK_VOLUME", "70");
|
||||
plat_setenv("TOTK_VOLUME", "70");
|
||||
CHECK(cfg_reset(vol, 1) == 0, "reset: an environment-set key is skipped");
|
||||
_putenv_s("TOTK_VOLUME", "");
|
||||
plat_setenv("TOTK_VOLUME", NULL);
|
||||
CHECK(EQ(cfg_get("TOTK_VOLUME"), "60"), "reset: the skipped key keeps its value");
|
||||
CHECK(cfg_save_to(path) == 1, "reset: save after a skip");
|
||||
got = plat_file_read(path, &got_n);
|
||||
@@ -200,7 +201,7 @@ int probe_cfg(void) {
|
||||
cfg_set("TOTK_VOLUME", "20");
|
||||
char bad[1100];
|
||||
snprintf(bad, sizeof bad, "%s", path);
|
||||
strcpy(strrchr(bad, '\\') + 1, "no-such-dir\\cfg-probe.ini");
|
||||
strcpy(plat_last_sep(bad) + 1, "no-such-dir/cfg-probe.ini");
|
||||
CHECK(cfg_save_to(bad) == 0 && cfg_last_save_ok() == 0 && EQ(cfg_get("TOTK_VOLUME"), "20"),
|
||||
"save status: an unwritable path fails, the value stays");
|
||||
cfg_set("TOTK_VOLUME", "21");
|
||||
@@ -261,11 +262,64 @@ int probe_cfg(void) {
|
||||
CHECK((int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768, "mouse: clamped to the unit circle");
|
||||
input_mouse_stick(30, 0, 1.0 / 60, 0, st);
|
||||
CHECK(st[0] == 0 && st[1] == 0, "mouse: sensitivity 0 is off");
|
||||
input_pad_stick(3000, -2000, 0.15f, st);
|
||||
CHECK(st[0] == 0 && st[1] == 0, "pad stick: an 11% drift inside a 15% dead zone reads centred");
|
||||
input_pad_stick(4000, -3000, 0, st);
|
||||
CHECK(st[0] == 4000 && st[1] == -3000, "pad stick: dead zone 0 passes the stick unchanged");
|
||||
input_pad_stick(32767, 0, 0.15f, st);
|
||||
CHECK(st[0] == 32767 && st[1] == 0, "pad stick: full deflection still reaches full");
|
||||
input_pad_stick(-32768, -32768, 0.15f, st);
|
||||
CHECK(st[0] < 0 && st[1] < 0 && st[0] >= -32767 && (int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768,
|
||||
"pad stick: a square gate's corner stays on the unit circle");
|
||||
input_pad_stick(0, 32767 / 2 + 1, 0.2f, st);
|
||||
CHECK(abs(st[1] - 32767 * 3 / 8) <= 3, "pad stick: half deflection past a 20% zone is (0.5 - 0.2) / 0.8 of full");
|
||||
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");
|
||||
CHECK(input_pad_layout_pick("Xbox", 1) == PAD_LAYOUT_XBOX && input_pad_layout_pick("nintendo", 0) == PAD_LAYOUT_NINTENDO,
|
||||
"pad layout: a set layout wins over the labels");
|
||||
CHECK(input_pad_layout_pick("xbox_prompts", 0) == PAD_LAYOUT_NINTENDO &&
|
||||
input_pad_layout_pick("Xbox_Prompts", 1) == PAD_LAYOUT_NINTENDO,
|
||||
"pad layout: 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) &&
|
||||
@@ -280,6 +334,13 @@ int probe_cfg(void) {
|
||||
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_SOUTH] = 1}, PAD_LAYOUT_XBOX) == 0,
|
||||
"pad layout: a bind line replaces the layout's own button");
|
||||
input_map_reset();
|
||||
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_XBOX) == 0,
|
||||
"pad layout: a back paddle is unbound by default");
|
||||
CHECK(input_map_bind("ZL", "pad:default, pad:rpaddle1") &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_ZL] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL,
|
||||
"bind: pad:rpaddle1 adds a back paddle, pad:default keeps the trigger");
|
||||
input_map_reset();
|
||||
// Bind lines for stick directions and default parts; the remapping rows' edits (input_map.c)
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_I] = 1;
|
||||
@@ -331,24 +392,42 @@ int probe_cfg(void) {
|
||||
CHECK(!strcmp(line, "none"), "edit: a stick direction without keys is none");
|
||||
// defaults live in the CfgMeta table: cfg_value and the typed accessors fall back to it
|
||||
cfg_load("cfg-probe-missing.ini");
|
||||
_putenv_s("TOTK_FPS60", "");
|
||||
_putenv_s("TOTK_MOUSE_SENS", "");
|
||||
_putenv_s("TOTK_FREE_CORES", "");
|
||||
plat_setenv("TOTK_FPS60", NULL);
|
||||
plat_setenv("TOTK_MOUSE_SENS", NULL);
|
||||
plat_setenv("TOTK_FREE_CORES", NULL);
|
||||
CHECK(cfg_bool("TOTK_FPS60") == 1, "default: unset FPS60 is on");
|
||||
CHECK(EQ(cfg_value("TOTK_VK_PRESENT"), "mailbox") && cfg_int("TOTK_SPLIT_DRAWS") == 64, "default: text and integer");
|
||||
_putenv_s("TOTK_MOUSE_SENS", "2.5");
|
||||
plat_setenv("TOTK_MOUSE_SENS", "2.5");
|
||||
CHECK(cfg_double("TOTK_MOUSE_SENS") == 2.5, "double: the environment value");
|
||||
_putenv_s("TOTK_MOUSE_SENS", "fast");
|
||||
plat_setenv("TOTK_MOUSE_SENS", "fast");
|
||||
CHECK(cfg_double("TOTK_MOUSE_SENS") == 1.0, "double: a value that does not parse gives the default");
|
||||
_putenv_s("TOTK_MOUSE_SENS", "");
|
||||
plat_setenv("TOTK_MOUSE_SENS", NULL);
|
||||
cfg_set("TOTK_MOUSE_SENS", "3");
|
||||
CHECK(cfg_double("TOTK_MOUSE_SENS") == 3.0 && cfg_int("TOTK_MOUSE_SENS") == 3, "a cfg_set value beats the default");
|
||||
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores unset: off");
|
||||
_putenv_s("TOTK_FREE_CORES", "0");
|
||||
plat_setenv("TOTK_FREE_CORES", "0");
|
||||
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores 0 gives the same as unset");
|
||||
_putenv_s("TOTK_FREE_CORES", "1");
|
||||
plat_setenv("TOTK_FREE_CORES", "1");
|
||||
CHECK(cfg_bool("TOTK_FREE_CORES") == 1, "free_cores 1: on");
|
||||
_putenv_s("TOTK_FREE_CORES", "");
|
||||
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++) {
|
||||
@@ -357,14 +436,15 @@ int probe_cfg(void) {
|
||||
listed++;
|
||||
char name[96];
|
||||
snprintf(name, sizeof name, "TOTK_%s", m->key);
|
||||
_putenv_s(name, "");
|
||||
plat_setenv(name, "");
|
||||
CHECK(EQ(cfg_value(name), m->def), "cfg_value returns the table default");
|
||||
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;) {
|
||||
@@ -376,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,71 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/color_grade.comp; do not edit.
|
||||
// source sha256 c93d6ac75667bdee5442d7e94fbf0bd6e7fe7250caa98974424aead10f554106
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t color_grade_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x00000073, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00060010, 0x00000004,
|
||||
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000000d, 0x0000000b, 0x0000001c,
|
||||
0x00030047, 0x00000015, 0x00000002, 0x00050048, 0x00000015, 0x00000000, 0x00000023, 0x00000000,
|
||||
0x00050048, 0x00000015, 0x00000001, 0x00000023, 0x00000008, 0x00050048, 0x00000015, 0x00000002,
|
||||
0x00000023, 0x00000010, 0x00040047, 0x00000029, 0x00000021, 0x00000000, 0x00040047, 0x00000029,
|
||||
0x00000022, 0x00000000, 0x00030047, 0x0000005d, 0x00000019, 0x00040047, 0x0000005d, 0x00000021,
|
||||
0x00000001, 0x00040047, 0x0000005d, 0x00000022, 0x00000000, 0x00040047, 0x0000006a, 0x0000000b,
|
||||
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
|
||||
0x00000020, 0x00000001, 0x00040017, 0x00000007, 0x00000006, 0x00000002, 0x00040015, 0x0000000a,
|
||||
0x00000020, 0x00000000, 0x00040017, 0x0000000b, 0x0000000a, 0x00000003, 0x00040020, 0x0000000c,
|
||||
0x00000001, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000001, 0x00040017, 0x0000000e,
|
||||
0x0000000a, 0x00000002, 0x00030016, 0x00000013, 0x00000020, 0x00040017, 0x00000014, 0x00000013,
|
||||
0x00000004, 0x0005001e, 0x00000015, 0x00000007, 0x00000007, 0x00000014, 0x00040020, 0x00000016,
|
||||
0x00000009, 0x00000015, 0x0004003b, 0x00000016, 0x00000017, 0x00000009, 0x0004002b, 0x00000006,
|
||||
0x00000018, 0x00000001, 0x00040020, 0x00000019, 0x00000009, 0x00000007, 0x00020014, 0x0000001c,
|
||||
0x00040017, 0x0000001d, 0x0000001c, 0x00000002, 0x00040017, 0x00000023, 0x00000013, 0x00000003,
|
||||
0x00090019, 0x00000026, 0x00000013, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
|
||||
0x00000000, 0x0003001b, 0x00000027, 0x00000026, 0x00040020, 0x00000028, 0x00000000, 0x00000027,
|
||||
0x0004003b, 0x00000028, 0x00000029, 0x00000000, 0x0004002b, 0x00000006, 0x0000002c, 0x00000000,
|
||||
0x0004002b, 0x00000006, 0x00000033, 0x00000002, 0x0004002b, 0x0000000a, 0x00000034, 0x00000000,
|
||||
0x00040020, 0x00000035, 0x00000009, 0x00000013, 0x0004002b, 0x00000013, 0x0000003c, 0x3f000000,
|
||||
0x0004002b, 0x0000000a, 0x0000003f, 0x00000001, 0x0004002b, 0x00000013, 0x00000046, 0x3e59b3d0,
|
||||
0x0004002b, 0x00000013, 0x00000047, 0x3f371759, 0x0004002b, 0x00000013, 0x00000048, 0x3d93dd98,
|
||||
0x0006002c, 0x00000023, 0x00000049, 0x00000046, 0x00000047, 0x00000048, 0x0004002b, 0x0000000a,
|
||||
0x0000004d, 0x00000002, 0x0004002b, 0x00000013, 0x00000053, 0x00000000, 0x0006002c, 0x00000023,
|
||||
0x00000054, 0x00000053, 0x00000053, 0x00000053, 0x0004002b, 0x0000000a, 0x00000056, 0x00000003,
|
||||
0x00090019, 0x0000005b, 0x00000013, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
|
||||
0x00000002, 0x00040020, 0x0000005c, 0x00000000, 0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d,
|
||||
0x00000000, 0x0004002b, 0x00000013, 0x00000061, 0x3f800000, 0x0004002b, 0x0000000a, 0x00000069,
|
||||
0x00000008, 0x0006002c, 0x0000000b, 0x0000006a, 0x00000069, 0x00000069, 0x0000003f, 0x0006002c,
|
||||
0x00000023, 0x00000071, 0x0000003c, 0x0000003c, 0x0000003c, 0x0006002c, 0x00000023, 0x00000072,
|
||||
0x00000061, 0x00000061, 0x00000061, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x000300f7, 0x0000006b, 0x00000000, 0x000300fb, 0x00000034, 0x0000006c,
|
||||
0x000200f8, 0x0000006c, 0x0004003d, 0x0000000b, 0x0000000f, 0x0000000d, 0x0007004f, 0x0000000e,
|
||||
0x00000010, 0x0000000f, 0x0000000f, 0x00000000, 0x00000001, 0x0004007c, 0x00000007, 0x00000011,
|
||||
0x00000010, 0x00050041, 0x00000019, 0x0000001a, 0x00000017, 0x00000018, 0x0004003d, 0x00000007,
|
||||
0x0000001b, 0x0000001a, 0x000500af, 0x0000001d, 0x0000001e, 0x00000011, 0x0000001b, 0x0004009a,
|
||||
0x0000001c, 0x0000001f, 0x0000001e, 0x000300f7, 0x00000021, 0x00000000, 0x000400fa, 0x0000001f,
|
||||
0x00000020, 0x00000021, 0x000200f8, 0x00000020, 0x000200f9, 0x0000006b, 0x000200f8, 0x00000021,
|
||||
0x0004003d, 0x00000027, 0x0000002a, 0x00000029, 0x00050041, 0x00000019, 0x0000002d, 0x00000017,
|
||||
0x0000002c, 0x0004003d, 0x00000007, 0x0000002e, 0x0000002d, 0x00050080, 0x00000007, 0x0000002f,
|
||||
0x00000011, 0x0000002e, 0x00040064, 0x00000026, 0x00000030, 0x0000002a, 0x0007005f, 0x00000014,
|
||||
0x00000031, 0x00000030, 0x0000002f, 0x00000002, 0x0000002c, 0x0008004f, 0x00000023, 0x00000032,
|
||||
0x00000031, 0x00000031, 0x00000000, 0x00000001, 0x00000002, 0x00060041, 0x00000035, 0x00000036,
|
||||
0x00000017, 0x00000033, 0x00000034, 0x0004003d, 0x00000013, 0x00000037, 0x00000036, 0x00060050,
|
||||
0x00000023, 0x00000039, 0x00000037, 0x00000037, 0x00000037, 0x00050081, 0x00000023, 0x0000003a,
|
||||
0x00000032, 0x00000039, 0x00050083, 0x00000023, 0x0000003e, 0x0000003a, 0x00000071, 0x00060041,
|
||||
0x00000035, 0x00000040, 0x00000017, 0x00000033, 0x0000003f, 0x0004003d, 0x00000013, 0x00000041,
|
||||
0x00000040, 0x0005008e, 0x00000023, 0x00000042, 0x0000003e, 0x00000041, 0x00050081, 0x00000023,
|
||||
0x00000044, 0x00000042, 0x00000071, 0x00050094, 0x00000013, 0x0000004a, 0x00000044, 0x00000049,
|
||||
0x00060050, 0x00000023, 0x0000004b, 0x0000004a, 0x0000004a, 0x0000004a, 0x00060041, 0x00000035,
|
||||
0x0000004e, 0x00000017, 0x00000033, 0x0000004d, 0x0004003d, 0x00000013, 0x0000004f, 0x0000004e,
|
||||
0x00060050, 0x00000023, 0x00000050, 0x0000004f, 0x0000004f, 0x0000004f, 0x0008000c, 0x00000023,
|
||||
0x00000051, 0x00000001, 0x0000002e, 0x0000004b, 0x00000044, 0x00000050, 0x0007000c, 0x00000023,
|
||||
0x00000055, 0x00000001, 0x00000028, 0x00000051, 0x00000054, 0x00060041, 0x00000035, 0x00000057,
|
||||
0x00000017, 0x00000033, 0x00000056, 0x0004003d, 0x00000013, 0x00000058, 0x00000057, 0x00060050,
|
||||
0x00000023, 0x00000059, 0x00000058, 0x00000058, 0x00000058, 0x0007000c, 0x00000023, 0x0000005a,
|
||||
0x00000001, 0x0000001a, 0x00000055, 0x00000059, 0x0004003d, 0x0000005b, 0x0000005e, 0x0000005d,
|
||||
0x0008000c, 0x00000023, 0x00000064, 0x00000001, 0x0000002b, 0x0000005a, 0x00000054, 0x00000072,
|
||||
0x00050051, 0x00000013, 0x00000065, 0x00000064, 0x00000000, 0x00050051, 0x00000013, 0x00000066,
|
||||
0x00000064, 0x00000001, 0x00050051, 0x00000013, 0x00000067, 0x00000064, 0x00000002, 0x00070050,
|
||||
0x00000014, 0x00000068, 0x00000065, 0x00000066, 0x00000067, 0x00000061, 0x00040063, 0x0000005e,
|
||||
0x00000011, 0x00000068, 0x000200f9, 0x0000006b, 0x000200f8, 0x0000006b, 0x000100fd, 0x00010038,
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
+9
-17
@@ -12,17 +12,13 @@
|
||||
// is also fixed at init; they are looked up only after the flat table misses.
|
||||
#include "dispatch.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
#define TABLE_SPAN_LIMIT (1ull << 30) // guest bytes covered by the table, and its size in bytes
|
||||
#define HOST_CAP 8192 // host function registry, power of two
|
||||
@@ -51,6 +47,12 @@ void rt_fatal(const char *fmt, ...) {
|
||||
rt_vlogf(fmt, ap);
|
||||
rt_log_raw("\n", 1);
|
||||
va_end(ap);
|
||||
// A failed allocation (malloc, realloc, VirtualAlloc) means the commit limit was reached: this line tells a leak of
|
||||
// ours (process private bytes near the limit) from a machine short of page file.
|
||||
double proc, used, limit;
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
rt_log("fatal: memory commit: this process %.0f MB, the system %.0f of %.0f MB", proc, used, limit);
|
||||
kern_free_cores_note();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_Exit(70);
|
||||
@@ -92,21 +94,11 @@ void rt_register_host(uint64_t addr, GuestFn fn) {
|
||||
}
|
||||
|
||||
static void *table_alloc(uint64_t bytes) {
|
||||
#ifdef _WIN32
|
||||
return VirtualAlloc(NULL, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); // zero pages, backed on first write
|
||||
#else
|
||||
void *p = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
return p == MAP_FAILED ? NULL : p;
|
||||
#endif
|
||||
return plat_mem_alloc(bytes); // zero pages, backed on first write
|
||||
}
|
||||
|
||||
static void table_seal(void *p, uint64_t bytes) {
|
||||
#ifdef _WIN32
|
||||
DWORD old;
|
||||
if (!VirtualProtect(p, bytes, PAGE_READONLY, &old)) rt_fatal("dispatch: VirtualProtect failed (%lu)", GetLastError());
|
||||
#else
|
||||
if (mprotect(p, bytes, PROT_READ)) rt_fatal("dispatch: mprotect failed");
|
||||
#endif
|
||||
if (!plat_mem_protect((uint64_t)(uintptr_t)p, bytes, PLAT_PROT_READ)) rt_fatal("dispatch: sealing the table failed");
|
||||
}
|
||||
|
||||
void rt_dispatch_init(const RtModule *mods, int count) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
File renamed without changes.
+30
-33
@@ -1,5 +1,5 @@
|
||||
// TOTK_FLIGHT=1 flight recorder: a ring of the last TOTK_FLIGHT_FRAMES (600) presents, one record each (interval,
|
||||
// render-thread blocked time per perf kind, event counts) and a 320x180 thumbnail each (gfx_window.c, blitted on the
|
||||
// render-thread blocked time per perf kind, event counts) and a 320x180 thumbnail each (gfx_present.c, blitted on the
|
||||
// GPU by the present thread). A dump goes to <dir>/<YYYYMMDD-HHMMSS>-f<frame>/: frames.tsv and f<frame>.png per
|
||||
// thumbnail, written by a background thread. Triggers: F8 while the game window has focus, a frame interval over
|
||||
// TOTK_FLIGHT_SPIKE_MS (100; 0 off; at most one dump per 10 s), and TOTK_FLIGHT_AT=<frame>[,<frame>...]. A dump holds
|
||||
@@ -7,7 +7,6 @@
|
||||
//
|
||||
// Records come from perf_present (prof_win.c) on the render thread; the counts are differences of the TOTK_TRACE=perf
|
||||
// running totals and of vk_exec_frame. Off, the only cost is the flight_on branch in perf_present and vk_backend.c.
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -16,8 +15,10 @@
|
||||
#include "flight.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define FLIGHT_POST 30 // presents recorded after the trigger before the dump starts
|
||||
#define FLIGHT_SPIKE_GAP_NS 10000000000ull
|
||||
@@ -41,7 +42,7 @@ typedef struct {
|
||||
int n; // records, oldest first
|
||||
uint64_t trig_frame, trig_interval_us;
|
||||
const char *why;
|
||||
char dir[MAX_PATH];
|
||||
char dir[PLAT_PATH_MAX];
|
||||
Rec r[];
|
||||
} DumpJob;
|
||||
|
||||
@@ -54,7 +55,7 @@ static uint64_t dump_at, trig_frame, trig_interval_us, last_spike_ns;
|
||||
static const char *trig_why;
|
||||
static uint64_t at_frames[16];
|
||||
static int n_at;
|
||||
static char base_dir[MAX_PATH];
|
||||
static char base_dir[PLAT_PATH_MAX];
|
||||
static uint64_t prev_cnt[PERF_CNT_N], prev_xlated;
|
||||
static VkExecFrame prev_exec;
|
||||
static volatile uint64_t cost_ns, cost_max_ns; // render-thread time in flight_frame, for the dump log line
|
||||
@@ -79,15 +80,14 @@ void flight_init(void) {
|
||||
p = *end == ',' ? end + 1 : end;
|
||||
}
|
||||
const char *d = cfg_get("TOTK_FLIGHT_DIR");
|
||||
char tmp[MAX_PATH];
|
||||
char tmp[PLAT_PATH_MAX];
|
||||
if (d && *d) snprintf(tmp, sizeof tmp, "%s", d);
|
||||
else { // <exe dir>/../flight: build/flight for build/v121*/totk.exe
|
||||
DWORD k = GetModuleFileNameA(NULL, tmp, sizeof tmp);
|
||||
char *slash = k ? strrchr(tmp, '\\') : NULL;
|
||||
if (slash) snprintf(slash, sizeof tmp - (size_t)(slash - tmp), "\\..\\flight");
|
||||
char *slash = plat_exe_path(tmp, sizeof tmp) ? plat_last_sep(tmp) : NULL;
|
||||
if (slash) snprintf(slash, sizeof tmp - (size_t)(slash - tmp), "/../flight");
|
||||
else snprintf(tmp, sizeof tmp, "flight");
|
||||
}
|
||||
if (!GetFullPathNameA(tmp, sizeof base_dir, base_dir, NULL)) snprintf(base_dir, sizeof base_dir, "%s", tmp);
|
||||
if (!plat_full_path(tmp, base_dir, sizeof base_dir)) snprintf(base_dir, sizeof base_dir, "%s", tmp);
|
||||
thumbs_on = env_u("TOTK_FLIGHT_THUMBS", 1) != 0;
|
||||
if (thumbs_on) vk_thumbs_enable(ring_n);
|
||||
flight_on = 1;
|
||||
@@ -225,15 +225,15 @@ static int write_png(const char *path, const uint8_t *rgba, PngBufs *pb) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- dump thread
|
||||
static DWORD WINAPI dump_main(void *arg) {
|
||||
static void dump_main(void *arg) {
|
||||
DumpJob *j = arg;
|
||||
uint64_t t_start = GetTickCount64();
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
CreateDirectoryA(base_dir, NULL);
|
||||
if (!CreateDirectoryA(j->dir, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
||||
rt_log("flight: cannot create %s (error %lu)", j->dir, GetLastError());
|
||||
uint64_t t_start = plat_now_ms();
|
||||
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
|
||||
plat_file_mkdir_p(j->dir);
|
||||
if (!plat_dir_exists(j->dir)) {
|
||||
rt_log("flight: cannot create %s", j->dir);
|
||||
free(j);
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
uint8_t *have = calloc((size_t)j->n, 1);
|
||||
int n_thumbs = 0;
|
||||
@@ -250,23 +250,23 @@ static DWORD WINAPI dump_main(void *arg) {
|
||||
// Oldest first: each slot is copied out long before the presents that go on meanwhile reach it again. The newest
|
||||
// few complete only when their present fence has been waited for, so they get a second try.
|
||||
for (int pass = 0; pass < 2 && pb.raw && pb.z && pb.head && px; pass++) {
|
||||
if (pass) Sleep(200);
|
||||
if (pass) plat_sleep_ms(200);
|
||||
for (int i = pass ? (j->n > 8 ? j->n - 8 : 0) : 0; i < j->n; i++) {
|
||||
if (have[i] || !vk_thumbs_read(j->r[i].frame, px)) continue;
|
||||
char path[MAX_PATH + 32];
|
||||
snprintf(path, sizeof path, "%s\\f%06llu.png", j->dir, (unsigned long long)j->r[i].frame);
|
||||
char path[PLAT_PATH_MAX + 32];
|
||||
snprintf(path, sizeof path, "%s/f%06llu.png", j->dir, (unsigned long long)j->r[i].frame);
|
||||
if (write_png(path, px, &pb)) have[i] = 1, n_thumbs++;
|
||||
}
|
||||
}
|
||||
free(px), free(pb.raw), free(pb.z), free(pb.head);
|
||||
}
|
||||
char path[MAX_PATH + 32];
|
||||
snprintf(path, sizeof path, "%s\\frames.tsv", j->dir);
|
||||
char path[PLAT_PATH_MAX + 32];
|
||||
snprintf(path, sizeof path, "%s/frames.tsv", j->dir);
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
rt_log("flight: cannot write %s", path);
|
||||
free(have), free(j);
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
fprintf(f, "# totk flight dump: %s at frame %llu, interval %.1f ms\n", j->why, (unsigned long long)j->trig_frame,
|
||||
j->trig_interval_us / 1000.0);
|
||||
@@ -285,11 +285,11 @@ static DWORD WINAPI dump_main(void *arg) {
|
||||
rt_log("flight: dump %s: %s at frame %llu (%.1f ms), frames %llu-%llu, %d thumbnails, written in %llu ms; "
|
||||
"recorder %.1f us per frame (max %.0f us)", j->dir,
|
||||
j->why, (unsigned long long)j->trig_frame, j->trig_interval_us / 1000.0, (unsigned long long)j->r[0].frame,
|
||||
(unsigned long long)j->r[j->n - 1].frame, n_thumbs, (unsigned long long)(GetTickCount64() - t_start),
|
||||
(unsigned long long)j->r[j->n - 1].frame, n_thumbs, (unsigned long long)(plat_now_ms() - t_start),
|
||||
recorded ? cost_ns / 1e3 / (double)recorded : 0.0, cost_max_ns / 1e3);
|
||||
free(have);
|
||||
free(j);
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
|
||||
static void start_dump(void) {
|
||||
@@ -301,13 +301,11 @@ static void start_dump(void) {
|
||||
j->trig_interval_us = trig_interval_us;
|
||||
j->why = trig_why;
|
||||
for (int i = 0; i < n; i++) j->r[i] = ring[(recorded - (uint64_t)n + (uint64_t)i) % (uint64_t)ring_n];
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
snprintf(j->dir, sizeof j->dir, "%s\\%04u%02u%02u-%02u%02u%02u-f%llu", base_dir, st.wYear, st.wMonth, st.wDay, st.wHour,
|
||||
st.wMinute, st.wSecond, (unsigned long long)trig_frame);
|
||||
HANDLE h = CreateThread(NULL, 0, dump_main, j, 0, NULL);
|
||||
if (h) CloseHandle(h);
|
||||
else rt_log("flight: cannot start the dump thread (error %lu)", GetLastError()), free(j);
|
||||
struct tm st;
|
||||
plat_local_time(&st);
|
||||
snprintf(j->dir, sizeof j->dir, "%s/%04d%02d%02d-%02d%02d%02d-f%llu", base_dir, st.tm_year + 1900, st.tm_mon + 1,
|
||||
st.tm_mday, st.tm_hour, st.tm_min, st.tm_sec, (unsigned long long)trig_frame);
|
||||
if (!plat_thread_spawn(dump_main, j, "flight dump", 0)) rt_log("flight: cannot start the dump thread"), free(j);
|
||||
}
|
||||
|
||||
static void trigger(uint64_t frame, uint64_t interval_ns, const char *why) {
|
||||
@@ -351,8 +349,7 @@ void flight_frame(uint64_t frame, uint64_t now_ns, uint64_t interval_ns, const u
|
||||
recorded++;
|
||||
|
||||
static int key_was;
|
||||
HWND w = gfx_window_handle();
|
||||
int key = w && w == GetForegroundWindow() && (GetAsyncKeyState(VK_F8) & 0x8000);
|
||||
int key = input_flight_key();
|
||||
if (key && !key_was) trigger(frame, interval_ns, "F8");
|
||||
key_was = key;
|
||||
for (int i = 0; i < n_at; i++)
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ void flight_init(void); // prof_start calls it
|
||||
void flight_frame(uint64_t frame, uint64_t now_ns, uint64_t interval_ns, const uint64_t blocked_ns[],
|
||||
const volatile int64_t counts[]);
|
||||
|
||||
// gfx_window.c: the present thread blits every presented frame into a host-visible ring of n thumbnails.
|
||||
// gfx_present.c: the present thread blits every presented frame into a host-visible ring of n thumbnails.
|
||||
void vk_thumbs_enable(int n); // before the first present; n slots of FLIGHT_THUMB_W x FLIGHT_THUMB_H
|
||||
void vk_thumbs_frame(uint64_t frame); // the present thread names the frame of the vk_present_image call that follows
|
||||
// Copies frame's thumbnail as RGBA when the ring still holds it complete; 0 when it was never written, is still in
|
||||
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
// Correctly rounded fma/fmaf for x86-64 builds without FMA3: the -march=x86-64-v2 build (docs/building.md).
|
||||
//
|
||||
// runtime/simd.h lowers AArch64 FMADD/FMSUB and the RECPS/RSQRTS helpers to __builtin_elementwise_fma, which clang
|
||||
// turns into a call to fma/fmaf when the target has no FMA instruction. The C runtime is not a substitute: on CPUs
|
||||
// without FMA3 the UCRT's software fallback is documented as rounding incorrectly by up to half an ulp, while the FP
|
||||
// lowering needs the single rounding the ARM instruction gives. This file supplies it.
|
||||
//
|
||||
// Adapted from musl src/math/fma.c and src/math/fmaf.c (MIT, Copyright (c) 2005-2020 Rich Felker and contributors).
|
||||
// The result is rounded in the MXCSR rounding mode the caller set (simd.h simd_rm_enter), and MXCSR FTZ and DAZ are
|
||||
// honoured as the hardware FMA would, so runtime/simd.h's fast paths keep their assumptions: denormal inputs read as
|
||||
// zero and denormal results as zero under FPCR.FZ, without a trip through the slow path.
|
||||
//
|
||||
// The definitions exist only where the compiler would emit the calls. A -march=x86-64-v3 build defines __FMA__ and
|
||||
// emits vfmadd instead, so this file contributes nothing there.
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(__x86_64__) && !defined(__FMA__)
|
||||
|
||||
#include <float.h>
|
||||
#include <immintrin.h>
|
||||
|
||||
#define MXCSR_DAZ 0x0040u
|
||||
#define MXCSR_FTZ 0x8000u
|
||||
|
||||
static double fma_flush_d(double x) {
|
||||
union { double f; uint64_t i; } u = {x};
|
||||
if (!(u.i & 0x7ff0000000000000ull) && (u.i & 0x000fffffffffffffull)) u.i &= 0x8000000000000000ull;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static float fma_flush_f(float x) {
|
||||
union { float f; uint32_t i; } u = {x};
|
||||
if (!(u.i & 0x7f800000u) && (u.i & 0x007fffffu)) u.i &= 0x80000000u;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
// ---- float: the exact product fits in a double, so one extra term and a round-to-odd step decide the float
|
||||
// rounding. Halfway and tiny results are the only ones where rounding the double first can change the float result.
|
||||
float fmaf(float x, float y, float z) {
|
||||
unsigned csr = _mm_getcsr();
|
||||
if (csr & MXCSR_DAZ) x = fma_flush_f(x), y = fma_flush_f(y), z = fma_flush_f(z);
|
||||
double xy = (double)x * y; // exact
|
||||
union { double r; uint64_t i; } u = {xy + z};
|
||||
int e = u.i >> 52 & 0x7ff;
|
||||
int halfway = (u.i & 0x1fffffff) == 0x10000000;
|
||||
int tiny = e <= 0x3ff - 126 && e >= 0x3ff - 149;
|
||||
if (halfway || tiny) {
|
||||
if (e != 0x7ff) {
|
||||
// r+t is the exact sum in nearest rounding; otherwise t keeps its sign and whether it is zero, which
|
||||
// is all the round-to-odd step needs. An odd sticky bit makes the final conversion round once, in
|
||||
// whichever mode MXCSR asks for.
|
||||
int s = u.i >> 63;
|
||||
double t = s == (xy < z) ? xy - u.r + z : z - u.r + xy;
|
||||
if (t) {
|
||||
u.i -= s ^ (t < 0);
|
||||
u.i |= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
float r = (float)u.r;
|
||||
if (csr & MXCSR_FTZ) r = fma_flush_f(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- double: the significands are multiplied and aligned as integers, then converted back with one rounding.
|
||||
#define ASUINT64(x) ((union { double f; uint64_t i; }){x}).i
|
||||
#define ZEROINFNAN (0x7ff - 0x3ff - 52 - 1)
|
||||
|
||||
struct fma_num { uint64_t m; int e; int sign; };
|
||||
|
||||
static struct fma_num fma_normalize(double x) {
|
||||
uint64_t ix = ASUINT64(x);
|
||||
int e = ix >> 52;
|
||||
int sign = e & 0x800;
|
||||
e &= 0x7ff;
|
||||
if (!e) {
|
||||
ix = ASUINT64(x * 0x1p63);
|
||||
e = ix >> 52 & 0x7ff;
|
||||
e = e ? e - 63 : 0x800;
|
||||
}
|
||||
ix &= (1ull << 52) - 1;
|
||||
ix |= 1ull << 52;
|
||||
ix <<= 1;
|
||||
e -= 0x3ff + 52 + 1;
|
||||
return (struct fma_num){ix, e, sign};
|
||||
}
|
||||
|
||||
static void fma_mul(uint64_t *hi, uint64_t *lo, uint64_t x, uint64_t y) {
|
||||
uint64_t xlo = (uint32_t)x, xhi = x >> 32;
|
||||
uint64_t ylo = (uint32_t)y, yhi = y >> 32;
|
||||
uint64_t t1 = xlo * ylo;
|
||||
uint64_t t2 = xlo * yhi + xhi * ylo;
|
||||
uint64_t t3 = xhi * yhi;
|
||||
*lo = t1 + (t2 << 32);
|
||||
*hi = t3 + (t2 >> 32) + (t1 > *lo);
|
||||
}
|
||||
|
||||
// scalbn from musl src/math/scalbn.c (MIT): the fma's final scaling, kept here so the result does not depend on the
|
||||
// C runtime's version. Its multiplications honour MXCSR like any other host FP operation.
|
||||
static double fma_scalbn(double x, int n) {
|
||||
union { double f; uint64_t i; } u;
|
||||
double y = x;
|
||||
if (n > 1023) {
|
||||
y *= 0x1p1023;
|
||||
n -= 1023;
|
||||
if (n > 1023) {
|
||||
y *= 0x1p1023;
|
||||
n -= 1023;
|
||||
if (n > 1023) n = 1023;
|
||||
}
|
||||
} else if (n < -1022) {
|
||||
// Make sure the final n < -53, so the subnormal range rounds only once.
|
||||
y *= 0x1p-1022 * 0x1p53;
|
||||
n += 1022 - 53;
|
||||
if (n < -1022) {
|
||||
y *= 0x1p-1022 * 0x1p53;
|
||||
n += 1022 - 53;
|
||||
if (n < -1022) n = -1022;
|
||||
}
|
||||
}
|
||||
u.i = (uint64_t)(0x3ff + n) << 52;
|
||||
return y * u.f;
|
||||
}
|
||||
|
||||
double fma(double x, double y, double z) {
|
||||
#pragma STDC FENV_ACCESS ON
|
||||
unsigned csr = _mm_getcsr();
|
||||
if (csr & MXCSR_DAZ) x = fma_flush_d(x), y = fma_flush_d(y), z = fma_flush_d(z);
|
||||
|
||||
struct fma_num nx = fma_normalize(x), ny = fma_normalize(y), nz = fma_normalize(z);
|
||||
if (nx.e >= ZEROINFNAN || ny.e >= ZEROINFNAN) return x * y + z;
|
||||
if (nz.e >= ZEROINFNAN) {
|
||||
if (nz.e > ZEROINFNAN) return x * y; // z == 0
|
||||
return z;
|
||||
}
|
||||
|
||||
uint64_t rhi, rlo, zhi, zlo;
|
||||
fma_mul(&rhi, &rlo, nx.m, ny.m);
|
||||
|
||||
// Align the exponents: shift z left by kz and r right by kr with kz + kr == d.
|
||||
int e = nx.e + ny.e;
|
||||
int d = nz.e - e;
|
||||
if (d > 0) {
|
||||
if (d < 64) {
|
||||
zlo = nz.m << d;
|
||||
zhi = nz.m >> (64 - d);
|
||||
} else {
|
||||
zlo = 0;
|
||||
zhi = nz.m;
|
||||
e = nz.e - 64;
|
||||
d -= 64;
|
||||
if (d == 0) {
|
||||
} else if (d < 64) {
|
||||
rlo = rhi << (64 - d) | rlo >> d | !!(rlo << (64 - d));
|
||||
rhi = rhi >> d;
|
||||
} else {
|
||||
rlo = 1;
|
||||
rhi = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
zhi = 0;
|
||||
d = -d;
|
||||
if (d == 0) {
|
||||
zlo = nz.m;
|
||||
} else if (d < 64) {
|
||||
zlo = nz.m >> d | !!(nz.m << (64 - d));
|
||||
} else {
|
||||
zlo = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int sign = nx.sign ^ ny.sign;
|
||||
int samesign = !(sign ^ nz.sign);
|
||||
int nonzero = 1;
|
||||
if (samesign) {
|
||||
rlo += zlo;
|
||||
rhi += zhi + (rlo < zlo);
|
||||
} else {
|
||||
uint64_t t = rlo;
|
||||
rlo -= zlo;
|
||||
rhi = rhi - zhi - (t < rlo);
|
||||
if (rhi >> 63) {
|
||||
rlo = -rlo;
|
||||
rhi = -rhi - !!rlo;
|
||||
sign = !sign;
|
||||
}
|
||||
nonzero = !!rhi;
|
||||
}
|
||||
|
||||
// Keep the top 63 bits in rhi; the last bit is sticky.
|
||||
if (nonzero) {
|
||||
e += 64;
|
||||
d = __builtin_clzll(rhi) - 1;
|
||||
rhi = rhi << d | rlo >> (64 - d) | !!(rlo << d);
|
||||
} else if (rlo) {
|
||||
d = __builtin_clzll(rlo) - 1;
|
||||
if (d < 0)
|
||||
rhi = rlo >> 1 | (rlo & 1);
|
||||
else
|
||||
rhi = rlo << d;
|
||||
} else {
|
||||
return x * y + z; // exact +-0
|
||||
}
|
||||
e -= d;
|
||||
|
||||
int64_t i = rhi;
|
||||
if (sign) i = -i;
|
||||
double r = i; // |r| is in [0x1p62, 0x1p63]
|
||||
|
||||
if (e < -1022 - 62) {
|
||||
// The result is subnormal before rounding.
|
||||
if (e == -1022 - 63) {
|
||||
double c = 0x1p63;
|
||||
if (sign) c = -c;
|
||||
if (r == c) {
|
||||
// The smallest normal after rounding: whether it underflows depends on the architecture. A double
|
||||
// to float conversion imitates the before-rounding rule the guest has.
|
||||
float fltmin = 0x0.ffffff8p-63 * FLT_MIN * r;
|
||||
r = DBL_MIN / FLT_MIN * fltmin;
|
||||
return (csr & MXCSR_FTZ) ? fma_flush_d(r) : r;
|
||||
}
|
||||
// One bit is lost when scaled: add another top bit so the conversion rounds only once if inexact.
|
||||
if (rhi << 53) {
|
||||
i = rhi >> 1 | (rhi & 1) | 1ull << 62;
|
||||
if (sign) i = -i;
|
||||
r = i;
|
||||
r = 2 * r - c;
|
||||
// Raise underflow portably, so the compiler cannot optimize the expression away.
|
||||
double tiny = DBL_MIN / FLT_MIN * r;
|
||||
r += (double)(tiny * tiny) * (r - r);
|
||||
}
|
||||
} else {
|
||||
// Only round once when scaled.
|
||||
d = 10;
|
||||
i = (rhi >> d | !!(rhi << (64 - d))) << d;
|
||||
if (sign) i = -i;
|
||||
r = i;
|
||||
}
|
||||
}
|
||||
r = fma_scalbn(r, e);
|
||||
if (csr & MXCSR_FTZ) r = fma_flush_d(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
// The host's own folder dialog, for menu pages that point at a folder (the Amiibo tab): folder_pick_win.cpp with Win32,
|
||||
// folder_pick_sdl.c with SDL. The menu has no text entry, so a folder is chosen here or by walking the page's list.
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// done gets the chosen folder (UTF-8) or NULL when the player cancelled or the host has no dialog.
|
||||
typedef void (*FolderPickDone)(const char *path, void *ctx);
|
||||
|
||||
// Opens the dialog at start (may be relative or missing) and returns at once; done runs later on another thread, once.
|
||||
// The caller keeps one dialog open at a time.
|
||||
void folder_pick(const char *start, FolderPickDone done, void *ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
// The SDL front end of folder_pick.h: SDL_ShowOpenFolderDialog (the desktop portal, else zenity), started on the window
|
||||
// thread, which runs SDL's event loop that delivers the portal's answer.
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "folder_pick.h"
|
||||
#include "host.h"
|
||||
#include "input_sdl.h"
|
||||
|
||||
typedef struct {
|
||||
char start[PATH_MAX]; // absolute, or empty: the dialog's own default
|
||||
FolderPickDone done;
|
||||
void *ctx;
|
||||
} PickReq;
|
||||
|
||||
static void SDLCALL on_pick(void *userdata, const char *const *list, int filter) {
|
||||
(void)filter;
|
||||
PickReq *r = userdata;
|
||||
if (!list) rt_log("folder_pick: the folder dialog failed (%s)", SDL_GetError());
|
||||
r->done(list && list[0] ? list[0] : NULL, r->ctx);
|
||||
free(r);
|
||||
}
|
||||
|
||||
static void show(void *arg) {
|
||||
PickReq *r = arg;
|
||||
SDL_ShowOpenFolderDialog(on_pick, r, gfx_window_sdl(), r->start[0] ? r->start : NULL, false);
|
||||
}
|
||||
|
||||
void folder_pick(const char *start, FolderPickDone done, void *ctx) {
|
||||
PickReq *r = calloc(1, sizeof *r);
|
||||
if (!r) {
|
||||
done(NULL, ctx);
|
||||
return;
|
||||
}
|
||||
r->done = done;
|
||||
r->ctx = ctx;
|
||||
if (start && *start && !realpath(start, r->start)) r->start[0] = 0;
|
||||
gfx_window_sdl_run(show, r);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// The Win32 front end of folder_pick.h: the shell's folder dialog (IFileOpenDialog with FOS_PICKFOLDERS), owned by the
|
||||
// game window, on a thread of its own so the present thread that draws the menu keeps running.
|
||||
#include <windows.h>
|
||||
#include <shobjidl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
|
||||
extern "C" {
|
||||
#include "folder_pick.h"
|
||||
#include "host.h"
|
||||
#include "input_win.h"
|
||||
#include "platform/platform.h"
|
||||
}
|
||||
|
||||
struct PickReq {
|
||||
std::string start;
|
||||
FolderPickDone done;
|
||||
void *ctx;
|
||||
};
|
||||
|
||||
static std::wstring wide(const std::string &utf8) {
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
|
||||
std::wstring w(n > 0 ? n : 1, L'\0');
|
||||
if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], n);
|
||||
w.resize(wcslen(w.c_str()));
|
||||
return w;
|
||||
}
|
||||
|
||||
static std::string utf8(const wchar_t *w) {
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
|
||||
std::string s(n > 0 ? n : 1, '\0');
|
||||
if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w, -1, &s[0], n, NULL, NULL);
|
||||
s.resize(strlen(s.c_str()));
|
||||
return s;
|
||||
}
|
||||
|
||||
// The dialog wants an absolute folder that exists; anything else leaves it at the shell's last folder.
|
||||
static void set_start(IFileOpenDialog *d, const std::string &start) {
|
||||
wchar_t full[MAX_PATH * 2];
|
||||
if (start.empty() || !GetFullPathNameW(wide(start).c_str(), MAX_PATH * 2, full, NULL)) return;
|
||||
IShellItem *item = NULL;
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(full, NULL, IID_PPV_ARGS(&item)))) {
|
||||
d->SetFolder(item);
|
||||
item->Release();
|
||||
}
|
||||
}
|
||||
|
||||
static void pick_main(void *arg) {
|
||||
PickReq *r = (PickReq *)arg;
|
||||
std::string chosen;
|
||||
HRESULT co = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||
IFileOpenDialog *d = NULL;
|
||||
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d));
|
||||
if (SUCCEEDED(hr)) {
|
||||
FILEOPENDIALOGOPTIONS opt = 0;
|
||||
d->GetOptions(&opt);
|
||||
d->SetOptions(opt | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
||||
set_start(d, r->start);
|
||||
hr = d->Show(gfx_window_handle());
|
||||
IShellItem *item = NULL;
|
||||
if (SUCCEEDED(hr) && SUCCEEDED(hr = d->GetResult(&item))) {
|
||||
PWSTR path = NULL;
|
||||
if (SUCCEEDED(hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
|
||||
chosen = utf8(path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
item->Release();
|
||||
}
|
||||
d->Release();
|
||||
}
|
||||
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_CANCELLED)) rt_log("folder_pick: the folder dialog failed (0x%08lx)", (unsigned long)hr);
|
||||
r->done(chosen.empty() ? NULL : chosen.c_str(), r->ctx);
|
||||
if (SUCCEEDED(co)) CoUninitialize();
|
||||
delete r;
|
||||
}
|
||||
|
||||
extern "C" void folder_pick(const char *start, FolderPickDone done, void *ctx) {
|
||||
PickReq *r = new PickReq{start ? start : "", done, ctx};
|
||||
if (!plat_thread_spawn(pick_main, r, "folder dialog", 0)) {
|
||||
rt_log("folder_pick: cannot start the dialog thread");
|
||||
done(NULL, ctx);
|
||||
delete r;
|
||||
}
|
||||
}
|
||||
+36
-12
@@ -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
|
||||
@@ -24,7 +36,7 @@
|
||||
//
|
||||
// The world clock (frame_step_world_us) sums the steps from the first present that finds the player actor away from
|
||||
// the origin, where the title screen keeps it: the end of the first save load. Recorded and scripted input
|
||||
// (input_win.c) runs on it, so a replay presses at the same game time at any frame rate and after a first load of any
|
||||
// (input.c) runs on it, so a replay presses at the same game time at any frame rate and after a first load of any
|
||||
// length; a later load (fast travel) still shifts what follows by its length.
|
||||
//
|
||||
// TOTK_TRACE=perf adds a line per 120 presents: fps, the game seconds stepped against wall seconds, and with
|
||||
@@ -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,10 +180,14 @@ 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.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).
|
||||
// 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.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 : 0x4728538)), scene = module ? read_ptr(module + 0x1e8) : 0;
|
||||
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;
|
||||
uint64_t comps = read_ptr(scene + 0x58), resident = comps ? read_ptr(comps + 13 * 8) : 0;
|
||||
@@ -254,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,12 +38,19 @@ 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_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
|
||||
|
||||
// ---- fs_host.c
|
||||
wchar_t *fs_wide(const char *utf8); // UTF-16 copy for the W APIs; free() it. Fatal on
|
||||
// invalid UTF-8
|
||||
void fs_host_set_profile(const char *dir); // profile directory; before the first storage mount only
|
||||
// (default rt_config()->profile)
|
||||
int fs_save_busy(void); // a save is being written: a save file open for writing,
|
||||
|
||||
+570
-490
File diff suppressed because it is too large.
Load diff
+5
-2
@@ -16,7 +16,7 @@ const OvNvState *ov_nv_state(void); // ov_gfx.c
|
||||
#define RT_GPU_VA_BASE 0x8000000000ull // 2^39, never mapped on the host
|
||||
#define RT_GPU_VA_SIZE (64ull << 30)
|
||||
|
||||
// ---- window, vsync, focus (gfx_window.c, vk_device.c). Host threads only; never run guest code.
|
||||
// ---- window, vsync, focus (gfx_present.c and the window back end, gfx_wsys.h; vk_device.c). Host threads only; never run guest code.
|
||||
void gfx_window_start(void); // window thread, Vulkan device, swapchain; idempotent
|
||||
void gfx_vsync_bind(uint64_t system_event); // nn::vi vsync event; signalled at 60 Hz
|
||||
uint64_t gfx_vsync_ticks(void); // ticks since start
|
||||
@@ -50,7 +50,7 @@ const GfxShader *gfx_shader_await(int stage, const uint8_t *control, const uint8
|
||||
uint64_t gfx_shader_async_done(void); // translations finished by the background worker
|
||||
void gfx_shader_counts(uint64_t *keys, uint64_t *translated); // distinct keys in memory, translations done (TOTK_TRACE=perf)
|
||||
// Translator answers for this device (vk_caps.c): shaderFloat64, storage offset rounding, subgroup size per vs/fs/cs.
|
||||
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3]);
|
||||
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits);
|
||||
const uint8_t *gfx_shader_key(const GfxShader *s); // the 32-byte cache key (stable while the translator is)
|
||||
const uint8_t *gfx_shader_input_id(const GfxShader *s); // the 32-byte input id (stable across translators)
|
||||
// By input id from memory or disk; xlate 1 also translates it from its stored inputs. NULL if absent.
|
||||
@@ -73,6 +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 };
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
// Needs no game files.
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "gfx.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static int failures, checks;
|
||||
#define CHECK(cond, ...) do { checks++; if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
|
||||
@@ -65,8 +65,8 @@ static void check_write_watch(void) {
|
||||
// read) thaws it and holds it thawed; SetHeapSize below decommits it.
|
||||
mem_written_pages(heap, 0x200000, pages, 8);
|
||||
CHECK(mem_freeze(heap) == 1 && mem_frozen(heap), "freeze of an idle committed 2 MiB region");
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
CHECK(VirtualQuery((void *)(uintptr_t)heap, &mbi, sizeof mbi) && mbi.Protect == PAGE_READONLY, "frozen region is not read-only");
|
||||
uint64_t end;
|
||||
CHECK(plat_mem_state(heap, heap + 1, &end) == PLAT_PROT_READ, "frozen region is not read-only");
|
||||
p[0x3000] = 7;
|
||||
CHECK(!mem_frozen(heap) && p[0x3000] == 7, "a write into a frozen region did not thaw it");
|
||||
n = mem_written_pages(heap, 0x200000, pages, 8);
|
||||
@@ -88,12 +88,11 @@ static void check_write_watch(void) {
|
||||
// A region with reserved holes freezes its committed pages only, and a write thaws them without committing a hole.
|
||||
uint64_t ra = a & ~0x1fffffull;
|
||||
CHECK(mem_freeze(ra) == 1 && mem_frozen(ra), "freeze of a 2 MiB region with reserved holes");
|
||||
CHECK(VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) && mbi.Protect == PAGE_READONLY, "its committed pages are not read-only");
|
||||
CHECK(plat_mem_state(a, a + 1, &end) == PLAT_PROT_READ, "its committed pages are not read-only");
|
||||
*(volatile uint8_t *)(uintptr_t)(a + 0x2000) = 9;
|
||||
CHECK(!mem_frozen(ra) && *(volatile uint8_t *)(uintptr_t)(a + 0x2000) == 9, "a write did not thaw the region with holes");
|
||||
CHECK(VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) && mbi.Protect == PAGE_READWRITE && mbi.RegionSize == 0x4000,
|
||||
"the thawed pages are not read-write");
|
||||
CHECK(VirtualQuery((void *)(uintptr_t)(a + 0x4000), &mbi, sizeof mbi) && mbi.State == MEM_RESERVE, "a hole got committed");
|
||||
CHECK(plat_mem_state(a, a + 0x10000, &end) == PLAT_PROT_RW && end == a + 0x4000, "the thawed pages are not read-write");
|
||||
CHECK(plat_mem_state(a + 0x4000, a + 0x4001, &end) == PLAT_MEM_RESERVED, "a hole got committed");
|
||||
printf("write-watch: region with reserved holes frozen and thawed by a write\n");
|
||||
CHECK(mem_unmap_physical(a, 0x4000) == 0, "UnmapPhysicalMemory");
|
||||
CHECK(mem_written_pages(RT_KERNEL_BASE, 0x1000, pages, 8) == UINT64_MAX, "kernel region is not write-watched");
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
// Win32 window, Vulkan surface and swapchain, present blit, 60 Hz vsync thread, focus.
|
||||
// Vulkan surface and swapchain, present blit, 60 Hz vsync thread, focus: the window-system-free half of the window
|
||||
// (the other half: gfx_window_win.c with Win32, gfx_window_sdl.c with SDL, through gfx_wsys.h).
|
||||
// Swapchain set-up ported from LAS Recomp, runtime/nvn/vulkan/device.c:254-330 (https://github.com/peiikai/LASRecomp @
|
||||
// da35b83a9611d046a9a681aae668d7085954d37f, MIT, Copyright (c) 2026 Peiikai), SDL replaced by Win32. Focus and vsync
|
||||
// behaviour from MacNativeGames, games/totk/runtime/frontend.m:88-97, 175-176, 350-352 (https://github.com/ly05010419/
|
||||
// MacNativeGames @ b75be8cfa704df3701e86130c8ced268e79d6b11, MIT, Copyright (c) 2026 MacNativeGames contributors).
|
||||
// da35b83a9611d046a9a681aae668d7085954d37f, MIT, Copyright (c) 2026 Peiikai). Focus and vsync behaviour from
|
||||
// MacNativeGames, games/totk/runtime/frontend.m:88-97, 175-176, 350-352 (https://github.com/ly05010419/MacNativeGames @
|
||||
// b75be8cfa704df3701e86130c8ced268e79d6b11, MIT, Copyright (c) 2026 MacNativeGames contributors).
|
||||
//
|
||||
// Threads: the window thread owns the HWND and its message loop and never runs guest code. The vsync thread signals the
|
||||
// Threads: the window thread (the back end's) owns the window and never runs guest code. The vsync thread signals the
|
||||
// bound system events. Presents come from any host thread (the back end's present worker) and take g_pl; every queue
|
||||
// submission takes the recursive queue lock (vk_queue_lock). The swapchain is rebuilt lazily: WM_SIZE only marks it
|
||||
// dirty, the next present (or gfx_window_resize) rebuilds it.
|
||||
// submission takes the recursive queue lock (vk_queue_lock). The swapchain is rebuilt lazily: a resize only marks it
|
||||
// dirty (gfx_window_mark_dirty), the next present (or gfx_window_resize) rebuilds it.
|
||||
//
|
||||
// The in-game menu (ui/menu.h) draws over the game image in the present's command buffer, after the blit; its keyboard
|
||||
// and mouse come from the window procedure (ui/menu_win.cpp). The window's size and fullscreen state are the settings
|
||||
// TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, applied at start and whenever a cfg_set changes them (window_settings_live).
|
||||
//
|
||||
// The process is per-monitor DPI aware (totk.manifest): every size here is physical pixels, TOTK_WINDOW_SIZE included,
|
||||
// and the swapchain matches the screen pixel for pixel. A window moved to a monitor at another display scale keeps its
|
||||
// client size (WM_GETDPISCALEDSIZE); the menu takes its text size floor from the window's DPI (gfx_window_dpi).
|
||||
// The in-game menu (ui/menu.h) draws over the game image in the present's command buffer, after the blit. The window's
|
||||
// size and fullscreen state are the settings TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, which the back end applies at start
|
||||
// and whenever a cfg_set changes them (window_settings_live -> gfx_window_apply_settings).
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "flight.h"
|
||||
#include "fs.h"
|
||||
#include "gfx.h"
|
||||
#include "input.h"
|
||||
#include "gfx_wsys.h"
|
||||
#include "nvn_api.h"
|
||||
#include "platform/platform.h"
|
||||
#include "ui/menu.h"
|
||||
#include "vk.h"
|
||||
|
||||
@@ -36,28 +33,86 @@
|
||||
#define VSYNC_HZ 60
|
||||
|
||||
// ---- queue lock
|
||||
static CRITICAL_SECTION g_ql;
|
||||
__attribute__((constructor)) static void window_init_locks(void);
|
||||
void vk_queue_lock(void) { EnterCriticalSection(&g_ql); }
|
||||
void vk_queue_unlock(void) { LeaveCriticalSection(&g_ql); }
|
||||
static PlatRMutex g_ql;
|
||||
void vk_queue_lock(void) { plat_rmutex_lock(&g_ql); }
|
||||
void vk_queue_unlock(void) { plat_rmutex_unlock(&g_ql); }
|
||||
|
||||
// ---- window state
|
||||
static HWND g_hwnd;
|
||||
static HANDLE g_wnd_ready;
|
||||
// ---- window state shared with the back end
|
||||
static VkSurfaceKHR g_surface;
|
||||
static volatile LONG g_dirty; // WM_SIZE seen; swapchain stale
|
||||
static volatile LONG g_focused = 1;
|
||||
static volatile LONG g_focus_msgs;
|
||||
static CRITICAL_SECTION g_start_lock; // gfx_window_start / vsync start
|
||||
static int g_started, g_wnd_failed;
|
||||
static int g_background; // TOTK_BACKGROUND_WINDOW: the window stays behind the user's windows
|
||||
static int g_fs_pending; // window thread: TOTK_FULLSCREEN waits for the first activation
|
||||
static volatile LONG g_dpi = USER_DEFAULT_SCREEN_DPI; // the window's DPI, set by the window thread
|
||||
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
|
||||
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 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);
|
||||
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. 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) {
|
||||
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"); }
|
||||
|
||||
void gfx_cfg_window_size(int *w, int *h) {
|
||||
int x = 0, y = 0;
|
||||
if (sscanf(cfg_value("TOTK_WINDOW_SIZE"), "%dx%d", &x, &y) != 2 || x < 640 || y < 360 || x > 7680 || y > 4320)
|
||||
sscanf(cfg_meta("WINDOW_SIZE")->def, "%dx%d", &x, &y); // unparsable or out of range: the default
|
||||
*w = x;
|
||||
*h = y;
|
||||
}
|
||||
|
||||
// The refresh rate of the window's monitor (TOTK_FPS_LIMIT=display), measured by the back end's window thread when the
|
||||
// window opens, lands on another monitor or the display mode changes; readers take the stored value.
|
||||
static volatile int32_t g_rate_mhz; // millihertz, 0 unknown
|
||||
|
||||
void gfx_window_set_refresh(double hz) {
|
||||
int32_t mhz = (int32_t)(hz * 1000 + 0.5);
|
||||
if (plat_xchg32(&g_rate_mhz, mhz) != mhz) rt_log("gfx: monitor refresh rate %.3f Hz", hz);
|
||||
}
|
||||
|
||||
double gfx_window_refresh_hz(void) { return plat_cas32(&g_rate_mhz, 0, 0) / 1000.0; }
|
||||
|
||||
// ---- swapchain and present state (g_pl)
|
||||
static CRITICAL_SECTION g_pl;
|
||||
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;
|
||||
@@ -93,207 +148,82 @@ static VkBuffer g_th_buf;
|
||||
static VmaAllocation g_th_alloc;
|
||||
static uint8_t *g_th_ptr;
|
||||
|
||||
static void window_init_locks(void) {
|
||||
InitializeCriticalSection(&g_ql);
|
||||
InitializeCriticalSection(&g_pl);
|
||||
InitializeCriticalSection(&g_start_lock);
|
||||
}
|
||||
|
||||
// ---- vsync
|
||||
static SRWLOCK g_vs_lock = SRWLOCK_INIT;
|
||||
static PlatMutex g_vs_lock = PLAT_MUTEX_INIT;
|
||||
static uint64_t g_vs_ev[16];
|
||||
static int g_vs_nev;
|
||||
static volatile long *g_vs_ctr[4];
|
||||
static volatile int32_t *g_vs_ctr[4];
|
||||
static int g_vs_nctr;
|
||||
static volatile LONGLONG g_ticks;
|
||||
static volatile LONGLONG g_vs_start_ns;
|
||||
static volatile int64_t g_ticks;
|
||||
static volatile int64_t g_vs_start_ns;
|
||||
static int g_vs_started;
|
||||
|
||||
static DWORD WINAPI vsync_main(void *arg) {
|
||||
static void vsync_main(void *arg) {
|
||||
(void)arg;
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
||||
HANDLE t = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
if (!t) { rt_log("gfx: no high-resolution timer; vsync falls back to the system timer tick"); t = CreateWaitableTimerW(NULL, TRUE, NULL); }
|
||||
plat_thread_priority(PLAT_PRIO_HIGHEST);
|
||||
uint64_t n = 0;
|
||||
for (;;) {
|
||||
uint64_t start = (uint64_t)g_vs_start_ns;
|
||||
uint64_t due = start + (n + 1) * VSYNC_NS / VSYNC_HZ;
|
||||
uint64_t now = kern_now_ns();
|
||||
if (now > due + 100000000ull) { // the process stalled: restart the schedule, no catch-up burst
|
||||
g_vs_start_ns = (LONGLONG)(now - (n + 1) * VSYNC_NS / VSYNC_HZ);
|
||||
g_vs_start_ns = (int64_t)(now - (n + 1) * VSYNC_NS / VSYNC_HZ);
|
||||
} else if (due > now) {
|
||||
LARGE_INTEGER d;
|
||||
d.QuadPart = -(LONGLONG)((due - now) / 100);
|
||||
if (d.QuadPart == 0) d.QuadPart = -1;
|
||||
SetWaitableTimer(t, &d, 0, NULL, NULL, FALSE);
|
||||
WaitForSingleObject(t, INFINITE);
|
||||
plat_wait_ns(due - now);
|
||||
}
|
||||
n++;
|
||||
InterlockedExchange64(&g_ticks, (LONGLONG)n);
|
||||
plat_xchg64(&g_ticks, (int64_t)n);
|
||||
uint64_t ev[16];
|
||||
volatile long *ctr[4];
|
||||
volatile int32_t *ctr[4];
|
||||
int ne, nc;
|
||||
AcquireSRWLockShared(&g_vs_lock);
|
||||
plat_mutex_lock_shared(&g_vs_lock);
|
||||
ne = g_vs_nev;
|
||||
nc = g_vs_nctr;
|
||||
memcpy(ev, g_vs_ev, sizeof ev);
|
||||
memcpy(ctr, g_vs_ctr, sizeof ctr);
|
||||
ReleaseSRWLockShared(&g_vs_lock);
|
||||
for (int i = 0; i < nc; i++) InterlockedIncrement(ctr[i]);
|
||||
plat_mutex_unlock_shared(&g_vs_lock);
|
||||
for (int i = 0; i < nc; i++) plat_inc32(ctr[i]);
|
||||
for (int i = 0; i < ne; i++) kern_signal_system_event(ev[i]);
|
||||
}
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
|
||||
static void vsync_ensure(void) {
|
||||
EnterCriticalSection(&g_start_lock);
|
||||
plat_rmutex_lock(&g_start_lock);
|
||||
if (!g_vs_started) {
|
||||
g_vs_started = 1;
|
||||
g_vs_start_ns = (LONGLONG)kern_now_ns();
|
||||
HANDLE h = CreateThread(NULL, 0, vsync_main, NULL, 0, NULL);
|
||||
if (!h) rt_fatal("gfx: cannot start the vsync thread (error %lu)", GetLastError());
|
||||
CloseHandle(h);
|
||||
g_vs_start_ns = (int64_t)kern_now_ns();
|
||||
if (!plat_thread_spawn(vsync_main, NULL, "vsync", 0)) rt_fatal("gfx: cannot start the vsync thread");
|
||||
}
|
||||
LeaveCriticalSection(&g_start_lock);
|
||||
plat_rmutex_unlock(&g_start_lock);
|
||||
}
|
||||
|
||||
void gfx_vsync_bind(uint64_t system_event) {
|
||||
AcquireSRWLockExclusive(&g_vs_lock);
|
||||
plat_mutex_lock(&g_vs_lock);
|
||||
int have = 0;
|
||||
for (int i = 0; i < g_vs_nev; i++) have |= g_vs_ev[i] == system_event;
|
||||
if (!have) {
|
||||
if (g_vs_nev == 16) rt_fatal("gfx: more than 16 vsync events");
|
||||
g_vs_ev[g_vs_nev++] = system_event;
|
||||
}
|
||||
ReleaseSRWLockExclusive(&g_vs_lock);
|
||||
plat_mutex_unlock(&g_vs_lock);
|
||||
vsync_ensure();
|
||||
}
|
||||
|
||||
void gfx_vsync_bind_counter(volatile long *counter) {
|
||||
AcquireSRWLockExclusive(&g_vs_lock);
|
||||
void gfx_vsync_bind_counter(volatile int32_t *counter) {
|
||||
plat_mutex_lock(&g_vs_lock);
|
||||
if (g_vs_nctr == 4) rt_fatal("gfx: more than 4 vsync counters");
|
||||
g_vs_ctr[g_vs_nctr++] = counter;
|
||||
ReleaseSRWLockExclusive(&g_vs_lock);
|
||||
plat_mutex_unlock(&g_vs_lock);
|
||||
vsync_ensure();
|
||||
}
|
||||
|
||||
uint64_t gfx_vsync_ticks(void) { return (uint64_t)InterlockedCompareExchange64(&g_ticks, 0, 0); }
|
||||
uint64_t gfx_vsync_ticks(void) { return (uint64_t)plat_cas64(&g_ticks, 0, 0); }
|
||||
uint64_t gfx_vsync_time_ns(uint64_t tick) { return (uint64_t)g_vs_start_ns + tick * VSYNC_NS / VSYNC_HZ; }
|
||||
|
||||
// ---- focus
|
||||
int gfx_window_focused(void) { return g_focused; }
|
||||
unsigned gfx_window_focus_messages(void) { return (unsigned)g_focus_msgs; }
|
||||
HWND gfx_window_handle(void) { return g_hwnd; }
|
||||
|
||||
// ---- window thread
|
||||
#define WND_STYLE WS_OVERLAPPEDWINDOW
|
||||
#define WM_APP_SETTINGS (WM_APP + 1)
|
||||
#define WM_APP_FULLSCREEN (WM_APP + 2) // the fullscreen setting, held back until the window is first activated
|
||||
|
||||
static WINDOWPLACEMENT g_saved_place = {sizeof g_saved_place}; // the window behind fullscreen (window thread)
|
||||
|
||||
static int is_fullscreen(HWND h) { return !(GetWindowLongW(h, GWL_STYLE) & WS_OVERLAPPEDWINDOW); }
|
||||
|
||||
unsigned gfx_window_dpi(void) { return (unsigned)InterlockedCompareExchange(&g_dpi, 0, 0); }
|
||||
|
||||
// The outer size of a framed window whose client area is cw x ch. The frame is measured at the window's DPI, or the
|
||||
// system DPI before the window exists: plain AdjustWindowRect always takes the system DPI, which is wrong on a monitor
|
||||
// at another display scale.
|
||||
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
|
||||
RECT r = {0, 0, cw, ch};
|
||||
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, h ? GetDpiForWindow(h) : GetDpiForSystem());
|
||||
*ow = r.right - r.left;
|
||||
*oh = r.bottom - r.top;
|
||||
}
|
||||
|
||||
// TOTK_FULLSCREEN=1: borderless window over the monitor. TOTK_WINDOW_SIZE=<w>x<h>: client area of the window (the
|
||||
// swapchain follows the window; the game image is scaled to it).
|
||||
static int cfg_fullscreen(void) {
|
||||
return cfg_bool("TOTK_FULLSCREEN");
|
||||
}
|
||||
|
||||
static void cfg_window_size(int *w, int *h) {
|
||||
int x = 0, y = 0;
|
||||
if (sscanf(cfg_value("TOTK_WINDOW_SIZE"), "%dx%d", &x, &y) != 2 || x < 640 || y < 360 || x > 7680 || y > 4320)
|
||||
sscanf(cfg_meta("WINDOW_SIZE")->def, "%dx%d", &x, &y); // unparsable or out of range: the default
|
||||
*w = x;
|
||||
*h = y;
|
||||
}
|
||||
|
||||
// Windows turns the display off and sleeps after the idle timeout, and gamepad input does not count as activity. While
|
||||
// the window is up and not minimized it holds the display and the system on, as other games do. The state belongs to
|
||||
// the calling thread, so only the window thread (window_main and wnd_proc) calls this; the process exit releases it.
|
||||
static void keep_awake(int on) {
|
||||
static int cur = -1;
|
||||
if (on == cur) return;
|
||||
cur = on;
|
||||
SetThreadExecutionState(on ? ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED : ES_CONTINUOUS);
|
||||
}
|
||||
|
||||
// F11: borderless window over the whole monitor, and back to the saved window. The swapchain follows through WM_SIZE.
|
||||
// Not Alt+Enter: Enter is the Plus button (input_win.c). A background window (test runs) keeps its place behind the
|
||||
// user's windows and takes no focus.
|
||||
static void toggle_fullscreen(HWND h) {
|
||||
LONG style = GetWindowLongW(h, GWL_STYLE);
|
||||
if (style & WS_OVERLAPPEDWINDOW) {
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
if (!GetWindowPlacement(h, &g_saved_place) || !GetMonitorInfoW(MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY), &mi))
|
||||
return;
|
||||
SetWindowLongW(h, GWL_STYLE, (style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP);
|
||||
SetWindowPos(h, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
|
||||
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
||||
SWP_NOOWNERZORDER | SWP_FRAMECHANGED | (g_background ? SWP_NOZORDER | SWP_NOACTIVATE : 0));
|
||||
} else {
|
||||
SetWindowLongW(h, GWL_STYLE, (style & ~WS_POPUP) | WND_STYLE);
|
||||
if (g_background) g_saved_place.showCmd = SW_SHOWNOACTIVATE;
|
||||
SetWindowPlacement(h, &g_saved_place);
|
||||
SetWindowPos(h, NULL, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED |
|
||||
(g_background ? SWP_NOACTIVATE : 0));
|
||||
}
|
||||
}
|
||||
|
||||
// The window size and fullscreen state last taken from the settings (window thread). apply_settings acts on a change of
|
||||
// either value only, so a window resized by hand keeps its size when another setting changes.
|
||||
static char g_applied_size[32];
|
||||
static int g_applied_fs;
|
||||
|
||||
static void note_applied(void) {
|
||||
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("TOTK_WINDOW_SIZE"));
|
||||
g_applied_fs = cfg_fullscreen();
|
||||
}
|
||||
|
||||
// Window thread: the window takes the size and fullscreen state of the settings when they changed. In fullscreen the
|
||||
// size goes into the placement restored when fullscreen ends.
|
||||
static void apply_settings(HWND h) {
|
||||
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = cfg_fullscreen(), fs_changed = fs != g_applied_fs;
|
||||
note_applied();
|
||||
if (size_changed) {
|
||||
int cw, ch, ow, oh;
|
||||
cfg_window_size(&cw, &ch);
|
||||
outer_size(h, cw, ch, &ow, &oh);
|
||||
if (is_fullscreen(h)) {
|
||||
RECT *n = &g_saved_place.rcNormalPosition;
|
||||
n->right = n->left + ow;
|
||||
n->bottom = n->top + oh;
|
||||
} else {
|
||||
RECT have;
|
||||
if (GetClientRect(h, &have) && (have.right != cw || have.bottom != ch)) {
|
||||
if (IsZoomed(h)) ShowWindow(h, SW_RESTORE);
|
||||
SetWindowPos(h, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fs_changed && fs != is_fullscreen(h)) {
|
||||
input_cursor_suspend(1);
|
||||
toggle_fullscreen(h);
|
||||
input_cursor_suspend(0);
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_window_apply_settings(void) {
|
||||
if (g_hwnd) PostMessageW(g_hwnd, WM_APP_SETTINGS, 0, 0);
|
||||
}
|
||||
|
||||
// Present path (g_pl held): a settings change (cfg_version) reaches the window thread, which applies a changed size or
|
||||
// fullscreen; a changed TOTK_VK_PRESENT rebuilds the swapchain at this present.
|
||||
@@ -304,209 +234,16 @@ static void window_settings_live(void) {
|
||||
unsigned v = cfg_version();
|
||||
if (started && v == seen) return;
|
||||
seen = v;
|
||||
res_fov_live();
|
||||
const char *e = cfg_value("TOTK_VK_PRESENT");
|
||||
if (!started || strncmp(e, present, sizeof present - 1)) {
|
||||
snprintf(present, sizeof present, "%s", e);
|
||||
if (started) InterlockedExchange(&g_dirty, 1);
|
||||
if (started) plat_xchg32(&g_dirty, 1);
|
||||
}
|
||||
if (started) gfx_window_apply_settings();
|
||||
started = 1;
|
||||
}
|
||||
|
||||
void gfx_window_request_close(void) {
|
||||
if (g_hwnd) PostMessageW(g_hwnd, WM_CLOSE, 0, 0);
|
||||
}
|
||||
|
||||
// ---- refresh rate of the window's monitor (TOTK_FPS_LIMIT=display). The window thread measures it when the window
|
||||
// opens, lands on another monitor or the display mode changes; readers take the stored value. QueryDisplayConfig gives
|
||||
// the exact rational rate (59.94, 143.998); EnumDisplaySettings' whole hertz is the fallback.
|
||||
static HMONITOR g_rate_mon;
|
||||
static volatile LONG g_rate_mhz; // millihertz, 0 unknown
|
||||
|
||||
static double monitor_rate_hz(HMONITOR mon) {
|
||||
MONITORINFOEXW mi = {.cbSize = sizeof mi};
|
||||
if (!GetMonitorInfoW(mon, (MONITORINFO *)&mi)) return 0;
|
||||
double hz = 0;
|
||||
UINT32 np = 0, nm = 0;
|
||||
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &np, &nm) == ERROR_SUCCESS && np && nm) {
|
||||
DISPLAYCONFIG_PATH_INFO *paths = calloc(np, sizeof *paths);
|
||||
DISPLAYCONFIG_MODE_INFO *modes = calloc(nm, sizeof *modes);
|
||||
if (paths && modes && QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &np, paths, &nm, modes, NULL) == ERROR_SUCCESS)
|
||||
for (UINT32 i = 0; i < np && !hz; i++) {
|
||||
DISPLAYCONFIG_SOURCE_DEVICE_NAME src = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
|
||||
.size = sizeof src,
|
||||
.adapterId = paths[i].sourceInfo.adapterId,
|
||||
.id = paths[i].sourceInfo.id}};
|
||||
DISPLAYCONFIG_RATIONAL r = paths[i].targetInfo.refreshRate;
|
||||
if (DisplayConfigGetDeviceInfo(&src.header) == ERROR_SUCCESS && !wcscmp(src.viewGdiDeviceName, mi.szDevice) &&
|
||||
r.Denominator)
|
||||
hz = (double)r.Numerator / r.Denominator;
|
||||
}
|
||||
free(paths);
|
||||
free(modes);
|
||||
}
|
||||
DEVMODEW dm = {.dmSize = sizeof dm};
|
||||
if (!hz && EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
|
||||
hz = dm.dmDisplayFrequency; // 0 and 1 mean the hardware default
|
||||
return hz;
|
||||
}
|
||||
|
||||
// Window thread. force: the display mode changed, measure even on the same monitor.
|
||||
static void refresh_rate_update(HWND h, int force) {
|
||||
HMONITOR mon = MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY);
|
||||
if (!force && mon == g_rate_mon) return;
|
||||
g_rate_mon = mon;
|
||||
double hz = monitor_rate_hz(mon);
|
||||
LONG mhz = (LONG)(hz * 1000 + 0.5);
|
||||
if (InterlockedExchange(&g_rate_mhz, mhz) != mhz) rt_log("gfx: monitor refresh rate %.3f Hz", hz);
|
||||
}
|
||||
|
||||
double gfx_window_refresh_hz(void) { return InterlockedCompareExchange(&g_rate_mhz, 0, 0) / 1000.0; }
|
||||
|
||||
int gfx_window_monitor_size(int *w, int *h) {
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
if (!g_hwnd || !GetMonitorInfoW(MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) return 0;
|
||||
*w = mi.rcMonitor.right - mi.rcMonitor.left;
|
||||
*h = mi.rcMonitor.bottom - mi.rcMonitor.top;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
|
||||
LRESULT input_res;
|
||||
if (input_window_msg(h, m, w, l, &input_res)) return input_res;
|
||||
if (menu_window_msg(h, m, w, l, &input_res)) return input_res;
|
||||
switch (m) {
|
||||
case WM_KEYDOWN:
|
||||
if (w == VK_F11 && !(l & (1 << 30))) { // bit 30: auto-repeat
|
||||
input_cursor_suspend(1);
|
||||
toggle_fullscreen(h);
|
||||
input_cursor_suspend(0);
|
||||
cfg_set("TOTK_FULLSCREEN", is_fullscreen(h) ? "1" : "0"); // the menu shows it; the next start keeps it
|
||||
cfg_save();
|
||||
note_applied(); // what cfg_get reads now (the environment may still override it)
|
||||
}
|
||||
break;
|
||||
case WM_APP_SETTINGS:
|
||||
apply_settings(h);
|
||||
return 0;
|
||||
case WM_APP_FULLSCREEN:
|
||||
if (cfg_fullscreen() && !is_fullscreen(h)) {
|
||||
input_cursor_suspend(1);
|
||||
toggle_fullscreen(h);
|
||||
input_cursor_suspend(0);
|
||||
}
|
||||
return 0;
|
||||
case WM_SIZE:
|
||||
keep_awake(w != SIZE_MINIMIZED);
|
||||
if (w != SIZE_MINIMIZED) InterlockedExchange(&g_dirty, 1);
|
||||
refresh_rate_update(h, 0);
|
||||
return 0;
|
||||
case WM_EXITSIZEMOVE: // dragged onto another monitor
|
||||
refresh_rate_update(h, 0);
|
||||
break;
|
||||
case WM_DISPLAYCHANGE:
|
||||
refresh_rate_update(h, 1);
|
||||
break;
|
||||
case WM_GETDPISCALEDSIZE: // a framed window keeps its client size in pixels on a monitor at another scale
|
||||
if (!is_fullscreen(h) && !IsZoomed(h)) {
|
||||
RECT c, r;
|
||||
if (GetClientRect(h, &c)) {
|
||||
r = (RECT){0, 0, c.right, c.bottom};
|
||||
if (AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, LOWORD(w))) {
|
||||
*(SIZE *)l = (SIZE){r.right - r.left, r.bottom - r.top};
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // DefWindowProc: Windows scales the size with the DPI
|
||||
case WM_DPICHANGED: { // display scale changed, or the window reached a monitor at another scale
|
||||
const RECT *s = (const RECT *)l;
|
||||
InterlockedExchange(&g_dpi, LOWORD(w));
|
||||
rt_log("gfx: window DPI %u (%u%%)", LOWORD(w), LOWORD(w) * 100u / USER_DEFAULT_SCREEN_DPI);
|
||||
if (is_fullscreen(h)) { // keep covering the monitor the suggested rect lies on
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
if (GetMonitorInfoW(MonitorFromRect(s, MONITOR_DEFAULTTONEAREST), &mi))
|
||||
SetWindowPos(h, NULL, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
|
||||
mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
} else {
|
||||
SetWindowPos(h, NULL, s->left, s->top, s->right - s->left, s->bottom - s->top, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_ACTIVATEAPP: {
|
||||
int in = w != 0;
|
||||
InterlockedExchange(&g_focused, in);
|
||||
oe_set_focus(in); // pinned to 1 by TOTK_ASSUME_FOCUS inside
|
||||
oe_push_message(OE_MSG_FOCUS_STATE_CHANGED);
|
||||
InterlockedIncrement(&g_focus_msgs);
|
||||
if (in && g_fs_pending) {
|
||||
g_fs_pending = 0;
|
||||
PostMessageW(h, WM_APP_FULLSCREEN, 0, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_CLOSE:
|
||||
// The process ends with the window, as in MNG, once a save Commit still running in the background is done.
|
||||
rt_log("gfx: window closed; exiting");
|
||||
fs_save_flush();
|
||||
vk_pipeline_save();
|
||||
prof_write(); // ExitProcess runs no atexit handlers
|
||||
ExitProcess(0);
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
}
|
||||
return DefWindowProcW(h, m, w, l);
|
||||
}
|
||||
|
||||
static DWORD WINAPI window_main(void *arg) {
|
||||
(void)arg;
|
||||
HINSTANCE inst = GetModuleHandleW(NULL);
|
||||
// Icon resource 1 of totk.rc (runtime/totk.ico): the title bar, the taskbar button and Alt+Tab.
|
||||
WNDCLASSEXW wc = {.cbSize = sizeof wc, .lpfnWndProc = wnd_proc, .hInstance = inst,
|
||||
.hIcon = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXICON),
|
||||
GetSystemMetrics(SM_CYICON), LR_SHARED),
|
||||
.hIconSm = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
|
||||
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
|
||||
.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW), .lpszClassName = L"TotKRecompWindow"};
|
||||
RegisterClassExW(&wc);
|
||||
int cw, ch, ow, oh;
|
||||
cfg_window_size(&cw, &ch);
|
||||
outer_size(NULL, cw, ch, &ow, &oh);
|
||||
// The window opens behind the user's windows without taking focus: it shows the game's blank loading frames for a
|
||||
// while, and must not cover what the player works on meanwhile. The game still counts itself in focus until the
|
||||
// first WM_ACTIVATEAPP, so loading goes on, and keyboard input waits for the foreground (input_win.c). Its taskbar
|
||||
// button flashes until it is brought up, and TOTK_FULLSCREEN waits for that first activation.
|
||||
// TOTK_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
|
||||
int background = g_background = cfg_get("TOTK_BACKGROUND_WINDOW") && atoi(cfg_get("TOTK_BACKGROUND_WINDOW"));
|
||||
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
|
||||
NULL, inst, NULL);
|
||||
if (!g_hwnd) {
|
||||
rt_log("gfx: CreateWindowEx failed (%lu)", GetLastError());
|
||||
g_wnd_failed = 1;
|
||||
} else {
|
||||
// The frame was measured at the system DPI; the window may have opened on a monitor at another scale.
|
||||
InterlockedExchange(&g_dpi, (LONG)GetDpiForWindow(g_hwnd));
|
||||
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
if (!background) {
|
||||
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
|
||||
FlashWindowEx(&fw);
|
||||
g_fs_pending = cfg_fullscreen();
|
||||
}
|
||||
note_applied();
|
||||
refresh_rate_update(g_hwnd, 1);
|
||||
keep_awake(1);
|
||||
}
|
||||
SetEvent(g_wnd_ready);
|
||||
if (g_wnd_failed) return 1;
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, NULL, 0, 0) > 0) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- swapchain
|
||||
static const char *g_present_got = ""; // the TOTK_VK_PRESENT value of the mode the swapchain got; "" before one
|
||||
|
||||
@@ -546,7 +283,7 @@ static void flip_locked(void) {
|
||||
VkResult r = vkQueuePresentKHR(vk_ctx()->gfx_queue, &pi);
|
||||
vk_queue_unlock();
|
||||
vk_device_fault_report(r);
|
||||
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) InterlockedExchange(&g_dirty, 1);
|
||||
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1);
|
||||
}
|
||||
|
||||
static void free_capture(void) {
|
||||
@@ -564,10 +301,10 @@ static int create_swapchain(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
VkSurfaceCapabilitiesKHR caps;
|
||||
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(c->phys, g_surface, &caps) != VK_SUCCESS) return 0;
|
||||
RECT rc = {0};
|
||||
GetClientRect(g_hwnd, &rc);
|
||||
int cw = 0, ch = 0;
|
||||
wsys_client_size(&cw, &ch);
|
||||
VkExtent2D ext = caps.currentExtent.width != UINT32_MAX ? caps.currentExtent
|
||||
: (VkExtent2D){(uint32_t)rc.right, (uint32_t)rc.bottom};
|
||||
: (VkExtent2D){(uint32_t)cw, (uint32_t)ch};
|
||||
if (!ext.width || !ext.height) return 0;
|
||||
uint32_t nf = 0;
|
||||
vkGetPhysicalDeviceSurfaceFormatsKHR(c->phys, g_surface, &nf, NULL);
|
||||
@@ -575,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++)
|
||||
@@ -611,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);
|
||||
@@ -624,7 +364,7 @@ static int create_swapchain(void) {
|
||||
}
|
||||
free_capture();
|
||||
vk_queue_unlock();
|
||||
InterlockedExchange(&g_dirty, 0);
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -654,15 +394,15 @@ static int ensure_capture_buffer(void) {
|
||||
}
|
||||
|
||||
void vk_thumbs_enable(int n) {
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
if (!g_th_n && n > 0 && (g_th_tag = calloc((size_t)n, sizeof *g_th_tag))) g_th_n = n;
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
}
|
||||
|
||||
void vk_thumbs_frame(uint64_t frame) {
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
g_th_frame = frame;
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
}
|
||||
|
||||
static void free_thumbs(void) {
|
||||
@@ -694,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,
|
||||
@@ -711,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;
|
||||
}
|
||||
@@ -745,7 +487,7 @@ static void record_thumb(VkCommandBuffer cb, VkImage dst, unsigned fence_slot) {
|
||||
|
||||
int vk_thumbs_read(uint64_t frame, uint8_t *rgba) {
|
||||
int ok = 0;
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
if (g_th_n && g_th_ptr && frame) {
|
||||
unsigned ring = (unsigned)(frame % (uint64_t)g_th_n);
|
||||
if (g_th_tag[ring] == frame) {
|
||||
@@ -755,7 +497,7 @@ int vk_thumbs_read(uint64_t frame, uint8_t *rgba) {
|
||||
ok = g_th_fmt == VK_FORMAT_B8G8R8A8_UNORM || g_th_fmt == VK_FORMAT_B8G8R8A8_SRGB ? 2 : 1;
|
||||
}
|
||||
}
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
if (ok == 2)
|
||||
for (size_t i = 0; i < TH_BYTES; i += 4) {
|
||||
uint8_t t = rgba[i];
|
||||
@@ -765,17 +507,17 @@ int vk_thumbs_read(uint64_t frame, uint8_t *rgba) {
|
||||
}
|
||||
|
||||
void vk_present_capture(int on) {
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
g_capture = on;
|
||||
if (!on) g_cap_valid = 0;
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
}
|
||||
|
||||
int vk_present_captured(const uint8_t **px, int *w, int *h, VkFormat *fmt) {
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
int ok = g_cap_valid;
|
||||
if (ok) { *px = g_cap_ptr; *w = g_cap_w; *h = g_cap_h; *fmt = g_sc_fmt; }
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -785,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)];
|
||||
@@ -796,16 +539,30 @@ static void save_capture(const char *path) {
|
||||
rt_log("gfx: menu test shot %s (%dx%d)", path, g_cap_w, g_cap_h);
|
||||
}
|
||||
|
||||
// The rectangle {x, y, w, h} of a dw x dh swapchain image the cw x ch picture goes into: the whole image with
|
||||
// TOTK_ASPECT=stretch, else (fit, the default) the largest one of the picture's own aspect ratio, centred (a 16:9 picture
|
||||
// on the Steam Deck's 1280x800 screen gets bars of 40 rows above and below; a 16:9 picture in a 21:9 window, bars at the
|
||||
// sides).
|
||||
void gfx_fit_rect(int cw, int ch, int dw, int dh, int out[4]) {
|
||||
out[0] = out[1] = 0, out[2] = dw, out[3] = dh;
|
||||
if (cw <= 0 || ch <= 0 || !strcmp(cfg_value("TOTK_ASPECT"), "stretch")) return;
|
||||
if ((int64_t)dw * ch > (int64_t)dh * cw) out[2] = (int)(((int64_t)dh * cw + ch / 2) / ch); // window wider: pillarbox
|
||||
else out[3] = (int)(((int64_t)dw * ch + cw / 2) / cw); // taller: letterbox
|
||||
// A side of the window's parity keeps the two bars equal; a picture one pixel short of the window fills it
|
||||
out[2] += (dw - out[2]) & 1, out[3] += (dh - out[3]) & 1;
|
||||
out[0] = (dw - out[2]) / 2, out[1] = (dh - out[3]) / 2;
|
||||
}
|
||||
|
||||
static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4], int defer) {
|
||||
int general = layout == VK_IMAGE_LAYOUT_GENERAL;
|
||||
gfx_window_start();
|
||||
VkCtx *c = vk_ctx();
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
flip_locked();
|
||||
if (!g_sc || InterlockedCompareExchange(&g_dirty, 0, 0)) {
|
||||
if (!IsIconic(g_hwnd) && !create_swapchain() && !g_sc) goto out;
|
||||
if (!g_sc || plat_cas32(&g_dirty, 0, 0)) {
|
||||
if (!wsys_minimized() && !create_swapchain() && !g_sc) goto out;
|
||||
}
|
||||
if (IsIconic(g_hwnd) || !g_sc) goto out;
|
||||
if (wsys_minimized() || !g_sc) goto out;
|
||||
|
||||
unsigned slot = g_frame % FRAMES;
|
||||
if (vkWaitForFences(c->device, 1, &g_fence[slot], VK_TRUE, 5000000000ull) != VK_SUCCESS) {
|
||||
@@ -815,14 +572,15 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
uint32_t idx = 0;
|
||||
VkResult r = vkAcquireNextImageKHR(c->device, g_sc, 1000000000ull, g_acquire[slot], VK_NULL_HANDLE, &idx);
|
||||
if (r == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
InterlockedExchange(&g_dirty, 1);
|
||||
plat_xchg32(&g_dirty, 1);
|
||||
if (!create_swapchain()) goto out;
|
||||
r = vkAcquireNextImageKHR(c->device, g_sc, 1000000000ull, g_acquire[slot], VK_NULL_HANDLE, &idx);
|
||||
}
|
||||
vk_device_fault_report(r);
|
||||
if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) goto out; // lost, not ready: skip the frame
|
||||
if (r == VK_SUBOPTIMAL_KHR) InterlockedExchange(&g_dirty, 1);
|
||||
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;
|
||||
@@ -837,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);
|
||||
@@ -855,38 +614,63 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
|
||||
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
const int rect[4] = {cx, cy, cw, ch};
|
||||
// FSR (TOTK_UPSCALER) when the swapchain is larger than the crop; else the bilinear blit
|
||||
int fsr = general && vk_fsr_record(cb, slot, src, fmt, w, h, rect, dst, g_sc_ext.width, g_sc_ext.height);
|
||||
g_scaler = fsr ? 2 : (uint32_t)cw != g_sc_ext.width || (uint32_t)ch != g_sc_ext.height;
|
||||
int to[4]; // where the picture goes in the swapchain image: {x, y, w, h}
|
||||
gfx_fit_rect(cw, ch, (int)g_sc_ext.width, (int)g_sc_ext.height, to);
|
||||
if (to[2] != (int)g_sc_ext.width || to[3] != (int)g_sc_ext.height) { // black bars beside or above and below it
|
||||
VkClearColorValue black = {.float32 = {0, 0, 0, 1}};
|
||||
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
vkCmdClearColorImage(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &all);
|
||||
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
}
|
||||
// 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;
|
||||
const int *prect = graded ? grect : rect;
|
||||
// FSR (TOTK_UPSCALER) when the picture's rectangle is larger than the crop; else the bilinear blit
|
||||
int fsr = general && vk_fsr_record(cb, slot, psrc, pfmt, graded ? cw : w, graded ? ch : h, prect, dst, to);
|
||||
g_scaler = fsr ? 2 : cw != to[2] || ch != to[3];
|
||||
if (!fsr) {
|
||||
if (general) // a GENERAL source (vk_texture.c images): earlier submissions' writes become visible to the blit here
|
||||
sync_barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
|
||||
VkImageBlit region = {
|
||||
.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{cx, cy, 0}, {cx + cw, cy + ch, 1}},
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{0, 0, 0}, {(int)g_sc_ext.width, (int)g_sc_ext.height, 1}}};
|
||||
vkCmdBlitImage(cb, src, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst,
|
||||
.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
||||
.srcOffsets = {{prect[0], prect[1], 0}, {prect[0] + cw, prect[1] + ch, 1}},
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{to[0], to[1], 0}, {to[0] + to[2], to[1] + to[3], 1}}};
|
||||
vkCmdBlitImage(cb, psrc, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_LINEAR);
|
||||
if (general) // later submissions that write the source wait for the blit's read
|
||||
sync_barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
|
||||
sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
|
||||
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 0);
|
||||
}
|
||||
// last: the image's layout, and the stage and access of its last write, as each step leaves it
|
||||
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;
|
||||
@@ -943,7 +727,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
|
||||
if (menu_shot) save_capture(menu_shot);
|
||||
}
|
||||
out:
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
}
|
||||
|
||||
void vk_present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4]) {
|
||||
@@ -955,25 +739,23 @@ void vk_present_blit(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int
|
||||
}
|
||||
|
||||
void vk_present_flip(void) {
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
flip_locked();
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
}
|
||||
|
||||
void gfx_window_resize(int cw, int chh) {
|
||||
gfx_window_start();
|
||||
int ow, oh;
|
||||
outer_size(g_hwnd, cw, chh, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
EnterCriticalSection(&g_pl);
|
||||
wsys_resize(cw, chh);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
create_swapchain();
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
}
|
||||
|
||||
// Called by vk_shutdown before the device goes away; the window and its thread stay.
|
||||
void gfx_window_release_vk(void) {
|
||||
VkCtx *c = vk_ctx();
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
if (c && g_started) {
|
||||
vk_queue_lock();
|
||||
vkDeviceWaitIdle(c->device);
|
||||
@@ -981,6 +763,9 @@ void gfx_window_release_vk(void) {
|
||||
g_flip_on = 0; // its blit is done (device idle); the swapchain goes away unpresented
|
||||
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++) {
|
||||
@@ -998,28 +783,18 @@ void gfx_window_release_vk(void) {
|
||||
g_sc = VK_NULL_HANDLE; g_surface = VK_NULL_HANDLE; g_pool = VK_NULL_HANDLE; g_sc_n = 0;
|
||||
g_started = 0; // the window and its thread stay; the next gfx_window_start makes a new surface
|
||||
}
|
||||
LeaveCriticalSection(&g_pl);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
}
|
||||
|
||||
void gfx_window_start(void) {
|
||||
EnterCriticalSection(&g_start_lock);
|
||||
plat_rmutex_lock(&g_start_lock);
|
||||
if (!g_started) {
|
||||
if (!vk_init()) rt_fatal("gfx: no usable Vulkan device (see the vk: lines above)");
|
||||
VkCtx *c = vk_ctx();
|
||||
if (!g_hwnd) {
|
||||
g_wnd_failed = 0;
|
||||
g_wnd_ready = CreateEventW(NULL, TRUE, FALSE, NULL);
|
||||
HANDLE t = CreateThread(NULL, 0, window_main, NULL, 0, NULL);
|
||||
if (!t) rt_fatal("gfx: cannot start the window thread (error %lu)", GetLastError());
|
||||
CloseHandle(t);
|
||||
WaitForSingleObject(g_wnd_ready, INFINITE);
|
||||
CloseHandle(g_wnd_ready);
|
||||
if (g_wnd_failed) rt_fatal("gfx: window creation failed");
|
||||
}
|
||||
VkWin32SurfaceCreateInfoKHR si = {.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
|
||||
.hinstance = GetModuleHandleW(NULL), .hwnd = g_hwnd};
|
||||
VkResult vr = vkCreateWin32SurfaceKHR(c->instance, &si, NULL, &g_surface);
|
||||
if (vr != VK_SUCCESS) rt_fatal("gfx: vkCreateWin32SurfaceKHR failed (%d)", (int)vr);
|
||||
static int opened;
|
||||
if (!opened && !(opened = wsys_open())) rt_fatal("gfx: window creation failed");
|
||||
g_surface = wsys_surface(c->instance);
|
||||
if (!g_surface) rt_fatal("gfx: no Vulkan surface for the window");
|
||||
|
||||
VkCommandPoolCreateInfo pci = {.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
|
||||
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = c->gfx_family};
|
||||
@@ -1035,14 +810,14 @@ void gfx_window_start(void) {
|
||||
}
|
||||
g_frame = 0;
|
||||
g_started = 1;
|
||||
EnterCriticalSection(&g_pl);
|
||||
plat_rmutex_lock(&g_pl);
|
||||
create_swapchain();
|
||||
LeaveCriticalSection(&g_pl);
|
||||
RECT rc = {0};
|
||||
GetClientRect(g_hwnd, &rc);
|
||||
rt_log("gfx: window %ldx%ld at DPI %u, swapchain %ux%u format %d", rc.right, rc.bottom, gfx_window_dpi(),
|
||||
g_sc_ext.width, g_sc_ext.height, (int)g_sc_fmt);
|
||||
plat_rmutex_unlock(&g_pl);
|
||||
int cw = 0, ch = 0;
|
||||
wsys_client_size(&cw, &ch);
|
||||
rt_log("gfx: window %dx%d at DPI %u, swapchain %ux%u format %d", cw, ch, gfx_window_dpi(), g_sc_ext.width,
|
||||
g_sc_ext.height, (int)g_sc_fmt);
|
||||
}
|
||||
LeaveCriticalSection(&g_start_lock);
|
||||
plat_rmutex_unlock(&g_start_lock);
|
||||
vsync_ensure();
|
||||
}
|
||||
+394
-214
File diff suppressed because it is too large.
Load diff
+46
-37
@@ -1,14 +1,15 @@
|
||||
// --probe-shader: the shader service client (gfx_shader.c) against 32 census shaders. Needs build/xlate/totk_xlate.exe and
|
||||
// build/v121/shaders/ (tools/shader_census.py); uses a throwaway cache directory.
|
||||
// a census, build/<version>/shaders/ (tools/shader_census.py, probe_census_dir); uses a throwaway cache directory.
|
||||
// 1. Translate 32 shaders (mixed stages) through gfx_shader_get; each SPIR-V starts with the magic 0x07230203.
|
||||
// 2. Drop the memory cache and ask again: 32 hits from disk, 0 service requests. A third pass returns the same pointers.
|
||||
// 3. Send a malformed line straight to the service: it answers with an error line and still serves a valid request.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
int gfx_shader_raw_request(const char *line, char *reply, size_t reply_size);
|
||||
uint64_t gfx_shader_requests(void);
|
||||
@@ -48,16 +49,23 @@ static const char *str_field(const char *from, const char *to, const char *name,
|
||||
|
||||
static int hexval(char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; }
|
||||
|
||||
// The repository root: the nearest parent of totk.exe holding build/v121/shaders/jobs.json.
|
||||
static int find_root(char *root, size_t n) {
|
||||
char exe[MAX_PATH * 2];
|
||||
DWORD len = GetModuleFileNameA(NULL, exe, sizeof exe);
|
||||
if (!len || len >= sizeof exe) return 0;
|
||||
for (char *cut = strrchr(exe, '\\'); cut; cut = strrchr(exe, '\\')) {
|
||||
*cut = 0;
|
||||
char p[MAX_PATH * 2 + 40];
|
||||
snprintf(p, sizeof p, "%s\\build\\v121\\shaders\\jobs.json", exe);
|
||||
if (GetFileAttributesA(p) != INVALID_FILE_ATTRIBUTES) return snprintf(root, n, "%s", exe) < (int)n;
|
||||
// The shader census the probes translate (tools/shader_census.py --version <ver>): build/<ver>/shaders in the nearest
|
||||
// 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_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);
|
||||
if (!len || len >= sizeof exe) return 0;
|
||||
for (char *cut = plat_last_sep(exe); cut; cut = plat_last_sep(exe)) {
|
||||
*cut = 0;
|
||||
char p[PLAT_PATH_MAX * 2 + 40];
|
||||
snprintf(p, sizeof p, "%s/build/%s/shaders/jobs.json", exe, versions[v]);
|
||||
if (plat_file_exists(p)) return snprintf(dir, n, "%s/build/%s/shaders", exe, versions[v]) < (int)n;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -95,15 +103,15 @@ static int pick_entries(const char *jobs, Entry *out) {
|
||||
return got;
|
||||
}
|
||||
|
||||
static char root[MAX_PATH * 2];
|
||||
static char root[PLAT_PATH_MAX * 2]; // the census folder (probe_census_dir)
|
||||
|
||||
// Builds the program the game would hand NVN for a census entry and translates it.
|
||||
static const GfxShader *translate(const Entry *e, int *err) {
|
||||
char path[MAX_PATH * 2 + 64];
|
||||
char path[PLAT_PATH_MAX * 2 + 64];
|
||||
uint8_t c1[8192], control[0x880] = {0};
|
||||
if (e->c1_len > sizeof c1) return *err = 1, NULL;
|
||||
for (size_t b = 0; b < e->c1_len; b++) c1[b] = (uint8_t)(hexval(e->c1[b * 2]) << 4 | hexval(e->c1[b * 2 + 1]));
|
||||
snprintf(path, sizeof path, "%s\\build\\v121\\shaders\\code\\%s.code", root, e->key);
|
||||
snprintf(path, sizeof path, "%s/code/%s.code", root, e->key);
|
||||
size_t body_size;
|
||||
char *body = slurp(path, &body_size);
|
||||
if (!body) return *err = 1, NULL;
|
||||
@@ -120,28 +128,27 @@ static const GfxShader *translate(const Entry *e, int *err) {
|
||||
return s;
|
||||
}
|
||||
|
||||
static int remove_entry(const char *name, int kind, uint64_t size, void *dir) {
|
||||
(void)size;
|
||||
char f[PLAT_PATH_MAX * 2];
|
||||
snprintf(f, sizeof f, "%s/%s", (const char *)dir, name);
|
||||
if (kind == PLAT_FS_FILE) plat_fs_unlink(f);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void remove_dir(const char *dir) {
|
||||
char pat[MAX_PATH * 2];
|
||||
snprintf(pat, sizeof pat, "%s\\*", dir);
|
||||
WIN32_FIND_DATAA fd;
|
||||
HANDLE h = FindFirstFileA(pat, &fd);
|
||||
if (h != INVALID_HANDLE_VALUE) {
|
||||
do {
|
||||
char f[MAX_PATH * 2];
|
||||
snprintf(f, sizeof f, "%s\\%s", dir, fd.cFileName);
|
||||
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) DeleteFileA(f);
|
||||
} while (FindNextFileA(h, &fd));
|
||||
FindClose(h);
|
||||
}
|
||||
RemoveDirectoryA(dir);
|
||||
plat_fs_list(dir, remove_entry, (void *)dir);
|
||||
plat_fs_rmdir(dir);
|
||||
}
|
||||
|
||||
#define FAIL_IF(cond, ...) do { if (cond) { printf("probe-shader: FAILED: " __VA_ARGS__); printf("\n"); remove_dir(dir); return 1; } } while (0)
|
||||
|
||||
int probe_shader(void) {
|
||||
char path[MAX_PATH * 2 + 64];
|
||||
CHECK(find_root(root, sizeof root), "build/v121/shaders/jobs.json not found (run tools/shader_census.py)");
|
||||
snprintf(path, sizeof path, "%s\\build\\v121\\shaders\\jobs.json", root);
|
||||
char path[PLAT_PATH_MAX * 2 + 64];
|
||||
CHECK(probe_census_dir(root, sizeof root),
|
||||
"no build/v%d/shaders/jobs.json above the exe (run tools/shader_census.py --version v%d)", rt_game_version,
|
||||
rt_game_version);
|
||||
snprintf(path, sizeof path, "%s/jobs.json", root);
|
||||
size_t jobs_size;
|
||||
char *jobs = slurp(path, &jobs_size);
|
||||
CHECK(jobs, "cannot read %s", path);
|
||||
@@ -149,9 +156,11 @@ int probe_shader(void) {
|
||||
int count = pick_entries(jobs, entries);
|
||||
CHECK(count == WANT, "only %d census shaders usable", count);
|
||||
|
||||
char dir[MAX_PATH];
|
||||
snprintf(dir, sizeof dir, "%s\\totk-shader-probe-%lu", getenv("TEMP") ? getenv("TEMP") : ".", GetCurrentProcessId());
|
||||
CHECK(_putenv_s("TOTK_SHADER_CACHE", dir) == 0, "cannot set TOTK_SHADER_CACHE");
|
||||
char dir[PLAT_PATH_MAX];
|
||||
char tmp[PLAT_PATH_MAX];
|
||||
if (!plat_temp_dir(tmp, sizeof tmp)) strcpy(tmp, ".");
|
||||
snprintf(dir, sizeof dir, "%s/totk-shader-probe-%u", tmp, plat_process_id());
|
||||
CHECK(plat_setenv("TOTK_SHADER_CACHE", dir), "cannot set TOTK_SHADER_CACHE");
|
||||
|
||||
// pass 1: translate through the service
|
||||
const GfxShader *first[WANT];
|
||||
@@ -198,16 +207,16 @@ int probe_shader(void) {
|
||||
FAIL_IF(strncmp(reply, "fail ", 5), "malformed line answered with \"%s\"", reply);
|
||||
FAIL_IF(!gfx_shader_raw_request("{\"job\":{\"stage\":\"ps\"},\"out\":\"x\"}", reply, sizeof reply) || strncmp(reply, "fail ", 5),
|
||||
"job without key not rejected");
|
||||
char dir_json[MAX_PATH * 2], line[MAX_PATH * 4 + 1024], *o = dir_json;
|
||||
char dir_json[PLAT_PATH_MAX * 2], line[PLAT_PATH_MAX * 4 + 1024], *o = dir_json;
|
||||
for (const char *s = dir; *s; s++) { if (*s == '\\') *o++ = '\\'; *o++ = *s; }
|
||||
*o = 0;
|
||||
char esc_root[MAX_PATH * 2];
|
||||
char esc_root[PLAT_PATH_MAX * 2];
|
||||
o = esc_root;
|
||||
for (const char *s = root; *s; s++) { if (*s == '\\') *o++ = '\\'; *o++ = *s; }
|
||||
*o = 0;
|
||||
const Entry *e = &entries[0];
|
||||
snprintf(line, sizeof line,
|
||||
"{\"job\":{\"key\":\"%.64sa\",\"stage\":\"%s\",\"debug\":false,\"c1\":\"%.*s\",\"code\":\"%s\\\\build\\\\v121\\\\shaders\\\\code\\\\%.64s.code\"},\"out\":\"%s\"}",
|
||||
"{\"job\":{\"key\":\"%.64sa\",\"stage\":\"%s\",\"debug\":false,\"c1\":\"%.*s\",\"code\":\"%s/code/%.64s.code\"},\"out\":\"%s\"}",
|
||||
e->key, e->stage == GFX_STAGE_VS ? "vs" : e->stage == GFX_STAGE_FS ? "ps" : "cs", (int)(e->c1_len * 2), e->c1,
|
||||
esc_root, e->key, dir_json);
|
||||
FAIL_IF(!gfx_shader_raw_request(line, reply, sizeof reply), "service did not answer a valid request after the malformed one");
|
||||
|
||||
+39
-34
@@ -1,14 +1,16 @@
|
||||
// --probe-window: presents 300 frames of a cycling clear colour through vk_present_image, reads the swapchain image back
|
||||
// and compares every pixel, measures the vsync rate, flips the focus state with synthetic WM_ACTIVATEAPP messages and
|
||||
// resizes the window. Run with TOTK_VK_VALIDATION=1 to count validation errors.
|
||||
// and compares every pixel, measures the vsync rate, flips the focus state and drives the cursor release with synthetic
|
||||
// window events (the window back end's wsys_probe_*, gfx_wsys.h) and resizes the window. Run with TOTK_VK_VALIDATION=1
|
||||
// to count validation errors.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "gfx.h"
|
||||
#include "gfx_wsys.h"
|
||||
#include "input.h"
|
||||
#include "ui/menu.h"
|
||||
#include "vk.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define SRC_W 640
|
||||
#define SRC_H 360
|
||||
@@ -110,20 +112,27 @@ static int present_and_check(Source *s, int first, int i, int *w_out, int *h_out
|
||||
else if (fmt == VK_FORMAT_R8G8B8A8_UNORM) { want[0] = rgb[0]; want[1] = rgb[1]; want[2] = rgb[2]; }
|
||||
else { printf("probe-window: swapchain format %d is not UNORM RGBA/BGRA\n", (int)fmt); return 3; }
|
||||
want[3] = 255;
|
||||
uint32_t wv;
|
||||
uint32_t wv, black = 0xff000000u; // opaque black in both byte orders
|
||||
memcpy(&wv, want, 4);
|
||||
int r[4]; // the picture's rectangle; bars of black around it when the window's shape differs
|
||||
gfx_fit_rect(SRC_W, SRC_H, w, h, r);
|
||||
const uint32_t *p = (const uint32_t *)px;
|
||||
for (long k = 0, n = (long)w * h; k < n; k++)
|
||||
if (p[k] != wv) {
|
||||
printf("probe-window: frame %d pixel %ld is %08x, expected %08x\n", i, k, p[k], wv);
|
||||
for (long k = 0, n = (long)w * h; k < n; k++) {
|
||||
int x = (int)(k % w), y = (int)(k / w);
|
||||
uint32_t e = x >= r[0] && x < r[0] + r[2] && y >= r[1] && y < r[1] + r[3] ? wv : black;
|
||||
if (p[k] != e) {
|
||||
printf("probe-window: frame %d pixel %ld (%d, %d) is %08x, expected %08x\n", i, k, x, y, p[k], e);
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
*w_out = w;
|
||||
*h_out = h;
|
||||
return 0;
|
||||
}
|
||||
|
||||
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();
|
||||
if (!c) return fail("no Vulkan device");
|
||||
@@ -136,11 +145,11 @@ int probe_window(void) {
|
||||
if (present_and_check(&s, i == 0, i, &w, &h)) return fail("presented pixels differ");
|
||||
printf("probe-window: %d presents, readback %dx%d exact\n", FRAMES, w, h);
|
||||
|
||||
// Focus: synthetic WM_ACTIVATEAPP off and on, delivered synchronously on the window thread.
|
||||
// Focus: synthetic focus loss and gain, delivered synchronously on the window thread.
|
||||
unsigned m0 = gfx_window_focus_messages();
|
||||
SendMessageW(gfx_window_handle(), WM_ACTIVATEAPP, FALSE, 0);
|
||||
wsys_probe_focus(0);
|
||||
int off = gfx_window_focused();
|
||||
SendMessageW(gfx_window_handle(), WM_ACTIVATEAPP, TRUE, 0);
|
||||
wsys_probe_focus(1);
|
||||
int on = gfx_window_focused();
|
||||
unsigned m1 = gfx_window_focus_messages();
|
||||
if (off != 0 || on != 1 || m1 - m0 != 2) {
|
||||
@@ -149,19 +158,17 @@ int probe_window(void) {
|
||||
}
|
||||
printf("probe-window: focus off/on pushed 2 messages (15)\n");
|
||||
|
||||
// Cursor release: Esc releases, a client click recaptures, a title bar activation releases. The probe window may not be
|
||||
// the foreground window, so this checks the release flag, not the clip state.
|
||||
HWND hw = gfx_window_handle();
|
||||
SendMessageW(hw, WM_KEYDOWN, VK_ESCAPE, 0);
|
||||
// Cursor release: Esc releases, a client click recaptures, a title bar activation releases (where the window system
|
||||
// reports one). The probe window may not be the foreground window, so this checks the release flag, not the capture.
|
||||
wsys_probe_escape();
|
||||
int esc = input_cursor_released();
|
||||
SendMessageW(hw, WM_LBUTTONDOWN, MK_LBUTTON, 0);
|
||||
wsys_probe_click(0);
|
||||
int click = input_cursor_released();
|
||||
SendMessageW(hw, WM_MOUSEACTIVATE, (WPARAM)hw, MAKELPARAM(HTCLIENT, WM_LBUTTONDOWN));
|
||||
wsys_probe_click(1);
|
||||
int client = input_cursor_released();
|
||||
SendMessageW(hw, WM_MOUSEACTIVATE, (WPARAM)hw, MAKELPARAM(HTCAPTION, WM_LBUTTONDOWN));
|
||||
int caption = input_cursor_released();
|
||||
SendMessageW(hw, WM_ACTIVATEAPP, FALSE, 0);
|
||||
SendMessageW(hw, WM_ACTIVATEAPP, TRUE, 0);
|
||||
int caption = wsys_probe_click(2) ? input_cursor_released() : 1;
|
||||
wsys_probe_focus(0);
|
||||
wsys_probe_focus(1);
|
||||
int back = input_cursor_released();
|
||||
if (menu_is_open()) menu_toggle(); // Esc opened the menu: the presents below compare the bare game image
|
||||
if (!esc || click || client || !caption || back) {
|
||||
@@ -172,33 +179,31 @@ int probe_window(void) {
|
||||
|
||||
// Resize: the client area changes, the swapchain follows, presenting still reads back exact.
|
||||
gfx_window_resize(1280, 720);
|
||||
RECT rc;
|
||||
GetClientRect(gfx_window_handle(), &rc);
|
||||
int cw = 0, ch = 0;
|
||||
wsys_client_size(&cw, &ch);
|
||||
for (int i = 0; i < 10; i++)
|
||||
if (present_and_check(&s, 0, FRAMES + i, &w, &h)) return fail("presents after resize");
|
||||
if (w != rc.right || h != rc.bottom) {
|
||||
printf("probe-window: readback %dx%d, client area %ldx%ld\n", w, h, (long)rc.right, (long)rc.bottom);
|
||||
if (w != cw || h != ch) {
|
||||
printf("probe-window: readback %dx%d, client area %dx%d\n", w, h, cw, ch);
|
||||
return fail("swapchain size after resize");
|
||||
}
|
||||
printf("probe-window: resized to %dx%d, presents still exact\n", w, h);
|
||||
vk_present_capture(0);
|
||||
|
||||
// Vsync: count ticks between two tick edges about 5 s apart.
|
||||
static volatile long ticks;
|
||||
static volatile int32_t ticks;
|
||||
gfx_vsync_bind_counter(&ticks);
|
||||
LARGE_INTEGER f, t0, t1;
|
||||
QueryPerformanceFrequency(&f);
|
||||
long a = ticks;
|
||||
while (ticks == a) Sleep(1);
|
||||
QueryPerformanceCounter(&t0);
|
||||
while (ticks == a) plat_sleep_ms(1);
|
||||
uint64_t t0 = plat_now_ns();
|
||||
long n0 = ticks;
|
||||
Sleep(5000);
|
||||
plat_sleep_ms(5000);
|
||||
long b = ticks;
|
||||
while (ticks == b) Sleep(1);
|
||||
QueryPerformanceCounter(&t1);
|
||||
while (ticks == b) plat_sleep_ms(1);
|
||||
uint64_t t1 = plat_now_ns();
|
||||
long n1 = ticks;
|
||||
double rate = (double)(n1 - n0) * (double)f.QuadPart / (double)(t1.QuadPart - t0.QuadPart);
|
||||
printf("probe-window: vsync %ld ticks in %.3f s = %.3f Hz\n", n1 - n0, (double)(t1.QuadPart - t0.QuadPart) / (double)f.QuadPart, rate);
|
||||
double rate = (double)(n1 - n0) * 1e9 / (double)(t1 - t0);
|
||||
printf("probe-window: vsync %ld ticks in %.3f s = %.3f Hz\n", n1 - n0, (double)(t1 - t0) / 1e9, rate);
|
||||
if (rate < 60.0 * 0.995 || rate > 60.0 * 1.005) return fail("vsync rate");
|
||||
|
||||
source_destroy(&s);
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
// The SDL3 back end of the window (gfx_wsys.h), for hosts without Win32: the window on a thread of its own that also
|
||||
// runs SDL's event loop, F11 fullscreen (borderless, the desktop's mode), the settings TOTK_WINDOW_SIZE and
|
||||
// TOTK_FULLSCREEN, the display's refresh rate, focus, the Vulkan surface. The swapchain and the present are
|
||||
// gfx_present.c's. Keyboard and mouse events go to input_sdl.c and to the menu (ui/menu_sdl.cpp).
|
||||
//
|
||||
// SDL wants its video calls on the thread that initialised video, so this thread does them all: the others hand it
|
||||
// work through run_on_window (a user event and a wait) or post a user event. Video is initialised before the window
|
||||
// exists: the Vulkan instance asks for SDL's surface extensions first (gfx_window_vk_extensions), the window comes with
|
||||
// gfx_window_start (wsys_open).
|
||||
//
|
||||
// TOTK_WINDOW_SIZE is in pixels. On a display that scales windows (Wayland, HiDPI) the window asks for its pixels in
|
||||
// the display's units (SDL_GetWindowDisplayScale), and SDL reports mouse positions in those units: input_sdl.c and the
|
||||
// menu scale them back (gfx_window_sdl_density).
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "gfx.h"
|
||||
#include "gfx_wsys.h"
|
||||
#include "input.h"
|
||||
#include "input_sdl.h"
|
||||
#include "ui/menu.h"
|
||||
#include "vk.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static SDL_Window *g_win;
|
||||
static uint32_t g_ev_base; // SDL_RegisterEvents: the user events below
|
||||
enum { EV_RUN, EV_SETTINGS, EV_CLOSE, EV_COUNT };
|
||||
static PlatEvent g_video_ready; // manual-reset: SDL video is up (or failed)
|
||||
static int g_video_ok;
|
||||
static PlatOnce g_thread_once = PLAT_ONCE_INIT;
|
||||
static uint32_t g_window_tid; // the window thread, which runs the video calls
|
||||
static volatile int32_t g_density_milli = 1000; // pixels per window unit, x1000
|
||||
|
||||
// ---- work on the window thread
|
||||
typedef struct {
|
||||
void (*fn)(void *arg);
|
||||
void *arg;
|
||||
PlatEvent done;
|
||||
} RunReq;
|
||||
|
||||
static void run_on_window(void (*fn)(void *arg), void *arg) {
|
||||
if (plat_thread_id() == g_window_tid) {
|
||||
fn(arg);
|
||||
return;
|
||||
}
|
||||
RunReq r = {fn, arg};
|
||||
plat_event_init(&r.done, 1, 0);
|
||||
SDL_Event e = {.user = {.type = g_ev_base + EV_RUN, .data1 = &r}};
|
||||
if (!SDL_PushEvent(&e)) rt_fatal("gfx: cannot reach the window thread (%s)", SDL_GetError());
|
||||
plat_event_wait(&r.done);
|
||||
}
|
||||
|
||||
static void post(int ev) {
|
||||
SDL_Event e = {.user = {.type = g_ev_base + (uint32_t)ev}};
|
||||
SDL_PushEvent(&e);
|
||||
}
|
||||
|
||||
double gfx_window_sdl_density(void) { return __atomic_load_n(&g_density_milli, __ATOMIC_RELAXED) / 1000.0; }
|
||||
|
||||
SDL_Window *gfx_window_sdl(void) { return g_win; }
|
||||
|
||||
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg) { run_on_window(fn, arg); }
|
||||
|
||||
// ---- settings: size and fullscreen
|
||||
static char g_applied_size[32];
|
||||
static int g_applied_fs;
|
||||
|
||||
static void note_applied(void) {
|
||||
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("TOTK_WINDOW_SIZE"));
|
||||
g_applied_fs = gfx_cfg_fullscreen();
|
||||
}
|
||||
|
||||
static int is_fullscreen(void) { return (SDL_GetWindowFlags(g_win) & SDL_WINDOW_FULLSCREEN) != 0; }
|
||||
|
||||
// The display scale as a DPI, 96 at 100% (the menu's text size floor); 96 before the window exists.
|
||||
unsigned gfx_window_dpi(void) {
|
||||
float scale = g_win ? SDL_GetWindowDisplayScale(g_win) : 0;
|
||||
return scale > 0 ? (unsigned)(scale * 96 + 0.5f) : 96;
|
||||
}
|
||||
|
||||
// The window's size in its units for w x h pixels.
|
||||
static void set_pixel_size(int w, int h) {
|
||||
float scale = SDL_GetWindowDisplayScale(g_win);
|
||||
if (!(scale > 0)) scale = 1;
|
||||
SDL_SetWindowSize(g_win, (int)(w / scale + 0.5f), (int)(h / scale + 0.5f));
|
||||
}
|
||||
|
||||
static void set_fullscreen(int on) {
|
||||
input_cursor_suspend(1);
|
||||
SDL_SetWindowFullscreen(g_win, on); // the desktop's mode: borderless over the display
|
||||
SDL_SyncWindow(g_win);
|
||||
input_cursor_suspend(0);
|
||||
}
|
||||
|
||||
// Window thread: the window takes the size and fullscreen state of the settings when they changed. A window resized by
|
||||
// hand keeps its size when another setting changes.
|
||||
static void apply_settings(void) {
|
||||
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = gfx_cfg_fullscreen();
|
||||
int fs_changed = fs != g_applied_fs;
|
||||
note_applied();
|
||||
if (size_changed) {
|
||||
int w, h;
|
||||
gfx_cfg_window_size(&w, &h);
|
||||
if (is_fullscreen()) SDL_SetWindowFullscreen(g_win, false);
|
||||
set_pixel_size(w, h);
|
||||
if (is_fullscreen() || (fs && !fs_changed)) SDL_SetWindowFullscreen(g_win, true);
|
||||
}
|
||||
if (fs_changed && fs != is_fullscreen()) set_fullscreen(fs);
|
||||
}
|
||||
|
||||
void gfx_window_apply_settings(void) {
|
||||
if (g_win) post(EV_SETTINGS);
|
||||
}
|
||||
|
||||
void gfx_window_request_close(void) {
|
||||
if (g_win) post(EV_CLOSE);
|
||||
}
|
||||
|
||||
// ---- the display's refresh rate and size
|
||||
static void refresh_rate_update(void) {
|
||||
const SDL_DisplayMode *m = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(g_win));
|
||||
double hz = 0;
|
||||
if (m && m->refresh_rate_denominator) hz = (double)m->refresh_rate_numerator / m->refresh_rate_denominator;
|
||||
else if (m) hz = m->refresh_rate;
|
||||
gfx_window_set_refresh(hz);
|
||||
}
|
||||
|
||||
int gfx_window_monitor_size(int *w, int *h) {
|
||||
if (!g_win) return 0;
|
||||
const SDL_DisplayMode *m = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(g_win));
|
||||
if (!m) return 0;
|
||||
*w = (int)(m->w * m->pixel_density + 0.5f);
|
||||
*h = (int)(m->h * m->pixel_density + 0.5f);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void density_update(void) {
|
||||
float d = SDL_GetWindowPixelDensity(g_win);
|
||||
__atomic_store_n(&g_density_milli, (int32_t)((d > 0 ? d : 1) * 1000 + 0.5f), __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
// ---- the window thread
|
||||
static void handle(const SDL_Event *e) {
|
||||
if (e->type == g_ev_base + EV_RUN) {
|
||||
RunReq *r = e->user.data1;
|
||||
r->fn(r->arg);
|
||||
plat_event_set(&r->done);
|
||||
return;
|
||||
}
|
||||
if (e->type == g_ev_base + EV_SETTINGS) {
|
||||
if (g_win) apply_settings();
|
||||
return;
|
||||
}
|
||||
if (e->type == g_ev_base + EV_CLOSE) gfx_window_closed();
|
||||
if (!g_win) return;
|
||||
input_sdl_event(e);
|
||||
menu_sdl_event(e);
|
||||
switch (e->type) {
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
if (e->key.scancode == SDL_SCANCODE_F11 && !e->key.repeat) {
|
||||
set_fullscreen(!is_fullscreen());
|
||||
cfg_set("TOTK_FULLSCREEN", is_fullscreen() ? "1" : "0"); // the menu shows it; the next start keeps it
|
||||
cfg_save();
|
||||
note_applied();
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
case SDL_EVENT_WINDOW_RESIZED:
|
||||
case SDL_EVENT_WINDOW_RESTORED:
|
||||
density_update();
|
||||
gfx_window_mark_dirty();
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
|
||||
case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
|
||||
case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED:
|
||||
density_update();
|
||||
refresh_rate_update();
|
||||
gfx_window_mark_dirty();
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
gfx_window_focus_changed(e->type == SDL_EVENT_WINDOW_FOCUS_GAINED);
|
||||
break;
|
||||
case SDL_EVENT_QUIT:
|
||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
gfx_window_closed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void window_main(void *arg) {
|
||||
(void)arg;
|
||||
g_window_tid = plat_thread_id();
|
||||
SDL_SetHint(SDL_HINT_APP_ID, "totk-recomp"); // Wayland's app id: the desktop file and the taskbar match it
|
||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "0");
|
||||
g_video_ok = SDL_Init(SDL_INIT_VIDEO);
|
||||
if (!g_video_ok) rt_log("gfx: SDL video init failed (%s)", SDL_GetError());
|
||||
// SDL's Vulkan queries need its Vulkan loader, which a window would load at creation: the instance and the device
|
||||
// are made before the window.
|
||||
else if (!SDL_Vulkan_LoadLibrary(NULL)) {
|
||||
rt_log("gfx: SDL cannot load Vulkan (%s)", SDL_GetError());
|
||||
g_video_ok = 0;
|
||||
} else {
|
||||
g_ev_base = SDL_RegisterEvents(EV_COUNT);
|
||||
rt_log("gfx: SDL %d.%d.%d, video driver %s", SDL_VERSIONNUM_MAJOR(SDL_GetVersion()),
|
||||
SDL_VERSIONNUM_MINOR(SDL_GetVersion()), SDL_VERSIONNUM_MICRO(SDL_GetVersion()), SDL_GetCurrentVideoDriver());
|
||||
}
|
||||
plat_event_set(&g_video_ready);
|
||||
if (!g_video_ok) return;
|
||||
for (;;) {
|
||||
SDL_Event e;
|
||||
if (SDL_WaitEventTimeout(&e, 100)) handle(&e);
|
||||
else input_sdl_idle(); // a cursor change requested from another thread meanwhile
|
||||
}
|
||||
}
|
||||
|
||||
static void video_start(void *arg) {
|
||||
(void)arg;
|
||||
plat_event_init(&g_video_ready, 1, 0);
|
||||
if (!plat_thread_spawn(window_main, NULL, "window", 0)) rt_fatal("gfx: cannot start the window thread");
|
||||
plat_event_wait(&g_video_ready);
|
||||
}
|
||||
|
||||
static int video_up(void) {
|
||||
plat_once(&g_thread_once, video_start, NULL);
|
||||
return g_video_ok;
|
||||
}
|
||||
|
||||
int gfx_window_vk_extensions(const char **out, int max) {
|
||||
if (!video_up()) return 0;
|
||||
Uint32 n = 0;
|
||||
const char *const *ext = SDL_Vulkan_GetInstanceExtensions(&n);
|
||||
if (!ext) {
|
||||
rt_log("gfx: SDL_Vulkan_GetInstanceExtensions failed (%s)", SDL_GetError());
|
||||
return 0;
|
||||
}
|
||||
int k = 0;
|
||||
for (Uint32 i = 0; i < n && k < max; i++) out[k++] = ext[i];
|
||||
return k;
|
||||
}
|
||||
|
||||
int gfx_window_vk_presentable(VkInstance inst, VkPhysicalDevice pd, uint32_t family) {
|
||||
return video_up() && SDL_Vulkan_GetPresentationSupport(inst, pd, family);
|
||||
}
|
||||
|
||||
// ---- gfx_wsys.h
|
||||
static int open_ok;
|
||||
|
||||
static void open_window(void *arg) {
|
||||
(void)arg;
|
||||
int w, h;
|
||||
gfx_cfg_window_size(&w, &h);
|
||||
g_win = SDL_CreateWindow("TotK Recomp", w, h, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
|
||||
if (!g_win) {
|
||||
rt_log("gfx: SDL_CreateWindow failed (%s)", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
set_pixel_size(w, h); // w x h were window units; the setting is in pixels
|
||||
density_update();
|
||||
note_applied();
|
||||
refresh_rate_update();
|
||||
if (cfg_get("TOTK_BACKGROUND_WINDOW")) rt_log("gfx: TOTK_BACKGROUND_WINDOW has no effect with SDL windows");
|
||||
if (gfx_cfg_fullscreen()) SDL_SetWindowFullscreen(g_win, true);
|
||||
SDL_SyncWindow(g_win);
|
||||
input_sdl_window(g_win);
|
||||
open_ok = 1;
|
||||
}
|
||||
|
||||
int wsys_open(void) {
|
||||
if (!video_up()) return 0;
|
||||
if (!g_win) run_on_window(open_window, NULL);
|
||||
return open_ok;
|
||||
}
|
||||
|
||||
typedef struct { VkInstance inst; VkSurfaceKHR surface; } SurfaceReq;
|
||||
|
||||
static void make_surface(void *arg) {
|
||||
SurfaceReq *r = arg;
|
||||
if (!SDL_Vulkan_CreateSurface(g_win, r->inst, NULL, &r->surface)) {
|
||||
rt_log("gfx: SDL_Vulkan_CreateSurface failed (%s)", SDL_GetError());
|
||||
r->surface = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
VkSurfaceKHR wsys_surface(VkInstance inst) {
|
||||
SurfaceReq r = {inst, VK_NULL_HANDLE};
|
||||
run_on_window(make_surface, &r);
|
||||
return r.surface;
|
||||
}
|
||||
|
||||
void wsys_client_size(int *w, int *h) {
|
||||
*w = *h = 0;
|
||||
if (g_win) SDL_GetWindowSizeInPixels(g_win, w, h);
|
||||
}
|
||||
|
||||
int wsys_minimized(void) { return g_win && (SDL_GetWindowFlags(g_win) & (SDL_WINDOW_MINIMIZED | SDL_WINDOW_HIDDEN)); }
|
||||
|
||||
static void resize_window(void *arg) {
|
||||
const int *wh = arg;
|
||||
set_pixel_size(wh[0], wh[1]);
|
||||
SDL_SyncWindow(g_win);
|
||||
}
|
||||
|
||||
void wsys_resize(int w, int h) {
|
||||
int wh[2] = {w, h};
|
||||
run_on_window(resize_window, wh);
|
||||
}
|
||||
|
||||
// ---- --probe-window: synthetic events through the event handler, on the window thread
|
||||
static void probe_event(void *arg) { handle(arg); }
|
||||
|
||||
void wsys_probe_focus(int in) {
|
||||
SDL_Event e = {.window = {.type = in ? SDL_EVENT_WINDOW_FOCUS_GAINED : SDL_EVENT_WINDOW_FOCUS_LOST,
|
||||
.windowID = SDL_GetWindowID(g_win)}};
|
||||
run_on_window(probe_event, &e);
|
||||
}
|
||||
|
||||
void wsys_probe_escape(void) {
|
||||
SDL_Event e = {.key = {.type = SDL_EVENT_KEY_DOWN, .scancode = SDL_SCANCODE_ESCAPE, .key = SDLK_ESCAPE, .down = true,
|
||||
.windowID = SDL_GetWindowID(g_win)}};
|
||||
run_on_window(probe_event, &e);
|
||||
e.key.type = SDL_EVENT_KEY_UP, e.key.down = false;
|
||||
run_on_window(probe_event, &e);
|
||||
}
|
||||
|
||||
// SDL reports no activation by a title bar click: only the client click exists.
|
||||
int wsys_probe_click(int where) {
|
||||
if (where == 2) return 0;
|
||||
SDL_Event e = {.button = {.type = SDL_EVENT_MOUSE_BUTTON_DOWN, .button = SDL_BUTTON_LEFT, .down = true,
|
||||
.windowID = SDL_GetWindowID(g_win)}};
|
||||
run_on_window(probe_event, &e);
|
||||
e.button.type = SDL_EVENT_MOUSE_BUTTON_UP, e.button.down = false;
|
||||
run_on_window(probe_event, &e);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
// The Win32 back end of the window (gfx_wsys.h): the window and its message loop on a thread of their own, F11
|
||||
// fullscreen, the settings TOTK_WINDOW_SIZE and TOTK_FULLSCREEN, the monitor's refresh rate, focus, the Vulkan surface.
|
||||
// The swapchain and the present are gfx_present.c's. The in-game menu's keyboard and mouse come from the window
|
||||
// procedure (ui/menu_win.cpp), and so do the game's (input_win.c).
|
||||
//
|
||||
// The process is per-monitor DPI aware (totk.manifest): every size here is physical pixels, TOTK_WINDOW_SIZE included,
|
||||
// and the swapchain matches the screen pixel for pixel. A window moved to a monitor at another display scale keeps its
|
||||
// client size (WM_GETDPISCALEDSIZE); the menu takes its text size floor from the window's DPI (gfx_window_dpi).
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
#include <imm.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "flight.h"
|
||||
#include "fs.h"
|
||||
#include "gfx.h"
|
||||
#include "gfx_wsys.h"
|
||||
#include "input.h"
|
||||
#include "input_win.h"
|
||||
#include "nvn_api.h"
|
||||
#include "ui/menu.h"
|
||||
#include "vk.h"
|
||||
#include "platform/platform.h"
|
||||
#include <vulkan/vulkan_win32.h>
|
||||
|
||||
// ---- window state
|
||||
static HWND g_hwnd;
|
||||
static PlatEvent g_wnd_ready; // manual-reset: the window thread made the window or gave up
|
||||
static int g_wnd_failed;
|
||||
static int g_background; // TOTK_BACKGROUND_WINDOW: the window stays behind the user's windows
|
||||
static int g_fs_pending; // window thread: TOTK_FULLSCREEN waits for the first activation
|
||||
static volatile int32_t g_dpi = USER_DEFAULT_SCREEN_DPI; // the window's DPI, set by the window thread
|
||||
|
||||
HWND gfx_window_handle(void) { return g_hwnd; }
|
||||
|
||||
int gfx_window_vk_extensions(const char **out, int max) {
|
||||
if (max < 2) return 0;
|
||||
out[0] = VK_KHR_SURFACE_EXTENSION_NAME;
|
||||
out[1] = VK_KHR_WIN32_SURFACE_EXTENSION_NAME;
|
||||
return 2;
|
||||
}
|
||||
|
||||
int gfx_window_vk_presentable(VkInstance inst, VkPhysicalDevice pd, uint32_t family) {
|
||||
(void)inst;
|
||||
return vkGetPhysicalDeviceWin32PresentationSupportKHR(pd, family) != VK_FALSE;
|
||||
}
|
||||
|
||||
// ---- window thread
|
||||
#define WND_STYLE WS_OVERLAPPEDWINDOW
|
||||
#define WM_APP_SETTINGS (WM_APP + 1)
|
||||
#define WM_APP_FULLSCREEN (WM_APP + 2) // the fullscreen setting, held back until the window is first activated
|
||||
|
||||
static WINDOWPLACEMENT g_saved_place = {sizeof g_saved_place}; // the window behind fullscreen (window thread)
|
||||
|
||||
static int is_fullscreen(HWND h) { return !(GetWindowLongW(h, GWL_STYLE) & WS_OVERLAPPEDWINDOW); }
|
||||
|
||||
#define cfg_fullscreen gfx_cfg_fullscreen
|
||||
#define cfg_window_size gfx_cfg_window_size
|
||||
|
||||
unsigned gfx_window_dpi(void) { return (unsigned)plat_cas32(&g_dpi, 0, 0); }
|
||||
|
||||
// The outer size of a framed window whose client area is cw x ch. The frame is measured at the window's DPI, or the
|
||||
// system DPI before the window exists: plain AdjustWindowRect always takes the system DPI, which is wrong on a monitor
|
||||
// at another display scale.
|
||||
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
|
||||
RECT r = {0, 0, cw, ch};
|
||||
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, h ? GetDpiForWindow(h) : GetDpiForSystem());
|
||||
*ow = r.right - r.left;
|
||||
*oh = r.bottom - r.top;
|
||||
}
|
||||
|
||||
// Windows turns the display off and sleeps after the idle timeout, and gamepad input does not count as activity. While
|
||||
// the window is up and not minimized it holds the display and the system on, as other games do. The state belongs to
|
||||
// the calling thread, so only the window thread (window_main and wnd_proc) calls this; the process exit releases it.
|
||||
static void keep_awake(int on) {
|
||||
static int cur = -1;
|
||||
if (on == cur) return;
|
||||
cur = on;
|
||||
SetThreadExecutionState(on ? ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED : ES_CONTINUOUS);
|
||||
}
|
||||
|
||||
// F11: borderless window over the whole monitor, and back to the saved window. The swapchain follows through WM_SIZE.
|
||||
// Not Alt+Enter: Enter is the Plus button (input_win.c). A background window (test runs) keeps its place behind the
|
||||
// user's windows and takes no focus.
|
||||
static void toggle_fullscreen(HWND h) {
|
||||
LONG style = GetWindowLongW(h, GWL_STYLE);
|
||||
if (style & WS_OVERLAPPEDWINDOW) {
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
if (!GetWindowPlacement(h, &g_saved_place) || !GetMonitorInfoW(MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY), &mi))
|
||||
return;
|
||||
SetWindowLongW(h, GWL_STYLE, (style & ~WS_OVERLAPPEDWINDOW) | WS_POPUP);
|
||||
SetWindowPos(h, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
|
||||
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
||||
SWP_NOOWNERZORDER | SWP_FRAMECHANGED | (g_background ? SWP_NOZORDER | SWP_NOACTIVATE : 0));
|
||||
} else {
|
||||
SetWindowLongW(h, GWL_STYLE, (style & ~WS_POPUP) | WND_STYLE);
|
||||
if (g_background) g_saved_place.showCmd = SW_SHOWNOACTIVATE;
|
||||
SetWindowPlacement(h, &g_saved_place);
|
||||
SetWindowPos(h, NULL, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED |
|
||||
(g_background ? SWP_NOACTIVATE : 0));
|
||||
}
|
||||
}
|
||||
|
||||
// The window size and fullscreen state last taken from the settings (window thread). apply_settings acts on a change of
|
||||
// either value only, so a window resized by hand keeps its size when another setting changes.
|
||||
static char g_applied_size[32];
|
||||
static int g_applied_fs;
|
||||
|
||||
static void note_applied(void) {
|
||||
snprintf(g_applied_size, sizeof g_applied_size, "%s", cfg_value("TOTK_WINDOW_SIZE"));
|
||||
g_applied_fs = cfg_fullscreen();
|
||||
}
|
||||
|
||||
// Window thread: the window takes the size and fullscreen state of the settings when they changed. In fullscreen the
|
||||
// size goes into the placement restored when fullscreen ends.
|
||||
static void apply_settings(HWND h) {
|
||||
int size_changed = strcmp(cfg_value("TOTK_WINDOW_SIZE"), g_applied_size) != 0, fs = cfg_fullscreen(), fs_changed = fs != g_applied_fs;
|
||||
note_applied();
|
||||
if (size_changed) {
|
||||
int cw, ch, ow, oh;
|
||||
cfg_window_size(&cw, &ch);
|
||||
outer_size(h, cw, ch, &ow, &oh);
|
||||
if (is_fullscreen(h)) {
|
||||
RECT *n = &g_saved_place.rcNormalPosition;
|
||||
n->right = n->left + ow;
|
||||
n->bottom = n->top + oh;
|
||||
} else {
|
||||
RECT have;
|
||||
if (GetClientRect(h, &have) && (have.right != cw || have.bottom != ch)) {
|
||||
if (IsZoomed(h)) ShowWindow(h, SW_RESTORE);
|
||||
SetWindowPos(h, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fs_changed && fs != is_fullscreen(h)) {
|
||||
input_cursor_suspend(1);
|
||||
toggle_fullscreen(h);
|
||||
input_cursor_suspend(0);
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_window_apply_settings(void) {
|
||||
if (g_hwnd) PostMessageW(g_hwnd, WM_APP_SETTINGS, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
void gfx_window_request_close(void) {
|
||||
if (g_hwnd) PostMessageW(g_hwnd, WM_CLOSE, 0, 0);
|
||||
}
|
||||
|
||||
// ---- refresh rate of the window's monitor (TOTK_FPS_LIMIT=display), measured when the window opens, lands on another
|
||||
// monitor or the display mode changes (gfx_window_set_refresh keeps it). QueryDisplayConfig gives the exact rational
|
||||
// rate (59.94, 143.998); EnumDisplaySettings' whole hertz is the fallback.
|
||||
static HMONITOR g_rate_mon;
|
||||
|
||||
static double monitor_rate_hz(HMONITOR mon) {
|
||||
MONITORINFOEXW mi = {.cbSize = sizeof mi};
|
||||
if (!GetMonitorInfoW(mon, (MONITORINFO *)&mi)) return 0;
|
||||
double hz = 0;
|
||||
UINT32 np = 0, nm = 0;
|
||||
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &np, &nm) == ERROR_SUCCESS && np && nm) {
|
||||
DISPLAYCONFIG_PATH_INFO *paths = calloc(np, sizeof *paths);
|
||||
DISPLAYCONFIG_MODE_INFO *modes = calloc(nm, sizeof *modes);
|
||||
if (paths && modes && QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &np, paths, &nm, modes, NULL) == ERROR_SUCCESS)
|
||||
for (UINT32 i = 0; i < np && !hz; i++) {
|
||||
DISPLAYCONFIG_SOURCE_DEVICE_NAME src = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
|
||||
.size = sizeof src,
|
||||
.adapterId = paths[i].sourceInfo.adapterId,
|
||||
.id = paths[i].sourceInfo.id}};
|
||||
DISPLAYCONFIG_RATIONAL r = paths[i].targetInfo.refreshRate;
|
||||
if (DisplayConfigGetDeviceInfo(&src.header) == ERROR_SUCCESS && !wcscmp(src.viewGdiDeviceName, mi.szDevice) &&
|
||||
r.Denominator)
|
||||
hz = (double)r.Numerator / r.Denominator;
|
||||
}
|
||||
free(paths);
|
||||
free(modes);
|
||||
}
|
||||
DEVMODEW dm = {.dmSize = sizeof dm};
|
||||
if (!hz && EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
|
||||
hz = dm.dmDisplayFrequency; // 0 and 1 mean the hardware default
|
||||
return hz;
|
||||
}
|
||||
|
||||
// Window thread. force: the display mode changed, measure even on the same monitor.
|
||||
static void refresh_rate_update(HWND h, int force) {
|
||||
HMONITOR mon = MonitorFromWindow(h, MONITOR_DEFAULTTOPRIMARY);
|
||||
if (!force && mon == g_rate_mon) return;
|
||||
g_rate_mon = mon;
|
||||
gfx_window_set_refresh(monitor_rate_hz(mon));
|
||||
}
|
||||
|
||||
int gfx_window_monitor_size(int *w, int *h) {
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
if (!g_hwnd || !GetMonitorInfoW(MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) return 0;
|
||||
*w = mi.rcMonitor.right - mi.rcMonitor.left;
|
||||
*h = mi.rcMonitor.bottom - mi.rcMonitor.top;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
|
||||
LRESULT input_res;
|
||||
if (input_window_msg(h, m, w, l, &input_res)) return input_res;
|
||||
if (menu_window_msg(h, m, w, l, &input_res)) return input_res;
|
||||
switch (m) {
|
||||
case WM_KEYDOWN:
|
||||
if (w == VK_F11 && !(l & (1 << 30))) { // bit 30: auto-repeat
|
||||
input_cursor_suspend(1);
|
||||
toggle_fullscreen(h);
|
||||
input_cursor_suspend(0);
|
||||
cfg_set("TOTK_FULLSCREEN", is_fullscreen(h) ? "1" : "0"); // the menu shows it; the next start keeps it
|
||||
cfg_save();
|
||||
note_applied(); // what cfg_get reads now (the environment may still override it)
|
||||
}
|
||||
break;
|
||||
case WM_APP_SETTINGS:
|
||||
apply_settings(h);
|
||||
return 0;
|
||||
case WM_APP_FULLSCREEN:
|
||||
if (cfg_fullscreen() && !is_fullscreen(h)) {
|
||||
input_cursor_suspend(1);
|
||||
toggle_fullscreen(h);
|
||||
input_cursor_suspend(0);
|
||||
}
|
||||
return 0;
|
||||
case WM_SIZE:
|
||||
keep_awake(w != SIZE_MINIMIZED);
|
||||
if (w != SIZE_MINIMIZED) gfx_window_mark_dirty();
|
||||
refresh_rate_update(h, 0);
|
||||
return 0;
|
||||
case WM_EXITSIZEMOVE: // dragged onto another monitor
|
||||
refresh_rate_update(h, 0);
|
||||
break;
|
||||
case WM_DISPLAYCHANGE:
|
||||
refresh_rate_update(h, 1);
|
||||
break;
|
||||
case WM_GETDPISCALEDSIZE: // a framed window keeps its client size in pixels on a monitor at another scale
|
||||
if (!is_fullscreen(h) && !IsZoomed(h)) {
|
||||
RECT c, r;
|
||||
if (GetClientRect(h, &c)) {
|
||||
r = (RECT){0, 0, c.right, c.bottom};
|
||||
if (AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, LOWORD(w))) {
|
||||
*(SIZE *)l = (SIZE){r.right - r.left, r.bottom - r.top};
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // DefWindowProc: Windows scales the size with the DPI
|
||||
case WM_DPICHANGED: { // display scale changed, or the window reached a monitor at another scale
|
||||
const RECT *s = (const RECT *)l;
|
||||
plat_xchg32(&g_dpi, LOWORD(w));
|
||||
rt_log("gfx: window DPI %u (%u%%)", LOWORD(w), LOWORD(w) * 100u / USER_DEFAULT_SCREEN_DPI);
|
||||
if (is_fullscreen(h)) { // keep covering the monitor the suggested rect lies on
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
if (GetMonitorInfoW(MonitorFromRect(s, MONITOR_DEFAULTTONEAREST), &mi))
|
||||
SetWindowPos(h, NULL, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left,
|
||||
mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
} else {
|
||||
SetWindowPos(h, NULL, s->left, s->top, s->right - s->left, s->bottom - s->top, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_ACTIVATEAPP: {
|
||||
int in = w != 0;
|
||||
gfx_window_focus_changed(in);
|
||||
if (in && g_fs_pending) {
|
||||
g_fs_pending = 0;
|
||||
PostMessageW(h, WM_APP_FULLSCREEN, 0, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_CLOSE:
|
||||
gfx_window_closed();
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
}
|
||||
return DefWindowProcW(h, m, w, l);
|
||||
}
|
||||
|
||||
static void window_main(void *arg) {
|
||||
(void)arg;
|
||||
HINSTANCE inst = GetModuleHandleW(NULL);
|
||||
// Icon resource 1 of totk.rc (runtime/totk.ico): the title bar, the taskbar button and Alt+Tab.
|
||||
WNDCLASSEXW wc = {.cbSize = sizeof wc, .lpfnWndProc = wnd_proc, .hInstance = inst,
|
||||
.hIcon = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXICON),
|
||||
GetSystemMetrics(SM_CYICON), LR_SHARED),
|
||||
.hIconSm = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
|
||||
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
|
||||
.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW), .lpszClassName = L"TotKRecompWindow"};
|
||||
RegisterClassExW(&wc);
|
||||
int cw, ch, ow, oh;
|
||||
cfg_window_size(&cw, &ch);
|
||||
outer_size(NULL, cw, ch, &ow, &oh);
|
||||
// The window opens behind the user's windows without taking focus: it shows the game's blank loading frames for a
|
||||
// while, and must not cover what the player works on meanwhile. The game still counts itself in focus until the
|
||||
// first WM_ACTIVATEAPP, so loading goes on, and keyboard input waits for the foreground (input_win.c). Its taskbar
|
||||
// button flashes until it is brought up, and TOTK_FULLSCREEN waits for that first activation.
|
||||
// TOTK_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
|
||||
int background = g_background = cfg_get("TOTK_BACKGROUND_WINDOW") && atoi(cfg_get("TOTK_BACKGROUND_WINDOW"));
|
||||
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
|
||||
NULL, inst, NULL);
|
||||
if (!g_hwnd) {
|
||||
rt_log("gfx: CreateWindowEx failed (%lu)", GetLastError());
|
||||
g_wnd_failed = 1;
|
||||
} else {
|
||||
// An IME switched on over the game attaches to this window and then shows its candidate window and eats the keys
|
||||
// it composes, although there is nothing here to type into (a Chinese IME takes Shift, the B button by default).
|
||||
// Detaching the game's window leaves the software keyboard alone: its edit box sits in a window of its own
|
||||
// (swkbd_win.c) and gets the IME it needs.
|
||||
ImmAssociateContext(g_hwnd, NULL);
|
||||
// The frame was measured at the system DPI; the window may have opened on a monitor at another scale.
|
||||
plat_xchg32(&g_dpi, (int32_t)GetDpiForWindow(g_hwnd));
|
||||
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
if (!background) {
|
||||
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
|
||||
FlashWindowEx(&fw);
|
||||
g_fs_pending = cfg_fullscreen();
|
||||
}
|
||||
note_applied();
|
||||
refresh_rate_update(g_hwnd, 1);
|
||||
keep_awake(1);
|
||||
}
|
||||
plat_event_set(&g_wnd_ready);
|
||||
if (g_wnd_failed) return;
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, NULL, 0, 0) > 0) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- gfx_wsys.h
|
||||
// Makes the window on its thread and waits until it is there.
|
||||
int wsys_open(void) {
|
||||
g_wnd_failed = 0;
|
||||
plat_event_init(&g_wnd_ready, 1, 0);
|
||||
if (!plat_thread_spawn(window_main, NULL, "window", 0)) rt_fatal("gfx: cannot start the window thread");
|
||||
plat_event_wait(&g_wnd_ready);
|
||||
return !g_wnd_failed;
|
||||
}
|
||||
|
||||
VkSurfaceKHR wsys_surface(VkInstance inst) {
|
||||
VkWin32SurfaceCreateInfoKHR si = {.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
|
||||
.hinstance = GetModuleHandleW(NULL), .hwnd = g_hwnd};
|
||||
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||
VkResult r = vkCreateWin32SurfaceKHR(inst, &si, NULL, &surface);
|
||||
if (r != VK_SUCCESS) rt_log("gfx: vkCreateWin32SurfaceKHR failed (%d)", (int)r);
|
||||
return r == VK_SUCCESS ? surface : VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void wsys_client_size(int *w, int *h) {
|
||||
RECT rc = {0};
|
||||
GetClientRect(g_hwnd, &rc);
|
||||
*w = rc.right, *h = rc.bottom;
|
||||
}
|
||||
|
||||
int wsys_minimized(void) { return IsIconic(g_hwnd) != 0; }
|
||||
|
||||
void wsys_resize(int cw, int ch) {
|
||||
int ow, oh;
|
||||
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, NULL, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
void wsys_probe_focus(int in) { SendMessageW(g_hwnd, WM_ACTIVATEAPP, in ? TRUE : FALSE, 0); }
|
||||
|
||||
void wsys_probe_escape(void) { SendMessageW(g_hwnd, WM_KEYDOWN, VK_ESCAPE, 0); }
|
||||
|
||||
int wsys_probe_click(int where) {
|
||||
if (where == 0) SendMessageW(g_hwnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
|
||||
else SendMessageW(g_hwnd, WM_MOUSEACTIVATE, (WPARAM)g_hwnd, MAKELPARAM(where == 1 ? HTCLIENT : HTCAPTION, WM_LBUTTONDOWN));
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Between gfx_present.c (swapchain, present, vsync) and the window system's back end (gfx_window_win.c with Win32,
|
||||
// gfx_window_sdl.c with SDL). Host threads only.
|
||||
#pragma once
|
||||
#include "vk.h"
|
||||
|
||||
// ---- what the back end provides
|
||||
int wsys_open(void); // makes the window (and its thread); 0 on failure. Once.
|
||||
VkSurfaceKHR wsys_surface(VkInstance inst); // a new Vulkan surface of the window; VK_NULL_HANDLE on failure
|
||||
void wsys_client_size(int *w, int *h); // the drawable area in pixels
|
||||
int wsys_minimized(void); // nothing to present into
|
||||
void wsys_resize(int w, int h); // gfx_window_resize: the drawable area becomes w x h
|
||||
// --probe-window (gfx_window_probe.c): synthetic window events, handled on the window thread before these return.
|
||||
void wsys_probe_focus(int in); // the window gains (1) or loses (0) the focus
|
||||
void wsys_probe_escape(void); // Esc pressed
|
||||
// A left click: 0 in the client area, 1 activating the window from its client area, 2 activating it from the title bar.
|
||||
// 0 when the window system has no such event (then the caller skips that check).
|
||||
int wsys_probe_click(int where);
|
||||
|
||||
// ---- what gfx_present.c provides the back end
|
||||
void gfx_window_mark_dirty(void); // the window changed size: the next present rebuilds the swapchain
|
||||
void gfx_window_focus_changed(int in); // the window gained (1) or lost (0) the focus
|
||||
void gfx_window_closed(void); // the player closed the window: saves, then ends the process
|
||||
void gfx_window_set_refresh(double hz); // the refresh rate of the window's monitor, 0 unknown
|
||||
int gfx_cfg_fullscreen(void); // TOTK_FULLSCREEN
|
||||
void gfx_cfg_window_size(int *w, int *h); // TOTK_WINDOW_SIZE, the default when unparsable or out of range
|
||||
@@ -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,
|
||||
};
|
||||
+34
-6
@@ -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
|
||||
@@ -91,11 +94,19 @@ static inline void ret_d(Cpu *c, double d) { uint64_t b; memcpy(&b, &d, 8); c->v
|
||||
typedef struct { const char *name; GuestFn fn; } RtHostEntry;
|
||||
#define RT_CAT2(a, b) a##b
|
||||
#define RT_CAT(a, b) RT_CAT2(a, b)
|
||||
// File-scope declarations; fn must be declared before. The loader binds them by symbol name.
|
||||
// File-scope declarations; fn must be declared before. The loader binds them by symbol name. The entries of every
|
||||
// object file meet in one section per kind, which hostfn.c walks: on PE/COFF ".ovr$m", which the linker sorts between
|
||||
// hostfn.c's ".ovr$a" and ".ovr$z" markers; on ELF "totk_ovr", which the linker brackets with __start_totk_ovr and
|
||||
// __stop_totk_ovr.
|
||||
#ifdef _WIN32
|
||||
#define RT_HOST_SECTION(kind) section("." #kind "$m")
|
||||
#else
|
||||
#define RT_HOST_SECTION(kind) section("totk_" #kind)
|
||||
#endif
|
||||
#define HOST_OVERRIDE(sym, fn) /* replaces sym in every GOT/GLOB_DAT/ABS64 of every module */ \
|
||||
__attribute__((used, section(".ovr$m"))) static const RtHostEntry RT_CAT(rt_ovr_, __COUNTER__) = {sym, fn}
|
||||
__attribute__((used, RT_HOST_SECTION(ovr))) static const RtHostEntry RT_CAT(rt_ovr_, __COUNTER__) = {sym, fn}
|
||||
#define HOST_OBSERVER(sym, fn) /* JUMP_SLOT only; fn calls rt_original(c) */ \
|
||||
__attribute__((used, section(".obs$m"))) static const RtHostEntry RT_CAT(rt_obs_, __COUNTER__) = {sym, fn}
|
||||
__attribute__((used, RT_HOST_SECTION(obs))) static const RtHostEntry RT_CAT(rt_obs_, __COUNTER__) = {sym, fn}
|
||||
uint64_t hostfn_register(const char *name, GuestFn fn); // synthetic address; before hostfn_seal only. The same
|
||||
// (name, fn) pair returns the same address
|
||||
GuestFn hostfn_override(const char *name); // NULL if none
|
||||
@@ -176,13 +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: 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_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
|
||||
@@ -233,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
|
||||
@@ -240,8 +261,13 @@ 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)
|
||||
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
|
||||
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
|
||||
void res_fov_live(void); // TOTK_FOV to the camera (set fov); on a settings change
|
||||
// ---- code patches (patches.c) and the frame step (frame_rate.c)
|
||||
void rt_patches_init(void); // after ldr_load: switches from the environment, the patch
|
||||
// page committed; logs the sets
|
||||
@@ -281,13 +307,15 @@ void rt_boot(void); // boot.c
|
||||
// modules, registers its host functions, then seals (hostfn_seal, or implicitly at the first rt_call). Return the
|
||||
// process exit code.
|
||||
int probe_cfg(void); // cfg_probe.c
|
||||
int probe_dpi(void); // dpi_probe.c
|
||||
int probe_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
|
||||
int probe_nvn(void), probe_nvn_tex(void); // nvn_probe.c, nvn_tex_probe.c
|
||||
int probe_window(void); // gfx_window.c
|
||||
int probe_window(void); // gfx_window_probe.c
|
||||
int probe_shader(void); // gfx_shader.c
|
||||
int probe_census_dir(char *dir, size_t n); // gfx_shader_probe.c: build/<version>/shaders for the probes
|
||||
int probe_vkmem(void), probe_vkexec(void), probe_astc(void); // tex_probe.c, vk_exec_probe.c, astc_probe.c
|
||||
int probe_movie(void); // movie_probe.c: host VP9 movie::Decoder on one RomFS movie
|
||||
int vk_pipeline_prepare_shaders(void); // vk_pipeline.c: --prepare-shaders (tools/prepare_shaders.py)
|
||||
|
||||
+12
-1
@@ -14,11 +14,22 @@
|
||||
#define MAX_HOSTFN 4096 // dispatch.c holds HOST_CAP / 2 host functions
|
||||
#define MAX_ADDED 64
|
||||
|
||||
#ifdef _WIN32
|
||||
// PE/COFF: the linker sorts .ovr$a < .ovr$m (entries, host.h) < .ovr$z by the text after '$'.
|
||||
__attribute__((section(".ovr$a"), used)) static const RtHostEntry ovr_start[1];
|
||||
__attribute__((section(".ovr$z"), used)) static const RtHostEntry ovr_stop[1];
|
||||
__attribute__((section(".obs$a"), used)) static const RtHostEntry obs_start[1];
|
||||
__attribute__((section(".obs$z"), used)) static const RtHostEntry obs_stop[1];
|
||||
#else
|
||||
// ELF: the linker defines __start_<section> and __stop_<section> around a section named like a C identifier. Weak: a
|
||||
// build without observers has no such section, and both bounds are then NULL.
|
||||
extern const RtHostEntry __start_totk_ovr[] __attribute__((weak)), __stop_totk_ovr[] __attribute__((weak));
|
||||
extern const RtHostEntry __start_totk_obs[] __attribute__((weak)), __stop_totk_obs[] __attribute__((weak));
|
||||
#define ovr_start __start_totk_ovr
|
||||
#define ovr_stop __stop_totk_ovr
|
||||
#define obs_start __start_totk_obs
|
||||
#define obs_stop __stop_totk_obs
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
@@ -110,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);
|
||||
|
||||
+760
@@ -0,0 +1,760 @@
|
||||
// Host input for nn::hid (runtime/ov_input.c): an SDL3 gamepad (no SDL window needed), the keyboard and the mouse,
|
||||
// merged into Switch npad button bits and stick axes. Button order follows libnx HidNpadButton (ISC). The key and pad
|
||||
// bindings are the tables in input_map.c (totk.ini overrides them). The keyboard and the mouse come from the window
|
||||
// system's back end (input_wsys.h: input_win.c with Win32, input_sdl.c with SDL), which also owns the cursor capture.
|
||||
// Keys and their map: docs/controls.md.
|
||||
// The gamepad also rumbles (input_rumble) and, when it has a gyro and an accelerometer, feeds the six-axis sensor from a
|
||||
// thread of its own (input_motion_start). Every SDL call on the gamepad is made under `lock`.
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "input_map.h"
|
||||
#include "input_wsys.h"
|
||||
#include "nvn_api.h"
|
||||
#include "ui/menu.h"
|
||||
#include "vk.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
enum { STICK_MAX = 32767, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
|
||||
|
||||
static PlatMutex lock = PLAT_MUTEX_INIT;
|
||||
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
|
||||
static SDL_Gamepad *pad;
|
||||
static uint64_t next_scan;
|
||||
static int pad_rumble; // the open gamepad has rumble motors
|
||||
static volatile int32_t pad_labels[4]; // ... and the labels of its south, east, west, north buttons (PAD_LABEL_*), any thread
|
||||
static int pad_east_a; // ... and labels its east face button A (Nintendo layout under TOTK_PAD_LAYOUT=auto)
|
||||
static int pad_layout = -1; // PAD_LAYOUT_* in effect for the open gamepad, -1 to pick again; live (cfg_version)
|
||||
static unsigned pad_layout_seen;
|
||||
static volatile int32_t pad_layout_shown = -1; // pad_layout for the menu (any thread), -1 without a gamepad
|
||||
static SDL_JoystickID sensor_pad; // the gamepad whose sensors the motion thread set up, 0 none
|
||||
static int sensor_ok; // ... and it has both sensors, enabled
|
||||
static int motion_thread; // 0 not started, 1 running, -1 off (TOTK_MOTION=0, no SDL)
|
||||
|
||||
// SDL buttons of the gamepad inputs (input_map.h PAD_*), by position; PAD_ZL and PAD_ZR are the triggers (axes).
|
||||
static const SDL_GamepadButton pad_sdl[PAD_COUNT] = {
|
||||
[PAD_SOUTH] = SDL_GAMEPAD_BUTTON_SOUTH, [PAD_EAST] = SDL_GAMEPAD_BUTTON_EAST, [PAD_WEST] = SDL_GAMEPAD_BUTTON_WEST,
|
||||
[PAD_NORTH] = SDL_GAMEPAD_BUTTON_NORTH, [PAD_LSTICK] = SDL_GAMEPAD_BUTTON_LEFT_STICK,
|
||||
[PAD_RSTICK] = SDL_GAMEPAD_BUTTON_RIGHT_STICK, [PAD_L] = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
|
||||
[PAD_R] = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, [PAD_START] = SDL_GAMEPAD_BUTTON_START, [PAD_BACK] = SDL_GAMEPAD_BUTTON_BACK,
|
||||
[PAD_DPAD_LEFT] = SDL_GAMEPAD_BUTTON_DPAD_LEFT, [PAD_DPAD_UP] = SDL_GAMEPAD_BUTTON_DPAD_UP,
|
||||
[PAD_DPAD_RIGHT] = SDL_GAMEPAD_BUTTON_DPAD_RIGHT, [PAD_DPAD_DOWN] = SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
||||
[PAD_LPADDLE1] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, [PAD_RPADDLE1] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1,
|
||||
[PAD_LPADDLE2] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, [PAD_RPADDLE2] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2,
|
||||
};
|
||||
|
||||
// TOTK_INPUT_SCRIPT=<file>: lines "<frame> <button> [hold]" or "<frame> LSTICK|RSTICK <x> <y> [hold]", ORed into the real input.
|
||||
// <frame> and its hold are 60 Hz ticks of game time since the first present (frame_step_ticks: presents at 60 fps), so
|
||||
// the title menus get the presses at the same game time at any frame limit. A line "@<ms> ..." is timed instead: <ms>
|
||||
// and its hold are milliseconds of the world clock (frame_step_world_us, game time since the loaded save's first
|
||||
// frame), so it lands at the same game time at any frame rate and load length. A timed event shorter than a frame is
|
||||
// applied once. In a script with timed lines (a recording) the untimed ones belong before the world clock: they stop
|
||||
// when it starts, as the load ends at another tick in each run.
|
||||
// at, hold: ticks, or microseconds of the world clock when timed.
|
||||
typedef struct { uint64_t at, hold; int bit, stick, timed; int32_t x, y; int logged; char text[48]; } ScriptEv;
|
||||
static const struct { const char *name; int bit; } script_buttons[] = {
|
||||
{"A", B_A}, {"B", B_B}, {"X", B_X}, {"Y", B_Y}, {"LCLICK", B_STICK_L}, {"RCLICK", B_STICK_R}, {"L", B_L}, {"R", B_R},
|
||||
{"ZL", B_ZL}, {"ZR", B_ZR}, {"PLUS", B_PLUS}, {"MINUS", B_MINUS}, {"LEFT", B_LEFT}, {"UP", B_UP}, {"RIGHT", B_RIGHT}, {"DOWN", B_DOWN},
|
||||
};
|
||||
static ScriptEv *script_ev;
|
||||
static int script_n, script_state; // 0 not loaded, 1 loaded
|
||||
static int script_mixed; // the script has timed lines: its untimed ones end where the world clock starts
|
||||
|
||||
static void script_load(void) {
|
||||
script_state = 1;
|
||||
const char *path = cfg_get("TOTK_INPUT_SCRIPT");
|
||||
if (!path || !*path) return;
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) { rt_log("input: cannot open TOTK_INPUT_SCRIPT %s", path); return; }
|
||||
char line[256];
|
||||
int cap = 0, ln = 0;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
ln++;
|
||||
char *h = strchr(line, '#');
|
||||
if (h) *h = 0;
|
||||
char name[32];
|
||||
double at;
|
||||
int off = 0, timed = line[0] == '@';
|
||||
if (sscanf(line + timed, "%lf %31s%n", &at, name, &off) < 2 || at < 0) continue;
|
||||
off += timed;
|
||||
double unit = timed ? 1000.0 : 1.0; // ms to us, or presents
|
||||
ScriptEv e = {.at = (uint64_t)(at * unit + 0.5), .hold = timed ? 66667 : 4, .bit = -1, .timed = timed};
|
||||
long a, b;
|
||||
double c;
|
||||
if (!strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
|
||||
int n = sscanf(line + off, "%ld %ld %lf", &a, &b, &c);
|
||||
if (n < 2) { rt_log("input: script line %d: %s needs x y [hold]", ln, name); continue; }
|
||||
e.stick = name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
|
||||
if (n >= 3 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
|
||||
snprintf(e.text, sizeof e.text, "%s %d %d", name, e.x, e.y);
|
||||
} else {
|
||||
for (size_t i = 0; i < sizeof script_buttons / sizeof *script_buttons; i++)
|
||||
if (!strcmp(name, script_buttons[i].name)) e.bit = script_buttons[i].bit;
|
||||
if (e.bit < 0) { rt_log("input: script line %d: unknown button %s", ln, name); continue; }
|
||||
if (sscanf(line + off, "%lf", &c) == 1 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
|
||||
snprintf(e.text, sizeof e.text, "%s", name);
|
||||
}
|
||||
if (script_n == cap) {
|
||||
cap = cap ? cap * 2 : 32;
|
||||
script_ev = realloc(script_ev, (size_t)cap * sizeof *script_ev);
|
||||
if (!script_ev) { script_n = 0; fclose(f); return; }
|
||||
}
|
||||
script_ev[script_n++] = e;
|
||||
script_mixed |= timed;
|
||||
}
|
||||
fclose(f);
|
||||
rt_log("input: script %s, %d events", path, script_n);
|
||||
}
|
||||
|
||||
static void script_apply(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
|
||||
if (!script_state) script_load();
|
||||
if (!script_n) return;
|
||||
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), tick = frame_step_ticks();
|
||||
int64_t w = frame_step_world_us();
|
||||
for (int i = 0; i < script_n; i++) {
|
||||
ScriptEv *e = &script_ev[i];
|
||||
if (e->timed ? w < 0 : script_mixed && w >= 0) continue;
|
||||
uint64_t now = e->timed ? (uint64_t)w : tick;
|
||||
if (now < e->at || (now >= e->at + e->hold && (e->logged || !e->timed))) continue;
|
||||
if (!e->logged) {
|
||||
e->logged = 1;
|
||||
if (e->timed) rt_log("input: script frame %llu at %.1f ms: %s", (unsigned long long)fr, w / 1000.0, e->text);
|
||||
else rt_log("input: script frame %llu tick %llu: %s", (unsigned long long)fr, (unsigned long long)tick, e->text);
|
||||
}
|
||||
if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
|
||||
else *b |= 1ull << e->bit;
|
||||
}
|
||||
}
|
||||
|
||||
// TOTK_INPUT_RECORD=<file>: writes the merged input as TOTK_INPUT_SCRIPT lines, one per button press and per stick
|
||||
// segment, so a played session replays as a script. A stick segment ends when an axis moves by more than REC_STICK_STEP.
|
||||
// An event that begins on the world clock is written timed ("@<ms>", hold in ms), one before it in ticks
|
||||
// (frame_step_ticks). When the world clock starts, held buttons and sticks are cut there and go on as timed events.
|
||||
enum { REC_STICK_STEP = 2048 };
|
||||
static FILE *rec;
|
||||
static int rec_state; // 0 not opened, 1 recording, -1 off
|
||||
static uint64_t rec_down[16]; // tick + 1 at which each button went down, 0 while up
|
||||
static int64_t rec_down_w[16]; // the world clock then, -1 before it started
|
||||
static uint64_t rec_stick_from[2]; // tick + 1 at which the current non-zero stick segment began
|
||||
static int64_t rec_stick_w[2];
|
||||
static int32_t rec_stick[2][2];
|
||||
static int64_t rec_last_w = -1; // the world clock at the previous apply
|
||||
|
||||
// "<tick> <what> <hold>" or "@<ms> <what> <hold ms>": from tick from and world clock from_w to now.
|
||||
static void rec_line(uint64_t from, int64_t from_w, uint64_t fr, int64_t w, const char *what) {
|
||||
if (from_w >= 0 && w >= from_w)
|
||||
fprintf(rec, "@%.1f %s %.1f\n", from_w / 1000.0, what, (w > from_w ? w - from_w : 1000) / 1000.0);
|
||||
else
|
||||
fprintf(rec, "%llu %s %llu\n", (unsigned long long)from, what, (unsigned long long)(fr > from ? fr - from : 1));
|
||||
}
|
||||
|
||||
static void rec_close_stick(int s, uint64_t fr, int64_t w) {
|
||||
if (!rec_stick_from[s]) return;
|
||||
char what[48];
|
||||
snprintf(what, sizeof what, "%s %d %d", s ? "RSTICK" : "LSTICK", rec_stick[s][0], rec_stick[s][1]);
|
||||
rec_line(rec_stick_from[s] - 1, rec_stick_w[s], fr, w, what);
|
||||
rec_stick_from[s] = 0;
|
||||
}
|
||||
|
||||
static void rec_flush_all(void) {
|
||||
if (rec_state != 1) return;
|
||||
uint64_t fr = frame_step_ticks() + 1;
|
||||
int64_t w = frame_step_world_us();
|
||||
for (int i = 0; i < 16; i++)
|
||||
if (rec_down[i]) { rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name); rec_down[i] = 0; }
|
||||
rec_close_stick(0, fr, w);
|
||||
rec_close_stick(1, fr, w);
|
||||
fflush(rec);
|
||||
}
|
||||
|
||||
static void rec_apply(uint64_t b, const int32_t sl[2], const int32_t sr[2]) {
|
||||
if (!rec_state) {
|
||||
const char *path = cfg_get("TOTK_INPUT_RECORD"); // no default: not recording when unset
|
||||
rec = path && *path ? fopen(path, "w") : NULL;
|
||||
rec_state = rec ? 1 : -1;
|
||||
if (path && *path && !rec) rt_log("input: cannot open TOTK_INPUT_RECORD %s", path);
|
||||
if (rec) { fprintf(rec, "# Recorded input (TOTK_INPUT_RECORD); frame = 60 Hz ticks of game time, @ms = world clock.\n"); atexit(rec_flush_all);
|
||||
rt_log("input: recording to %s", path); }
|
||||
}
|
||||
if (rec_state != 1) return;
|
||||
uint64_t fr = frame_step_ticks();
|
||||
int64_t w = frame_step_world_us();
|
||||
int wrote = 0;
|
||||
if (w >= 0 && rec_last_w < 0) { // the world clock starts: what is held goes on as timed events from @0
|
||||
for (int i = 0; i < 16; i++)
|
||||
if (rec_down[i]) {
|
||||
rec_line(rec_down[i] - 1, -1, fr, -1, script_buttons[i].name);
|
||||
rec_down[i] = fr + 1, rec_down_w[i] = w;
|
||||
wrote = 1;
|
||||
}
|
||||
for (int s = 0; s < 2; s++)
|
||||
if (rec_stick_from[s]) {
|
||||
rec_close_stick(s, fr, -1);
|
||||
rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
|
||||
wrote = 1;
|
||||
}
|
||||
}
|
||||
rec_last_w = w;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int bit = script_buttons[i].bit, down = (b >> bit & 1) != 0;
|
||||
if (down && !rec_down[i]) rec_down[i] = fr + 1, rec_down_w[i] = w;
|
||||
else if (!down && rec_down[i]) {
|
||||
rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name);
|
||||
rec_down[i] = 0;
|
||||
wrote = 1;
|
||||
}
|
||||
}
|
||||
const int32_t *st[2] = {sl, sr};
|
||||
for (int s = 0; s < 2; s++) {
|
||||
int moved = abs(st[s][0] - rec_stick[s][0]) > REC_STICK_STEP || abs(st[s][1] - rec_stick[s][1]) > REC_STICK_STEP ||
|
||||
((st[s][0] || st[s][1]) != (rec_stick[s][0] || rec_stick[s][1]));
|
||||
if (!moved) continue;
|
||||
if (rec_stick_from[s]) { rec_close_stick(s, fr, w); wrote = 1; }
|
||||
rec_stick[s][0] = st[s][0]; rec_stick[s][1] = st[s][1];
|
||||
if (st[s][0] || st[s][1]) rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
|
||||
}
|
||||
if (wrote) fflush(rec);
|
||||
}
|
||||
|
||||
static float mouse_sens = -1; // TOTK_MOUSE_SENS, 0 = mouse camera off; live (cfg_version)
|
||||
static unsigned mouse_sens_seen;
|
||||
|
||||
static float mouse_sens_get(void) {
|
||||
unsigned ver = cfg_version();
|
||||
if (mouse_sens >= 0 && ver == mouse_sens_seen) return mouse_sens;
|
||||
mouse_sens_seen = ver;
|
||||
double v = cfg_double("TOTK_MOUSE_SENS");
|
||||
mouse_sens = (float)(v < 0 ? 0 : v > 10 ? 10 : v);
|
||||
return mouse_sens;
|
||||
}
|
||||
|
||||
|
||||
// ---- 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];
|
||||
uint64_t now = plat_now_ns();
|
||||
int32_t dx, dy;
|
||||
if (!ikb_captured()) {
|
||||
ikb_mouse_take(&dx, &dy);
|
||||
held[0] = held[1] = 0;
|
||||
frame = ~0ull;
|
||||
} else {
|
||||
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
double age = frame == ~0ull ? 0 : (double)(now - t0) / 1e9;
|
||||
if (fr != frame) {
|
||||
double dt = age < 0.004 ? 0.004 : age > 0.1 ? 0.1 : age;
|
||||
ikb_mouse_take(&dx, &dy);
|
||||
input_mouse_stick(dx, dy, dt, mouse_sens_get(), held);
|
||||
frame = fr;
|
||||
t0 = now;
|
||||
} else if (age > 0.1) {
|
||||
held[0] = held[1] = 0; // frames stopped: do not hold a deflection
|
||||
}
|
||||
}
|
||||
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[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(int s) {
|
||||
unsigned ver = cfg_version();
|
||||
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.
|
||||
static void stick(SDL_GamepadAxis ax, SDL_GamepadAxis ay, float dead, int32_t out[2]) {
|
||||
input_pad_stick(SDL_GetGamepadAxis(pad, ax), -(int32_t)SDL_GetGamepadAxis(pad, ay), dead, out);
|
||||
}
|
||||
|
||||
// Open the first gamepad when none is open or the open one went away; logs once per change.
|
||||
static void scan_pads(void) {
|
||||
if (pad && SDL_GamepadConnected(pad)) return;
|
||||
if (pad) {
|
||||
SDL_CloseGamepad(pad); pad = NULL; sensor_pad = 0; sensor_ok = 0;
|
||||
plat_xchg32(&pad_layout_shown, -1);
|
||||
for (int i = 0; i < 4; i++) plat_xchg32(&pad_labels[i], PAD_LABEL_NONE);
|
||||
rt_log("input: gamepad disconnected");
|
||||
}
|
||||
int n = 0;
|
||||
SDL_JoystickID *ids = SDL_GetGamepads(&n);
|
||||
for (int i = 0; ids && i < n && !pad; i++) pad = SDL_OpenGamepad(ids[i]);
|
||||
SDL_free(ids);
|
||||
if (!pad) return;
|
||||
pad_rumble = SDL_GetBooleanProperty(SDL_GetGamepadProperties(pad), SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false);
|
||||
pad_east_a = SDL_GetGamepadButtonLabel(pad, SDL_GAMEPAD_BUTTON_EAST) == SDL_GAMEPAD_BUTTON_LABEL_A;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
SDL_GamepadButtonLabel lb = SDL_GetGamepadButtonLabel(pad, pad_sdl[PAD_SOUTH + i]);
|
||||
plat_xchg32(&pad_labels[i], lb == SDL_GAMEPAD_BUTTON_LABEL_A ? PAD_LABEL_A
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_B ? PAD_LABEL_B
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_X ? PAD_LABEL_X
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_Y ? PAD_LABEL_Y
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_CROSS ? PAD_LABEL_CROSS
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_CIRCLE ? PAD_LABEL_CIRCLE
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_SQUARE ? PAD_LABEL_SQUARE
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE ? PAD_LABEL_TRIANGLE
|
||||
: PAD_LABEL_NONE);
|
||||
}
|
||||
pad_layout = -1;
|
||||
int motion = SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL);
|
||||
rt_log("input: gamepad \"%s\" (%s, %s, east button %s)", SDL_GetGamepadName(pad), pad_rumble ? "rumble" : "no rumble",
|
||||
motion ? "gyro and accelerometer" : "no motion sensors", pad_east_a ? "labelled A" : "not labelled A");
|
||||
}
|
||||
|
||||
// Under lock: the layout of the open gamepad, picked again when a setting or the gamepad changed; logs each change.
|
||||
static int pad_layout_get(void) {
|
||||
unsigned ver = cfg_version();
|
||||
if (pad_layout >= 0 && ver == pad_layout_seen) return pad_layout;
|
||||
pad_layout_seen = ver;
|
||||
int was = pad_layout;
|
||||
const char *set = cfg_value("TOTK_PAD_LAYOUT");
|
||||
pad_layout = input_pad_layout_pick(set, pad_east_a);
|
||||
if (pad_layout != was)
|
||||
rt_log("input: gamepad layout %s (pad_layout = %s)", pad_layout == PAD_LAYOUT_XBOX ? "xbox" : "nintendo", set);
|
||||
plat_xchg32(&pad_layout_shown, pad_layout);
|
||||
return pad_layout;
|
||||
}
|
||||
|
||||
// The TOTK_BIND_* values last applied, one per line; the tables are built again when one changes (the menu's remapping rows write
|
||||
// them while the game runs). Under lock.
|
||||
static unsigned binds_seen = ~0u;
|
||||
static char binds_applied[4096];
|
||||
|
||||
static void binds_refresh(void) {
|
||||
unsigned v = cfg_version();
|
||||
if (v == binds_seen) return;
|
||||
binds_seen = v;
|
||||
char now[sizeof binds_applied];
|
||||
size_t len = 0;
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
char var[32];
|
||||
snprintf(var, sizeof var, "TOTK_BIND_%s", input_action_name(a));
|
||||
const char *s = cfg_get(var);
|
||||
len += (size_t)snprintf(now + len, len < sizeof now ? sizeof now - len : 0, "%s\n", s ? s : "");
|
||||
}
|
||||
if (!strcmp(now, binds_applied)) return;
|
||||
memcpy(binds_applied, now, sizeof now);
|
||||
input_map_configure();
|
||||
}
|
||||
|
||||
// TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry> (test runs): an SDL virtual gamepad whose sticks rest at these raw SDL axis values
|
||||
// (-32768..32767, down is +y), attached before the first scan so it is the gamepad the game reads: drift and full
|
||||
// deflection go through the same mapping and dead zone as a real pad.
|
||||
static void test_pad_attach(void) {
|
||||
static SDL_Joystick *joy; // kept open: a virtual joystick's axes are set through an open handle
|
||||
const char *e = cfg_get("TOTK_TEST_PAD");
|
||||
int v[4];
|
||||
if (!e || !*e) return;
|
||||
if (sscanf(e, "%d,%d,%d,%d", &v[0], &v[1], &v[2], &v[3]) != 4) {
|
||||
rt_log("input: TOTK_TEST_PAD=%s is not <lx>,<ly>,<rx>,<ry>; no test gamepad", e);
|
||||
return;
|
||||
}
|
||||
SDL_VirtualJoystickDesc d;
|
||||
SDL_INIT_INTERFACE(&d);
|
||||
d.type = SDL_JOYSTICK_TYPE_GAMEPAD;
|
||||
d.naxes = SDL_GAMEPAD_AXIS_COUNT;
|
||||
d.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
|
||||
d.name = "TotK test gamepad";
|
||||
SDL_JoystickID id = SDL_AttachVirtualJoystick(&d);
|
||||
joy = id ? SDL_OpenJoystick(id) : NULL;
|
||||
if (!joy) {
|
||||
rt_log("input: test gamepad not attached (%s)", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
static const SDL_GamepadAxis axes[4] = {SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_RIGHTX,
|
||||
SDL_GAMEPAD_AXIS_RIGHTY};
|
||||
for (int i = 0; i < 4; i++)
|
||||
SDL_SetJoystickVirtualAxis(joy, axes[i], (Sint16)(v[i] < -32768 ? -32768 : v[i] > 32767 ? 32767 : v[i]));
|
||||
rt_log("input: test gamepad attached, sticks at %d,%d %d,%d", v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
static void start(void) {
|
||||
binds_refresh();
|
||||
const char *e = cfg_get("TOTK_INPUT");
|
||||
if (e && !strcmp(e, "none")) { state = -1; rt_log("input: disabled (TOTK_INPUT=none)"); return; }
|
||||
if (!SDL_Init(SDL_INIT_GAMEPAD)) { state = -1; rt_log("input: SDL gamepad init failed (%s), keyboard only", SDL_GetError()); }
|
||||
else state = 1;
|
||||
if (state == 1) test_pad_attach();
|
||||
if (state == 1) {
|
||||
scan_pads();
|
||||
if (!pad) rt_log("input: no gamepad (keyboard only until one is connected)");
|
||||
}
|
||||
if (state == -1) state = 2; // keyboard only
|
||||
}
|
||||
|
||||
static void stick_bits(uint64_t *b, int32_t x, int32_t y, int first) {
|
||||
if (x < -STICK_DIR) *b |= 1ull << (first + 0);
|
||||
if (y > STICK_DIR) *b |= 1ull << (first + 1);
|
||||
if (x > STICK_DIR) *b |= 1ull << (first + 2);
|
||||
if (y < -STICK_DIR) *b |= 1ull << (first + 3);
|
||||
}
|
||||
|
||||
static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
|
||||
int64_t a = (int64_t)*x * *x + (int64_t)*y * *y, b = (int64_t)kx * kx + (int64_t)ky * ky;
|
||||
if (b > a) { *x = kx; *y = ky; }
|
||||
}
|
||||
|
||||
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
|
||||
static int pad_physical(uint8_t pd[PAD_COUNT]) {
|
||||
if ((!pad || !SDL_GamepadConnected(pad)) && plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
|
||||
SDL_UpdateGamepads();
|
||||
if (!pad || !SDL_GamepadConnected(pad)) return 0;
|
||||
for (int p = PAD_SOUTH; p < PAD_ZL; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]);
|
||||
pd[PAD_ZL] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_ON;
|
||||
pd[PAD_ZR] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_ON;
|
||||
for (int p = PAD_ZR + 1; p < PAD_COUNT; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]); // false without paddles
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Under lock, state 1: the open gamepad's buttons (Switch bits after TOTK_PAD_LAYOUT and the TOTK_BIND_* lines) and
|
||||
// sticks; 0 without a connected gamepad.
|
||||
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());
|
||||
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};
|
||||
plat_mutex_lock(&lock);
|
||||
if (!state) start();
|
||||
if (state > 0) {
|
||||
binds_refresh();
|
||||
if (state == 1) pad_read(&b, sl, sr);
|
||||
if (ikb_foreground()) {
|
||||
uint8_t down[KEY_COUNT] = {0};
|
||||
// Mouse buttons count only while the cursor is captured, and a button already held when the capture began
|
||||
// (the click that refocused the window) is ignored until it is released.
|
||||
static int was_captured;
|
||||
static uint8_t mouse_ignore[KEY_COUNT];
|
||||
int cap = ikb_captured();
|
||||
for (int k = 1; k < KEY_COUNT; k++) {
|
||||
down[k] = ikb_down(k);
|
||||
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
|
||||
if (cap && !was_captured) mouse_ignore[k] = down[k];
|
||||
if (!down[k]) mouse_ignore[k] = 0;
|
||||
if (!cap || mouse_ignore[k]) down[k] = 0;
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
script_apply(&b, sl, sr);
|
||||
rec_apply(b, sl, sr);
|
||||
// Minus and Plus held together (gamepad or keys) open or close the menu and never reach the game
|
||||
static int combo_held;
|
||||
int32_t st[4] = {sl[0], sl[1], sr[0], sr[1]};
|
||||
int toggle = input_menu_gate(&combo_held, menu_is_open(), &b, st);
|
||||
static int overlay_held; // L3+R3 step the performance overlay (F10)
|
||||
int overlay = input_overlay_gate(&overlay_held, &b);
|
||||
sl[0] = st[0], sl[1] = st[1], sr[0] = st[2], sr[1] = st[3];
|
||||
plat_mutex_unlock(&lock);
|
||||
if (toggle) menu_toggle();
|
||||
if (overlay) menu_perf_cycle();
|
||||
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
|
||||
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
|
||||
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
|
||||
}
|
||||
|
||||
// Text entry (swkbd_*.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
|
||||
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
|
||||
// g_block: 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
|
||||
static volatile int32_t g_block;
|
||||
static volatile int32_t g_menu; // the in-game menu, the same way (input_menu)
|
||||
|
||||
int input_cursor_allowed(void) {
|
||||
const char *in = cfg_get("TOTK_INPUT");
|
||||
return g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 && !(in && !strcmp(in, "none"));
|
||||
}
|
||||
|
||||
void input_block(int on) {
|
||||
plat_xchg32(&g_block, on ? 1 : 2);
|
||||
ikb_cursor_update();
|
||||
}
|
||||
|
||||
// The in-game menu (ui/menu.cpp) blocks the game's input the same way: g_menu 0 closed, 1 open, 2 closed, waiting for
|
||||
// the release. The cursor is released while it is open, for the menu's mouse.
|
||||
void input_menu(int open) {
|
||||
plat_xchg32(&g_menu, open ? 1 : 2);
|
||||
if (!open) ikb_menu_closed(); // the capture before Esc comes back
|
||||
ikb_cursor_update();
|
||||
}
|
||||
|
||||
// 1 while flag (g_block, g_menu) blocks the game's input; state 2 ends at the first poll with nothing held. A stick
|
||||
// axis counts as held as well: with the walk key the keyboard's sticks move part way without setting a direction bit, so
|
||||
// a gate that watched the buttons alone would let that movement through the moment the menu or the text entry closes.
|
||||
static int blocked(volatile int32_t *flag, uint64_t buttons, const int32_t *axes) {
|
||||
int32_t k = *flag;
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
|
||||
poll_merged(buttons, lx, ly, rx, ry);
|
||||
}
|
||||
|
||||
int input_pad_read(uint64_t *buttons, int32_t *lx, int32_t *ly) {
|
||||
uint64_t b = 0;
|
||||
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
|
||||
plat_mutex_lock(&lock);
|
||||
if (!state) start();
|
||||
if (state > 0) binds_refresh();
|
||||
int ok = state == 1 && pad_read(&b, sl, sr);
|
||||
plat_mutex_unlock(&lock);
|
||||
*buttons = b;
|
||||
*lx = sl[0];
|
||||
*ly = sl[1];
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint16_t rumble_motor(float a, float curve, float strength) {
|
||||
if (!(a > 0) || !(strength > 0)) return 0;
|
||||
float v = powf(a > 1 ? 1 : a, curve) * strength * 65535.0f + 0.5f;
|
||||
return v >= 65535.0f ? 65535 : (uint16_t)v;
|
||||
}
|
||||
|
||||
int input_pad_layout(void) {
|
||||
return (int)plat_cas32(&pad_layout_shown, 0, 0);
|
||||
}
|
||||
|
||||
int input_pad_label(int pad_id) {
|
||||
if (pad_id < PAD_SOUTH || pad_id > PAD_NORTH) return PAD_LABEL_NONE;
|
||||
return (int)plat_cas32(&pad_labels[pad_id - PAD_SOUTH], 0, 0);
|
||||
}
|
||||
|
||||
void input_map_snapshot(InputMap *m) {
|
||||
plat_mutex_lock(&lock);
|
||||
if (!state) start();
|
||||
if (state > 0) binds_refresh();
|
||||
*m = input_map;
|
||||
plat_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
int input_capture(int reset, int *id) {
|
||||
static uint8_t prev_k[KEY_COUNT], prev_p[PAD_COUNT];
|
||||
static int prev_esc;
|
||||
uint8_t k[KEY_COUNT] = {0}, p[PAD_COUNT] = {0};
|
||||
int fg = ikb_foreground();
|
||||
for (int i = 1; i < KEY_COUNT; i++) k[i] = fg && ikb_down(i);
|
||||
int esc = fg && ikb_escape();
|
||||
plat_mutex_lock(&lock);
|
||||
if (!state) start();
|
||||
if (state == 1) pad_physical(p);
|
||||
plat_mutex_unlock(&lock);
|
||||
int got = INPUT_CAPTURE_NONE;
|
||||
if (!reset) {
|
||||
if (esc && !prev_esc) got = INPUT_CAPTURE_CANCEL;
|
||||
for (int i = 1; i < KEY_COUNT && !got; i++)
|
||||
if (k[i] && !prev_k[i]) got = INPUT_CAPTURE_KEY, *id = i;
|
||||
for (int i = 1; i < PAD_COUNT && !got; i++)
|
||||
if (p[i] && !prev_p[i]) got = INPUT_CAPTURE_PAD, *id = i;
|
||||
}
|
||||
memcpy(prev_k, k, sizeof k);
|
||||
memcpy(prev_p, p, sizeof p);
|
||||
prev_esc = esc;
|
||||
return got;
|
||||
}
|
||||
|
||||
int input_rumble_triggers_enabled(void) {
|
||||
return cfg_bool("TOTK_RUMBLE_TRIGGERS");
|
||||
}
|
||||
|
||||
int input_rumble(uint16_t low, uint16_t high, uint32_t ms) {
|
||||
int ok = 0;
|
||||
static int triggers = -1;
|
||||
static unsigned seen;
|
||||
if (triggers < 0 || seen != cfg_version()) seen = cfg_version(), triggers = input_rumble_triggers_enabled();
|
||||
plat_mutex_lock(&lock);
|
||||
if (state == 1 && pad && pad_rumble) {
|
||||
ok = SDL_RumbleGamepad(pad, low, high, ms);
|
||||
if (triggers) SDL_RumbleGamepadTriggers(pad, high, high, ms); // unsupported pads return false: ignored
|
||||
}
|
||||
plat_mutex_unlock(&lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// SDL gamepad sensor axes (SDL_sensor.h, a gamepad held in front of you): x to the right, y up out of the face (lying
|
||||
// flat face up, +y points at the ceiling), z toward the player (out of the bottom edge). The accelerometer reads the
|
||||
// push against gravity in m/s^2 (lying flat: y = +9.81); the gyro reads rad/s, counter-clockwise positive (right hand).
|
||||
// Switch six-axis axes (nn::hid, as MacNativeGames' GCMotion feed and its Pro Controller HID path use them): x to the
|
||||
// right, y toward the top edge (away from the player), z out of the face; acceleration as gravity reads (lying flat:
|
||||
// z = -1 G). So Switch x = SDL x, Switch y = -SDL z, Switch z = SDL y: a rotation (right-handed to right-handed), so the
|
||||
// gyro keeps its sense, while the acceleration is also negated (push -> gravity) and scaled to G.
|
||||
void input_motion_axes(const float a[3], const float g[3], double acc[3], double gyro[3]) {
|
||||
acc[0] = -a[0] / SDL_STANDARD_GRAVITY;
|
||||
acc[1] = a[2] / SDL_STANDARD_GRAVITY;
|
||||
acc[2] = -a[1] / SDL_STANDARD_GRAVITY;
|
||||
gyro[0] = g[0];
|
||||
gyro[1] = -g[2];
|
||||
gyro[2] = g[1];
|
||||
}
|
||||
|
||||
// Under lock: (re)enable the sensors when the open gamepad changed; 1 if it has both and they are on.
|
||||
static int sensors_ready(void) {
|
||||
SDL_JoystickID id = SDL_GetGamepadID(pad);
|
||||
if (id == sensor_pad) return sensor_ok;
|
||||
sensor_pad = id;
|
||||
// Nothing drains the SDL event queue: no event per sample (the data still updates, SDL_SendJoystickSensor)
|
||||
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_SENSOR_UPDATE, false);
|
||||
sensor_ok =SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL) &&
|
||||
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_GYRO, true) &&
|
||||
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, true);
|
||||
if (sensor_ok) rt_log("input: motion sensors on (gyro %.0f Hz, accelerometer %.0f Hz)",
|
||||
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_GYRO),
|
||||
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_ACCEL));
|
||||
else rt_log("input: \"%s\" has no gyro and accelerometer: the six-axis sensor reads a controller at rest",
|
||||
SDL_GetGamepadName(pad));
|
||||
return sensor_ok;
|
||||
}
|
||||
|
||||
int input_motion_enabled(void) {
|
||||
static int on = -1;
|
||||
static unsigned seen;
|
||||
unsigned v = cfg_version();
|
||||
if (on < 0 || v != seen) {
|
||||
seen = v;
|
||||
on = cfg_bool("TOTK_MOTION");
|
||||
}
|
||||
return on;
|
||||
}
|
||||
|
||||
// 250 Hz: the gamepads update (reports read, rumble expiry) and the newest gyro and accelerometer sample goes into the
|
||||
// fusion. Without a gamepad with sensors, or with motion off (TOTK_MOTION=0, live), nothing is fed and the six-axis
|
||||
// sensor reads at rest.
|
||||
static void motion_main(void *arg) {
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
plat_wait_ns(4000000);
|
||||
float a[3], g[3];
|
||||
int have = 0;
|
||||
plat_mutex_lock(&lock);
|
||||
if (pad && SDL_GamepadConnected(pad)) {
|
||||
SDL_UpdateGamepads();
|
||||
have = sensors_ready() && SDL_GetGamepadSensorData(pad, SDL_SENSOR_ACCEL, a, 3) &&
|
||||
SDL_GetGamepadSensorData(pad, SDL_SENSOR_GYRO, g, 3);
|
||||
}
|
||||
plat_mutex_unlock(&lock);
|
||||
if (!have || !input_motion_enabled()) continue;
|
||||
double acc[3], gyro[3];
|
||||
input_motion_axes(a, g, acc, gyro);
|
||||
motion_feed(motion_global(), acc, gyro, kern_now_ns());
|
||||
}
|
||||
}
|
||||
|
||||
void input_motion_start(void) {
|
||||
plat_mutex_lock(&lock);
|
||||
if (!state) start();
|
||||
if (!motion_thread) {
|
||||
if (!input_motion_enabled()) rt_log("input: motion off (TOTK_MOTION=0); it can be turned on while the game runs");
|
||||
if (state != 1) motion_thread = -1;
|
||||
else {
|
||||
if (!plat_thread_spawn(motion_main, NULL, "motion", 0)) rt_fatal("input: cannot start the motion thread");
|
||||
motion_thread = 1;
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&lock);
|
||||
}
|
||||
+9
-11
@@ -1,9 +1,6 @@
|
||||
// Host input sampling (runtime/input_win.c): SDL3 gamepad and the keyboard as one Switch npad.
|
||||
// Host input sampling (runtime/input.c, with input_win.c or input_sdl.c): SDL3 gamepad and the keyboard as one Switch npad.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
// Bits of libnx HidNpadButton (A=0 B=1 X=2 Y=3 StickL=4 StickR=5 L=6 R=7 ZL=8 ZR=9 Plus=10 Minus=11 Left=12 Up=13 Right=14
|
||||
// Down=15) and the stick directions (StickLLeft..Down 16-19, StickRLeft..Down 20-23, derived from the sticks).
|
||||
@@ -20,9 +17,14 @@ void input_menu(int open);
|
||||
// Minus+Plus combo and the neutral pad while the menu is open (input_map.c, window-system free): *held is the caller's memory
|
||||
// of the combo, sticks are lx, ly, rx, ry. Returns 1 in the poll where the combo goes down (toggle the menu once).
|
||||
int input_menu_gate(int *held, int menu_open, uint64_t *buttons, int32_t sticks[4]);
|
||||
int input_overlay_gate(int *held, uint64_t *buttons); // L3+R3: 1 in the poll both go down
|
||||
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,
|
||||
@@ -38,18 +40,14 @@ void input_map_snapshot(struct InputMap *m);
|
||||
int input_capture(int reset, int *id);
|
||||
// The character on a punctuation key (KEY_MINUS..KEY_GRAVE) in the keyboard layout in use, 0 for another key.
|
||||
uint32_t input_key_char(int key);
|
||||
// F8, the flight recorder's key (no KEY_ id), held while the game window is in front.
|
||||
int input_flight_key(void);
|
||||
|
||||
// Mouse camera and cursor (input_win.c, Win32 window thread). The window procedure calls input_window_msg first for raw
|
||||
// mouse input, cursor hiding and confinement; it returns 1 with *res set when the message is handled. input_cursor_suspend(1)
|
||||
// releases the cursor around an F11 window change, (0) captures it again once the client area has its new size.
|
||||
#ifdef _WIN32
|
||||
int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res);
|
||||
int input_cursor_released(void); // Esc or a non-client activation released the cursor (probe)
|
||||
// The menu's Win32 adapter (ui/menu_win.cpp), called next: F1 / Esc open or close the menu; while it is open, keys,
|
||||
// mouse and focus go to it. Returns 1 with *res set when the message is fully handled.
|
||||
int menu_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res);
|
||||
#endif
|
||||
void input_cursor_suspend(int on);
|
||||
int input_cursor_released(void); // Esc or a non-client activation released the cursor (probe)
|
||||
|
||||
// Rumble: SDL_RumbleGamepad on the open gamepad, low (heavy, left) and high (light, right) motor 0..65535 for ms
|
||||
// milliseconds. 1 if a gamepad with rumble took it. Callable from any thread.
|
||||
|
||||
+55
-14
@@ -44,7 +44,7 @@ static const char *const action_names[ACTION_COUNT] = {
|
||||
"LSTICK_LEFT", "LSTICK_UP", "LSTICK_RIGHT", "LSTICK_DOWN", "RSTICK_LEFT", "RSTICK_UP", "RSTICK_RIGHT", "RSTICK_DOWN"};
|
||||
static const char *const pad_names[PAD_COUNT] = {
|
||||
NULL, "south", "east", "west", "north", "lstick", "rstick", "l", "r", "start", "back",
|
||||
"dpad_left", "dpad_up", "dpad_right", "dpad_down", "zl", "zr"};
|
||||
"dpad_left", "dpad_up", "dpad_right", "dpad_down", "zl", "zr", "lpaddle1", "rpaddle1", "lpaddle2", "rpaddle2"};
|
||||
static const char *const key_names[KEY_COUNT] = { // each group starts at its id; the ids of a group are consecutive
|
||||
[KEY_A] = "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u",
|
||||
"v", "w", "x", "y", "z",
|
||||
@@ -67,13 +67,13 @@ void input_map_reset(void) { input_map = defaults; }
|
||||
|
||||
static int key_id(const char *name) {
|
||||
for (int k = KEY_A; k < KEY_COUNT; k++)
|
||||
if (key_names[k] && !_stricmp(key_names[k], name)) return k;
|
||||
if (key_names[k] && !strcasecmp(key_names[k], name)) return k;
|
||||
return KEY_NONE;
|
||||
}
|
||||
|
||||
static int pad_id(const char *name) {
|
||||
for (int p = PAD_SOUTH; p < PAD_COUNT; p++)
|
||||
if (!_stricmp(pad_names[p], name)) return p;
|
||||
if (!strcasecmp(pad_names[p], name)) return p;
|
||||
return PAD_NONE;
|
||||
}
|
||||
|
||||
@@ -120,25 +120,25 @@ static void pads_put(InputMap *m, int layout, int action, const uint8_t *list, i
|
||||
int input_map_bind(const char *button, const char *list) {
|
||||
int action = -1;
|
||||
for (int a = 0; a < ACTION_COUNT; a++)
|
||||
if (!_stricmp(action_names[a], button)) action = a;
|
||||
if (!strcasecmp(action_names[a], button)) action = a;
|
||||
if (action < 0) { rt_log("input: bind: unknown button '%s'", button); return 0; }
|
||||
uint8_t keys[INPUT_BINDING_MAX], pads[INPUT_PAD_BINDING_MAX];
|
||||
int nk = 0, np = 0, none = 0, key_def = 0, pad_def = 0, bad = 0;
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof buf, "%s", list);
|
||||
for (char *save = NULL, *tok = strtok_s(buf, ",", &save); tok; tok = strtok_s(NULL, ",", &save)) {
|
||||
for (char *save = NULL, *tok = strtok_r(buf, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) {
|
||||
tok = trim(tok);
|
||||
if (!_stricmp(tok, "none")) { none = 1; continue; }
|
||||
if (!strcasecmp(tok, "none")) { none = 1; continue; }
|
||||
int ok = 0;
|
||||
if (!_strnicmp(tok, "pad:", 4) && action < B_SL_LEFT) {
|
||||
if (!strncasecmp(tok, "pad:", 4) && action < B_SL_LEFT) {
|
||||
const char *name = trim(tok + 4);
|
||||
int p = pad_id(name);
|
||||
if (!_stricmp(name, "default")) pad_def = ok = 1;
|
||||
if (!strcasecmp(name, "default")) pad_def = ok = 1;
|
||||
else if (p && np < INPUT_PAD_BINDING_MAX) pads[np++] = (uint8_t)p, ok = 1;
|
||||
} else if (!_strnicmp(tok, "key:", 4)) {
|
||||
} else if (!strncasecmp(tok, "key:", 4)) {
|
||||
const char *name = trim(tok + 4);
|
||||
int k = key_id(name);
|
||||
if (!_stricmp(name, "default")) key_def = ok = 1;
|
||||
if (!strcasecmp(name, "default")) key_def = ok = 1;
|
||||
else if (k && nk < INPUT_BINDING_MAX) keys[nk++] = (uint8_t)k, ok = 1;
|
||||
}
|
||||
if (!ok) {
|
||||
@@ -283,11 +283,23 @@ void input_map_line(const InputMap *m, int action, char *out, size_t n) {
|
||||
}
|
||||
|
||||
int input_pad_layout_pick(const char *setting, int east_labelled_a) {
|
||||
if (setting && !_stricmp(setting, "nintendo")) return PAD_LAYOUT_NINTENDO;
|
||||
if (setting && !_stricmp(setting, "xbox")) return PAD_LAYOUT_XBOX;
|
||||
if (setting && (!strcasecmp(setting, "nintendo") || !strcasecmp(setting, "xbox_prompts"))) return PAD_LAYOUT_NINTENDO;
|
||||
if (setting && !strcasecmp(setting, "xbox")) return PAD_LAYOUT_XBOX;
|
||||
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;
|
||||
@@ -325,12 +337,30 @@ void input_mouse_stick(int32_t dx, int32_t dy, double dt_s, float sens, int32_t
|
||||
out[1] = (int32_t)lrint(y);
|
||||
}
|
||||
|
||||
void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]) {
|
||||
double fx = x / (double)STICK_MAX, fy = y / (double)STICK_MAX, m = sqrt(fx * fx + fy * fy);
|
||||
out[0] = out[1] = 0;
|
||||
if (m <= dead) return;
|
||||
double k = ((m > 1 ? 1 : m) - dead) / (1 - dead) / m; // past the edge (a square gate's corner) the length is 1
|
||||
out[0] = (int32_t)lrint(fmax(-1, fmin(1, fx * k)) * STICK_MAX);
|
||||
out[1] = (int32_t)lrint(fmax(-1, fmin(1, fy * k)) * STICK_MAX);
|
||||
}
|
||||
|
||||
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.
|
||||
// Returns 1 in the poll where both go down: the caller toggles the menu once.
|
||||
int input_menu_gate(int *held, int menu_open, uint64_t *buttons, int32_t sticks[4]) {
|
||||
const uint64_t combo = 1ull << B_MINUS | 1ull << B_PLUS;
|
||||
static int chord(int *held, uint64_t *buttons, uint64_t combo) {
|
||||
uint64_t raw = *buttons & combo;
|
||||
int edge = 0;
|
||||
if (*held) {
|
||||
@@ -340,9 +370,20 @@ int input_menu_gate(int *held, int menu_open, uint64_t *buttons, int32_t sticks[
|
||||
edge = 1;
|
||||
}
|
||||
if (*held) *buttons &= ~combo;
|
||||
return edge;
|
||||
}
|
||||
|
||||
int input_menu_gate(int *held, int menu_open, uint64_t *buttons, int32_t sticks[4]) {
|
||||
int edge = chord(held, buttons, 1ull << B_MINUS | 1ull << B_PLUS);
|
||||
if (menu_open) {
|
||||
*buttons = 0;
|
||||
sticks[0] = sticks[1] = sticks[2] = sticks[3] = 0;
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
|
||||
// Both stick clicks held together step the performance overlay as F10 does (a gamepad has no F10: the Steam Deck in
|
||||
// Game Mode). The same latch as the menu's above; the game sees neither click while it holds.
|
||||
int input_overlay_gate(int *held, uint64_t *buttons) {
|
||||
return chord(held, buttons, 1ull << B_STICK_L | 1ull << B_STICK_R);
|
||||
}
|
||||
+30
-3
@@ -34,11 +34,14 @@ enum {
|
||||
};
|
||||
|
||||
// Gamepad inputs by position (Nintendo layout, as SDL names them), platform-neutral like the key ids. ZL and ZR are the
|
||||
// analog triggers; input_win.c translates SDL buttons and axes to these (one place).
|
||||
// analog triggers; input_win.c translates SDL buttons and axes to these (one place). The paddles are the back buttons
|
||||
// of an Xbox Elite, DualSense Edge or Steam Controller (SDL's LEFT_PADDLE1 and so on); no Switch button has them by
|
||||
// default, a bind line or the remapping rows add them.
|
||||
enum {
|
||||
PAD_NONE = 0,
|
||||
PAD_SOUTH, PAD_EAST, PAD_WEST, PAD_NORTH, PAD_LSTICK, PAD_RSTICK, PAD_L, PAD_R, PAD_START, PAD_BACK,
|
||||
PAD_DPAD_LEFT, PAD_DPAD_UP, PAD_DPAD_RIGHT, PAD_DPAD_DOWN, PAD_ZL, PAD_ZR,
|
||||
PAD_LPADDLE1, PAD_RPADDLE1, PAD_LPADDLE2, PAD_RPADDLE2,
|
||||
PAD_COUNT
|
||||
};
|
||||
|
||||
@@ -77,9 +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); 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);
|
||||
|
||||
@@ -91,6 +108,16 @@ void input_map_stick(const uint8_t down[KEY_COUNT], int stick, int32_t out[2]);
|
||||
// Mouse motion in raw counts over dt_s seconds to right-stick deflection: full deflection at 3000 / sens counts/s.
|
||||
// Clamped to the unit circle, up (negative dy) is +y.
|
||||
void input_mouse_stick(int32_t dx, int32_t dy, double dt_s, float sens, int32_t out[2]);
|
||||
// A gamepad stick (x, y in -32768..32767, up is +y) through a round dead zone of `dead` (fraction of full deflection,
|
||||
// 0..0.5): inside the circle the stick reads 0, and the rest of the travel is scaled to start at 0 at the edge, so a
|
||||
// drifting stick rests at 0 and full deflection still reaches full. The result stays inside the unit square.
|
||||
void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]);
|
||||
// The 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.
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// The SDL3 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): SDL scancodes to
|
||||
// the key ids of input_map.h, the mouse camera's relative motion and the cursor capture (SDL's relative mouse mode).
|
||||
// Key and button state comes from the window's events (window thread) and is read by any thread. Keys and their map:
|
||||
// docs/controls.md.
|
||||
//
|
||||
// The capture follows the Win32 back end's rules (input_win.c): captured while the window has the focus, the mouse
|
||||
// camera is on and no text entry or menu is open; Esc releases it until a click in the window (and closing the menu
|
||||
// opened by that Esc gives it back); losing the focus releases it until the window has the focus again.
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "input_map.h"
|
||||
#include "input_sdl.h"
|
||||
#include "input_wsys.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static SDL_Window *win;
|
||||
static volatile uint8_t key_held[KEY_COUNT];
|
||||
static volatile int32_t esc_held, f8_held, focused;
|
||||
static volatile int32_t mouse_dx, mouse_dy; // relative motion since the last ikb_mouse_take, whole counts
|
||||
static float rem_x, rem_y; // window thread: the fractions SDL reported beyond them
|
||||
static volatile int32_t captured; // relative mouse mode is on
|
||||
enum { CUR_REL = 1, CUR_ESC = 2 }; // as input_win.c: released by Esc or a click elsewhere; by Esc
|
||||
static volatile int32_t cur_rel, cur_suspend, cur_pending;
|
||||
static uint32_t wake_event; // SDL_RegisterEvents: wakes the window thread for a cursor change
|
||||
|
||||
// SDL scancode of a key id, 0 for an id without a key (the mouse buttons).
|
||||
static SDL_Scancode key_sc(int key) {
|
||||
if (key >= KEY_A && key <= KEY_Z) return (SDL_Scancode)(SDL_SCANCODE_A + (key - KEY_A));
|
||||
if (key >= KEY_1 && key <= KEY_9) return (SDL_Scancode)(SDL_SCANCODE_1 + (key - KEY_1));
|
||||
if (key >= KEY_NUM1 && key <= KEY_NUM9) return (SDL_Scancode)(SDL_SCANCODE_KP_1 + (key - KEY_NUM1));
|
||||
switch (key) {
|
||||
case KEY_0: return SDL_SCANCODE_0;
|
||||
case KEY_NUM0: return SDL_SCANCODE_KP_0;
|
||||
case KEY_SPACE: return SDL_SCANCODE_SPACE;
|
||||
case KEY_TAB: return SDL_SCANCODE_TAB;
|
||||
case KEY_LSHIFT: return SDL_SCANCODE_LSHIFT;
|
||||
case KEY_LCTRL: return SDL_SCANCODE_LCTRL;
|
||||
case KEY_UP: return SDL_SCANCODE_UP;
|
||||
case KEY_DOWN: return SDL_SCANCODE_DOWN;
|
||||
case KEY_LEFT: return SDL_SCANCODE_LEFT;
|
||||
case KEY_RIGHT: return SDL_SCANCODE_RIGHT;
|
||||
case KEY_ENTER: return SDL_SCANCODE_RETURN;
|
||||
case KEY_BACKSPACE: return SDL_SCANCODE_BACKSPACE;
|
||||
case KEY_RSHIFT: return SDL_SCANCODE_RSHIFT;
|
||||
case KEY_RCTRL: return SDL_SCANCODE_RCTRL;
|
||||
case KEY_LALT: return SDL_SCANCODE_LALT;
|
||||
case KEY_CAPSLOCK: return SDL_SCANCODE_CAPSLOCK;
|
||||
case KEY_INSERT: return SDL_SCANCODE_INSERT;
|
||||
case KEY_DELETE: return SDL_SCANCODE_DELETE;
|
||||
case KEY_HOME: return SDL_SCANCODE_HOME;
|
||||
case KEY_END: return SDL_SCANCODE_END;
|
||||
case KEY_PAGEUP: return SDL_SCANCODE_PAGEUP;
|
||||
case KEY_PAGEDOWN: return SDL_SCANCODE_PAGEDOWN;
|
||||
case KEY_F2: return SDL_SCANCODE_F2;
|
||||
case KEY_F3: return SDL_SCANCODE_F3;
|
||||
case KEY_F4: return SDL_SCANCODE_F4;
|
||||
case KEY_F5: return SDL_SCANCODE_F5;
|
||||
case KEY_F6: return SDL_SCANCODE_F6;
|
||||
case KEY_F7: return SDL_SCANCODE_F7;
|
||||
case KEY_F9: return SDL_SCANCODE_F9;
|
||||
case KEY_F12: return SDL_SCANCODE_F12;
|
||||
case KEY_NUM_ADD: return SDL_SCANCODE_KP_PLUS;
|
||||
case KEY_NUM_SUB: return SDL_SCANCODE_KP_MINUS;
|
||||
case KEY_NUM_MUL: return SDL_SCANCODE_KP_MULTIPLY;
|
||||
case KEY_NUM_DIV: return SDL_SCANCODE_KP_DIVIDE;
|
||||
case KEY_NUM_DECIMAL: return SDL_SCANCODE_KP_PERIOD;
|
||||
case KEY_MINUS: return SDL_SCANCODE_MINUS;
|
||||
case KEY_EQUALS: return SDL_SCANCODE_EQUALS;
|
||||
case KEY_LBRACKET: return SDL_SCANCODE_LEFTBRACKET;
|
||||
case KEY_RBRACKET: return SDL_SCANCODE_RIGHTBRACKET;
|
||||
case KEY_SEMICOLON: return SDL_SCANCODE_SEMICOLON;
|
||||
case KEY_APOSTROPHE: return SDL_SCANCODE_APOSTROPHE;
|
||||
case KEY_COMMA: return SDL_SCANCODE_COMMA;
|
||||
case KEY_PERIOD: return SDL_SCANCODE_PERIOD;
|
||||
case KEY_SLASH: return SDL_SCANCODE_SLASH;
|
||||
case KEY_BACKSLASH: return SDL_SCANCODE_BACKSLASH;
|
||||
case KEY_GRAVE: return SDL_SCANCODE_GRAVE;
|
||||
}
|
||||
return SDL_SCANCODE_UNKNOWN;
|
||||
}
|
||||
|
||||
// The key id of a scancode (the inverse of key_sc, built on first use), 0 for none.
|
||||
static int key_of_sc(SDL_Scancode sc) {
|
||||
static uint8_t map[SDL_SCANCODE_COUNT];
|
||||
static int built;
|
||||
if (!built) {
|
||||
for (int k = 1; k < KEY_COUNT; k++) {
|
||||
SDL_Scancode s = key_sc(k);
|
||||
if (s != SDL_SCANCODE_UNKNOWN) map[s] = (uint8_t)k;
|
||||
}
|
||||
built = 1;
|
||||
}
|
||||
return sc > 0 && sc < SDL_SCANCODE_COUNT ? map[sc] : 0;
|
||||
}
|
||||
|
||||
static int mouse_key(uint8_t button) {
|
||||
switch (button) {
|
||||
case SDL_BUTTON_LEFT: return KEY_MOUSE_L;
|
||||
case SDL_BUTTON_RIGHT: return KEY_MOUSE_R;
|
||||
case SDL_BUTTON_MIDDLE: return KEY_MOUSE_M;
|
||||
case SDL_BUTTON_X1: return KEY_MOUSE_X1;
|
||||
case SDL_BUTTON_X2: return KEY_MOUSE_X2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t input_key_char(int key) {
|
||||
if (key < KEY_MINUS || key > KEY_GRAVE) return 0;
|
||||
SDL_Keycode k = SDL_GetKeyFromScancode(key_sc(key), SDL_KMOD_NONE, false);
|
||||
return k & SDLK_SCANCODE_MASK ? 0 : (uint32_t)k; // a keycode with the mask bit names no character
|
||||
}
|
||||
|
||||
int input_flight_key(void) { return focused && f8_held; }
|
||||
|
||||
// ---- the cursor (window thread)
|
||||
static void cursor_apply(void) {
|
||||
__atomic_store_n(&cur_pending, 0, __ATOMIC_RELAXED);
|
||||
if (!win) return;
|
||||
int want = focused && !(cur_rel & CUR_REL) && !cur_suspend && input_cursor_allowed() &&
|
||||
!(SDL_GetWindowFlags(win) & SDL_WINDOW_MINIMIZED);
|
||||
if (want == captured) return;
|
||||
if (!SDL_SetWindowRelativeMouseMode(win, want)) {
|
||||
rt_log("input: relative mouse mode %s failed (%s)", want ? "on" : "off", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
plat_xchg32(&mouse_dx, 0);
|
||||
plat_xchg32(&mouse_dy, 0);
|
||||
rem_x = rem_y = 0;
|
||||
plat_xchg32(&captured, want);
|
||||
}
|
||||
|
||||
// Any thread: the window thread applies the change at its next event.
|
||||
void ikb_cursor_update(void) {
|
||||
plat_xchg32(&cur_pending, 1);
|
||||
if (wake_event) {
|
||||
SDL_Event e = {.user = {.type = wake_event}};
|
||||
SDL_PushEvent(&e);
|
||||
}
|
||||
}
|
||||
|
||||
void input_cursor_suspend(int on) {
|
||||
plat_xchg32(&cur_suspend, on != 0);
|
||||
ikb_cursor_update();
|
||||
}
|
||||
|
||||
int input_cursor_released(void) { return (cur_rel & CUR_REL) != 0; }
|
||||
|
||||
void ikb_menu_closed(void) {
|
||||
int32_t o = cur_rel;
|
||||
if (o & CUR_ESC) plat_cas32(&cur_rel, 0, o);
|
||||
}
|
||||
|
||||
void input_sdl_window(SDL_Window *w) {
|
||||
win = w;
|
||||
wake_event = SDL_RegisterEvents(1);
|
||||
plat_xchg32(&focused, (SDL_GetWindowFlags(w) & SDL_WINDOW_INPUT_FOCUS) != 0);
|
||||
cursor_apply();
|
||||
}
|
||||
|
||||
void input_sdl_idle(void) {
|
||||
if (cur_pending) cursor_apply();
|
||||
}
|
||||
|
||||
void input_sdl_event(const SDL_Event *e) {
|
||||
switch (e->type) {
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP: {
|
||||
int down = e->type == SDL_EVENT_KEY_DOWN;
|
||||
if (e->key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
plat_xchg32(&esc_held, down);
|
||||
// Esc is unbound in the game layout: it releases the cursor and opens the menu (ui/menu_sdl.cpp)
|
||||
if (down && !e->key.repeat && !(cur_rel & CUR_REL)) plat_xchg32(&cur_rel, CUR_REL | CUR_ESC);
|
||||
}
|
||||
if (e->key.scancode == SDL_SCANCODE_F8) plat_xchg32(&f8_held, down);
|
||||
int k = key_of_sc(e->key.scancode);
|
||||
if (k) key_held[k] = (uint8_t)down;
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP: {
|
||||
int down = e->type == SDL_EVENT_MOUSE_BUTTON_DOWN;
|
||||
int k = mouse_key(e->button.button);
|
||||
if (k) key_held[k] = (uint8_t)down;
|
||||
if (down && (cur_rel & CUR_REL)) plat_xchg32(&cur_rel, 0); // a click in the window captures again
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
if (captured) {
|
||||
rem_x += e->motion.xrel;
|
||||
rem_y += e->motion.yrel;
|
||||
int32_t dx = (int32_t)rem_x, dy = (int32_t)rem_y;
|
||||
rem_x -= (float)dx, rem_y -= (float)dy;
|
||||
if (dx) plat_add32(&mouse_dx, dx);
|
||||
if (dy) plat_add32(&mouse_dy, dy);
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
plat_xchg32(&focused, 1);
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
plat_xchg32(&focused, 0);
|
||||
plat_xchg32(&cur_rel, 0); // coming back (Alt+Tab) captures again
|
||||
memset((void *)key_held, 0, sizeof key_held); // the key-ups go elsewhere
|
||||
plat_xchg32(&esc_held, 0);
|
||||
plat_xchg32(&f8_held, 0);
|
||||
break;
|
||||
}
|
||||
cursor_apply();
|
||||
}
|
||||
|
||||
// ---- input_wsys.h
|
||||
int ikb_foreground(void) { return focused != 0; }
|
||||
int ikb_down(int key) { return key > 0 && key < KEY_COUNT && key_held[key]; }
|
||||
int ikb_escape(void) { return esc_held != 0; }
|
||||
int ikb_captured(void) { return captured != 0; }
|
||||
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
|
||||
*dx = plat_xchg32(&mouse_dx, 0);
|
||||
*dy = plat_xchg32(&mouse_dy, 0);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// The SDL3 window's input seams (Linux and other hosts without Win32): gfx_window_sdl.c owns the window and its event
|
||||
// loop, input_sdl.c the keyboard, the mouse and the cursor, ui/menu_sdl.cpp the menu's events. Window thread, except
|
||||
// where noted.
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// gfx_window_sdl.c
|
||||
SDL_Window *gfx_window_sdl(void); // the game window, NULL before gfx_window_start; any thread
|
||||
double gfx_window_sdl_density(void); // pixels per window unit (mouse positions are in units); any thread
|
||||
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg); // runs fn on the window thread and waits; any thread
|
||||
|
||||
// input_sdl.c
|
||||
void input_sdl_window(SDL_Window *w); // the game window was created
|
||||
void input_sdl_event(const SDL_Event *e); // every event, first
|
||||
void input_sdl_idle(void); // no event for a while: a cursor change asked for from another thread
|
||||
|
||||
// ui/menu_sdl.cpp
|
||||
void menu_sdl_event(const SDL_Event *e); // every event, after input_sdl_event
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+26
-619
@@ -1,223 +1,22 @@
|
||||
// Host input for nn::hid (runtime/ov_input.c): an SDL3 gamepad (no SDL window, no SDL video), the keyboard and the mouse,
|
||||
// merged into Switch npad button bits and stick axes. Button order follows libnx HidNpadButton (ISC). The key and pad bindings
|
||||
// are the tables in input_map.c (totk.ini overrides them); this file translates Win32 virtual keys to its key ids, reads the mouse (raw input, WM_INPUT)
|
||||
// and owns the cursor capture. Keys and their map: docs/controls.md.
|
||||
// The gamepad also rumbles (input_rumble) and, when it has a gyro and an accelerometer, feeds the six-axis sensor from a
|
||||
// thread of its own (input_motion_start). Every SDL call on the gamepad is made under `lock`.
|
||||
// The Win32 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): Win32 virtual
|
||||
// keys to the key ids of input_map.h, the mouse camera's raw motion (raw input, WM_INPUT) and the cursor capture.
|
||||
// Keys and their map: docs/controls.md.
|
||||
#include <windows.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "input_win.h"
|
||||
#include "input_map.h"
|
||||
#include "input_wsys.h"
|
||||
#include "nvn_api.h"
|
||||
#include "ui/menu.h"
|
||||
#include "vk.h"
|
||||
|
||||
enum { STICK_MAX = 32767, STICK_DEAD = 2500, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
|
||||
|
||||
static SRWLOCK lock = SRWLOCK_INIT;
|
||||
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
|
||||
static SDL_Gamepad *pad;
|
||||
static uint64_t next_scan;
|
||||
static int pad_rumble; // the open gamepad has rumble motors
|
||||
static volatile LONG pad_labels[4]; // ... and the labels of its south, east, west, north buttons (PAD_LABEL_*), any thread
|
||||
static int pad_east_a; // ... and labels its east face button A (Nintendo layout under TOTK_PAD_LAYOUT=auto)
|
||||
static int pad_layout = -1; // PAD_LAYOUT_* in effect for the open gamepad, -1 to pick again; live (cfg_version)
|
||||
static unsigned pad_layout_seen;
|
||||
static volatile LONG pad_layout_shown = -1; // pad_layout for the menu (any thread), -1 without a gamepad
|
||||
static SDL_JoystickID sensor_pad; // the gamepad whose sensors the motion thread set up, 0 none
|
||||
static int sensor_ok; // ... and it has both sensors, enabled
|
||||
static int motion_thread; // 0 not started, 1 running, -1 off (TOTK_MOTION=0, no SDL)
|
||||
|
||||
// SDL buttons of the gamepad inputs (input_map.h PAD_*), by position; PAD_ZL and PAD_ZR are the triggers (axes).
|
||||
static const SDL_GamepadButton pad_sdl[PAD_COUNT] = {
|
||||
[PAD_SOUTH] = SDL_GAMEPAD_BUTTON_SOUTH, [PAD_EAST] = SDL_GAMEPAD_BUTTON_EAST, [PAD_WEST] = SDL_GAMEPAD_BUTTON_WEST,
|
||||
[PAD_NORTH] = SDL_GAMEPAD_BUTTON_NORTH, [PAD_LSTICK] = SDL_GAMEPAD_BUTTON_LEFT_STICK,
|
||||
[PAD_RSTICK] = SDL_GAMEPAD_BUTTON_RIGHT_STICK, [PAD_L] = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
|
||||
[PAD_R] = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, [PAD_START] = SDL_GAMEPAD_BUTTON_START, [PAD_BACK] = SDL_GAMEPAD_BUTTON_BACK,
|
||||
[PAD_DPAD_LEFT] = SDL_GAMEPAD_BUTTON_DPAD_LEFT, [PAD_DPAD_UP] = SDL_GAMEPAD_BUTTON_DPAD_UP,
|
||||
[PAD_DPAD_RIGHT] = SDL_GAMEPAD_BUTTON_DPAD_RIGHT, [PAD_DPAD_DOWN] = SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
||||
};
|
||||
|
||||
// TOTK_INPUT_SCRIPT=<file>: lines "<frame> <button> [hold]" or "<frame> LSTICK|RSTICK <x> <y> [hold]", ORed into the real input.
|
||||
// <frame> and its hold are 60 Hz ticks of game time since the first present (frame_step_ticks: presents at 60 fps), so
|
||||
// the title menus get the presses at the same game time at any frame limit. A line "@<ms> ..." is timed instead: <ms>
|
||||
// and its hold are milliseconds of the world clock (frame_step_world_us, game time since the loaded save's first
|
||||
// frame), so it lands at the same game time at any frame rate and load length. A timed event shorter than a frame is
|
||||
// applied once. In a script with timed lines (a recording) the untimed ones belong before the world clock: they stop
|
||||
// when it starts, as the load ends at another tick in each run.
|
||||
// at, hold: ticks, or microseconds of the world clock when timed.
|
||||
typedef struct { uint64_t at, hold; int bit, stick, timed; int32_t x, y; int logged; char text[48]; } ScriptEv;
|
||||
static const struct { const char *name; int bit; } script_buttons[] = {
|
||||
{"A", B_A}, {"B", B_B}, {"X", B_X}, {"Y", B_Y}, {"LCLICK", B_STICK_L}, {"RCLICK", B_STICK_R}, {"L", B_L}, {"R", B_R},
|
||||
{"ZL", B_ZL}, {"ZR", B_ZR}, {"PLUS", B_PLUS}, {"MINUS", B_MINUS}, {"LEFT", B_LEFT}, {"UP", B_UP}, {"RIGHT", B_RIGHT}, {"DOWN", B_DOWN},
|
||||
};
|
||||
static ScriptEv *script_ev;
|
||||
static int script_n, script_state; // 0 not loaded, 1 loaded
|
||||
static int script_mixed; // the script has timed lines: its untimed ones end where the world clock starts
|
||||
|
||||
static void script_load(void) {
|
||||
script_state = 1;
|
||||
const char *path = cfg_get("TOTK_INPUT_SCRIPT");
|
||||
if (!path || !*path) return;
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) { rt_log("input: cannot open TOTK_INPUT_SCRIPT %s", path); return; }
|
||||
char line[256];
|
||||
int cap = 0, ln = 0;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
ln++;
|
||||
char *h = strchr(line, '#');
|
||||
if (h) *h = 0;
|
||||
char name[32];
|
||||
double at;
|
||||
int off = 0, timed = line[0] == '@';
|
||||
if (sscanf(line + timed, "%lf %31s%n", &at, name, &off) < 2 || at < 0) continue;
|
||||
off += timed;
|
||||
double unit = timed ? 1000.0 : 1.0; // ms to us, or presents
|
||||
ScriptEv e = {.at = (uint64_t)(at * unit + 0.5), .hold = timed ? 66667 : 4, .bit = -1, .timed = timed};
|
||||
long a, b;
|
||||
double c;
|
||||
if (!strcmp(name, "LSTICK") || !strcmp(name, "RSTICK")) {
|
||||
int n = sscanf(line + off, "%ld %ld %lf", &a, &b, &c);
|
||||
if (n < 2) { rt_log("input: script line %d: %s needs x y [hold]", ln, name); continue; }
|
||||
e.stick = name[0] == 'L' ? 1 : 2; e.x = (int32_t)a; e.y = (int32_t)b;
|
||||
if (n >= 3 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
|
||||
snprintf(e.text, sizeof e.text, "%s %d %d", name, e.x, e.y);
|
||||
} else {
|
||||
for (size_t i = 0; i < sizeof script_buttons / sizeof *script_buttons; i++)
|
||||
if (!strcmp(name, script_buttons[i].name)) e.bit = script_buttons[i].bit;
|
||||
if (e.bit < 0) { rt_log("input: script line %d: unknown button %s", ln, name); continue; }
|
||||
if (sscanf(line + off, "%lf", &c) == 1 && c > 0) e.hold = (uint64_t)(c * unit + 0.5);
|
||||
snprintf(e.text, sizeof e.text, "%s", name);
|
||||
}
|
||||
if (script_n == cap) {
|
||||
cap = cap ? cap * 2 : 32;
|
||||
script_ev = realloc(script_ev, (size_t)cap * sizeof *script_ev);
|
||||
if (!script_ev) { script_n = 0; fclose(f); return; }
|
||||
}
|
||||
script_ev[script_n++] = e;
|
||||
script_mixed |= timed;
|
||||
}
|
||||
fclose(f);
|
||||
rt_log("input: script %s, %d events", path, script_n);
|
||||
}
|
||||
|
||||
static void script_apply(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
|
||||
if (!script_state) script_load();
|
||||
if (!script_n) return;
|
||||
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), tick = frame_step_ticks();
|
||||
int64_t w = frame_step_world_us();
|
||||
for (int i = 0; i < script_n; i++) {
|
||||
ScriptEv *e = &script_ev[i];
|
||||
if (e->timed ? w < 0 : script_mixed && w >= 0) continue;
|
||||
uint64_t now = e->timed ? (uint64_t)w : tick;
|
||||
if (now < e->at || (now >= e->at + e->hold && (e->logged || !e->timed))) continue;
|
||||
if (!e->logged) {
|
||||
e->logged = 1;
|
||||
if (e->timed) rt_log("input: script frame %llu at %.1f ms: %s", (unsigned long long)fr, w / 1000.0, e->text);
|
||||
else rt_log("input: script frame %llu tick %llu: %s", (unsigned long long)fr, (unsigned long long)tick, e->text);
|
||||
}
|
||||
if (e->stick) { int32_t *s = e->stick == 1 ? sl : sr; s[0] = e->x; s[1] = e->y; }
|
||||
else *b |= 1ull << e->bit;
|
||||
}
|
||||
}
|
||||
|
||||
// TOTK_INPUT_RECORD=<file>: writes the merged input as TOTK_INPUT_SCRIPT lines, one per button press and per stick
|
||||
// segment, so a played session replays as a script. A stick segment ends when an axis moves by more than REC_STICK_STEP.
|
||||
// An event that begins on the world clock is written timed ("@<ms>", hold in ms), one before it in ticks
|
||||
// (frame_step_ticks). When the world clock starts, held buttons and sticks are cut there and go on as timed events.
|
||||
enum { REC_STICK_STEP = 2048 };
|
||||
static FILE *rec;
|
||||
static int rec_state; // 0 not opened, 1 recording, -1 off
|
||||
static uint64_t rec_down[16]; // tick + 1 at which each button went down, 0 while up
|
||||
static int64_t rec_down_w[16]; // the world clock then, -1 before it started
|
||||
static uint64_t rec_stick_from[2]; // tick + 1 at which the current non-zero stick segment began
|
||||
static int64_t rec_stick_w[2];
|
||||
static int32_t rec_stick[2][2];
|
||||
static int64_t rec_last_w = -1; // the world clock at the previous apply
|
||||
|
||||
// "<tick> <what> <hold>" or "@<ms> <what> <hold ms>": from tick from and world clock from_w to now.
|
||||
static void rec_line(uint64_t from, int64_t from_w, uint64_t fr, int64_t w, const char *what) {
|
||||
if (from_w >= 0 && w >= from_w)
|
||||
fprintf(rec, "@%.1f %s %.1f\n", from_w / 1000.0, what, (w > from_w ? w - from_w : 1000) / 1000.0);
|
||||
else
|
||||
fprintf(rec, "%llu %s %llu\n", (unsigned long long)from, what, (unsigned long long)(fr > from ? fr - from : 1));
|
||||
}
|
||||
|
||||
static void rec_close_stick(int s, uint64_t fr, int64_t w) {
|
||||
if (!rec_stick_from[s]) return;
|
||||
char what[48];
|
||||
snprintf(what, sizeof what, "%s %d %d", s ? "RSTICK" : "LSTICK", rec_stick[s][0], rec_stick[s][1]);
|
||||
rec_line(rec_stick_from[s] - 1, rec_stick_w[s], fr, w, what);
|
||||
rec_stick_from[s] = 0;
|
||||
}
|
||||
|
||||
static void rec_flush_all(void) {
|
||||
if (rec_state != 1) return;
|
||||
uint64_t fr = frame_step_ticks() + 1;
|
||||
int64_t w = frame_step_world_us();
|
||||
for (int i = 0; i < 16; i++)
|
||||
if (rec_down[i]) { rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name); rec_down[i] = 0; }
|
||||
rec_close_stick(0, fr, w);
|
||||
rec_close_stick(1, fr, w);
|
||||
fflush(rec);
|
||||
}
|
||||
|
||||
static void rec_apply(uint64_t b, const int32_t sl[2], const int32_t sr[2]) {
|
||||
if (!rec_state) {
|
||||
const char *path = cfg_get("TOTK_INPUT_RECORD"); // no default: not recording when unset
|
||||
rec = path && *path ? fopen(path, "w") : NULL;
|
||||
rec_state = rec ? 1 : -1;
|
||||
if (path && *path && !rec) rt_log("input: cannot open TOTK_INPUT_RECORD %s", path);
|
||||
if (rec) { fprintf(rec, "# Recorded input (TOTK_INPUT_RECORD); frame = 60 Hz ticks of game time, @ms = world clock.\n"); atexit(rec_flush_all);
|
||||
rt_log("input: recording to %s", path); }
|
||||
}
|
||||
if (rec_state != 1) return;
|
||||
uint64_t fr = frame_step_ticks();
|
||||
int64_t w = frame_step_world_us();
|
||||
int wrote = 0;
|
||||
if (w >= 0 && rec_last_w < 0) { // the world clock starts: what is held goes on as timed events from @0
|
||||
for (int i = 0; i < 16; i++)
|
||||
if (rec_down[i]) {
|
||||
rec_line(rec_down[i] - 1, -1, fr, -1, script_buttons[i].name);
|
||||
rec_down[i] = fr + 1, rec_down_w[i] = w;
|
||||
wrote = 1;
|
||||
}
|
||||
for (int s = 0; s < 2; s++)
|
||||
if (rec_stick_from[s]) {
|
||||
rec_close_stick(s, fr, -1);
|
||||
rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
|
||||
wrote = 1;
|
||||
}
|
||||
}
|
||||
rec_last_w = w;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int bit = script_buttons[i].bit, down = (b >> bit & 1) != 0;
|
||||
if (down && !rec_down[i]) rec_down[i] = fr + 1, rec_down_w[i] = w;
|
||||
else if (!down && rec_down[i]) {
|
||||
rec_line(rec_down[i] - 1, rec_down_w[i], fr, w, script_buttons[i].name);
|
||||
rec_down[i] = 0;
|
||||
wrote = 1;
|
||||
}
|
||||
}
|
||||
const int32_t *st[2] = {sl, sr};
|
||||
for (int s = 0; s < 2; s++) {
|
||||
int moved = abs(st[s][0] - rec_stick[s][0]) > REC_STICK_STEP || abs(st[s][1] - rec_stick[s][1]) > REC_STICK_STEP ||
|
||||
((st[s][0] || st[s][1]) != (rec_stick[s][0] || rec_stick[s][1]));
|
||||
if (!moved) continue;
|
||||
if (rec_stick_from[s]) { rec_close_stick(s, fr, w); wrote = 1; }
|
||||
rec_stick[s][0] = st[s][0]; rec_stick[s][1] = st[s][1];
|
||||
if (st[s][0] || st[s][1]) rec_stick_from[s] = fr + 1, rec_stick_w[s] = w;
|
||||
}
|
||||
if (wrote) fflush(rec);
|
||||
}
|
||||
|
||||
// Win32 virtual key of a key id (the one place that knows them), 0 for an id without a key.
|
||||
static int key_vk(int key) {
|
||||
if (key >= KEY_A && key <= KEY_Z) return 'A' + (key - KEY_A);
|
||||
@@ -300,27 +99,12 @@ static LONG rel_clear(LONG o) { (void)o; return 0; }
|
||||
static LONG rel_menu_closed(LONG o) { return o & CUR_ESC ? 0 : o; }
|
||||
static volatile LONG cur_suspend; // F11 or a modal size or move loop under way
|
||||
static SRWLOCK cur_lock = SRWLOCK_INIT; // serialises cursor_update (window thread, input_block callers)
|
||||
static volatile LONG g_block; // text entry state, see input_block
|
||||
static volatile LONG g_menu; // in-game menu state, see input_menu
|
||||
static float mouse_sens = -1; // TOTK_MOUSE_SENS, 0 = mouse camera off; live (cfg_version)
|
||||
static unsigned mouse_sens_seen;
|
||||
|
||||
static float mouse_sens_get(void) {
|
||||
unsigned ver = cfg_version();
|
||||
if (mouse_sens >= 0 && ver == mouse_sens_seen) return mouse_sens;
|
||||
mouse_sens_seen = ver;
|
||||
double v = cfg_double("TOTK_MOUSE_SENS");
|
||||
mouse_sens = (float)(v < 0 ? 0 : v > 10 ? 10 : v);
|
||||
return mouse_sens;
|
||||
}
|
||||
|
||||
// Hide the cursor and confine it to the client area while the window is focused, not suspended, not behind a text entry
|
||||
// or the menu, and the mouse camera is on; release it otherwise. Safe from any thread.
|
||||
static void cursor_update(HWND h) {
|
||||
const char *in = cfg_get("TOTK_INPUT");
|
||||
AcquireSRWLockExclusive(&cur_lock);
|
||||
int want = h && cur_active && !(cur_rel & CUR_REL) && !cur_suspend && g_block != 1 && g_menu != 1 && mouse_sens_get() > 0 &&
|
||||
!(in && !strcmp(in, "none")) && GetForegroundWindow() == h && !IsIconic(h);
|
||||
int want = h && cur_active && !(cur_rel & CUR_REL) && !cur_suspend && input_cursor_allowed() &&
|
||||
GetForegroundWindow() == h && !IsIconic(h);
|
||||
RECT r;
|
||||
POINT tl = {0, 0};
|
||||
if (want && GetClientRect(h, &r) && ClientToScreen(h, &tl)) {
|
||||
@@ -342,7 +126,7 @@ void input_cursor_suspend(int on) {
|
||||
cursor_update(gfx_window_handle());
|
||||
}
|
||||
|
||||
// Window procedure hook (gfx_window.c calls it first). Returns 1 with *res set when the message is fully handled.
|
||||
// Window procedure hook (gfx_window_win.c calls it first). Returns 1 with *res set when the message is fully handled.
|
||||
int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
|
||||
switch (m) {
|
||||
case WM_CREATE: {
|
||||
@@ -399,408 +183,31 @@ int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
|
||||
|
||||
static int key_down(int vk) { return (GetAsyncKeyState(vk) & 0x8000) != 0; }
|
||||
|
||||
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`.
|
||||
static void mouse_camera(int32_t out[2]) {
|
||||
static uint64_t frame = ~0ull;
|
||||
static LARGE_INTEGER t0;
|
||||
static int32_t held[2];
|
||||
LARGE_INTEGER now, f;
|
||||
QueryPerformanceCounter(&now);
|
||||
QueryPerformanceFrequency(&f);
|
||||
if (!cur_captured) {
|
||||
InterlockedExchange(&mouse_dx, 0);
|
||||
InterlockedExchange(&mouse_dy, 0);
|
||||
held[0] = held[1] = 0;
|
||||
frame = ~0ull;
|
||||
} else {
|
||||
uint64_t fr = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
||||
double age = frame == ~0ull ? 0 : (double)(now.QuadPart - t0.QuadPart) / (double)f.QuadPart;
|
||||
if (fr != frame) {
|
||||
double dt = age < 0.004 ? 0.004 : age > 0.1 ? 0.1 : age;
|
||||
input_mouse_stick(InterlockedExchange(&mouse_dx, 0), InterlockedExchange(&mouse_dy, 0), dt, mouse_sens_get(), held);
|
||||
frame = fr;
|
||||
t0 = now;
|
||||
} else if (age > 0.1) {
|
||||
held[0] = held[1] = 0; // frames stopped: do not hold a deflection
|
||||
}
|
||||
}
|
||||
out[0] = held[0];
|
||||
out[1] = held[1];
|
||||
}
|
||||
|
||||
static int32_t axis(int16_t v) {
|
||||
int32_t a = v < -STICK_MAX ? -STICK_MAX : v;
|
||||
return a > -STICK_DEAD && a < STICK_DEAD ? 0 : a;
|
||||
}
|
||||
|
||||
// Open the first gamepad when none is open or the open one went away; logs once per change.
|
||||
static void scan_pads(void) {
|
||||
if (pad && SDL_GamepadConnected(pad)) return;
|
||||
if (pad) {
|
||||
SDL_CloseGamepad(pad); pad = NULL; sensor_pad = 0; sensor_ok = 0;
|
||||
InterlockedExchange(&pad_layout_shown, -1);
|
||||
for (int i = 0; i < 4; i++) InterlockedExchange(&pad_labels[i], PAD_LABEL_NONE);
|
||||
rt_log("input: gamepad disconnected");
|
||||
}
|
||||
int n = 0;
|
||||
SDL_JoystickID *ids = SDL_GetGamepads(&n);
|
||||
for (int i = 0; ids && i < n && !pad; i++) pad = SDL_OpenGamepad(ids[i]);
|
||||
SDL_free(ids);
|
||||
if (!pad) return;
|
||||
pad_rumble = SDL_GetBooleanProperty(SDL_GetGamepadProperties(pad), SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false);
|
||||
pad_east_a = SDL_GetGamepadButtonLabel(pad, SDL_GAMEPAD_BUTTON_EAST) == SDL_GAMEPAD_BUTTON_LABEL_A;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
SDL_GamepadButtonLabel lb = SDL_GetGamepadButtonLabel(pad, pad_sdl[PAD_SOUTH + i]);
|
||||
InterlockedExchange(&pad_labels[i], lb == SDL_GAMEPAD_BUTTON_LABEL_A ? PAD_LABEL_A
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_B ? PAD_LABEL_B
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_X ? PAD_LABEL_X
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_Y ? PAD_LABEL_Y
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_CROSS ? PAD_LABEL_CROSS
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_CIRCLE ? PAD_LABEL_CIRCLE
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_SQUARE ? PAD_LABEL_SQUARE
|
||||
: lb == SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE ? PAD_LABEL_TRIANGLE
|
||||
: PAD_LABEL_NONE);
|
||||
}
|
||||
pad_layout = -1;
|
||||
int motion = SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL);
|
||||
rt_log("input: gamepad \"%s\" (%s, %s, east button %s)", SDL_GetGamepadName(pad), pad_rumble ? "rumble" : "no rumble",
|
||||
motion ? "gyro and accelerometer" : "no motion sensors", pad_east_a ? "labelled A" : "not labelled A");
|
||||
}
|
||||
|
||||
// Under lock: the layout of the open gamepad, picked again when a setting or the gamepad changed; logs each change.
|
||||
static int pad_layout_get(void) {
|
||||
unsigned ver = cfg_version();
|
||||
if (pad_layout >= 0 && ver == pad_layout_seen) return pad_layout;
|
||||
pad_layout_seen = ver;
|
||||
int was = pad_layout;
|
||||
const char *set = cfg_value("TOTK_PAD_LAYOUT");
|
||||
pad_layout = input_pad_layout_pick(set, pad_east_a);
|
||||
if (pad_layout != was)
|
||||
rt_log("input: gamepad layout %s (pad_layout = %s)", pad_layout == PAD_LAYOUT_XBOX ? "xbox" : "nintendo", set);
|
||||
InterlockedExchange(&pad_layout_shown, pad_layout);
|
||||
return pad_layout;
|
||||
}
|
||||
|
||||
// The TOTK_BIND_* values last applied, one per line; the tables are built again when one changes (the menu's remapping rows write
|
||||
// them while the game runs). Under lock.
|
||||
static unsigned binds_seen = ~0u;
|
||||
static char binds_applied[4096];
|
||||
|
||||
static void binds_refresh(void) {
|
||||
unsigned v = cfg_version();
|
||||
if (v == binds_seen) return;
|
||||
binds_seen = v;
|
||||
char now[sizeof binds_applied];
|
||||
size_t len = 0;
|
||||
for (int a = 0; a < ACTION_COUNT; a++) {
|
||||
char var[32];
|
||||
snprintf(var, sizeof var, "TOTK_BIND_%s", input_action_name(a));
|
||||
const char *s = cfg_get(var);
|
||||
len += (size_t)snprintf(now + len, len < sizeof now ? sizeof now - len : 0, "%s\n", s ? s : "");
|
||||
}
|
||||
if (!strcmp(now, binds_applied)) return;
|
||||
memcpy(binds_applied, now, sizeof now);
|
||||
input_map_configure();
|
||||
}
|
||||
|
||||
static void start(void) {
|
||||
binds_refresh();
|
||||
const char *e = cfg_get("TOTK_INPUT");
|
||||
if (e && !strcmp(e, "none")) { state = -1; rt_log("input: disabled (TOTK_INPUT=none)"); return; }
|
||||
if (!SDL_Init(SDL_INIT_GAMEPAD)) { state = -1; rt_log("input: SDL gamepad init failed (%s), keyboard only", SDL_GetError()); }
|
||||
else state = 1;
|
||||
if (state == 1) {
|
||||
scan_pads();
|
||||
if (!pad) rt_log("input: no gamepad (keyboard only until one is connected)");
|
||||
}
|
||||
if (state == -1) state = 2; // keyboard only
|
||||
}
|
||||
|
||||
static void stick_bits(uint64_t *b, int32_t x, int32_t y, int first) {
|
||||
if (x < -STICK_DIR) *b |= 1ull << (first + 0);
|
||||
if (y > STICK_DIR) *b |= 1ull << (first + 1);
|
||||
if (x > STICK_DIR) *b |= 1ull << (first + 2);
|
||||
if (y < -STICK_DIR) *b |= 1ull << (first + 3);
|
||||
}
|
||||
|
||||
static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
|
||||
int64_t a = (int64_t)*x * *x + (int64_t)*y * *y, b = (int64_t)kx * kx + (int64_t)ky * ky;
|
||||
if (b > a) { *x = kx; *y = ky; }
|
||||
}
|
||||
|
||||
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
|
||||
static int pad_physical(uint8_t pd[PAD_COUNT]) {
|
||||
if ((!pad || !SDL_GamepadConnected(pad)) && GetTickCount64() >= next_scan) { next_scan = GetTickCount64() + RESCAN_MS; scan_pads(); }
|
||||
SDL_UpdateGamepads();
|
||||
if (!pad || !SDL_GamepadConnected(pad)) return 0;
|
||||
for (int p = PAD_SOUTH; p < PAD_ZL; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]);
|
||||
pd[PAD_ZL] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_ON;
|
||||
pd[PAD_ZR] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_ON;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Under lock, state 1: the open gamepad's buttons (Switch bits after TOTK_PAD_LAYOUT and the TOTK_BIND_* lines) and
|
||||
// sticks; 0 without a connected gamepad.
|
||||
static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
|
||||
uint8_t pd[PAD_COUNT] = {0};
|
||||
if (!pad_physical(pd)) return 0;
|
||||
*b |= input_map_pad_buttons(pd, pad_layout_get());
|
||||
sl[0] = axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX));
|
||||
sl[1] = -axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY));
|
||||
sr[0] = axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTX));
|
||||
sr[1] = -axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTY));
|
||||
return 1;
|
||||
}
|
||||
|
||||
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};
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
if (!state) start();
|
||||
if (state > 0) {
|
||||
binds_refresh();
|
||||
if (state == 1) pad_read(&b, sl, sr);
|
||||
HWND w = gfx_window_handle();
|
||||
if (w && w == GetForegroundWindow()) {
|
||||
uint8_t down[KEY_COUNT] = {0};
|
||||
// Mouse buttons count only while the cursor is captured, and a button already held when the capture began
|
||||
// (the click that refocused the window) is ignored until it is released.
|
||||
static int was_captured;
|
||||
static uint8_t mouse_ignore[KEY_COUNT];
|
||||
int cap = cur_captured != 0;
|
||||
for (int k = 1; k < KEY_COUNT; k++) {
|
||||
int vk = key_vk(k);
|
||||
down[k] = vk && key_down(vk);
|
||||
if (k < KEY_MOUSE_L || k > KEY_MOUSE_X2) continue;
|
||||
if (cap && !was_captured) mouse_ignore[k] = down[k];
|
||||
if (!down[k]) mouse_ignore[k] = 0;
|
||||
if (!cap || mouse_ignore[k]) down[k] = 0;
|
||||
}
|
||||
was_captured = cap;
|
||||
b |= input_map_buttons(down);
|
||||
int32_t k[2];
|
||||
input_map_stick(down, 0, k);
|
||||
pick(&sl[0], &sl[1], k[0], k[1]);
|
||||
input_map_stick(down, 1, k);
|
||||
pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
mouse_camera(k);
|
||||
pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
}
|
||||
}
|
||||
script_apply(&b, sl, sr);
|
||||
rec_apply(b, sl, sr);
|
||||
// Minus and Plus held together (gamepad or keys) open or close the menu and never reach the game
|
||||
static int combo_held;
|
||||
int32_t st[4] = {sl[0], sl[1], sr[0], sr[1]};
|
||||
int toggle = input_menu_gate(&combo_held, menu_is_open(), &b, st);
|
||||
sl[0] = st[0], sl[1] = st[1], sr[0] = st[2], sr[1] = st[3];
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
if (toggle) menu_toggle();
|
||||
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
|
||||
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
|
||||
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
|
||||
}
|
||||
|
||||
// Text entry (swkbd_win.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
|
||||
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
|
||||
// g_block (above): 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
|
||||
void input_block(int on) {
|
||||
InterlockedExchange(&g_block, on ? 1 : 2);
|
||||
cursor_update(gfx_window_handle());
|
||||
}
|
||||
|
||||
// The in-game menu (ui/menu.cpp) blocks the game's input the same way: g_menu 0 closed, 1 open, 2 closed, waiting for
|
||||
// the release. The cursor is released while it is open, for the menu's mouse.
|
||||
void input_menu(int open) {
|
||||
InterlockedExchange(&g_menu, open ? 1 : 2);
|
||||
if (!open) cur_rel_update(rel_menu_closed); // the capture before Esc comes back
|
||||
cursor_update(gfx_window_handle());
|
||||
}
|
||||
|
||||
// 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 LONG *flag, uint64_t buttons) {
|
||||
LONG k = *flag;
|
||||
return k && !(k == 2 && !buttons && InterlockedCompareExchange(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);
|
||||
if (!text && !menu) return;
|
||||
*buttons = 0; *lx = *ly = *rx = *ry = 0;
|
||||
}
|
||||
|
||||
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
|
||||
poll_merged(buttons, lx, ly, rx, ry);
|
||||
}
|
||||
|
||||
int input_pad_read(uint64_t *buttons, int32_t *lx, int32_t *ly) {
|
||||
uint64_t b = 0;
|
||||
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
if (!state) start();
|
||||
if (state > 0) binds_refresh();
|
||||
int ok = state == 1 && pad_read(&b, sl, sr);
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
*buttons = b;
|
||||
*lx = sl[0];
|
||||
*ly = sl[1];
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint16_t rumble_motor(float a, float curve, float strength) {
|
||||
if (!(a > 0) || !(strength > 0)) return 0;
|
||||
float v = powf(a > 1 ? 1 : a, curve) * strength * 65535.0f + 0.5f;
|
||||
return v >= 65535.0f ? 65535 : (uint16_t)v;
|
||||
}
|
||||
|
||||
int input_pad_layout(void) {
|
||||
return (int)InterlockedCompareExchange(&pad_layout_shown, 0, 0);
|
||||
}
|
||||
|
||||
int input_pad_label(int pad_id) {
|
||||
if (pad_id < PAD_SOUTH || pad_id > PAD_NORTH) return PAD_LABEL_NONE;
|
||||
return (int)InterlockedCompareExchange(&pad_labels[pad_id - PAD_SOUTH], 0, 0);
|
||||
}
|
||||
|
||||
void input_map_snapshot(InputMap *m) {
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
if (!state) start();
|
||||
if (state > 0) binds_refresh();
|
||||
*m = input_map;
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
}
|
||||
|
||||
int input_capture(int reset, int *id) {
|
||||
static uint8_t prev_k[KEY_COUNT], prev_p[PAD_COUNT];
|
||||
static int prev_esc;
|
||||
uint8_t k[KEY_COUNT] = {0}, p[PAD_COUNT] = {0};
|
||||
int input_flight_key(void) {
|
||||
HWND w = gfx_window_handle();
|
||||
int fg = w && w == GetForegroundWindow();
|
||||
for (int i = 1; i < KEY_COUNT; i++) k[i] = fg && key_down(key_vk(i));
|
||||
int esc = fg && key_down(VK_ESCAPE);
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
if (!state) start();
|
||||
if (state == 1) pad_physical(p);
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
int got = INPUT_CAPTURE_NONE;
|
||||
if (!reset) {
|
||||
if (esc && !prev_esc) got = INPUT_CAPTURE_CANCEL;
|
||||
for (int i = 1; i < KEY_COUNT && !got; i++)
|
||||
if (k[i] && !prev_k[i]) got = INPUT_CAPTURE_KEY, *id = i;
|
||||
for (int i = 1; i < PAD_COUNT && !got; i++)
|
||||
if (p[i] && !prev_p[i]) got = INPUT_CAPTURE_PAD, *id = i;
|
||||
}
|
||||
memcpy(prev_k, k, sizeof k);
|
||||
memcpy(prev_p, p, sizeof p);
|
||||
prev_esc = esc;
|
||||
return got;
|
||||
return w && w == GetForegroundWindow() && key_down(VK_F8);
|
||||
}
|
||||
|
||||
int input_rumble_triggers_enabled(void) {
|
||||
return cfg_bool("TOTK_RUMBLE_TRIGGERS");
|
||||
// ---- input_wsys.h
|
||||
int ikb_foreground(void) {
|
||||
HWND w = gfx_window_handle();
|
||||
return w && w == GetForegroundWindow();
|
||||
}
|
||||
|
||||
int input_rumble(uint16_t low, uint16_t high, uint32_t ms) {
|
||||
int ok = 0;
|
||||
static int triggers = -1;
|
||||
static unsigned seen;
|
||||
if (triggers < 0 || seen != cfg_version()) seen = cfg_version(), triggers = input_rumble_triggers_enabled();
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
if (state == 1 && pad && pad_rumble) {
|
||||
ok = SDL_RumbleGamepad(pad, low, high, ms);
|
||||
if (triggers) SDL_RumbleGamepadTriggers(pad, high, high, ms); // unsupported pads return false: ignored
|
||||
}
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
return ok;
|
||||
int ikb_down(int key) {
|
||||
int vk = key_vk(key);
|
||||
return vk && key_down(vk);
|
||||
}
|
||||
|
||||
// SDL gamepad sensor axes (SDL_sensor.h, a gamepad held in front of you): x to the right, y up out of the face (lying
|
||||
// flat face up, +y points at the ceiling), z toward the player (out of the bottom edge). The accelerometer reads the
|
||||
// push against gravity in m/s^2 (lying flat: y = +9.81); the gyro reads rad/s, counter-clockwise positive (right hand).
|
||||
// Switch six-axis axes (nn::hid, as MacNativeGames' GCMotion feed and its Pro Controller HID path use them): x to the
|
||||
// right, y toward the top edge (away from the player), z out of the face; acceleration as gravity reads (lying flat:
|
||||
// z = -1 G). So Switch x = SDL x, Switch y = -SDL z, Switch z = SDL y: a rotation (right-handed to right-handed), so the
|
||||
// gyro keeps its sense, while the acceleration is also negated (push -> gravity) and scaled to G.
|
||||
void input_motion_axes(const float a[3], const float g[3], double acc[3], double gyro[3]) {
|
||||
acc[0] = -a[0] / SDL_STANDARD_GRAVITY;
|
||||
acc[1] = a[2] / SDL_STANDARD_GRAVITY;
|
||||
acc[2] = -a[1] / SDL_STANDARD_GRAVITY;
|
||||
gyro[0] = g[0];
|
||||
gyro[1] = -g[2];
|
||||
gyro[2] = g[1];
|
||||
int ikb_escape(void) { return key_down(VK_ESCAPE); }
|
||||
|
||||
int ikb_captured(void) { return cur_captured != 0; }
|
||||
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
|
||||
*dx = InterlockedExchange(&mouse_dx, 0);
|
||||
*dy = InterlockedExchange(&mouse_dy, 0);
|
||||
}
|
||||
|
||||
// Under lock: (re)enable the sensors when the open gamepad changed; 1 if it has both and they are on.
|
||||
static int sensors_ready(void) {
|
||||
SDL_JoystickID id = SDL_GetGamepadID(pad);
|
||||
if (id == sensor_pad) return sensor_ok;
|
||||
sensor_pad = id;
|
||||
// Nothing drains the SDL event queue: no event per sample (the data still updates, SDL_SendJoystickSensor)
|
||||
SDL_SetEventEnabled(SDL_EVENT_GAMEPAD_SENSOR_UPDATE, false);
|
||||
sensor_ok =SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO) && SDL_GamepadHasSensor(pad, SDL_SENSOR_ACCEL) &&
|
||||
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_GYRO, true) &&
|
||||
SDL_SetGamepadSensorEnabled(pad, SDL_SENSOR_ACCEL, true);
|
||||
if (sensor_ok) rt_log("input: motion sensors on (gyro %.0f Hz, accelerometer %.0f Hz)",
|
||||
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_GYRO),
|
||||
(double)SDL_GetGamepadSensorDataRate(pad, SDL_SENSOR_ACCEL));
|
||||
else rt_log("input: \"%s\" has no gyro and accelerometer: the six-axis sensor reads a controller at rest",
|
||||
SDL_GetGamepadName(pad));
|
||||
return sensor_ok;
|
||||
}
|
||||
void ikb_cursor_update(void) { cursor_update(gfx_window_handle()); }
|
||||
|
||||
int input_motion_enabled(void) {
|
||||
static int on = -1;
|
||||
static unsigned seen;
|
||||
unsigned v = cfg_version();
|
||||
if (on < 0 || v != seen) {
|
||||
seen = v;
|
||||
on = cfg_bool("TOTK_MOTION");
|
||||
}
|
||||
return on;
|
||||
}
|
||||
|
||||
// 250 Hz: the gamepads update (reports read, rumble expiry) and the newest gyro and accelerometer sample goes into the
|
||||
// fusion. Without a gamepad with sensors, or with motion off (TOTK_MOTION=0, live), nothing is fed and the six-axis
|
||||
// sensor reads at rest.
|
||||
static DWORD WINAPI motion_main(void *arg) {
|
||||
(void)arg;
|
||||
HANDLE timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
for (;;) {
|
||||
if (timer) {
|
||||
LARGE_INTEGER due = {.QuadPart = -40000}; // 4 ms
|
||||
SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE);
|
||||
WaitForSingleObject(timer, INFINITE);
|
||||
} else Sleep(4);
|
||||
float a[3], g[3];
|
||||
int have = 0;
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
if (pad && SDL_GamepadConnected(pad)) {
|
||||
SDL_UpdateGamepads();
|
||||
have = sensors_ready() && SDL_GetGamepadSensorData(pad, SDL_SENSOR_ACCEL, a, 3) &&
|
||||
SDL_GetGamepadSensorData(pad, SDL_SENSOR_GYRO, g, 3);
|
||||
}
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
if (!have || !input_motion_enabled()) continue;
|
||||
double acc[3], gyro[3];
|
||||
input_motion_axes(a, g, acc, gyro);
|
||||
motion_feed(motion_global(), acc, gyro, kern_now_ns());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void input_motion_start(void) {
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
if (!state) start();
|
||||
if (!motion_thread) {
|
||||
if (!input_motion_enabled()) rt_log("input: motion off (TOTK_MOTION=0); it can be turned on while the game runs");
|
||||
if (state != 1) motion_thread = -1;
|
||||
else {
|
||||
HANDLE h = CreateThread(NULL, 0, motion_main, NULL, 0, NULL);
|
||||
if (!h) rt_fatal("input: motion thread CreateThread failed (error %lu)", GetLastError());
|
||||
CloseHandle(h);
|
||||
motion_thread = 1;
|
||||
}
|
||||
}
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
}
|
||||
void ikb_menu_closed(void) { cur_rel_update(rel_menu_closed); }
|
||||
@@ -0,0 +1,9 @@
|
||||
// The Win32 window's input seams: gfx_window_win.c's window procedure calls these first. Windows only.
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
|
||||
HWND gfx_window_handle(void); // gfx_window_win.c; NULL before gfx_window_start
|
||||
int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res);
|
||||
// The menu's Win32 adapter (ui/menu_win.cpp), called next: F1 / Esc open or close the menu; while it is open, keys,
|
||||
// mouse and focus go to it. Returns 1 with *res set when the message is fully handled.
|
||||
int menu_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res);
|
||||
@@ -0,0 +1,16 @@
|
||||
// Between input.c (gamepad, merging, scripts) and the keyboard and mouse of the window system's back end (input_win.c
|
||||
// with Win32, input_sdl.c with SDL). Any thread.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// ---- what the back end provides
|
||||
int ikb_foreground(void); // the game window is in front: keys count
|
||||
int ikb_down(int key); // key id (input_map.h KEY_*) held, mouse buttons included
|
||||
int ikb_escape(void); // Esc held (no key id: it belongs to the menu)
|
||||
int ikb_captured(void); // the cursor is captured for the mouse camera
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy); // raw mouse motion since the last call, in counts
|
||||
void ikb_cursor_update(void); // what input_cursor_allowed says may have changed
|
||||
void ikb_menu_closed(void); // the menu closed: a capture that Esc gave up comes back
|
||||
|
||||
// ---- what input.c provides the back end
|
||||
int input_cursor_allowed(void); // no text entry or menu open, the mouse camera on, input not disabled
|
||||
+19
-43
@@ -4,7 +4,6 @@
|
||||
// and the service table. The SDK client code runs unchanged: requests are read from the calling thread's message
|
||||
// buffer (TLR, or the user buffer of 0x22) and answered in place. Unknown ports, services and commands stop with their
|
||||
// name and id instead of returning a fake success. Also --probe-ipc.
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -12,6 +11,8 @@
|
||||
#include "audren.h"
|
||||
#include "host.h"
|
||||
#include "ipc.h"
|
||||
#include "nfp.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define CMIF_IN_MAGIC 0x49434653u // "SFCI"
|
||||
#define CMIF_OUT_MAGIC 0x4f434653u // "SFCO"
|
||||
@@ -25,26 +26,27 @@ 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 long g_live;
|
||||
static volatile long long g_released, g_requests;
|
||||
static volatile int32_t g_live;
|
||||
static volatile int64_t g_released, g_requests;
|
||||
static _Thread_local const Cpu *t_req; // guest context of the request being served
|
||||
|
||||
long ipc_sessions_live(void) { return InterlockedCompareExchange(&g_live, 0, 0); }
|
||||
uint64_t ipc_sessions_released(void) { return (uint64_t)InterlockedCompareExchange64(&g_released, 0, 0); }
|
||||
long ipc_sessions_live(void) { return plat_cas32(&g_live, 0, 0); }
|
||||
uint64_t ipc_sessions_released(void) { return (uint64_t)plat_cas64(&g_released, 0, 0); }
|
||||
|
||||
// ---- sessions
|
||||
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)) {
|
||||
IpcSession *s = calloc(1, sizeof *s);
|
||||
if (!s) rt_fatal("ipc: session allocation");
|
||||
*s = (IpcSession){name, handler, state, close, 1};
|
||||
InterlockedIncrement(&g_live);
|
||||
plat_inc32(&g_live);
|
||||
return kern_handle_new(KERN_SESSION, s);
|
||||
}
|
||||
|
||||
static uint32_t session_clone(IpcSession *s) {
|
||||
InterlockedIncrement(&s->refs);
|
||||
plat_inc32(&s->refs);
|
||||
return kern_handle_new(KERN_SESSION, s);
|
||||
}
|
||||
|
||||
@@ -55,21 +57,16 @@ void ipc_handle_closed(int kind, void *obj) {
|
||||
}
|
||||
if (kind != KERN_SESSION) rt_fatal("ipc_handle_closed: handle kind %d", kind);
|
||||
IpcSession *s = obj;
|
||||
if (InterlockedDecrement(&s->refs)) return;
|
||||
if (plat_dec32(&s->refs)) return;
|
||||
if (rt_trace & RT_TRACE_IPC) rt_log("ipc: %s closed", s->name);
|
||||
if (s->close) s->close(s->state);
|
||||
free(s);
|
||||
InterlockedDecrement(&g_live);
|
||||
InterlockedIncrement64(&g_released);
|
||||
plat_dec32(&g_live);
|
||||
plat_inc64(&g_released);
|
||||
}
|
||||
|
||||
// ---- helpers for services
|
||||
static int readable(uint64_t a, uint64_t n) {
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (!VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) || mbi.State != MEM_COMMIT) return 0;
|
||||
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return 0;
|
||||
return a + n <= (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
}
|
||||
static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); }
|
||||
|
||||
void ipc_backtrace(void) {
|
||||
const Cpu *c = t_req;
|
||||
@@ -213,7 +210,7 @@ static void dispatch(IpcSession *s, uint32_t *m) {
|
||||
unsigned ndata;
|
||||
parse(m, &rq, &words, &ndata);
|
||||
if (rq.type == T_CLOSE || rq.type == T_TIPC_CLOSE) return;
|
||||
InterlockedIncrement64(&g_requests);
|
||||
plat_inc64(&g_requests);
|
||||
IpcResponse rs;
|
||||
memset(&rs, 0, sizeof rs);
|
||||
int tipc = rq.type >= T_TIPC_BASE;
|
||||
@@ -655,33 +652,12 @@ static void child_main(Cpu *c) {
|
||||
|
||||
// Re-run this executable with the same arguments and PROBE_CHILD_ENV set; it must stop with the named fatal.
|
||||
static void probe_unknown_service(void) {
|
||||
SECURITY_ATTRIBUTES sa = {sizeof sa, NULL, TRUE};
|
||||
HANDLE rd, wr;
|
||||
if (!CreatePipe(&rd, &wr, &sa, 0)) rt_fatal("probe-ipc: CreatePipe failed (error %lu)", GetLastError());
|
||||
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
||||
STARTUPINFOW si = {.cb = sizeof si, .dwFlags = STARTF_USESTDHANDLES, .hStdInput = GetStdHandle(STD_INPUT_HANDLE),
|
||||
.hStdOutput = wr, .hStdError = wr};
|
||||
PROCESS_INFORMATION pi;
|
||||
wchar_t *cmdline = _wcsdup(GetCommandLineW());
|
||||
SetEnvironmentVariableA(PROBE_CHILD_ENV, "1");
|
||||
BOOL ok = CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
|
||||
SetEnvironmentVariableA(PROBE_CHILD_ENV, NULL);
|
||||
free(cmdline);
|
||||
CloseHandle(wr);
|
||||
if (!ok) rt_fatal("probe-ipc: CreateProcessW failed (error %lu)", GetLastError());
|
||||
static char out[65536];
|
||||
DWORD n, len = 0;
|
||||
while (len < sizeof out - 1 && ReadFile(rd, out + len, (DWORD)(sizeof out - 1 - len), &n, NULL) && n) len += n;
|
||||
out[len] = 0;
|
||||
CloseHandle(rd);
|
||||
WaitForSingleObject(pi.hProcess, 30000);
|
||||
DWORD code = 0;
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
int code = plat_run_self(PROBE_CHILD_ENV, "1", out, sizeof out, 30000);
|
||||
if (code < 0) rt_fatal("probe-ipc: cannot start the child");
|
||||
const char *want = "FATAL: sm: service \"" PROBE_UNKNOWN_SERVICE "\" is not implemented";
|
||||
const char *line = strstr(out, want);
|
||||
printf("child GetServiceHandle(%s): exit code %lu, %s\n", PROBE_UNKNOWN_SERVICE, (unsigned long)code,
|
||||
printf("child GetServiceHandle(%s): exit code %d, %s\n", PROBE_UNKNOWN_SERVICE, code,
|
||||
line ? "named fatal seen" : "named fatal MISSING");
|
||||
if (line) printf(" child: %.*s\n", (int)strcspn(line, "\r\n"), line);
|
||||
CHECK(code == 70 && line, "unknown service fatal (child output: %s)", out);
|
||||
@@ -738,7 +714,7 @@ static void probe_sdk_client(Cpu *c) {
|
||||
unsigned trace = rt_trace;
|
||||
rt_trace |= RT_TRACE_IPC; // log the requests the SDK sends during set-up
|
||||
sdk_prepare(c);
|
||||
uint64_t requests0 = (uint64_t)InterlockedCompareExchange64(&g_requests, 0, 0);
|
||||
uint64_t requests0 = (uint64_t)plat_cas64(&g_requests, 0, 0);
|
||||
uint64_t released0 = ipc_sessions_released();
|
||||
int32_t *param = mem_kernel_alloc(0x100, 64);
|
||||
rt_call(c, stub(SYM_INIT_PARAM), 1, (uint64_t)(uintptr_t)param);
|
||||
@@ -783,7 +759,7 @@ static void probe_sdk_client(Cpu *c) {
|
||||
"SDK render loop");
|
||||
rt_call(c, stub(SYM_STOP), 2, handle[0], handle[1]);
|
||||
rt_call(c, stub(SYM_CLOSE), 2, handle[0], handle[1]);
|
||||
uint64_t requests = (uint64_t)InterlockedCompareExchange64(&g_requests, 0, 0) - requests0;
|
||||
uint64_t requests = (uint64_t)plat_cas64(&g_requests, 0, 0) - requests0;
|
||||
printf("sdk: CloseAudioRenderer: renderers running %ld, sessions released +%llu; %llu requests served for the "
|
||||
"lifted client (sm: kept open by the SDK: %ld session)\n", audren_renderers_live(),
|
||||
(unsigned long long)(ipc_sessions_released() - released0), (unsigned long long)requests,
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ struct IpcSession {
|
||||
IpcHandler handler;
|
||||
void *state;
|
||||
void (*close)(void *state);
|
||||
long refs;
|
||||
int32_t refs;
|
||||
};
|
||||
|
||||
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)); // handle
|
||||
|
||||
+305
-201
@@ -8,9 +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).
|
||||
#include <windows.h>
|
||||
#define __USE_MINGW_SETJMP_NON_SEH // setjmp(b) = _setjmp(b, NULL): longjmp restores registers, no SEH unwinding
|
||||
#include <setjmp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -63,7 +60,7 @@ struct RtThread {
|
||||
int prio, core;
|
||||
uint64_t mask;
|
||||
int started, exited, closed, cancel, is_main;
|
||||
CONDITION_VARIABLE cv;
|
||||
PlatCond cv;
|
||||
// current wait
|
||||
int wkind, woke;
|
||||
uint64_t waddr, wkey;
|
||||
@@ -76,15 +73,14 @@ 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
|
||||
void *base_fiber; // the host thread as a host fiber, after the first switch
|
||||
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
|
||||
Fiber *leaving; // fiber just switched away from, released by whoever resumes next
|
||||
int exit_pending; // ExitThread on a fiber: unwind once back on the thread's own context
|
||||
HANDLE timer; // high-resolution waitable timer for SleepThread
|
||||
// TOTK_TRACE=sched accounting (holding G)
|
||||
DWORD tid; // host thread id
|
||||
uint32_t tid; // host thread id
|
||||
int st; // S_* state since st_since
|
||||
uint64_t st_since, st_ns[S_N], nblk, npre, pre_since, cpu_last;
|
||||
uint64_t wobj; // what the current wait is on: owner handle, key, address or first handle
|
||||
@@ -94,7 +90,7 @@ struct RtThread {
|
||||
uint64_t sp_n, sp_ho, sp_site; // spin wait passes and core handoffs, the last loop head (the thread writes them)
|
||||
RtThread *target; // as a core waiter: the owner core_kick asked to give its core up for it (holding G)
|
||||
// TOTK_ASYNC_PREEMPT (holding G)
|
||||
HANDLE hthr; // the host thread, for SuspendThread / Get/SetThreadContext; open while it may own a core
|
||||
PlatIntr *hthr; // the host thread, for plat_intr_stop / resume; open while it may own a core
|
||||
uint64_t ap_rip; // the interrupted host rip kern_ap_stub returns to
|
||||
uint64_t ap_last; // the last redirect attempt on this owner
|
||||
int ap_pending; // redirected to kern_ap_stub, which has not reached kern_ap_yield yet
|
||||
@@ -110,7 +106,7 @@ struct RtThread {
|
||||
// owner that released
|
||||
};
|
||||
|
||||
static SRWLOCK G = SRWLOCK_INIT;
|
||||
static PlatMutex G = PLAT_MUTEX_INIT;
|
||||
static struct { int type; void *obj; } g_h[MAX_H];
|
||||
static _Thread_local RtThread *tcur;
|
||||
_Static_assert(offsetof(RtThread, preempt) == 0, "host.h kern_preempt_pending reads the first int");
|
||||
@@ -119,8 +115,8 @@ static RtThread *g_all, *g_wait, *g_main;
|
||||
static uint64_t g_next_id = 0x51, g_wseq, g_created;
|
||||
static uint64_t g_main_stack;
|
||||
|
||||
static void lock(void) { AcquireSRWLockExclusive(&G); }
|
||||
static void unlock(void) { ReleaseSRWLockExclusive(&G); }
|
||||
static void lock(void) { plat_mutex_lock(&G); }
|
||||
static void unlock(void) { plat_mutex_unlock(&G); }
|
||||
|
||||
uint64_t kern_now_ns(void) { return plat_now_ns(); }
|
||||
|
||||
@@ -239,7 +235,7 @@ static void wake(RtThread *t, uint32_t res) {
|
||||
t->waker = tcur ? tcur->handle : 0;
|
||||
if (rt_trace & RT_TRACE_SCHED) t->wake_at = kern_now_ns();
|
||||
if (g_crit_on) crit_add(CRIT_WAKE, t->handle, t->waker, 0, 0, t->wake_at);
|
||||
WakeConditionVariable(&t->cv);
|
||||
plat_cond_signal(&t->cv);
|
||||
}
|
||||
|
||||
static void wake_sync_waiters(void) {
|
||||
@@ -268,7 +264,7 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
|
||||
uint64_t deadline = forever ? 0 : now + (uint64_t)ns;
|
||||
while (!t->woke) {
|
||||
if (forever) {
|
||||
SleepConditionVariableSRW(&t->cv, &G, INFINITE, 0);
|
||||
plat_cond_wait(&t->cv, &G);
|
||||
continue;
|
||||
}
|
||||
now = kern_now_ns();
|
||||
@@ -278,7 +274,7 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
|
||||
return R_TIMEOUT;
|
||||
}
|
||||
uint64_t ms = (deadline - now + 999999) / 1000000;
|
||||
SleepConditionVariableSRW(&t->cv, &G, ms > 0xFFFFFFF0u ? 0xFFFFFFF0u : (DWORD)ms, 0);
|
||||
plat_cond_wait_ms(&t->cv, &G, ms > 0xFFFFFFF0u ? 0xFFFFFFF0u : (uint32_t)ms);
|
||||
}
|
||||
if (t->wake_at) {
|
||||
uint64_t d = kern_now_ns() - t->wake_at;
|
||||
@@ -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;
|
||||
|
||||
@@ -306,12 +308,22 @@ static void ap_init(void);
|
||||
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: 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)
|
||||
@@ -320,7 +332,7 @@ static int core_allowed(const RtThread *t, int c) { return (int)((t->mask >> c)
|
||||
// Here a better waiter only marks the owner, which gives the core up at its next SVC or indirect call; an owner in a
|
||||
// stretch of game code with neither keeps it (TOTK_CORE_DIAG: 0.5-1.1 ms of thread-time per frame, waits to 16 ms and
|
||||
// more). With it on, three parts, all of which MacNativeGames found necessary:
|
||||
// - The waiter interrupts the owner (SuspendThread, GetThreadContext) when it has waited AP_FIRST_NS, then every
|
||||
// - The waiter interrupts the owner (plat_intr_stop) when it has waited AP_FIRST_NS, then every
|
||||
// millisecond: if the owner's host rip is in the lifted modules' code (g_gen_lo..g_gen_hi) and its host depth is 0
|
||||
// (not in a host call or SVC, nor in game code a host call made), it holds no host lock, so its rip is moved to
|
||||
// kern_ap_stub, which saves every volatile register, gives the core up like a checkpoint (kern_ap_yield) and jumps
|
||||
@@ -375,8 +387,8 @@ static void ap_init(void) {
|
||||
}
|
||||
rt_log("kernel: asynchronous preemption of virtual cores %s, priority inheritance and preemption disable %s; lifted "
|
||||
"code exe+%#llx-exe+%#llx", g_ap_int ? "on" : "off", g_ap ? "on (TOTK_ASYNC_PREEMPT=0 turns all off)" : "off",
|
||||
(unsigned long long)(g_gen_lo ? g_gen_lo - (uintptr_t)GetModuleHandleW(NULL) : 0),
|
||||
(unsigned long long)(g_gen_hi ? g_gen_hi - (uintptr_t)GetModuleHandleW(NULL) : 0));
|
||||
(unsigned long long)(g_gen_lo ? g_gen_lo - plat_exe_base() : 0),
|
||||
(unsigned long long)(g_gen_hi ? g_gen_hi - plat_exe_base() : 0));
|
||||
}
|
||||
|
||||
// Horizon's priority inheritance: o counts at the priority of the best thread blocked on a guest mutex it owns (the
|
||||
@@ -403,6 +415,19 @@ static int ap_disabled(const RtThread *t) { return g_ap > 0 && ap_tlr(t)[0]; }
|
||||
// in kern_ap_yield and jumps back to kern_ap_rip(), whose value it first stores in the return slot it reserved at
|
||||
// entry, so a stack walk sees the interrupted function as its caller. No ret: the jump keeps call/return pairs
|
||||
// balanced. The waiter never writes the owner's stack.
|
||||
// An x86-64-v2 build (no __AVX__) saves XMM0-15 with SSE moves into the same 32-byte slots instead: its game code
|
||||
// keeps nothing in the upper YMM halves, and the CPUs it is for have no VEX encoding, so a vmovdqa there faults with
|
||||
// an illegal instruction the first time a core is interrupted.
|
||||
#ifdef __AVX__
|
||||
#define AP_VMOV "vmovdqa"
|
||||
#define AP_V "%ymm"
|
||||
#define AP_VZEROUPPER " vzeroupper\n"
|
||||
#else
|
||||
#define AP_VMOV "movdqa"
|
||||
#define AP_V "%xmm"
|
||||
#define AP_VZEROUPPER ""
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
__asm__(".text\n"
|
||||
".globl kern_ap_stub\n"
|
||||
".def kern_ap_stub; .scl 2; .type 32; .endef\n"
|
||||
@@ -433,48 +458,48 @@ __asm__(".text\n"
|
||||
".seh_setframe %rbp, 0\n"
|
||||
".seh_endprologue\n"
|
||||
" andq $-32, %rsp\n"
|
||||
" subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15
|
||||
" vmovdqa %ymm0, 64(%rsp)\n"
|
||||
" vmovdqa %ymm1, 96(%rsp)\n"
|
||||
" vmovdqa %ymm2, 128(%rsp)\n"
|
||||
" vmovdqa %ymm3, 160(%rsp)\n"
|
||||
" vmovdqa %ymm4, 192(%rsp)\n"
|
||||
" vmovdqa %ymm5, 224(%rsp)\n"
|
||||
" vmovdqa %ymm6, 256(%rsp)\n"
|
||||
" vmovdqa %ymm7, 288(%rsp)\n"
|
||||
" vmovdqa %ymm8, 320(%rsp)\n"
|
||||
" vmovdqa %ymm9, 352(%rsp)\n"
|
||||
" vmovdqa %ymm10, 384(%rsp)\n"
|
||||
" vmovdqa %ymm11, 416(%rsp)\n"
|
||||
" vmovdqa %ymm12, 448(%rsp)\n"
|
||||
" vmovdqa %ymm13, 480(%rsp)\n"
|
||||
" vmovdqa %ymm14, 512(%rsp)\n"
|
||||
" vmovdqa %ymm15, 544(%rsp)\n"
|
||||
" subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX)
|
||||
" " AP_VMOV " " AP_V "0, 64(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "1, 96(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "2, 128(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "3, 160(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "4, 192(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "5, 224(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "6, 256(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "7, 288(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "8, 320(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "9, 352(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "10, 384(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "11, 416(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "12, 448(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "13, 480(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "14, 512(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "15, 544(%rsp)\n"
|
||||
" stmxcsr 32(%rsp)\n"
|
||||
" movl $0x1f80, 36(%rsp)\n"
|
||||
" ldmxcsr 36(%rsp)\n"
|
||||
" cld\n"
|
||||
" vzeroupper\n"
|
||||
AP_VZEROUPPER
|
||||
" call kern_ap_rip\n"
|
||||
" movq %rax, 72(%rbp)\n" // the return slot: rbp + 8 pushes + pushfq
|
||||
" call kern_ap_yield\n"
|
||||
" ldmxcsr 32(%rsp)\n"
|
||||
" vmovdqa 64(%rsp), %ymm0\n"
|
||||
" vmovdqa 96(%rsp), %ymm1\n"
|
||||
" vmovdqa 128(%rsp), %ymm2\n"
|
||||
" vmovdqa 160(%rsp), %ymm3\n"
|
||||
" vmovdqa 192(%rsp), %ymm4\n"
|
||||
" vmovdqa 224(%rsp), %ymm5\n"
|
||||
" vmovdqa 256(%rsp), %ymm6\n"
|
||||
" vmovdqa 288(%rsp), %ymm7\n"
|
||||
" vmovdqa 320(%rsp), %ymm8\n"
|
||||
" vmovdqa 352(%rsp), %ymm9\n"
|
||||
" vmovdqa 384(%rsp), %ymm10\n"
|
||||
" vmovdqa 416(%rsp), %ymm11\n"
|
||||
" vmovdqa 448(%rsp), %ymm12\n"
|
||||
" vmovdqa 480(%rsp), %ymm13\n"
|
||||
" vmovdqa 512(%rsp), %ymm14\n"
|
||||
" vmovdqa 544(%rsp), %ymm15\n"
|
||||
" " AP_VMOV " 64(%rsp), " AP_V "0\n"
|
||||
" " AP_VMOV " 96(%rsp), " AP_V "1\n"
|
||||
" " AP_VMOV " 128(%rsp), " AP_V "2\n"
|
||||
" " AP_VMOV " 160(%rsp), " AP_V "3\n"
|
||||
" " AP_VMOV " 192(%rsp), " AP_V "4\n"
|
||||
" " AP_VMOV " 224(%rsp), " AP_V "5\n"
|
||||
" " AP_VMOV " 256(%rsp), " AP_V "6\n"
|
||||
" " AP_VMOV " 288(%rsp), " AP_V "7\n"
|
||||
" " AP_VMOV " 320(%rsp), " AP_V "8\n"
|
||||
" " AP_VMOV " 352(%rsp), " AP_V "9\n"
|
||||
" " AP_VMOV " 384(%rsp), " AP_V "10\n"
|
||||
" " AP_VMOV " 416(%rsp), " AP_V "11\n"
|
||||
" " AP_VMOV " 448(%rsp), " AP_V "12\n"
|
||||
" " AP_VMOV " 480(%rsp), " AP_V "13\n"
|
||||
" " AP_VMOV " 512(%rsp), " AP_V "14\n"
|
||||
" " AP_VMOV " 544(%rsp), " AP_V "15\n"
|
||||
" movq %rbp, %rsp\n"
|
||||
" popq %rbp\n"
|
||||
" popq %r11\n"
|
||||
@@ -488,11 +513,94 @@ __asm__(".text\n"
|
||||
" leaq 8(%rsp), %rsp\n"
|
||||
" jmpq *-8(%rsp)\n"
|
||||
".seh_endproc\n");
|
||||
#else
|
||||
// The System V version: rsi and rdi are volatile there too, and the interrupted code may keep data in the 128 bytes
|
||||
// below its stack pointer (the red zone), so the return slot and the saves go below that. Without a red zone in the
|
||||
// way the slot is at entry - 136; the exit restores rsp to the entry value and jumps through the slot.
|
||||
__asm__(".text\n"
|
||||
".globl kern_ap_stub\n"
|
||||
".type kern_ap_stub, @function\n"
|
||||
".p2align 4\n"
|
||||
"kern_ap_stub:\n"
|
||||
" .cfi_startproc\n"
|
||||
" leaq -136(%rsp), %rsp\n" // not sub: the flags are still the interrupted code's until pushfq
|
||||
" pushfq\n"
|
||||
" pushq %rax\n"
|
||||
" pushq %rcx\n"
|
||||
" pushq %rdx\n"
|
||||
" pushq %rsi\n"
|
||||
" pushq %rdi\n"
|
||||
" pushq %r8\n"
|
||||
" pushq %r9\n"
|
||||
" pushq %r10\n"
|
||||
" pushq %r11\n"
|
||||
" pushq %rbp\n"
|
||||
" movq %rsp, %rbp\n"
|
||||
" andq $-32, %rsp\n"
|
||||
" subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX)
|
||||
" " AP_VMOV " " AP_V "0, 64(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "1, 96(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "2, 128(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "3, 160(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "4, 192(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "5, 224(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "6, 256(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "7, 288(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "8, 320(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "9, 352(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "10, 384(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "11, 416(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "12, 448(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "13, 480(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "14, 512(%rsp)\n"
|
||||
" " AP_VMOV " " AP_V "15, 544(%rsp)\n"
|
||||
" stmxcsr 32(%rsp)\n"
|
||||
" movl $0x1f80, 36(%rsp)\n"
|
||||
" ldmxcsr 36(%rsp)\n"
|
||||
" cld\n"
|
||||
AP_VZEROUPPER
|
||||
" call kern_ap_rip\n"
|
||||
" movq %rax, 88(%rbp)\n" // the return slot: rbp + 10 pushes + pushfq
|
||||
" call kern_ap_yield\n"
|
||||
" ldmxcsr 32(%rsp)\n"
|
||||
" " AP_VMOV " 64(%rsp), " AP_V "0\n"
|
||||
" " AP_VMOV " 96(%rsp), " AP_V "1\n"
|
||||
" " AP_VMOV " 128(%rsp), " AP_V "2\n"
|
||||
" " AP_VMOV " 160(%rsp), " AP_V "3\n"
|
||||
" " AP_VMOV " 192(%rsp), " AP_V "4\n"
|
||||
" " AP_VMOV " 224(%rsp), " AP_V "5\n"
|
||||
" " AP_VMOV " 256(%rsp), " AP_V "6\n"
|
||||
" " AP_VMOV " 288(%rsp), " AP_V "7\n"
|
||||
" " AP_VMOV " 320(%rsp), " AP_V "8\n"
|
||||
" " AP_VMOV " 352(%rsp), " AP_V "9\n"
|
||||
" " AP_VMOV " 384(%rsp), " AP_V "10\n"
|
||||
" " AP_VMOV " 416(%rsp), " AP_V "11\n"
|
||||
" " AP_VMOV " 448(%rsp), " AP_V "12\n"
|
||||
" " AP_VMOV " 480(%rsp), " AP_V "13\n"
|
||||
" " AP_VMOV " 512(%rsp), " AP_V "14\n"
|
||||
" " AP_VMOV " 544(%rsp), " AP_V "15\n"
|
||||
" movq %rbp, %rsp\n"
|
||||
" popq %rbp\n"
|
||||
" popq %r11\n"
|
||||
" popq %r10\n"
|
||||
" popq %r9\n"
|
||||
" popq %r8\n"
|
||||
" popq %rdi\n"
|
||||
" popq %rsi\n"
|
||||
" popq %rdx\n"
|
||||
" popq %rcx\n"
|
||||
" popq %rax\n"
|
||||
" popfq\n"
|
||||
" leaq 136(%rsp), %rsp\n"
|
||||
" jmpq *-136(%rsp)\n"
|
||||
" .cfi_endproc\n"
|
||||
".size kern_ap_stub, .-kern_ap_stub\n");
|
||||
#endif
|
||||
|
||||
enum { AP_NONE, AP_RETRY, AP_SENT };
|
||||
|
||||
// The waiter w interrupts its target owner if that runs top-level game code (holding G; the owner is suspended only
|
||||
// between SuspendThread and ResumeThread, during which nothing here takes a lock).
|
||||
// The waiter w interrupts its target owner if that runs top-level game code (holding G; the owner is stopped only
|
||||
// between plat_intr_stop and plat_intr_resume, during which nothing here takes a lock).
|
||||
static int ap_try(RtThread *w) {
|
||||
RtThread *o = w->target;
|
||||
if (!o || o->cur_core < 0 || !o->preempt) return AP_NONE;
|
||||
@@ -510,17 +618,14 @@ static int ap_try(RtThread *w) {
|
||||
uint64_t now = kern_now_ns();
|
||||
if (o->ap_last && now - o->ap_last < AP_GAP_NS) return AP_RETRY;
|
||||
o->ap_last = now;
|
||||
if (SuspendThread(o->hthr) == (DWORD)-1) {
|
||||
uint64_t rip, rsp;
|
||||
if (!plat_intr_stop(o->hthr, &rip, &rsp)) {
|
||||
g_ap_fail++;
|
||||
return AP_RETRY;
|
||||
}
|
||||
CONTEXT ctx;
|
||||
memset(&ctx, 0, sizeof ctx);
|
||||
ctx.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; // set back whole, so the kernel restores every GPR
|
||||
int r = AP_RETRY;
|
||||
if (!GetThreadContext(o->hthr, &ctx))
|
||||
g_ap_fail++;
|
||||
else if (ctx.Rip < g_gen_lo || ctx.Rip > g_gen_hi)
|
||||
uint64_t to = 0;
|
||||
if (rip < g_gen_lo || rip > g_gen_hi)
|
||||
g_ap_pc++;
|
||||
else if (__atomic_load_n(&o->host_depth, __ATOMIC_RELAXED) > 0)
|
||||
g_ap_host++;
|
||||
@@ -528,23 +633,21 @@ static int ap_try(RtThread *w) {
|
||||
tl[1] = 1;
|
||||
g_ap_deferred++;
|
||||
} else {
|
||||
o->ap_rip = ctx.Rip;
|
||||
o->ap_rip = rip;
|
||||
o->ap_pending = 1;
|
||||
ctx.Rip = (DWORD64)(uintptr_t)kern_ap_stub;
|
||||
if (SetThreadContext(o->hthr, &ctx)) {
|
||||
g_ap_sent++;
|
||||
r = AP_SENT;
|
||||
} else {
|
||||
o->ap_pending = 0;
|
||||
g_ap_fail++;
|
||||
}
|
||||
to = (uint64_t)(uintptr_t)kern_ap_stub;
|
||||
}
|
||||
if (!plat_intr_resume(o->hthr, to)) {
|
||||
o->ap_pending = 0;
|
||||
g_ap_fail++;
|
||||
} else if (to) {
|
||||
g_ap_sent++;
|
||||
r = AP_SENT;
|
||||
}
|
||||
ResumeThread(o->hthr);
|
||||
if (r == AP_SENT && g_ap_log < AP_LOG_N) {
|
||||
g_ap_log++;
|
||||
rt_log("kernel: interrupt %#x p%d at exe+%#llx rsp %#llx for %#x p%d", o->handle, o->prio,
|
||||
(unsigned long long)(o->ap_rip - (uintptr_t)GetModuleHandleW(NULL)), (unsigned long long)ctx.Rsp,
|
||||
w->handle, w->prio);
|
||||
(unsigned long long)(o->ap_rip - plat_exe_base()), (unsigned long long)rsp, w->handle, w->prio);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -665,30 +768,18 @@ static void dg_done(RtThread *t) {
|
||||
// its guest lr. Holding G; the owner is resumed before anything that could take a lock it holds.
|
||||
static void dg_sample(RtThread *t) {
|
||||
RtThread *o = t->target;
|
||||
if (!o || o->cur_core < 0 || !o->tid || g_dg_samples >= DG_SAMPLES) return;
|
||||
HANDLE h = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, o->tid);
|
||||
if (!h) return;
|
||||
CONTEXT ctx;
|
||||
memset(&ctx, 0, sizeof ctx);
|
||||
ctx.ContextFlags = CONTEXT_CONTROL;
|
||||
int depth = 0, ok = 0;
|
||||
if (SuspendThread(h) != (DWORD)-1) {
|
||||
ok = GetThreadContext(h, &ctx);
|
||||
depth = __atomic_load_n(&o->host_depth, __ATOMIC_RELAXED);
|
||||
ResumeThread(h);
|
||||
}
|
||||
CloseHandle(h);
|
||||
if (!ok) return;
|
||||
if (!o || o->cur_core < 0 || !o->hthr || g_dg_samples >= DG_SAMPLES) return;
|
||||
uint64_t rip, rsp;
|
||||
if (!plat_intr_stop(o->hthr, &rip, &rsp)) return;
|
||||
int depth = __atomic_load_n(&o->host_depth, __ATOMIC_RELAXED);
|
||||
plat_intr_resume(o->hthr, 0);
|
||||
g_dg_samples++;
|
||||
HMODULE m = NULL;
|
||||
wchar_t mw[MAX_PATH] = L"?";
|
||||
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
(LPCWSTR)(uintptr_t)ctx.Rip, &m) && m)
|
||||
GetModuleFileNameW(m, mw, MAX_PATH);
|
||||
const wchar_t *mn = wcsrchr(mw, L'\\') ? wcsrchr(mw, L'\\') + 1 : mw;
|
||||
char mn[256] = "?";
|
||||
uintptr_t m = 0;
|
||||
plat_module_of((uintptr_t)rip, mn, sizeof mn, &m);
|
||||
char lr[128];
|
||||
rt_log("core-diag: sample owner %#x p%d depth %d rip %ls+%#llx, last svc %#x, guest lr %s (waiter %#x p%d, %s, "
|
||||
"%.2f ms)", o->handle, o->prio, depth, mn, (unsigned long long)(ctx.Rip - (uintptr_t)m), o->dg_svc,
|
||||
rt_log("core-diag: sample owner %#x p%d depth %d rip %s+%#llx, last svc %#x, guest lr %s (waiter %#x p%d, %s, "
|
||||
"%.2f ms)", o->handle, o->prio, depth, mn, (unsigned long long)(rip - m), o->dg_svc,
|
||||
o->cur ? ldr_describe(o->cur->x[30], lr, sizeof lr) : "-", t->handle, t->prio,
|
||||
t->dg_cls == DG_HOST ? "host" : "game", (kern_now_ns() - t->dg_since) / 1e6);
|
||||
}
|
||||
@@ -788,7 +879,7 @@ static void core_grant(int c) {
|
||||
core_count();
|
||||
if (g_diag > 0) b->dg_rel = g_dg_rel;
|
||||
if (rt_trace & RT_TRACE_SCHED) b->grant_at = kern_now_ns();
|
||||
WakeConditionVariable(&b->cv);
|
||||
plat_cond_signal(&b->cv);
|
||||
}
|
||||
|
||||
static void core_put(RtThread *t) {
|
||||
@@ -897,17 +988,18 @@ static void core_take(RtThread *t) {
|
||||
if (g_ap_int && t->target && t->cur_core < 0) { // most waits end within microseconds: spin before interrupting
|
||||
uint64_t end = kern_now_ns() + AP_FIRST_NS;
|
||||
unlock();
|
||||
while (__atomic_load_n(&t->cur_core, __ATOMIC_RELAXED) < 0 && kern_now_ns() < end) YieldProcessor();
|
||||
while (__atomic_load_n(&t->cur_core, __ATOMIC_RELAXED) < 0 && kern_now_ns() < end) plat_pause();
|
||||
lock();
|
||||
if (t->cur_core >= 0) g_ap_spin_ok++;
|
||||
}
|
||||
int sampled = 0;
|
||||
while (t->cur_core < 0) {
|
||||
DWORD ms = INFINITE;
|
||||
uint32_t ms = 0; // 0: until signalled
|
||||
if (g_ap_int && ap_try(t) != AP_NONE) ms = 1; // the owner keeps its core: again in a millisecond
|
||||
int sample = g_diag > 0 && !sampled && (t->dg_cls == DG_GAME || t->dg_cls == DG_HOST);
|
||||
if (sample && ms == INFINITE) ms = 2;
|
||||
SleepConditionVariableSRW(&t->cv, &G, ms, 0);
|
||||
if (sample && !ms) ms = 2;
|
||||
if (ms) plat_cond_wait_ms(&t->cv, &G, ms);
|
||||
else plat_cond_wait(&t->cv, &G);
|
||||
if (sample && t->cur_core < 0 && kern_now_ns() - t->dg_since >= 2000000) {
|
||||
sampled = 1;
|
||||
dg_sample(t);
|
||||
@@ -943,7 +1035,7 @@ int kern_core_away(void) {
|
||||
core_put(t);
|
||||
acct(t, S_HOST);
|
||||
if (rt_trace & RT_TRACE_SCHED) // the caller as an exe RVA (totk.map)
|
||||
t->wobj = (uintptr_t)__builtin_return_address(0) - (uintptr_t)GetModuleHandleW(NULL);
|
||||
t->wobj = (uintptr_t)__builtin_return_address(0) - plat_exe_base();
|
||||
unlock();
|
||||
return 1;
|
||||
}
|
||||
@@ -975,7 +1067,7 @@ static _Thread_local uint64_t sw_head, sw_t0, sw_last;
|
||||
|
||||
void rt_spin_gate(Cpu *c, uint64_t head) {
|
||||
(void)c;
|
||||
YieldProcessor();
|
||||
plat_pause();
|
||||
RtThread *t = tcur;
|
||||
if (!t) return;
|
||||
if (g_spin_on < 0) {
|
||||
@@ -996,7 +1088,7 @@ void rt_spin_gate(Cpu *c, uint64_t head) {
|
||||
return;
|
||||
t->sp_ho++;
|
||||
int away = kern_core_away();
|
||||
SwitchToThread();
|
||||
plat_yield();
|
||||
kern_core_back(&away);
|
||||
sw_last = kern_now_ns();
|
||||
}
|
||||
@@ -1194,15 +1286,11 @@ void kern_sched_report(uint64_t frame) {
|
||||
r->sl_n = t->sl_n, r->sl_req = t->sl_req, r->sl_act = t->sl_act;
|
||||
r->sp_n = t->sp_n, r->sp_ho = t->sp_ho, r->sp_site = t->sp_site;
|
||||
r->cpu = 0;
|
||||
HANDLE th = t->tid ? OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, t->tid) : NULL;
|
||||
FILETIME a, b, k, u;
|
||||
if (th && GetThreadTimes(th, &a, &b, &k, &u)) {
|
||||
uint64_t c = (((uint64_t)k.dwHighDateTime << 32 | k.dwLowDateTime) +
|
||||
((uint64_t)u.dwHighDateTime << 32 | u.dwLowDateTime)) * 100;
|
||||
uint64_t c = t->tid ? plat_thread_cpu_ns(t->tid) : 0;
|
||||
if (c) {
|
||||
r->cpu = t->cpu_last ? c - t->cpu_last : 0;
|
||||
t->cpu_last = c;
|
||||
}
|
||||
if (th) CloseHandle(th);
|
||||
}
|
||||
for (int k = 0; k < 2; k++) {
|
||||
const WaitRec *src = k ? g_mw : g_rw;
|
||||
@@ -1311,10 +1399,10 @@ static void lock_acquire_for(RtThread *t, uint64_t addr, uint32_t tag) {
|
||||
// ---- threads
|
||||
static RtThread *thr_new(int prio, int core, uint64_t mask) {
|
||||
RtThread *t = calloc(1, sizeof *t);
|
||||
Cpu *c = _aligned_malloc(sizeof(Cpu), 64);
|
||||
Cpu *c = plat_aligned_alloc(sizeof(Cpu), 64);
|
||||
if (!t || !c) rt_fatal("kernel: thread allocation");
|
||||
memset(c, 0, sizeof *c);
|
||||
InitializeConditionVariable(&t->cv);
|
||||
plat_cond_init(&t->cv);
|
||||
t->prio = prio;
|
||||
t->core = core;
|
||||
t->mask = mask;
|
||||
@@ -1347,22 +1435,20 @@ static void thr_free(RtThread *t) { // holding G; t has exited and its handle
|
||||
}
|
||||
mem_kernel_free(t->tlr, TLR_SIZE + THREAD_OBJ_SIZE);
|
||||
mem_kernel_free(t->tls, TLS_SIZE);
|
||||
_aligned_free(t->cpu);
|
||||
plat_aligned_free(t->cpu);
|
||||
free(t);
|
||||
}
|
||||
|
||||
// The end of every guest thread, on its own host context after the unwind: give the core up, signal the thread
|
||||
// object, free it if its handle is already closed. t is not touched after the unlock.
|
||||
static void thread_finish(RtThread *t) {
|
||||
if (t->timer) CloseHandle(t->timer);
|
||||
t->timer = NULL;
|
||||
if (t->base_fiber) ConvertFiberToThread();
|
||||
if (t->base_fiber) plat_fiber_unself();
|
||||
t->base_fiber = NULL;
|
||||
tcur = NULL;
|
||||
lock();
|
||||
core_put(t);
|
||||
acct(t, S_OFF);
|
||||
if (t->hthr) CloseHandle(t->hthr); // no waiter interrupts a thread without a core
|
||||
if (t->hthr) plat_intr_free(t->hthr); // no waiter interrupts a thread without a core
|
||||
t->hthr = NULL;
|
||||
t->exited = 1;
|
||||
wake_sync_waiters();
|
||||
@@ -1370,40 +1456,39 @@ 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 only).
|
||||
static HANDLE ap_open_self(void) {
|
||||
if (!cores_enabled() || !g_ap_int) return NULL;
|
||||
HANDLE h = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION,
|
||||
FALSE, GetCurrentThreadId());
|
||||
if (!h) rt_log("kernel: OpenThread for asynchronous preemption failed (error %lu)", GetLastError());
|
||||
// The calling host thread, for ap_try (TOTK_ASYNC_PREEMPT) and dg_sample (TOTK_CORE_DIAG).
|
||||
static PlatIntr *ap_open_self(void) {
|
||||
if (!cores_enabled() || (!g_ap_int && g_diag <= 0)) return NULL;
|
||||
PlatIntr *h = plat_intr_self();
|
||||
if (!h) rt_log("kernel: no interruption handle for this thread: it is not preempted asynchronously");
|
||||
return h;
|
||||
}
|
||||
|
||||
static DWORD WINAPI thread_main(void *p) {
|
||||
static void thread_main(void *p) {
|
||||
RtThread *t = p;
|
||||
tcur = t;
|
||||
t->tid = GetCurrentThreadId();
|
||||
HANDLE self = ap_open_self();
|
||||
t->tid = plat_thread_id();
|
||||
PlatIntr *self = ap_open_self();
|
||||
lock();
|
||||
t->hthr = self;
|
||||
while (!t->started) SleepConditionVariableSRW(&t->cv, &G, INFINITE, 0);
|
||||
while (!t->started) plat_cond_wait(&t->cv, &G);
|
||||
core_take(t);
|
||||
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,
|
||||
ldr_describe(t->entry, d, sizeof d));
|
||||
}
|
||||
thread_finish(tcur);
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t kern_threads_created(void) {
|
||||
@@ -1424,17 +1509,16 @@ uint32_t kern_main_thread(Cpu **cpu) {
|
||||
if (g_main) rt_fatal("kern_main_thread: the main thread already runs on another host thread");
|
||||
const RtGuestLayout *L = mem_layout();
|
||||
uint64_t stack = L->stack.base + L->stack.size - MAIN_STACK;
|
||||
if (!VirtualAlloc((void *)(uintptr_t)stack, MAIN_STACK, MEM_COMMIT, PAGE_READWRITE))
|
||||
rt_fatal("kernel: commit the main thread stack at %#llx failed (error %lu)", (unsigned long long)stack,
|
||||
GetLastError());
|
||||
if (!plat_mem_commit(stack, MAIN_STACK))
|
||||
rt_fatal("kernel: commit the main thread stack at %#llx failed", (unsigned long long)stack);
|
||||
RtThread *t = thr_new(MAIN_PRIO, MAIN_CORE, 7);
|
||||
t->started = 1;
|
||||
t->is_main = 1;
|
||||
t->stack_top = stack + MAIN_STACK;
|
||||
t->cpu->sp = t->stack_top;
|
||||
t->tid = GetCurrentThreadId();
|
||||
t->tid = plat_thread_id();
|
||||
perf_main_thread();
|
||||
HANDLE self = ap_open_self();
|
||||
PlatIntr *self = ap_open_self();
|
||||
lock();
|
||||
t->hthr = self;
|
||||
thr_link(t);
|
||||
@@ -1453,44 +1537,33 @@ void kern_main_stack(uint64_t *base, uint64_t *size) {
|
||||
*size = b ? MAIN_STACK : 0;
|
||||
}
|
||||
|
||||
static DWORD WINAPI guest_main(void *p) {
|
||||
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 0;
|
||||
return;
|
||||
}
|
||||
|
||||
void kern_run_on_guest_main(void (*fn)(Cpu *c)) {
|
||||
HANDLE h = CreateThread(NULL, HOST_STACK, guest_main, (void *)fn, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
|
||||
if (!h) rt_fatal("kernel: CreateThread for the guest main thread failed (error %lu)", GetLastError());
|
||||
WaitForSingleObject(h, INFINITE);
|
||||
CloseHandle(h);
|
||||
PlatThread *h = plat_thread_new_ex(guest_main, (void *)fn, "guest main", HOST_STACK);
|
||||
if (!h) rt_fatal("kernel: cannot start the guest main thread");
|
||||
plat_thread_join(h);
|
||||
}
|
||||
|
||||
// The high-resolution waitable timer ends a wait 0.3-0.5 ms late at best: the SDK's 20 us polling sleeps (main thread
|
||||
// and job workers, about 1.5 per frame each) took 460-515 us. Sleeps up to SLEEP_SPIN_NS spin on the performance
|
||||
// counter instead; the virtual core is already given up. Linux: clock_nanosleep wakes within about 50 us (timer slack),
|
||||
// so a Linux host spins only below that, or not at all.
|
||||
#define SLEEP_SPIN_NS 1000000
|
||||
// The OS timer ends a wait late by up to PLAT_WAIT_LATE_NS (Windows' high-resolution waitable timer: 0.3-0.5 ms; the
|
||||
// SDK's 20 us polling sleeps, main thread and job workers, about 1.5 per frame each, took 460-515 us). Sleeps up to
|
||||
// twice that spin on the clock instead (1 ms on Windows, 120 us on Linux); the virtual core is already given up.
|
||||
#define SLEEP_SPIN_NS (2 * PLAT_WAIT_LATE_NS)
|
||||
static void sleep_ns(RtThread *t, int64_t ns) {
|
||||
(void)t;
|
||||
if (ns <= SLEEP_SPIN_NS) {
|
||||
uint64_t end = kern_now_ns() + (uint64_t)ns;
|
||||
while (kern_now_ns() < end) YieldProcessor();
|
||||
while (kern_now_ns() < end) plat_pause();
|
||||
return;
|
||||
}
|
||||
if (!t->timer) {
|
||||
t->timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
|
||||
if (!t->timer) t->timer = CreateWaitableTimerExW(NULL, NULL, 0, TIMER_ALL_ACCESS);
|
||||
if (!t->timer) rt_fatal("kernel: CreateWaitableTimerExW failed (error %lu)", GetLastError());
|
||||
}
|
||||
LARGE_INTEGER due;
|
||||
int64_t units = ns / 100 + (ns % 100 != 0); // 100 ns units, rounded up
|
||||
due.QuadPart = -(units > 0 ? units : 1);
|
||||
if (!SetWaitableTimer(t->timer, &due, 0, NULL, NULL, FALSE))
|
||||
rt_fatal("kernel: SetWaitableTimer failed (error %lu)", GetLastError());
|
||||
WaitForSingleObject(t->timer, INFINITE);
|
||||
plat_wait_ns((uint64_t)ns);
|
||||
}
|
||||
|
||||
// ---- synchronization objects
|
||||
@@ -1579,9 +1652,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
lock();
|
||||
thr_link(t);
|
||||
unlock();
|
||||
HANDLE h = CreateThread(NULL, HOST_STACK, thread_main, t, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
|
||||
if (!h) rt_fatal("kernel: CreateThread failed (error %lu)", GetLastError());
|
||||
CloseHandle(h);
|
||||
if (!plat_thread_spawn(thread_main, t, "guest", HOST_STACK)) rt_fatal("kernel: cannot start a guest thread");
|
||||
if (rt_trace & RT_TRACE_SVC) {
|
||||
char d[128];
|
||||
rt_log("CreateThread %#x entry %s stack %#llx prio %d core %d", t->handle,
|
||||
@@ -1596,7 +1667,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
RtThread *t = h_get((uint32_t)x[0], O_THREAD);
|
||||
if (t) {
|
||||
t->started = 1;
|
||||
WakeConditionVariable(&t->cv);
|
||||
plat_cond_signal(&t->cv);
|
||||
}
|
||||
unlock();
|
||||
x[0] = t ? R_OK : R_BAD_HANDLE;
|
||||
@@ -1609,7 +1680,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
t->leaving = t->fiber;
|
||||
t->fiber = NULL;
|
||||
t->cur = t->cpu;
|
||||
SwitchToFiber(t->base_fiber);
|
||||
plat_fiber_switch(t->base_fiber);
|
||||
rt_fatal("kernel: ExitThread on a fiber came back");
|
||||
}
|
||||
exit_now(t);
|
||||
@@ -1646,7 +1717,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
core_take(t);
|
||||
}
|
||||
unlock();
|
||||
if (!give) SwitchToThread();
|
||||
if (!give) plat_yield();
|
||||
return 1;
|
||||
}
|
||||
case 0x0C: { // GetThreadPriority(w1 handle -> w1)
|
||||
@@ -1829,7 +1900,9 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
if (type != 0) {
|
||||
int32_t nv = val + 1;
|
||||
if (type == 2) {
|
||||
if (cnt <= 0) nv = nw > 0 ? val - 1 : val + 1;
|
||||
// Waking all (count <= 0) with waiters subtracts 2, not 1 (mesosphere
|
||||
// KAddressArbiter::SignalAndModifyByWaitingCountIfEqual; yuzu, LAS Recomp and nxnative agree).
|
||||
if (cnt <= 0) nv = nw > 0 ? val - 2 : val + 1;
|
||||
else nv = nw == 0 ? val + 1 : nw <= cnt ? val - 1 : val;
|
||||
}
|
||||
uint32_t e = (uint32_t)val;
|
||||
@@ -1893,8 +1966,13 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
int idx = -1;
|
||||
lock();
|
||||
g_sync_calls++;
|
||||
// Horizon's order (mesosphere KSynchronizationObject::Wait): a signalled object, then timeout 0, then a pending
|
||||
// CancelSynchronization. The cancel is for the next wait that would block: a poll or a wait on a signalled
|
||||
// object leaves it pending. A cancel that wakes this wait (CancelSynchronization wakes it with R_CANCELLED; a
|
||||
// signal wakes it with -1 to look again) ends it cancelled, signalled objects or not.
|
||||
uint32_t woke = 0;
|
||||
for (;;) {
|
||||
if (t->cancel) {
|
||||
if (woke == R_CANCELLED) {
|
||||
t->cancel = 0;
|
||||
r = R_CANCELLED;
|
||||
break;
|
||||
@@ -1910,6 +1988,15 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
r = R_OK;
|
||||
break;
|
||||
}
|
||||
if (ns == 0) {
|
||||
r = R_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
if (t->cancel) {
|
||||
t->cancel = 0;
|
||||
r = R_CANCELLED;
|
||||
break;
|
||||
}
|
||||
int64_t left = ns;
|
||||
if (ns > 0) {
|
||||
uint64_t spent = kern_now_ns() - start;
|
||||
@@ -1921,7 +2008,8 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
}
|
||||
t->wobj = n ? hs[0] | (uint64_t)n << 32 : 0;
|
||||
g_sync_blocks++;
|
||||
if (block(t, W_SYNC, left) == R_TIMEOUT) {
|
||||
woke = block(t, W_SYNC, left);
|
||||
if (woke == R_TIMEOUT) {
|
||||
r = R_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
@@ -1936,7 +2024,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
|
||||
RtThread *t = h_get((uint32_t)x[0], O_THREAD);
|
||||
if (t) {
|
||||
t->cancel = 1;
|
||||
if (t->wkind == W_SYNC) wake(t, (uint32_t)-1);
|
||||
if (t->wkind == W_SYNC) wake(t, R_CANCELLED);
|
||||
}
|
||||
unlock();
|
||||
x[0] = t ? R_OK : R_BAD_HANDLE;
|
||||
@@ -1997,21 +2085,31 @@ void kern_event_signal(uint32_t hw) {
|
||||
|
||||
// System events of host services: the SDK's own nn::os::CreateSystemEvent creates the kernel event (SVC 0x45) on a
|
||||
// guest thread; the writable handle is kept, so any thread (host audio tick included) can signal it later.
|
||||
// The guest may destroy such an event and attach the same SystemEventType again: the game does that with the nfp
|
||||
// activate and deactivate events before each detection. nn::os::DestroySystemEvent zeroes the state byte at +0x28
|
||||
// (0 not initialised, 1 event, 2 inter-process event; nn::os::TryWaitSystemEvent aborts on 0) and closes both
|
||||
// handles, so a kept handle counts only while that byte is nonzero.
|
||||
#define SE_STATE 0x28
|
||||
static struct { uint64_t ev; uint32_t wh; } g_se[512];
|
||||
static int g_nse;
|
||||
static SRWLOCK g_se_lock = SRWLOCK_INIT;
|
||||
static PlatMutex g_se_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
static int se_live(uint64_t ev) {
|
||||
return __atomic_load_n((const uint8_t *)(uintptr_t)(ev + SE_STATE), __ATOMIC_RELAXED) != 0;
|
||||
}
|
||||
|
||||
static uint32_t se_find(uint64_t ev) {
|
||||
AcquireSRWLockShared(&g_se_lock);
|
||||
plat_mutex_lock_shared(&g_se_lock);
|
||||
uint32_t wh = 0;
|
||||
for (int i = 0; i < g_nse && !wh; i++)
|
||||
if (g_se[i].ev == ev) wh = g_se[i].wh;
|
||||
ReleaseSRWLockShared(&g_se_lock);
|
||||
plat_mutex_unlock_shared(&g_se_lock);
|
||||
return wh;
|
||||
}
|
||||
|
||||
// A bound event the guest destroyed since is created again and its table entry takes the new writable handle.
|
||||
void kern_bind_system_event(Cpu *c, uint64_t ev, int autoclear) {
|
||||
if (se_find(ev)) return;
|
||||
if (se_find(ev) && se_live(ev)) return;
|
||||
if (!tcur || !c) rt_fatal("kern_bind_system_event(%#llx) on a host thread", (unsigned long long)ev);
|
||||
static uint64_t create, getw;
|
||||
if (!create) {
|
||||
@@ -2022,16 +2120,22 @@ void kern_bind_system_event(Cpu *c, uint64_t ev, int autoclear) {
|
||||
uint32_t r = (uint32_t)rt_call(c, create, 3, ev, (uint64_t)(autoclear ? 1 : 0), (uint64_t)1);
|
||||
if (r) rt_fatal("kern_bind_system_event: CreateSystemEvent(%#llx) failed %#x", (unsigned long long)ev, r);
|
||||
uint32_t wh = (uint32_t)rt_call(c, getw, 1, ev);
|
||||
AcquireSRWLockExclusive(&g_se_lock);
|
||||
if (g_nse == (int)(sizeof g_se / sizeof *g_se)) rt_fatal("kern_bind_system_event: table full");
|
||||
g_se[g_nse].ev = ev;
|
||||
g_se[g_nse++].wh = wh;
|
||||
ReleaseSRWLockExclusive(&g_se_lock);
|
||||
plat_mutex_lock(&g_se_lock);
|
||||
int i = 0;
|
||||
while (i < g_nse && g_se[i].ev != ev) i++;
|
||||
if (i == g_nse) {
|
||||
if (g_nse == (int)(sizeof g_se / sizeof *g_se)) rt_fatal("kern_bind_system_event: table full");
|
||||
g_se[g_nse++].ev = ev;
|
||||
}
|
||||
g_se[i].wh = wh;
|
||||
plat_mutex_unlock(&g_se_lock);
|
||||
}
|
||||
|
||||
// An event not bound yet is bound on the spot with manual clear, which needs a guest thread.
|
||||
// An event not bound yet is bound on the spot with manual clear, which needs a guest thread. A bound event the guest
|
||||
// destroyed is not signalled: its handles are closed and the kernel hands their numbers to new objects.
|
||||
void kern_signal_system_event(uint64_t ev) {
|
||||
uint32_t wh = se_find(ev);
|
||||
if (wh && !se_live(ev)) return;
|
||||
if (!wh) {
|
||||
kern_bind_system_event(kern_cpu(), ev, 0);
|
||||
wh = se_find(ev);
|
||||
@@ -2048,19 +2152,19 @@ enum { F_NEW, F_STARTED, F_DONE };
|
||||
struct Fiber {
|
||||
uint64_t addr, func, arg, stack, size;
|
||||
Cpu *cpu;
|
||||
void *host;
|
||||
PlatFiber *host;
|
||||
int state;
|
||||
volatile int running;
|
||||
};
|
||||
static Fiber *g_fibers[FIBER_MAX];
|
||||
static SRWLOCK g_fiber_lock = SRWLOCK_INIT;
|
||||
static PlatMutex g_fiber_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
static Fiber *fiber_find(uint64_t addr) {
|
||||
Fiber *f = NULL;
|
||||
AcquireSRWLockShared(&g_fiber_lock);
|
||||
plat_mutex_lock_shared(&g_fiber_lock);
|
||||
for (int i = 0; i < FIBER_MAX && !f; i++)
|
||||
if (g_fibers[i] && g_fibers[i]->addr == addr) f = g_fibers[i];
|
||||
ReleaseSRWLockShared(&g_fiber_lock);
|
||||
plat_mutex_unlock_shared(&g_fiber_lock);
|
||||
return f;
|
||||
}
|
||||
|
||||
@@ -2069,7 +2173,7 @@ static void fiber_switch_to(RtThread *t, const Cpu *from, Fiber *to);
|
||||
// After a host fiber switch: release the context left behind, and finish an ExitThread issued on a fiber.
|
||||
static void fiber_resumed(void) {
|
||||
RtThread *t = tcur;
|
||||
rt_fpcr_sync(t->cur); // the host fiber resumes with its own MXCSR (FIBER_FLAG_FLOAT_SWITCH)
|
||||
rt_fpcr_sync(t->cur); // the host fiber resumes with its own MXCSR (platform.h fibers switch it)
|
||||
if (t->leaving) {
|
||||
__atomic_store_n(&t->leaving->running, 0, __ATOMIC_RELEASE);
|
||||
t->leaving = NULL;
|
||||
@@ -2077,7 +2181,7 @@ static void fiber_resumed(void) {
|
||||
if (t->exit_pending && !t->fiber) exit_now(t);
|
||||
}
|
||||
|
||||
static void WINAPI fiber_main(void *p) {
|
||||
static void fiber_main(void *p) {
|
||||
Fiber *f = p;
|
||||
fiber_resumed();
|
||||
Cpu *c = f->cpu;
|
||||
@@ -2094,8 +2198,8 @@ static void WINAPI fiber_main(void *p) {
|
||||
|
||||
static void fiber_switch_to(RtThread *t, const Cpu *from, Fiber *to) {
|
||||
if (!t->base_fiber) {
|
||||
t->base_fiber = ConvertThreadToFiberEx(NULL, FIBER_FLAG_FLOAT_SWITCH);
|
||||
if (!t->base_fiber) rt_fatal("kernel: ConvertThreadToFiberEx failed (error %lu)", GetLastError());
|
||||
t->base_fiber = plat_fiber_self();
|
||||
if (!t->base_fiber) rt_fatal("kernel: cannot make the thread a fiber");
|
||||
}
|
||||
Cpu *dst = to ? to->cpu : t->cpu;
|
||||
if (to) {
|
||||
@@ -2118,7 +2222,7 @@ static void fiber_switch_to(RtThread *t, const Cpu *from, Fiber *to) {
|
||||
t->fiber = to;
|
||||
t->cur = dst;
|
||||
int depth = t->host_depth;
|
||||
SwitchToFiber(to ? to->host : t->base_fiber);
|
||||
plat_fiber_switch(to ? to->host : t->base_fiber);
|
||||
t->host_depth = depth;
|
||||
fiber_resumed();
|
||||
}
|
||||
@@ -2130,17 +2234,17 @@ void kern_fiber_initialize(Cpu *c) {
|
||||
if (f && f->running) rt_fatal("InitializeFiber(%#llx): the fiber is running", (unsigned long long)addr);
|
||||
if (!f) {
|
||||
f = calloc(1, sizeof *f);
|
||||
Cpu *fc = _aligned_malloc(sizeof(Cpu), 64);
|
||||
Cpu *fc = plat_aligned_alloc(sizeof(Cpu), 64);
|
||||
if (!f || !fc) rt_fatal("kernel: fiber allocation");
|
||||
f->cpu = fc;
|
||||
AcquireSRWLockExclusive(&g_fiber_lock);
|
||||
plat_mutex_lock(&g_fiber_lock);
|
||||
int i = 0;
|
||||
while (i < FIBER_MAX && g_fibers[i]) i++;
|
||||
if (i == FIBER_MAX) rt_fatal("kernel: more than %d fibers", FIBER_MAX);
|
||||
g_fibers[i] = f;
|
||||
ReleaseSRWLockExclusive(&g_fiber_lock);
|
||||
plat_mutex_unlock(&g_fiber_lock);
|
||||
} else if (f->host) {
|
||||
DeleteFiber(f->host);
|
||||
plat_fiber_free(f->host);
|
||||
}
|
||||
memset(f->cpu, 0, sizeof *f->cpu);
|
||||
f->cpu->excl_addr = ~0ull;
|
||||
@@ -2151,25 +2255,25 @@ void kern_fiber_initialize(Cpu *c) {
|
||||
f->size = c->x[4];
|
||||
f->state = F_NEW;
|
||||
f->running = 0;
|
||||
f->host = CreateFiberEx(0x10000, HOST_STACK, FIBER_FLAG_FLOAT_SWITCH, fiber_main, f);
|
||||
if (!f->host) rt_fatal("kernel: CreateFiberEx failed (error %lu)", GetLastError());
|
||||
f->host = plat_fiber_new(HOST_STACK, fiber_main, f);
|
||||
if (!f->host) rt_fatal("kernel: cannot create a host fiber");
|
||||
}
|
||||
|
||||
// FinalizeFiber(x0 FiberType*)
|
||||
void kern_fiber_finalize(Cpu *c) {
|
||||
uint64_t addr = c->x[0];
|
||||
AcquireSRWLockExclusive(&g_fiber_lock);
|
||||
plat_mutex_lock(&g_fiber_lock);
|
||||
Fiber *f = NULL;
|
||||
for (int i = 0; i < FIBER_MAX && !f; i++)
|
||||
if (g_fibers[i] && g_fibers[i]->addr == addr) {
|
||||
f = g_fibers[i];
|
||||
g_fibers[i] = NULL;
|
||||
}
|
||||
ReleaseSRWLockExclusive(&g_fiber_lock);
|
||||
plat_mutex_unlock(&g_fiber_lock);
|
||||
if (!f) rt_fatal("FinalizeFiber(%#llx): not an initialized fiber", (unsigned long long)addr);
|
||||
if (f->running) rt_fatal("FinalizeFiber(%#llx): the fiber is running", (unsigned long long)addr);
|
||||
DeleteFiber(f->host);
|
||||
_aligned_free(f->cpu);
|
||||
plat_fiber_free(f->host);
|
||||
plat_aligned_free(f->cpu);
|
||||
free(f);
|
||||
}
|
||||
|
||||
|
||||
+55
-20
@@ -3,7 +3,6 @@
|
||||
// PLT stub for nn::os::GetSystemTick, through the lifted sdk function, to rt_ticks.
|
||||
//
|
||||
// Every probe thread is created with SVC 0x08 and drives the kernel through rt_svc, the entry the lifted code uses.
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -111,10 +110,10 @@ static void user_unlock(Cpu *c, volatile uint32_t *a, uint32_t tag) {
|
||||
// ---- lock handoff order
|
||||
typedef struct { volatile uint32_t *lock, *key; uint32_t tag, r; int idx, held; } LockJob;
|
||||
static volatile uint32_t g_lockw __attribute__((aligned(8))), g_keyw __attribute__((aligned(8)));
|
||||
static volatile long g_order_n;
|
||||
static volatile int32_t g_order_n;
|
||||
static int g_order[8];
|
||||
|
||||
static void record(int idx) { g_order[InterlockedIncrement(&g_order_n) - 1] = idx; }
|
||||
static void record(int idx) { g_order[plat_inc32(&g_order_n) - 1] = idx; }
|
||||
|
||||
static void lock_waiter(Cpu *c) {
|
||||
LockJob *j = arg_p(c, 0);
|
||||
@@ -306,9 +305,18 @@ static void test_arbiter(Cpu *c) {
|
||||
CHECK(sig(c, 2, 5, 0) == 0 && g_arb == 6, "signal type 2, no waiter, count 0: value %d (want 6)", (int32_t)g_arb);
|
||||
ArbJob jc = {.type = 2, .value = 6, .timeout = -1};
|
||||
h = arb_spawn(c, &jc);
|
||||
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 5, "signal type 2, 1 waiter, count -1: value %d (want 5)", (int32_t)g_arb);
|
||||
// Waking all (count <= 0) with waiters subtracts 2 (mesosphere KAddressArbiter::SignalAndModifyByWaitingCountIfEqual).
|
||||
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 4, "signal type 2, 1 waiter, count -1: value %d (want 4)", (int32_t)g_arb);
|
||||
join(c, h);
|
||||
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0, "wait type 2 results %#x %#x %#x", ja.r, jb.r, jc.r);
|
||||
g_arb = 6;
|
||||
ArbJob jd = {.type = 2, .value = 6, .timeout = -1}, je = jd;
|
||||
uint32_t hd = arb_spawn(c, &jd), he = arb_spawn(c, &je);
|
||||
CHECK(sig(c, 2, 6, 0) == 0 && g_arb == 4, "signal type 2, 2 waiters, count 0: value %d (want 4)", (int32_t)g_arb);
|
||||
join(c, hd);
|
||||
join(c, he);
|
||||
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0 && jd.r == 0 && je.r == 0, "wait type 2 results %#x %#x %#x %#x %#x", ja.r,
|
||||
jb.r, jc.r, jd.r, je.r);
|
||||
g_arb = 5;
|
||||
CHECK(sig(c, 2, 7, 1) == KERN_R_BAD_STATE, "signal type 2 with a different value");
|
||||
// timeout, invalid type
|
||||
uint64_t t0 = kern_now_ns();
|
||||
@@ -321,11 +329,17 @@ static void test_arbiter(Cpu *c) {
|
||||
}
|
||||
|
||||
// ---- events
|
||||
typedef struct { uint32_t h, r, idx; int64_t timeout; volatile int delay, go; } EvJob;
|
||||
// pre: a first wait on h0 with timeout t0 (result r0, index idx0) before the wait on h.
|
||||
typedef struct { uint32_t h, r, idx, h0, r0, idx0; int64_t timeout, t0; volatile int delay, go; int pre; } EvJob;
|
||||
|
||||
static void event_waiter(Cpu *c) {
|
||||
EvJob *j = arg_p(c, 0);
|
||||
while (j->delay && !j->go) sleep_ms(c, 1);
|
||||
if (j->pre) {
|
||||
uint32_t h0[1] = {j->h0};
|
||||
j->r0 = sv(c, 0x18, 0, ga(h0), 1, (uint64_t)j->t0);
|
||||
j->idx0 = (uint32_t)c->x[1];
|
||||
}
|
||||
uint32_t hs[1] = {j->h};
|
||||
j->r = sv(c, 0x18, 0, ga(hs), 1, (uint64_t)j->timeout);
|
||||
j->idx = (uint32_t)c->x[1];
|
||||
@@ -376,6 +390,26 @@ static void test_events(Cpu *c) {
|
||||
join(c, h);
|
||||
CHECK(g.r == KERN_R_CANCELLED, "wait after a cancel: %#x", g.r);
|
||||
|
||||
// A pending cancel is for the next WaitSynchronization that would block (mesosphere KSynchronizationObject::Wait:
|
||||
// signalled objects first, then timeout 0, then the cancel): a poll times out and a wait on a signalled object
|
||||
// succeeds, both leaving it pending; the next blocking wait ends cancelled. The blocking waits have a 500 ms
|
||||
// timeout, so a cancel used up too early shows as a timeout instead of a hang.
|
||||
CHECK(sv(c, 0x11, hw2, 0, 0, 0) == 0, "SignalEvent of the second event");
|
||||
EvJob p = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr, .t0 = 0};
|
||||
EvJob q = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr2, .t0 = -1};
|
||||
EvJob *pq[2] = {&p, &q};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
h = create(c, a_event_waiter, pq[i], 44, -2);
|
||||
start(c, h);
|
||||
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization before the %s", i ? "signalled wait" : "poll");
|
||||
pq[i]->go = 1;
|
||||
join(c, h);
|
||||
}
|
||||
CHECK(p.r0 == KERN_R_TIMEOUT, "poll (timeout 0) with a cancel pending: %#x (want timeout)", p.r0);
|
||||
CHECK(p.r == KERN_R_CANCELLED, "blocking wait after the poll: %#x (want cancelled)", p.r);
|
||||
CHECK(q.r0 == 0 && q.idx0 == 0, "wait on a signalled event with a cancel pending: %#x (want success)", q.r0);
|
||||
CHECK(q.r == KERN_R_CANCELLED, "blocking wait after the signalled wait: %#x (want cancelled)", q.r);
|
||||
|
||||
uint32_t sw = kern_event_new(), sr = kern_event_readable(sw);
|
||||
CHECK(wait1(c, sr, 0) == KERN_R_TIMEOUT, "service event before the signal");
|
||||
kern_event_signal(sw);
|
||||
@@ -383,23 +417,24 @@ static void test_events(Cpu *c) {
|
||||
uint32_t handles[] = {sr, sw, hr2, hw2, hr, hw};
|
||||
for (unsigned i = 0; i < 6; i++) CHECK(sv(c, 0x16, handles[i], 0, 0, 0) == 0, "CloseHandle %#x", handles[i]);
|
||||
CHECK(sv(c, 0x16, hr, 0, 0, 0) == KERN_R_BAD_HANDLE, "CloseHandle twice");
|
||||
printf("events: signal, clear, reset, two handles, cancel while waiting and before, service events, 10 ms "
|
||||
printf("events: signal, clear, reset, two handles, cancel while waiting and before, a pending cancel kept by a poll "
|
||||
"and a signalled wait, service events, 10 ms "
|
||||
"timeout after %.2f ms: %s\n", ms, failures == before ? "ok" : "FAIL");
|
||||
}
|
||||
|
||||
// ---- virtual cores: count the threads running guest code at once, and that no two of them share a core number
|
||||
static volatile long g_running, g_run_max, g_core_use[3], g_core_clash;
|
||||
static volatile int32_t g_running, g_run_max, g_core_use[3], g_core_clash;
|
||||
|
||||
static void core_worker(Cpu *c) {
|
||||
for (int i = 0; i < CORE_RUNS; i++) {
|
||||
uint32_t core = sv(c, 0x10, 0, 0, 0, 0);
|
||||
if (core > 2 || InterlockedIncrement(&g_core_use[core > 2 ? 0 : core]) != 1) InterlockedIncrement(&g_core_clash);
|
||||
long n = InterlockedIncrement(&g_running), m;
|
||||
while (n > (m = g_run_max) && InterlockedCompareExchange(&g_run_max, n, m) != m) {}
|
||||
if (core > 2 || plat_inc32(&g_core_use[core > 2 ? 0 : core]) != 1) plat_inc32(&g_core_clash);
|
||||
long n = plat_inc32(&g_running), m;
|
||||
while (n > (m = g_run_max) && plat_cas32(&g_run_max, n, m) != m) {}
|
||||
uint64_t until = kern_now_ns() + 20000;
|
||||
while (kern_now_ns() < until) {}
|
||||
InterlockedDecrement(&g_running);
|
||||
InterlockedDecrement(&g_core_use[core > 2 ? 0 : core]);
|
||||
plat_dec32(&g_running);
|
||||
plat_dec32(&g_core_use[core > 2 ? 0 : core]);
|
||||
sv(c, 0x0B, i % 8 == 7 ? 50000 : 0, 0, 0, 0);
|
||||
}
|
||||
exit_thread(c);
|
||||
@@ -416,18 +451,18 @@ static void test_cores(Cpu *c) {
|
||||
for (int i = 0; i < CORE_WORKERS; i++) join(c, h[i]);
|
||||
int now, held_max;
|
||||
kern_core_stats(&now, &held_max);
|
||||
DWORD cpus = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
|
||||
uint32_t cpus = plat_cpu_usable();
|
||||
int gate = !cfg_bool("TOTK_FREE_CORES"); // the kernel's own switch
|
||||
int ok = g_run_max <= 3 && held_max <= 3 && !g_core_clash && (cpus < 4 || g_run_max == 3);
|
||||
printf("virtual cores: %d threads x %d runs on %lu host CPUs: at most %ld running guest code at once, at most %d "
|
||||
"core holders (kernel count), %ld core-number clashes: %s\n", CORE_WORKERS, CORE_RUNS, cpus, g_run_max,
|
||||
printf("virtual cores: %d threads x %d runs on %u host CPUs: at most %d running guest code at once, at most %d "
|
||||
"core holders (kernel count), %d core-number clashes: %s\n", CORE_WORKERS, CORE_RUNS, cpus, g_run_max,
|
||||
held_max, g_core_clash, !gate ? "not checked (TOTK_FREE_CORES is set)" : ok ? "ok" : "FAIL");
|
||||
CHECK(ok || !gate, "virtual cores");
|
||||
}
|
||||
|
||||
// ---- fibers: two fibers switch to each other; each checks its callee-saved registers, sp and tpidr survive
|
||||
typedef struct { uint64_t self, other; int id; } FiberJob;
|
||||
static volatile long g_switches, g_runs[3];
|
||||
static volatile int32_t g_switches, g_runs[3];
|
||||
static volatile int g_last_fiber;
|
||||
static long g_fiber_bad, g_fiber_turns;
|
||||
|
||||
@@ -487,7 +522,7 @@ static void test_fibers(Cpu *c) {
|
||||
rt_call(c, a_fib_fin, 1, fb);
|
||||
int ok = g_switches == FIBER_SWITCHES && g_runs[1] == FIBER_SWITCHES / 2 && g_runs[2] == FIBER_SWITCHES / 2 &&
|
||||
!g_fiber_turns && !g_fiber_bad && base_ok;
|
||||
printf("fibers: %ld switches (fiber 1 %ld, fiber 2 %ld, %ld out of turn) in %.1f ms (%.3f us each), %ld register "
|
||||
printf("fibers: %d switches (fiber 1 %d, fiber 2 %d, %ld out of turn) in %.1f ms (%.3f us each), %ld register "
|
||||
"mismatches, thread context %s: %s\n", g_switches, g_runs[1], g_runs[2], g_fiber_turns, ms,
|
||||
ms * 1000.0 / (double)(g_switches ? g_switches : 1), g_fiber_bad, base_ok ? "preserved" : "CHANGED",
|
||||
ok ? "ok" : "FAIL");
|
||||
@@ -593,14 +628,14 @@ static void test_lifted_ticks(Cpu *c) {
|
||||
|
||||
// svc 0x07 ExitProcess: the path up to the final ExitProcess (logged, pipeline cache saved, logs flushed) runs with the
|
||||
// test hook in its place, which must see exit code 0; the SVC then returns to the probe.
|
||||
static volatile long exit_calls;
|
||||
static volatile int32_t exit_calls;
|
||||
static volatile unsigned exit_code = 99;
|
||||
static void exit_hook(unsigned code) { exit_code = code, exit_calls++; }
|
||||
static void test_exit_process(Cpu *c) {
|
||||
svc_test_exit_hook = exit_hook;
|
||||
sv(c, 0x07, 0, 0, 0, 0);
|
||||
svc_test_exit_hook = NULL;
|
||||
CHECK(exit_calls == 1 && exit_code == 0, "ExitProcess reached the exit %ld times with code %u", exit_calls, exit_code);
|
||||
CHECK(exit_calls == 1 && exit_code == 0, "ExitProcess reached the exit %d times with code %u", exit_calls, exit_code);
|
||||
printf("ExitProcess: exit path reached once with code %u\n", exit_code);
|
||||
}
|
||||
|
||||
|
||||
+26
-35
@@ -6,13 +6,13 @@
|
||||
//
|
||||
// The lifted C was generated from the unrelocated words of .text and .rodata and embeds absolute guest addresses, so
|
||||
// each module must sit at its layout base and relocations may only write the data segment.
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "host.h"
|
||||
#include "layout.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
// hostfn.c, for the loader only.
|
||||
uint64_t hostfn_register_observer(const char *name, GuestFn fn, uint64_t original);
|
||||
@@ -55,7 +55,7 @@ static Mod mods[MOD_COUNT];
|
||||
static int nmods;
|
||||
static uint64_t identity_refs, stubs;
|
||||
static int *ovr_used, *obs_used; // per linked table entry, plus probe overrides counted separately
|
||||
static SRWLOCK plt_lock = SRWLOCK_INIT;
|
||||
static PlatMutex plt_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
int ldr_count(void) { return nmods; }
|
||||
const RtLoaded *ldr_module(int i) { return i >= 0 && i < nmods ? &mods[i].pub : NULL; }
|
||||
@@ -64,21 +64,9 @@ static uint32_t rd32(uint64_t a) { return LD32(a); }
|
||||
|
||||
// ---- files
|
||||
static uint8_t *read_file(const char *path, size_t *len) {
|
||||
HANDLE h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (h == INVALID_HANDLE_VALUE) return NULL;
|
||||
LARGE_INTEGER size;
|
||||
uint8_t *p = NULL;
|
||||
if (GetFileSizeEx(h, &size) && size.QuadPart < (1ll << 31)) {
|
||||
p = malloc(size.QuadPart ? (size_t)size.QuadPart : 1);
|
||||
DWORD got = 0;
|
||||
if (p && ReadFile(h, p, (DWORD)size.QuadPart, &got, NULL) && got == (DWORD)size.QuadPart) {
|
||||
*len = (size_t)size.QuadPart;
|
||||
} else {
|
||||
free(p);
|
||||
p = NULL;
|
||||
}
|
||||
}
|
||||
CloseHandle(h);
|
||||
uint64_t size = 0;
|
||||
uint8_t *p = plat_file_read(path, &size);
|
||||
if (p) *len = (size_t)size;
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -293,9 +281,8 @@ static void load_module(Mod *m, const char *dir, const RtLayoutEntry *lay) {
|
||||
if (size != lay->size)
|
||||
rt_fatal("loader: %s image is %#llx bytes, layout.h says %#llx: generated C and game files differ", lay->name,
|
||||
(unsigned long long)size, (unsigned long long)lay->size);
|
||||
if (!VirtualAlloc((void *)(uintptr_t)lay->base, size, MEM_COMMIT, PAGE_READWRITE))
|
||||
rt_fatal("loader: cannot commit %s at %#llx (error %lu)", lay->name, (unsigned long long)lay->base,
|
||||
GetLastError());
|
||||
if (!plat_mem_commit(lay->base, size))
|
||||
rt_fatal("loader: cannot commit %s at %#llx", lay->name, (unsigned long long)lay->base);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if ((uint64_t)seg[i].mem_off + seg[i].size > size) rt_fatal("loader: %s segment %d exceeds the image", lay->name, i);
|
||||
nso_segment(raw, &seg[i], (uint8_t *)(uintptr_t)(lay->base + seg[i].mem_off), lay->name, i);
|
||||
@@ -450,9 +437,8 @@ static void verify_identity(void) {
|
||||
|
||||
// .text and .rodata read-only: guest code never executes from guest memory, and nothing may write them.
|
||||
static void seal(Mod *m) {
|
||||
DWORD old;
|
||||
if (!VirtualProtect((void *)(uintptr_t)m->pub.base, m->pub.data_off, PAGE_READONLY, &old))
|
||||
rt_fatal("loader: cannot seal %s (error %lu)", m->pub.name, GetLastError());
|
||||
if (!plat_mem_protect(m->pub.base, m->pub.data_off, PLAT_PROT_READ))
|
||||
rt_fatal("loader: cannot seal %s", m->pub.name);
|
||||
}
|
||||
|
||||
void ldr_load(const char *exefs_dir) {
|
||||
@@ -547,9 +533,9 @@ uint64_t ldr_plt_stub(const char *module, const char *name) {
|
||||
for (int i = 0; i < nmods; i++)
|
||||
if (!strcmp(mods[i].pub.name, module)) m = &mods[i];
|
||||
if (!m) return 0;
|
||||
AcquireSRWLockExclusive(&plt_lock);
|
||||
plat_mutex_lock(&plt_lock);
|
||||
if (!m->plt_slot) find_plt(m);
|
||||
ReleaseSRWLockExclusive(&plt_lock);
|
||||
plat_mutex_unlock(&plt_lock);
|
||||
for (uint64_t j = 0; j < m->njmprel; j++) {
|
||||
uint32_t si = (uint32_t)(m->jmprel[j].info >> 32);
|
||||
if ((uint32_t)m->jmprel[j].info != R_JUMP_SLOT || strcmp(sym_name(m, si), name)) continue;
|
||||
@@ -566,8 +552,8 @@ uint64_t ldr_plt_stub(const char *module, const char *name) {
|
||||
}
|
||||
|
||||
// ---- --probe-loader
|
||||
// Expected values, measured on the 1.2.1 and 1.4.3 modules; the 1.4.3 relocation totals match the ones
|
||||
// MacNativeGames' port notes give for that version.
|
||||
// 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;
|
||||
@@ -578,22 +564,27 @@ 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_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},
|
||||
};
|
||||
static const Expect expect_143[] = {
|
||||
{"main", {499078, 1393, 28, 650}, 501149, 2071, 0, 680, {0, 16, 656}, 8, 0x10b4f10, 10717},
|
||||
{"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 : 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)
|
||||
|
||||
#define PROBE_SYMBOL "_ZN2nn2os13GetSystemTickEv"
|
||||
#define PROBE_MAGIC 0x5eed0123456789abull
|
||||
static volatile long probe_hits;
|
||||
static volatile int32_t probe_hits;
|
||||
|
||||
static void probe_override(Cpu *c) {
|
||||
InterlockedIncrement(&probe_hits);
|
||||
plat_inc32(&probe_hits);
|
||||
c->x[0] = PROBE_MAGIC;
|
||||
}
|
||||
|
||||
@@ -614,9 +605,9 @@ static void check_segments(const char *dir) {
|
||||
free(buf);
|
||||
}
|
||||
free(raw);
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
int ro = VirtualQuery((void *)(uintptr_t)m->pub.base, &mbi, sizeof mbi) && mbi.Protect == PAGE_READONLY &&
|
||||
mbi.RegionSize >= m->pub.data_off;
|
||||
uint64_t ro_end;
|
||||
int ro = plat_mem_state(m->pub.base, m->pub.base + m->pub.data_off, &ro_end) == PLAT_PROT_READ &&
|
||||
ro_end == m->pub.base + m->pub.data_off;
|
||||
printf(" %-8s text %#x+%#x rodata %#x+%#x: bytes %s the NSO, %s\n", m->pub.name, seg[0].mem_off, seg[0].size,
|
||||
seg[1].mem_off, seg[1].size, same ? "equal" : "DIFFER FROM", ro ? "read-only" : "NOT READ-ONLY");
|
||||
CHECK(same, "%s .text/.rodata changed after relocation", m->pub.name);
|
||||
@@ -650,7 +641,7 @@ static void check_got(void) {
|
||||
CHECK(stub, "no PLT stub in main for %s", PROBE_SYMBOL);
|
||||
|
||||
hostfn_seal();
|
||||
Cpu *c = _aligned_malloc(sizeof(Cpu), 64);
|
||||
Cpu *c = plat_aligned_alloc(sizeof(Cpu), 64);
|
||||
memset(c, 0, sizeof *c);
|
||||
uint8_t *stack = mem_kernel_alloc(0x10000, 16);
|
||||
c->sp = (uint64_t)(uintptr_t)stack + 0x10000;
|
||||
@@ -666,7 +657,7 @@ static void check_got(void) {
|
||||
CHECK(r2 == PROBE_MAGIC && h2 == 2, "the lifted PLT stub did not reach the host function");
|
||||
CHECK(c->x[30] == 0 && c->pc == 0 && c->x[19] == 0x1919, "rt_call did not restore x30/pc or lost x19");
|
||||
mem_kernel_free(stack, 0x10000);
|
||||
_aligned_free(c);
|
||||
plat_aligned_free(c);
|
||||
}
|
||||
|
||||
int probe_loader(void) {
|
||||
|
||||
+78
-103
@@ -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
|
||||
@@ -18,11 +19,7 @@
|
||||
// Also here: the runtime services the generated code calls besides rt_dispatch and rt_svc (rt_brk, rt_udf,
|
||||
// rt_ticks, the function profiler hook), logging, SHA-256, and the crash reporter (a vectored exception handler that
|
||||
// names the lifted function, the guest registers and the guest frame-pointer chain).
|
||||
#include <windows.h>
|
||||
#include <bcrypt.h>
|
||||
#include <limits.h>
|
||||
#include <psapi.h>
|
||||
#include <timeapi.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -30,6 +27,7 @@
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
#include "sha256.h"
|
||||
|
||||
#define MAX_MODULES 16
|
||||
|
||||
@@ -46,7 +44,7 @@ unsigned rt_trace;
|
||||
enum { LOG_EARLY, LOG_FILE, LOG_OFF };
|
||||
static int g_log_state;
|
||||
static PlatAppend *g_log_file;
|
||||
static char g_log_path[MAX_PATH * 3];
|
||||
static char g_log_path[PLAT_PATH_MAX * 3];
|
||||
static char g_log_early[32768];
|
||||
static size_t g_log_early_n;
|
||||
static uint64_t g_log_bytes;
|
||||
@@ -100,23 +98,23 @@ const char *rt_log_path(void) {
|
||||
// Opens the log file for a game run (game = 1) and writes the buffered lines to it; anything else drops them. Runs on
|
||||
// the main thread before the boot starts other threads.
|
||||
static void log_open(int game) {
|
||||
char dir[MAX_PATH * 3], from[MAX_PATH * 3 + 16], to[MAX_PATH * 3 + 16];
|
||||
char dir[PLAT_PATH_MAX * 3], from[PLAT_PATH_MAX * 3 + 16], to[PLAT_PATH_MAX * 3 + 16];
|
||||
char *cut = NULL;
|
||||
if (game && !plat_stderr_path(dir, sizeof dir) && plat_exe_path(dir, sizeof dir)) {
|
||||
cut = strrchr(dir, '\\');
|
||||
cut = plat_last_sep(dir);
|
||||
char *f = strrchr(dir, '/');
|
||||
if (f > cut) cut = f;
|
||||
}
|
||||
if (cut && (size_t)(cut - dir) + sizeof "\\logs" <= sizeof dir) {
|
||||
strcpy(cut, "\\logs");
|
||||
if (cut && (size_t)(cut - dir) + sizeof "/logs" <= sizeof dir) {
|
||||
strcpy(cut, "/logs");
|
||||
plat_file_mkdir_p(dir);
|
||||
for (int i = LOG_KEEP - 1; i > 0; i--) { // totk.3.log -> totk.4.log, ..., totk.log -> totk.1.log
|
||||
if (i > 1) snprintf(from, sizeof from, "%s\\totk.%d.log", dir, i - 1);
|
||||
else snprintf(from, sizeof from, "%s\\totk.log", dir);
|
||||
snprintf(to, sizeof to, "%s\\totk.%d.log", dir, i);
|
||||
if (i > 1) snprintf(from, sizeof from, "%s/totk.%d.log", dir, i - 1);
|
||||
else snprintf(from, sizeof from, "%s/totk.log", dir);
|
||||
snprintf(to, sizeof to, "%s/totk.%d.log", dir, i);
|
||||
if (plat_file_exists(from)) plat_file_replace(from, to);
|
||||
}
|
||||
snprintf(g_log_path, sizeof g_log_path, "%s\\totk.log", dir);
|
||||
snprintf(g_log_path, sizeof g_log_path, "%s/totk.log", dir);
|
||||
g_log_file = plat_append_open(g_log_path);
|
||||
}
|
||||
if (!g_log_file) {
|
||||
@@ -148,29 +146,7 @@ static void parse_trace(void) {
|
||||
}
|
||||
}
|
||||
|
||||
void rt_sha256(const void *p, size_t n, uint8_t out[32]) {
|
||||
static BCRYPT_ALG_HANDLE alg;
|
||||
BCRYPT_ALG_HANDLE a = __atomic_load_n(&alg, __ATOMIC_ACQUIRE);
|
||||
if (!a) {
|
||||
if (BCryptOpenAlgorithmProvider(&a, BCRYPT_SHA256_ALGORITHM, NULL, 0)) rt_fatal("SHA-256 provider unavailable");
|
||||
BCRYPT_ALG_HANDLE expected = NULL;
|
||||
if (!__atomic_compare_exchange_n(&alg, &expected, a, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
|
||||
BCryptCloseAlgorithmProvider(a, 0);
|
||||
a = expected;
|
||||
}
|
||||
}
|
||||
BCRYPT_HASH_HANDLE h;
|
||||
if (BCryptCreateHash(a, &h, NULL, 0, NULL, 0, 0)) rt_fatal("SHA-256: BCryptCreateHash failed");
|
||||
const uint8_t *s = p;
|
||||
while (n) {
|
||||
ULONG k = n > 0x40000000 ? 0x40000000 : (ULONG)n;
|
||||
if (BCryptHashData(h, (PUCHAR)s, k, 0)) rt_fatal("SHA-256: BCryptHashData failed");
|
||||
s += k;
|
||||
n -= k;
|
||||
}
|
||||
if (BCryptFinishHash(h, out, 32, 0)) rt_fatal("SHA-256: BCryptFinishHash failed");
|
||||
BCryptDestroyHash(h);
|
||||
}
|
||||
void rt_sha256(const void *p, size_t n, uint8_t out[32]) { sha256(p, n, out); }
|
||||
|
||||
// ---- configuration
|
||||
static RtConfig cfg;
|
||||
@@ -194,17 +170,9 @@ void rt_udf(Cpu *c, uint64_t pc) {
|
||||
// CNTPCT_EL0: the Switch counter runs at 19.2 MHz. The scale is set before main, so no thread reads it half written.
|
||||
static PlatScale tick_scale;
|
||||
|
||||
__attribute__((constructor)) static void tick_scale_init(void) {
|
||||
LARGE_INTEGER f;
|
||||
QueryPerformanceFrequency(&f);
|
||||
tick_scale = plat_scale(19200000u, (uint64_t)f.QuadPart);
|
||||
}
|
||||
__attribute__((constructor)) static void tick_scale_init(void) { tick_scale = plat_scale(19200000u, plat_counter_hz()); }
|
||||
|
||||
uint64_t rt_ticks(void) {
|
||||
LARGE_INTEGER t;
|
||||
QueryPerformanceCounter(&t);
|
||||
return plat_scaled(tick_scale, (uint64_t)t.QuadPart);
|
||||
}
|
||||
uint64_t rt_ticks(void) { return plat_scaled(tick_scale, plat_counter()); }
|
||||
|
||||
// ---- crash reporter
|
||||
typedef struct { uintptr_t host; uint64_t guest; } FnRev;
|
||||
@@ -216,7 +184,8 @@ static int by_host(const void *a, const void *b) {
|
||||
return x < y ? -1 : x > y;
|
||||
}
|
||||
|
||||
// The lifted function that contains a host code address, by the nearest function start below it.
|
||||
// The lifted function that contains a host code address, by the nearest function start below it; 0 outside the lifted
|
||||
// code.
|
||||
static uint64_t lifted_function(uintptr_t rip) {
|
||||
if (!fn_rev) {
|
||||
static RtModule mods[MAX_MODULES];
|
||||
@@ -230,6 +199,9 @@ static uint64_t lifted_function(uintptr_t rip) {
|
||||
fn_rev[fn_rev_n++] = (FnRev){(uintptr_t)mods[i].fns[k], mods[i].addrs[k]};
|
||||
qsort(fn_rev, fn_rev_n, sizeof *fn_rev, by_host);
|
||||
}
|
||||
// The lifted code is one range with the host code outside it (kernel.c ap_init checks that); an address above the
|
||||
// last function start is taken for host code, which costs only the last function's body.
|
||||
if (!fn_rev_n || rip > fn_rev[fn_rev_n - 1].host) return 0;
|
||||
uint64_t lo = 0, hi = fn_rev_n;
|
||||
while (lo < hi) {
|
||||
uint64_t mid = (lo + hi) / 2;
|
||||
@@ -239,36 +211,26 @@ static uint64_t lifted_function(uintptr_t rip) {
|
||||
return lo ? fn_rev[lo - 1].guest : 0;
|
||||
}
|
||||
|
||||
static int readable(uint64_t a, uint64_t n) {
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (!VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) || mbi.State != MEM_COMMIT) return 0;
|
||||
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return 0;
|
||||
return a + n <= (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
}
|
||||
static int readable(uint64_t a, uint64_t n) { return plat_mem_readable(a, n); }
|
||||
|
||||
static LONG CALLBACK crash_handler(EXCEPTION_POINTERS *ep) {
|
||||
static volatile LONG busy;
|
||||
DWORD code = ep->ExceptionRecord->ExceptionCode;
|
||||
if (code != EXCEPTION_ACCESS_VIOLATION && code != EXCEPTION_ILLEGAL_INSTRUCTION &&
|
||||
code != EXCEPTION_INT_DIVIDE_BY_ZERO && code != EXCEPTION_PRIV_INSTRUCTION && code != EXCEPTION_STACK_OVERFLOW &&
|
||||
code != EXCEPTION_IN_PAGE_ERROR)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
uintptr_t rip = (uintptr_t)ep->ContextRecord->Rip;
|
||||
HMODULE self = GetModuleHandleW(NULL);
|
||||
MODULEINFO mi;
|
||||
if (!GetModuleInformation(GetCurrentProcess(), self, &mi, sizeof mi) || rip - (uintptr_t)mi.lpBaseOfDll >= mi.SizeOfImage)
|
||||
return EXCEPTION_CONTINUE_SEARCH; // not our code: leave it to the handler that expects it
|
||||
if (InterlockedExchange(&busy, 1)) ExitProcess(71);
|
||||
static void crash_handler(const PlatCrash *x) {
|
||||
static volatile int32_t busy;
|
||||
if (plat_xchg32(&busy, 1)) plat_exit(71);
|
||||
char d[160];
|
||||
rt_logf("FATAL: exception %#lx at host %p (totk.exe+%#llx)", code, (void *)rip,
|
||||
(unsigned long long)(rip - (uintptr_t)mi.lpBaseOfDll));
|
||||
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2)
|
||||
rt_logf(", %s address %#llx",
|
||||
ep->ExceptionRecord->ExceptionInformation[0] == 1 ? "writing" : ep->ExceptionRecord->ExceptionInformation[0] == 8 ? "executing" : "reading",
|
||||
(unsigned long long)ep->ExceptionRecord->ExceptionInformation[1]);
|
||||
uintptr_t rip = x->rip;
|
||||
rt_logf("FATAL: %s (%#x) at host %p (totk+%#llx)", x->what, x->code, (void *)rip, (unsigned long long)x->offset);
|
||||
if (x->access >= 0)
|
||||
rt_logf(", %s address %#llx", x->access == 1 ? "writing" : x->access == 8 ? "executing" : "reading",
|
||||
(unsigned long long)x->addr);
|
||||
rt_log_raw("\n", 1);
|
||||
uint64_t f = lifted_function(rip);
|
||||
if (f) rt_logf(" in lifted function f_%llx = %s\n", (unsigned long long)f, ldr_describe(f, d, sizeof d));
|
||||
for (int i = 0; i < x->nframes; i++) { // abort and invalid parameter: where the host code called it
|
||||
uint64_t g = lifted_function(x->frames[i]);
|
||||
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - (rip - x->offset)));
|
||||
if (g) rt_logf(" in lifted function f_%llx", (unsigned long long)g);
|
||||
rt_log_raw("\n", 1);
|
||||
}
|
||||
Cpu *c = kern_cpu();
|
||||
if (c) {
|
||||
for (int i = 0; i < 31; i++)
|
||||
@@ -285,29 +247,36 @@ static LONG CALLBACK crash_handler(EXCEPTION_POINTERS *ep) {
|
||||
} else {
|
||||
rt_logf(" not on a guest thread\n");
|
||||
}
|
||||
kern_free_cores_note();
|
||||
fflush(stderr);
|
||||
ExitProcess(71);
|
||||
plat_exit(71);
|
||||
}
|
||||
|
||||
// ---- self-test: layout, dispatch table, host-function registry, without game files
|
||||
static int failures;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
|
||||
|
||||
static double now_s(void) {
|
||||
LARGE_INTEGER t, f;
|
||||
QueryPerformanceCounter(&t);
|
||||
QueryPerformanceFrequency(&f);
|
||||
return (double)t.QuadPart / (double)f.QuadPart;
|
||||
}
|
||||
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.
|
||||
@@ -325,9 +294,8 @@ static void check_layout(void) {
|
||||
CHECK(lay[i].base > lay[i - 1].base, "%s base is not above %s", lay[i].name, lay[i - 1].name);
|
||||
CHECK(lay[i - 1].base + lay[i - 1].size <= lay[i].base, "%s overlaps %s", lay[i - 1].name, lay[i].name);
|
||||
}
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
int ours = VirtualQuery((void *)(uintptr_t)lay[i].base, &mbi, sizeof mbi) && mbi.State == MEM_RESERVE &&
|
||||
(uint64_t)(uintptr_t)mbi.AllocationBase <= lay[i].base;
|
||||
uint64_t end;
|
||||
int ours = plat_mem_state(lay[i].base, lay[i].base + 1, &end) == PLAT_MEM_RESERVED;
|
||||
CHECK(ours, "%s is not inside the reserved module window", lay[i].name);
|
||||
}
|
||||
const RtGuestLayout *g = mem_layout();
|
||||
@@ -447,14 +415,14 @@ static int selftest(void) {
|
||||
(double)s.table_bytes / (1 << 20), dt * 1e3);
|
||||
check_modules(mods, n);
|
||||
CHECK(s.functions > 0, "no functions linked");
|
||||
Cpu *c = _aligned_malloc(sizeof(Cpu), 64);
|
||||
Cpu *c = plat_aligned_alloc(sizeof(Cpu), 64);
|
||||
memset(c, 0, sizeof *c);
|
||||
uint64_t r = rt_call(c, probe, 1, (uint64_t)7);
|
||||
CHECK(rt_dispatch_lookup(probe) && r == 42 && host_probe_hits == 1 && c->x[30] == 0,
|
||||
"a registered host function does not dispatch through rt_call");
|
||||
CHECK(!rt_dispatch_lookup(probe + 16), "an unregistered host-function address dispatches");
|
||||
CHECK(rt_dispatch_lookup(RT_RET_HOST), "the rt_call return sentinel is not registered");
|
||||
_aligned_free(c);
|
||||
plat_aligned_free(c);
|
||||
bench(mods, n);
|
||||
|
||||
void *k1 = mem_kernel_alloc(0x200, 16), *k2 = mem_kernel_alloc(0x2000, 0x1000);
|
||||
@@ -464,25 +432,25 @@ static int selftest(void) {
|
||||
CHECK(mem_kernel_alloc(0x200, 16) == k1, "a freed kernel-region block is not reused");
|
||||
check_memory();
|
||||
|
||||
PROCESS_MEMORY_COUNTERS pmc = {.cb = sizeof pmc};
|
||||
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc))
|
||||
printf("process: peak working set %.1f MiB\n", (double)pmc.PeakWorkingSetSize / (1 << 20));
|
||||
float used, total, proc;
|
||||
plat_mem_info(&used, &total, &proc);
|
||||
if (proc >= 0) printf("process: working set %.0f MiB\n", proc);
|
||||
printf("selftest: %s (%d failures)\n", failures ? "FAILED" : "passed", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Paths for a copied folder (tools/package_build.py). Per path: TOTK_EXEFS, TOTK_ROMFS, TOTK_PROFILE_DIR, else the
|
||||
// folder next to totk.exe (exefs, romfs, profile) if it exists, else the path baked in at build time. A relative
|
||||
// value is relative to totk.exe's folder.
|
||||
static char pp_exefs[MAX_PATH * 2], pp_romfs[MAX_PATH * 2], pp_profile[MAX_PATH * 2];
|
||||
// folder next to totk.exe (exefs with main.npdm, romfs with romfs.bin, any profile folder) if it is there, else the
|
||||
// path baked in at build time. A relative value is relative to totk.exe's folder.
|
||||
static char pp_exefs[PLAT_PATH_MAX * 2], pp_romfs[PLAT_PATH_MAX * 2], pp_profile[PLAT_PATH_MAX * 2];
|
||||
|
||||
static const char *portable_path(const char *dir, const char *var, const char *sub, const char *must, char *out, size_t cap,
|
||||
const char *fallback) {
|
||||
@@ -490,36 +458,40 @@ static const char *portable_path(const char *dir, const char *var, const char *s
|
||||
if (e && *e) {
|
||||
int abs_path = e[0] == '\\' || e[0] == '/' || e[1] == ':';
|
||||
if (abs_path) snprintf(out, cap, "%s", e);
|
||||
else snprintf(out, cap, "%s\\%s", dir, e);
|
||||
else snprintf(out, cap, "%s/%s", dir, e);
|
||||
return out;
|
||||
}
|
||||
char probe[MAX_PATH * 3];
|
||||
snprintf(out, cap, "%s\\%s", dir, sub);
|
||||
snprintf(probe, sizeof probe, "%s\\%s", out, must);
|
||||
return GetFileAttributesA(probe) != INVALID_FILE_ATTRIBUTES ? out : fallback;
|
||||
char probe[PLAT_PATH_MAX * 3];
|
||||
snprintf(out, cap, "%s/%s", dir, sub);
|
||||
if (!must) return plat_dir_exists(out) ? out : fallback; // the folder itself is the sign
|
||||
snprintf(probe, sizeof probe, "%s/%s", out, must);
|
||||
return plat_file_exists(probe) ? out : fallback;
|
||||
}
|
||||
|
||||
static void portable_paths(RtConfig *c) {
|
||||
char exe[MAX_PATH * 2];
|
||||
DWORD n = GetModuleFileNameA(NULL, exe, sizeof exe);
|
||||
char *cut = n && n < sizeof exe ? strrchr(exe, '\\') : NULL;
|
||||
char exe[PLAT_PATH_MAX * 2];
|
||||
int n = plat_exe_path(exe, sizeof exe);
|
||||
char *cut = n && n < sizeof exe ? plat_last_sep(exe) : NULL;
|
||||
if (!cut) return;
|
||||
*cut = 0;
|
||||
c->exefs = portable_path(exe, "TOTK_EXEFS", "exefs", "main.npdm", pp_exefs, sizeof pp_exefs, c->exefs);
|
||||
c->romfs = portable_path(exe, "TOTK_ROMFS", "romfs", "romfs.bin", pp_romfs, sizeof pp_romfs, c->romfs);
|
||||
c->profile = portable_path(exe, "TOTK_PROFILE_DIR", "profile", ".", pp_profile, sizeof pp_profile, c->profile);
|
||||
c->profile = portable_path(exe, "TOTK_PROFILE_DIR", "profile", NULL, pp_profile, sizeof pp_profile, c->profile);
|
||||
}
|
||||
|
||||
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);
|
||||
AddVectoredExceptionHandler(0, crash_handler);
|
||||
plat_crash_hook(crash_handler);
|
||||
parse_trace();
|
||||
prof_start();
|
||||
// Timed guest waits (kernel.c block_inner) sleep in whole milliseconds; at the default 15.6 ms tick a 1 ms wait
|
||||
// costs a full tick. TOTK_TIMER_1MS=0 keeps the default for comparison.
|
||||
if (cfg_bool("TOTK_TIMER_1MS")) timeBeginPeriod(1);
|
||||
if (cfg_bool("TOTK_TIMER_1MS")) plat_timer_1ms();
|
||||
cfg = (RtConfig){.exefs = rt_default_exefs, .romfs = rt_default_romfs, .profile = rt_default_profile,
|
||||
.gpu = cfg_get("TOTK_GPU")};
|
||||
portable_paths(&cfg); // command-line paths below still win
|
||||
@@ -560,7 +532,10 @@ int main(int argc, char **argv) {
|
||||
if (!strcmp(mode, "--selftest")) return selftest();
|
||||
if (!strcmp(mode, "--probe-loader")) return probe_loader();
|
||||
if (!strcmp(mode, "--probe-cfg")) return probe_cfg();
|
||||
if (!strcmp(mode, "--probe-dpi")) return probe_dpi();
|
||||
#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();
|
||||
|
||||
+90
-139
@@ -6,9 +6,8 @@
|
||||
//
|
||||
// Every region is a plain reservation; pages are committed and decommitted at 4 KiB granularity. Decommitted pages
|
||||
// come back zeroed, so no stale-page bookkeeping is needed. The stack and heap block and the alias region are
|
||||
// reserved with MEM_WRITE_WATCH: mem_written_pages reports the pages the CPU wrote, which
|
||||
// reserved write-watched (platform.h plat_mem_reserve): mem_written_pages reports the pages the CPU wrote, which
|
||||
// the Vulkan back end uploads again.
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -16,6 +15,7 @@
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "layout.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define PAGE 0x1000ull
|
||||
#define GiB (1ull << 30)
|
||||
@@ -48,7 +48,7 @@ enum { ATTR_PERMISSION_LOCKED = 16 };
|
||||
|
||||
static RtGuestLayout L;
|
||||
static uint64_t mods_lo, mods_hi; // fixed module window
|
||||
static SRWLOCK lock = SRWLOCK_INIT;
|
||||
static PlatMutex lock = PLAT_MUTEX_INIT;
|
||||
static uint64_t heap_size, alias_bytes;
|
||||
static uint8_t *alias_pages; // 1 per mapped 4 KiB page of the alias region
|
||||
typedef struct { uint64_t dst, src, size; } StackMap;
|
||||
@@ -58,14 +58,7 @@ static uint8_t *module_perm[MOD_COUNT]; // per data page of each module: perm
|
||||
|
||||
static uint64_t align_up(uint64_t v, uint64_t a) { return (v + a - 1) & ~(a - 1); }
|
||||
|
||||
static void *reserve_at(uint64_t at, uint64_t size, DWORD extra) {
|
||||
void *p = VirtualAlloc((void *)(uintptr_t)at, size, MEM_RESERVE | extra, PAGE_NOACCESS);
|
||||
if (p && p != (void *)(uintptr_t)at) {
|
||||
VirtualFree(p, 0, MEM_RELEASE);
|
||||
p = NULL;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
static int reserve_at(uint64_t at, uint64_t size, int watch) { return plat_mem_reserve(at, size, watch); }
|
||||
|
||||
// ---- frozen regions
|
||||
// A 2 MiB region of the watched reservations that nothing wrote for a while can be made read-only (mem_freeze), so the
|
||||
@@ -79,9 +72,9 @@ static void *reserve_at(uint64_t at, uint64_t size, DWORD extra) {
|
||||
#define FRZ_HOLD_MS 5000u
|
||||
static volatile uint8_t frz[RT_ADDRESS_LIMIT >> FRZ_SHIFT];
|
||||
static uint32_t thaw_ms[RT_ADDRESS_LIMIT >> FRZ_SHIFT]; // frz_lock: GetTickCount of the last thaw request, 0 never
|
||||
static volatile LONG nfrz;
|
||||
static SRWLOCK frz_lock = SRWLOCK_INIT;
|
||||
static volatile LONG64 frz_count[4]; // freezes; thaws by a write fault, by mem_thaw, by a mapping change
|
||||
static volatile int32_t nfrz;
|
||||
static PlatMutex frz_lock = PLAT_MUTEX_INIT;
|
||||
static volatile int64_t frz_count[4]; // freezes; thaws by a write fault, by mem_thaw, by a mapping change
|
||||
|
||||
static int watched(uint64_t addr, uint64_t size) {
|
||||
const RtGuestLayout *g = &L;
|
||||
@@ -91,27 +84,18 @@ static int watched(uint64_t addr, uint64_t size) {
|
||||
|
||||
// frz_lock held exclusively. why: 1 a write fault, 2 mem_thaw (holds the region thawed), 3 a mapping change here.
|
||||
static void thaw_locked(uint64_t r, int why) {
|
||||
if (why == 2) thaw_ms[r] = (uint32_t)GetTickCount64() | 1;
|
||||
if (why == 2) thaw_ms[r] = (uint32_t)plat_now_ms() | 1;
|
||||
if (!frz[r]) return;
|
||||
DWORD old;
|
||||
uint64_t a = r << FRZ_SHIFT, end = a + FRZ_SIZE;
|
||||
if (!VirtualProtect((void *)(uintptr_t)a, FRZ_SIZE, PAGE_READWRITE, &old)) {
|
||||
// Not all committed (freeze_locked left reserved holes): the read-only runs only. Every committed page of a
|
||||
// frozen region was read-write at the freeze, and mapping changes thaw before they run, so these are all ours.
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
while (a < end && VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi)) {
|
||||
uint64_t b = (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (b > end) b = end;
|
||||
if (mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READONLY &&
|
||||
!VirtualProtect((void *)(uintptr_t)a, b - a, PAGE_READWRITE, &old))
|
||||
rt_fatal("memory: thawing %#llx+%#llx failed (error %lu)", (unsigned long long)a,
|
||||
(unsigned long long)(b - a), GetLastError());
|
||||
a = b;
|
||||
}
|
||||
}
|
||||
// The read-only runs only: freeze_locked left reserved holes (heap pages given back) as they were. Every committed
|
||||
// page of a frozen region was read-write at the freeze, and mapping changes thaw before they run, so these are all
|
||||
// ours.
|
||||
uint64_t a = r << FRZ_SHIFT, end = a + FRZ_SIZE, b;
|
||||
for (; a < end; a = b)
|
||||
if (plat_mem_state(a, end, &b) == PLAT_PROT_READ && !plat_mem_protect(a, b - a, PLAT_PROT_RW))
|
||||
rt_fatal("memory: thawing %#llx+%#llx failed", (unsigned long long)a, (unsigned long long)(b - a));
|
||||
__atomic_store_n(&frz[r], 0, __ATOMIC_RELEASE);
|
||||
InterlockedDecrement(&nfrz);
|
||||
InterlockedIncrement64(&frz_count[why]);
|
||||
plat_dec32(&nfrz);
|
||||
plat_inc64(&frz_count[why]);
|
||||
}
|
||||
|
||||
// frz_lock held exclusively.
|
||||
@@ -122,9 +106,9 @@ static void thaw_range_locked(uint64_t a, uint64_t n, int why) {
|
||||
}
|
||||
|
||||
void mem_thaw(uint64_t a, uint64_t n) {
|
||||
AcquireSRWLockExclusive(&frz_lock); // also when nothing is frozen: the hold keeps a read's buffer writable
|
||||
plat_mutex_lock(&frz_lock); // also when nothing is frozen: the hold keeps a read's buffer writable
|
||||
thaw_range_locked(a, n, 2);
|
||||
ReleaseSRWLockExclusive(&frz_lock);
|
||||
plat_mutex_unlock(&frz_lock);
|
||||
}
|
||||
|
||||
// frz_lock held exclusively. Makes the committed pages of region r read-only when every one of them is read-write;
|
||||
@@ -132,21 +116,16 @@ void mem_thaw(uint64_t a, uint64_t n) {
|
||||
static int freeze_locked(uint64_t r) {
|
||||
uint64_t a = r << FRZ_SHIFT, end = a + FRZ_SIZE, run[16][2];
|
||||
int n = 0;
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
for (uint64_t x = a; x < end; ) {
|
||||
if (!VirtualQuery((void *)(uintptr_t)x, &mbi, sizeof mbi)) return 0;
|
||||
uint64_t y = (uint64_t)(uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (y > end) y = end;
|
||||
if (mbi.State == MEM_COMMIT) {
|
||||
if (mbi.Protect != PAGE_READWRITE || n == 16) return 0;
|
||||
for (uint64_t x = a, y; x < end; x = y) {
|
||||
int st = plat_mem_state(x, end, &y);
|
||||
if (st >= PLAT_PROT_NONE) {
|
||||
if (st != PLAT_PROT_RW || n == 16) return 0;
|
||||
run[n][0] = x, run[n][1] = y, n++;
|
||||
} else if (mbi.State != MEM_RESERVE) return 0;
|
||||
x = y;
|
||||
} else if (st != PLAT_MEM_RESERVED) return 0;
|
||||
}
|
||||
DWORD old;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!VirtualProtect((void *)(uintptr_t)run[i][0], run[i][1] - run[i][0], PAGE_READONLY, &old)) {
|
||||
while (i--) VirtualProtect((void *)(uintptr_t)run[i][0], run[i][1] - run[i][0], PAGE_READWRITE, &old);
|
||||
if (!plat_mem_protect(run[i][0], run[i][1] - run[i][0], PLAT_PROT_READ)) {
|
||||
while (i--) plat_mem_protect(run[i][0], run[i][1] - run[i][0], PLAT_PROT_RW);
|
||||
return 0;
|
||||
}
|
||||
return n > 0;
|
||||
@@ -156,16 +135,16 @@ int mem_freeze(uint64_t a) {
|
||||
if ((a & (FRZ_SIZE - 1)) || !watched(a, FRZ_SIZE)) return 0;
|
||||
uint64_t r = a >> FRZ_SHIFT;
|
||||
int ok = 0;
|
||||
AcquireSRWLockExclusive(&frz_lock);
|
||||
plat_mutex_lock(&frz_lock);
|
||||
if (frz[r]) ok = 1;
|
||||
else if (thaw_ms[r] && (int32_t)((uint32_t)GetTickCount64() - thaw_ms[r]) < (int32_t)FRZ_HOLD_MS) ok = 0; // |1 may be ahead
|
||||
else if (thaw_ms[r] && (int32_t)((uint32_t)plat_now_ms() - thaw_ms[r]) < (int32_t)FRZ_HOLD_MS) ok = 0; // |1 may be ahead
|
||||
else if (freeze_locked(r)) {
|
||||
__atomic_store_n(&frz[r], 1, __ATOMIC_RELEASE);
|
||||
InterlockedIncrement(&nfrz);
|
||||
InterlockedIncrement64(&frz_count[0]);
|
||||
plat_inc32(&nfrz);
|
||||
plat_inc64(&frz_count[0]);
|
||||
ok = 1;
|
||||
}
|
||||
ReleaseSRWLockExclusive(&frz_lock);
|
||||
plat_mutex_unlock(&frz_lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -173,83 +152,60 @@ int mem_frozen(uint64_t a) { return a < RT_ADDRESS_LIMIT && __atomic_load_n(&frz
|
||||
|
||||
void mem_freeze_stats(uint64_t *frozen, uint64_t *freezes, uint64_t *fault_thaws, uint64_t *other_thaws) {
|
||||
*frozen = (uint64_t)__atomic_load_n(&nfrz, __ATOMIC_RELAXED);
|
||||
*freezes = (uint64_t)InterlockedExchange64(&frz_count[0], 0);
|
||||
*fault_thaws = (uint64_t)InterlockedExchange64(&frz_count[1], 0);
|
||||
*other_thaws = (uint64_t)InterlockedExchange64(&frz_count[2], 0) + (uint64_t)InterlockedExchange64(&frz_count[3], 0);
|
||||
*freezes = (uint64_t)plat_xchg64(&frz_count[0], 0);
|
||||
*fault_thaws = (uint64_t)plat_xchg64(&frz_count[1], 0);
|
||||
*other_thaws = (uint64_t)plat_xchg64(&frz_count[2], 0) + (uint64_t)plat_xchg64(&frz_count[3], 0);
|
||||
}
|
||||
|
||||
// The first write into a frozen region thaws it and retries. A fault in a region another thread thawed meanwhile
|
||||
// retries too; any other fault goes on to the crash handler.
|
||||
static LONG CALLBACK thaw_handler(EXCEPTION_POINTERS *ep) {
|
||||
const EXCEPTION_RECORD *er = ep->ExceptionRecord;
|
||||
if (er->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || er->NumberParameters < 2 || er->ExceptionInformation[0] != 1)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
uint64_t a = er->ExceptionInformation[1];
|
||||
if (a >= RT_ADDRESS_LIMIT || !watched(a, 1)) return EXCEPTION_CONTINUE_SEARCH;
|
||||
AcquireSRWLockExclusive(&frz_lock);
|
||||
// The platform's write-fault hook (plat_mem_fault_hook): 1 retries the write.
|
||||
static int thaw_fault(uint64_t a) {
|
||||
if (a >= RT_ADDRESS_LIMIT || !watched(a, 1)) return 0;
|
||||
plat_mutex_lock(&frz_lock);
|
||||
int was = frz[a >> FRZ_SHIFT];
|
||||
thaw_locked(a >> FRZ_SHIFT, 1);
|
||||
ReleaseSRWLockExclusive(&frz_lock);
|
||||
if (was) return EXCEPTION_CONTINUE_EXECUTION;
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (VirtualQuery((void *)(uintptr_t)a, &mbi, sizeof mbi) && mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READWRITE)
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
plat_mutex_unlock(&frz_lock);
|
||||
if (was) return 1;
|
||||
uint64_t end;
|
||||
return plat_mem_state(a & ~(PAGE - 1), (a & ~(PAGE - 1)) + PAGE, &end) == PLAT_PROT_RW;
|
||||
}
|
||||
|
||||
// Mapping changes thaw the regions they touch and run under frz_lock, so no freeze sees them half done.
|
||||
static void commit(uint64_t a, uint64_t n, const char *what) {
|
||||
AcquireSRWLockExclusive(&frz_lock);
|
||||
plat_mutex_lock(&frz_lock);
|
||||
thaw_range_locked(a, n, 3);
|
||||
int ok = !n || VirtualAlloc((void *)(uintptr_t)a, n, MEM_COMMIT, PAGE_READWRITE);
|
||||
ReleaseSRWLockExclusive(&frz_lock);
|
||||
if (!ok)
|
||||
rt_fatal("memory: commit %s %#llx+%#llx failed (error %lu)", what, (unsigned long long)a,
|
||||
(unsigned long long)n, GetLastError());
|
||||
int ok = !n || plat_mem_commit(a, n);
|
||||
plat_mutex_unlock(&frz_lock);
|
||||
if (!ok) rt_fatal("memory: commit %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
|
||||
}
|
||||
|
||||
static void decommit(uint64_t a, uint64_t n, const char *what) {
|
||||
AcquireSRWLockExclusive(&frz_lock);
|
||||
plat_mutex_lock(&frz_lock);
|
||||
thaw_range_locked(a, n, 3);
|
||||
int ok = !n || VirtualFree((void *)(uintptr_t)a, n, MEM_DECOMMIT);
|
||||
ReleaseSRWLockExclusive(&frz_lock);
|
||||
if (!ok)
|
||||
rt_fatal("memory: decommit %s %#llx+%#llx failed (error %lu)", what, (unsigned long long)a,
|
||||
(unsigned long long)n, GetLastError());
|
||||
int ok = !n || plat_mem_decommit(a, n);
|
||||
plat_mutex_unlock(&frz_lock);
|
||||
if (!ok) rt_fatal("memory: decommit %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
|
||||
}
|
||||
|
||||
static void protect(uint64_t a, uint64_t n, DWORD prot, const char *what) {
|
||||
DWORD old;
|
||||
AcquireSRWLockExclusive(&frz_lock);
|
||||
static void protect(uint64_t a, uint64_t n, int prot, const char *what) {
|
||||
plat_mutex_lock(&frz_lock);
|
||||
thaw_range_locked(a, n, 3);
|
||||
int ok = !n || VirtualProtect((void *)(uintptr_t)a, n, prot, &old);
|
||||
ReleaseSRWLockExclusive(&frz_lock);
|
||||
if (!ok)
|
||||
rt_fatal("memory: protect %s %#llx+%#llx failed (error %lu)", what, (unsigned long long)a,
|
||||
(unsigned long long)n, GetLastError());
|
||||
int ok = !n || plat_mem_protect(a, n, prot);
|
||||
plat_mutex_unlock(&frz_lock);
|
||||
if (!ok) rt_fatal("memory: protect %s %#llx+%#llx failed", what, (unsigned long long)a, (unsigned long long)n);
|
||||
}
|
||||
|
||||
// The process is re-run when a region is taken: Windows places DLLs, heaps and thread stacks at random addresses,
|
||||
// and a fresh process gets a fresh layout. The parent waits and exits with the child's code.
|
||||
// and a fresh process gets a fresh layout.
|
||||
static void reexec_or_fail(const char *what) {
|
||||
const char *e = cfg_get("TOTK_REEXEC");
|
||||
int n = e ? atoi(e) : 0;
|
||||
if (n >= REEXEC_LIMIT) rt_fatal("memory: %s is not free after %d attempts", what, n);
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof buf, "%d", n + 1);
|
||||
SetEnvironmentVariableA("TOTK_REEXEC", buf);
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
STARTUPINFOW si = {.cb = sizeof si};
|
||||
PROCESS_INFORMATION pi;
|
||||
wchar_t *cmd = _wcsdup(GetCommandLineW()); // CreateProcessW may write its command line
|
||||
if (!cmd || !CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
|
||||
rt_fatal("memory: %s is not free and re-running the process failed (error %lu)", what, GetLastError());
|
||||
CloseHandle(pi.hThread);
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
DWORD code = 70;
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
ExitProcess(code);
|
||||
plat_setenv("TOTK_REEXEC", buf);
|
||||
plat_reexec();
|
||||
}
|
||||
|
||||
void mem_init(void) {
|
||||
@@ -266,14 +222,14 @@ void mem_init(void) {
|
||||
// Stack and heap block, then the alias region above it, both searched below the module window.
|
||||
uint64_t low = 0, alias = 0;
|
||||
for (uint64_t at = LOW_SEARCH_FROM; at + LOW_SIZE <= mods_lo && !low; at += GiB)
|
||||
if (reserve_at(at, LOW_SIZE, MEM_WRITE_WATCH)) low = at;
|
||||
if (reserve_at(at, LOW_SIZE, 1)) low = at;
|
||||
if (!low) reexec_or_fail("a 6 GiB stack and heap block below 2^39");
|
||||
for (uint64_t at = align_up(low + LOW_SIZE, ALIAS_ALIGN); at + ALIAS_SIZE <= mods_lo && !alias; at += ALIAS_ALIGN)
|
||||
if (reserve_at(at, ALIAS_SIZE, MEM_WRITE_WATCH)) alias = at;
|
||||
if (reserve_at(at, ALIAS_SIZE, 1)) alias = at;
|
||||
if (!alias) reexec_or_fail("a 64 GiB alias region below 2^39");
|
||||
|
||||
AddVectoredExceptionHandler(1, thaw_handler); // before the crash handler
|
||||
alias_pages = VirtualAlloc(NULL, ALIAS_PAGES, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
plat_mem_fault_hook(thaw_fault); // before the crash handler
|
||||
alias_pages = calloc(ALIAS_PAGES, 1);
|
||||
if (!alias_pages) rt_fatal("memory: cannot allocate the alias page map");
|
||||
// totk.ini is read here, once the address space is claimed and before the first setting: the pool size below
|
||||
// depends on TOTK_APP_POOL and on the resolution settings (resolution.c decides them once, on this first call).
|
||||
@@ -293,20 +249,15 @@ void mem_init(void) {
|
||||
|
||||
const RtGuestLayout *mem_layout(void) { return &L; }
|
||||
|
||||
// Adapted from LAS Recomp runtime/platform/windows.c:40-63 @ da35b83 (MIT, Copyright (c) 2026 Peiikai): GetWriteWatch
|
||||
// with the reset flag. A range inside one watched reservation (stack and heap block, or alias region) only.
|
||||
static uint64_t written_pages(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max, DWORD flags) {
|
||||
// A range inside one watched reservation (stack and heap block, or alias region) only.
|
||||
static uint64_t written_pages(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max, int reset) {
|
||||
if (!max || !size) return 0;
|
||||
if (!watched(addr, size)) return UINT64_MAX;
|
||||
ULONG_PTR n = (ULONG_PTR)max;
|
||||
DWORD gran;
|
||||
_Static_assert(sizeof(PVOID) == sizeof(uint64_t), "page addresses are written straight into a uint64_t array");
|
||||
if (GetWriteWatch(flags, (void *)(uintptr_t)addr, (SIZE_T)size, (PVOID *)pages, &n, &gran)) return UINT64_MAX;
|
||||
return n;
|
||||
return plat_mem_written(addr, size, pages, max, reset);
|
||||
}
|
||||
|
||||
uint64_t mem_written_pages(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max) {
|
||||
return written_pages(addr, size, pages, max, WRITE_WATCH_FLAG_RESET);
|
||||
return written_pages(addr, size, pages, max, 1);
|
||||
}
|
||||
|
||||
uint64_t mem_written_peek(uint64_t addr, uint64_t size, uint64_t *pages, uint64_t max) {
|
||||
@@ -324,15 +275,15 @@ static int inside(uint64_t a, uint64_t n, RtRange r) { return a >= r.base && n <
|
||||
|
||||
// ---- 0x01 SetHeapSize
|
||||
uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
plat_mutex_lock(&lock);
|
||||
if (size > HEAP_CAP || size % 0x200000 || size + image_bytes() > L.pool) {
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
plat_mutex_unlock(&lock);
|
||||
return R_INVALID_STATE;
|
||||
}
|
||||
if (size > heap_size) commit(L.heap.base + heap_size, size - heap_size, "heap");
|
||||
if (size < heap_size) decommit(L.heap.base + size, heap_size - size, "heap");
|
||||
heap_size = size;
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
plat_mutex_unlock(&lock);
|
||||
if (addr) *addr = L.heap.base;
|
||||
return R_OK;
|
||||
}
|
||||
@@ -379,16 +330,16 @@ uint32_t mem_set_permission(uint64_t a, uint64_t n, uint32_t perm) {
|
||||
int i = module_data(a, n);
|
||||
if (i < 0) return R_INVALID_STATE;
|
||||
const RtLoaded *m = ldr_module(i);
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
plat_mutex_lock(&lock);
|
||||
uint8_t *t = perm_table(i) + (a - m->base - m->data_off) / PAGE;
|
||||
for (uint64_t p = 0; p < n / PAGE; p++)
|
||||
if (t[p] & ATTR_PERMISSION_LOCKED) {
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
plat_mutex_unlock(&lock);
|
||||
return R_INVALID_STATE;
|
||||
}
|
||||
memset(t, (int)perm, n / PAGE);
|
||||
protect(a, n, perm == PERM_RW ? PAGE_READWRITE : perm == PERM_R ? PAGE_READONLY : PAGE_NOACCESS, "module data");
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
protect(a, n, perm == PERM_RW ? PLAT_PROT_RW : perm == PERM_R ? PLAT_PROT_READ : PLAT_PROT_NONE, "module data");
|
||||
plat_mutex_unlock(&lock);
|
||||
rt_log("%s data permission +%#llx size %#llx perm %u", m->name, (unsigned long long)(a - m->base),
|
||||
(unsigned long long)n, perm);
|
||||
return R_OK;
|
||||
@@ -402,10 +353,10 @@ uint32_t mem_set_attribute(uint64_t a, uint64_t n, uint32_t mask, uint32_t at) {
|
||||
int i = module_data(a, n);
|
||||
if (i < 0) return R_INVALID_STATE;
|
||||
const RtLoaded *m = ldr_module(i);
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
plat_mutex_lock(&lock);
|
||||
uint8_t *t = perm_table(i) + (a - m->base - m->data_off) / PAGE;
|
||||
for (uint64_t p = 0; p < n / PAGE; p++) t[p] |= ATTR_PERMISSION_LOCKED;
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
plat_mutex_unlock(&lock);
|
||||
return R_OK;
|
||||
}
|
||||
|
||||
@@ -424,7 +375,7 @@ static uint32_t stack_move(int unmap, uint64_t dst, uint64_t src, uint64_t n) {
|
||||
int in_heap = inside(src, n, (RtRange){L.heap.base, heap_size});
|
||||
int in_alias = inside(src, n, L.alias);
|
||||
if (!in_heap && !in_alias) return R_INVALID_REGION;
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
plat_mutex_lock(&lock);
|
||||
uint32_t r = R_OK;
|
||||
if (in_alias && !alias_all_mapped(src, n)) {
|
||||
r = R_INVALID_REGION;
|
||||
@@ -438,7 +389,7 @@ static uint32_t stack_move(int unmap, uint64_t dst, uint64_t src, uint64_t n) {
|
||||
r = R_INVALID_STATE;
|
||||
goto out;
|
||||
}
|
||||
protect(src, n, PAGE_READWRITE, "moved stack source");
|
||||
protect(src, n, PLAT_PROT_RW, "moved stack source");
|
||||
memcpy((void *)(uintptr_t)src, (const void *)(uintptr_t)dst, n);
|
||||
decommit(dst, n, "moved stack");
|
||||
stack_maps[found] = stack_maps[--nstack_maps];
|
||||
@@ -455,11 +406,11 @@ static uint32_t stack_move(int unmap, uint64_t dst, uint64_t src, uint64_t n) {
|
||||
}
|
||||
commit(dst, n, "moved stack");
|
||||
memcpy((void *)(uintptr_t)dst, (const void *)(uintptr_t)src, n);
|
||||
protect(src, n, PAGE_NOACCESS, "moved stack source");
|
||||
protect(src, n, PLAT_PROT_NONE, "moved stack source");
|
||||
stack_maps[nstack_maps++] = (StackMap){dst, src, n};
|
||||
}
|
||||
out:
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
plat_mutex_unlock(&lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -486,7 +437,7 @@ static uint32_t alias_map(int unmap, uint64_t a, uint64_t n) {
|
||||
if (!n || ((a | n) & (PAGE - 1))) return R_INVALID_ADDRESS;
|
||||
if (!inside(a, n, L.alias)) return R_INVALID_REGION;
|
||||
uint64_t first = (a - L.alias.base) / PAGE, count = n / PAGE;
|
||||
AcquireSRWLockExclusive(&lock);
|
||||
plat_mutex_lock(&lock);
|
||||
uint32_t r = R_OK;
|
||||
if (unmap) {
|
||||
for (unsigned i = 0; i < nstack_maps; i++)
|
||||
@@ -509,7 +460,7 @@ static uint32_t alias_map(int unmap, uint64_t a, uint64_t n) {
|
||||
alias_bytes += fresh * PAGE;
|
||||
}
|
||||
out:
|
||||
ReleaseSRWLockExclusive(&lock);
|
||||
plat_mutex_unlock(&lock);
|
||||
if (r) rt_log("%sMapPhysicalMemory %#llx+%#llx failed %#x (alias %#llx used, heap %#llx)", unmap ? "Un" : "",
|
||||
(unsigned long long)a, (unsigned long long)n, r, (unsigned long long)alias_bytes,
|
||||
(unsigned long long)heap_size);
|
||||
@@ -559,7 +510,7 @@ static uint64_t run_start(const uint8_t *p, uint64_t i, uint8_t v) { // first
|
||||
uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
|
||||
RtMemoryInfo r = {.base = a & ~(PAGE - 1), .size = PAGE};
|
||||
uint64_t low = L.aslr.base;
|
||||
AcquireSRWLockShared(&lock);
|
||||
plat_mutex_lock_shared(&lock);
|
||||
int mi;
|
||||
if (a >= RT_ADDRESS_LIMIT) {
|
||||
r.base = RT_ADDRESS_LIMIT;
|
||||
@@ -625,7 +576,7 @@ uint32_t mem_query(uint64_t a, RtMemoryInfo *out) {
|
||||
r.base = lo;
|
||||
r.size = hi - lo;
|
||||
}
|
||||
ReleaseSRWLockShared(&lock);
|
||||
plat_mutex_unlock_shared(&lock);
|
||||
if (out) *out = r;
|
||||
return R_OK;
|
||||
}
|
||||
@@ -640,9 +591,9 @@ int mem_info(uint32_t id, uint64_t *out) {
|
||||
case 5: v = L.heap.size; break;
|
||||
case 6: case 21: v = L.pool; break;
|
||||
case 7: case 22:
|
||||
AcquireSRWLockShared(&lock);
|
||||
plat_mutex_lock_shared(&lock);
|
||||
v = image_bytes() + heap_size + alias_bytes;
|
||||
ReleaseSRWLockShared(&lock);
|
||||
plat_mutex_unlock_shared(&lock);
|
||||
break;
|
||||
case 12: v = L.aslr.base; break;
|
||||
case 13: v = L.aslr.size; break;
|
||||
@@ -664,7 +615,7 @@ int mem_info(uint32_t id, uint64_t *out) {
|
||||
typedef struct KFree { struct KFree *next; } KFree;
|
||||
static KFree *kfree_list[KCLASSES];
|
||||
static uint64_t kbump, kcommitted;
|
||||
static SRWLOCK klock = SRWLOCK_INIT;
|
||||
static PlatMutex klock = PLAT_MUTEX_INIT;
|
||||
|
||||
static int kclass(uint64_t size) {
|
||||
int s = KMIN_SHIFT;
|
||||
@@ -679,7 +630,7 @@ void *mem_kernel_alloc(uint64_t size, uint64_t align) {
|
||||
int k = kclass(want ? want : 1);
|
||||
if (k >= KCLASSES) rt_fatal("mem_kernel_alloc: %#llx bytes", (unsigned long long)size);
|
||||
uint64_t bytes = 1ull << (k + KMIN_SHIFT), al = bytes < KCOMMIT ? bytes : KCOMMIT;
|
||||
AcquireSRWLockExclusive(&klock);
|
||||
plat_mutex_lock(&klock);
|
||||
KFree *f = kfree_list[k];
|
||||
void *p;
|
||||
if (f) {
|
||||
@@ -698,7 +649,7 @@ void *mem_kernel_alloc(uint64_t size, uint64_t align) {
|
||||
kbump = a + bytes;
|
||||
p = (void *)(uintptr_t)a;
|
||||
}
|
||||
ReleaseSRWLockExclusive(&klock);
|
||||
plat_mutex_unlock(&klock);
|
||||
memset(p, 0, size ? size : 1);
|
||||
return p;
|
||||
}
|
||||
@@ -710,9 +661,9 @@ void mem_kernel_free(void *p, uint64_t size) {
|
||||
int k = kclass(size ? size : 1);
|
||||
// A block allocated with a larger alignment sits in a larger class; freeing it into the smaller class only
|
||||
// wastes the rest.
|
||||
AcquireSRWLockExclusive(&klock);
|
||||
plat_mutex_lock(&klock);
|
||||
KFree *f = p;
|
||||
f->next = kfree_list[k];
|
||||
kfree_list[k] = f;
|
||||
ReleaseSRWLockExclusive(&klock);
|
||||
plat_mutex_unlock(&klock);
|
||||
}
|
||||
Loaded 100 of 229 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user