mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Unlock the frame rate, shorten cold starts, and describe the Ocarina build in the docs.
Ocarina steps its game by the clock (the same frame-numbered walk covers half the distance at 120 fps), so in a build without the fps60 and realtime patch sets the pacer holds frames to the frame limit alone: any rate from 30 to 1000, the monitor's rate, or unlimited (164 fps average in Kokiri Forest). The menu always shows the row, with 165, 240 and Unlimited added. NX2NAVI_PACE_HZ sets the pacer's rate for tests. A compute pipeline is compiled outside the pipeline lock, and one compile serves the replay and the boot prebuild alike; the prebuild uses half the CPUs. The guest waits at most NX2NAVI_PREWARM_WAIT_MS (3000) for the boot prewarm, which then goes on in the background (a cold start waited 9.8 s). NX2NAVI_TEST_PAD_AT attaches the test gamepad late and presses it, to test a controller switched on in play. README, docs/architecture.md, docs/controls.md and AGENTS.md describe the Ocarina build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# NX2navi
|
||||
|
||||
Static recompilation of *Ocarina of Time* (Switch, ARM64) into a native Windows x86-64 exe with a Vulkan renderer. Design: [docs/architecture.md](docs/architecture.md). Toolchain, build commands and tests: [docs/building.md](docs/building.md).
|
||||
Static recompilation of *Ocarina of Time* (Switch 2, ARM64, game version 1.1.1) into a native Windows x86-64 exe with a Vulkan renderer. Design: [docs/architecture.md](docs/architecture.md). Toolchain, build commands and tests: [docs/building.md](docs/building.md).
|
||||
|
||||
## Game content boundary
|
||||
|
||||
|
||||
94
README.md
94
README.md
@@ -1,92 +1,56 @@
|
||||
# NX2navi
|
||||
# Ocarina Recomp
|
||||
|
||||
**NX2navi** is a static recompilation of *The Legend of Zelda: Ocarina of Time* (Nintendo Switch, 2023) into a native x86-64 application for Windows and Linux ([docs/architecture.md](docs/architecture.md)).
|
||||
**Ocarina Recomp** is a static recompilation of *The Legend of Zelda: Ocarina of Time* for Nintendo Switch 2 (game
|
||||
version 1.1.1) into a native x86-64 application for Windows, with a Vulkan renderer
|
||||
([docs/architecture.md](docs/architecture.md)).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **No game files are included.** You need your own legally obtained copy of *Ocarina of Time*, 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.
|
||||
> **No game files are included.** You need your own legally obtained copy of the game, dumped from your own console.
|
||||
> 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 and Linux; visual bugs and crashes are still expected.
|
||||
Early development. Kokiri Forest and the areas around it play at 60 fps; visual bugs and crashes are still expected.
|
||||
|
||||
Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## Features
|
||||
|
||||
- Native code, not emulation
|
||||
- Game updates 1.1.0, 1.2.1 and 1.4.3; 1.2.1 and 1.4.3 saves carry over between them
|
||||
- Takes NSP and XCI game files, and compressed NSZ and XCZ as they are: the setup unpacks them itself
|
||||
- 60 fps by default; up to 144 fps or the monitor's refresh rate (experimental)
|
||||
- Custom resolutions (1080p, 2k...), supersampling and AMD FSR 1 upscaling
|
||||
- NVIDIA DLSS / DLAA on RTX GPUs: the setup downloads `nvngx_dlss.dll` from NVIDIA's DLSS SDK repository
|
||||
(github.com/NVIDIA/DLSS, NVIDIA RTX SDK license) when it finds an RTX GPU; untick it in the setup to skip it
|
||||
- HDR10 output with peak, game and HUD brightness settings
|
||||
- 16:10 (Steam Deck, handhelds), 3:2 (Surface, Framework), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
|
||||
- Anisotropic texture filtering up to 16x, for sharp ground into the distance
|
||||
- In-game settings menu (F1) and performance overlay (F10)
|
||||
- Discord Rich Presence ("Playing NX2navi" with the play time), off by default, own application id supported
|
||||
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
|
||||
- Keyboard, mouse and gamepad with remapping, rumble and gyro
|
||||
- 60 fps, at the game's own resolution
|
||||
- AMD FSR 1 or bilinear scaling to the window, with a sharpness setting; black bars or stretch
|
||||
- Colour profiles and brightness, contrast, saturation and gamma settings
|
||||
- In-game settings menu (F1, or Minus and Plus together on a gamepad) and performance overlay (F10)
|
||||
- Keyboard, mouse and gamepads with remapping, Nintendo or Xbox button layout, rumble and gyro; the game follows
|
||||
whichever connected gamepad you press
|
||||
- Stereo or 5.1 surround sound output
|
||||
- Xbox button prompts: LB, RB, LT, RT, Menu and View, and A, B, X and Y as labelled on the button to press, with either button layout
|
||||
- Shaders prepared at install instead of during play
|
||||
- Mods made for emulators: unpack a mod into the `mods` folder next to `nx2navi.exe`; switch and order them in the menu. RomFS
|
||||
mods, a mod manager'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
|
||||
|
||||
- VRR
|
||||
- Docked or handheld console mode
|
||||
- Graphics cards with less than 20 GiB of video memory keep the game's textures compressed (BC3), about half the
|
||||
video memory at the same speed
|
||||
|
||||
## Installing
|
||||
|
||||
### Windows
|
||||
The setup program for this game is not ready yet. [docs/building.md](docs/building.md) describes building from
|
||||
source. Settings are in the in-game menu and in `nx2navi.ini` ([nx2navi.ini.example](nx2navi.ini.example) lists every
|
||||
key). Controls: [docs/controls.md](docs/controls.md).
|
||||
|
||||
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 `NX2navi-Setup-<version>-windows.zip` from [Releases](../../releases/latest) and unpack it.
|
||||
2. Start `NX2navi-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. With
|
||||
an NVIDIA RTX GPU it also downloads NVIDIA DLSS, unless you untick *Download NVIDIA DLSS*; turn DLSS on in the
|
||||
menu (F1).
|
||||
4. Play `nx2navi.exe` from the install folder.
|
||||
|
||||
### 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 `NX2navi-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 NX2navi-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 `nx2navi` 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 `nx2navi.ini` ([nx2navi.ini.example](nx2navi.ini.example) lists every key). Controls: [docs/controls.md](docs/controls.md).
|
||||
|
||||
## Credits
|
||||
|
||||
NX2navi builds on the projects below. What was taken from each, with its license, is listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
- [LAS Recomp](https://github.com/peiikai/LASRecomp)
|
||||
- [MacNativeGames](https://github.com/ly05010419/MacNativeGames)
|
||||
- [d2r-mobile](https://github.com/jhaizhou-ops/d2r-mobile) and [nxnative](https://github.com/seedds/nxnative)
|
||||
- [SavageWindRecomp](https://github.com/Revivedx/SavageWindRecomp)
|
||||
- [Ryujinx](https://github.com/Ryujinx/Ryujinx)
|
||||
- [deko3d](https://github.com/devkitPro/deko3d)
|
||||
- [hactool](https://github.com/SciresM/hactool)
|
||||
- [SDL 3](https://libsdl.org), [Dear ImGui](https://github.com/ocornut/imgui), [AMD FSR 1](https://github.com/GPUOpen-Effects/FidelityFX-FSR), [Vulkan Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator), [libopus](https://opus-codec.org), [libvpx](https://chromium.googlesource.com/webm/libvpx), [Zstandard](https://github.com/facebook/zstd)
|
||||
- [Switchbrew](https://switchbrew.org) and the Switch homebrew community
|
||||
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, `VK_EXT_external_memory_host` and `VK_EXT_mesh_shader`.
|
||||
|
||||
## License
|
||||
|
||||
The project's own code is under the [MIT License](LICENSE); third-party code keeps its own license ([THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)). The license grants nothing for Nintendo's game.
|
||||
The project's own code is under the [MIT License](LICENSE); third-party code keeps its own license
|
||||
([THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)). The license grants nothing for Nintendo's game.
|
||||
|
||||
NX2navi is an independent, non-commercial fan project, not affiliated with or endorsed by Nintendo. *The Legend of Zelda*, *Ocarina of Time* and *Nintendo Switch* are trademarks of Nintendo.
|
||||
Ocarina Recomp is an independent, non-commercial fan project, not affiliated with or endorsed by Nintendo. *The Legend
|
||||
of Zelda*, *Ocarina of Time* and *Nintendo Switch* are trademarks of Nintendo.
|
||||
|
||||
## Building from source
|
||||
|
||||
[docs/building.md](docs/building.md) lists the toolchain and the steps. Everything the build writes stays under `build\` and `generated\`, which git ignores; run `tools/check_content.py` before a commit. Testers: [docs/tester-guide.md](docs/tester-guide.md).
|
||||
[docs/building.md](docs/building.md) lists the toolchain and the steps. Everything the build writes stays under
|
||||
`build\` and `generated\`, which git ignores; run `tools/check_content.py` before a commit.
|
||||
|
||||
Most of the code and documentation is written by AI coding agents (Claude Code) under the maintainer's direction; the maintainer plays every build and decides what is merged. [AGENTS.md](AGENTS.md) holds the rules they work under.
|
||||
Most of the code and documentation is written by AI coding agents (Claude Code) under the maintainer's direction; the
|
||||
maintainer plays every build and decides what is merged. [AGENTS.md](AGENTS.md) holds the rules they work under.
|
||||
|
||||
@@ -1,148 +1,113 @@
|
||||
# Architecture
|
||||
|
||||
How the Switch build of *Ocarina of Time* becomes a native Windows x86-64 program. The upstream projects it builds on are listed under Credits in the [README](../README.md).
|
||||
|
||||
## Starting points
|
||||
|
||||
Two MIT projects cover different halves of the problem:
|
||||
|
||||
| | LAS Recomp | MacNativeGames |
|
||||
|---|---|---|
|
||||
| Game | *Link's Awakening* | *Ocarina of Time* 1.0.0–1.4.3 |
|
||||
| CPU | ARM64 lifted to C, runs on x86-64 | Original ARM64 code runs directly on Apple silicon |
|
||||
| OS boundary | IPC services and SVCs | ~163 `nn::*` SDK functions replaced by symbol, 38 SVCs |
|
||||
| Graphics | Own NVN layer on Vulkan, own Maxwell translator | NVN front end + Metal 4 back end, Ryujinx translator |
|
||||
| Useful to us | Lifter, NEON-to-C lowering, Windows memory layer, tests | Everything NX2navi-specific: host behaviour, NVN front end, version tables, patches, shader list |
|
||||
|
||||
MacNativeGames cannot run on x86-64 as is: its CPU path executes ARM64 code natively. LAS Recomp's host only covers what *Link's Awakening* calls. The port takes the CPU half from LAS Recomp and the game half from MacNativeGames, and writes the parts neither has: a lifter that scales to NX2navi, and a Vulkan back end for the NVN front end.
|
||||
How the Switch 2 build of *Ocarina of Time* (game version 1.1.1) becomes a native Windows x86-64 program. Code taken
|
||||
from other projects, with its license, is listed in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md).
|
||||
|
||||
## Pipeline
|
||||
|
||||
### 1. Input
|
||||
|
||||
- Base 1.0.0 (`0100F2C0115B6000`) and one cumulative update (`0100F2C0115B6800`), NSP or XCI, plus the keys of the user's console.
|
||||
- ExeFS and RomFS extraction: MacNativeGames' Python extractor (Python 3.14 for `compression.zstd`, `.venv-py314`).
|
||||
- Update merging: hactool at `1d64a83` with MacNativeGames' BKTR patches, built by `tools/build_hactool.py`.
|
||||
- Verification: MacNativeGames' `expected-v*.json` hashes of ExeFS, RomFS and fonts.
|
||||
- Primary version: 1.4.3, the last update; performance work targets it. It replaced all three modules of the earlier updates (new SDK 20.5.22, RELR relocations, audio renderer REV15) and builds with `--version v143`, `recomp/patches-v143.txt`. 1.2.1 is a second target on the same build path; 1.0.0, 1.1.2 and 1.2.1 share `sdk`, `subsdk0` and `rtld` byte for byte, so only `main` differs among them. Saves load on either.
|
||||
- Windows driver: `tools/extract.py` runs the vendored extractors (`tools/vendor/macnativegames/`) and identifies the update by the SHA-256 of its `main`. hactool has no Darwin `funopen` here, so the base Program NCA is copied out of the NSP before the BKTR merge (about 17 GB of temporary space).
|
||||
- The ExeFS and RomFS of the user's own dump. Everything extracted or generated stays under `build/`, `generated/` or
|
||||
the user's own folders, never in the repository (`tools/check_content.py`).
|
||||
|
||||
### 2. CPU: lift ARM64 to C
|
||||
|
||||
- Modules: `main`, `sdk`, `subsdk0`. `rtld` is dropped and linking is done by the host, as MacNativeGames does.
|
||||
- Base: LAS Recomp's hand-written decoder (`lifter.py`) and NEON/FP lowering (`simd_c.py`, `simd.h`), MIT. Output: one C function per guest function, direct calls for `BL`, an address-keyed table for indirect branches.
|
||||
- Rewritten for NX2navi scale:
|
||||
- generator runs in parallel processes, with bounded memory per worker;
|
||||
- jump tables resolved by our own symbolic pass instead of Unicorn, which keeps GPL code out of the shipped generator;
|
||||
- `LDXP`/`STXP` (128-bit exclusive pairs) supported;
|
||||
- no 64 MB limit on `main`; `subsdk*` loaded;
|
||||
- NZCV and the six most used general registers of each function kept in C locals instead of the `Cpu`
|
||||
- Modules: `main`, `sdk` and the `subsdk*` modules. `rtld` is dropped; the host links the modules.
|
||||
- A hand-written decoder (`recomp/lifter.py`) and NEON/FP lowering (`recomp/simd_c.py`, `runtime/simd.h`) emit one C
|
||||
function per guest function, direct calls for `BL` and an address-keyed table for indirect branches.
|
||||
- The generator runs in parallel processes, with bounded memory per worker.
|
||||
- Jump tables are resolved by a symbolic pass of our own (`recomp/jumptables.py`).
|
||||
- `LDXP`/`STXP` (128-bit exclusive pairs) are supported.
|
||||
- NZCV and the six most used general registers of each function live in C locals instead of the `Cpu`
|
||||
(`recomp/flags.py`, `recomp/regs.py`): registers are stored back where they changed, before calls, SVCs and
|
||||
exits, and reloaded after them. A call reloads only X0-X18 and X30, as AAPCS64 has the callee preserve X19-X29 and
|
||||
SP; `gen.py --no-callee-saved-trust` reloads those too.
|
||||
- Coverage oracle: MacNativeGames' NSO rewriter lists every site where game code touches the OS (SVCs, TLS reads, counter reads, `x18`, exclusive windows, jump tables): 68,968 sites in 1.4.3 `main`. The lifter must handle every one.
|
||||
- Memory: guest address equals host address, all below 2^39 (IPC encodes 39-bit addresses). The heap, stacks and the alias region are reserved with `MEM_WRITE_WATCH`, so the graphics back end can ask which pages the CPU wrote since the last upload (`runtime/memory.c`).
|
||||
- Indirect branches go through `rt_dispatch` (`runtime/dispatch.c`), which looks the target up in the address table and tail-calls it.
|
||||
- Testing: LAS Recomp's differential harness, every lifted instruction class checked against Unicorn (test-only, never linked into the game).
|
||||
exits, and reloaded after them. A call reloads only X0-X18 and X30, as AAPCS64 has the callee preserve X19-X29
|
||||
and SP; `gen.py --no-callee-saved-trust` reloads those too.
|
||||
- Memory: guest address equals host address, all below 2^39 (IPC encodes 39-bit addresses). The heap, stacks and the
|
||||
alias region are reserved with `MEM_WRITE_WATCH`, so the graphics back end can ask which pages the CPU wrote since
|
||||
the last upload (`runtime/memory.c`).
|
||||
- Indirect branches go through `rt_dispatch` (`runtime/dispatch.c`), which looks the target up in the address table
|
||||
and tail-calls it.
|
||||
- Testing: a differential harness checks every lifted instruction class against Unicorn (test-only, never linked into
|
||||
the game).
|
||||
|
||||
### 3. Host
|
||||
|
||||
- Portable C from MacNativeGames: threads and synchronisation, IPC, memory services, fs/RomFS, time, account, applet, audio renderer and effects. The Apple pieces (mach VM, CommonCrypto, AudioToolbox, GameController) are replaced with Win32 and SDL3.
|
||||
- SDK boundary at the `nn::*` function level, matching MacNativeGames. Only `audren:u` and `sm:` are real IPC services.
|
||||
- Window, keyboard and mouse: Win32 (`gfx_window.c`, `input_win.c`). Gamepad and audio output: SDL3. Voice: libopus (BSD). Movies: the SDK's `movie::Decoder` is replaced by a host VP9 decoder over libvpx (BSD, static; `runtime/ov_movie.c`), because FFmpeg from MSYS2 is a GPL build.
|
||||
- zstd: most RomFS files of 1.4.3 are zstd frames. The patch set `zstd` sends the game's two decoder entry points to
|
||||
libzstd on the host (`ov_zstd.c`, `NX2NAVI_ZSTD_NATIVE`); the lifted decoder took almost three times as long.
|
||||
- The SDK boundary is at the `nn::*` function level: threads and synchronisation, IPC, memory, fs/RomFS, time,
|
||||
account, applet, hid, audio renderer and effects run as host code. Only `audren:u` and `sm:` are real IPC services.
|
||||
- Window, keyboard and mouse: Win32 (`gfx_window_win.c`, `input_win.c`). Gamepads and audio output: SDL3 (every
|
||||
connected gamepad is open; the game reads the one pressed last, `input.c`). Voice: libopus. Movies: a host VP9
|
||||
decoder over libvpx replaces the SDK's `movie::Decoder` (`runtime/ov_movie.c`).
|
||||
- Settings and menu: every key with its default is in one table (`cfg.c`), read from `nx2navi.ini` and `NX2NAVI_*`
|
||||
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, 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.
|
||||
drawn into the swapchain image (`runtime/ui/`); their text is in `ui_text.c`.
|
||||
- OS calls the host makes go through `runtime/platform/` (`platform.h`, `win32.c`).
|
||||
|
||||
#### Threads and cores
|
||||
|
||||
Every guest thread is a host thread. NX2navi assumes Horizon's scheduler: three application cores, a thread runs only on a core in its affinity mask, and a higher-priority thread takes a core from a lower-priority one. The game keeps per-core data without locks and relies on that, so the host models it (`runtime/kernel.c`):
|
||||
Every guest thread is a host thread. The game assumes Horizon's scheduler: three application cores, a thread runs
|
||||
only on a core in its affinity mask, and a higher-priority thread takes a core from a lower-priority one. The game
|
||||
keeps per-core data without locks and relies on that, so the host models it (`runtime/kernel.c`):
|
||||
|
||||
- Three virtual cores. A guest thread runs guest code only while it holds a core; the others wait in a priority-ordered list.
|
||||
- Preemption. When a waiting thread has a better priority than a holder on a core it may use, exactly one holder is flagged. The holder checks the flag at its next SVC and at every indirect call (`rt_dispatch`: two loads when nobody waits), checks again under the kernel lock that a better thread still waits, and only then hands its core over. The indirect-call check comes from SavageWindRecomp's BotW port; checking only at SVCs left a better thread waiting up to 26 ms.
|
||||
- Asynchronous preemption (from MacNativeGames 8bf0893, on unless `NX2NAVI_ASYNC_PREEMPT=0`). Game code can run for
|
||||
milliseconds with neither (`NX2NAVI_CORE_DIAG=1` measures it): a waiter that has spun 250 us suspends the holder and,
|
||||
if its host rip is in lifted code at host depth 0 (not inside a host call or SVC), moves it to `kern_ap_stub`, which
|
||||
saves the volatile registers and gives the core up there. A holder of a guest mutex counts at the priority of its
|
||||
best waiter, and one inside an SDK spin lock (TLR+0x100 `disable_count`) is not preempted: its interrupt flag is set
|
||||
and the switch happens at SVC 0x36. `NX2NAVI_ASYNC_PREEMPT=2` keeps those two without interrupting. The interruption is
|
||||
Win32 (SuspendThread, SetThreadContext); another host needs a signal handler there, as MacNativeGames has.
|
||||
- Host work gives the core back. Queue submits, presents, GPU waits, shader translation, `nn::fs` calls and NVN memory pool setup run without a core (`NVN_BLOCKING`, `kern_core_away`), as the Switch blocks those threads in IPC, so a thread blocked in the driver or on a file does not hold up guest threads.
|
||||
- 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 (`NX2NAVI_SPIN`, `NX2NAVI_SPIN_HANDOFF_US`).
|
||||
- `free_cores = 1` (`NX2NAVI_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).
|
||||
- Three virtual cores. A guest thread runs guest code only while it holds a core; the others wait in a
|
||||
priority-ordered list.
|
||||
- Preemption. When a waiting thread has a better priority than a holder on a core it may use, exactly one holder is
|
||||
flagged. The holder checks the flag at its next SVC and at every indirect call (`rt_dispatch`), checks again under
|
||||
the kernel lock that a better thread still waits, and only then hands its core over.
|
||||
- Asynchronous preemption (on unless `NX2NAVI_ASYNC_PREEMPT=0`): a waiter that has spun 250 us suspends the holder
|
||||
and, if its host rip is in lifted code at host depth 0, moves it to `kern_ap_stub`, which saves the volatile
|
||||
registers and gives the core up there. A holder of a guest mutex counts at the priority of its best waiter, and one
|
||||
inside an SDK spin lock is not preempted.
|
||||
- Host work gives the core back. Queue submits, presents, GPU waits, shader translation, `nn::fs` calls and NVN memory
|
||||
pool setup run without a core (`NVN_BLOCKING`, `kern_core_away`).
|
||||
- Spin waits. Loops that only another thread can end 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 (`NX2NAVI_SPIN`, `NX2NAVI_SPIN_HANDOFF_US`).
|
||||
|
||||
### 4. Graphics
|
||||
|
||||
- NVN front end from MacNativeGames (`nvn_api_*.c`, `nvn_obj.h`, texture layout and deswizzle). The game records command buffers through it; nothing reaches the GPU until the game submits.
|
||||
- Vulkan 1.3 back end in place of MacNativeGames' Metal one. At `QueueSubmitCommands` the recorded NVN commands are replayed into Vulkan command buffers (`vk_exec*.c`).
|
||||
- Memory pools. Apple's unified memory let MacNativeGames use the game's pools zero-copy. The back end imports the pools as host memory (`VK_EXT_external_memory_host`). On a discrete GPU, pools bound as buffers are mirrored in device-local memory and the pages the CPU wrote (write watch, above) are uploaded before a replay; idle pool regions are made read-only so the write-watch queries skip them (`vk_memory.c`). GPU-rendered textures are written back when the CPU reads them: on a pool invalidate, on a CPU copy, and for small targets once the CPU waits for a fence (`vk_texture.c`).
|
||||
- Replay thread. `NX2NAVI_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is off by default. Where the game waits for the GPU it overlaps the replay with the game threads (a GTX 1650 in the Depths, issue #88), a gain measured only there; while it was the default, a GTX 1660 Ti at the 60 fps cap with the GPU at 26 % reported uneven motion (#659). It adds up to a frame of latency; `replay_async = 1` turns it on.
|
||||
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. It is off by default on AMD GPUs, where the driver loses the device or hangs the GPU at startup with it (`NX2NAVI_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'
|
||||
`NX2NAVI_UBO_SNAPSHOT`); without it, late draws read another object's matrix.
|
||||
- Driver data. The NVN driver keeps a per-stage constant bank (c0) that shaders read for things like point size, dispatch size and base instance. The back end fills it the way the driver does.
|
||||
- Shaders: the Ryujinx translator 1.3.2 with MacNativeGames' patches and ours (`tools/xlate/patches/`), built with .NET 10 NativeAOT as `nx2navi_xlate.exe` and run as a separate process. Its SPIR-V goes to Vulkan directly. `tools/prepare_shaders.py` and `nx2navi.exe --prepare-shaders` translate and compile the variants of MacNativeGames' shared list ahead of play (29,587 for 1.4.3); the rest are translated on first use and cached on disk.
|
||||
- Pipelines: `VK_EXT_graphics_pipeline_library` where the driver has it. Shader stages are compiled ahead into libraries; a draw that meets a new state links them at once (fast link), and an optimised pipeline replaces the linked one when a background thread finishes it. Pipeline keys seen in play are saved and built at boot. A draw whose pipeline is not ready is skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise, so one-shot passes are never lost; a full-screen draw is skipped as well rather than waiting (`NX2NAVI_PIPE_SKIP`); the stage libraries a waiting draw needs are built on idle workers in parallel (`NX2NAVI_PIPE_WAIT`). Without the extension, a missing pipeline is built on a worker and the draw waits or is skipped by the same rule.
|
||||
- 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. 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 fifteen sizes: 21:9 2560x1080, 3440x1440, 3840x1600, 5120x2160,
|
||||
32:9 3840x1080, 5120x1440, 16:10 1280x800, 1680x1050, 1920x1200, 2560x1600 and 3:2 1920x1280, 2160x1440, 2256x1504,
|
||||
2880x1920, 3000x2000). A wider layout moves the HUD to the
|
||||
screen's corners, a narrower one to its top and bottom edges. Other sizes get the camera of their shape and the 16:9
|
||||
HUD stretched over it. The `aspect` setting (`fit`, the default; `stretch`) decides what the present does
|
||||
when the window's shape differs from the output's: `fit` letterboxes, where a 16:9 game in a wider window used to be
|
||||
stretched.
|
||||
- HDR (`hdr = 1`, `vk_hdr.c`). The game tonemaps its scene into an r11g11b10f target, upscales it and draws the HUD
|
||||
into another float target, and a last draw copies that into the 8-bit sRGB window texture it presents. With HDR on,
|
||||
the tonemap's fragment shader (recognised by its constants and resource names, not a hash) runs as
|
||||
`shaders/hdr_tonemap.frag`, which keeps highlights up to the display's peak; the present reads the float target the
|
||||
window texture was copied from (any other last draw into it, such as a menu or a movie, leaves the window texture,
|
||||
shown at the HUD's white); a compute pass turns the picture into PQ code values in Rec. 2020 for an HDR10
|
||||
swapchain, which FSR 1 or the blit then scale; the in-game menu is drawn into an RGBA8 image and blended on at the
|
||||
HUD's white. Without an HDR10 surface format (Windows with "Use HDR" off, a compositor without colour management)
|
||||
the swapchain stays SDR. `NX2NAVI_SHOTS` reads the window texture, so its pictures clip highlights in HDR.
|
||||
- DLAA (`vk_dlaa.c`, `dlaa`, off by default). NVIDIA DLSS at the scene's own size, through the driver's NGX core loaded
|
||||
at run time (no SDK code compiled in). It runs on the R11G11B10F light target right before the bloom chain's first
|
||||
pass, so post effects and the HUD read the anti-aliased picture. The scene's draws get a sub-pixel Halton viewport
|
||||
jitter; camera motion vectors are reprojected from the scene depth through this and last frame's view-projection of
|
||||
the scene camera block (constant bank c4), in double precision on the CPU. Per-object motion (`dlaa_objmv`,
|
||||
`vk_mvprev.c`, the per-object section of `vk_exec_draw.c`, translator patch 0015): solid scene draws whose position
|
||||
reads an object's own uniform blocks (bones c5, place c7, c6) are drawn a second time with a vertex shader variant
|
||||
that computes last frame's position from last frame's blocks (the game writes them into two copies that take turns;
|
||||
the uniform snapshot keeps both) and writes the difference into the motion image. `NX2NAVI_MV_CHECK` measures the
|
||||
vectors against the picture, `NX2NAVI_DLAA_TIMES` times the passes on the GPU. `fxaa` switches the game's own FXAA pass.
|
||||
- NVN 2 front end (`nvn_api_*.c`, `nvn_obj.h`, texture layout and deswizzle, the Switch 2 GOB layout). The game
|
||||
records command buffers through it; nothing reaches the GPU until the game submits.
|
||||
- Vulkan 1.3 back end. At `QueueSubmitCommands` the recorded NVN commands are replayed into Vulkan command buffers on
|
||||
the submitting thread (`vk_exec*.c`); a recorder thread makes the `vkCmd*` calls from a ring of packets
|
||||
(`vk_rec.c`).
|
||||
- Memory pools are imported 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) plus the ranges the game flushes are
|
||||
uploaded before a replay (`vk_memory.c`); without the mirrors the GPU reads them over PCIe and Kokiri Forest runs at
|
||||
7 fps. GPU-rendered textures are written back when the CPU reads them.
|
||||
- Uniform snapshot. The game rewrites per-object uniform blocks soon after it submits the frame that reads them; 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`).
|
||||
- Driver data. The NVN driver keeps a per-stage constant bank (c0). The back end fills it as the driver does: storage
|
||||
and uniform tables, push constants and, for NVN 2 compute, the dispatch's group counts in bytes 0-0xb.
|
||||
- Storage buffers the shaders reach through pointer tables in memory are bound at the address the table holds
|
||||
(`GfxShaderRes.ptr`, `vk_exec_draw.c ptr_addr`).
|
||||
- Bindless textures: the texture and sampler pools are descriptor arrays kept in step with the game's pool writes
|
||||
(`vk_exec_draw.c pool_sync`); slots that resolve alike share one lookup.
|
||||
- Shaders: Ampere (SM86) code is lowered to Maxwell (`tools/xlate/AmpereLowering.cs`) and translated to SPIR-V by the
|
||||
Ryujinx shader translator 1.3.2 with our patches (`tools/xlate/patches/`), built with .NET 10 NativeAOT as
|
||||
`nx2navi_xlate.exe` and run as separate processes. Translations are cached on disk; shaders and pipelines seen in
|
||||
play are prepared at the next boot, which waits for them at most `NX2NAVI_PREWARM_WAIT_MS` and then lets them finish
|
||||
in the background.
|
||||
- Mesh and task shaders run as `VK_EXT_mesh_shader`; indirect mesh draws read NVN's `{count, first}` record through a
|
||||
ring of Vulkan `{x, y, z}` records filled on the GPU.
|
||||
- Pipelines: `VK_EXT_graphics_pipeline_library` where the driver has it: stage libraries compiled ahead, a fast link
|
||||
at a new state, an optimised pipeline replacing it from a background thread. A draw whose pipeline is not ready is
|
||||
skipped when one of its pass's targets was rendered in the previous frame, and waits otherwise. Compute pipelines
|
||||
are compiled outside the pipeline lock, and one compile serves every thread that needs it.
|
||||
- The game's tensor-core upscaler is replaced by a decode-and-scale pass (`vk_tonemap.c`); `vk_fsr.c` runs AMD FSR 1
|
||||
(EASU, RCAS) on the present when the window is larger than the picture.
|
||||
- ASTC textures are uploaded as they are where the device samples ASTC, and decoded by a compute shader everywhere
|
||||
else (`vk_astc.c`); under 20 GiB of video memory the decoder encodes them to BC3.
|
||||
- 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`, `NX2NAVI_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`).
|
||||
- Allocation: Vulkan Memory Allocator. Debugging: Vulkan validation layers, per-pass frame dumps (`NX2NAVI_DUMP`, `vk_dump.c`) and a flight recorder (`NX2NAVI_FLIGHT`). RenderDoc cannot capture, because it hides `VK_EXT_external_memory_host` from the application.
|
||||
- Allocation: Vulkan Memory Allocator. Debugging: Vulkan validation layers, per-pass frame dumps (`NX2NAVI_DUMP`,
|
||||
`vk_dump.c`), a sampling profiler (`NX2NAVI_PROFILE`, `tools/prof_report.py`) and a flight recorder
|
||||
(`NX2NAVI_FLIGHT`). RenderDoc cannot capture, because it hides `VK_EXT_external_memory_host` from the application.
|
||||
|
||||
Maxwell behaviour the back end has to match, and the NX2navi bugs other ports fixed: [maxwell.md](maxwell.md).
|
||||
|
||||
Why Vulkan rather than D3D12 or wgpu:
|
||||
|
||||
- The translator emits SPIR-V, which Vulkan consumes as is. D3D12 would add a SPIR-V to DXIL step; wgpu would add SPIR-V to WGSL through Naga and drop features the game uses (bindless textures, some atomics).
|
||||
- LAS Recomp's Vulkan back end is a working reference on Windows.
|
||||
- The same back end runs on Linux.
|
||||
Maxwell behaviour the back end has to match: [maxwell.md](maxwell.md).
|
||||
|
||||
### 5. Toolchain
|
||||
|
||||
- Generated C: MSYS2 UCRT64 Clang + LLD. The generated code uses GNU extensions (`__int128`, `musttail`, vector types) that `cl.exe` cannot compile.
|
||||
- Generated C: MSYS2 UCRT64 Clang + LLD. The generated code uses GNU extensions (`__int128`, `musttail`, vector
|
||||
types) that `cl.exe` cannot compile.
|
||||
- Host code: the same Clang, C11.
|
||||
- Builds: `tools/build_game.py` writes a Ninja file and runs MSYS2 Ninja. Every object is hashed before linking, because a faulty RAM module once flipped bits in stored objects.
|
||||
- Analysis: Ghidra with the Switch loader, Capstone, `llvm-objdump`.
|
||||
- Players: the installer ([installer.md](installer.md)) builds the game code on the player's machine with a bundled llvm-mingw, so no recompiled code is ever distributed.
|
||||
|
||||
- Builds write a Ninja file and run MSYS2 Ninja. Every object is hashed before linking, because a faulty RAM module
|
||||
once flipped bits in stored objects.
|
||||
|
||||
130
docs/controls.md
130
docs/controls.md
@@ -1,6 +1,14 @@
|
||||
# Controls
|
||||
|
||||
Input comes from the first SDL3 gamepad, the keyboard and the mouse, merged into one Switch controller (`runtime/input_win.c`). Buttons are ORed; each stick takes the source with the larger deflection. Keys and mouse buttons count only while the game window is in the foreground. `NX2NAVI_INPUT=none` disables both (automated runs). `NX2NAVI_*` settings can also be set in `nx2navi.ini` next to `nx2navi.exe` (`input = none`, `motion = 0`, `rumble = 0`); the environment overrides the file. `NX2NAVI_RUMBLE_STRENGTH` is the rumble strength in percent (0 to 300, default 130; 0 is the same as `rumble = 0`). `NX2NAVI_RUMBLE_CURVE` is the exponent applied to the amplitude before the strength (0.2 to 1.5, default 0.6; lower makes weak pulses stronger, 1 is linear). `NX2NAVI_RUMBLE_TRIGGERS=1` also drives the impulse triggers of an Xbox pad.
|
||||
Input comes from SDL3 gamepads, the keyboard and the mouse, merged into one Switch controller (`runtime/input.c`,
|
||||
`runtime/input_win.c`). Every connected gamepad is kept open and the game reads the one pressed last; a gamepad
|
||||
switched on in play is found within a second. Buttons are ORed; each stick takes the source with the larger
|
||||
deflection. Keys and mouse buttons count only while the game window is in the foreground. `NX2NAVI_INPUT=none`
|
||||
disables both (automated runs). `NX2NAVI_*` settings can also be set in `nx2navi.ini` next to the exe (`input = none`,
|
||||
`motion = 0`, `rumble = 0`); the environment overrides the file. `NX2NAVI_RUMBLE_STRENGTH` is the rumble strength in
|
||||
percent (0 to 300, default 130; 0 is the same as `rumble = 0`). `NX2NAVI_RUMBLE_CURVE` is the exponent applied to the
|
||||
amplitude before the strength (0.2 to 1.5, default 0.6; lower makes weak pulses stronger, 1 is linear).
|
||||
`NX2NAVI_RUMBLE_TRIGGERS=1` also drives the impulse triggers of an Xbox pad.
|
||||
|
||||
`NX2NAVI_PAD_LAYOUT` (`pad_layout`, the *Button layout* row on the Controls tab, live) picks which face button is Switch A:
|
||||
|
||||
@@ -8,9 +16,11 @@ 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*. Ocarina's prompts always show the Switch letters: the Xbox and PC prompt folders below are made from NX2navi's files and do not exist for Ocarina, so its menu has no *Button prompts* row. 0.1.9's `pad_layout = xbox_prompts` is read as `pad_layout = nintendo` with `prompts = xbox`, and the menu writes it that way.
|
||||
The log names the layout in effect (`input: gamepad layout ...`) and each gamepad's first press
|
||||
(`input: gamepad "...": first press`); the menu shows the layout under *Automatic*. The game's prompts show the Switch
|
||||
letters.
|
||||
|
||||
The actions are Ocarina's, as its own *Basic Controls* screen (Plus menu, System) names them:
|
||||
The actions are the game's own, as its *Basic Controls* screen (Plus menu, System) names them:
|
||||
|
||||
| Switch | Gamepad (by position, Nintendo layout) | Keyboard and mouse | Action in the world |
|
||||
|---|---|---|---|
|
||||
@@ -33,55 +43,7 @@ The actions are Ocarina's, as its own *Basic Controls* screen (Plus menu, System
|
||||
| Plus | Start | Tab | menu: items, equipment, collection, system |
|
||||
| Minus | Back | M | map |
|
||||
|
||||
The game's *Swap Jump Controls* option trades B and X (jump on B, put away on X); the bindings stay on the Switch buttons, so Space then puts away and Left Ctrl jumps. Left Shift is A, so holding it dashes; E presses A once for talking and opening. 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 on neighbouring keys. The mouse wheel is unbound. The right-stick shake (`bind_SHAKE`, NX2navi's object) has no default key and no row in Ocarina's menu. 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
|
||||
|
||||
NX2navi only: the folders are made from NX2navi's UI archive and fonts (`Font/Font*.Nin_NX_NVN.bfarc.zs`); Ocarina's are `Font2/*.Nin_Ounce_NVN2` and have no such folders.
|
||||
|
||||
`NX2NAVI_PROMPTS` (`prompts`, the *Button prompts* row on the Controls tab, read at start) picks the controller whose buttons the game's prompts show: `nintendo` (default) keeps the game's own, `xbox` shows an Xbox controller's, `pc` the keyboard and mouse (PC button prompts, below).
|
||||
|
||||
- L, R, ZL, ZR, Plus and Minus become LB, RB, LT, RT, Menu and View, and the d-pad prompts a cross with an arrow for each direction meant. The letters on the keys are cut out in the game's own text font.
|
||||
- A, B, X and Y name the label of the button the pad layout presses, so each prompt names the button to press. `pad_layout = nintendo` plays an Xbox pad by position: the right button, labelled B, does Switch A (interact, confirm), and the bottom one, labelled A, does Switch B (run, cancel), so the prompts swap A with B and X with Y. `xbox` and `auto` put Switch A on the button labelled A, and the letters stay.
|
||||
|
||||
| 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 `nx2navi.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`.
|
||||
|
||||
## PC button prompts
|
||||
|
||||
NX2navi only, as the Xbox prompts above.
|
||||
|
||||
`prompts = pc` (*PC* on the *Button prompts* row) shows the key or mouse button the default keyboard bindings press each Switch button with, whatever the pad layout. The game loads `romfs-patches-pc` in place of `romfs-patches`, written by `tools/make_romfs_patches.py --prompts pc` (`button_icons.py`, `pc_icons`), which the setup runs next to the Xbox ones:
|
||||
|
||||
| Switch | Prompt |
|
||||
|---|---|
|
||||
| A, B, X, Y | E, Shift, Space, left click |
|
||||
| L, R, ZL, ZR | Q, R, right click, F |
|
||||
| Plus, Minus | Tab, M |
|
||||
| D-pad | 1 (left), 2 (up), 3 (right), 4 (down), as a cross or the keys of the directions meant |
|
||||
| Left stick, its press | W A S D (or the keys of the directions meant), Ctrl |
|
||||
| Right stick, its press | the mouse, with an arrow for each direction meant, C |
|
||||
|
||||
- The glyphs keep the width of the game's own, so the right-aligned guides do not run into their text. A key is a rounded square with its label cut out in the game's own text fonts; a mouse button is a mouse with that button filled and the others hollow.
|
||||
- The button guide in play draws every button from images of the Common archive (`Nt_KeyTexA/B/X/Y/L/ZL_00`, `Nt_KeyTexLSt_00` and `_01`, the stick tilted and pressed): all of them are painted with the same icons. The letters of the action prompt's diamond are one character of the Normal font each: E, an arrow for Shift, a bar for Space, L and R for the mouse buttons.
|
||||
- Like the Xbox prompts they follow the defaults and not the bindings. `--binds <nx2navi.ini>` draws the first key of each `bind_` line of that file instead, for a folder made by hand.
|
||||
- At a game resolution of another shape than 16:9 the game loads `romfs-patches-pc-<w>x<h>` in place of `romfs-patches-<w>x<h>` when it is there: the same prompts with that shape's UI layout, `make_romfs_patches.py --prompts pc --aspect <w>x<h> --out <install>\romfs-patches-pc-<w>x<h>`. The setup does not write those, so without one the Switch prompts stay (*off at a game resolution not 16:9*).
|
||||
The game's *Swap Jump Controls* option trades B and X (jump on B, put away on X); the bindings stay on the Switch buttons, so Space then puts away and Left Ctrl jumps. Left Shift is A, so holding it dashes; E presses A once for talking and opening. 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 on neighbouring keys. 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.
|
||||
|
||||
## Window keys
|
||||
|
||||
@@ -95,16 +57,16 @@ The Minus+Plus combo never reaches the game. In the menu the arrow keys, the D-p
|
||||
|
||||
## Remapping
|
||||
|
||||
The *Controls* tab of the settings menu lists, under the button layout, every Switch button and the eight stick directions the keys drive (movement and camera), each with two key cells and a controller cell. Each row's description says what the button does in Ocarina. Select a cell, press A or Enter, then press the new key, mouse button or controller button; Esc or five seconds without a press cancel, and a controller button cancels a key cell. Delete, Backspace or X clears the selected cell. A key another action has moves to the new one (the description names the action it left); a controller button swaps with the one it replaces. A remapped controller button is physical: from then on the face buttons (A B X Y) stay where they are in both layouts, until the buttons are put back as the layout in use has them or *Reset all buttons* is chosen. F1, F8, F10, F11 and Esc cannot be bound (the menu, the flight recorder and the window keep them). Punctuation keys are named by their place on a US keyboard; the menu shows the character the keyboard layout in use puts there.
|
||||
The *Controls* tab of the settings menu lists, under the button layout, every Switch button and the eight stick directions the keys drive (movement and camera), each with two key cells and a controller cell. Each row's description says what the button does in the game. Select a cell, press A or Enter, then press the new key, mouse button or controller button; Esc or five seconds without a press cancel, and a controller button cancels a key cell. Delete, Backspace or X clears the selected cell. A key another action has moves to the new one (the description names the action it left); a controller button swaps with the one it replaces. A remapped controller button is physical: from then on the face buttons (A B X Y) stay where they are in both layouts, until the buttons are put back as the layout in use has them or *Reset all buttons* is chosen. F1, F8, F10, F11 and Esc cannot be bound (the menu, the flight recorder and the window keep them). Punctuation keys are named by their place on a US keyboard; the menu shows the character the keyboard layout in use puts there.
|
||||
|
||||
The tab writes `nx2navi.ini` lines, one per Switch button or stick direction: `bind_<button> = <entry>, <entry>, ...`. The button is `A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK`, or a stick direction `LSTICK_LEFT LSTICK_UP LSTICK_RIGHT LSTICK_DOWN` (movement, W A S D by default) and `RSTICK_...` (camera, the arrow keys), or `SHAKE` (keys only, no default key; NX2navi's object shake). An entry is `pad:<name>` or `key:<name>`:
|
||||
The tab writes `nx2navi.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 lpaddle1 rpaddle1 lpaddle2 rpaddle2` (`zl`, `zr` are the triggers; the paddles are the back buttons of an Xbox Elite, DualSense Edge or Steam Controller, upper pair 1, lower pair 2, and no Switch button has them by default)
|
||||
- key names: `a` to `z`, `0` to `9`, `f2` to `f7`, `f9`, `f12`, `num0` to `num9`, `space tab enter backspace lshift rshift lctrl rctrl lalt capslock insert delete home end pageup pagedown up down left right`, `num_add num_sub num_mul num_div num_decimal`, `minus equals lbracket rbracket semicolon apostrophe comma period slash backslash grave`, `mouse_l mouse_r mouse_m mouse_x1 mouse_x2`
|
||||
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
|
||||
- `none` leaves the button unbound.
|
||||
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `NX2NAVI_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input_win.c` builds the tables again at its next poll).
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `NX2NAVI_BIND_<button>` environment variables work like any other setting. A changed line applies at once (`runtime/input.c` builds the tables again at its next poll).
|
||||
|
||||
A pad that reports its face buttons swapped (A/B and X/Y, as some GuliKit models do) is fixed with:
|
||||
|
||||
@@ -117,55 +79,17 @@ bind_Y = pad:north, key:mouse_l
|
||||
|
||||
## Mouse camera
|
||||
|
||||
Ocarina's build has no code patch sets, so the mouse always turns its camera by stick emulation (the last paragraphs of this section) and its menu has no *Mouse camera* row; the direct mode below is NX2navi 1.4.3's.
|
||||
|
||||
`mouse_mode = direct` (the default) turns the camera by the raw mouse motion on game 1.4.3, through hooks in the game's camera code (`runtime/mouse_direct.c`, the `mouse_direct` sites of `recomp/patches-v143.txt`). None of them changes a step of the gamepad alone. The free-orbit camera receives raw-count
|
||||
angular increments at its final spherical-angle output: 0.08 degrees per count on both axes at sensitivity 1, with
|
||||
no time multiplier, curve or carry into later frames. The controller's persistent pitch/yaw move with the output.
|
||||
Mouse input is removed from its stick path first, so the camera's later vertical signed-square curve and velocity
|
||||
filters cannot process it a second time. Other camera modes receive input after the radial dead zone, unit-circle
|
||||
clamp and stick response exponent; object has a separate hook after its manipulation input's power curves.
|
||||
The game's camera inversion and gamepad response remain in effect. `mouse_sens` scales raw counts; its scale
|
||||
differs from the stick-emulation mode. Start at 1.
|
||||
The Controls tab switches between *Direct* and *Stick emulation* (`mouse_mode = stick`).
|
||||
The orbit camera keeps its desired pitch at the final angular output, including stationary frames, so a later game
|
||||
filter cannot recenter it. The persistent filter state accounts for the camera's current vertical offset instead of
|
||||
accumulating corrections. A gamepad vertical input or the game's lock-on/override releases that mouse target.
|
||||
While L is held, mouse counts move the ability wheel's virtual cursor, which stays where it was when the mouse stops.
|
||||
The horizontal quick lists (D-pad up/left/right: materials, shields and weapons) use separate 32 ms right-stick steps,
|
||||
with at least 32 ms of neutral between them. At sensitivity 1, 32 horizontal counts request a step; a large flick
|
||||
cannot queue continued scrolling after the mouse stops. Vertical movement is ignored in these lists. Switching or
|
||||
releasing a selector resets its cursor/step state without sending the last movement to the camera. Selector input is
|
||||
not consumed by the direct camera or object hooks.
|
||||
The mouse never stands in for a stick shake: to detach a fused part, press G (`bind_SHAKE`). A press swings the right
|
||||
stick left and right for 0.8 s, holding the key keeps it swinging. The arrow keys or the gamepad's right stick shaken by
|
||||
hand work too. A mouse gesture for it would also fire while a held object is turned back and forth.
|
||||
A step of the gamepad alone, with no mouse motion and no pitch the mouse holds, reaches the camera exactly as without
|
||||
the hooks.
|
||||
The mapping below is still used when the camera-input hook is unavailable or the game disables camera input.
|
||||
Games 1.1.0 and 1.2.1 have no hooks and keep stick emulation whatever `mouse_mode` says.
|
||||
|
||||
In stick-emulation mode, mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * NX2NAVI_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
|
||||
Mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * NX2NAVI_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
|
||||
|
||||
`NX2NAVI_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. `NX2NAVI_INPUT=none` disables the capture too.
|
||||
|
||||
`NX2NAVI_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. `NX2NAVI_LSTICK_DEADZONE` and `NX2NAVI_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows under *Analog sticks* on the Controls tab) set one stick each and fall back to `NX2NAVI_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.
|
||||
|
||||
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
|
||||
|
||||
## Mouse in the game's menus
|
||||
|
||||
NX2navi 1.4.3 only (the screens it reads are NX2navi's eui menus); Ocarina's menus take the keys and the pad.
|
||||
|
||||
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild, the Purah Pad's camera and the map stay gameplay: the mouse keeps the camera there. When the menu closes the cursor is captured again.
|
||||
|
||||
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`NX2NAVI_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `NX2NAVI_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
|
||||
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. With no gamepad 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 object 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.
|
||||
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, for example one that emulates a DualSense: that gives buttons, rumble, gyro and accelerometer. The log then shows `input: gamepad "DualSense Wireless Controller" ... (rumble, gyro and accelerometer, ...)` and `input: motion sensors on`. Bridges that output a DualShock 4 whose gyro and accelerometer never change give buttons and rumble but no motion. Run one bridge at a time; Steam Input can also claim the controller, so close Steam if the bridge does not find it. With several virtual and real gamepads connected the game follows whichever one is pressed.
|
||||
|
||||
## Scripted input
|
||||
|
||||
@@ -174,21 +98,13 @@ Bridges that output a ViGEm DualShock 4 (ProCon2Windows, connectyourpro as teste
|
||||
```
|
||||
<frame> <button> [hold_frames] # A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK; hold defaults to 4
|
||||
<frame> LSTICK|RSTICK <x> <y> [hold_frames] # -32767..32767, up is +y
|
||||
<frame> MOUSE <dx> <dy> # direct-mode raw counts, once; down is +dy
|
||||
@<ms> <button> [hold_ms] # timed on the world clock
|
||||
@<ms> LSTICK|RSTICK <x> <y> [hold_ms]
|
||||
@<ms> MOUSE <dx> <dy>
|
||||
```
|
||||
|
||||
`<frame>` counts 60 Hz ticks of game time since the first frame (presented frames at 60 fps), so the title menus get the presses at the same game time at any frame limit. `@<ms>` counts milliseconds of the world clock: game time stepped since the end of the first save load (the player leaves the title screen's origin), so a timed event lands at the same game time at any frame rate and load length. A later load, such as fast travel, still shifts what follows. In a script with timed lines the untimed ones stop when the world clock starts, as the load ends at another tick in each run. Each event logs once (`input: script frame N tick T: A`). `tools/input/load_save.txt` loads the first save from the title screen.
|
||||
`<frame>` counts 60 Hz ticks of game time since the first frame (presented frames at 60 fps). Each event logs once (`input: script frame N tick T: A`).
|
||||
|
||||
`NX2NAVI_INPUT_RECORD=<file>` (`input_record` in `nx2navi.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.
|
||||
`NX2NAVI_INPUT_RECORD=<file>` (`input_record` in `nx2navi.ini`) writes the merged input in the same format, one line per button press and per stick segment. The file replays as a script, which makes it the repro to attach to a bug report.
|
||||
|
||||
In direct mouse mode the recording also writes one `MOUSE` event per consumed raw displacement. These are one-shot
|
||||
displacements rather than held stick segments. They replay without cursor capture, including with `input = none`;
|
||||
the game must be accepting camera input when the event reaches its camera update.
|
||||
|
||||
`NX2NAVI_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 `NX2NAVI_STICK_DEADZONE` like a real pad's, and `NX2NAVI_INPUT_RECORD` shows what reached the game.
|
||||
`NX2NAVI_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 `NX2NAVI_STICK_DEADZONE` like a real pad's, and `NX2NAVI_INPUT_RECORD` shows what reached the game. `NX2NAVI_TEST_PAD_AT=<ms>` attaches it that long after input starts instead and presses its south button a second later, which tests a controller switched on in play.
|
||||
|
||||
## RenderDoc capture (blocked)
|
||||
|
||||
|
||||
@@ -366,8 +366,17 @@ void rt_boot(void) {
|
||||
ipc_applet_event_init();
|
||||
hostfn_seal();
|
||||
uint64_t held = plat_now_ms();
|
||||
if (warm) plat_thread_join(warm);
|
||||
else vk_pipeline_prewarm();
|
||||
// At most NX2NAVI_PREWARM_WAIT_MS (default 3000): a warm start is ready in about 0.3 s, but a cold one (a new
|
||||
// translator, a new driver) translated 2,132 shaders and built 1,579 pipelines for 9.8 s while the window stayed
|
||||
// black, for shaders the game first needs in play, well after its intro. Past the wait the prewarm goes on in the
|
||||
// background; 0 waits for all of it. A --shot image waits for all of it, so that it does not depend on build timing.
|
||||
const char *pw = cfg_get("NX2NAVI_PREWARM_WAIT_MS");
|
||||
uint64_t wait_ms = pw && *pw ? strtoull(pw, NULL, 10) : 3000;
|
||||
if (warm && (rt_config()->shot || !wait_ms)) plat_thread_join(warm);
|
||||
else if (warm && !plat_thread_join_ns(warm, wait_ms * 1000000ull)) {
|
||||
plat_thread_detach(warm);
|
||||
rt_log("boot: the prewarm goes on in the background after %llu ms", (unsigned long long)wait_ms);
|
||||
} else if (!warm) vk_pipeline_prewarm();
|
||||
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)(plat_process_uptime_ns() / 1000000), (unsigned long long)held);
|
||||
|
||||
@@ -48,8 +48,8 @@ static const char *const known[] = {
|
||||
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
|
||||
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
|
||||
"MV_SURVEY",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "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_WAIT_MS", "PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
||||
@@ -57,7 +57,7 @@ static const char *const known[] = {
|
||||
"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",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PAD_AT", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
@@ -135,7 +135,7 @@ static const CfgMeta metas[] = {
|
||||
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
|
||||
{"PCSS", "0", "0..200", RESTART},
|
||||
// Game
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|165|240|display|unlimited", 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},
|
||||
|
||||
@@ -115,26 +115,37 @@ int frame_step_realtime(void) { return step_word != NULL; }
|
||||
static int fps60_set = -2;
|
||||
static int game_interval = 1; // the present interval of the newest frame (frame_step_present)
|
||||
|
||||
// The limit can change anything only with the fps60 or realtime patch set (NX2navi's builds). Without them (Ocarina) the
|
||||
// game picks its own present interval and the pacer follows it (nvn_api_obj.c present_period).
|
||||
int frame_limit_adjustable(void) { return rt_patch_set("fps60") >= 0 || rt_patch_set("realtime") >= 0; }
|
||||
// A build without the fps60 and realtime patch sets (Ocarina) steps its game by the clock itself: walked by the same
|
||||
// frame-numbered input script, Link covered half the distance at 120 fps that he covers at 60. There the pacer holds
|
||||
// frames to the limit alone (nvn_api_obj.c present_period), the game's own 60 Hz interval aside, and the limit may be
|
||||
// any rate or none (LIMIT_FREE_MAX_HZ, "unlimited"); NX2navi's physics fails past LIMIT_MAX_HZ, Ocarina has no such floor.
|
||||
#define LIMIT_FREE_MAX_HZ 1000
|
||||
int frame_free_running(void) { return rt_patch_set("fps60") < 0 && rt_patch_set("realtime") < 0; }
|
||||
int frame_limit_adjustable(void) { return 1; }
|
||||
static unsigned limit_seen;
|
||||
static int limit_read;
|
||||
static int limit_hz = -1; // 0: display; -1 before the first read
|
||||
static int limit_hz = -1; // 0: display; -2: unlimited (free-running builds); -1 before the first read
|
||||
|
||||
static int limit_parse(void) {
|
||||
const char *e = cfg_get("NX2NAVI_FPS_LIMIT"), *old = cfg_get("NX2NAVI_FPS60");
|
||||
if (!e || !*e) return old && !strcmp(old, "0") ? 30 : 60;
|
||||
if (!strcmp(e, "display")) return 0;
|
||||
int free_run = frame_free_running();
|
||||
if (!strcmp(e, "unlimited")) {
|
||||
if (free_run) return -2;
|
||||
rt_log("frame rate: NX2NAVI_FPS_LIMIT=unlimited needs a game that steps by the clock; %d", LIMIT_MAX_HZ);
|
||||
return LIMIT_MAX_HZ;
|
||||
}
|
||||
char *end;
|
||||
long n = strtol(e, &end, 10);
|
||||
if (!*end && free_run && n >= LIMIT_MIN_HZ) return n > LIMIT_FREE_MAX_HZ ? LIMIT_FREE_MAX_HZ : (int)n;
|
||||
if (!*end && n > LIMIT_MAX_HZ) {
|
||||
rt_log("frame rate: NX2NAVI_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: NX2NAVI_FPS_LIMIT=%s is not %d..%d or display; 60", e, LIMIT_MIN_HZ, LIMIT_MAX_HZ);
|
||||
rt_log("frame rate: NX2NAVI_FPS_LIMIT=%s is not %d..%d, display or unlimited; 60", e, LIMIT_MIN_HZ, LIMIT_MAX_HZ);
|
||||
return 60;
|
||||
}
|
||||
|
||||
@@ -146,10 +157,10 @@ static void frame_limit_live(void) {
|
||||
int hz = limit_parse();
|
||||
if (hz == limit_hz) return;
|
||||
limit_hz = hz;
|
||||
if (!frame_limit_adjustable()) {
|
||||
if (hz != 60)
|
||||
rt_log("frame rate: NX2NAVI_FPS_LIMIT=%s does nothing in this build (no fps60 or realtime patch set); the "
|
||||
"game sets its own rate", cfg_value("NX2NAVI_FPS_LIMIT"));
|
||||
if (frame_free_running()) {
|
||||
if (hz == -2) rt_log("frame rate: unlimited (the game steps by the clock)");
|
||||
else if (hz) rt_log("frame rate: limit %d fps (the game steps by the clock)", hz);
|
||||
else rt_log("frame rate: limit at the monitor's refresh rate (the game steps by the clock)");
|
||||
return;
|
||||
}
|
||||
if (fps60_set == -2) fps60_set = rt_patch_set("fps60");
|
||||
@@ -163,9 +174,11 @@ static void frame_limit_live(void) {
|
||||
|
||||
uint64_t frame_step_cap_ns(void) {
|
||||
frame_limit_live();
|
||||
if (limit_hz == -2) return 0; // unlimited: the pacer does not hold frames
|
||||
double hz = limit_hz ? limit_hz : gfx_window_refresh_hz();
|
||||
if (hz < LIMIT_MIN_HZ) hz = 60; // display rate unknown (no window yet)
|
||||
if (hz > LIMIT_MAX_HZ) hz = LIMIT_MAX_HZ;
|
||||
double max = frame_free_running() ? LIMIT_FREE_MAX_HZ : LIMIT_MAX_HZ;
|
||||
if (hz > max) hz = max;
|
||||
return (uint64_t)(1e9 / hz + 0.5);
|
||||
}
|
||||
|
||||
@@ -173,7 +186,12 @@ uint64_t frame_step_cap_ns(void) {
|
||||
// most without the real-time step; the game's own rate (60 Hz over its present interval) in a build without the patch
|
||||
// sets; 0 before the first frame read the setting.
|
||||
double frame_step_limit_hz(void) {
|
||||
if (!frame_limit_adjustable()) return 60.0 / game_interval;
|
||||
if (frame_free_running()) { // 0 for unlimited, as before the first read
|
||||
frame_limit_live();
|
||||
if (limit_hz == -2 || limit_hz == -1) return 0;
|
||||
double r = limit_hz ? limit_hz : gfx_window_refresh_hz();
|
||||
return r < LIMIT_MIN_HZ ? 60 : r;
|
||||
}
|
||||
int hz = limit_hz;
|
||||
if (hz < 0) return 0;
|
||||
double r = hz ? hz : gfx_window_refresh_hz();
|
||||
|
||||
@@ -289,7 +289,8 @@ void *rt_patch_page(void); // the page the patch
|
||||
void frame_step_init(void); // after rt_patches_init: the real-time step's first value
|
||||
int frame_step_realtime(void); // the real-time step is on: presents ignore the interval
|
||||
uint64_t frame_step_cap_ns(void); // with the real-time step: the shortest frame in ns
|
||||
int frame_limit_adjustable(void); // the build has the fps60 or realtime patch set
|
||||
int frame_limit_adjustable(void); // the frame limit setting changes the rate (always)
|
||||
int frame_free_running(void); // no fps60/realtime patch set: the game steps by the clock
|
||||
double frame_step_limit_hz(void); // the frame rate the limit gives (overlay); 0 unknown
|
||||
int64_t frame_step_world_us(void); // game time since the loaded save's first frame, -1 before
|
||||
// (NX2NAVI_FPS_LIMIT, live)
|
||||
|
||||
@@ -513,9 +513,13 @@ static void binds_refresh(void) {
|
||||
|
||||
// NX2NAVI_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
|
||||
// deflection go through the same mapping and dead zone as a real pad. NX2NAVI_TEST_PAD_AT=<ms>: attached that long after
|
||||
// input starts instead, like a controller switched on in play, and its south button pressed for half a second one
|
||||
// second later (the log then shows the hot-plug and the press arriving: scan_pads, pad_physical).
|
||||
static SDL_Joystick *test_joy; // kept open: a virtual joystick's axes and buttons are set through an open handle
|
||||
static uint64_t test_attach_ns, test_press_ns; // NX2NAVI_TEST_PAD_AT: when to attach, when to press (0: done)
|
||||
|
||||
static void test_pad_make(void) {
|
||||
const char *e = cfg_get("NX2NAVI_TEST_PAD");
|
||||
int v[4];
|
||||
if (!e || !*e) return;
|
||||
@@ -530,18 +534,42 @@ static void test_pad_attach(void) {
|
||||
d.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
|
||||
d.name = "NX2navi test gamepad";
|
||||
SDL_JoystickID id = SDL_AttachVirtualJoystick(&d);
|
||||
joy = id ? SDL_OpenJoystick(id) : NULL;
|
||||
if (!joy) {
|
||||
test_joy = id ? SDL_OpenJoystick(id) : NULL;
|
||||
if (!test_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]));
|
||||
SDL_SetJoystickVirtualAxis(test_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 test_pad_attach(void) {
|
||||
const char *at = cfg_get("NX2NAVI_TEST_PAD_AT");
|
||||
if (at && *at && cfg_get("NX2NAVI_TEST_PAD")) {
|
||||
test_attach_ns = plat_now_ns() + strtoull(at, NULL, 10) * 1000000ull;
|
||||
return;
|
||||
}
|
||||
test_pad_make();
|
||||
}
|
||||
|
||||
// Under lock, every poll: the delayed attach and the press of NX2NAVI_TEST_PAD_AT.
|
||||
static void test_pad_tick(void) {
|
||||
uint64_t now = plat_now_ns();
|
||||
if (test_attach_ns && now >= test_attach_ns) {
|
||||
test_attach_ns = 0;
|
||||
test_pad_make();
|
||||
if (test_joy) test_press_ns = now + 1000000000ull;
|
||||
}
|
||||
if (test_press_ns && test_joy && now >= test_press_ns) {
|
||||
int down = now < test_press_ns + 500000000ull;
|
||||
SDL_SetJoystickVirtualButton(test_joy, SDL_GAMEPAD_BUTTON_SOUTH, down);
|
||||
if (!down) test_press_ns = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void start(void) {
|
||||
binds_refresh();
|
||||
const char *e = cfg_get("NX2NAVI_INPUT");
|
||||
@@ -591,6 +619,7 @@ static int32_t shake_stick(int held) {
|
||||
|
||||
// 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]) {
|
||||
test_pad_tick();
|
||||
// every RESCAN_MS, not only without a gamepad: a second controller, or one that wakes up after the start, is found
|
||||
if (plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
|
||||
SDL_UpdateGamepads();
|
||||
|
||||
@@ -735,7 +735,10 @@ NVN_API(nvnWindowSetCrop) {
|
||||
// 2nd or 3rd tick would only add latency and 50 ms frames.
|
||||
#define VSYNC_HZ 60ull
|
||||
static uint64_t present_period(const win_t *o) {
|
||||
if (frame_step_realtime()) return frame_step_cap_ns();
|
||||
if (frame_step_realtime() || frame_free_running()) return frame_step_cap_ns(); // frame_rate.c: the limit alone
|
||||
static int pace_hz = -1; // NX2NAVI_PACE_HZ=<n> (test runs): present at n per second whatever the game's interval
|
||||
if (pace_hz < 0) pace_hz = cfg_get("NX2NAVI_PACE_HZ") ? atoi(cfg_get("NX2NAVI_PACE_HZ")) : 0;
|
||||
if (pace_hz > 0) return 1000000000ull / (uint64_t)pace_hz;
|
||||
return (o->interval > 0 ? (uint64_t)o->interval : 1ull) * 1000000000ull / VSYNC_HZ;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ int res_adjustable(void);
|
||||
const char *const *vk_color_preset(const char *name);
|
||||
double frame_step_limit_hz(void);
|
||||
int frame_limit_adjustable(void);
|
||||
int frame_free_running(void);
|
||||
extern const int rt_game_version;
|
||||
extern const char rt_port_version[];
|
||||
void cheats_frame(uint64_t frame); // cheats.c
|
||||
@@ -633,21 +634,24 @@ static void row_game_lang(void) {
|
||||
// The frame limit (frame_rate.c): unset, the older fps60 = 0 still means 30. The monitor choice shows the rate it gives.
|
||||
static void row_fps_limit() {
|
||||
// 40 and 45: the Steam Deck's screen runs at 40 to 60 Hz (the OLED to 90); 40 divides 40 and 120 Hz evenly
|
||||
static const char *const vals[] = {"30", "40", "45", "60", "90", "120", "144", "display"};
|
||||
static const UiTextId ids[] = {UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60,
|
||||
UI_MENU_FPS_90, UI_MENU_FPS_120, UI_MENU_FPS_144, UI_MENU_FPS_DISPLAY};
|
||||
enum { N = sizeof vals / sizeof *vals };
|
||||
const char *names[N];
|
||||
for (int i = 0; i < N; i++) names[i] = menu_text(ids[i]);
|
||||
// 165 and 240: common gaming monitors; unlimited only where the game steps by the clock (frame_rate.c)
|
||||
static const char *const vals[] = {"30", "40", "45", "60", "90", "120", "144", "165", "240", "display", "unlimited"};
|
||||
static const UiTextId ids[] = {UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60,
|
||||
UI_MENU_FPS_90, UI_MENU_FPS_120, UI_MENU_FPS_144, UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_144, UI_MENU_FPS_DISPLAY, UI_MENU_FPS_UNLIMITED};
|
||||
int n = frame_free_running() ? 11 : 10;
|
||||
const char *names[11];
|
||||
for (int i = 0; i < n; i++) names[i] = menu_text(ids[i]);
|
||||
names[7] = "165 FPS", names[8] = "240 FPS";
|
||||
static char display[64];
|
||||
double hz = gfx_window_refresh_hz();
|
||||
if (hz > 0) {
|
||||
snprintf(display, sizeof display, menu_text(UI_MENU_FPS_DISPLAY_HZ), hz);
|
||||
names[N - 1] = display;
|
||||
names[9] = display;
|
||||
}
|
||||
const char *set = cfg_get("NX2NAVI_FPS_LIMIT"), *old = cfg_get("NX2NAVI_FPS60");
|
||||
int cur = (!set || !*set) && old && !strcmp(old, "0") ? 0 : -1;
|
||||
opt_choice_list("NX2NAVI_FPS_LIMIT", UI_MENU_FPS_LIMIT, vals, names, N, UI_MENU_FPS_LIMIT_DESC, cur);
|
||||
opt_choice_list("NX2NAVI_FPS_LIMIT", UI_MENU_FPS_LIMIT, vals, names, n, UI_MENU_FPS_LIMIT_DESC, cur);
|
||||
}
|
||||
|
||||
// The menu's name of a NX2NAVI_VK_PRESENT mode (gfx_window_present_mode); NULL for another.
|
||||
@@ -727,9 +731,7 @@ static void tab_screen(float scale) {
|
||||
|
||||
static void tab_game(float scale) {
|
||||
opt_begin("##game", scale);
|
||||
// The limit needs the fps60 or realtime patch set (frame_rate.c frame_limit_adjustable): Ocarina's build has
|
||||
// neither and runs at the rate it picks itself, so the row would change nothing
|
||||
if (frame_limit_adjustable()) row_fps_limit();
|
||||
row_fps_limit(); // frame_rate.c: Ocarina steps by the clock, so the pacer alone sets its rate
|
||||
row_game_lang();
|
||||
static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD};
|
||||
opt_choice("NX2NAVI_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC);
|
||||
|
||||
@@ -166,12 +166,13 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FXAA_DESC] = "The game's own FXAA edge smoothing. It blurs fine detail a little; with DLAA on it is not needed.",
|
||||
|
||||
[UI_MENU_FPS_LIMIT] = "Frame rate limit",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "The most frames per second the game draws. Above 60 is experimental and needs the "
|
||||
"real-time step (realtime = 1 in nx2navi.ini, the default).",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "The most frames per second the game draws. The game keeps time by the clock, so it "
|
||||
"plays at the same speed at any rate. Unlimited draws as fast as the PC can.",
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS",
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
// the monitor's refresh rate; printf format with the rate in Hz when it is known
|
||||
[UI_MENU_FPS_DISPLAY] = "Monitor rate", [UI_MENU_FPS_DISPLAY_HZ] = "Monitor rate (%.0f Hz)",
|
||||
[UI_MENU_FPS_UNLIMITED] = "Unlimited",
|
||||
[UI_MENU_REALTIME] = "Game speed follows the clock",
|
||||
[UI_MENU_REALTIME_DESC] = "Steps the game by real time, so a slow frame does not slow the game down.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Cloth on the clock",
|
||||
@@ -640,11 +641,12 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FXAA_DESC] = "Собственное сглаживание FXAA. Слегка размывает мелкие детали; с DLAA оно не нужно.",
|
||||
|
||||
[UI_MENU_FPS_LIMIT] = "Ограничение частоты кадров",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "Наибольшее число кадров в секунду. Выше 60 — экспериментальный режим, нужен шаг по "
|
||||
"реальному времени (realtime = 1 в nx2navi.ini, по умолчанию).",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "Наибольшее число кадров в секунду. Игра идёт по часам, поэтому её скорость не "
|
||||
"зависит от частоты кадров. «Без ограничения» рисует так быстро, как может ПК.",
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS",
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "Как у монитора", [UI_MENU_FPS_DISPLAY_HZ] = "Как у монитора (%.0f Гц)",
|
||||
[UI_MENU_FPS_UNLIMITED] = "Без ограничения",
|
||||
[UI_MENU_REALTIME] = "Скорость игры по часам",
|
||||
[UI_MENU_REALTIME_DESC] = "Игра идёт по реальному времени, медленный кадр её не замедляет.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Ткань по часам",
|
||||
@@ -1068,8 +1070,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_SHARPNESS_NEEDS_FSR] = "só com AMD FSR 1",
|
||||
|
||||
[UI_MENU_FPS_LIMIT] = "Limite de quadros",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "O máximo de quadros por segundo que o jogo desenha. Acima de 60 é experimental e "
|
||||
"precisa do passo em tempo real (realtime = 1 no nx2navi.ini, o padrão).",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "O máximo de quadros por segundo que o jogo desenha. O jogo segue o relógio, então a "
|
||||
"velocidade é a mesma em qualquer taxa. Sem limite desenha o mais rápido que o PC puder.",
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS",
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_ASSUME_FOCUS] = "Continuar rodando fora de foco",
|
||||
@@ -1078,6 +1080,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
"a janela perde o foco, como no Switch ao apertar Home; um carregamento interrompido "
|
||||
"assim pode parar de vez.",
|
||||
[UI_MENU_FPS_DISPLAY] = "Taxa do monitor", [UI_MENU_FPS_DISPLAY_HZ] = "Taxa do monitor (%.0f Hz)",
|
||||
[UI_MENU_FPS_UNLIMITED] = "Sem limite",
|
||||
[UI_MENU_REALTIME] = "Velocidade do jogo pelo relógio",
|
||||
[UI_MENU_REALTIME_DESC] = "Avança o jogo pelo tempo real, para que um quadro lento não deixe o jogo mais lento.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Tecidos pelo relógio",
|
||||
@@ -1600,8 +1603,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_COLOR_GAMMA] = "伽马",
|
||||
[UI_MENU_COLOR_GAMMA_DESC] = "小于 1 提亮暗部与中间调,大于 1 压暗;1 表示不调整。",
|
||||
[UI_MENU_FPS_LIMIT] = "帧率上限",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "游戏每秒绘制的最高帧数。高于 60 帧为实验性功能,"
|
||||
"需要实时步进(nx2navi.ini 中 realtime = 1,默认开启)。",
|
||||
[UI_MENU_FPS_LIMIT_DESC] = "游戏每秒绘制的最高帧数。游戏按时钟计时,任何帧率下速度都相同。"
|
||||
"不限制时以电脑能达到的最快速度绘制。",
|
||||
[UI_MENU_FPS_30] = "30 FPS",
|
||||
[UI_MENU_FPS_40] = "40 FPS",
|
||||
[UI_MENU_FPS_45] = "45 FPS",
|
||||
@@ -1611,6 +1614,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "显示器刷新率",
|
||||
[UI_MENU_FPS_DISPLAY_HZ] = "显示器刷新率(%.0f Hz)",
|
||||
[UI_MENU_FPS_UNLIMITED] = "不限制",
|
||||
[UI_MENU_REALTIME] = "游戏速度跟随时钟",
|
||||
[UI_MENU_REALTIME_DESC] = "按真实时间推进游戏,帧率低时游戏速度也不会变慢。",
|
||||
[UI_MENU_CLOTH_REALTIME] = "布料跟随时钟",
|
||||
|
||||
@@ -61,7 +61,7 @@ typedef enum {
|
||||
// Game
|
||||
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90,
|
||||
UI_MENU_FPS_120, UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_DISPLAY, UI_MENU_FPS_DISPLAY_HZ,
|
||||
UI_MENU_FPS_DISPLAY, UI_MENU_FPS_DISPLAY_HZ, UI_MENU_FPS_UNLIMITED,
|
||||
UI_MENU_REALTIME, UI_MENU_REALTIME_DESC,
|
||||
UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC,
|
||||
UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC,
|
||||
|
||||
@@ -2897,6 +2897,29 @@ static void journal_mark(int slot, const GfxWarmEnt *e) {
|
||||
plat_file_pwrite(SK.journal, rec, sizeof rec, (uint64_t)slot * JR_SLOT);
|
||||
}
|
||||
|
||||
// Compute shaders whose pipeline a thread is compiling now (P.lock): a cold driver takes about 2 s for one of Ocarina's
|
||||
// large compute shaders. The replay used to compile under P.lock, which held every other pipeline lookup that long, and
|
||||
// compiled a second copy of a pipeline the boot prebuild (lb_compute) was already compiling.
|
||||
#define CS_FLIGHT 32
|
||||
static const GfxShader *cs_flight[CS_FLIGHT];
|
||||
|
||||
static int cs_flight_has(const GfxShader *cs) {
|
||||
for (int i = 0; i < CS_FLIGHT; i++)
|
||||
if (cs_flight[i] == cs) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cs_flight_add(const GfxShader *cs) { // 0 when the table is full (the caller compiles anyway)
|
||||
for (int i = 0; i < CS_FLIGHT; i++)
|
||||
if (!cs_flight[i]) return cs_flight[i] = cs, 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void cs_flight_remove(const GfxShader *cs) {
|
||||
for (int i = 0; i < CS_FLIGHT; i++)
|
||||
if (cs_flight[i] == cs) cs_flight[i] = NULL;
|
||||
}
|
||||
|
||||
VkPipeline vk_pipeline_compute(const GfxShader *cs, const VkPipeInfo **info) {
|
||||
if (info) *info = NULL;
|
||||
if (!cs || !ensure_init()) return VK_NULL_HANDLE;
|
||||
@@ -2904,6 +2927,12 @@ VkPipeline vk_pipeline_compute(const GfxShader *cs, const VkPipeInfo **info) {
|
||||
uint64_t h = key_hash(&k, sizeof k);
|
||||
plat_mutex_lock(&P.lock);
|
||||
Pipe *p = pipe_find(&k, sizeof k, h);
|
||||
while (!p && cs_flight_has(cs)) { // another thread is compiling it: its result, not a second compile
|
||||
plat_mutex_unlock(&P.lock);
|
||||
plat_sleep_ms(1);
|
||||
plat_mutex_lock(&P.lock);
|
||||
p = pipe_find(&k, sizeof k, h);
|
||||
}
|
||||
if (p) {
|
||||
P.stats.hits++;
|
||||
} else {
|
||||
@@ -2912,19 +2941,27 @@ VkPipeline vk_pipeline_compute(const GfxShader *cs, const VkPipeInfo **info) {
|
||||
Layout *l = compute_layout(cs, &B, &why);
|
||||
VkPipeline pipe = VK_NULL_HANDLE;
|
||||
if (!why) {
|
||||
int flight = cs_flight_add(cs);
|
||||
plat_mutex_unlock(&P.lock); // the compile runs without the lock
|
||||
uint64_t p0 = perf_begin();
|
||||
pipe = compute_create(cs, l);
|
||||
perf_end(PERF_PIPE, p0);
|
||||
plat_mutex_lock(&P.lock);
|
||||
if (flight) cs_flight_remove(cs);
|
||||
if (!pipe) why = "pipeline creation failed";
|
||||
}
|
||||
if (why) {
|
||||
P.stats.failures++;
|
||||
rt_log("vk_pipeline: compute pipeline not built: %s", why);
|
||||
if ((p = pipe_find(&k, sizeof k, h))) { // another thread added it meanwhile (no flight slot was free)
|
||||
if (pipe) vkDestroyPipeline(P.device, pipe, NULL);
|
||||
} else {
|
||||
P.stats.compute++;
|
||||
LB.cs_draw++;
|
||||
if (why) {
|
||||
P.stats.failures++;
|
||||
rt_log("vk_pipeline: compute pipeline not built: %s", why);
|
||||
} else {
|
||||
P.stats.compute++;
|
||||
LB.cs_draw++;
|
||||
}
|
||||
p = pipe_add(&k, sizeof k, h, pipe, l, l ? &B : NULL, VK_PIPELINE_BIND_POINT_COMPUTE, 0);
|
||||
}
|
||||
p = pipe_add(&k, sizeof k, h, pipe, l, l ? &B : NULL, VK_PIPELINE_BIND_POINT_COMPUTE, 0);
|
||||
}
|
||||
plat_mutex_unlock(&P.lock);
|
||||
if (!p) return VK_NULL_HANDLE;
|
||||
@@ -2973,8 +3010,9 @@ static void lb_compute(const GfxShader *cs) {
|
||||
Bindings B = {0};
|
||||
const char *why = NULL;
|
||||
lb_lock();
|
||||
Layout *l = pipe_find(&k, sizeof k, h) ? NULL : compute_layout(cs, &B, &why);
|
||||
int have = !l && !why;
|
||||
int busy = cs_flight_has(cs); // the replay compiles it now (vk_pipeline_compute)
|
||||
Layout *l = busy || pipe_find(&k, sizeof k, h) ? NULL : compute_layout(cs, &B, &why);
|
||||
int have = !l && !why, flight = l ? cs_flight_add(cs) : 0;
|
||||
lb_unlock();
|
||||
if (!l) {
|
||||
lb_count(2, have ? 1 : 2, 0);
|
||||
@@ -2984,6 +3022,7 @@ static void lb_compute(const GfxShader *cs) {
|
||||
VkPipeline pipe = compute_create(cs, l);
|
||||
double ms = now_ms() - t0;
|
||||
lb_lock();
|
||||
if (flight) cs_flight_remove(cs);
|
||||
if (pipe && !pipe_find(&k, sizeof k, h)) {
|
||||
pipe_add(&k, sizeof k, h, pipe, l, &B, VK_PIPELINE_BIND_POINT_COMPUTE, 0);
|
||||
P.stats.compute++;
|
||||
@@ -3131,7 +3170,7 @@ static void lb_state(const uint32_t pr_hist[16], const uint32_t fs_hist[4]) {
|
||||
// Boot: compute shaders of the warm list (recorded and prepared), then the first KEEP recorded graphics variants.
|
||||
static void lb_start(const uint32_t pr_hist[16], const uint32_t fs_hist[4]) {
|
||||
const char *e = cfg_value("NX2NAVI_LIB_PREBUILD"), *ke = cfg_get("NX2NAVI_LIB_PREBUILD_KEEP");
|
||||
const char *te = cfg_get("NX2NAVI_LIB_PREBUILD_THREADS"); // no fixed default: a quarter of the CPUs when unset
|
||||
const char *te = cfg_get("NX2NAVI_LIB_PREBUILD_THREADS"); // no fixed default: half the CPUs when unset
|
||||
if (!A.on || !cfg_bool("NX2NAVI_LIB_PREBUILD")) return;
|
||||
char dir[PLAT_PATH_MAX * 2];
|
||||
if (cache_dir(dir, sizeof dir)) skip_init(dir);
|
||||
@@ -3154,7 +3193,7 @@ static void lb_start(const uint32_t pr_hist[16], const uint32_t fs_hist[4]) {
|
||||
lb_state(pr_hist, fs_hist);
|
||||
LB.idle = 1;
|
||||
int cpus = (int)plat_cpu_usable();
|
||||
int t = te && *te ? atoi(te) : cpus / 4;
|
||||
int t = te && *te ? atoi(te) : cpus / 2; // idle priority; a cold driver takes ~2 s per large compute shader
|
||||
t = t < 1 ? 1 : t > LB_MAX_THREADS ? LB_MAX_THREADS : t;
|
||||
if ((size_t)t > n) t = (int)n;
|
||||
LB.t0 = now_ms();
|
||||
|
||||
Reference in New Issue
Block a user