mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Import the base tree (MIT, see LICENSE)
This commit is contained in:
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.ps1 text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
3
.githooks/pre-commit
Normal file
3
.githooks/pre-commit
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# Enable once per clone: git config core.hooksPath .githooks
|
||||
exec python tools/check_content.py --staged
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: ['https://www.donationalerts.com/r/another project']
|
||||
100
.github/ISSUE_TEMPLATE/1-crash.yml
vendored
Normal file
100
.github/ISSUE_TEMPLATE/1-crash.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: Crash
|
||||
description: The game closes, freezes or shows a FATAL message.
|
||||
labels: [bug, crash]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first: they cover the known crashes and their workarounds.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/another recompilation project/issues?q=is%3Aissue) for this crash.
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "NX2navi Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last-good
|
||||
attributes:
|
||||
label: Last version that worked
|
||||
description: If an earlier version did not crash at this point, which one.
|
||||
- type: textarea
|
||||
id: system
|
||||
attributes:
|
||||
label: System info
|
||||
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. On a Steam Deck, add LCD or OLED.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: Mods and cheats
|
||||
description: The folders in mods/ with their download links, or "none". Say whether it also crashes without them.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: when
|
||||
attributes:
|
||||
label: When does it crash?
|
||||
options:
|
||||
- At startup, before the Nintendo logo
|
||||
- Between the Nintendo logo and the title screen
|
||||
- While loading a save
|
||||
- In gameplay
|
||||
- In a cutscene
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Can you keep playing?
|
||||
options:
|
||||
- "Yes, it does not happen again after a restart, or I can avoid it"
|
||||
- "No, it happens every time at the same point"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What you did before the crash
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### The log
|
||||
|
||||
- The log of the run that crashed is `logs\nx2navi.log` in the folder of `nx2navi.exe` (F1, Advanced tab, Open log folder). Copy it before you start the game again: the next start renames it to `nx2navi.1.log`, and after five starts it is gone.
|
||||
- Attach the whole file. Do not cut or edit it: the crash report is at the end, the GPU and driver lines are at the start.
|
||||
- The log says `device lost` or `VK_ERROR_DEVICE_LOST` and the GPU is an NVIDIA one: set GPU pass marks to On (F1, Advanced tab, Diagnostics), restart the game, make it crash again and attach that log. The `vk: pass marks` lines then name what the GPU was drawing.
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Drag nx2navi.log into this field.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Save and input recording
|
||||
|
||||
When the crash comes back at one place in the game (an area, a shrine, a cutscene, a quest step), the save that gets there lets us crash it on our machine.
|
||||
|
||||
1. Zip the `profile` folder next to `nx2navi.exe` and attach it. Say which slot you load, or that you use the autosave.
|
||||
2. If it takes more than walking a few steps from the save: turn on Input recording (F1, Advanced tab, Diagnostics), restart the game, load the save, make it crash, and attach `input-record.txt` from the logs folder together with the `nx2navi.log` of that run.
|
||||
- type: textarea
|
||||
id: save
|
||||
attributes:
|
||||
label: Save and input recording
|
||||
description: Drag the zipped profile folder and input-record.txt into this field, when the crash comes back at one place.
|
||||
74
.github/ISSUE_TEMPLATE/2-graphics.yml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/2-graphics.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Graphics glitch
|
||||
description: Artifacts, missing or broken textures, flicker, wrong colours or shadows.
|
||||
labels: [bug, graphics]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first: they cover the known glitches on each GPU.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/another recompilation project/issues?q=is%3Aissue) for this glitch.
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "NX2navi Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last-good
|
||||
attributes:
|
||||
label: Last version that worked
|
||||
description: If an earlier version drew this correctly, which one.
|
||||
- type: textarea
|
||||
id: system
|
||||
attributes:
|
||||
label: System info
|
||||
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. It lists the settings you changed too. On a Steam Deck, add LCD or OLED.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: Mods and cheats
|
||||
description: The folders in mods/ with their download links, or "none". Say whether the glitch also shows without them. Turn off ReShade and other overlays that draw into the game before you report.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshot
|
||||
attributes:
|
||||
label: Screenshot or video
|
||||
description: Drag the file into this field. Say where in the game it was taken. Flicker needs a video.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: What looks wrong and when
|
||||
description: Does it show all the time, in one place, or after some time in play? Does it go away after a restart, or when you change a setting?
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Can you keep playing?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No, I cannot see enough to play"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Drag logs\nx2navi.log from the folder of nx2navi.exe into this field, from the run that showed the glitch. Copy it before you start the game again.
|
||||
validations:
|
||||
required: true
|
||||
108
.github/ISSUE_TEMPLATE/3-performance.yml
vendored
Normal file
108
.github/ISSUE_TEMPLATE/3-performance.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: Performance
|
||||
description: Low frame rate, freezes, hitches, stutter or long loading.
|
||||
labels: [performance]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first.
|
||||
|
||||
Shaders the setup did not prepare are built the first time they are needed, so a short hitch the first time an area or effect shows is expected. Report it when it happens again in the same place.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/another recompilation project/issues?q=is%3Aissue) for this problem.
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "NX2navi Recomp 0.1.0, game 1.4.3".
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last-good
|
||||
attributes:
|
||||
label: Last version that ran better
|
||||
description: If an earlier version was faster in the same place, which one.
|
||||
- type: textarea
|
||||
id: system
|
||||
attributes:
|
||||
label: System info
|
||||
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. It lists the settings you changed too. On a Steam Deck, add LCD or OLED.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: machine
|
||||
attributes:
|
||||
label: Machine
|
||||
options:
|
||||
- Desktop PC
|
||||
- Laptop, on mains power
|
||||
- Laptop, on battery
|
||||
- Steam Deck
|
||||
- Other handheld PC
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: Mods and cheats
|
||||
description: The folders in mods/ with their download links, or "none".
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: kind
|
||||
attributes:
|
||||
label: What kind of problem?
|
||||
options:
|
||||
- The frame rate is low all the time
|
||||
- Short freezes or hitches that come back in the same place
|
||||
- The frame rate is fine, but motion is not smooth
|
||||
- It gets slower the longer I play
|
||||
- Loading takes long
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: where
|
||||
attributes:
|
||||
label: Where and what you were doing
|
||||
description: For example "Lookout Landing, walking to the tower", "the Depths below Hyrule Castle", "fighting a Talus in the Sky". How long into the session it started.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: overlay
|
||||
attributes:
|
||||
label: Performance overlay
|
||||
description: Press F10 twice (or both stick clicks) for the full panel. Stay a minute in the place and write down the frame rate, the mean and the longest frame time, or attach a screenshot of the panel.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Recording the log
|
||||
|
||||
1. In the settings menu (F1), Advanced tab, Diagnostics: turn on Performance log. For freezes and hitches, also turn on Flight recorder. Restart the game: both take effect at the next start.
|
||||
2. Play at least three minutes in the place where it happens. The log gets a frame-time summary every 60 s, and a line for every frame over 50 ms that names what the frame waited for.
|
||||
3. With the flight recorder on, press F8 right after a hitch. It also saves on its own after every frame longer than the freeze threshold (100 ms), at most once per 10 s.
|
||||
4. Close the game with Quit game in the menu or the window's close button. Do not end it from Task Manager: the last summary is written when the game exits.
|
||||
5. Copy `logs\nx2navi.log` from the folder of `nx2navi.exe` before you start the game again.
|
||||
6. Each flight recorder save is a folder; the log line `flight: dump` names it. Zip the folders and attach them.
|
||||
7. Turn both off again afterwards: they cost a little frame time.
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: Drag nx2navi.log into this field.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: flight
|
||||
attributes:
|
||||
label: Flight recorder saves
|
||||
description: Drag the zipped folders into this field, for freezes and hitches.
|
||||
94
.github/ISSUE_TEMPLATE/4-bug.yml
vendored
Normal file
94
.github/ISSUE_TEMPLATE/4-bug.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: Other bug
|
||||
description: Sound, controls, the settings menu, the installer, or game logic.
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Check the pinned issues at the top of the issue list first. Crashes, graphics glitches and performance have their own templates.
|
||||
|
||||
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I read the pinned issues and [searched open and closed issues](https://github.com/another recompilation project/issues?q=is%3Aissue) for this bug.
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
options:
|
||||
- Audio
|
||||
- Controls and input
|
||||
- Settings menu and overlay
|
||||
- Installer
|
||||
- Game logic and physics
|
||||
- Something else
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The title line of the settings menu (F1), for example "NX2navi Recomp 0.1.0, game 1.4.3". For the installer, the version of NX2navi-Setup.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last-good
|
||||
attributes:
|
||||
label: Last version that worked
|
||||
description: If an earlier version did this correctly, which one.
|
||||
- type: textarea
|
||||
id: system
|
||||
attributes:
|
||||
label: System info
|
||||
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. On a Steam Deck, add LCD or OLED. For the installer, write your OS and the install disk.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: Mods and cheats
|
||||
description: The folders in mods/ with their download links, or "none". Say whether it also happens without them.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Can you keep playing?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No, it stops the game or a quest"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What happened and how to reproduce it
|
||||
description: What you expected, what happened instead, and the steps that lead there.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### What to attach for your area
|
||||
|
||||
Close the game with Quit game in the menu or the window's close button, then copy `logs\nx2navi.log` from the folder of `nx2navi.exe` before you start the game again.
|
||||
|
||||
**Audio.** Open `nx2navi.ini` in the folder of `nx2navi.exe` and add the line `trace = audio`; when the file already has `trace = perf` (Performance log on), change it to `trace = perf,audio`. Start the game, play at least two minutes where the sound goes wrong and close the game. The log then has an `audio:` line every 5 s with the buffer underruns. Name the output device and how it is connected (speakers, headphones, USB, Bluetooth, HDMI), and attach a short video with sound if you can. Remove the line afterwards.
|
||||
|
||||
**Controls and input.** Name the controller and how it is connected (USB, Bluetooth, Steam Input). Does it also happen with keyboard and mouse?
|
||||
|
||||
**Controls, game logic, anything you can trigger again.** Turn on Input recording (F1, Advanced tab, Diagnostics), restart the game, load a save, make the bug happen and close the game. Attach `input-record.txt` from the logs folder and a zip of the save you loaded: the `profile` folder next to `nx2navi.exe`. The recording replays what you did, so we can see the bug on our machine.
|
||||
|
||||
**Installer.** Attach the newest `install-*.log` from `temp\logs` in the folder of `NX2navi-Setup.exe` (on Linux: `.setup-temp/logs` in the install folder), and the log file the error message names.
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log and other files
|
||||
description: Drag nx2navi.log and the files your area asks for into this field.
|
||||
validations:
|
||||
required: true
|
||||
17
.github/ISSUE_TEMPLATE/5-feature.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/5-feature.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Feature request
|
||||
description: A setting, option or support for something the port does not do yet.
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: duplicates
|
||||
attributes:
|
||||
label: Existing issues
|
||||
options:
|
||||
- label: I [searched open and closed issues](https://github.com/another recompilation project/issues?q=is%3Aissue) and the Planned section of the README for this request.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: request
|
||||
attributes:
|
||||
label: What you want and why
|
||||
validations:
|
||||
required: true
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question or help with setup
|
||||
url: https://github.com/another recompilation project/discussions
|
||||
about: Installing, updating and configuring the port. Ask in Discussions.
|
||||
15
.github/workflows/content-guard.yml
vendored
Normal file
15
.github/workflows/content-guard.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Content guard
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: python tools/check_content.py
|
||||
76
.gitignore
vendored
Normal file
76
.gitignore
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# Reference checkouts of upstream projects (cloned locally, never committed)
|
||||
/refs/
|
||||
|
||||
# Downloaded toolchain binaries and Python environments
|
||||
/toolchain/
|
||||
/.venv/
|
||||
/.venv-py314/
|
||||
__pycache__/
|
||||
|
||||
# Personal paths to game files and keys
|
||||
/config.local.toml
|
||||
|
||||
# Game inputs and everything derived from them. Never commit these.
|
||||
# tools/check_content.py enforces the same rule in CI and the pre-commit hook.
|
||||
/game/
|
||||
/build/
|
||||
/generated/
|
||||
*.xci
|
||||
*.xcz
|
||||
*.nsp
|
||||
*.nsz
|
||||
*.nca
|
||||
*.ncz
|
||||
*.tik
|
||||
*.keys
|
||||
*.nso
|
||||
*.npdm
|
||||
*.zs
|
||||
*.pack
|
||||
*.sarc
|
||||
*.bfres
|
||||
*.bntx
|
||||
*.bnsh
|
||||
*.webm
|
||||
|
||||
# Build outputs
|
||||
*.exe
|
||||
*.dll
|
||||
*.pdb
|
||||
*.ilk
|
||||
*.obj
|
||||
*.o
|
||||
*.lib
|
||||
*.a
|
||||
*.so
|
||||
*.so.*
|
||||
*.AppImage
|
||||
|
||||
# .NET build output of the shader translator (tools/build_xlate.py)
|
||||
/tools/xlate/bin/
|
||||
/tools/xlate/obj/
|
||||
|
||||
# Coding-agent setup and working notes (skills, hooks, playbook, backlog, research notes, milestone plans): local to
|
||||
# the maintainer's machine. tools/worktree.py links .claude and notes into task worktrees.
|
||||
/.claude/
|
||||
/.agents/
|
||||
/CLAUDE.local.md
|
||||
/skills-lock.json
|
||||
/notes/
|
||||
/docs/milestones/
|
||||
|
||||
# Editor settings
|
||||
.vs/
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Patch leftovers, logs and OS files
|
||||
*.orig
|
||||
*.rej
|
||||
*.log
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
.DS_Store
|
||||
|
||||
# Agent task worktrees (tools/worktree.py)
|
||||
/.worktrees/
|
||||
34
AGENTS.md
Normal file
34
AGENTS.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# NX2navi Recomp
|
||||
|
||||
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).
|
||||
|
||||
## Game content boundary
|
||||
|
||||
This repository is public. Game data lives outside it and every derived file stays in ignored folders.
|
||||
|
||||
- The user's game and key paths are in `config.local.toml` (ignored). Pass the keys file path to tools; the tools read it themselves. Key values stay out of your output, logs, commands and files.
|
||||
- Everything extracted or generated goes under `build/` or `generated/`.
|
||||
- Every file the code generator emits carries the marker defined as `GENERATED_MARKER` in `tools/check_content.py`.
|
||||
- Run `python tools/check_content.py --staged` before proposing a commit. It is the same check CI runs.
|
||||
|
||||
## Reusing code
|
||||
|
||||
`refs/` holds the upstream checkouts (`tools/fetch_refs.sh` recreates it). Read-only.
|
||||
|
||||
- Copy only MIT, BSD, ISC, zlib or Apache-2.0 code, keep its header, and add a row to `THIRD_PARTY_NOTICES.md`.
|
||||
- the hardware, suyu, OpenSmash-Ultimate and other GPL code: read for behaviour, write our own.
|
||||
- `tools/vendor/` stays byte-identical to upstream; Windows changes go in `tools/extract.py`.
|
||||
|
||||
## Code
|
||||
|
||||
- Build C with MSYS2 UCRT64 Clang (`C:\msys64\ucrt64\bin`). Generated C uses GNU extensions, so `cl.exe` is out. Call Ninja from that directory by full path or put it first on PATH.
|
||||
- Two Python environments: `.venv` (3.12) for the lifter and the build, `.venv-py314` for the extraction and shader scripts, which need `compression.zstd`.
|
||||
- Generated C is never edited by hand: change the generator (`recomp/`).
|
||||
- Every lifter change passes the differential tests against Unicorn ([docs/building.md](docs/building.md), "Checks"). After changing `recomp/jumptables.py`, `tests\jumptables_check.py` reports 0 disagreements on 1.2.1 and 1.4.3.
|
||||
- `recomp/layout.py` alone decides module bases.
|
||||
- Graphics bugs: [docs/maxwell.md](docs/maxwell.md) lists the Maxwell behaviour the Vulkan path must match and the NX2navi bugs other ports fixed.
|
||||
- Comments say what the code does and why, in words that stand on their own. Cite our code by file plus function (a line number may follow as a hint) and upstream code by project, file and function: `refs/` moves, so its line numbers rot. Commit hashes belong to the attribution of copied code.
|
||||
|
||||
## Writing
|
||||
|
||||
Code, comments, docs and commit messages are in English. Commit messages are plain sentences without type prefixes.
|
||||
42
CONTRIBUTING.md
Normal file
42
CONTRIBUTING.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Contributing
|
||||
|
||||
## The one hard rule
|
||||
|
||||
Nothing that came out of the game goes into the repository: no game files, keys, extracted data, recompiled C, compiled binaries, shaders, shader caches, fonts, audio or movies. Not in commits, not in issues, not in pull requests. Only information about the game goes in: addresses, hashes, patch code we wrote, shader and bind-state identifiers.
|
||||
|
||||
Two checks enforce this:
|
||||
|
||||
- CI runs `tools/check_content.py` on every push and pull request.
|
||||
- The same check runs before each local commit once you enable the hook:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
A failing check is never bypassed with `--no-verify`. If it flags a file by mistake, change the check in a separate pull request and say why.
|
||||
|
||||
Links to game files, keys or firmware in issues or pull requests get the issue closed and the content removed.
|
||||
|
||||
## Code from other projects
|
||||
|
||||
Code may be copied only from projects under MIT, BSD, ISC, zlib or Apache-2.0. Each copy keeps its original header and gets a row in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) with the source commit.
|
||||
|
||||
GPL projects (the hardware, suyu, OpenSmash-Ultimate, Unicorn inside the game binary) can be read to understand behaviour, but their code is not copied. Unicorn is fine as a separate test tool.
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Use the issue form. Include the port version and game update from the title line of the settings menu (F1), your GPU and driver (Copy system info in the menu's Advanced tab copies them), the log (`logs\nx2navi.log` next to `nx2navi.exe`; after a crash, copy it before you start the game again), and the steps to reproduce. Screenshots are fine; recordings of long gameplay are not needed.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- One topic per pull request.
|
||||
- Describe the problem, then what changed.
|
||||
- Say how you tested it: which version, which scene, how long.
|
||||
- A change to the lifter (`recomp/`) or the CPU headers passes the differential tests in [docs/building.md](docs/building.md), "Checks".
|
||||
- Commit messages are plain sentences, without `feat:` or other prefixes.
|
||||
- AI-assisted code is welcome. You are responsible for having read and run it.
|
||||
|
||||
## Code and docs
|
||||
|
||||
- Code, comments, commit messages and documentation are in English.
|
||||
- Generated code is never edited by hand: change the generator.
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Valiantsin Dzerakh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
92
README.md
Normal file
92
README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# NX2navi Recomp
|
||||
|
||||
**NX2navi Recomp** 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)).
|
||||
|
||||
> [!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.
|
||||
>
|
||||
> 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.
|
||||
|
||||
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 Recomp" with the play time), off by default, own application id supported
|
||||
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
|
||||
- Keyboard, mouse and gamepad with remapping, rumble and gyro
|
||||
- Stereo or 5.1 surround sound output
|
||||
- Xbox button prompts: LB, RB, LT, RT, Menu and View, and A, B, X and Y as labelled on the button to press, with either button layout
|
||||
- Shaders prepared at install instead of during play
|
||||
- Mods made for emulators: unpack a mod into the `mods` folder next to `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
|
||||
|
||||
## Installing
|
||||
|
||||
### Windows
|
||||
|
||||
Requirements: Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA machines get a faster build), a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, about 36 GiB free during setup (20 GiB after).
|
||||
|
||||
1. Download `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 Recomp 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
|
||||
|
||||
## 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.
|
||||
|
||||
NX2navi 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).
|
||||
|
||||
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.
|
||||
107
THIRD_PARTY_NOTICES.md
Normal file
107
THIRD_PARTY_NOTICES.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Third-party notices
|
||||
|
||||
Code copied or adapted from another project is listed here with its source, the exact commit, its license, and the files it went into. Each copied file keeps its original copyright header.
|
||||
|
||||
| Component | Source and commit | License | Used in |
|
||||
|---|---|---|---|
|
||||
| MacNativeGames extraction scripts and verification hashes | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11`, Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/vendor/macnativegames/` (unmodified), `tools/data/expected-v*.json` |
|
||||
| LAS Recomp lifter, code generator, FP/SIMD lowering, CPU headers and differential tests | https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f`, Copyright (c) 2026 Peiikai | MIT | `recomp/lifter.py`, `recomp/simd_c.py`, `recomp/nso.py`, `recomp/gen.py`, `recomp/module.py`, `tests/jumptables_check.py`, `runtime/cpu.h`, `runtime/simd.h`, `runtime/simd_fp.h`, `tests/diff/` |
|
||||
| LAS Recomp host runtime: NSO loader, host-function table, guest-space reservation and re-execution, guest thread start | https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f`, Copyright (c) 2026 Peiikai | MIT | `runtime/loader.c`, `runtime/hostfn.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/boot.c`; `runtime/vk_device.c`, `runtime/gfx_window.c`, `runtime/tex_astc.c` (`runtime/nvn/astc.c`; its decode ported to GLSL in `runtime/shaders/astc_decode.comp`, compiled into `runtime/astc_decode_spv.h`, and its tables copied into `runtime/vk_astc.c`), `runtime/vk_formats.c` (format table of `runtime/nvn/vulkan/textures.c`), `runtime/vk_memory.c` (page write generations of `runtime/nvn/vulkan/exec.c`, deferred destruction of `textures.c`), `runtime/vk_texture.c` (image creation, uploads and samplers of `runtime/nvn/vulkan/textures.c`), `runtime/vk_sync.c` (timeline submissions and waits of `runtime/nvn/vulkan/exec.c`), `runtime/vk_pipeline.c` (blend, compare and stencil mappings, vertex format table with its 3-component fallback and pipeline cache file of `runtime/nvn/vulkan/exec.c` and `device.c`), `runtime/vk_exec.c` (pass begin and end, scissor clip and attachment clears of `runtime/nvn/vulkan/exec.c`), `runtime/vk_exec_xfer.c` (copies of `runtime/nvn/vulkan/exec.c`), `runtime/vk_exec_draw.c` (uniform buffer spans, texture and sampler descriptors and dispatch flow of `runtime/nvn/vulkan/exec.c`); in-game menu: `runtime/ui/menu.cpp` (Dear ImGui Vulkan set-up of `runtime/nvn/vulkan/imgui_glue.cpp`) |
|
||||
| MacNativeGames NX2navi host runtime: dynamic linking, identity check, module permissions, guest memory and SVCs, threads, IPC, audio renderer, system services, RomFS and save storage, boot sequence, the NVN proc name list | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/runtime/`, `games/<game>/scripts/prepare_runtime.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/loader.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/svc.c`, `runtime/ipc.c`, `runtime/ipc.h`, `runtime/audren.c`, `runtime/audren.h`, `runtime/audio_mixer.c`, `runtime/audio_effects.c`, `runtime/audio_effects.h`, `runtime/audio_reverb.c`, `runtime/audio_reverb.h` (SDL3 output replaces CoreAudio), `runtime/ov_system.c` (also `shared_font.c`: the BFTTF layout behind nn::pl), `runtime/ov_input.c` (also `hid_feedback.c`: vibration and six-axis handles and calls), `runtime/motion_fusion.c` (six-axis fusion of `motion_fusion.c`), `runtime/ov_media.c` (hardware Opus decoder over libopus), `runtime/swkbd_win.c` (`swkbd_host.c`: ShowKeyboardArg offsets, UTF-8/UTF-16 conversion, disabled-key filter, result codes; Win32 window replaces the macOS text bar), `runtime/ov_gfx.c`, `runtime/romfs.c`, `runtime/romfs_path.h`, `runtime/fs_host.c`, `runtime/fs.h`, `runtime/boot.c`; `runtime/gfx_window.c`; `tools/gen_nvn_procs.py`, `runtime/nvn_procs.inc` (`NVN/names.txt` and the `NVN_EXTRA` names of `nx2navi_nvn_ext.c`) |
|
||||
| MacNativeGames NX2navi frame-rate patches: 60 fps present-interval controller, real-time frame step and its present sampling, hair and cloth clock, trampoline slow-motion check (instruction words and 1.2.1 sites), player position chain; quick-menu capture background RomFS overlay | https://github.com/ly05010419/MacNativeGames @ `ebe621de99bfc48f3c42f2d2aa73e04fbcc6eae2` (`games/<game>/runtime/game_patches.c`, `game_version.h`, `world_survey.c`, `games/<game>/scripts/make_romfs_patches.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/frame_rate.c`, `recomp/patches-v121.txt`, `tools/make_romfs_patches.py` |
|
||||
| MacNativeGames NX2navi 1.4.3 fade fix: the four distance / LOD and pre-actor fade sites, their instruction words and the floor of the fade step at 1 | https://github.com/ly05010419/MacNativeGames @ `3e9e7f67fdfdd3404134dcfa0f24969a9425a33f` (`games/<game>/runtime/game_patches_v143.c` patch_fade_increment), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set fade; the floor without flags, clz >> 5, as in pull request #78) |
|
||||
| MacNativeGames NX2navi 1.2.1 cursor and aim steps: the map cursor, vertical object aim and vertical gyro aim sites, their instruction words and the step scaled by the frame ratio | https://github.com/ly05010419/MacNativeGames @ `363a6eb50434387f7d623349934bcf1e910be48f` (`games/<game>/runtime/game_patches.c` patch_cursor_speed), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v121.txt` (set cursor) |
|
||||
| MacNativeGames NX2navi aspect ratio: the camera and viewport sites that hand the perspective projection its aspect ratio, and the UI layout edits for a canvas wider than 16:9 | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/<game>/runtime/aspect_patch.h`, `aspect_math.h`, `game_patches_v143.c`, `games/<game>/scripts/ultrawide_layout.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set aspect), `tools/ultrawide_layout.py`, `tools/make_romfs_patches.py` (--aspect) |
|
||||
| MacNativeGames NX2navi button icons: the Xbox controller's shoulder buttons, triggers, Menu, View and d-pad drawn with letters from the game's own text fonts, the .bfotf key, the button guide's images and their block-linear layout, the resource size table raised for the archives that grew | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/<game>/scripts/make_button_icons.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/button_icons.py`, `tools/make_romfs_patches.py` (--prompts) |
|
||||
| RenoDX NX2navi mod (game mod by souperman9, tonemapper by Musa, RenoDX framework by ShortFuse, RenoVK fork by Ritsu): the approach of the HDR tonemap (the game's 1 - e^-x curve kept below a pivot and continued along its tangent above it, the saturation grade applied to the colour scaled into 0..1) and the observation that the passes after the tonemap keep values above 1. No code copied; the shader is our own | https://github.com/clshortfuse/renodx @ `b6f87272ad1bfdc0e6444910a93606815944a085` (`src/games/<game>/tonemap.slang`, `addon.cpp`), Copyright (c) 2025 Carlos Lopez Jr. | MIT | `runtime/shaders/hdr_tonemap.frag`, `runtime/vk_hdr.c` |
|
||||
| an ultrawide mod, the community mod for emulators: the list of UI panes the ultrawide layout edits and the distances they move (data only, no code copied; the edit code is our own, and MacNativeGames' `ultrawide_layout.py` in the row above already carries the same values). The mod's repository URL and licence version are not recorded in this repository | an ultrawide mod (community mod; URL not recorded here) | GPL (version not recorded here) | `tools/ultrawide_layout.py` (`KEEP_ROOT`, `edits`, `anim_edits`) |
|
||||
| MacNativeGames NX2navi virtual-core preemption: asynchronous preemption of an owner in top-level game code, priority inheritance through guest mutexes, the SDK's preemption-disable protocol (TLR+0x100/+0x102, SVC 0x36), the core-wait diagnostics | https://github.com/ly05010419/MacNativeGames @ `33e8fa83052ea1ae09a3583137008e9797e12950` (`games/<game>/runtime/threads.c`, commits 8bf0893 and 19730cd), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/kernel.c` (SuspendThread and a register-saving stub replace SIGUSR2), `runtime/hostfn.c`, `runtime/svc.c` (host depth) |
|
||||
| MacNativeGames RomFS index builder (`NX2NAVIRFS1` layout) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/scripts/prepare_romfs_backend.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/romfs_index.py` |
|
||||
| d2r-mobile host thread environment (TLR, TLS, thread object), the origin of MacNativeGames' thread layer; d2r-mobile NVN host object layouts, command record format and NVN front end, as adapted for NX2navi in MacNativeGames' `d2r_gfx` copy | https://github.com/jhaizhou-ops/d2r-mobile @ `53e7c5bb9ca3ea53e2160005be8fbe920cd9cb53` (`dm/host/thread.c`, `dm/host/hle_thread.c`); the NVN layouts and front end via https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/third_party/d2r_gfx/gfx/nvn/nvn_obj.h`, the two types of `nvn_tex.h`, `hle_nvn.c`, `nx2navi_nvn_ext.c`, `nvn_impl.h`, `nvn_hmap.h`, `nvn_pool_index.h`, the request checks and mapping semantics of `nx2navi_vpool.c`, the generation stamps of `nvn_dirty.c`, texture mirroring and texel copies of `nvn_mtl.m`; licence in `games/<game>/third_party/d2r_gfx/LICENSE`), Copyright (c) 2026 jhaizhou | MIT | `runtime/kernel.c`; `runtime/nvn_obj.h`; `runtime/nvn_api.h`, `runtime/nvn_api_core.c`, `runtime/nvn_hmap.h`, `runtime/nvn_pool_index.h`; `runtime/nvn_api_obj.c` (also `nvn_pace.h`); `runtime/nvn_api_cmd.c`, `runtime/nvn_ext.c`; `runtime/vk_memory.c`, `runtime/vk_texture.c`; `runtime/vk_sync.c` (side effects written after GPU completion, `hle_nvn.c`); `runtime/vk_pipeline.c` (pipeline key and state mapping of `nvn_mtl.m` `pso_key_fill` / `pso_desc`, its `vfmt` vertex formats and the missing-varying fix-up of `msl_varyings`); `runtime/vk_exec.c`, `runtime/vk_exec.h` (op loop, pure-state ops and state defaults of `nvn_mtl.m` `mtl_exec` / `state_op` / `state_reset`); `runtime/vk_exec_xfer.c` (copy, clear, event, counter and conditional-render semantics of `nvn_mtl.m` `copy_t2b` / `copy_b2t` / `copy_t2t` / `clear_tex` / `cond_eval`); `runtime/vk_exec_draw.c` (bind ops, draw and dispatch flow, vertex streams of `nvn_mtl.m` `state_op` / `bind_vertex_streams` / `draw_inner` / `dispatch`) |
|
||||
| nxnative NVN front end (d2r-mobile's successor): texture view depth-stencil mode bits, stencil-view rule, `Downsample` and indirect draw and dispatch records | https://github.com/seedds/nxnative @ `d5b380b3a80495609c2e751bb994911a638935ad` (`dm/gfx/nvn/hle_nvn.c`, `nvn_obj.h`, `nvn_mtl.m`), Copyright (c) 2026 jhaizhou | MIT | `runtime/nvn_obj.h`, `runtime/nvn_ext.c`, `runtime/nvn_api_cmd.c`, `runtime/vk_exec_draw.c` |
|
||||
| nxnative spin wait search and gate: loops that only another thread can end, polls that take a spin lock, the gate's wait and hand-off rule; MacNativeGames' NX2navi port of the gate | https://github.com/seedds/nxnative @ `6d349844880eb0527be77d7e0e4b8d999c0ae2f4` (`dm/tools/patch_nso.py` "Spin waits", `dm/tools/tests/test_spin_waits.py`, `dm/host/hle_thread.c` dm_spin_wait), Copyright (c) 2026 seedds; https://github.com/ly05010419/MacNativeGames @ `a038e6be7608e6f4eba6fc0381026cebf24ef4a3` (`games/<game>/runtime/threads.c` runtime_spin_wait), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/spin_waits.py`, `tests/spin_waits_test.py`, `runtime/kernel.c` (rt_spin_gate) |
|
||||
| NVN type definitions reconstructed from Darksiders II DWARF information, with MacNativeGames' NX2navi size annotations | h1k421, via https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/third_party/d2r_gfx/gfx/nvn/include/nvn.h`), Copyright (c) 2020 h1k421 | MIT | `runtime/nvn.h` (unmodified) |
|
||||
| Ryujinx.Audio voice state and effects, as ported in MacNativeGames' audio renderer | https://github.com/Ryujinx/Ryujinx, via MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/runtime/audio_renderer.c`, `audio_effects.c`, `audio_reverb.c`; licence text in `games/<game>/third_party/licenses/Ryujinx-Audio.txt`), Copyright (c) Ryujinx Team and Contributors | MIT | `runtime/audren.c` |
|
||||
| REA agent skill `reverse-engineer-anything` | https://github.com/morluto/rea @ `799e8265b254e225dc1dabdd25e56f0ab7f86bd3`, Copyright (c) 2026 morluto | MIT | `.claude/skills/reverse-engineer-anything/`, `.agents/skills/reverse-engineer-anything/` (pinned in `.claude/skills/skills-lock.json`) |
|
||||
| Ryujinx shader translator 1.3.2 (`Ryujinx.Graphics.Shader`, `Spv.Generator`, `Ryujinx.Common`) with MacNativeGames' four optimizer patches and `nx2navi_xlate` tool | https://github.com/Ryujinx/Ryujinx, via MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`vendor/ryujinx`, `games/<game>/tools/nx2navi_xlate/`), Copyright (c) Ryujinx Team and Contributors; tool Copyright (c) 2026 jhaizhou | MIT | `tools/vendor/ryujinx/` (unmodified), `tools/xlate/`, `runtime/gfx_shader.c` (job layout from `tools/shader_census.py`; c2 handle ranges of `nx2navi_xlate/Service.cs` for the reflection), `runtime/vk_pipeline.c` (the translator's set/binding and support-buffer conventions) |
|
||||
| deko3d block-linear image layout, as ported in MacNativeGames' `nvn_tex.c` (Apple parts dropped, layout math unchanged; licence text in `games/<game>/third_party/d2r_gfx/LICENSE.deko3d`, reproduced in the file header) | https://github.com/devkitPro/deko3d (`source/dk_image.cpp`), via MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/third_party/d2r_gfx/gfx/nvn/nvn_tex.c`, `nvn_tex.h`), Copyright (C) 2018-2020 fincs | zlib | `runtime/nvn_tex.c`, `runtime/nvn_tex.h` |
|
||||
| MacNativeGames texture auto-layout goldens (40 original-SDK size and block-height cases) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/tests/texture_auto_layout/probe.c`), Copyright (c) 2026 jhaizhou | MIT | `runtime/nvn_tex_probe.c`; `runtime/nvn_probe.c` (four of the cases, through the procs) |
|
||||
| MacNativeGames block-linear deswizzler (address function; copy loop split out as `tex_bl_copy`) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/third_party/d2r_gfx/gfx/tex/nvn_deswizzle.c`), Copyright (c) 2026 jhaizhou | MIT | `runtime/tex_deswizzle.c`, `runtime/tex.h` |
|
||||
| SDL 3.4.18, dynamic link, `SDL3.dll` copied next to `nx2navi.exe`. Developer builds use the MSYS2 build (`mingw-w64-ucrt-x86_64-sdl3`, which imports `libiconv-2.dll`, LGPL-2.1, unmodified, copied beside it); the installer release ships the upstream release `SDL3-3.4.18-win32-x64.zip`, which imports no libiconv | https://libsdl.org, Copyright (c) 1997-2026 Sam Lantinga | zlib | `runtime/input_win.c` (gamepad input, rumble, gyro and accelerometer) |
|
||||
| libopus (MSYS2 `mingw-w64-ucrt-x86_64-opus`), dynamic link, `libopus-0.dll` copied next to `nx2navi.exe` | https://opus-codec.org, Copyright (c) 2001-2023 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon | BSD-3-Clause | `runtime/ov_media.c` (nn::codec hardware Opus decoder) |
|
||||
| Dear ImGui 1.91.9 (core and Vulkan back end, unmodified), as vendored by LAS Recomp | https://github.com/ocornut/imgui, via https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f` (`third_party/imgui/`), Copyright (c) 2014-2025 Omar Cornut | MIT | `runtime/third_party/imgui/` (compiled into `nx2navi.exe` for the in-game menu, `runtime/ui/`) |
|
||||
| SavageWindRecomp settings menu: two-pane option rows (row look, left / right stepping, hold speed-up, description pane), the menu style and tab bar | https://github.com/Revivedx/SavageWindRecomp @ `575ad88e02f9a89a9a885d90039d48af33c982f4` (`runtime/ui/option_rows.mm`, `runtime/ui/menu.mm`), Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors, Copyright (c) 2026 Peiikai | MIT | `runtime/ui/option_rows.cpp`, `runtime/ui/option_rows.h`, `runtime/ui/menu.cpp` |
|
||||
| SavageWindRecomp performance overlay: the frame ring and its one-second summary, the NVML calls, the sampling loop, the full panel's layout | https://github.com/Revivedx/SavageWindRecomp @ `575ad88e02f9a89a9a885d90039d48af33c982f4` (`runtime/perfmon.c`, `runtime/perfmon.h`, `perf_overlay_draw` in `runtime/ui/menu.mm`), Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors, Copyright (c) 2026 Peiikai | MIT | `runtime/perfmon.c`, `runtime/perfmon.h`, `runtime/ui/menu.cpp` |
|
||||
| Roboto Medium font (Dear ImGui's `misc/fonts/Roboto-Medium.ttf`, unmodified) | https://fonts.google.com/specimen/Roboto, via https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f` (`third_party/imgui/misc/fonts/`), Copyright 2011 Google Inc. | Apache-2.0 | `runtime/ui/fonts/Roboto-Medium.ttf` (licence text in `runtime/ui/fonts/LICENSE-Roboto.txt`), embedded by `runtime/ui/menu.cpp` |
|
||||
| Vulkan Memory Allocator 3.4.0 (MSYS2 `mingw-w64-ucrt-x86_64-vulkan-memory-allocator`), header-only, compiled into `nx2navi.exe` | https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator, Copyright (c) 2017-2026 Advanced Micro Devices, Inc. | MIT | `runtime/vk_vma.cpp` (the allocator behind `runtime/vk_memory.c`) |
|
||||
| MacNativeGames host movie decoder (`movie::Decoder` for VP9 over libvpx: entry list, buffer and event semantics, NV12 output format, PTS pacing) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/runtime/movie_host.c`, `movie_host.h`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/ov_movie.c`, `runtime/movie.h` |
|
||||
| libvpx 1.17.0 (MSYS2 `mingw-w64-ucrt-x86_64-libvpx`), static link (`libvpx.a`), no DLL shipped; the release notices must carry its BSD licence text and the WebM Project's `PATENTS` grant | https://chromium.googlesource.com/webm/libvpx, Copyright (c) 2010, The WebM Project authors | BSD-3-Clause | `runtime/ov_movie.c` (VP9 decoding of the `Event/Movie` WebM files) |
|
||||
| MacNativeGames NX2navi resolution patches: output, swapchain and scene size sites and instruction words for 1.2.1 and 1.4.3, the data-page layout, the HeapDef heap names and sizes | https://github.com/ly05010419/MacNativeGames @ `aaf6ba1f494fabaa209d049e528d6f923dfe822a` (`games/<game>/runtime/game_patches.c`, `game_patches_v143.c`, `game_version.h`, `games/<game>/scripts/make_output_res_overlay.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v121.txt`, `recomp/patches-v143.txt`, `runtime/resolution.c` |
|
||||
| AMD FidelityFX Super Resolution 1.0 (`ffx_a.h`, `ffx_fsr1.h`, unmodified; licence text in `runtime/shaders/ffx/LICENSE.txt`) | https://github.com/GPUOpen-Effects/FidelityFX-FSR @ `a21ffb8f6c13233ba336352bdff293894c706575`, Copyright (c) 2021 Advanced Micro Devices, Inc. | MIT | `runtime/shaders/ffx/`, `runtime/shaders/fsr_easu.comp`, `runtime/shaders/fsr_rcas.comp` (compiled into `runtime/fsr_easu_spv.h`, `runtime/fsr_rcas_spv.h`) |
|
||||
| AMD Display Library (ADL) declarations: `AdapterInfo`, `ADLSingleSensorData`, `ADLPMLogDataOutput`, the `ADL_PMLOG_*` sensor numbers and the `ADL2_*` function signatures | https://github.com/GPUOpen-LibrariesAndSDKs/display-library @ `84c881cb2136f04287eb3af12de00a9e2867aa9e` (`include/adl_structures.h`, `include/adl_defines.h`, `include/adl_sdk.h`), Copyright (c) 2016 - 2022 Advanced Micro Devices, Inc. | MIT | `runtime/perfmon.c` |
|
||||
| Zstandard 1.5.7 (MSYS2 `mingw-w64-ucrt-x86_64-zstd`), static link | https://github.com/facebook/zstd, Copyright (c) Meta Platforms, Inc. and affiliates | BSD-3-Clause | `runtime/resolution.c` (the HeapDef table), `runtime/ov_zstd.c` (the game's zstd frames decoded on the host) |
|
||||
| musl `fma.c`, `fmaf.c` and `scalbn.c`, adapted into one file for the x86-64-v2 build | https://github.com/kraj/musl @ `5a56ad13e1e0790c06122978aad2a14d6b9efc1a` (mirror of https://git.musl-libc.org/cgit/musl), Copyright (c) 2005-2020 Rich Felker and contributors | MIT | `runtime/fma.c` (the correctly rounded fma/fmaf the FP lowering needs on CPUs without FMA3) |
|
||||
|
||||
## Compiler runtime in a source build
|
||||
|
||||
A build with the MSYS2 UCRT64 toolchain of `docs/building.md` links these into `nx2navi.exe`. The installer release builds with llvm-mingw instead (its row below).
|
||||
|
||||
| Component | Version | License | How it is used |
|
||||
|---|---|---|---|
|
||||
| GCC runtime (`libstdc++`) | 16.2.0 | GPL-3.0 with the GCC Runtime Library Exception | linked into `nx2navi.exe` |
|
||||
| winpthreads (mingw-w64) | 14.0.0 | MIT / BSD | linked into `nx2navi.exe` |
|
||||
| libiconv | 1.19 | LGPL-2.1+ | `libiconv-2.dll`, unmodified, next to `nx2navi.exe`: MSYS2's `SDL3.dll` imports it |
|
||||
|
||||
## Build-time tools (not distributed)
|
||||
|
||||
These tools run on the developer's or player's machine during the build. They are not part of this repository or of any release.
|
||||
|
||||
| Tool | Source | License | Purpose |
|
||||
|---|---|---|---|
|
||||
| hactool (the installer release ships it, see below) | https://github.com/SciresM/hactool @ `1d64a83450e025622f3468c28fc4164dad2c5ef6`, with patches from MacNativeGames | ISC | NCA and update container extraction |
|
||||
| Capstone | https://www.capstone-engine.org | BSD-3-Clause | ARM64 disassembly for analysis |
|
||||
| Unicorn | https://www.unicorn-engine.org | GPL-2.0 (Python bindings BSD) | Differential testing of the lifter; never linked into the game |
|
||||
|
||||
## Distributed in the installer release
|
||||
|
||||
`tools/make_release.py` assembles these into the release folder of `docs/installer.md`. Versions and sha256 of every download are pinned in that script; licence texts are in the folder's `licenses/` and in each component's own folder.
|
||||
|
||||
| Component | Source | License | Where in the release |
|
||||
|---|---|---|---|
|
||||
| Tcl/Tk 9.0.4, rebuilt with Xft for the Linux installer | https://github.com/tcltk/tcl and https://github.com/tcltk/tk, tag `core-9-0-4`; archives pinned by SHA-256 in `tools/make_release_linux.py` | Tcl/Tk licence (BSD-style) | `python/lib/`; licence texts in `licenses/tcl-license.terms` and `licenses/tk-license.terms` |
|
||||
| libXft 2 and libXrender 1, the sniper SDK sysroot's shared libraries, bundled beside the Xft Tk of the Linux installer; their Debian `copyright` files go into `licenses/` | https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20261021.266025/ | HPND (MIT-style) | `python/lib/`; `licenses/libXft-copyright.txt`, `licenses/libXrender-copyright.txt` |
|
||||
| CPython 3.14.9 embeddable package, with `tkinter` and Tcl/Tk 9.0.4 taken from the python.org 3.14.9 installer's component MSI (unpacked, never run); includes `vcruntime140*.dll` (Microsoft, redistributable with Python), OpenSSL, libffi, SQLite, zlib, libtommath | https://www.python.org/downloads/release/python-3149/ | PSF-2.0 (Tcl/Tk: Tcl/Tk licence, BSD-style) | `python/` |
|
||||
| numpy 2.5.4 (with its bundled OpenBLAS), lz4 4.4.5, cryptography 50.0.2 (with its bundled OpenSSL), fontTools 4.68.0, cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | BSD-3-Clause; lz4 BSD-3-Clause; cryptography Apache-2.0 OR BSD-3-Clause; fontTools MIT; cffi MIT; pycparser BSD-3-Clause | `python/Lib/site-packages/` |
|
||||
| llvm-mingw 20261020 (LLVM/Clang/LLD 23, mingw-w64, libc++, libunwind, compiler-rt), x86_64 subset | https://github.com/mstorsjo/llvm-mingw/releases/tag/20261020 | Apache-2.0 with LLVM exception; mingw-w64 ZPL-2.1/BSD/public domain; libc++ and libunwind Apache-2.0 with LLVM exception and MIT/UIUC; winpthreads MIT | `llvm/` |
|
||||
| SDL 3.4.18 upstream Windows release (`SDL3.dll`) and mingw development package (headers, `libSDL3.dll.a`) | https://github.com/libsdl-org/SDL/releases/tag/release-3.4.18 | zlib | `bin/SDL3.dll`, `runtime/deps/` |
|
||||
| libopus 1.6.1 (MSYS2 `mingw-w64-ucrt-x86_64-opus` 1.6.1-2; `libopus-0.dll` imports only KERNEL32 and the UCRT api-ms-win-crt DLLs), headers and import library | https://repo.msys2.org/mingw/ucrt64/ | BSD-3-Clause | `bin/libopus-0.dll`, `runtime/deps/` |
|
||||
| libvpx 1.17.0 (MSYS2 `mingw-w64-ucrt-x86_64-libvpx` 1.17.0-1), headers and static library compiled into `nx2navi.exe`; `libvpx-LICENSE.txt` and `libvpx-PATENTS.txt` come from the v1.17.0 source tag | https://github.com/webmproject/libvpx | BSD-3-Clause with the WebM patent grant | `runtime/deps/`, `licenses/` |
|
||||
| Zstandard 1.5.7 (MSYS2 `mingw-w64-ucrt-x86_64-zstd` 1.5.7-2), headers and static library compiled into `nx2navi.exe`; `zstd-LICENSE.txt` from the package | https://github.com/facebook/zstd | BSD-3-Clause | `runtime/deps/`, `licenses/` |
|
||||
| Vulkan-Headers and Vulkan-Loader 1.4.363.0 (MSYS2 packages; headers and `libvulkan-1.dll.a` only, no loader DLL) | https://github.com/KhronosGroup | Apache-2.0 OR MIT | `runtime/deps/` |
|
||||
| Vulkan Memory Allocator 3.4.0 (MSYS2 package), header | https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator | MIT | `runtime/deps/` |
|
||||
| Ninja 1.13.2 (`ninja-win.zip`) | https://github.com/ninja-build/ninja/releases/tag/v1.13.2 | Apache-2.0 | `bin/ninja.exe` |
|
||||
| hactool (SciresM @ `1d64a83450e025622f3468c28fc4164dad2c5ef6` plus `tools/hactool/portable.patch`, from MacNativeGames' `build_hactool()`: `MNG_TITLE_KEYS` and relaxed BKTR layout checks; our `tools/hactool/unicode.patch`: an embedded manifest that makes UTF-8 the process code page; our `tools/hactool/mbedtls-asm-clobbers.patch`: the clobbers the bundled mbedtls's x86-64 multiply asm lacks), built by `tools/build_hactool.py` with MSYS2 UCRT64 gcc; imports only KERNEL32 and the UCRT | https://github.com/SciresM/hactool | ISC | `bin/hactool.exe` |
|
||||
| `nx2navi_xlate.exe` (Ryujinx shader translator, NativeAOT build of `tools/xlate/`, see the Ryujinx row above) | built by `tools/build_xlate.py` | MIT | `bin/nx2navi_xlate.exe` |
|
||||
| MacNativeGames shader variant list | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/<game>/assets/shader_list.tsv`), Copyright (c) 2026 MacNativeGames contributors | MIT | `data/shader_list.tsv` |
|
||||
|
||||
## Distributed in the Linux release (AppImage)
|
||||
|
||||
`tools/make_release_linux.py` assembles these into the folder of `docs/installer.md` ("Linux release") and packs it as
|
||||
`NX2navi-Setup-x86_64.AppImage`. Versions and sha256 of every download are pinned in that script; licence texts are in the
|
||||
folder's `licenses/` and in each component's own folder.
|
||||
|
||||
| Component | Source | License | Where in the release |
|
||||
|---|---|---|---|
|
||||
| CPython 3.14.10, python-build-standalone 20261026 `install_only` build, with `tkinter` and the libraries it bundles (OpenSSL, libffi, SQLite, zlib, Tcl/Tk and others; their licences ship in the folder) | https://github.com/astral-sh/python-build-standalone/releases/tag/20261026 | PSF-2.0; bundled libraries under their own licences | `python/` |
|
||||
| numpy 2.5.4 (with the libraries its manylinux wheel bundles), lz4 4.4.5, cryptography 50.0.2, cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | as in the installer release row above | `python/lib/python3.14/site-packages/` |
|
||||
| LLVM 23.1.4 official Linux X64 release: `clang`, `lld`, `llvm-ar`, `llvm-ranlib`, `llvm-objcopy`, `llvm-strip` and the clang resource headers; `llvm-LICENSE.txt` from the `llvmorg-23.1.4` source tag | https://github.com/llvm/llvm-project/releases/tag/llvmorg-23.1.4 | Apache-2.0 with LLVM exception | `llvm/` |
|
||||
| Steam Runtime 3 "sniper" SDK sysroot 3.0.20261021.266025 (Debian 11 based), only the headers, libraries and objects a game build reads: glibc 2.31 (headers, `crt*.o`, `libc_nonshared.a`, link libraries), GCC 10 `libstdc++.a` and `libgcc`, SDL 3.4.18, libopus, libvpx 1.9 (static), the Vulkan loader's link library; their Debian `copyright` files go into `licenses/` | https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20261021.266025/ | glibc LGPL-2.1+; libstdc++ and libgcc GPL-3.0 with the GCC Runtime Library Exception; SDL zlib; libopus BSD-3-Clause; libvpx BSD-3-Clause with the WebM patent grant (`libvpx-PATENTS.txt` from the v1.17.0 source tag); Vulkan loader Apache-2.0 | `llvm/sysroot/`, `bin/libSDL3.so.0` (the sysroot's, copied next to the game by the setup), `licenses/` |
|
||||
| Vulkan-Headers 1.4.363 (source tag) | https://github.com/KhronosGroup/Vulkan-Headers/tree/v1.4.363 | Apache-2.0 OR MIT | `runtime/deps/include/` |
|
||||
| Vulkan Memory Allocator 3.4.0, header (the licence text is its header comment) | https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/tree/v3.4.0 | MIT | `runtime/deps/include/vma/` |
|
||||
| Zstandard 1.5.7, built from the release tarball into a static `libzstd.a` with the folder's clang and sysroot | https://github.com/facebook/zstd/releases/tag/v1.5.7 | BSD-3-Clause | `runtime/deps/` |
|
||||
| Ninja 1.13.2 (`ninja-linux.zip`) | https://github.com/ninja-build/ninja/releases/tag/v1.13.2 | Apache-2.0 | `bin/ninja` |
|
||||
| hactool (as in the installer release row above), built by `tools/build_hactool.py --cc` with the folder's clang and sysroot; includes its bundled mbedtls | https://github.com/SciresM/hactool | ISC; mbedtls Apache-2.0 | `bin/hactool` |
|
||||
| `nx2navi_xlate` (NativeAOT linux-x64 build of `tools/xlate/`, linked with the folder's clang, lld and sysroot) | built by `tools/build_xlate.py --llvm --sysroot` | MIT | `bin/nx2navi_xlate` |
|
||||
| AppImage type-2 runtime (static; includes libfuse and squashfuse), the AppImage's first part; appimagetool packs the folder and is not distributed | https://github.com/AppImage/type2-runtime | MIT; libfuse LGPL-2.1; squashfuse BSD-2-Clause | the AppImage header |
|
||||
| MacNativeGames shader variant list | as in the installer release row above | MIT | `data/shader_list.tsv` |
|
||||
| Weeping of the Empire menu cursor: eui's focus box read through ScreenMgr, BoxCursorMgr and the screens' node lists, AnimButton's hit test, the stale-hit-pane fallback to the parts panes, the three writes of moveBoxCursor, the structure offsets (1.0.0; the ScreenMgr instance placed in 1.4.3 by us) | https://github.com/daniel-madrid-07/weeping-of-the-empire @ `df0586cdc5702ca19bbebfa59697e0b6980f6237` (`engine/runtime/nx2navi_menu_cursor.cpp`), Copyright (c) 2026 daniel-madrid-07 | MIT | `runtime/menu_cursor.c` |
|
||||
18
config.example.toml
Normal file
18
config.example.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
# Copy to config.local.toml (ignored by git) and point it at your own files.
|
||||
|
||||
[input]
|
||||
# Base game 1.0.0, title id 0100F2C0115B6000, as NSP or XCI.
|
||||
base = 'C:\Games\NX2navi\base.nsp'
|
||||
# Cumulative update NSPs, title id 0100F2C0115B6800. Each is identified by its code hash; 1.4.3 and 1.2.1 are supported.
|
||||
# Leave the list empty for 1.0.0 only.
|
||||
updates = ['C:\Games\NX2navi\update-1.4.3.nsp']
|
||||
# Keys dumped from your own console. Never printed or copied by the tools.
|
||||
keys = 'C:\Switch\prod.keys'
|
||||
# Optional: a firmware NCA directory (an unpacked firmware, or an emulator's installed registered/ directory).
|
||||
# tools/extract_fonts.py takes the system shared fonts from it; the game needs them only with Chinese or Korean text.
|
||||
# The installer asks for the same folder (--firmware) when the language needs those fonts.
|
||||
# firmware = 'C:\Switch\firmware'
|
||||
|
||||
[work]
|
||||
# Extracted files, generated C and builds go here. Allow about 60 GB.
|
||||
dir = 'build'
|
||||
148
docs/architecture.md
Normal file
148
docs/architecture.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 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.
|
||||
|
||||
## 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).
|
||||
|
||||
### 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`
|
||||
(`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).
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
|
||||
#### 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`):
|
||||
|
||||
- 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).
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
### 5. Toolchain
|
||||
|
||||
- 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.
|
||||
|
||||
263
docs/building.md
Normal file
263
docs/building.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Building from source
|
||||
|
||||
Windows 10 or later, an x86-64 CPU with SSE4.2, and a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`. The
|
||||
default build targets x86-64-v3 (AVX2/FMA); a CPU without them builds with `--cpu v2` instead and runs slower (see
|
||||
"Build"). Allow about 60 GB for the work folder. Extraction wants about 55 GB free while it runs and keeps about 35 GB
|
||||
afterwards.
|
||||
|
||||
Players who only want to play can use the [installer](installer.md) instead: it builds the game on their machine
|
||||
without this toolchain.
|
||||
|
||||
## Toolchain
|
||||
|
||||
Versions are the ones the build was verified with; newer patch releases are fine.
|
||||
|
||||
| Tool | Version | Install | Used for |
|
||||
|---|---|---|---|
|
||||
| MSYS2 UCRT64 | clang/lld 23.1.3 | msys2.org installer to `C:\msys64`, packages below | compiling the generated C and the runtime |
|
||||
| Visual Studio Build Tools | 2026, C++ workload, Windows SDK 10.0.26100 | winget `Microsoft.VisualStudio.BuildTools` with `--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended` | the linker .NET NativeAOT calls |
|
||||
| .NET SDK | 10.0 | winget `Microsoft.DotNet.SDK.10` | the shader translator (`nx2navi_xlate.exe`) |
|
||||
| Vulkan SDK | 1.4.363 | winget `KhronosGroup.VulkanSDK` | validation layers, `vulkaninfo` |
|
||||
| uv | 0.13 | standalone installer from astral.sh | the two Python environments |
|
||||
| hactool | 1.4.0 @ `1d64a83` | `.venv\Scripts\python tools\build_hactool.py`, see below | NCA and update extraction |
|
||||
|
||||
MSYS2 packages, after `pacman -Syuu` (run it twice; the first pass updates the core and exits):
|
||||
|
||||
```sh
|
||||
pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-{clang,lld,ninja,gcc,make,pkgconf,sdl3,opus,libvpx,zstd,vulkan-devel,\
|
||||
vulkan-memory-allocator,vulkan-validation-layers,shaderc,spirv-tools} base-devel git unzip
|
||||
```
|
||||
|
||||
`ffmpeg` from MSYS2 is a GPLv3 build and is never linked; the movies go through libvpx.
|
||||
|
||||
PATH:
|
||||
|
||||
- Add `C:\Program Files (x86)\Microsoft Visual Studio\Installer` to the user PATH: NativeAOT needs `vswhere.exe`, or
|
||||
the publish fails at the link step (MSB3073).
|
||||
- Another `cmake` or `ninja` earlier on PATH (a 32-bit winlibs MinGW, for example) is picked up by accident. The build
|
||||
scripts call Ninja from `C:\msys64\ucrt64\bin` themselves; from a shell, put `C:\msys64\ucrt64\bin` first.
|
||||
|
||||
### hactool
|
||||
|
||||
`tools/build_hactool.py` fetches SciresM/hactool at `1d64a83` into `toolchain\src\hactool`, applies the three patches in
|
||||
`tools/hactool/` and builds a static `toolchain\hactool\hactool.exe` with MSYS2 UCRT64 gcc (a few seconds):
|
||||
|
||||
- `portable.patch`: the parts of MacNativeGames' `build_hactool()` that run on Windows, the `MNG_TITLE_KEYS` variable and
|
||||
the two BKTR fixes in `nca.c` for the subsection table padding and the subsection layout of newer cumulative updates;
|
||||
- `unicode.patch`: a manifest with `<activeCodePage>UTF-8</activeCodePage>`, so paths under a non-ASCII user profile
|
||||
reach it intact;
|
||||
- `mbedtls-asm-clobbers.patch`: the `memory` and `cc` clobbers the bundled mbedtls 2.6.1's x86-64 multiply asm lacks.
|
||||
Without them a clang build (the Linux release) fails every RSA signature check and the update merge stops.
|
||||
|
||||
hactool reads `-k <prod.keys>`; MSYS2 shells set `HOME`, which hides its default key location, so pass `-k` there.
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root, in PowerShell. Every step that depends on the game version takes it as `<ver>`: `v143` for
|
||||
1.4.3, `v121` for 1.2.1 or `v110` for 1.1.0. The tools default to `v121` when `--version` is left out. Saves load on any
|
||||
of them.
|
||||
|
||||
1. Python environments (once). `.venv` runs the lifter and the build; `.venv-py314` runs the extraction scripts, which
|
||||
need the stdlib `compression.zstd` of Python 3.14:
|
||||
|
||||
```powershell
|
||||
uv venv --python 3.12 .venv
|
||||
uv pip install --python .venv/Scripts/python.exe -r tools/requirements.txt
|
||||
uv venv --python 3.14 .venv-py314
|
||||
uv pip install --python .venv-py314/Scripts/python.exe -r tools/requirements-py314.txt
|
||||
```
|
||||
|
||||
2. Copy `config.example.toml` to `config.local.toml` and set the paths to your base game, the update and your console's
|
||||
keys file. `config.local.toml` is ignored by git. The tools read the keys file themselves.
|
||||
|
||||
3. Extract and verify. This writes `build\extracted\v0` (1.0.0) and `build\extracted\<ver>` for each update in
|
||||
`updates`, identified by its hash; a hash mismatch stops the run. With another `[work] dir` in `config.local.toml`,
|
||||
`extracted` goes there instead, and `build_game.py` reads the ExeFS and RomFS from there:
|
||||
|
||||
```powershell
|
||||
.venv-py314\Scripts\python tools\extract.py
|
||||
```
|
||||
|
||||
4. Shader translator (no game files; again after a change to its sources, and `tools\make_release.py` stops on an older
|
||||
one). This publishes `build\xlate\nx2navi_xlate.exe`:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\build_xlate.py
|
||||
```
|
||||
|
||||
5. Lift the modules to C, write the RomFS overlay (the frame-rate fix), then compile and link
|
||||
`build\<ver>\nx2navi.exe`:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python recomp\gen.py --exefs build\extracted\<ver>\exefs --out generated\<ver> --patches recomp\patches-<ver>.txt
|
||||
.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver>
|
||||
.venv\Scripts\python tools\build_game.py --version <ver>
|
||||
```
|
||||
|
||||
`--cpu v2` targets x86-64-v2 (SSE4.2, no AVX2/FMA) for older CPUs; the default `v3` needs AVX2. The v2 build links
|
||||
`runtime/fma.c`, a correctly rounded software fma the FP lowering needs, and runs slower. Inline assembly,
|
||||
target attributes and intrinsics are not bound by `-march`, so a v2 build runs `tools/check_isa.py` over the runtime
|
||||
objects before it links: any AVX, BMI or other instruction above x86-64-v2 outside a function listed there as
|
||||
dispatched by a runtime CPU check stops the build.
|
||||
|
||||
`gen.py` sizes its emit pool from the CPU count and the free memory; `--jobs N` sets it, and so does the environment variable `NX2NAVI_LIFT_JOBS=N`, which the installer's lift step honours too (it has no `--jobs`). When a worker runs out of memory, the pool restarts with half the workers (`emit: out of memory with N workers, retrying with M`), down to one; only an out-of-memory error with one worker fails the step.
|
||||
|
||||
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
|
||||
A compiler cache in front of clang shortens it further: with `SCCACHE` set to an sccache executable, `build_game.py`
|
||||
writes it into the ninja file ahead of `cc`, and every lift rewrites the generated chunks with the same contents, so
|
||||
a second build of the same version hits all of their objects:
|
||||
|
||||
```powershell
|
||||
$env:SCCACHE = "path\to\sccache.exe"
|
||||
.venv\Scripts\python tools\build_game.py --version <ver>
|
||||
```
|
||||
`--patches` builds in the frame-rate, resolution and other patch sets, switchable at run time. The link copies
|
||||
`SDL3.dll`, `libiconv-2.dll` and `libopus-0.dll` next to the exe.
|
||||
For Xbox button prompts, run `make_romfs_patches.py` twice more, with `--prompts xbox` and `--prompts xbox-label`:
|
||||
they write `build\<ver>\romfs-patches-xbox` and `romfs-patches-xbox-label` ([controls.md](controls.md), "Xbox
|
||||
button prompts"), which `build_game.py` copies next to `nx2navi.exe` like `romfs-patches`. `--prompts pc` writes
|
||||
`romfs-patches-pc`, the keyboard and mouse prompts ([controls.md](controls.md), "PC button prompts").
|
||||
|
||||
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
|
||||
for the shared shader list:
|
||||
|
||||
```powershell
|
||||
.venv-py314\Scripts\python tools\prepare_shaders.py build\<ver> --version <ver>
|
||||
```
|
||||
|
||||
Without this step the first launch compiles shaders and pipelines during play and stutters until the caches fill.
|
||||
The AMD driver keeps its pipeline cache per exe path and holds a limited amount: preparing another build folder can
|
||||
evict this one's. `nx2navi.exe --warm-shaders` rebuilds it from the shader cache.
|
||||
When the driver's shader compiler crashes on a variant (NVIDIA 615.78.08 on an RTX 5080 does, issue #30),
|
||||
`prepare_shaders.py` runs the driver step again with `--warm-shaders`, up to three times, without the variants the
|
||||
driver was building at the crash. `shader-prepare-skip.txt` next to the exe lists them with the GPU and driver
|
||||
version; they build at their first draw instead, another driver version builds them again, and so does deleting the
|
||||
file.
|
||||
|
||||
7. Run. The game keeps running when its window loses the focus (`assume_focus = 1` in `nx2navi.ini`, the default; the
|
||||
Game tab switches it). With 0 it pauses, as it does on the Switch when Home is pressed.
|
||||
|
||||
```powershell
|
||||
build\<ver>\nx2navi.exe
|
||||
```
|
||||
|
||||
Settings live in `nx2navi.ini` next to `nx2navi.exe` ([nx2navi.ini.example](../nx2navi.ini.example) lists them). Saves go to
|
||||
`build\profile` for `v121` and `build\profile-<ver>` for other versions (`build_game.py --profile` changes it).
|
||||
|
||||
### Portable folder
|
||||
|
||||
`.venv\Scripts\python tools\package_build.py --version <ver>` writes `build\<ver>-portable`, which runs on another of
|
||||
your machines without MSYS2, Python or .NET. `romfs.bin` stays out unless you pass `--with-romfs`; otherwise put it in
|
||||
`romfs\` next to the exe or point `NX2NAVI_ROMFS` at its folder. Run `nx2navi.exe --prepare-shaders` once in the copied
|
||||
folder. The folder holds code and data derived from your game: for your own machines only.
|
||||
|
||||
### Profile-guided build
|
||||
|
||||
`build_game.py --pgo <file>.profdata` compiles the generated code at -O2 with an execution profile, and the runtime
|
||||
with it too: hot paths laid out straight, hot calls inlined, code that never ran compiled for size. On 1.4.3 the fight
|
||||
bench ran 12-15% faster per frame and a fight outside the training about 6%. The installer builds with
|
||||
`data/pgo-<ver>.profdata` of the release when the update has one, else at -O1.
|
||||
|
||||
A profile only fits the generated C it was trained on: functions are matched by name and control-flow hash, so a
|
||||
function whose C changes loses its counts without a warning. Retrain after a lifter or generator change, from a clean
|
||||
`recomp/`. The instrumented build needs llvm-mingw, as MSYS2's clang has no profile runtime:
|
||||
|
||||
```powershell
|
||||
$llvm = "toolchain\llvm-mingw-<date>\llvm-mingw-<date>-ucrt-x86_64"
|
||||
.venv\Scripts\python tools\build_game.py --version v143 --out build\v143-pgogen --toolchain $llvm --pgo gen
|
||||
```
|
||||
|
||||
Play `build\v143-pgogen\nx2navi.exe` through varied scenes (fights, towns, riding, building, the Depths, menus) for 20-30
|
||||
minutes; it is slower than a normal build. It writes `build\v143-pgogen\pgo\*.profraw` every minute and at exit.
|
||||
Scripted runs from saves (`tools/run_game.py`) add to the same file. Then:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python tools\pgo.py merge build\v143-pgogen --version v143
|
||||
```
|
||||
|
||||
writes `build\pgo\pgo-v143.profdata` and its record (`.json`: the `recomp/` tree it fits). `make_release.py` ships every
|
||||
profile of `build\pgo` in `data\` and warns when a record names another `recomp/` tree than the one released.
|
||||
|
||||
## Linux (in progress)
|
||||
|
||||
The runtime builds and runs on x86-64 Linux with a window, input and menu on SDL3 (`*_sdl.c`) in place of the Win32
|
||||
files (`*_win.c`); `runtime/platform/linux.c` implements `platform.h`. Status: the selftest and the probes pass; 1.2.1
|
||||
plays at 60 fps on RADV (RX 7800 XT) and NVIDIA (RTX 3060); 1.4.3 builds, probes and plays on RADV (RX 6600, and the
|
||||
Vega 7 of a Ryzen 5 4600G at about 34 fps). Long play sessions and the Steam Deck itself are not verified yet.
|
||||
|
||||
Requirements:
|
||||
|
||||
- Linux 6.7 or later: the write watch uses userfaultfd write protection with `PAGEMAP_SCAN`. SteamOS 3.8 qualifies.
|
||||
- A Vulkan 1.3 driver with `VK_EXT_external_memory_host` (RADV, NVIDIA).
|
||||
- Packages on Arch:
|
||||
|
||||
```sh
|
||||
pacman -S --needed clang lld ninja make sdl3 opus libvpx zstd vulkan-headers vulkan-icd-loader \
|
||||
vulkan-validation-layers shaderc spirv-tools dotnet-sdk uv git
|
||||
```
|
||||
|
||||
Vulkan Memory Allocator is a single header (AUR `vulkan-memory-allocator`). Without it, `build_game.py` fetches the
|
||||
version the Windows release pins (3.4.0) into `toolchain/vma/` on first use and checks its hash.
|
||||
|
||||
The steps are the Windows ones with POSIX paths: `.venv/bin/python` in place of `.venv\Scripts\python`, and `nx2navi` in
|
||||
place of `nx2navi.exe`. `build_game.py` takes clang, lld and Ninja from PATH (or the ones in `<dir>/bin` with
|
||||
`--toolchain <dir>`) and links against the system libraries, with `libstdc++` static and an `$ORIGIN` rpath;
|
||||
`build_hactool.py` builds `hactool` with make. `tools/prepare_shaders.py` reads the shader list from
|
||||
`refs/MacNativeGames` at `origin/HEAD`, which must be the pinned commit with its history (the list format changed later).
|
||||
|
||||
The probes open a window, so from an SSH session they need the desktop session's display, or they stop at `gpu 0
|
||||
skipped: no graphics+compute+present queue family`: on Wayland `XDG_RUNTIME_DIR=/run/user/$(id -u)
|
||||
WAYLAND_DISPLAY=wayland-1 SDL_VIDEODRIVER=wayland` (the socket name is in `$XDG_RUNTIME_DIR`), on X11 `DISPLAY=:0`.
|
||||
|
||||
Memory: on Windows the GPU imports guest memory and `GetWriteWatch` tracks the CPU's writes to it. On Linux,
|
||||
re-protecting imported memory invalidates the driver's userptr buffer on every submit (about 1.3 ms per 16 MiB on
|
||||
amdgpu), and importing pins the pages, which drops their write protection. Each CPU-visible pool therefore gets a
|
||||
host-visible copy (shadow storage, `runtime/vk_memory.c`): the pages the CPU wrote go in before each submit, and the GPU's
|
||||
writes come back at its fence. In play that costs about 0.2 ms a frame and doubles the memory of those pools (about
|
||||
1.1 GB). `NX2NAVI_SHADOW=1` turns it on under Windows too. The Steam Deck's RADV has `VK_EXT_external_memory_host` as
|
||||
well: the shadow copies are chosen for the measured cost above, not for a missing extension.
|
||||
|
||||
The Deck's GPU (Van Gogh) has no native ASTC, so its textures go through the compute decoder (`runtime/vk_astc.c`,
|
||||
`NX2NAVI_ASTC`) as on desktop AMD GPUs without it; frame times measured there include it.
|
||||
|
||||
## Checks
|
||||
|
||||
Before a commit:
|
||||
|
||||
- `python tools/check_content.py --staged`: nothing game-derived is staged. CI runs the same check;
|
||||
`git config core.hooksPath .githooks` runs it before each local commit.
|
||||
- `build\<ver>\nx2navi.exe --selftest`: layout, dispatch table and host-function registry, without game files.
|
||||
|
||||
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
|
||||
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
|
||||
files); a change to the settings passes `nx2navi.exe --probe-cfg`.
|
||||
|
||||
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
|
||||
change passes these, all run with `.venv`:
|
||||
|
||||
| Test | Checks | Game files |
|
||||
|---|---|---|
|
||||
| `tests\diff\lift.py` | instructions sampled from the extracted modules; `--coverage` lists every word the lifter rejects or traps | yes |
|
||||
| `tests\diff\simd.py` | the whole FP/SIMD encoding space | no |
|
||||
| `tests\diff\atomic.py` | exclusive load/store sequences and ARMv8.1 atomics | no |
|
||||
| `tests\diff\func.py` | whole functions as the generator emits them, loops, jump tables and calls included (calls go to a stub that checks the registers at the call) | yes |
|
||||
| `tests\flags_check.py` | the function-local flag helpers of `runtime\cpu.h` against its packed ones | no |
|
||||
| `tests\excl_mt.py` | the exclusive monitor helpers under contention and ABA, on several threads | no |
|
||||
| `tests\jumptables_check.py` | `recomp/jumptables.py` against a Unicorn oracle; must report 0 disagreements, on 1.4.3 too | yes |
|
||||
|
||||
A `--cpu v2` build passes the same differential tests with `--cflags=-march=x86-64-v2`; they compile `runtime/fma.c`
|
||||
too, so they check the software fma the v2 build links rather than the C runtime's.
|
||||
|
||||
Runtime probes, each a mode of `nx2navi.exe` (the list is at the top of `runtime/main.c`): `--probe-kernel`,
|
||||
`--probe-vkexec`, `--probe-nvn-tex` and `--probe-shader` cover most runtime changes. `--probe-shader` and the draw
|
||||
part of `--probe-vkexec` translate shaders of the census below: they look for `build/<ver>/shaders/jobs.json` of the
|
||||
exe's own version first, then of the other one.
|
||||
|
||||
Shaders: `.venv-py314\Scripts\python tools\shader_census.py --version <ver> --translate` extracts every Maxwell shader
|
||||
to `build\<ver>\shaders\` and runs it through the translator and `spirv-val`; failure groups go to `failures.txt`.
|
||||
`tools\xlate_smoke.py` checks the translator alone, without game files.
|
||||
|
||||
## Reverse engineering (optional)
|
||||
|
||||
- Ghidra 12.1.2 with Adubbz's Ghidra-Switch-Loader in `toolchain\ghidra\` (JDK 21) opens the game modules (NSO).
|
||||
- `tools\fetch_refs.sh` clones the upstream projects this port builds on into `refs\`, read-only.
|
||||
74
docs/cheats.md
Normal file
74
docs/cheats.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Cheats
|
||||
|
||||
The port reads Atmosphère cheat files, the format Switch homebrew and the emulators use. It ships no cheat files: a cheat file goes into a mod in the `mods` folder next to `nx2navi.exe`, like any other mod ([mods.md](mods.md)). Open the menu (F1) and switch the cheats on the Cheats tab. The switches are saved under `[cheats]` in `nx2navi.ini`.
|
||||
|
||||
## Adding a cheat file
|
||||
|
||||
1. Find the cheat file for your update. Cheat sites and collections name it after the first 8 bytes of the game's build id:
|
||||
|
||||
| Update | File |
|
||||
|---|---|
|
||||
| 1.2.1 | `9B4E43650501A4D4.txt` |
|
||||
| 1.4.3 | `277178B7DBA1B6D4.txt` |
|
||||
|
||||
The Cheats tab names the file the installed game reads when no mod holds one, and `logs\nx2navi.log` says `cheats: no enabled mod holds cheats/<id>.txt`.
|
||||
2. Put it in the `cheats` folder of a mod, a folder of its own under `mods`:
|
||||
|
||||
```
|
||||
mods\
|
||||
My cheats\
|
||||
cheats\
|
||||
277178B7DBA1B6D4.txt
|
||||
```
|
||||
|
||||
A download laid out for the Switch's SD card or an emulator works as it is: unpack it into a folder of its own under `mods`, and the port finds `atmosphere\contents\0100F2C0115B6000\cheats`, `contents\0100F2C0115B6000\cheats` or `0100F2C0115B6000\cheats` inside it. The file name's case does not matter.
|
||||
3. Start the game. Cheats that write the game's memory work now.
|
||||
4. Cheats that change the game's code need the game rebuilt with them. When the game starts and finds such cheats it did not build, it opens the setup's rebuild window, which builds them in (a few minutes; the shaders stay prepared) and starts the game again. Until then they are greyed out with "after the rebuild". When the rebuild fails or you cancel it, the game starts without them and does not try again for the same cheats; "Rebuild the game now" on the Cheats tab tries again. `mods_rebuild = off` under `[mods]` in `nx2navi.ini` turns the automatic rebuild off; running `NX2navi-Setup.exe` on the same folder does the same job by hand. Saves and settings are kept.
|
||||
5. Switch the cheats on the Cheats tab of the menu.
|
||||
|
||||
The Cheats tab lists the cheats grouped by mod, in the order of the Mods tab. A mod switched off on the Mods tab, or one the port cannot use, contributes no cheats; the change applies at the next start. The Mods tab says how many cheats each mod brought.
|
||||
|
||||
## The file format
|
||||
|
||||
Standard Atmosphère: a `[name]` line starts a cheat, `{name}` a master cheat that is always on, and the cheat's opcodes follow as 8-digit hex words. A name with no opcodes is shown as a heading. The file's title line (the one with `TID=`) is not shown.
|
||||
|
||||
```
|
||||
[The Legend of Zelda: Ocarina of Time v1.4.3 TID=0100F2C0115B6000 BID=277178B7DBA1B6D4]
|
||||
|
||||
[Player]
|
||||
[A code cheat]
|
||||
040A0000 XXXXXXXX YYYYYYYY
|
||||
|
||||
[A data cheat (Hold ZL+X)]
|
||||
80000104
|
||||
580F0000 XXXXXXXX
|
||||
640F0000 00000000 YYYYYYYY
|
||||
20000000
|
||||
```
|
||||
|
||||
The words above are placeholders: `XXXXXXXX` an offset, `YYYYYYYY` a value.
|
||||
|
||||
Each cheat's switch is `cheat_<hash> = 1`, where the hash is FNV-1a 32 of `<mod folder name>/<cheat name>`. A name repeated in one file gets `#2`, `#3` and so on. The build names each code cheat the same way, so renaming a mod's folder resets its cheats' switches to off and makes the next start rebuild its code cheats. Rename the folder before you switch its cheats on.
|
||||
|
||||
## How cheats run
|
||||
|
||||
The game's code is compiled ahead of time, so a cheat cannot write new instructions into it while the game runs. The port sorts each cheat by what it writes (`tools/cheats.py` at setup, `runtime/cheats.c` in game):
|
||||
|
||||
| Kind | What the cheat does | How the port runs it |
|
||||
|---|---|---|
|
||||
| code | Opcode 0 writes into main's code | Built into the game at setup as a patch set (`recomp/patches.py`); switches at once. |
|
||||
| key | Code writes inside a button condition (`8` mask, writes, `21` else, the original words, `20`) | A patch set that is on while the cheat is on and the buttons are held. |
|
||||
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A, every presented frame while on. |
|
||||
| master | `{name}` with code writes | A patch set that is always on. |
|
||||
| restore | Writes back only the game's original instructions | Not needed: switching a cheat off restores them. Greyed out. |
|
||||
|
||||
A cheat whose code writes call or jump into a code cave the cheat writes itself (into main's zero padding) gets the cave inlined at the call site. The setup builds the code cheats of every mod in the `mods` folder, switched on or off, so switching a mod on later needs only a restart. A code cheat added or edited after the last build waits for the rebuild; a data cheat runs at the next start.
|
||||
|
||||
Not supported, and shown greyed out with the reason:
|
||||
|
||||
- the 0xC and 0xF opcode extensions;
|
||||
- opcode 5 with a register offset;
|
||||
- code writes mixed with other opcodes, except the button pattern above;
|
||||
- code writes that change the CPU flags differently from the game's instruction, or code caves with branches or pc-relative instructions.
|
||||
|
||||
`build-cache\cheats.status` next to `nx2navi.exe`, written by the setup, records which cheats were built and how; the log line `cheats: ...` sums them up at start. When the code cheats cannot be built, the setup warns and installs the game without them.
|
||||
183
docs/controls.md
Normal file
183
docs/controls.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# 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.
|
||||
|
||||
`NX2NAVI_PAD_LAYOUT` (`pad_layout`, the *Button layout* row on the Controls tab, live) picks which face button is Switch A:
|
||||
|
||||
- `nintendo`: by position, as on a Switch (the table below).
|
||||
- `xbox`: south A, east B, west X, north Y, as an Xbox pad labels them; on a PlayStation pad cross is A, circle B, square X, triangle Y.
|
||||
- `auto` (default): `nintendo` for a pad whose east button SDL reports labelled A (Switch Pro Controller, Nintendo-layout pads), `xbox` for the others.
|
||||
|
||||
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. Which controller's buttons the game's prompts show is `NX2NAVI_PROMPTS` (see Xbox button prompts below). 0.1.9's `pad_layout = xbox_prompts` is read as `pad_layout = nintendo` with `prompts = xbox`, and the menu writes it that way.
|
||||
|
||||
| Switch | Gamepad (by position, Nintendo layout) | Keyboard and mouse | Action in the world |
|
||||
|---|---|---|---|
|
||||
| Left stick | left stick | W A S D | move |
|
||||
| Right stick | right stick | mouse; arrow keys as a second source | camera |
|
||||
| Y | west | left mouse button | attack |
|
||||
| ZL | left trigger (over 50%) | right mouse button, Z | shield, lock on |
|
||||
| ZR | right trigger (over 50%) | F, X | bow |
|
||||
| X | north | Space | jump |
|
||||
| B | south | Left Shift | run, cancel |
|
||||
| A | east | E | interact, pick up |
|
||||
| L | left shoulder | Q | abilities |
|
||||
| R | right shoulder | R | throw weapon |
|
||||
| Left stick click | left stick click | Left Ctrl | crouch |
|
||||
| Right stick click | right stick click | C | first-person view |
|
||||
| D-pad left, up, right, down | D-pad | 1 2 3 4 | quick-select |
|
||||
| Plus | Start | Tab | menu, inventory |
|
||||
| Minus | Back | M | map |
|
||||
| Right stick shaken left and right | shake the right stick | G | detach a fused part from what object holds |
|
||||
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
|
||||
## Xbox button prompts
|
||||
|
||||
`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
|
||||
|
||||
`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*).
|
||||
|
||||
## Window keys
|
||||
|
||||
| Key | Gamepad | Action |
|
||||
|---|---|---|
|
||||
| F1 or Esc | Minus and Plus together | open or close the settings menu; the game gets no input while it is open |
|
||||
| F10 | L3 + R3 (both stick clicks) | performance overlay: off, frame rate, full panel |
|
||||
| F11 | | borderless fullscreen on or off, saved to `nx2navi.ini` |
|
||||
|
||||
The Minus+Plus combo never reaches the game. In the menu the arrow keys, the D-pad or the left stick move, Enter or A selects, Q and E or L and R switch tabs, Esc or B goes back (Switch button names, as in the table above).
|
||||
|
||||
## Remapping
|
||||
|
||||
The *Controls* tab of the settings menu lists, under the button layout, every Switch button, the eight stick directions the keys drive (movement and camera) and the shake key, each with two key cells and a controller cell. Select a cell, press A or Enter, then press the new key, mouse button or controller button; Esc or five seconds without a press cancel, and a controller button cancels a key cell. Delete, Backspace or X clears the selected cell. A key another action has moves to the new one (the description names the action it left); a controller button swaps with the one it replaces. A remapped controller button is physical: from then on the face buttons (A B X Y) stay where they are in both layouts, until the buttons are put back as the layout in use has them or *Reset all buttons* is chosen. F1, F8, F10, F11 and Esc cannot be bound (the menu, the flight recorder and the window keep them). Punctuation keys are named by their place on a US keyboard; the menu shows the character the keyboard layout in use puts there.
|
||||
|
||||
The tab writes `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, G by default). An entry is `pad:<name>` or `key:<name>`:
|
||||
|
||||
- pad names: `south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down lpaddle1 rpaddle1 lpaddle2 rpaddle2` (`zl`, `zr` are the triggers; the paddles are the back buttons of an Xbox Elite, DualSense Edge or Steam Controller, upper pair 1, lower pair 2, and no Switch button has them by default)
|
||||
- key names: `a` to `z`, `0` to `9`, `f2` to `f7`, `f9`, `f12`, `num0` to `num9`, `space tab enter backspace lshift rshift lctrl rctrl lalt capslock insert delete home end pageup pagedown up down left right`, `num_add num_sub num_mul num_div num_decimal`, `minus equals lbracket rbracket semicolon apostrophe comma period slash backslash grave`, `mouse_l mouse_r mouse_m mouse_x1 mouse_x2`
|
||||
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
|
||||
- `none` leaves the button unbound.
|
||||
|
||||
A line replaces every default of that button, pad and keyboard, so list the keys you still want or use `key:default`. Pad names are physical positions, so a line means the same in both layouts and replaces the layout's own button for that Switch button. Stick directions take keys only: the controller's sticks and the mouse camera always work. Buttons without a line keep the table above. Names are case-insensitive. An invalid entry logs one `input: bind ...` line and is skipped; a line without a valid entry changes nothing. The `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 pad that reports its face buttons swapped (A/B and X/Y, as some GuliKit models do) is fixed with:
|
||||
|
||||
```
|
||||
bind_A = pad:south, key:E
|
||||
bind_B = pad:east, key:lshift
|
||||
bind_X = pad:west, key:space
|
||||
bind_Y = pad:north, key:mouse_l
|
||||
```
|
||||
|
||||
## Mouse camera
|
||||
|
||||
`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.
|
||||
|
||||
`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
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Scripted input
|
||||
|
||||
`NX2NAVI_INPUT_SCRIPT=<file>` replays presses. It is ORed with real input and still applies under `NX2NAVI_INPUT=none`. One event per line, `#` starts a comment:
|
||||
|
||||
```
|
||||
<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.
|
||||
|
||||
`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.
|
||||
|
||||
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.
|
||||
|
||||
## RenderDoc capture (blocked)
|
||||
|
||||
`NX2NAVI_RDOC=<frame>[,...]` (`runtime/rdoc_win.c`; `NX2NAVI_RDOC_DIR`, `NX2NAVI_RDOC_DLL`) loads RenderDoc before `vkCreateInstance`. RenderDoc hides `VK_EXT_external_memory_host`, which the back end requires for guest pool imports (`runtime/vk_memory.c`), so device creation fails under it. A capture needs a copy-based pool fallback first.
|
||||
238
docs/installer.md
Normal file
238
docs/installer.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Installer
|
||||
|
||||
A public release that turns the player's own game files into a playable NX2navi folder. The release carries no game
|
||||
data and no code derived from the game: the installer builds the game on the player's machine. The installed folder
|
||||
holds only the game; the installer and its tools stay outside it.
|
||||
|
||||
## What the player does
|
||||
|
||||
1. Downloads `NX2navi-Setup-<version>-windows.zip` from GitHub Releases (none is published yet), unpacks it anywhere and
|
||||
starts `NX2navi-Setup.exe`.
|
||||
2. Picks:
|
||||
- the base game (XCI or NSP) and the 1.1.0, 1.2.1 or 1.4.3 update (NSP); an NSZ or XCZ (compressed by the nsz
|
||||
tool) works as it is, the setup unpacks it itself;
|
||||
- their `prod.keys`;
|
||||
- optionally the firmware folder (`system/Contents/registered` of an unpacked firmware or an emulator's install),
|
||||
needed for Chinese, Korean and Traditional Chinese text;
|
||||
- the game language and the install folder.
|
||||
3. Waits for the setup, one time. The window shows each step and its progress.
|
||||
4. Plays `nx2navi.exe` from the install folder. The installer and the source NSP or XCI (or NSZ, XCZ) can be deleted.
|
||||
|
||||
The install folder (`main.c` `portable_paths()` reads this layout; it stays portable):
|
||||
|
||||
```
|
||||
<install folder>/
|
||||
nx2navi.exe, SDL3.dll, libopus-0.dll, nx2navi_xlate.exe
|
||||
nvngx_dlss.dll NVIDIA DLSS, downloaded by the setup's DLSS step when the player keeps it ticked
|
||||
nx2navi.ini language; the other keys as commented defaults (nx2navi.ini.example)
|
||||
exefs/, romfs/ extracted from the player's files
|
||||
shared-fonts/ the Switch system fonts, with Chinese, Korean or Traditional Chinese text
|
||||
profile/ saves
|
||||
mods/ the player's mods, one folder per mod (created by the menu's "Open mods folder")
|
||||
logs/ nx2navi.log of the last run, nx2navi.1.log to nx2navi.4.log of the runs before it
|
||||
shader and pipeline caches prepared by the setup; shader-cache/shader-cache.pack holds the translations
|
||||
nx2navi-install.json what built this folder: release version, input hashes, finished steps
|
||||
```
|
||||
|
||||
## Updating the port
|
||||
|
||||
The player runs a newer installer on the same folder. It reads `nx2navi-install.json`, keeps `romfs/`, `exefs/`,
|
||||
`shared-fonts/`, `profile/` and `nx2navi.ini` after checking their hashes, rebuilds `nx2navi.exe` and re-prepares the
|
||||
shaders. A newer release that changes nothing game-derived skips the rebuild.
|
||||
|
||||
Given an update file other than the recorded one (1.1.0, then 1.2.1, then 1.4.3), the installer extracts and builds again from it
|
||||
and keeps `profile/`. Saves carry over both ways between 1.2.1 and 1.4.3: 1.4.3 converts a 1.2.1 save on load, and 1.2.1
|
||||
loads the saves 1.4.3 wrote.
|
||||
|
||||
## Setup steps
|
||||
|
||||
1. **Check** before writing anything: an NSZ or XCZ input is unpacked first (below), the files open with the keys, the update is 1.1.0, 1.2.1 or 1.4.3 (by hash, as
|
||||
`tools/extract.py`), the keys are new enough for the update, enough space is free on the target disk (about 36 GiB at the peak of a
|
||||
first install, 20 GiB after it), the CPU is at least x86-64-v2 (SSE4.2; with AVX2/FMA the faster `v3` build is used,
|
||||
otherwise the `v2` one), a Vulkan 1.3 GPU exists. Each failure is one clear message.
|
||||
2. **Fonts** with Chinese, Korean or Traditional Chinese text and a firmware folder: extract the Switch system fonts
|
||||
from the firmware into `shared-fonts/` (MacNativeGames' `extract_shared_fonts.py`, which verifies every NCA layer
|
||||
and BFTTF header). Without a firmware folder the setup warns and goes on; the game then lacks those glyphs.
|
||||
3. **Extract** ExeFS and RomFS into the install folder and verify the hashes.
|
||||
4. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
|
||||
to the installer. The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets first
|
||||
(`tools/cheats.py`, `tools/exefs_patches.py`, status in `build-cache/cheats.status` and `exefs.status`); a file the
|
||||
tools refuse costs those cheats or patches, not the install.
|
||||
5. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
|
||||
hashes and the game version's patch page) and link
|
||||
them with the prebuilt runtime library for the host's level (`libnx2navirt.a` or `libnx2navirt-v2.a`) into `nx2navi.exe`
|
||||
(`build_game.py --runtime-lib`; about 3 min on 24 cores).
|
||||
6. **Prepare** (only when the runtime, the translator or the game version changed, not for a new lift) the RomFS
|
||||
patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
|
||||
`nx2navi.exe --prepare-shaders`) in the install folder, then fold the shader cache's loose files (four per shader, about
|
||||
117,000) into `shader-cache/shader-cache.pack` (`tools/pack_shader_cache.py`). nx2navi.exe reads the pack first and
|
||||
writes the translations it makes in play as loose files; running the tool again with the game closed folds those in.
|
||||
The patches are written four times: `romfs-patches`, `romfs-patches-xbox` and `romfs-patches-xbox-label`
|
||||
(`--prompts`: Xbox button prompts for `prompts = xbox`, the first for `pad_layout = nintendo`, the second for `xbox`
|
||||
and `auto`), and `romfs-patches-pc` (keyboard and mouse prompts for `prompts = pc`). The three prompt folders are
|
||||
optional: when one cannot be made, the setup warns and finishes.
|
||||
When the graphics driver crashes while building the shaders, `prepare_shaders.py` runs that part again without the
|
||||
variants it was building ([building.md](building.md), step 6), and the setup finishes with a warning that names how
|
||||
many were left out.
|
||||
7. **DLSS** (the window's tick box, on by default, or `--dlss`; Windows with an NVIDIA RTX GPU): download
|
||||
`nvngx_dlss.dll` from NVIDIA's own DLSS SDK repository (github.com/NVIDIA/DLSS, a pinned tag, NVIDIA RTX SDK
|
||||
license) into the install folder and check its size and SHA-256 (`steps.py`, `Dlss`). The release does not ship
|
||||
the library; NGX loads it from the folder of nx2navi.exe for `dlaa` and the DLSS modes. A DLSS library already in the
|
||||
folder (a newer one the player put there) is kept. Without an RTX GPU the step only logs and does nothing; a failed
|
||||
download warns, the game runs without DLSS, and the next run of the setup tries again.
|
||||
8. **Clean up** the temp folder, except the generated C and the objects (about 1.3 GB), and write
|
||||
`build-cache/rebuild.txt` in the install folder.
|
||||
|
||||
A cancelled or failed setup resumes from the last finished step (`nx2navi-install.json`).
|
||||
|
||||
## Rebuilding for the mods
|
||||
|
||||
The generated C and the objects stay in the temp folder, so the next lift rewrites only the chunks whose C changed and
|
||||
Ninja recompiles only those: a new or edited code cheat costs the lift (under a minute on 24 cores, a few minutes on 4)
|
||||
and a few dozen chunks, not a full compile, and the shaders stay prepared.
|
||||
|
||||
`build-cache/rebuild.txt` holds the command that opens the setup window in its rebuild mode on the install folder
|
||||
(`installer/gui.py --rebuild <install folder>`, started through `python -c` so the release folder is on `sys.path`
|
||||
whatever the working folder), one argument per line. When an enabled mod has code cheats or ExeFS patch files the build
|
||||
lacks, `nx2navi.exe` runs it at start and exits (`runtime/cheats.c` `rebuild_if_needed`); the
|
||||
window runs the setup on the folder at once and starts the game again when it succeeds. A failed or cancelled rebuild
|
||||
offers to start the game with the code it has; `build-cache/rebuild.skip` then keeps the same files from starting it
|
||||
again, until "Rebuild the game now" on the Cheats or Mods tab. `[mods] mods_rebuild = off` turns it off. The rebuild
|
||||
needs the release folder the setup ran from: a moved or deleted release folder means running a setup again.
|
||||
|
||||
## Command line
|
||||
|
||||
`installer/` (stdlib only; `installer/layout.py` maps the release folder and, in a repository, the dev toolchain):
|
||||
|
||||
```
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR] [--root DIR] [--temp DIR] [--verify]
|
||||
```
|
||||
|
||||
- On a folder with `nx2navi-install.json` only `--dest` is needed; the game files only when the extraction must run again.
|
||||
- `--firmware DIR` is the folder holding the firmware NCAs: an unpacked firmware, or the emulators' installed
|
||||
`Contents/registered` (the parent `Contents` folder works too). It is read only with Chinese, Korean or Traditional
|
||||
Chinese text, and only when `shared-fonts/` is not already in the install folder.
|
||||
- stdout: one JSON event per line, types `start`, `plan`, `step` (started, done, skipped), `progress` (step, phase,
|
||||
done/total/unit), `space` (bytes needed and free on the install disk and the temp disk), `warning`, `error`, `end` (`installer/events.py`). Messages are ids with parameters;
|
||||
`installer/text/en.json` holds their English text. The human-readable log goes to `<temp>/logs/`.
|
||||
- Exit codes: 0 done, 1 failed, 2 bad command line, 3 cancelled. The window cancels with Ctrl+Break to the
|
||||
installer's process group.
|
||||
- Each step records a key over its inputs (runner.py). A step with the recorded key and intact output is skipped, so
|
||||
resume and update are one rule; output kept from another release is re-hashed first.
|
||||
- Peak space on the install disk is about 36 GiB, not 22: the extraction copies both Program NCAs (16.3 GiB) next to
|
||||
the merged RomFS (15.9 GiB) before it deletes them. The check computes the exact figure.
|
||||
- Window (`python -m installer.gui`, `installer/gui.py` draws, `installer/guilogic.py` decides): form, step list with
|
||||
progress, space line, collapsible log, Cancel (Ctrl+Break to the CLI's process group, then it waits for the exit),
|
||||
finish screen with Open folder. English, Russian, Brazilian Portuguese or Simplified Chinese, chosen from the Windows
|
||||
display language (zh-CN and zh-SG give Chinese; the Traditional Chinese regions give English), switchable in the
|
||||
window; every sentence is an id in `installer/text/<en|ru|pt-BR|zh-Hans>.json`. A Chinese window still starts with
|
||||
English as the game language, because Chinese game text needs the Switch shared fonts, which the setup extracts only
|
||||
from a firmware folder. A folder with
|
||||
`nx2navi-install.json` needs only the folder. `--selftest [--shots DIR]` builds every screen in every language and
|
||||
closes itself.
|
||||
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the self-closing window and one end-to-end run
|
||||
of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its docstring).
|
||||
- `tests/installer_test.py`: bookkeeping with fake steps. `tests/installer_e2e.py`: a dev-mode run with the files of
|
||||
`config.local.toml`.
|
||||
|
||||
## Release layout
|
||||
|
||||
The unpacked release; `tools/make_release.py` assembles it and `--zip` packs it:
|
||||
|
||||
```
|
||||
NX2navi-Setup.exe starts python/pythonw.exe -m installer.gui (installer/launcher/launcher.c, no console)
|
||||
installer/ the installer: steps, CLI, window, en/ru/zh-Hans text catalog
|
||||
python/ Python 3.14.9 embeddable + numpy, lz4, cryptography, cffi, pycparser (+ tkinter for the window)
|
||||
recomp/, tools/ the lifter and the build, extraction and shader scripts the steps run (copied from the repo)
|
||||
llvm/ llvm-mingw, x86_64-only subset (Apache-2.0 with LLVM exception; mingw-w64 parts permissive)
|
||||
runtime/ libnx2navirt.a, libnx2navirt-v2.a and the headers the generated code and modules.c/romfs.c/patches.c
|
||||
include
|
||||
bin/ nx2navi_xlate.exe, hactool.exe, SDL3.dll, libopus-0.dll, ninja.exe
|
||||
data/ shader_list.tsv (MacNativeGames, MIT); pgo-<ver>.profdata + .json, the execution profile an
|
||||
update's code and the runtime libraries compile with (building.md, "Profile-guided build")
|
||||
THIRD_PARTY_NOTICES.md
|
||||
nx2navi.ini.example, VERSION the default ini; the release string (the repository's VERSION plus the commit)
|
||||
```
|
||||
|
||||
- SDL3 comes from the upstream release, not MSYS2: MSYS2's DLL imports the LGPL libiconv.
|
||||
- Every bundled third-party piece gets a row in `THIRD_PARTY_NOTICES.md`.
|
||||
- The release contains no code, data, text or assets of the game; `make_release.py` runs `tools/check_content.py` over
|
||||
the folder before it finishes. The execution profiles name lifted functions by address (`f_<address>`) and hold
|
||||
branch counts and control-flow hashes of the generated C.
|
||||
|
||||
## Switch system fonts
|
||||
|
||||
Simplified Chinese, Traditional Chinese and Korean text needs the Switch system fonts (`0-standard.bfttf` and the other
|
||||
nn::pl shared fonts). The game does not ship them. Without them it exits before its first frame in those languages
|
||||
(`rt_fatal` in `runtime/ov_system.c`). Supply them from your own console, in either way:
|
||||
|
||||
- Give the setup the firmware folder (`--firmware DIR`, or the window's firmware field); the Fonts step extracts them to
|
||||
`shared-fonts/` in the install folder.
|
||||
- Run `python tools/extract_fonts.py --firmware <dir>` with `.venv-py314` and the keys from `config.local.toml`, or
|
||||
point `NX2NAVI_SHARED_FONTS` at a folder that holds the extracted fonts.
|
||||
|
||||
## Linux release
|
||||
|
||||
`tools/make_release_linux.py` builds the same setup for Linux and packs it as `NX2navi-Setup-x86_64.AppImage` (the static
|
||||
type-2 runtime: no libfuse2 needed on the host). The player makes it executable and starts it; on a Steam Deck, from
|
||||
Desktop Mode. The command-line setup needs the desktop session's display (`DISPLAY` or `WAYLAND_DISPLAY`) because preparing the
|
||||
shaders opens a window; the check step fails at once without it. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
|
||||
Xft against the sniper sysroot so Russian text uses system TrueType fonts. The setup builds the game into the folder they pick, as on Windows, and the game is `nx2navi` there with
|
||||
`libSDL3.so.0` beside it. The folder inside the AppImage:
|
||||
|
||||
```
|
||||
AppRun starts python/bin/python3 -m installer.gui (no bytecode written into the read-only mount)
|
||||
nx2navi-setup.desktop, nx2navi-setup.png
|
||||
installer/, recomp/, tools/, data/, licenses/, THIRD_PARTY_NOTICES.md, nx2navi.ini.example, VERSION as on Windows
|
||||
python/ python-build-standalone 3.14 (with tkinter) + the installer's packages; a .pth puts the folder,
|
||||
tools/ and recomp/ on sys.path
|
||||
llvm/ LLVM 23 (clang, lld, llvm-ar) and llvm/sysroot: the files of the Steam Runtime 3 "sniper" SDK the
|
||||
game's compile and link read (about 130 files)
|
||||
runtime/ libnx2navirt.a, libnx2navirt-v2.a, headers, the install-side sources, nx2navi.png; deps/ with the Vulkan
|
||||
headers, VMA and a static zstd
|
||||
bin/ nx2navi_xlate, hactool, ninja, libSDL3.so.0
|
||||
```
|
||||
|
||||
- What we compile is built against the sniper sysroot (glibc 2.31): the game, hostcheck, hactool and the translator
|
||||
need no newer glibc. The game links Opus, libvpx, zstd and libstdc++ statically and needs only glibc, the system's
|
||||
Vulkan loader and the SDL3 beside it. The setup itself needs glibc 2.34 (Ubuntu 22.04, Debian 12, Fedora 35,
|
||||
SteamOS 3): the official LLVM binaries and the `cryptography` wheel are built against it. `make_release_linux.py`
|
||||
checks every ELF file in the folder against that floor (`check_glibc`), and the setup's check step stops with one
|
||||
message on an older system.
|
||||
- The setup's temp folder is `.setup-temp` inside the install folder (`--temp` changes it): the AppImage is mounted
|
||||
read-only, and a Steam Deck's `/home` is often too full for the generated C while the game goes to the SD card.
|
||||
- The game keeps Mesa's shader cache in `mesa-cache` beside `nx2navi` unless `MESA_SHADER_CACHE_DIR` is set, for the
|
||||
same reason: RADV fills its default folder in `~/.cache` up to 1 GB.
|
||||
- The window follows `LANG` for English or Russian, cancels through SIGTERM to the CLI's process group (the CLI handles
|
||||
it as Ctrl+Break on Windows) and opens the finished folder with `xdg-open`.
|
||||
- Requirements beyond Windows': Linux 6.7 or later (the write watch, `docs/building.md`) and glibc 2.34.
|
||||
- Building the release needs one game version extracted and lifted (`tools/extract.py`, `recomp/gen.py`, either
|
||||
version): the runtime library includes the generated `layout.h`, whose shared part (`MOD_COUNT`, `MOD_MAIN_BASE`)
|
||||
is the same for both. `tools/make_release.py` takes `generated/v121`, else `generated/v143`.
|
||||
|
||||
## Constraints
|
||||
|
||||
- The installer steps contain no Win32 calls: Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The keys file path is passed to the tools; key values never appear in the UI, logs or files.
|
||||
- The window is a front end over the CLI (`python -m installer --base ... --update ... --keys ... --lang ... --dest
|
||||
...`), which prints one JSON progress event per line. Tests and CI use the CLI.
|
||||
- Paths may hold any Unicode characters (a Cyrillic user name puts the temp and install folders under it). Every exe
|
||||
the setup runs or builds reads its arguments and paths as UTF-8 through an `activeCodePage` manifest: hactool
|
||||
(our patch, [building.md](building.md)), nx2navi.exe (`runtime/nx2navi.rc`), ninja (upstream). The Python steps write
|
||||
their files and logs as UTF-8, and `build_game.py` quotes paths with spaces on its Ninja command lines.
|
||||
- A game file whose data fails its hash checks or ends early stops the setup with `check.input_damaged`, which names
|
||||
the file and asks to copy it again (`extract.DamagedFile`). A failed merge checks the base Program NCA alone to tell
|
||||
which file is damaged.
|
||||
- Unsigned exe: SmartScreen warns on first start.
|
||||
- NSZ and XCZ input (game files compressed by the nsz tool: NCAs stored as NCZ, their sections decrypted and
|
||||
compressed with zstd): the check step tells them by their content, not their name (an NSP or XCI holding NCZ files),
|
||||
so the file dialog has one filter for all four. It unpacks each into `<temp>/unpacked/<base|update>.nsp`
|
||||
(`tools/ncz.py`), an NSP of the NSZ's files or of the XCZ's secure partition, and the extraction reads that.
|
||||
Unpacking encrypts the sections again, so every NCA comes back byte for byte, and checks each against the hash its
|
||||
name carries; a mismatch is `check.input_damaged`. It needs the unpacked size free in the temp folder (checked
|
||||
first), runs at about 500 MB/s, and an interrupted setup reuses a finished NSP of the same file. The extract step
|
||||
deletes the folder when it is done. An XCZ update is refused like an XCI one.
|
||||
|
||||
## Later
|
||||
|
||||
- Re-warm the driver cache on start after a move, GPU or driver change.
|
||||
235
docs/maxwell.md
Normal file
235
docs/maxwell.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Maxwell / NVN behaviour a port must reproduce
|
||||
|
||||
Reference for diagnosing graphics bugs. Each item: what Maxwell (GM20B, 3D class `B197`, compute `B1C0`) and NVN do,
|
||||
what Vulkan / AMD (RDNA4, our GPU0) do instead, how emulators and ports handle it, and the source. Items are ordered by
|
||||
how likely they are to cause visible artifacts. "Ours:" marks what our runtime does (grep-level look, see section 7;
|
||||
our code is cited by file and function). Licensing: the hardware/suyu are GPL, cited for behaviour only.
|
||||
|
||||
Source shorthands:
|
||||
|
||||
- **OGD**: NVIDIA open-gpu-doc `classes/3d/clb197.h`, `classes/compute/clb1c0.h` (MIT-style),
|
||||
<https://github.com/NVIDIA/open-gpu-doc/tree/master/classes>. Offsets below are method offsets in `clb197.h`.
|
||||
- **G80TEX**: Mesa nouveau `src/gallium/drivers/nouveau/nv50/g80_texture.xml.h` (MIT),
|
||||
<https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/drivers/nouveau/nv50/g80_texture.xml.h>.
|
||||
- **RYU-SU**: Ryujinx `Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs` (MIT), mirror at 1.1.1137:
|
||||
<https://git.dawidpotocki.com/mirror/ryujinx/plain/src/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs?h=1.1.1137>.
|
||||
- **RYU-SH**: Ryujinx shader translator vendored at `refs/MacNativeGames/vendor/ryujinx/src/Ryujinx.Graphics.Shader/`
|
||||
(Ryujinx 1.3.2, MIT). Paths below are relative to it.
|
||||
- **NXN**: nxnative's graphics notes, `refs/nxnative/docs/agents/` (MIT). nxnative has rewritten them since; each
|
||||
row states the behaviour and fix it took from them.
|
||||
- **MNG**: `refs/MacNativeGames` @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (MIT); line numbers below are at that
|
||||
commit.
|
||||
- **DK3D**: `refs/deko3d` @ `373bfd43d8b2d805fa2eb7d406dbb823032df958` (zlib, fincs), the homebrew Maxwell driver for Switch. Written
|
||||
against the hardware, not the NVN API: what it programs is what the GPU needs. Paths relative to `source/`. `.def` files give
|
||||
methods as word indices (`engine_3d.def`); byte offset = index x 4, so `0x5A1` = OGD `0x1684`. No conditional rendering, no
|
||||
provoking-vertex conversion, no 8-bit indices in it.
|
||||
- **YZ-2305 / YZ-2306**: the hardware progress reports May / June 2023,
|
||||
<https://the hardware-mirror.github.io/entry/the hardware-progress-report-may-2023/>,
|
||||
<https://the hardware-mirror.github.io/entry/the hardware-progress-report-jun-2023/> (behaviour only).
|
||||
- **VKSPEC**: Vulkan 1.4 specification, <https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html>;
|
||||
**GLSL450**: SPIR-V GLSL.std.450 extended instructions, <https://registry.khronos.org/SPIR-V/specs/unified1/GLSL.std.450.html>.
|
||||
|
||||
## 1. Render targets
|
||||
|
||||
| # | Maxwell / NVN | Vulkan / AMD | Emulators / ports | Source |
|
||||
|---|---|---|---|---|
|
||||
| 1.1 | **Colour target the fragment shader does not write keeps its old texel**, even with blending enabled. | Undefined value written/blended. | NXN: zero write mask and no blend for unwritten outputs (`reflection colors`). Already fixed here. | NXN; MNG `games/<game>/tests/metal_semantics/probe.m:5` |
|
||||
| 1.2 | **Unwritten vertex outputs read as (0, 0, 0, 1)** in the next stage, including the unwritten components of a partly written output (VS writes xyz, FS divides by w). | FS input with no VS output: interface mismatch, value undefined; an unwritten component of a written output is undefined (AMD: whatever the register holds, often 0). | Ryujinx initialises exactly the components the next stage reads, but only when stages are linked (`Translation/Translator.cs:212-227`, `NextInputAttributesComponents`); position starts (0,0,0,1) (`:259`). NXN Link's Awakening: sun shadow lookups divided by w = 0, scene dim, no shadows. Ours: missing outputs padded with (0, 0, 0, 1), unlinked outputs initialised by translator patch 0006 (section 7, check 2). | NXN |
|
||||
| 1.3 | **Clears honour the scissor and the clear's own channel / stencil write mask**, and may come between draws inside a pass. `SET_CLEAR_SURFACE_CONTROL` (0x10f8) has `RESPECT_STENCIL_MASK`, `USE_CLEAR_RECT`, `USE_SCISSOR0`, `USE_VIEWPORT_CLIP0`. | `vkCmdClearAttachments` takes a rect but **ignores the colour write mask**; load-op clears are whole-attachment. | NXN/MNG draw a triangle with the clear's mask (`clear_color_tri`, `clear_ds_tri`); NX2navi uses mid-pass, partial-mask and partial-scissor clears (NXN). the hardware: "red dot" was clears hitting the wrong RT index (YZ-2305, PR 10243). Hardware recipe (DK3D): `CLEAR_BUFFERS` (`0x674` = 0x19d0) carries the colour channel mask (bits 2-5), depth (0), stencil (1), target id (6-9) and layer (10-20) in one word; `ClearBufferFlags` (`0x43E` = 0x10f8) is set once to `StencilMask | Scissor` (`maxwell/gpu_3d_base.cpp:69`), so every clear is cut by scissor 0 and its stencil bits by the stencil front write mask register, and DK3D's colour clear does not program `ColorWriteMask` (`0x680`). A stencil clear writes `StencilFrontMask` (`0x4E7`) and `ClearStencil` (`0x368`) just before the clear and restores the shadowed state after (`gpu_3d_base.cpp:497-510`). Multisample raster is switched off around a colour clear (`:459, :465`). Arrays: an MME macro repeats the clear for every layer (`maxwell/clear_buffers.mme:5-22`). No pass concept: clears sit between draws. Ours: masked colour and stencil clears are **skipped** (check 3). | OGD 0x10f8; NXN; DK3D `maxwell/gpu_3d_base.cpp:444-512`, `engine_3d.def:305, 644` |
|
||||
| 1.4 | **Conditional rendering** (`SET_RENDER_ENABLE_A..C` 0x1550-0x1558, NVN `SetRenderEnableConditional`) compares two 64-bit counters at `addr` and `addr+16` on the GPU, in command order. NX2navi wraps nearly every clear in it. A `TIMESTAMP` report writes 0 to the counter field. | `VK_EXT_conditional_rendering` tests one 32-bit word against zero. | NXN: honouring it fixed the black 3D scene (light-volume depth never cleared). deko3d has no conditional rendering at all (no `SET_RENDER_ENABLE` in `engine_3d.def`), so it cannot settle the compare rule. Ours: evaluated on the CPU at record time; a report from an unfinished submission counts as 0 (check 5). | OGD; NXN |
|
||||
| 1.5 | **Depth mode** is runtime state: `nvnDeviceSetDepthMode` near = -w (GL, NVN default) or 0. Register `SET_VIEWPORT_Z_CLIP` (0x0d7c) `NEGATIVE_W_TO_POSITIVE_W` / `ZERO_TO_POSITIVE_W`. | Vulkan clips z < 0 unless `VK_EXT_depth_clip_control` `negativeOneToOne`. | Ryujinx infers the mode from the viewport transform, falling back to the register (`GetDepthMode`, RYU-SU). NXN: wrong mode either clips everything (black window) or puts all depth in 0.5..1 (DOF blurs the screen). Ours: `negativeOneToOne` from NVN state. DK3D programs both halves: `SetDepthMode` (`0x35F` = 0xd7c) selects the pre-transform clip range, and the viewport transform maps z to [near, far] itself: scale z = `0.5 * (far - near)` and offset z = `0.5 * (near + far)` for -1..1, scale `far - near` and offset `near` for 0..1 (`maxwell/gpu_3d_base.cpp:143-147, 336-343`). Same maths as Vulkan `negativeOneToOne`. | OGD 0x0d7c; NXN-64, NXN; DK3D `maxwell/gpu_3d_base.cpp:136-147, 321-362` |
|
||||
| 1.6 | **Depth clamp / clip**: `SET_VIEWPORT_CLIP_CONTROL` (0x193c) has separate `PIXEL_MIN_Z`/`PIXEL_MAX_Z` CLIP or CLAMP and geometry-clip modes. | `depthClampEnable` also disables z clipping (unless `VK_EXT_depth_clip_enable`). RADV had a dynamic depth clamp driver bug causing NX2navi vertex explosions. | Ryujinx: clamp = `!(clip & DepthClampDisabled)` (RYU-SU `UpdateDepthClampState`). the hardware disabled dynamic depth clamp on affected RADV (YZ-2305, PR 10262). DK3D has one switch, `depthClampEnable`, and writes `VIEW_VOLUME_CLIP_CONTROL` (`0x64F` = 0x193c) with `0x101A` when on and `0x181D` when off (default at setup is `0x181D`); it labels the bits "todo: figure out what this actually does". Bits 3-4 (`DepthClampNear/Far`) are set in both values, bits 0-2 and 11 differ. There is no separate depth-clip toggle. The viewport near/far registers (`Viewport` array words 2-3) carry the clamp range and are swapped when far < near (`:349-350`). Vulkan clamp to [min, max] of the viewport depth matches. | OGD 0x193c; DK3D `maxwell/gpu_3d_state.cpp:42`, `gpu_3d_base.cpp:135-139, 349-350`, `engine_3d.def:621` |
|
||||
| 1.7 | **Depth formats**: D24S8 is native; D24 quantises to 2^-24 everywhere in 0..1. Polygon offset units are in D24 steps for D24 targets; `SET_DEPTH_BIAS_CONTROL` (0x1110) `DEPTH_FORMAT_DEPENDENT`. | RX 9070 has no D24S8 attachment; D32F has finer steps near 0 and a per-primitive exponent-based r. EQUAL / LEQUAL tests between a pre-pass and a later pass, and decal biases, behave differently. | the hardware rescales depth-bias units when D24 is emulated as D32F; Ryujinx passes `units / 2` (register is twice the API value) (RYU-SU `UpdateDepthBiasState`). MNG: D16 load-clear 0.5 → 32767 vs fragment 0.5 → 32768 made object/Recall classification tiles visible. Ours: D24S8 → D32_SFLOAT_S8, bias not rescaled (check 4). DK3D writes `PolygonOffsetUnits` (`0x56F` = 0x15bc) as `constantFactor * 2.0f`, clamp to `0x61F`, factor to `0x55B` (`maxwell/gpu_3d_state.cpp:148-157`), confirming the x2 register. | OGD 0x1110, 0x15bc, 0x156c; MNG `docs/how-it-works.md:57-61`; DK3D `maxwell/gpu_3d_state.cpp:154` |
|
||||
| 1.8 | **Position invariance**: identical position math gives identical depth across programs (pre-pass then EQUAL / LEQUAL). | Only for outputs declared `Invariant`. | Ryujinx decorates position `Invariant` (`CodeGen/Spirv/Declarations.cs:571`) and marks float ops `NoContraction` unless `QueryHostReducedPrecision` (`CodeGen/Spirv/Instructions.cs:2047`). NXN: Odyssey ground z-fought without it; LA's foliage drawn with depth EQUAL vanished when the pre-pass failed. Ours: translator default (all NoContraction). | NXN |
|
||||
| 1.9 | **Sampling a texture that is also a bound render target**: Maxwell's texture cache is not coherent with ROP writes; NVN requires `nvnCommandBufferBarrier(INVALIDATE_TEXTURE)` between writing and sampling (methods `INVALIDATE_TEXTURE_DATA_CACHE` 0x1338, `..._NO_WFI` 0x1288). Reading a different level/layer, or read-only depth while testing, is fine. | Same image as attachment and sampled in one pass is a feedback loop: undefined unless `VK_EXT_attachment_feedback_loop_layout`; on AMD DCC/HTILE-compressed data is not readable by the sampler without decompression. | the hardware: pixelated object glow fixed with barriers on attachment feedback loops (YZ-2305, PR 10402). BotW black textures on GCN worked around with `AMD_DEBUG=nohyperz` (HTILE off) (<https://git.openpunk.com/OpenPunk/the hardware/wiki/commit/bd401539420182cbbf8bc638b042536a7e690c3f>). Ours: an NVN barrier ends the pass and inserts a full memory barrier; no detection of a sampled texture that is a current attachment (check 10). DK3D barrier recipe (`maxwell/gpu_base.cpp:49-115`): the ordering part and the cache part are separate. Ordering: Tiles = `TiledCacheBarrier` (`0x3DF`), Fragments = `FragmentBarrier` (`0x378`), Primitives = `WaitForIdle` (`0x44`), Full = gpfifo `SetReference` + new gpfifo entry with `NoPrefetch`, so later command and indirect-parameter fetch happens after everything finished. Tiles and Fragments need an implicit WFI before a cache invalidate. Caches (each opt-in): texture data `InvalidateTextureDataCache` (`0x3DD`, WFI) or `..NoWfi` (`0x4A2`); shader `InvalidateShaderCaches` (`0x369`: instruction, global data, constant); descriptors `InvalidateTextureHeaderCacheNoWfi` (`0x50A`) + `InvalidateSamplerCacheNoWfi` (`0x509`); zcull `0x4B2`; L2 gpfifo `MemOpB` `L2FlushDirty` + `L2SysmemInvalidate`. The texture-cache invalidate is its own flag; ordering alone does not refresh it. Every queue flush ends with image + shader + descriptor + L2 invalidation "to ensure the visibility of CPU updates between flushes" (`:28-47`): CPU writes into textures and descriptors reach the GPU at the next submit boundary, not mid-submit (2.2). | OGD; DK3D `maxwell/gpu_base.cpp:28-115` |
|
||||
| 1.10 | **Duplicate attachments**: the same texture bound at two colour slots with write mask 0 on one does not interfere (ROP writes only enabled channels). | Tile-based or compressed attachments may store the unwritten copy over the written one. | NXN BotW G-buffer (c1 = c2, c3 = c4): leaves the duplicate slot unattached (`rt_dup_slot`). | NXN |
|
||||
| 1.11 | **Per-target write masks**: `RtColorMask[i]`, or entry 0 for all when `RtColorMaskShared`. `SET_SINGLE_CT_WRITE_CONTROL` (0x0f90). A colour target with format 0 is disabled. | Per-attachment `colorWriteMask`. | RYU-SU `UpdateRtColorMask`, `IsRtEnabled` ("Colors are disabled by writing 0 to the format"). | OGD 0x0f90 |
|
||||
| 1.12 | **sRGB render target views**: NVN encodes on write when the view format is sRGB over a linear texture. Maxwell also has a global `SET_SRGB_WRITE` (0x15b8). | Format of the attachment view decides; needs `MUTABLE_FORMAT`. | NXN Mario: attaching the base texture stored linear values, picture darker. Ours: RT views take the view format (`view_format`). | OGD 0x15b8; NXN |
|
||||
| 1.13 | **Compressed and 3D targets**: agl's GPU BC4 encoder renders blocks through an `RGBA16UI` view, one plane of a 32×32×32 BC4 3D texture per pass. NVN renders a 3D texture slice by slice. | Needs `BLOCK_TEXEL_VIEW_COMPATIBLE` + `EXTENDED_USAGE`, `2D_ARRAY_COMPATIBLE`. | NXN shadow textures (`bcrt_attach`). the hardware: wrong block depth on 3D slices gave Gloom textures on terrain (YZ-2305, PR 10433). Already fixed here. | NXN |
|
||||
| 1.14 | **Draw with no render target** (fragment shader only writes an SSBO, 1×1 viewport) runs normally. | Needs an attachment-less pass with explicit render area. | NXN `bare_dims`: cave ambient light came back. Ours: area of viewport 0 (`vk_exec.c` `resolve_rt`). | NXN |
|
||||
| 1.15 | **Window origin**: NVN default lower-left (GL); NX2navi switches to upper-left. Y flip and scissor flip follow. | Vulkan upper-left; negative viewport height to flip. | Ryujinx flips scissor against screen height when Y-negate is on (RYU-SU `UpdateScissorState`). | NXN |
|
||||
| 1.16 | **Multiple viewports / scissors** (16), selected by `ViewportIndex` output. | Needs `multiViewport` and `shaderOutputViewportIndex`. | Ours: one viewport, one scissor; differing scissors logged (`nvn_api_cmd.c`). | OGD 0x192c |
|
||||
| 1.17 | **Fragment output saturation** per target: `SET_PS_SATURATE` (0x13a8) clamps output n to 0..1 or -1..1 in fixed function. NVN does not expose it (GLSLC emits `.SAT` in code). | None. | Not emulated by Ryujinx; listed so a clamp difference is not chased in the wrong place. | OGD 0x13a8 |
|
||||
| 1.18 | **New memory reads as zero** (fresh pool pages); feedback effects never clear their state texture. | New image content undefined. | NXN `tex_zero_fill`. | NXN |
|
||||
| 1.19 | **MSAA**: `nvnCommandBufferDownsample` resolves; depth-stencil downsamples are copies the game does itself. | `vkCmdResolveImage` colour only; integer formats pick one sample. | NXN LA: stencil lost in the half-size depth copy, no AO. | NXN |
|
||||
|
||||
## 2. Textures and samplers
|
||||
|
||||
| # | Maxwell / NVN | Vulkan / AMD | Emulators / ports | Source |
|
||||
|---|---|---|---|---|
|
||||
| 2.1 | **Automatic block height**: NVN truncates `height_in_blocks * 1.5 / 8` before picking the GOB power of two (deko3d rounds up). Wrong H reads padding: 160×160 BC3 warp pad half black. Allocation size is the same, so size checks do not catch it. Mip levels shrink the block height while the level fits in half of it. | Opaque optimal tiling. | MNG fixed (40 SDK-measured cases, `games/<game>/tests/texture_auto_layout/probe.c`). Ours: truncating form already (`runtime/nvn_tex.c`, the block-height pick). DK3D reads: height in block rows (rounded up), x 1.5, then `(h + 7) / 8` rounded up, then the largest power of two <= that, capped at 16 GOBs (`dk_image.cpp:444-447`, `pickTileSize` `:13-20`); 3D uses `depth + depth / 2` directly as the GOB count (`:451-454`); video images are fixed at 2 GOBs (`:438`). Per level, the shift drops while `unit << (shift - 1) >= level dimension` with unit 8 rows / 1 slice (`adjustTileSize` `:23-37`, used at `:89-91`), width tile is always 0. Array layers are padded to the block shrunk to the full height / depth (`alignLayerSize` `:57-72`). Every one of these matches `nvn_tex.c` except the rounding. | MNG `docs/how-it-works.md:47-53`; DK3D `dk_image.cpp:13-37, 57-72, 80-103, 436-455` |
|
||||
| 2.2 | **Textures filled by the CPU with no NVN flush** (NX2navi terrain: two 121-layer ASTC 8×8 arrays, 42 MB each; BC4 material textures read back with `ReadTexels` and copied into a 5120×5120 roughness atlas). The CPU and GPU share memory, so no flush is needed on the Switch. | Host images are copies; must detect CPU writes. | NXN write-protects texture pages (`tex_dirty_upload`); zero ASTC decodes magenta, zero BC black. the hardware: small CPU writes into GPU-used pages not invalidated gave slow rain/snow particles and teleport particle corruption (YZ-2306, PR 10942). Ours: `GetWriteWatch` generations (`vk_memory.c`, `mem_written_pages`). | NXN-175 |
|
||||
| 2.3 | **Same storage, new texture object every frame** (transient targets, deferred handles). Same storage with a different descriptor reinterprets the same block-linear bytes. | Two `VkImage`s over one allocation with optimal tiling do not share a defined layout (AMD tiling and DCC metadata are per image); a new image starts `UNDEFINED`. | NXN shares host textures by `(pool, offset, description)`; MNG keeps a "live texture at address" redirect. the hardware: whole areas black for a frame on camera changes from texture-cache alias/overlap ordering (YZ-2305, PR 10418). Ours: GPU-only pool textures are `VkImage`s bound onto pool memory (check 7). | NXN; MNG `docs/how-it-works.md:67-71` |
|
||||
| 2.4 | **Deferred texture handles** (`BindTexturesDeferred`): the GPU reads the handle from memory when the command runs; BotW records command blocks once and writes handles later. | n/a | NXN BotW: handles copied at record time were zero, 3D scene black. | NXN |
|
||||
| 2.5 | **Border colours**: any RGBA (TSC words 4-7, plus sRGB border fields). NX2navi/BotW grass interaction map clamps to (0.5, 0.5, 0.5, 0.5), its "untouched" value. | Three fixed colours unless `VK_EXT_custom_border_color` (+ `customBorderColorWithoutFormat`); integer formats need `INT_CUSTOM`. | MNG emulates in the shader; with transparent black, grass away from the player lay flat and showed no cut (MNG issue #49). Ours: custom border when supported, else transparent black (check 11). | G80TEX; MNG `games/<game>/third_party/d2r_gfx/gfx/nvn/nvn_mtl.m:5041-5045` |
|
||||
| 2.6 | **sRGB decode** is a TIC bit (`G80_TIC_2_SRGB_CONVERSION`) separate from the format; TSC has its own `SRGB_CONVERSION` (word 0 bit 13). NVN exposes sRGB as formats. | Format decides. | NVN-level translation by format is enough. | G80TEX:197, :377 |
|
||||
| 2.7 | **Swizzles** per texture and per view (`TextureBuilderSetSwizzle`, `TextureViewSetSwizzle`); BotW layout captures rebuild a texture on the same storage with (1, 1, 1, R) or alpha 1. Default per-format mapping R8 → (R, 0, 0, 1). | `VkComponentMapping` on the view. | NXN: font atlas white boxes, HUD dark boxes without it. Ours: mapped (`vk_exec_draw.c` `get_view`). | NXN |
|
||||
| 2.8 | **Sample dimension comes from the instruction**: a non-array sample of an array texture reads layer 0; 1,512 BotW shaders sample a cube as a cube array. | View type must match the declared type. | NXN: 2D array bound as 2D gave one pale colour (washed-out scene); cubes created as 6-layer arrays with cube / cube-array views. Ours: views per resource type (`get_view`). | NXN |
|
||||
| 2.9 | **LOD**: TSC min/max LOD clamp, LOD bias, `TWO_D_NO_MIPMAP` texture type, a min filter without mips stays on the base level; TIC carries its own max anisotropy and aniso quality fields. | `minLod`/`maxLod`, `mipLodBias` limited by `maxSamplerLodBias`. | the hardware: anisotropy heuristics for mods forcing trilinear (YZ-2306, PR 10744). Ours: non-mip filter → `maxLod = minLod + 0.25`, bias clamped (`vk_texture.c` `make_sampler`). | G80TEX |
|
||||
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | the hardware adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/the hardware/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). Ours: the translator biases gather coordinates by the host's `subTexelPrecisionBits` on every non-NVIDIA GPU (`vk_caps.c` → job `gather_bias` → Ryujinx `TexturePass.InsertCoordGatherBias`); NX2navi on RADV drew dashed dark lines along wall and floor edges in the prologue caves without it. `NX2NAVI_GATHER_BIAS=0` turns it off. | — |
|
||||
| 2.11 | **Gather offsets**: `TLD4.PTP` takes four per-texel offsets, possibly from registers. | `ConstOffsets` must be constant. | Ours: `QueryHostSupportsNonConstantTextureOffset() => false` makes Ryujinx emulate register offsets (`tools/xlate/Program.cs`). | RYU-SH `Translation/Optimizations` (TexturePass) |
|
||||
| 2.13 | **TXD gradient register order**: NX2navi's terrain shaders fill the gradient registers of a `TXD` per coordinate (du/dx, du/dy, dv/dx, dv/dy). | Vulkan's `Grad` takes dPdx (du/dx, dv/dx) and dPdy (du/dy, dv/dy). | Ryujinx's `EmitTxd` takes the first two registers as dPdx and the last two as dPdy, so the mip level came from the wrong derivatives and was too sharp on terrain faces turned across the screen: patches that followed the faces crawled when the camera moved (1.4.3, the Depths; found by the user from their F9 frame, a gradient sample's coordinate and gradients traced back to `DPdx` and `DPdy` of the interpolants). Ours: `gfx_shader.c` `patch_txd_transpose` swaps the two inner components of every two-component gradient pair in the loaded SPIR-V (the shader cache is untouched). `NX2NAVI_TXD_TRANSPOSE=0` turns it off. Only 2D gradients are handled; 3D and cube ones are left as translated. | — |
|
||||
| 2.12 | **Depth compare** (`TSC` depth compare + `TIC_4_DEPTH_TEXTURE`), min/max reduction filtering. | `compareEnable`; reduction mode needs `samplerFilterMinmax` and is not allowed with compare. | Ours: reduction dropped when compare is on (`vk_texture.c` `make_sampler`). | G80TEX |
|
||||
| 2.13 | **ASTC / BC**: native ASTC; zero ASTC block = error colour on hosts. | AMD and NVIDIA desktop GPUs lack ASTC: decode to RGBA8. the hardware offered ASTC recompression to BC1/BC3 for low VRAM (YZ-2305, PR 10398). | Ours: native upload where the device samples ASTC, else a compute-shader decode (`vk_astc.c`, `NX2NAVI_ASTC`), CPU decode (`tex_astc.c`) as the fallback. | `runtime/vk_astc.c` |
|
||||
| 2.14 | **Linear CPU-written textures** (`FLAGS_LINEAR`, small ramps, glyph caches): CPU writes visible to the GPU directly. | Must re-upload or use linear images over host memory. | NXN Mario: sky black without its 64×1 RGBA16F ramps. | NXN |
|
||||
|
||||
## 3. Shader math
|
||||
|
||||
| # | Maxwell | Vulkan / AMD | Emulators / ports | Source |
|
||||
|---|---|---|---|---|
|
||||
| 3.1 | **FMNMX and `.SAT` with NaN return the other operand / 0** (IEEE minNum). Games rely on this to stop NaN spreading (one NaN pixel blown up by bloom or temporal passes flickers or fills the screen). | `GLSL.std.450 FMin/FMax/FClamp`: result undefined when an operand is NaN (NMin/NMax are the NaN-dropping forms). AMD hardware min/max usually drops NaN; the compiler may still assume no NaN. | Ryujinx emits FMin/FMax/FClamp (`CodeGen/Spirv/Instructions.cs:336, 956, 972`; saturate = FClamp 0..1, `Instructions/InstEmitAluHelper.cs:419-423`). NXN translated shaders use `isnan` selects; MNG measured Metal min/max/saturate drop NaN (`probe.m:1-4`). | GLSL450; NXN |
|
||||
| 3.2 | **FMZ multiply mode** (`FMUL/FFMA/...` field `Fmz`, `.FMZ`): 0 × anything = 0, including Inf/NaN (DX9 rule). | IEEE: 0 × Inf = NaN. | Ryujinx decodes `Fmz` (`Decoders/InstDecoders.cs:306-309, 1901`) but no emitter reads it. Ours: same. Census needed (check 13). | RYU-SH |
|
||||
| 3.3 | **Denormals**: per-instruction `.FTZ`. | AMD flushes fp32 denormals by default; `VK_KHR_shader_float_controls` sets it per shader. | Ryujinx ignores `.FTZ`. Low risk. | RYU-SH (no `Ftz` use in `Instructions/`) |
|
||||
| 3.4 | **Warp size 32**: `SHFL`, `VOTE`, `S2R LANEID`, ballots are 32-bit. | AMD RDNA runs wave32 or wave64 per shader (the driver chooses; RADV fragment default wave64); `VK_EXT_subgroup_size_control` `requiredSubgroupSize = 32` fixes it. | Ryujinx adapts lane masks to `QueryHostSubgroupSize()` (default 32, `IGpuAccessor.cs:218`; `Instructions/InstEmitWarp.cs:72, 106`, `InstEmitMove.cs:175-185`). Ours: not overridden, no required size (check 6). | <https://docs.mesa3d.org/envvars.html> (`RADV_PERFTEST=pswave32`) |
|
||||
| 3.5 | **Discard (`KIL`)** keeps the quad's derivatives valid (demote semantics). | `OpKill` terminates; later derivatives in the quad undefined. | Ryujinx emits `OpKill` (`CodeGen/Spirv/Instructions.cs:508`). | — |
|
||||
| 3.6 | **Implicit-LOD sampling in divergent control flow** works on Maxwell (TEX unit uses quad neighbours). | Undefined in non-uniform control flow; AMD whole-quad mode usually works. | — | VKSPEC (derivatives) |
|
||||
| 3.7 | **Integer division** does not exist (compiled to `XMAD`/`IMAD` sequences); MUFU.RCP(0) = +Inf, LG2(0) = -Inf. | Same IEEE edge cases through `1/x`, `Log2`. | Low risk. | — |
|
||||
| 3.8 | **FP64** in some NX2navi cutscene shaders. | No FP64 on some hosts. | the hardware FP64 → FP32 (YZ-2306, PR 10693); ours: `softfloat64` job flag (`tools/xlate/Program.cs` `QueryHostSupportsShaderFloat64`). | — |
|
||||
| 3.9 | **Integer vertex inputs read as raw bits** (bone indices `R8UI`). | Float-declared inputs convert integers. | NXN Mario: stretched characters; re-translated with `uint` inputs. Ours: attribute types passed to `QueryGraphicsState` (`Program.cs`). | NXN |
|
||||
| 3.10 | **BFE / BFI fields past bit 31** (as PTX `bfe` / `bfi`): a field that runs past bit 31 is cut there; one that starts past it extracts 0 (signed: bit 31 repeated) and inserts nothing. NX2navi 1.4.3's Depths terrain vertex shaders (42 variants) take the position from a register, `((2y + x) * 8) & 0xff` with 5-bit x and y, so up to 248. | `OpBitField*Extract` / `OpBitFieldInsert` undefined when offset or offset + count exceeds 32; AMD's bitfield instructions take offset and width modulo 32. | Ryujinx passes the fields through. UltraTears saw the Depths walls and ground missing on the Steam Deck and a Linux RX 570 (RADV). Ours: clamped where the field is not a constant inside the word (translator patch 0012). RX 9070 on Windows drew the Depths the same before and after. | PTX ISA `bfe`, `bfi` |
|
||||
| 3.11 | **SHF by 0** gives the unshifted word (C for `SHF.L`, A for `SHF.R`). | Ryujinx's funnel shifts the other word by 32: undefined in SPIR-V; AMD shifts take the low 5 bits and give A \| C. | the hardware fixed Depths terrain gaps in its funnel shift (PR 10459). Ours: a select on a zero amount (translator patch 0013). In 1.4.3 every register SHF pairs its word with 0, so the result was right already. | — |
|
||||
| 3.12 | **Registers read before any write** on a path (paths the structurizer adds included) have no defined value in the game's code; UltraTears reports 0 from the NVIDIA driver. | An `OpVariable` without an initializer loads an undefined value; Mesa folds undefined values freely. | Ours: on Mesa drivers (RADV, ANV, NVK; `vk_caps_driver`) every function-local variable starts at 0 (`OpConstantNull`, job flag `zero_locals`, translator patch 0016; `NX2NAVI_ZERO_LOCALS` overrides). Elsewhere the load stays undefined: a zero start on every driver (the old patch 0014) made the whole picture shake by about 0.9 output pixels a frame with DLSS or DLAA on an RTX 4080 SUPER (#612). It also moved the SPIR-V hash of the game's FXAA shader out of `FXAA_FS` (`vk_dlaa.c` `vk_dlaa_skip_draw`), the likely cause of the shake; that path is not traced. | — |
|
||||
|
||||
## 4. Memory model
|
||||
|
||||
| # | Maxwell / Tegra | Vulkan / AMD discrete | Emulators / ports | Source |
|
||||
|---|---|---|---|---|
|
||||
| 4.1 | **One memory**: the CPU reads GPU-written buffers and textures after a fence; the GPU sees CPU writes after `FlushMappedRange` or, in practice, immediately (NX2navi skips flushes). Block-linear layout is defined, so a buffer copy into texture storage is a texture upload. | Images live in VRAM with private tiling; buffer bytes copied into image memory are not texels. | the hardware PR 10942 (small CPU writes); MNG alias visibility barriers for copies through two buffers over one allocation. Ours: GPU-written ranges of CPU-visible pools marked at fence completion; GPU-only pools nothing (check 8). | MNG `docs/how-it-works.md:67-71` |
|
||||
| 4.2 | **Counter reports** (`ReportCounter`, `SET_REPORT_SEMAPHORE_A..D` 0x1b00): `{counter, timestamp}`; TIMESTAMP puts 0 in the counter; SAMPLES_PASSED counts visible samples; ZCULL stats. | Occlusion queries are separate objects; results copied to a buffer. | NXN NX2navi: timestamp counter 0 matters for conditional clears. Ours: every report writes `{0, time}`; occlusion counts are always 0 (check 5). DK3D writes `SET_REPORT_SEMAPHORE_OFFSET` (`0x6C0` = 0x1b00) with payload `0` and `OPERATION=Counter`, `STRUCTURE_SIZE=FourWords` (`dk_variable.cpp:193-240`): TIMESTAMP = unit `Crop`, counter `Payload`, written when the ROP has retired every earlier draw; SAMPLES_PASSED = unit `Crop`, counter `0x15`; ZCULL stats = four one-word reports at +0, +4, +8, +12. TIMESTAMP_TOP is not a 3D report: a gpfifo `Semaphore` release of 16 bytes with `ReleaseWfiDisable`, written when the command processor reaches it, without waiting for earlier work (`:246-253`). So `TIMESTAMP - TIMESTAMP_TOP` around a span is its GPU duration. Layout: two 64-bit words, counter then timestamp (`Primer.md:578`). Each report is followed by a tiled-cache flush (`:256`). `ResetCounter` (`0x54C` = 0x1530) zeroes SAMPLES_PASSED; it is the reset for the timestamp types too (`:282-300`). Unit: `dkTimestampToNs(ts) = ts * 625 / 384`, i.e. **614.4 MHz** GPU ticks (1.63 ns), not the 19.2 MHz system tick (`include/deko3d.h:1367-1373`) (check 21). | OGD 0x1b00; NXN; DK3D `dk_variable.cpp:183-300`, `include/deko3d.h:1367` |
|
||||
| 4.3 | **Semaphores / events** written by the GPU in order; CPU polls them. | Writes on a completion thread after the fence. | Ours: `vk_completion_add`, submit order. | — |
|
||||
| 4.4 | **Inline uniform updates** (`UpdateUniformBuffer`) happen in command order between draws; reading the buffer at record time gives the last value. | Must snapshot. | NXN: snapshotted at encode time; Mario's wave sim diverged when two writes reordered. | NXN |
|
||||
| 4.5 | **GPU-written index buffers** (8-bit indices written earlier in the same submission). | No `uint8` index type without `VK_EXT_index_type_uint8`; CPU widening reads stale bytes. | NXN widens on the GPU (`wide_run`). Ours: CPU widening at record time; device-local byte indices skipped (check 9). | NXN |
|
||||
| 4.6 | **Compute shared memory**: 48 KiB per block on GM20B. | AMD 64 KiB. | Ours: `QueryComputeSharedMemorySize` from job (`Program.cs`). | OGD `clb1c0.h` |
|
||||
| 4.7 | **Virtual memory pools** map physical pool pages; the same bytes reachable at two GPU addresses. | Two `VkBuffer`s over one allocation need alias-aware barriers. | MNG `MTL4VisibilityOptionResourceAlias`. | MNG `docs/how-it-works.md:67-71` |
|
||||
| 4.8 | **Command list calls**: a `CallCommands` / `CallList` is not a GPU subroutine. DK3D `dkCmdBufCallList` stores a `Call` control entry (`dk_cmdbuf.cpp:327-338`); `dkQueueSubmitCommands` walks the entries on the CPU and recurses into the called list at submit (`dk_queue.cpp:287-296`), emitting gpfifo entries that point at the command words, which the GPU fetches later (prefetch can be switched off per entry, `:176`). So list structure is fixed at submit, command words are read when fetched. | n/a | Ours: `OP_CALL` is walked at replay, `CallCommands` inlines the recording at record time (`vk_exec.c` `walk`, `nvn_api_cmd.c` `inline_recording`). Equivalent for control flow; deferred bind handles are the data read late (2.4). | DK3D `dk_cmdbuf.cpp:327`, `dk_queue.cpp:280-298` |
|
||||
|
||||
## 5. Fixed function
|
||||
|
||||
| # | Maxwell / NVN | Vulkan / AMD | Emulators / ports | Source |
|
||||
|---|---|---|---|---|
|
||||
| 5.1 | **Provoking vertex: last** (GL convention, NVN default; `SET_PROVOKING_VERTEX` 0x1684, NVN 55 `SetProvokingVertexMode`). Flat-interpolated outputs (instance/particle ids, material indices, flat normals) come from the last vertex. | **First vertex** unless `VK_EXT_provoking_vertex` with `provokingVertexMode = LAST_VERTEX`. | Ours: last-vertex mode where `provokingVertexLast` is available (`vk_pipeline.c` `create_graphics`; check 1). DK3D: `ProvokingVertexLast` (`0x5A1`) is set to 1 at queue setup and per rasterizer state from `DkProvokingVertex` (`maxwell/gpu_3d_base.cpp:76`, `gpu_3d_state.cpp:67`); `DkRasterizerState` defaults to Last (`include/deko3d.h:823`). Confirms last as the hardware-side default. | OGD 0x1684; `runtime/nvn_ext.c`; DK3D `include/deko3d.h:794-823` |
|
||||
| 5.2 | **Primitive restart**: any index value, compared against the raw index for any index size; can apply to lists (`SET_DA_PRIMITIVE_RESTART_TOPOLOGY_CHANGE`) and to non-indexed draws (`..._VERTEX_ARRAY` 0x0de8). | Restart only at all-ones of the index type; lists need `primitiveTopologyListRestart`. | Ryujinx enables it for draw-arrays only on GL (RYU-SU comment). NXN: NX2navi terrain strips use 0xffff. Ours: non-all-ones restart not replayed, logged. DK3D: `PrimitiveRestartEnable` (`0x591` = 0x1644) + `PrimitiveRestartIndex` (`0x592` = 0x1648), index written only when enabled; any value; `PrimitiveRestartWithDrawArrays` (`0x37A` = 0x0de8) is forced 0 at setup (`maxwell/gpu_3d_ibo.cpp:21-30`, `gpu_3d_base.cpp:73`). Nothing on lists vs strips beyond that. | OGD 0x1644, 0x1648; DK3D `maxwell/gpu_3d_ibo.cpp:21-30` |
|
||||
| 5.3 | **Point size**: `SET_POINT_SIZE` (0x1518) when the shader does not write it; GLSLC clamps a written size to a driver constant. | Point size must be written by the shader. | Ryujinx writes the `PointSize` from graphics state at VS start (`Translation/EmitterContext.cs:84-88`); ours reads the `SetPointSize` state from the support buffer (translator patch 0008). | NXN |
|
||||
| 5.4 | **Line width**, smooth lines (`SET_LINE_WIDTH_FLOAT` 0x13b0, `SET_ANTI_ALIASED_LINE`). | `wideLines`, `VK_EXT_line_rasterization`. | Ours: 1 px, logged. | OGD |
|
||||
| 5.5 | **Polygon offset units** are twice the API value in the register; NVN API takes GL units. | `depthBiasConstantFactor` in format-dependent r. | Ryujinx `units / 2f` from the register (RYU-SU). Ours takes NVN API units (right) but D32F instead of D24 (1.7). DK3D `constantFactor * 2.0f` (1.7). | OGD 0x15bc, 0x187c; DK3D `maxwell/gpu_3d_state.cpp:154` |
|
||||
| 5.6 | **Dual-source blending**: SRC1 factors read the shader's second output. | Needs fragment output `Location 0, Index 1`. | Ryujinx declares `Index` only when `DualSourceBlendEnable` (`Translation/Translator.cs:196`, `CodeGen/Spirv/Declarations.cs:608-617`). Ours: SRC1 factors mapped (`vk_pipeline.c` `blend_factor`) but the translator is always given `false` (`Program.cs` `QueryGraphicsState`) (check 12). | — |
|
||||
| 5.7 | **Alpha-to-coverage** with dither (`SET_ALPHA_TO_COVERAGE_DITHER_CONTROL` 0x12e0), alpha test (fixed function on Maxwell). | A2C without dither; no alpha test. | Ryujinx emulates alpha test and A2C dither in the shader from graphics state; ours passes `AlphaTestOp.Always`, A2C false to the translator (`Program.cs` `QueryGraphicsState`). | OGD 0x12e0, 0x16b4 |
|
||||
| 5.8 | **Logic ops** on colour targets. | `logicOp` feature; ignored on float targets. | Ours: in the pipeline key. | OGD 0x19c4 |
|
||||
| 5.9 | **Vertex formats**: `*SCALED` 8/16-bit, RGB10A2 SNORM/SCALED, 3-component formats, 32-bit scaled. | No 32-bit SCALED; 3-component formats often unsupported. | Ours: 32-bit scaled read as integers (logged), 3-component widened (`vk_pipeline.c` vertex format table). | — |
|
||||
| 5.10 | **Transform feedback**, rasterizer discard. | `VK_EXT_transform_feedback`. | Ours: rasterizer discard without XFB = no draw (`nvn_api_cmd.c`). | OGD 0x0744 |
|
||||
| 5.11 | **Depth bounds, sample mask, conservative raster**. | Optional features. | Ours: ignored, logged (`nvn_ext.c`). | OGD 0x0fa4 |
|
||||
| 5.12 | **Blend factors on targets without alpha** (RGBX emulated as RGBA). | Destination alpha reads the stored value. | Ryujinx replaces DST_ALPHA factors on no-alpha targets (`FilterBlendFactor`, RYU-SU). | RYU-SU |
|
||||
|
||||
## 6. Known BotW / NX2navi issues in other projects
|
||||
|
||||
| Game | Symptom | Cause | Fix / link |
|
||||
|---|---|---|---|
|
||||
| NX2navi | 3D scene black | conditional clears ignored | NXN |
|
||||
| NX2navi | cave magenta / black | CPU-filled ASTC/BC with no flush never uploaded | NXN |
|
||||
| NX2navi | stone blocks unlit by torches | roughness atlas built from `ReadTexels` of never-uploaded BC4 | NXN |
|
||||
| NX2navi | BC4 3D texture draws lost | compressed render target | NXN |
|
||||
| NX2navi | missing ambient light, bloom halo | draw with no render target skipped | NXN |
|
||||
| NX2navi | warp pad half black | block height rounding | MNG `docs/how-it-works.md:49-53` |
|
||||
| NX2navi | coloured squares with object / Recall | D16 clear vs fragment rounding with depth EQUAL | MNG `docs/how-it-works.md:57-61` |
|
||||
| NX2navi/BotW | grass flat away from Link, no cut | custom border colour (0.5) | MNG `nvn_mtl.m:5041-5045` |
|
||||
| NX2navi | cloud flicker, missing geometry in the Depths | shader recompiler bugs | YZ-2305 PR 10234 |
|
||||
| NX2navi | red dot on screen | clear targeted the wrong RT index | YZ-2305 PR 10243 |
|
||||
| NX2navi | scrambled Link/terrain at 2× | buffer-cache clears during sync | YZ-2305 PR 10249 |
|
||||
| NX2navi | vertex explosions (RADV) | driver dynamic depth clamp bug | YZ-2305 PR 10262 |
|
||||
| NX2navi | pixelated object glow | attachment feedback loop without barriers | YZ-2305 PR 10402 |
|
||||
| NX2navi | areas black for a frame on camera changes | texture-cache alias/overlap order | YZ-2305 PR 10418 |
|
||||
| NX2navi | Gloom textures on terrain | block-depth adjustment on 3D slices | YZ-2305 PR 10433 |
|
||||
| NX2navi | terrain gaps at distance (Depths) | one-character shader recompiler error | YZ-2305 PR 10459 |
|
||||
| NX2navi | mangled textures on RDNA3 | not found (AMD test case) | YZ-2305 |
|
||||
| BotW/NX2navi | slow rain/snow, particle corruption on teleport | small CPU writes into GPU-used pages not invalidated | YZ-2306 PR 10942 |
|
||||
| NX2navi | AO broken above 1008p mods | rescale thresholds | YZ-2306 PR 10675 |
|
||||
| NX2navi | "detective mode" Link on AMD | AMD EDS3 colour-blend driver regression | YZ-2306 PR 10946 |
|
||||
| BotW | shadow artifacts at object edges (AMD) | gather rounding | the hardware commit 780240e (2.10) |
|
||||
| NX2navi | dashed dark lines along wall and floor edges, changing with the camera angle (AMD, RADV) | gather rounding | gather bias (2.10) |
|
||||
| NX2navi | blocky foliage shadows that jump instead of sliding | not a GPU difference: the engine sizes its sun shadow map to the area it covers, down to about 400 near the ground, and each change of size moves the shadow texels | fixed size, `shadow_res` (the shadow set of `recomp/patches-v121.txt`; the community mods force 1024 the same way) |
|
||||
| BotW | black textures on GCN | HTILE (depth compression) | `AMD_DEBUG=nohyperz` wiki (1.9) |
|
||||
| BotW | 3D scene black | deferred texture handles | NXN |
|
||||
| BotW | albedo scattered squares | duplicate colour attachments | NXN |
|
||||
| BotW | HUD dark boxes, black stamina wheel | capture texture swizzle rebuild | NXN |
|
||||
|
||||
## 7. Checks for our runtime
|
||||
|
||||
Grep-level look on 2026-10-22, statuses updated 2026-10-26; "saw" is what the grep and short reads showed, not a test result.
|
||||
|
||||
1. **Provoking vertex.** Vulkan draws flat outputs from the first vertex, NVN from the last. `vk_pipeline.c` enables
|
||||
`provokingVertexLast` and chains `VkPipelineRasterizationProvokingVertexStateCreateInfoEXT{LAST_VERTEX}` in
|
||||
`create_graphics`; without the feature it logs that flat outputs come from the first vertex. **Fixed.**
|
||||
2. **Unwritten vertex outputs.** `spirv_add_outputs` (`vk_pipeline.c`) adds outputs initialised to (0, 0, 0, 1) for
|
||||
FS inputs the VS lacks. Our translator never links stages, so Ryujinx's `InitializeVertexOutputs` would initialise
|
||||
no user output; translator patch 0006 initialises the unlinked outputs. **Fixed.**
|
||||
3. **Masked clears.** `vk_exec.c` skips colour clears with a partial channel mask (`W_CLEAR_MASK`); `clear_ds` skips stencil
|
||||
clears with a partial mask (`W_STENCIL_MASK`). NX2navi issues such clears (NXN). Check the log for
|
||||
`colour clear with channel mask` and `stencil clear with mask`. DK3D (1.3) confirms what the replay must do: colour clear = scissor 0 + channel mask, stencil clear = `value & mask` merged under the front write mask; so a draw with `colorWriteMask` / `stencilWriteMask` set to the clear mask is the exact equivalent. **Missing.**
|
||||
4. **D24 → D32F.** `vk_formats.c` maps D24S8 to `D32_SFLOAT_S8_UINT`; `vk_exec_draw.c` `record_fixed` passes NVN units
|
||||
unscaled. Depth EQUAL passes and decal offsets can z-fight. Look at: per-pass depth-test op histogram, EQUAL passes
|
||||
against D24 targets. **N/A for NX2navi**: a census of the field and fight saves (2026-10-22) saw depth targets
|
||||
0x34 (DEPTH32F) and 0x32 (DEPTH16) only; biased draws (units -5 and 1, factor -0.5 and -0.2) are on DEPTH32F.
|
||||
5. **Counters and conditional rendering.** `vk_exec_xfer.c` (as of 2026-10-22): every report writes `{0, time}`;
|
||||
SAMPLES_PASSED is always 0; a condition is read on the CPU at record time and an unfinished report reads 0. Grep the
|
||||
NVN log for counter types other than TIMESTAMP; any occlusion-driven visibility (flares, culling) is off. **Fixed 2026-10-22**:
|
||||
SAMPLES_PASSED counts come from Vulkan occlusion queries between RESET and REPORT (`vk_exec_xfer.c`, ~4 queries
|
||||
per frame in the field, ~21 in a fight); the game's CPU reads them (guest f_70010c3298) to cull objects.
|
||||
6. **Subgroup size.** The translator assumes 32; RDNA4 may run fragment shaders in wave64. `vk_caps.c` and
|
||||
`vk_pipeline.c` `pin_subgroup32` pin stages to 32 lanes with `VkPipelineShaderStageRequiredSubgroupSizeCreateInfo`
|
||||
where `subgroupSizeControl` allows it, and otherwise tell the translator the GPU's own size
|
||||
(`NX2NAVI_VK_SUBGROUP_PIN=0` forces that). **Fixed.**
|
||||
7. **Images aliased onto GPU-only pool memory.** `vk_texture.c` `bind_pool_memory` binds each `VkImage` into its pool
|
||||
allocation; textures are keyed by address plus descriptor, and each new image starts `UNDEFINED` (`init_image`). Two descriptors over the
|
||||
same storage, or bytes written through a buffer, do not see each other's data on AMD. Check: count distinct keys per
|
||||
address per frame. **Risk** (the hardware PR 10418 class).
|
||||
8. **GPU writes into CPU-visible pools.** `note_gpu_write` (`vk_exec_xfer.c`) stamps pages when the fence
|
||||
completes, so a texture sampled later in the same submission keeps its old upload; GPU-only pools are skipped.
|
||||
MNG saw at most one buffer copy per frame in NX2navi, so low priority. **Risk.**
|
||||
9. **Index buffers.** `vk_exec_draw.c` `op_draw_idx`: byte indices widened on the CPU at record time, skipped in
|
||||
device-local memory; `restart_for`: a non-all-ones restart index logged and dropped. Census (2026-10-22): 130-170 byte-index draws/frame widened on the
|
||||
CPU, 0 device-local, 0 non-all-ones restarts. **OK**; `VK_KHR_index_type_uint8` (supported) would drop the CPU cost.
|
||||
10. **Feedback loops.** Census (2026-10-22): 425/frame (field) to 667 (fight), G-buffer material/decal shaders
|
||||
sampling the 1600x900 targets of the open pass. The terrain G-buffer pass reads its own albedo, normal and R32F
|
||||
targets at the fragment's pixel; on the RX 9070 the reads returned stale data as ~16x14 px dark rectangles on the
|
||||
ground, and the sage spirit next to Link was not drawn. `vk_exec_draw.c` (feedback loops) now detects such a draw:
|
||||
with `VK_EXT_attachment_feedback_loop_layout` and `dynamicRenderingLocalRead` the target moves to the feedback
|
||||
layout and a by-region barrier precedes the draw; without them the pass restarts behind a full barrier
|
||||
(`NX2NAVI_FEEDBACK`). **Fixed.**
|
||||
11. **Border colours.** `vk_texture.c` `make_sampler`: custom colour only with `customBorderColorWithoutFormat`, else transparent
|
||||
black; integer formats use the float path. Confirm the RX 9070 reports the feature (grass map needs 0.5).
|
||||
**OK if supported.**
|
||||
12. **Dual-source blend.** `vk_pipeline.c` `blend_factor` maps SRC1 factors; `Program.cs` `QueryGraphicsState` always passes
|
||||
`dualSourceBlendEnable = false`, so no `Index 1` output exists. Log pipelines with SRC1 factors. **Mismatch if used.**
|
||||
13. **FMZ / NaN.** Count `FMUL/FFMA/FMUL32I/FFMA32I` words with the `Fmz` field set (`Decoders/InstDecoders.cs:1901`
|
||||
bit positions) in `build/v121/shaders/`; FMin/FMax/FClamp NaN results are undefined (3.1). Census (2026-10-22):
|
||||
FMZ in 1 instruction of 12397 shaders, FTZ in 12241 shaders, FMNMX in 7457. The translator emits GLSL.std.450
|
||||
FMin/FMax (Ryujinx `GenerateMinimum`), not NMin/NMax. **FMZ: no fix needed. FMNMX: OK** (RX 9070 test: FMin/FMax(NaN, 1) = 1 both orders, FClamp(NaN, 0, 1) = 0).
|
||||
14. **Point size / line width.** Census: `SetLineWidth 4.5` ~1/frame (2 line draws into a 144x144 R8 target),
|
||||
`SetPointSize 5` ~14/frame; 4 point VS rely on it. Point size: translator patch 0008 reads it from the support
|
||||
buffer, **fixed**. Line width: 1 px, non-default values logged (`nvn_ext.c`), **mismatch, minor**.
|
||||
15. **Block height.** `nvn_tex.c` truncates like NVN (2.1). DK3D rounds up (`dk_image.cpp:446`); every other step matches. **OK.**
|
||||
16. **Depth mode and clamp.** `vk_pipeline.c` `create_graphics` (`negativeOneToOne` from `nvn_depth_minus_w`, `depthClampEnable`
|
||||
from NVN state). DK3D agrees on the z maths (1.5) and the clamp switch (1.6). **OK.**
|
||||
17. **Depth bias argument order.** `vk_exec_draw.c` `record_fixed` (units, clamp, factor); DK3D takes the same three and doubles units for the register (1.7). **OK.**
|
||||
18. **Sampler LOD.** `vk_texture.c` `make_sampler`. **OK.**
|
||||
19. **Viewport / scissor arrays.** Single viewport (`vk_exec_draw.c` `record_fixed`), extra scissors logged. Census: 0 differing. **OK.**
|
||||
|
||||
20. **Depth-compare sampling in compute** (VUID-vkCmdDispatch-None-06479): one compute shader samples a 4x4 R32F
|
||||
CPU-written placeholder through an R32_SFLOAT view with compare on, ~1/frame. Needs a D32 copy. **Minor.**
|
||||
21. **Report timestamps: unit and instant.** `write_counter` (`vk_exec_xfer.c`) writes host `ns * 12 / 625` (19.2 MHz
|
||||
ticks) at fence completion for every report; `nvnDeviceGetTimestampInNanoseconds` converts back with the same factor
|
||||
(`nvn_ext.c`). DK3D: GPU ticks convert at 625/384 ns (614.4 MHz), TIMESTAMP is written when the ROP has retired
|
||||
the earlier work, TIMESTAMP_TOP when the command processor reaches it (4.2). Two consequences for the engine's GPU time (dynamic resolution and speed follow it):
|
||||
(a) if the 1.2.1 consumer scales raw report ticks with its own constant rather than the NVN conversion call, our
|
||||
19.2 MHz unit is 32x off from the hardware's; find that consumer before fixing the unit; (b) the delta needs
|
||||
`vkCmdWriteTimestamp2` with `TOP_OF_PIPE` for TIMESTAMP_TOP and `BOTTOM_OF_PIPE` for TIMESTAMP, per submission
|
||||
segment, converted to whichever unit (a) shows. **Mismatch (instant); unit unverified.**
|
||||
|
||||
## 8. Sources not read yet
|
||||
|
||||
- Mesa NVK (`src/nouveau/vulkan`, MIT): how it maps Vulkan provoking vertex, depth clip, restart, feedback loops onto B197.
|
||||
- envytools rnndb `graph/gf100_3d.xml`, `tsc.xml`, `tic.xml` (MIT): field-level TIC/TSC meanings (LOD snap, trilinear opt).
|
||||
- Ryujinx `Ryujinx.Graphics.Gpu/Image/TextureCache.cs`, `Texture.cs` (MIT): alias/overlap rules, depth texture swizzle.
|
||||
- Ryujinx NX2navi/BotW PRs and issues (GitHub repo gone; mirrors blocked or 404 on 2026-10-22).
|
||||
- the hardware progress reports 2023-08 to 2024-02 and suyu/sudachi trackers (behaviour only).
|
||||
- switchbrew wiki NVN and GPU pages.
|
||||
107
docs/mods.md
Normal file
107
docs/mods.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Mods
|
||||
|
||||
NX2navi Recomp loads the mods players make for the Switch emulators (Ryujinx, the hardware and its forks): texture and model
|
||||
packs, gameplay and text changes from GameBanana and similar sites, a mod manager's merged output, code patches (`.ips`,
|
||||
`.pchtxt`) and Atmosphère cheat files. No mod or cheat file ships with the port.
|
||||
|
||||
## Install a mod
|
||||
|
||||
1. Open the settings menu (F1, Esc, or Minus and Plus together on a controller), go to the Mods tab and choose
|
||||
**Open mods folder**. That is the `mods` folder next to `nx2navi.exe`; the menu creates it when it is missing.
|
||||
2. Make one folder per mod in it and unpack the mod's archive into that folder. The mod's own folders (`romfs`,
|
||||
`exefs`, `cheats`) must sit directly inside the mod's folder:
|
||||
|
||||
```
|
||||
mods\
|
||||
Better Grass\
|
||||
romfs\
|
||||
...
|
||||
Fast Travel Anywhere\
|
||||
exefs\
|
||||
patch.pchtxt
|
||||
My cheats\
|
||||
cheats\
|
||||
277178B7DBA1B6D4.txt
|
||||
```
|
||||
|
||||
An archive laid out for a Switch SD card or an emulator works as it is. Inside the mod's folder the port also finds
|
||||
`atmosphere\contents\0100F2C0115B6000\romfs`, `contents\0100F2C0115B6000\romfs` and `0100F2C0115B6000\romfs`, the
|
||||
same places for `exefs` and `cheats`, and `atmosphere\exefs_patches\<name>\` for code patches.
|
||||
3. Start the game. A mod that patches the game's code makes the game rebuild itself first (see Code patches below).
|
||||
4. Open the Mods tab to check the mod. Each row says how many game files the mod replaces and adds, how many cheats it
|
||||
brings, and why a part of it is not used.
|
||||
|
||||
## Order and switches
|
||||
|
||||
The Mods tab lists every folder in `mods`. Select a mod and press A or Enter to switch it on or off. The top face button
|
||||
or Ctrl+Up moves it up, the left face button or Ctrl+Down moves it down. When two mods change the same file, the
|
||||
higher one wins; the lower mod's row says how many of its files the higher one covers. Changes apply after a restart.
|
||||
|
||||
The menu saves the order and the switches in `nx2navi.ini`, under `[mods]`, by folder name: `mods_order` and
|
||||
`mods_disabled`. A folder the list does not name yet, such as a new or renamed one, goes below the named ones.
|
||||
|
||||
### Mods that change the same files
|
||||
|
||||
Two mods that edit the same game file do not combine: the higher one wins the whole file. Merge such mods with
|
||||
a mod manager and set its export location to an empty folder inside
|
||||
`mods`, for example `mods\merged`. The exported folder then counts as one mod here. Mods packaged for a mod manager only
|
||||
(`.tkcl` files) go the same way.
|
||||
|
||||
The port merges the resource size table on its own: at every start with mods on, it combines the tables the mods ship
|
||||
and raises the entries for files a mod made larger or added. A separate RESTBL mod is not needed.
|
||||
|
||||
## Code patches (ExeFS)
|
||||
|
||||
A mod with `.ips` or `.pchtxt` files patches the game's code. A patch is for one game update: an `.ips` file is named
|
||||
after the build id of the update's code, a `.pchtxt` file names it on its `@nsobid` line. The game's code is compiled
|
||||
ahead of time, so these patches are built into `nx2navi.exe`:
|
||||
|
||||
1. The first start after such a mod is added closes the game and opens the setup's rebuild window. The rebuild takes
|
||||
a few minutes (the shaders stay prepared), then the game starts again. Saves and settings are kept.
|
||||
2. Until the rebuild succeeds, the whole mod stays off, its other files included.
|
||||
3. After the rebuild the mod switches on and off from the Mods tab like any other, without another rebuild.
|
||||
|
||||
The rebuild needs the setup folder the game was installed from. When that folder was moved or deleted, run
|
||||
`NX2navi-Setup.exe` again on the same install folder: it does the same job. `mods_rebuild = off` under `[mods]` in
|
||||
`nx2navi.ini` turns the automatic rebuild off.
|
||||
|
||||
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
|
||||
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
|
||||
|
||||
## Cheats
|
||||
|
||||
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
|
||||
that change the game's code need the same rebuild as code patches. [cheats.md](cheats.md) explains the file names for
|
||||
each game update, the rebuild and which cheats cannot run.
|
||||
|
||||
## Renaming a mod folder
|
||||
|
||||
The port keys a mod by its folder name. A renamed folder counts as a new mod:
|
||||
|
||||
- it moves below the mods the list names, and comes back switched on;
|
||||
- its cheats come back switched off;
|
||||
- its code patches and code cheats are built again, so the next start runs the rebuild.
|
||||
|
||||
Rename mod folders before you set them up in the menu, not after.
|
||||
|
||||
## What does not work
|
||||
|
||||
| On the Mods tab | Why | What to do |
|
||||
|---|---|---|
|
||||
| This folder has no romfs, exefs or cheats files. | The archive was unpacked one level too deep (`mods\Mod\Mod\romfs`), or it holds no game files. | Move the folders up so `romfs` sits directly in the mod's folder. |
|
||||
| ... files are made for another game version and are left out. | The mod's files carry another update's version in their names (`.Product.<number>.`). | Download the mod's release for your game update. |
|
||||
| Its code patches (exefs) are made for another game version. | The patch files name another update's build id. | Same: get the release for your update. |
|
||||
| This mod brings its own game code (a subsdk plugin or a replaced main). | Mods such as UltraCam ship compiled Switch code. The port cannot load it. | No workaround; the whole mod stays off. |
|
||||
| This mod patches the game code (exefs) in a way the port cannot apply. | The patch writes the game's data, not its code, or changes code in a form the port cannot build. | No workaround; the reason follows on the row. |
|
||||
| The folder name contains \|. | The mod list uses `\|` between names. | Rename the folder. |
|
||||
|
||||
A mod never stops the game from starting. Whatever the port cannot use is left out and shown on the Mods tab.
|
||||
|
||||
## When something looks wrong
|
||||
|
||||
1. Switch the newest mod off on the Mods tab and restart. If the problem goes away, check that the mod is made for your
|
||||
game update and does not fight another mod over the same files.
|
||||
2. `logs\nx2navi.log` next to `nx2navi.exe` names every mod at the start (lines starting with `mods:`), the cheats
|
||||
(`cheats:`) and the resource size table (`RomFS: resource size table:`). Open log folder on the Advanced tab of
|
||||
the menu shows the logs.
|
||||
3. When reporting a problem, attach `nx2navi.log` and list your mods with the links you downloaded them from.
|
||||
109
docs/tester-guide.md
Normal file
109
docs/tester-guide.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Tester guide: running NX2navi Recomp on your GPU
|
||||
|
||||
This guide is for testing the build on a GPU other than the developer's (AMD RX 9070). You build it from source with
|
||||
your own copy of the game and your own console keys. Nothing from the game is in this repository, and nothing you build
|
||||
from it may be shared.
|
||||
|
||||
## What you need
|
||||
|
||||
- Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA recommended: they build the default, faster version), about
|
||||
60 GB free.
|
||||
- A GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, with a current driver.
|
||||
- *Ocarina of Time* base game (1.0.0) and the 1.1.0, 1.2.1 or 1.4.3 update, dumped from your own Switch as NSP or XCI.
|
||||
- `prod.keys` dumped from your own Switch. Never send it to anyone, and never paste it into a log or an issue.
|
||||
|
||||
## Build
|
||||
|
||||
1. Follow [building.md](building.md): the toolchain, then build steps 1-6 (Python environments, `config.local.toml`
|
||||
with your own paths, extraction, the shader translator, lifting and linking, shader preparation). The result is
|
||||
`build\<ver>\nx2navi.exe`, with `<ver>` the game version you built (`v143` or `v121`).
|
||||
2. Check the build without the game: `build\<ver>\nx2navi.exe --selftest` must print a pass.
|
||||
3. To run the build on another of your machines, package a portable folder ([building.md](building.md), "Portable
|
||||
folder"). The folder holds game code: for your own machines only.
|
||||
4. The installer ([installer.md](installer.md)) is the other route to a machine without the toolchain: it builds the
|
||||
game there from your own files. No installer release is published yet.
|
||||
|
||||
## Run and collect the log
|
||||
|
||||
Every run writes its log to `logs\nx2navi.log` next to `nx2navi.exe`; the four runs before it are kept as `nx2navi.1.log` to
|
||||
`nx2navi.4.log`, and Open log folder in the settings menu's Advanced tab (F1) shows them. Copy the log before you start the
|
||||
game again. A file stops at 64 MiB. Started with `2> file`, the game writes its log to that file instead.
|
||||
|
||||
From `cmd.exe` in the repository root, with the validation layer on:
|
||||
|
||||
```bat
|
||||
set NX2NAVI_VK_VALIDATION=1
|
||||
build\<ver>\nx2navi.exe
|
||||
```
|
||||
|
||||
`NX2NAVI_VK_VALIDATION=1` turns on the Vulkan validation layer (it comes with the Vulkan SDK). It makes the game slower;
|
||||
do one run with it and one without (`set NX2NAVI_VK_VALIDATION=`).
|
||||
|
||||
If the game stops with a lost device (the log says `device lost` or `VK_ERROR_DEVICE_LOST`), the lines starting with
|
||||
`vk: pass marks` name the render pass or dispatch the GPU was running. AMD GPUs write them by default. On an NVIDIA GPU,
|
||||
`set NX2NAVI_PASS_CRUMBS=1` (or GPU pass marks: On in the Advanced tab) before the run that reproduces the crash.
|
||||
|
||||
The game keeps running behind other windows (`assume_focus = 0` in `nx2navi.ini` pauses it instead). Shaders the preparation did not cover compile on first use, so
|
||||
the first minutes of play can stutter.
|
||||
Play into the open field (load a save, walk around for a minute, fight something if you can), then close the window.
|
||||
|
||||
## Choosing the GPU
|
||||
|
||||
A laptop with an iGPU and a discrete GPU, or two cards, lists every GPU at the top of the log (`vk: gpu 0: ...`).
|
||||
|
||||
- `set NX2NAVI_GPU_NAME=Radeon` picks the first usable GPU whose name contains that text (case does not matter).
|
||||
- `set NX2NAVI_GPU_INDEX=1` picks GPU 1 from that list.
|
||||
|
||||
The line `vk: using gpu N: <name>` says which one ran.
|
||||
|
||||
## Mods
|
||||
|
||||
Mods and cheats made for the emulators go into `mods/` next to `nx2navi.exe`, one folder per mod. [mods.md](mods.md) covers
|
||||
installing them, their order, code patches, what does not work and what to check when something looks wrong;
|
||||
[cheats.md](cheats.md) covers cheat files. The log names every mod at the start (`mods: <name>: ...`); when you
|
||||
report a problem with mods, list them with their download links.
|
||||
|
||||
## What to send back
|
||||
|
||||
1. `logs\nx2navi.log` from each run. It contains no keys and no game files. The lines that matter most start with
|
||||
`vk caps:` (GPU, driver, features and the fallbacks taken), `vk: gpu`, `validation` and `gfx_shader:`.
|
||||
2. GPU model, driver version, CPU, RAM, and whether it is a laptop. Copy system info in the settings menu's Advanced
|
||||
tab (F1) puts these and your changed settings on the clipboard. Say which game update you ran.
|
||||
3. What you saw: how far it got (menu, loading, field), roughly how smooth it ran, and screenshots of anything wrong
|
||||
(black or missing areas, flickering, wrong colours, missing grass). Win+Shift+S is fine.
|
||||
4. If it crashed or hung: what you were doing. The log of that run ends with the crash report, so send it whole.
|
||||
5. For a bug you can trigger again: a recording of your input (`set NX2NAVI_INPUT_RECORD=input.txt`, see
|
||||
[controls.md](controls.md), "Scripted input") and the save you started from.
|
||||
|
||||
## Steam Deck
|
||||
|
||||
Build or install on Linux ([installer.md](installer.md), "Linux release"), in Desktop Mode. The game then runs in either
|
||||
mode; for Game Mode, add `nx2navi` from the install folder to Steam as a non-Steam game.
|
||||
|
||||
The game recognises the Deck (LCD and OLED) and uses its device profile for every setting `nx2navi.ini` does not set:
|
||||
fullscreen, the screen's own 1280x800 (16:10: the camera and interface take that shape), the game's own 3D scale,
|
||||
vsync and a 40 fps limit. These are starting points, not measurements. What helps most:
|
||||
|
||||
1. The performance overlay (F10, or both stick clicks L3 + R3) in full mode: the frame rate, mean and longest frame
|
||||
time over a minute in Lookout Landing, in the Depths and in the Sky, with the settings named.
|
||||
2. The same with `fps_limit` 30, 45 and 60 and `render_scale` 50, 67 and game, so the defaults can be picked from
|
||||
measurements. Steam's own performance overlay (the ... button) adds power draw and temperatures.
|
||||
3. LCD or OLED, SteamOS version (`uname -r` in Desktop Mode's terminal), and the game update.
|
||||
4. Anything that looks stretched, squeezed or cut off, with a screenshot. At 16:10 the menus keep their 16:9
|
||||
proportions in the middle of the taller screen, the HUD sits at the top and bottom edges, and full-screen
|
||||
backgrounds reach them; pre-rendered cutscenes play stretched.
|
||||
|
||||
`device_profile = desktop` in `nx2navi.ini` turns the profile off; `deck` turns it on elsewhere, to try the Deck's
|
||||
settings on a PC.
|
||||
|
||||
## Switches for the developer's questions
|
||||
|
||||
Only set these when asked; each forces a fallback the build picks by itself on GPUs that need it.
|
||||
|
||||
| Variable | Effect |
|
||||
|---|---|
|
||||
| `NX2NAVI_VK_NO_FLOAT64=1` | Translate shaders without 64-bit floats |
|
||||
| `NX2NAVI_VK_SSBO_ALIGN=64` | Treat storage buffers as needing 64-byte alignment |
|
||||
| `NX2NAVI_VK_SUBGROUP_PIN=0` | Do not pin shaders to 32 lanes; tell the translator the GPU's own subgroup size |
|
||||
|
||||
Each switch makes the first launch translate every shader again.
|
||||
11
installer/__init__.py
Normal file
11
installer/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""The NX2navi installer: builds a playable folder from the player's own game files (docs/installer.md).
|
||||
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR] [--root DIR]
|
||||
[--temp DIR]
|
||||
|
||||
Modules: layout (where the bundled tools are, release or repository), events (JSON progress on stdout and the log
|
||||
file), state (nx2navi-install.json), runner (which steps run: resume and update), steps (the eight steps), ini (nx2navi.ini),
|
||||
cli (the command line). text/en.json holds the English text of every message id the events carry.
|
||||
|
||||
Standard library only, apart from what the extraction tools import (cryptography). Runs on Python 3.14.
|
||||
"""
|
||||
5
installer/__main__.py
Normal file
5
installer/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
|
||||
from installer.cli import main
|
||||
|
||||
sys.exit(main())
|
||||
94
installer/cli.py
Normal file
94
installer/cli.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR]
|
||||
[--root DIR] [--temp DIR] [--verify] [--dlss]
|
||||
|
||||
A first install needs every option. --firmware names the Switch firmware folder (an unpacked firmware or an emulator's
|
||||
Contents/registered); the setup extracts the system shared fonts from it when the language is Chinese, Korean or
|
||||
Traditional Chinese. On a folder with nx2navi-install.json (resume after a cancel or failure, or update with a newer
|
||||
release) only --dest is needed: --lang defaults to the recorded language, and the game files are needed only when the
|
||||
extraction has to run again. Prints JSON events on stdout (events.py). Exit code: 0 done, 1 failed, 2 bad command
|
||||
line, 3 cancelled (Ctrl+C or Ctrl+Break).
|
||||
|
||||
--dlss downloads NVIDIA's DLSS library (nvngx_dlss.dll) from NVIDIA's DLSS SDK repository into the install folder on
|
||||
Windows with an NVIDIA RTX GPU (steps.py, Dlss).
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from . import layout as layouts
|
||||
from . import state as statefile
|
||||
from .events import Reporter
|
||||
from .runner import Context, run_steps
|
||||
from .steps import all_steps, migrate_state
|
||||
|
||||
EXIT = {'ok': 0, 'failed': 1, 'cancelled': 3}
|
||||
|
||||
|
||||
class Parser(argparse.ArgumentParser):
|
||||
def error(self, message):
|
||||
Reporter().error('cli.usage', {'detail': message})
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# The temp folder inside the install folder when the layout names none (the Linux release); the check step does not
|
||||
# count it when it requires an empty install folder
|
||||
TEMP_IN_DEST = '.setup-temp'
|
||||
|
||||
|
||||
def temp_folder(base, dest):
|
||||
"""One temp folder per install folder, so two installs never share generated files."""
|
||||
tag = hashlib.sha256(str(dest).lower().encode()).hexdigest()[:8]
|
||||
return base / f'{dest.name or "root"}-{tag}'
|
||||
|
||||
|
||||
def cancel(signum, frame):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = Parser(prog='python -m installer', description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--base', type=Path, help='base game, XCI or NSP (or NSZ, XCZ: unpacked first)')
|
||||
ap.add_argument('--update', type=Path, help='the update NSP or NSZ: 1.1.0, 1.2.1 or 1.4.3')
|
||||
ap.add_argument('--keys', type=Path, help='prod.keys')
|
||||
ap.add_argument('--firmware', type=Path, help='Switch firmware folder (Contents/registered) for the shared fonts '
|
||||
'of Chinese, Korean and Traditional Chinese text')
|
||||
ap.add_argument('--lang', help='game language, a Switch language code (en-US, en-GB, ja, de, ru, ...)')
|
||||
ap.add_argument('--dest', type=Path, required=True, help='install folder')
|
||||
ap.add_argument('--root', type=Path, help='release folder or repository (default: the folder above installer/)')
|
||||
ap.add_argument('--temp', type=Path, help='temp folder (default: temp/ of the release on Windows, .setup-temp in '
|
||||
'the install folder on Linux, build/install-temp in a repository)')
|
||||
ap.add_argument('--verify', action='store_true', help='hash every kept file, also those this release made')
|
||||
ap.add_argument('--dlss', action='store_true', help="download NVIDIA's nvngx_dlss.dll (NVIDIA RTX SDK license) next "
|
||||
'to the game, for dlaa and the DLSS modes')
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
# The window cancels with Ctrl+Break to the installer's process group (CREATE_NEW_PROCESS_GROUP), which also reaches
|
||||
# the tools it runs; Python's default for it ends the process without recording anything.
|
||||
for name in ('SIGBREAK', 'SIGTERM'):
|
||||
if hasattr(signal, name):
|
||||
signal.signal(getattr(signal, name), cancel)
|
||||
layout = layouts.find(args.root)
|
||||
dest = args.dest.resolve()
|
||||
base = args.temp or layout.temp
|
||||
temp = temp_folder(base.resolve(), dest) if base else dest / TEMP_IN_DEST
|
||||
log = temp / 'logs' / time.strftime('install-%Y%m%d-%H%M%S.log')
|
||||
rep = Reporter(log_path=log)
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
state = statefile.load(dest)
|
||||
except statefile.StateError as e:
|
||||
rep.error(e.id, {'path': str(e.path)})
|
||||
rep.emit('end', result='failed', seconds=0)
|
||||
return 1
|
||||
ctx = Context(layout, dest, temp, rep, args, state=state, verify=args.verify)
|
||||
migrate_state(ctx)
|
||||
steps = all_steps()
|
||||
rep.emit('start', release=layout.release, mode=ctx.mode, dev=layout.dev, dest=str(dest), log=str(log),
|
||||
steps=[s.id for s in steps])
|
||||
result = run_steps(steps, ctx)
|
||||
rep.emit('end', result=result, seconds=round(time.monotonic() - t0, 1))
|
||||
rep.close()
|
||||
return EXIT[result]
|
||||
123
installer/events.py
Normal file
123
installer/events.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Progress events: one JSON object per line on stdout for the window and tests, the same in English to a log file.
|
||||
|
||||
Every event has "type" and "t" (seconds since the start). Types:
|
||||
|
||||
start release, mode (install, resume, update), dest, log, steps (the step ids in order)
|
||||
step step, state: started | done (seconds) | skipped (reason: current, not_needed)
|
||||
progress step, phase, and done/total/unit (bytes, chunks, objects, shaders) when the amount is known
|
||||
space dest, need, free (bytes on the install disk; with the temp folder on the same disk need includes it),
|
||||
temp, temp_need, temp_free, same_disk
|
||||
warning id, params
|
||||
error id, params, step (absent outside a step)
|
||||
end result: ok | failed | cancelled, seconds
|
||||
|
||||
Text is never the payload: ids name a step ("step.<id>"), a phase ("phase.<step>.<phase>") or a message ("<id>"),
|
||||
and installer/text/en.json holds their English text with {param} placeholders. The log renders events with it.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
CATALOG = Path(__file__).resolve().parent / 'text' / 'en.json'
|
||||
PROGRESS_EVERY = 0.5 # seconds between two progress events of one phase
|
||||
|
||||
|
||||
def load_catalog(path=CATALOG):
|
||||
return json.loads(path.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
GAME_EXE = 'nx2navi.exe' if os.name == 'nt' else 'nx2navi' # {game_exe} in the catalogs
|
||||
|
||||
|
||||
def render(catalog, id, params=None):
|
||||
"""The English text of id, or the id itself when the catalog lacks it."""
|
||||
text = catalog.get(id, id)
|
||||
try:
|
||||
return text.format(**{'game_exe': GAME_EXE, **(params or {})})
|
||||
except (KeyError, IndexError, ValueError):
|
||||
return f'{text} {params}'
|
||||
|
||||
|
||||
class Reporter:
|
||||
def __init__(self, out=None, log_path=None):
|
||||
self.out = out or sys.stdout
|
||||
self.t0 = time.monotonic()
|
||||
self.catalog = load_catalog()
|
||||
self.lock = threading.Lock()
|
||||
self.log_file = None
|
||||
self.last_progress = {}
|
||||
if log_path:
|
||||
Path(log_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
self.log_file = open(log_path, 'a', encoding='utf-8', buffering=1)
|
||||
self.log_path = log_path
|
||||
|
||||
def elapsed(self):
|
||||
return time.monotonic() - self.t0
|
||||
|
||||
def log(self, text):
|
||||
"""A line for the log file only."""
|
||||
if self.log_file:
|
||||
with self.lock:
|
||||
self.log_file.write(f'[{self.elapsed():8.1f}] {text}\n')
|
||||
|
||||
def emit(self, type, **fields):
|
||||
event = {'type': type, 't': round(self.elapsed(), 2), **fields}
|
||||
with self.lock:
|
||||
self.out.write(json.dumps(event, ensure_ascii=False) + '\n')
|
||||
self.out.flush()
|
||||
self.log(self.describe(event))
|
||||
return event
|
||||
|
||||
def describe(self, e):
|
||||
c = self.catalog
|
||||
kind = e['type']
|
||||
if kind == 'step':
|
||||
name = render(c, 'step.' + e['step'])
|
||||
extra = f' ({e["seconds"]:.1f} s)' if 'seconds' in e else f' ({e["reason"]})' if 'reason' in e else ''
|
||||
return f'{name}: {e["state"]}{extra}'
|
||||
if kind == 'progress':
|
||||
text = render(c, f'phase.{e["step"]}.{e["phase"]}')
|
||||
if e.get('total'):
|
||||
text += f' {e["done"]}/{e["total"]} {e.get("unit", "")}'.rstrip()
|
||||
return text
|
||||
if kind in ('warning', 'error'):
|
||||
return f'{kind}: {render(c, e["id"], e.get("params"))} [{e["id"]}]'
|
||||
return json.dumps(e, ensure_ascii=False)
|
||||
|
||||
def step(self, step, state, **fields):
|
||||
return self.emit('step', step=step, state=state, **fields)
|
||||
|
||||
def progress(self, step, phase, done=None, total=None, unit=None, force=False):
|
||||
"""A progress event, at most one per PROGRESS_EVERY per phase unless it is the first or the last."""
|
||||
now = time.monotonic()
|
||||
key = (step, phase)
|
||||
last = self.last_progress.get(key)
|
||||
final = total is not None and done is not None and done >= total
|
||||
if last is not None and not final and not force and now - last < PROGRESS_EVERY:
|
||||
return None
|
||||
self.last_progress[key] = now
|
||||
fields = {'step': step, 'phase': phase}
|
||||
if done is not None:
|
||||
fields['done'] = done
|
||||
if total is not None:
|
||||
fields['total'] = total
|
||||
if unit:
|
||||
fields['unit'] = unit
|
||||
return self.emit('progress', **fields)
|
||||
|
||||
def warning(self, id, **params):
|
||||
return self.emit('warning', id=id, params=params)
|
||||
|
||||
def error(self, id, params, step=None):
|
||||
fields = {'id': id, 'params': params}
|
||||
if step:
|
||||
fields['step'] = step
|
||||
return self.emit('error', **fields)
|
||||
|
||||
def close(self):
|
||||
if self.log_file:
|
||||
self.log_file.close()
|
||||
self.log_file = None
|
||||
679
installer/gui.py
Normal file
679
installer/gui.py
Normal file
@@ -0,0 +1,679 @@
|
||||
"""The installer window: python -m installer.gui
|
||||
|
||||
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the optional firmware
|
||||
folder, the game language and the install folder, runs `python -m installer ...` as a subprocess and shows its JSON
|
||||
events. Decisions live in guilogic.py; this file only draws. The window text is in
|
||||
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids.
|
||||
|
||||
--selftest [--shots DIR] build every screen in every language and close by itself (PNG per screen with --shots)
|
||||
--lang en|ru|pt-BR|zh-Hans window language (default: the Windows display language)
|
||||
--rebuild DIR rebuild the install folder DIR for its mods (nx2navi.exe starts this, build-cache/rebuild.txt):
|
||||
the run starts at once with that folder, and the game starts again when it succeeds
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
try:
|
||||
from PIL import ImageGrab # only the screenshots of --selftest --shots need it; the release has no PIL
|
||||
except ImportError:
|
||||
ImageGrab = None
|
||||
|
||||
from . import ini
|
||||
from . import layout as layouts
|
||||
from .guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, UI_LANGUAGE_LABELS, UI_LANGUAGES, Catalog, Form, Model,
|
||||
Process, amount, default_game_language, describe_event, parse_line, windows_ui_language)
|
||||
|
||||
POLL_MS = 100
|
||||
GLYPH = {PENDING: '\u25cb', RUNNING: '\u25b6', DONE: '\u2714', SKIPPED: '\u2013', FAILED: '\u2716'}
|
||||
COLOR = {PENDING: '#808080', RUNNING: '#1a5fb4', DONE: '#26a269', SKIPPED: '#808080', FAILED: '#c01c28'}
|
||||
PAD = 14
|
||||
|
||||
|
||||
def fix_tcl_paths():
|
||||
"""The embeddable Python keeps Tcl/Tk next to python.exe (python/tcl); point Tcl at it when it is not found."""
|
||||
tcl = Path(sys.executable).resolve().parent / 'tcl'
|
||||
if tcl.is_dir() and 'TCL_LIBRARY' not in os.environ:
|
||||
for lib, var in (('tcl9.0', 'TCL_LIBRARY'), ('tk9.0', 'TK_LIBRARY')):
|
||||
if (tcl / lib).is_dir():
|
||||
os.environ[var] = str(tcl / lib)
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self, root, ui_lang, layout, run_args=None, rebuild=None):
|
||||
self.root, self.layout = root, layout
|
||||
self.rebuild = rebuild # the install folder of --rebuild, or None
|
||||
self.cat = Catalog(ui_lang)
|
||||
self.model = Model()
|
||||
self.process = None
|
||||
self.closing = False
|
||||
self.run_args = run_args or {} # extra CLI options for tests: temp, verify
|
||||
self.log_entries = [] # events and raw lines of the run, rendered by render_log
|
||||
self.bound = [] # (widget, option, id, params) retranslated on a language switch
|
||||
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'firmware', 'dest')}
|
||||
self.vars['lang'] = tk.StringVar()
|
||||
self.lang_code = default_game_language(ui_lang)
|
||||
self.screen = None
|
||||
self.fitted = None # the (width, height) fit last gave the window
|
||||
|
||||
root.title(self.cat('ui.title'))
|
||||
icon = layout.runtime / 'nx2navi.ico'
|
||||
png = layout.runtime / 'nx2navi.png' # Tk loads no .ico off Windows; the Linux release converts it
|
||||
if os.name == 'nt' and icon.is_file():
|
||||
root.iconbitmap(default=str(icon))
|
||||
elif png.is_file():
|
||||
self.icon = tk.PhotoImage(file=str(png))
|
||||
root.iconphoto(True, self.icon)
|
||||
root.protocol('WM_DELETE_WINDOW', self.on_close)
|
||||
self.ui_font = 'Segoe UI' if os.name == 'nt' else 'DejaVu Sans'
|
||||
self.mono_font = 'Consolas' if os.name == 'nt' else 'DejaVu Sans Mono'
|
||||
style = ttk.Style(root)
|
||||
style.configure('Title.TLabel', font=(self.ui_font, 15, 'bold'))
|
||||
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=8)
|
||||
style.configure('Err.TLabel', foreground='#c01c28')
|
||||
style.configure('Version.TLabel', foreground='#808080')
|
||||
|
||||
top = ttk.Frame(root, padding=(PAD, PAD, PAD, 0))
|
||||
top.pack(fill='x')
|
||||
self.title = self.label(top, 'ui.title', style='Title.TLabel')
|
||||
self.title.pack(side='left')
|
||||
version = ttk.Label(top, style='Version.TLabel')
|
||||
self.bind_text(version, 'text', 'ui.version', version=layout.release)
|
||||
version.pack(side='left', padx=(10, 0), pady=(6, 0))
|
||||
sw = ttk.Frame(top)
|
||||
sw.pack(side='right')
|
||||
self.ui_var = tk.StringVar(value=self.cat.lang) # the window language code; the box shows its name
|
||||
labels = [UI_LANGUAGE_LABELS[c] for c in UI_LANGUAGES]
|
||||
self.ui_box = ttk.Combobox(sw, values=labels, state='readonly', width=max(map(len, labels)) + 1)
|
||||
self.ui_box.set(UI_LANGUAGE_LABELS[self.cat.lang])
|
||||
self.ui_box.bind('<<ComboboxSelected>>', self.pick_language)
|
||||
self.ui_box.pack(side='left')
|
||||
self.body = ttk.Frame(root, padding=PAD)
|
||||
self.body.pack(fill='both', expand=True)
|
||||
self.frames = {'form': self.build_form(), 'run': self.build_run(), 'finish': self.build_finish()}
|
||||
self.vars['lang'].set(self.lang_label(self.lang_code))
|
||||
self.vars['dest'].trace_add('write', lambda *_: self.refresh_form())
|
||||
self.show('form')
|
||||
if rebuild:
|
||||
self.bind_text(self.title, 'text', 'ui.rebuild_title')
|
||||
self.vars['dest'].set(str(rebuild))
|
||||
root.after(POLL_MS, self.start)
|
||||
|
||||
# ---- text -----------------------------------------------------------------------------------------------------
|
||||
|
||||
def bind_text(self, widget, option, id, **params):
|
||||
self.bound.append((widget, option, id, params))
|
||||
widget.configure(**{option: self.cat(id, **params)})
|
||||
|
||||
def label(self, parent, id, **kw):
|
||||
w = ttk.Label(parent, **kw)
|
||||
self.bind_text(w, 'text', id)
|
||||
return w
|
||||
|
||||
def button(self, parent, id, command, **kw):
|
||||
w = ttk.Button(parent, command=command, **kw)
|
||||
self.bind_text(w, 'text', id)
|
||||
return w
|
||||
|
||||
def retext(self):
|
||||
self.root.title(self.cat('ui.title'))
|
||||
for widget, option, id, params in self.bound:
|
||||
widget.configure(**{option: self.cat(id, **params)})
|
||||
self.fill_languages()
|
||||
self.refresh_form()
|
||||
self.refresh_run()
|
||||
self.refresh_finish()
|
||||
self.render_log()
|
||||
|
||||
def pick_language(self, _event=None):
|
||||
self.ui_var.set(UI_LANGUAGES[self.ui_box.current()])
|
||||
self.switch_language()
|
||||
|
||||
def switch_language(self):
|
||||
self.cat.set_language(self.ui_var.get())
|
||||
self.ui_box.set(UI_LANGUAGE_LABELS[self.ui_var.get()])
|
||||
code = self.selected_lang()
|
||||
self.retext()
|
||||
self.vars['lang'].set(self.lang_label(code))
|
||||
self.fit()
|
||||
|
||||
def lang_label(self, code):
|
||||
return self.cat('lang.' + code)
|
||||
|
||||
def fill_languages(self):
|
||||
code = self.selected_lang()
|
||||
self.lang_box.configure(values=[self.lang_label(c) for c in ini.LANGUAGES])
|
||||
self.vars['lang'].set(self.lang_label(code))
|
||||
|
||||
def selected_lang(self):
|
||||
"""The game language code behind the combobox text (kept when it is not one of the labels)."""
|
||||
shown = self.vars['lang'].get()
|
||||
for code in ini.LANGUAGES:
|
||||
for lang in UI_LANGUAGES:
|
||||
if shown == Catalog(lang)('lang.' + code):
|
||||
return code
|
||||
return self.lang_code
|
||||
|
||||
# ---- screens --------------------------------------------------------------------------------------------------
|
||||
|
||||
def show(self, name):
|
||||
for f in self.frames.values():
|
||||
f.pack_forget()
|
||||
self.frames[name].pack(fill='both', expand=True)
|
||||
self.screen = name
|
||||
self.fit()
|
||||
|
||||
def fit(self):
|
||||
"""Size the window to its largest screen (an unpacked frame still reports the size its children ask for), so
|
||||
a screen or language switch neither cuts text nor moves the buttons. The window only grows: opening the log
|
||||
makes it taller, and a size the user dragged it to stays. Every text change that can add lines calls it again,
|
||||
or pack takes the missing height from the button bars."""
|
||||
self.root.update_idletasks()
|
||||
sizes = [(f.winfo_reqwidth(), f.winfo_reqheight()) for f in self.frames.values()]
|
||||
chrome = self.root.winfo_reqheight() - self.body.winfo_reqheight() # the title row above the body
|
||||
w = max(self.root.winfo_reqwidth(), max(s[0] for s in sizes) + 2 * PAD)
|
||||
h = chrome + max(s[1] for s in sizes) + 2 * PAD
|
||||
if (w, h) == self.fitted: # refresh_run calls it on every poll: leave the window alone while nothing grew
|
||||
return
|
||||
self.fitted = (w, h)
|
||||
self.root.minsize(w, h)
|
||||
self.root.geometry(f'{max(w, self.root.winfo_width())}x{max(h, self.root.winfo_height())}')
|
||||
|
||||
def notice(self, parent):
|
||||
w = ttk.Label(parent, style='Notice.TLabel', wraplength=620, justify='left')
|
||||
self.bind_text(w, 'text', 'ui.notice')
|
||||
return w
|
||||
|
||||
def build_form(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f) # packed first so pack shrinks the text above it, never the buttons
|
||||
bar.pack(side='bottom', fill='x', pady=(12, 0))
|
||||
self.start_button = self.button(bar, 'ui.start', self.start)
|
||||
self.start_button.pack(side='right')
|
||||
self.form_intro = self.label(f, 'ui.intro', wraplength=620, justify='left')
|
||||
self.form_intro.pack(anchor='w', pady=(0, 8))
|
||||
self.notice(f).pack(fill='x', pady=(0, 10))
|
||||
grid = ttk.Frame(f)
|
||||
grid.pack(fill='x')
|
||||
grid.columnconfigure(1, weight=1)
|
||||
rows = (('base', 'ui.base', 'ui.pick_game'), ('update', 'ui.update', 'ui.pick_update'),
|
||||
('keys', 'ui.keys', 'ui.pick_keys'))
|
||||
self.form_rows = {}
|
||||
for i, (name, label_id, pick_id) in enumerate(rows):
|
||||
lab = self.label(grid, label_id)
|
||||
lab.grid(row=i, column=0, sticky='w', pady=4, padx=(0, 10))
|
||||
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
b = self.button(grid, 'ui.browse', lambda n=name, p=pick_id: self.pick_file(n, p))
|
||||
b.grid(row=i, column=2, padx=(8, 0))
|
||||
self.form_rows[name] = lab
|
||||
i = len(rows)
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(8, 0))
|
||||
i += 1
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=4)
|
||||
self.lang_box = ttk.Combobox(grid, textvariable=self.vars['lang'], state='readonly', width=30)
|
||||
self.lang_box.grid(row=i, column=1, sticky='w', pady=4)
|
||||
self.lang_touched = False
|
||||
self.lang_box.bind('<<ComboboxSelected>>', lambda _e: setattr(self, 'lang_touched', True))
|
||||
self.fill_languages()
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(8, 0))
|
||||
self.label(f, 'ui.compressed_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
# On by default and named with its source and licence: the player sees what is downloaded and can untick it
|
||||
self.dlss_var = tk.BooleanVar(value=True)
|
||||
dlss = ttk.Checkbutton(f, variable=self.dlss_var)
|
||||
self.bind_text(dlss, 'text', 'ui.dlss')
|
||||
dlss.pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.dlss_help', wraplength=620, justify='left').pack(anchor='w', padx=(22, 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(8, 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=620, justify='left')
|
||||
self.form_error.pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.keys_private', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
return f
|
||||
|
||||
def build_run(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f)
|
||||
bar.pack(side='bottom', fill='x', pady=(8, 0))
|
||||
self.cancel_button = self.button(bar, 'ui.cancel', self.cancel)
|
||||
self.cancel_button.pack(side='right')
|
||||
self.log_toggle = self.button(bar, 'ui.show_log', self.toggle_log)
|
||||
self.log_toggle.pack(side='left')
|
||||
self.run_header = ttk.Label(f, font=(self.ui_font, 11, 'bold'))
|
||||
self.run_header.pack(anchor='w')
|
||||
self.step_rows = {}
|
||||
self.steps_box = ttk.Frame(f)
|
||||
self.steps_box.pack(fill='x', pady=(8, 8))
|
||||
self.phase_label = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.phase_label.pack(anchor='w')
|
||||
self.bar = ttk.Progressbar(f, mode='determinate', maximum=1000)
|
||||
self.bar.pack(fill='x', pady=(4, 6))
|
||||
self.space_label = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.space_label.pack(anchor='w')
|
||||
self.warn_label = ttk.Label(f, wraplength=620, justify='left', foreground='#9a6700')
|
||||
self.warn_label.pack(anchor='w')
|
||||
self.log_frame = ttk.Frame(f)
|
||||
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=(self.mono_font, 9))
|
||||
scroll = ttk.Scrollbar(self.log_frame, command=self.log.yview)
|
||||
self.log.configure(yscrollcommand=scroll.set)
|
||||
scroll.pack(side='right', fill='y')
|
||||
self.log.pack(side='left', fill='both', expand=True)
|
||||
self.log_open = False
|
||||
return f
|
||||
|
||||
def build_finish(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f)
|
||||
bar.pack(side='bottom', fill='x', pady=(12, 0))
|
||||
self.button(bar, 'ui.close', self.on_close).pack(side='right')
|
||||
self.open_button = self.button(bar, 'ui.open_folder', self.open_folder)
|
||||
self.open_button.pack(side='right', padx=(0, 8))
|
||||
self.back_button = self.button(bar, 'ui.back', lambda: self.show('form'))
|
||||
self.back_button.pack(side='left')
|
||||
if self.rebuild: # a failed or cancelled rebuild: the game starts with the code it has
|
||||
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, 8))
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=620, justify='left')
|
||||
self.finish_title.pack(anchor='w')
|
||||
self.finish_text = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.finish_text.pack(anchor='w', pady=(8, 8))
|
||||
self.finish_steps = ttk.Frame(f)
|
||||
self.finish_steps.pack(fill='x', pady=(0, 8))
|
||||
self.finish_notice = self.notice(f)
|
||||
self.finish_notice.pack(fill='x', pady=(8, 0))
|
||||
return f
|
||||
|
||||
# ---- form -----------------------------------------------------------------------------------------------------
|
||||
|
||||
def form(self):
|
||||
return Form(base=self.vars['base'].get(), update=self.vars['update'].get(), keys=self.vars['keys'].get(),
|
||||
firmware=self.vars['firmware'].get(), lang=self.selected_lang(), dest=self.vars['dest'].get(),
|
||||
dlss=self.dlss_var.get())
|
||||
|
||||
def refresh_form(self):
|
||||
form = self.form()
|
||||
state = form.installed_state()
|
||||
if state is not None:
|
||||
self.update_hint.configure(text=self.cat('ui.update_mode'))
|
||||
recorded = ini.canonical_language(state.get('lang'))
|
||||
if recorded in ini.LANGUAGES and not self.lang_touched:
|
||||
self.vars['lang'].set(self.lang_label(recorded))
|
||||
else:
|
||||
self.update_hint.configure(text='')
|
||||
self.fit()
|
||||
|
||||
def pick_file(self, name, pick_id):
|
||||
# One filter for every game file: the setup tells an NSZ or XCZ from a plain file by its content (Check.unpack)
|
||||
types = [(self.cat('ui.type_game'), '*.nsp *.xci *.nsz *.xcz'), (self.cat('ui.type_all'), '*.*')]
|
||||
if name == 'keys':
|
||||
types = [(self.cat('ui.type_keys'), '*.keys'), (self.cat('ui.type_all'), '*.*')]
|
||||
path = filedialog.askopenfilename(title=self.cat(pick_id), filetypes=types)
|
||||
if path:
|
||||
self.vars[name].set(os.path.normpath(path))
|
||||
|
||||
def pick_firmware(self):
|
||||
path = filedialog.askdirectory(title=self.cat('ui.pick_firmware'), mustexist=True)
|
||||
if path:
|
||||
self.vars['firmware'].set(os.path.normpath(path))
|
||||
|
||||
def pick_dest(self):
|
||||
path = filedialog.askdirectory(title=self.cat('ui.pick_dest'), mustexist=False)
|
||||
if path:
|
||||
self.vars['dest'].set(os.path.normpath(path))
|
||||
|
||||
# ---- run ------------------------------------------------------------------------------------------------------
|
||||
|
||||
def start(self):
|
||||
form = self.form()
|
||||
problems = form.problems()
|
||||
if problems:
|
||||
self.form_error.configure(text=' '.join(self.cat(p) for p in problems))
|
||||
self.fit()
|
||||
return
|
||||
self.form_error.configure(text='')
|
||||
# in a repository the Python running this window: the tools' embeddable Python there has no ._pth entry for
|
||||
# the repository, so `-m installer` would not find the package
|
||||
python = Path(sys.executable) if self.layout.dev else self.layout.python
|
||||
command = form.command(python, root=self.layout.root if self.layout.dev else None,
|
||||
temp=self.run_args.get('temp'), verify_hashes=self.run_args.get('verify', False))
|
||||
self.begin(command, self.layout.root)
|
||||
|
||||
def begin(self, command, cwd):
|
||||
self.model = Model()
|
||||
self.model.dest = self.vars['dest'].get().strip()
|
||||
self.clear_log()
|
||||
self.build_step_rows(self.model.steps)
|
||||
self.cancel_button.configure(state='normal')
|
||||
self.bound = [b for b in self.bound if b[0] is not self.cancel_button]
|
||||
self.bind_text(self.cancel_button, 'text', 'ui.cancel')
|
||||
self.show('run')
|
||||
self.refresh_run()
|
||||
self.process = Process(command, cwd=cwd, helper_python=self.layout.python)
|
||||
self.root.after(POLL_MS, self.poll)
|
||||
|
||||
def build_step_rows(self, steps):
|
||||
for w in self.steps_box.winfo_children():
|
||||
w.destroy()
|
||||
self.step_rows = {}
|
||||
for s in steps:
|
||||
row = ttk.Frame(self.steps_box)
|
||||
row.pack(fill='x', pady=1)
|
||||
glyph = ttk.Label(row, width=3)
|
||||
glyph.pack(side='left')
|
||||
name = ttk.Label(row)
|
||||
name.pack(side='left')
|
||||
self.step_rows[s] = (glyph, name)
|
||||
|
||||
def poll(self):
|
||||
if self.process is None:
|
||||
return
|
||||
try:
|
||||
while True:
|
||||
line = self.process.lines.get_nowait()
|
||||
if line is not None:
|
||||
self.feed(line)
|
||||
except Exception:
|
||||
pass
|
||||
code = self.process.poll()
|
||||
self.refresh_run()
|
||||
if code is None:
|
||||
self.root.after(POLL_MS, self.poll)
|
||||
return
|
||||
self.model.finish(code)
|
||||
self.process = None
|
||||
if self.rebuild and self.model.result == 'ok' and not self.closing:
|
||||
self.start_game()
|
||||
return
|
||||
self.refresh_run()
|
||||
self.refresh_finish()
|
||||
self.show('finish')
|
||||
if self.closing:
|
||||
self.root.destroy()
|
||||
|
||||
def feed(self, line):
|
||||
"""One stdout line of the CLI: an event updates the model and the log, anything else goes to the log as is."""
|
||||
event = parse_line(line)
|
||||
if event is None:
|
||||
if line.strip():
|
||||
self.log_entries.append(line.rstrip())
|
||||
self.append_log(line.rstrip())
|
||||
return
|
||||
before = list(self.model.steps)
|
||||
self.model.apply(event)
|
||||
if self.model.steps != before:
|
||||
self.build_step_rows(self.model.steps)
|
||||
self.log_entries.append(event)
|
||||
self.append_log(describe_event(self.cat, event))
|
||||
|
||||
def refresh_run(self):
|
||||
m = self.model
|
||||
for s, (glyph, name) in self.step_rows.items():
|
||||
state = m.states.get(s, PENDING)
|
||||
glyph.configure(text=GLYPH[state], foreground=COLOR[state])
|
||||
name.configure(text=self.cat('step.' + s) + (f' ({self.cat("ui.state_" + state)})'
|
||||
if state in (SKIPPED, FAILED) else ''),
|
||||
font=(self.ui_font, 10, 'bold') if state == RUNNING else (self.ui_font, 10))
|
||||
mode = m.mode or 'install'
|
||||
self.run_header.configure(text=self.cat('ui.running_' + mode if mode in ('install', 'resume', 'update')
|
||||
else 'ui.running_install'))
|
||||
if m.current and m.phase:
|
||||
text = self.cat(f'phase.{m.current}.{m.phase}')
|
||||
if m.total:
|
||||
text += ' ' + amount(self.cat, m.done, m.total, m.unit)
|
||||
elif m.current:
|
||||
text = self.cat('step.' + m.current)
|
||||
else:
|
||||
text = ''
|
||||
self.phase_label.configure(text=text)
|
||||
frac = m.fraction()
|
||||
if frac is None:
|
||||
if str(self.bar.cget('mode')) != 'indeterminate':
|
||||
self.bar.configure(mode='indeterminate')
|
||||
self.bar.start(15)
|
||||
else:
|
||||
if str(self.bar.cget('mode')) == 'indeterminate':
|
||||
self.bar.stop()
|
||||
self.bar.configure(mode='determinate')
|
||||
self.bar.configure(value=frac * 1000)
|
||||
self.space_label.configure(text=m.need_text(self.cat))
|
||||
self.warn_label.configure(text='\n'.join(self.cat(i, **p) for i, p in m.warnings))
|
||||
self.fit() # new step rows and warnings make the screen taller
|
||||
|
||||
def toggle_log(self):
|
||||
self.log_open = not self.log_open
|
||||
if self.log_open:
|
||||
self.log_frame.pack(fill='both', expand=True, pady=(8, 0))
|
||||
else:
|
||||
self.log_frame.pack_forget()
|
||||
self.bound = [b for b in self.bound if b[0] is not self.log_toggle]
|
||||
self.bind_text(self.log_toggle, 'text', 'ui.hide_log' if self.log_open else 'ui.show_log')
|
||||
self.fit()
|
||||
|
||||
def clear_log(self):
|
||||
self.log_entries = []
|
||||
self.render_log()
|
||||
|
||||
def render_log(self):
|
||||
"""The whole log from the kept events in the current language (raw CLI lines stay as they are)."""
|
||||
self.log.configure(state='normal')
|
||||
self.log.delete('1.0', 'end')
|
||||
for entry in self.log_entries:
|
||||
text = entry if isinstance(entry, str) else describe_event(self.cat, entry)
|
||||
if text:
|
||||
self.log.insert('end', text + '\n')
|
||||
self.log.see('end')
|
||||
self.log.configure(state='disabled')
|
||||
|
||||
def append_log(self, text):
|
||||
if text:
|
||||
self.log.configure(state='normal')
|
||||
self.log.insert('end', text + '\n')
|
||||
self.log.see('end')
|
||||
self.log.configure(state='disabled')
|
||||
|
||||
def cancel(self):
|
||||
if self.process:
|
||||
self.cancel_button.configure(state='disabled')
|
||||
self.bound = [b for b in self.bound if b[0] is not self.cancel_button]
|
||||
self.bind_text(self.cancel_button, 'text', 'ui.cancelling')
|
||||
self.process.cancel()
|
||||
|
||||
def on_close(self):
|
||||
if self.process:
|
||||
if not messagebox.askyesno(self.cat('ui.title'), self.cat('ui.confirm_cancel')):
|
||||
return
|
||||
self.closing = True
|
||||
self.cancel()
|
||||
return
|
||||
self.root.destroy()
|
||||
|
||||
# ---- finish ---------------------------------------------------------------------------------------------------
|
||||
|
||||
def refresh_finish(self):
|
||||
m = self.model
|
||||
result = m.result or 'ok'
|
||||
self.finish_title.configure(text=self.cat('ui.result_' + result))
|
||||
if result == 'ok':
|
||||
text = self.cat('ui.done_text', dest=m.dest)
|
||||
elif result == 'cancelled':
|
||||
text = self.cat('ui.cancelled_text')
|
||||
else:
|
||||
err = m.error or ('ui.process_died', {'code': m.exit_code}, None)
|
||||
text = self.cat(err[0], **err[1])
|
||||
if m.log_path:
|
||||
text += '\n\n' + self.cat('ui.log_file', path=m.log_path)
|
||||
self.finish_text.configure(text=text)
|
||||
for w in self.finish_steps.winfo_children():
|
||||
w.destroy()
|
||||
for s in m.steps:
|
||||
state = m.states.get(s, PENDING)
|
||||
ttk.Label(self.finish_steps, text=f'{GLYPH[state]} {self.cat("step." + s)}',
|
||||
foreground=COLOR[state]).pack(anchor='w')
|
||||
if result == 'ok':
|
||||
self.open_button.state(['!disabled'])
|
||||
else:
|
||||
self.open_button.state(['disabled' if not Path(m.dest or '.').is_dir() else '!disabled'])
|
||||
|
||||
def start_game(self):
|
||||
"""Starts the game of the install folder and closes the window."""
|
||||
dest = Path(self.vars['dest'].get().strip())
|
||||
exe = dest / ('nx2navi.exe' if os.name == 'nt' else 'nx2navi')
|
||||
flags = subprocess.DETACHED_PROCESS if os.name == 'nt' else 0
|
||||
if exe.is_file():
|
||||
subprocess.Popen([str(exe)], cwd=str(dest), creationflags=flags, close_fds=True)
|
||||
self.root.destroy()
|
||||
|
||||
def open_folder(self):
|
||||
dest = self.model.dest
|
||||
if dest and Path(dest).is_dir():
|
||||
if os.name == 'nt':
|
||||
os.startfile(dest)
|
||||
else:
|
||||
subprocess.Popen(['xdg-open', dest], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
|
||||
|
||||
# ---- self test ---------------------------------------------------------------------------------------------------
|
||||
|
||||
SAMPLE_START = {'type': 'start', 'release': 'selftest', 'mode': 'install', 'dest': 'Z:/selftest/NX2navi', 'log': 'D:\\log.txt',
|
||||
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup']}
|
||||
SAMPLE_RUN = [
|
||||
SAMPLE_START,
|
||||
{'type': 'plan', 'run': ['check', 'fonts', 'extract'], 'skip': {}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'started'},
|
||||
{'type': 'progress', 'step': 'check', 'phase': 'inputs'},
|
||||
{'type': 'space', 'dest': 'Z:/selftest/NX2navi', 'need': 36_400_000_000, 'free': 210_000_000_000, 'temp': 'C:\\t',
|
||||
'temp_need': 0, 'temp_free': 0, 'same_disk': True},
|
||||
{'type': 'warning', 'id': 'warn.lang_needs_fonts', 'params': {'lang': 'ko'}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'done', 'seconds': 12.0},
|
||||
{'type': 'step', 'step': 'extract', 'state': 'started'},
|
||||
{'type': 'progress', 'step': 'extract', 'phase': 'copy', 'done': 6_000_000_000, 'total': 16_000_000_000,
|
||||
'unit': 'bytes'},
|
||||
]
|
||||
|
||||
|
||||
SAMPLE_OK = [SAMPLE_START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'fonts': 'current', 'lift': 'current',
|
||||
'compile': 'current', 'prepare': 'current',
|
||||
'cleanup': 'not_needed'}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'started'},
|
||||
{'type': 'step', 'step': 'check', 'state': 'done', 'seconds': 1.0},
|
||||
{'type': 'step', 'step': 'extract', 'state': 'started'},
|
||||
{'type': 'step', 'step': 'extract', 'state': 'done', 'seconds': 1.0},
|
||||
{'type': 'step', 'step': 'fonts', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'lift', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'compile', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'prepare', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'cleanup', 'state': 'done', 'seconds': 0.1},
|
||||
{'type': 'end', 'result': 'ok', 'seconds': 3}]
|
||||
|
||||
|
||||
def grab(root, path):
|
||||
if ImageGrab is None:
|
||||
raise SystemExit('--shots needs Pillow')
|
||||
root.attributes('-topmost', True)
|
||||
root.update()
|
||||
x, y, w, h = root.winfo_rootx(), root.winfo_rooty(), root.winfo_width(), root.winfo_height()
|
||||
ImageGrab.grab(bbox=(x, y, x + w, y + h), all_screens=True).save(path)
|
||||
|
||||
|
||||
def selftest(layout, shots=None):
|
||||
"""Every screen in every language; the window closes itself. Returns 0 when nothing raised."""
|
||||
fix_tcl_paths()
|
||||
root = tk.Tk()
|
||||
root.geometry('+60+40')
|
||||
app = App(root, 'en', layout)
|
||||
steps, mixed = [], []
|
||||
|
||||
def shot(name):
|
||||
root.update()
|
||||
if shots:
|
||||
Path(shots).mkdir(parents=True, exist_ok=True)
|
||||
grab(root, Path(shots) / f'{name}.png')
|
||||
|
||||
def run_all():
|
||||
for lang in UI_LANGUAGES:
|
||||
app.ui_var.set(lang)
|
||||
app.switch_language()
|
||||
tag = lang
|
||||
app.vars['dest'].set('Z:/selftest/NX2navi')
|
||||
app.vars['base'].set('Z:/selftest/nx2navi.xci')
|
||||
app.vars['update'].set('Z:/selftest/nx2navi-1.2.1.nsp')
|
||||
app.vars['keys'].set('Z:/selftest/prod.keys')
|
||||
app.show('form')
|
||||
app.start() # missing files: shows the problems
|
||||
steps.append(app.form_error.cget('text'))
|
||||
shot(f'form-{tag}')
|
||||
app.model = Model()
|
||||
app.clear_log()
|
||||
app.build_step_rows(app.model.steps)
|
||||
app.show('run')
|
||||
for e in SAMPLE_RUN:
|
||||
app.feed(json.dumps(e))
|
||||
app.log_open = False
|
||||
app.toggle_log()
|
||||
app.refresh_run()
|
||||
shot(f'run-{tag}')
|
||||
if lang == 'en': # the log follows the window language: no English line may remain after the switch
|
||||
app.ui_var.set('ru')
|
||||
app.switch_language()
|
||||
text = app.log.get('1.0', 'end')
|
||||
mixed.append('Checking' in text or 'Extracting' not in text and 'Распаковка' not in text)
|
||||
app.ui_var.set('en')
|
||||
app.switch_language()
|
||||
app.model = Model()
|
||||
for e in SAMPLE_OK:
|
||||
app.model.apply(e)
|
||||
app.model.dest = 'Z:/selftest/NX2navi'
|
||||
app.model.finish(0)
|
||||
app.refresh_finish()
|
||||
app.show('finish')
|
||||
shot(f'finish-ok-{tag}')
|
||||
app.model = Model()
|
||||
for e in SAMPLE_RUN[:3] + [{'type': 'error', 'id': 'check.disk_space', 'step': 'check',
|
||||
'params': {'path': 'Z:/selftest/NX2navi', 'need_gb': 36.4, 'free_gb': 20.1}},
|
||||
{'type': 'end', 'result': 'failed', 'seconds': 3}]:
|
||||
app.model.apply(e)
|
||||
app.model.finish(1)
|
||||
app.refresh_finish()
|
||||
shot(f'finish-failed-{tag}')
|
||||
app.model = Model()
|
||||
app.model.finish(3)
|
||||
app.refresh_finish()
|
||||
shot(f'finish-cancelled-{tag}')
|
||||
app.toggle_log()
|
||||
root.destroy()
|
||||
|
||||
root.after(200, run_all)
|
||||
root.after(60000, root.destroy) # never leaves a window open
|
||||
root.mainloop()
|
||||
return 0 if len(steps) == len(UI_LANGUAGES) and all(steps) and mixed == [False] else 1
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--selftest', action='store_true')
|
||||
ap.add_argument('--shots', type=Path)
|
||||
ap.add_argument('--lang', choices=UI_LANGUAGES)
|
||||
ap.add_argument('--root', type=Path)
|
||||
ap.add_argument('--rebuild', type=Path)
|
||||
args = ap.parse_args(argv)
|
||||
layout = layouts.find(args.root)
|
||||
if args.selftest:
|
||||
return selftest(layout, args.shots)
|
||||
fix_tcl_paths()
|
||||
root = tk.Tk()
|
||||
App(root, args.lang or windows_ui_language(), layout, rebuild=args.rebuild)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
405
installer/guilogic.py
Normal file
405
installer/guilogic.py
Normal file
@@ -0,0 +1,405 @@
|
||||
"""The installer window without Tk: text catalogs, the form, the event model and the CLI subprocess.
|
||||
|
||||
gui.py draws; everything it decides lives here so tests/installer_gui_test.py runs it headless.
|
||||
|
||||
Catalog text of an id in the window language (installer/text/<lang>.json), English when that lacks it
|
||||
Form what the player entered; command() and problems() build and check the CLI call
|
||||
Model folds the CLI's JSON events (events.py) into step states, progress, space, errors and the result
|
||||
Process runs `python -m installer ...`, cancels it with Ctrl+Break and feeds lines to a queue
|
||||
"""
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from . import ini
|
||||
from . import state as statefile
|
||||
from .events import render
|
||||
|
||||
TEXT = Path(__file__).resolve().parent / 'text'
|
||||
UI_LANGUAGES = ('en', 'ru', 'pt-BR', 'zh-Hans')
|
||||
# what the language switch in the window's corner shows for each
|
||||
UI_LANGUAGE_LABELS = {'en': 'English', 'ru': 'Русский', 'pt-BR': 'Português (Brasil)', 'zh-Hans': '简体中文'}
|
||||
STEP_ORDER = ('check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup')
|
||||
# message parameter -> catalog id prefix of the names that replace its raw values
|
||||
PARAM_NAMES = {'lang': 'lang.', 'known': 'lang.', 'step': 'step.', 'input': 'param.input.'}
|
||||
LANG_RU = 0x19 # primary language id of Russian in a Windows LANGID
|
||||
LANG_PT_BR = 0x0416 # Portuguese (Brazil), the whole LANGID: Portugal's Portuguese stays English
|
||||
# Windows LANGIDs that write Simplified Chinese: zh-CN, zh-SG. zh-TW, zh-HK and zh-MO write Traditional Chinese, which
|
||||
# the window does not have.
|
||||
LANGIDS_ZH_HANS = (0x0804, 0x1004)
|
||||
|
||||
|
||||
def load_texts(lang):
|
||||
path = TEXT / f'{lang}.json'
|
||||
return json.loads(path.read_text(encoding='utf-8')) if path.is_file() else {}
|
||||
|
||||
|
||||
class Catalog:
|
||||
"""Window text: ids from text/<lang>.json, then text/en.json, then the id itself."""
|
||||
|
||||
def __init__(self, lang='en'):
|
||||
self.english = load_texts('en')
|
||||
self.set_language(lang)
|
||||
|
||||
def set_language(self, lang):
|
||||
self.lang = lang if lang in UI_LANGUAGES else 'en'
|
||||
self.texts = load_texts(self.lang)
|
||||
|
||||
def has(self, id):
|
||||
return id in self.texts or id in self.english
|
||||
|
||||
def __call__(self, id, **params):
|
||||
text = self.texts.get(id) or self.english.get(id)
|
||||
if text is None:
|
||||
return id
|
||||
return render({id: text}, id, {k: self.param(k, v) for k, v in params.items()})
|
||||
|
||||
def param(self, name, value):
|
||||
"""A message parameter as the player should read it: language codes, step ids and input names become the
|
||||
catalog's names (the ones the pickers and the step list show); the rest stays as it is."""
|
||||
prefix = PARAM_NAMES.get(name)
|
||||
if prefix:
|
||||
if isinstance(value, (list, tuple)):
|
||||
return ', '.join(self.param(name, v) for v in value)
|
||||
ident = prefix + str(value)
|
||||
if self.has(ident):
|
||||
return self(ident)
|
||||
if name == 'known':
|
||||
return ', '.join(self(prefix + c) if self.has(prefix + c) else c for c in str(value).split(', '))
|
||||
return flat(value)
|
||||
|
||||
|
||||
def flat(value):
|
||||
return ', '.join(str(v) for v in value) if isinstance(value, (list, tuple)) else value
|
||||
|
||||
|
||||
def windows_ui_language():
|
||||
"""The window language for the display language: the Windows one (ui_language_for_langid); elsewhere LC_ALL,
|
||||
LC_MESSAGES or LANG ('ru', 'pt_BR', 'zh_CN' or 'zh_SG', else English)."""
|
||||
if os.name != 'nt':
|
||||
loc = (os.environ.get('LC_ALL') or os.environ.get('LC_MESSAGES') or os.environ.get('LANG') or '').lower()
|
||||
return ('ru' if loc.startswith('ru') else 'pt-BR' if loc.startswith(('pt_br', 'pt-br'))
|
||||
else 'zh-Hans' if loc.startswith(('zh_cn', 'zh-cn', 'zh_sg', 'zh-sg', 'zh_hans', 'zh-hans')) else 'en')
|
||||
try:
|
||||
langid = ctypes.windll.kernel32.GetUserDefaultUILanguage()
|
||||
except (AttributeError, OSError):
|
||||
return 'en'
|
||||
return ui_language_for_langid(langid)
|
||||
|
||||
|
||||
def ui_language_for_langid(langid):
|
||||
"""'ru' for a Russian Windows LANGID, 'pt-BR' for Brazilian Portuguese, 'zh-Hans' for a Simplified Chinese one,
|
||||
else 'en'."""
|
||||
if langid & 0x3ff == LANG_RU:
|
||||
return 'ru'
|
||||
if langid == LANG_PT_BR:
|
||||
return 'pt-BR'
|
||||
if langid in LANGIDS_ZH_HANS:
|
||||
return 'zh-Hans'
|
||||
return 'en'
|
||||
|
||||
|
||||
def default_game_language(ui):
|
||||
"""The game language the form starts with. A Chinese window still starts with English: Chinese game text needs
|
||||
the Switch shared fonts (ini.NEED_SHARED_FONTS), which the setup extracts only from a firmware folder, and the game
|
||||
stops when they are missing; the player can still pick Chinese."""
|
||||
return 'ru' if ui == 'ru' else 'en-US'
|
||||
|
||||
|
||||
def gib(n):
|
||||
"""Bytes as decimal gigabytes with one digit, the unit the check.disk_space message uses."""
|
||||
return f'{n / 1e9:.1f}'
|
||||
|
||||
|
||||
def amount(cat, done, total, unit):
|
||||
"""'6.0 / 16.0 GB', '120 / 4000 files'..., the progress of a phase whose size is known."""
|
||||
if unit == 'bytes':
|
||||
return cat('ui.amount_bytes', done=gib(done or 0), total=gib(total))
|
||||
id = f'ui.amount_{unit}'
|
||||
return cat(id if cat.has(id) else 'ui.amount_other', done=done or 0, total=total, unit=unit or '')
|
||||
|
||||
|
||||
# ---- the form ----------------------------------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Form:
|
||||
base: str = ''
|
||||
update: str = ''
|
||||
keys: str = ''
|
||||
firmware: str = ''
|
||||
lang: str = ''
|
||||
dest: str = ''
|
||||
dlss: bool = False
|
||||
|
||||
def installed_state(self):
|
||||
"""The recorded state of the install folder (nx2navi-install.json), or None. Never raises."""
|
||||
if not self.dest.strip():
|
||||
return None
|
||||
try:
|
||||
return statefile.load(Path(self.dest.strip()))
|
||||
except statefile.StateError:
|
||||
return None
|
||||
|
||||
def update_mode(self):
|
||||
return self.installed_state() is not None
|
||||
|
||||
def problems(self):
|
||||
"""Ids of the entries that are missing or wrong, in screen order. A folder with nx2navi-install.json needs only
|
||||
itself; the CLI asks for the game files when it must extract again."""
|
||||
found = []
|
||||
update = self.update_mode()
|
||||
if not self.dest.strip():
|
||||
found.append('ui.need_dest')
|
||||
if not update:
|
||||
for name in ('base', 'update', 'keys'):
|
||||
value = getattr(self, name).strip()
|
||||
if not value:
|
||||
found.append(f'ui.need_{name}')
|
||||
elif not Path(value).is_file():
|
||||
found.append(f'ui.missing_{name}')
|
||||
if self.lang not in ini.LANGUAGES:
|
||||
found.append('ui.need_lang')
|
||||
else:
|
||||
for name in ('base', 'update', 'keys'):
|
||||
value = getattr(self, name).strip()
|
||||
if value and not Path(value).is_file():
|
||||
found.append(f'ui.missing_{name}')
|
||||
if self.firmware.strip() and not Path(self.firmware.strip()).is_dir():
|
||||
found.append('ui.missing_firmware')
|
||||
return found
|
||||
|
||||
def command(self, python, root=None, temp=None, verify_hashes=False):
|
||||
cmd = [str(python), '-m', 'installer']
|
||||
for flag, value in (('--base', self.base), ('--update', self.update), ('--keys', self.keys),
|
||||
('--firmware', self.firmware), ('--lang', self.lang), ('--dest', self.dest)):
|
||||
if value.strip():
|
||||
cmd += [flag, value.strip()]
|
||||
if root:
|
||||
cmd += ['--root', str(root)]
|
||||
if temp:
|
||||
cmd += ['--temp', str(temp)]
|
||||
if verify_hashes:
|
||||
cmd.append('--verify')
|
||||
if self.dlss:
|
||||
cmd.append('--dlss')
|
||||
return cmd
|
||||
|
||||
|
||||
# ---- events -> state ---------------------------------------------------------------------------------------------
|
||||
|
||||
PENDING, RUNNING, DONE, SKIPPED, FAILED = 'pending', 'running', 'done', 'skipped', 'failed'
|
||||
|
||||
|
||||
def parse_line(line):
|
||||
"""The event dict of a stdout line, or None when it is not a JSON event (a traceback line, noise)."""
|
||||
line = line.strip()
|
||||
if not line.startswith('{'):
|
||||
return None
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except ValueError:
|
||||
return None
|
||||
return event if isinstance(event, dict) and isinstance(event.get('type'), str) else None
|
||||
|
||||
|
||||
class Model:
|
||||
"""What the window shows, folded from events. apply() takes one event; nothing else mutates it."""
|
||||
|
||||
def __init__(self):
|
||||
self.steps = list(STEP_ORDER)
|
||||
self.states = {s: PENDING for s in self.steps}
|
||||
self.mode = ''
|
||||
self.release = ''
|
||||
self.log_path = ''
|
||||
self.dest = ''
|
||||
self.current = None # step id running
|
||||
self.phase = None
|
||||
self.done = self.total = None
|
||||
self.unit = None
|
||||
self.space = None # the space event
|
||||
self.warnings = [] # (id, params)
|
||||
self.error = None # (id, params, step)
|
||||
self.result = None # ok | failed | cancelled once the end event arrived
|
||||
self.seconds = None
|
||||
self.exit_code = None
|
||||
|
||||
def apply(self, e):
|
||||
kind = e['type']
|
||||
if kind == 'start':
|
||||
self.__init__() # a new run: nothing of an earlier one stays
|
||||
self.steps = list(e.get('steps') or STEP_ORDER)
|
||||
self.states = {s: PENDING for s in self.steps}
|
||||
self.mode, self.release = e.get('mode', ''), e.get('release', '')
|
||||
self.log_path, self.dest = e.get('log', ''), e.get('dest', '')
|
||||
elif kind == 'plan':
|
||||
for s, reason in (e.get('skip') or {}).items():
|
||||
if s in self.states and self.states[s] == PENDING:
|
||||
self.states[s] = SKIPPED
|
||||
elif kind == 'step':
|
||||
s, state = e.get('step'), e.get('state')
|
||||
if s not in self.states:
|
||||
self.states[s] = PENDING
|
||||
self.steps.append(s)
|
||||
if state == 'started':
|
||||
self.states[s], self.current = RUNNING, s
|
||||
self.phase = self.done = self.total = self.unit = None
|
||||
elif state == 'done':
|
||||
self.states[s] = DONE
|
||||
self.current = None if self.current == s else self.current
|
||||
elif state == 'skipped':
|
||||
self.states[s] = SKIPPED
|
||||
elif kind == 'progress':
|
||||
if e.get('step') in self.states and self.states[e['step']] == PENDING:
|
||||
self.states[e['step']] = RUNNING
|
||||
self.current = e.get('step', self.current)
|
||||
self.phase = e.get('phase')
|
||||
self.done, self.total, self.unit = e.get('done'), e.get('total'), e.get('unit')
|
||||
elif kind == 'space':
|
||||
self.space = e
|
||||
elif kind == 'warning':
|
||||
self.warnings.append((e.get('id', ''), e.get('params') or {}))
|
||||
elif kind == 'error':
|
||||
self.error = (e.get('id', ''), e.get('params') or {}, e.get('step'))
|
||||
if e.get('step') in self.states:
|
||||
self.states[e['step']] = FAILED
|
||||
elif kind == 'end':
|
||||
self.result = e.get('result')
|
||||
self.seconds = e.get('seconds')
|
||||
self.current = None
|
||||
|
||||
def fraction(self):
|
||||
"""0..1 of the current phase, or None when its size is unknown."""
|
||||
if self.total:
|
||||
return max(0.0, min(1.0, (self.done or 0) / self.total))
|
||||
return None
|
||||
|
||||
def finish(self, exit_code):
|
||||
"""The process ended. A missing end event (the process died) counts as failed, or cancelled on exit code 3."""
|
||||
self.exit_code = exit_code
|
||||
if self.result is None:
|
||||
self.result = 'cancelled' if exit_code == 3 else 'failed'
|
||||
if self.result == 'failed' and self.error is None:
|
||||
self.error = ('ui.process_died', {'code': exit_code}, self.current)
|
||||
for s, state in self.states.items():
|
||||
if state == RUNNING:
|
||||
self.states[s] = FAILED if self.result == 'failed' else PENDING
|
||||
|
||||
def need_text(self, cat):
|
||||
"""The space line, or '' before the check step reported."""
|
||||
sp = self.space
|
||||
if not sp:
|
||||
return ''
|
||||
key = 'ui.space_one' if sp.get('same_disk') or not sp.get('temp_need') else 'ui.space_two'
|
||||
return cat(key, path=sp.get('dest', ''), need=gib(sp.get('need', 0)), free=gib(sp.get('free', 0)),
|
||||
temp=sp.get('temp', ''), temp_need=gib(sp.get('temp_need', 0)),
|
||||
temp_free=gib(sp.get('temp_free', 0)))
|
||||
|
||||
|
||||
def describe_event(cat, e):
|
||||
"""One log line for an event, in the window language."""
|
||||
kind = e['type']
|
||||
if kind == 'step':
|
||||
text = f'{cat("step." + e["step"])}: {cat("ui.state_" + e["state"])}'
|
||||
if 'seconds' in e:
|
||||
text += f' ({cat("ui.seconds", n=format(e["seconds"], ".1f"))})'
|
||||
elif e.get('reason'):
|
||||
text += f' ({e["reason"]})'
|
||||
return text
|
||||
if kind == 'progress':
|
||||
text = cat(f'phase.{e["step"]}.{e["phase"]}')
|
||||
if e.get('total'):
|
||||
text += ' ' + amount(cat, e.get('done'), e['total'], e.get('unit'))
|
||||
return text
|
||||
if kind in ('warning', 'error'):
|
||||
return f'{cat("ui.log_" + kind)}: {cat(e["id"], **(e.get("params") or {}))}'
|
||||
if kind == 'space':
|
||||
return cat('ui.log_space', need=gib(e.get('need', 0)), free=gib(e.get('free', 0)))
|
||||
if kind == 'end':
|
||||
return f'{cat("ui.log_end")}: {cat("ui.result_" + str(e.get("result")))}'
|
||||
return ''
|
||||
|
||||
|
||||
# ---- the CLI subprocess ------------------------------------------------------------------------------------------
|
||||
|
||||
CREATE_NEW_PROCESS_GROUP = 0x200
|
||||
CREATE_NO_WINDOW = 0x08000000
|
||||
|
||||
_BREAK = """
|
||||
import ctypes, sys, time
|
||||
k = ctypes.windll.kernel32
|
||||
pid = int(sys.argv[1])
|
||||
k.FreeConsole()
|
||||
if k.AttachConsole(pid):
|
||||
k.SetConsoleCtrlHandler(None, True)
|
||||
k.GenerateConsoleCtrlEvent(1, pid)
|
||||
time.sleep(1)
|
||||
k.FreeConsole()
|
||||
else:
|
||||
sys.exit(1)
|
||||
"""
|
||||
|
||||
|
||||
def send_break(proc, python=None):
|
||||
"""Ctrl+Break to the process group of proc (started with CREATE_NEW_PROCESS_GROUP). The window itself may have no
|
||||
console (pythonw), so a short helper process attaches to the child's console and sends it. Elsewhere SIGTERM to
|
||||
proc's session (Process starts it in one of its own), which installer/cli.py handles as Ctrl+Break."""
|
||||
if os.name != 'nt':
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGTERM)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
helper = [str(python or sys.executable), '-c', _BREAK, str(proc.pid)]
|
||||
flags = CREATE_NO_WINDOW if os.name == 'nt' else 0
|
||||
r = subprocess.run(helper, creationflags=flags, capture_output=True)
|
||||
return r.returncode == 0
|
||||
|
||||
|
||||
class Process:
|
||||
"""`python -m installer ...` with its stdout lines going to self.lines (a queue).
|
||||
|
||||
poll() returns the exit code once the process ended and its output is drained, else None."""
|
||||
|
||||
def __init__(self, command, cwd=None, helper_python=None, creationflags=None):
|
||||
self.command = command
|
||||
self.helper_python = helper_python
|
||||
self.lines = queue.Queue()
|
||||
env = dict(os.environ, PYTHONIOENCODING='utf-8', PYTHONUTF8='1', PYTHONUNBUFFERED='1')
|
||||
if os.name == 'nt':
|
||||
flags = (CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) if creationflags is None else creationflags
|
||||
group = {'creationflags': flags}
|
||||
else:
|
||||
group = {'start_new_session': True}
|
||||
self.proc = subprocess.Popen(command, cwd=cwd, env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, **group)
|
||||
self.cancelled_at = None
|
||||
self.reader = threading.Thread(target=self.read, daemon=True)
|
||||
self.reader.start()
|
||||
|
||||
def read(self):
|
||||
for raw in iter(self.proc.stdout.readline, b''):
|
||||
self.lines.put(raw.decode('utf-8', errors='replace'))
|
||||
self.proc.stdout.close()
|
||||
|
||||
def cancel(self):
|
||||
if self.cancelled_at is None and self.proc.poll() is None:
|
||||
self.cancelled_at = time.monotonic()
|
||||
send_break(self.proc, self.helper_python)
|
||||
|
||||
def poll(self):
|
||||
code = self.proc.poll()
|
||||
if code is not None and not self.reader.is_alive() and self.lines.empty():
|
||||
return code
|
||||
return None
|
||||
|
||||
def kill(self):
|
||||
if self.proc.poll() is None:
|
||||
self.proc.kill()
|
||||
98
installer/hostcheck.c
Normal file
98
installer/hostcheck.c
Normal file
@@ -0,0 +1,98 @@
|
||||
// Host check of the installer's first step, compiled with the bundled clang at install time: does the CPU run
|
||||
// -march=x86-64-v3 code (or only x86-64-v2), and does a Vulkan 1.3 device exist. Prints one JSON object on stdout.
|
||||
// The Vulkan loader is opened at run time, so a machine without one gets "loader": false instead of a failed start.
|
||||
#include <cpuid.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <vulkan/vulkan.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#define open_lib() LoadLibraryA("vulkan-1.dll")
|
||||
#define lib_sym(l, n) ((void *)GetProcAddress((l), (n)))
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#define open_lib() dlopen("libvulkan.so.1", RTLD_NOW)
|
||||
#define lib_sym(l, n) dlsym((l), (n))
|
||||
#endif
|
||||
|
||||
static void json_string(const char *s) {
|
||||
putchar('"');
|
||||
for (; *s; s++) {
|
||||
if (*s == '"' || *s == '\\') printf("\\%c", *s);
|
||||
else if ((unsigned char)*s < 0x20) printf("\\u%04x", *s);
|
||||
else putchar(*s);
|
||||
}
|
||||
putchar('"');
|
||||
}
|
||||
|
||||
// The x86-64-v2 level from CPUID: SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, CMPXCHG16B and LAHF/SAHF. Every CPU with
|
||||
// SSE4.2 has the others, but the level is what the v2 build is compiled for, so check them all.
|
||||
static int x86_64_v2(void) {
|
||||
unsigned a, b, c, d;
|
||||
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
|
||||
const unsigned sse3 = 1u << 0, ssse3 = 1u << 9, cx16 = 1u << 13, sse41 = 1u << 19, sse42 = 1u << 20,
|
||||
popcnt = 1u << 23;
|
||||
if ((c & (sse3 | ssse3 | cx16 | sse41 | sse42 | popcnt)) != (sse3 | ssse3 | cx16 | sse41 | sse42 | popcnt))
|
||||
return 0;
|
||||
if (!__get_cpuid(0x80000001, &a, &b, &c, &d)) return 0;
|
||||
return (c & (1u << 0)) != 0; // LAHF/SAHF
|
||||
}
|
||||
|
||||
// The x86-64-v3 level from CPUID: AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT and MOVBE, with the OS saving the YMM state.
|
||||
// Not __builtin_cpu_supports("x86-64-v3"): clang reads that from the C runtime's CPU model, and libgcc before GCC 12
|
||||
// (the Linux release's sysroot has GCC 10) has no level bits, so it answers false on every CPU.
|
||||
static int x86_64_v3(void) {
|
||||
if (!x86_64_v2()) return 0;
|
||||
unsigned a, b, c, d;
|
||||
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
|
||||
const unsigned fma = 1u << 12, movbe = 1u << 22, osxsave = 1u << 27, avx = 1u << 28, f16c = 1u << 29;
|
||||
if ((c & (fma | movbe | osxsave | avx | f16c)) != (fma | movbe | osxsave | avx | f16c)) return 0;
|
||||
unsigned lo, hi;
|
||||
__asm__("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
|
||||
if ((lo & 6) != 6) return 0; // XMM and YMM state enabled by the OS
|
||||
if (!__get_cpuid_count(7, 0, &a, &b, &c, &d)) return 0;
|
||||
const unsigned bmi1 = 1u << 3, avx2 = 1u << 5, bmi2 = 1u << 8;
|
||||
if ((b & (bmi1 | avx2 | bmi2)) != (bmi1 | avx2 | bmi2)) return 0;
|
||||
if (!__get_cpuid(0x80000001, &a, &b, &c, &d)) return 0;
|
||||
return (c & (1u << 5)) != 0; // LZCNT (ABM)
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("{\"x86_64_v2\": %s, \"x86_64_v3\": %s, ", x86_64_v2() ? "true" : "false",
|
||||
x86_64_v3() ? "true" : "false");
|
||||
void *lib = open_lib();
|
||||
PFN_vkGetInstanceProcAddr gipa = lib ? (PFN_vkGetInstanceProcAddr)lib_sym(lib, "vkGetInstanceProcAddr") : NULL;
|
||||
if (!gipa) {
|
||||
printf("\"loader\": false, \"devices\": []}\n");
|
||||
return 0;
|
||||
}
|
||||
printf("\"loader\": true, ");
|
||||
PFN_vkCreateInstance create = (PFN_vkCreateInstance)gipa(NULL, "vkCreateInstance");
|
||||
VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "nx2navi-install",
|
||||
.apiVersion = VK_API_VERSION_1_3};
|
||||
VkInstanceCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app};
|
||||
VkInstance inst = VK_NULL_HANDLE;
|
||||
VkResult r = create ? create(&ci, NULL, &inst) : VK_ERROR_INITIALIZATION_FAILED;
|
||||
if (r != VK_SUCCESS) {
|
||||
printf("\"instance\": %d, \"devices\": []}\n", (int)r);
|
||||
return 0;
|
||||
}
|
||||
PFN_vkEnumeratePhysicalDevices enumerate = (PFN_vkEnumeratePhysicalDevices)gipa(inst, "vkEnumeratePhysicalDevices");
|
||||
PFN_vkGetPhysicalDeviceProperties props = (PFN_vkGetPhysicalDeviceProperties)gipa(inst, "vkGetPhysicalDeviceProperties");
|
||||
PFN_vkDestroyInstance destroy = (PFN_vkDestroyInstance)gipa(inst, "vkDestroyInstance");
|
||||
VkPhysicalDevice devs[16];
|
||||
uint32_t n = 16;
|
||||
if (enumerate(inst, &n, devs) < 0) n = 0;
|
||||
printf("\"instance\": 0, \"devices\": [");
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
VkPhysicalDeviceProperties p;
|
||||
props(devs[i], &p);
|
||||
printf("%s{\"name\": ", i ? ", " : "");
|
||||
json_string(p.deviceName);
|
||||
printf(", \"api\": [%u, %u, %u], \"type\": %d}", VK_API_VERSION_MAJOR(p.apiVersion),
|
||||
VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion), (int)p.deviceType);
|
||||
}
|
||||
printf("]}\n");
|
||||
destroy(inst, NULL);
|
||||
return 0;
|
||||
}
|
||||
78
installer/ini.py
Normal file
78
installer/ini.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""nx2navi.ini: set the game language and keep everything else (keys and syntax as runtime/cfg.c reads them).
|
||||
|
||||
A new file starts from nx2navi.ini.example, whose lines are all commented defaults. The language goes into `lang`
|
||||
(a Switch language code, runtime/ov_system.c): an active `lang = ...` line is rewritten in place, else the commented
|
||||
`# lang = ...` line of the example is activated, else the line is added to [language] (created at the end if
|
||||
missing). Other lines, comments and line order stay as they are.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# runtime/ov_system.c: the codes NX2NAVI_LANG offers. Each language appears once, under the code the game accepts.
|
||||
LANGUAGES = ('ja', 'en-US', 'en-GB', 'fr', 'fr-CA', 'de', 'it', 'es', 'es-419', 'nl', 'pt', 'pt-BR', 'ru', 'ko',
|
||||
'zh-Hans', 'zh-Hant')
|
||||
# Older installer builds wrote these two codes; they are read as the canonical ones and rewritten on the next install.
|
||||
ALIASES = {'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant'}
|
||||
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the fonts step extracts from the
|
||||
# firmware folder when the player gives one.
|
||||
NEED_SHARED_FONTS = ('ko', 'zh-Hans', 'zh-Hant')
|
||||
|
||||
ACTIVE = re.compile(r'^(\s*)(?:nx2navi_)?lang(\s*=\s*)(.*?)(\s*)$', re.IGNORECASE)
|
||||
COMMENTED = re.compile(r'^\s*[#;]\s*(?:nx2navi_)?lang\s*=.*$', re.IGNORECASE)
|
||||
SECTION = re.compile(r'^\s*\[(.*)\]\s*$')
|
||||
|
||||
|
||||
def canonical_language(lang):
|
||||
"""The code LANGUAGES lists for lang: an alias of an older installer build is mapped, anything else is kept."""
|
||||
return ALIASES.get(lang, lang)
|
||||
|
||||
|
||||
def set_language(text, lang):
|
||||
"""text with `lang = <lang>` (an alias written as its canonical code); see the module docstring."""
|
||||
lang = canonical_language(lang)
|
||||
lines = text.splitlines()
|
||||
for i, line in enumerate(lines):
|
||||
m = ACTIVE.match(line)
|
||||
if m:
|
||||
lines[i] = f'{m.group(1)}lang{m.group(2)}{lang}'
|
||||
return '\n'.join(lines) + '\n'
|
||||
for i, line in enumerate(lines):
|
||||
if COMMENTED.match(line):
|
||||
lines[i] = f'lang = {lang}'
|
||||
return '\n'.join(lines) + '\n'
|
||||
for i, line in enumerate(lines):
|
||||
m = SECTION.match(line)
|
||||
if m and m.group(1).strip().lower() == 'language':
|
||||
lines.insert(i + 1, f'lang = {lang}')
|
||||
return '\n'.join(lines) + '\n'
|
||||
if lines and lines[-1].strip():
|
||||
lines.append('')
|
||||
lines += ['[language]', f'lang = {lang}']
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def get_language(text):
|
||||
for line in text.splitlines():
|
||||
m = ACTIVE.match(line)
|
||||
if m:
|
||||
return m.group(3)
|
||||
return None
|
||||
|
||||
|
||||
def write(dest, lang, example=None):
|
||||
"""Write dest/nx2navi.ini with lang; an existing file keeps its other lines. Returns the path."""
|
||||
path = Path(dest) / 'nx2navi.ini'
|
||||
if path.is_file():
|
||||
text = path.read_text(encoding='utf-8-sig')
|
||||
elif example and Path(example).is_file():
|
||||
text = Path(example).read_text(encoding='utf-8')
|
||||
else:
|
||||
text = ''
|
||||
new = set_language(text, lang)
|
||||
if path.is_file() and new == text:
|
||||
return path
|
||||
tmp = path.with_name('nx2navi.ini.tmp')
|
||||
tmp.write_text(new, encoding='utf-8', newline='\n')
|
||||
os.replace(tmp, path)
|
||||
return path
|
||||
51
installer/launcher/launcher.c
Normal file
51
installer/launcher/launcher.c
Normal file
@@ -0,0 +1,51 @@
|
||||
/* NX2navi-Setup.exe: starts python\pythonw.exe -m installer.gui with this exe's folder as the working directory.
|
||||
|
||||
No console window. The two message strings come from the catalog (installer/text/<lang>.json, ids launcher.title and
|
||||
launcher.python_missing); tools/make_release.py writes them into launcher_text.h before it compiles this file.
|
||||
Build: llvm/bin/llvm-windres -I runtime -O coff launcher.rc -o launcher_rc.o, then
|
||||
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o NX2navi-Setup.exe */
|
||||
#include <windows.h>
|
||||
#include <wchar.h>
|
||||
|
||||
#include "launcher_text.h"
|
||||
|
||||
#define LANGID_RUSSIAN 0x19 /* primary language id */
|
||||
#define LANGID_PT_BR 0x0416 /* the whole LANGID: Portugal's Portuguese stays English */
|
||||
#define PATH_CHARS (MAX_PATH * 2)
|
||||
|
||||
/* The language of the window (installer/guilogic.py ui_language_for_langid): Russian, Simplified Chinese (zh-CN and
|
||||
zh-SG; the Traditional Chinese regions get English, as the window has no Traditional Chinese), else English. */
|
||||
static void fail(void)
|
||||
{
|
||||
LANGID ui = GetUserDefaultUILanguage();
|
||||
int russian = (ui & 0x3ff) == LANGID_RUSSIAN, brazilian = ui == LANGID_PT_BR;
|
||||
int chinese = ui == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED) ||
|
||||
ui == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SINGAPORE);
|
||||
MessageBoxW(NULL,
|
||||
russian ? LAUNCHER_PYTHON_MISSING_RU
|
||||
: brazilian ? LAUNCHER_PYTHON_MISSING_PT_BR
|
||||
: chinese ? LAUNCHER_PYTHON_MISSING_ZH_HANS
|
||||
: LAUNCHER_PYTHON_MISSING_EN,
|
||||
russian ? LAUNCHER_TITLE_RU : brazilian ? LAUNCHER_TITLE_PT_BR : chinese ? LAUNCHER_TITLE_ZH_HANS
|
||||
: LAUNCHER_TITLE_EN,
|
||||
MB_OK | MB_ICONERROR);
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, int show)
|
||||
{
|
||||
wchar_t folder[PATH_CHARS], python[PATH_CHARS], command[PATH_CHARS + 64];
|
||||
DWORD n = GetModuleFileNameW(NULL, folder, PATH_CHARS);
|
||||
STARTUPINFOW si = { sizeof si };
|
||||
PROCESS_INFORMATION pi;
|
||||
(void)instance; (void)previous; (void)command_line; (void)show;
|
||||
if (n == 0 || n >= PATH_CHARS) { fail(); return 1; }
|
||||
while (n > 0 && folder[n - 1] != L'\\') n--;
|
||||
folder[n > 0 ? n - 1 : 0] = 0;
|
||||
if (swprintf(python, PATH_CHARS, L"%ls\\python\\pythonw.exe", folder) < 0 ||
|
||||
GetFileAttributesW(python) == INVALID_FILE_ATTRIBUTES) { fail(); return 1; }
|
||||
if (swprintf(command, PATH_CHARS + 64, L"\"%ls\" -m installer.gui", python) < 0) { fail(); return 1; }
|
||||
if (!CreateProcessW(python, command, NULL, NULL, FALSE, 0, NULL, folder, &si, &pi)) { fail(); return 1; }
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
return 0;
|
||||
}
|
||||
3
installer/launcher/launcher.rc
Normal file
3
installer/launcher/launcher.rc
Normal file
@@ -0,0 +1,3 @@
|
||||
/* The icon of NX2navi-Setup.exe: runtime/nx2navi.ico, the icon of nx2navi.exe. tools/make_release.py compiles this file with
|
||||
llvm-windres -I runtime and links it with launcher.c. */
|
||||
1 ICON "nx2navi.ico"
|
||||
193
installer/layout.py
Normal file
193
installer/layout.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Where the installer finds what it runs: the one place that maps the release folder and a repository checkout.
|
||||
|
||||
Release (docs/installer.md "Release layout"), root = the unpacked folder:
|
||||
|
||||
python/python.exe runs every tool (numpy, lz4, cryptography, compression.zstd); its ._pth puts .., ../tools
|
||||
and ../recomp on sys.path, so `python -m installer` imports from the release folder
|
||||
recomp/, tools/ the lifter and the scripts; recomp/patches-<v>.txt, tools/data/expected-v<version>.json for
|
||||
each supported update (SUPPORTED)
|
||||
llvm/ llvm-mingw (bin/clang.exe)
|
||||
runtime/ libnx2navirt.a and libnx2navirt-v2.a (x86-64-v3 / v2), the headers, install-side sources (romfs.c,
|
||||
patches.c, ov_zstd.c) and nx2navi.ico; runtime/deps/ holds
|
||||
include/ + lib/ of Vulkan, SDL3, Opus, libvpx and VMA (build_game.py --deps)
|
||||
bin/ nx2navi_xlate.exe (+ nx2navi_xlate.id), hactool.exe, ninja.exe, SDL3.dll, libopus-0.dll
|
||||
data/shader_list.tsv the MacNativeGames shader list
|
||||
data/pgo-<v>.profdata the execution profile the update's code is compiled with (tools/pgo.py; Windows only);
|
||||
without one the game is built at -O1
|
||||
nx2navi.ini.example
|
||||
VERSION the release version, one line
|
||||
temp/ the default temp folder (created)
|
||||
|
||||
Linux release (the AppImage's folder, docs/installer.md "Linux release"): python/bin/python3 (python-build-standalone,
|
||||
with tkinter), llvm/ with clang, lld and llvm-ar plus llvm/sysroot (the glibc headers and libraries the game links
|
||||
against), runtime/deps/ with include/ and the static Opus, libvpx and zstd, bin/ without .exe and with libSDL3.so.0.
|
||||
|
||||
Dev mode, root = a repository checkout (no python/python.exe): build/py314-embed/py/python.exe (the embeddable
|
||||
Python the release ships, with the same packages), else this Python; toolchain/llvm-mingw-*/ and toolchain/deps-mingw;
|
||||
runtime/ as the sources with build/install-rtlib/libnx2navirt.a and libnx2navirt-v2.a (tools/build_game.py --make-runtime-lib
|
||||
--out build/install-rtlib, once per --cpu); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/nx2navi_xlate.exe; the shader list from
|
||||
refs/MacNativeGames; build/pgo/pgo-<v>.profdata when there is one; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
|
||||
dev mode takes clang, lld and ninja from PATH and the system's libraries (no deps folder, no SDL3 copy).
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
WIN = os.name == 'nt'
|
||||
EXE = '.exe' if WIN else ''
|
||||
UCRT = Path(r'C:\msys64\ucrt64\bin')
|
||||
SUPPORTED = ('1.2.1', '1.4.3', '1.1.0') # updates the installer builds; the check identifies the player's by hash
|
||||
|
||||
|
||||
def version_tag(version):
|
||||
"""'1.4.3' -> 'v143': the version name of build_game.py, make_romfs_patches.py and the patch tables."""
|
||||
return 'v' + version.replace('.', '')
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Layout:
|
||||
root: Path
|
||||
dev: bool
|
||||
release: str
|
||||
python: Path # runs the tools
|
||||
tools: Path
|
||||
recomp: Path
|
||||
llvm: Path # llvm-mingw tree; on Linux the tree with bin/clang (a release's, or the system's /usr)
|
||||
deps: Path | None # include/ and lib/ for the link; None: the system's (Linux dev mode)
|
||||
runtime: Path # runtime headers and install-side sources
|
||||
runtime_lib: Path # libnx2navirt.a
|
||||
ninja: Path
|
||||
hactool: Path
|
||||
xlate: Path # nx2navi_xlate.exe; a nx2navi_xlate.id beside it is copied too
|
||||
dlls: tuple = field(default_factory=tuple)
|
||||
shader_list: Path | None = None # None: dev, read from refs/MacNativeGames through git
|
||||
ini_example: Path | None = None
|
||||
temp: Path | None = None
|
||||
sysroot: Path | None = None # Linux release: what the game links against, instead of the host's
|
||||
runtime_lib_v2: Path | None = None # libnx2navirt-v2.a: the runtime for CPUs without AVX2 (build_game.py --cpu v2)
|
||||
pgo_dir: Path | None = None # pgo-<v>.profdata (tools/pgo.py); None: no profiles (Linux)
|
||||
|
||||
@property
|
||||
def clang(self):
|
||||
return self.llvm / 'bin' / ('clang' + EXE)
|
||||
|
||||
def runtime_lib_for(self, cpu):
|
||||
"""The prebuilt runtime library for cpu (v2 or v3). None in dev mode when it was not built: the runtime is
|
||||
then compiled from the sources in runtime/ instead."""
|
||||
lib = self.runtime_lib_v2 if cpu == 'v2' else self.runtime_lib
|
||||
if lib and lib.is_file():
|
||||
return lib
|
||||
return None if self.dev else lib
|
||||
|
||||
def pgo(self, version):
|
||||
"""The execution profile build_game.py --pgo compiles the update's code with, None without one (the game is
|
||||
then built at -O1)."""
|
||||
p = self.pgo_dir / f'pgo-{version_tag(version)}.profdata' if self.pgo_dir else None
|
||||
return p if p and p.is_file() else None
|
||||
|
||||
def patches(self, version):
|
||||
"""The code patch table for recomp/gen.py."""
|
||||
return self.recomp / f'patches-{version_tag(version)}.txt'
|
||||
|
||||
def expected(self, version):
|
||||
"""The expected hashes of the update."""
|
||||
return self.tools / 'data' / f'expected-v{version}.json'
|
||||
|
||||
def missing(self, cpu='v3'):
|
||||
"""Paths the steps need that do not exist. cpu picks the runtime library the host will use."""
|
||||
lib = self.runtime_lib_for(cpu)
|
||||
need = [self.python, self.tools / 'extract.py', self.tools / 'ncz.py', self.tools / 'build_game.py',
|
||||
self.tools / 'prepare_shaders.py',
|
||||
self.tools / 'pack_shader_cache.py', self.tools / 'make_romfs_patches.py', self.tools / 'ultrawide_layout.py',
|
||||
self.tools / 'button_icons.py',
|
||||
self.tools / 'vendor' / 'macnativegames' / 'extract_shared_fonts.py',
|
||||
self.recomp / 'gen.py',
|
||||
self.clang,
|
||||
*((self.deps / 'include', self.deps / 'lib') if self.deps else ()), self.runtime, *([lib] if lib else ()),
|
||||
self.ninja, self.hactool,
|
||||
self.xlate, *self.dlls, *(f(v) for v in SUPPORTED for f in (self.patches, self.expected))]
|
||||
if self.shader_list:
|
||||
need.append(self.shader_list)
|
||||
if self.sysroot:
|
||||
need.append(self.sysroot)
|
||||
return [str(p) for p in need if not p.exists()]
|
||||
|
||||
|
||||
def release_python(root):
|
||||
return root / 'python' / ('python.exe' if WIN else 'bin/python3')
|
||||
|
||||
|
||||
def is_release(root):
|
||||
return release_python(root).is_file()
|
||||
|
||||
|
||||
def release_layout(root):
|
||||
version = root / 'VERSION'
|
||||
return Layout(
|
||||
root=root, dev=False,
|
||||
release=version.read_text(encoding='utf-8').strip() if version.is_file() else 'unknown',
|
||||
python=release_python(root),
|
||||
tools=root / 'tools', recomp=root / 'recomp',
|
||||
llvm=root / 'llvm', deps=root / 'runtime' / 'deps', runtime=root / 'runtime',
|
||||
runtime_lib=root / 'runtime' / 'libnx2navirt.a',
|
||||
runtime_lib_v2=root / 'runtime' / 'libnx2navirt-v2.a',
|
||||
ninja=root / 'bin' / ('ninja' + EXE), hactool=root / 'bin' / ('hactool' + EXE),
|
||||
xlate=root / 'bin' / ('nx2navi_xlate' + EXE),
|
||||
dlls=(root / 'bin' / 'SDL3.dll', root / 'bin' / 'libopus-0.dll') if WIN else (root / 'bin' / 'libSDL3.so.0',),
|
||||
shader_list=root / 'data' / 'shader_list.tsv', ini_example=root / 'nx2navi.ini.example',
|
||||
pgo_dir=root / 'data' if WIN else None,
|
||||
# Linux: None, the install folder's .setup-temp (installer/cli.py): the AppImage is mounted read-only, and on
|
||||
# a Steam Deck /home is often too full for the generated C while the install folder is on the SD card
|
||||
temp=root / 'temp' if WIN else None,
|
||||
sysroot=None if WIN else root / 'llvm' / 'sysroot')
|
||||
|
||||
|
||||
def dev_layout(root):
|
||||
if not WIN:
|
||||
return dev_layout_posix(root)
|
||||
commit = subprocess.run(['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'], capture_output=True,
|
||||
text=True).stdout.strip()
|
||||
embed = root / 'build' / 'py314-embed' / 'py' / ('python' + EXE)
|
||||
trees = sorted(p.parent.parent for p in (root / 'toolchain').glob('llvm-mingw-*/llvm-mingw-*/bin/clang' + EXE))
|
||||
deps = root / 'toolchain' / 'deps-mingw'
|
||||
semver = (root / 'VERSION').read_text(encoding='utf-8').strip()
|
||||
return Layout(
|
||||
root=root, dev=True, release=f'{semver}-dev+{commit or "unknown"}',
|
||||
python=embed if embed.is_file() else Path(sys.executable),
|
||||
tools=root / 'tools', recomp=root / 'recomp',
|
||||
llvm=trees[-1] if trees else root / 'toolchain' / 'llvm-mingw-missing', deps=deps,
|
||||
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libnx2navirt.a',
|
||||
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libnx2navirt-v2.a',
|
||||
ninja=UCRT / ('ninja' + EXE), hactool=root / 'toolchain' / 'hactool' / ('hactool' + EXE),
|
||||
xlate=root / 'build' / 'xlate' / ('nx2navi_xlate' + EXE),
|
||||
dlls=tuple(sorted((deps / 'bin').glob('*.dll'))),
|
||||
shader_list=None, ini_example=root / 'nx2navi.ini.example', temp=root / 'build' / 'install-temp',
|
||||
pgo_dir=root / 'build' / 'pgo')
|
||||
|
||||
|
||||
def dev_commit(root):
|
||||
return subprocess.run(['git', '-C', str(root), 'rev-parse', '--short', 'HEAD'], capture_output=True,
|
||||
text=True).stdout.strip()
|
||||
|
||||
|
||||
def dev_layout_posix(root):
|
||||
clang, ninja = shutil.which('clang'), shutil.which('ninja')
|
||||
semver = (root / 'VERSION').read_text(encoding='utf-8').strip()
|
||||
return Layout(
|
||||
root=root, dev=True, release=f'{semver}-dev+{dev_commit(root) or "unknown"}',
|
||||
python=Path(sys.executable), tools=root / 'tools', recomp=root / 'recomp',
|
||||
llvm=Path(clang).parent.parent if clang else root / 'toolchain' / 'clang-missing', deps=None,
|
||||
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libnx2navirt.a',
|
||||
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libnx2navirt-v2.a',
|
||||
ninja=Path(ninja) if ninja else root / 'toolchain' / 'ninja-missing',
|
||||
hactool=root / 'toolchain' / 'hactool' / 'hactool', xlate=root / 'build' / 'xlate' / 'nx2navi_xlate',
|
||||
shader_list=None, ini_example=root / 'nx2navi.ini.example', temp=root / 'build' / 'install-temp')
|
||||
|
||||
|
||||
def find(root=None):
|
||||
"""The layout of root (default: the folder above this package)."""
|
||||
root = Path(root or Path(__file__).resolve().parent.parent).resolve()
|
||||
return release_layout(root) if is_release(root) else dev_layout(root)
|
||||
151
installer/runner.py
Normal file
151
installer/runner.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Which steps run, in what order, and the bookkeeping around them: fresh install, resume and update are one rule.
|
||||
|
||||
Each step has a key: a fingerprint of everything its output depends on, computed before anything runs (a step's key
|
||||
includes the keys of the steps it reads from, so a change upstream reaches every step below it). A step is skipped
|
||||
when nx2navi-install.json records it with the same key and its output still matches the record (Step.current, cheap).
|
||||
Otherwise it runs. A transient step (its output lives in the temp folder and only the next step reads it) runs only
|
||||
when the next step runs. `always` steps (check, cleanup) run every time.
|
||||
|
||||
Update mode is the same rule with a newer release: the extraction's key depends only on the expected hashes, so a
|
||||
release that changes the lifter or the runtime keeps romfs/ and exefs/ and rebuilds from lift on. Before keeping the
|
||||
output of a step recorded by another release (or with --verify), the check step calls Step.verify (expensive: the full
|
||||
RomFS hash); a mismatch makes the step run again.
|
||||
|
||||
A failed or cancelled run leaves the finished steps recorded; the next run starts at the first step that is not.
|
||||
"""
|
||||
import datetime
|
||||
import traceback
|
||||
|
||||
from . import state as statefile
|
||||
|
||||
|
||||
class InstallError(Exception):
|
||||
"""A failure with a message id (installer/text/en.json) and its parameters."""
|
||||
|
||||
def __init__(self, id, **params):
|
||||
super().__init__(id)
|
||||
self.id, self.params = id, params
|
||||
|
||||
|
||||
class Step:
|
||||
id = ''
|
||||
transient = False
|
||||
always = False
|
||||
|
||||
def key(self, ctx):
|
||||
"""Fingerprint of the step's inputs; may read ctx.keys of earlier steps."""
|
||||
return ''
|
||||
|
||||
def current(self, ctx, record):
|
||||
"""True when the recorded output is still in place (cheap checks only)."""
|
||||
return True
|
||||
|
||||
def verify(self, ctx, record):
|
||||
"""True when the recorded output passes the full check (hashes); called before keeping another release's
|
||||
output."""
|
||||
return True
|
||||
|
||||
def run(self, ctx):
|
||||
"""Do the work. Returns fields to record with the step."""
|
||||
return {}
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self, layout, dest, temp, reporter, options, state=None, verify=False):
|
||||
self.layout, self.dest, self.temp, self.reporter, self.options = layout, dest, temp, reporter, options
|
||||
self.state = state or statefile.empty(layout.release)
|
||||
self.verify = verify
|
||||
self.mode = 'install' if state is None else 'resume' if state.get('release') == layout.release else 'update'
|
||||
self.keys, self.runs, self.reasons, self.steps, self.forced = {}, {}, {}, [], set()
|
||||
|
||||
def record(self, step_id):
|
||||
return self.state['steps'].get(step_id)
|
||||
|
||||
def will_run(self, step_id):
|
||||
return self.runs.get(step_id, False)
|
||||
|
||||
def save(self):
|
||||
self.dest.mkdir(parents=True, exist_ok=True)
|
||||
statefile.save(self.dest, self.state)
|
||||
|
||||
def plan(self, forced=()):
|
||||
"""Fill keys (once), runs and reasons. forced: step ids that must run."""
|
||||
if not self.keys:
|
||||
for s in self.steps:
|
||||
self.keys[s.id] = s.key(self)
|
||||
for s in self.steps:
|
||||
rec = self.record(s.id)
|
||||
if s.always or s.id in forced:
|
||||
self.runs[s.id] = True
|
||||
else:
|
||||
self.runs[s.id] = not (rec and rec.get('key') == self.keys[s.id] and s.current(self, rec))
|
||||
self.reasons[s.id] = '' if self.runs[s.id] else 'current'
|
||||
for i in range(len(self.steps) - 2, -1, -1):
|
||||
s, after = self.steps[i], self.steps[i + 1]
|
||||
if s.transient and self.runs[s.id] and not self.runs[after.id]:
|
||||
self.runs[s.id], self.reasons[s.id] = False, 'not_needed'
|
||||
self.forced = set(forced)
|
||||
|
||||
def verify_kept(self, progress_step):
|
||||
"""Full check of every kept step output that another release recorded (or of all with --verify). A step that
|
||||
fails it is planned to run. Returns the ids that failed."""
|
||||
failed = []
|
||||
for s in self.steps:
|
||||
rec = self.record(s.id)
|
||||
if self.runs[s.id] or self.reasons[s.id] != 'current' or s.always:
|
||||
continue
|
||||
if not (self.verify or rec.get('release') != self.layout.release):
|
||||
continue
|
||||
if not s.verify(self, rec):
|
||||
failed.append(s.id)
|
||||
if failed:
|
||||
for f in failed:
|
||||
self.reporter.warning('state.output_changed', step=f)
|
||||
self.plan(self.forced | set(failed))
|
||||
return failed
|
||||
|
||||
|
||||
def now_utc():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
|
||||
def run_steps(steps, ctx):
|
||||
"""Run the plan. Returns 'ok', 'failed' or 'cancelled'; events and the log say why."""
|
||||
rep = ctx.reporter
|
||||
ctx.steps = steps
|
||||
current = None
|
||||
try:
|
||||
ctx.plan()
|
||||
rep.emit('plan', run=[s.id for s in steps if ctx.runs[s.id]],
|
||||
skip={s.id: ctx.reasons[s.id] for s in steps if not ctx.runs[s.id]})
|
||||
for s in steps:
|
||||
if not ctx.runs[s.id]:
|
||||
rep.step(s.id, 'skipped', reason=ctx.reasons[s.id])
|
||||
continue
|
||||
current = s.id
|
||||
rep.step(s.id, 'started')
|
||||
t0 = rep.elapsed()
|
||||
fields = s.run(ctx) or {}
|
||||
seconds = round(rep.elapsed() - t0, 1)
|
||||
if not s.always:
|
||||
ctx.state['steps'][s.id] = {'key': ctx.keys[s.id], 'release': ctx.layout.release,
|
||||
'finished': now_utc(), 'seconds': seconds, **fields}
|
||||
ctx.save() # the first save, after the check, creates the install folder
|
||||
rep.step(s.id, 'done', seconds=seconds)
|
||||
current = None
|
||||
ctx.state['release'] = ctx.layout.release
|
||||
ctx.save()
|
||||
return 'ok'
|
||||
except InstallError as e:
|
||||
rep.error(e.id, e.params, step=current)
|
||||
return 'failed'
|
||||
except statefile.StateError as e:
|
||||
rep.error(e.id, {'path': str(e.path)}, step=current)
|
||||
return 'failed'
|
||||
except KeyboardInterrupt:
|
||||
rep.log(f'cancelled during {current}')
|
||||
return 'cancelled'
|
||||
except Exception as e:
|
||||
rep.log(traceback.format_exc())
|
||||
rep.error('internal', {'exception': type(e).__name__, 'detail': str(e)}, step=current)
|
||||
return 'failed'
|
||||
54
installer/state.py
Normal file
54
installer/state.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""nx2navi-install.json in the install folder: what built it and which steps finished.
|
||||
|
||||
{"format": 1, "release": "...", "lang": "en-US",
|
||||
"inputs": {"base": {"name", "size", "sha256"}, "update": {...}},
|
||||
"steps": {"<step>": {"key": "<input fingerprint>", "release": "...", "finished": "<UTC time>", "seconds": 12.3,
|
||||
...the step's own fields}}}
|
||||
|
||||
A step's key fingerprints everything its output depends on (runner.py), so a step whose recorded key equals the
|
||||
current one and whose output still checks out is not run again. The file is replaced atomically after each step.
|
||||
No key value or keys file content is ever stored.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
NAME = 'nx2navi-install.json'
|
||||
FORMAT = 1
|
||||
|
||||
|
||||
class StateError(Exception):
|
||||
def __init__(self, id, path):
|
||||
super().__init__(id)
|
||||
self.id, self.path = id, path
|
||||
|
||||
|
||||
def empty(release):
|
||||
return {'format': FORMAT, 'release': release, 'inputs': {}, 'steps': {}}
|
||||
|
||||
|
||||
def load(dest):
|
||||
"""The state of dest, or None when the folder has none. StateError for an unreadable or newer file."""
|
||||
path = Path(dest) / NAME
|
||||
if not path.is_file():
|
||||
return None
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
raise StateError('state.unreadable', path) from None
|
||||
if not isinstance(data, dict) or not isinstance(data.get('steps'), dict):
|
||||
raise StateError('state.unreadable', path)
|
||||
if data.get('format', 0) > FORMAT:
|
||||
raise StateError('state.newer_format', path)
|
||||
return data
|
||||
|
||||
|
||||
def save(dest, data):
|
||||
path = Path(dest) / NAME
|
||||
tmp = path.with_name(NAME + '.tmp')
|
||||
with open(tmp, 'w', encoding='utf-8', newline='\n') as f:
|
||||
json.dump(data, f, indent=1, ensure_ascii=False)
|
||||
f.write('\n')
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.replace(tmp, path)
|
||||
1267
installer/steps.py
Normal file
1267
installer/steps.py
Normal file
File diff suppressed because it is too large
Load Diff
208
installer/text/en.json
Normal file
208
installer/text/en.json
Normal file
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"step.check": "Checking the computer and the game files",
|
||||
"step.fonts": "Extracting the system fonts",
|
||||
"step.extract": "Extracting the game",
|
||||
"step.lift": "Translating the game code to C",
|
||||
"step.compile": "Compiling {game_exe}",
|
||||
"step.prepare": "Preparing the shaders",
|
||||
"step.dlss": "Downloading NVIDIA DLSS",
|
||||
"step.cleanup": "Cleaning up",
|
||||
|
||||
"phase.check.host": "Checking the processor and the graphics card",
|
||||
"phase.check.verify": "Checking the files already installed",
|
||||
"phase.check.verify_romfs": "Checking the installed game data",
|
||||
"phase.check.inputs": "Opening the game files with the keys",
|
||||
"phase.check.unpack_base": "Unpacking the compressed base game",
|
||||
"phase.check.unpack_update": "Unpacking the compressed update",
|
||||
"phase.check.versions": "Checking the game versions",
|
||||
"phase.fonts.extract": "Extracting the shared fonts from the firmware",
|
||||
"phase.fonts.copy": "Copying the shared fonts into the install folder",
|
||||
"phase.extract.hash_base": "Computing the checksum of the base game",
|
||||
"phase.extract.hash_update": "Computing the checksum of the update",
|
||||
"phase.extract.exefs": "Extracting the game code",
|
||||
"phase.extract.copy": "Copying the game data",
|
||||
"phase.extract.merge": "Applying the update to the game data",
|
||||
"phase.extract.diagnose": "Finding which game file is damaged",
|
||||
"phase.extract.index": "Verifying the game data",
|
||||
"phase.lift.discover": "Finding the functions",
|
||||
"phase.lift.emit": "Writing C code",
|
||||
"phase.compile.compile": "Compiling",
|
||||
"phase.compile.link": "Linking {game_exe}",
|
||||
"phase.prepare.romfs_patches": "Writing the 60 fps game data patches",
|
||||
"phase.prepare.seed": "Reading the shaders from the game data",
|
||||
"phase.prepare.translate": "Translating the shaders",
|
||||
"phase.prepare.libraries": "Building the shaders for the graphics driver",
|
||||
"phase.prepare.pack": "Packing the shader cache into one file",
|
||||
"phase.dlss.download": "Downloading nvngx_dlss.dll from NVIDIA's DLSS SDK",
|
||||
"phase.cleanup.ini": "Writing nx2navi.ini",
|
||||
"phase.cleanup.temp": "Removing temporary files",
|
||||
|
||||
"cli.usage": "Invalid command line: {detail}",
|
||||
"internal": "Unexpected error ({exception}): {detail}",
|
||||
"state.unreadable": "{path} cannot be read. Choose another folder or delete that file.",
|
||||
"state.newer_format": "{path} was written by a newer installer. Use that installer or another folder.",
|
||||
"state.output_changed": "Installed files of step '{step}' changed since they were made; that step runs again.",
|
||||
|
||||
"check.release_incomplete": "The installer folder is incomplete; missing: {paths}. Unpack the release again.",
|
||||
"check.dest_not_folder": "The install location {path} is a file, not a folder.",
|
||||
"check.dest_not_empty": "The install folder {path} is not empty and holds no earlier installation. Choose an empty folder.",
|
||||
"check.lang_missing": "Choose the game language.",
|
||||
"check.lang_unknown": "Unknown language '{lang}'. Known: {known}.",
|
||||
"check.toolchain_broken": "The bundled compiler does not work: {detail}",
|
||||
"check.glibc_old": "This system's C library is glibc {version}; the setup's compiler needs {need} or later (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 and newer have it). Updating the system fixes this.",
|
||||
"check.host_probe_failed": "The computer check did not run (exit code {code}).",
|
||||
"check.cpu_too_old": "This processor lacks x86-64-v2 (SSE4.2, POPCNT and CMPXCHG16B), the oldest level the game is built for. Intel Nehalem (2008) or AMD Bulldozer (2011) and newer have it.",
|
||||
"check.vulkan_missing": "No Vulkan driver is installed. Install the latest driver for your graphics card.",
|
||||
"check.vulkan_no_device": "Vulkan finds no graphics card. Install the latest driver for your graphics card.",
|
||||
"check.vulkan_too_old": "The game needs a graphics card with Vulkan 1.3; found: {devices}. Update the graphics driver.",
|
||||
"check.no_display": "Preparing the shaders needs a display. Run the setup from the desktop session, or export DISPLAY=:0 (on a Steam Deck in Game Mode also XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.0 or 1.2.1 or 1.4.3 update and prod.keys.",
|
||||
"check.input_missing": "File not found ({input}): {path}",
|
||||
"check.keys_unreadable": "The keys file cannot be read. Choose a prod.keys file.",
|
||||
"check.keys_no_header_key": "The keys file has no header_key. Dump prod.keys from your console again.",
|
||||
"check.keys_wrong": "The keys cannot open the {input} file. Use the prod.keys of your console.",
|
||||
"check.keys_too_old": "The keys file is too old for the {input} file (needs key generation {generation}). Dump prod.keys from a console with newer firmware.",
|
||||
"check.not_game_file": "The {input} file is not an XCI, NSP, XCZ or NSZ game file.",
|
||||
"check.file_unreadable": "The {input} file cannot be read: {detail}",
|
||||
"check.unpack_failed": "The compressed {input} file could not be unpacked: {detail}",
|
||||
"check.ticket_personalized": "The {input} file carries a personalized ticket; a common ticket is needed.",
|
||||
"check.ticket_missing": "The {input} file lacks the ticket its content needs.",
|
||||
"check.base_not_nx2navi": "The base game file is not The Legend of Zelda: Ocarina of Time ({detail}).",
|
||||
"check.base_is_update": "The base game file is an update. Choose the base game NSP or XCI (version 1.0.0) here and this file as the update.",
|
||||
"check.base_combined": "The base game file holds several packages (base game, update or DLC in one file). Choose a base game dump that contains only the game.",
|
||||
"check.update_not_nsp": "The update must be an NSP or NSZ file.",
|
||||
"check.update_is_base": "The update file holds a base game. Choose the 1.1.0 or 1.2.1 or 1.4.3 update NSP.",
|
||||
"check.update_not_nx2navi": "The update file is not a Ocarina of Time update ({detail}).",
|
||||
"check.exefs_failed": "The game code of the {input} file failed its checks; see {log}.",
|
||||
"check.input_damaged": "The {input} file is damaged or incomplete: {path} ({detail}). Copy that file again from its source, or dump it again, then run the setup again.",
|
||||
"check.base_wrong_version": "The base game is not version 1.0.0 ({modules} differ).",
|
||||
"check.update_wrong_version": "The update is version {found}; this release takes {wanted}.",
|
||||
"check.disk_space": "Not enough free space for {path}: {need_gb} GB needed, {free_gb} GB free.",
|
||||
"check.firmware_missing": "The firmware folder {path} does not exist. Choose the firmware's Contents/registered folder.",
|
||||
"check.firmware_empty": "The firmware folder {path} holds no NCA files. Choose the firmware's Contents/registered folder.",
|
||||
"check.fonts_keys_needed": "The shared fonts for this language are not installed yet; choose prod.keys so the setup can extract them from the firmware.",
|
||||
|
||||
"extract.exefs_mismatch": "The extracted game code does not match version {version} ({modules}).",
|
||||
"extract.romfs_merge_failed": "Applying the update to the game data failed: {detail}. Details: {log}",
|
||||
"extract.romfs_mismatch": "The extracted game data does not match version {version} (size {size}, checksum {sha256}...).",
|
||||
"fonts.failed": "Extracting the shared fonts failed (exit code {code}): {detail}. Details: {log}",
|
||||
"fonts.incomplete": "The shared font extraction did not write every file ({files}).",
|
||||
"fonts.copy_failed": "The shared fonts cannot be copied into {path}: {detail}",
|
||||
"lift.failed": "Translating the game code failed (exit code {code}). Details: {log}",
|
||||
"compile.failed": "Compiling {game_exe} failed (exit code {code}). Details: {log}",
|
||||
"compile.exe_in_use": "{path} is in use. Close the game and run the installer again.",
|
||||
"prepare.xlate_in_use": "{path} is in use. Close the game, end any nx2navi_xlate process left from an earlier run, then run the installer again.",
|
||||
"prepare.shader_list_missing": "The shader list is missing.",
|
||||
"prepare.romfs_patches_failed": "Writing the game data patches failed (exit code {code}). Details: {log}",
|
||||
"prepare.shaders_failed": "Preparing the shaders failed (exit code {code}). Details: {log}",
|
||||
"cleanup.romfs_unreadable": "The game data in {path} is missing or cannot be read after the install. Restart Windows, then run the installer again on the same folder: it extracts the game data again and keeps the rest.",
|
||||
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
|
||||
"cleanup.ini_failed": "nx2navi.ini cannot be written: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, and none were given. The game will not start in this language until they are added. Choose the firmware folder (Contents/registered), or see \"Switch system fonts\" in docs/installer.md.",
|
||||
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
|
||||
"warn.cheats_skipped": "The cheats of the mods folder that change the game's code could not be built; the game installs without them. Details: {log}",
|
||||
"warn.exefs_skipped": "The mods' ExeFS patches (.ips, .pchtxt) could not be built; the game installs without them. Details: {log}",
|
||||
"warn.xbox_prompts_failed": "The Xbox or PC button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
|
||||
"warn.shaders_skipped": "The graphics driver crashed while building some shaders, so {count} were left out of the preparation. The game builds them when it first needs them, and the driver may crash there too; a driver update may fix it. Details: {log}",
|
||||
"warn.kernel_old": "Linux {version} lacks the write tracking the game relies on (6.7 or later); the game may run very slowly. Updating the system fixes this.",
|
||||
"warn.dlss_kept": "A DLSS library is already in the install folder ({path}); the setup keeps it.",
|
||||
"warn.dlss_download_failed": "NVIDIA DLSS could not be downloaded from {url}: {detail}. The game runs without it; the next setup run tries again.",
|
||||
|
||||
"ui.title": "NX2navi Setup",
|
||||
"ui.rebuild_title": "Rebuilding the game for your mods",
|
||||
"ui.start_game": "Start the game",
|
||||
"ui.version": "version {version}",
|
||||
"ui.intro": "Builds a playable Ocarina of Time folder from your own game files. This takes a while and runs once.",
|
||||
"ui.notice": "The folder this setup builds contains the game. It is for your own use only; do not share or upload it.",
|
||||
"ui.keys_private": "The keys file is read in place. Its contents are never shown, copied or stored.",
|
||||
"ui.base": "Base game (NSP, XCI, NSZ or XCZ)",
|
||||
"ui.update": "Update 1.1.0 or 1.2.1 or 1.4.3 (NSP or NSZ)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "Firmware folder (optional)",
|
||||
"ui.game_language": "Game language",
|
||||
"ui.dest": "Install folder",
|
||||
"ui.browse": "Browse...",
|
||||
"ui.pick_game": "Choose the base game file",
|
||||
"ui.pick_update": "Choose the 1.1.0 or 1.2.1 or 1.4.3 update file",
|
||||
"ui.pick_keys": "Choose prod.keys",
|
||||
"ui.pick_firmware": "Choose the Switch firmware folder (Contents/registered)",
|
||||
"ui.pick_dest": "Choose the install folder",
|
||||
"ui.firmware_help": "For Chinese, Korean and Traditional Chinese text: the firmware's Contents/registered folder, for example from Ryujinx or The hardware. The setup copies the Switch system fonts from it into the install folder.",
|
||||
"ui.dlss": "Download NVIDIA DLSS (nvngx_dlss.dll) for NVIDIA RTX graphics cards",
|
||||
"ui.dlss_help": "From NVIDIA's DLSS SDK on GitHub, under NVIDIA's RTX SDK license (github.com/NVIDIA/DLSS). It makes the dlaa and DLSS settings work; without it they do nothing.",
|
||||
"ui.compressed_help": "Compressed NSZ and XCZ files work as they are: the setup unpacks them by itself (until the game data is extracted, that needs as much free space as the unpacked files take).",
|
||||
"ui.type_game": "Game files (NSP, XCI, NSZ, XCZ)",
|
||||
"ui.type_keys": "Key files (*.keys)",
|
||||
"ui.type_all": "All files",
|
||||
"ui.start": "Start",
|
||||
"ui.cancel": "Cancel",
|
||||
"ui.cancelling": "Cancelling...",
|
||||
"ui.show_log": "Show log",
|
||||
"ui.hide_log": "Hide log",
|
||||
"ui.close": "Close",
|
||||
"ui.back": "Back",
|
||||
"ui.open_folder": "Open folder",
|
||||
"ui.update_mode": "This folder holds an earlier installation. The setup continues or updates it; the game files are asked for only if they are needed again.",
|
||||
"ui.need_dest": "Choose the install folder.",
|
||||
"ui.need_base": "Choose the base game file.",
|
||||
"ui.need_update": "Choose the 1.1.0 or 1.2.1 or 1.4.3 update file.",
|
||||
"ui.need_keys": "Choose the prod.keys file.",
|
||||
"ui.need_lang": "Choose the game language.",
|
||||
"ui.missing_base": "The base game file does not exist.",
|
||||
"ui.missing_update": "The update file does not exist.",
|
||||
"ui.missing_keys": "The keys file does not exist.",
|
||||
"ui.missing_firmware": "The firmware folder does not exist.",
|
||||
"ui.running_install": "Installing",
|
||||
"ui.running_resume": "Continuing the earlier setup",
|
||||
"ui.running_update": "Updating the installation",
|
||||
"ui.state_started": "started",
|
||||
"ui.state_done": "done",
|
||||
"ui.state_skipped": "skipped",
|
||||
"ui.state_pending": "waiting",
|
||||
"ui.state_running": "running",
|
||||
"ui.state_failed": "failed",
|
||||
"ui.log_warning": "Warning",
|
||||
"ui.log_error": "Error",
|
||||
"ui.log_space": "Space needed: {need} GB, free: {free} GB",
|
||||
"ui.log_end": "Finished",
|
||||
"ui.result_ok": "The game is ready",
|
||||
"ui.result_failed": "The setup failed",
|
||||
"ui.result_cancelled": "The setup was cancelled",
|
||||
"ui.done_text": "Start {game_exe} from {dest}. The setup program and the game files you chose are no longer needed.",
|
||||
"ui.cancelled_text": "The finished steps are kept. Start the setup again with the same folder to continue.",
|
||||
"ui.log_file": "Log file: {path}",
|
||||
"ui.process_died": "The setup stopped unexpectedly (exit code {code}).",
|
||||
"ui.confirm_cancel": "The setup is running. Cancel it and close the window?",
|
||||
"ui.space_one": "Free space needed on {path}: {need} GB (free: {free} GB).",
|
||||
"ui.space_two": "Free space needed on {path}: {need} GB (free: {free} GB); on {temp}: {temp_need} GB (free: {temp_free} GB).",
|
||||
"ui.amount_bytes": "{done} / {total} GB",
|
||||
"ui.amount_chunks": "{done} / {total} parts",
|
||||
"ui.amount_objects": "{done} / {total} files",
|
||||
"ui.amount_shaders": "{done} / {total} shaders",
|
||||
"ui.amount_other": "{done} / {total} {unit}",
|
||||
"ui.seconds": "{n} s",
|
||||
"param.input.base": "base game",
|
||||
"param.input.update": "update",
|
||||
"param.input.keys": "keys",
|
||||
|
||||
"launcher.title": "NX2navi Setup",
|
||||
"launcher.python_missing": "The setup files are incomplete: the python folder next to NX2navi-Setup.exe is missing. Unpack the whole archive again and start NX2navi-Setup.exe from the unpacked folder.",
|
||||
|
||||
"lang.ja": "Japanese",
|
||||
"lang.en-US": "English (US)",
|
||||
"lang.en-GB": "English (UK)",
|
||||
"lang.fr": "French",
|
||||
"lang.fr-CA": "French (Canada)",
|
||||
"lang.de": "German",
|
||||
"lang.it": "Italian",
|
||||
"lang.es": "Spanish",
|
||||
"lang.es-419": "Spanish (Latin America)",
|
||||
"lang.nl": "Dutch",
|
||||
"lang.pt": "Portuguese",
|
||||
"lang.pt-BR": "Portuguese (Brazil)",
|
||||
"lang.ru": "Russian",
|
||||
"lang.ko": "Korean",
|
||||
"lang.zh-Hans": "Chinese (Simplified)",
|
||||
"lang.zh-Hant": "Chinese (Traditional)"
|
||||
}
|
||||
208
installer/text/pt-BR.json
Normal file
208
installer/text/pt-BR.json
Normal file
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"step.check": "Verificando o computador e os arquivos do jogo",
|
||||
"step.fonts": "Extraindo as fontes do sistema",
|
||||
"step.extract": "Extraindo o jogo",
|
||||
"step.lift": "Traduzindo o código do jogo para C",
|
||||
"step.compile": "Compilando o {game_exe}",
|
||||
"step.prepare": "Preparando os shaders",
|
||||
"step.dlss": "Baixando o NVIDIA DLSS",
|
||||
"step.cleanup": "Finalizando",
|
||||
|
||||
"phase.check.host": "Verificando o processador e a placa de vídeo",
|
||||
"phase.check.verify": "Verificando os arquivos já instalados",
|
||||
"phase.check.verify_romfs": "Verificando os dados do jogo instalados",
|
||||
"phase.check.inputs": "Abrindo os arquivos do jogo com as chaves",
|
||||
"phase.check.unpack_base": "Descompactando o jogo base compactado",
|
||||
"phase.check.unpack_update": "Descompactando a atualização compactada",
|
||||
"phase.check.versions": "Verificando as versões do jogo",
|
||||
"phase.fonts.extract": "Extraindo as fontes compartilhadas do firmware",
|
||||
"phase.fonts.copy": "Copiando as fontes compartilhadas para a pasta de instalação",
|
||||
"phase.extract.hash_base": "Calculando o checksum do jogo base",
|
||||
"phase.extract.hash_update": "Calculando o checksum da atualização",
|
||||
"phase.extract.exefs": "Extraindo o código do jogo",
|
||||
"phase.extract.copy": "Copiando os dados do jogo",
|
||||
"phase.extract.merge": "Aplicando a atualização aos dados do jogo",
|
||||
"phase.extract.diagnose": "Procurando o arquivo do jogo danificado",
|
||||
"phase.extract.index": "Verificando os dados do jogo",
|
||||
"phase.lift.discover": "Encontrando as funções",
|
||||
"phase.lift.emit": "Gerando o código C",
|
||||
"phase.compile.compile": "Compilando",
|
||||
"phase.compile.link": "Ligando o {game_exe}",
|
||||
"phase.prepare.romfs_patches": "Gravando os patches de 60 fps nos dados do jogo",
|
||||
"phase.prepare.seed": "Lendo os shaders dos dados do jogo",
|
||||
"phase.prepare.translate": "Traduzindo os shaders",
|
||||
"phase.prepare.libraries": "Montando os shaders para o driver de vídeo",
|
||||
"phase.prepare.pack": "Juntando o cache de shaders num único arquivo",
|
||||
"phase.dlss.download": "Baixando o nvngx_dlss.dll do DLSS SDK da NVIDIA",
|
||||
"phase.cleanup.ini": "Gravando o nx2navi.ini",
|
||||
"phase.cleanup.temp": "Removendo os arquivos temporários",
|
||||
|
||||
"cli.usage": "Linha de comando inválida: {detail}",
|
||||
"internal": "Erro inesperado ({exception}): {detail}",
|
||||
"state.unreadable": "Não foi possível ler {path}. Escolha outra pasta ou apague esse arquivo.",
|
||||
"state.newer_format": "{path} foi gravado por um instalador mais novo. Use esse instalador ou outra pasta.",
|
||||
"state.output_changed": "Os arquivos instalados da etapa '{step}' mudaram desde que foram criados; essa etapa será executada de novo.",
|
||||
|
||||
"check.release_incomplete": "A pasta do instalador está incompleta; faltam: {paths}. Extraia o pacote de novo.",
|
||||
"check.dest_not_folder": "O local de instalação {path} é um arquivo, não uma pasta.",
|
||||
"check.dest_not_empty": "A pasta de instalação {path} não está vazia e não tem uma instalação anterior. Escolha uma pasta vazia.",
|
||||
"check.lang_missing": "Escolha o idioma do jogo.",
|
||||
"check.lang_unknown": "Idioma desconhecido '{lang}'. Conhecidos: {known}.",
|
||||
"check.toolchain_broken": "O compilador incluído não funciona: {detail}",
|
||||
"check.glibc_old": "A biblioteca C deste sistema é a glibc {version}; o compilador da instalação precisa da {need} ou mais nova (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 e mais novos têm). Atualizar o sistema resolve.",
|
||||
"check.host_probe_failed": "A verificação do computador não rodou (código de saída {code}).",
|
||||
"check.cpu_too_old": "Este processador não tem x86-64-v2 (SSE4.2, POPCNT e CMPXCHG16B), o nível mais antigo para o qual o jogo é compilado. Intel Nehalem (2008) ou AMD Bulldozer (2011) e mais novos têm.",
|
||||
"check.vulkan_missing": "Nenhum driver Vulkan está instalado. Instale o driver mais recente da sua placa de vídeo.",
|
||||
"check.vulkan_no_device": "O Vulkan não encontra nenhuma placa de vídeo. Instale o driver mais recente da sua placa de vídeo.",
|
||||
"check.vulkan_too_old": "O jogo precisa de uma placa de vídeo com Vulkan 1.3; encontrado: {devices}. Atualize o driver de vídeo.",
|
||||
"check.no_display": "Preparar os shaders exige uma tela. Execute a instalação na sessão da área de trabalho ou use export DISPLAY=:0 (no Steam Deck em Game Mode, também XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "Os arquivos do jogo são necessários: escolha o jogo base, a atualização 1.1.0, 1.2.1 ou 1.4.3 e o prod.keys.",
|
||||
"check.input_missing": "Arquivo não encontrado ({input}): {path}",
|
||||
"check.keys_unreadable": "Não foi possível ler o arquivo de chaves. Escolha um arquivo prod.keys.",
|
||||
"check.keys_no_header_key": "O arquivo de chaves não tem header_key. Extraia o prod.keys do seu console de novo.",
|
||||
"check.keys_wrong": "As chaves não abrem o arquivo de {input}. Use o prod.keys do seu console.",
|
||||
"check.keys_too_old": "O arquivo de chaves é antigo demais para o arquivo de {input} (precisa da geração de chaves {generation}). Extraia o prod.keys de um console com firmware mais novo.",
|
||||
"check.not_game_file": "O arquivo de {input} não é um arquivo de jogo XCI, NSP, XCZ ou NSZ.",
|
||||
"check.file_unreadable": "Não foi possível ler o arquivo de {input}: {detail}",
|
||||
"check.unpack_failed": "Não foi possível descompactar o arquivo compactado de {input}: {detail}",
|
||||
"check.ticket_personalized": "O arquivo de {input} tem um ticket personalizado; é preciso um ticket comum.",
|
||||
"check.ticket_missing": "O arquivo de {input} não tem o ticket de que o conteúdo precisa.",
|
||||
"check.base_not_nx2navi": "O arquivo do jogo base não é The Legend of Zelda: Ocarina of Time ({detail}).",
|
||||
"check.base_is_update": "O arquivo do jogo base é uma atualização. Escolha aqui o NSP ou XCI do jogo base (versão 1.0.0) e este arquivo como a atualização.",
|
||||
"check.base_combined": "O arquivo do jogo base contém vários pacotes (jogo base, atualização ou DLC em um só arquivo). Escolha um dump do jogo base que contenha só o jogo.",
|
||||
"check.update_not_nsp": "A atualização precisa ser um arquivo NSP ou NSZ.",
|
||||
"check.update_is_base": "O arquivo de atualização contém um jogo base. Escolha o NSP da atualização 1.1.0, 1.2.1 ou 1.4.3.",
|
||||
"check.update_not_nx2navi": "O arquivo de atualização não é uma atualização de Ocarina of Time ({detail}).",
|
||||
"check.exefs_failed": "O código do jogo no arquivo de {input} falhou nas verificações; veja {log}.",
|
||||
"check.input_damaged": "O arquivo de {input} está danificado ou incompleto: {path} ({detail}). Copie esse arquivo de novo da origem, ou extraia-o de novo, e então execute a instalação outra vez.",
|
||||
"check.base_wrong_version": "O jogo base não é a versão 1.0.0 ({modules} diferem).",
|
||||
"check.update_wrong_version": "A atualização é a versão {found}; esta versão do instalador aceita {wanted}.",
|
||||
"check.disk_space": "Espaço livre insuficiente em {path}: são necessários {need_gb} GB, há {free_gb} GB livres.",
|
||||
"check.firmware_missing": "A pasta de firmware {path} não existe. Escolha a pasta Contents/registered do firmware.",
|
||||
"check.firmware_empty": "A pasta de firmware {path} não contém arquivos NCA. Escolha a pasta Contents/registered do firmware.",
|
||||
"check.fonts_keys_needed": "As fontes compartilhadas deste idioma ainda não estão instaladas; escolha o prod.keys para que a instalação possa extraí-las do firmware.",
|
||||
|
||||
"extract.exefs_mismatch": "O código do jogo extraído não corresponde à versão {version} ({modules}).",
|
||||
"extract.romfs_merge_failed": "Falha ao aplicar a atualização aos dados do jogo: {detail}. Detalhes: {log}",
|
||||
"extract.romfs_mismatch": "Os dados do jogo extraídos não correspondem à versão {version} (tamanho {size}, checksum {sha256}...).",
|
||||
"fonts.failed": "A extração das fontes compartilhadas falhou (código de saída {code}): {detail}. Detalhes: {log}",
|
||||
"fonts.incomplete": "A extração das fontes compartilhadas não gravou todos os arquivos ({files}).",
|
||||
"fonts.copy_failed": "Não foi possível copiar as fontes compartilhadas para {path}: {detail}",
|
||||
"lift.failed": "Falha ao traduzir o código do jogo (código de saída {code}). Detalhes: {log}",
|
||||
"compile.failed": "Falha ao compilar o {game_exe} (código de saída {code}). Detalhes: {log}",
|
||||
"compile.exe_in_use": "{path} está em uso. Feche o jogo e execute o instalador de novo.",
|
||||
"prepare.xlate_in_use": "{path} está em uso. Feche o jogo, encerre qualquer processo nx2navi_xlate que tenha sobrado de uma execução anterior e execute o instalador de novo.",
|
||||
"prepare.shader_list_missing": "A lista de shaders está faltando.",
|
||||
"prepare.romfs_patches_failed": "Falha ao gravar os patches nos dados do jogo (código de saída {code}). Detalhes: {log}",
|
||||
"prepare.shaders_failed": "Falha ao preparar os shaders (código de saída {code}). Detalhes: {log}",
|
||||
"cleanup.romfs_unreadable": "Os dados do jogo em {path} estão faltando ou não podem ser lidos depois da instalação. Reinicie o Windows e execute o instalador de novo na mesma pasta: ele extrai os dados do jogo outra vez e mantém o resto.",
|
||||
"prepare.pack_failed": "Falha ao juntar o cache de shaders (código de saída {code}). Detalhes: {log}",
|
||||
"cleanup.ini_failed": "Não foi possível gravar o nx2navi.ini: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Textos em {lang} precisam das fontes de sistema do Switch, e nenhuma foi informada. O jogo não inicia neste idioma até que sejam adicionadas. Escolha a pasta do firmware (Contents/registered) ou veja \"Switch system fonts\" em docs/installer.md.",
|
||||
"warn.cheats_skipped": "Não foi possível compilar os cheats da pasta de mods que alteram o código do jogo; o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.exefs_skipped": "Não foi possível compilar os patches ExeFS dos mods (.ips, .pchtxt); o jogo é instalado sem eles. Detalhes: {log}",
|
||||
"warn.npdm_differs": "O main.npdm difere do dump de referência da versão {version}; só os módulos de código são usados.",
|
||||
"warn.xbox_prompts_failed": "Não foi possível preparar os ícones de botão do Xbox ou do PC (código de saída {code}); o jogo mantém os ícones do Switch. Detalhes: {log}",
|
||||
"warn.shaders_skipped": "O driver gráfico travou ao compilar alguns shaders, então {count} ficaram fora da preparação. O jogo os compila quando precisar deles pela primeira vez, e o driver pode travar ali também; uma atualização do driver pode resolver. Detalhes: {log}",
|
||||
"warn.kernel_old": "O Linux {version} não tem o rastreamento de escrita de que o jogo depende (6.7 ou mais novo); o jogo pode rodar muito devagar. Atualizar o sistema resolve.",
|
||||
"warn.dlss_kept": "Já existe uma biblioteca DLSS na pasta de instalação ({path}); a instalação a mantém.",
|
||||
"warn.dlss_download_failed": "Não foi possível baixar o NVIDIA DLSS de {url}: {detail}. O jogo funciona sem ele; a próxima execução da instalação tenta de novo.",
|
||||
|
||||
"ui.title": "Instalação do NX2navi",
|
||||
"ui.rebuild_title": "Recompilando o jogo para os seus mods",
|
||||
"ui.start_game": "Iniciar o jogo",
|
||||
"ui.version": "versão {version}",
|
||||
"ui.intro": "Monta uma pasta jogável de Ocarina of Time a partir dos seus próprios arquivos do jogo. Isso demora um pouco e só precisa ser feito uma vez.",
|
||||
"ui.notice": "A pasta que esta instalação monta contém o jogo. Ela é só para seu uso pessoal; não compartilhe nem envie para a internet.",
|
||||
"ui.keys_private": "O arquivo de chaves é lido no lugar. O conteúdo dele nunca é mostrado, copiado nem guardado.",
|
||||
"ui.base": "Jogo base (NSP, XCI, NSZ ou XCZ)",
|
||||
"ui.update": "Atualização 1.1.0, 1.2.1 ou 1.4.3 (NSP ou NSZ)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "Pasta de firmware (opcional)",
|
||||
"ui.game_language": "Idioma do jogo",
|
||||
"ui.dest": "Pasta de instalação",
|
||||
"ui.browse": "Procurar...",
|
||||
"ui.pick_game": "Escolha o arquivo do jogo base",
|
||||
"ui.pick_update": "Escolha o arquivo da atualização 1.1.0, 1.2.1 ou 1.4.3",
|
||||
"ui.pick_keys": "Escolha o prod.keys",
|
||||
"ui.pick_firmware": "Escolha a pasta de firmware do Switch (Contents/registered)",
|
||||
"ui.pick_dest": "Escolha a pasta de instalação",
|
||||
"ui.firmware_help": "Para textos em chinês, coreano e chinês tradicional: a pasta Contents/registered do firmware, por exemplo do Ryujinx ou do The hardware. A instalação copia dela as fontes do sistema do Switch para a pasta de instalação.",
|
||||
"ui.dlss": "Baixar o NVIDIA DLSS (nvngx_dlss.dll) para placas de vídeo NVIDIA RTX",
|
||||
"ui.dlss_help": "Do DLSS SDK da NVIDIA no GitHub, sob a licença NVIDIA RTX SDK (github.com/NVIDIA/DLSS). Sem ele as opções dlaa e DLSS não fazem nada.",
|
||||
"ui.compressed_help": "Arquivos compactados NSZ e XCZ funcionam do jeito que estão: a instalação os descompacta sozinha (para isso, até os dados do jogo serem extraídos, ela precisa de tanto espaço livre quanto ocupam os arquivos descompactados).",
|
||||
"ui.type_game": "Arquivos de jogo (NSP, XCI, NSZ, XCZ)",
|
||||
"ui.type_keys": "Arquivos de chaves (*.keys)",
|
||||
"ui.type_all": "Todos os arquivos",
|
||||
"ui.start": "Iniciar",
|
||||
"ui.cancel": "Cancelar",
|
||||
"ui.cancelling": "Cancelando...",
|
||||
"ui.show_log": "Mostrar log",
|
||||
"ui.hide_log": "Ocultar log",
|
||||
"ui.close": "Fechar",
|
||||
"ui.back": "Voltar",
|
||||
"ui.open_folder": "Abrir pasta",
|
||||
"ui.update_mode": "Esta pasta tem uma instalação anterior. A instalação vai continuar ou atualizá-la; os arquivos do jogo só são pedidos se forem necessários de novo.",
|
||||
"ui.need_dest": "Escolha a pasta de instalação.",
|
||||
"ui.need_base": "Escolha o arquivo do jogo base.",
|
||||
"ui.need_update": "Escolha o arquivo da atualização 1.1.0, 1.2.1 ou 1.4.3.",
|
||||
"ui.need_keys": "Escolha o arquivo prod.keys.",
|
||||
"ui.need_lang": "Escolha o idioma do jogo.",
|
||||
"ui.missing_base": "O arquivo do jogo base não existe.",
|
||||
"ui.missing_update": "O arquivo de atualização não existe.",
|
||||
"ui.missing_keys": "O arquivo de chaves não existe.",
|
||||
"ui.missing_firmware": "A pasta de firmware não existe.",
|
||||
"ui.running_install": "Instalando",
|
||||
"ui.running_resume": "Continuando a instalação anterior",
|
||||
"ui.running_update": "Atualizando a instalação",
|
||||
"ui.state_started": "iniciada",
|
||||
"ui.state_done": "concluída",
|
||||
"ui.state_skipped": "pulada",
|
||||
"ui.state_pending": "aguardando",
|
||||
"ui.state_running": "em andamento",
|
||||
"ui.state_failed": "falhou",
|
||||
"ui.log_warning": "Aviso",
|
||||
"ui.log_error": "Erro",
|
||||
"ui.log_space": "Espaço necessário: {need} GB, livre: {free} GB",
|
||||
"ui.log_end": "Concluído",
|
||||
"ui.result_ok": "O jogo está pronto",
|
||||
"ui.result_failed": "A instalação falhou",
|
||||
"ui.result_cancelled": "A instalação foi cancelada",
|
||||
"ui.done_text": "Abra o {game_exe} em {dest}. O programa de instalação e os arquivos do jogo que você escolheu não são mais necessários.",
|
||||
"ui.cancelled_text": "As etapas concluídas são mantidas. Inicie a instalação de novo com a mesma pasta para continuar.",
|
||||
"ui.log_file": "Arquivo de log: {path}",
|
||||
"ui.process_died": "A instalação parou de forma inesperada (código de saída {code}).",
|
||||
"ui.confirm_cancel": "A instalação está em andamento. Cancelar e fechar a janela?",
|
||||
"ui.space_one": "Espaço livre necessário em {path}: {need} GB (livre: {free} GB).",
|
||||
"ui.space_two": "Espaço livre necessário em {path}: {need} GB (livre: {free} GB); em {temp}: {temp_need} GB (livre: {temp_free} GB).",
|
||||
"ui.amount_bytes": "{done} / {total} GB",
|
||||
"ui.amount_chunks": "{done} / {total} partes",
|
||||
"ui.amount_objects": "{done} / {total} arquivos",
|
||||
"ui.amount_shaders": "{done} / {total} shaders",
|
||||
"ui.amount_other": "{done} / {total} {unit}",
|
||||
"ui.seconds": "{n} s",
|
||||
"param.input.base": "jogo base",
|
||||
"param.input.update": "atualização",
|
||||
"param.input.keys": "chaves",
|
||||
|
||||
"launcher.title": "Instalação do NX2navi",
|
||||
"launcher.python_missing": "Os arquivos de instalação estão incompletos: a pasta python ao lado do NX2navi-Setup.exe não existe. Extraia o arquivo compactado inteiro de novo e abra o NX2navi-Setup.exe a partir da pasta extraída.",
|
||||
|
||||
"lang.ja": "Japonês",
|
||||
"lang.en-US": "Inglês (EUA)",
|
||||
"lang.en-GB": "Inglês (Reino Unido)",
|
||||
"lang.fr": "Francês",
|
||||
"lang.fr-CA": "Francês (Canadá)",
|
||||
"lang.de": "Alemão",
|
||||
"lang.it": "Italiano",
|
||||
"lang.es": "Espanhol",
|
||||
"lang.es-419": "Espanhol (América Latina)",
|
||||
"lang.nl": "Holandês",
|
||||
"lang.pt": "Português",
|
||||
"lang.pt-BR": "Português (Brasil)",
|
||||
"lang.ru": "Russo",
|
||||
"lang.ko": "Coreano",
|
||||
"lang.zh-Hans": "Chinês (Simplificado)",
|
||||
"lang.zh-Hant": "Chinês (Tradicional)"
|
||||
}
|
||||
208
installer/text/ru.json
Normal file
208
installer/text/ru.json
Normal file
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"step.check": "Проверка компьютера и игровых файлов",
|
||||
"step.fonts": "Извлечение системных шрифтов",
|
||||
"step.extract": "Распаковка игры",
|
||||
"step.lift": "Перевод кода игры в C",
|
||||
"step.compile": "Сборка {game_exe}",
|
||||
"step.prepare": "Подготовка шейдеров",
|
||||
"step.dlss": "Загрузка NVIDIA DLSS",
|
||||
"step.cleanup": "Уборка",
|
||||
|
||||
"phase.check.host": "Проверка процессора и видеокарты",
|
||||
"phase.check.verify": "Проверка уже установленных файлов",
|
||||
"phase.check.verify_romfs": "Проверка установленных данных игры",
|
||||
"phase.check.inputs": "Открытие игровых файлов с помощью ключей",
|
||||
"phase.check.unpack_base": "Распаковка сжатой базовой игры",
|
||||
"phase.check.unpack_update": "Распаковка сжатого обновления",
|
||||
"phase.check.versions": "Проверка версий игры",
|
||||
"phase.fonts.extract": "Извлечение общих шрифтов из прошивки",
|
||||
"phase.fonts.copy": "Копирование общих шрифтов в папку установки",
|
||||
"phase.extract.hash_base": "Подсчёт контрольной суммы базовой игры",
|
||||
"phase.extract.hash_update": "Подсчёт контрольной суммы обновления",
|
||||
"phase.extract.exefs": "Распаковка кода игры",
|
||||
"phase.extract.copy": "Копирование данных игры",
|
||||
"phase.extract.merge": "Применение обновления к данным игры",
|
||||
"phase.extract.diagnose": "Поиск повреждённого файла игры",
|
||||
"phase.extract.index": "Проверка данных игры",
|
||||
"phase.lift.discover": "Поиск функций",
|
||||
"phase.lift.emit": "Запись кода на C",
|
||||
"phase.compile.compile": "Компиляция",
|
||||
"phase.compile.link": "Компоновка {game_exe}",
|
||||
"phase.prepare.romfs_patches": "Запись патчей данных игры для 60 кадров/с",
|
||||
"phase.prepare.seed": "Чтение шейдеров из данных игры",
|
||||
"phase.prepare.translate": "Трансляция шейдеров",
|
||||
"phase.prepare.libraries": "Сборка шейдеров для драйвера видеокарты",
|
||||
"phase.prepare.pack": "Упаковка кэша шейдеров в один файл",
|
||||
"phase.dlss.download": "Загрузка nvngx_dlss.dll из DLSS SDK от NVIDIA",
|
||||
"phase.cleanup.ini": "Запись nx2navi.ini",
|
||||
"phase.cleanup.temp": "Удаление временных файлов",
|
||||
|
||||
"cli.usage": "Неверные параметры командной строки: {detail}",
|
||||
"internal": "Непредвиденная ошибка ({exception}): {detail}",
|
||||
"state.unreadable": "Не удаётся прочитать {path}. Выберите другую папку или удалите этот файл.",
|
||||
"state.newer_format": "Файл {path} создан более новой версией установки. Используйте её или выберите другую папку.",
|
||||
"state.output_changed": "Установленные файлы шага «{step}» изменились после создания; этот шаг выполнится заново.",
|
||||
|
||||
"check.release_incomplete": "Папка установки неполная, не хватает: {paths}. Распакуйте архив заново.",
|
||||
"check.dest_not_folder": "Место установки {path} — это файл, а не папка.",
|
||||
"check.dest_not_empty": "Папка установки {path} не пуста и не содержит прежней установки. Выберите пустую папку.",
|
||||
"check.lang_missing": "Выберите язык игры.",
|
||||
"check.lang_unknown": "Неизвестный язык «{lang}». Доступные: {known}.",
|
||||
"check.toolchain_broken": "Встроенный компилятор не работает: {detail}",
|
||||
"check.glibc_old": "Библиотека C этой системы — glibc {version}; компилятору установщика нужна {need} или новее (она есть в Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 и новее). Обновление системы это исправит.",
|
||||
"check.host_probe_failed": "Проверка компьютера не выполнилась (код выхода {code}).",
|
||||
"check.cpu_too_old": "Этому процессору не хватает x86-64-v2 (SSE4.2, POPCNT и CMPXCHG16B) — минимального уровня, под который собрана игра. Он есть у Intel Nehalem (2008) и AMD Bulldozer (2011) и новее.",
|
||||
"check.vulkan_missing": "Драйвер Vulkan не установлен. Установите свежий драйвер видеокарты.",
|
||||
"check.vulkan_no_device": "Vulkan не находит видеокарту. Установите свежий драйвер видеокарты.",
|
||||
"check.vulkan_too_old": "Игре нужна видеокарта с Vulkan 1.3; найдено: {devices}. Обновите драйвер видеокарты.",
|
||||
"check.no_display": "Для подготовки шейдеров нужен дисплей. Запустите установку из сеанса рабочего стола или выполните export DISPLAY=:0 (на Steam Deck в игровом режиме ещё XDG_RUNTIME_DIR=/run/user/1000).",
|
||||
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.0 или 1.2.1 или 1.4.3 и prod.keys.",
|
||||
"check.input_missing": "Файл не найден ({input}): {path}",
|
||||
"check.keys_unreadable": "Не удаётся прочитать файл ключей. Выберите файл prod.keys.",
|
||||
"check.keys_no_header_key": "В файле ключей нет header_key. Снова получите prod.keys со своей консоли.",
|
||||
"check.keys_wrong": "Эти ключи не открывают файл «{input}». Используйте prod.keys со своей консоли.",
|
||||
"check.keys_too_old": "Файл ключей слишком старый для файла «{input}» (нужно поколение ключей {generation}). Получите prod.keys с консоли с более новой прошивкой.",
|
||||
"check.not_game_file": "Файл «{input}» не является игровым файлом XCI, NSP, XCZ или NSZ.",
|
||||
"check.file_unreadable": "Не удаётся прочитать файл «{input}»: {detail}",
|
||||
"check.unpack_failed": "Не удалось распаковать сжатый файл «{input}»: {detail}",
|
||||
"check.ticket_personalized": "В файле «{input}» личный тикет; нужен общий тикет.",
|
||||
"check.ticket_missing": "В файле «{input}» нет тикета, который нужен его содержимому.",
|
||||
"check.base_not_nx2navi": "Файл базовой игры — не The Legend of Zelda: Ocarina of Time ({detail}).",
|
||||
"check.base_is_update": "Файл базовой игры — это обновление. Выберите здесь NSP или XCI базовой игры (версия 1.0.0), а этот файл укажите как обновление.",
|
||||
"check.base_combined": "Файл базовой игры содержит несколько пакетов (игра, обновление или DLC в одном файле). Выберите дамп, в котором только базовая игра.",
|
||||
"check.update_not_nsp": "Обновление должно быть файлом NSP или NSZ.",
|
||||
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.1.0 или 1.2.1 или 1.4.3.",
|
||||
"check.update_not_nx2navi": "Файл обновления — не обновление Ocarina of Time ({detail}).",
|
||||
"check.exefs_failed": "Код игры из файла «{input}» не прошёл проверку; подробности в {log}.",
|
||||
"check.input_damaged": "Файл «{input}» повреждён или скопирован не полностью: {path} ({detail}). Скопируйте этот файл заново из источника или сделайте дамп ещё раз, затем снова запустите установку.",
|
||||
"check.base_wrong_version": "Базовая игра не версии 1.0.0 (отличаются модули: {modules}).",
|
||||
"check.update_wrong_version": "Версия обновления {found}; этот выпуск принимает {wanted}.",
|
||||
"check.disk_space": "Не хватает свободного места для {path}: нужно {need_gb} ГБ, свободно {free_gb} ГБ.",
|
||||
"check.firmware_missing": "Папки прошивки {path} нет. Выберите папку Contents/registered прошивки.",
|
||||
"check.firmware_empty": "В папке прошивки {path} нет файлов NCA. Выберите папку Contents/registered прошивки.",
|
||||
"check.fonts_keys_needed": "Общие шрифты для этого языка ещё не установлены; выберите prod.keys, чтобы установка извлекла их из прошивки.",
|
||||
|
||||
"extract.exefs_mismatch": "Распакованный код игры не соответствует версии {version} ({modules}).",
|
||||
"extract.romfs_merge_failed": "Не удалось применить обновление к данным игры: {detail}. Подробности: {log}",
|
||||
"extract.romfs_mismatch": "Распакованные данные игры не соответствуют версии {version} (размер {size}, контрольная сумма {sha256}...).",
|
||||
"fonts.failed": "Не удалось извлечь общие шрифты (код выхода {code}): {detail}. Подробности: {log}",
|
||||
"fonts.incomplete": "При извлечении общих шрифтов записаны не все файлы ({files}).",
|
||||
"fonts.copy_failed": "Не удаётся скопировать общие шрифты в {path}: {detail}",
|
||||
"lift.failed": "Не удалось перевести код игры (код выхода {code}). Подробности: {log}",
|
||||
"compile.failed": "Не удалось собрать {game_exe} (код выхода {code}). Подробности: {log}",
|
||||
"compile.exe_in_use": "Файл {path} занят. Закройте игру и запустите установку снова.",
|
||||
"prepare.xlate_in_use": "Файл {path} занят. Закройте игру, завершите процесс nx2navi_xlate, оставшийся от прошлого запуска, и запустите установку снова.",
|
||||
"prepare.shader_list_missing": "Нет списка шейдеров.",
|
||||
"prepare.romfs_patches_failed": "Не удалось записать патчи данных игры (код выхода {code}). Подробности: {log}",
|
||||
"prepare.shaders_failed": "Не удалось подготовить шейдеры (код выхода {code}). Подробности: {log}",
|
||||
"cleanup.romfs_unreadable": "Данные игры в {path} пропали или не читаются после установки. Перезагрузите Windows и запустите установщик снова на ту же папку: он заново распакует данные игры, остальное сохранится.",
|
||||
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
|
||||
"cleanup.ini_failed": "Не удаётся записать nx2navi.ini: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, а они не указаны. Игра не запустится на этом языке, пока их не добавят. Выберите папку прошивки (Contents/registered) или см. раздел «Switch system fonts» в docs/installer.md.",
|
||||
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
|
||||
"warn.cheats_skipped": "Не удалось собрать читы из папки модов, меняющие код игры; игра установится без них. Подробности: {log}",
|
||||
"warn.exefs_skipped": "Не удалось собрать ExeFS-патчи модов (.ips, .pchtxt); игра установится без них. Подробности: {log}",
|
||||
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox или PC (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
|
||||
"warn.shaders_skipped": "Графический драйвер аварийно завершился при сборке части шейдеров, поэтому {count} из них пропущены при подготовке. Игра соберёт их, когда они впервые понадобятся, и драйвер может упасть и там; обновление драйвера может это исправить. Подробности: {log}",
|
||||
"warn.kernel_old": "В Linux {version} нет отслеживания записи, на которое опирается игра (нужна версия 6.7 или новее); игра может работать очень медленно. Обновление системы это исправит.",
|
||||
"warn.dlss_kept": "В папке установки уже есть библиотека DLSS ({path}); установка её оставляет.",
|
||||
"warn.dlss_download_failed": "Не удалось загрузить NVIDIA DLSS с {url}: {detail}. Игра работает и без него; при следующем запуске установка попробует снова.",
|
||||
|
||||
"ui.title": "Установка NX2navi",
|
||||
"ui.rebuild_title": "Пересборка игры под ваши моды",
|
||||
"ui.start_game": "Запустить игру",
|
||||
"ui.version": "версия {version}",
|
||||
"ui.intro": "Собирает готовую к запуску папку с Ocarina of Time из ваших игровых файлов. Занимает много времени и выполняется один раз.",
|
||||
"ui.notice": "Папка, которую создаёт установка, содержит игру. Она предназначена только для личного использования: не передавайте и не выкладывайте её.",
|
||||
"ui.keys_private": "Файл ключей читается на месте. Его содержимое не показывается, не копируется и не сохраняется.",
|
||||
"ui.base": "Базовая игра (NSP, XCI, NSZ или XCZ)",
|
||||
"ui.update": "Обновление 1.1.0 или 1.2.1 или 1.4.3 (NSP или NSZ)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "Папка прошивки (необязательно)",
|
||||
"ui.game_language": "Язык игры",
|
||||
"ui.dest": "Папка установки",
|
||||
"ui.browse": "Обзор...",
|
||||
"ui.pick_game": "Выберите файл базовой игры",
|
||||
"ui.pick_update": "Выберите файл обновления 1.1.0 или 1.2.1 или 1.4.3",
|
||||
"ui.pick_keys": "Выберите prod.keys",
|
||||
"ui.pick_firmware": "Выберите папку прошивки Switch (Contents/registered)",
|
||||
"ui.pick_dest": "Выберите папку установки",
|
||||
"ui.firmware_help": "Для текста на китайском и корейском: папка Contents/registered прошивки, например из Ryujinx или The hardware. Установка скопирует из неё системные шрифты Switch в папку установки.",
|
||||
"ui.dlss": "Загрузить NVIDIA DLSS (nvngx_dlss.dll) для видеокарт NVIDIA RTX",
|
||||
"ui.dlss_help": "Из DLSS SDK от NVIDIA на GitHub, по лицензии NVIDIA RTX SDK (github.com/NVIDIA/DLSS). Без него настройки dlaa и DLSS ничего не делают.",
|
||||
"ui.compressed_help": "Сжатые файлы NSZ и XCZ подходят как есть: установка сама их распакует (для этого, пока извлекаются данные игры, нужно столько свободного места, сколько занимают распакованные файлы).",
|
||||
"ui.type_game": "Файлы игры (NSP, XCI, NSZ, XCZ)",
|
||||
"ui.type_keys": "Файлы ключей (*.keys)",
|
||||
"ui.type_all": "Все файлы",
|
||||
"ui.start": "Начать",
|
||||
"ui.cancel": "Отмена",
|
||||
"ui.cancelling": "Отмена...",
|
||||
"ui.show_log": "Показать журнал",
|
||||
"ui.hide_log": "Скрыть журнал",
|
||||
"ui.close": "Закрыть",
|
||||
"ui.back": "Назад",
|
||||
"ui.open_folder": "Открыть папку",
|
||||
"ui.update_mode": "В этой папке уже есть установка. Программа продолжит или обновит её; файлы игры понадобятся, только если их снова придётся читать.",
|
||||
"ui.need_dest": "Выберите папку установки.",
|
||||
"ui.need_base": "Выберите файл базовой игры.",
|
||||
"ui.need_update": "Выберите файл обновления 1.1.0 или 1.2.1 или 1.4.3.",
|
||||
"ui.need_keys": "Выберите файл prod.keys.",
|
||||
"ui.need_lang": "Выберите язык игры.",
|
||||
"ui.missing_base": "Файл базовой игры не найден.",
|
||||
"ui.missing_update": "Файл обновления не найден.",
|
||||
"ui.missing_keys": "Файл ключей не найден.",
|
||||
"ui.missing_firmware": "Папки прошивки нет.",
|
||||
"ui.running_install": "Установка",
|
||||
"ui.running_resume": "Продолжение прерванной установки",
|
||||
"ui.running_update": "Обновление установки",
|
||||
"ui.state_started": "начат",
|
||||
"ui.state_done": "готово",
|
||||
"ui.state_skipped": "пропущен",
|
||||
"ui.state_pending": "ожидает",
|
||||
"ui.state_running": "выполняется",
|
||||
"ui.state_failed": "ошибка",
|
||||
"ui.log_warning": "Предупреждение",
|
||||
"ui.log_error": "Ошибка",
|
||||
"ui.log_space": "Нужно места: {need} ГБ, свободно: {free} ГБ",
|
||||
"ui.log_end": "Завершено",
|
||||
"ui.result_ok": "Игра готова",
|
||||
"ui.result_failed": "Установка не удалась",
|
||||
"ui.result_cancelled": "Установка отменена",
|
||||
"ui.done_text": "Запустите {game_exe} из папки {dest}. Программа установки и выбранные игровые файлы больше не нужны.",
|
||||
"ui.cancelled_text": "Завершённые шаги сохранены. Чтобы продолжить, снова запустите установку с той же папкой.",
|
||||
"ui.log_file": "Файл журнала: {path}",
|
||||
"ui.process_died": "Установка неожиданно остановилась (код выхода {code}).",
|
||||
"ui.confirm_cancel": "Установка выполняется. Отменить её и закрыть окно?",
|
||||
"ui.space_one": "Нужно свободного места на {path}: {need} ГБ (свободно: {free} ГБ).",
|
||||
"ui.space_two": "Нужно свободного места на {path}: {need} ГБ (свободно: {free} ГБ); на {temp}: {temp_need} ГБ (свободно: {temp_free} ГБ).",
|
||||
"ui.amount_bytes": "{done} / {total} ГБ",
|
||||
"ui.amount_chunks": "{done} / {total} частей",
|
||||
"ui.amount_objects": "{done} / {total} файлов",
|
||||
"ui.amount_shaders": "{done} / {total} шейдеров",
|
||||
"ui.amount_other": "{done} / {total} {unit}",
|
||||
"ui.seconds": "{n} с",
|
||||
"param.input.base": "базовая игра",
|
||||
"param.input.update": "обновление",
|
||||
"param.input.keys": "ключи",
|
||||
|
||||
"launcher.title": "Установка NX2navi",
|
||||
"launcher.python_missing": "Файлы установки неполные: рядом с NX2navi-Setup.exe нет папки python. Распакуйте архив целиком заново и запустите NX2navi-Setup.exe из распакованной папки.",
|
||||
|
||||
"lang.ja": "Японский",
|
||||
"lang.en-US": "Английский (США)",
|
||||
"lang.en-GB": "Английский (Великобритания)",
|
||||
"lang.fr": "Французский",
|
||||
"lang.fr-CA": "Французский (Канада)",
|
||||
"lang.de": "Немецкий",
|
||||
"lang.it": "Итальянский",
|
||||
"lang.es": "Испанский",
|
||||
"lang.es-419": "Испанский (Латинская Америка)",
|
||||
"lang.nl": "Нидерландский",
|
||||
"lang.pt": "Португальский",
|
||||
"lang.pt-BR": "Португальский (Бразилия)",
|
||||
"lang.ru": "Русский",
|
||||
"lang.ko": "Корейский",
|
||||
"lang.zh-Hans": "Китайский (упрощённый)",
|
||||
"lang.zh-Hant": "Китайский (традиционный)"
|
||||
}
|
||||
208
installer/text/zh-Hans.json
Normal file
208
installer/text/zh-Hans.json
Normal file
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"step.check": "检查电脑和游戏文件",
|
||||
"step.fonts": "正在提取系统字体",
|
||||
"step.extract": "解包游戏",
|
||||
"step.lift": "将游戏代码转换为 C 代码",
|
||||
"step.compile": "编译 {game_exe}",
|
||||
"step.prepare": "准备着色器",
|
||||
"step.dlss": "下载 NVIDIA DLSS",
|
||||
"step.cleanup": "清理",
|
||||
|
||||
"phase.check.host": "检查处理器和显卡",
|
||||
"phase.check.verify": "检查已安装的文件",
|
||||
"phase.check.verify_romfs": "检查已安装的游戏数据",
|
||||
"phase.check.inputs": "用密钥打开游戏文件",
|
||||
"phase.check.unpack_base": "正在解压压缩的游戏本体",
|
||||
"phase.check.unpack_update": "正在解压压缩的更新",
|
||||
"phase.check.versions": "检查游戏版本",
|
||||
"phase.fonts.extract": "正在从固件中提取共享字体",
|
||||
"phase.fonts.copy": "正在将共享字体复制到安装文件夹",
|
||||
"phase.extract.hash_base": "计算游戏本体的校验和",
|
||||
"phase.extract.hash_update": "计算更新的校验和",
|
||||
"phase.extract.exefs": "解包游戏代码",
|
||||
"phase.extract.copy": "复制游戏数据",
|
||||
"phase.extract.merge": "将更新应用到游戏数据",
|
||||
"phase.extract.diagnose": "查找损坏的游戏文件",
|
||||
"phase.extract.index": "校验游戏数据",
|
||||
"phase.lift.discover": "查找函数",
|
||||
"phase.lift.emit": "生成 C 代码",
|
||||
"phase.compile.compile": "编译",
|
||||
"phase.compile.link": "链接 {game_exe}",
|
||||
"phase.prepare.romfs_patches": "写入 60 帧游戏数据补丁",
|
||||
"phase.prepare.seed": "从游戏数据中读取着色器",
|
||||
"phase.prepare.translate": "转译着色器",
|
||||
"phase.prepare.libraries": "为显卡驱动构建着色器",
|
||||
"phase.prepare.pack": "将着色器缓存打包成一个文件",
|
||||
"phase.dlss.download": "从 NVIDIA 的 DLSS SDK 下载 nvngx_dlss.dll",
|
||||
"phase.cleanup.ini": "写入 nx2navi.ini",
|
||||
"phase.cleanup.temp": "删除临时文件",
|
||||
|
||||
"cli.usage": "命令行无效:{detail}",
|
||||
"internal": "意外错误({exception}):{detail}",
|
||||
"state.unreadable": "无法读取 {path}。请选择其他文件夹,或删除该文件。",
|
||||
"state.newer_format": "{path} 是由更新版本的安装程序写入的。请使用那个安装程序,或选择其他文件夹。",
|
||||
"state.output_changed": "步骤“{step}”生成的已安装文件在生成后被改动过;该步骤将重新运行。",
|
||||
|
||||
"check.release_incomplete": "安装程序文件夹不完整,缺少:{paths}。请重新解压发布包。",
|
||||
"check.dest_not_folder": "安装位置 {path} 是一个文件,而不是文件夹。",
|
||||
"check.dest_not_empty": "安装文件夹 {path} 不是空的,其中也没有之前的安装。请选择一个空文件夹。",
|
||||
"check.lang_missing": "请选择游戏语言。",
|
||||
"check.lang_unknown": "未知语言“{lang}”。可用语言:{known}。",
|
||||
"check.toolchain_broken": "自带的编译器无法工作:{detail}",
|
||||
"check.glibc_old": "此系统的 C 库为 glibc {version};安装程序的编译器需要 {need} 或更新版本(Ubuntu 22.04、Debian 12、Fedora 35、SteamOS 3 及更新版本都满足)。更新系统即可解决。",
|
||||
"check.host_probe_failed": "电脑检查未能运行(退出代码 {code})。",
|
||||
"check.cpu_too_old": "此处理器不支持 x86-64-v2(SSE4.2、POPCNT 和 CMPXCHG16B),这是游戏所需的最低指令集。Intel Nehalem(2008 年)或 AMD Bulldozer(2011 年)及更新的处理器都支持。",
|
||||
"check.vulkan_missing": "未安装 Vulkan 驱动。请为你的显卡安装最新的驱动程序。",
|
||||
"check.vulkan_no_device": "Vulkan 找不到显卡。请为你的显卡安装最新的驱动程序。",
|
||||
"check.vulkan_too_old": "游戏需要支持 Vulkan 1.3 的显卡;检测到:{devices}。请更新显卡驱动程序。",
|
||||
"check.no_display": "准备着色器需要显示器。请在桌面会话中运行安装程序,或执行 export DISPLAY=:0(Steam Deck 游戏模式下还需 XDG_RUNTIME_DIR=/run/user/1000)。",
|
||||
"check.inputs_needed": "需要游戏文件:请选择游戏本体、1.1.0、1.2.1 或 1.4.3 更新以及 prod.keys。",
|
||||
"check.input_missing": "找不到文件({input}):{path}",
|
||||
"check.keys_unreadable": "无法读取密钥文件。请选择一个 prod.keys 文件。",
|
||||
"check.keys_no_header_key": "密钥文件中没有 header_key。请从你的主机重新导出 prod.keys。",
|
||||
"check.keys_wrong": "这些密钥无法打开{input}文件。请使用你自己主机的 prod.keys。",
|
||||
"check.keys_too_old": "密钥文件对{input}文件来说太旧(需要密钥代数 {generation})。请从固件更新的主机导出 prod.keys。",
|
||||
"check.not_game_file": "{input}文件不是 XCI、NSP、XCZ 或 NSZ 游戏文件。",
|
||||
"check.file_unreadable": "无法读取{input}文件:{detail}",
|
||||
"check.unpack_failed": "无法解压压缩的{input}文件:{detail}",
|
||||
"check.ticket_personalized": "{input}文件带有个人化票据(personalized ticket);需要通用票据(common ticket)。",
|
||||
"check.ticket_missing": "{input}文件缺少其内容所需的票据(ticket)。",
|
||||
"check.base_not_nx2navi": "游戏本体文件不是《塞尔达传说 王国之泪》({detail})。",
|
||||
"check.base_is_update": "基础游戏文件是一个更新。请在这里选择基础游戏的 NSP 或 XCI(版本 1.0.0),并把此文件选为更新。",
|
||||
"check.base_combined": "基础游戏文件包含多个内容包(基础游戏、更新或 DLC 合在一个文件中)。请选择只包含游戏本体的基础游戏转储。",
|
||||
"check.update_not_nsp": "更新必须是 NSP 或 NSZ 文件。",
|
||||
"check.update_is_base": "更新文件里是游戏本体。请选择 1.1.0、1.2.1 或 1.4.3 的更新 NSP。",
|
||||
"check.update_not_nx2navi": "更新文件不是《王国之泪》的更新({detail})。",
|
||||
"check.exefs_failed": "{input}文件的游戏代码未通过检查;详见 {log}。",
|
||||
"check.input_damaged": "{input}文件已损坏或不完整:{path}({detail})。请从来源重新复制该文件或重新导出,然后再次运行安装程序。",
|
||||
"check.base_wrong_version": "游戏本体不是 1.0.0 版本({modules} 不一致)。",
|
||||
"check.update_wrong_version": "更新的版本是 {found};此发布版需要 {wanted}。",
|
||||
"check.disk_space": "{path} 的可用空间不足:需要 {need_gb} GB,可用 {free_gb} GB。",
|
||||
"check.firmware_missing": "固件文件夹 {path} 不存在。请选择固件的 Contents/registered 文件夹。",
|
||||
"check.firmware_empty": "固件文件夹 {path} 中没有 NCA 文件。请选择固件的 Contents/registered 文件夹。",
|
||||
"check.fonts_keys_needed": "此语言的共享字体尚未安装;请选择 prod.keys,以便安装程序从固件中提取它们。",
|
||||
|
||||
"extract.exefs_mismatch": "解包出的游戏代码与 {version} 版本不符({modules})。",
|
||||
"extract.romfs_merge_failed": "将更新应用到游戏数据失败:{detail}。详情:{log}",
|
||||
"extract.romfs_mismatch": "解包出的游戏数据与 {version} 版本不符(大小 {size},校验和 {sha256}...)。",
|
||||
"fonts.failed": "提取共享字体失败(退出代码 {code}):{detail}。详情:{log}",
|
||||
"fonts.incomplete": "共享字体提取没有写出全部文件({files})。",
|
||||
"fonts.copy_failed": "无法将共享字体复制到 {path}:{detail}",
|
||||
"lift.failed": "游戏代码转换失败(退出代码 {code})。详情:{log}",
|
||||
"compile.failed": "编译 {game_exe} 失败(退出代码 {code})。详情:{log}",
|
||||
"compile.exe_in_use": "{path} 正在使用中。请关闭游戏后重新运行安装程序。",
|
||||
"prepare.xlate_in_use": "{path} 正在被占用。请关闭游戏,结束上次残留的 nx2navi_xlate 进程,再重新运行安装程序。",
|
||||
"prepare.shader_list_missing": "缺少着色器列表。",
|
||||
"prepare.romfs_patches_failed": "写入游戏数据补丁失败(退出代码 {code})。详情:{log}",
|
||||
"prepare.shaders_failed": "准备着色器失败(退出代码 {code})。详情:{log}",
|
||||
"cleanup.romfs_unreadable": "安装完成后,{path} 中的游戏数据缺失或无法读取。请重启 Windows,然后对同一文件夹再次运行安装程序:它会重新提取游戏数据,其余内容保留。",
|
||||
"prepare.pack_failed": "打包着色器缓存失败(退出代码 {code})。详情:{log}",
|
||||
"cleanup.ini_failed": "无法写入 nx2navi.ini:{detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "{lang}文本需要 Switch 系统字体,但未提供。在添加这些字体之前,游戏无法以该语言启动。请选择固件文件夹(Contents/registered),或参阅 docs/installer.md 中的“Switch system fonts”一节。",
|
||||
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
|
||||
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
|
||||
"warn.xbox_prompts_failed": "无法准备 Xbox 或 PC 按键提示(退出代码 {code});游戏将继续使用 Switch 按键提示。详情:{log}",
|
||||
"warn.shaders_skipped": "显卡驱动在编译部分着色器时崩溃,因此有 {count} 个着色器未被预先准备。游戏会在首次需要时编译它们,驱动在那时也可能崩溃;更新驱动可能会解决。详情:{log}",
|
||||
"warn.kernel_old": "Linux {version} 缺少游戏依赖的写入跟踪功能(需要 6.7 或更新版本);游戏可能运行得很慢。更新系统即可解决。",
|
||||
"warn.dlss_kept": "安装文件夹中已有 DLSS 库({path});安装程序将保留它。",
|
||||
"warn.dlss_download_failed": "无法从 {url} 下载 NVIDIA DLSS:{detail}。游戏无需它也能运行;下次运行安装程序时会重试。",
|
||||
|
||||
"ui.title": "NX2navi 安装程序",
|
||||
"ui.rebuild_title": "正在为你的模组重新构建游戏",
|
||||
"ui.start_game": "启动游戏",
|
||||
"ui.version": "版本 {version}",
|
||||
"ui.intro": "用你自己的游戏文件构建一个可以游玩的《王国之泪》文件夹。耗时较长,只需运行一次。",
|
||||
"ui.notice": "此安装程序构建的文件夹包含游戏本身,仅供你个人使用;请勿分享或上传。",
|
||||
"ui.keys_private": "密钥文件只在原位置读取,其内容绝不会被显示、复制或保存。",
|
||||
"ui.base": "游戏本体(NSP、XCI、NSZ 或 XCZ)",
|
||||
"ui.update": "1.1.0、1.2.1 或 1.4.3 更新(NSP 或 NSZ)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "固件文件夹(可选)",
|
||||
"ui.game_language": "游戏语言",
|
||||
"ui.dest": "安装文件夹",
|
||||
"ui.browse": "浏览...",
|
||||
"ui.pick_game": "选择游戏本体文件",
|
||||
"ui.pick_update": "选择 1.1.0、1.2.1 或 1.4.3 更新文件",
|
||||
"ui.pick_keys": "选择 prod.keys",
|
||||
"ui.pick_firmware": "选择 Switch 固件文件夹(Contents/registered)",
|
||||
"ui.pick_dest": "选择安装文件夹",
|
||||
"ui.firmware_help": "用于中文、韩文和繁体中文文本:固件的 Contents/registered 文件夹,例如来自 Ryujinx 或 The hardware。安装程序会从中把 Switch 系统字体复制到安装文件夹。",
|
||||
"ui.dlss": "为 NVIDIA RTX 显卡下载 NVIDIA DLSS(nvngx_dlss.dll)",
|
||||
"ui.dlss_help": "来自 GitHub 上 NVIDIA 的 DLSS SDK,遵循 NVIDIA RTX SDK 许可(github.com/NVIDIA/DLSS)。没有它,dlaa 和 DLSS 设置不起作用。",
|
||||
"ui.compressed_help": "压缩的 NSZ 和 XCZ 文件可以直接使用:安装程序会自行解压(在游戏数据提取完成之前,需要与解压后文件大小相同的可用空间)。",
|
||||
"ui.type_game": "游戏文件(NSP、XCI、NSZ、XCZ)",
|
||||
"ui.type_keys": "密钥文件(*.keys)",
|
||||
"ui.type_all": "所有文件",
|
||||
"ui.start": "开始",
|
||||
"ui.cancel": "取消",
|
||||
"ui.cancelling": "正在取消...",
|
||||
"ui.show_log": "显示日志",
|
||||
"ui.hide_log": "隐藏日志",
|
||||
"ui.close": "关闭",
|
||||
"ui.back": "返回",
|
||||
"ui.open_folder": "打开文件夹",
|
||||
"ui.update_mode": "此文件夹中已有之前的安装。安装程序会继续或更新它;只有在需要时才会再次要求提供游戏文件。",
|
||||
"ui.need_dest": "请选择安装文件夹。",
|
||||
"ui.need_base": "请选择游戏本体文件。",
|
||||
"ui.need_update": "请选择 1.1.0、1.2.1 或 1.4.3 更新文件。",
|
||||
"ui.need_keys": "请选择 prod.keys 文件。",
|
||||
"ui.need_lang": "请选择游戏语言。",
|
||||
"ui.missing_base": "游戏本体文件不存在。",
|
||||
"ui.missing_update": "更新文件不存在。",
|
||||
"ui.missing_keys": "密钥文件不存在。",
|
||||
"ui.missing_firmware": "固件文件夹不存在。",
|
||||
"ui.running_install": "正在安装",
|
||||
"ui.running_resume": "正在继续之前的安装",
|
||||
"ui.running_update": "正在更新安装",
|
||||
"ui.state_started": "已开始",
|
||||
"ui.state_done": "完成",
|
||||
"ui.state_skipped": "已跳过",
|
||||
"ui.state_pending": "等待中",
|
||||
"ui.state_running": "进行中",
|
||||
"ui.state_failed": "失败",
|
||||
"ui.log_warning": "警告",
|
||||
"ui.log_error": "错误",
|
||||
"ui.log_space": "所需空间:{need} GB,可用:{free} GB",
|
||||
"ui.log_end": "已结束",
|
||||
"ui.result_ok": "游戏已准备就绪",
|
||||
"ui.result_failed": "安装失败",
|
||||
"ui.result_cancelled": "安装已取消",
|
||||
"ui.done_text": "从 {dest} 启动 {game_exe}。安装程序和你选择的游戏文件都不再需要了。",
|
||||
"ui.cancelled_text": "已完成的步骤会保留。用同一个文件夹再次运行安装程序即可继续。",
|
||||
"ui.log_file": "日志文件:{path}",
|
||||
"ui.process_died": "安装程序意外停止(退出代码 {code})。",
|
||||
"ui.confirm_cancel": "安装正在进行。要取消安装并关闭窗口吗?",
|
||||
"ui.space_one": "{path} 需要的可用空间:{need} GB(可用:{free} GB)。",
|
||||
"ui.space_two": "{path} 需要的可用空间:{need} GB(可用:{free} GB);{temp} 需要:{temp_need} GB(可用:{temp_free} GB)。",
|
||||
"ui.amount_bytes": "{done} / {total} GB",
|
||||
"ui.amount_chunks": "{done} / {total} 部分",
|
||||
"ui.amount_objects": "{done} / {total} 个文件",
|
||||
"ui.amount_shaders": "{done} / {total} 个着色器",
|
||||
"ui.amount_other": "{done} / {total} {unit}",
|
||||
"ui.seconds": "{n} 秒",
|
||||
"param.input.base": "游戏本体",
|
||||
"param.input.update": "更新",
|
||||
"param.input.keys": "密钥",
|
||||
|
||||
"launcher.title": "NX2navi 安装程序",
|
||||
"launcher.python_missing": "安装文件不完整:NX2navi-Setup.exe 旁边缺少 python 文件夹。请重新解压整个压缩包,然后从解压出的文件夹中启动 NX2navi-Setup.exe。",
|
||||
|
||||
"lang.ja": "日语",
|
||||
"lang.en-US": "英语(美国)",
|
||||
"lang.en-GB": "英语(英国)",
|
||||
"lang.fr": "法语",
|
||||
"lang.fr-CA": "法语(加拿大)",
|
||||
"lang.de": "德语",
|
||||
"lang.it": "意大利语",
|
||||
"lang.es": "西班牙语",
|
||||
"lang.es-419": "西班牙语(拉丁美洲)",
|
||||
"lang.nl": "荷兰语",
|
||||
"lang.pt": "葡萄牙语",
|
||||
"lang.pt-BR": "葡萄牙语(巴西)",
|
||||
"lang.ru": "俄语",
|
||||
"lang.ko": "韩语",
|
||||
"lang.zh-Hans": "中文(简体)",
|
||||
"lang.zh-Hant": "中文(繁体)"
|
||||
}
|
||||
386
nx2navi.ini.example
Normal file
386
nx2navi.ini.example
Normal file
@@ -0,0 +1,386 @@
|
||||
# nx2navi.ini: settings for nx2navi.exe. Copy this file to nx2navi.ini next to nx2navi.exe and remove the '#' of a line to change it.
|
||||
# Each line shows the default; on a Steam Deck the device profile ([display]) changes a few of them. A NX2NAVI_*
|
||||
# environment variable of the same name (NX2NAVI_FPS_LIMIT for fps_limit) overrides the file. Keys are case-insensitive;
|
||||
# '#' and ';' start a comment; [sections] only group lines.
|
||||
|
||||
[frame rate]
|
||||
# Frame rate limit: 30, 40, 45, 60, 90, 120, 144, any whole rate from 30 to 240, or display (the refresh rate of the
|
||||
# window's monitor, at most 240). A higher rate gives 240: above it the game's physics runs out of memory. Above 60
|
||||
# is experimental and needs realtime = 1. Changes while the game runs take effect at once.
|
||||
# With mailbox or fifo presentation (vk_present, the default) a frame reaches the screen at a vertical blank, so a limit
|
||||
# that does not divide the monitor's refresh rate keeps each frame on screen for a different number of refreshes. On a
|
||||
# 320 Hz monitor 80, 64, 40 and 32 divide it and 90 does not: 90 fps alternates 3 and 4 refreshes per frame, which reads
|
||||
# as judder while the frame counter still shows 90. Likewise on 240 Hz 60, 48 and 40 divide it and on 144 Hz 72, 48 and
|
||||
# 36 do, while 60 does not. vk_present = immediate shows a frame as soon as it is presented, without this quantisation,
|
||||
# at the cost of tearing. A monitor running with variable refresh (FreeSync, G-Sync) follows the frames instead, so
|
||||
# there any limit inside its range is even.
|
||||
# fps_limit = 60
|
||||
# Older key, read only while fps_limit is unset: fps60 = 0 means a limit of 30.
|
||||
# fps60 = 1
|
||||
# Real-time frame step: game speed follows the clock instead of the frame count.
|
||||
# realtime = 1
|
||||
# Cloth simulation on the real-time clock. Needs realtime = 1.
|
||||
# cloth_realtime = 1
|
||||
# Fix for physics that bounce at high frame rates.
|
||||
# highfps_fix = 1
|
||||
|
||||
[performance]
|
||||
# The keys of this section and of [diagnostics] are for testing and troubleshooting, and take effect at the next start.
|
||||
# The Advanced tab of the settings menu (F1) shows all of them except zstd_native and zstd_check.
|
||||
|
||||
# Scheduling.
|
||||
# A thread waiting for a virtual core interrupts the owner when it runs game code: 1 = on, 2 = priority inheritance
|
||||
# without the interrupt, 0 = off (the old scheduling).
|
||||
# async_preempt = 1
|
||||
# Replay NVN submissions on their own thread, at most one frame behind the game, 0 or 1. Default: off. Helps where the
|
||||
# game waits for the GPU; adds up to a frame of latency.
|
||||
# replay_async = 0
|
||||
# EXPERIMENTAL, THE GAME CRASHES WITH 1. 1 lifts the virtual core gate: guest threads run without the console's limit
|
||||
# of one thread per core, and the game keeps per-core data without locks. With 1, 1.2.1 and 1.4.3 crash or hang,
|
||||
# usually at start (#212). Keep 0 to play.
|
||||
# free_cores = 0
|
||||
# Windows timer resolution of 1 ms for steadier frame pacing (0 = the Windows default).
|
||||
# timer_1ms = 1
|
||||
|
||||
# Shaders and pipelines.
|
||||
# Draws whose pipeline is still building: default waits on a reserved worker; draw is the previous scheduling.
|
||||
# pipe_wait = default
|
||||
# Which draws are dropped instead of waiting for their pipeline: strict (only draws into per-frame passes), any (also
|
||||
# draws into passes whose target the previous frame rendered) or fullscreen (those and full-screen draws).
|
||||
# pipe_skip = fullscreen
|
||||
# Build shader stage libraries in the background ahead of time, and the threads for it (1 to 16; default: a quarter of
|
||||
# the CPU threads).
|
||||
# lib_prebuild = 1
|
||||
# lib_prebuild_threads = 4
|
||||
# Build known pipelines ahead of time.
|
||||
# pipe_prebuild = 1
|
||||
# Threads that translate known shaders at start (1 to 64; default: half the CPU threads, at most 16).
|
||||
# prewarm_xlate = 8
|
||||
# Vulkan graphics pipeline library, 0 or 1. Default: on.
|
||||
# gpl = 1
|
||||
|
||||
# Textures and memory.
|
||||
# ASTC texture decoding: native (the GPU's own ASTC support), gpu (a compute shader), cpu, or auto (native when the GPU
|
||||
# has it, gpu otherwise).
|
||||
# astc = auto
|
||||
# With astc = gpu: re-encode the decoded ASTC textures to BC3, a quarter of the video memory, textures slightly blockier,
|
||||
# 0 or 1. Default: on when the GPU has less than 5 GB of video memory, off otherwise.
|
||||
# astc_bc = 1
|
||||
# Game memory shared with the GPU: auto (mirror on a discrete GPU, import on an integrated one), import (host memory
|
||||
# imported into Vulkan), mirror (pools used as buffers get a copy in video memory) or verify (mirror plus a check of
|
||||
# every copy; slow). On a discrete GPU import can be far slower than mirror, not slightly slower: the GPU reads the
|
||||
# game's pools over PCIe. A 5070 Ti Laptop went from 6 ms to 135-230 ms of GPU time per frame with import (5 fps on the
|
||||
# title screen, bound: gpu, every line of the perf window waiting on a fence). The log names the mode in use: "mem:
|
||||
# mode import" or "mem: mode mirror".
|
||||
# memory = auto
|
||||
# A new texture created over a live texture's memory starts with that content instead of empty.
|
||||
# tex_alias = 1
|
||||
# Host zstd decoder in place of the game's (0 = the game's), and a check that decodes every call both ways and logs
|
||||
# the differences (1 = on; slow).
|
||||
# zstd_native = 1
|
||||
# zstd_check = 0
|
||||
|
||||
# Render fixes.
|
||||
# Wait for the game to finish rewriting level-of-detail data before the GPU reads it: stops objects flashing in the
|
||||
# wrong place and a flat far field for a frame.
|
||||
# lod_guard = 1
|
||||
# Fill the driver-reserved constant bank c0 (storage sizes, dispatch size, point size, base instance) instead of zeros.
|
||||
# fix_c0 = 1
|
||||
# New GPU memory pools start zero-filled, as Switch memory does.
|
||||
# fix_zero = 1
|
||||
# Optimisations, 0 turns each off: skip bindings that did not change, remember shader lookups, reuse the previous
|
||||
# draw's pipeline when its state repeats, remember texture descriptors between draws, update descriptors in bulk,
|
||||
# skip push constants that did not change.
|
||||
# bind_skip = 1
|
||||
# shader_memo = 1
|
||||
# pipe_memo = 1
|
||||
# tex_memo = 1
|
||||
# desc_template = 1
|
||||
# push_skip = 1
|
||||
# Submit GPU work in parts every N draws between passes so the GPU starts earlier (0 to 1000; 0 = off), each part at
|
||||
# least split_us microseconds long (0 to 20000).
|
||||
# split_draws = 64
|
||||
# split_us = 1000
|
||||
|
||||
# Vulkan.
|
||||
# Vulkan validation layer: checks every call and slows the game down a lot. Needs the layer installed (Vulkan SDK).
|
||||
# vk_validation = 0
|
||||
# A thread of its own makes the Vulkan calls of each frame, 0 or 1. Default: on, except on AMD GPUs.
|
||||
# vk_recorder = 1
|
||||
# Dynamic vertex input, 0 or 1. Default: on, except on Intel's older integrated GPU drivers (Vulkan 1.3.215 and older).
|
||||
# vk_dyn_vertex = 1
|
||||
# Dynamic alpha to coverage, 0 or 1. Default: on, except on AMD's driver for RDNA 2 and older (RX 400 to RX 6000),
|
||||
# which applies alpha to coverage while the dynamic state says off and draws the characters black.
|
||||
# vk_dyn_a2c = 1
|
||||
# Pin the shader subgroup size to 32 when the GPU allows it.
|
||||
# vk_subgroup_pin = 1
|
||||
|
||||
[display]
|
||||
# Device profile: auto (the Steam Deck when this is one), desktop or deck. On a Steam Deck the keys this file does not
|
||||
# set default to fullscreen = 1, window_size = 1280x800, output_res = 1280x800, render_scale = game, vk_present = fifo
|
||||
# and fps_limit = 40 (starting points, not measured yet). Read at start.
|
||||
# device_profile = auto
|
||||
# Presentation mode: mailbox (default when the driver offers it, vsync otherwise), fifo (vsync) or immediate.
|
||||
# vk_present = mailbox
|
||||
# Window client area in screen pixels (640x360 to 7680x4320), and borderless fullscreen. Windows display scaling does
|
||||
# not apply: at 150% scale, 1920x1080 covers the area 1280x720 covers at 100%. F11 switches fullscreen and saves it
|
||||
# here. Changes while the game runs take effect at once.
|
||||
# window_size = 1920x1080
|
||||
# fullscreen = 0
|
||||
# 1, the default: the game keeps running while you are in another window; the keyboard stays quiet until the window is
|
||||
# in front again. 0: it pauses when its window loses focus, as it does on the Switch when Home is pressed; a load
|
||||
# interrupted that way can stop for good. Read at each focus change.
|
||||
# assume_focus = 1
|
||||
# Performance overlay in the top right corner: off, fps, or full (frame rate, mean and longest frame time). F10
|
||||
# cycles the modes in game, as do both stick clicks (L3 + R3) on a controller.
|
||||
# perf_overlay = off
|
||||
# Which GPU to use: its index in the log, or part of its name. Default: the first discrete GPU with Vulkan 1.3.
|
||||
# gpu_index = 0
|
||||
# gpu_name = NVIDIA
|
||||
# Game resolution: the size the game draws its picture and interface in (1280x720 to 7680x4320). Above 1920x1080 the
|
||||
# game's texture heap and memory pool grow with it. Another shape than 16:9, such as 3440x1440 on a 21:9 monitor or
|
||||
# the Steam Deck's 1280x800 (16:10), gives the camera's view that shape and lays the HUD out for it. The installer
|
||||
# prepares the UI layout for fifteen sizes: 2560x1080, 3440x1440, 3840x1600, 5120x2160 (21:9), 3840x1080, 5120x1440
|
||||
# (32:9), 1280x800, 1680x1050, 1920x1200, 2560x1600 (16:10), 1920x1280, 2160x1440, 2256x1504, 2880x1920 and 3000x2000
|
||||
# (3:2). Any other size
|
||||
# gets the camera's view but a 16:9 HUD, stretched over the picture, unless you build its layout with
|
||||
# tools/make_romfs_patches.py --aspect WxH. Read at start.
|
||||
# output_res = 1920x1080
|
||||
# 3D render scale: the scene's size relative to the game resolution, game (5/6, the game's own 1600x900 at 1080p) or
|
||||
# a percentage (25 to 400, at most 4096 pixels a side); above 100 supersamples the scene (anti-aliasing). Read at start.
|
||||
# render_scale = game
|
||||
# Scaling to the window when it is larger than the game resolution: fsr (AMD FSR 1) or bilinear, and FSR's sharpening
|
||||
# from 0 (off) to 100. Changes while the game runs take effect at once.
|
||||
# upscaler = fsr
|
||||
# sharpness = 80
|
||||
# Picture shape when the window's shape differs from the game resolution's (a 16:9 picture in a 21:9 fullscreen
|
||||
# window): fit, the default, keeps the shape, with black bars at the sides or above and below, so a 16:9 game in a wider
|
||||
# window is no longer stretched as it used to be; stretch fills the window. Changes while the game runs take effect at
|
||||
# once.
|
||||
# aspect = fit
|
||||
# Field of view, in percent of the game's own: 70 to 140, 100 (the default) leaves the camera as it is. Higher shows
|
||||
# more of the world around Link. Game version 1.4.3 only. Changes while the game runs take effect at once.
|
||||
# fov = 100
|
||||
# Anisotropic filtering of surface textures: game (the level the game asks for, as on the Switch) or 2, 4, 8, 16. Keeps
|
||||
# ground, roads and walls seen at a shallow angle sharp into the distance. Only filtered, mipmapped textures that tile
|
||||
# are raised; shadow maps, data maps and screen-sized buffers keep the game's setting. Read at start.
|
||||
# aniso = game
|
||||
# Colour adjustment of the picture, HUD included, before it is scaled to the window: brightness (-30 to 30, added in %),
|
||||
# contrast and saturation (70 to 150 and 50 to 200 %), gamma (0.8 to 1.2, below 1 brightens dark tones). At 0, 100,
|
||||
# 100 and 1 nothing runs. color_preset is the menu's profile (default, vivid, clarity, custom), which sets the four.
|
||||
# Changes while the game runs take effect at once.
|
||||
# color_preset = default
|
||||
# color_brightness = 0
|
||||
# color_contrast = 100
|
||||
# color_saturation = 100
|
||||
# color_gamma = 1
|
||||
# Sun shadow map size: game (the game's own, which changes with the view and drops to about 400 near the ground, so
|
||||
# leaf shadows look blocky and jump) or a fixed 256, 512, 768, 1024, 1536 or 2048. Above 960 the texture heap grows with
|
||||
# it. 256 is coarser than the game's own and is for integrated GPUs short of memory bandwidth. Read at start.
|
||||
# shadow_res = game
|
||||
|
||||
# Shadow map resolution: the game's sun cascades are 960x960 (and its static shadow maps 1440 and 720) whatever the game
|
||||
# resolution; 2 to 4 draws them that many times larger on each side, for sharper shadows. 1 = the game's. The game's
|
||||
# shadow edge filter is narrowed by the same factor (translator patch 0011), so close shadow edges don't look doubled.
|
||||
# Read at start.
|
||||
# shadow_scale = 1
|
||||
# Sun shadow cascade range in percent of the game's (its cascades end at 20, 40, 80 and 160 m), 50 to 400: higher moves
|
||||
# the steps where shadows turn softer further away, and spreads the shadow map over more ground. Read at start.
|
||||
# shadow_range = 100
|
||||
# Screen-space contact shadows, strength in percent (0 = off): after the game lights its scene, each pixel traces a
|
||||
# short way toward the sun through the depth buffer and darkens where something close blocks it, adding the small
|
||||
# shadows at feet, under stones and in grass that the shadow maps miss. Read at start.
|
||||
# contact_shadows = 0
|
||||
# Soft sun shadows (PCSS), percent of the default penumbra width (0 to 200; 0 = the game's own four-tap filter): sharp
|
||||
# where an object meets the ground and softer the further its shadow falls. Done in the translated shaders (translator
|
||||
# patch 0010), so the shader cache is rebuilt once after an update. Read at start.
|
||||
# pcss = 0
|
||||
|
||||
# NVIDIA DLSS / DLAA for the 3D scene, on NVIDIA RTX GPUs with the DLSS model from NVIDIA's DLSS SDK next to the
|
||||
# executable (Windows: nvngx_dlss.dll next to nx2navi.exe; Linux: libnvidia-ngx-dlss.so.<version> next to nx2navi, with the
|
||||
# NVIDIA driver's libnvidia-ngx.so.1 installed); elsewhere the setting does nothing. The HUD is drawn after it and is not touched.
|
||||
# Read at start.
|
||||
# dlaa = 0
|
||||
# What DLSS works from: dlaa (the 3D scene at full size), quality, balanced, performance or ultra_performance (the
|
||||
# scene at 67, 58, 50 or 33 % of the output, scaled up by DLSS; these replace render_scale and sharpen texture filtering
|
||||
# to match). They apply only with dlaa = 1 and the DLSS model next to the executable; otherwise the scene follows
|
||||
# render_scale. Changes while the game runs take effect at once (1.4.3; 1.2.1 at the next start).
|
||||
# dlss = dlaa
|
||||
# The DLSS model: default (NVIDIA's advice per mode: K for dlaa, quality and balanced, M for performance, L for
|
||||
# ultra_performance), k (K in every mode) or j (the other transformer model: slightly less ghosting behind moving
|
||||
# characters, a little more flicker). Any other model by its letter, a to m: on slower RTX cards the older model e is
|
||||
# cheaper. Changes while the game runs take effect at once (DLSS starts a new history).
|
||||
# dlss_preset = default
|
||||
# Per-object motion vectors for DLSS: characters, animals and moving objects get their own motion instead of the
|
||||
# camera's, so DLSS keeps their history instead of smearing or rejecting it. Costs some CPU time each frame (about 1-2 ms
|
||||
# on a laptop CPU); 0 leaves camera-only motion. Read at start.
|
||||
# dlaa_objmv = 1
|
||||
# Characters' trails: where a character, animal or held item moves without motion vectors of its own (all of them with
|
||||
# dlaa_objmv = 0), DLSS can drag a faint copy of it. The G-buffer marks those objects; on them and where they were the
|
||||
# frame before, DLSS is told to favour the current frame by this percentage (0, 35, 65 or 100; 0 off). Higher removes
|
||||
# more of the trails and leaves those edges a little less smooth. Changes while the game runs take effect at once.
|
||||
# dlss_char_mask = 65
|
||||
# The game's own FXAA after the post effects; with DLAA on it is not needed. Changes while the game runs take effect at
|
||||
# once.
|
||||
# fxaa = 1
|
||||
|
||||
# HDR output: hdr = 1 sends the picture as HDR10 (needs HDR on in Windows, "Use HDR", or a Wayland compositor with
|
||||
# colour management; without it the picture stays SDR). The game's tonemap keeps highlights up to hdr_peak_nits (your
|
||||
# display's peak); hdr_game_nits is the picture's white (paper white, 203 is the HDR reference) and hdr_ui_nits the
|
||||
# HUD's and the menu's. hdr is read at start, the nits at once. The colour profile above is for SDR only.
|
||||
# hdr = 0
|
||||
# hdr_peak_nits = 1000
|
||||
# hdr_game_nits = 203
|
||||
# hdr_ui_nits = 203
|
||||
# Highlight hue correction in percent (0 to 100): bright colours such as fire turn towards white as in the game's SDR
|
||||
# picture, matching the light they cast; 0 keeps them fully saturated. Changes take effect at once.
|
||||
# hdr_hue = 80
|
||||
|
||||
[language]
|
||||
# Language: a Windows locale such as de-DE, or a Switch code (en-US, ja, zh-Hant, ...). Default: the Windows locale.
|
||||
# locale = en-US
|
||||
# lang = en-US
|
||||
# System mode reported to the game: docked or handheld.
|
||||
# operation_mode = docked
|
||||
# Language of the settings menu and the overlay: en, ru, pt-BR or zh-Hans. Default: the Windows display language, English if
|
||||
# it is none of them.
|
||||
# menu_lang = en
|
||||
|
||||
[discord]
|
||||
# Rich Presence: "Playing NX2navi Recomp" with the play time in your Discord status while the game runs, through the
|
||||
# local IPC of a running Discord client. 1 = on; the Menu tab of the settings menu (F1) switches it too, and the
|
||||
# change reaches the presence thread at once.
|
||||
# discord_rpc = 0
|
||||
# The application whose name and artwork the status shows. The default is the project's own application,
|
||||
# registered as "NX2navi Recomp" with the port's green ring icon as its art. Register your own at
|
||||
# discord.com/developers (name and art are yours to choose) and put its id here.
|
||||
# discord_app_id = 1567232599846293504
|
||||
# The key of the application's uploaded art asset shown as the status's large image.
|
||||
# discord_image = nx2navi_512x512
|
||||
|
||||
[audio]
|
||||
# none mixes without opening an audio device.
|
||||
# audio = default
|
||||
# Audio the output queues before it starts, in ms (0 to 500).
|
||||
# audio_prefill_ms = 20
|
||||
# Volume in percent (0 to 100), on top of the game's own. Changes while the game runs take effect at once.
|
||||
# volume = 100
|
||||
# Speakers: stereo, 5.1 or auto (5.1 when the Windows speaker setup has six or more channels, stereo otherwise). The
|
||||
# game is told the same channel count and mixes for it. Takes effect at the next start.
|
||||
# audio_channels = stereo
|
||||
|
||||
[input]
|
||||
# Gamepad motion controls (0 = off) and vibration (0 = off).
|
||||
# motion = 1
|
||||
# rumble = 1
|
||||
# Rumble strength in percent (0 to 300; 0 = off) and the exponent on the amplitude (0.2 to 1.5; lower = weak pulses
|
||||
# stronger, 1 = linear). rumble_triggers = 1 also drives the impulse triggers of an Xbox pad.
|
||||
# rumble_strength = 130
|
||||
# rumble_curve = 0.6
|
||||
# rumble_triggers = 0
|
||||
# Which gamepad button is Switch A: nintendo (by position: east A, south B, west Y, north X), xbox (south A, east B,
|
||||
# west X, north Y, as an Xbox pad labels them; cross A on a PlayStation pad) or auto (nintendo for a pad with Nintendo
|
||||
# labels, xbox for the others). The bind_ lines below apply on top. Changes while the game runs take effect at once.
|
||||
# 0.1.9's xbox_prompts is read as nintendo with prompts = xbox.
|
||||
# pad_layout = auto
|
||||
# The buttons the game's prompts show: nintendo (the game's own), xbox (LB RB LT RT, Menu, View and a d-pad cross,
|
||||
# and A B X Y as labelled on the button pad_layout presses: with nintendo, A shows as B and X as Y). Read at start
|
||||
# (docs/controls.md, "Xbox button prompts"). pc: the key or mouse button the keyboard defaults press each button with
|
||||
# (E, Shift, Space, left click, Q, R, right click, F, Tab, M, 1 to 4, W A S D, Ctrl, C) and the mouse for the right
|
||||
# stick, whatever pad_layout (docs/controls.md, "PC button prompts").
|
||||
# prompts = nintendo
|
||||
# Gamepad stick dead zone in percent of full deflection (0 to 50). Raise it for a stick that drifts: inside the circle
|
||||
# the stick reads centred, and the rest of its travel is scaled to start there.
|
||||
# stick_deadzone = 8
|
||||
# Right-stick camera speed (0.1 to 3): multiplies the game's own camera turn rates, top speed included, at each of the
|
||||
# five camera sensitivities in the game's Options. Keys bound to the right stick follow it; the mouse keeps mouse_sens.
|
||||
# Read at start.
|
||||
# stick_sens = 1
|
||||
# The dead zone of one stick, in place of stick_deadzone for that stick (0 to 50; unset = stick_deadzone). The Controls
|
||||
# tab shows each stick's deflection now, so a drifting stick's resting value can be read off.
|
||||
# lstick_deadzone = 8
|
||||
# rstick_deadzone = 8
|
||||
# Mouse camera mode: direct (default) or stick. Direct turns the camera by the raw mouse motion, the same on both axes,
|
||||
# with no stick dead zone or response curve (game 1.4.3; other game versions use stick emulation). Stick makes the
|
||||
# mouse a right stick. Neither changes the gamepad. The Controls tab changes this while the game runs.
|
||||
# mouse_mode = direct
|
||||
# The mouse in the game's menus, 1.4.3 only (title, inventory, System tab, Options, shops, dialogue choices): while a
|
||||
# menu has the game's focus box the pointer shows, the item under it takes the focus, a left click is A on it and a
|
||||
# right click is B. 0 leaves the menus to the keys and the pad. Changes while the game runs.
|
||||
# menu_cursor = 1
|
||||
# Mouse camera sensitivity (0 to 10; 0 = no mouse camera and no cursor capture).
|
||||
# mouse_sens = 1
|
||||
# The keyboard's sticks are all or nothing, so the game only sees a full tilt from them; while this key is held the
|
||||
# keyboard-driven stick values shrink and the keyboard can walk instead of run. Key names as in the bind_ lines below,
|
||||
# none turns it off. 0.2 to 0.9, the default holds for anything else. Only the keyboard is scaled: the mouse camera and
|
||||
# the gamepad are unchanged. The Controls tab sets both. The default stops just above half because a keyboard sample of
|
||||
# 32767 times 0.5 is one short of the 16384 the game's stick-direction bits need, and below that the keyboard stick
|
||||
# reads as a partial tilt only.
|
||||
# walk_key = lshift
|
||||
# walk_scale = 0.51
|
||||
# Write the input of this run to a file that replays as a script (docs/controls.md, "Scripted input"). Default: off.
|
||||
# input_record = input.txt
|
||||
# Remap buttons: the Controls tab of the settings menu (F1) writes these lines. One line per Switch button or stick
|
||||
# direction: bind_<A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK> or bind_<LSTICK RSTICK>_<LEFT UP RIGHT
|
||||
# DOWN>, or bind_SHAKE (G: shakes the right stick, which detaches a fused part from what object holds) = comma
|
||||
# separated pad:<name> and key:<name> entries, key:default / pad:default for that part's defaults, or none.
|
||||
# A line replaces that button's defaults (pad and keyboard); buttons without a line keep them. Stick directions and
|
||||
# SHAKE take keys only. Invalid entries are logged and skipped. Changes while the game runs take effect at once.
|
||||
# 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 (the back buttons of an Xbox Elite, DualSense Edge or Steam Controller)
|
||||
# key names: a-z 0-9 f2-f7 f9 f12 num0-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
|
||||
# Defaults (pad_layout = nintendo): A = pad:east, key:E. B = pad:south, key:lshift. X = pad:north, key:space.
|
||||
# Y = pad:west, key:mouse_l. A pad that reports A/B and X/Y swapped (some GuliKit models): set pad_layout = xbox, or
|
||||
# swap them back here (keyboard keys stay):
|
||||
# bind_A = pad:south, key:E
|
||||
# bind_B = pad:east, key:lshift
|
||||
# bind_X = pad:west, key:space
|
||||
# bind_Y = pad:north, key:mouse_l
|
||||
|
||||
[mods]
|
||||
# Folder with the player's mods, one subfolder per mod (<name>/romfs, <name>/exefs, <name>/cheats). Default: mods next
|
||||
# to nx2navi.exe.
|
||||
# mods_dir =
|
||||
# Load order, top first, names separated by |. The in-game menu (Mods) writes it; a mod not listed goes to the bottom.
|
||||
# mods_order =
|
||||
# Mods switched off, names separated by |.
|
||||
# mods_disabled =
|
||||
# auto: when an enabled mod has cheats that change the game's code and the game was built without them, the game starts
|
||||
# the setup's rebuild at start (a few minutes) and starts again after it. off: the game starts with the code it has.
|
||||
# A rebuild that failed or was cancelled is not started again for the same cheats. Default: auto.
|
||||
# mods_rebuild = auto
|
||||
|
||||
[files]
|
||||
# Developer overlay: a folder with RomFS files and a manifest.tsv (tools/make_romfs_patches.py format). Players put
|
||||
# their mods into the mods folder instead ([mods]). Default: none.
|
||||
# romfs_mods =
|
||||
# Folder with the Switch shared fonts (BFTTF files, tools/extract_fonts.py output), for Chinese and Korean text.
|
||||
# Default: shared-fonts next to nx2navi.exe. NX2NAVI_SHARED_FONTS overrides it.
|
||||
# shared_fonts =
|
||||
|
||||
[cheats]
|
||||
# One line per cheat of the mods' cheat files, mods\<mod>\cheats\<build id>.txt (docs/cheats.md); the Cheats tab of
|
||||
# the menu writes them. cheat_<hash> = 1 switches a cheat on, the hash being FNV-1a 32 of "<mod folder>/<cheat name>".
|
||||
# Default: every cheat off.
|
||||
# cheat_0123abcd = 1
|
||||
|
||||
[diagnostics]
|
||||
# Extra lines in the log, comma separated: svc, host, ipc, fs, gfx, perf, sched, audio, present or all. The menu's
|
||||
# Performance log sets perf (frame timings). Default: none.
|
||||
# trace = perf
|
||||
# Flight recorder: keeps the last frames and saves them to a flight folder next to the folder of nx2navi.exe when a frame
|
||||
# takes longer than flight_spike_ms (0 to 60000).
|
||||
# flight = 0
|
||||
# flight_spike_ms = 100
|
||||
# GPU pass marks: after a GPU crash (device lost) the log names the render pass or dispatch the GPU had started and the
|
||||
# last one it finished. Unset: on AMD GPUs (VK_AMD_buffer_marker). 1: also on NVIDIA GPUs
|
||||
# (VK_NV_device_diagnostic_checkpoints), which NVIDIA says costs a little. 0: off.
|
||||
# pass_crumbs = 1
|
||||
# Render targets up to this size (bytes, 0 to 1073741824) go back to their game memory whenever the game waits for a
|
||||
# GPU fence. A larger target goes back too unless the GPU drew it in each of the last 16 frames. A game that reads a
|
||||
# larger target drawn every frame without invalidating it sees stale bytes at the default. With trace = gfx the log
|
||||
# names each dirty target that stays on the GPU ("fence write-back skipped-drawn-every-frame") and each one over 65536
|
||||
# bytes that went back ("over-default"). Raising it costs a copy per such target and wait.
|
||||
# wb_fence_limit = 65536
|
||||
144
recomp/flags.py
Normal file
144
recomp/flags.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Function-local NZCV for recomp/gen.py.
|
||||
|
||||
The lifter writes flags the way the guest does: every flag-setting instruction packs N, Z, C and V into c->nzcv
|
||||
(subs64() and friends in runtime/cpu.h) and every reader unpacks them (COND_xx(c)). Compilers keep NZCV inside a
|
||||
function, and AAPCS64 neither passes it to a callee nor preserves it across a call. So a generated function keeps its
|
||||
flags in a local `Flags F` (runtime/cpu.h), which clang holds in registers, and writes c->nzcv only where it leaves
|
||||
for another function other than by a call or a return (see plan()).
|
||||
|
||||
A flag-setting instruction of a known kind (subtract, add or logic result; 32 or 64 bits) stores its operands in F.a
|
||||
and F.b. A reader all of whose reaching definitions are of one kind computes its condition from those operands, so
|
||||
`cmp x1, x2; b.lt` becomes a signed compare in C. Instructions whose result is only known packed (CCMP, FCMP, MSR
|
||||
NZCV, ADCS) write F.nz, the packed form; a reader whose definitions are of several kinds reads F.nz, and those
|
||||
definitions then also write it.
|
||||
|
||||
A read that can see flags a callee left reads the caller's flags from before the call instead. AAPCS64 code never
|
||||
reads a callee's flags, so such a read only sits on a path that does not run: in NX2navi it follows calls to noreturn
|
||||
functions (asserts) that the compiler placed before another branch's target. Plan.after_call counts them.
|
||||
"""
|
||||
import re
|
||||
|
||||
KIND_DEF = re.compile(r'\b(subs|adds|logic)(32|64)\(c, ')
|
||||
CARRY_DEF = re.compile(r'\b(adcs|sbcs)(32|64)\(c, ')
|
||||
CARRY_USE = re.compile(r'\b(adc|sbc)(32|64)\(c, ')
|
||||
COND = re.compile(r'COND_([A-Z]{2})\(c\)')
|
||||
NZ_DEF = 'c->nzcv = '
|
||||
NZ_USE = '(c->nzcv)'
|
||||
KIND_NAME = {'subs': 'sub', 'adds': 'add', 'logic': 'logic'}
|
||||
|
||||
ENTRY, CALL = 'entry', 'call'
|
||||
|
||||
|
||||
class FlagError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def effect(code):
|
||||
"""(reads flags, kind of the flags written or None) of one instruction's C."""
|
||||
reads = bool(COND.search(code) or CARRY_USE.search(code) or CARRY_DEF.search(code) or NZ_USE in code)
|
||||
if NZ_DEF in code or CARRY_DEF.search(code):
|
||||
return reads, 'nz'
|
||||
defs = KIND_DEF.findall(code)
|
||||
if len(defs) > 1:
|
||||
raise FlagError(f'several flag definitions in one instruction: {code}')
|
||||
if defs:
|
||||
op, size = defs[0]
|
||||
return reads, KIND_NAME[op] + size
|
||||
return reads, None
|
||||
|
||||
|
||||
class Plan:
|
||||
"""Per instruction: the kind its flag reads use, whether its flag definition also writes F.nz, and what it writes
|
||||
to c->nzcv before it leaves for another function."""
|
||||
|
||||
def __init__(self, n):
|
||||
self.reader = [None] * n
|
||||
self.pack = [False] * n
|
||||
self.exit = [None] * n # kind of the flags to publish in c->nzcv at a tail exit, or None
|
||||
self.entry = False # F.nz starts as c->nzcv: flags from the entry reach a read or an exit
|
||||
self.entry_read = False # they reach a read, not only an exit
|
||||
self.after_call = 0 # reads that can see flags a callee left (see the module docstring)
|
||||
self.used = False
|
||||
|
||||
def decl(self):
|
||||
"""Declaration of F at the top of the function."""
|
||||
return 'Flags F = {.nz = c->nzcv};' if self.entry else 'Flags F = {0};'
|
||||
|
||||
def publish(self, i):
|
||||
"""Statement writing the flags to c->nzcv at instruction i's tail exit ('' when it need not)."""
|
||||
k = self.exit[i]
|
||||
return f'c->nzcv = fnz_{k}(&F); ' if k else ''
|
||||
|
||||
def rewrite(self, i, code):
|
||||
"""code of instruction i with its flag reads and writes on F."""
|
||||
rk = self.reader[i]
|
||||
_, dk = effect(code)
|
||||
if rk:
|
||||
code = COND.sub(lambda m: f'fc_{rk}(&F, CC_{m.group(1)})', code)
|
||||
code = CARRY_DEF.sub(lambda m: f'{m.group(1)}{m.group(2)}f(&F, fc_{rk}(&F, CC_CS), ', code)
|
||||
code = CARRY_USE.sub(lambda m: f'{m.group(1)}{m.group(2)}f(fc_{rk}(&F, CC_CS), ', code)
|
||||
code = code.replace(NZ_USE, f'(fnz_{rk}(&F))')
|
||||
if dk == 'nz':
|
||||
code = code.replace(NZ_DEF, 'F.nz = ')
|
||||
code = re.sub(r'\(void\)(subs|adds)(32|64)\(c, ', lambda m: f'F.nz = nz_{KIND_NAME[m.group(1)]}{m.group(2)}(',
|
||||
code)
|
||||
elif dk:
|
||||
code = KIND_DEF.sub(lambda m: f'f{KIND_NAME[m.group(1)]}{m.group(2)}(&F, ', code)
|
||||
if self.pack[i]:
|
||||
code += f' F.nz = fnz_{dk}(&F);'
|
||||
if 'c->nzcv' in code or COND.search(code) or KIND_DEF.search(code) or CARRY_USE.search(code):
|
||||
raise FlagError(f'flag access left after the rewrite: {code}')
|
||||
return code
|
||||
|
||||
|
||||
def plan(effects, succ, calls, exits):
|
||||
"""effects[i]: (reads, kind or None); succ[i]: successor indices inside the function; calls: indices of calls,
|
||||
whose callee may leave any flags; exits: indices that can leave for another function other than by a call or a
|
||||
return (a tail branch, falling into the next function, a br through rt_dispatch).
|
||||
|
||||
Discovery can start a function inside another one (a branch target, a materialised address), and the code
|
||||
before it then falls or branches into it with live flags. So a tail exit publishes the flags in c->nzcv, and a
|
||||
function whose reads can see its entry flags starts F.nz from c->nzcv. Calls and returns publish nothing: AAPCS64
|
||||
code neither passes flags to a callee nor reads a callee's, so a call's flags count as no definition."""
|
||||
n = len(effects)
|
||||
p = Plan(n)
|
||||
if not any(r or k for r, k in effects):
|
||||
return p
|
||||
p.used = True
|
||||
reach = [set() for _ in range(n)]
|
||||
reach[0].add(ENTRY)
|
||||
work = [0]
|
||||
while work:
|
||||
i = work.pop()
|
||||
out = {i} if effects[i][1] else ({CALL} if i in calls else reach[i])
|
||||
for s in succ[i]:
|
||||
if not out <= reach[s]:
|
||||
reach[s] |= out
|
||||
work.append(s)
|
||||
|
||||
def kind(d):
|
||||
return 'nz' if d == ENTRY else effects[d][1]
|
||||
|
||||
def use(r):
|
||||
"""Kind a use of the definitions r reads; several kinds read the packed form, which they then all write."""
|
||||
kinds = {kind(d) for d in r}
|
||||
if ENTRY in r:
|
||||
p.entry = True
|
||||
if len(kinds) == 1:
|
||||
return kinds.pop()
|
||||
for d in r:
|
||||
if d != ENTRY:
|
||||
p.pack[d] = True
|
||||
return 'nz' # several kinds, or none (unreachable)
|
||||
|
||||
for i, (reads, _) in enumerate(effects):
|
||||
if reads:
|
||||
p.after_call += CALL in reach[i]
|
||||
p.reader[i] = use(reach[i] - {CALL})
|
||||
p.entry_read |= ENTRY in reach[i]
|
||||
for i in exits:
|
||||
# Entry flags alone are still in c->nzcv.
|
||||
r = reach[i] - {CALL}
|
||||
if r and r != {ENTRY}:
|
||||
p.exit[i] = use(r)
|
||||
return p
|
||||
818
recomp/gen.py
Normal file
818
recomp/gen.py
Normal file
@@ -0,0 +1,818 @@
|
||||
# Adapted from LAS Recomp tools/gen.py @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
||||
"""Generate C for the ExeFS modules: function discovery, then parallel emission into chunked C files.
|
||||
|
||||
Usage: gen.py --exefs build/extracted/v121/exefs --out generated/v121 [--modules main,subsdk0,sdk] [--jobs N]
|
||||
[--limit N] [--chunk-fns N] [--chunk-bytes B] [--big-fn B] [--patches FILE]
|
||||
|
||||
Output, under --out:
|
||||
layout.h, layout.json module bases (recomp/layout.py)
|
||||
<mod>/<mod>_NNNN.c one `void f_<addr>(Cpu *c)` per guest function, about --chunk-bytes of C per file
|
||||
<mod>/<mod>_table.c sorted <mod>_fn_addrs[] / <mod>_fn_ptrs[] / <mod>_fn_count for rt_dispatch
|
||||
<mod>/manifest.json chunk list, function count, statistics and timings
|
||||
patches.h code patch sets for the runtime (recomp/patches.py; no sets without --patches)
|
||||
|
||||
Function discovery runs in this process, vectorised over the module's .text words. Function starts are the module
|
||||
seeds (.eh_frame_hdr, exported FUNC symbols, RELATIVE relocation targets), every BL target, and code addresses
|
||||
materialised by ADR or ADRP+ADD (except an ADR that anchors a jump table: one with a `br` in the next 16
|
||||
instructions, or one an ADD register form uses as its base right after). A function is [start, next start). A direct branch whose target lies outside its
|
||||
function makes the target a new start, until no new start appears.
|
||||
|
||||
Emission runs in worker processes, one chunk per task. Each worker loads the modules itself, lifts one function at a
|
||||
time and resolves its jump tables (recomp/jumptables.py), so memory per worker does not grow with the module.
|
||||
Chunks are sized from a per-instruction estimate of the C they produce, so their names and contents only depend on
|
||||
the input; a chunk whose text did not change is not rewritten and keeps its mtime.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
sys.path.insert(0, str(ROOT / 'recomp'))
|
||||
|
||||
import flags # noqa: E402
|
||||
import hostmem # noqa: E402
|
||||
import jumptables # noqa: E402
|
||||
import patches # noqa: E402
|
||||
import regs # noqa: E402
|
||||
from check_content import GENERATED_MARKER # noqa: E402
|
||||
from layout import layout, write as write_layout # noqa: E402
|
||||
from lifter import hx # noqa: E402
|
||||
from module import Module # noqa: E402
|
||||
|
||||
TERMINATORS = ('b', 'br', 'ret', 'trap')
|
||||
MUSTTAIL = '__attribute__((musttail)) return'
|
||||
|
||||
# Estimated bytes of C per instruction, by class, for chunking (estimate_sizes()).
|
||||
# Least squares over every function of main, subsdk0 and sdk (1.2.1) gives 62 / 75 / 48 bytes per instruction, 38
|
||||
# per function and 4.5 per instruction of a function with a `br` (switches; the all-label fallback costs about 60
|
||||
# per instruction but is rare); the median error per function is 7 %.
|
||||
EST_INT = 62
|
||||
EST_SIMD = 75
|
||||
EST_SIMD_LDST = 48
|
||||
EST_FN = 38 # per function: signature, FN_ENTER, closing brace
|
||||
EST_BR_LABELS = 10 # per instruction of a function with a `br`: switch cases, or labels if it falls back
|
||||
|
||||
|
||||
def fname(a):
|
||||
return f'f_{a:x}'
|
||||
|
||||
|
||||
def header(what):
|
||||
return f'// {GENERATED_MARKER}: {what}, generated by recomp/gen.py from game code. Do not edit or commit.\n'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ discovery
|
||||
|
||||
def _sext(v, n):
|
||||
v = v.astype(np.int64)
|
||||
return np.where(v & (1 << (n - 1)), v - (1 << n), v)
|
||||
|
||||
|
||||
def discover(mod):
|
||||
"""Function starts of mod as a sorted int64 array of word indices into mod.words, plus discovery statistics."""
|
||||
w = mod.words
|
||||
n = len(w)
|
||||
|
||||
def idx(mask):
|
||||
return np.flatnonzero(mask).astype(np.int64)
|
||||
|
||||
seeds = (np.asarray(mod.seeds(), dtype=np.int64) - mod.lo) >> 2
|
||||
|
||||
# B and BL: imm26. BL targets are function starts; B targets join the fixpoint below.
|
||||
b_all = idx((w & 0x7C000000) == 0x14000000)
|
||||
imm = _sext(w[b_all] & 0x03FFFFFF, 26)
|
||||
is_bl = (w[b_all] >> 31).astype(bool)
|
||||
bl_tgt = b_all[is_bl] + imm[is_bl]
|
||||
br_src, br_tgt = [b_all[~is_bl]], [b_all[~is_bl] + imm[~is_bl]]
|
||||
# B.cond (the lifter accepts o1 = 1 too), CBZ/CBNZ: imm19 at bit 5. TBZ/TBNZ: imm14 at bit 5.
|
||||
for mask, val, nb in ((0xFE000010, 0x54000000, 19), (0x7E000000, 0x34000000, 19), (0x7E000000, 0x36000000, 14)):
|
||||
s = idx((w & mask) == val)
|
||||
br_src.append(s)
|
||||
br_tgt.append(s + _sext((w[s] >> 5) & ((1 << nb) - 1), nb))
|
||||
br_src, br_tgt = np.concatenate(br_src), np.concatenate(br_tgt)
|
||||
ok = (br_tgt >= 0) & (br_tgt < n)
|
||||
br_src, br_tgt = br_src[ok], br_tgt[ok]
|
||||
|
||||
# ADR: pc + imm21. ADRP: page + (imm21 << 12), then ADD (imm, 64-bit, unshifted) of the same register within
|
||||
# 8 instructions. Targets must be aligned and in .text.
|
||||
def imm21(s):
|
||||
v = w[s]
|
||||
return _sext(((v >> 5) & 0x7FFFF) << 2 | ((v >> 29) & 3), 21)
|
||||
|
||||
pc0 = np.int64(mod.lo)
|
||||
adr = idx((w & 0x9F000000) == 0x10000000)
|
||||
# Clang's jump tables add a table entry to an ADR anchor, the first case block, then `br`. That anchor is not a
|
||||
# function (it would split the switch): an ADR with a `br` in the next 16 instructions is not a seed, nor is one
|
||||
# whose register is the base of an ADD (shifted or extended register) in the next 4 instructions
|
||||
# (`adr xA, L; ldrb wO, [..]; add xA, xA, wO, lsl #2`), however far the scheduler moved the `br`.
|
||||
br = idx((w & 0xFFFFFC1F) == 0xD61F0000)
|
||||
nxt = np.searchsorted(br, adr)
|
||||
has_br = nxt < len(br)
|
||||
near = np.zeros(len(adr), dtype=bool)
|
||||
near[has_br] = br[nxt[has_br]] - adr[has_br] <= 16
|
||||
rd = w[adr] & 31
|
||||
for k in range(1, 5):
|
||||
x = w[np.minimum(adr + k, n - 1)]
|
||||
near |= (((x & 0xFF200000) == 0x8B000000) | ((x & 0xFFE00000) == 0x8B200000)) & (((x >> 5) & 31) == rd)
|
||||
adr = adr[~near]
|
||||
adr_bytes = pc0 + adr * 4 + imm21(adr)
|
||||
is_adrp = (w & 0x9F000000) == 0x90000000
|
||||
add = idx((w & 0xFFC00000) == 0x91000000)
|
||||
rn = ((w[add] >> 5) & 31).astype(np.int64)
|
||||
found = np.full(len(add), -1, dtype=np.int64)
|
||||
for k in range(1, 9):
|
||||
p = add - k
|
||||
cand = (found < 0) & (p >= 0)
|
||||
pc = np.where(cand, p, 0)
|
||||
cand &= is_adrp[pc] & ((w[pc] & 31).astype(np.int64) == rn)
|
||||
found[cand] = p[cand]
|
||||
hit = found >= 0
|
||||
p = found[hit]
|
||||
page = ((pc0 + p * 4) & ~np.int64(0xFFF)) + (imm21(p) << 12)
|
||||
add_bytes = page + ((w[add[hit]] >> 10) & 0xFFF).astype(np.int64)
|
||||
mat = np.concatenate([adr_bytes, add_bytes])
|
||||
mat = mat[(mat >= mod.lo) & (mat < mod.hi) & (mat % 4 == 0)]
|
||||
mat = (mat - pc0) >> 2
|
||||
|
||||
bl_tgt = bl_tgt[(bl_tgt >= 0) & (bl_tgt < n)]
|
||||
starts = np.unique(np.concatenate([seeds, bl_tgt, mat]))
|
||||
stats = {'seeds': int(len(seeds)), 'bl_targets': int(len(np.unique(bl_tgt))),
|
||||
'materialised_targets': int(len(np.unique(mat))), 'initial_starts': int(len(starts))}
|
||||
|
||||
rounds = 0
|
||||
while True:
|
||||
rounds += 1
|
||||
f = np.searchsorted(starts, br_src, side='right') - 1
|
||||
live = f >= 0
|
||||
f = np.where(live, f, 0)
|
||||
lo = starts[f]
|
||||
hi = np.where(f + 1 < len(starts), starts[np.minimum(f + 1, len(starts) - 1)], n)
|
||||
out = live & ((br_tgt < lo) | (br_tgt >= hi))
|
||||
new = np.setdiff1d(br_tgt[out], starts, assume_unique=False)
|
||||
if not len(new):
|
||||
break
|
||||
starts = np.union1d(starts, new)
|
||||
stats['rounds'] = rounds
|
||||
stats['functions'] = int(len(starts))
|
||||
return starts, stats
|
||||
|
||||
|
||||
def estimate_sizes(mod, starts):
|
||||
"""Estimated bytes of C per function (for chunking only; the manifest records the real sizes)."""
|
||||
w = mod.words
|
||||
cost = np.full(len(w), EST_INT, dtype=np.int64)
|
||||
cost[(w & 0x0E000000) == 0x0E000000] = EST_SIMD # FP/AdvSIMD data processing
|
||||
cost[((w & 0x0A000000) == 0x08000000) & (((w >> 26) & 1) == 1)] = EST_SIMD_LDST # FP/SIMD load/store
|
||||
has_br = np.zeros(len(w), dtype=np.int64)
|
||||
has_br[np.flatnonzero((w & 0xFFFFFC1F) == 0xD61F0000)] = 1 # BR Xn
|
||||
csum = np.concatenate([[0], np.cumsum(cost)])
|
||||
bsum = np.concatenate([[0], np.cumsum(has_br)])
|
||||
ends = np.append(starts[1:], len(w))
|
||||
size = csum[ends] - csum[starts] + EST_FN
|
||||
with_br = (bsum[ends] - bsum[starts]) > 0
|
||||
size[with_br] += (ends[with_br] - starts[with_br]) * EST_BR_LABELS
|
||||
return size
|
||||
|
||||
|
||||
def partition(est, max_fns, max_bytes, big_fn):
|
||||
"""Contiguous [i0, i1) function ranges: at most max_fns functions and about max_bytes of C each; a function
|
||||
estimated above big_fn gets a file of its own so large functions compile in parallel."""
|
||||
chunks, i0, size = [], 0, 0
|
||||
for i, s in enumerate(est.tolist()):
|
||||
if s > big_fn:
|
||||
if i > i0:
|
||||
chunks.append((i0, i))
|
||||
chunks.append((i, i + 1))
|
||||
i0, size = i + 1, 0
|
||||
continue
|
||||
if i > i0 and (i - i0 >= max_fns or size + s > max_bytes):
|
||||
chunks.append((i0, i))
|
||||
i0, size = i, 0
|
||||
size += s
|
||||
if len(est) > i0:
|
||||
chunks.append((i0, len(est)))
|
||||
return chunks
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ emission
|
||||
|
||||
class Emitter:
|
||||
"""C for the functions of one module. starts is the set of every function start of the module."""
|
||||
|
||||
def __init__(self, mod, starts, patched=None, reg_locals=regs.LOCALS, callee_saved=True, spins=None):
|
||||
self.mod = mod
|
||||
self.starts = starts
|
||||
self.patched = patched or {} # {address: [(set index, [replacement words])]} (recomp/patches.py)
|
||||
self.spins = spins or {} # {spin wait back-edge: its head} (recomp/patches.py `spin`)
|
||||
self.reg_locals = reg_locals # recomp/regs.py: registers per function in C locals
|
||||
self.kept = regs.CALLEE_SAVED if callee_saved else frozenset() # not reloaded after a call
|
||||
self.uses_patches = False
|
||||
self.stats = Counter()
|
||||
self.unsupported = Counter()
|
||||
|
||||
def scan(self, insns, entry, end):
|
||||
"""Labels and jump tables of one function: {pc: sorted targets} for resolved tables, 'all' for an
|
||||
unresolved computed br."""
|
||||
mod, labels, tables = self.mod, set(), {}
|
||||
for i, ins in enumerate(insns):
|
||||
k = ins.kind
|
||||
if k in ('b', 'bcond'):
|
||||
if entry <= ins.target < end:
|
||||
labels.add(ins.target)
|
||||
elif k == 'br':
|
||||
a = entry + 4 * i
|
||||
jt = jumptables.resolve(mod, a, entry, end)
|
||||
if jt:
|
||||
tables[a] = jt
|
||||
labels.update(jt)
|
||||
self.stats['jump_tables'] += 1
|
||||
self.stats['jump_table_targets'] += len(jt)
|
||||
elif jumptables.is_computed(mod, a, entry):
|
||||
tables[a] = 'all'
|
||||
self.stats['all_label_fallbacks'] += 1
|
||||
else:
|
||||
self.stats['br_dispatch'] += 1
|
||||
elif k == 'trap':
|
||||
self.stats['traps'] += 1
|
||||
c = ins.code
|
||||
if c.startswith('rt_udf') and '/*' in c:
|
||||
self.stats['unsupported'] += 1
|
||||
msg = c[c.index('/*') + 2:c.rindex('*/')].strip()
|
||||
self.unsupported[re.sub(r'\b[0-9a-f]{8}\b', '', msg).strip()] += 1
|
||||
elif k == 'asm':
|
||||
raise ValueError(f'{entry + 4 * i:#x}: asm instruction from the portable lifter')
|
||||
for a in self.patched: # a replacement's branch into the function needs its label
|
||||
if entry <= a < end:
|
||||
for words in self.replacements(a):
|
||||
r = words[-1]
|
||||
if r.kind in ('b', 'bcond') and entry <= r.target < end:
|
||||
labels.add(r.target)
|
||||
return labels, tables
|
||||
|
||||
def replacements(self, a):
|
||||
"""The lifted words of each alternative of the patched site a, in set order. Only the last word of an
|
||||
alternative may branch (bl, b, b.cond / cbz / tbz, ret)."""
|
||||
out = []
|
||||
for _, words in self.patched[a]:
|
||||
lifted = [self.mod.lifter.lift(w, a) for w in words]
|
||||
for j, r in enumerate(lifted):
|
||||
last = j == len(lifted) - 1
|
||||
if r.kind != 'op' and not (last and r.kind in ('bl', 'b', 'bcond', 'ret')):
|
||||
raise patches.PatchError(f'{a:#x}: replacement word {words[j]:08x} is a {r.kind}')
|
||||
out.append(lifted)
|
||||
return out
|
||||
|
||||
def flow(self, insns, entry, end, tables):
|
||||
"""Successors of each instruction inside the function [entry, end). Index len(insns) stands for falling
|
||||
through into the next function and has none. A patched site has the successors of all its alternatives."""
|
||||
def idx(t):
|
||||
return [(t - entry) // 4] if entry <= t < end else []
|
||||
|
||||
def of(ins, i):
|
||||
k = ins.kind
|
||||
if k in ('op', 'bl', 'blr'):
|
||||
return [i + 1]
|
||||
if k == 'b':
|
||||
return idx(ins.target)
|
||||
if k == 'bcond':
|
||||
return idx(ins.target) + [i + 1]
|
||||
if k == 'br':
|
||||
jt = tables.get(entry + 4 * i)
|
||||
return [t for x in jt for t in idx(x)] if jt and jt != 'all' else []
|
||||
return [] # ret, trap
|
||||
|
||||
succ = []
|
||||
for i, ins in enumerate(insns):
|
||||
s = of(ins, i)
|
||||
a = entry + 4 * i
|
||||
if a in self.patched:
|
||||
for words in self.replacements(a):
|
||||
s += [t for t in of(words[-1], i) if t not in s]
|
||||
succ.append(s)
|
||||
succ.append([])
|
||||
return succ
|
||||
|
||||
def flag_plan(self, insns, entry, end, tables, succ):
|
||||
"""recomp/flags.py plan for the function: each instruction's flag use over the flow succ (flow())."""
|
||||
n = len(insns)
|
||||
calls, effects, exits = set(), [], {n}
|
||||
for i, ins in enumerate(insns):
|
||||
k, a = ins.kind, entry + 4 * i
|
||||
if k in ('bl', 'blr'):
|
||||
calls.add(i)
|
||||
elif k in ('b', 'bcond') and not entry <= ins.target < end:
|
||||
exits.add(i)
|
||||
elif k == 'br':
|
||||
exits.add(i) # through rt_dispatch, also a table's default
|
||||
if a in self.patched:
|
||||
effects.append(self.patched_effect(ins, a))
|
||||
for words in self.replacements(a):
|
||||
r = words[-1]
|
||||
if r.kind == 'bl':
|
||||
calls.add(i)
|
||||
elif r.kind in ('b', 'bcond') and not entry <= r.target < end:
|
||||
exits.add(i)
|
||||
else:
|
||||
effects.append(flags.effect(ins.cond if k == 'bcond' else ins.code or ''))
|
||||
effects.append((False, None))
|
||||
return flags.plan(effects, succ, calls, exits)
|
||||
|
||||
def patched_effect(self, ins, a):
|
||||
"""Flag effect of a patched site: every alternative (the original and each replacement) must write the same
|
||||
flags, in at most one word each; the site reads flags when any alternative does (a b.cond reads them)."""
|
||||
alts = []
|
||||
for words in [[ins]] + self.replacements(a):
|
||||
fx = [flags.effect(w.code or '') if w.kind == 'op' else flags.effect(w.cond) for w in words
|
||||
if w.kind in ('op', 'bcond')]
|
||||
fx = [e for e in fx if e[0] or e[1]]
|
||||
if len(fx) > 1:
|
||||
raise patches.PatchError(f'{a:#x}: several flag-using words in one alternative')
|
||||
alts.append(fx[0] if fx else (False, None))
|
||||
if any(e[1] != alts[0][1] for e in alts):
|
||||
raise patches.PatchError(f'{a:#x}: the alternatives write different flags ({[e[1] for e in alts]})')
|
||||
reads = any(e[0] for e in alts)
|
||||
if (alts[0][1] or reads) and (ins.kind == 'bl' or any(w[-1].kind == 'bl' for w in self.replacements(a))):
|
||||
raise patches.PatchError(f'{a:#x}: a patched call that uses flags')
|
||||
return reads, alts[0][1]
|
||||
|
||||
def function(self, entry, end):
|
||||
"""(lines of C, referenced function starts) for the guest function [entry, end)."""
|
||||
mod, starts = self.mod, self.starts
|
||||
insns = [mod.lift(a) for a in range(entry, end, 4)]
|
||||
labels, tables = self.scan(insns, entry, end)
|
||||
refs = set()
|
||||
all_labels = 'all' in tables.values()
|
||||
if all_labels:
|
||||
labels = set(range(entry, end, 4))
|
||||
# An unresolved computed br re-enters the function through fb_<entry>, a C function of its own, which local
|
||||
# flags would not survive; such functions keep NZCV in c->nzcv.
|
||||
# The same holds for registers in locals (recomp/regs.py): fb_<entry> keeps them in the Cpu too.
|
||||
local = not all_labels
|
||||
spill, reload, reload_call = (regs.SPILL, regs.RELOAD, regs.RELOAD_CALL) if local else ('', '', '')
|
||||
succ = self.flow(insns, entry, end, tables) if local else None
|
||||
fp = self.flag_plan(insns, entry, end, tables, succ) if local else None
|
||||
if fp and fp.used:
|
||||
self.stats['local_flag_functions'] += 1
|
||||
self.stats['flag_reads_direct'] += sum(1 for k in fp.reader if k and k != 'nz')
|
||||
self.stats['flag_reads_packed'] += sum(1 for k in fp.reader if k == 'nz')
|
||||
self.stats['flag_entry_functions'] += fp.entry
|
||||
self.stats['flag_entry_reads'] += fp.entry_read
|
||||
self.stats['flag_reads_after_call'] += fp.after_call
|
||||
self.stats['flag_publishing_exits'] += sum(1 for k in fp.exit if k)
|
||||
|
||||
def fx(i, code):
|
||||
return fp.rewrite(i, code) if fp and fp.used else code
|
||||
|
||||
def publish(i):
|
||||
return fp.publish(i) if fp and fp.used else ''
|
||||
|
||||
def jump(t, i):
|
||||
if entry <= t < end:
|
||||
return f'goto L_{t:x};'
|
||||
if t in starts:
|
||||
refs.add(t)
|
||||
return f'{publish(i)}{spill}{MUSTTAIL} {fname(t)}(c);'
|
||||
self.stats['external_branches'] += 1
|
||||
return f'{publish(i)}{spill}c->pc = {hx(t)}; {MUSTTAIL} rt_dispatch(c);'
|
||||
|
||||
if all_labels:
|
||||
# An unresolved computed br re-enters the function through fb_<entry>, which switches on c->pc to any
|
||||
# instruction. A goto to a shared dispatch block inside the body instead turns every instruction into
|
||||
# a loop header, and clang's loop passes then take tens of seconds on one function.
|
||||
fb = f'fb_{entry:x}'
|
||||
cases = ' '.join(f'case {hx(t)}: goto L_{t:x};' for t in range(entry, end, 4))
|
||||
redispatch = f'{MUSTTAIL} {fb}(c);'
|
||||
out = [f'static void {fb}(Cpu *c);',
|
||||
f'void {fname(entry)}(Cpu *c) {{', f' FN_ENTER({hx(entry)});', f' c->pc = {hx(entry)};',
|
||||
f' {redispatch}', '}',
|
||||
f'static void {fb}(Cpu *c) {{', f' switch (c->pc) {{ {cases} default: break; }}',
|
||||
f' {MUSTTAIL} rt_dispatch(c);']
|
||||
else:
|
||||
redispatch = f'{MUSTTAIL} rt_dispatch(c);'
|
||||
out = [f'void {fname(entry)}(Cpu *c) {{', f' FN_ENTER({hx(entry)});']
|
||||
if fp.used:
|
||||
out.append(' ' + fp.decl())
|
||||
|
||||
def call(t, a):
|
||||
if t in starts:
|
||||
refs.add(t)
|
||||
return f'c->x[30] = {hx(a + 4)}; {spill}{fname(t)}(c);{reload_call}'
|
||||
self.stats['external_calls'] += 1
|
||||
return f'c->x[30] = {hx(a + 4)}; {spill}c->pc = {hx(t)}; rt_dispatch(c);{reload_call}'
|
||||
|
||||
def op(i, code):
|
||||
code = fx(i, code)
|
||||
return f'{spill}{code}{reload}' if regs.ESCAPE.search(code) else code
|
||||
|
||||
def one(i, r, a):
|
||||
"""C of one lifted word of a patched site."""
|
||||
if r.kind == 'op':
|
||||
return op(i, r.code)
|
||||
if r.kind == 'bl':
|
||||
return call(r.target, a)
|
||||
if r.kind == 'b':
|
||||
return jump(r.target, i)
|
||||
if r.kind == 'bcond':
|
||||
return f'if ({fx(i, r.cond)}) {{ {jump(r.target, i)} }}'
|
||||
if r.kind == 'ret':
|
||||
return f'{spill}return;'
|
||||
raise patches.PatchError(f'{a:#x}: patched instruction is a {r.kind}')
|
||||
|
||||
def patch(i, ins, a):
|
||||
"""if (switch) { replacement } else if ... else { original } for a patched site (recomp/patches.py)."""
|
||||
if ins.kind not in ('op', 'bl', 'b', 'bcond'):
|
||||
raise patches.PatchError(f'{a:#x}: patched instruction is a {ins.kind}')
|
||||
line = ' '
|
||||
for (k, _), words in zip(self.patched[a], self.replacements(a)):
|
||||
line += f' if (rt_patch_on[{k}]) {{ {" ".join(one(i, r, a) for r in words)} }} else'
|
||||
self.uses_patches = True
|
||||
self.stats['patched_sites'] += 1
|
||||
return f'{line} {{ {one(i, ins, a)} }}'
|
||||
|
||||
body = [] # (instruction index or None for a label, line); a line belongs to the instruction it lifts
|
||||
|
||||
def emit(i, line):
|
||||
body.append((i, line))
|
||||
|
||||
k = None
|
||||
for i, ins in enumerate(insns):
|
||||
a = entry + 4 * i
|
||||
if a in labels:
|
||||
emit(None, f'L_{a:x}:;')
|
||||
k = ins.kind
|
||||
if a in self.patched:
|
||||
emit(i, patch(i, ins, a))
|
||||
elif k == 'op':
|
||||
emit(i, ' ' + op(i, ins.code))
|
||||
elif k == 'trap':
|
||||
emit(i, f' {spill}{fx(i, ins.code)}')
|
||||
elif k in ('b', 'bcond'):
|
||||
j = jump(ins.target, i)
|
||||
if self.spins.get(a) == ins.target and entry <= ins.target < end:
|
||||
j = f'rt_spin_gate(c, {hx(ins.target)}); {j}' # a pass of a spin wait
|
||||
self.stats['spin_gates'] += 1
|
||||
emit(i, ' ' + j if k == 'b' else f' if ({fx(i, ins.cond)}) {{ {j} }}')
|
||||
elif k == 'bl':
|
||||
emit(i, ' ' + call(ins.target, a))
|
||||
elif k == 'blr':
|
||||
emit(i, f' c->pc = {ins.target}; c->x[30] = {hx(a + 4)}; {spill}rt_dispatch(c);{reload_call}')
|
||||
elif k == 'br':
|
||||
jt = tables.get(a)
|
||||
if jt == 'all':
|
||||
emit(i, f' c->pc = {ins.target}; {redispatch}')
|
||||
elif jt:
|
||||
cases = ' '.join(f'case {hx(t)}: goto L_{t:x};' for t in jt)
|
||||
emit(i, f' switch ({ins.target}) {{ {cases} default: break; }}')
|
||||
emit(i, f' {publish(i)}{spill}c->pc = {ins.target}; {redispatch}')
|
||||
else:
|
||||
emit(i, f' {publish(i)}{spill}c->pc = {ins.target}; {MUSTTAIL} rt_dispatch(c);')
|
||||
elif k == 'ret':
|
||||
emit(i, f' {spill}return;')
|
||||
else:
|
||||
raise ValueError(f'{a:#x}: unknown instruction kind {k!r}')
|
||||
n = len(insns)
|
||||
if k not in TERMINATORS or end - 4 in self.patched: # falls through into the next function
|
||||
if end < mod.hi:
|
||||
emit(n, ' ' + jump(end, n))
|
||||
else:
|
||||
emit(n, f' {spill}rt_udf(c, {hx(end)});')
|
||||
if local:
|
||||
texts = [''] * (n + 1)
|
||||
for i, line in body:
|
||||
if i is not None:
|
||||
texts[i] += line
|
||||
clean = {i for i, ins in enumerate(insns) if entry + 4 * i not in self.patched
|
||||
and (ins.kind in ('bl', 'blr') or (ins.kind == 'op' and regs.RELOAD in texts[i]))}
|
||||
rp = regs.plan(texts, succ, clean, self.reg_locals, self.kept)
|
||||
if rp.regs:
|
||||
out.append(' ' + rp.decl())
|
||||
self.stats['local_reg_functions'] += bool(rp.regs)
|
||||
self.stats['reg_spills'] += sum(len(s) for s in rp.spill)
|
||||
out += [line if i is None else rp.resolve(i, line) for i, line in body]
|
||||
else:
|
||||
out += [line for _, line in body]
|
||||
out.append('}')
|
||||
self.stats['functions'] += 1
|
||||
self.stats['instructions'] += len(insns)
|
||||
return out, refs
|
||||
|
||||
|
||||
def chunk_text(name, number, emitter, fns):
|
||||
"""C text of one chunk. fns: [(entry, end)]."""
|
||||
body, refs = [], set()
|
||||
emitter.uses_patches = False
|
||||
for entry, end in fns:
|
||||
lines, r = emitter.function(entry, end)
|
||||
body.append('\n'.join(lines))
|
||||
refs |= r
|
||||
# Every referenced function gets a prototype, including ones defined later in this chunk.
|
||||
protos = ''.join(f'void {fname(r)}(Cpu *c);\n' for r in sorted(refs))
|
||||
if emitter.uses_patches:
|
||||
protos += 'extern uint8_t rt_patch_on[];\n'
|
||||
return (header(f'{name} chunk {number:04d}') + '#include "cpu.h"\n#include "simd.h"\n\n' + protos + '\n'
|
||||
+ '\n\n'.join(body) + '\n')
|
||||
|
||||
|
||||
def write_if_changed(path, text):
|
||||
"""Returns True if the file was written. An unchanged file keeps its mtime, so the build skips it. A changed one
|
||||
is written under a temporary name and renamed into place, so a reader never sees it half written."""
|
||||
data = text.encode()
|
||||
try:
|
||||
if path.stat().st_size == len(data) and path.read_bytes() == data:
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
tmp = path.with_name(f'.{path.name}.{os.getpid()}.tmp')
|
||||
tmp.write_bytes(data)
|
||||
try:
|
||||
_retry(os.replace, tmp, path)
|
||||
except OSError:
|
||||
tmp.unlink(missing_ok=True)
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def _retry(fn, *args):
|
||||
"""Windows refuses to rewrite or delete a file another process has open or mapped (clang maps its sources, so a
|
||||
build running on the output holds them; so can a scanner or an editor). Waits up to 30 s for it."""
|
||||
for i in range(120):
|
||||
try:
|
||||
return fn(*args)
|
||||
except OSError as e:
|
||||
if i == 119 or isinstance(e, FileNotFoundError):
|
||||
raise
|
||||
time.sleep(0.25)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ workers
|
||||
|
||||
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
|
||||
|
||||
|
||||
def _worker_init(mods, starts, table, opts, jobs):
|
||||
_W['jobs'] = jobs
|
||||
_W['mods'] = mods
|
||||
_W['starts'] = starts
|
||||
_W['patches'] = table
|
||||
_W['opts'] = opts # Emitter keyword arguments
|
||||
_W['emitters'] = {}
|
||||
|
||||
|
||||
def _emitter(name):
|
||||
e = _W['emitters'].get(name)
|
||||
if e is None:
|
||||
path, base = _W['mods'][name]
|
||||
mod = Module(name, path, base, 'c')
|
||||
e = _W['emitters'][name] = Emitter(mod, set(_W['starts'][name].tolist()),
|
||||
_W['patches'].for_module(name, base),
|
||||
spins=_W['patches'].spins_for(name, base), **_W['opts'])
|
||||
return e
|
||||
|
||||
|
||||
def _emit_chunk(task):
|
||||
"""Worker: write one chunk. Returns its statistics."""
|
||||
name, number, path, fns = task
|
||||
t0 = time.perf_counter()
|
||||
# Test hook: a pool wider than this raises the MemoryError a busy machine does, so the retry can be exercised.
|
||||
limit = os.environ.get('NX2NAVI_LIFT_TEST_OOM_ABOVE')
|
||||
if limit and _W['jobs'] > int(limit) and number % 2 == 1:
|
||||
raise MemoryError
|
||||
e = _emitter(name)
|
||||
e.stats.clear()
|
||||
e.unsupported.clear()
|
||||
text = chunk_text(name, number, e, fns)
|
||||
error = None
|
||||
try:
|
||||
written = write_if_changed(Path(path), text)
|
||||
except OSError as ex:
|
||||
written, error = False, f'{path}: {ex}'
|
||||
return {'module': name, 'number': number, 'file': Path(path).name, 'functions': len(fns), 'bytes': len(text),
|
||||
'written': written, 'error': error, 'seconds': time.perf_counter() - t0, 'stats': dict(e.stats),
|
||||
'unsupported': dict(e.unsupported)}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ driver
|
||||
|
||||
def table_text(name, addrs):
|
||||
lines = [header(f'{name} function table'), '#include "cpu.h"\n\n']
|
||||
lines += [f'void {fname(a)}(Cpu *c);\n' for a in addrs]
|
||||
lines.append(f'\nconst uint64_t {name}_fn_addrs[] = {{\n')
|
||||
lines.append(',\n'.join(hx(a) for a in addrs))
|
||||
lines.append(f'\n}};\nconst GuestFn {name}_fn_ptrs[] = {{\n')
|
||||
lines.append(',\n'.join(fname(a) for a in addrs))
|
||||
lines.append(f'\n}};\nconst uint64_t {name}_fn_count = {len(addrs)};\n')
|
||||
return ''.join(lines)
|
||||
|
||||
|
||||
def remove_stale(moddir, name, nchunks, errors):
|
||||
"""Delete chunks an earlier run wrote beyond this run's count, and temporary files an interrupted run left."""
|
||||
pat = re.compile(re.escape(name) + r'_(\d{4})\.c')
|
||||
removed = 0
|
||||
for p in moddir.iterdir():
|
||||
m = pat.fullmatch(p.name)
|
||||
if (m and int(m.group(1)) >= nchunks) or (p.name.startswith('.') and p.name.endswith('.tmp')):
|
||||
try:
|
||||
_retry(p.unlink)
|
||||
removed += 1
|
||||
except OSError as ex:
|
||||
errors.append(f'{p}: {ex}')
|
||||
return removed
|
||||
|
||||
|
||||
# Peak memory of one emit worker on 1.4.3 is 1.3 GiB (1.5 GiB with margin); the parent holds about 0.7 GiB.
|
||||
WORKER_MEM = 1536 << 20
|
||||
MEM_SHARE = 0.8 # of the free memory the workers may take, the rest is for the system and other programs
|
||||
|
||||
|
||||
def pick_jobs(requested):
|
||||
"""Worker count and the reason for it: --jobs if given, else the CPU count capped by the free memory."""
|
||||
if requested:
|
||||
return requested, '--jobs'
|
||||
env = os.environ.get('NX2NAVI_LIFT_JOBS', '').strip()
|
||||
if env:
|
||||
try:
|
||||
if int(env) >= 1:
|
||||
return int(env), 'NX2NAVI_LIFT_JOBS'
|
||||
except ValueError:
|
||||
pass
|
||||
print(f'ignoring NX2NAVI_LIFT_JOBS={env!r}: not a positive integer', file=sys.stderr, flush=True)
|
||||
cpus = os.cpu_count() or 4
|
||||
free = hostmem.available_ram()
|
||||
if free is None:
|
||||
return cpus, f'{cpus} CPUs, free memory unknown'
|
||||
by_mem = max(1, int(free * MEM_SHARE) // WORKER_MEM)
|
||||
return min(cpus, by_mem), f'{cpus} CPUs, {free / 2**30:.1f} GiB free memory allows {by_mem}'
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument('--exefs', type=Path, required=True)
|
||||
ap.add_argument('--out', type=Path, required=True)
|
||||
ap.add_argument('--modules', default='main,subsdk0,sdk')
|
||||
ap.add_argument('--jobs', type=int, help='emit workers (default: by free memory and CPU count)')
|
||||
ap.add_argument('--limit', type=int, default=0, help='emit only the first N functions of each module')
|
||||
ap.add_argument('--chunk-fns', type=int, default=3000, help='functions per chunk at most')
|
||||
ap.add_argument('--chunk-bytes', type=int, default=3000000, help='estimated bytes of C per chunk')
|
||||
ap.add_argument('--big-fn', type=int, default=300000, help='estimated bytes of C from which a function gets '
|
||||
'its own chunk')
|
||||
ap.add_argument('--patches', type=Path, help='code patch table (recomp/patches.py)')
|
||||
ap.add_argument('--reg-locals', type=int, default=regs.LOCALS,
|
||||
help=f'guest registers per function kept in C locals (recomp/regs.py; default {regs.LOCALS}, 0: none)')
|
||||
ap.add_argument('--no-callee-saved-trust', action='store_true',
|
||||
help='reload X19-X29 and SP after every call too, instead of trusting AAPCS64 (slower; for '
|
||||
'telling a broken callee from a generator bug)')
|
||||
args = ap.parse_args()
|
||||
opts = {'reg_locals': args.reg_locals, 'callee_saved': not args.no_callee_saved_trust}
|
||||
|
||||
t0 = time.perf_counter()
|
||||
mods = layout(args.exefs)
|
||||
write_layout(mods, args.out)
|
||||
table = patches.load(args.patches, mods) if args.patches else patches.Table()
|
||||
write_if_changed(args.out / 'patches.h', patches.header_text(table, GENERATED_MARKER))
|
||||
want = args.modules.split(',')
|
||||
unknown = set(want) - {n for n, _, _ in mods}
|
||||
if unknown:
|
||||
raise SystemExit(f'not in {args.exefs}: {", ".join(sorted(unknown))}')
|
||||
|
||||
paths, starts_abs, info, tasks = {}, {}, {}, []
|
||||
for name, base, _ in mods:
|
||||
if name not in want:
|
||||
continue
|
||||
t = time.perf_counter()
|
||||
path = args.exefs / name
|
||||
mod = Module(name, path, base, 'c')
|
||||
patches.check(table, mod, name)
|
||||
starts, dstats = discover(mod)
|
||||
est = estimate_sizes(mod, starts)
|
||||
abs_starts = mod.lo + starts * 4
|
||||
n = len(starts) if not args.limit else min(args.limit, len(starts))
|
||||
ends = np.append(abs_starts[1:], mod.hi)
|
||||
chunks = partition(est[:n], args.chunk_fns, args.chunk_bytes, args.big_fn)
|
||||
moddir = args.out / name
|
||||
moddir.mkdir(parents=True, exist_ok=True)
|
||||
for number, (i0, i1) in enumerate(chunks):
|
||||
fns = list(zip(abs_starts[i0:i1].tolist(), ends[i0:i1].tolist()))
|
||||
tasks.append((int(est[i0:i1].sum()), (name, number, str(moddir / f'{name}_{number:04d}.c'), fns)))
|
||||
paths[name] = (str(path), base)
|
||||
starts_abs[name] = abs_starts
|
||||
info[name] = {'base': base, 'lo': mod.lo, 'hi': mod.hi, 'emitted': abs_starts[:n].tolist(),
|
||||
'chunks': len(chunks), 'discover': dstats, 'discover_seconds': time.perf_counter() - t,
|
||||
'moddir': moddir}
|
||||
print(f'{name}: {dstats["functions"]} functions ({dstats["seeds"]} seeds, {dstats["rounds"]} rounds), '
|
||||
f'{len(chunks)} chunks, discovery {time.perf_counter() - t:.1f}s', file=sys.stderr, flush=True)
|
||||
del mod
|
||||
|
||||
# Largest chunks first, so the pool does not end on a long tail.
|
||||
tasks.sort(key=lambda t: -t[0])
|
||||
results = {name: [] for name in info}
|
||||
done_at, errors = {}, []
|
||||
t_emit = time.perf_counter()
|
||||
ctx = mp.get_context('spawn')
|
||||
jobs, why = pick_jobs(args.jobs)
|
||||
pending = [t for _, t in tasks]
|
||||
emitted = 0
|
||||
while True:
|
||||
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
|
||||
try:
|
||||
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
|
||||
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
|
||||
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
|
||||
results[r['module']].append(r)
|
||||
if r['error']:
|
||||
errors.append(r['error'])
|
||||
done_at[r['module']] = time.perf_counter() - t_emit
|
||||
emitted += 1
|
||||
if emitted % 200 == 0 or emitted == len(tasks):
|
||||
print(f'emitted {emitted}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s',
|
||||
file=sys.stderr, flush=True)
|
||||
break
|
||||
except MemoryError:
|
||||
if jobs == 1:
|
||||
raise
|
||||
smaller = jobs // 2
|
||||
print(f'emit: out of memory with {jobs} workers, retrying with {smaller}', file=sys.stderr, flush=True)
|
||||
jobs, why = smaller, 'after out of memory'
|
||||
done = {(r['module'], r['number']) for rs in results.values() for r in rs}
|
||||
pending = [t for t in pending if (t[0], t[1]) not in done]
|
||||
|
||||
for name, d in info.items():
|
||||
rs = sorted(results[name], key=lambda r: r['number'])
|
||||
moddir = d['moddir']
|
||||
stats, unsupported = Counter(), Counter()
|
||||
for r in rs:
|
||||
stats.update(r['stats'])
|
||||
unsupported.update(r['unsupported'])
|
||||
t = time.perf_counter()
|
||||
table_written = write_if_changed(moddir / f'{name}_table.c', table_text(name, d['emitted']))
|
||||
removed = remove_stale(moddir, name, d['chunks'], errors)
|
||||
manifest = {
|
||||
'module': name,
|
||||
'marker': GENERATED_MARKER,
|
||||
'base': f'{d["base"]:#x}',
|
||||
'text': [f'{d["lo"]:#x}', f'{d["hi"]:#x}'],
|
||||
'function_count': len(d['emitted']),
|
||||
'chunks': [r['file'] for r in rs],
|
||||
'table': f'{name}_table.c',
|
||||
'chunk_info': [{'file': r['file'], 'functions': r['functions'], 'bytes': r['bytes'],
|
||||
'seconds': round(r['seconds'], 2)} for r in rs],
|
||||
'stats': {
|
||||
**d['discover'],
|
||||
'emitted_functions': stats['functions'],
|
||||
'instructions': stats['instructions'],
|
||||
'jump_tables': stats['jump_tables'],
|
||||
'jump_table_targets': stats['jump_table_targets'],
|
||||
'all_label_fallbacks': stats['all_label_fallbacks'],
|
||||
'br_dispatch': stats['br_dispatch'],
|
||||
'traps': stats['traps'],
|
||||
'patched_sites': stats['patched_sites'],
|
||||
'local_flag_functions': stats['local_flag_functions'],
|
||||
'flag_reads_direct': stats['flag_reads_direct'],
|
||||
'flag_reads_packed': stats['flag_reads_packed'],
|
||||
'flag_entry_functions': stats['flag_entry_functions'],
|
||||
'flag_entry_reads': stats['flag_entry_reads'],
|
||||
'flag_reads_after_call': stats['flag_reads_after_call'],
|
||||
'flag_publishing_exits': stats['flag_publishing_exits'],
|
||||
'spin_gates': stats['spin_gates'],
|
||||
'unsupported': stats['unsupported'],
|
||||
'external_branches': stats['external_branches'],
|
||||
'external_calls': stats['external_calls'],
|
||||
'c_bytes': sum(r['bytes'] for r in rs),
|
||||
'largest_chunk_bytes': max((r['bytes'] for r in rs), default=0),
|
||||
'chunks_written': sum(r['written'] for r in rs) + table_written,
|
||||
'stale_chunks_removed': removed,
|
||||
},
|
||||
'unsupported_census': dict(unsupported.most_common(50)),
|
||||
'timings': {
|
||||
'discover_s': round(d['discover_seconds'], 2),
|
||||
'emit_wall_s': round(done_at.get(name, 0.0), 2),
|
||||
'emit_worker_s': round(sum(r['seconds'] for r in rs), 2),
|
||||
'table_s': round(time.perf_counter() - t, 2),
|
||||
},
|
||||
}
|
||||
write_if_changed(moddir / 'manifest.json', json.dumps(manifest, indent=1) + '\n')
|
||||
s = manifest['stats']
|
||||
print(f'{name}: {s["emitted_functions"]} functions, {len(rs)} chunks, {s["c_bytes"] / 1e6:.0f} MB of C, '
|
||||
f'{s["jump_tables"]} jump tables, {s["all_label_fallbacks"]} all-label fallbacks, '
|
||||
f'{s["br_dispatch"]} br via dispatch, {s["traps"]} traps ({s["unsupported"]} unsupported)',
|
||||
file=sys.stderr)
|
||||
print(f'total {time.perf_counter() - t0:.0f}s', file=sys.stderr)
|
||||
if errors:
|
||||
print(f'{len(errors)} files could not be written or removed (held open by another process?):',
|
||||
file=sys.stderr)
|
||||
for e in errors:
|
||||
print(f' {e}', file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
1345
recomp/jumptables.py
Normal file
1345
recomp/jumptables.py
Normal file
File diff suppressed because it is too large
Load Diff
61
recomp/layout.py
Normal file
61
recomp/layout.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Guest address layout of the loaded modules, computed from their sizes.
|
||||
|
||||
Guest addresses equal host addresses, and the lifter writes absolute addresses into the generated C, so the
|
||||
generator and the runtime must agree on every module base. This is the single place that decides them: the generator
|
||||
calls layout() and writes the result into generated/<version>/layout.h and layout.json, which the runtime and the
|
||||
build read.
|
||||
|
||||
Modules are placed in Horizon's load order (rtld is dropped; the host links the modules):
|
||||
main, subsdk0..9, sdk. Each base is aligned to 2 MiB. Everything must stay below 2^39, the limit of the addresses
|
||||
IPC buffers can encode.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from nso import Nso
|
||||
|
||||
FIRST_BASE = 0x7000000000
|
||||
ALIGN = 0x200000
|
||||
ADDRESS_LIMIT = 1 << 39
|
||||
ORDER = ['main'] + [f'subsdk{i}' for i in range(10)] + ['sdk']
|
||||
|
||||
|
||||
def align_up(v, a=ALIGN):
|
||||
return (v + a - 1) & ~(a - 1)
|
||||
|
||||
|
||||
def layout(exefs):
|
||||
"""[(name, base, size)] for the modules present in the ExeFS directory, in load order."""
|
||||
exefs = Path(exefs)
|
||||
out, base = [], FIRST_BASE
|
||||
for name in ORDER:
|
||||
path = exefs / name
|
||||
if not path.exists():
|
||||
continue
|
||||
size = Nso(str(path)).size
|
||||
out.append((name, base, size))
|
||||
base = align_up(base + size)
|
||||
if base > ADDRESS_LIMIT:
|
||||
raise ValueError(f'modules end at {base:#x}, above 2^39')
|
||||
return out
|
||||
|
||||
|
||||
def write(mods, outdir):
|
||||
"""layout.json for Python tools and layout.h for the runtime."""
|
||||
outdir = Path(outdir)
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
(outdir / 'layout.json').write_text(json.dumps(
|
||||
[{'name': n, 'base': f'{b:#x}', 'size': f'{s:#x}'} for n, b, s in mods], indent=1) + '\n')
|
||||
lines = ['// Module layout chosen by recomp/layout.py. Generated; do not edit.', '#pragma once', '']
|
||||
for n, b, s in mods:
|
||||
lines.append(f'#define MOD_{n.upper()}_BASE {b:#x}ull')
|
||||
lines.append(f'#define MOD_{n.upper()}_SIZE {s:#x}ull')
|
||||
lines += ['', f'#define MOD_COUNT {len(mods)}',
|
||||
'#define MOD_LIST(X) ' + ' '.join(f'X({n}, {n.upper()})' for n, _, _ in mods), '']
|
||||
(outdir / 'layout.h').write_text('\n'.join(lines))
|
||||
|
||||
|
||||
def read(outdir):
|
||||
"""[(name, base, size)] from a layout.json written by write()."""
|
||||
data = json.loads((Path(outdir) / 'layout.json').read_text())
|
||||
return [(m['name'], int(m['base'], 16), int(m['size'], 16)) for m in data]
|
||||
805
recomp/lifter.py
Normal file
805
recomp/lifter.py
Normal file
@@ -0,0 +1,805 @@
|
||||
# Adapted from LAS Recomp tools/lifter.py @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
||||
"""AArch64 -> C static recompiler.
|
||||
|
||||
Integer, memory, and control flow are translated to C. FP/SIMD instructions (and CRC32) are
|
||||
either re-emitted as inline assembly (simd='asm', AArch64 hosts), with guest V registers
|
||||
loaded from / stored to the Cpu struct around each batch, so their semantics are
|
||||
bit-exact by construction, or lowered to portable C (simd='c', tools/simd_c.py + runtime/simd.h).
|
||||
"""
|
||||
import struct
|
||||
|
||||
# Cpu struct offsets (must match runtime/cpu.h)
|
||||
OFF_X = 0
|
||||
OFF_SP = 256
|
||||
OFF_PC = 264
|
||||
OFF_NZCV = 272
|
||||
OFF_V = 512
|
||||
|
||||
COND_NAMES = ['eq', 'ne', 'cs', 'cc', 'mi', 'pl', 'vs', 'vc', 'hi', 'ls', 'ge', 'lt', 'gt', 'le', 'al', 'nv']
|
||||
SCRATCH = [9, 10, 11, 12, 13, 14, 15, 16, 17]
|
||||
|
||||
|
||||
class Unsupported(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def bits(w, lo, n):
|
||||
return (w >> lo) & ((1 << n) - 1)
|
||||
|
||||
|
||||
def sext(v, n):
|
||||
return v - (1 << n) if v & (1 << (n - 1)) else v
|
||||
|
||||
|
||||
def ones(n):
|
||||
return (1 << n) - 1
|
||||
|
||||
|
||||
def ror(v, r, size):
|
||||
r %= size
|
||||
return ((v >> r) | (v << (size - r))) & ones(size) if r else v
|
||||
|
||||
|
||||
def replicate(v, esize, size):
|
||||
out = 0
|
||||
for i in range(0, size, esize):
|
||||
out |= v << i
|
||||
return out
|
||||
|
||||
|
||||
def decode_bit_masks(N, imms, immr, size, immediate):
|
||||
x = (N << 6) | (~imms & 0x3F)
|
||||
if x == 0:
|
||||
raise Unsupported('reserved bitmask')
|
||||
length = x.bit_length() - 1
|
||||
if length < 1:
|
||||
raise Unsupported('reserved bitmask')
|
||||
levels = ones(length)
|
||||
if immediate and (imms & levels) == levels:
|
||||
raise Unsupported('reserved bitmask')
|
||||
S = imms & levels
|
||||
R = immr & levels
|
||||
diff = (S - R) & 0x3F
|
||||
esize = 1 << length
|
||||
d = diff & ones(length)
|
||||
welem = ones(S + 1)
|
||||
telem = ones(d + 1)
|
||||
wmask = replicate(ror(welem, R, esize), esize, size)
|
||||
tmask = replicate(telem, esize, size)
|
||||
return wmask, tmask
|
||||
|
||||
|
||||
def hx(v):
|
||||
return f'0x{v:x}ull'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- register helpers
|
||||
|
||||
def X(r):
|
||||
return '0ull' if r == 31 else f'c->x[{r}]'
|
||||
|
||||
|
||||
def XS(r):
|
||||
return 'c->sp' if r == 31 else f'c->x[{r}]'
|
||||
|
||||
|
||||
def W(r):
|
||||
return '0u' if r == 31 else f'(uint32_t)c->x[{r}]'
|
||||
|
||||
|
||||
def WS(r):
|
||||
return '(uint32_t)c->sp' if r == 31 else f'(uint32_t)c->x[{r}]'
|
||||
|
||||
|
||||
def R(sf, r, sp=False):
|
||||
if sf:
|
||||
return XS(r) if sp else X(r)
|
||||
return WS(r) if sp else W(r)
|
||||
|
||||
|
||||
def set_reg(sf, r, expr, sp=False):
|
||||
"""Statement writing expr to Rd (zero-extending 32-bit results)."""
|
||||
if r == 31 and not sp:
|
||||
return f'(void)({expr});'
|
||||
dst = 'c->sp' if r == 31 else f'c->x[{r}]'
|
||||
if sf:
|
||||
return f'{dst} = (uint64_t)({expr});'
|
||||
return f'{dst} = (uint32_t)({expr});'
|
||||
|
||||
|
||||
def shift_expr(sf, val, typ, amt):
|
||||
if amt == 0 and typ != 3:
|
||||
return val
|
||||
if typ == 0:
|
||||
return f'({val} << {amt})'
|
||||
if typ == 1:
|
||||
return f'({val} >> {amt})'
|
||||
if typ == 2:
|
||||
return f'(uint64_t)((int64_t){val} >> {amt})' if sf else f'(uint32_t)((int32_t){val} >> {amt})'
|
||||
return f'ROR{64 if sf else 32}({val}, {amt})'
|
||||
|
||||
|
||||
def extend_expr(r, option, shift):
|
||||
base = X(r)
|
||||
e = {
|
||||
0: f'(uint64_t)(uint8_t){base}',
|
||||
1: f'(uint64_t)(uint16_t){base}',
|
||||
2: f'(uint64_t)(uint32_t){base}',
|
||||
3: base,
|
||||
4: f'(uint64_t)(int64_t)(int8_t){base}',
|
||||
5: f'(uint64_t)(int64_t)(int16_t){base}',
|
||||
6: f'(uint64_t)(int64_t)(int32_t){base}',
|
||||
7: base,
|
||||
}[option]
|
||||
return f'({e} << {shift})' if shift else e
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- instruction classes
|
||||
|
||||
class Insn:
|
||||
"""Result of lifting one instruction."""
|
||||
__slots__ = ('kind', 'code', 'target', 'cond', 'asm')
|
||||
|
||||
def __init__(self, kind, code='', target=None, cond=None, asm=None):
|
||||
self.kind = kind # 'op', 'asm', 'b', 'bcond', 'bl', 'blr', 'br', 'ret', 'trap'
|
||||
self.code = code # C statements (for 'op'), or condition expr for 'bcond'
|
||||
self.target = target # branch target (int) or register expr
|
||||
self.cond = cond
|
||||
self.asm = asm # AsmInsn for 'asm'
|
||||
|
||||
|
||||
class AsmInsn:
|
||||
"""An FP/SIMD instruction to pass through as inline asm."""
|
||||
__slots__ = ('word', 'vregs', 'gprs', 'flags', 'consts')
|
||||
|
||||
def __init__(self, word, vregs, gprs, flags=False, consts=None):
|
||||
self.word = word
|
||||
self.vregs = vregs # set of V register numbers touched
|
||||
self.gprs = gprs # list of (bit position, guest reg or 'sp')
|
||||
self.flags = flags
|
||||
self.consts = consts or [] # list of (bit position, constant) loaded into scratch
|
||||
|
||||
|
||||
def is_fp_simd_dp(w):
|
||||
return bits(w, 25, 3) == 0b111
|
||||
|
||||
|
||||
def is_ldst(w):
|
||||
return bits(w, 25, 1) == 0 and bits(w, 27, 1) == 1
|
||||
|
||||
|
||||
class Lifter:
|
||||
def __init__(self, base, simd='asm'):
|
||||
self.base = base # absolute load address of the module (guest == host)
|
||||
if simd not in ('asm', 'c'):
|
||||
raise ValueError(f'simd={simd!r}')
|
||||
self.sc = None
|
||||
if simd == 'c':
|
||||
# simd_c imports lifter at module level, so this import cannot move to the top.
|
||||
from simd_c import SimdC
|
||||
self.sc = SimdC()
|
||||
|
||||
def lift(self, w, pc):
|
||||
"""pc is absolute. Returns an Insn."""
|
||||
op0 = bits(w, 25, 4)
|
||||
if (op0 & 0b1110) == 0b1000:
|
||||
return self.dp_imm(w, pc)
|
||||
if (op0 & 0b1110) == 0b1010:
|
||||
return self.branch_sys(w, pc)
|
||||
if (op0 & 0b0101) == 0b0100:
|
||||
return self.ldst(w, pc)
|
||||
if (op0 & 0b0111) == 0b0101:
|
||||
return self.dp_reg(w, pc)
|
||||
if (op0 & 0b0111) == 0b0111:
|
||||
return self.fp_simd(w, pc)
|
||||
# padding, trap words (e7ffdefe), header data: trap if ever executed
|
||||
return Insn('trap', f'rt_udf(c, {hx(pc)});')
|
||||
|
||||
# ------------------------------------------------------------ data processing (imm)
|
||||
|
||||
def dp_imm(self, w, pc):
|
||||
op = bits(w, 23, 3)
|
||||
sf = bits(w, 31, 1)
|
||||
rd, rn = bits(w, 0, 5), bits(w, 5, 5)
|
||||
if op in (0, 1): # PC-relative
|
||||
imm = sext((bits(w, 5, 19) << 2) | bits(w, 29, 2), 21)
|
||||
if bits(w, 31, 1):
|
||||
val = (pc & ~0xFFF) + (imm << 12)
|
||||
else:
|
||||
val = pc + imm
|
||||
return Insn('op', set_reg(1, rd, hx(val & ones(64))))
|
||||
if op == 2: # add/sub imm
|
||||
opc, S, sh = bits(w, 30, 1), bits(w, 29, 1), bits(w, 22, 1)
|
||||
imm = bits(w, 10, 12) << (12 if sh else 0)
|
||||
a = R(sf, rn, sp=True)
|
||||
if S:
|
||||
fn = ('subs' if opc else 'adds') + ('64' if sf else '32')
|
||||
return Insn('op', set_reg(sf, rd, f'{fn}(c, {a}, {hx(imm)})'))
|
||||
return Insn('op', set_reg(sf, rd, f'{a} {"-" if opc else "+"} {hx(imm)}', sp=True))
|
||||
if op == 4: # logical imm
|
||||
opc, N = bits(w, 29, 2), bits(w, 22, 1)
|
||||
if not sf and N:
|
||||
raise Unsupported('logical imm N=1 32-bit')
|
||||
size = 64 if sf else 32
|
||||
imm, _ = decode_bit_masks(N, bits(w, 10, 6), bits(w, 16, 6), size, True)
|
||||
a = R(sf, rn)
|
||||
if opc == 0:
|
||||
return Insn('op', set_reg(sf, rd, f'{a} & {hx(imm)}', sp=True))
|
||||
if opc == 1:
|
||||
return Insn('op', set_reg(sf, rd, f'{a} | {hx(imm)}', sp=True))
|
||||
if opc == 2:
|
||||
return Insn('op', set_reg(sf, rd, f'{a} ^ {hx(imm)}', sp=True))
|
||||
return Insn('op', set_reg(sf, rd, f'logic{size}(c, {a} & {hx(imm)})'))
|
||||
if op == 5: # move wide
|
||||
opc, hw = bits(w, 29, 2), bits(w, 21, 2)
|
||||
if not sf and hw > 1:
|
||||
raise Unsupported('movw hw')
|
||||
imm = bits(w, 5, 16) << (hw * 16)
|
||||
if opc == 0:
|
||||
return Insn('op', set_reg(sf, rd, hx(~imm & ones(64 if sf else 32))))
|
||||
if opc == 2:
|
||||
return Insn('op', set_reg(sf, rd, hx(imm)))
|
||||
if opc == 3:
|
||||
mask = ~(0xFFFF << (hw * 16)) & ones(64)
|
||||
return Insn('op', set_reg(sf, rd, f'({X(rd)} & {hx(mask)}) | {hx(imm)}'))
|
||||
raise Unsupported('movw opc')
|
||||
if op == 6: # bitfield
|
||||
opc, N = bits(w, 29, 2), bits(w, 22, 1)
|
||||
immr, imms = bits(w, 16, 6), bits(w, 10, 6)
|
||||
size = 64 if sf else 32
|
||||
if N != sf:
|
||||
raise Unsupported('bitfield N')
|
||||
wmask, tmask = decode_bit_masks(N, imms, immr, size, False)
|
||||
src = R(sf, rn)
|
||||
rot = f'ROR{size}({src}, {immr})' if immr else src
|
||||
if opc == 2: # UBFM
|
||||
return Insn('op', set_reg(sf, rd, f'{rot} & {hx(wmask & tmask)}'))
|
||||
if opc == 0: # SBFM
|
||||
top = f'(uint{size}_t)(-(int{size}_t)(({src} >> {imms}) & 1))'
|
||||
return Insn('op', set_reg(
|
||||
sf, rd, f'({top} & {hx(~tmask & ones(size))}) | ({rot} & {hx(wmask & tmask)})'))
|
||||
if opc == 1: # BFM
|
||||
dst = R(sf, rd)
|
||||
bot = f'(({dst} & {hx(~wmask & ones(size))}) | ({rot} & {hx(wmask)}))'
|
||||
return Insn('op', set_reg(
|
||||
sf, rd, f'({dst} & {hx(~tmask & ones(size))}) | ({bot} & {hx(tmask)})'))
|
||||
raise Unsupported('bitfield opc')
|
||||
if op == 7: # EXTR
|
||||
rm, lsb = bits(w, 16, 5), bits(w, 10, 6)
|
||||
size = 64 if sf else 32
|
||||
if lsb == 0:
|
||||
return Insn('op', set_reg(sf, rd, R(sf, rm)))
|
||||
return Insn('op', set_reg(
|
||||
sf, rd, f'({R(sf, rm)} >> {lsb}) | ((uint{size}_t){R(sf, rn)} << {size - lsb})'))
|
||||
raise Unsupported(f'dp_imm {w:08x}')
|
||||
|
||||
# ------------------------------------------------------------ branches / system
|
||||
|
||||
def branch_sys(self, w, pc):
|
||||
if bits(w, 26, 5) == 0b00101: # B / BL
|
||||
t = (pc + sext(bits(w, 0, 26), 26) * 4) & ones(64)
|
||||
return Insn('bl' if bits(w, 31, 1) else 'b', target=t)
|
||||
if bits(w, 25, 7) == 0b0101010 and not bits(w, 4, 1): # B.cond
|
||||
t = pc + sext(bits(w, 5, 19), 19) * 4
|
||||
cond = bits(w, 0, 4)
|
||||
if cond >= 14:
|
||||
return Insn('b', target=t)
|
||||
return Insn('bcond', target=t, cond=f'COND_{COND_NAMES[cond].upper()}(c)')
|
||||
if bits(w, 25, 6) == 0b011010: # CBZ/CBNZ
|
||||
sf, nz, rt = bits(w, 31, 1), bits(w, 24, 1), bits(w, 0, 5)
|
||||
t = pc + sext(bits(w, 5, 19), 19) * 4
|
||||
return Insn('bcond', target=t, cond=f'{R(sf, rt)} {"!=" if nz else "=="} 0')
|
||||
if bits(w, 25, 6) == 0b011011: # TBZ/TBNZ
|
||||
b = (bits(w, 31, 1) << 5) | bits(w, 19, 5)
|
||||
nz, rt = bits(w, 24, 1), bits(w, 0, 5)
|
||||
t = pc + sext(bits(w, 5, 14), 14) * 4
|
||||
return Insn('bcond', target=t, cond=f'(({X(rt)} >> {b}) & 1) {"!=" if nz else "=="} 0')
|
||||
if bits(w, 25, 7) == 0b1101011: # branch register
|
||||
opc, rn = bits(w, 21, 4), bits(w, 5, 5)
|
||||
if bits(w, 16, 5) != 31 or bits(w, 10, 6) or bits(w, 0, 5):
|
||||
raise Unsupported('branch reg variant')
|
||||
if opc == 0:
|
||||
return Insn('br', target=X(rn))
|
||||
if opc == 1:
|
||||
return Insn('blr', target=X(rn))
|
||||
if opc == 2:
|
||||
return Insn('ret')
|
||||
raise Unsupported('branch reg opc')
|
||||
if bits(w, 24, 8) == 0b11010100: # exception generation
|
||||
opc, ll, imm = bits(w, 21, 3), bits(w, 0, 5), bits(w, 5, 16)
|
||||
if opc == 0 and ll == 1:
|
||||
return Insn('op', f'rt_svc(c, {imm}, {hx(pc)});')
|
||||
if opc == 1 and ll == 0:
|
||||
return Insn('trap', f'rt_brk(c, {imm}, {hx(pc)});')
|
||||
raise Unsupported('exception')
|
||||
if bits(w, 22, 10) == 0b1101010100:
|
||||
return self.system(w, pc)
|
||||
raise Unsupported(f'branch_sys {w:08x}')
|
||||
|
||||
def system(self, w, pc):
|
||||
L = bits(w, 21, 1)
|
||||
op0, op1, crn, crm, op2, rt = (bits(w, 19, 2), bits(w, 16, 3), bits(w, 12, 4),
|
||||
bits(w, 8, 4), bits(w, 5, 3), bits(w, 0, 5))
|
||||
if op0 == 0 and not L:
|
||||
if crn == 2 and op1 == 3 and rt == 31: # hints
|
||||
return Insn('op', '/* hint */')
|
||||
if crn == 3 and op1 == 3 and rt == 31: # barriers
|
||||
if op2 == 2:
|
||||
return Insn('op', 'c->excl_addr = ~0ull;')
|
||||
return Insn('op', '__atomic_thread_fence(__ATOMIC_SEQ_CST);')
|
||||
raise Unsupported(f'pstate/system {w:08x}')
|
||||
if op0 == 1: # SYS
|
||||
if not L and op1 == 3 and crn == 7 and crm == 4 and op2 == 1: # DC ZVA
|
||||
return Insn('op', f'memset((void*)(uintptr_t)({X(rt)} & ~63ull), 0, 64);')
|
||||
return Insn('op', '/* cache maintenance */')
|
||||
key = (op0, op1, crn, crm, op2)
|
||||
if L: # MRS
|
||||
src = {
|
||||
(3, 3, 13, 0, 3): 'c->tpidrro',
|
||||
(3, 3, 13, 0, 2): 'c->tpidr',
|
||||
(3, 3, 14, 0, 0): '19200000ull',
|
||||
(3, 3, 14, 0, 1): 'rt_ticks()',
|
||||
(3, 3, 14, 0, 2): 'rt_ticks()',
|
||||
(3, 3, 4, 2, 0): 'c->nzcv',
|
||||
(3, 3, 4, 4, 0): 'c->fpcr' if self.sc else 'rt_get_fpcr()',
|
||||
(3, 3, 4, 4, 1): 'c->fpsr' if self.sc else 'rt_get_fpsr()',
|
||||
(3, 3, 0, 0, 7): '4ull',
|
||||
(3, 3, 0, 0, 1): '0x8444c004ull',
|
||||
}.get(key)
|
||||
if src is None:
|
||||
raise Unsupported(f'mrs {key}')
|
||||
return Insn('op', set_reg(1, rt, src))
|
||||
dst = {
|
||||
(3, 3, 13, 0, 2): 'c->tpidr = {v};',
|
||||
(3, 3, 4, 2, 0): 'c->nzcv = {v} & 0xf0000000ull;',
|
||||
(3, 3, 4, 4, 0): 'c->fpcr = {v} & 0x7c00000ull; rt_fpcr_sync(c);' if self.sc else 'rt_set_fpcr({v});',
|
||||
(3, 3, 4, 4, 1): 'c->fpsr = {v} & 0xf800009full;' if self.sc else 'rt_set_fpsr({v});',
|
||||
}.get(key)
|
||||
if dst is None:
|
||||
raise Unsupported(f'msr {key}')
|
||||
return Insn('op', dst.format(v=X(rt)))
|
||||
|
||||
# ------------------------------------------------------------ loads / stores
|
||||
|
||||
def ldst(self, w, pc):
|
||||
V = bits(w, 26, 1)
|
||||
if V:
|
||||
if self.sc:
|
||||
return Insn('op', self.sc.ldst(w, pc))
|
||||
return self.ldst_simd(w, pc)
|
||||
if bits(w, 24, 6) == 0b001000:
|
||||
return self.ldst_excl(w, pc)
|
||||
if bits(w, 27, 3) == 0b011 and bits(w, 24, 2) == 0: # literal
|
||||
opc, rt = bits(w, 30, 2), bits(w, 0, 5)
|
||||
addr = pc + sext(bits(w, 5, 19), 19) * 4
|
||||
if opc == 0:
|
||||
return Insn('op', set_reg(0, rt, f'LD32({hx(addr)})'))
|
||||
if opc == 1:
|
||||
return Insn('op', set_reg(1, rt, f'LD64({hx(addr)})'))
|
||||
if opc == 2:
|
||||
return Insn('op', set_reg(1, rt, f'(int64_t)(int32_t)LD32({hx(addr)})'))
|
||||
return Insn('op', '/* prfm */')
|
||||
if bits(w, 27, 3) == 0b101:
|
||||
return self.ldst_pair(w)
|
||||
if bits(w, 27, 3) == 0b111:
|
||||
return self.ldst_reg(w)
|
||||
if (w & 0xFFE00000) == 0xD9200000 and bits(w, 10, 2):
|
||||
return self.stg(w)
|
||||
raise Unsupported(f'ldst {w:08x}')
|
||||
|
||||
def stg(self, w):
|
||||
"""STG (post-index, signed offset, pre-index). Horizon runs without MTE, so storing an allocation tag
|
||||
changes no memory; only the base writeback remains (1.4.3 sdk+0x640050)."""
|
||||
mode, rn = bits(w, 10, 2), bits(w, 5, 5)
|
||||
if mode == 2:
|
||||
return Insn('op', '/* stg */')
|
||||
off = sext(bits(w, 12, 9), 9) << 4
|
||||
dst = 'c->sp' if rn == 31 else f'c->x[{rn}]'
|
||||
return Insn('op', f'{dst} = {XS(rn)} + {hx(off & 0xFFFFFFFFFFFFFFFF)}; /* stg */')
|
||||
|
||||
def ldst_excl(self, w, pc):
|
||||
size, o2, L, o1, rs, o0, rn, rt = (bits(w, 30, 2), bits(w, 23, 1), bits(w, 22, 1), bits(w, 21, 1),
|
||||
bits(w, 16, 5), bits(w, 15, 1), bits(w, 5, 5), bits(w, 0, 5))
|
||||
nbits = 8 << size
|
||||
addr = XS(rn)
|
||||
if o1:
|
||||
if o2:
|
||||
return self.cas(size, rs, rt, addr)
|
||||
if bits(w, 31, 1):
|
||||
return self.excl_pair(bits(w, 30, 1), L, rs, bits(w, 10, 5), rt, addr)
|
||||
return self.casp(bits(w, 30, 1), rs, rt, addr)
|
||||
if o2 == 0:
|
||||
if L:
|
||||
return Insn('op', set_reg(1, rt, f'ldx{nbits}(c, {addr})'))
|
||||
return Insn('op', f'{{ uint32_t st = stx{nbits}(c, {addr}, {X(rt)}); {set_reg(0, rs, "st")} }}')
|
||||
if L:
|
||||
return Insn('op', set_reg(1, rt, f'lda{nbits}({addr})'))
|
||||
return Insn('op', f'stl{nbits}({addr}, {X(rt)});')
|
||||
|
||||
def excl_pair(self, sz, L, rs, rt2, rt, addr):
|
||||
"""LDXP/LDAXP/STXP/STLXP. Two W registers form one 64-bit exclusive, two X registers one 128-bit."""
|
||||
if L:
|
||||
if sz:
|
||||
return Insn('op', f'{{ __uint128_t p = ldxp128(c, {addr}); '
|
||||
f'{set_reg(1, rt, "(uint64_t)p")} {set_reg(1, rt2, "(uint64_t)(p >> 64)")} }}')
|
||||
return Insn('op', f'{{ uint64_t p = ldx64(c, {addr}); '
|
||||
f'{set_reg(0, rt, "p")} {set_reg(0, rt2, "p >> 32")} }}')
|
||||
if sz:
|
||||
st = f'stxp128(c, {addr}, {X(rt)}, {X(rt2)})'
|
||||
else:
|
||||
st = f'stx64(c, {addr}, (uint64_t){W(rt)} | ((uint64_t){W(rt2)} << 32))'
|
||||
return Insn('op', f'{{ uint32_t st = {st}; {set_reg(0, rs, "st")} }}')
|
||||
|
||||
def cas(self, size, rs, rt, addr):
|
||||
"""CAS{A}{L}{B,H}: Rs is the compare value and receives the old memory value."""
|
||||
t = f'uint{8 << size}_t'
|
||||
return Insn('op', f'{{ {t} e = ({t}){X(rs)}; __atomic_compare_exchange_n(({t} *)(uintptr_t)({addr}), &e, '
|
||||
f'({t}){X(rt)}, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); {set_reg(size == 3, rs, "e")} }}')
|
||||
|
||||
def casp(self, sz, rs, rt, addr):
|
||||
"""CASP{A}{L}: register pairs (Rs, Rs+1) compared and (Rt, Rt+1) stored as one 64- or 128-bit access."""
|
||||
if rs & 1 or rt & 1:
|
||||
raise Unsupported('casp odd register')
|
||||
if sz:
|
||||
t = '__uint128_t'
|
||||
e = f'(({t}){X(rs + 1)} << 64) | {X(rs)}'
|
||||
v = f'(({t}){X(rt + 1)} << 64) | {X(rt)}'
|
||||
lo, hi = set_reg(1, rs, '(uint64_t)e'), set_reg(1, rs + 1, '(uint64_t)(e >> 64)')
|
||||
else:
|
||||
t = 'uint64_t'
|
||||
e = f'(uint64_t){W(rs)} | ((uint64_t){W(rs + 1)} << 32)'
|
||||
v = f'(uint64_t){W(rt)} | ((uint64_t){W(rt + 1)} << 32)'
|
||||
lo, hi = set_reg(0, rs, 'e'), set_reg(0, rs + 1, 'e >> 32')
|
||||
return Insn('op', f'{{ {t} e = {e}; __atomic_compare_exchange_n(({t} *)(uintptr_t)({addr}), &e, {v}, 0, '
|
||||
f'__ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); {lo} {hi} }}')
|
||||
|
||||
def atomic(self, w):
|
||||
"""LSE LD<op>{A}{L}{B,H} and SWP{A}{L}{B,H}: Rt receives the old memory value, Rs is the operand."""
|
||||
size, A, R, rs, o3, opc, rn, rt = (bits(w, 30, 2), bits(w, 23, 1), bits(w, 22, 1), bits(w, 16, 5),
|
||||
bits(w, 15, 1), bits(w, 12, 3), bits(w, 5, 5), bits(w, 0, 5))
|
||||
nbits = 8 << size
|
||||
t = f'uint{nbits}_t'
|
||||
p, v = f'({t} *)(uintptr_t)({XS(rn)})', f'({t}){X(rs)}'
|
||||
order = ('__ATOMIC_RELAXED', '__ATOMIC_RELEASE', '__ATOMIC_ACQUIRE', '__ATOMIC_ACQ_REL')[A << 1 | R]
|
||||
if o3:
|
||||
if opc:
|
||||
raise Unsupported('ldapr')
|
||||
expr = f'__atomic_exchange_n({p}, {v}, {order})'
|
||||
elif opc < 4:
|
||||
fn, operand = (('add', v), ('and', f'({t})~{X(rs)}'), ('xor', v), ('or', v))[opc]
|
||||
expr = f'__atomic_fetch_{fn}({p}, {operand}, {order})'
|
||||
else:
|
||||
fn = ('smax', 'smin', 'umax', 'umin')[opc - 4]
|
||||
expr = f'atomic_{fn}{nbits}({XS(rn)}, {v})'
|
||||
return Insn('op', set_reg(size == 3, rt, expr))
|
||||
|
||||
def ldst_pair(self, w):
|
||||
opc, mode, L = bits(w, 30, 2), bits(w, 23, 2), bits(w, 22, 1)
|
||||
imm7, rt2, rn, rt = sext(bits(w, 15, 7), 7), bits(w, 10, 5), bits(w, 5, 5), bits(w, 0, 5)
|
||||
if opc == 3:
|
||||
raise Unsupported('pair opc 3')
|
||||
scale = 3 if opc == 2 else 2
|
||||
off = imm7 << scale
|
||||
base = XS(rn)
|
||||
post = mode == 1
|
||||
wb = mode in (1, 3)
|
||||
ea = base if post else (f'{base} + {off}' if off >= 0 else f'{base} - {-off}')
|
||||
lines = [f'uint64_t ea = {ea};']
|
||||
if L:
|
||||
if opc == 2:
|
||||
lines.append('uint64_t a = LD64(ea), b = LD64(ea + 8);')
|
||||
elif opc == 1: # LDPSW
|
||||
lines.append('uint64_t a = (int64_t)(int32_t)LD32(ea), b = (int64_t)(int32_t)LD32(ea + 4);')
|
||||
else:
|
||||
lines.append('uint64_t a = LD32(ea), b = LD32(ea + 4);')
|
||||
else:
|
||||
if opc == 2:
|
||||
lines.append(f'ST64(ea, {X(rt)}); ST64(ea + 8, {X(rt2)});')
|
||||
else:
|
||||
lines.append(f'ST32(ea, {W(rt)}); ST32(ea + 4, {W(rt2)});')
|
||||
if wb:
|
||||
dst = 'c->sp' if rn == 31 else f'c->x[{rn}]'
|
||||
if not post:
|
||||
lines.append(f'{dst} = ea;')
|
||||
else:
|
||||
lines.append(f'{dst} = ea + {off};' if off >= 0 else f'{dst} = ea - {-off};')
|
||||
if L:
|
||||
lines.append(set_reg(1, rt, 'a'))
|
||||
lines.append(set_reg(1, rt2, 'b'))
|
||||
return Insn('op', '{ ' + ' '.join(lines) + ' }')
|
||||
|
||||
def ldst_reg(self, w):
|
||||
size, opc, rn, rt = bits(w, 30, 2), bits(w, 22, 2), bits(w, 5, 5), bits(w, 0, 5)
|
||||
base = XS(rn)
|
||||
wb = None
|
||||
if bits(w, 24, 2) == 1:
|
||||
ea = f'{base} + {hx(bits(w, 10, 12) << size)}'
|
||||
else:
|
||||
kind = bits(w, 10, 2)
|
||||
if bits(w, 21, 1):
|
||||
if kind == 0:
|
||||
return self.atomic(w)
|
||||
if kind != 2:
|
||||
raise Unsupported('ldst reg pac')
|
||||
rm, option, S = bits(w, 16, 5), bits(w, 13, 3), bits(w, 12, 1)
|
||||
if option not in (2, 3, 6, 7):
|
||||
raise Unsupported('ldst reg option')
|
||||
ea = f'{base} + {extend_expr(rm, option, size if S else 0)}'
|
||||
else:
|
||||
imm9 = sext(bits(w, 12, 9), 9)
|
||||
sign_imm = f'+ {imm9}' if imm9 >= 0 else f'- {-imm9}'
|
||||
if kind in (0, 2): # unscaled, unprivileged
|
||||
ea = f'{base} {sign_imm}'
|
||||
elif kind == 1: # post
|
||||
ea = base
|
||||
wb = f'{base} {sign_imm}'
|
||||
else: # pre
|
||||
ea = f'{base} {sign_imm}'
|
||||
wb = 'ea'
|
||||
nbits = 8 << size
|
||||
if opc == 0:
|
||||
parts = [f'ST{nbits}(ea, {X(rt) if size == 3 else W(rt)});']
|
||||
load = None
|
||||
elif opc == 1:
|
||||
parts = [f'uint64_t v = LD{nbits}(ea);']
|
||||
load = set_reg(1, rt, 'v')
|
||||
elif size == 3:
|
||||
return Insn('op', '/* prfm */')
|
||||
elif size == 2 and opc == 3:
|
||||
raise Unsupported('ldst size2 opc3')
|
||||
else:
|
||||
parts = [f'uint64_t v = (uint64_t)(int64_t)(int{nbits}_t)LD{nbits}(ea);']
|
||||
load = set_reg(1 if opc == 2 else 0, rt, 'v')
|
||||
parts.insert(0, f'uint64_t ea = {ea};')
|
||||
if wb:
|
||||
# writeback before the register load, so a load into rn wins
|
||||
parts.append(f'{"c->sp" if rn == 31 else f"c->x[{rn}]"} = {wb};')
|
||||
if load:
|
||||
parts.append(load)
|
||||
return Insn('op', '{ ' + ' '.join(parts) + ' }')
|
||||
|
||||
def ldst_simd(self, w, pc):
|
||||
"""FP/SIMD loads and stores are passed through as asm; GPR address regs are remapped."""
|
||||
rt = bits(w, 0, 5)
|
||||
if bits(w, 27, 3) == 0b011 and bits(w, 24, 2) == 0: # literal -> LDR (imm, offset 0) via scratch
|
||||
opc = bits(w, 30, 2)
|
||||
size, o = {0: (2, 1), 1: (3, 1), 2: (0, 3)}[opc]
|
||||
addr = pc + sext(bits(w, 5, 19), 19) * 4
|
||||
new = (size << 30) | 0x3D000000 | (o << 22) | (31 << 5) | rt
|
||||
return Insn('asm', asm=AsmInsn(new, {rt}, [], consts=[(5, addr)]))
|
||||
if bits(w, 31, 1) == 0 and bits(w, 24, 6) in (0b001100, 0b001101): # structures
|
||||
post = bits(w, 23, 1)
|
||||
rn, rm = bits(w, 5, 5), bits(w, 16, 5)
|
||||
if bits(w, 24, 1) == 0:
|
||||
n = {0: 4, 2: 4, 4: 3, 6: 3, 7: 1, 8: 2, 10: 2}.get(bits(w, 12, 4))
|
||||
if n is None:
|
||||
raise Unsupported('ldst multiple opcode')
|
||||
else:
|
||||
n = ((bits(w, 13, 1) << 1) | bits(w, 21, 1)) + 1
|
||||
vregs = {(rt + i) % 32 for i in range(n)}
|
||||
gprs = [(5, 'sp' if rn == 31 else rn)]
|
||||
if post and rm != 31:
|
||||
gprs.append((16, rm))
|
||||
return Insn('asm', asm=AsmInsn(w, vregs, gprs))
|
||||
rn = bits(w, 5, 5)
|
||||
gprs = [(5, 'sp' if rn == 31 else rn)]
|
||||
vregs = {rt}
|
||||
if bits(w, 27, 3) == 0b101: # pair
|
||||
vregs.add(bits(w, 10, 5))
|
||||
elif bits(w, 27, 3) == 0b111 and bits(w, 24, 2) == 0 and bits(w, 21, 1) and bits(w, 10, 2) == 2:
|
||||
rm = bits(w, 16, 5)
|
||||
if rm != 31:
|
||||
gprs.append((16, rm))
|
||||
return Insn('asm', asm=AsmInsn(w, vregs, gprs))
|
||||
|
||||
# ------------------------------------------------------------ data processing (reg)
|
||||
|
||||
def dp_reg(self, w, pc):
|
||||
sf = bits(w, 31, 1)
|
||||
rd, rn, rm = bits(w, 0, 5), bits(w, 5, 5), bits(w, 16, 5)
|
||||
size = 64 if sf else 32
|
||||
op1 = bits(w, 28, 1)
|
||||
if not op1:
|
||||
if bits(w, 24, 4) == 0b1010: # logical shifted
|
||||
opc, sh, N, amt = bits(w, 29, 2), bits(w, 22, 2), bits(w, 21, 1), bits(w, 10, 6)
|
||||
if not sf and amt >= 32:
|
||||
raise Unsupported('logical shift amount')
|
||||
b = shift_expr(sf, R(sf, rm), sh, amt)
|
||||
if N:
|
||||
b = f'(uint{size}_t)~{b}'
|
||||
a = R(sf, rn)
|
||||
expr = {0: f'{a} & {b}', 1: f'{a} | {b}', 2: f'{a} ^ {b}', 3: f'{a} & {b}'}[opc]
|
||||
if opc == 3:
|
||||
return Insn('op', set_reg(sf, rd, f'logic{size}(c, (uint{size}_t)({expr}))'))
|
||||
return Insn('op', set_reg(sf, rd, expr))
|
||||
if bits(w, 24, 4) == 0b1011: # add/sub shifted or extended
|
||||
op, S = bits(w, 30, 1), bits(w, 29, 1)
|
||||
if bits(w, 21, 1) == 0:
|
||||
sh, amt = bits(w, 22, 2), bits(w, 10, 6)
|
||||
if sh == 3:
|
||||
raise Unsupported('add shift ror')
|
||||
a = R(sf, rn)
|
||||
b = shift_expr(sf, R(sf, rm), sh, amt)
|
||||
sp_ok = False
|
||||
else:
|
||||
if bits(w, 22, 2):
|
||||
raise Unsupported('add ext opt')
|
||||
option, amt = bits(w, 13, 3), bits(w, 10, 3)
|
||||
a = R(sf, rn, sp=True)
|
||||
b = extend_expr(rm, option, amt)
|
||||
if not sf:
|
||||
b = f'(uint32_t)({b})'
|
||||
sp_ok = not S
|
||||
if S:
|
||||
fn = ('subs' if op else 'adds') + str(size)
|
||||
return Insn('op', set_reg(sf, rd, f'{fn}(c, {a}, (uint{size}_t)({b}))'))
|
||||
return Insn('op', set_reg(sf, rd, f'{a} {"-" if op else "+"} {b}', sp=sp_ok))
|
||||
raise Unsupported(f'dp_reg {w:08x}')
|
||||
op2 = bits(w, 21, 4)
|
||||
if bits(w, 24, 4) == 0b1011: # 3-source
|
||||
op54, op31, o0, ra = bits(w, 29, 2), bits(w, 21, 3), bits(w, 15, 1), bits(w, 10, 5)
|
||||
if op54:
|
||||
raise Unsupported('dp3 op54')
|
||||
sgn = '-' if o0 else '+'
|
||||
if op31 == 0:
|
||||
return Insn('op', set_reg(sf, rd, f'{R(sf, ra)} {sgn} {R(sf, rn)} * {R(sf, rm)}'))
|
||||
if op31 == 1:
|
||||
return Insn('op', set_reg(1, rd, f'{X(ra)} {sgn} (uint64_t)((int64_t)(int32_t){W(rn)} * (int64_t)(int32_t){W(rm)})'))
|
||||
if op31 == 5:
|
||||
return Insn('op', set_reg(1, rd, f'{X(ra)} {sgn} (uint64_t){W(rn)} * (uint64_t){W(rm)}'))
|
||||
if op31 == 2:
|
||||
return Insn('op', set_reg(
|
||||
1, rd, f'(uint64_t)(((__int128)(int64_t){X(rn)} * (__int128)(int64_t){X(rm)}) >> 64)'))
|
||||
if op31 == 6:
|
||||
return Insn('op', set_reg(1, rd, f'(uint64_t)(((unsigned __int128){X(rn)} * {X(rm)}) >> 64)'))
|
||||
raise Unsupported('dp3')
|
||||
if op2 == 0: # add/sub with carry
|
||||
op, S = bits(w, 30, 1), bits(w, 29, 1)
|
||||
fn = ('sbc' if op else 'adc') + ('s' if S else '') + str(size)
|
||||
return Insn('op', set_reg(sf, rd, f'{fn}(c, {R(sf, rn)}, {R(sf, rm)})'))
|
||||
if op2 == 2: # conditional compare
|
||||
op, imm_form, cond, nzcv = bits(w, 30, 1), bits(w, 11, 1), bits(w, 12, 4), bits(w, 0, 4)
|
||||
b = hx(rm) if imm_form else R(sf, rm)
|
||||
fn = ('subs' if op else 'adds') + str(size)
|
||||
return Insn('op', f'if ({cond_expr(cond)}) (void){fn}(c, {R(sf, rn)}, {b}); '
|
||||
f'else c->nzcv = {hx(nzcv << 28)};')
|
||||
if op2 == 4: # conditional select
|
||||
op, o2, cond = bits(w, 30, 1), bits(w, 10, 2), bits(w, 12, 4)
|
||||
b = R(sf, rm)
|
||||
alt = {(0, 0): b, (0, 1): f'{b} + 1', (1, 0): f'~{b}', (1, 1): f'-{b}'}.get((op, o2))
|
||||
if alt is None:
|
||||
raise Unsupported('csel variant')
|
||||
return Insn('op', set_reg(sf, rd, f'({cond_expr(cond)}) ? {R(sf, rn)} : (uint{size}_t)({alt})'))
|
||||
if op2 == 6:
|
||||
opcode = bits(w, 10, 6)
|
||||
if bits(w, 30, 1): # 1-source
|
||||
if bits(w, 16, 5):
|
||||
raise Unsupported('dp1 opcode2')
|
||||
a = R(sf, rn)
|
||||
expr = {
|
||||
0: f'rbit{size}({a})',
|
||||
1: f'rev16_{size}({a})',
|
||||
2: f'__builtin_bswap32({a})' if not sf else f'rev32_64({a})',
|
||||
3: f'__builtin_bswap64({a})' if sf else None,
|
||||
4: f'clz{size}({a})',
|
||||
5: f'cls{size}({a})',
|
||||
}.get(opcode)
|
||||
if expr is None:
|
||||
raise Unsupported('dp1')
|
||||
return Insn('op', set_reg(sf, rd, expr))
|
||||
if 16 <= opcode <= 23: # CRC32: host has the same instruction
|
||||
if self.sc:
|
||||
return Insn('op', self.sc.crc(w))
|
||||
gprs = [(p, r) for p, r in ((0, rd), (5, rn), (16, rm)) if r != 31]
|
||||
return Insn('asm', asm=AsmInsn(w, set(), gprs))
|
||||
a, b = R(sf, rn), R(sf, rm)
|
||||
expr = {
|
||||
2: f'udiv{size}({a}, {b})',
|
||||
3: f'sdiv{size}({a}, {b})',
|
||||
8: f'{a} << ({b} & {size - 1})',
|
||||
9: f'{a} >> ({b} & {size - 1})',
|
||||
10: f'(uint{size}_t)((int{size}_t){a} >> ({b} & {size - 1}))',
|
||||
11: f'ROR{size}({a}, {b} & {size - 1})',
|
||||
}.get(opcode)
|
||||
if expr is None:
|
||||
raise Unsupported('dp2')
|
||||
return Insn('op', set_reg(sf, rd, expr))
|
||||
raise Unsupported(f'dp_reg {w:08x}')
|
||||
|
||||
# ------------------------------------------------------------ FP / SIMD data processing
|
||||
|
||||
def fp_simd(self, w, pc):
|
||||
if self.sc:
|
||||
return Insn('op', self.sc.dp(w, pc))
|
||||
rd, rn, rm, ra = bits(w, 0, 5), bits(w, 5, 5), bits(w, 16, 5), bits(w, 10, 5)
|
||||
vregs = {rd, rn, rm, ra}
|
||||
gprs = []
|
||||
flags = False
|
||||
if (w & 0x5F20FC00) == 0x1E200000: # FP <-> integer
|
||||
opcode = bits(w, 16, 3)
|
||||
if opcode in (0, 1, 4, 5, 6):
|
||||
gprs = [(0, rd)]
|
||||
vregs = {rn}
|
||||
else:
|
||||
gprs = [(5, rn)]
|
||||
vregs = {rd}
|
||||
elif (w & 0x5F200000) == 0x1E000000: # FP <-> fixed point
|
||||
opcode = bits(w, 16, 3)
|
||||
if opcode in (0, 1):
|
||||
gprs = [(0, rd)]
|
||||
vregs = {rn}
|
||||
else:
|
||||
gprs = [(5, rn)]
|
||||
vregs = {rd}
|
||||
elif (w & 0x9FE08400) == 0x0E000400 and bits(w, 29, 1) == 0: # AdvSIMD copy
|
||||
imm4 = bits(w, 11, 4)
|
||||
if imm4 in (1, 3):
|
||||
gprs = [(5, rn)]
|
||||
vregs = {rd}
|
||||
elif imm4 in (5, 7):
|
||||
gprs = [(0, rd)]
|
||||
vregs = {rn}
|
||||
else:
|
||||
vregs = {rd, rn}
|
||||
elif (w & 0xBFE08C00) == 0x0E000000: # TBL/TBX
|
||||
n = bits(w, 13, 2) + 1
|
||||
vregs = {rd, rm} | {(rn + i) % 32 for i in range(n)}
|
||||
if (w & 0x5F203C00) == 0x1E202000 or (w & 0x5F200C00) in (0x1E200400, 0x1E200C00):
|
||||
flags = True
|
||||
gprs = [(p, r) for p, r in gprs if r != 31] # register 31 is zr in these encodings
|
||||
return Insn('asm', asm=AsmInsn(w, vregs, gprs, flags))
|
||||
|
||||
|
||||
def cond_expr(cond):
|
||||
return f'COND_{COND_NAMES[cond].upper()}(c)' if cond < 14 else '1'
|
||||
|
||||
|
||||
def emit_asm_batch(batch):
|
||||
"""Emit one inline-asm block for a run of FP/SIMD instructions."""
|
||||
vregs = set()
|
||||
gmap = {}
|
||||
flags = False
|
||||
consts = []
|
||||
words = []
|
||||
for a in batch:
|
||||
vregs |= a.vregs
|
||||
flags |= a.flags
|
||||
w = a.word
|
||||
for pos, g in a.gprs:
|
||||
if g not in gmap:
|
||||
if len(gmap) + len(consts) >= len(SCRATCH):
|
||||
raise Unsupported('scratch exhausted')
|
||||
gmap[g] = SCRATCH[len(gmap) + len(consts)]
|
||||
w = (w & ~(31 << pos)) | (gmap[g] << pos)
|
||||
for pos, val in a.consts:
|
||||
s = SCRATCH[len(gmap) + len(consts)]
|
||||
consts.append((s, val))
|
||||
w = (w & ~(31 << pos)) | (s << pos)
|
||||
words.append(w)
|
||||
lines = []
|
||||
ins = ['"r"(c)']
|
||||
for g, s in gmap.items():
|
||||
off = OFF_SP if g == 'sp' else OFF_X + 8 * g
|
||||
lines.append(f'ldr x{s}, [%0, #{off}]')
|
||||
for i, (s, val) in enumerate(consts):
|
||||
ins.append(f'"r"({hx(val)})')
|
||||
lines.append(f'mov x{s}, %{len(ins) - 1}')
|
||||
for v in sorted(vregs):
|
||||
lines.append(f'ldr q{v}, [%0, #{OFF_V + 16 * v}]')
|
||||
if flags:
|
||||
lines.append(f'ldr x16, [%0, #{OFF_NZCV}]')
|
||||
lines.append('msr nzcv, x16')
|
||||
lines += [f'.inst 0x{x:08x}' for x in words]
|
||||
if flags:
|
||||
lines.append('mrs x16, nzcv')
|
||||
lines.append(f'str x16, [%0, #{OFF_NZCV}]')
|
||||
for v in sorted(vregs):
|
||||
lines.append(f'str q{v}, [%0, #{OFF_V + 16 * v}]')
|
||||
for g, s in gmap.items():
|
||||
off = OFF_SP if g == 'sp' else OFF_X + 8 * g
|
||||
lines.append(f'str x{s}, [%0, #{off}]')
|
||||
clob = [f'"v{v}"' for v in sorted(vregs)]
|
||||
clob += [f'"x{s}"' for s in sorted(set(gmap.values()) | {s for s, _ in consts} | ({16} if flags else set()))]
|
||||
clob += ['"cc"', '"memory"']
|
||||
body = '\\n\\t'.join(lines)
|
||||
return f'__asm__ volatile("{body}" :: {", ".join(ins)} : {", ".join(clob)});'
|
||||
112
recomp/module.py
Normal file
112
recomp/module.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""One loaded NSO module as the generator and its analyses see it: image at a guest base, .text as words,
|
||||
function seeds, and bounded-cache instruction lifting.
|
||||
|
||||
The function seeds and the .eh_frame_hdr reader are adapted from LAS Recomp tools/analyze.py and tools/gen.py
|
||||
@ da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
||||
"""
|
||||
import struct
|
||||
from collections import OrderedDict
|
||||
|
||||
import numpy as np
|
||||
|
||||
from lifter import Insn, Lifter, Unsupported, hx
|
||||
from nso import Nso, R_AARCH64_RELATIVE
|
||||
|
||||
LIFT_CACHE = 1 << 16 # lifted instructions kept per module; analyses revisit nearby addresses only
|
||||
|
||||
|
||||
def _read_encoded(img, off, enc, pc_base):
|
||||
fmt, app = enc & 0x0F, enc & 0x70
|
||||
if fmt == 0x0B: # sdata4
|
||||
v, size = struct.unpack_from('<i', img, off)[0], 4
|
||||
elif fmt == 0x03: # udata4
|
||||
v, size = struct.unpack_from('<I', img, off)[0], 4
|
||||
elif fmt in (0x0C, 0x04): # sdata8, udata8
|
||||
v, size = struct.unpack_from('<q' if fmt == 0x0C else '<Q', img, off)[0], 8
|
||||
else:
|
||||
raise ValueError(f'unsupported pointer encoding {enc:#x}')
|
||||
if app == 0x10: # pcrel
|
||||
v += off
|
||||
elif app == 0x30: # datarel
|
||||
v += pc_base
|
||||
return v, size
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self, name, path, base, simd='c'):
|
||||
self.name = name
|
||||
self.nso = Nso(str(path))
|
||||
self.base = base
|
||||
n = self.nso
|
||||
self.image = bytes(n.image)
|
||||
self.lo = base + n.text[0]
|
||||
self.hi = self.lo + n.text[1]
|
||||
self.words = np.frombuffer(self.image, dtype='<u4', count=n.text[1] // 4, offset=n.text[0])
|
||||
self.lifter = Lifter(base, simd)
|
||||
self._cache = OrderedDict()
|
||||
|
||||
# ---- memory
|
||||
|
||||
def in_text(self, a):
|
||||
return self.lo <= a < self.hi and a % 4 == 0
|
||||
|
||||
def in_image(self, a, n=1):
|
||||
return self.base <= a and a + n <= self.base + len(self.image)
|
||||
|
||||
def word(self, a):
|
||||
return int(self.words[(a - self.lo) >> 2])
|
||||
|
||||
def read(self, a, n, signed=False):
|
||||
"""Little-endian integer of n bytes at guest address a (unrelocated image)."""
|
||||
o = a - self.base
|
||||
return int.from_bytes(self.image[o:o + n], 'little', signed=signed)
|
||||
|
||||
# ---- lifting
|
||||
|
||||
def lift(self, a):
|
||||
"""Lifted instruction at a; Unsupported becomes an rt_udf trap."""
|
||||
ins = self._cache.get(a)
|
||||
if ins is not None:
|
||||
self._cache.move_to_end(a)
|
||||
return ins
|
||||
try:
|
||||
ins = self.lifter.lift(self.word(a), a)
|
||||
except Unsupported as e:
|
||||
ins = Insn('trap', f'rt_udf(c, {hx(a)}); /* {e} */')
|
||||
self._cache[a] = ins
|
||||
if len(self._cache) > LIFT_CACHE:
|
||||
self._cache.popitem(last=False)
|
||||
return ins
|
||||
|
||||
# ---- function seeds
|
||||
|
||||
def eh_frame_functions(self):
|
||||
"""Function starts from .eh_frame_hdr's binary search table (absolute addresses)."""
|
||||
img, hdr = self.image, self.nso.eh_start
|
||||
if not hdr or hdr >= len(img):
|
||||
return []
|
||||
_, ptr_enc, cnt_enc, tbl_enc = img[hdr:hdr + 4]
|
||||
off = hdr + 4
|
||||
_, sz = _read_encoded(img, off, ptr_enc, hdr)
|
||||
off += sz
|
||||
count, sz = _read_encoded(img, off, cnt_enc, hdr)
|
||||
off += sz
|
||||
starts = []
|
||||
for _ in range(count):
|
||||
loc, sz = _read_encoded(img, off, tbl_enc, hdr)
|
||||
off += sz
|
||||
_, sz = _read_encoded(img, off, tbl_enc, hdr)
|
||||
off += sz
|
||||
starts.append(self.base + loc)
|
||||
return starts
|
||||
|
||||
def seeds(self):
|
||||
"""Known function starts: .eh_frame_hdr, exported FUNC symbols, R_AARCH64_RELATIVE targets in .text."""
|
||||
starts = set(self.eh_frame_functions())
|
||||
for _, info, shndx, value, _ in self.nso.syms:
|
||||
if shndx and (info & 0xF) == 2:
|
||||
starts.add(self.base + value)
|
||||
for _, typ, _, add in self.nso.relocs:
|
||||
if typ == R_AARCH64_RELATIVE:
|
||||
starts.add(self.base + add)
|
||||
return sorted(a for a in starts if self.in_text(a))
|
||||
134
recomp/nso.py
Normal file
134
recomp/nso.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# Adapted from LAS Recomp tools/nso.py @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
||||
"""NSO loader: decompress segments, build the memory image, parse MOD0/dynamic info."""
|
||||
import struct
|
||||
import sys
|
||||
from collections import Counter
|
||||
|
||||
import lz4.block
|
||||
|
||||
DT_NULL, DT_NEEDED, DT_PLTRELSZ, DT_HASH, DT_STRTAB, DT_SYMTAB, DT_RELA, DT_RELASZ = 0, 1, 2, 4, 5, 6, 7, 8
|
||||
DT_STRSZ, DT_JMPREL, DT_RELACOUNT = 10, 23, 0x6FFFFFF9
|
||||
DT_RELRSZ, DT_RELR, DT_GNU_HASH = 0x23, 0x24, 0x6FFFFEF5
|
||||
R_AARCH64_ABS64, R_AARCH64_GLOB_DAT, R_AARCH64_JUMP_SLOT, R_AARCH64_RELATIVE = 257, 1025, 1026, 1027
|
||||
|
||||
|
||||
class Nso:
|
||||
def __init__(self, path):
|
||||
raw = open(path, 'rb').read()
|
||||
assert raw[:4] == b'NSO0', path
|
||||
flags = struct.unpack_from('<I', raw, 0xC)[0]
|
||||
self.build_id = raw[0x40:0x60].rstrip(b'\0').hex()
|
||||
segs = []
|
||||
for i in range(3):
|
||||
file_off, mem_off, dec_size = struct.unpack_from('<III', raw, 0x10 + i * 0x10)
|
||||
comp_size = struct.unpack_from('<I', raw, 0x60 + i * 4)[0]
|
||||
data = raw[file_off:file_off + comp_size]
|
||||
if flags & (1 << i):
|
||||
data = lz4.block.decompress(data, uncompressed_size=dec_size)
|
||||
segs.append((mem_off, data))
|
||||
self.bss_size = struct.unpack_from('<I', raw, 0x3C)[0]
|
||||
self.text = (segs[0][0], len(segs[0][1]))
|
||||
self.rodata = (segs[1][0], len(segs[1][1]))
|
||||
self.data = (segs[2][0], len(segs[2][1]))
|
||||
end = segs[2][0] + len(segs[2][1]) + self.bss_size
|
||||
self.size = (end + 0xFFF) & ~0xFFF
|
||||
img = bytearray(self.size)
|
||||
for off, d in segs:
|
||||
img[off:off + len(d)] = d
|
||||
self.image = img
|
||||
self._parse_mod0()
|
||||
|
||||
def u32(self, off): return struct.unpack_from('<I', self.image, off)[0]
|
||||
def u64(self, off): return struct.unpack_from('<Q', self.image, off)[0]
|
||||
|
||||
def _parse_mod0(self):
|
||||
mod0 = self.u32(4)
|
||||
assert self.image[mod0:mod0 + 4] == b'MOD0'
|
||||
rel = lambda i: mod0 + struct.unpack_from('<i', self.image, mod0 + 4 + i * 4)[0]
|
||||
self.dynamic, self.bss_start, self.bss_end = rel(0), rel(1), rel(2)
|
||||
self.eh_start, self.eh_end, self.module_obj = rel(3), rel(4), rel(5)
|
||||
dyn = {}
|
||||
needed = []
|
||||
off = self.dynamic
|
||||
while True:
|
||||
tag, val = self.u64(off), self.u64(off + 8)
|
||||
off += 16
|
||||
if tag == DT_NULL:
|
||||
break
|
||||
if tag == DT_NEEDED:
|
||||
needed.append(val)
|
||||
dyn.setdefault(tag, val)
|
||||
self.dyn = dyn
|
||||
strtab = dyn.get(DT_STRTAB, 0)
|
||||
self.strtab = strtab
|
||||
self.needed = [self.cstr(strtab + n) for n in needed]
|
||||
# dynsym count from DT_HASH nchain, or from DT_GNU_HASH (1.4.3 sdk and subsdk0 have no DT_HASH)
|
||||
self.syms = []
|
||||
nsyms = self.symbol_count()
|
||||
if nsyms and DT_SYMTAB in dyn:
|
||||
st = dyn[DT_SYMTAB]
|
||||
for i in range(nsyms):
|
||||
o = st + i * 24
|
||||
name, info, other, shndx = struct.unpack_from('<IBBH', self.image, o)
|
||||
value, size = struct.unpack_from('<QQ', self.image, o + 8)
|
||||
self.syms.append((self.cstr(strtab + name), info, shndx, value, size))
|
||||
self.relocs = []
|
||||
for base_tag, size_tag in ((DT_RELA, DT_RELASZ), (DT_JMPREL, DT_PLTRELSZ)):
|
||||
if base_tag in dyn:
|
||||
b, n = dyn[base_tag], dyn[size_tag] // 24
|
||||
for i in range(n):
|
||||
r_off, r_info, r_add = struct.unpack_from('<QQq', self.image, b + i * 24)
|
||||
self.relocs.append((r_off, r_info & 0xFFFFFFFF, r_info >> 32, r_add))
|
||||
# DT_RELR (1.4.3): packed RELATIVE relocations whose addend is the word in place. An even entry is an address
|
||||
# (relocate it, continue after it); an odd entry is a bitmap of the next 63 words.
|
||||
if DT_RELR in dyn:
|
||||
where = 0
|
||||
for i in range(dyn[DT_RELRSZ] // 8):
|
||||
e = self.u64(dyn[DT_RELR] + i * 8)
|
||||
if e & 1 == 0:
|
||||
self.relocs.append((e, R_AARCH64_RELATIVE, 0, self.u64(e)))
|
||||
where = e + 8
|
||||
continue
|
||||
for k in range(63):
|
||||
if e >> (k + 1) & 1:
|
||||
a = where + k * 8
|
||||
self.relocs.append((a, R_AARCH64_RELATIVE, 0, self.u64(a)))
|
||||
where += 63 * 8
|
||||
|
||||
def symbol_count(self):
|
||||
"""Number of .dynsym entries: DT_HASH nchain, else one past the last symbol a DT_GNU_HASH chain reaches."""
|
||||
dyn = self.dyn
|
||||
if DT_HASH in dyn:
|
||||
return self.u32(dyn[DT_HASH] + 4)
|
||||
if DT_GNU_HASH not in dyn:
|
||||
return 0
|
||||
h = dyn[DT_GNU_HASH]
|
||||
nbuckets, symoffset, bloom_size = self.u32(h), self.u32(h + 4), self.u32(h + 8)
|
||||
buckets = h + 16 + bloom_size * 8
|
||||
last = max([self.u32(buckets + 4 * i) for i in range(nbuckets)] + [0])
|
||||
if last < symoffset:
|
||||
return symoffset
|
||||
chains = buckets + 4 * nbuckets
|
||||
while not self.u32(chains + 4 * (last - symoffset)) & 1:
|
||||
last += 1
|
||||
return last + 1
|
||||
|
||||
def cstr(self, off):
|
||||
end = self.image.index(b'\0', off)
|
||||
return self.image[off:end].decode('utf-8', 'replace')
|
||||
|
||||
def imports(self):
|
||||
return [s for s in self.syms if s[0] and s[2] == 0]
|
||||
|
||||
def exports(self):
|
||||
return [s for s in self.syms if s[0] and s[2] != 0]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
for p in sys.argv[1:]:
|
||||
n = Nso(p)
|
||||
print(f'== {p} build {n.build_id[:16]} image 0x{n.size:x}')
|
||||
print(f' text 0x{n.text[0]:x}+0x{n.text[1]:x} ro 0x{n.rodata[0]:x}+0x{n.rodata[1]:x} '
|
||||
f'data 0x{n.data[0]:x}+0x{n.data[1]:x} bss 0x{n.bss_size:x}')
|
||||
print(f' needed {n.needed} syms {len(n.syms)} imports {len(n.imports())} exports {len(n.exports())}')
|
||||
print(f' relocs {dict(Counter(r[1] for r in n.relocs))}')
|
||||
957
recomp/patches-v110.txt
Normal file
957
recomp/patches-v110.txt
Normal file
@@ -0,0 +1,957 @@
|
||||
# Code patches for NX2navi 1.1.0 main (recomp/patches.py). The sites come from MacNativeGames
|
||||
# games/<game>/runtime/game_patches.c (MIT, Copyright (c) 2026 MacNativeGames contributors) at upstream ebe621d, through
|
||||
# its 1.2.1 addresses in game_version.h (2db612b); each one was located again in 1.1.0 by instruction-window matching
|
||||
# and its original words checked against this build's main. The patch page holds two floats the runtime writes at every
|
||||
# present (frame_rate.c): +0 the 30 Hz ratio (seconds * 30), +4 the frame step in seconds.
|
||||
|
||||
set fps60 NX2NAVI_FPS60 1
|
||||
set realtime NX2NAVI_REALTIME 1
|
||||
set cloth NX2NAVI_CLOTH_REALTIME 1
|
||||
set bounce NX2NAVI_HIGHFPS_FIX 1
|
||||
set output NX2NAVI_PATCH_OUTPUT 0
|
||||
set scene NX2NAVI_PATCH_SCENE 0
|
||||
|
||||
# 60 fps: the adaptive present-interval controller (main+0x8f26c8, 1.0.0 0x8f6bec) gets the minimum interval 1.
|
||||
site fps60 main 90e978 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
|
||||
site fps60 main 90e97c 1a89b10a 1a9fa52a # clamp: csinc w10, w9, wzr, ge (desired = max(desired, 1))
|
||||
site fps60 main 90e9a0 b9401a8a d503201f # lower trigger: nop the stored-candidate load
|
||||
site fps60 main 90ea30 6b08015f 7100055f # raise path: cmp w10, #1
|
||||
site fps60 main 90ea40 1a88c1ac 1a9fc5ac # raise path: csinc w12, w13, wzr, gt (next lower candidate floors at 1)
|
||||
site fps60 main 90ea94 b9400288 52800028 # lower path: mov w8, #1 (minimum interval 1)
|
||||
site fps60 main 90eab0 2a0a03f5 1a9fa555 # lower path: csinc w21, w10, wzr, ge (result never below 1)
|
||||
|
||||
# Real-time step: the VFR step (main+0x7ede6c, 1.0.0 0x7a83e4) and the global ratio after the controller call
|
||||
# (main+0x8f2394, 1.0.0 0x8f68b8) read ratio and seconds from the patch page instead of interval * 0.5 and interval / 60.
|
||||
site realtime main 833e4c 1e220020 @page:x9 # scvtf s0, w1 -> adrp x9, page
|
||||
site realtime main 833e50 52a84e08 bd400120 # mov w8, #60.0 -> ldr s0, [x9] (ratio)
|
||||
site realtime main 833e54 1e270101 bd400521 # fmov s1, w8 -> ldr s1, [x9, #4] (seconds)
|
||||
site realtime main 833e58 1e2c1002 d503201f # fmov s2, #0.5 -> nop
|
||||
site realtime main 833e64 1e211801 d503201f # fdiv s1, s0, s1 -> nop
|
||||
site realtime main 833e68 1e220800 d503201f # fmul s0, s0, s2 -> nop
|
||||
site realtime main 90e5d0 bd425e60 @page:x9 # ldr s0, [x19, #0x25c] -> adrp x9, page
|
||||
site realtime main 90e5d4 1e2c1001 bd400120 # fmov s1, #0.5 -> ldr s0, [x9] (global ratio)
|
||||
site realtime main 90e5e4 5e21d800 d503201f # scvtf s0, s0 -> nop
|
||||
site realtime main 90e5e8 1e210800 d503201f # fmul s0, s0, s1 -> nop
|
||||
|
||||
# Hair and cloth (upstream 290ec9f, 8f8022f): the actor animation update builds a second clock {seconds = factor / 30,
|
||||
# ratio = factor} per rendered frame. Three inlined places: at the fmul, seconds = measured seconds * factor; at the
|
||||
# store of the factor into the ratio slot, measured ratio * factor. The call of the shared update (1.1.0
|
||||
# main+0x8ecb40, 1.2.1 main+0x89822c) scales s2 (seconds) and s3 (ratio) by the measured ratio. Upstream's
|
||||
# veneer bodies, inlined at the site.
|
||||
site cloth main 9efc20 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
|
||||
site cloth main 9efc1c bd0117e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0137ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x114]
|
||||
site cloth main 9f0bcc 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
|
||||
site cloth main 9f0ba8 bd0117e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0137ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x114]
|
||||
site cloth main a05a04 1e210801 f81f0fe9 @page:x9 bd400521 1e210801 f84107e9 # fmul s1, s0, s1
|
||||
site cloth main a05a08 bd0137e0 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f081f bd0157ff 3dc007ff f94003e9 910083ff # str s0, [sp, #0x134]
|
||||
site cloth main 9f044c 97fbf1bd @page:x8 bd400104 1e240842 1e240863 97fbf1bd # bl 0x8ecb40
|
||||
|
||||
# Trampolines (upstream 27c3b43): the slow-motion check compares time object +0x28 (30 / (ratio * time scale)) with
|
||||
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
|
||||
site bounce main bee7e0 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
|
||||
|
||||
# Output size (upstream games/<game>/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
|
||||
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
|
||||
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
|
||||
# through main+0xcc4160) from page +32; the renderer itself goes to page +40.
|
||||
site output main eb8cfc d280f008 @page:x8 # mov x8, #0x780 -> adrp x8, page
|
||||
site output main eb8d00 f2c08708 f9401908 # movk x8, #0x438, lsl #32 -> ldr x8, [x8, #48]
|
||||
site output main cea5c8 aa1403e0 @page:x21 # mov x0, x20 -> adrp x21, page
|
||||
site output main cea5cc 94000081 f90016b3 # bl 0xcc4160 -> str x19, [x21, #40]
|
||||
site output main cea5d0 f9400015 f94012b5 # ldr x21, [x0] -> ldr x21, [x21, #32]
|
||||
|
||||
# Scene render size (upstream patch_render_res): the renderer's +0xcc4 / +0xcc8, which size every scene render target,
|
||||
# take two floats from page +16 instead of the graphics parameters' docked 1600x900.
|
||||
site scene main cea5e0 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
|
||||
site scene main cea5e8 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
|
||||
site scene main cea5ec 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
|
||||
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v110/exefs --patches recomp/patches-v110.txt: loops that only
|
||||
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), NX2NAVI_SPIN=0 keeps the core.
|
||||
|
||||
# spin waits found by tools/spin_waits.py on the 1.1.x modules
|
||||
# main: 869 spin waits
|
||||
spin main ac600 ac620 ac630 # 13 instructions; loads
|
||||
spin main ac680 ac6a8 ac6b8 # 15 instructions; loads
|
||||
spin main ac6f0 ac718 ac728 # 15 instructions; loads
|
||||
spin main e8544 e854c # 3 instructions; loads
|
||||
spin main 2457f0 245804 # 6 instructions; loads
|
||||
spin main 644bbc 644bd0 # 6 instructions; loads
|
||||
spin main 6456ac 6456c0 # 6 instructions; loads
|
||||
spin main 6459d4 6459e8 # 6 instructions; loads
|
||||
spin main 645d58 645d6c # 6 instructions; loads
|
||||
spin main 6460c4 6460d8 # 6 instructions; loads
|
||||
spin main 64649c 6464b0 # 6 instructions; loads
|
||||
spin main 646740 646754 # 6 instructions; loads
|
||||
spin main 6467d4 6467e8 # 6 instructions; loads
|
||||
spin main 646e6c 646e80 # 6 instructions; loads
|
||||
spin main 647080 647094 # 6 instructions; loads
|
||||
spin main 6484f4 648510 # 8 instructions; loads
|
||||
spin main 6485a4 6485c0 # 8 instructions; loads
|
||||
spin main 649fb0 649fc4 # 6 instructions; loads
|
||||
spin main 64a064 64a078 # 6 instructions; loads
|
||||
spin main 64a3d0 64a3e4 # 6 instructions; loads
|
||||
spin main 64afac 64afc8 # 8 instructions; loads
|
||||
spin main 64b404 64b418 # 6 instructions; loads
|
||||
spin main 64b980 64b994 # 6 instructions; loads
|
||||
spin main 64c68c 64c6a0 # 6 instructions; loads
|
||||
spin main 64c978 64c98c # 6 instructions; loads
|
||||
spin main 64ca28 64ca3c # 6 instructions; loads
|
||||
spin main 64d378 64d38c # 6 instructions; loads
|
||||
spin main 64d8d8 64d8ec # 6 instructions; loads
|
||||
spin main 64db90 64dba4 # 6 instructions; loads
|
||||
spin main 64de18 64de2c # 6 instructions; loads
|
||||
spin main 64de40 64de54 # 6 instructions; loads
|
||||
spin main 64de68 64de7c # 6 instructions; loads
|
||||
spin main 64e6c8 64e6dc # 6 instructions; loads
|
||||
spin main 64e6f0 64e704 # 6 instructions; loads
|
||||
spin main 64e8ac 64e8c0 # 6 instructions; loads
|
||||
spin main 64e9a4 64e9b8 # 6 instructions; loads
|
||||
spin main 64fb50 64fb64 # 6 instructions; loads
|
||||
spin main 652eac 652ec0 # 6 instructions; loads
|
||||
spin main 652ed4 652ee8 # 6 instructions; loads
|
||||
spin main 652efc 652f10 # 6 instructions; loads
|
||||
spin main 654590 6545a4 # 6 instructions; loads
|
||||
spin main 654c5c 654c70 # 6 instructions; loads
|
||||
spin main 654cfc 654d10 # 6 instructions; loads
|
||||
spin main 655124 655138 # 6 instructions; loads
|
||||
spin main 655c14 655c28 # 6 instructions; loads
|
||||
spin main 656f80 656f94 # 6 instructions; loads
|
||||
spin main 658e60 658e74 # 6 instructions; loads
|
||||
spin main 658ebc 658ed0 # 6 instructions; loads
|
||||
spin main 659b28 659b3c # 6 instructions; loads
|
||||
spin main 659b50 659b64 # 6 instructions; loads
|
||||
spin main 65c9e0 65c9f4 # 6 instructions; loads
|
||||
spin main 65ca08 65ca1c # 6 instructions; loads
|
||||
spin main 65ca30 65ca44 # 6 instructions; loads
|
||||
spin main 65ca58 65ca6c # 6 instructions; loads
|
||||
spin main 65ca80 65ca94 # 6 instructions; loads
|
||||
spin main 65caa8 65cabc # 6 instructions; loads
|
||||
spin main 65cad0 65cae4 # 6 instructions; loads
|
||||
spin main 65caf8 65cb0c # 6 instructions; loads
|
||||
spin main 65cb20 65cb34 # 6 instructions; loads
|
||||
spin main 65cb48 65cb5c # 6 instructions; loads
|
||||
spin main 65cb70 65cb84 # 6 instructions; loads
|
||||
spin main 65cb98 65cbac # 6 instructions; loads
|
||||
spin main 65cbc0 65cbd4 # 6 instructions; loads
|
||||
spin main 65d4b0 65d4c4 # 6 instructions; loads
|
||||
spin main 65d4d8 65d4ec # 6 instructions; loads
|
||||
spin main 65d500 65d514 # 6 instructions; loads
|
||||
spin main 65d528 65d53c # 6 instructions; loads
|
||||
spin main 65da54 65da68 # 6 instructions; loads
|
||||
spin main 65de00 65de14 # 6 instructions; loads
|
||||
spin main 65e200 65e214 # 6 instructions; loads
|
||||
spin main 65e6ec 65e700 # 6 instructions; loads
|
||||
spin main 65ecbc 65ecd0 # 6 instructions; loads
|
||||
spin main 65ece4 65ecf8 # 6 instructions; loads
|
||||
spin main 663588 66359c # 6 instructions; loads
|
||||
spin main 663b0c 663b20 # 6 instructions; loads
|
||||
spin main 663da0 663db4 # 6 instructions; loads
|
||||
spin main 663dec 663e00 # 6 instructions; loads
|
||||
spin main 6645a4 6645b8 # 6 instructions; loads
|
||||
spin main 6645cc 6645e0 # 6 instructions; loads
|
||||
spin main 666530 666544 # 6 instructions; loads
|
||||
spin main 666570 666584 # 6 instructions; loads
|
||||
spin main 6671d4 6671e8 # 6 instructions; loads
|
||||
spin main 6671fc 667210 # 6 instructions; loads
|
||||
spin main 6676c0 6676d4 # 6 instructions; loads
|
||||
spin main 667cfc 667d10 # 6 instructions; loads
|
||||
spin main 66f34c 66f360 # 6 instructions; loads
|
||||
spin main 6708f8 67090c # 6 instructions; loads
|
||||
spin main 670920 670934 # 6 instructions; loads
|
||||
spin main 670948 67095c # 6 instructions; loads
|
||||
spin main 673f34 673f48 # 6 instructions; loads
|
||||
spin main 67556c 675580 # 6 instructions; loads
|
||||
spin main 676954 676968 # 6 instructions; loads
|
||||
spin main 676994 6769a8 # 6 instructions; loads
|
||||
spin main 676e6c 676e80 # 6 instructions; loads
|
||||
spin main 677148 67715c # 6 instructions; loads
|
||||
spin main 6771c8 6771dc # 6 instructions; loads
|
||||
spin main 677484 677498 # 6 instructions; loads
|
||||
spin main 677df0 677e04 # 6 instructions; loads
|
||||
spin main 6792a8 6792bc # 6 instructions; loads
|
||||
spin main 67a984 67a998 # 6 instructions; loads
|
||||
spin main 698ef8 698f0c # 6 instructions; loads
|
||||
spin main 6b6d8c 6b6da0 # 6 instructions; loads
|
||||
spin main 6b922c 6b9240 # 6 instructions; loads
|
||||
spin main 6c5408 6c541c # 6 instructions; loads
|
||||
spin main 6c5614 6c5628 # 6 instructions; loads
|
||||
spin main 6ccce0 6cccf4 # 6 instructions; loads
|
||||
spin main 6ccd98 6ccdac # 6 instructions; loads
|
||||
spin main 6d3c9c 6d3cb0 # 6 instructions; loads
|
||||
spin main 6d3ff8 6d400c # 6 instructions; loads
|
||||
spin main 6d61cc 6d61e0 # 6 instructions; loads
|
||||
spin main 6d6210 6d6224 # 6 instructions; loads
|
||||
spin main 6d6254 6d6268 # 6 instructions; loads
|
||||
spin main 6d6298 6d62ac # 6 instructions; loads
|
||||
spin main 6d75c4 6d75d8 # 6 instructions; loads
|
||||
spin main 6d79dc 6d79f0 # 6 instructions; loads
|
||||
spin main 6d9398 6d93ac # 6 instructions; loads
|
||||
spin main 6dd8d0 6dd8e4 # 6 instructions; loads
|
||||
spin main 6de150 6de164 # 6 instructions; loads
|
||||
spin main 6e073c 6e0750 # 6 instructions; loads
|
||||
spin main 6e0a94 6e0aa8 # 6 instructions; loads
|
||||
spin main 6e4eb8 6e4ecc # 6 instructions; loads
|
||||
spin main 6eb590 6eb5a4 # 6 instructions; loads
|
||||
spin main 6eccac 6eccc0 # 6 instructions; loads
|
||||
spin main 6ed9b4 6ed9c8 # 6 instructions; loads
|
||||
spin main 6ee69c 6ee6b0 # 6 instructions; loads
|
||||
spin main 6f94c0 6f94d4 # 6 instructions; loads
|
||||
spin main 6f9738 6f974c # 6 instructions; loads
|
||||
spin main 6fa324 6fa338 # 6 instructions; loads
|
||||
spin main 70d844 70d858 # 6 instructions; loads
|
||||
spin main 70dc74 70dc88 # 6 instructions; loads
|
||||
spin main 710a70 710a84 # 6 instructions; loads
|
||||
spin main 711848 71185c # 6 instructions; loads
|
||||
spin main 7125a4 7125b8 # 6 instructions; loads
|
||||
spin main 716474 716488 # 6 instructions; loads
|
||||
spin main 719a30 719a44 # 6 instructions; loads
|
||||
spin main 72cd78 72cd8c # 6 instructions; loads
|
||||
spin main 72f66c 72f680 # 6 instructions; loads
|
||||
spin main 737684 737698 # 6 instructions; loads
|
||||
spin main 73e0dc 73e0f0 # 6 instructions; loads
|
||||
spin main 73ed14 73ed28 # 6 instructions; loads
|
||||
spin main 783520 783534 # 6 instructions; loads
|
||||
spin main 797924 797938 # 6 instructions; loads
|
||||
spin main 79794c 797960 # 6 instructions; loads
|
||||
spin main 797c08 797c1c # 6 instructions; loads
|
||||
spin main 797c64 797c78 # 6 instructions; loads
|
||||
spin main 798ff4 799008 # 6 instructions; loads
|
||||
spin main 79cbec 79cc00 # 6 instructions; loads
|
||||
spin main 7ae134 7ae148 # 6 instructions; loads
|
||||
spin main 7ae388 7ae39c # 6 instructions; loads
|
||||
spin main 7c292c 7c2940 # 6 instructions; loads
|
||||
spin main 7d618c 7d61a0 # 6 instructions; loads
|
||||
spin main 7d904c 7d9060 # 6 instructions; loads
|
||||
spin main 7e11c0 7e11d4 # 6 instructions; loads
|
||||
spin main 7e1c9c 7e1cb0 # 6 instructions; loads
|
||||
spin main 7e24a0 7e24b4 # 6 instructions; loads
|
||||
spin main 7ed89c 7ed8b0 # 6 instructions; loads
|
||||
spin main 7eef28 7eef3c # 6 instructions; loads
|
||||
spin main 7f515c 7f5170 # 6 instructions; loads
|
||||
spin main 801468 80147c # 6 instructions; loads
|
||||
spin main 801490 8014a4 # 6 instructions; loads
|
||||
spin main 8014b8 8014cc # 6 instructions; loads
|
||||
spin main 80a0a4 80a0b8 # 6 instructions; loads
|
||||
spin main 811500 811514 # 6 instructions; loads
|
||||
spin main 816c40 816c54 # 6 instructions; loads
|
||||
spin main 819450 819464 # 6 instructions; loads
|
||||
spin main 85df6c 85e068 # 4 instructions; loads
|
||||
spin main 863220 863234 # 6 instructions; loads
|
||||
spin main 86352c 863540 # 6 instructions; loads
|
||||
spin main 863a80 863a94 # 6 instructions; loads
|
||||
spin main 863b94 863ba8 # 6 instructions; loads
|
||||
spin main 86543c 865450 # 6 instructions; loads
|
||||
spin main 865464 865478 # 6 instructions; loads
|
||||
spin main 87e564 87e578 # 6 instructions; loads
|
||||
spin main 87e58c 87e5a0 # 6 instructions; loads
|
||||
spin main 880d44 880d58 # 6 instructions; loads
|
||||
spin main 8810f4 881108 # 6 instructions; loads
|
||||
spin main 8814c8 8814dc # 6 instructions; loads
|
||||
spin main 88189c 8818b0 # 6 instructions; loads
|
||||
spin main 887ce8 887cfc # 6 instructions; loads
|
||||
spin main 887f50 887f64 # 6 instructions; loads
|
||||
spin main 8884ec 888500 # 6 instructions; loads
|
||||
spin main 888618 88862c # 6 instructions; loads
|
||||
spin main 888748 88875c # 6 instructions; loads
|
||||
spin main 8888e0 8888f4 # 6 instructions; loads
|
||||
spin main 89183c 891850 # 6 instructions; loads
|
||||
spin main 892ccc 892cec # 9 instructions; loads
|
||||
spin main 89540c 89541c # 5 instructions; loads
|
||||
spin main 895668 895694 # 12 instructions; loads
|
||||
spin main 8956a8 8956b8 # 5 instructions; loads
|
||||
spin main 8da7dc 8da7e4 # 3 instructions; loads
|
||||
spin main 8da7ec 8da7f0 # 2 instructions; loads
|
||||
spin main 8da858 8da860 # 3 instructions; loads
|
||||
spin main 8fb190 8fb1a4 # 6 instructions; loads
|
||||
spin main 9045bc 9045d0 # 6 instructions; loads
|
||||
spin main 9045e4 9045f8 # 6 instructions; loads
|
||||
spin main 90460c 904620 # 6 instructions; loads
|
||||
spin main 904634 904648 # 6 instructions; loads
|
||||
spin main 905b20 905b34 # 6 instructions; loads
|
||||
spin main 9062f0 906304 # 6 instructions; loads
|
||||
spin main 91019c 9101b0 # 6 instructions; loads
|
||||
spin main 910720 910734 # 6 instructions; loads
|
||||
spin main 9363cc 9363e0 # 6 instructions; loads
|
||||
spin main 93655c 936570 # 6 instructions; loads
|
||||
spin main 93819c 9381b0 # 6 instructions; loads
|
||||
spin main 940060 94007c # 8 instructions; loads
|
||||
spin main 94012c 940148 # 8 instructions; loads
|
||||
spin main 97864c 978660 # 6 instructions; loads
|
||||
spin main 99aa8c 99aabc # 13 instructions; loads
|
||||
spin main 99aad0 99aae0 # 5 instructions; loads
|
||||
spin main 9a5fd4 9a6004 # 13 instructions; loads
|
||||
spin main 9a6018 9a6028 # 5 instructions; loads
|
||||
spin main 9b2700 9b2710 # 5 instructions; loads
|
||||
spin main 9f395c 9f3970 # 6 instructions; loads
|
||||
spin main 9f39e0 9f39f4 # 6 instructions; loads
|
||||
spin main 9f3b2c 9f3b40 # 6 instructions; loads
|
||||
spin main 9f3bf8 9f3c0c # 6 instructions; loads
|
||||
spin main 9f6f5c 9f6f70 # 6 instructions; loads
|
||||
spin main a11908 a1191c # 6 instructions; loads
|
||||
spin main a1257c a12590 # 6 instructions; loads
|
||||
spin main a14d6c a14d80 # 6 instructions; loads
|
||||
spin main a1b0a4 a1b0b8 # 6 instructions; loads
|
||||
spin main a1b0ec a1b100 # 6 instructions; loads
|
||||
spin main a1c3dc a1c3f0 # 6 instructions; loads
|
||||
spin main a2b268 a2b27c # 6 instructions; loads
|
||||
spin main a2cab8 a2cacc # 6 instructions; loads
|
||||
spin main a2f228 a2f23c # 6 instructions; loads
|
||||
spin main a8e920 a8e934 # 6 instructions; loads
|
||||
spin main aa2c20 aa2c34 # 6 instructions; loads
|
||||
spin main aa5108 aa511c # 6 instructions; loads
|
||||
spin main aa9f94 aa9fa8 # 6 instructions; loads
|
||||
spin main aaa674 aaa688 # 6 instructions; loads
|
||||
spin main aad564 aad578 # 6 instructions; loads
|
||||
spin main ab245c ab24b4 # 7 instructions; loads
|
||||
spin main ac8874 ac88fc # 35 instructions; loads
|
||||
spin main af4040 af4054 # 6 instructions; loads
|
||||
spin main af4068 af407c # 6 instructions; loads
|
||||
spin main af4090 af40a4 # 6 instructions; loads
|
||||
spin main af40cc af40e0 # 6 instructions; loads
|
||||
spin main af40f4 af4108 # 6 instructions; loads
|
||||
spin main af411c af4130 # 6 instructions; loads
|
||||
spin main b47a74 b47a88 # 6 instructions; loads
|
||||
spin main b4e080 b4e094 # 6 instructions; loads
|
||||
spin main b529f4 b52a08 # 6 instructions; loads
|
||||
spin main b52a7c b52a90 # 6 instructions; loads
|
||||
spin main b52cfc b52d10 # 6 instructions; loads
|
||||
spin main b52dd0 b52de4 # 6 instructions; loads
|
||||
spin main b53458 b5346c # 6 instructions; loads
|
||||
spin main b535a8 b535bc # 6 instructions; loads
|
||||
spin main b54320 b54334 # 6 instructions; loads
|
||||
spin main b545fc b54610 # 6 instructions; loads
|
||||
spin main b5662c b56640 # 6 instructions; loads
|
||||
spin main b56654 b56668 # 6 instructions; loads
|
||||
spin main b71800 b71814 # 6 instructions; loads
|
||||
spin main b71828 b7183c # 6 instructions; loads
|
||||
spin main b7b82c b7b840 # 6 instructions; loads
|
||||
spin main b80970 b80984 # 6 instructions; loads
|
||||
spin main b86fe8 b86ffc # 6 instructions; loads
|
||||
spin main b8702c b87040 # 6 instructions; loads
|
||||
spin main b87070 b87084 # 6 instructions; loads
|
||||
spin main b8775c b87770 # 6 instructions; loads
|
||||
spin main b97d6c b97d80 # 6 instructions; loads
|
||||
spin main b97ed4 b97ee8 # 6 instructions; loads
|
||||
spin main b9c628 b9c63c # 6 instructions; loads
|
||||
spin main b9c6c8 b9c6dc # 6 instructions; loads
|
||||
spin main b9c6f0 b9c704 # 6 instructions; loads
|
||||
spin main b9c7b8 b9c7cc # 6 instructions; loads
|
||||
spin main ba1db4 ba1dc8 # 6 instructions; loads
|
||||
spin main ba72e8 ba72fc # 6 instructions; loads
|
||||
spin main bb1070 bb1084 # 6 instructions; loads
|
||||
spin main bb1098 bb10ac # 6 instructions; loads
|
||||
spin main bb6c38 bb6c4c # 6 instructions; loads
|
||||
spin main bb7fa0 bb7fb4 # 6 instructions; loads
|
||||
spin main bd27c8 bd27d8 # 5 instructions; loads
|
||||
spin main bdd678 bdd68c # 6 instructions; loads
|
||||
spin main bf2ccc bf2ce0 # 6 instructions; loads
|
||||
spin main bf2e24 bf2e38 # 6 instructions; loads
|
||||
spin main bf32a0 bf32b4 # 6 instructions; loads
|
||||
spin main bf3730 bf3744 # 6 instructions; loads
|
||||
spin main c0a554 c0a568 # 6 instructions; loads
|
||||
spin main c0a6b4 c0a6c8 # 6 instructions; loads
|
||||
spin main c0a850 c0a864 # 6 instructions; loads
|
||||
spin main c0d758 c0d76c # 6 instructions; loads
|
||||
spin main c0d8b0 c0d8c4 # 6 instructions; loads
|
||||
spin main c0dd3c c0dd50 # 6 instructions; loads
|
||||
spin main c15ffc c16010 # 6 instructions; loads
|
||||
spin main c1665c c16670 # 6 instructions; loads
|
||||
spin main c2315c c23170 # 6 instructions; loads
|
||||
spin main c2374c c23760 # 6 instructions; loads
|
||||
spin main c23998 c239ac # 6 instructions; loads
|
||||
spin main c2d128 c2d13c # 6 instructions; loads
|
||||
spin main c3a9cc c3a9e0 # 6 instructions; loads
|
||||
spin main c5ab4c c5ab60 # 6 instructions; loads
|
||||
spin main c70388 c7039c # 6 instructions; loads
|
||||
spin main c703b0 c703c4 # 6 instructions; loads
|
||||
spin main c94f70 c94f84 # 6 instructions; loads
|
||||
spin main c95a00 c95a14 # 6 instructions; loads
|
||||
spin main c96764 c96778 # 6 instructions; loads
|
||||
spin main c96948 c9695c # 6 instructions; loads
|
||||
spin main c96ce4 c96cf8 # 6 instructions; loads
|
||||
spin main c96d54 c96d68 # 6 instructions; loads
|
||||
spin main c97400 c97414 # 6 instructions; loads
|
||||
spin main c97ca0 c97cb4 # 6 instructions; loads
|
||||
spin main c99ba0 c99bb4 # 6 instructions; loads
|
||||
spin main c99c90 c99ca4 # 6 instructions; loads
|
||||
spin main c9abac c9abc0 # 6 instructions; loads
|
||||
spin main c9ac8c c9aca0 # 6 instructions; loads
|
||||
spin main c9b2e0 c9b2f4 # 6 instructions; loads
|
||||
spin main c9d9f4 c9da08 # 6 instructions; loads
|
||||
spin main c9db3c c9db50 # 6 instructions; loads
|
||||
spin main c9df14 c9df30 # 8 instructions; loads
|
||||
spin main ccaaa8 ccaabc # 6 instructions; loads
|
||||
spin main cd38a4 cd38b8 # 6 instructions; loads
|
||||
spin main cda7c4 cda7d8 # 6 instructions; loads
|
||||
spin main cdb388 cdb39c # 6 instructions; loads
|
||||
spin main cdb3cc cdb3e0 # 6 instructions; loads
|
||||
spin main cdbab4 cdbac8 # 6 instructions; loads
|
||||
spin main cdbaf8 cdbb0c # 6 instructions; loads
|
||||
spin main cdbce4 cdbcf8 # 6 instructions; loads
|
||||
spin main cdce0c cdce20 # 6 instructions; loads
|
||||
spin main cecedc cecef0 # 6 instructions; loads
|
||||
spin main d0fbb4 d0fbc8 # 6 instructions; loads
|
||||
spin main d1acd0 d1ace4 # 6 instructions; loads
|
||||
spin main d1dc54 d1dc60 d1dcac d1dcc4 # 29 instructions; loads
|
||||
spin main d2273c d22750 # 6 instructions; loads
|
||||
spin main d231bc d231d0 # 6 instructions; loads
|
||||
spin main d23398 d233ac # 6 instructions; loads
|
||||
spin main d2e810 d2e824 # 6 instructions; loads
|
||||
spin main d3d8dc d3d8f0 # 6 instructions; loads
|
||||
spin main d4c3a8 d4c3bc # 6 instructions; loads
|
||||
spin main d4ffc4 d4ffd8 # 6 instructions; loads
|
||||
spin main d50a78 d50a8c # 6 instructions; loads
|
||||
spin main d50b04 d50b18 # 6 instructions; loads
|
||||
spin main d50bfc d50c10 # 6 instructions; loads
|
||||
spin main d50e60 d50e74 # 6 instructions; loads
|
||||
spin main d50e88 d50e9c # 6 instructions; loads
|
||||
spin main d50ebc d50ed0 # 6 instructions; loads
|
||||
spin main d510b8 d510cc # 6 instructions; loads
|
||||
spin main d51afc d51b18 # 8 instructions; loads
|
||||
spin main d52478 d5248c # 6 instructions; loads
|
||||
spin main d527f4 d52808 # 6 instructions; loads
|
||||
spin main d5283c d52850 # 6 instructions; loads
|
||||
spin main d52864 d52878 # 6 instructions; loads
|
||||
spin main d5288c d528a0 # 6 instructions; loads
|
||||
spin main d528f8 d5290c # 6 instructions; loads
|
||||
spin main d52920 d52934 # 6 instructions; loads
|
||||
spin main d529a8 d529bc # 6 instructions; loads
|
||||
spin main d52ab0 d52ac4 # 6 instructions; loads
|
||||
spin main d91344 d91358 # 6 instructions; loads
|
||||
spin main d91970 d91984 # 6 instructions; loads
|
||||
spin main d91cc0 d91cd4 # 6 instructions; loads
|
||||
spin main d91dc0 d91dd4 # 6 instructions; loads
|
||||
spin main d92280 d92294 # 6 instructions; loads
|
||||
spin main d923cc d923e0 # 6 instructions; loads
|
||||
spin main d93600 d93614 # 6 instructions; loads
|
||||
spin main d94100 d94114 # 6 instructions; loads
|
||||
spin main d9412c d94140 # 6 instructions; loads
|
||||
spin main d99098 d990ac # 6 instructions; loads
|
||||
spin main d9c260 d9c298 # 8 instructions; loads
|
||||
spin main d9c2c8 d9c300 # 8 instructions; loads
|
||||
spin main d9fe34 d9fe48 # 6 instructions; loads
|
||||
spin main da1eb4 da1ec8 # 6 instructions; loads
|
||||
spin main da1edc da1ef0 # 6 instructions; loads
|
||||
spin main da7010 da7024 # 6 instructions; loads
|
||||
spin main da70f8 da710c # 6 instructions; loads
|
||||
spin main dade1c dade30 # 6 instructions; loads
|
||||
spin main db58dc db58f0 # 6 instructions; loads
|
||||
spin main dbb0e4 dbb0f8 # 6 instructions; loads
|
||||
spin main dbca30 dbca4c # 8 instructions; loads
|
||||
spin main dbfc20 dbfc34 # 6 instructions; loads
|
||||
spin main dbff00 dbff14 # 6 instructions; loads
|
||||
spin main dc78fc dc7910 # 6 instructions; loads
|
||||
spin main dcaa88 dcaa9c # 6 instructions; loads
|
||||
spin main dcb2ac dcb2c0 # 6 instructions; loads
|
||||
spin main dcb9c8 dcb9dc # 6 instructions; loads
|
||||
spin main dd4a28 dd4a3c # 6 instructions; loads
|
||||
spin main dd6414 dd6428 # 6 instructions; loads
|
||||
spin main dd7498 dd74ac # 6 instructions; loads
|
||||
spin main dd78b4 dd78c8 # 6 instructions; loads
|
||||
spin main ddf338 ddf34c # 6 instructions; loads
|
||||
spin main de7324 de7338 # 6 instructions; loads
|
||||
spin main deb218 deb22c # 6 instructions; loads
|
||||
spin main df249c df24b0 # 6 instructions; loads
|
||||
spin main df2930 df2944 # 6 instructions; loads
|
||||
spin main dfab0c dfab20 # 6 instructions; loads
|
||||
spin main e1a248 e1a25c # 6 instructions; loads
|
||||
spin main e27b10 e27b24 # 6 instructions; loads
|
||||
spin main e28428 e2843c # 6 instructions; loads
|
||||
spin main e38cac e38cc0 # 6 instructions; loads
|
||||
spin main e38f90 e38fa4 # 6 instructions; loads
|
||||
spin main e39274 e39288 # 6 instructions; loads
|
||||
spin main e39558 e3956c # 6 instructions; loads
|
||||
spin main e40b1c e40b30 # 6 instructions; loads
|
||||
spin main e41478 e4148c # 6 instructions; loads
|
||||
spin main e450bc e450d0 # 6 instructions; loads
|
||||
spin main e45400 e45414 # 6 instructions; loads
|
||||
spin main e51aa0 e51ab4 # 6 instructions; loads
|
||||
spin main e51c08 e51c1c # 6 instructions; loads
|
||||
spin main e51cf4 e51d08 # 6 instructions; loads
|
||||
spin main e8e42c e8e440 # 6 instructions; loads
|
||||
spin main e90628 e9063c # 6 instructions; loads
|
||||
spin main e930d8 e930ec # 6 instructions; loads
|
||||
spin main e94c64 e94c78 # 6 instructions; loads
|
||||
spin main e9557c e95590 # 6 instructions; loads
|
||||
spin main e966e4 e966f8 # 6 instructions; loads
|
||||
spin main e9711c e97130 # 6 instructions; loads
|
||||
spin main e97164 e97178 # 6 instructions; loads
|
||||
spin main e9c134 e9c148 # 6 instructions; loads
|
||||
spin main ea4c28 ea4c3c # 6 instructions; loads
|
||||
spin main ea4c78 ea4c8c # 6 instructions; loads
|
||||
spin main ea4cc4 ea4cd8 # 6 instructions; loads
|
||||
spin main ea4d10 ea4d24 # 6 instructions; loads
|
||||
spin main eb07cc eb07d4 # 3 instructions; loads
|
||||
spin main eb07dc eb07e0 # 2 instructions; loads
|
||||
spin main eb0808 eb0810 # 3 instructions; loads
|
||||
spin main eb1548 eb155c # 6 instructions; loads
|
||||
spin main eb4240 eb4254 # 6 instructions; loads
|
||||
spin main eb5aa8 eb5abc # 6 instructions; loads
|
||||
spin main eb637c eb6390 # 6 instructions; loads
|
||||
spin main eb7954 eb7968 # 6 instructions; loads
|
||||
spin main ebf63c ebf650 # 6 instructions; loads
|
||||
spin main ebfb5c ebfb70 # 6 instructions; loads
|
||||
spin main ebfb98 ebfbac # 6 instructions; loads
|
||||
spin main ebfd50 ebfd64 # 6 instructions; loads
|
||||
spin main ebffb4 ebffc8 # 6 instructions; loads
|
||||
spin main eca534 eca548 # 6 instructions; loads
|
||||
spin main eca6f8 eca70c # 6 instructions; loads
|
||||
spin main ecb088 ecb09c # 6 instructions; loads
|
||||
spin main ed26e8 ed26fc # 6 instructions; loads
|
||||
spin main ed293c ed2950 # 6 instructions; loads
|
||||
spin main ed2ab0 ed2ac4 # 6 instructions; loads
|
||||
spin main ed2c24 ed2c38 # 6 instructions; loads
|
||||
spin main ed2ed0 ed2ee4 # 6 instructions; loads
|
||||
spin main ed3348 ed335c # 6 instructions; loads
|
||||
spin main ed351c ed3530 # 6 instructions; loads
|
||||
spin main ed3cf4 ed3d08 # 6 instructions; loads
|
||||
spin main ed42e0 ed42f4 # 6 instructions; loads
|
||||
spin main ed430c ed4320 # 6 instructions; loads
|
||||
spin main ed5294 ed52a8 # 6 instructions; loads
|
||||
spin main ed5658 ed566c # 6 instructions; loads
|
||||
spin main ed5680 ed5694 # 6 instructions; loads
|
||||
spin main ed79e8 ed79fc # 6 instructions; loads
|
||||
spin main ed7a28 ed7a3c # 6 instructions; loads
|
||||
spin main ed7a50 ed7a64 # 6 instructions; loads
|
||||
spin main ed7a78 ed7a8c # 6 instructions; loads
|
||||
spin main ed80ac ed80c0 # 6 instructions; loads
|
||||
spin main ed80d4 ed80e8 # 6 instructions; loads
|
||||
spin main ed934c ed9360 # 6 instructions; loads
|
||||
spin main ed94a0 ed94b4 # 6 instructions; loads
|
||||
spin main ed9a50 ed9a64 # 6 instructions; loads
|
||||
spin main ede7fc ede810 # 6 instructions; loads
|
||||
spin main ee0ce4 ee0cf8 # 6 instructions; loads
|
||||
spin main eebd28 eebd3c # 6 instructions; loads
|
||||
spin main ef9ed4 ef9ee8 # 6 instructions; loads
|
||||
spin main efa7a4 efa7b8 # 6 instructions; loads
|
||||
spin main efa7d4 efa7e8 # 6 instructions; loads
|
||||
spin main efad94 efada8 # 6 instructions; loads
|
||||
spin main efadc4 efadd8 # 6 instructions; loads
|
||||
spin main efdac8 efdadc # 6 instructions; loads
|
||||
spin main f01fa4 f01fb8 # 6 instructions; loads
|
||||
spin main f022e4 f022f8 # 6 instructions; loads
|
||||
spin main f02394 f023a8 # 6 instructions; loads
|
||||
spin main f02434 f02448 # 6 instructions; loads
|
||||
spin main f024e4 f024f8 # 6 instructions; loads
|
||||
spin main f3c87c f3c890 # 6 instructions; loads
|
||||
spin main f3d54c f3d560 # 6 instructions; loads
|
||||
spin main f3dba8 f3dbbc # 6 instructions; loads
|
||||
spin main f3dbd4 f3dbe8 # 6 instructions; loads
|
||||
spin main f547a4 f547b8 # 6 instructions; loads
|
||||
spin main f54d60 f54d74 # 6 instructions; loads
|
||||
spin main f57494 f574a8 # 6 instructions; loads
|
||||
spin main f66ef8 f66f0c # 6 instructions; loads
|
||||
spin main f66f20 f66f34 # 6 instructions; loads
|
||||
spin main f67170 f67184 # 6 instructions; loads
|
||||
spin main f671fc f67210 # 6 instructions; loads
|
||||
spin main f67224 f67238 # 6 instructions; loads
|
||||
spin main f67254 f67268 # 6 instructions; loads
|
||||
spin main f684f0 f68504 # 6 instructions; loads
|
||||
spin main f6a1ec f6a200 # 6 instructions; loads
|
||||
spin main f6a3f8 f6a40c # 6 instructions; loads
|
||||
spin main f6afc4 f6afd8 # 6 instructions; loads
|
||||
spin main f712a4 f712b8 # 6 instructions; loads
|
||||
spin main f71400 f71414 # 6 instructions; loads
|
||||
spin main f7155c f71570 # 6 instructions; loads
|
||||
spin main f716b8 f716cc # 6 instructions; loads
|
||||
spin main f729dc f729f0 # 6 instructions; loads
|
||||
spin main f73084 f73098 # 6 instructions; loads
|
||||
spin main f73148 f7315c # 6 instructions; loads
|
||||
spin main f76574 f76588 # 6 instructions; loads
|
||||
spin main f7c8ec f7c900 # 6 instructions; loads
|
||||
spin main f83bd0 f83be4 # 6 instructions; loads
|
||||
spin main f870f0 f87104 # 6 instructions; loads
|
||||
spin main f94eac f94ec0 # 6 instructions; loads
|
||||
spin main fa6580 fa6594 # 6 instructions; loads
|
||||
spin main fa65a8 fa65bc # 6 instructions; loads
|
||||
spin main fabb50 fabb64 # 6 instructions; loads
|
||||
spin main fabe74 fabe88 # 6 instructions; loads
|
||||
spin main fac8a0 fac8b4 # 6 instructions; loads
|
||||
spin main fac8c4 fac8d8 # 6 instructions; loads
|
||||
spin main face58 face6c # 6 instructions; loads
|
||||
spin main fc0a28 fc0a3c # 6 instructions; loads
|
||||
spin main fc0c68 fc0c7c # 6 instructions; loads
|
||||
spin main fc0e8c fc0ea0 # 6 instructions; loads
|
||||
spin main fc1054 fc1068 # 6 instructions; loads
|
||||
spin main fc1178 fc118c # 6 instructions; loads
|
||||
spin main fcb18c fcb1a8 # 8 instructions; loads
|
||||
spin main fccfe8 fccffc # 6 instructions; loads
|
||||
spin main fcd554 fcd640 # 4 instructions; loads
|
||||
spin main fcdc5c fcdce4 # 4 instructions; loads
|
||||
spin main fd2d30 fd2d44 # 6 instructions; loads
|
||||
spin main fd6250 fd6264 # 6 instructions; loads
|
||||
spin main fdf818 fdf82c # 6 instructions; loads
|
||||
spin main fdfb30 fdfb44 # 6 instructions; loads
|
||||
spin main fe0fa8 fe0fbc # 6 instructions; loads
|
||||
spin main fe1ad8 fe1aec # 6 instructions; loads
|
||||
spin main fea3a0 fea3b4 # 6 instructions; loads
|
||||
spin main fead08 fead1c # 6 instructions; loads
|
||||
spin main feb168 feb17c # 6 instructions; loads
|
||||
spin main feb1d0 feb1e4 # 6 instructions; loads
|
||||
spin main ff2af8 ff2b0c # 6 instructions; loads
|
||||
spin main ff5680 ff5694 # 6 instructions; loads
|
||||
spin main ff58e0 ff58f4 # 6 instructions; loads
|
||||
spin main ff9660 ff9674 # 6 instructions; loads
|
||||
spin main ffacec ffad00 # 6 instructions; loads
|
||||
spin main 1001500 1001514 # 6 instructions; loads
|
||||
spin main 1002c28 1002c3c # 6 instructions; loads
|
||||
spin main 1010b3c 1010b50 # 6 instructions; loads
|
||||
spin main 1010ef4 1010f08 # 6 instructions; loads
|
||||
spin main 1011244 1011258 # 6 instructions; loads
|
||||
spin main 1011544 1011558 # 6 instructions; loads
|
||||
spin main 1011a9c 1011ab0 # 6 instructions; loads
|
||||
spin main 1011e34 1011e48 # 6 instructions; loads
|
||||
spin main 10121b4 10121c8 # 6 instructions; loads
|
||||
spin main 1012228 101223c # 6 instructions; loads
|
||||
spin main 1013150 1013164 # 6 instructions; loads
|
||||
spin main 101349c 10134b0 # 6 instructions; loads
|
||||
spin main 1013920 1013934 # 6 instructions; loads
|
||||
spin main 1013b6c 1013b80 # 6 instructions; loads
|
||||
spin main 1014950 1014964 # 6 instructions; loads
|
||||
spin main 10149e8 10149fc # 6 instructions; loads
|
||||
spin main 1015bac 1015bc0 # 6 instructions; loads
|
||||
spin main 10160c0 10160d4 # 6 instructions; loads
|
||||
spin main 10166f8 101670c # 6 instructions; loads
|
||||
spin main 10167b4 10167c8 # 6 instructions; loads
|
||||
spin main 1016870 1016884 # 6 instructions; loads
|
||||
spin main 101692c 1016940 # 6 instructions; loads
|
||||
spin main 10170f0 1017104 # 6 instructions; loads
|
||||
spin main 10191dc 10191f0 # 6 instructions; loads
|
||||
spin main 101948c 10194a0 # 6 instructions; loads
|
||||
spin main 1019778 101978c # 6 instructions; loads
|
||||
spin main 1019a2c 1019a40 # 6 instructions; loads
|
||||
spin main 1019cf0 1019d04 # 6 instructions; loads
|
||||
spin main 1019ff4 101a008 # 6 instructions; loads
|
||||
spin main 101a054 101a068 # 6 instructions; loads
|
||||
spin main 101b594 101b5b0 # 8 instructions; loads
|
||||
spin main 101c7b4 101c7c8 # 6 instructions; loads
|
||||
spin main 101c7fc 101c810 # 6 instructions; loads
|
||||
spin main 101c844 101c858 # 6 instructions; loads
|
||||
spin main 101c88c 101c8a0 # 6 instructions; loads
|
||||
spin main 101c8d4 101c8e8 # 6 instructions; loads
|
||||
spin main 1023190 10231a4 # 6 instructions; loads
|
||||
spin main 10461ac 10461c0 # 6 instructions; loads
|
||||
spin main 1047278 104728c # 6 instructions; loads
|
||||
spin main 1050a8c 1050aa0 # 6 instructions; loads
|
||||
spin main 1055074 10550bc # 3 instructions; loads
|
||||
spin main 1056bd4 1056be8 # 6 instructions; loads
|
||||
spin main 106037c 1060390 # 6 instructions; loads
|
||||
spin main 1066bf4 1066c08 # 6 instructions; loads
|
||||
spin main 106bc38 106bc4c # 6 instructions; loads
|
||||
spin main 106be80 106be94 # 6 instructions; loads
|
||||
spin main 1070220 1070234 # 6 instructions; loads
|
||||
spin main 1071264 1071278 # 6 instructions; loads
|
||||
spin main 1071788 107179c # 6 instructions; loads
|
||||
spin main 1073910 1073924 # 6 instructions; loads
|
||||
spin main 10815e0 10815f4 # 6 instructions; loads
|
||||
spin main 108cb28 108cb3c # 6 instructions; loads
|
||||
spin main 1095678 109568c # 6 instructions; loads
|
||||
spin main 10956c0 10956d4 # 6 instructions; loads
|
||||
spin main 109f134 109f148 # 6 instructions; loads
|
||||
spin main 10bc820 10bc834 # 6 instructions; loads
|
||||
spin main 10be4c8 10be4dc # 6 instructions; loads
|
||||
spin main 10be4f0 10be504 # 6 instructions; loads
|
||||
spin main 10c2d4c 10c2d60 # 6 instructions; loads
|
||||
spin main 10c3748 10c375c # 6 instructions; loads
|
||||
spin main 10d5ef8 10d5f0c # 6 instructions; loads
|
||||
spin main 10da208 10da21c # 6 instructions; loads
|
||||
spin main 10e3edc 10e3ef0 # 6 instructions; loads
|
||||
spin main 10e5ee4 10e5ef8 # 6 instructions; loads
|
||||
spin main 10ec860 10ec874 # 6 instructions; loads
|
||||
spin main 10ec8dc 10ec8f0 # 6 instructions; loads
|
||||
spin main 10f1db4 10f1dc8 # 6 instructions; loads
|
||||
spin main 10ff3b4 10ff3c8 # 6 instructions; loads
|
||||
spin main 10ff788 10ff79c # 6 instructions; loads
|
||||
spin main 10ffef0 10fff04 # 6 instructions; loads
|
||||
spin main 1100500 1100514 # 6 instructions; loads
|
||||
spin main 1103e80 1103e94 # 6 instructions; loads
|
||||
spin main 110ca34 110cb68 # 4 instructions; loads
|
||||
spin main 110cab8 110cb4c # 4 instructions; loads
|
||||
spin main 1117420 1117434 # 6 instructions; loads
|
||||
spin main 1117448 111745c # 6 instructions; loads
|
||||
spin main 111bf5c 111bf70 # 6 instructions; loads
|
||||
spin main 111c1bc 111c1d0 # 6 instructions; loads
|
||||
spin main 111c918 111c92c # 6 instructions; loads
|
||||
spin main 112aef0 112af04 # 6 instructions; loads
|
||||
spin main 1143e28 1143e3c # 6 instructions; loads
|
||||
spin main 1145110 1145124 # 6 instructions; loads
|
||||
spin main 1145138 114514c # 6 instructions; loads
|
||||
spin main 1149244 1149258 # 6 instructions; loads
|
||||
spin main 114b868 114b87c # 6 instructions; loads
|
||||
spin main 114f850 114f864 # 6 instructions; loads
|
||||
spin main 114fcdc 114fcf0 # 6 instructions; loads
|
||||
spin main 1150cbc 1150cd0 # 6 instructions; loads
|
||||
spin main 115aa38 115aa4c # 6 instructions; loads
|
||||
spin main 117d7c8 117d7dc # 6 instructions; loads
|
||||
spin main 118a000 118a014 # 6 instructions; loads
|
||||
spin main 118a3d8 118a3ec # 6 instructions; loads
|
||||
spin main 118f65c 118f670 # 6 instructions; loads
|
||||
spin main 1193d30 1193d44 # 6 instructions; loads
|
||||
spin main 1198130 1198144 # 6 instructions; loads
|
||||
spin main 1198158 119816c # 6 instructions; loads
|
||||
spin main 1198e48 1198e5c # 6 instructions; loads
|
||||
spin main 119c414 119c428 # 6 instructions; loads
|
||||
spin main 11a0778 11a078c # 6 instructions; loads
|
||||
spin main 11a0fcc 11a0fe0 # 6 instructions; loads
|
||||
spin main 11a5884 11a5898 # 6 instructions; loads
|
||||
spin main 11bbd80 11bbd94 # 6 instructions; loads
|
||||
spin main 11bd154 11bd168 # 6 instructions; loads
|
||||
spin main 11bd17c 11bd190 # 6 instructions; loads
|
||||
spin main 11e5fc8 11e5fdc # 6 instructions; loads
|
||||
spin main 11e5ff0 11e6004 # 6 instructions; loads
|
||||
spin main 11ea5b8 11ea5cc # 6 instructions; loads
|
||||
spin main 11eccf8 11ecd0c # 6 instructions; loads
|
||||
spin main 11f50d8 11f50ec # 6 instructions; loads
|
||||
spin main 11f5624 11f5638 # 6 instructions; loads
|
||||
spin main 12010e8 12010fc # 6 instructions; loads
|
||||
spin main 120ecc0 120ecd4 # 6 instructions; loads
|
||||
spin main 12171d8 12171ec # 6 instructions; loads
|
||||
spin main 1217cd8 1217cec # 6 instructions; loads
|
||||
spin main 1219124 1219138 # 6 instructions; loads
|
||||
spin main 1231ca8 1231cbc # 6 instructions; loads
|
||||
spin main 1231e58 1231e6c # 6 instructions; loads
|
||||
spin main 1234784 1234798 # 6 instructions; loads
|
||||
spin main 1234d14 1234d28 # 6 instructions; loads
|
||||
spin main 1236974 1236988 # 6 instructions; loads
|
||||
spin main 1236b1c 1236b30 # 6 instructions; loads
|
||||
spin main 12479cc 12479e0 # 6 instructions; loads
|
||||
spin main 1261af8 1261b0c # 6 instructions; loads
|
||||
spin main 1261f20 1261f34 # 6 instructions; loads
|
||||
spin main 1262200 1262214 # 6 instructions; loads
|
||||
spin main 1268448 126845c # 6 instructions; loads
|
||||
spin main 1283a80 1283a94 # 6 instructions; loads
|
||||
spin main 128ba28 128ba3c # 6 instructions; loads
|
||||
spin main 128ba90 128baa4 # 6 instructions; loads
|
||||
spin main 128bc40 128bc54 # 6 instructions; loads
|
||||
spin main 128bc6c 128bc80 # 6 instructions; loads
|
||||
spin main 128bc98 128bcac # 6 instructions; loads
|
||||
spin main 128bcc4 128bcd8 # 6 instructions; loads
|
||||
spin main 128c468 128c47c # 6 instructions; loads
|
||||
spin main 128c490 128c4a4 # 6 instructions; loads
|
||||
spin main 12924ec 1292500 # 6 instructions; loads
|
||||
spin main 1292e4c 1292e60 # 6 instructions; loads
|
||||
spin main 1292eb4 1292ec8 # 6 instructions; loads
|
||||
spin main 129458c 12945a0 # 6 instructions; loads
|
||||
spin main 12966e0 12966f4 # 6 instructions; loads
|
||||
spin main 129e62c 129e640 # 6 instructions; loads
|
||||
spin main 129f954 129f968 # 6 instructions; loads
|
||||
spin main 12a61b0 12a61c4 # 6 instructions; loads
|
||||
spin main 12ab4cc 12ab4e0 # 6 instructions; loads
|
||||
spin main 12ab4f4 12ab508 # 6 instructions; loads
|
||||
spin main 12c1228 12c123c # 6 instructions; loads
|
||||
spin main 12c1250 12c1264 # 6 instructions; loads
|
||||
spin main 12ca068 12ca07c # 6 instructions; loads
|
||||
spin main 12d040c 12d0420 # 6 instructions; loads
|
||||
spin main 12e3820 12e3834 # 6 instructions; loads
|
||||
spin main 12e8e80 12e8e94 # 6 instructions; loads
|
||||
spin main 12e9640 12e9654 # 6 instructions; loads
|
||||
spin main 1309bc8 1309bdc # 6 instructions; loads
|
||||
spin main 1314ed8 1314eec # 6 instructions; loads
|
||||
spin main 131553c 1315550 # 6 instructions; loads
|
||||
spin main 1322ccc 1322dac # 11 instructions; loads
|
||||
spin main 13277f4 1327808 # 6 instructions; loads
|
||||
spin main 1327ab8 1327acc # 6 instructions; loads
|
||||
spin main 132a1e0 132a1f4 # 6 instructions; loads
|
||||
spin main 132b0a8 132b0bc # 6 instructions; loads
|
||||
spin main 132b87c 132b890 # 6 instructions; loads
|
||||
spin main 1332fc0 1332fd4 # 6 instructions; loads
|
||||
spin main 1363450 1363464 # 6 instructions; loads
|
||||
spin main 13feca8 13fecbc # 6 instructions; loads
|
||||
spin main 13fee40 13fee54 # 6 instructions; loads
|
||||
spin main 157c12c 157c140 # 6 instructions; loads
|
||||
spin main 157dfe0 157dff4 # 6 instructions; loads
|
||||
spin main 1585a20 1585a34 # 6 instructions; loads
|
||||
spin main 1585ee0 1585ef4 # 6 instructions; loads
|
||||
spin main 158c4c8 158c4dc # 6 instructions; loads
|
||||
spin main 1599a14 1599a28 # 6 instructions; loads
|
||||
spin main 159aed0 159aee4 # 6 instructions; loads
|
||||
spin main 159aef8 159af0c # 6 instructions; loads
|
||||
spin main 15a2a20 15a2a34 # 6 instructions; loads
|
||||
spin main 15a3228 15a323c # 6 instructions; loads
|
||||
spin main 15a34e8 15a34fc # 6 instructions; loads
|
||||
spin main 15a3510 15a3524 # 6 instructions; loads
|
||||
spin main 15a3f44 15a3f58 # 6 instructions; loads
|
||||
spin main 15ae678 15ae68c # 6 instructions; loads
|
||||
spin main 15b8d78 15b8d8c # 6 instructions; loads
|
||||
spin main 15b9218 15b922c # 6 instructions; loads
|
||||
spin main 15b9320 15b9334 # 6 instructions; loads
|
||||
spin main 15b9638 15b964c # 6 instructions; loads
|
||||
spin main 15c6c9c 15c6cb0 # 6 instructions; loads
|
||||
spin main 15d5380 15d5394 # 6 instructions; loads
|
||||
spin main 15e8a0c 15e8a20 # 6 instructions; loads
|
||||
spin main 15e9ad4 15e9ae8 # 6 instructions; loads
|
||||
spin main 15e9b7c 15e9b90 # 6 instructions; loads
|
||||
spin main 15ef1c4 15ef1d8 # 6 instructions; loads
|
||||
spin main 15ef304 15ef318 # 6 instructions; loads
|
||||
spin main 15ef32c 15ef340 # 6 instructions; loads
|
||||
spin main 1607238 160724c # 6 instructions; loads
|
||||
spin main 160779c 16077b0 # 6 instructions; loads
|
||||
spin main 16173e0 16173f4 # 6 instructions; loads
|
||||
spin main 1623748 162375c # 6 instructions; loads
|
||||
spin main 1627bdc 1627bf0 # 6 instructions; loads
|
||||
spin main 1627c2c 1627c40 # 6 instructions; loads
|
||||
spin main 1628eb8 1628ecc # 6 instructions; loads
|
||||
spin main 16298a8 16298bc # 6 instructions; loads
|
||||
spin main 16340f8 163410c # 6 instructions; loads
|
||||
spin main 1636748 163675c # 6 instructions; loads
|
||||
spin main 1636ed4 1636ee8 # 6 instructions; loads
|
||||
spin main 163b7f0 163b804 # 6 instructions; loads
|
||||
spin main 1648354 1648368 # 6 instructions; loads
|
||||
spin main 1651478 165148c # 6 instructions; loads
|
||||
spin main 1653730 1653744 # 6 instructions; loads
|
||||
spin main 1656c5c 1656c70 # 6 instructions; loads
|
||||
spin main 16596b0 16596b8 # 3 instructions; loads
|
||||
spin main 16596c0 16596c4 # 2 instructions; loads
|
||||
spin main 16596f8 1659700 # 3 instructions; loads
|
||||
spin main 165cb68 165cb7c # 6 instructions; loads
|
||||
spin main 1677368 167737c # 6 instructions; loads
|
||||
spin main 16dc224 16dc238 # 6 instructions; loads
|
||||
spin main 173d61c 173d630 # 6 instructions; loads
|
||||
spin main 1747354 1747368 # 6 instructions; loads
|
||||
spin main 1747918 174792c # 6 instructions; loads
|
||||
spin main 17a0a20 17a0a34 # 6 instructions; loads
|
||||
spin main 1932774 1932788 # 6 instructions; loads
|
||||
spin main 1933ea8 1933ebc # 6 instructions; loads
|
||||
spin main 1962e88 1962e90 # 3 instructions; loads
|
||||
spin main 1962e98 1962e9c # 2 instructions; loads
|
||||
spin main 1962f20 1962f28 # 3 instructions; loads
|
||||
spin main 196300c 1963014 # 3 instructions; loads
|
||||
spin main 196301c 1963020 # 2 instructions; loads
|
||||
spin main 19630c8 19630d0 # 3 instructions; loads
|
||||
spin main 1975850 1975858 # 3 instructions; loads
|
||||
spin main 1975860 1975864 # 2 instructions; loads
|
||||
spin main 1975888 1975890 # 3 instructions; loads
|
||||
spin main 19758c0 19758c8 # 3 instructions; loads
|
||||
spin main 19758d0 19758d4 # 2 instructions; loads
|
||||
spin main 19758fc 1975904 # 3 instructions; loads
|
||||
spin main 19e5048 19e505c # 6 instructions; loads
|
||||
spin main 1a8b8b4 1a8b8c4 # 5 instructions; loads
|
||||
spin main 1b65f9c 1b65fb0 # 6 instructions; loads
|
||||
spin main 1b66058 1b6606c # 6 instructions; loads
|
||||
spin main 1b66114 1b66128 # 6 instructions; loads
|
||||
spin main 1b66714 1b66728 # 6 instructions; loads
|
||||
spin main 1b667d0 1b667e4 # 6 instructions; loads
|
||||
spin main 1b6688c 1b668a0 # 6 instructions; loads
|
||||
spin main 1dff370 1dff37c # 4 instructions; loads
|
||||
spin main 21e6508 21e6530 # 4 instructions; loads
|
||||
spin main 222a3e0 222a3fc # 8 instructions; loads
|
||||
spin main 22322d0 22322d8 # 3 instructions; loads
|
||||
spin main 2232358 2232360 # 3 instructions; loads
|
||||
spin main 22323e8 2232410 # 3 instructions; loads
|
||||
spin main 223931c 223932c # 5 instructions; loads
|
||||
spin main 225dc10 225dc18 # 3 instructions; loads
|
||||
spin main 225dc20 225dc24 # 2 instructions; loads
|
||||
spin main 225dcac 225dcb4 # 3 instructions; loads
|
||||
spin main 225dd38 225dd40 # 3 instructions; loads
|
||||
spin main 225dd48 225dd4c # 2 instructions; loads
|
||||
spin main 225dd64 225dd6c # 3 instructions; loads
|
||||
spin main 22b72dc 22b72f0 # 6 instructions; loads
|
||||
spin main 22b7440 22b7454 # 6 instructions; loads
|
||||
spin main 22c60ac 22c60d0 # 4 instructions; loads
|
||||
spin main 22c619c 22c61a8 # 4 instructions; loads
|
||||
spin main 233ce40 233ce54 # 6 instructions; loads
|
||||
spin main 240ccb0 240ccb8 # 3 instructions; loads
|
||||
spin main 240ccc0 240ccc4 # 2 instructions; loads
|
||||
spin main 240cd40 240cd48 # 3 instructions; loads
|
||||
spin main 240d57c 240d584 # 3 instructions; loads
|
||||
spin main 240d58c 240d590 # 2 instructions; loads
|
||||
spin main 240d5fc 240d604 # 3 instructions; loads
|
||||
spin main 240da9c 240daa4 # 3 instructions; loads
|
||||
spin main 240daac 240dab0 # 2 instructions; loads
|
||||
spin main 240db3c 240db44 # 3 instructions; loads
|
||||
spin main 240dc80 240dc88 # 3 instructions; loads
|
||||
spin main 240dc90 240dc94 # 2 instructions; loads
|
||||
spin main 240dce8 240dcf0 # 3 instructions; loads
|
||||
spin main 240dd34 240dd3c # 3 instructions; loads
|
||||
spin main 240dd44 240dd48 # 2 instructions; loads
|
||||
spin main 240ddfc 240de04 # 3 instructions; loads
|
||||
spin main 240de0c 240de10 # 2 instructions; loads
|
||||
spin main 240de50 240de58 # 3 instructions; loads
|
||||
spin main 240de80 240de88 # 3 instructions; loads
|
||||
spin main 240df74 240df7c # 3 instructions; loads
|
||||
spin main 240df84 240df88 # 2 instructions; loads
|
||||
spin main 240e068 240e070 # 3 instructions; loads
|
||||
spin main 240e0fc 240e104 # 3 instructions; loads
|
||||
spin main 240e10c 240e110 # 2 instructions; loads
|
||||
spin main 240e1c4 240e1cc # 3 instructions; loads
|
||||
spin main 240e1e4 240e1e8 # 2 instructions; loads
|
||||
spin main 240e288 240e290 # 3 instructions; loads
|
||||
spin main 240e2b8 240e2c0 # 3 instructions; loads
|
||||
spin main 240e300 240e308 # 3 instructions; loads
|
||||
spin main 240e310 240e314 # 2 instructions; loads
|
||||
spin main 240e338 240e340 # 3 instructions; loads
|
||||
spin main 240e384 240e38c # 3 instructions; loads
|
||||
spin main 240e394 240e398 # 2 instructions; loads
|
||||
spin main 240e3c0 240e3c8 # 3 instructions; loads
|
||||
spin main 240e91c 240e924 # 3 instructions; loads
|
||||
spin main 240e92c 240e930 # 2 instructions; loads
|
||||
spin main 240e954 240e95c # 3 instructions; loads
|
||||
spin main 240f088 240f090 # 3 instructions; loads
|
||||
spin main 240f098 240f09c # 2 instructions; loads
|
||||
spin main 240f0c0 240f0c8 # 3 instructions; loads
|
||||
spin main 2410984 241098c # 3 instructions; loads
|
||||
spin main 2410994 2410998 # 2 instructions; loads
|
||||
spin main 2410a18 2410a20 # 3 instructions; loads
|
||||
spin main 2410a40 2410a48 # 3 instructions; loads
|
||||
spin main 2410a50 2410a54 # 2 instructions; loads
|
||||
spin main 2410ad4 2410adc # 3 instructions; loads
|
||||
spin main 2410e70 2410e78 # 3 instructions; loads
|
||||
spin main 2410e80 2410e84 # 2 instructions; loads
|
||||
spin main 2410ed4 2410edc # 3 instructions; loads
|
||||
spin main 2410f1c 2410f24 # 3 instructions; loads
|
||||
spin main 2410f2c 2410f30 # 2 instructions; loads
|
||||
spin main 2410fcc 2410fd4 # 3 instructions; loads
|
||||
spin main 241102c 2411034 # 3 instructions; loads
|
||||
spin main 241103c 2411040 # 2 instructions; loads
|
||||
spin main 24110c4 24110cc # 3 instructions; loads
|
||||
spin main 2413d28 2413d30 # 3 instructions; loads
|
||||
spin main 2413d38 2413d3c # 2 instructions; loads
|
||||
spin main 2413dc8 2413dd0 # 3 instructions; loads
|
||||
spin main 2413e04 2413e0c # 3 instructions; loads
|
||||
spin main 2413e14 2413e18 # 2 instructions; loads
|
||||
spin main 2413e84 2413e8c # 3 instructions; loads
|
||||
spin main 2416358 2416360 # 3 instructions; loads
|
||||
spin main 2416368 241636c # 2 instructions; loads
|
||||
spin main 2416388 2416390 # 3 instructions; loads
|
||||
spin main 2462470 2462484 # 6 instructions; loads
|
||||
spin main 2462498 24624ac # 6 instructions; loads
|
||||
spin main 24651f4 2465208 # 6 instructions; loads
|
||||
spin main 2465688 246569c # 6 instructions; loads
|
||||
spin main 2474d20 2474d34 # 6 instructions; loads
|
||||
spin main 2475154 2475168 # 6 instructions; loads
|
||||
spin main 2479134 2479148 # 6 instructions; loads
|
||||
spin main 2479b7c 2479b90 # 6 instructions; loads
|
||||
spin main 247a378 247a38c # 6 instructions; loads
|
||||
spin main 2493050 2493064 # 6 instructions; loads
|
||||
spin main 2886500 2886514 # 6 instructions; loads
|
||||
spin main 28865bc 28865d0 # 6 instructions; loads
|
||||
spin main 2886678 288668c # 6 instructions; loads
|
||||
spin main 2887484 2887498 # 6 instructions; loads
|
||||
spin main 2887540 2887554 # 6 instructions; loads
|
||||
spin main 28875fc 2887610 # 6 instructions; loads
|
||||
spin main 28876b8 28876cc # 6 instructions; loads
|
||||
spin main 2887774 2887788 # 6 instructions; loads
|
||||
spin main 2887830 2887844 # 6 instructions; loads
|
||||
spin main 28878ec 2887900 # 6 instructions; loads
|
||||
spin main 28879a8 28879bc # 6 instructions; loads
|
||||
spin main 2887e70 2887e84 # 6 instructions; loads
|
||||
spin main 2887f2c 2887f40 # 6 instructions; loads
|
||||
spin main 2a3e540 2a3e548 # 3 instructions; loads
|
||||
spin main 2a3e550 2a3e554 # 2 instructions; loads
|
||||
spin main 2a3e58c 2a3e594 # 3 instructions; loads
|
||||
# subsdk0: 8 spin waits
|
||||
spin subsdk0 2090 20a4 # 6 instructions; loads
|
||||
spin subsdk0 278b4 278c8 # 6 instructions; loads
|
||||
spin subsdk0 bcf28 bcfd0 bcfd8 # 14 instructions; loads
|
||||
spin subsdk0 bd0d0 bd178 bd180 # 14 instructions; loads
|
||||
spin subsdk0 c03a0 c03b4 # 6 instructions; loads
|
||||
spin subsdk0 1bbefc 1bbf08 # 4 instructions; loads
|
||||
spin subsdk0 1bd838 1bd84c # 6 instructions; loads
|
||||
spin subsdk0 1c0c68 1c0c74 # 4 instructions; loads
|
||||
# sdk: 5 spin waits
|
||||
spin sdk f5218 f5350 # 14 instructions; loads
|
||||
spin sdk 23a590 23a5d8 # 19 instructions; loads
|
||||
spin sdk 23a658 23a6a0 # 19 instructions; loads
|
||||
spin sdk 23a7fc 23a918 # 19 instructions; loads
|
||||
spin sdk 3d3250 3d3270 # 9 instructions; loads
|
||||
980
recomp/patches-v121.txt
Normal file
980
recomp/patches-v121.txt
Normal file
@@ -0,0 +1,980 @@
|
||||
# Code patches for NX2navi 1.2.1 main (recomp/patches.py). Ported from MacNativeGames games/<game>/runtime/game_patches.c
|
||||
# (MIT, Copyright (c) 2026 MacNativeGames contributors) at upstream ebe621d; 1.2.1 sites from its game_version.h
|
||||
# (2db612b), each original word checked against the 1.2.1 main. The patch page holds two floats the runtime writes at
|
||||
# every present (frame_rate.c): +0 the 30 Hz ratio (seconds * 30), +4 the frame step in seconds.
|
||||
|
||||
set fps60 NX2NAVI_FPS60 1
|
||||
set realtime NX2NAVI_REALTIME 1
|
||||
set cloth NX2NAVI_CLOTH_REALTIME 1
|
||||
set bounce NX2NAVI_HIGHFPS_FIX 1
|
||||
set output NX2NAVI_PATCH_OUTPUT 0
|
||||
set scene NX2NAVI_PATCH_SCENE 0
|
||||
set shadow NX2NAVI_PATCH_SHADOW 0
|
||||
|
||||
# 60 fps: the adaptive present-interval controller (main+0x8f26c8, 1.0.0 0x8f6bec) gets the minimum interval 1.
|
||||
site fps60 main 8f273c 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
|
||||
site fps60 main 8f2740 1a89b10a 1a9fa52a # clamp: csinc w10, w9, wzr, ge (desired = max(desired, 1))
|
||||
site fps60 main 8f2764 b9401a8a d503201f # lower trigger: nop the stored-candidate load
|
||||
site fps60 main 8f27f4 6b08015f 7100055f # raise path: cmp w10, #1
|
||||
site fps60 main 8f2804 1a88c1ac 1a9fc5ac # raise path: csinc w12, w13, wzr, gt (next lower candidate floors at 1)
|
||||
site fps60 main 8f2858 b9400288 52800028 # lower path: mov w8, #1 (minimum interval 1)
|
||||
site fps60 main 8f2874 2a0a03f5 1a9fa555 # lower path: csinc w21, w10, wzr, ge (result never below 1)
|
||||
|
||||
# Real-time step: the VFR step (main+0x7ede6c, 1.0.0 0x7a83e4) and the global ratio after the controller call
|
||||
# (main+0x8f2394, 1.0.0 0x8f68b8) read ratio and seconds from the patch page instead of interval * 0.5 and interval / 60.
|
||||
site realtime main 7ede7c 1e220020 @page:x9 # scvtf s0, w1 -> adrp x9, page
|
||||
site realtime main 7ede80 52a84e08 bd400120 # mov w8, #60.0 -> ldr s0, [x9] (ratio)
|
||||
site realtime main 7ede84 1e270101 bd400521 # fmov s1, w8 -> ldr s1, [x9, #4] (seconds)
|
||||
site realtime main 7ede88 1e2c1002 d503201f # fmov s2, #0.5 -> nop
|
||||
site realtime main 7ede94 1e211801 d503201f # fdiv s1, s0, s1 -> nop
|
||||
site realtime main 7ede98 1e220800 d503201f # fmul s0, s0, s2 -> nop
|
||||
site realtime main 8f2394 bd425e60 @page:x9 # ldr s0, [x19, #0x25c] -> adrp x9, page
|
||||
site realtime main 8f2398 1e2c1001 bd400120 # fmov s1, #0.5 -> ldr s0, [x9] (global ratio)
|
||||
site realtime main 8f23a8 5e21d800 d503201f # scvtf s0, s0 -> nop
|
||||
site realtime main 8f23ac 1e210800 d503201f # fmul s0, s0, s1 -> nop
|
||||
|
||||
# Equipment-switch cooldown (issue #18, found by JieML1100 on 1.4.3): the player update (main+0x852d24) steps the
|
||||
# countdown at player state +0x109c through the timer step (main+0xec98cc) only while it is above FLT_EPSILON, but the
|
||||
# equipment gate (main+0xcae6f8) blocks weapon, shield and bow changes while it is above 0. A measured ratio can leave
|
||||
# a remainder in (0, FLT_EPSILON] that is never stepped again; stepping while above 0 takes it below 0.
|
||||
site realtime main 852d28 1e270101 1e2703e1 # fmov s1, w8 (FLT_EPSILON) -> fmov s1, wzr
|
||||
|
||||
# Hair and cloth (upstream 290ec9f, 8f8022f): the actor animation update builds a second clock {seconds = factor / 30,
|
||||
# ratio = factor} per rendered frame. Three inlined places: at the fmul, seconds = measured seconds * factor; at the
|
||||
# store of the factor into the ratio slot, measured ratio * factor. The call of the shared update (main+0x89822c)
|
||||
# scales s2 (seconds) and s3 (ratio) by the measured ratio. Upstream's veneer bodies, inlined at the site.
|
||||
site cloth main 82192c 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
|
||||
site cloth main 821928 bd0127e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0147ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x124]
|
||||
site cloth main 822a64 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
|
||||
site cloth main 822a40 bd0127e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0147ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x124]
|
||||
site cloth main 83182c 1e210801 f81f0fe9 @page:x9 bd400521 1e210801 f84107e9 # fmul s1, s0, s1
|
||||
site cloth main 831830 bd0137e0 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f081f bd0157ff 3dc007ff f94003e9 910083ff # str s0, [sp, #0x134]
|
||||
site cloth main 8222f0 9401d7cf @page:x8 bd400104 1e240842 1e240863 9401d7cf # bl 0x89822c
|
||||
|
||||
# Trampolines (upstream 27c3b43): the slow-motion check compares time object +0x28 (30 / (ratio * time scale)) with
|
||||
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
|
||||
site bounce main bda798 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
|
||||
|
||||
# Cursor and aim steps (upstream game_patches.c patch_cursor_speed, 363a6eb). These updates read the game's 30 Hz frame
|
||||
# ratio for their smoothing but leave it out of the step itself, so the map cursor and the vertical object aim moved
|
||||
# 2x as fast at 60 fps and 4x at 120 as at the shipping 30 fps (issue #44). Each site multiplies the step by that ratio,
|
||||
# so the speed is the shipping one at any frame rate. x9 and s16 are free: the next read of x9 follows a write, the
|
||||
# function does not touch s16, and x20 keeps the update's frame object for the whole function.
|
||||
set cursor NX2NAVI_HIGHFPS_FIX 1
|
||||
site cursor main ec3d9c bd400361 f94002e9 bd400930 1e300800 bd400361 # map pan speed s0 *= [[x23] + 8], then ldr s1, [x27]
|
||||
site cursor main 1d48d64 1e2a198c 1e2a198c bd400a90 1e30098c # fdiv s12, s12, s10, then s12 *= [x20 + 8]: vertical object aim
|
||||
site cursor main 1d484e4 1e200923 1e200923 bd400a90 1e300863 # fmul s3, s9, s0, then s3 *= [x20 + 8]: vertical gyro aim (Recall)
|
||||
|
||||
# Output size (upstream games/<game>/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
|
||||
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
|
||||
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
|
||||
# through main+0xcc4160) from page +32; the renderer itself goes to page +40.
|
||||
site output main e7aa3c d280f008 @page:x8 # mov x8, #0x780 -> adrp x8, page
|
||||
site output main e7aa40 f2c08708 f9401908 # movk x8, #0x438, lsl #32 -> ldr x8, [x8, #48]
|
||||
site output main cc3f50 aa1403e0 @page:x21 # mov x0, x20 -> adrp x21, page
|
||||
site output main cc3f54 94000083 f90016b3 # bl 0xcc4160 -> str x19, [x21, #40]
|
||||
site output main cc3f58 f9400015 f94012b5 # ldr x21, [x0] -> ldr x21, [x21, #32]
|
||||
|
||||
# Scene render size (upstream patch_render_res): the renderer's +0xcc4 / +0xcc8, which size every scene render target,
|
||||
# take two floats from page +16 instead of the graphics parameters' docked 1600x900.
|
||||
site scene main cc3f68 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
|
||||
site scene main cc3f70 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
|
||||
site scene main cc3f74 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
|
||||
|
||||
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches.c, the 1.2.1 sites of aspect_install;
|
||||
# runtime/resolution.c res_init writes the output's width / height to page +56 and switches the set on). The same four
|
||||
# sites as patches-v143.txt, with the same original words: two camera sites (update and setup) load the camera's copy of
|
||||
# DefaultAspect 1.777778, two viewport routines divide width by height. The replacement loads the ratio from the page
|
||||
# instead (Hor+). x10 is kept on the stack, so no register liveness is assumed.
|
||||
set aspect NX2NAVI_ASPECT_PATCH 0
|
||||
site aspect main cb4b10 bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera update)
|
||||
site aspect main 12521cc bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera setup)
|
||||
site aspect main 19b36f0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
site aspect main 1ecdf68 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
|
||||
|
||||
# Texture pool offsets past 2 GiB (MacNativeGames ca311e2d). The texture loader keeps a texture's place in its pool as a
|
||||
# 32-bit offset (main+0xad4294: sub x25, x9, x10, stored as the word w25) and widens it as a signed number for
|
||||
# nvnTextureBuilderSetStorage's offset and for SetPackagedTextureData's source (the pool's CPU address + offset). The
|
||||
# shipped heap table keeps every pool far below 2 GiB; a texture pack's heap table with NX2NAVI_APP_POOL raised can make
|
||||
# one larger (MacNativeGames saw a 3.58 GiB pool), and every texture past 2 GiB then got storage 4 GiB below its pool.
|
||||
# The offset is widened unsigned instead, the same value below 2 GiB, right up to 4 GiB. 1.4.3 needs no patch: its
|
||||
# loaders (main+0x583c34, main+0x27ed214) keep the offset in 64 bits.
|
||||
set pooloff NX2NAVI_POOL_OFFSET_PATCH 1
|
||||
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
|
||||
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
|
||||
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
|
||||
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), NX2NAVI_SPIN=0 keeps the core.
|
||||
spin main ac600 ac620 ac630 # 13 instructions; loads
|
||||
spin main ac680 ac6a8 ac6b8 # 15 instructions; loads
|
||||
spin main ac6f0 ac718 ac728 # 15 instructions; loads
|
||||
spin main e8544 e854c # 3 instructions; loads
|
||||
spin main 2457f0 245804 # 6 instructions; loads
|
||||
spin main 634d3c 634d50 # 6 instructions; loads
|
||||
spin main 65ddf8 65de0c # 6 instructions; loads
|
||||
spin main 65fe1c 65fe30 # 6 instructions; loads
|
||||
spin main 664350 664364 # 6 instructions; loads
|
||||
spin main 664558 66456c # 6 instructions; loads
|
||||
spin main 66764c 667660 # 6 instructions; loads
|
||||
spin main 66fce0 66fcf4 # 6 instructions; loads
|
||||
spin main 66fe64 66fe78 # 6 instructions; loads
|
||||
spin main 670af8 670b0c # 6 instructions; loads
|
||||
spin main 670ee8 670efc # 6 instructions; loads
|
||||
spin main 671214 671228 # 6 instructions; loads
|
||||
spin main 673a3c 673a50 # 6 instructions; loads
|
||||
spin main 673a80 673a94 # 6 instructions; loads
|
||||
spin main 673ac4 673ad8 # 6 instructions; loads
|
||||
spin main 673b08 673b1c # 6 instructions; loads
|
||||
spin main 678774 678788 # 6 instructions; loads
|
||||
spin main 678814 678828 # 6 instructions; loads
|
||||
spin main 67a270 67a284 # 6 instructions; loads
|
||||
spin main 67a5c8 67a5dc # 6 instructions; loads
|
||||
spin main 67b8a4 67b8b8 # 6 instructions; loads
|
||||
spin main 67dde8 67ddfc # 6 instructions; loads
|
||||
spin main 682650 682664 # 6 instructions; loads
|
||||
spin main 683c5c 683c70 # 6 instructions; loads
|
||||
spin main 68b1fc 68b210 # 6 instructions; loads
|
||||
spin main 68ff0c 68ff20 # 6 instructions; loads
|
||||
spin main 6917ec 691800 # 6 instructions; loads
|
||||
spin main 6a5338 6a534c # 6 instructions; loads
|
||||
spin main 6a55b4 6a55c8 # 6 instructions; loads
|
||||
spin main 6a9488 6a949c # 6 instructions; loads
|
||||
spin main 6a98b8 6a98cc # 6 instructions; loads
|
||||
spin main 6ac5bc 6ac5d0 # 6 instructions; loads
|
||||
spin main 6afad0 6afae4 # 6 instructions; loads
|
||||
spin main 6bb02c 6bb040 # 6 instructions; loads
|
||||
spin main 6bb078 6bb08c # 6 instructions; loads
|
||||
spin main 6c8680 6c8694 # 6 instructions; loads
|
||||
spin main 6ca094 6ca0a8 # 6 instructions; loads
|
||||
spin main 6d1404 6d1418 # 6 instructions; loads
|
||||
spin main 6d8280 6d8294 # 6 instructions; loads
|
||||
spin main 6de7b4 6de7c8 # 6 instructions; loads
|
||||
spin main 6e0740 6e0754 # 6 instructions; loads
|
||||
spin main 6ec574 6ec588 # 6 instructions; loads
|
||||
spin main 6ec6d4 6ec6e8 # 6 instructions; loads
|
||||
spin main 708250 708264 # 6 instructions; loads
|
||||
spin main 708d1c 708d30 # 6 instructions; loads
|
||||
spin main 7090dc 7090f0 # 6 instructions; loads
|
||||
spin main 7093ec 709400 # 6 instructions; loads
|
||||
spin main 709754 709768 # 6 instructions; loads
|
||||
spin main 709b2c 709b40 # 6 instructions; loads
|
||||
spin main 709dd0 709de4 # 6 instructions; loads
|
||||
spin main 709e64 709e78 # 6 instructions; loads
|
||||
spin main 70a444 70a458 # 6 instructions; loads
|
||||
spin main 70a718 70a72c # 6 instructions; loads
|
||||
spin main 70bb8c 70bba8 # 8 instructions; loads
|
||||
spin main 70bc3c 70bc58 # 8 instructions; loads
|
||||
spin main 70d6e4 70d6f8 # 6 instructions; loads
|
||||
spin main 70d8c4 70d8d8 # 6 instructions; loads
|
||||
spin main 70db14 70db28 # 6 instructions; loads
|
||||
spin main 70e7b4 70e7d0 # 8 instructions; loads
|
||||
spin main 70ec04 70ec18 # 6 instructions; loads
|
||||
spin main 70f140 70f154 # 6 instructions; loads
|
||||
spin main 70fd10 70fd24 # 6 instructions; loads
|
||||
spin main 70fe4c 70fe60 # 6 instructions; loads
|
||||
spin main 7101b8 7101cc # 6 instructions; loads
|
||||
spin main 7109a4 7109b8 # 6 instructions; loads
|
||||
spin main 710f04 710f18 # 6 instructions; loads
|
||||
spin main 7111a4 7111b8 # 6 instructions; loads
|
||||
spin main 711424 711438 # 6 instructions; loads
|
||||
spin main 71144c 711460 # 6 instructions; loads
|
||||
spin main 711474 711488 # 6 instructions; loads
|
||||
spin main 711ccc 711ce0 # 6 instructions; loads
|
||||
spin main 711cf4 711d08 # 6 instructions; loads
|
||||
spin main 712cd0 712ce4 # 6 instructions; loads
|
||||
spin main 712dc0 712dd4 # 6 instructions; loads
|
||||
spin main 7130f8 71310c # 6 instructions; loads
|
||||
spin main 716398 7163ac # 6 instructions; loads
|
||||
spin main 7163c0 7163d4 # 6 instructions; loads
|
||||
spin main 7163e8 7163fc # 6 instructions; loads
|
||||
spin main 717b40 717b54 # 6 instructions; loads
|
||||
spin main 7180fc 718110 # 6 instructions; loads
|
||||
spin main 71819c 7181b0 # 6 instructions; loads
|
||||
spin main 7187c8 7187dc # 6 instructions; loads
|
||||
spin main 719124 719138 # 6 instructions; loads
|
||||
spin main 71a638 71a64c # 6 instructions; loads
|
||||
spin main 71cec8 71cedc # 6 instructions; loads
|
||||
spin main 71cef0 71cf04 # 6 instructions; loads
|
||||
spin main 71fcac 71fcc0 # 6 instructions; loads
|
||||
spin main 71fcd4 71fce8 # 6 instructions; loads
|
||||
spin main 71fcfc 71fd10 # 6 instructions; loads
|
||||
spin main 71fd24 71fd38 # 6 instructions; loads
|
||||
spin main 71fd4c 71fd60 # 6 instructions; loads
|
||||
spin main 71fd74 71fd88 # 6 instructions; loads
|
||||
spin main 71fd9c 71fdb0 # 6 instructions; loads
|
||||
spin main 71fdc4 71fdd8 # 6 instructions; loads
|
||||
spin main 71fdec 71fe00 # 6 instructions; loads
|
||||
spin main 71fe14 71fe28 # 6 instructions; loads
|
||||
spin main 71fe3c 71fe50 # 6 instructions; loads
|
||||
spin main 71fe64 71fe78 # 6 instructions; loads
|
||||
spin main 71fe8c 71fea0 # 6 instructions; loads
|
||||
spin main 720678 72068c # 6 instructions; loads
|
||||
spin main 7206a0 7206b4 # 6 instructions; loads
|
||||
spin main 7206c8 7206dc # 6 instructions; loads
|
||||
spin main 7206f0 720704 # 6 instructions; loads
|
||||
spin main 720b0c 720b20 # 6 instructions; loads
|
||||
spin main 720f40 720f54 # 6 instructions; loads
|
||||
spin main 7214d0 7214e4 # 6 instructions; loads
|
||||
spin main 721a90 721aa4 # 6 instructions; loads
|
||||
spin main 721f28 721f3c # 6 instructions; loads
|
||||
spin main 721f50 721f64 # 6 instructions; loads
|
||||
spin main 72590c 725920 # 6 instructions; loads
|
||||
spin main 726340 726354 # 6 instructions; loads
|
||||
spin main 7265d4 7265e8 # 6 instructions; loads
|
||||
spin main 726620 726634 # 6 instructions; loads
|
||||
spin main 727d28 727d3c # 6 instructions; loads
|
||||
spin main 727d50 727d64 # 6 instructions; loads
|
||||
spin main 729da0 729db4 # 6 instructions; loads
|
||||
spin main 729de8 729dfc # 6 instructions; loads
|
||||
spin main 72aa08 72aa1c # 6 instructions; loads
|
||||
spin main 72aa30 72aa44 # 6 instructions; loads
|
||||
spin main 72aef4 72af08 # 6 instructions; loads
|
||||
spin main 72b4cc 72b4e0 # 6 instructions; loads
|
||||
spin main 732c8c 732ca0 # 6 instructions; loads
|
||||
spin main 732cd0 732ce4 # 6 instructions; loads
|
||||
spin main 732cf8 732d0c # 6 instructions; loads
|
||||
spin main 733be8 733bfc # 6 instructions; loads
|
||||
spin main 737fc4 737fd8 # 6 instructions; loads
|
||||
spin main 7395fc 739610 # 6 instructions; loads
|
||||
spin main 73a8b8 73a8cc # 6 instructions; loads
|
||||
spin main 73a8f8 73a90c # 6 instructions; loads
|
||||
spin main 73add0 73ade4 # 6 instructions; loads
|
||||
spin main 73aff4 73b008 # 6 instructions; loads
|
||||
spin main 73b074 73b088 # 6 instructions; loads
|
||||
spin main 73b3f0 73b404 # 6 instructions; loads
|
||||
spin main 73bd4c 73bd60 # 6 instructions; loads
|
||||
spin main 73d330 73d344 # 6 instructions; loads
|
||||
spin main 73ea0c 73ea20 # 6 instructions; loads
|
||||
spin main 7877ac 7877c0 # 6 instructions; loads
|
||||
spin main 78c7b0 78c7c4 # 6 instructions; loads
|
||||
spin main 78c8ac 78c8c0 # 6 instructions; loads
|
||||
spin main 78d5ec 78d600 # 6 instructions; loads
|
||||
spin main 78d7d0 78d7e4 # 6 instructions; loads
|
||||
spin main 78db70 78db84 # 6 instructions; loads
|
||||
spin main 78dbe0 78dbf4 # 6 instructions; loads
|
||||
spin main 78e298 78e2ac # 6 instructions; loads
|
||||
spin main 78ebb8 78ebcc # 6 instructions; loads
|
||||
spin main 791164 791178 # 6 instructions; loads
|
||||
spin main 7911b8 7911cc # 6 instructions; loads
|
||||
spin main 791c28 791c3c # 6 instructions; loads
|
||||
spin main 791cac 791cc0 # 6 instructions; loads
|
||||
spin main 7923dc 7923f0 # 6 instructions; loads
|
||||
spin main 7946f8 79470c # 6 instructions; loads
|
||||
spin main 79474c 794760 # 6 instructions; loads
|
||||
spin main 795dfc 795e10 # 6 instructions; loads
|
||||
spin main 795f58 795f6c # 6 instructions; loads
|
||||
spin main 79632c 796348 # 8 instructions; loads
|
||||
spin main 7a23ac 7a23c0 # 6 instructions; loads
|
||||
spin main 7a23d4 7a23e8 # 6 instructions; loads
|
||||
spin main 7a267c 7a2690 # 6 instructions; loads
|
||||
spin main 7a26d8 7a26ec # 6 instructions; loads
|
||||
spin main 7a3a5c 7a3a70 # 6 instructions; loads
|
||||
spin main 7aec90 7aeca4 # 6 instructions; loads
|
||||
spin main 7b5c70 7b5c84 # 6 instructions; loads
|
||||
spin main 7bc3b8 7bc3cc # 6 instructions; loads
|
||||
spin main 7bc60c 7bc620 # 6 instructions; loads
|
||||
spin main 7db040 7db054 # 6 instructions; loads
|
||||
spin main 7ddfb0 7ddfc4 # 6 instructions; loads
|
||||
spin main 809758 80976c # 6 instructions; loads
|
||||
spin main 825de4 825df8 # 6 instructions; loads
|
||||
spin main 825e68 825e7c # 6 instructions; loads
|
||||
spin main 825fb4 825fc8 # 6 instructions; loads
|
||||
spin main 826080 826094 # 6 instructions; loads
|
||||
spin main 828158 82816c # 6 instructions; loads
|
||||
spin main 838340 838354 # 6 instructions; loads
|
||||
spin main 842fbc 842fd0 # 6 instructions; loads
|
||||
spin main 8440e8 8440fc # 6 instructions; loads
|
||||
spin main 8464f4 846508 # 6 instructions; loads
|
||||
spin main 84cbb0 84cbc4 # 6 instructions; loads
|
||||
spin main 84cbf8 84cc0c # 6 instructions; loads
|
||||
spin main 84d430 84d444 # 6 instructions; loads
|
||||
spin main 85db98 85dbac # 6 instructions; loads
|
||||
spin main 85ed2c 85ed40 # 6 instructions; loads
|
||||
spin main 860d64 860d78 # 6 instructions; loads
|
||||
spin main 867f4c 868048 # 4 instructions; loads
|
||||
spin main 8876e0 8876f4 # 6 instructions; loads
|
||||
spin main 887c50 887c64 # 6 instructions; loads
|
||||
spin main 892ff4 893008 # 6 instructions; loads
|
||||
spin main 893448 89345c # 6 instructions; loads
|
||||
spin main 893a04 893a18 # 6 instructions; loads
|
||||
spin main 893b14 893b28 # 6 instructions; loads
|
||||
spin main 895210 895224 # 6 instructions; loads
|
||||
spin main 895238 89524c # 6 instructions; loads
|
||||
spin main 8b6b60 8b6b74 # 6 instructions; loads
|
||||
spin main 8b7ff0 8b8010 # 9 instructions; loads
|
||||
spin main 8ceed8 8ceeec # 6 instructions; loads
|
||||
spin main 8cf204 8cf218 # 6 instructions; loads
|
||||
spin main 8cf520 8cf534 # 6 instructions; loads
|
||||
spin main 8cf6b8 8cf6cc # 6 instructions; loads
|
||||
spin main 8cf940 8cf954 # 6 instructions; loads
|
||||
spin main 8cfb94 8cfba8 # 6 instructions; loads
|
||||
spin main 8e763c 8e764c # 5 instructions; loads
|
||||
spin main 8e7898 8e78c4 # 12 instructions; loads
|
||||
spin main 8e78d8 8e78e8 # 5 instructions; loads
|
||||
spin main 8e8114 8e8128 # 6 instructions; loads
|
||||
spin main 90750c 907520 # 6 instructions; loads
|
||||
spin main 907534 907548 # 6 instructions; loads
|
||||
spin main 90755c 907570 # 6 instructions; loads
|
||||
spin main 907584 907598 # 6 instructions; loads
|
||||
spin main 908a84 908a98 # 6 instructions; loads
|
||||
spin main 909250 909264 # 6 instructions; loads
|
||||
spin main 91b3f4 91b408 # 6 instructions; loads
|
||||
spin main 91b41c 91b430 # 6 instructions; loads
|
||||
spin main 91dc0c 91dc20 # 6 instructions; loads
|
||||
spin main 91dfbc 91dfd0 # 6 instructions; loads
|
||||
spin main 91e390 91e3a4 # 6 instructions; loads
|
||||
spin main 91e764 91e778 # 6 instructions; loads
|
||||
spin main 923cc4 923cd8 # 6 instructions; loads
|
||||
spin main 95517c 9551ac # 13 instructions; loads
|
||||
spin main 9551c0 9551d0 # 5 instructions; loads
|
||||
spin main 95f63c 95f66c # 13 instructions; loads
|
||||
spin main 95f680 95f690 # 5 instructions; loads
|
||||
spin main 96bb74 96bb84 # 5 instructions; loads
|
||||
spin main 97266c 972688 # 8 instructions; loads
|
||||
spin main 972738 972754 # 8 instructions; loads
|
||||
spin main 9b677c 9b6790 # 6 instructions; loads
|
||||
spin main 9d73b8 9d73cc # 6 instructions; loads
|
||||
spin main 9e10ac 9e10c0 # 6 instructions; loads
|
||||
spin main 9e9470 9e9484 # 6 instructions; loads
|
||||
spin main 9ed71c 9ed730 # 6 instructions; loads
|
||||
spin main 9f7adc 9f7af0 # 6 instructions; loads
|
||||
spin main 9f7b04 9f7b18 # 6 instructions; loads
|
||||
spin main 9f7b2c 9f7b40 # 6 instructions; loads
|
||||
spin main a06cd8 a06cec # 6 instructions; loads
|
||||
spin main a0cfe8 a0cffc # 6 instructions; loads
|
||||
spin main a2abc0 a2abd4 # 6 instructions; loads
|
||||
spin main a6d6f8 a6d70c # 6 instructions; loads
|
||||
spin main a6edec a6ee00 # 6 instructions; loads
|
||||
spin main a6ee30 a6ee44 # 6 instructions; loads
|
||||
spin main a6ee74 a6ee88 # 6 instructions; loads
|
||||
spin main a6f560 a6f574 # 6 instructions; loads
|
||||
spin main a7e134 a7e1bc # 35 instructions; loads
|
||||
spin main a84b24 a84b38 # 6 instructions; loads
|
||||
spin main a84e3c a84e50 # 6 instructions; loads
|
||||
spin main a84eec a84f00 # 6 instructions; loads
|
||||
spin main a84f8c a84fa0 # 6 instructions; loads
|
||||
spin main a8503c a85050 # 6 instructions; loads
|
||||
spin main ab8898 ab88ac # 6 instructions; loads
|
||||
spin main ac3cd8 ac3cec # 6 instructions; loads
|
||||
spin main ac3d64 ac3d78 # 6 instructions; loads
|
||||
spin main ac3fe4 ac3ff8 # 6 instructions; loads
|
||||
spin main ac40e4 ac40f8 # 6 instructions; loads
|
||||
spin main ac4740 ac4754 # 6 instructions; loads
|
||||
spin main ac4890 ac48a4 # 6 instructions; loads
|
||||
spin main ac5604 ac5618 # 6 instructions; loads
|
||||
spin main ac58e0 ac58f4 # 6 instructions; loads
|
||||
spin main ac798c ac79a0 # 6 instructions; loads
|
||||
spin main ac79b4 ac79c8 # 6 instructions; loads
|
||||
spin main ae567c ae5690 # 6 instructions; loads
|
||||
spin main ae6da0 ae6db4 # 6 instructions; loads
|
||||
spin main b1267c b12690 # 6 instructions; loads
|
||||
spin main b12d5c b12d70 # 6 instructions; loads
|
||||
spin main b15df0 b15e04 # 6 instructions; loads
|
||||
spin main b1afbc b1b014 # 7 instructions; loads
|
||||
spin main b2fa8c b2faa0 # 6 instructions; loads
|
||||
spin main b35374 b35388 # 6 instructions; loads
|
||||
spin main b386f4 b38708 # 6 instructions; loads
|
||||
spin main b3871c b38730 # 6 instructions; loads
|
||||
spin main b38744 b38758 # 6 instructions; loads
|
||||
spin main b38780 b38794 # 6 instructions; loads
|
||||
spin main b387a8 b387bc # 6 instructions; loads
|
||||
spin main b387d0 b387e4 # 6 instructions; loads
|
||||
spin main b53ce8 b53cfc # 6 instructions; loads
|
||||
spin main b53e04 b53e18 # 6 instructions; loads
|
||||
spin main b54178 b5418c # 6 instructions; loads
|
||||
spin main b54688 b5469c # 6 instructions; loads
|
||||
spin main b599f0 b59a04 # 6 instructions; loads
|
||||
spin main b59a18 b59a2c # 6 instructions; loads
|
||||
spin main b5f4ec b5f4fc # 5 instructions; loads
|
||||
spin main b6f988 b6f99c # 6 instructions; loads
|
||||
spin main b6faa4 b6fab8 # 6 instructions; loads
|
||||
spin main b6fe28 b6fe3c # 6 instructions; loads
|
||||
spin main b707b0 b707c4 # 6 instructions; loads
|
||||
spin main b70814 b70828 # 6 instructions; loads
|
||||
spin main b70974 b70988 # 6 instructions; loads
|
||||
spin main b70b08 b70b1c # 6 instructions; loads
|
||||
spin main b74104 b74118 # 6 instructions; loads
|
||||
spin main b7426c b74280 # 6 instructions; loads
|
||||
spin main b7e57c b7e590 # 6 instructions; loads
|
||||
spin main b940fc b94110 # 6 instructions; loads
|
||||
spin main b973a0 b973b4 # 6 instructions; loads
|
||||
spin main b97998 b979ac # 6 instructions; loads
|
||||
spin main b97be4 b97bf8 # 6 instructions; loads
|
||||
spin main bbaaf4 bbab08 # 6 instructions; loads
|
||||
spin main bbab1c bbab30 # 6 instructions; loads
|
||||
spin main bc1fd8 bc1fec # 6 instructions; loads
|
||||
spin main bc2078 bc208c # 6 instructions; loads
|
||||
spin main bc20a0 bc20b4 # 6 instructions; loads
|
||||
spin main bc2168 bc217c # 6 instructions; loads
|
||||
spin main be3d24 be3d38 # 6 instructions; loads
|
||||
spin main bf9fc4 bf9fd8 # 6 instructions; loads
|
||||
spin main bfb184 bfb198 # 6 instructions; loads
|
||||
spin main c1e5c0 c1e5d4 # 6 instructions; loads
|
||||
spin main c4657c c46590 # 6 instructions; loads
|
||||
spin main c50574 c50588 # 6 instructions; loads
|
||||
spin main c51810 c51824 # 6 instructions; loads
|
||||
spin main cb300c cb3020 # 6 instructions; loads
|
||||
spin main cb3f68 cb3f7c # 6 instructions; loads
|
||||
spin main cb3fac cb3fc0 # 6 instructions; loads
|
||||
spin main cb4694 cb46a8 # 6 instructions; loads
|
||||
spin main cb46d8 cb46ec # 6 instructions; loads
|
||||
spin main cb48c4 cb48d8 # 6 instructions; loads
|
||||
spin main cb59ec cb5a00 # 6 instructions; loads
|
||||
spin main cb6f68 cb6f7c # 6 instructions; loads
|
||||
spin main cba458 cba46c # 6 instructions; loads
|
||||
spin main cce918 cce92c # 6 instructions; loads
|
||||
spin main cd0144 cd0150 cd019c cd01b4 # 29 instructions; loads
|
||||
spin main cd1fc0 cd1fd4 # 6 instructions; loads
|
||||
spin main cf3c94 cf3ca8 # 6 instructions; loads
|
||||
spin main d0b2a8 d0b2bc # 6 instructions; loads
|
||||
spin main d105b4 d105c8 # 6 instructions; loads
|
||||
spin main d11018 d1102c # 6 instructions; loads
|
||||
spin main d1109c d110b0 # 6 instructions; loads
|
||||
spin main d11148 d1115c # 6 instructions; loads
|
||||
spin main d113a0 d113b4 # 6 instructions; loads
|
||||
spin main d113c8 d113dc # 6 instructions; loads
|
||||
spin main d113fc d11410 # 6 instructions; loads
|
||||
spin main d11644 d11658 # 6 instructions; loads
|
||||
spin main d12028 d12044 # 8 instructions; loads
|
||||
spin main d129a8 d129bc # 6 instructions; loads
|
||||
spin main d12d80 d12d94 # 6 instructions; loads
|
||||
spin main d12dc8 d12ddc # 6 instructions; loads
|
||||
spin main d12df0 d12e04 # 6 instructions; loads
|
||||
spin main d12e18 d12e2c # 6 instructions; loads
|
||||
spin main d12e84 d12e98 # 6 instructions; loads
|
||||
spin main d12eb0 d12ec4 # 6 instructions; loads
|
||||
spin main d12ed8 d12eec # 6 instructions; loads
|
||||
spin main d12fe0 d12ff4 # 6 instructions; loads
|
||||
spin main d17248 d1725c # 6 instructions; loads
|
||||
spin main d17f58 d17f6c # 6 instructions; loads
|
||||
spin main d189d0 d189e4 # 6 instructions; loads
|
||||
spin main d18bac d18bc0 # 6 instructions; loads
|
||||
spin main d2d2a0 d2d2b4 # 6 instructions; loads
|
||||
spin main d2d430 d2d444 # 6 instructions; loads
|
||||
spin main d337e4 d337f8 # 6 instructions; loads
|
||||
spin main d33e1c d33e30 # 6 instructions; loads
|
||||
spin main d34178 d3418c # 6 instructions; loads
|
||||
spin main d34278 d3428c # 6 instructions; loads
|
||||
spin main d3472c d34740 # 6 instructions; loads
|
||||
spin main d3486c d34880 # 6 instructions; loads
|
||||
spin main d35a9c d35ab0 # 6 instructions; loads
|
||||
spin main d365a8 d365bc # 6 instructions; loads
|
||||
spin main d365d4 d365e8 # 6 instructions; loads
|
||||
spin main d38898 d388ac # 6 instructions; loads
|
||||
spin main d3d6c0 d3d6d4 # 6 instructions; loads
|
||||
spin main d3d6e8 d3d6fc # 6 instructions; loads
|
||||
spin main d3f05c d3f070 # 6 instructions; loads
|
||||
spin main d54c90 d54ca4 # 6 instructions; loads
|
||||
spin main d54e14 d54e28 # 6 instructions; loads
|
||||
spin main d55940 d55954 # 6 instructions; loads
|
||||
spin main d56848 d5685c # 6 instructions; loads
|
||||
spin main d5ef1c d5ef30 # 6 instructions; loads
|
||||
spin main d5ef80 d5ef94 # 6 instructions; loads
|
||||
spin main d66218 d6622c # 6 instructions; loads
|
||||
spin main d69544 d69558 # 6 instructions; loads
|
||||
spin main d6956c d69580 # 6 instructions; loads
|
||||
spin main d7663c d76650 # 6 instructions; loads
|
||||
spin main d7b1e4 d7b1f8 # 6 instructions; loads
|
||||
spin main d7ca84 d7caa0 # 8 instructions; loads
|
||||
spin main d808f4 d80908 # 6 instructions; loads
|
||||
spin main d82a8c d82aa0 # 6 instructions; loads
|
||||
spin main d82c10 d82c24 # 6 instructions; loads
|
||||
spin main d85fc8 d86000 # 8 instructions; loads
|
||||
spin main d86030 d86068 # 8 instructions; loads
|
||||
spin main d8bb9c d8bbb0 # 6 instructions; loads
|
||||
spin main d8be7c d8be90 # 6 instructions; loads
|
||||
spin main d8dfe4 d8dff8 # 6 instructions; loads
|
||||
spin main d8f9d4 d8f9e8 # 6 instructions; loads
|
||||
spin main d90a58 d90a6c # 6 instructions; loads
|
||||
spin main d90e74 d90e88 # 6 instructions; loads
|
||||
spin main d92afc d92b10 # 6 instructions; loads
|
||||
spin main d93320 d93334 # 6 instructions; loads
|
||||
spin main d93b10 d93b24 # 6 instructions; loads
|
||||
spin main d9cbe8 d9cbfc # 6 instructions; loads
|
||||
spin main d9fe88 d9fe9c # 6 instructions; loads
|
||||
spin main dc8368 dc837c # 6 instructions; loads
|
||||
spin main de3ec0 de3ed4 # 6 instructions; loads
|
||||
spin main dec308 dec31c # 6 instructions; loads
|
||||
spin main dfe0bc dfe0d0 # 6 instructions; loads
|
||||
spin main e3ef08 e3ef1c # 6 instructions; loads
|
||||
spin main e3f1ec e3f200 # 6 instructions; loads
|
||||
spin main e3f4d0 e3f4e4 # 6 instructions; loads
|
||||
spin main e3f7b4 e3f7c8 # 6 instructions; loads
|
||||
spin main e42c54 e42c68 # 6 instructions; loads
|
||||
spin main e4a83c e4a850 # 6 instructions; loads
|
||||
spin main e4ab60 e4ab74 # 6 instructions; loads
|
||||
spin main e5413c e54150 # 6 instructions; loads
|
||||
spin main e56300 e56314 # 6 instructions; loads
|
||||
spin main e599b8 e599cc # 6 instructions; loads
|
||||
spin main e59cc4 e59cd8 # 6 instructions; loads
|
||||
spin main e59db0 e59dc4 # 6 instructions; loads
|
||||
spin main e5dea0 e5deb4 # 6 instructions; loads
|
||||
spin main e5eb04 e5eb18 # 6 instructions; loads
|
||||
spin main e602c4 e602d8 # 6 instructions; loads
|
||||
spin main e64d84 e64d98 # 6 instructions; loads
|
||||
spin main e67d5c e67d70 # 6 instructions; loads
|
||||
spin main e686b8 e686cc # 6 instructions; loads
|
||||
spin main e6bd64 e6bd78 # 6 instructions; loads
|
||||
spin main e75788 e7579c # 6 instructions; loads
|
||||
spin main e7a790 e7a7a4 # 6 instructions; loads
|
||||
spin main e892bc e892d0 # 6 instructions; loads
|
||||
spin main e892f4 e89308 # 6 instructions; loads
|
||||
spin main e894b4 e894c8 # 6 instructions; loads
|
||||
spin main e8959c e895b0 # 6 instructions; loads
|
||||
spin main e92610 e92624 # 6 instructions; loads
|
||||
spin main e99480 e99494 # 6 instructions; loads
|
||||
spin main e995d4 e995e8 # 6 instructions; loads
|
||||
spin main e99b84 e99b98 # 6 instructions; loads
|
||||
spin main e9a670 e9a684 # 6 instructions; loads
|
||||
spin main e9a8c4 e9a8d8 # 6 instructions; loads
|
||||
spin main e9aa38 e9aa4c # 6 instructions; loads
|
||||
spin main e9abac e9abc0 # 6 instructions; loads
|
||||
spin main e9ae58 e9ae6c # 6 instructions; loads
|
||||
spin main e9b338 e9b34c # 6 instructions; loads
|
||||
spin main e9b4a4 e9b4b8 # 6 instructions; loads
|
||||
spin main e9bc7c e9bc90 # 6 instructions; loads
|
||||
spin main e9c268 e9c27c # 6 instructions; loads
|
||||
spin main e9c294 e9c2a8 # 6 instructions; loads
|
||||
spin main e9d21c e9d230 # 6 instructions; loads
|
||||
spin main e9d5e8 e9d5fc # 6 instructions; loads
|
||||
spin main e9d610 e9d624 # 6 instructions; loads
|
||||
spin main e9f978 e9f98c # 6 instructions; loads
|
||||
spin main e9f9b8 e9f9cc # 6 instructions; loads
|
||||
spin main e9f9e0 e9f9f4 # 6 instructions; loads
|
||||
spin main e9fa08 e9fa1c # 6 instructions; loads
|
||||
spin main e9ffec ea0000 # 6 instructions; loads
|
||||
spin main ea0014 ea0028 # 6 instructions; loads
|
||||
spin main ea1454 ea1468 # 6 instructions; loads
|
||||
spin main eacd8c eacda0 # 6 instructions; loads
|
||||
spin main eb8ec0 eb8ed4 # 6 instructions; loads
|
||||
spin main ed2f5c ed2f70 # 6 instructions; loads
|
||||
spin main ed34f0 ed3504 # 6 instructions; loads
|
||||
spin main ed3dc0 ed3dd4 # 6 instructions; loads
|
||||
spin main ed3df0 ed3e04 # 6 instructions; loads
|
||||
spin main ed43b4 ed43c8 # 6 instructions; loads
|
||||
spin main ed43e4 ed43f8 # 6 instructions; loads
|
||||
spin main ed7870 ed7878 # 3 instructions; loads
|
||||
spin main ed7880 ed7884 # 2 instructions; loads
|
||||
spin main ed78ac ed78b4 # 3 instructions; loads
|
||||
spin main ed85a8 ed85bc # 6 instructions; loads
|
||||
spin main ed9ff8 eda00c # 6 instructions; loads
|
||||
spin main eda1bc eda1d0 # 6 instructions; loads
|
||||
spin main f0dcc4 f0dcd8 # 6 instructions; loads
|
||||
spin main f0e598 f0e5ac # 6 instructions; loads
|
||||
spin main f11920 f11934 # 6 instructions; loads
|
||||
spin main f11f7c f11f90 # 6 instructions; loads
|
||||
spin main f11fa8 f11fbc # 6 instructions; loads
|
||||
spin main f17f98 f17fac # 6 instructions; loads
|
||||
spin main f1c328 f1c33c # 6 instructions; loads
|
||||
spin main f2403c f24050 # 6 instructions; loads
|
||||
spin main f36b5c f36b70 # 6 instructions; loads
|
||||
spin main f36b84 f36b98 # 6 instructions; loads
|
||||
spin main f36dd4 f36de8 # 6 instructions; loads
|
||||
spin main f36e68 f36e7c # 6 instructions; loads
|
||||
spin main f36e90 f36ea4 # 6 instructions; loads
|
||||
spin main f36eb8 f36ecc # 6 instructions; loads
|
||||
spin main f3b5dc f3b5f0 # 6 instructions; loads
|
||||
spin main f3b7e8 f3b7fc # 6 instructions; loads
|
||||
spin main f3c3b4 f3c3c8 # 6 instructions; loads
|
||||
spin main f3dcc8 f3dcdc # 6 instructions; loads
|
||||
spin main f4181c f41830 # 6 instructions; loads
|
||||
spin main f41978 f4198c # 6 instructions; loads
|
||||
spin main f41ad4 f41ae8 # 6 instructions; loads
|
||||
spin main f41c30 f41c44 # 6 instructions; loads
|
||||
spin main f46af0 f46b04 # 6 instructions; loads
|
||||
spin main f487d8 f487ec # 6 instructions; loads
|
||||
spin main f4f108 f4f11c # 6 instructions; loads
|
||||
spin main f4f7f4 f4f808 # 6 instructions; loads
|
||||
spin main f4f880 f4f894 # 6 instructions; loads
|
||||
spin main f53f9c f53fb0 # 6 instructions; loads
|
||||
spin main f5df2c f5df40 # 6 instructions; loads
|
||||
spin main f5f458 f5f46c # 6 instructions; loads
|
||||
spin main f6f51c f6f530 # 6 instructions; loads
|
||||
spin main f6f840 f6f854 # 6 instructions; loads
|
||||
spin main f70270 f70284 # 6 instructions; loads
|
||||
spin main f70294 f702a8 # 6 instructions; loads
|
||||
spin main f70828 f7083c # 6 instructions; loads
|
||||
spin main f7947c f79490 # 6 instructions; loads
|
||||
spin main f794a4 f794b8 # 6 instructions; loads
|
||||
spin main f7e318 f7e32c # 6 instructions; loads
|
||||
spin main f8db54 f8db68 # 6 instructions; loads
|
||||
spin main f8f9f4 f8fa08 # 6 instructions; loads
|
||||
spin main f90178 f9018c # 6 instructions; loads
|
||||
spin main f96a00 f96a14 # 6 instructions; loads
|
||||
spin main f96c40 f96c54 # 6 instructions; loads
|
||||
spin main f96e64 f96e78 # 6 instructions; loads
|
||||
spin main f96fdc f96ff0 # 6 instructions; loads
|
||||
spin main f97158 f9716c # 6 instructions; loads
|
||||
spin main fa26d4 fa26e8 # 6 instructions; loads
|
||||
spin main fa5a18 fa5a2c # 6 instructions; loads
|
||||
spin main faa880 faa894 # 6 instructions; loads
|
||||
spin main faab1c faab30 # 6 instructions; loads
|
||||
spin main fabc28 fabc3c # 6 instructions; loads
|
||||
spin main fac5d0 fac5e4 # 6 instructions; loads
|
||||
spin main faece4 faecf8 # 6 instructions; loads
|
||||
spin main faf014 faf028 # 6 instructions; loads
|
||||
spin main fb4494 fb44b0 # 8 instructions; loads
|
||||
spin main fb68f0 fb6904 # 6 instructions; loads
|
||||
spin main fb7130 fb7144 # 6 instructions; loads
|
||||
spin main fb7590 fb75a4 # 6 instructions; loads
|
||||
spin main fb75f8 fb760c # 6 instructions; loads
|
||||
spin main fb8ea4 fb8f90 # 4 instructions; loads
|
||||
spin main fb9598 fb9620 # 4 instructions; loads
|
||||
spin main fbfb8c fbfba0 # 6 instructions; loads
|
||||
spin main fcbea0 fcbeb4 # 6 instructions; loads
|
||||
spin main fcc110 fcc124 # 6 instructions; loads
|
||||
spin main fd281c fd2830 # 6 instructions; loads
|
||||
spin main fda1c4 fda1d8 # 6 instructions; loads
|
||||
spin main fe7c74 fe7c88 # 6 instructions; loads
|
||||
spin main fe802c fe8040 # 6 instructions; loads
|
||||
spin main fe837c fe8390 # 6 instructions; loads
|
||||
spin main fe867c fe8690 # 6 instructions; loads
|
||||
spin main fe8bd4 fe8be8 # 6 instructions; loads
|
||||
spin main fe8f6c fe8f80 # 6 instructions; loads
|
||||
spin main fe92ec fe9300 # 6 instructions; loads
|
||||
spin main fe9360 fe9374 # 6 instructions; loads
|
||||
spin main fea288 fea29c # 6 instructions; loads
|
||||
spin main fea5d4 fea5e8 # 6 instructions; loads
|
||||
spin main feaa58 feaa6c # 6 instructions; loads
|
||||
spin main feaca4 feacb8 # 6 instructions; loads
|
||||
spin main feba88 feba9c # 6 instructions; loads
|
||||
spin main febb20 febb34 # 6 instructions; loads
|
||||
spin main fecce4 feccf8 # 6 instructions; loads
|
||||
spin main fed1f8 fed20c # 6 instructions; loads
|
||||
spin main fed830 fed844 # 6 instructions; loads
|
||||
spin main fed8ec fed900 # 6 instructions; loads
|
||||
spin main fed9a8 fed9bc # 6 instructions; loads
|
||||
spin main feda64 feda78 # 6 instructions; loads
|
||||
spin main fee228 fee23c # 6 instructions; loads
|
||||
spin main ff0314 ff0328 # 6 instructions; loads
|
||||
spin main ff05c4 ff05d8 # 6 instructions; loads
|
||||
spin main ff08b0 ff08c4 # 6 instructions; loads
|
||||
spin main ff0b64 ff0b78 # 6 instructions; loads
|
||||
spin main ff0e28 ff0e3c # 6 instructions; loads
|
||||
spin main ff112c ff1140 # 6 instructions; loads
|
||||
spin main ff118c ff11a0 # 6 instructions; loads
|
||||
spin main ff26d0 ff26ec # 8 instructions; loads
|
||||
spin main ff38e0 ff38f4 # 6 instructions; loads
|
||||
spin main ff3920 ff3934 # 6 instructions; loads
|
||||
spin main ff3960 ff3974 # 6 instructions; loads
|
||||
spin main ff39a0 ff39b4 # 6 instructions; loads
|
||||
spin main ff39e0 ff39f4 # 6 instructions; loads
|
||||
spin main ff65b0 ff65c4 # 6 instructions; loads
|
||||
spin main ff96ec ff9700 # 6 instructions; loads
|
||||
spin main 1017c00 1017c14 # 6 instructions; loads
|
||||
spin main 101a9e4 101a9f8 # 6 instructions; loads
|
||||
spin main 101c0b8 101c0cc # 6 instructions; loads
|
||||
spin main 10242b0 10242c4 # 6 instructions; loads
|
||||
spin main 102bc78 102bcc0 # 3 instructions; loads
|
||||
spin main 1036db4 1036dc8 # 6 instructions; loads
|
||||
spin main 1042cf0 1042cf8 # 3 instructions; loads
|
||||
spin main 1042d00 1042d04 # 2 instructions; loads
|
||||
spin main 1042d6c 1042d74 # 3 instructions; loads
|
||||
spin main 10587c4 10587d8 # 6 instructions; loads
|
||||
spin main 1059808 105981c # 6 instructions; loads
|
||||
spin main 1059d2c 1059d40 # 6 instructions; loads
|
||||
spin main 105beb0 105bec4 # 6 instructions; loads
|
||||
spin main 105e700 105e714 # 6 instructions; loads
|
||||
spin main 1066350 1066364 # 6 instructions; loads
|
||||
spin main 1075340 1075354 # 6 instructions; loads
|
||||
spin main 1086b00 1086b14 # 6 instructions; loads
|
||||
spin main 1086b48 1086b5c # 6 instructions; loads
|
||||
spin main 10a671c 10a6730 # 6 instructions; loads
|
||||
spin main 10a9640 10a9654 # 6 instructions; loads
|
||||
spin main 10ab2e8 10ab2fc # 6 instructions; loads
|
||||
spin main 10ab310 10ab324 # 6 instructions; loads
|
||||
spin main 10af3b8 10af3cc # 6 instructions; loads
|
||||
spin main 10af9a8 10af9bc # 6 instructions; loads
|
||||
spin main 10b6d0c 10b6d20 # 6 instructions; loads
|
||||
spin main 10c765c 10c7670 # 6 instructions; loads
|
||||
spin main 10d588c 10d58a0 # 6 instructions; loads
|
||||
spin main 10dabd4 10dabe8 # 6 instructions; loads
|
||||
spin main 10dafa8 10dafbc # 6 instructions; loads
|
||||
spin main 10db6e0 10db6f4 # 6 instructions; loads
|
||||
spin main 10dbc78 10dbc8c # 6 instructions; loads
|
||||
spin main 10de660 10de674 # 6 instructions; loads
|
||||
spin main 10ebcac 10ebcc0 # 6 instructions; loads
|
||||
spin main 10ebd28 10ebd3c # 6 instructions; loads
|
||||
spin main 10f3274 10f3288 # 6 instructions; loads
|
||||
spin main 10f329c 10f32b0 # 6 instructions; loads
|
||||
spin main 10f9384 10f94b8 # 4 instructions; loads
|
||||
spin main 10f9408 10f949c # 4 instructions; loads
|
||||
spin main 1106318 110632c # 6 instructions; loads
|
||||
spin main 1106860 1106874 # 6 instructions; loads
|
||||
spin main 1111040 1111054 # 6 instructions; loads
|
||||
spin main 1111b8c 1111ba0 # 6 instructions; loads
|
||||
spin main 1111dec 1111e00 # 6 instructions; loads
|
||||
spin main 111256c 1112580 # 6 instructions; loads
|
||||
spin main 1115ca4 1115cb8 # 6 instructions; loads
|
||||
spin main 1117c88 1117c9c # 6 instructions; loads
|
||||
spin main 111b654 111b668 # 6 instructions; loads
|
||||
spin main 113bf9c 113bfb0 # 6 instructions; loads
|
||||
spin main 113bfc4 113bfd8 # 6 instructions; loads
|
||||
spin main 11404a0 11404b4 # 6 instructions; loads
|
||||
spin main 114f7e8 114f7fc # 6 instructions; loads
|
||||
spin main 115891c 1158930 # 6 instructions; loads
|
||||
spin main 1159860 1159874 # 6 instructions; loads
|
||||
spin main 116b594 116b5a8 # 6 instructions; loads
|
||||
spin main 116b9ec 116ba00 # 6 instructions; loads
|
||||
spin main 116ba14 116ba28 # 6 instructions; loads
|
||||
spin main 1173168 117317c # 6 instructions; loads
|
||||
spin main 1178e1c 1178e30 # 6 instructions; loads
|
||||
spin main 117af24 117af38 # 6 instructions; loads
|
||||
spin main 118444c 1184460 # 6 instructions; loads
|
||||
spin main 1184828 118483c # 6 instructions; loads
|
||||
spin main 1185b8c 1185ba0 # 6 instructions; loads
|
||||
spin main 1186c48 1186c5c # 6 instructions; loads
|
||||
spin main 118ba50 118ba64 # 6 instructions; loads
|
||||
spin main 118ca3c 118ca50 # 6 instructions; loads
|
||||
spin main 1191f94 1191fa8 # 6 instructions; loads
|
||||
spin main 1191fbc 1191fd0 # 6 instructions; loads
|
||||
spin main 1192b18 1192b2c # 6 instructions; loads
|
||||
spin main 119b934 119b948 # 6 instructions; loads
|
||||
spin main 119d40c 119d420 # 6 instructions; loads
|
||||
spin main 11b1d80 11b1d94 # 6 instructions; loads
|
||||
spin main 11b4ca0 11b4cb4 # 6 instructions; loads
|
||||
spin main 11b6300 11b6314 # 6 instructions; loads
|
||||
spin main 11b8e58 11b8e6c # 6 instructions; loads
|
||||
spin main 11bea60 11bea74 # 6 instructions; loads
|
||||
spin main 11d8218 11d822c # 6 instructions; loads
|
||||
spin main 11db090 11db0a4 # 6 instructions; loads
|
||||
spin main 11db0b8 11db0cc # 6 instructions; loads
|
||||
spin main 1201688 120169c # 6 instructions; loads
|
||||
spin main 12048a4 12048b8 # 6 instructions; loads
|
||||
spin main 12048cc 12048e0 # 6 instructions; loads
|
||||
spin main 1205868 120587c # 6 instructions; loads
|
||||
spin main 1206350 1206364 # 6 instructions; loads
|
||||
spin main 1226308 122631c # 6 instructions; loads
|
||||
spin main 1226898 12268ac # 6 instructions; loads
|
||||
spin main 12374a8 12374bc # 6 instructions; loads
|
||||
spin main 1252260 1252274 # 6 instructions; loads
|
||||
spin main 1252688 125269c # 6 instructions; loads
|
||||
spin main 1252968 125297c # 6 instructions; loads
|
||||
spin main 1271260 1271274 # 6 instructions; loads
|
||||
spin main 127b710 127b724 # 6 instructions; loads
|
||||
spin main 127b778 127b78c # 6 instructions; loads
|
||||
spin main 127b928 127b93c # 6 instructions; loads
|
||||
spin main 127b954 127b968 # 6 instructions; loads
|
||||
spin main 127b980 127b994 # 6 instructions; loads
|
||||
spin main 127b9ac 127b9c0 # 6 instructions; loads
|
||||
spin main 127c150 127c164 # 6 instructions; loads
|
||||
spin main 127c178 127c18c # 6 instructions; loads
|
||||
spin main 12816c8 12816dc # 6 instructions; loads
|
||||
spin main 1282010 1282024 # 6 instructions; loads
|
||||
spin main 1282078 128208c # 6 instructions; loads
|
||||
spin main 1285674 1285688 # 6 instructions; loads
|
||||
spin main 12877c8 12877dc # 6 instructions; loads
|
||||
spin main 128dd24 128dd38 # 6 instructions; loads
|
||||
spin main 128f04c 128f060 # 6 instructions; loads
|
||||
spin main 12af6ac 12af6c0 # 6 instructions; loads
|
||||
spin main 12af6d4 12af6e8 # 6 instructions; loads
|
||||
spin main 12ba674 12ba688 # 6 instructions; loads
|
||||
spin main 12cc354 12cc368 # 6 instructions; loads
|
||||
spin main 12fc134 12fc148 # 6 instructions; loads
|
||||
spin main 12fc798 12fc7ac # 6 instructions; loads
|
||||
spin main 13098cc 13098e0 # 6 instructions; loads
|
||||
spin main 130ce14 130cef4 # 11 instructions; loads
|
||||
spin main 1312798 13127ac # 6 instructions; loads
|
||||
spin main 131363c 1313650 # 6 instructions; loads
|
||||
spin main 1313900 1313914 # 6 instructions; loads
|
||||
spin main 131b4c8 131b4dc # 6 instructions; loads
|
||||
spin main 134c220 134c234 # 6 instructions; loads
|
||||
spin main 13e7d68 13e7d7c # 6 instructions; loads
|
||||
spin main 13e7f00 13e7f14 # 6 instructions; loads
|
||||
spin main 1571e58 1571e6c # 6 instructions; loads
|
||||
spin main 1578834 1578848 # 6 instructions; loads
|
||||
spin main 1585d04 1585d18 # 6 instructions; loads
|
||||
spin main 1587128 158713c # 6 instructions; loads
|
||||
spin main 1587150 1587164 # 6 instructions; loads
|
||||
spin main 158ec94 158eca8 # 6 instructions; loads
|
||||
spin main 158f7d4 158f7e8 # 6 instructions; loads
|
||||
spin main 158fab0 158fac4 # 6 instructions; loads
|
||||
spin main 158fad8 158faec # 6 instructions; loads
|
||||
spin main 1590c38 1590c4c # 6 instructions; loads
|
||||
spin main 15a8180 15a8194 # 6 instructions; loads
|
||||
spin main 15a8618 15a862c # 6 instructions; loads
|
||||
spin main 15a8720 15a8734 # 6 instructions; loads
|
||||
spin main 15a8a38 15a8a4c # 6 instructions; loads
|
||||
spin main 15c4eb8 15c4ecc # 6 instructions; loads
|
||||
spin main 15d8974 15d8988 # 6 instructions; loads
|
||||
spin main 15d9f60 15d9f74 # 6 instructions; loads
|
||||
spin main 15da008 15da01c # 6 instructions; loads
|
||||
spin main 15df528 15df53c # 6 instructions; loads
|
||||
spin main 15df668 15df67c # 6 instructions; loads
|
||||
spin main 15df690 15df6a4 # 6 instructions; loads
|
||||
spin main 15f7dfc 15f7e10 # 6 instructions; loads
|
||||
spin main 15f8360 15f8374 # 6 instructions; loads
|
||||
spin main 15f98a0 15f98b4 # 6 instructions; loads
|
||||
spin main 1608f04 1608f18 # 6 instructions; loads
|
||||
spin main 161523c 1615250 # 6 instructions; loads
|
||||
spin main 161966c 1619680 # 6 instructions; loads
|
||||
spin main 16196bc 16196d0 # 6 instructions; loads
|
||||
spin main 161a810 161a824 # 6 instructions; loads
|
||||
spin main 1625758 162576c # 6 instructions; loads
|
||||
spin main 1627da8 1627dbc # 6 instructions; loads
|
||||
spin main 1628534 1628548 # 6 instructions; loads
|
||||
spin main 162ce60 162ce74 # 6 instructions; loads
|
||||
spin main 1639828 163983c # 6 instructions; loads
|
||||
spin main 1641574 1641588 # 6 instructions; loads
|
||||
spin main 1643838 164384c # 6 instructions; loads
|
||||
spin main 16496b4 16496bc # 3 instructions; loads
|
||||
spin main 16496c4 16496c8 # 2 instructions; loads
|
||||
spin main 16496fc 1649704 # 3 instructions; loads
|
||||
spin main 1691fa4 1691fb8 # 6 instructions; loads
|
||||
spin main 16cc754 16cc768 # 6 instructions; loads
|
||||
spin main 172da0c 172da20 # 6 instructions; loads
|
||||
spin main 1737834 1737848 # 6 instructions; loads
|
||||
spin main 1737df8 1737e0c # 6 instructions; loads
|
||||
spin main 1790da0 1790db4 # 6 instructions; loads
|
||||
spin main 1922fec 1923000 # 6 instructions; loads
|
||||
spin main 1924710 1924724 # 6 instructions; loads
|
||||
spin main 1953798 19537a0 # 3 instructions; loads
|
||||
spin main 19537a8 19537ac # 2 instructions; loads
|
||||
spin main 1953830 1953838 # 3 instructions; loads
|
||||
spin main 195391c 1953924 # 3 instructions; loads
|
||||
spin main 195392c 1953930 # 2 instructions; loads
|
||||
spin main 19539d8 19539e0 # 3 instructions; loads
|
||||
spin main 1966130 1966138 # 3 instructions; loads
|
||||
spin main 1966140 1966144 # 2 instructions; loads
|
||||
spin main 1966168 1966170 # 3 instructions; loads
|
||||
spin main 19661a0 19661a8 # 3 instructions; loads
|
||||
spin main 19661b0 19661b4 # 2 instructions; loads
|
||||
spin main 19661dc 19661e4 # 3 instructions; loads
|
||||
spin main 19d6938 19d694c # 6 instructions; loads
|
||||
spin main 1a7ce6c 1a7ce7c # 5 instructions; loads
|
||||
spin main 1b5662c 1b56640 # 6 instructions; loads
|
||||
spin main 1b566e8 1b566fc # 6 instructions; loads
|
||||
spin main 1b567a4 1b567b8 # 6 instructions; loads
|
||||
spin main 1b56da4 1b56db8 # 6 instructions; loads
|
||||
spin main 1b56e60 1b56e74 # 6 instructions; loads
|
||||
spin main 1b56f1c 1b56f30 # 6 instructions; loads
|
||||
spin main 1d34e90 1d34ea4 # 6 instructions; loads
|
||||
spin main 1df2cac 1df2cb8 # 4 instructions; loads
|
||||
spin main 21dc108 21dc130 # 4 instructions; loads
|
||||
spin main 221fe50 221fe6c # 8 instructions; loads
|
||||
spin main 2227d40 2227d48 # 3 instructions; loads
|
||||
spin main 2227dc8 2227dd0 # 3 instructions; loads
|
||||
spin main 2227e58 2227e80 # 3 instructions; loads
|
||||
spin main 222ed8c 222ed9c # 5 instructions; loads
|
||||
spin main 2255970 2255978 # 3 instructions; loads
|
||||
spin main 2255980 2255984 # 2 instructions; loads
|
||||
spin main 2255a0c 2255a14 # 3 instructions; loads
|
||||
spin main 2255a98 2255aa0 # 3 instructions; loads
|
||||
spin main 2255aa8 2255aac # 2 instructions; loads
|
||||
spin main 2255ac4 2255acc # 3 instructions; loads
|
||||
spin main 22aeafc 22aeb10 # 6 instructions; loads
|
||||
spin main 22aec60 22aec74 # 6 instructions; loads
|
||||
spin main 22bd7bc 22bd7e0 # 4 instructions; loads
|
||||
spin main 22bd8ac 22bd8b8 # 4 instructions; loads
|
||||
spin main 23352a4 23352b8 # 6 instructions; loads
|
||||
spin main 2406890 2406898 # 3 instructions; loads
|
||||
spin main 24068a0 24068a4 # 2 instructions; loads
|
||||
spin main 2406920 2406928 # 3 instructions; loads
|
||||
spin main 240715c 2407164 # 3 instructions; loads
|
||||
spin main 240716c 2407170 # 2 instructions; loads
|
||||
spin main 24071dc 24071e4 # 3 instructions; loads
|
||||
spin main 240767c 2407684 # 3 instructions; loads
|
||||
spin main 240768c 2407690 # 2 instructions; loads
|
||||
spin main 240771c 2407724 # 3 instructions; loads
|
||||
spin main 2407860 2407868 # 3 instructions; loads
|
||||
spin main 2407870 2407874 # 2 instructions; loads
|
||||
spin main 24078c8 24078d0 # 3 instructions; loads
|
||||
spin main 2407914 240791c # 3 instructions; loads
|
||||
spin main 2407924 2407928 # 2 instructions; loads
|
||||
spin main 24079dc 24079e4 # 3 instructions; loads
|
||||
spin main 24079ec 24079f0 # 2 instructions; loads
|
||||
spin main 2407a30 2407a38 # 3 instructions; loads
|
||||
spin main 2407a60 2407a68 # 3 instructions; loads
|
||||
spin main 2407b54 2407b5c # 3 instructions; loads
|
||||
spin main 2407b64 2407b68 # 2 instructions; loads
|
||||
spin main 2407c48 2407c50 # 3 instructions; loads
|
||||
spin main 2407cdc 2407ce4 # 3 instructions; loads
|
||||
spin main 2407cec 2407cf0 # 2 instructions; loads
|
||||
spin main 2407da4 2407dac # 3 instructions; loads
|
||||
spin main 2407dc4 2407dc8 # 2 instructions; loads
|
||||
spin main 2407e68 2407e70 # 3 instructions; loads
|
||||
spin main 2407e98 2407ea0 # 3 instructions; loads
|
||||
spin main 2407ee0 2407ee8 # 3 instructions; loads
|
||||
spin main 2407ef0 2407ef4 # 2 instructions; loads
|
||||
spin main 2407f18 2407f20 # 3 instructions; loads
|
||||
spin main 2407f64 2407f6c # 3 instructions; loads
|
||||
spin main 2407f74 2407f78 # 2 instructions; loads
|
||||
spin main 2407fa0 2407fa8 # 3 instructions; loads
|
||||
spin main 24084fc 2408504 # 3 instructions; loads
|
||||
spin main 240850c 2408510 # 2 instructions; loads
|
||||
spin main 2408534 240853c # 3 instructions; loads
|
||||
spin main 2408c68 2408c70 # 3 instructions; loads
|
||||
spin main 2408c78 2408c7c # 2 instructions; loads
|
||||
spin main 2408ca0 2408ca8 # 3 instructions; loads
|
||||
spin main 240a564 240a56c # 3 instructions; loads
|
||||
spin main 240a574 240a578 # 2 instructions; loads
|
||||
spin main 240a5f8 240a600 # 3 instructions; loads
|
||||
spin main 240a620 240a628 # 3 instructions; loads
|
||||
spin main 240a630 240a634 # 2 instructions; loads
|
||||
spin main 240a6b4 240a6bc # 3 instructions; loads
|
||||
spin main 240aa50 240aa58 # 3 instructions; loads
|
||||
spin main 240aa60 240aa64 # 2 instructions; loads
|
||||
spin main 240aab4 240aabc # 3 instructions; loads
|
||||
spin main 240aafc 240ab04 # 3 instructions; loads
|
||||
spin main 240ab0c 240ab10 # 2 instructions; loads
|
||||
spin main 240abac 240abb4 # 3 instructions; loads
|
||||
spin main 240ac0c 240ac14 # 3 instructions; loads
|
||||
spin main 240ac1c 240ac20 # 2 instructions; loads
|
||||
spin main 240aca4 240acac # 3 instructions; loads
|
||||
spin main 240d908 240d910 # 3 instructions; loads
|
||||
spin main 240d918 240d91c # 2 instructions; loads
|
||||
spin main 240d9a8 240d9b0 # 3 instructions; loads
|
||||
spin main 240d9e4 240d9ec # 3 instructions; loads
|
||||
spin main 240d9f4 240d9f8 # 2 instructions; loads
|
||||
spin main 240da64 240da6c # 3 instructions; loads
|
||||
spin main 240ff38 240ff40 # 3 instructions; loads
|
||||
spin main 240ff48 240ff4c # 2 instructions; loads
|
||||
spin main 240ff68 240ff70 # 3 instructions; loads
|
||||
spin main 245c2ec 245c300 # 6 instructions; loads
|
||||
spin main 245c314 245c328 # 6 instructions; loads
|
||||
spin main 245f0dc 245f0f0 # 6 instructions; loads
|
||||
spin main 245f570 245f584 # 6 instructions; loads
|
||||
spin main 246ea9c 246eab0 # 6 instructions; loads
|
||||
spin main 246eee8 246eefc # 6 instructions; loads
|
||||
spin main 2472f60 2472f74 # 6 instructions; loads
|
||||
spin main 24739ac 24739c0 # 6 instructions; loads
|
||||
spin main 24741a8 24741bc # 6 instructions; loads
|
||||
spin main 248f710 248f724 # 6 instructions; loads
|
||||
spin main 287ff50 287ff64 # 6 instructions; loads
|
||||
spin main 288000c 2880020 # 6 instructions; loads
|
||||
spin main 28800c8 28800dc # 6 instructions; loads
|
||||
spin main 2880ed4 2880ee8 # 6 instructions; loads
|
||||
spin main 2880f90 2880fa4 # 6 instructions; loads
|
||||
spin main 288104c 2881060 # 6 instructions; loads
|
||||
spin main 2881108 288111c # 6 instructions; loads
|
||||
spin main 28811c4 28811d8 # 6 instructions; loads
|
||||
spin main 2881280 2881294 # 6 instructions; loads
|
||||
spin main 288133c 2881350 # 6 instructions; loads
|
||||
spin main 28813f8 288140c # 6 instructions; loads
|
||||
spin main 28818c0 28818d4 # 6 instructions; loads
|
||||
spin main 288197c 2881990 # 6 instructions; loads
|
||||
spin main 2a38860 2a38868 # 3 instructions; loads
|
||||
spin main 2a38870 2a38874 # 2 instructions; loads
|
||||
spin main 2a388ac 2a388b4 # 3 instructions; loads
|
||||
spin subsdk0 2090 20a4 # 6 instructions; loads
|
||||
spin subsdk0 278b4 278c8 # 6 instructions; loads
|
||||
spin subsdk0 bcf28 bcfd0 bcfd8 # 14 instructions; loads
|
||||
spin subsdk0 bd0d0 bd178 bd180 # 14 instructions; loads
|
||||
spin subsdk0 c03a0 c03b4 # 6 instructions; loads
|
||||
spin subsdk0 1bbefc 1bbf08 # 4 instructions; loads
|
||||
spin subsdk0 1bd838 1bd84c # 6 instructions; loads
|
||||
spin subsdk0 1c0c68 1c0c74 # 4 instructions; loads
|
||||
spin sdk f5218 f5350 # 14 instructions; loads
|
||||
spin sdk 23a590 23a5d8 # 19 instructions; loads
|
||||
spin sdk 23a658 23a6a0 # 19 instructions; loads
|
||||
spin sdk 23a7fc 23a918 # 19 instructions; loads
|
||||
spin sdk 3d3250 3d3270 # 9 instructions; loads
|
||||
|
||||
# Sun shadow map size (runtime/resolution.c shadow_init switches the set on). Each frame the engine sizes its shadow
|
||||
# cascades (a D16 4-layer array) in main+0xcebdf4: a base size (960 in the field), interpolated down by how close the
|
||||
# cover is, rounded up to 8 at main+0xcec22c and stored as the width (+0x3d8 of the cascade set) and, through the packed
|
||||
# store at +0xe4, the height (+0x3f8). Near the ground it falls to about 400, which draws foliage shadows blocky, and
|
||||
# every change of size moves the shadow texels, so leaf shadows jump instead of sliding. The page's fixed size (+92)
|
||||
# goes in just before the rounding, as the community's shadow resolution mods do (1.1.0: mov w11, #1024 over the
|
||||
# computation). Found by a write watch on the size field (the writer was this function, and no other).
|
||||
site shadow main cec22c 11001d6b @page:x11 b9405d6b 11001d6b # add w11, w11, #7 -> adrp x11, page; ldr w11, [x11, #92]; add w11, w11, #7
|
||||
1003
recomp/patches-v143.txt
Normal file
1003
recomp/patches-v143.txt
Normal file
File diff suppressed because it is too large
Load Diff
158
recomp/patches.py
Normal file
158
recomp/patches.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Code patch table for the generator: guest instructions replaced in the lifted C, switchable at run time.
|
||||
|
||||
Lifted code cannot be byte-patched when the game runs, so a patch is applied when the C is generated. A patch file
|
||||
(gen.py --patches) has two kinds of lines; `#` starts a comment, fields are separated by whitespace:
|
||||
|
||||
set <name> <env> <default> a patch set: a run-time switch <env>=0/1, <default> when it is unset
|
||||
site <set> <module> <address> <original> <replacement> [<replacement> ...]
|
||||
|
||||
spin <module> <head> <back-edge> [<back-edge> ...]
|
||||
|
||||
<address> is a module offset (hex), <original> the instruction word the module has there (checked; a mismatch stops
|
||||
the generator), each <replacement> an instruction word (hex) or `@page:x<n>`, which is `adrp x<n>, <patch page>`
|
||||
encoded for the site's address. The patch page is the first 4 KiB page after main's image (layout.py leaves the rest
|
||||
of the 2 MiB-aligned module slot free); the runtime commits it and owns its contents. Words in use, by byte offset:
|
||||
+0..15 the frame-rate step (runtime/frame_rate.c), +16..55 the output, swapchain and scene sizes
|
||||
(runtime/resolution.c res_init), +56..59 the camera's aspect ratio (runtime/resolution.c res_init, set aspect), +60..63
|
||||
the field-of-view factor (runtime/resolution.c res_fov_live, set fov), +92..95 the sun shadow map size
|
||||
(runtime/resolution.c shadow_init, set shadow). A new set takes words outside these.
|
||||
|
||||
The replacement words are lifted at the site's address in place of the original instruction, and the generated C reads
|
||||
`if (rt_patch_on[<set>]) { replacement } else { original }`: one byte load and a branch per executed site. Several
|
||||
sets may patch one address (cheats that write different values there): `if (a) {..} else if (b) {..} else {..}`, the
|
||||
first set in file order wins. The original may be any instruction but br, blr and ret; a replacement's last word may be
|
||||
a bl (lifted with the site's return address), a b or b.cond / cbz / tbz (its target joins the function's labels when
|
||||
inside it) or a ret; earlier words must not branch. Every alternative of a site writes the same flags. The generator
|
||||
writes patches.h (set names, switches, the page address) next to layout.h for the runtime.
|
||||
|
||||
A `spin` line names a spin wait found by tools/spin_waits.py: a loop with head <head> that only another thread can end,
|
||||
and the direct branches back to it (module offsets). Each back-edge (checked: a b, b.cond, cbz / cbnz or tbz / tbnz to
|
||||
<head>) is lifted as `rt_spin_gate(c, <head>); goto <head>`, which keeps every register and only adds time
|
||||
(runtime/kernel.c kern_spin_gate).
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
PAGE = 0x1000
|
||||
|
||||
|
||||
class PatchError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def adrp(pc, target, rd):
|
||||
imm = (target >> 12) - (pc >> 12)
|
||||
if not -(1 << 20) <= imm < (1 << 20):
|
||||
raise PatchError(f'{pc:#x}: patch page {target:#x} out of ADRP range')
|
||||
imm &= 0x1FFFFF
|
||||
return 0x90000000 | (imm & 3) << 29 | (imm >> 2) << 5 | rd
|
||||
|
||||
|
||||
class Table:
|
||||
"""sets: [(name, env, default)]; sites: {module: {offset: [(set index, [word | ('page', reg)], original)]}} (the
|
||||
alternatives of a site in file order); page."""
|
||||
|
||||
def __init__(self, sets=(), sites=None, page=0, spins=None):
|
||||
self.sets = list(sets)
|
||||
self.sites = sites or {}
|
||||
self.page = page
|
||||
self.spins = spins or {} # {module: {back-edge offset: head offset}}
|
||||
|
||||
def spins_for(self, name, base):
|
||||
"""{absolute back-edge address: absolute head address}."""
|
||||
return {base + b: base + h for b, h in self.spins.get(name, {}).items()}
|
||||
|
||||
def for_module(self, name, base):
|
||||
"""{absolute address: [(set index, [words])]} with @page resolved."""
|
||||
out = {}
|
||||
for off, alts in self.sites.get(name, {}).items():
|
||||
a = base + off
|
||||
out[a] = [(k, [adrp(a, self.page, w[1]) if isinstance(w, tuple) else w for w in repl])
|
||||
for k, repl, _ in alts]
|
||||
return out
|
||||
|
||||
|
||||
def load(path, mods):
|
||||
"""Table from a patch file; mods is layout() output [(name, base, size)]."""
|
||||
sets, index, sites, spins = [], {}, {}, {}
|
||||
bases = {n: (b, s) for n, b, s in mods}
|
||||
for no, raw in enumerate(Path(path).read_text().splitlines(), 1):
|
||||
f = raw.split('#', 1)[0].split()
|
||||
if not f:
|
||||
continue
|
||||
where = f'{path}:{no}'
|
||||
if f[0] == 'set' and len(f) == 4:
|
||||
if f[1] in index:
|
||||
raise PatchError(f'{where}: set {f[1]} defined twice')
|
||||
index[f[1]] = len(sets)
|
||||
sets.append((f[1], f[2], int(f[3])))
|
||||
elif f[0] == 'site' and len(f) >= 6:
|
||||
if f[1] not in index:
|
||||
raise PatchError(f'{where}: unknown set {f[1]}')
|
||||
if f[2] not in bases:
|
||||
raise PatchError(f'{where}: module {f[2]} not in the layout')
|
||||
off, orig = int(f[3], 16), int(f[4], 16)
|
||||
repl = []
|
||||
for w in f[5:]:
|
||||
if w.startswith('@page:x'):
|
||||
repl.append(('page', int(w[7:])))
|
||||
else:
|
||||
repl.append(int(w, 16))
|
||||
alts = sites.setdefault(f[2], {}).setdefault(off, [])
|
||||
if any(k == index[f[1]] for k, _, _ in alts):
|
||||
raise PatchError(f'{where}: {f[2]}+{off:#x} patched twice by set {f[1]}')
|
||||
if alts and alts[0][2] != orig:
|
||||
raise PatchError(f'{where}: {f[2]}+{off:#x} expects {orig:08x}, an earlier line {alts[0][2]:08x}')
|
||||
alts.append((index[f[1]], repl, orig))
|
||||
elif f[0] == 'spin' and len(f) >= 4:
|
||||
if f[1] not in bases:
|
||||
raise PatchError(f'{where}: module {f[1]} not in the layout')
|
||||
mod, head = spins.setdefault(f[1], {}), int(f[2], 16)
|
||||
for b in f[3:]:
|
||||
b = int(b, 16)
|
||||
if b in mod:
|
||||
raise PatchError(f'{where}: {f[1]}+{b:#x} is a back-edge twice')
|
||||
mod[b] = head
|
||||
else:
|
||||
raise PatchError(f'{where}: cannot parse {raw!r}')
|
||||
base, size = bases['main'][0], bases['main'][1]
|
||||
page = (base + size + PAGE - 1) & ~(PAGE - 1)
|
||||
nxt = min((b for _, b, _ in mods if b > base), default=None)
|
||||
if nxt is not None and page + PAGE > nxt:
|
||||
raise PatchError(f'no room for the patch page after main ({page:#x})')
|
||||
return Table(sets, sites, page, spins)
|
||||
|
||||
|
||||
def check(table, mod, name):
|
||||
"""Stops on a site whose original word differs from the module's."""
|
||||
for off, [(_, _, want), *_] in table.sites.get(name, {}).items():
|
||||
a = mod.base + off
|
||||
if not mod.in_text(a):
|
||||
raise PatchError(f'{name}+{off:#x}: patch site outside .text')
|
||||
have = mod.word(a)
|
||||
if have != want:
|
||||
raise PatchError(f'{name}+{off:#x}: module has {have:08x}, the patch expects {want:08x}')
|
||||
for b, head in table.spins.get(name, {}).items():
|
||||
a = mod.base + b
|
||||
if not mod.in_text(a) or branch_target(mod.word(a), a) != mod.base + head:
|
||||
raise PatchError(f'{name}+{b:#x}: not a direct branch to the spin wait head {name}+{head:#x}')
|
||||
|
||||
|
||||
def branch_target(w, a):
|
||||
"""Target of the direct branch w at a (b, b.cond, cbz / cbnz, tbz / tbnz), or None."""
|
||||
if w >> 26 == 0x05:
|
||||
return a + ((((w & 0x3FFFFFF) ^ 0x2000000) - 0x2000000) << 2)
|
||||
if w & 0xFF000010 == 0x54000000 or w & 0x7E000000 == 0x34000000:
|
||||
return a + ((((w >> 5) & 0x7FFFF) ^ 0x40000) - 0x40000 << 2)
|
||||
if w & 0x7E000000 == 0x36000000:
|
||||
return a + ((((w >> 5) & 0x3FFF) ^ 0x2000) - 0x2000 << 2)
|
||||
return None
|
||||
|
||||
|
||||
def header_text(table, marker):
|
||||
lines = [f'// {marker}: code patch sets, generated by recomp/gen.py --patches. Do not edit or commit.',
|
||||
'#pragma once', '', f'#define RT_PATCH_COUNT {len(table.sets)}',
|
||||
f'#define RT_PATCH_PAGE {table.page:#x}ull',
|
||||
'// X(index, name, env switch, default)',
|
||||
'#define RT_PATCH_SETS(X) ' + ' '.join(f'X({i}, {n}, "{e}", {d})' for i, (n, e, d) in
|
||||
enumerate(table.sets)), '']
|
||||
return '\n'.join(lines)
|
||||
107
recomp/regs.py
Normal file
107
recomp/regs.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Function-local general registers for recomp/gen.py.
|
||||
|
||||
The lifter reads and writes the guest's X0-X30 and SP in the Cpu (c->x[n], c->sp), so every guest instruction loads
|
||||
and stores memory, and the C compiler cannot keep a register in a host register across a guest store: guest stores go
|
||||
through pointers that may alias the Cpu. A generated function instead copies the LOCALS registers it uses most into
|
||||
locals at its entry (xN, sp_), works on those, and syncs with the Cpu only where something else reads or writes them:
|
||||
before a call, an SVC, a trap, a return or a tail exit it stores the registers it changed since the last sync (spill),
|
||||
and after an SVC it loads them all again (reload). The other registers stay in the Cpu.
|
||||
|
||||
After a call it reloads only X0-X18 and X30: AAPCS64 has the callee preserve X19-X29 and SP, so their locals still
|
||||
hold what the Cpu holds. What would break that does not occur in NX2navi: main and subsdk0 import neither longjmp nor
|
||||
the C++ unwinder, sdk's longjmp, __cxa_throw and _Unwind_* have no direct callers, and the runtime writes no register
|
||||
above X3 of a running thread's Cpu except X30, which rt_call restores (runtime/hostfn.c). Guest fibers each run on a host fiber of their own with their own Cpu (runtime/kernel.c),
|
||||
and ExitThread leaves by longjmp without returning. gen.py --no-callee-saved-trust reloads those registers too.
|
||||
|
||||
Only plain assignments write registers in the lifted C (`c->x[n] = ...`, `c->sp = ...`), and no code takes a
|
||||
register's address, so the rewrite is textual. A register counts as dirty from a write until the next spill on some
|
||||
path: plan() runs that forward over the function's control flow, so a call spills only what changed since the
|
||||
previous call or the entry. Spilling a register that is not dirty is harmless (the local equals the Cpu), so where a
|
||||
case is unclear the plan errs on spilling more.
|
||||
"""
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
REG = re.compile(r'c->x\[(\d+)\]|c->sp\b')
|
||||
WRITE = re.compile(r'c->(?:x\[(\d+)\]|(sp))\s*=(?!=)')
|
||||
# Lifted instructions that hand the Cpu to the runtime: SVCs return, BRK and undefined words do not.
|
||||
ESCAPE = re.compile(r'\brt_(?:svc|brk|udf)\(c\b')
|
||||
SP = 31 # index of SP in the register sets
|
||||
# Markers gen.py puts where the Cpu must be current, resolved by Plan.resolve(): spill before; reload all, or reload
|
||||
# what a call may change, after.
|
||||
SPILL, RELOAD, RELOAD_CALL = '\x00S', '\x00R', '\x00C'
|
||||
CALLEE_SAVED = frozenset(range(19, 30)) | {SP}
|
||||
# Locals per function: the most used registers. More cost more than they save, as clang then spills them to the stack
|
||||
# and the syncs stay (300 hot NX2navi functions: fewest instructions and memory operands at 4-6).
|
||||
LOCALS = 6
|
||||
|
||||
|
||||
def name(r):
|
||||
return 'sp_' if r == SP else f'x{r}'
|
||||
|
||||
|
||||
def cpu(r):
|
||||
return 'c->sp' if r == SP else f'c->x[{r}]'
|
||||
|
||||
|
||||
def refs(text):
|
||||
"""Registers text reads or writes, once per occurrence."""
|
||||
return [SP if m.group(0) == 'c->sp' else int(m.group(1)) for m in REG.finditer(text)]
|
||||
|
||||
|
||||
def written(text):
|
||||
return {SP if m.group(2) else int(m.group(1)) for m in WRITE.finditer(text)}
|
||||
|
||||
|
||||
class Plan:
|
||||
def __init__(self, regs, spill, kept):
|
||||
self.regs = regs # sorted registers the function keeps in locals
|
||||
self.local = set(regs)
|
||||
self.spill = spill # per instruction: registers to store at its sync points
|
||||
self.kept = kept # registers a call leaves as they were (CALLEE_SAVED, or none)
|
||||
|
||||
def decl(self):
|
||||
"""Locals at the top of the function, loaded from the Cpu."""
|
||||
return 'uint64_t ' + ', '.join(f'{name(r)} = {cpu(r)}' for r in self.regs) + ';' if self.regs else ''
|
||||
|
||||
def resolve(self, i, text):
|
||||
"""text of instruction i with its registers on the locals and its sync markers expanded."""
|
||||
def local(m):
|
||||
r = SP if m.group(0) == 'c->sp' else int(m.group(1))
|
||||
return name(r) if r in self.local else m.group(0)
|
||||
|
||||
text = REG.sub(local, text)
|
||||
spill = ''.join(f'{cpu(r)} = {name(r)}; ' for r in sorted(self.spill[i]))
|
||||
reload = ''.join(f' {name(r)} = {cpu(r)};' for r in self.regs)
|
||||
reload_call = ''.join(f' {name(r)} = {cpu(r)};' for r in self.regs if r not in self.kept)
|
||||
return text.replace(SPILL, spill).replace(RELOAD, reload).replace(RELOAD_CALL, reload_call)
|
||||
|
||||
|
||||
def plan(texts, succ, clean, n_locals=LOCALS, kept=CALLEE_SAVED):
|
||||
"""texts[i]: instruction i's C with markers; succ[i]: successor indices inside the function; clean: instructions
|
||||
after which every local equals the Cpu (a call or an SVC that spills and reloads on every path through it);
|
||||
n_locals: how many registers to keep in locals; kept: registers a call does not reload."""
|
||||
n = len(texts)
|
||||
uses = Counter(r for t in texts for r in refs(t))
|
||||
regs = {r for r, _ in sorted(uses.items(), key=lambda kv: (-kv[1], kv[0]))[:n_locals]}
|
||||
writes = [written(t) & regs for t in texts]
|
||||
dirty = [set() for _ in range(n)]
|
||||
seen = [False] * n
|
||||
work = [0] if n else []
|
||||
if n:
|
||||
seen[0] = True
|
||||
while work:
|
||||
i = work.pop()
|
||||
out = set() if i in clean else dirty[i] | writes[i]
|
||||
for s in succ[i]:
|
||||
if not seen[s] or not out <= dirty[s]:
|
||||
seen[s] = True
|
||||
dirty[s] |= out
|
||||
work.append(s)
|
||||
every = set().union(*writes) if writes else set()
|
||||
for i in range(n):
|
||||
if not seen[i]: # not reached from the entry in the plan's flow: anything may be dirty
|
||||
dirty[i] = every
|
||||
# A sync point stores what was dirty on entry to its instruction and what the instruction wrote before it
|
||||
# (c->x[30] of a call): all the instruction's writes, which is at most a little more.
|
||||
return Plan(sorted(regs), [dirty[i] | writes[i] if SPILL in texts[i] else set() for i in range(n)], kept)
|
||||
1157
recomp/simd_c.py
Normal file
1157
recomp/simd_c.py
Normal file
File diff suppressed because it is too large
Load Diff
400
runtime/amiibo.c
Normal file
400
runtime/amiibo.c
Normal file
@@ -0,0 +1,400 @@
|
||||
// Amiibo dump loader and Horizon OS NFP state emulation.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "amiibo.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static PlatMutex g_amiibo_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
// 540-byte raw NTAG215 binary dump
|
||||
static uint8_t g_dump[AMIIBO_DUMP_SIZE];
|
||||
static bool g_has_dump = false;
|
||||
static AmiiboInfo g_info;
|
||||
|
||||
static bool g_device_present = true;
|
||||
static bool g_detecting = false;
|
||||
static bool g_tag_present = false;
|
||||
static bool g_mounted = false;
|
||||
static bool g_auto_remove = true;
|
||||
static float g_auto_remove_delay_s = 2.5f;
|
||||
|
||||
static uint64_t g_tap_timestamp_ns = 0;
|
||||
static bool g_timer_active = false;
|
||||
|
||||
static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
|
||||
if (size != AMIIBO_DUMP_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C). An amiibo's is F1 10 FF EE, not the E1 of a plain
|
||||
// NDEF tag (3dbrew, "Amiibo"); the CC is not encrypted, so encrypted dumps carry it too.
|
||||
static const uint8_t amiibo_cc[4] = {0xF1, 0x10, 0xFF, 0xEE};
|
||||
if (memcmp(data + 0x0C, amiibo_cc, sizeof amiibo_cc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// UID sanity: UID (bytes 0..2 and 4..7) cannot be all zeroes or all 0xFF.
|
||||
if ((data[0] == 0 && data[1] == 0 && data[2] == 0 && data[4] == 0 &&
|
||||
data[5] == 0 && data[6] == 0 && data[7] == 0) ||
|
||||
(data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[4] == 0xFF &&
|
||||
data[5] == 0xFF && data[6] == 0xFF && data[7] == 0xFF)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ISO 14443-3 / NTAG215 Bit Check Character (BCC) verification.
|
||||
// Page 0 byte 3 (BCC0) = 0x88 ^ UID[0] ^ UID[1] ^ UID[2]
|
||||
// Page 2 byte 0 (BCC1) = UID[3] ^ UID[4] ^ UID[5] ^ UID[6]
|
||||
uint8_t bcc0 = (uint8_t)(0x88 ^ data[0] ^ data[1] ^ data[2]);
|
||||
uint8_t bcc1 = (uint8_t)(data[4] ^ data[5] ^ data[6] ^ data[7]);
|
||||
if (data[3] != bcc0 || data[8] != bcc1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// The 7-byte UID of an NTAG215 dump: bytes 0..2, then 4..7. Byte 3 is the check byte BCC0.
|
||||
static void copy_uid(uint8_t out[7], const uint8_t *dump) {
|
||||
memcpy(out, dump, 3);
|
||||
memcpy(out + 3, dump + 4, 4);
|
||||
}
|
||||
|
||||
static void parse_dump_info(const char *path) {
|
||||
memset(&g_info, 0, sizeof(g_info));
|
||||
if (path) {
|
||||
snprintf(g_info.path, sizeof(g_info.path), "%s", path);
|
||||
const char *slash = strrchr(path, '\\');
|
||||
const char *fslash = strrchr(path, '/');
|
||||
const char *name = slash ? slash + 1 : (fslash ? fslash + 1 : path);
|
||||
if (slash && fslash) {
|
||||
name = (slash > fslash ? slash : fslash) + 1;
|
||||
}
|
||||
snprintf(g_info.filename, sizeof(g_info.filename), "%s", name);
|
||||
}
|
||||
|
||||
copy_uid(g_info.uuid, g_dump);
|
||||
g_info.uuid_len = 7;
|
||||
|
||||
// Amiibo ID at offset 0x54 (8 bytes, 3dbrew "Amiibo"): character ID and variant, figure type, model number
|
||||
// (big-endian), series, format version 02
|
||||
memcpy(g_info.character_id, g_dump + 0x54, 3);
|
||||
g_info.nfp_type = g_dump[0x57];
|
||||
g_info.numbering_id = ((uint16_t)g_dump[0x58] << 8) | g_dump[0x59];
|
||||
g_info.series_id = g_dump[0x5A];
|
||||
g_info.version = g_dump[0x5B];
|
||||
}
|
||||
|
||||
void amiibo_init(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_device_present = true;
|
||||
g_detecting = false;
|
||||
g_tag_present = false;
|
||||
g_mounted = false;
|
||||
g_has_dump = false;
|
||||
g_timer_active = false;
|
||||
memset(g_dump, 0, sizeof(g_dump));
|
||||
memset(&g_info, 0, sizeof(g_info));
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_set_device_present(bool present) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_device_present = present;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_is_device_present(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_device_present;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
// While the game waits for a tag it stops and starts detection again several times a second (tens with the menu open),
|
||||
// so the log names the first few and then a count every 200th start.
|
||||
static uint64_t g_detect_starts;
|
||||
static uint64_t g_detect_stop_ns; // the last stop: the menu holds "searching" across the game's restarts
|
||||
|
||||
void amiibo_start_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_detecting = true;
|
||||
uint64_t n = ++g_detect_starts;
|
||||
if (n <= 3) rt_log("amiibo: start detection");
|
||||
else if (n % 200 == 0) rt_log("amiibo: detection started %llu times so far", (unsigned long long)n);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_stop_detection(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_detecting) g_detect_stop_ns = plat_now_ns();
|
||||
g_detecting = false;
|
||||
if (g_detect_starts <= 3) rt_log("amiibo: stop detection");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_is_detecting(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_detecting;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool amiibo_mount(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_device_present || !g_tag_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return false;
|
||||
}
|
||||
g_mounted = true;
|
||||
rt_log("amiibo: mounted tag '%s'", g_info.filename[0] ? g_info.filename : "unknown");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_unmount(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_mounted = false;
|
||||
rt_log("amiibo: unmounted tag");
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_is_mounted(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_mounted;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
AmiiboDeviceState amiibo_get_device_state(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_device_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_UNAVAILABLE;
|
||||
}
|
||||
if (g_mounted) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_TAG_MOUNTED;
|
||||
}
|
||||
if (g_tag_present) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_TAG_FOUND;
|
||||
}
|
||||
if (g_detecting) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_SEARCHING;
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return AMIIBO_DEV_STATE_INITIALIZED;
|
||||
}
|
||||
|
||||
AmiiboDeviceState amiibo_get_reader_state(void) {
|
||||
AmiiboDeviceState state = amiibo_get_device_state();
|
||||
if (state != AMIIBO_DEV_STATE_INITIALIZED) return state;
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool recent = g_detect_stop_ns && plat_now_ns() - g_detect_stop_ns < 1000000000ull;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return recent ? AMIIBO_DEV_STATE_SEARCHING : state;
|
||||
}
|
||||
|
||||
bool amiibo_load_file(const char *path) {
|
||||
if (!path || !*path) return false;
|
||||
uint64_t size = 0;
|
||||
uint8_t *data = plat_file_read(path, &size);
|
||||
if (!data) {
|
||||
rt_log("amiibo: failed to read file: %s", path);
|
||||
return false;
|
||||
}
|
||||
if (size != AMIIBO_DUMP_SIZE) {
|
||||
rt_log("amiibo: invalid dump size (%llu bytes, expected exactly %d): %s",
|
||||
(unsigned long long)size, AMIIBO_DUMP_SIZE, path);
|
||||
free(data);
|
||||
return false;
|
||||
}
|
||||
if (!amiibo_validate_dump(data, size)) {
|
||||
rt_log("amiibo: dump failed NTAG215 structure sanity checks: %s", path);
|
||||
free(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
memcpy(g_dump, data, AMIIBO_DUMP_SIZE);
|
||||
g_has_dump = true;
|
||||
parse_dump_info(path);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
|
||||
free(data);
|
||||
rt_log("amiibo: loaded dump '%s' (char_id: %02x%02x%02x, series: %02x, num: %04x)",
|
||||
g_info.filename, g_info.character_id[0], g_info.character_id[1], g_info.character_id[2],
|
||||
g_info.series_id, g_info.numbering_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_tap_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_has_dump) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return;
|
||||
}
|
||||
g_tag_present = true;
|
||||
g_tap_timestamp_ns = plat_now_ns();
|
||||
if (g_auto_remove) {
|
||||
g_timer_active = true;
|
||||
}
|
||||
rt_log("amiibo: tag tapped on reader ('%s')", g_info.filename);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_scan_and_tap(const char *path) {
|
||||
if (!amiibo_load_file(path)) return false;
|
||||
amiibo_tap_tag();
|
||||
return true;
|
||||
}
|
||||
|
||||
void amiibo_remove_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_tag_present || g_mounted) {
|
||||
g_tag_present = false;
|
||||
g_mounted = false;
|
||||
g_timer_active = false;
|
||||
rt_log("amiibo: tag removed from reader");
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_tick(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (g_timer_active && g_tag_present) {
|
||||
uint64_t now = plat_now_ns();
|
||||
uint64_t diff = now > g_tap_timestamp_ns ? now - g_tap_timestamp_ns : 0;
|
||||
uint64_t limit_ns = (uint64_t)(g_auto_remove_delay_s * 1000000000.0f);
|
||||
if (diff >= limit_ns) {
|
||||
g_tag_present = false;
|
||||
g_mounted = false;
|
||||
g_timer_active = false;
|
||||
rt_log("amiibo: auto-removed tag after %.1fs timeout", (double)g_auto_remove_delay_s);
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_set_auto_remove(bool enable) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
g_auto_remove = enable;
|
||||
if (!enable) g_timer_active = false;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
bool amiibo_get_auto_remove(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_auto_remove;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
void amiibo_set_auto_remove_delay_s(float seconds) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (seconds >= 0.5f && seconds <= 10.0f) g_auto_remove_delay_s = seconds;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
float amiibo_get_auto_remove_delay_s(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
float res = g_auto_remove_delay_s;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool amiibo_has_tag(void) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
bool res = g_has_dump && g_tag_present;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
const AmiiboInfo *amiibo_get_current_info(void) {
|
||||
return &g_info;
|
||||
}
|
||||
|
||||
const uint8_t *amiibo_get_raw_data(void) {
|
||||
return g_dump;
|
||||
}
|
||||
|
||||
void amiibo_get_tag_info(void *out_tag_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_tag_info;
|
||||
memset(out, 0, 0x58);
|
||||
// UUID: offset 0x00, 10 bytes max
|
||||
copy_uid(out, g_dump);
|
||||
out[0x0A] = 7; // UUID length
|
||||
// Protocol at offset 0x20: TypeA = 1
|
||||
*(uint32_t *)(out + 0x20) = 1;
|
||||
// TagType at offset 0x24: Type2 / NTAG215 = 2
|
||||
*(uint32_t *)(out + 0x24) = 2;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_get_model_info(void *out_model_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_model_info;
|
||||
memset(out, 0, 0x40);
|
||||
// nn::nfp::ModelInfo from the amiibo ID at 0x54: character ID and variant in the tag's byte order, series, model
|
||||
// number little-endian (big-endian on the tag), figure type. The game tells figures of one character apart by the
|
||||
// model number.
|
||||
memcpy(out + 0x00, g_dump + 0x54, 3); // character ID, variant
|
||||
out[0x03] = g_dump[0x5A]; // series
|
||||
out[0x04] = g_dump[0x59]; // model number
|
||||
out[0x05] = g_dump[0x58];
|
||||
out[0x06] = g_dump[0x57]; // figure type
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_get_common_info(void *out_common_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_common_info;
|
||||
memset(out, 0, 0x40);
|
||||
// Last write date: 0x00..0x03
|
||||
out[0x00] = 0xE7; out[0x01] = 0x07; // 2023, u16 little-endian
|
||||
out[0x02] = 0x05; out[0x03] = 0x0C; // May 12
|
||||
// Write counter: 0x04..0x05
|
||||
out[0x04] = 0x00; out[0x05] = 0x01;
|
||||
// Version: 0x06
|
||||
out[0x06] = 0x02;
|
||||
// Application area size: 0x08..0x0B = 0xD8 (216 bytes)
|
||||
*(uint32_t *)(out + 0x08) = 0xD8;
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
void amiibo_get_register_info(void *out_register_info) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
uint8_t *out = (uint8_t *)out_register_info;
|
||||
memset(out, 0, 0x100);
|
||||
// Mii CharInfo at 0x00..0x57 (dummy valid mii)
|
||||
out[0x00] = 0x03; // version 3
|
||||
// First write date at 0x58..0x5B
|
||||
out[0x58] = 0xE7; out[0x59] = 0x07; // 2023, u16 little-endian
|
||||
out[0x5A] = 0x05; out[0x5B] = 0x0C;
|
||||
// Amiibo Name at 0x5C: copy up to 40 bytes NUL-terminated
|
||||
const char *name = g_info.filename[0] ? g_info.filename : "Amiibo";
|
||||
snprintf((char *)(out + 0x5C), 0x29, "%s", name);
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
}
|
||||
|
||||
int amiibo_get_application_area(void *out_buf, uint32_t max_size) {
|
||||
plat_mutex_lock(&g_amiibo_lock);
|
||||
if (!g_has_dump || !out_buf) {
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return 0;
|
||||
}
|
||||
uint32_t to_copy = max_size < 0xD8 ? max_size : 0xD8;
|
||||
// Decrypted/payload application area starts at offset 0xDC (or 0x0A0..0x178 in raw dump)
|
||||
if (0xDC + to_copy <= AMIIBO_DUMP_SIZE) {
|
||||
memcpy(out_buf, g_dump + 0xDC, to_copy);
|
||||
} else {
|
||||
memset(out_buf, 0, to_copy);
|
||||
}
|
||||
plat_mutex_unlock(&g_amiibo_lock);
|
||||
return (int)to_copy;
|
||||
}
|
||||
87
runtime/amiibo.h
Normal file
87
runtime/amiibo.h
Normal file
@@ -0,0 +1,87 @@
|
||||
// Amiibo loader and state management subsystem (540-byte NTAG215 binary dumps).
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define AMIIBO_DUMP_SIZE 540
|
||||
|
||||
// Device state corresponding to nn::nfp / nn::nfc DeviceState
|
||||
typedef enum {
|
||||
AMIIBO_DEV_STATE_INITIALIZED = 0,
|
||||
AMIIBO_DEV_STATE_SEARCHING = 1,
|
||||
AMIIBO_DEV_STATE_TAG_FOUND = 2,
|
||||
AMIIBO_DEV_STATE_TAG_REMOVED = 3,
|
||||
AMIIBO_DEV_STATE_TAG_MOUNTED = 4,
|
||||
AMIIBO_DEV_STATE_UNAVAILABLE = 5
|
||||
} AmiiboDeviceState;
|
||||
|
||||
// Amiibo identification info
|
||||
typedef struct {
|
||||
uint8_t uuid[10];
|
||||
uint8_t uuid_len;
|
||||
uint8_t character_id[3];
|
||||
uint8_t series_id;
|
||||
uint16_t numbering_id;
|
||||
uint8_t nfp_type;
|
||||
uint8_t version;
|
||||
char path[512];
|
||||
char filename[128];
|
||||
} AmiiboInfo;
|
||||
|
||||
// Subsystem lifecycle
|
||||
void amiibo_init(void);
|
||||
|
||||
// Hardware / device emulation status
|
||||
void amiibo_set_device_present(bool present);
|
||||
bool amiibo_is_device_present(void);
|
||||
|
||||
// Tag detection & lifecycle
|
||||
void amiibo_start_detection(void);
|
||||
void amiibo_stop_detection(void);
|
||||
bool amiibo_is_detecting(void);
|
||||
|
||||
// Tag mounting
|
||||
bool amiibo_mount(void);
|
||||
void amiibo_unmount(void);
|
||||
bool amiibo_is_mounted(void);
|
||||
|
||||
// State query
|
||||
AmiiboDeviceState amiibo_get_device_state(void);
|
||||
// The state the menu shows: searching also within a second after a stop, since the game restarts detection many times
|
||||
// a second while it waits for a tag. The game reads amiibo_get_device_state.
|
||||
AmiiboDeviceState amiibo_get_reader_state(void);
|
||||
|
||||
// File loading & triggering actions
|
||||
bool amiibo_load_file(const char *path);
|
||||
bool amiibo_scan_and_tap(const char *path);
|
||||
void amiibo_tap_tag(void);
|
||||
void amiibo_remove_tag(void);
|
||||
|
||||
// Timer tick called during menu / frame updates
|
||||
void amiibo_tick(void);
|
||||
|
||||
// Options
|
||||
void amiibo_set_auto_remove(bool enable);
|
||||
bool amiibo_get_auto_remove(void);
|
||||
void amiibo_set_auto_remove_delay_s(float seconds);
|
||||
float amiibo_get_auto_remove_delay_s(void);
|
||||
|
||||
// Tag inspection
|
||||
bool amiibo_has_tag(void);
|
||||
const AmiiboInfo *amiibo_get_current_info(void);
|
||||
const uint8_t *amiibo_get_raw_data(void);
|
||||
|
||||
// Horizon OS buffers extraction (called by NFP IPC/override services)
|
||||
void amiibo_get_tag_info(void *out_tag_info);
|
||||
void amiibo_get_model_info(void *out_model_info);
|
||||
void amiibo_get_common_info(void *out_common_info);
|
||||
void amiibo_get_register_info(void *out_register_info);
|
||||
int amiibo_get_application_area(void *out_buf, uint32_t max_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
1711
runtime/astc_decode_spv.h
Normal file
1711
runtime/astc_decode_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
396
runtime/astc_probe.c
Normal file
396
runtime/astc_probe.c
Normal file
@@ -0,0 +1,396 @@
|
||||
// --probe-astc: the compute ASTC decoder (vk_astc.c, shaders/astc_decode.comp) against astc_decode_ldr, bit for bit,
|
||||
// for every block footprint, linear and sRGB. Synthetic blocks only (no game files): valid void extents, invalid and
|
||||
// HDR void extents, random blocks kept by what they exercise (1-4 partitions, dual plane with 1-3 partitions, every
|
||||
// weight grid and quantization the random modes reach), then plain random blocks (mostly error blocks, HDR endpoint
|
||||
// modes among them). Each case decodes two regions into two mip levels of an RGBA8 image with cropped edges, and
|
||||
// compares both the decoder's texels (readback) and what the image holds afterwards. The same regions also go through
|
||||
// the BC3 encoder (NX2NAVI_ASTC_BC) into a BC3 image, whose blocks are decoded here and held to error bounds (bc_check).
|
||||
// Also times one 2048x2048 texture on the CPU (astc_decode_ldr_mt) and on the GPU (wall time of the upload batch).
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "platform/platform.h"
|
||||
#include "host.h"
|
||||
#include "tex.h"
|
||||
#include "vk.h"
|
||||
|
||||
static uint64_t g_rng = 0x2545F4914F6CDD1Dull;
|
||||
static uint64_t rnd64(void) {
|
||||
g_rng ^= g_rng << 13; g_rng ^= g_rng >> 7; g_rng ^= g_rng << 17;
|
||||
return g_rng;
|
||||
}
|
||||
|
||||
static const uint8_t dims[14][2] = {{4, 4}, {5, 4}, {5, 5}, {6, 5}, {6, 6}, {8, 5}, {8, 6},
|
||||
{8, 8}, {10, 5}, {10, 6}, {10, 8}, {10, 10}, {12, 10}, {12, 12}};
|
||||
|
||||
// Buckets of valid blocks: partitions 1-4 single plane, then dual plane with 1-3 partitions.
|
||||
enum { NB = 7, PER_BUCKET = 48 };
|
||||
static const char *const bucket_name[NB] = {"1p", "2p", "3p", "4p", "dual 1p", "dual 2p", "dual 3p"};
|
||||
static uint64_t g_bucket[NB], g_void, g_err, g_blocks, g_texels, g_bc_cases;
|
||||
static double g_bc_db = 99; // the lowest BC3 PSNR of all cases
|
||||
|
||||
static int dual_bit(uint32_t m) {
|
||||
if (!(m & 3) && (m >> 7 & 3) == 2) return 0; // the 6..10 x 6..10 modes have no dual plane
|
||||
return (int)(m >> 10 & 1);
|
||||
}
|
||||
|
||||
static void void_block(uint8_t b[16], int kind) {
|
||||
// kind 0: valid LDR extent (random coordinates s0 < s1, t0 < t1), 1: all-ones extent, 2: s0 >= s1 (error),
|
||||
// 3: HDR (error), 4: reserved bits not 11 (error)
|
||||
uint64_t lo = 0x1FC | 3ull << 10, s0 = rnd64() & 0xFFF, s1 = s0 + 1 + (rnd64() & 0xFFF), t0 = rnd64() & 0xFFF,
|
||||
t1 = t0 + 1 + (rnd64() & 0xFFF);
|
||||
if (s1 > 0x1FFF) s1 = 0x1FFF;
|
||||
if (t1 > 0x1FFF) t1 = 0x1FFF;
|
||||
if (kind == 1) s0 = s1 = t0 = t1 = 0x1FFF;
|
||||
if (kind == 2) { uint64_t x = s0; s0 = s1; s1 = x; }
|
||||
lo |= s0 << 12 | s1 << 25 | t0 << 38 | t1 << 51;
|
||||
if (kind == 3) lo |= 0x200;
|
||||
if (kind == 4) lo &= ~(1ull << 11);
|
||||
uint64_t hi = rnd64();
|
||||
memcpy(b, &lo, 8), memcpy(b + 8, &hi, 8);
|
||||
}
|
||||
|
||||
// n blocks for footprint bw x bh: the buckets first, void extents, then random.
|
||||
static void make_blocks(uint8_t *blk, size_t n, uint32_t bw, uint32_t bh) {
|
||||
size_t k = 0;
|
||||
uint8_t tmp[12 * 12 * 4];
|
||||
int have[NB] = {0};
|
||||
for (int tries = 0; tries < 400000 && k < n; tries++) {
|
||||
uint64_t v[2] = {rnd64(), rnd64()};
|
||||
uint8_t *b = (uint8_t *)v;
|
||||
uint32_t m = (uint32_t)(v[0] & 0x7FF);
|
||||
if ((m & 0x1FF) == 0x1FC || !astc_decode_block(b, bw, bh, tries & 1, tmp)) continue;
|
||||
int p = (int)(v[0] >> 11 & 3), bk = dual_bit(m) ? 4 + p : p;
|
||||
if (bk >= NB || have[bk] >= PER_BUCKET) continue;
|
||||
have[bk]++, g_bucket[bk]++;
|
||||
memcpy(blk + 16 * k++, b, 16);
|
||||
}
|
||||
for (int i = 0; i < 40 && k < n; i++, g_void++) void_block(blk + 16 * k++, i % 5);
|
||||
for (; k < n; k++) {
|
||||
uint64_t v[2] = {rnd64(), rnd64()};
|
||||
memcpy(blk + 16 * k, v, 16);
|
||||
if (!astc_decode_block(blk + 16 * k, bw, bh, false, tmp)) g_err++;
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
VkImage img;
|
||||
VmaAllocation alloc;
|
||||
} Img;
|
||||
|
||||
static int make_image(VkCtx *c, uint32_t w, uint32_t h, uint32_t levels, VkFormat format, Img *o) {
|
||||
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = format, .extent = {w, h, 1},
|
||||
.mipLevels = levels, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
||||
return vmaCreateImage(c->vma, &ci, &ai, &o->img, &o->alloc, NULL) == VK_SUCCESS;
|
||||
}
|
||||
|
||||
static void barrier(VkCommandBuffer cb, VkPipelineStageFlags2 src, VkAccessFlags2 sa, VkPipelineStageFlags2 dst,
|
||||
VkAccessFlags2 da) {
|
||||
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = src, .srcAccessMask = sa,
|
||||
.dstStageMask = dst, .dstAccessMask = da};
|
||||
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
|
||||
vkCmdPipelineBarrier2(cb, &di);
|
||||
}
|
||||
#define ALL_RW VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT
|
||||
|
||||
static void to_general(VkCommandBuffer cb, VkImage img) {
|
||||
VkImageMemoryBarrier2 ib = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_NONE,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
||||
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = img, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, 1}};
|
||||
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &ib};
|
||||
vkCmdPipelineBarrier2(cb, &di);
|
||||
}
|
||||
|
||||
static double now_ms(void) { return (double)plat_now_ns() / 1e6; }
|
||||
|
||||
// Decodes regions (w0 x h0 at level 0, w1 x h1 at level 1) of blk on the GPU; out_rb: the decoder's texels, out_img:
|
||||
// the image's two levels read back. 0 on a Vulkan failure.
|
||||
static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32_t w0, uint32_t h0, uint32_t w1,
|
||||
uint32_t h1, uint32_t bw, uint32_t bh, int srgb, uint8_t *out_rb, uint8_t *out_img, double *ms) {
|
||||
Img im;
|
||||
if (!make_image(c, w0, h0, 2, srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, &im)) return 0;
|
||||
size_t t0 = (size_t)w0 * h0 * 4, t1 = (size_t)w1 * h1 * 4;
|
||||
VkCommandBuffer cb = vk_upload_begin();
|
||||
VkBuffer sb, rb, ib;
|
||||
VmaAllocation ra, ia;
|
||||
uint8_t *stg = vk_staging((n0 + n1) * 16, &sb, NULL);
|
||||
uint8_t *rmap = vk_staging(t0 + t1, &rb, &ra), *imap = vk_staging(t0 + t1, &ib, &ia);
|
||||
memcpy(stg, blk, (n0 + n1) * 16);
|
||||
to_general(cb, im.img);
|
||||
VkAstcRegion ar[2] = {{.src = 0, .w = w0, .h = h0, .slices = 1, .level = 0},
|
||||
{.src = n0 * 16, .w = w1, .h = h1, .slices = 1, .level = 1}};
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, 0, im.img, VK_IMAGE_ASPECT_COLOR_BIT, rb);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
|
||||
VkBufferImageCopy cp[2] = {{.bufferOffset = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w0, h0, 1}},
|
||||
{.bufferOffset = t0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 1, 0, 1}, .imageExtent = {w1, h1, 1}}};
|
||||
vkCmdCopyImageToBuffer(cb, im.img, VK_IMAGE_LAYOUT_GENERAL, ib, 2, cp);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT);
|
||||
vk_upload_end();
|
||||
double a = now_ms();
|
||||
vk_upload_flush(1);
|
||||
if (ms) *ms = now_ms() - a;
|
||||
vmaInvalidateAllocation(c->vma, ra, 0, VK_WHOLE_SIZE);
|
||||
vmaInvalidateAllocation(c->vma, ia, 0, VK_WHOLE_SIZE);
|
||||
if (out_rb) memcpy(out_rb, rmap, t0 + t1);
|
||||
if (out_img) memcpy(out_img, imap, t0 + t1);
|
||||
vmaDestroyBuffer(c->vma, rb, ra), vmaDestroyBuffer(c->vma, ib, ia);
|
||||
vk_defer_image(im.img, im.alloc);
|
||||
vk_upload_flush(1);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The same two regions decoded and encoded to BC3 (NX2NAVI_ASTC_BC) into a BC3 image; out: its blocks read back, level 0
|
||||
// then level 1, rows of 4x4 blocks. 0 on a Vulkan failure or when nothing was recorded.
|
||||
static int gpu_bc(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32_t w0, uint32_t h0, uint32_t w1, uint32_t h1,
|
||||
uint32_t bw, uint32_t bh, int srgb, uint8_t *out) {
|
||||
Img im;
|
||||
if (!make_image(c, w0, h0, 2, srgb ? VK_FORMAT_BC3_SRGB_BLOCK : VK_FORMAT_BC3_UNORM_BLOCK, &im)) return 0;
|
||||
size_t b0 = (size_t)((w0 + 3) / 4) * ((h0 + 3) / 4) * 16, b1 = (size_t)((w1 + 3) / 4) * ((h1 + 3) / 4) * 16;
|
||||
VkCommandBuffer cb = vk_upload_begin();
|
||||
VkBuffer sb, ib;
|
||||
VmaAllocation ia;
|
||||
uint8_t *stg = vk_staging((n0 + n1) * 16, &sb, NULL), *imap = vk_staging(b0 + b1, &ib, &ia);
|
||||
memcpy(stg, blk, (n0 + n1) * 16);
|
||||
to_general(cb, im.img);
|
||||
VkAstcRegion ar[2] = {{.src = 0, .w = w0, .h = h0, .slices = 1, .level = 0},
|
||||
{.src = n0 * 16, .w = w1, .h = h1, .slices = 1, .level = 1}};
|
||||
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
|
||||
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, 1, im.img, VK_IMAGE_ASPECT_COLOR_BIT, VK_NULL_HANDLE);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
|
||||
VkBufferImageCopy cp[2] = {{.bufferOffset = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w0, h0, 1}},
|
||||
{.bufferOffset = b0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 1, 0, 1}, .imageExtent = {w1, h1, 1}}};
|
||||
vkCmdCopyImageToBuffer(cb, im.img, VK_IMAGE_LAYOUT_GENERAL, ib, 2, cp);
|
||||
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT);
|
||||
vk_upload_end();
|
||||
vk_upload_flush(1);
|
||||
vmaInvalidateAllocation(c->vma, ia, 0, VK_WHOLE_SIZE);
|
||||
memcpy(out, imap, b0 + b1);
|
||||
vmaDestroyBuffer(c->vma, ib, ia);
|
||||
vk_defer_image(im.img, im.alloc);
|
||||
vk_upload_flush(1);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// One BC3 block to 16 RGBA8 texels, as the format defines it (565 endpoints widened by bit replication; the colour
|
||||
// block always in four-colour mode). pos: each texel's place on the line from endpoint 0 (0) to endpoint 1 (3).
|
||||
static void bc3_decode(const uint8_t b[16], uint8_t t[16][4], uint8_t ep[2][3], int pos[16]) {
|
||||
uint8_t a[8] = {b[0], b[1]}, p[4][3];
|
||||
if (a[0] > a[1])
|
||||
for (int i = 1; i < 7; i++) a[i + 1] = (uint8_t)(((7 - i) * a[0] + i * a[1] + 3) / 7);
|
||||
else {
|
||||
for (int i = 1; i < 5; i++) a[i + 1] = (uint8_t)(((5 - i) * a[0] + i * a[1] + 2) / 5);
|
||||
a[6] = 0, a[7] = 255;
|
||||
}
|
||||
uint64_t ai = 0;
|
||||
for (int i = 0; i < 6; i++) ai |= (uint64_t)b[2 + i] << 8 * i;
|
||||
for (int k = 0; k < 2; k++) {
|
||||
unsigned v = b[8 + 2 * k] | b[9 + 2 * k] << 8, r = v >> 11, g = v >> 5 & 63, bl = v & 31;
|
||||
p[k][0] = (uint8_t)(r << 3 | r >> 2), p[k][1] = (uint8_t)(g << 2 | g >> 4), p[k][2] = (uint8_t)(bl << 3 | bl >> 2);
|
||||
}
|
||||
for (int ch = 0; ch < 3; ch++)
|
||||
p[2][ch] = (uint8_t)((2 * p[0][ch] + p[1][ch] + 1) / 3), p[3][ch] = (uint8_t)((p[0][ch] + 2 * p[1][ch] + 1) / 3);
|
||||
uint32_t ci = b[12] | b[13] << 8 | b[14] << 16 | (uint32_t)b[15] << 24;
|
||||
static const int code_pos[4] = {0, 3, 1, 2};
|
||||
for (int i = 0; i < 16; i++) {
|
||||
memcpy(t[i], p[ci >> 2 * i & 3], 3);
|
||||
t[i][3] = a[ai >> 3 * i & 7];
|
||||
pos[i] = code_pos[ci >> 2 * i & 3];
|
||||
}
|
||||
memcpy(ep, p, 6);
|
||||
}
|
||||
|
||||
// BC3 is lossy, so these are bounds, not bit-exactness. In every block: alpha within a fourteenth of the block's alpha
|
||||
// range (the eight-value mode's half step); each texel on the palette entry nearest its projection onto the endpoint
|
||||
// line (within rounding, when the endpoints are apart); a block of one colour within the 565 rounding, alpha exact.
|
||||
// Per level, a PSNR floor against astc_decode_ldr for gross damage: the random blocks are noise, so it is low (19 dB
|
||||
// at worst for 5x5 level 1 on an RX 9070). *db: the level's PSNR.
|
||||
enum { BC_MIN_DB = 15 };
|
||||
static int bc_check(const uint8_t *want, const uint8_t *blocks, uint32_t w, uint32_t h, const char *what, double *db) {
|
||||
uint32_t bx = (w + 3) / 4, by = (h + 3) / 4;
|
||||
double se = 0;
|
||||
for (uint32_t y4 = 0; y4 < by; y4++)
|
||||
for (uint32_t x4 = 0; x4 < bx; x4++) {
|
||||
uint8_t t[16][4], ep[2][3], lo[4] = {255, 255, 255, 255}, hi[4] = {0};
|
||||
int pos[16];
|
||||
bc3_decode(blocks + 16 * ((size_t)y4 * bx + x4), t, ep, pos);
|
||||
double axis[3], len2 = 0;
|
||||
for (int ch = 0; ch < 3; ch++) axis[ch] = (double)ep[1][ch] - ep[0][ch], len2 += axis[ch] * axis[ch];
|
||||
for (int pass = 0; pass < 2; pass++)
|
||||
for (int i = 0; i < 16; i++) {
|
||||
uint32_t x = x4 * 4 + (i & 3), y = y4 * 4 + (i >> 2);
|
||||
if (x >= w || y >= h) continue;
|
||||
const uint8_t *s = want + 4 * ((size_t)y * w + x);
|
||||
if (!pass) {
|
||||
for (int ch = 0; ch < 4; ch++) lo[ch] = s[ch] < lo[ch] ? s[ch] : lo[ch], hi[ch] = s[ch] > hi[ch] ? s[ch] : hi[ch];
|
||||
continue;
|
||||
}
|
||||
int flat = !memcmp(lo, hi, 4), lim[4] = {5, 3, 5, 0};
|
||||
if (len2 >= 64) { // shorter lines: rounding alone can move a texel to the next entry
|
||||
double proj = 0;
|
||||
for (int ch = 0; ch < 3; ch++) proj += ((double)s[ch] - ep[0][ch]) * axis[ch];
|
||||
proj = proj / len2 * 3;
|
||||
proj = proj < 0 ? 0 : proj > 3 ? 3 : proj;
|
||||
if (fabs(pos[i] - proj) > 0.75) { // a wrong code mapping is a whole place off
|
||||
printf("probe-astc: FAIL BC3 %s: texel (%u,%u) on palette place %d, its projection is %.2f\n",
|
||||
what, x, y, pos[i], proj);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
for (int ch = 0; ch < 4; ch++) {
|
||||
int d = abs((int)t[i][ch] - s[ch]);
|
||||
se += (double)d * d;
|
||||
if ((ch == 3 && d > (hi[3] - lo[3]) / 14 + 1) || (flat && d > lim[ch])) {
|
||||
printf("probe-astc: FAIL BC3 %s: texel (%u,%u) channel %d: astc %02x%02x%02x%02x bc3 %02x%02x%02x%02x\n",
|
||||
what, x, y, ch, s[0], s[1], s[2], s[3], t[i][0], t[i][1], t[i][2], t[i][3]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
double mse = se / ((double)w * h * 4);
|
||||
*db = mse > 0 ? 10 * log10(255.0 * 255.0 / mse) : 99;
|
||||
if (*db < BC_MIN_DB) {
|
||||
printf("probe-astc: FAIL BC3 %s: PSNR %.1f dB, below %d\n", what, *db, BC_MIN_DB);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int first_diff(const uint8_t *a, const uint8_t *b, size_t texels, size_t *at) {
|
||||
for (size_t i = 0; i < texels; i++)
|
||||
if (memcmp(a + 4 * i, b + 4 * i, 4)) { *at = i; return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_case(VkCtx *c, uint32_t bw, uint32_t bh, int srgb) {
|
||||
// level 0: 37 x 23 blocks, cropped by 1-3 texels; level 1 half of that, other blocks
|
||||
uint32_t w0 = 37 * bw - 1 - (bw > 5), h0 = 23 * bh - 2, w1 = w0 > 1 ? w0 >> 1 : 1, h1 = h0 > 1 ? h0 >> 1 : 1;
|
||||
size_t n0 = (size_t)((w0 + bw - 1) / bw) * ((h0 + bh - 1) / bh), n1 = (size_t)((w1 + bw - 1) / bw) * ((h1 + bh - 1) / bh);
|
||||
uint8_t *blk = malloc((n0 + n1) * 16);
|
||||
make_blocks(blk, n0, bw, bh);
|
||||
make_blocks(blk + n0 * 16, n1, bw, bh);
|
||||
size_t t0 = (size_t)w0 * h0, t1 = (size_t)w1 * h1;
|
||||
uint8_t *want = malloc((t0 + t1) * 4), *rb = malloc((t0 + t1) * 4), *im = malloc((t0 + t1) * 4);
|
||||
memset(rb, 0x5A, (t0 + t1) * 4), memset(im, 0xA5, (t0 + t1) * 4);
|
||||
astc_decode_ldr(blk, w0, h0, bw, bh, srgb, want);
|
||||
astc_decode_ldr(blk + n0 * 16, w1, h1, bw, bh, srgb, want + t0 * 4);
|
||||
int bad = 0;
|
||||
if (!gpu_decode(c, blk, n0, n1, w0, h0, w1, h1, bw, bh, srgb, rb, im, NULL)) {
|
||||
printf("probe-astc: FAIL %ux%u %s: the GPU decode was not recorded\n", bw, bh, srgb ? "sRGB" : "linear");
|
||||
bad = 1;
|
||||
}
|
||||
for (int lv = 0; lv < 2 && !bad; lv++) {
|
||||
size_t off = lv ? t0 * 4 : 0, n = lv ? t1 : t0, at;
|
||||
uint32_t w = lv ? w1 : w0;
|
||||
for (int which = 0; which < 2 && !bad; which++) {
|
||||
const uint8_t *got = (which ? im : rb) + off;
|
||||
if (first_diff(want + off, got, n, &at)) {
|
||||
const uint8_t *e = want + off + 4 * at, *g = got + 4 * at;
|
||||
printf("probe-astc: FAIL %ux%u %s level %d (%s): texel (%zu,%zu) cpu %02x%02x%02x%02x gpu %02x%02x%02x%02x\n",
|
||||
bw, bh, srgb ? "sRGB" : "linear", lv, which ? "image" : "decoder output", at % w, at / w,
|
||||
e[0], e[1], e[2], e[3], g[0], g[1], g[2], g[3]);
|
||||
bad = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bad && vk_astc_bc()) {
|
||||
size_t b0 = (size_t)((w0 + 3) / 4) * ((h0 + 3) / 4) * 16, b1 = (size_t)((w1 + 3) / 4) * ((h1 + 3) / 4) * 16;
|
||||
uint8_t *bc = malloc(b0 + b1);
|
||||
char what[2][48];
|
||||
double db[2] = {0, 0};
|
||||
for (int lv = 0; lv < 2; lv++) snprintf(what[lv], sizeof what[lv], "%ux%u %s level %d", bw, bh, srgb ? "sRGB" : "linear", lv);
|
||||
if (!gpu_bc(c, blk, n0, n1, w0, h0, w1, h1, bw, bh, srgb, bc)) {
|
||||
printf("probe-astc: FAIL BC3 %s: the encode was not recorded\n", what[0]);
|
||||
bad = 1;
|
||||
}
|
||||
bad = bad || bc_check(want, bc, w0, h0, what[0], &db[0]) || bc_check(want + t0 * 4, bc + b0, w1, h1, what[1], &db[1]);
|
||||
for (int lv = 0; lv < 2 && !bad; lv++)
|
||||
if (db[lv] < g_bc_db) g_bc_db = db[lv];
|
||||
g_bc_cases += !bad;
|
||||
free(bc);
|
||||
}
|
||||
g_blocks += n0 + n1, g_texels += t0 + t1;
|
||||
free(blk), free(want), free(rb), free(im);
|
||||
return bad;
|
||||
}
|
||||
|
||||
// One 2048 x 2048 texture of valid blocks: the CPU worker-pool decode against the GPU time of the dispatches alone
|
||||
// (vk_astc.c timestamps, NX2NAVI_ASTC_TIME) and the wall time of the upload batch.
|
||||
static void timing(VkCtx *c, uint32_t bw, uint32_t bh) {
|
||||
uint32_t w = 2048, h = 2048;
|
||||
size_t n = (size_t)((w + bw - 1) / bw) * ((h + bh - 1) / bh);
|
||||
uint8_t *blk = malloc(n * 16), *out = malloc((size_t)w * h * 4), tmp[12 * 12 * 4];
|
||||
for (size_t k = 0; k < 4096;) { // valid, non-constant blocks: error blocks would flatter the GPU
|
||||
uint64_t v[2] = {rnd64(), rnd64()};
|
||||
if ((v[0] & 0x1FF) != 0x1FC && astc_decode_block((uint8_t *)v, bw, bh, false, tmp)) memcpy(blk + 16 * k++, v, 16);
|
||||
}
|
||||
for (size_t k = 4096; k < n; k++) memcpy(blk + 16 * k, blk + 16 * (k % 4096), 16);
|
||||
double a = now_ms();
|
||||
astc_decode_ldr_mt(blk, w, h, bw, bh, false, out);
|
||||
double cpu = now_ms() - a, wall = 0;
|
||||
vk_astc_time_enable(1);
|
||||
uint64_t t0, c0, t1, c1;
|
||||
double u0, u1;
|
||||
vk_astc_time_get(bw, bh, &t0, &c0, &u0);
|
||||
for (int i = 0; i < 4; i++) gpu_decode(c, blk, n, 1, w, h, 1, 1, bw, bh, 0, NULL, NULL, i ? &wall : NULL);
|
||||
vk_astc_time_get(bw, bh, &t1, &c1, &u1);
|
||||
vk_astc_time_enable(0);
|
||||
double mtex = (double)(t1 - t0) / 1e6;
|
||||
printf("probe-astc: 2048x2048 %ux%u: CPU (worker pool) %.1f ms; GPU dispatches %.0f us per M texel (%llu decodes "
|
||||
"timed, %.2f M texels), batch wall %.1f ms\n", bw, bh, cpu, mtex > 0 ? (u1 - u0) / mtex : 0.0,
|
||||
(unsigned long long)(c1 - c0), mtex, wall);
|
||||
free(blk), free(out);
|
||||
}
|
||||
|
||||
int probe_astc(void) {
|
||||
plat_setenv("NX2NAVI_ASTC_BC", "1"); // also builds the BC3 encoder; the RGBA8 cases pass bc 0 and do not use it
|
||||
if (!vk_init()) { printf("probe-astc: FAIL vk_init\n"); return 1; }
|
||||
VkCtx *c = vk_ctx();
|
||||
printf("probe-astc: GPU %s, textureCompressionASTC_LDR %d, mode %d\n", c->props.deviceName, c->has_astc_ldr,
|
||||
vk_astc_mode());
|
||||
int bad = 0;
|
||||
if (!vk_astc_gpu()) {
|
||||
printf("probe-astc: FAIL the compute decoder is not available (NX2NAVI_ASTC=cpu or pipeline creation failed)\n");
|
||||
bad = 1;
|
||||
}
|
||||
for (int i = 0; i < 14 && !bad; i++)
|
||||
for (int s = 0; s < 2 && !bad; s++) bad = test_case(c, dims[i][0], dims[i][1], s);
|
||||
for (int b = 0; b < NB && !bad; b++)
|
||||
if (!g_bucket[b]) { printf("probe-astc: FAIL no valid %s block was generated\n", bucket_name[b]); bad = 1; }
|
||||
if (!bad && !vk_astc_bc()) printf("probe-astc: BC3 encoder not available on this device: not checked\n");
|
||||
else if (!bad) printf("probe-astc: BC3 encode: %llu cases within the bounds, lowest PSNR %.1f dB (floor %d)\n",
|
||||
(unsigned long long)g_bc_cases, g_bc_db, BC_MIN_DB);
|
||||
if (!bad) {
|
||||
timing(c, 4, 4);
|
||||
timing(c, 6, 6);
|
||||
timing(c, 8, 8);
|
||||
timing(c, 12, 12);
|
||||
}
|
||||
vk_mem_shutdown();
|
||||
vk_shutdown();
|
||||
long errors = vk_validation_errors_total();
|
||||
if (bad || errors) {
|
||||
printf("probe-astc: FAILED (%s, %ld validation errors)\n", bad ? "a check failed" : "checks passed", errors);
|
||||
return 1;
|
||||
}
|
||||
printf("probe-astc: passed (14 footprints x linear/sRGB, %llu blocks, %llu texels bit-exact with astc_decode_ldr; "
|
||||
"valid blocks by kind: 1p %llu, 2p %llu, 3p %llu, 4p %llu, dual 1p %llu, dual 2p %llu, dual 3p %llu; "
|
||||
"%llu void extents, %llu random error blocks; 0 validation errors)\n",
|
||||
(unsigned long long)g_blocks, (unsigned long long)g_texels, (unsigned long long)g_bucket[0],
|
||||
(unsigned long long)g_bucket[1], (unsigned long long)g_bucket[2], (unsigned long long)g_bucket[3],
|
||||
(unsigned long long)g_bucket[4], (unsigned long long)g_bucket[5], (unsigned long long)g_bucket[6],
|
||||
(unsigned long long)g_void, (unsigned long long)g_err);
|
||||
return 0;
|
||||
}
|
||||
188
runtime/audio_effects.c
Normal file
188
runtime/audio_effects.c
Normal file
@@ -0,0 +1,188 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/audio_effects.c @ b75be8c (MIT, Copyright (c) 2026 MacNativeGames contributors).
|
||||
// Algorithms from Ryujinx.Audio (MIT, Copyright (c) Ryujinx Team and Contributors).
|
||||
/* Delay, standard reverb and compressor DSP, ported from Ryujinx.Audio.
|
||||
Copyright (c) Ryujinx Team and Contributors. MIT license:
|
||||
third_party/licenses/Ryujinx-Audio.txt. Runtime samples are normalized floats. */
|
||||
#include "audio_effects.h"
|
||||
#include "host.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct { float *data; unsigned max, count, at; } DelayLine;
|
||||
struct AudioEffectDsp {
|
||||
unsigned type, revision, channels;
|
||||
union { AudrenDelayParam delay; AudrenReverbParam reverb; AudrenCompressorParam compressor; } p;
|
||||
DelayLine line[10];
|
||||
float history[6], direct[4], previous[4], decay_rate;
|
||||
unsigned early_time[10], pre_time;
|
||||
float early_gain[10];
|
||||
float input_mean, gain_mean, last_target, last_alpha, output_gain, makeup;
|
||||
float stats_min, stats_mean, stats_last[6];
|
||||
};
|
||||
static int channel_ok(unsigned n) { return n==1 || n==2 || n==4 || n==6; }
|
||||
static unsigned sample_count(unsigned rate, float ms) { return (unsigned)nearbyintf(rate*ms); }
|
||||
/* The ring has `count` slots. The reference DelayLine wraps its cursor at
|
||||
CurrentSampleCount-1 while its taps index modulo CurrentSampleCount, so once
|
||||
per turn every tap reads a slot that is never written (a one-sample dropout
|
||||
per tap, audible as grain). The cursor and the taps share one ring length here;
|
||||
each delay is one sample longer than the reference. */
|
||||
static void line_set(DelayLine *d, unsigned n) { d->count=n<d->max?n:d->max; d->at=0; }
|
||||
static void line_init(DelayLine *d, unsigned rate, float ms) {
|
||||
d->max=sample_count(rate,ms); d->data=calloc((size_t)d->max+1,sizeof(float));
|
||||
if(!d->data)rt_fatal("audio effect delay allocation"); line_set(d,d->max);
|
||||
}
|
||||
static float line_read(const DelayLine *d) { return d->data[d->at]; }
|
||||
static float line_update(DelayLine *d,float v) {
|
||||
float old=line_read(d);d->data[d->at++]=v;
|
||||
if(d->at >= d->count)d->at=0;
|
||||
return old;
|
||||
}
|
||||
static float line_tap(const DelayLine *d,unsigned sample,int offset) {
|
||||
int at=(int)d->at-(int)sample-offset;
|
||||
if(d->count){at%=(int)d->count;if(at<0)at+=(int)d->count;}
|
||||
if(at<0 || (unsigned)at>d->max)rt_fatal("audio effect delay tap bounds");
|
||||
return d->data[at];
|
||||
}
|
||||
static float gain10(float x) { return x>=0?1:x<=-5.3f?0:powf(10,x); }
|
||||
static const float fdn_time[4][4]={{53.953247f,79.192566f,116.238770f,130.615295f},{53.953247f,79.192566f,116.238770f,170.615295f},{5,10,5,10},{47.03f,71,103,170}};
|
||||
static const float decay_time[4][4]={{7,9,13,17},{7,9,13,17},{1,1,1,1},{7,7,13,9}};
|
||||
/* Early reflections are indexed by EarlyMode. These are Ryujinx.Audio's five
|
||||
tables; its own version reads the first row for every mode. */
|
||||
static const float early_time[5][10]={
|
||||
{0,3.5f,2.8f,3.9f,2.7f,13.4f,7.9f,8.4f,9.9f,12},
|
||||
{0,11.8f,5.5f,11.2f,10.4f,38.1f,22.2f,29.6f,21.2f,24.8f},
|
||||
{0,41.5f,20.5f,41.3f,0,29.5f,33.8f,45.2f,46.8f,0},
|
||||
{33.1f,43.3f,22.8f,37.9f,14.9f,35.3f,17.9f,34.2f,0,43.3f},
|
||||
{0}
|
||||
};
|
||||
static const float early_gain[5][10]={
|
||||
{.70f,.68f,.70f,.68f,.70f,.68f,.70f,.68f,.68f,.68f},
|
||||
{.70f,.68f,.70f,.68f,.70f,.68f,.68f,.68f,.68f,.68f},
|
||||
{.50f,.70f,.70f,.68f,.50f,.68f,.68f,.70f,.68f,0},
|
||||
{.93f,.92f,.87f,.86f,.94f,.81f,.80f,.77f,.76f,.65f},
|
||||
{0}
|
||||
};
|
||||
static const float pre_time[5]={12.5f,40,50,50,0};
|
||||
static void validate_delay(const AudrenDelayParam *p) {
|
||||
unsigned rate=(p->sample_rate>>14)/1000;
|
||||
if(!channel_ok(p->channels) || p->channel_max<p->channels || rate<1 || rate>192 || p->delay_max>20000 || p->delay>p->delay_max)
|
||||
rt_fatal("audio delay parameters");
|
||||
}
|
||||
static void validate_reverb(const AudrenReverbParam *p) {
|
||||
if(!channel_ok(p->channels) || p->channel_max<p->channels || p->sample_rate<16384 || p->sample_rate>192*16384 ||
|
||||
p->early_mode<0 || p->early_mode>4 || p->late_mode<0 || p->late_mode>3 || p->decay<=0 || p->hf_decay<=0 || p->pre_delay<0)
|
||||
rt_fatal("audio standard reverb parameters");
|
||||
}
|
||||
static void validate_compressor(const AudrenCompressorParam *p) {
|
||||
if(!channel_ok(p->channels) || p->channel_max<p->channels || !isfinite(p->threshold) || !(p->ratio>0) || !isfinite(p->ratio) ||
|
||||
!(p->input_gain>=0 && p->input_gain<=1) || !(p->attack>=0 && p->attack<=1) || !(p->release>=0 && p->release<=1) || !isfinite(p->output_gain))
|
||||
rt_fatal("audio compressor parameters");
|
||||
}
|
||||
AudioEffectDsp *audio_effect_create(unsigned type,const void *param,unsigned revision) {
|
||||
AudioEffectDsp *s=calloc(1,sizeof *s);if(!s)rt_fatal("audio effect allocation");
|
||||
s->type=type;s->revision=revision;s->gain_mean=s->last_target=s->stats_min=1;
|
||||
if(type==3) {
|
||||
AudrenDelayParam p;memcpy(&p,param,sizeof p);validate_delay(&p);s->channels=p.channels;
|
||||
for(unsigned c=0;c<s->channels;c++){line_init(&s->line[c],(p.sample_rate>>14)/1000,p.delay_max);line_set(&s->line[c],sample_count((p.sample_rate>>14)/1000,p.delay));}
|
||||
p.status=0;audio_effect_update(s,&p);
|
||||
} else if(type==4) {
|
||||
AudrenReverbParam p;memcpy(&p,param,sizeof p);validate_reverb(&p);s->channels=p.channels;unsigned khz=(unsigned)p.sample_rate>>14;
|
||||
for(int i=0;i<4;i++){line_init(&s->line[i],khz,fdn_time[1][i]);line_init(&s->line[i+4],khz,decay_time[1][i]);}
|
||||
line_init(&s->line[8],khz,revision>=3?350:150);line_init(&s->line[9],khz,5);
|
||||
p.status=0;audio_effect_update(s,&p);
|
||||
} else if(type==9) {
|
||||
AudrenCompressorParam p;memcpy(&p,param,sizeof p);validate_compressor(&p);s->channels=p.channels;
|
||||
p.status=0;audio_effect_update(s,&p);
|
||||
} else rt_fatal("audio effect DSP type");
|
||||
return s;
|
||||
}
|
||||
void audio_effect_free(AudioEffectDsp *s) { if(s){for(int i=0;i<10;i++)free(s->line[i].data);free(s);} }
|
||||
void audio_effect_update(AudioEffectDsp *s,const void *param) {
|
||||
if(s->type==3){
|
||||
AudrenDelayParam p;memcpy(&p,param,sizeof p);validate_delay(&p);
|
||||
if(p.channels!=s->channels || (s->p.delay.sample_rate && (p.sample_rate!=s->p.delay.sample_rate || p.delay_max!=s->p.delay.delay_max)))rt_fatal("audio delay requires reset");
|
||||
s->p.delay=p;
|
||||
} else if(s->type==4){
|
||||
AudrenReverbParam p;memcpy(&p,param,sizeof p);validate_reverb(&p);if(p.channels!=s->channels)rt_fatal("audio reverb requires reset");s->p.reverb=p;
|
||||
if(p.status>=2)return;
|
||||
unsigned rate=(unsigned)p.sample_rate>>14;
|
||||
for(int i=0;i<10;i++){
|
||||
unsigned n=sample_count(rate,early_time[p.early_mode][i]+p.pre_delay/16384.f);s->early_time[i]=(n<s->line[8].max?n:s->line[8].max)+1;
|
||||
s->early_gain[i]=early_gain[p.early_mode][i]*(p.early_gain/16384.f);
|
||||
}
|
||||
if(p.channels==2){s->early_gain[4]*=.5f;s->early_gain[5]*=.5f;}
|
||||
unsigned n=sample_count(rate,pre_time[p.early_mode]+p.pre_delay/16384.f);s->pre_time=n<s->line[8].max?n:s->line[8].max;
|
||||
float decay=p.decay/16384.f,hf=p.hf_decay/16384.f,co=cosf((1280.f/rate)*(3.14159265358979323846f/180));
|
||||
s->decay_rate=.6f*(1-p.coloration/16384.f);
|
||||
for(int i=0;i<4;i++){
|
||||
line_set(&s->line[i],sample_count(rate,fdn_time[p.late_mode][i]));line_set(&s->line[4+i],sample_count(rate,decay_time[p.late_mode][i]));
|
||||
float a=-3.f*(s->line[i].count+s->line[i+4].count),b=a/(decay*rate),c=0;
|
||||
if(hf<.995f){float e=gain10(((1/hf-1)*2)/100*(b/10)),f=1-e,g=2-e*2*co;c=(g-sqrtf(g*g-f*f*4))/(f*2);}
|
||||
s->direct[i]=gain10(b/1000)*(1-c)*.7071f;s->previous[i]=c;s->history[i]=0;
|
||||
}
|
||||
} else {
|
||||
AudrenCompressorParam p;memcpy(&p,param,sizeof p);validate_compressor(&p);if(p.channels!=s->channels)rt_fatal("audio compressor requires reset");s->p.compressor=p;
|
||||
if(p.status<2){s->last_alpha=p.attack;s->makeup=p.makeup?p.threshold*.5f*(1/p.ratio-1)-3:0;s->output_gain=powf(10,(p.output_gain+s->makeup)/20);}
|
||||
}
|
||||
}
|
||||
static void delay_process(AudioEffectDsp *s,float *const in[6],float *const out[6],int frames) {
|
||||
const AudrenDelayParam *p=&s->p.delay;unsigned n=s->channels;
|
||||
float fb=p->feedback/16384.f,spread=p->spread/16384.f,base=(1-spread)*fb*.98f,cross=spread*fb*.98f/(n>=4?2:1),lp=.95f*(p->lowpass/16384.f);
|
||||
static const int quad[4][2]={{1,2},{0,3},{0,3},{1,2}};
|
||||
static const int surround[6][2]={{2,4},{2,5},{0,1},{3,3},{0,5},{1,4}};
|
||||
for(int k=0;k<frames;k++){
|
||||
float x[6],d[6],v[6];for(unsigned c=0;c<n;c++){x[c]=in[c][k]*64;d[c]=line_read(&s->line[c]);}
|
||||
for(unsigned c=0;c<n;c++){
|
||||
float feedback=n==1 || (n==6 && c==3)?d[c]*fb:d[c]*base;
|
||||
if(n==2)feedback+=d[1-c]*cross;
|
||||
else if(n>=4 && !(n==6 && c==3)){const int *q=n==4?quad[c]:surround[c];feedback+=(d[q[0]]+d[q[1]])*cross;}
|
||||
v[c]=x[c]*(p->in_gain/16384.f)+feedback;
|
||||
float z=lp*s->history[c]+v[c]*(1-lp);s->history[c]=z;line_update(&s->line[c],z);
|
||||
out[c][k]=(x[c]*(p->dry_gain/16384.f)+d[c]*(p->out_gain/16384.f))/64;
|
||||
}
|
||||
}
|
||||
}
|
||||
static void reverb_process(AudioEffectDsp *s,float *const in[6],float *const out[6],int frames) {
|
||||
const AudrenReverbParam *p=&s->p.reverb;unsigned n=s->channels;
|
||||
static const int mono[10]={0,0,0,0,0,0,0,0,0,0},stereo[10]={0,0,1,1,0,1,0,0,1,1},quad[10]={0,0,1,1,0,1,2,2,3,3},surround[10]={0,0,1,1,4,4,2,2,3,3};
|
||||
static const int feedback_stereo[4]={2,0,3,1},output_stereo[4]={0,0,1,1};
|
||||
const int *map=n==1?mono:n==2?stereo:n==4?quad:surround;
|
||||
for(int k=0;k<frames;k++){
|
||||
float y[6]={0},x[6],fb[4],v[4],sum=0;
|
||||
for(int i=0;i<10;i++){float tap=line_tap(&s->line[8],s->early_time[i],0)*s->early_gain[i];y[map[i]]+=tap;if(n==6)y[5]+=tap;}
|
||||
if(n==6)y[5]*=.2f;
|
||||
for(unsigned c=0;c<n;c++){x[c]=in[c][k]*64;sum+=x[c]*(p->reverb_gain/16384.f);}line_update(&s->line[8],sum);
|
||||
unsigned at=s->pre_time;if(at>=s->line[8].count)at=s->line[8].count-1;
|
||||
float late=line_tap(&s->line[8],at,-1)*(p->late_gain/16384.f);
|
||||
for(int i=0;i<4;i++){v[i]=line_read(&s->line[i])*s->direct[i]+s->history[i]*s->previous[i];s->history[i]=v[i];}
|
||||
fb[0]=v[2]+v[1];fb[1]=-v[0]-v[3];fb[2]=v[0]-v[3];fb[3]=v[1]-v[2];
|
||||
for(int i=0;i<4;i++){float d=line_read(&s->line[4+i]),z=fb[i]+late-s->decay_rate*d;v[i]=line_update(&s->line[4+i],z)+z*s->decay_rate;line_update(&s->line[i],v[i]);}
|
||||
if(n==1)for(int i=0;i<4;i++)y[0]+=v[i];
|
||||
else if(n==2)for(int i=0;i<4;i++)y[output_stereo[i]]+=v[feedback_stereo[i]];
|
||||
else {for(int i=0;i<4;i++)y[i]+=v[i];if(n==6){y[5]+=v[3];y[4]+=line_update(&s->line[9],(v[2]-v[3])*.5f);}}
|
||||
for(unsigned c=0;c<n;c++)out[c][k]=(y[c]*(p->out_gain/16384.f)+x[c]*(p->dry_gain/16384.f))/64;
|
||||
}
|
||||
}
|
||||
static void compressor_process(AudioEffectDsp *s,float *const in[6],float *const out[6],int frames) {
|
||||
const AudrenCompressorParam *p=&s->p.compressor;
|
||||
if(p->reset_statistics){s->stats_min=1;s->stats_mean=0;memset(s->stats_last,0,sizeof s->stats_last);}
|
||||
for(int k=0;k<frames;k++){
|
||||
float x[6],mean=0;for(unsigned c=0;c<s->channels;c++){x[c]=in[c][k];mean+=x[c]*x[c];}mean/=s->channels;
|
||||
s->input_mean+=p->input_gain*(mean-s->input_mean);float db=10*log10f(fmaxf(s->input_mean,1e-10f)),z=1;
|
||||
if(db>=p->threshold-1.5f){float g=db>=p->threshold+1.5f?(1/p->ratio-1)*(db-p->threshold):(db-p->threshold+1.5f)*(db-p->threshold+1.5f)*(-(1-1/p->ratio)/6);z=powf(10,g/20);}
|
||||
float alpha,target=z,diff=s->last_target-z;
|
||||
if(diff>.08f)alpha=p->attack;
|
||||
else if(diff<-.08f)alpha=p->release;
|
||||
else {if(fabsf(s->gain_mean-z)>=.001f)target=s->last_target;alpha=s->last_alpha;}
|
||||
s->gain_mean+=alpha*(z-s->gain_mean);s->last_target=target;s->last_alpha=alpha;
|
||||
for(unsigned c=0;c<s->channels;c++){out[c][k]=x[c]*s->gain_mean*s->output_gain;s->stats_last[c]=fabsf(x[c]);}
|
||||
s->stats_min=fminf(s->stats_min,s->gain_mean*s->output_gain);s->stats_mean=fmaxf(s->stats_mean,mean);
|
||||
}
|
||||
}
|
||||
void audio_effect_process(AudioEffectDsp *s,float *const input[6],float *const output[6],int frames) {
|
||||
if(s->type==3)delay_process(s,input,output,frames);else if(s->type==4)reverb_process(s,input,output,frames);else compressor_process(s,input,output,frames);
|
||||
}
|
||||
void audio_effect_statistics(AudioEffectDsp *s,void *output,unsigned bytes) {
|
||||
if(s && s->type==9 && bytes>=32){float stats[8]={s->stats_mean,s->stats_min};memcpy(stats+2,s->stats_last,24);memcpy(output,stats,32);}
|
||||
}
|
||||
36
runtime/audio_effects.h
Normal file
36
runtime/audio_effects.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/* Additional DSP effects. Wire layouts and algorithms follow Ryujinx.Audio
|
||||
(MIT, third_party/licenses/Ryujinx-Audio.txt). No device or guest-memory access. */
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
typedef struct {
|
||||
uint8_t input[6], output[6]; uint16_t channel_max, channels;
|
||||
uint32_t delay_max, delay, sample_rate, in_gain, feedback, out_gain, dry_gain, spread, lowpass;
|
||||
uint8_t status;
|
||||
} AudrenDelayParam;
|
||||
typedef struct {
|
||||
uint8_t input[6], output[6]; uint16_t channel_max, channels;
|
||||
int32_t sample_rate, early_mode, early_gain, pre_delay, late_mode, late_gain,
|
||||
decay, hf_decay, coloration, reverb_gain, out_gain, dry_gain;
|
||||
uint8_t status;
|
||||
} AudrenReverbParam;
|
||||
typedef struct {
|
||||
uint8_t input[6], output[6]; uint16_t channel_max, channels; int32_t sample_rate;
|
||||
float threshold, ratio; int32_t attack_time, release_time;
|
||||
float input_gain, attack, release, output_gain;
|
||||
uint8_t status, makeup, statistics, reset_statistics;
|
||||
} AudrenCompressorParam;
|
||||
typedef struct { uint8_t input[24], output[24]; float volume[24]; uint32_t count; } AudrenBufferMixParam;
|
||||
typedef struct { uint8_t input[6], output[6]; int16_t numerator[3], denominator[2]; uint8_t channels, status; } AudrenBiquadEffectV1;
|
||||
typedef struct { uint8_t input[6], output[6]; uint32_t padding; float numerator[3], denominator[2]; uint8_t channels, status; uint16_t reserved; } AudrenBiquadEffectV2;
|
||||
_Static_assert(offsetof(AudrenDelayParam,status)==52,"delay wire layout");
|
||||
_Static_assert(offsetof(AudrenReverbParam,status)==64,"reverb wire layout");
|
||||
_Static_assert(offsetof(AudrenCompressorParam,status)==52,"compressor wire layout");
|
||||
_Static_assert(sizeof(AudrenBufferMixParam)==148,"buffer-mix wire layout");
|
||||
_Static_assert(sizeof(AudrenBiquadEffectV1)==24 && sizeof(AudrenBiquadEffectV2)==40,"biquad wire layouts");
|
||||
typedef struct AudioEffectDsp AudioEffectDsp;
|
||||
AudioEffectDsp *audio_effect_create(unsigned type, const void *param, unsigned revision);
|
||||
void audio_effect_free(AudioEffectDsp *s);
|
||||
void audio_effect_update(AudioEffectDsp *s, const void *param);
|
||||
void audio_effect_process(AudioEffectDsp *s, float *const input[6], float *const output[6], int frames);
|
||||
void audio_effect_statistics(AudioEffectDsp *s, void *output, unsigned bytes);
|
||||
971
runtime/audio_mixer.c
Normal file
971
runtime/audio_mixer.c
Normal file
@@ -0,0 +1,971 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/audio_mixer.c @ b75be8c (MIT, Copyright (c) 2026 MacNativeGames
|
||||
// contributors): the mixing stage (voice decode and resampling, splitters, mixes, effects, sinks) and the IAudioDevice
|
||||
// service. The CoreAudio output is replaced by SDL3 audio. Effect algorithms follow
|
||||
// Ryujinx.Audio (MIT, Copyright (c) Ryujinx Team and Contributors).
|
||||
//
|
||||
// Per 5 ms frame the render thread (audren.c, renderer lock held) decodes every started voice (PCM8/16/24/32, float,
|
||||
// DSP-ADPCM) along the sample path the voice clock consumes, resamples it linearly, runs the voice biquads, scales it
|
||||
// into its destination mix, folds the submixes, runs the mix effects and writes the device sink's mix (stereo, or 5.1
|
||||
// with NX2NAVI_AUDIO_CHANNELS, output_channels) into a single-producer single-consumer ring. The SDL audio thread drains
|
||||
// that ring without taking any lock: an empty ring
|
||||
// plays silence and never blocks the render thread or the guest. NX2NAVI_AUDIO=none (or NX2NAVI_AUDIO_OFF) mixes without a
|
||||
// device; NX2NAVI_AUDIO_PREFILL_MS (0..500, default 20, 60 on Linux) is the queued audio the output waits for after start or underrun.
|
||||
// The device's clock paces the render thread, as the audio hardware paces the Switch's DSP (audio_mixer_pace_ns): a
|
||||
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
|
||||
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart. LASRecomp and
|
||||
// SavageWindRecomp pace their renderers by the SDL stream the same way. Without a device, while the renderer is
|
||||
// stopped and while the device takes nothing (a stall), the render thread keeps real time on its own timer; what piled
|
||||
// up meanwhile is skipped on the SDL thread once the device resumes (more than QUEUE_SLACK_MS above the target plus the
|
||||
// device's request), as MacNativeGames' audio_mixer.c trims its ring after a device stall.
|
||||
#include <math.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "audio_effects.h"
|
||||
#include "audio_reverb.h"
|
||||
#include "audren.h"
|
||||
#include "host.h"
|
||||
#include "ipc.h"
|
||||
|
||||
#define MAX_FRAME 1024
|
||||
#define RING_FRAMES 16384 // output frames, about 340 ms at 48 kHz
|
||||
#define OUT_MAX 6 // output channels at most: 5.1
|
||||
#define QUEUE_SLACK_MS 40 // queued audio above the target plus the device's request that ring_pull skips
|
||||
#define STALL_MS 50 // the device took nothing this long (at least 4 periods): the timer paces
|
||||
#define SPLITTER_UNUSED 0xffffffffu
|
||||
#define dm_guest_write_prepare(p, n) ((void)(p), (void)(n)) // guest memory is plain host memory on Windows
|
||||
|
||||
typedef struct { float vol[24]; int32_t dest; uint8_t used; AudrenBiquadFloat biquads[2]; float state[2][2]; } SplitDest;
|
||||
typedef struct { int32_t ndest; int32_t *ids; } SplitInfo;
|
||||
typedef struct {
|
||||
Reverb3d *reverb; Reverb3dParam param; int pending;
|
||||
float *delay, detector[6], compression[6], peak[6], minimum[6]; int position[6], delay_max, channels;
|
||||
AudioEffectDsp *dsp; unsigned type; uint8_t wire[160];
|
||||
float biquad_z[6][2], numerator[3], denominator[2]; uint8_t biquad_channels;
|
||||
} EffectState;
|
||||
typedef struct Mixer {
|
||||
float *buf; int nbuf, frame;
|
||||
int *offset; // first mix buffer of each mix id
|
||||
SplitInfo *split; int nsplit; SplitDest *dest; int ndest;
|
||||
SDL_AudioStream *stream; int device_ok, rate;
|
||||
int channels; // output channels, 2 or 6 (output_channels)
|
||||
float ring[RING_FRAMES * OUT_MAX];
|
||||
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero, skipped_frames;
|
||||
_Atomic int playing; int ready; // ready: the callback's own state
|
||||
int target, period; // output target and device period, frames (output_open)
|
||||
uint64_t stall_ns;
|
||||
_Atomic uint64_t pulled_ns; // kern_now_ns of the device's last request, 0 before the first
|
||||
_Atomic uint64_t flush_until; _Atomic unsigned stop_epoch; unsigned callback_epoch;
|
||||
EffectState *effect; int *effect_order; unsigned effect_reported;
|
||||
uint64_t *sink_address; uint32_t *sink_size, *sink_offset, *sink_old, *sink_written;
|
||||
unsigned reported; // one-time reports
|
||||
float voice_scratch[6][MAX_FRAME], filter_scratch[MAX_FRAME], output_scratch[MAX_FRAME * OUT_MAX];
|
||||
double level_sum, level_peak; uint64_t level_count, level_frames; int first_sound_logged;
|
||||
int prefill; // render blocks
|
||||
} Mixer;
|
||||
|
||||
static _Atomic float device_volume = 1.0f;
|
||||
|
||||
// NX2NAVI_VOLUME: the player's volume in percent (0..100) on top of the game's own; a live setting (the
|
||||
// menu), parsed again when cfg_version changes. Render thread only.
|
||||
static float host_volume(void) {
|
||||
static float v = 1.0f;
|
||||
static unsigned seen = ~0u;
|
||||
unsigned ver = cfg_version();
|
||||
if (ver == seen) return v;
|
||||
seen = ver;
|
||||
double pct = cfg_double("NX2NAVI_VOLUME");
|
||||
v = (float)((pct < 0 ? 0 : pct > 100 ? 100 : pct) / 100);
|
||||
return v;
|
||||
}
|
||||
static unsigned audio_revision(const AudioRenderer *r) { unsigned byte = r->revision >> 24; return byte >= '0' ? byte - '0' : 0; }
|
||||
static void report_once(Mixer *m, unsigned bit, const char *msg, int value) {
|
||||
if (m->reported & (1u << bit)) return;
|
||||
m->reported |= 1u << bit;
|
||||
rt_log("audio: %s (%d)", msg, value);
|
||||
}
|
||||
|
||||
// ---- output: SDL3 audio stream fed from the ring
|
||||
// How much audio the output waits for before it starts, and again after an underrun: NX2NAVI_AUDIO_PREFILL_MS rounded up
|
||||
// to whole 5 ms render blocks. Returns -1 for a value outside 0..500 ms.
|
||||
// The Linux default is higher: at 20 ms PipeWire's queue (max(prefill, two 1024-sample periods)) underran
|
||||
// thousands of times within minutes in busy scenes, and 60 ms removed the underruns.
|
||||
#ifdef __linux__
|
||||
#define PREFILL_DEFAULT_MS 60
|
||||
#else
|
||||
#define PREFILL_DEFAULT_MS 20
|
||||
#endif
|
||||
static int prefill_blocks_for(const char *e) {
|
||||
if (!e || !*e) return PREFILL_DEFAULT_MS / 5;
|
||||
char *end;
|
||||
double ms = strtod(e, &end);
|
||||
if (end == e || *end || !(ms >= 0) || ms > 500) return -1;
|
||||
return (int)ceil(ms / 5.0);
|
||||
}
|
||||
|
||||
// NX2NAVI_AUDIO_CHANNELS: the output channels of this run, decided once. stereo gives 2; 5.1 gives 6 (FL FR C LFE RL RR,
|
||||
// the device sink's order and SDL's); auto gives 6 when the default playback device has at least six channels (a 5.1
|
||||
// or 7.1 speaker setup in Windows; SDL places 5.1 on 7.1) and 2 otherwise. IAudioDevice reports the same count to the
|
||||
// game (GetActiveChannelCount), which picks its sink layout from it. NX2NAVI_AUDIO=none: 2, without touching SDL.
|
||||
static int output_channels(void) {
|
||||
static _Atomic int decided;
|
||||
int n = atomic_load(&decided);
|
||||
if (n) return n;
|
||||
const char *want = cfg_value("NX2NAVI_AUDIO_CHANNELS"), *mode = cfg_get("NX2NAVI_AUDIO");
|
||||
if ((mode && !strcmp(mode, "none")) || cfg_get("NX2NAVI_AUDIO_OFF")) n = 2;
|
||||
else if (!strcmp(want, "5.1")) n = 6;
|
||||
else if (!strcmp(want, "stereo")) n = 2;
|
||||
else {
|
||||
SDL_AudioSpec dev = {0};
|
||||
n = SDL_InitSubSystem(SDL_INIT_AUDIO) && SDL_GetAudioDeviceFormat(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &dev, NULL) &&
|
||||
dev.channels >= 6 ? 6 : 2;
|
||||
rt_log("audio: NX2NAVI_AUDIO_CHANNELS=auto: the default device has %d channels, output %s", dev.channels,
|
||||
n == 6 ? "5.1" : "stereo");
|
||||
}
|
||||
int expected = 0;
|
||||
return atomic_compare_exchange_strong(&decided, &expected, n) ? n : expected;
|
||||
}
|
||||
|
||||
// Fills `frames` output frames from the ring (zeros for what is missing). Lock-free; runs on SDL's audio thread.
|
||||
static void ring_pull(Mixer *m, float *out, uint32_t frames) {
|
||||
const int oc = m->channels;
|
||||
atomic_fetch_add(&m->callback_frames, frames);
|
||||
unsigned epoch = atomic_load_explicit(&m->stop_epoch, memory_order_acquire);
|
||||
uint64_t rd = atomic_load(&m->rd), wr = atomic_load_explicit(&m->wr, memory_order_acquire), have = wr - rd;
|
||||
if (epoch != m->callback_epoch) { // the renderer stopped: drop what was queued before the stop
|
||||
uint64_t until = atomic_load_explicit(&m->flush_until, memory_order_acquire);
|
||||
if (until > rd) {
|
||||
rd = until < wr ? until : wr;
|
||||
atomic_store_explicit(&m->rd, rd, memory_order_release);
|
||||
}
|
||||
have = wr - rd;
|
||||
m->callback_epoch = epoch;
|
||||
m->ready = 0;
|
||||
}
|
||||
if (!atomic_load_explicit(&m->playing, memory_order_acquire)) {
|
||||
memset(out, 0, (size_t)frames * oc * sizeof(float));
|
||||
m->ready = 0;
|
||||
atomic_store_explicit(&m->rd, wr, memory_order_release);
|
||||
return;
|
||||
}
|
||||
uint64_t prefill = (uint64_t)m->frame * (uint64_t)m->prefill;
|
||||
if (prefill < frames) prefill = frames;
|
||||
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
|
||||
if (!m->ready) {
|
||||
if (have < prefill) {
|
||||
memset(out, 0, (size_t)frames * oc * sizeof(float));
|
||||
return;
|
||||
}
|
||||
m->ready = 1;
|
||||
}
|
||||
uint64_t keep = (prefill > (uint64_t)m->target ? prefill : (uint64_t)m->target) + frames;
|
||||
uint64_t slack = (uint64_t)m->rate * QUEUE_SLACK_MS / 1000;
|
||||
if (have > keep + slack) { // piled up while the device stalled: skip the oldest, keep target + request
|
||||
atomic_fetch_add(&m->skipped_frames, have - keep);
|
||||
rd = wr - keep;
|
||||
have = keep;
|
||||
}
|
||||
uint32_t n = frames < have ? frames : (uint32_t)have;
|
||||
uint64_t nonzero = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
uint64_t k = (rd + i) % RING_FRAMES;
|
||||
int sound = 0;
|
||||
for (int c = 0; c < oc; c++) {
|
||||
out[oc * i + c] = m->ring[oc * k + c];
|
||||
sound |= out[oc * i + c] != 0;
|
||||
}
|
||||
nonzero += sound;
|
||||
}
|
||||
atomic_fetch_add(&m->callback_nonzero, nonzero);
|
||||
if (n < frames) {
|
||||
memset(out + (size_t)oc * n, 0, (size_t)(frames - n) * oc * sizeof(float));
|
||||
atomic_fetch_add(&m->underruns, 1);
|
||||
atomic_fetch_add(&m->missing_frames, frames - n);
|
||||
m->ready = 0;
|
||||
}
|
||||
atomic_store_explicit(&m->rd, rd + n, memory_order_release);
|
||||
}
|
||||
|
||||
static void SDLCALL sdl_pull(void *user, SDL_AudioStream *stream, int additional, int total) {
|
||||
(void)total;
|
||||
Mixer *m = user;
|
||||
float tmp[OUT_MAX * 1024];
|
||||
const uint32_t frame_bytes = (uint32_t)m->channels * sizeof(float);
|
||||
atomic_store_explicit(&m->pulled_ns, kern_now_ns(), memory_order_release);
|
||||
while (additional > 0) {
|
||||
uint32_t frames = (uint32_t)additional / frame_bytes;
|
||||
if (!frames) break;
|
||||
if (frames > 1024) frames = 1024;
|
||||
ring_pull(m, tmp, frames);
|
||||
if (!SDL_PutAudioStreamData(stream, tmp, (int)(frames * frame_bytes))) break;
|
||||
additional -= (int)(frames * frame_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
static void output_open(Mixer *m, int rate) {
|
||||
const char *mode = cfg_get("NX2NAVI_AUDIO");
|
||||
if ((mode && !strcmp(mode, "none")) || cfg_get("NX2NAVI_AUDIO_OFF")) {
|
||||
rt_log("audio: NX2NAVI_AUDIO=none: mixing without a device");
|
||||
return;
|
||||
}
|
||||
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
rt_log("audio: SDL audio init failed (%s): mixing without sound", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
SDL_AudioSpec spec = {SDL_AUDIO_F32, m->channels, rate};
|
||||
m->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, sdl_pull, m);
|
||||
if (!m->stream) {
|
||||
rt_log("audio: no output device (%s): mixing without sound", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
if (!SDL_ResumeAudioStreamDevice(m->stream)) {
|
||||
rt_log("audio: starting the output failed (%s): mixing without sound", SDL_GetError());
|
||||
SDL_DestroyAudioStream(m->stream);
|
||||
m->stream = NULL;
|
||||
return;
|
||||
}
|
||||
m->device_ok = 1;
|
||||
SDL_AudioSpec dev = {0};
|
||||
int period = 0;
|
||||
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(m->stream), &dev, &period) || period <= 0) period = m->frame;
|
||||
m->period = period;
|
||||
m->target = m->frame * m->prefill > 2 * period ? m->frame * m->prefill : 2 * period;
|
||||
if (m->target > RING_FRAMES / 2) m->target = RING_FRAMES / 2;
|
||||
uint64_t period_ns = (uint64_t)period * 1000000000ull / (uint64_t)rate;
|
||||
m->stall_ns = 4 * period_ns > STALL_MS * 1000000ull ? 4 * period_ns : STALL_MS * 1000000ull;
|
||||
const char *name = SDL_GetAudioDeviceName(SDL_GetAudioStreamDevice(m->stream));
|
||||
rt_log("audio: SDL device '%s' opened, %d Hz %s float (device %d channels), prefill %d ms, device period %d frames; "
|
||||
"the device paces rendering at %.1f ms queued", name ? name : "?", rate, m->channels == 6 ? "5.1" : "stereo",
|
||||
dev.channels, m->prefill * 5, period, m->target * 1000.0 / rate);
|
||||
}
|
||||
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
|
||||
Mixer *m = r->mixer;
|
||||
if (!m || !m->device_ok || !atomic_load_explicit(&m->playing, memory_order_acquire)) return -1;
|
||||
uint64_t pulled = atomic_load_explicit(&m->pulled_ns, memory_order_acquire);
|
||||
if (!pulled || (now > pulled && now - pulled > m->stall_ns)) return -1;
|
||||
uint64_t have = atomic_load(&m->wr) - atomic_load_explicit(&m->rd, memory_order_acquire);
|
||||
if (have < (uint64_t)m->target) return 0;
|
||||
int64_t ns = (int64_t)((have - (uint64_t)m->target + 1) * 1000000000ull / (uint64_t)m->rate);
|
||||
return ns < 500000 ? 500000 : ns; // until the device has taken it below the target
|
||||
}
|
||||
|
||||
// Render thread only.
|
||||
static void output_push(Mixer *m, const float *out, int frames) {
|
||||
const int oc = m->channels;
|
||||
uint64_t wr = atomic_load(&m->wr), rd = atomic_load_explicit(&m->rd, memory_order_acquire);
|
||||
if (m->device_ok && wr + (uint64_t)frames - rd > RING_FRAMES) { // device stalled: drop this frame
|
||||
atomic_fetch_add(&m->overruns, 1);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < frames; i++) {
|
||||
uint64_t k = (wr + (uint64_t)i) % RING_FRAMES;
|
||||
memcpy(&m->ring[oc * k], &out[oc * i], (size_t)oc * sizeof(float));
|
||||
}
|
||||
atomic_store_explicit(&m->wr, wr + (uint64_t)frames, memory_order_release);
|
||||
if (!m->device_ok) atomic_store(&m->rd, wr + (uint64_t)frames); // no device: the ring only keeps the latest frames
|
||||
}
|
||||
|
||||
// NX2NAVI_AUDIO_DUMP=<path>: the sink mix before the clamp as raw interleaved float32 stereo, and every 5 s one log line with
|
||||
// how many frames were rendered less than 1 ms after the one before (a burst refill). File writes on the render thread:
|
||||
// for diagnosis only.
|
||||
static void sink_dump(const float *raw, int frames) {
|
||||
static FILE *f;
|
||||
static int opened;
|
||||
static uint64_t last, window_start, total, bursts;
|
||||
if (!opened) {
|
||||
opened = 1;
|
||||
const char *path = cfg_get("NX2NAVI_AUDIO_DUMP");
|
||||
if (path && *path) f = fopen(path, "wb");
|
||||
}
|
||||
if (!f) return;
|
||||
fwrite(raw, sizeof(float), (size_t)frames * 2, f);
|
||||
uint64_t now = kern_now_ns();
|
||||
total++;
|
||||
if (last && now - last < 1000000ull) bursts++;
|
||||
last = now;
|
||||
if (!window_start) window_start = now;
|
||||
if (now - window_start >= 5000000000ull) {
|
||||
rt_log("audio: dump: %llu frames in %.1f s, %llu rendered under 1 ms after the previous", (unsigned long long)total,
|
||||
(now - window_start) / 1e9, (unsigned long long)bursts);
|
||||
total = bursts = 0;
|
||||
window_start = now;
|
||||
fflush(f);
|
||||
}
|
||||
}
|
||||
|
||||
// Level meter (NX2NAVI_TRACE=audio): one line per 5 s of output, and the first nonzero frame.
|
||||
static void level_note(AudioRenderer *r, Mixer *m, const float *out, int frames) {
|
||||
if (!(rt_trace & RT_TRACE_AUDIO)) return;
|
||||
int sound = 0;
|
||||
for (int k = 0; k < m->channels * frames; k++) {
|
||||
double x = out[k];
|
||||
m->level_sum += x * x;
|
||||
if (fabs(x) > m->level_peak) m->level_peak = fabs(x);
|
||||
if (x != 0) sound = 1;
|
||||
}
|
||||
m->level_count += (uint64_t)m->channels * (uint64_t)frames;
|
||||
m->level_frames += (uint64_t)frames;
|
||||
if (sound && !m->first_sound_logged) {
|
||||
m->first_sound_logged = 1;
|
||||
rt_log("audio: first nonzero output after %llu frames", (unsigned long long)r->elapsed_frames);
|
||||
}
|
||||
if (m->level_frames >= (uint64_t)r->param.sample_rate * 5) {
|
||||
uint64_t wr = atomic_load(&m->wr), queued = wr - atomic_load(&m->rd);
|
||||
rt_log("audio: level rms %.4f peak %.4f over %.1f s; written %llu frames, underruns %llu, overruns %llu, "
|
||||
"missing frames %llu, callback frames %llu (nonzero %llu), queued %.1f ms, skipped %llu frames",
|
||||
sqrt(m->level_sum / (double)m->level_count), m->level_peak, m->level_frames / (double)r->param.sample_rate,
|
||||
(unsigned long long)wr, (unsigned long long)atomic_load(&m->underruns),
|
||||
(unsigned long long)atomic_load(&m->overruns), (unsigned long long)atomic_load(&m->missing_frames),
|
||||
(unsigned long long)atomic_load(&m->callback_frames), (unsigned long long)atomic_load(&m->callback_nonzero),
|
||||
queued * 1000.0 / r->param.sample_rate, (unsigned long long)atomic_load(&m->skipped_frames));
|
||||
m->level_sum = m->level_peak = 0;
|
||||
m->level_count = m->level_frames = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void audio_mixer_open(AudioRenderer *r) {
|
||||
Mixer *m = calloc(1, sizeof *m);
|
||||
if (!m) rt_fatal("audio mixer allocation");
|
||||
m->frame = r->param.sample_count;
|
||||
m->nbuf = r->param.mix_buffer_count;
|
||||
m->rate = r->param.sample_rate;
|
||||
if (m->frame <= 0 || m->frame > MAX_FRAME || m->nbuf <= 0 || m->nbuf > 512) rt_fatal("audio mixer frame / buffer count");
|
||||
m->buf = calloc((size_t)m->nbuf * (size_t)m->frame, sizeof(float));
|
||||
m->offset = calloc((size_t)r->param.submix_count + 1, sizeof(int));
|
||||
m->nsplit = r->param.splitter_count;
|
||||
m->ndest = r->param.splitter_destination_count;
|
||||
m->split = calloc((size_t)(m->nsplit ? m->nsplit : 1), sizeof *m->split);
|
||||
m->dest = calloc((size_t)(m->ndest ? m->ndest : 1), sizeof *m->dest);
|
||||
if (!m->buf || !m->offset || !m->split || !m->dest) rt_fatal("audio mixer tables");
|
||||
m->effect = calloc((size_t)(r->param.effect_count ? r->param.effect_count : 1), sizeof *m->effect);
|
||||
m->effect_order = calloc((size_t)(r->param.effect_count ? r->param.effect_count : 1), sizeof *m->effect_order);
|
||||
if (!m->effect || !m->effect_order) rt_fatal("audio effect tables");
|
||||
size_t sinks = (size_t)r->param.sink_count;
|
||||
m->sink_address = calloc(sinks, sizeof(uint64_t));
|
||||
m->sink_size = calloc(sinks, sizeof(uint32_t));
|
||||
m->sink_offset = calloc(sinks, sizeof(uint32_t));
|
||||
m->sink_old = calloc(sinks, sizeof(uint32_t));
|
||||
m->sink_written = calloc(sinks, sizeof(uint32_t));
|
||||
if (!m->sink_address || !m->sink_size || !m->sink_offset || !m->sink_old || !m->sink_written)
|
||||
rt_fatal("audio sink state allocation");
|
||||
const char *pe = cfg_get("NX2NAVI_AUDIO_PREFILL_MS");
|
||||
m->prefill = prefill_blocks_for(pe);
|
||||
if (m->prefill < 0) {
|
||||
rt_log("audio: NX2NAVI_AUDIO_PREFILL_MS=%s is not 0..500 ms: using %d", pe, PREFILL_DEFAULT_MS);
|
||||
m->prefill = PREFILL_DEFAULT_MS / 5;
|
||||
}
|
||||
m->channels = output_channels();
|
||||
r->mixer = m;
|
||||
atomic_store(&m->playing, r->state == 0);
|
||||
output_open(m, r->param.sample_rate);
|
||||
}
|
||||
|
||||
void audio_mixer_set_running(AudioRenderer *r, int running) {
|
||||
Mixer *m = r->mixer;
|
||||
if (!m) return;
|
||||
if (!running) {
|
||||
atomic_store_explicit(&m->flush_until, atomic_load_explicit(&m->wr, memory_order_acquire), memory_order_release);
|
||||
atomic_fetch_add_explicit(&m->stop_epoch, 1, memory_order_release);
|
||||
}
|
||||
atomic_store_explicit(&m->playing, running, memory_order_release);
|
||||
}
|
||||
|
||||
void audio_mixer_close(AudioRenderer *r) {
|
||||
Mixer *m = r->mixer;
|
||||
if (!m) return;
|
||||
if (m->stream) { // destroying the stream closes the device and waits for the callback to finish
|
||||
SDL_DestroyAudioStream(m->stream);
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
}
|
||||
for (int i = 0; i < m->nsplit; i++) free(m->split[i].ids);
|
||||
for (int i = 0; i < r->param.effect_count; i++) {
|
||||
audio_reverb_free(m->effect[i].reverb);
|
||||
audio_effect_free(m->effect[i].dsp);
|
||||
free(m->effect[i].delay);
|
||||
}
|
||||
free(m->sink_address);
|
||||
free(m->sink_size);
|
||||
free(m->sink_offset);
|
||||
free(m->sink_old);
|
||||
free(m->sink_written);
|
||||
free(m->effect);
|
||||
free(m->effect_order);
|
||||
free(m->buf);
|
||||
free(m->offset);
|
||||
free(m->split);
|
||||
free(m->dest);
|
||||
free(m);
|
||||
r->mixer = NULL;
|
||||
}
|
||||
|
||||
/* ---------- routing input: splitters and mixes ---------- */
|
||||
static void splitters_update(AudioRenderer *r, const uint8_t *p, uint32_t n) {
|
||||
Mixer *m = r->mixer;
|
||||
if (!n) return;
|
||||
uint32_t hdr[3];
|
||||
if (n < 0x20) rt_fatal("audio splitter section header");
|
||||
memcpy(hdr, p, 12);
|
||||
if (hdr[0] != 0x48444e53u) rt_fatal("audio splitter header magic (SNDH)");
|
||||
uint32_t ni = hdr[1], nd = hdr[2], at = 0x20;
|
||||
for (uint32_t i = 0; i < ni; i++) {
|
||||
uint32_t h[4]; if (at + 16 > n) rt_fatal("audio splitter info overruns section");
|
||||
memcpy(h, p + at, 16); at += 16;
|
||||
if (h[0] != 0x49444e53u || h[1] >= (uint32_t)m->nsplit || h[3] > (uint32_t)m->ndest || at + 4u * h[3] > n) {
|
||||
rt_log("audio: splitter info %u at %#x: %08x %08x %08x %08x (section %u bytes, infos %u, destinations %u)", i, at - 16, h[0], h[1], h[2], h[3], n, ni, nd);
|
||||
rt_fatal("audio splitter info (SNDI)");
|
||||
}
|
||||
SplitInfo *s = &m->split[h[1]]; free(s->ids);
|
||||
s->ndest = (int32_t)h[3]; s->ids = calloc(h[3] ? h[3] : 1, sizeof(int32_t));
|
||||
if (!s->ids) rt_fatal("audio splitter destinations");
|
||||
memcpy(s->ids, p + at, 4u * h[3]); at += 4u * h[3] + 12; /* 12 reserved bytes follow the ids (measured, NX2NAVI REV11) */
|
||||
if (at > n) rt_fatal("audio splitter info padding overruns section");
|
||||
for (uint32_t k = 0; k < h[3]; k++) if (s->ids[k] < 0 || s->ids[k] >= m->ndest) rt_fatal("audio splitter destination id");
|
||||
}
|
||||
for (uint32_t i = 0; i < nd; i++) {
|
||||
unsigned rev=(r->revision>>24)-'0',stride=rev>=15?0xa8:rev>=12?0x90:0x70;
|
||||
uint8_t e[0xa8]={0}; if (at>n||stride>n-at) rt_fatal("audio splitter destination overruns section");
|
||||
memcpy(e, p + at, stride); at += stride;
|
||||
uint32_t magic, id; int32_t dst; memcpy(&magic, e, 4); memcpy(&id, e + 4, 4); memcpy(&dst, e + 0x68, 4);
|
||||
if (magic != 0x44444e53u || id >= (uint32_t)m->ndest) rt_fatal("audio splitter destination (SNDD)");
|
||||
SplitDest *d = &m->dest[id]; memcpy(d->vol, e + 8, sizeof d->vol); d->dest = dst;
|
||||
d->used=e[rev>=15?0x9c:rev>=12?0x84:0x6c];
|
||||
for(unsigned f=0;f<2;f++){
|
||||
AudrenBiquadFloat b={0};
|
||||
if(rev>=15)memcpy(&b,e+0x6c+f*sizeof b,sizeof b);
|
||||
else if(rev>=12){AudrenBiquad q;memcpy(&q,e+0x6c+f*sizeof q,sizeof q);b.enable=q.enable;for(unsigned c=0;c<3;c++)b.numerator[c]=q.numerator[c]/16384.0f;for(unsigned c=0;c<2;c++)b.denominator[c]=q.denominator[c]/16384.0f;}
|
||||
if(b.enable){for(unsigned c=0;c<3;c++)if(!isfinite(b.numerator[c]))rt_fatal("audio splitter nonfinite numerator");for(unsigned c=0;c<2;c++)if(!isfinite(b.denominator[c]))rt_fatal("audio splitter nonfinite denominator");}
|
||||
if(!b.enable||!d->biquads[f].enable||!d->used)memset(d->state[f],0,sizeof d->state[f]);
|
||||
d->biquads[f]=b;
|
||||
}
|
||||
}
|
||||
if (at <= n && n - at < 16) at = n; /* the section is padded to 16 bytes; the padding is not cleared by the game (measured) */
|
||||
if (at != n) { rt_log("audio: splitter section %u bytes, parsed %u (infos %u, destinations %u)", n, at, ni, nd); rt_fatal("audio splitter section size"); }
|
||||
}
|
||||
void audio_mixer_update_routing(AudioRenderer *r, const uint8_t *splitters, uint32_t splitter_bytes, const uint8_t *mixes, uint32_t mix_bytes, int mix_count) {
|
||||
splitters_update(r, splitters, splitter_bytes);
|
||||
if (mix_bytes == (uint32_t)mix_count * sizeof(AudrenMixIn)) { memcpy(r->mixes, mixes, mix_bytes); return; }
|
||||
/* MixInParameterDirtyOnly: 0x20-byte header {u32 magic 0xCAFEBABE, u32 count, pad} followed by the dirty mixes */
|
||||
if (mix_bytes >= 0x20 && (mix_bytes - 0x20) % sizeof(AudrenMixIn) == 0) {
|
||||
uint32_t magic, n; memcpy(&magic, mixes, 4); memcpy(&n, mixes + 4, 4);
|
||||
if (magic != 0xcafebabeu || n * sizeof(AudrenMixIn) != mix_bytes - 0x20) rt_fatal("audio dirty mix header");
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
AudrenMixIn m; memcpy(&m, mixes + 0x20 + i * sizeof m, sizeof m);
|
||||
if (m.mix_id < 0 || m.mix_id >= mix_count) rt_fatal("audio mix id");
|
||||
r->mixes[m.mix_id] = m;
|
||||
}
|
||||
return;
|
||||
}
|
||||
rt_log("audio: mix section %u bytes for %d mixes", mix_bytes, mix_count);
|
||||
rt_fatal("audio mix section size");
|
||||
}
|
||||
|
||||
/* Effect parameters arrive once per IPC update, while the DSP may process multiple frames. */
|
||||
void audio_mixer_update_effects(AudioRenderer *r) {
|
||||
Mixer *m = r->mixer;
|
||||
for (int i=0;i<r->param.effect_count;i++) {
|
||||
const AudrenEffectIn *e=&r->effects[i]; EffectState *s=&m->effect[i];
|
||||
unsigned revision=audio_revision(r);
|
||||
int reset=e->is_new || s->type!=e->type;
|
||||
if(reset){audio_effect_free(s->dsp);s->dsp=NULL;memset(s->biquad_z,0,sizeof s->biquad_z);}
|
||||
if(e->type==3 || e->type==4 || e->type==9) {
|
||||
unsigned status,channels;int dimensions_changed=0;
|
||||
if(e->type==3){AudrenDelayParam p,old;memcpy(&p,e->specific,sizeof p);memcpy(&old,s->wire,sizeof old);status=p.status;channels=p.channels;dimensions_changed=p.channels!=old.channels || p.sample_rate!=old.sample_rate || p.delay_max!=old.delay_max;}
|
||||
else if(e->type==4){AudrenReverbParam p,old;memcpy(&p,e->specific,sizeof p);memcpy(&old,s->wire,sizeof old);status=p.status;channels=p.channels;dimensions_changed=p.channels!=old.channels || p.sample_rate!=old.sample_rate;}
|
||||
else {AudrenCompressorParam p,old;memcpy(&p,e->specific,sizeof p);memcpy(&old,s->wire,sizeof old);status=p.status;channels=p.channels;dimensions_changed=p.channels!=old.channels;}
|
||||
if(status==0 || dimensions_changed){audio_effect_free(s->dsp);s->dsp=NULL;}
|
||||
if(e->enabled && channels){if(!s->dsp)s->dsp=audio_effect_create(e->type,e->specific,revision);else audio_effect_update(s->dsp,e->specific);}
|
||||
} else if(s->dsp){audio_effect_free(s->dsp);s->dsp=NULL;}
|
||||
if(e->type==6){
|
||||
unsigned status;
|
||||
if(revision>=15){AudrenBiquadEffectV2 p;memcpy(&p,e->specific,sizeof p);s->biquad_channels=p.channels;status=p.status;memcpy(s->numerator,p.numerator,sizeof p.numerator);memcpy(s->denominator,p.denominator,sizeof p.denominator);}
|
||||
else {AudrenBiquadEffectV1 p;memcpy(&p,e->specific,sizeof p);s->biquad_channels=p.channels;status=p.status;for(int k=0;k<3;k++)s->numerator[k]=p.numerator[k]/16384.f;for(int k=0;k<2;k++)s->denominator[k]=p.denominator[k]/16384.f;}
|
||||
if(status==0 || (status==1 && revision<6))memset(s->biquad_z,0,sizeof s->biquad_z);
|
||||
if(e->enabled && (s->biquad_channels<1 || s->biquad_channels>6))rt_fatal("biquad effect channel count");
|
||||
}
|
||||
s->type=e->type;memcpy(s->wire,e->specific,sizeof s->wire);
|
||||
if(e->type!=5 || e->is_new) { audio_reverb_free(s->reverb); s->reverb=NULL; s->pending=1; }
|
||||
if(e->type==7) {
|
||||
AudrenLimiterParam p; memcpy(&p,e->specific,sizeof p);
|
||||
if(e->is_new || !s->delay || p.status==0 || p.channels!=s->channels || p.delay_max!=s->delay_max) {
|
||||
free(s->delay);s->delay=NULL;s->delay_max=p.delay_max;s->channels=p.channels;
|
||||
memset(s->detector,0,sizeof s->detector);memset(s->position,0,sizeof s->position);
|
||||
for(int c=0;c<6;c++)s->compression[c]=s->minimum[c]=1;
|
||||
memset(s->peak,0,sizeof s->peak);
|
||||
if(e->enabled) {
|
||||
if(p.channels<1 || p.channels>6 || p.delay_max<1 || p.delay_max>192000)rt_fatal("limiter delay dimensions");
|
||||
s->delay=calloc((size_t)p.channels*p.delay_max,sizeof(float));if(!s->delay)rt_fatal("limiter delay allocation");
|
||||
}
|
||||
}
|
||||
if(p.statistics_reset) { memset(s->peak,0,sizeof s->peak); for(int c=0;c<6;c++)s->minimum[c]=1; }
|
||||
} else if(s->delay) { free(s->delay);s->delay=NULL; }
|
||||
if(e->type!=5) continue;
|
||||
Reverb3dParam p; memcpy(&p,e->specific,sizeof p);
|
||||
if(s->reverb && p.sample_rate!=s->param.sample_rate) { audio_reverb_free(s->reverb); s->reverb=NULL; }
|
||||
if(memcmp(&p,&s->param,72) || p.status<2) s->pending=1;
|
||||
s->param=p;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- voice decoding along the voice clock's sample path ---------- */
|
||||
typedef struct { ServerVoice *v; unsigned index; uint64_t offset; int32_t loops; uint8_t valid[4]; int ended; int16_t h1, h2; int32_t abuf; uint64_t aat; } Walk;
|
||||
static int fmt_bytes(int f) { return f == FMT_PCM8 ? 1 : f == FMT_PCM16 ? 2 : f==FMT_PCM24?3:f == FMT_PCM32 || f == FMT_PCMF32 ? 4 : 0; }
|
||||
/* DSP-ADPCM: 8-byte frames of a predictor/scale byte and 14 4-bit samples; the history runs across samples. */
|
||||
static int16_t adpcm_next(Walk *w, const AudrenWaveBuffer *b, const int16_t *coef, uint64_t i) {
|
||||
const uint8_t *d = (const uint8_t *)(uintptr_t)b->address;
|
||||
uint64_t frame = i / 14, nib = i % 14;
|
||||
if ((frame + 1) * 8 > b->size) return 0;
|
||||
uint8_t ps = d[frame * 8]; int scale = 1 << (ps & 15), pred = (ps >> 4) & 7;
|
||||
int n = (d[frame * 8 + 1 + nib / 2] >> ((nib & 1) ? 0 : 4)) & 15; if (n >= 8) n -= 16;
|
||||
int64_t s = (int64_t)n * scale * 2048 + 1024 + (int64_t)coef[pred * 2] * w->h1 + (int64_t)coef[pred * 2 + 1] * w->h2;
|
||||
s >>= 11; if (s > 32767) s = 32767; if (s < -32768) s = -32768;
|
||||
w->h2 = w->h1; w->h1 = (int16_t)s; return (int16_t)s;
|
||||
}
|
||||
static float sample_at(Walk *w, int ch) {
|
||||
if (w->ended || !w->valid[w->index]) return 0;
|
||||
ServerVoice *v = w->v; const AudrenWaveBuffer *b = &v->buffers[w->index];
|
||||
uint64_t i = (uint64_t)b->start + w->offset; int f = v->in.sample_format, cc = (int)v->in.channel_count;
|
||||
if (f == FMT_ADPCM) {
|
||||
if (cc != 1 || !v->in.extra_address || v->in.extra_size < 32) return 0;
|
||||
const int16_t *coef = (const int16_t *)(uintptr_t)v->in.extra_address;
|
||||
if (w->abuf != (int32_t)w->index || w->aat > i + 1) { /* (re)start this buffer: from its context, else from the frame boundary with carried history */
|
||||
w->abuf = (int32_t)w->index;
|
||||
if (b->context_address && b->context_size >= 6) { const int16_t *c = (const int16_t *)(uintptr_t)b->context_address; w->h1 = c[1]; w->h2 = c[2]; w->aat = (uint64_t)b->start; }
|
||||
else w->aat = (uint64_t)b->start - (uint64_t)b->start % 14;
|
||||
if (i < w->aat) w->aat = i;
|
||||
}
|
||||
while (w->aat <= i) adpcm_next(w, b, coef, w->aat++);
|
||||
return w->h1 / 32768.0f;
|
||||
}
|
||||
int bytes = fmt_bytes(f); if (!bytes || ch >= cc) return 0;
|
||||
uint64_t at = (i * (uint64_t)cc + (uint64_t)ch) * (uint64_t)bytes;
|
||||
if (at + (uint64_t)bytes > b->size) return 0;
|
||||
const uint8_t *p = (const uint8_t *)(uintptr_t)b->address + at;
|
||||
switch (f) {
|
||||
case FMT_PCM8: return (int8_t)p[0] / 128.0f;
|
||||
case FMT_PCM16: { int16_t s; memcpy(&s, p, 2); return s / 32768.0f; }
|
||||
case FMT_PCM24: {uint32_t word=(uint32_t)p[0]|(uint32_t)p[1]<<8|(uint32_t)p[2]<<16;return ((int32_t)(word<<8)>>8)/8388608.f;}
|
||||
case FMT_PCM32: { int32_t s; memcpy(&s, p, 4); return (float)(s / 2147483648.0); }
|
||||
default: { float s; memcpy(&s, p, 4); return s; }
|
||||
}
|
||||
}
|
||||
static void walk_skip_empty(Walk *w) { /* the same as skip_empty in audio_renderer.c: no samples, played on arrival */
|
||||
while (!w->ended && w->valid[w->index] && audio_buffer_empty(&w->v->buffers[w->index])) {
|
||||
const AudrenWaveBuffer *b = &w->v->buffers[w->index];
|
||||
if (b->loop && b->loop_count < 0) break;
|
||||
w->valid[w->index] = 0; w->offset = 0; w->loops = 0; w->index = (w->index + 1) & 3;
|
||||
if (b->end_of_stream) w->ended = 1;
|
||||
}
|
||||
}
|
||||
static void walk_step(Walk *w) { /* the same transitions as advance_voice in audio_renderer.c */
|
||||
if (w->ended || !w->valid[w->index]) { w->ended = 1; return; }
|
||||
const AudrenWaveBuffer *b = &w->v->buffers[w->index];
|
||||
uint64_t length = (uint64_t)(b->end - b->start);
|
||||
if (++w->offset < length) return;
|
||||
if (b->loop && (b->loop_count < 0 || w->loops < b->loop_count)) { w->loops++; w->offset = b->loop_start > b->start && b->loop_start < b->end ? (uint64_t)(b->loop_start - b->start) : 0; return; }
|
||||
w->valid[w->index] = 0; w->offset = 0; w->loops = 0; w->index = (w->index + 1) & 3;
|
||||
if (b->end_of_stream) w->ended = 1;
|
||||
walk_skip_empty(w);
|
||||
}
|
||||
/* Decodes `frames` output samples per channel into out[c][k]; the renderer advances the voice afterwards by the same amount. */
|
||||
static double voice_step(const AudioRenderer *r, const ServerVoice *v) {
|
||||
return v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
|
||||
}
|
||||
/* Source samples the voice consumes over `frames` output samples, counted with the exact accumulation voice_render steps by
|
||||
(float rounding makes `fraction + seconds * rate * pitch` differ from it by one sample on some frames, a seam click);
|
||||
leaves the fractional position in v->fraction. */
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames) {
|
||||
const double step = voice_step(r, v);
|
||||
double frac = v->fraction;
|
||||
uint64_t consumed = 0;
|
||||
for (int k = 0; k < frames; k++) {
|
||||
frac += step;
|
||||
while (frac >= 1.0) { frac -= 1.0; consumed++; }
|
||||
}
|
||||
v->fraction = frac;
|
||||
return consumed;
|
||||
}
|
||||
static void voice_render(AudioRenderer *r, ServerVoice *v, float out[6][MAX_FRAME], int frames) {
|
||||
const double step = voice_step(r, v);
|
||||
Walk w = {v, v->index, v->offset, v->loops_done, {0}, 0, v->adpcm.history[0], v->adpcm.history[1], v->adpcm.buffer, v->adpcm.at};
|
||||
memcpy(w.valid, v->valid, sizeof w.valid);
|
||||
walk_skip_empty(&w);
|
||||
int cc = (int)v->in.channel_count; double frac = v->fraction;
|
||||
for (int k = 0; k < frames; k++) {
|
||||
Walk next = w; walk_step(&next);
|
||||
for (int c = 0; c < cc; c++) { float s0 = sample_at(&w, c), s1 = sample_at(&next, c); out[c][k] = (float)(s0 + (s1 - s0) * frac); }
|
||||
frac += step;
|
||||
while (frac >= 1.0) { frac -= 1.0; walk_step(&w); }
|
||||
}
|
||||
v->adpcm.history[0] = w.h1; v->adpcm.history[1] = w.h2; v->adpcm.buffer = w.abuf; v->adpcm.at = w.aat;
|
||||
}
|
||||
|
||||
/* ---------- mixing ---------- */
|
||||
static float *mixbuf(Mixer *m, int i) { return m->buf + (size_t)i * (size_t)m->frame; }
|
||||
static void accumulate(Mixer *m, int buffer, const float *src, float vol, int frames) {
|
||||
if (vol == 0 || buffer < 0 || buffer >= m->nbuf) return;
|
||||
float *d = mixbuf(m, buffer); for (int k = 0; k < frames; k++) d[k] += src[k] * vol;
|
||||
}
|
||||
static int mix_valid(AudioRenderer *r, int32_t id) { return id >= 0 && id <= r->param.submix_count && r->mixes[id].in_use; }
|
||||
static int mix_depth(AudioRenderer *r, int id) {
|
||||
int d = 0;
|
||||
for (int guard = 0; id > 0 && guard < 64; guard++, d++) {
|
||||
if (!mix_valid(r, r->mixes[id].dest_mix_id)) return d + 1;
|
||||
id = r->mixes[id].dest_mix_id;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/* Guest custom effects exchange int32 samples in two rings. Each ring has CPU and DSP
|
||||
0x40-byte headers followed by samples; only the DSP header's owned cursor is advanced. */
|
||||
static void mix_aux(AudioRenderer *r, int mix, const AudrenEffectIn *e) {
|
||||
Mixer *m=r->mixer; AudrenAuxParam p; memcpy(&p,e->specific,sizeof p);
|
||||
if(!p.send_info || !p.return_info || !p.capacity || !p.channels) return;
|
||||
if(p.channels>24 || p.capacity>16*1024*1024)rt_fatal("auxiliary effect dimensions");
|
||||
uint8_t *send=ipc_guest(p.send_info,128+(uint64_t)p.capacity*4), *ret=ipc_guest(p.return_info,128+(uint64_t)p.capacity*4);
|
||||
uint32_t *sh=(uint32_t *)(send+64),*rh=(uint32_t *)(ret+64); int32_t *sd=(int32_t *)(send+128),*rd=(int32_t *)(ret+128);
|
||||
uint32_t wr=sh[1],read=rh[0],total=p.channels*(uint32_t)m->frame;
|
||||
if(e->enabled) report_once(m,6,"auxiliary effect in use, channels",(int)p.channels);
|
||||
for(unsigned c=0;c<p.channels;c++) {
|
||||
if(p.input[c]>=r->mixes[mix].buffer_count || p.output[c]>=r->mixes[mix].buffer_count)rt_fatal("auxiliary effect mix buffer");
|
||||
float *input=mixbuf(m,m->offset[mix]+p.input[c]),*output=mixbuf(m,m->offset[mix]+p.output[c]);
|
||||
if(!e->enabled) { if(input!=output)memmove(output,input,m->frame*sizeof(float)); continue; }
|
||||
uint64_t w=(uint64_t)wr+c*m->frame,at=(uint64_t)read+c*m->frame;
|
||||
for(int k=0;k<m->frame;k++) {
|
||||
double scaled=trunc((double)input[k]*32768); int32_t value=scaled>INT32_MAX?INT32_MAX:scaled<INT32_MIN?INT32_MIN:!isfinite(scaled)?0:(int32_t)scaled;
|
||||
input[k]=value/32768.f;
|
||||
if(w<=p.capacity)sd[(w+(unsigned)k)%p.capacity]=value;
|
||||
}
|
||||
for(int k=0;k<m->frame;k++)output[k]=at<=p.capacity?rd[(at+(unsigned)k)%p.capacity]/32768.f:0;
|
||||
if(c==p.channels-1) { if(w<=p.capacity)sh[1]=(wr+total)%p.capacity; if(at<=p.capacity)rh[0]=(read+total)%p.capacity; }
|
||||
}
|
||||
if(!e->enabled) { sh[0]=sh[1]=sh[3]=0; rh[0]=rh[1]=rh[3]=0; }
|
||||
}
|
||||
|
||||
void audio_mixer_effect_result(AudioRenderer *r, int id, void *output, unsigned size) {
|
||||
Mixer *m=r->mixer;
|
||||
if(size>=48 && r->effects[id].type==7) { memcpy(output,m->effect[id].peak,24);memcpy((uint8_t *)output+24,m->effect[id].minimum,24); }
|
||||
if(r->effects[id].type==9)audio_effect_statistics(m->effect[id].dsp,output,size);
|
||||
}
|
||||
static void mix_limiter(AudioRenderer *r, int mix, int id) {
|
||||
Mixer *m=r->mixer; EffectState *s=&m->effect[id]; const AudrenEffectIn *e=&r->effects[id];
|
||||
AudrenLimiterParam p;memcpy(&p,e->specific,sizeof p);
|
||||
if(p.channels<1 || p.channels>6) { if(e->enabled)rt_fatal("limiter channel count");return; }
|
||||
if(e->enabled && (!s->delay || p.delay_min<1 || p.delay_min>p.delay_max || p.attack<0 || p.attack>1 || p.release<0 || p.release>1))rt_fatal("limiter parameters");
|
||||
for(int c=0;c<p.channels;c++) {
|
||||
if(p.input[c]>=r->mixes[mix].buffer_count || p.output[c]>=r->mixes[mix].buffer_count)rt_fatal("limiter mix buffer");
|
||||
float *input=mixbuf(m,m->offset[mix]+p.input[c]),*output=mixbuf(m,m->offset[mix]+p.output[c]);
|
||||
if(!e->enabled) { if(input!=output)memmove(output,input,m->frame*sizeof(float));continue; }
|
||||
for(int k=0;k<m->frame;k++) {
|
||||
float x=(input[k]*(32768.f/32767.f))*p.input_gain,level=fabsf(x);
|
||||
float coeff=level>s->detector[c]?p.attack:p.release;s->detector[c]+=coeff*(level-s->detector[c]);
|
||||
float attenuation=s->detector[c]>p.threshold?p.threshold/s->detector[c]:1;
|
||||
coeff=s->compression[c]>attenuation?p.attack:p.release;s->compression[c]+=coeff*(attenuation-s->compression[c]);
|
||||
int slot=c*p.delay_max+s->position[c];output[k]=s->delay[slot]*s->compression[c]*p.output_gain*(32767.f/32768.f);
|
||||
s->delay[slot]=x;s->position[c]=(s->position[c]+1)%p.delay_min;
|
||||
if(p.statistics_enabled) { s->peak[c]=fmaxf(s->peak[c],level);s->minimum[c]=fminf(s->minimum[c],s->compression[c]); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t effect_integer(float value) {
|
||||
double x=trunc((double)value*32768);return !isfinite(x)?0:x>INT32_MAX?INT32_MAX:x<INT32_MIN?INT32_MIN:(int32_t)x;
|
||||
}
|
||||
static void prepare_ring_write(uint8_t *base,uint32_t size,uint32_t at,uint64_t bytes) {
|
||||
if(bytes>=size){dm_guest_write_prepare(base,size);return;}
|
||||
uint32_t first=(uint32_t)bytes;if(first>size-at)first=size-at;
|
||||
dm_guest_write_prepare(base+at,first);if(bytes>first)dm_guest_write_prepare(base,(size_t)bytes-first);
|
||||
}
|
||||
/* SDK SampleCounter::Add advances the producer counter itself, not the
|
||||
producer-consumer difference or the ring cursor. Saturate on diff overflow. */
|
||||
static void effect_counter_add(uint32_t cpu,uint32_t *dsp,uint32_t count) {
|
||||
uint32_t previous=*dsp-cpu,next=*dsp+count;
|
||||
*dsp=next-cpu<previous?cpu-1:next;
|
||||
}
|
||||
static void mix_capture(AudioRenderer *r,int mix,const AudrenEffectIn *e) {
|
||||
Mixer *m=r->mixer;AudrenAuxParam p;memcpy(&p,e->specific,sizeof p);
|
||||
if(!p.send_info || !p.capacity || !p.channels)return;
|
||||
if(p.channels>24 || p.capacity>16*1024*1024)rt_fatal("capture effect dimensions");
|
||||
uint8_t *base=ipc_guest(p.send_info,128+(uint64_t)p.capacity*4);uint32_t *cpu=(uint32_t *)base,*dsp=(uint32_t *)(base+64);int32_t *data=(int32_t *)(base+128);
|
||||
dm_guest_write_prepare(base+64,64);
|
||||
if(!e->enabled){dsp[0]=dsp[1]=dsp[3]=0;return;}
|
||||
uint32_t wr=dsp[1],total=p.channels*(uint32_t)m->frame;
|
||||
for(unsigned c=0;c<p.channels;c++){
|
||||
if(p.input[c]>=r->mixes[mix].buffer_count)rt_fatal("capture effect input buffer");
|
||||
float *in=mixbuf(m,m->offset[mix]+p.input[c]);uint64_t at=(uint64_t)wr+c*m->frame;
|
||||
if(at<=p.capacity)prepare_ring_write((uint8_t *)data,p.capacity*4,(uint32_t)(at%p.capacity)*4,(uint64_t)m->frame*4);
|
||||
for(int k=0;k<m->frame;k++){int32_t value=effect_integer(in[k]);in[k]=value/32768.f;if(at<=p.capacity)data[(at+(unsigned)k)%p.capacity]=value;}
|
||||
}
|
||||
if((uint64_t)wr+(p.channels-1)*(uint32_t)m->frame<=p.capacity){
|
||||
uint32_t diff=dsp[3]-cpu[3];
|
||||
if(diff>=p.capacity)effect_counter_add(cpu[2],&dsp[2],total);
|
||||
dsp[1]=(wr+total)%p.capacity;effect_counter_add(cpu[3],&dsp[3],total);
|
||||
}
|
||||
}
|
||||
static void mix_simple_effect(AudioRenderer *r,int mix,int id) {
|
||||
Mixer *m=r->mixer;const AudrenEffectIn *e=&r->effects[id];EffectState *s=&m->effect[id];
|
||||
if(e->type==1){
|
||||
AudrenBufferMixParam p;memcpy(&p,e->specific,sizeof p);if(!e->enabled)return;
|
||||
if(p.count>24)rt_fatal("buffer-mix effect count");
|
||||
for(unsigned c=0;c<p.count;c++){
|
||||
if(p.input[c]>=r->mixes[mix].buffer_count || p.output[c]>=r->mixes[mix].buffer_count)rt_fatal("buffer-mix effect index");
|
||||
float *in=mixbuf(m,m->offset[mix]+p.input[c]),*out=mixbuf(m,m->offset[mix]+p.output[c]);
|
||||
for(int k=0;k<m->frame;k++)out[k]+=ceilf(in[k]*32768*p.volume[c])/32768;
|
||||
}return;
|
||||
}
|
||||
unsigned channels;if(e->type==6)channels=s->biquad_channels;else {uint16_t n;memcpy(&n,e->specific+14,2);channels=n;}
|
||||
if(channels<1 || channels>6){if(e->enabled)rt_fatal("audio DSP effect channels");return;}
|
||||
float *in[6]={0},*out[6]={0};unsigned revision=audio_revision(r);
|
||||
for(unsigned c=0;c<channels;c++){
|
||||
unsigned idx=c;
|
||||
if(channels==6 && ((e->type==3 && revision<11)||(e->type==4 && revision>=11))){static const unsigned map[6]={0,1,4,5,2,3};idx=map[c];}
|
||||
if(e->specific[idx]>=r->mixes[mix].buffer_count || e->specific[6+idx]>=r->mixes[mix].buffer_count)rt_fatal("audio DSP effect buffer index");
|
||||
in[c]=mixbuf(m,m->offset[mix]+e->specific[idx]);out[c]=mixbuf(m,m->offset[mix]+e->specific[6+idx]);
|
||||
}
|
||||
if(!e->enabled){for(unsigned c=0;c<channels;c++)if(in[c]!=out[c])memmove(out[c],in[c],(size_t)m->frame*sizeof(float));return;}
|
||||
if(e->type==6){
|
||||
for(unsigned c=0;c<channels;c++)for(int k=0;k<m->frame;k++){
|
||||
float x=in[c][k],y=s->numerator[0]*x+s->biquad_z[c][0];
|
||||
s->biquad_z[c][0]=s->numerator[1]*x+s->denominator[0]*y+s->biquad_z[c][1];s->biquad_z[c][1]=s->numerator[2]*x+s->denominator[1]*y;out[c][k]=y;
|
||||
}
|
||||
} else {if(!s->dsp)rt_fatal("audio DSP effect state missing");audio_effect_process(s->dsp,in,out,m->frame);}
|
||||
}
|
||||
static void mix_effects(AudioRenderer *r, int mix) {
|
||||
Mixer *m=r->mixer; int count=0;
|
||||
for(int i=0;i<r->param.effect_count;i++) if(r->effects[i].type && r->effects[i].mix_id==mix) {
|
||||
int j=count++; while(j>0 && r->effects[m->effect_order[j-1]].order>r->effects[i].order) { m->effect_order[j]=m->effect_order[j-1]; j--; } m->effect_order[j]=i;
|
||||
}
|
||||
for(int n=0;n<count;n++) {
|
||||
int id=m->effect_order[n]; const AudrenEffectIn *e=&r->effects[id]; EffectState *s=&m->effect[id];
|
||||
if(e->type==1 || e->type==3 || e->type==4 || e->type==6 || e->type==9){mix_simple_effect(r,mix,id);continue;}
|
||||
if(e->type==2) { mix_aux(r,mix,e); continue; }
|
||||
if(e->type==8) { mix_capture(r,mix,e);continue; }
|
||||
if(e->type==7) { mix_limiter(r,mix,id); continue; }
|
||||
if(e->type!=5) {
|
||||
if(e->enabled && e->type<32 && !(m->effect_reported&(1u<<e->type))) { m->effect_reported|=1u<<e->type; rt_log("audio: effect type %u not implemented",e->type); }
|
||||
continue;
|
||||
}
|
||||
Reverb3dParam *p=&s->param; float *input[6]={0},*output[6]={0};
|
||||
if(p->channels!=1 && p->channels!=2 && p->channels!=4 && p->channels!=6) { if(e->enabled)rt_fatal("audio reverb channel count"); continue; }
|
||||
for(int c=0;c<p->channels;c++) {
|
||||
int idx=c; /* REV11+ channel resources use L,R,C,LFE,BL,BR; the reference DSP uses legacy order. */
|
||||
if(p->channels==6 && (r->revision>>24)>='0'+11) { static const int map[6]={0,1,4,5,2,3}; idx=map[c]; }
|
||||
if(p->input[idx]>=r->mixes[mix].buffer_count || p->output[idx]>=r->mixes[mix].buffer_count)rt_fatal("audio reverb buffer index");
|
||||
input[c]=mixbuf(m,m->offset[mix]+p->input[idx]); output[c]=mixbuf(m,m->offset[mix]+p->output[idx]);
|
||||
}
|
||||
if(!e->enabled) { for(int c=0;c<p->channels;c++)if(input[c]!=output[c])memmove(output[c],input[c],m->frame*sizeof(float)); continue; }
|
||||
if(!s->reverb)s->reverb=audio_reverb_create(p); else if(s->pending)audio_reverb_update(s->reverb,p);
|
||||
s->pending=0; audio_reverb_process(s->reverb,input,output,m->frame);
|
||||
}
|
||||
}
|
||||
|
||||
static const float *splitter_filter(SplitDest *d,const float *input,float *output,int frames){
|
||||
if(!d->biquads[0].enable&&!d->biquads[1].enable)return input;
|
||||
memcpy(output,input,(size_t)frames*sizeof *output);
|
||||
for(unsigned f=0;f<2;f++)if(d->biquads[f].enable){
|
||||
const AudrenBiquadFloat *b=&d->biquads[f];float *z=d->state[f];
|
||||
for(int k=0;k<frames;k++){
|
||||
float x=output[k],y=b->numerator[0]*x+z[0];
|
||||
z[0]=b->numerator[1]*x+b->denominator[0]*y+z[1];
|
||||
z[1]=b->numerator[2]*x+b->denominator[1]*y;output[k]=y;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
typedef struct {
|
||||
uint64_t address;uint32_t size,channels,samples,last_read;
|
||||
uint8_t format,reserved[3],input[6],padding[2];
|
||||
} CircularSinkParam;
|
||||
_Static_assert(__builtin_offsetof(CircularSinkParam,input)==28,"circular sink wire layout");
|
||||
static void mix_circular_sink(AudioRenderer *r,int id) {
|
||||
Mixer *m=r->mixer;CircularSinkParam p;memcpy(&p,r->sinks[id].name,sizeof p);
|
||||
if(!p.address || !p.size)return;
|
||||
if(p.channels<1 || p.channels>6 || p.format!=FMT_PCM16 || p.size>256*1024*1024 || p.size%2)rt_fatal("circular audio sink parameters");
|
||||
if(m->sink_address[id]!=p.address || m->sink_size[id]!=p.size){m->sink_address[id]=p.address;m->sink_size[id]=p.size;m->sink_offset[id]=m->sink_old[id]=m->sink_written[id]=0;}
|
||||
uint8_t *data=ipc_guest(p.address,p.size);uint32_t at=m->sink_offset[id];
|
||||
prepare_ring_write(data,p.size,at,(uint64_t)p.channels*m->frame*2);
|
||||
float gain=r->mixes[0].in_use?r->mixes[0].volume:1;
|
||||
for(unsigned c=0;c<p.channels;c++){
|
||||
if(p.input[c]>=m->nbuf)rt_fatal("circular audio sink input index");
|
||||
float *in=mixbuf(m,p.input[c]);
|
||||
for(int k=0;k<m->frame;k++){
|
||||
float scaled=in[k]*gain*32768;int16_t value=!isfinite(scaled)?0:scaled>=32767?32767:scaled<=-32768?-32768:(int16_t)scaled;
|
||||
memcpy(data+at,&value,2);at=(at+2)%p.size;
|
||||
}
|
||||
}
|
||||
m->sink_written[id]=m->sink_old[id];m->sink_old[id]=m->sink_offset[id];m->sink_offset[id]=at;
|
||||
}
|
||||
uint32_t audio_mixer_sink_written(AudioRenderer *r,int id) {Mixer *m=r->mixer;return m?m->sink_written[id]:0;}
|
||||
|
||||
void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
(void)seconds;
|
||||
Mixer *m = r->mixer; if (!m) return;
|
||||
const int frames = m->frame, mc = r->param.submix_count + 1;
|
||||
memset(m->buf, 0, (size_t)m->nbuf * (size_t)frames * sizeof(float));
|
||||
int at = 0;
|
||||
for (int i = 0; i < mc; i++) { m->offset[i] = at; if (r->mixes[i].in_use) at += r->mixes[i].buffer_count; }
|
||||
if (at > m->nbuf) { rt_log("audio: mixes use %d buffers, renderer has %d", at, m->nbuf); rt_fatal("audio mix buffer layout"); }
|
||||
|
||||
float (*vs)[MAX_FRAME]=m->voice_scratch,*filtered=m->filter_scratch;
|
||||
for (int i = 0; i < r->param.voice_count; i++) {
|
||||
ServerVoice *v = &r->voices[i];
|
||||
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate || !v->valid[v->index]) continue;
|
||||
int cc = (int)v->in.channel_count; if (cc < 1 || cc > 6) { report_once(m, 2, "voice channel count unsupported", cc); continue; }
|
||||
voice_render(r, v, vs, frames);
|
||||
for (int f = 0; f < 2; f++) if (v->in.biquads[f].enable) {
|
||||
const AudrenBiquad *b = &v->in.biquads[f];
|
||||
const AudrenBiquadFloat *bf=&v->float_biquads[f];
|
||||
const float a0 = v->use_float_biquads?bf->numerator[0]:b->numerator[0]/16384.0f;
|
||||
const float a1 = v->use_float_biquads?bf->numerator[1]:b->numerator[1]/16384.0f;
|
||||
const float a2 = v->use_float_biquads?bf->numerator[2]:b->numerator[2]/16384.0f;
|
||||
const float b1 = v->use_float_biquads?bf->denominator[0]:b->denominator[0]/16384.0f;
|
||||
const float b2 = v->use_float_biquads?bf->denominator[1]:b->denominator[1]/16384.0f;
|
||||
for (int c = 0; c < cc; c++) {
|
||||
float *z = v->biquad_state[f][c];
|
||||
for (int k = 0; k < frames; k++) {
|
||||
float x = vs[c][k], y = a0 * x + z[0];
|
||||
z[0] = a1 * x + b1 * y + z[1];
|
||||
z[1] = a2 * x + b2 * y;
|
||||
vs[c][k] = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (v->in.splitter_id != SPLITTER_UNUSED && v->in.splitter_id < (uint32_t)m->nsplit) {
|
||||
const SplitInfo *s = &m->split[v->in.splitter_id];
|
||||
for (int c = 0; c < cc; c++) for (int k = c; k < s->ndest; k += cc) {
|
||||
SplitDest *d = &m->dest[s->ids[k]];
|
||||
if (!d->used || !mix_valid(r, d->dest)) continue;
|
||||
const float *source=splitter_filter(d,vs[c],filtered,frames);
|
||||
for (int b = 0; b < r->mixes[d->dest].buffer_count && b < 24; b++) accumulate(m, m->offset[d->dest] + b, source, d->vol[b] * v->in.volume, frames);
|
||||
}
|
||||
} else if (mix_valid(r, v->in.mix_id)) {
|
||||
for (int c = 0; c < cc; c++) {
|
||||
int32_t id = v->in.channel_ids[c]; if (id < 0 || id >= r->param.voice_count * 6) continue;
|
||||
const AudrenChannelIn *ch = &r->channels[id];
|
||||
for (int b = 0; b < r->mixes[v->in.mix_id].buffer_count && b < 24; b++) accumulate(m, m->offset[v->in.mix_id] + b, vs[c], ch->mix[b] * v->in.volume, frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* submixes: deepest first, so every mix is complete before it is folded into its destination */
|
||||
int order[256], n = 0, depth[256];
|
||||
for (int i = 1; i < mc && n < 256; i++) if (r->mixes[i].in_use) { depth[n] = mix_depth(r, i); order[n++] = i; }
|
||||
for (int a = 0; a < n; a++) for (int b = a + 1; b < n; b++) if (depth[b] > depth[a]) { int t = order[a]; order[a] = order[b]; order[b] = t; t = depth[a]; depth[a] = depth[b]; depth[b] = t; }
|
||||
for (int a = 0; a < n; a++) {
|
||||
mix_effects(r, order[a]);
|
||||
const AudrenMixIn *x = &r->mixes[order[a]]; int src = m->offset[order[a]];
|
||||
if (x->dest_splitter_id != SPLITTER_UNUSED && x->dest_splitter_id < (uint32_t)m->nsplit) {
|
||||
const SplitInfo *s = &m->split[x->dest_splitter_id];
|
||||
for (int c = 0; c < x->buffer_count && c < 24; c++) for (int k = c; k < s->ndest; k += x->buffer_count) {
|
||||
SplitDest *d = &m->dest[s->ids[k]];
|
||||
if (!d->used || !mix_valid(r, d->dest)) continue;
|
||||
const float *source=splitter_filter(d,mixbuf(m,src+c),filtered,frames);
|
||||
for (int b = 0; b < r->mixes[d->dest].buffer_count && b < 24; b++) accumulate(m, m->offset[d->dest] + b, source, d->vol[b] * x->volume, frames);
|
||||
}
|
||||
} else if (mix_valid(r, x->dest_mix_id)) {
|
||||
for (int c = 0; c < x->buffer_count && c < 24; c++)
|
||||
for (int b = 0; b < r->mixes[x->dest_mix_id].buffer_count && b < 24; b++) accumulate(m, m->offset[x->dest_mix_id] + b, mixbuf(m, src + c), x->mix[c][b] * x->volume, frames);
|
||||
}
|
||||
}
|
||||
if (r->mixes[0].in_use) mix_effects(r, 0);
|
||||
/* device sink */
|
||||
const AudrenSinkIn *sink = NULL;int circular=0;
|
||||
for (int i = 0; i < r->param.sink_count; i++) if (r->sinks[i].in_use) {
|
||||
if(r->sinks[i].type!=SINK_CIRCULAR){m->sink_address[i]=0;m->sink_size[i]=m->sink_offset[i]=m->sink_old[i]=m->sink_written[i]=0;}
|
||||
if (r->sinks[i].type == SINK_DEVICE && !sink) sink = &r->sinks[i];
|
||||
else if (r->sinks[i].type == SINK_CIRCULAR){mix_circular_sink(r,i);circular=1;}
|
||||
} else {m->sink_address[i]=0;m->sink_size[i]=m->sink_offset[i]=m->sink_old[i]=m->sink_written[i]=0;}
|
||||
const int oc = m->channels;
|
||||
float *out=m->output_scratch;
|
||||
memset(out, 0, sizeof(float) * (size_t)oc * (size_t)frames);
|
||||
if (sink && sink->input_count >= 1 && sink->input_count <= 6) {
|
||||
if (!(m->reported & (1u << 6))) {
|
||||
m->reported |= 1u << 6;
|
||||
rt_log("audio: device sink has %u inputs, output %s", sink->input_count, oc == 6 ? "5.1" : "stereo");
|
||||
}
|
||||
const float gain = host_volume() * atomic_load(&device_volume) * (r->mixes[0].in_use ? r->mixes[0].volume : 1.0f);
|
||||
const float *ch[6] = {0};
|
||||
float raw[2 * MAX_FRAME];
|
||||
for (uint32_t c = 0; c < sink->input_count; c++) if (sink->inputs[c] >= 0 && sink->inputs[c] < m->nbuf) ch[c] = mixbuf(m, sink->inputs[c]);
|
||||
for (int k = 0; k < frames; k++) {
|
||||
float s[OUT_MAX] = {0}; /* FL FR C LFE RL RR */
|
||||
if (sink->input_count >= 6 && oc == 6) {
|
||||
for (int c = 0; c < 6; c++) s[c] = ch[c] ? ch[c][k] : 0;
|
||||
} else if (sink->input_count >= 6) { /* 5.1 -> stereo; the LFE is left out, as the Switch's downmix does */
|
||||
float ff = sink->downmix_enabled ? sink->downmix[0] : 1.0f, cf = sink->downmix_enabled ? sink->downmix[1] : 0.7071f, sf = sink->downmix_enabled ? sink->downmix[2] : 0.7071f;
|
||||
float C = ch[2] ? ch[2][k] : 0;
|
||||
s[0] = ff * (ch[0] ? ch[0][k] : 0) + cf * C + sf * (ch[4] ? ch[4][k] : 0);
|
||||
s[1] = ff * (ch[1] ? ch[1][k] : 0) + cf * C + sf * (ch[5] ? ch[5][k] : 0);
|
||||
} else if (sink->input_count >= 2) { s[0] = ch[0] ? ch[0][k] : 0; s[1] = ch[1] ? ch[1][k] : 0; }
|
||||
else s[0] = s[1] = ch[0] ? ch[0][k] : 0;
|
||||
raw[2 * k] = s[0] * gain; raw[2 * k + 1] = s[1] * gain; /* NX2NAVI_AUDIO_DUMP: the front pair before clipping */
|
||||
for (int c = 0; c < oc; c++) {
|
||||
float x = s[c] * gain;
|
||||
out[oc * k + c] = x > 1 ? 1 : x < -1 ? -1 : x;
|
||||
}
|
||||
}
|
||||
sink_dump(raw, frames);
|
||||
} else if (!sink && !circular) report_once(m, 5, "no device sink in use", r->param.sink_count);
|
||||
level_note(r, m, out, frames);
|
||||
output_push(m, out, frames);
|
||||
}
|
||||
|
||||
// ---- IAudioDevice: one output device, stereo or 5.1 (output_channels)
|
||||
static const char device_name[] = "AudioTvOutput";
|
||||
|
||||
static void put_name(const IpcBuffer *b, IpcResponse *rs, int pointer) {
|
||||
if (!b->size) return;
|
||||
char *dst = ipc_guest(b->addr, b->size);
|
||||
size_t n = b->size < sizeof device_name ? b->size : sizeof device_name;
|
||||
memset(dst, 0, b->size);
|
||||
memcpy(dst, device_name, n);
|
||||
if (pointer) rs->xout[rs->nxout++] = (IpcBuffer){b->addr, b->size, b->index};
|
||||
}
|
||||
|
||||
static IpcBuffer out_buffer(const IpcRequest *rq, int *pointer) {
|
||||
*pointer = !(rq->nb && rq->b[0].size) && rq->nc && rq->c[0].size;
|
||||
return *pointer ? rq->c[0] : rq->nb ? rq->b[0] : (IpcBuffer){0};
|
||||
}
|
||||
|
||||
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
||||
static uint32_t change_event; // device change events: never signalled, the device does not change
|
||||
int pointer;
|
||||
switch (rq->cmd) {
|
||||
case 0: case 6: case 14: { // ListAudioDeviceName(Auto), ListAudioOutputDeviceName [13.0.0+]
|
||||
IpcBuffer b = out_buffer(rq, &pointer);
|
||||
int32_t n = b.size >= 0x100 ? 1 : 0;
|
||||
if (n) put_name(&b, rs, pointer);
|
||||
IPC_OUT(rs, n);
|
||||
return;
|
||||
}
|
||||
case 1: case 7: { // SetAudioDeviceOutputVolume(Auto)
|
||||
float v = IPC_ARG(rq, float, 0);
|
||||
atomic_store(&device_volume, isfinite(v) ? v : 0.0f);
|
||||
return;
|
||||
}
|
||||
case 2: case 8: { float v = atomic_load(&device_volume); IPC_OUT(rs, v); return; } // GetAudioDeviceOutputVolume(Auto)
|
||||
case 3: case 10: case 13: { // GetActiveAudioDeviceName(Auto), GetActiveAudioOutputDeviceName [13.0.0+]
|
||||
IpcBuffer b = out_buffer(rq, &pointer);
|
||||
put_name(&b, rs, pointer);
|
||||
return;
|
||||
}
|
||||
case 5: { int32_t n = output_channels(); IPC_OUT(rs, n); return; } // GetActiveChannelCount
|
||||
case 4: case 11: case 12: { // QueryAudioDeviceSystemEvent, ...InputEvent, ...OutputEvent
|
||||
uint32_t ev = __atomic_load_n(&change_event, __ATOMIC_ACQUIRE);
|
||||
if (!ev) {
|
||||
uint32_t mine = kern_event_new(), expected = 0;
|
||||
if (__atomic_compare_exchange_n(&change_event, &expected, mine, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE))
|
||||
ev = mine;
|
||||
else
|
||||
ev = expected; // the loser's event stays unused (no host-side close yet)
|
||||
}
|
||||
rs->copy[rs->ncopy++] = kern_event_readable(ev);
|
||||
return;
|
||||
}
|
||||
default: ipc_unimplemented(s, rq);
|
||||
}
|
||||
}
|
||||
83
runtime/audio_reverb.c
Normal file
83
runtime/audio_reverb.c
Normal file
@@ -0,0 +1,83 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/audio_reverb.c @ b75be8c (MIT, Copyright (c) 2026 MacNativeGames contributors).
|
||||
/* Compatible 3D reverb network, based on Ryujinx.Audio's Reverb3dState/Command,
|
||||
DelayLine3d and DecayDelay. Copyright (c) Ryujinx Team and Contributors.
|
||||
MIT license: third_party/licenses/Ryujinx-Audio.txt. Samples are normalized floats. */
|
||||
#include "audio_reverb.h"
|
||||
#include "host.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct { float *data; unsigned cap, delay, read, write; } Delay;
|
||||
struct Reverb3d {
|
||||
Reverb3dParam p; Delay fdn[4], allpass1[4], allpass2[4], pre, center;
|
||||
unsigned early_time[20], reflection_time;
|
||||
float pre_previous, pre_gain, pre_target, early_gain, late_gain, diffusion;
|
||||
float direct[4], current[4], output[4], history[4];
|
||||
};
|
||||
static const float fdn_min[4] = {5,6,13,14}, fdn_max[4] = {45.704f,82.782f,149.94f,271.58f};
|
||||
static const float ap1_max[4] = {17,13,9,7}, ap2_max[4] = {19,11,10,6};
|
||||
static const float early_times[20] = {.017136f,.059154f,.16173f,.39019f,.42526f,.45541f,.68974f,.74591f,.83384f,.8595f,0,.075024f,.16879f,.2999f,.33744f,.3719f,.59901f,.71674f,.81786f,.85166f};
|
||||
static const float early_gains[20] = {.67096f,.61027f,1,.35680f,.68361f,.65978f,.51939f,.24712f,.45945f,.45021f,.64196f,.54879f,.92925f,.38270f,.72867f,.69794f,.5464f,.24563f,.45214f,.44042f};
|
||||
static unsigned samples(unsigned khz, float ms) { return (unsigned)nearbyintf(khz * ms); }
|
||||
static void delay_set(Delay *d, unsigned n) { if (n > d->cap) rt_fatal("reverb delay exceeds capacity"); d->delay = n; d->write = (d->read + n) % (d->cap + 1); }
|
||||
static void delay_init(Delay *d, unsigned khz, float ms) { d->cap = samples(khz, ms); d->data = calloc(d->cap + 1, sizeof(float)); if (!d->data) rt_fatal("reverb delay allocation"); delay_set(d, d->cap); }
|
||||
static float delay_update(Delay *d, float v) {
|
||||
d->data[d->write++] = v; float out = d->data[d->read++];
|
||||
if (d->read > d->cap) d->read = 0;
|
||||
if (d->write > d->cap) d->write = 0;
|
||||
return out;
|
||||
}
|
||||
static float delay_tap(const Delay *d, unsigned at) { int i = (int)d->write - (int)at - 1; if (i < 0) i += (int)d->cap + 1; return d->data[i]; }
|
||||
static float allpass(Delay *d, float x, float gain) { float v = x - gain * d->data[d->read]; return delay_update(d, v) + gain * v; }
|
||||
static float gain10(float x) { return x >= 0 ? 1 : x <= -5.3f ? 0 : powf(10, x); }
|
||||
static float radians(float x) { return x * 3.14159265358979323846f / 180; }
|
||||
static void validate(const Reverb3dParam *p) {
|
||||
if ((p->channels != 1 && p->channels != 2 && p->channels != 4 && p->channels != 6) || p->sample_rate < 1000 || p->sample_rate > 192000 ||
|
||||
!(p->decay_time > 0) || !(p->hf_decay_ratio > 0) || !(p->density >= 0 && p->density <= 100) ||
|
||||
!(p->reflection_delay >= 0 && p->reverb_delay >= 0 && p->reflection_delay + p->reverb_delay <= .4f)) rt_fatal("invalid reverb3d parameters");
|
||||
}
|
||||
Reverb3d *audio_reverb_create(const Reverb3dParam *p) {
|
||||
validate(p); Reverb3d *s = calloc(1, sizeof *s); if (!s) rt_fatal("reverb allocation"); unsigned khz = p->sample_rate / 1000;
|
||||
for (int i=0;i<4;i++) { delay_init(&s->fdn[i],khz,fdn_max[i]); delay_init(&s->allpass1[i],khz,ap1_max[i]); delay_init(&s->allpass2[i],khz,ap2_max[i]); }
|
||||
delay_init(&s->pre,khz,400); delay_init(&s->center,khz,5); audio_reverb_update(s,p); return s;
|
||||
}
|
||||
void audio_reverb_free(Reverb3d *s) { if (!s) return; for (int i=0;i<4;i++) { free(s->fdn[i].data); free(s->allpass1[i].data); free(s->allpass2[i].data); } free(s->pre.data); free(s->center.data); free(s); }
|
||||
void audio_reverb_update(Reverb3d *s, const Reverb3dParam *p) {
|
||||
validate(p); s->p = *p; unsigned khz = p->sample_rate / 1000;
|
||||
memset(s->history,0,sizeof s->history); s->pre_previous=0;
|
||||
s->early_gain=gain10(fminf(p->room_gain+p->reflections_gain,5000)/2000); s->late_gain=gain10(fminf(p->room_gain+p->reverb_gain,5000)/2000);
|
||||
float hf=gain10(p->room_hf/2000);
|
||||
if (hf<1) { float a=1-hf,b=2-2*hf*cosf(radians(256*p->hf_reference/p->sample_rate)); s->pre_gain=(b-sqrtf(b*b-4*a*a))/(2*a); s->pre_target=1-s->pre_gain; }
|
||||
else { s->pre_gain=0; s->pre_target=1; }
|
||||
s->reflection_time=samples(khz,1000*(p->reflection_delay+p->reverb_delay)); s->diffusion=.6f*p->diffusion*.01f;
|
||||
for (int i=0;i<4;i++) {
|
||||
delay_set(&s->fdn[i],samples(khz,fdn_min[i]+p->density/100*(fdn_max[i]-fdn_min[i])));
|
||||
float n=(float)(s->fdn[i].delay+s->allpass1[i].delay+s->allpass2[i].delay);
|
||||
float a=-60*n/(p->decay_time*p->sample_rate),b=a/p->hf_decay_ratio;
|
||||
float angle=radians(128*.5f*p->hf_reference/p->sample_rate),c=cosf(angle)/sinf(angle),d=gain10((b-a)/40),e=gain10((b+a)/40)*.7071f;
|
||||
s->direct[i]=e*(d*c+1)/(c+d); s->current[i]=e*(1-d*c)/(c+d); s->output[i]=(c-d)/(c+d);
|
||||
}
|
||||
for (int i=0;i<20;i++) { unsigned n=samples(khz,p->reflection_delay*1000+early_times[i]*1000*(p->reverb_delay*.9998f+.02f)); s->early_time[i]=n<s->pre.cap?n:s->pre.cap; }
|
||||
}
|
||||
void audio_reverb_process(Reverb3d *s, float *const input[6], float *const output[6], int frames) {
|
||||
static const unsigned stereo[20]={0,0,0,1,1,1,1,0,0,0,1,1,1,0,0,0,0,1,1,1};
|
||||
static const unsigned quad[20]={0,0,0,1,1,1,1,2,2,2,1,1,1,0,0,0,0,3,3,3};
|
||||
/* Preserve the reference's legacy surround channel ordering (L,R,BL,BR,C,LFE). */
|
||||
static const unsigned surround[20]={4,5,0,5,0,5,1,5,1,5,1,5,1,5,2,5,2,5,2,5};
|
||||
const int channels=s->p.channels;
|
||||
for (int k=0;k<frames;k++) {
|
||||
float dry[6],early[6]={0},fb[4],v[4],sum=0,tap=delay_tap(&s->pre,s->reflection_time);
|
||||
for (int i=0;i<20;i++) { unsigned out=channels==1?0:channels==2?stereo[i]:channels==4?quad[i]:surround[i]; float x=delay_tap(&s->pre,s->early_time[i])*early_gains[i]; early[out]+=x; if(channels==6)early[out]+=x; }
|
||||
for (int c=0;c<channels;c++) { dry[c]=input[c][k]; sum+=dry[c]; }
|
||||
s->pre_previous=sum*s->pre_target+s->pre_previous*s->pre_gain; delay_update(&s->pre,s->pre_previous);
|
||||
for (int i=0;i<4;i++) { float x=s->fdn[i].data[s->fdn[i].read]; fb[i]=x*s->direct[i]+s->history[i]; s->history[i]=x*s->current[i]+fb[i]*s->output[i]; }
|
||||
const float feedback[4]={fb[2]+fb[1],-fb[0]-fb[3],fb[0]-fb[3],fb[1]-fb[2]};
|
||||
for (int i=0;i<4;i++) { v[i]=allpass(&s->allpass2[i],allpass(&s->allpass1[i],tap*s->late_gain+feedback[i],s->diffusion),-.9f*s->diffusion); delay_update(&s->fdn[i],v[i]); }
|
||||
for (int c=0;c<channels;c++) {
|
||||
if(channels==6 && c==4) output[c][k]+=early[c]*s->early_gain+delay_update(&s->center,(v[2]-v[3])*.5f)+dry[c]*s->p.dry_gain;
|
||||
else output[c][k]=early[c]*s->early_gain+v[c==5?3:c]+dry[c]*s->p.dry_gain;
|
||||
}
|
||||
if(channels==1)output[0][k]+=v[1];
|
||||
}
|
||||
}
|
||||
19
runtime/audio_reverb.h
Normal file
19
runtime/audio_reverb.h
Normal file
@@ -0,0 +1,19 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/audio_reverb.c @ b75be8c (MIT, Copyright (c) 2026 MacNativeGames
|
||||
// contributors), based on Ryujinx.Audio (MIT, Copyright (c) Ryujinx Team and Contributors).
|
||||
#ifndef NX2NAVI_AUDIO_REVERB_H
|
||||
#define NX2NAVI_AUDIO_REVERB_H
|
||||
#include <stdint.h>
|
||||
typedef struct {
|
||||
uint8_t input[6], output[6]; uint16_t channel_max, channels;
|
||||
uint32_t reserved, sample_rate;
|
||||
float room_hf, hf_reference, decay_time, hf_decay_ratio, room_gain, reflections_gain,
|
||||
reverb_gain, diffusion, reflection_delay, reverb_delay, density, dry_gain;
|
||||
uint8_t status;
|
||||
} Reverb3dParam;
|
||||
_Static_assert(__builtin_offsetof(Reverb3dParam, status) == 72, "reverb3d wire layout");
|
||||
typedef struct Reverb3d Reverb3d;
|
||||
Reverb3d *audio_reverb_create(const Reverb3dParam *p);
|
||||
void audio_reverb_free(Reverb3d *s);
|
||||
void audio_reverb_update(Reverb3d *s, const Reverb3dParam *p);
|
||||
void audio_reverb_process(Reverb3d *s, float *const input[6], float *const output[6], int frames);
|
||||
#endif
|
||||
501
runtime/audren.c
Normal file
501
runtime/audren.c
Normal file
@@ -0,0 +1,501 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/audio_renderer.c @ b75be8c (MIT, Copyright (c) 2026
|
||||
// MacNativeGames contributors). Its voice lifetime (play state, wave buffer release, flush) is ported from
|
||||
// Ryujinx.Audio VoiceInfo / DataSourceHelper (MIT, Copyright (c) Ryujinx Team and Contributors).
|
||||
// audren:u. The SDK's nn::audio client builds RequestUpdate input unchanged; this
|
||||
// service validates every section against the header, keeps server-side voice state, advances it in real time (one
|
||||
// frame of sample_count / sample_rate seconds per render tick, signalling the renderer's event) and writes the output
|
||||
// status the client expects. The mixer behind it is audio_mixer.c.
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "audren.h"
|
||||
#include "host.h"
|
||||
#include "ipc.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static volatile int32_t g_live;
|
||||
|
||||
long audren_renderers_live(void) { return plat_cas32(&g_live, 0, 0); }
|
||||
|
||||
static int revision_number(uint32_t rev) { return (int)(rev >> 24) - '0'; }
|
||||
|
||||
static void need(int ok, const char *what) {
|
||||
if (ok) return;
|
||||
ipc_backtrace();
|
||||
rt_fatal("audren: %s", what);
|
||||
}
|
||||
|
||||
static AudioRenderer *renderer_new(const AudrenParameter *p) {
|
||||
need(p->sample_rate == 48000 || p->sample_rate == 32000, "unsupported output rate");
|
||||
need(p->sample_count > 0 && p->sample_count <= 4800, "sample count");
|
||||
need(p->voice_count > 0 && p->voice_count <= 1024 && p->sink_count > 0 && p->sink_count <= 256 &&
|
||||
p->effect_count >= 0 && p->effect_count <= 4096 && p->submix_count >= 0 && p->submix_count < 256,
|
||||
"renderer counts");
|
||||
AudioRenderer *r = calloc(1, sizeof *r);
|
||||
need(r != NULL, "renderer allocation");
|
||||
r->param = *p;
|
||||
r->revision = p->revision;
|
||||
r->voices = calloc((size_t)p->voice_count, sizeof *r->voices);
|
||||
r->pools = calloc((size_t)p->effect_count + 4u * (size_t)p->voice_count, sizeof *r->pools);
|
||||
r->mixes = calloc((size_t)p->submix_count + 1, sizeof *r->mixes);
|
||||
r->sinks = calloc((size_t)p->sink_count, sizeof *r->sinks);
|
||||
r->effects = calloc((size_t)(p->effect_count ? p->effect_count : 1), sizeof *r->effects);
|
||||
r->channels = calloc((size_t)p->voice_count * 6, sizeof *r->channels);
|
||||
need(r->voices && r->pools && r->mixes && r->sinks && r->effects && r->channels, "renderer tables");
|
||||
plat_mutex_init(&r->lock);
|
||||
r->event = kern_event_new();
|
||||
r->state = 1; // stopped
|
||||
return r;
|
||||
}
|
||||
|
||||
static void renderer_free(AudioRenderer *r) {
|
||||
free(r->voices);
|
||||
free(r->pools);
|
||||
free(r->mixes);
|
||||
free(r->sinks);
|
||||
free(r->effects);
|
||||
free(r->channels);
|
||||
free(r);
|
||||
}
|
||||
|
||||
// ---- RequestUpdate
|
||||
typedef struct { const uint8_t *p, *end; } Reader;
|
||||
|
||||
static const uint8_t *take(Reader *in, uint32_t n, const char *what) {
|
||||
if ((size_t)(in->end - in->p) < n) {
|
||||
rt_log("audren: input section %s (%u bytes) overruns the request", what, n);
|
||||
need(0, "update input section size");
|
||||
}
|
||||
const uint8_t *p = in->p;
|
||||
in->p += n;
|
||||
return p;
|
||||
}
|
||||
|
||||
static void voice_reset(ServerVoice *v) {
|
||||
memset(v->valid, 0, sizeof v->valid);
|
||||
memset(v->pending, 0, sizeof v->pending);
|
||||
v->state = SV_STOPPED;
|
||||
v->flush = v->flush_from = 0;
|
||||
v->index = 0;
|
||||
v->offset = 0;
|
||||
v->fraction = 0;
|
||||
v->played = 0;
|
||||
v->consumed = 0;
|
||||
v->loops_done = 0;
|
||||
memset(&v->adpcm, 0, sizeof v->adpcm);
|
||||
memset(v->biquad_state, 0, sizeof v->biquad_state);
|
||||
}
|
||||
|
||||
static void update_voice(AudioRenderer *r, const AudrenVoiceIn *in) {
|
||||
need(in->id < (uint32_t)r->param.voice_count, "voice id out of range");
|
||||
ServerVoice *v = &r->voices[in->id];
|
||||
if (in->is_new || !in->in_use) voice_reset(v);
|
||||
v->in = *in;
|
||||
v->use_float_biquads = 0;
|
||||
if (!in->in_use) return;
|
||||
need(in->wave_buffer_index < 4, "wave buffer index");
|
||||
// The play state as the DSP keeps it (Ryujinx VoiceInfo.UpdatePlayState): Stop on a voice that is not stopped
|
||||
// becomes Stopping, which the next render frame turns into Stopped after giving back every queued buffer.
|
||||
if (in->play_state == PLAY_STARTED) v->state = SV_STARTED;
|
||||
else if (in->play_state == PLAY_PAUSED) v->state = SV_PAUSED;
|
||||
else if (v->state != SV_STOPPED) v->state = SV_STOPPING;
|
||||
for (unsigned i = 0; i < 4; i++) { // a new buffer waits until a render frame finds the voice playing
|
||||
const AudrenWaveBuffer *w = &in->wave_buffers[i];
|
||||
if (!w->sent) {
|
||||
v->buffers[i] = *w;
|
||||
v->pending[i] = 1;
|
||||
}
|
||||
}
|
||||
v->flush += in->flush_wave_buffer_count;
|
||||
v->flush_from = in->wave_buffer_index;
|
||||
if (in->clear_voice_drop) v->drops = 0;
|
||||
}
|
||||
|
||||
// No samples (no memory, no size or an empty range): the DSP counts the buffer as played when it reaches it, so a
|
||||
// stream's empty last buffer does not hold up the buffers queued behind it.
|
||||
int audio_buffer_empty(const AudrenWaveBuffer *b) { return !b->address || !b->size || b->end <= b->start; }
|
||||
|
||||
static void skip_empty(AudioRenderer *r, ServerVoice *v) {
|
||||
while (v->valid[v->index] && audio_buffer_empty(&v->buffers[v->index])) {
|
||||
const AudrenWaveBuffer *b = &v->buffers[v->index];
|
||||
if (b->loop && b->loop_count < 0) break; // an endless empty loop starves the voice, as on the DSP
|
||||
v->valid[v->index] = 0;
|
||||
v->consumed++;
|
||||
v->offset = 0;
|
||||
v->loops_done = 0;
|
||||
v->index = (v->index + 1) & 3;
|
||||
r->empty_played++;
|
||||
if (b->end_of_stream) {
|
||||
v->played = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Each render frame, before mixing (Ryujinx VoiceInfo.UpdateParametersForCommandGeneration): flush, stop, start.
|
||||
static void voices_prepare(AudioRenderer *r) {
|
||||
for (int k = 0; k < r->param.voice_count; k++) {
|
||||
ServerVoice *v = &r->voices[k];
|
||||
if (!v->in.in_use) continue;
|
||||
if (v->flush) { // FlushWaveBuffers: from the client's current buffer, the queued ones count as played
|
||||
unsigned i = v->flush_from & 3;
|
||||
for (uint32_t n = v->flush; n; n--, i = (i + 1) & 3) {
|
||||
v->pending[i] = 0;
|
||||
if (v->valid[i]) {
|
||||
v->valid[i] = 0;
|
||||
v->consumed++;
|
||||
v->index = (v->index + 1) & 3;
|
||||
v->offset = 0;
|
||||
v->loops_done = 0;
|
||||
r->flushed++;
|
||||
}
|
||||
}
|
||||
v->flush = 0;
|
||||
}
|
||||
if (v->state == SV_STOPPING) { // every queued buffer counts as played; unsent ones are dropped
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (v->valid[i]) {
|
||||
v->consumed++;
|
||||
v->index = (v->index + 1) & 3;
|
||||
r->stop_released++;
|
||||
}
|
||||
v->valid[i] = 0;
|
||||
v->pending[i] = 0;
|
||||
}
|
||||
v->offset = 0;
|
||||
v->played = 0;
|
||||
v->fraction = 0;
|
||||
v->loops_done = 0;
|
||||
v->state = SV_STOPPED;
|
||||
continue;
|
||||
}
|
||||
if (v->state == SV_STARTED) {
|
||||
for (int i = 0; i < 4; i++)
|
||||
if (v->pending[i]) {
|
||||
v->pending[i] = 0;
|
||||
v->valid[i] = 1;
|
||||
if ((unsigned)i == v->index) {
|
||||
v->offset = 0;
|
||||
v->loops_done = 0;
|
||||
}
|
||||
}
|
||||
skip_empty(r, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t effect_out_bytes(const AudioRenderer *r) { return revision_number(r->revision) >= 9 ? 0x90u : 0x10u; }
|
||||
|
||||
static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_size, uint8_t *output,
|
||||
uint64_t out_size, uint64_t perf_size) {
|
||||
need(in_size >= sizeof(AudrenHeader), "update input too small");
|
||||
AudrenHeader h;
|
||||
memcpy(&h, input, sizeof h);
|
||||
need(h.total <= in_size, "update total size");
|
||||
Reader in = {input + sizeof h, input + h.total};
|
||||
plat_mutex_lock(&r->lock);
|
||||
r->revision = h.revision;
|
||||
const int vc = r->param.voice_count, ec = r->param.effect_count, sc = r->param.sink_count,
|
||||
mc = r->param.submix_count + 1;
|
||||
const uint32_t npools = (uint32_t)ec + 4u * (uint32_t)vc;
|
||||
if (!r->logged) {
|
||||
r->logged = 1;
|
||||
rt_log("audren: update REV%d sizes behavior=%u pools=%u voices=%u channels=%u effects=%u mixes=%u sinks=%u "
|
||||
"perf=%u splitters=%u render=%u total=%u out=%llu perf_out=%llu",
|
||||
revision_number(h.revision), h.behavior, h.memory_pools, h.voices, h.voice_resources, h.effects,
|
||||
h.mixes, h.sinks, h.performance, h.splitters, h.render_info, h.total, (unsigned long long)out_size,
|
||||
(unsigned long long)perf_size);
|
||||
}
|
||||
need(h.behavior == 0x10, "behavior section size");
|
||||
const uint8_t *behavior = take(&in, h.behavior, "behavior");
|
||||
memcpy(&r->behavior_flags, behavior + 8, 8);
|
||||
need(h.memory_pools == npools * sizeof(AudrenPoolIn), "memory pool section size");
|
||||
const AudrenPoolIn *pools = (const AudrenPoolIn *)take(&in, h.memory_pools, "memory pools");
|
||||
need(h.voice_resources % sizeof(AudrenChannelIn) == 0 &&
|
||||
h.voice_resources / sizeof(AudrenChannelIn) <= (uint32_t)vc * 6,
|
||||
"voice channel section size");
|
||||
const AudrenChannelIn *chans = (const AudrenChannelIn *)take(&in, h.voice_resources, "voice channels");
|
||||
const int float_voices = revision_number(h.revision) >= 15;
|
||||
const size_t voice_stride = float_voices ? sizeof(AudrenVoiceInV2) : sizeof(AudrenVoiceIn);
|
||||
need(h.voices == (uint32_t)vc * voice_stride, "voice section size");
|
||||
const uint8_t *voices = take(&in, h.voices, "voices");
|
||||
need(h.effects == (uint32_t)ec * sizeof(AudrenEffectIn), "effect section size");
|
||||
const AudrenEffectIn *effects = (const AudrenEffectIn *)take(&in, h.effects, "effects");
|
||||
const uint8_t *splitters = take(&in, h.splitters, "splitters");
|
||||
const uint8_t *mixes = take(&in, h.mixes, "mixes");
|
||||
need(h.sinks == (uint32_t)sc * sizeof(AudrenSinkIn), "sink section size");
|
||||
const AudrenSinkIn *sinks = (const AudrenSinkIn *)take(&in, h.sinks, "sinks");
|
||||
take(&in, h.performance, "performance");
|
||||
take(&in, h.render_info, "render info");
|
||||
need(in.p == in.end, "unconsumed update input");
|
||||
|
||||
for (uint32_t i = 0; i < h.voice_resources / sizeof(AudrenChannelIn); i++) {
|
||||
need(chans[i].id < (uint32_t)vc * 6, "channel resource id");
|
||||
r->channels[chans[i].id] = chans[i];
|
||||
}
|
||||
for (int i = 0; i < vc; i++) {
|
||||
AudrenVoiceIn v;
|
||||
AudrenVoiceInV2 v2;
|
||||
if (float_voices) {
|
||||
memcpy(&v2, voices + i * voice_stride, sizeof v2);
|
||||
memset(&v, 0, sizeof v);
|
||||
memcpy(&v, v2.prefix, sizeof v2.prefix);
|
||||
memcpy((uint8_t *)&v + 0x3c, v2.tail, sizeof v2.tail);
|
||||
for (unsigned f = 0; f < 2; f++) {
|
||||
v.biquads[f].enable = v2.biquads[f].enable;
|
||||
if (!v.biquads[f].enable) continue;
|
||||
for (unsigned c = 0; c < 3; c++) need(isfinite(v2.biquads[f].numerator[c]), "voice biquad numerator");
|
||||
for (unsigned c = 0; c < 2; c++)
|
||||
need(isfinite(v2.biquads[f].denominator[c]), "voice biquad denominator");
|
||||
}
|
||||
} else
|
||||
memcpy(&v, voices + i * voice_stride, sizeof v);
|
||||
update_voice(r, &v);
|
||||
ServerVoice *server = &r->voices[v.id];
|
||||
server->use_float_biquads = (uint8_t)float_voices;
|
||||
if (float_voices) memcpy(server->float_biquads, v2.biquads, sizeof v2.biquads);
|
||||
}
|
||||
for (int i = 0; i < ec; i++) r->effects[i] = effects[i];
|
||||
audio_mixer_update_effects(r);
|
||||
audio_mixer_update_routing(r, splitters, h.splitters, mixes, h.mixes, mc);
|
||||
for (int i = 0; i < sc; i++) r->sinks[i] = sinks[i];
|
||||
|
||||
// ---- output
|
||||
const uint32_t eo = effect_out_bytes(r);
|
||||
AudrenHeader o = {0};
|
||||
o.revision = AUDREN_REV(revision_number(h.revision) >= 1 ? revision_number(h.revision) : 1);
|
||||
o.memory_pools = npools * sizeof(AudrenPoolOut);
|
||||
o.voices = (uint32_t)vc * sizeof(AudrenVoiceOut);
|
||||
o.effects = (uint32_t)ec * eo;
|
||||
o.sinks = (uint32_t)sc * sizeof(AudrenSinkOut);
|
||||
o.performance = 0x10;
|
||||
o.behavior = AUDREN_BEHAVIOR_OUT;
|
||||
o.render_info = AUDREN_RENDER_INFO_OUT;
|
||||
o.total = (uint32_t)sizeof o + o.memory_pools + o.voices + o.effects + o.sinks + o.performance + o.behavior +
|
||||
o.render_info;
|
||||
if (o.total > out_size) {
|
||||
rt_log("audren: output needs %u bytes, the client gave %llu (effect out %u)", o.total,
|
||||
(unsigned long long)out_size, eo);
|
||||
need(0, "update output size");
|
||||
}
|
||||
memset(output, 0, out_size);
|
||||
uint8_t *w = output;
|
||||
memcpy(w, &o, sizeof o);
|
||||
w += sizeof o;
|
||||
for (uint32_t i = 0; i < npools; i++) {
|
||||
uint32_t s = pools[i].state;
|
||||
if (s == POOL_REQUEST_ATTACH) s = POOL_ATTACHED;
|
||||
else if (s == POOL_REQUEST_DETACH) s = POOL_DETACHED;
|
||||
r->pools[i] = pools[i];
|
||||
r->pools[i].state = s;
|
||||
AudrenPoolOut po = {s, {0}};
|
||||
memcpy(w, &po, sizeof po);
|
||||
w += sizeof po;
|
||||
}
|
||||
for (int i = 0; i < vc; i++) {
|
||||
AudrenVoiceOut vo = {r->voices[i].played, r->voices[i].consumed, r->voices[i].drops};
|
||||
memcpy(w, &vo, sizeof vo);
|
||||
w += sizeof vo;
|
||||
}
|
||||
for (int i = 0; i < ec; i++) { // EffectStatus Enabled 3 / Disabled 4
|
||||
w[0] = effects[i].enabled ? 3 : 4;
|
||||
if (eo > 16) audio_mixer_effect_result(r, i, w + 16, eo - 16);
|
||||
w += eo;
|
||||
}
|
||||
for (int i = 0; i < sc; i++) {
|
||||
AudrenSinkOut so = {0};
|
||||
if (r->sinks[i].type == SINK_CIRCULAR) so.last_written_offset = audio_mixer_sink_written(r, i);
|
||||
memcpy(w, &so, sizeof so);
|
||||
w += sizeof so;
|
||||
}
|
||||
w += 0x10; // performance: nothing written
|
||||
w += AUDREN_BEHAVIOR_OUT; // behavior: no errors
|
||||
memcpy(w, &r->elapsed_frames, 8);
|
||||
plat_mutex_unlock(&r->lock);
|
||||
}
|
||||
|
||||
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
|
||||
// or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls
|
||||
static void advance_voice(AudioRenderer *r, ServerVoice *v) {
|
||||
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return;
|
||||
uint64_t samples = audio_voice_consumed(r, v, (int)r->param.sample_count);
|
||||
while (samples && v->valid[v->index]) {
|
||||
AudrenWaveBuffer *b = &v->buffers[v->index];
|
||||
uint64_t length = (uint64_t)(b->end - b->start), left = length - v->offset;
|
||||
uint64_t step = samples < left ? samples : left;
|
||||
v->offset += step;
|
||||
v->played += step;
|
||||
samples -= step;
|
||||
if (v->offset < length) break;
|
||||
if (b->loop && (b->loop_count < 0 || v->loops_done < b->loop_count)) {
|
||||
v->loops_done++;
|
||||
v->offset = b->loop_start > b->start && b->loop_start < b->end ? (uint64_t)(b->loop_start - b->start) : 0;
|
||||
continue;
|
||||
}
|
||||
v->valid[v->index] = 0;
|
||||
v->consumed++;
|
||||
v->offset = 0;
|
||||
v->loops_done = 0;
|
||||
v->index = (v->index + 1) & 3;
|
||||
if (b->end_of_stream) {
|
||||
v->played = 0;
|
||||
break;
|
||||
}
|
||||
skip_empty(r, v);
|
||||
}
|
||||
}
|
||||
|
||||
static void render_thread(void *p) {
|
||||
AudioRenderer *r = p;
|
||||
plat_thread_priority(PLAT_PRIO_HIGHEST);
|
||||
const uint64_t frame_ns = (uint64_t)r->param.sample_count * 1000000000ull / (uint64_t)r->param.sample_rate;
|
||||
const double frame = (double)r->param.sample_count / r->param.sample_rate;
|
||||
// When the device pulls a period (1024 samples on PipeWire, 4-5 frames) the ring drops below its target
|
||||
// and the frames would otherwise render back to back. On hardware the DSP renders one frame every 5 ms and
|
||||
// the game's audio thread works in between; AUX effects need that time to fill the return ring, or mix_aux
|
||||
// reads stale samples and the frame clicks at its seams. Device-paced frames stay at least 4/5 of a frame
|
||||
// apart, which still refills a period well before the device drains it. Linux only: on Windows SDL pulls
|
||||
// 480-sample periods, and a sink dump (NX2NAVI_AUDIO_DUMP) of the bursts there showed no more seam spikes than
|
||||
// chance, so a wait on this thread buys nothing.
|
||||
#ifdef __linux__
|
||||
const uint64_t gap_ns = frame_ns * 4 / 5;
|
||||
#else
|
||||
const uint64_t gap_ns = 0;
|
||||
#endif
|
||||
uint64_t next = kern_now_ns(), last = 0;
|
||||
while (plat_cas32(&r->running, 0, 0)) {
|
||||
uint64_t now = kern_now_ns();
|
||||
int64_t pace = audio_mixer_pace_ns(r, now);
|
||||
if (pace > 0) {
|
||||
plat_wait_ns((uint64_t)pace);
|
||||
continue;
|
||||
}
|
||||
if (pace == 0 && last && now - last < gap_ns) {
|
||||
plat_wait_ns(gap_ns - (now - last));
|
||||
continue;
|
||||
}
|
||||
if (pace == 0) {
|
||||
next = now + frame_ns; // where the timer goes on if the device stops pacing
|
||||
} else {
|
||||
if (next > now) plat_wait_ns(next - now);
|
||||
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
|
||||
next += frame_ns;
|
||||
}
|
||||
last = kern_now_ns();
|
||||
plat_mutex_lock(&r->lock);
|
||||
if (r->state == 0) {
|
||||
voices_prepare(r);
|
||||
audio_mixer_render(r, frame);
|
||||
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i]);
|
||||
r->elapsed_frames++;
|
||||
uint64_t given = r->stop_released + r->empty_played + r->flushed;
|
||||
if ((rt_trace & RT_TRACE_IPC) && given != r->logged_released && r->elapsed_frames % 1000 == 0) {
|
||||
r->logged_released = given;
|
||||
rt_log("audren: wave buffers given back: %llu by stopped voices, %llu without samples, %llu flushed",
|
||||
(unsigned long long)r->stop_released, (unsigned long long)r->empty_played,
|
||||
(unsigned long long)r->flushed);
|
||||
}
|
||||
}
|
||||
plat_mutex_unlock(&r->lock);
|
||||
kern_event_signal(r->event);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- IAudioRenderer
|
||||
static IpcBuffer pick(const IpcBuffer *a, unsigned na, const IpcBuffer *b, unsigned nb, unsigned i) {
|
||||
IpcBuffer x = i < na ? a[i] : (IpcBuffer){0}, y = i < nb ? b[i] : (IpcBuffer){0};
|
||||
return x.size ? x : y;
|
||||
}
|
||||
|
||||
static void renderer_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
||||
AudioRenderer *r = s->state;
|
||||
switch (rq->cmd) {
|
||||
case 0: { int32_t v = r->param.sample_rate; IPC_OUT(rs, v); return; } // GetSampleRate
|
||||
case 1: { int32_t v = r->param.sample_count; IPC_OUT(rs, v); return; } // GetSampleCount
|
||||
case 2: { int32_t v = r->param.mix_buffer_count; IPC_OUT(rs, v); return; } // GetMixBufferCount
|
||||
case 3: { uint32_t v = r->state; IPC_OUT(rs, v); return; } // GetState
|
||||
case 4: case 10: { // RequestUpdate(Auto): out(output), out(performance), in(input)
|
||||
IpcBuffer in = pick(rq->a, rq->na, rq->x, rq->nx, 0);
|
||||
IpcBuffer out = pick(rq->b, rq->nb, rq->c, rq->nc, 0), perf = pick(rq->b, rq->nb, rq->c, rq->nc, 1);
|
||||
if (!r->logged)
|
||||
rt_log("audren: RequestUpdate cmd %u in=%#llx+%#llx out=%#llx+%#llx perf=%#llx+%#llx (a%u b%u x%u c%u)",
|
||||
rq->cmd, (unsigned long long)in.addr, (unsigned long long)in.size, (unsigned long long)out.addr,
|
||||
(unsigned long long)out.size, (unsigned long long)perf.addr, (unsigned long long)perf.size, rq->na,
|
||||
rq->nb, rq->nx, rq->nc);
|
||||
need(in.size && out.size, "RequestUpdate buffers");
|
||||
request_update(r, ipc_guest(in.addr, in.size), in.size, ipc_guest(out.addr, out.size), out.size, perf.size);
|
||||
for (unsigned i = 0; i < rq->nc && rq->cmd == 10; i++)
|
||||
if (rq->c[i].size && (i >= rq->nb || !rq->b[i].size))
|
||||
rs->xout[rs->nxout++] = (IpcBuffer){rq->c[i].addr, rq->c[i].size, i};
|
||||
return;
|
||||
}
|
||||
case 5: // Start
|
||||
plat_mutex_lock(&r->lock);
|
||||
r->state = 0;
|
||||
audio_mixer_set_running(r, 1);
|
||||
plat_mutex_unlock(&r->lock);
|
||||
return;
|
||||
case 6: // Stop
|
||||
plat_mutex_lock(&r->lock);
|
||||
r->state = 1;
|
||||
audio_mixer_set_running(r, 0);
|
||||
plat_mutex_unlock(&r->lock);
|
||||
return;
|
||||
case 7: rs->copy[rs->ncopy++] = kern_event_readable(r->event); return; // QuerySystemEvent
|
||||
case 8: r->time_limit = IPC_ARG(rq, uint32_t, 0); return; // SetRenderingTimeLimit
|
||||
case 9: { uint32_t v = r->time_limit ? r->time_limit : 100; IPC_OUT(rs, v); return; } // GetRenderingTimeLimit
|
||||
default: ipc_unimplemented(s, rq);
|
||||
}
|
||||
}
|
||||
|
||||
static void renderer_close(void *p) {
|
||||
AudioRenderer *r = p;
|
||||
plat_xchg32(&r->running, 0);
|
||||
int away = kern_core_away();
|
||||
plat_thread_join(r->thread);
|
||||
kern_core_back(&away);
|
||||
plat_dec32(&g_live);
|
||||
audio_mixer_close(r);
|
||||
kern_handle_close(r->event);
|
||||
rt_log("audren: renderer closed after %llu frames", (unsigned long long)r->elapsed_frames);
|
||||
renderer_free(r);
|
||||
}
|
||||
|
||||
// ---- IAudioRendererManager
|
||||
void audren_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
|
||||
switch (rq->cmd) {
|
||||
case 0: { // OpenAudioRenderer(param, pad, work size, aruid; copy tmem, process; pid)
|
||||
AudrenParameter p = IPC_ARG(rq, AudrenParameter, 0);
|
||||
uint64_t work = IPC_ARG(rq, uint64_t, 0x38);
|
||||
need(rq->ncopy >= 1, "OpenAudioRenderer without a transfer memory handle");
|
||||
IpcTransferMemory *tm = kern_handle_get(rq->copy[0], KERN_TMEM);
|
||||
need(tm && tm->size >= work, "OpenAudioRenderer work buffer is not a transfer memory of the stated size");
|
||||
AudioRenderer *r = renderer_new(&p);
|
||||
r->running = 1;
|
||||
audio_mixer_open(r);
|
||||
r->thread = plat_thread_new(render_thread, r, "audren");
|
||||
if (!r->thread) rt_fatal("audren: cannot start the render thread");
|
||||
plat_inc32(&g_live);
|
||||
rt_log("audren: renderer open %d Hz x%d, mix buffers %d, voices %d, effects %d, submixes %d, sinks %d, "
|
||||
"splitters %d/%d, REV%d, work buffer %#llx+%#llx",
|
||||
p.sample_rate, p.sample_count, p.mix_buffer_count, p.voice_count, p.effect_count, p.submix_count,
|
||||
p.sink_count, p.splitter_count, p.splitter_destination_count, revision_number(p.revision),
|
||||
(unsigned long long)tm->addr, (unsigned long long)work);
|
||||
rs->move[rs->nmove++] = ipc_session_new("IAudioRenderer", renderer_handler, r, renderer_close);
|
||||
return;
|
||||
}
|
||||
case 1: { // GetWorkBufferSize(param) -> u64
|
||||
AudrenParameter p = IPC_ARG(rq, AudrenParameter, 0);
|
||||
need(p.sample_rate == 48000 || p.sample_rate == 32000, "GetWorkBufferSize: unsupported output rate");
|
||||
uint64_t v = AUDREN_WORK_BUFFER_BYTES;
|
||||
IPC_OUT(rs, v);
|
||||
return;
|
||||
}
|
||||
case 2: case 4: // GetAudioDeviceService(aruid), GetAudioDeviceServiceWithRevisionInfo(revision, aruid)
|
||||
rs->move[rs->nmove++] = ipc_session_new("IAudioDevice", audio_device_handler, NULL, NULL);
|
||||
return;
|
||||
default: ipc_unimplemented(s, rq);
|
||||
}
|
||||
}
|
||||
189
runtime/audren.h
Normal file
189
runtime/audren.h
Normal file
@@ -0,0 +1,189 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/audio_renderer.h and audio_mixer.h @ b75be8c (MIT, Copyright (c)
|
||||
// 2026 MacNativeGames contributors). Wire layouts follow the public libnx audren.h description.
|
||||
// audren:u, the audio renderer service: RequestUpdate wire formats, the server-side
|
||||
// renderer state, and the mixer interface (audio_mixer.c: the real mixer with SDL3 output).
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ipc.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
typedef struct {
|
||||
int32_t sample_rate, sample_count, mix_buffer_count, submix_count, voice_count, sink_count, effect_count,
|
||||
performance_frame_count;
|
||||
uint8_t voice_drop_enabled, unknown, rendering_device, execution_mode;
|
||||
int32_t splitter_count, splitter_destination_count;
|
||||
uint32_t external_context_size, revision;
|
||||
} AudrenParameter;
|
||||
_Static_assert(sizeof(AudrenParameter) == 0x34, "AudioRendererParameterInternal");
|
||||
|
||||
typedef struct {
|
||||
uint32_t revision, behavior, memory_pools, voices, voice_resources, effects, mixes, sinks, performance,
|
||||
splitters, render_info, reserved[4], total;
|
||||
} AudrenHeader;
|
||||
_Static_assert(sizeof(AudrenHeader) == 0x40, "UpdateDataHeader");
|
||||
|
||||
typedef struct { uint64_t address, size; uint32_t state, pad[3]; } AudrenPoolIn;
|
||||
typedef struct { uint32_t state, pad[3]; } AudrenPoolOut;
|
||||
enum { POOL_INVALID, POOL_NEW, POOL_REQUEST_DETACH, POOL_DETACHED, POOL_REQUEST_ATTACH, POOL_ATTACHED, POOL_RELEASED };
|
||||
|
||||
typedef struct { uint32_t id; float mix[24]; uint8_t in_use, pad[11]; } AudrenChannelIn;
|
||||
_Static_assert(sizeof(AudrenChannelIn) == 0x70, "voice channel resource");
|
||||
|
||||
typedef struct { uint8_t enable, pad; int16_t numerator[3], denominator[2]; } AudrenBiquad;
|
||||
typedef struct { uint8_t enable, pad[3]; float numerator[3], denominator[2]; } AudrenBiquadFloat;
|
||||
_Static_assert(sizeof(AudrenBiquadFloat) == 0x18, "REV15 float biquad");
|
||||
|
||||
typedef struct {
|
||||
uint64_t address, size;
|
||||
int32_t start, end;
|
||||
uint8_t loop, end_of_stream, sent, pad;
|
||||
int32_t loop_count;
|
||||
uint64_t context_address, context_size;
|
||||
int32_t loop_start, loop_end;
|
||||
} AudrenWaveBuffer;
|
||||
_Static_assert(sizeof(AudrenWaveBuffer) == 0x38, "wave buffer");
|
||||
|
||||
typedef struct {
|
||||
uint32_t id, node_id;
|
||||
uint8_t is_new, in_use, play_state, sample_format;
|
||||
uint32_t sample_rate;
|
||||
int32_t priority, sort_order;
|
||||
uint32_t channel_count;
|
||||
float pitch, volume;
|
||||
AudrenBiquad biquads[2];
|
||||
uint32_t wave_buffer_count;
|
||||
uint16_t wave_buffer_index, pad0;
|
||||
uint32_t pad1;
|
||||
uint64_t extra_address, extra_size;
|
||||
int32_t mix_id;
|
||||
uint32_t splitter_id;
|
||||
AudrenWaveBuffer wave_buffers[4];
|
||||
int32_t channel_ids[6];
|
||||
uint8_t clear_voice_drop, flush_wave_buffer_count, pad2[2], flags, pad3, src_quality, pad4[17];
|
||||
} AudrenVoiceIn;
|
||||
_Static_assert(sizeof(AudrenVoiceIn) == 0x170, "voice in parameter");
|
||||
// REV15 widens only the two filter records (Q14 -> float); the fields after them move by 24 bytes.
|
||||
typedef struct { uint8_t prefix[0x24]; AudrenBiquadFloat biquads[2]; uint8_t tail[0x134]; } AudrenVoiceInV2;
|
||||
_Static_assert(sizeof(AudrenVoiceInV2) == 0x188, "REV15 voice in parameter");
|
||||
typedef struct { uint64_t played_samples; uint32_t wave_buffers_consumed, voice_drops; } AudrenVoiceOut;
|
||||
enum { PLAY_STARTED, PLAY_STOPPED, PLAY_PAUSED };
|
||||
enum { FMT_INVALID, FMT_PCM8, FMT_PCM16, FMT_PCM24, FMT_PCM32, FMT_PCMF32, FMT_ADPCM };
|
||||
|
||||
typedef struct {
|
||||
float volume;
|
||||
uint32_t sample_rate;
|
||||
int32_t buffer_count;
|
||||
uint8_t in_use, dirty, pad0[2];
|
||||
int32_t mix_id;
|
||||
uint32_t effect_count, node_id, pad1[2];
|
||||
float mix[24][24];
|
||||
int32_t dest_mix_id;
|
||||
uint32_t dest_splitter_id, pad2;
|
||||
} AudrenMixIn;
|
||||
_Static_assert(sizeof(AudrenMixIn) == 0x930, "mix in parameter");
|
||||
|
||||
typedef struct {
|
||||
uint8_t type, in_use, pad0[2];
|
||||
uint32_t node_id;
|
||||
uint64_t pad1[3];
|
||||
char name[0x100];
|
||||
uint32_t input_count;
|
||||
int8_t inputs[6];
|
||||
uint8_t pad2, downmix_enabled;
|
||||
float downmix[4];
|
||||
} AudrenSinkIn;
|
||||
_Static_assert(sizeof(AudrenSinkIn) == 0x140, "sink in parameter");
|
||||
typedef struct { uint32_t last_written_offset, pad0; uint64_t pad1[3]; } AudrenSinkOut;
|
||||
enum { SINK_INVALID, SINK_DEVICE, SINK_CIRCULAR };
|
||||
|
||||
typedef struct {
|
||||
uint8_t type, is_new, enabled, pad0;
|
||||
int32_t mix_id;
|
||||
uint64_t buffer_address, buffer_size;
|
||||
int32_t order;
|
||||
uint32_t pad1;
|
||||
uint8_t specific[0xa0];
|
||||
} AudrenEffectIn;
|
||||
_Static_assert(sizeof(AudrenEffectIn) == 0xc0, "effect in parameter");
|
||||
|
||||
typedef struct {
|
||||
uint8_t input[6], output[6]; uint16_t channel_max, channels;
|
||||
int32_t sample_rate, lookahead_max, attack_time, release_time, lookahead;
|
||||
float attack, release, threshold, input_gain, output_gain;
|
||||
int32_t delay_min, delay_max; uint8_t status, statistics_enabled, statistics_reset, reserved;
|
||||
} AudrenLimiterParam;
|
||||
_Static_assert(sizeof(AudrenLimiterParam) == 68, "limiter effect wire layout");
|
||||
|
||||
typedef struct {
|
||||
uint8_t input[24], output[24]; uint32_t channels, sample_rate, capacity, channel_max;
|
||||
uint64_t send_info, send_storage, return_info, return_storage;
|
||||
uint32_t sample_size, total_samples, frame_samples;
|
||||
} AudrenAuxParam;
|
||||
_Static_assert(__builtin_offsetof(AudrenAuxParam, send_info) == 64, "auxiliary effect wire layout");
|
||||
|
||||
#define AUDREN_REV(n) (0x30564552u + ((uint32_t)(n) << 24)) // "REVn"
|
||||
#define AUDREN_WORK_BUFFER_BYTES 0x10000u // the host keeps its state in host memory
|
||||
#define AUDREN_BEHAVIOR_OUT 0xb0u
|
||||
#define AUDREN_RENDER_INFO_OUT 0x10u
|
||||
|
||||
// Server-side voice: the DSP's view of one client voice (Ryujinx.Audio VoiceInfo, as ported in MacNativeGames).
|
||||
typedef struct {
|
||||
AudrenVoiceIn in;
|
||||
AudrenBiquadFloat float_biquads[2];
|
||||
uint8_t use_float_biquads;
|
||||
AudrenWaveBuffer buffers[4];
|
||||
uint8_t valid[4];
|
||||
uint8_t pending[4]; // sent while the voice was not playing: valid once it plays
|
||||
uint8_t state; // SV_*: the client's Start / Stop / Pause, plus Stopping until the next frame
|
||||
uint32_t flush, flush_from; // FlushWaveBuffers since the last frame, from the client's current buffer
|
||||
unsigned index;
|
||||
uint64_t offset;
|
||||
double fraction;
|
||||
int32_t loops_done;
|
||||
uint64_t played;
|
||||
uint32_t consumed, drops;
|
||||
struct { int16_t history[2]; int32_t buffer; uint64_t at; } adpcm; // DSP-ADPCM decode cursor (buffer slot, next sample) and history
|
||||
float biquad_state[2][6][2]; // filter, channel: transposed direct-form II delays
|
||||
} ServerVoice;
|
||||
enum { SV_STOPPED, SV_STARTED, SV_PAUSED, SV_STOPPING };
|
||||
|
||||
typedef struct AudioRenderer {
|
||||
AudrenParameter param;
|
||||
PlatMutex lock;
|
||||
PlatThread *thread;
|
||||
volatile int32_t running;
|
||||
uint32_t state, revision, time_limit, event; // state 0 started, 1 stopped; event: writable handle
|
||||
int logged;
|
||||
uint64_t behavior_flags, elapsed_frames;
|
||||
uint64_t stop_released, empty_played, flushed, logged_released;
|
||||
ServerVoice *voices;
|
||||
AudrenPoolIn *pools;
|
||||
AudrenMixIn *mixes;
|
||||
AudrenSinkIn *sinks;
|
||||
AudrenEffectIn *effects;
|
||||
AudrenChannelIn *channels;
|
||||
void *mixer;
|
||||
} AudioRenderer;
|
||||
|
||||
// IAudioRendererManager, served under "audren:u" (ipc.c's service table).
|
||||
void audren_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs);
|
||||
long audren_renderers_live(void); // renderers whose render tick runs
|
||||
|
||||
// Mixer interface (audio_mixer.c). Every call but open/close/set_running runs with r->lock held.
|
||||
void audio_mixer_open(AudioRenderer *r);
|
||||
void audio_mixer_set_running(AudioRenderer *r, int running);
|
||||
void audio_mixer_close(AudioRenderer *r);
|
||||
void audio_mixer_update_routing(AudioRenderer *r, const uint8_t *splitters, uint32_t splitter_bytes,
|
||||
const uint8_t *mixes, uint32_t mix_bytes, int mix_count);
|
||||
void audio_mixer_update_effects(AudioRenderer *r);
|
||||
void audio_mixer_effect_result(AudioRenderer *r, int id, void *output, unsigned size);
|
||||
uint32_t audio_mixer_sink_written(AudioRenderer *r, int id);
|
||||
void audio_mixer_render(AudioRenderer *r, double seconds); // after audio_voices_prepare, before voices advance
|
||||
// Device pacing for the render thread at kern_now_ns `now`: 0 renders the next frame now, a positive value waits that
|
||||
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
|
||||
// device stalled).
|
||||
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames); // source samples over one frame; advances v->fraction
|
||||
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
|
||||
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
|
||||
253
runtime/bc3_encode_spv.h
Normal file
253
runtime/bc3_encode_spv.h
Normal file
@@ -0,0 +1,253 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/bc3_encode.comp; do not edit.
|
||||
// source sha256 ac318d358136b39a113a18474a815f447223667657c90f9e0356a392639d8541
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t bc3_encode_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x00000314, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000007f, 0x00060010, 0x00000004,
|
||||
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00030047, 0x0000001b, 0x00000002, 0x00050048,
|
||||
0x0000001b, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000001b, 0x00000001, 0x00000023,
|
||||
0x00000004, 0x00050048, 0x0000001b, 0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x0000001b,
|
||||
0x00000003, 0x00000023, 0x0000000c, 0x00050048, 0x0000001b, 0x00000004, 0x00000023, 0x00000010,
|
||||
0x00050048, 0x0000001b, 0x00000005, 0x00000023, 0x00000014, 0x00040047, 0x0000002c, 0x00000006,
|
||||
0x00000004, 0x00030047, 0x0000002d, 0x00000002, 0x00040048, 0x0000002d, 0x00000000, 0x00000018,
|
||||
0x00050048, 0x0000002d, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x0000002f, 0x00000018,
|
||||
0x00040047, 0x0000002f, 0x00000021, 0x00000000, 0x00040047, 0x0000002f, 0x00000022, 0x00000000,
|
||||
0x00040047, 0x0000007f, 0x0000000b, 0x0000001c, 0x00040047, 0x000001e9, 0x00000006, 0x00000010,
|
||||
0x00030047, 0x000001ea, 0x00000002, 0x00040048, 0x000001ea, 0x00000000, 0x00000019, 0x00050048,
|
||||
0x000001ea, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x000001ec, 0x00000019, 0x00040047,
|
||||
0x000001ec, 0x00000021, 0x00000001, 0x00040047, 0x000001ec, 0x00000022, 0x00000000, 0x00040047,
|
||||
0x0000020c, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
|
||||
0x00040015, 0x00000006, 0x00000020, 0x00000000, 0x00030016, 0x00000008, 0x00000020, 0x00040017,
|
||||
0x00000009, 0x00000008, 0x00000004, 0x00040017, 0x00000010, 0x00000008, 0x00000003, 0x0008001e,
|
||||
0x0000001b, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00040020,
|
||||
0x0000001c, 0x00000009, 0x0000001b, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000009, 0x00040015,
|
||||
0x0000001e, 0x00000020, 0x00000001, 0x0004002b, 0x0000001e, 0x0000001f, 0x00000002, 0x00040020,
|
||||
0x00000020, 0x00000009, 0x00000006, 0x0004002b, 0x00000006, 0x00000023, 0x00000001, 0x0004002b,
|
||||
0x0000001e, 0x00000027, 0x00000003, 0x0003001d, 0x0000002c, 0x00000006, 0x0003001e, 0x0000002d,
|
||||
0x0000002c, 0x00040020, 0x0000002e, 0x0000000c, 0x0000002d, 0x0004003b, 0x0000002e, 0x0000002f,
|
||||
0x0000000c, 0x0004002b, 0x0000001e, 0x00000030, 0x00000000, 0x00040020, 0x0000003f, 0x0000000c,
|
||||
0x00000006, 0x0004002b, 0x00000008, 0x00000043, 0x437f0000, 0x00040017, 0x00000047, 0x00000006,
|
||||
0x00000003, 0x0004002b, 0x00000008, 0x0000004b, 0x00000000, 0x0004002b, 0x00000006, 0x00000057,
|
||||
0x00000000, 0x0004002b, 0x0000001e, 0x0000005a, 0x0000000b, 0x0004002b, 0x0000001e, 0x0000005e,
|
||||
0x00000005, 0x0004002b, 0x00000006, 0x00000061, 0x00000002, 0x0004002b, 0x00000006, 0x00000069,
|
||||
0x0000001f, 0x0004002b, 0x00000006, 0x00000070, 0x0000003f, 0x00040020, 0x0000007e, 0x00000001,
|
||||
0x00000047, 0x0004003b, 0x0000007e, 0x0000007f, 0x00000001, 0x0004002b, 0x00000006, 0x00000084,
|
||||
0x00000003, 0x0004002b, 0x00000006, 0x00000086, 0x00000004, 0x00020014, 0x00000088, 0x0004002b,
|
||||
0x0000001e, 0x0000008b, 0x00000004, 0x00040020, 0x0000009a, 0x00000007, 0x00000009, 0x0007002c,
|
||||
0x00000009, 0x0000009c, 0x00000043, 0x00000043, 0x00000043, 0x00000043, 0x0007002c, 0x00000009,
|
||||
0x0000009e, 0x0000004b, 0x0000004b, 0x0000004b, 0x0000004b, 0x0004002b, 0x00000006, 0x000000a7,
|
||||
0x00000010, 0x0004001c, 0x000000a9, 0x00000009, 0x000000a7, 0x00040020, 0x000000aa, 0x00000007,
|
||||
0x000000a9, 0x0004002b, 0x0000001e, 0x000000d0, 0x00000001, 0x00040020, 0x000000d7, 0x00000007,
|
||||
0x00000008, 0x00040017, 0x000000df, 0x00000006, 0x00000002, 0x0004002b, 0x0000001e, 0x000000e4,
|
||||
0x00000008, 0x0004002b, 0x00000008, 0x00000101, 0x40e00000, 0x0004002b, 0x00000006, 0x00000110,
|
||||
0x00000007, 0x0004002b, 0x00000006, 0x0000011f, 0x00000020, 0x0004002b, 0x00000008, 0x000001b5,
|
||||
0x358637bd, 0x0004002b, 0x00000008, 0x000001ca, 0x40400000, 0x00040017, 0x000001e8, 0x00000006,
|
||||
0x00000004, 0x0003001d, 0x000001e9, 0x000001e8, 0x0003001e, 0x000001ea, 0x000001e9, 0x00040020,
|
||||
0x000001eb, 0x0000000c, 0x000001ea, 0x0004003b, 0x000001eb, 0x000001ec, 0x0000000c, 0x0004002b,
|
||||
0x0000001e, 0x00000204, 0x00000010, 0x00040020, 0x00000209, 0x0000000c, 0x000001e8, 0x0004002b,
|
||||
0x00000006, 0x0000020b, 0x00000008, 0x0006002c, 0x00000047, 0x0000020c, 0x0000020b, 0x0000020b,
|
||||
0x00000023, 0x0006002c, 0x00000010, 0x00000302, 0x0000004b, 0x0000004b, 0x0000004b, 0x0006002c,
|
||||
0x00000010, 0x00000303, 0x00000043, 0x00000043, 0x00000043, 0x0004002b, 0x00000008, 0x00000304,
|
||||
0x3d800000, 0x0007002c, 0x00000009, 0x00000305, 0x00000304, 0x00000304, 0x00000304, 0x00000304,
|
||||
0x0006002c, 0x00000010, 0x00000306, 0x00000304, 0x00000304, 0x00000304, 0x0004002b, 0x00000008,
|
||||
0x00000309, 0x3df8f8fa, 0x0004002b, 0x00000008, 0x0000030a, 0x3e7cfcfe, 0x0006002c, 0x00000010,
|
||||
0x0000030b, 0x00000309, 0x0000030a, 0x00000309, 0x0004002b, 0x00000008, 0x0000030d, 0x41039ce7,
|
||||
0x0004002b, 0x00000008, 0x0000030f, 0x40818619, 0x0004002b, 0x00000006, 0x00000310, 0x00000013,
|
||||
0x0004002b, 0x00000006, 0x00000311, 0xfffffff0, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
|
||||
0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x000000aa, 0x000000ab, 0x00000007, 0x000300f7,
|
||||
0x0000020d, 0x00000000, 0x000300fb, 0x00000057, 0x0000020e, 0x000200f8, 0x0000020e, 0x0004003d,
|
||||
0x00000047, 0x00000080, 0x0000007f, 0x00050041, 0x00000020, 0x00000082, 0x0000001d, 0x00000027,
|
||||
0x0004003d, 0x00000006, 0x00000083, 0x00000082, 0x00050080, 0x00000006, 0x00000085, 0x00000083,
|
||||
0x00000084, 0x00050086, 0x00000006, 0x00000087, 0x00000085, 0x00000086, 0x00050051, 0x00000006,
|
||||
0x0000008a, 0x00000080, 0x00000000, 0x00050041, 0x00000020, 0x0000008c, 0x0000001d, 0x0000008b,
|
||||
0x0004003d, 0x00000006, 0x0000008d, 0x0000008c, 0x000500ae, 0x00000088, 0x0000008e, 0x0000008a,
|
||||
0x0000008d, 0x000400a8, 0x00000088, 0x0000008f, 0x0000008e, 0x000300f7, 0x00000091, 0x00000000,
|
||||
0x000400fa, 0x0000008f, 0x00000090, 0x00000091, 0x000200f8, 0x00000090, 0x00050051, 0x00000006,
|
||||
0x00000093, 0x00000080, 0x00000001, 0x000500ae, 0x00000088, 0x00000095, 0x00000093, 0x00000087,
|
||||
0x000200f9, 0x00000091, 0x000200f8, 0x00000091, 0x000700f5, 0x00000088, 0x00000096, 0x0000008e,
|
||||
0x0000020e, 0x00000095, 0x00000090, 0x000300f7, 0x00000098, 0x00000000, 0x000400fa, 0x00000096,
|
||||
0x00000097, 0x00000098, 0x000200f8, 0x00000097, 0x000200f9, 0x0000020d, 0x000200f8, 0x00000098,
|
||||
0x000200f9, 0x000000a1, 0x000200f8, 0x000000a1, 0x000700f5, 0x00000009, 0x000002b0, 0x0000009c,
|
||||
0x00000098, 0x000000c4, 0x000000a2, 0x000700f5, 0x00000009, 0x000002af, 0x0000009e, 0x00000098,
|
||||
0x000000c9, 0x000000a2, 0x000700f5, 0x00000009, 0x000002ae, 0x0000009e, 0x00000098, 0x000000ce,
|
||||
0x000000a2, 0x000700f5, 0x00000006, 0x000002ad, 0x00000057, 0x00000098, 0x000000d1, 0x000000a2,
|
||||
0x000500b0, 0x00000088, 0x000000a8, 0x000002ad, 0x000000a7, 0x000400f6, 0x000000a3, 0x000000a2,
|
||||
0x00000000, 0x000400fa, 0x000000a8, 0x000000a2, 0x000000a3, 0x000200f8, 0x000000a2, 0x00050084,
|
||||
0x00000006, 0x000000af, 0x0000008a, 0x00000086, 0x000500c7, 0x00000006, 0x000000b1, 0x000002ad,
|
||||
0x00000084, 0x00050080, 0x00000006, 0x000000b2, 0x000000af, 0x000000b1, 0x00050051, 0x00000006,
|
||||
0x000000b4, 0x00000080, 0x00000001, 0x00050084, 0x00000006, 0x000000b5, 0x000000b4, 0x00000086,
|
||||
0x000500c2, 0x00000006, 0x000000b7, 0x000002ad, 0x0000001f, 0x00050080, 0x00000006, 0x000000b8,
|
||||
0x000000b5, 0x000000b7, 0x00050051, 0x00000006, 0x000000bd, 0x00000080, 0x00000002, 0x00050041,
|
||||
0x00000020, 0x00000216, 0x0000001d, 0x0000001f, 0x0004003d, 0x00000006, 0x00000217, 0x00000216,
|
||||
0x00050082, 0x00000006, 0x00000218, 0x00000217, 0x00000023, 0x0007000c, 0x00000006, 0x00000219,
|
||||
0x00000001, 0x00000026, 0x000000b2, 0x00000218, 0x00050082, 0x00000006, 0x0000021d, 0x00000083,
|
||||
0x00000023, 0x0007000c, 0x00000006, 0x0000021e, 0x00000001, 0x00000026, 0x000000b8, 0x0000021d,
|
||||
0x00050041, 0x00000020, 0x0000021f, 0x0000001d, 0x00000030, 0x0004003d, 0x00000006, 0x00000220,
|
||||
0x0000021f, 0x00050084, 0x00000006, 0x00000224, 0x000000bd, 0x00000083, 0x00050080, 0x00000006,
|
||||
0x00000226, 0x00000224, 0x0000021e, 0x00050084, 0x00000006, 0x00000229, 0x00000226, 0x00000217,
|
||||
0x00050080, 0x00000006, 0x0000022a, 0x00000220, 0x00000229, 0x00050080, 0x00000006, 0x0000022c,
|
||||
0x0000022a, 0x00000219, 0x00060041, 0x0000003f, 0x0000022d, 0x0000002f, 0x00000030, 0x0000022c,
|
||||
0x0004003d, 0x00000006, 0x0000022e, 0x0000022d, 0x0006000c, 0x00000009, 0x0000022f, 0x00000001,
|
||||
0x00000040, 0x0000022e, 0x0005008e, 0x00000009, 0x00000230, 0x0000022f, 0x00000043, 0x00050041,
|
||||
0x0000009a, 0x000000bf, 0x000000ab, 0x000002ad, 0x0003003e, 0x000000bf, 0x00000230, 0x0004003d,
|
||||
0x00000009, 0x000000c3, 0x000000bf, 0x0007000c, 0x00000009, 0x000000c4, 0x00000001, 0x00000025,
|
||||
0x000002b0, 0x000000c3, 0x0004003d, 0x00000009, 0x000000c8, 0x000000bf, 0x0007000c, 0x00000009,
|
||||
0x000000c9, 0x00000001, 0x00000028, 0x000002af, 0x000000c8, 0x0004003d, 0x00000009, 0x000000cc,
|
||||
0x000000bf, 0x00050081, 0x00000009, 0x000000ce, 0x000002ae, 0x000000cc, 0x00050080, 0x00000006,
|
||||
0x000000d1, 0x000002ad, 0x000000d0, 0x000200f9, 0x000000a1, 0x000200f8, 0x000000a3, 0x00050085,
|
||||
0x00000009, 0x000000d5, 0x000002ae, 0x00000305, 0x00050051, 0x00000008, 0x000000d9, 0x000002af,
|
||||
0x00000003, 0x0004006d, 0x00000006, 0x000000da, 0x000000d9, 0x00050051, 0x00000008, 0x000000dd,
|
||||
0x000002b0, 0x00000003, 0x0004006d, 0x00000006, 0x000000de, 0x000000dd, 0x000500c4, 0x00000006,
|
||||
0x000000e5, 0x000000de, 0x000000e4, 0x000500c5, 0x00000006, 0x000000e6, 0x000000da, 0x000000e5,
|
||||
0x00050050, 0x000000df, 0x000000e7, 0x000000e6, 0x00000057, 0x000500ac, 0x00000088, 0x000000ea,
|
||||
0x000000da, 0x000000de, 0x000300f7, 0x000000ec, 0x00000000, 0x000400fa, 0x000000ea, 0x000000eb,
|
||||
0x000000ec, 0x000200f8, 0x000000eb, 0x00050082, 0x00000006, 0x000000f0, 0x000000da, 0x000000de,
|
||||
0x00040070, 0x00000008, 0x000000f1, 0x000000f0, 0x000200f9, 0x000000f3, 0x000200f8, 0x000000f3,
|
||||
0x000700f5, 0x000000df, 0x000002c8, 0x000000e7, 0x000000eb, 0x000002f1, 0x000000f6, 0x000700f5,
|
||||
0x00000006, 0x000002b1, 0x00000057, 0x000000eb, 0x00000143, 0x000000f6, 0x000500b0, 0x00000088,
|
||||
0x000000f9, 0x000002b1, 0x000000a7, 0x000400f6, 0x000000f5, 0x000000f6, 0x00000000, 0x000400fa,
|
||||
0x000000f9, 0x000000f4, 0x000000f5, 0x000200f8, 0x000000f4, 0x00040070, 0x00000008, 0x000000fc,
|
||||
0x000000da, 0x00060041, 0x000000d7, 0x000000fe, 0x000000ab, 0x000002b1, 0x00000084, 0x0004003d,
|
||||
0x00000008, 0x000000ff, 0x000000fe, 0x00050083, 0x00000008, 0x00000100, 0x000000fc, 0x000000ff,
|
||||
0x00050085, 0x00000008, 0x00000102, 0x00000100, 0x00000101, 0x00050088, 0x00000008, 0x00000104,
|
||||
0x00000102, 0x000000f1, 0x0006000c, 0x00000008, 0x00000105, 0x00000001, 0x00000001, 0x00000104,
|
||||
0x0008000c, 0x00000008, 0x00000106, 0x00000001, 0x0000002b, 0x00000105, 0x0000004b, 0x00000101,
|
||||
0x0004006d, 0x00000006, 0x00000107, 0x00000106, 0x000500aa, 0x00000088, 0x0000010a, 0x00000107,
|
||||
0x00000057, 0x000300f7, 0x0000010d, 0x00000000, 0x000400fa, 0x0000010a, 0x0000010c, 0x0000010e,
|
||||
0x000200f8, 0x0000010c, 0x000200f9, 0x0000010d, 0x000200f8, 0x0000010e, 0x000500aa, 0x00000088,
|
||||
0x00000111, 0x00000107, 0x00000110, 0x000300f7, 0x00000114, 0x00000000, 0x000400fa, 0x00000111,
|
||||
0x00000113, 0x00000115, 0x000200f8, 0x00000113, 0x000200f9, 0x00000114, 0x000200f8, 0x00000115,
|
||||
0x00050080, 0x00000006, 0x00000117, 0x00000107, 0x00000023, 0x000200f9, 0x00000114, 0x000200f8,
|
||||
0x00000114, 0x000700f5, 0x00000006, 0x000002db, 0x00000023, 0x00000113, 0x00000117, 0x00000115,
|
||||
0x000200f9, 0x0000010d, 0x000200f8, 0x0000010d, 0x000700f5, 0x00000006, 0x000002dc, 0x00000057,
|
||||
0x0000010c, 0x000002db, 0x00000114, 0x00050084, 0x00000006, 0x0000011c, 0x00000084, 0x000002b1,
|
||||
0x00050080, 0x00000006, 0x0000011d, 0x000000a7, 0x0000011c, 0x000500b0, 0x00000088, 0x00000120,
|
||||
0x0000011d, 0x0000011f, 0x000300f7, 0x00000122, 0x00000000, 0x000400fa, 0x00000120, 0x00000121,
|
||||
0x00000122, 0x000200f8, 0x00000121, 0x000500c4, 0x00000006, 0x00000125, 0x000002dc, 0x0000011d,
|
||||
0x00050051, 0x00000006, 0x00000127, 0x000002c8, 0x00000000, 0x000500c5, 0x00000006, 0x00000128,
|
||||
0x00000127, 0x00000125, 0x00060052, 0x000000df, 0x0000028e, 0x00000128, 0x000002c8, 0x00000000,
|
||||
0x000200f9, 0x00000122, 0x000200f8, 0x00000122, 0x000700f5, 0x000000df, 0x000002e3, 0x000002c8,
|
||||
0x0000010d, 0x0000028e, 0x00000121, 0x00050080, 0x00000006, 0x0000012b, 0x0000011c, 0x00000310,
|
||||
0x000500ac, 0x00000088, 0x0000012c, 0x0000012b, 0x0000011f, 0x000300f7, 0x0000012e, 0x00000000,
|
||||
0x000400fa, 0x0000012c, 0x0000012d, 0x0000012e, 0x000200f8, 0x0000012d, 0x0007000c, 0x00000006,
|
||||
0x00000131, 0x00000001, 0x00000026, 0x0000011d, 0x0000011f, 0x00050082, 0x00000006, 0x00000132,
|
||||
0x0000011f, 0x00000131, 0x000500c2, 0x00000006, 0x00000133, 0x000002dc, 0x00000132, 0x000500ac,
|
||||
0x00000088, 0x00000135, 0x0000011d, 0x0000011f, 0x000300f7, 0x00000138, 0x00000000, 0x000400fa,
|
||||
0x00000135, 0x00000137, 0x0000013b, 0x000200f8, 0x00000137, 0x00050080, 0x00000006, 0x0000013a,
|
||||
0x0000011c, 0x00000311, 0x000200f9, 0x00000138, 0x000200f8, 0x0000013b, 0x000200f9, 0x00000138,
|
||||
0x000200f8, 0x00000138, 0x000700f5, 0x00000006, 0x000002e1, 0x0000013a, 0x00000137, 0x00000057,
|
||||
0x0000013b, 0x000500c4, 0x00000006, 0x0000013d, 0x00000133, 0x000002e1, 0x00050051, 0x00000006,
|
||||
0x0000013f, 0x000002e3, 0x00000001, 0x000500c5, 0x00000006, 0x00000140, 0x0000013f, 0x0000013d,
|
||||
0x00060052, 0x000000df, 0x00000291, 0x00000140, 0x000002e3, 0x00000001, 0x000200f9, 0x0000012e,
|
||||
0x000200f8, 0x0000012e, 0x000700f5, 0x000000df, 0x000002f1, 0x000002e3, 0x00000122, 0x00000291,
|
||||
0x00000138, 0x000200f9, 0x000000f6, 0x000200f8, 0x000000f6, 0x00050080, 0x00000006, 0x00000143,
|
||||
0x000002b1, 0x000000d0, 0x000200f9, 0x000000f3, 0x000200f8, 0x000000f5, 0x000200f9, 0x000000ec,
|
||||
0x000200f8, 0x000000ec, 0x000700f5, 0x000000df, 0x000002c7, 0x000000e7, 0x000000a3, 0x000002c8,
|
||||
0x000000f5, 0x0008004f, 0x00000010, 0x00000146, 0x000002b0, 0x000002b0, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x0008004f, 0x00000010, 0x00000149, 0x000002af, 0x000002af, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x000200f9, 0x0000014d, 0x000200f8, 0x0000014d, 0x000700f5, 0x00000008, 0x000002bb,
|
||||
0x0000004b, 0x000000ec, 0x00000169, 0x0000014e, 0x000700f5, 0x00000008, 0x000002b7, 0x0000004b,
|
||||
0x000000ec, 0x00000162, 0x0000014e, 0x000700f5, 0x00000006, 0x000002b6, 0x00000057, 0x000000ec,
|
||||
0x0000016b, 0x0000014e, 0x000500b0, 0x00000088, 0x00000153, 0x000002b6, 0x000000a7, 0x000400f6,
|
||||
0x0000014f, 0x0000014e, 0x00000000, 0x000400fa, 0x00000153, 0x0000014e, 0x0000014f, 0x000200f8,
|
||||
0x0000014e, 0x00050041, 0x0000009a, 0x00000156, 0x000000ab, 0x000002b6, 0x0004003d, 0x00000009,
|
||||
0x00000157, 0x00000156, 0x0008004f, 0x00000010, 0x00000158, 0x00000157, 0x00000157, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x0008004f, 0x00000010, 0x0000015a, 0x000000d5, 0x000000d5, 0x00000000,
|
||||
0x00000001, 0x00000002, 0x00050083, 0x00000010, 0x0000015b, 0x00000158, 0x0000015a, 0x00050051,
|
||||
0x00000008, 0x0000015d, 0x0000015b, 0x00000000, 0x00050051, 0x00000008, 0x0000015f, 0x0000015b,
|
||||
0x00000001, 0x00050085, 0x00000008, 0x00000160, 0x0000015d, 0x0000015f, 0x00050081, 0x00000008,
|
||||
0x00000162, 0x000002b7, 0x00000160, 0x00050051, 0x00000008, 0x00000164, 0x0000015b, 0x00000002,
|
||||
0x00050085, 0x00000008, 0x00000167, 0x00000164, 0x0000015f, 0x00050081, 0x00000008, 0x00000169,
|
||||
0x000002bb, 0x00000167, 0x00050080, 0x00000006, 0x0000016b, 0x000002b6, 0x000000d0, 0x000200f9,
|
||||
0x0000014d, 0x000200f8, 0x0000014f, 0x000500b8, 0x00000088, 0x0000016d, 0x000002b7, 0x0000004b,
|
||||
0x000300f7, 0x0000016f, 0x00000000, 0x000400fa, 0x0000016d, 0x0000016e, 0x0000016f, 0x000200f8,
|
||||
0x0000016e, 0x00050051, 0x00000008, 0x00000172, 0x000002b0, 0x00000000, 0x00050051, 0x00000008,
|
||||
0x00000174, 0x000002af, 0x00000000, 0x00060052, 0x00000010, 0x00000299, 0x00000174, 0x00000146,
|
||||
0x00000000, 0x00060052, 0x00000010, 0x0000029b, 0x00000172, 0x00000149, 0x00000000, 0x000200f9,
|
||||
0x0000016f, 0x000200f8, 0x0000016f, 0x000700f5, 0x00000010, 0x000002bd, 0x00000149, 0x0000014f,
|
||||
0x0000029b, 0x0000016e, 0x000700f5, 0x00000010, 0x000002bc, 0x00000146, 0x0000014f, 0x00000299,
|
||||
0x0000016e, 0x000500b8, 0x00000088, 0x00000179, 0x000002bb, 0x0000004b, 0x000300f7, 0x0000017b,
|
||||
0x00000000, 0x000400fa, 0x00000179, 0x0000017a, 0x0000017b, 0x000200f8, 0x0000017a, 0x00050051,
|
||||
0x00000008, 0x0000017e, 0x000002bc, 0x00000002, 0x00050051, 0x00000008, 0x00000180, 0x000002bd,
|
||||
0x00000002, 0x00060052, 0x00000010, 0x0000029f, 0x00000180, 0x000002bc, 0x00000002, 0x00060052,
|
||||
0x00000010, 0x000002a1, 0x0000017e, 0x000002bd, 0x00000002, 0x000200f9, 0x0000017b, 0x000200f8,
|
||||
0x0000017b, 0x000700f5, 0x00000010, 0x000002bf, 0x000002bc, 0x0000016f, 0x0000029f, 0x0000017a,
|
||||
0x000700f5, 0x00000010, 0x000002be, 0x000002bd, 0x0000016f, 0x000002a1, 0x0000017a, 0x00050083,
|
||||
0x00000010, 0x00000187, 0x000002be, 0x000002bf, 0x00050085, 0x00000010, 0x00000189, 0x00000187,
|
||||
0x00000306, 0x00050083, 0x00000010, 0x0000018d, 0x000002be, 0x00000189, 0x0008000c, 0x00000010,
|
||||
0x00000237, 0x00000001, 0x0000002b, 0x0000018d, 0x00000302, 0x00000303, 0x00050085, 0x00000010,
|
||||
0x0000023a, 0x00000237, 0x0000030b, 0x0006000c, 0x00000010, 0x0000023b, 0x00000001, 0x00000001,
|
||||
0x0000023a, 0x0004006d, 0x00000047, 0x0000023c, 0x0000023b, 0x00050051, 0x00000006, 0x0000023e,
|
||||
0x0000023c, 0x00000000, 0x000500c4, 0x00000006, 0x0000023f, 0x0000023e, 0x0000005a, 0x00050051,
|
||||
0x00000006, 0x00000241, 0x0000023c, 0x00000001, 0x000500c4, 0x00000006, 0x00000242, 0x00000241,
|
||||
0x0000005e, 0x000500c5, 0x00000006, 0x00000243, 0x0000023f, 0x00000242, 0x00050051, 0x00000006,
|
||||
0x00000245, 0x0000023c, 0x00000002, 0x000500c5, 0x00000006, 0x00000246, 0x00000243, 0x00000245,
|
||||
0x00050081, 0x00000010, 0x00000193, 0x000002bf, 0x00000189, 0x0008000c, 0x00000010, 0x0000024d,
|
||||
0x00000001, 0x0000002b, 0x00000193, 0x00000302, 0x00000303, 0x00050085, 0x00000010, 0x00000250,
|
||||
0x0000024d, 0x0000030b, 0x0006000c, 0x00000010, 0x00000251, 0x00000001, 0x00000001, 0x00000250,
|
||||
0x0004006d, 0x00000047, 0x00000252, 0x00000251, 0x00050051, 0x00000006, 0x00000254, 0x00000252,
|
||||
0x00000000, 0x000500c4, 0x00000006, 0x00000255, 0x00000254, 0x0000005a, 0x00050051, 0x00000006,
|
||||
0x00000257, 0x00000252, 0x00000001, 0x000500c4, 0x00000006, 0x00000258, 0x00000257, 0x0000005e,
|
||||
0x000500c5, 0x00000006, 0x00000259, 0x00000255, 0x00000258, 0x00050051, 0x00000006, 0x0000025b,
|
||||
0x00000252, 0x00000002, 0x000500c5, 0x00000006, 0x0000025c, 0x00000259, 0x0000025b, 0x000500ab,
|
||||
0x00000088, 0x00000199, 0x00000246, 0x0000025c, 0x000300f7, 0x0000019b, 0x00000000, 0x000400fa,
|
||||
0x00000199, 0x0000019a, 0x0000019b, 0x000200f8, 0x0000019a, 0x000500b0, 0x00000088, 0x0000019e,
|
||||
0x00000246, 0x0000025c, 0x000600a9, 0x00000006, 0x00000312, 0x0000019e, 0x00000246, 0x0000025c,
|
||||
0x000600a9, 0x00000006, 0x00000313, 0x0000019e, 0x0000025c, 0x00000246, 0x000500c2, 0x00000006,
|
||||
0x00000260, 0x00000313, 0x0000005a, 0x000500c7, 0x00000006, 0x00000261, 0x00000260, 0x00000069,
|
||||
0x00040070, 0x00000008, 0x00000262, 0x00000261, 0x00050085, 0x00000008, 0x00000264, 0x00000262,
|
||||
0x0000030d, 0x000500c2, 0x00000006, 0x00000266, 0x00000313, 0x0000005e, 0x000500c7, 0x00000006,
|
||||
0x00000267, 0x00000266, 0x00000070, 0x00040070, 0x00000008, 0x00000268, 0x00000267, 0x00050085,
|
||||
0x00000008, 0x0000026a, 0x00000268, 0x0000030f, 0x000500c7, 0x00000006, 0x0000026c, 0x00000313,
|
||||
0x00000069, 0x00040070, 0x00000008, 0x0000026d, 0x0000026c, 0x00050085, 0x00000008, 0x0000026f,
|
||||
0x0000026d, 0x0000030d, 0x00060050, 0x00000010, 0x00000270, 0x00000264, 0x0000026a, 0x0000026f,
|
||||
0x000500c2, 0x00000006, 0x00000274, 0x00000312, 0x0000005a, 0x000500c7, 0x00000006, 0x00000275,
|
||||
0x00000274, 0x00000069, 0x00040070, 0x00000008, 0x00000276, 0x00000275, 0x00050085, 0x00000008,
|
||||
0x00000278, 0x00000276, 0x0000030d, 0x000500c2, 0x00000006, 0x0000027a, 0x00000312, 0x0000005e,
|
||||
0x000500c7, 0x00000006, 0x0000027b, 0x0000027a, 0x00000070, 0x00040070, 0x00000008, 0x0000027c,
|
||||
0x0000027b, 0x00050085, 0x00000008, 0x0000027e, 0x0000027c, 0x0000030f, 0x000500c7, 0x00000006,
|
||||
0x00000280, 0x00000312, 0x00000069, 0x00040070, 0x00000008, 0x00000281, 0x00000280, 0x00050085,
|
||||
0x00000008, 0x00000283, 0x00000281, 0x0000030d, 0x00060050, 0x00000010, 0x00000284, 0x00000278,
|
||||
0x0000027e, 0x00000283, 0x00050083, 0x00000010, 0x000001b0, 0x00000284, 0x00000270, 0x00050094,
|
||||
0x00000008, 0x000001b4, 0x000001b0, 0x000001b0, 0x0007000c, 0x00000008, 0x000001b6, 0x00000001,
|
||||
0x00000028, 0x000001b4, 0x000001b5, 0x000200f9, 0x000001b8, 0x000200f8, 0x000001b8, 0x000700f5,
|
||||
0x00000006, 0x000002d0, 0x00000057, 0x0000019a, 0x000001e5, 0x000001bb, 0x000700f5, 0x00000006,
|
||||
0x000002c2, 0x00000057, 0x0000019a, 0x000001e7, 0x000001bb, 0x000500b0, 0x00000088, 0x000001be,
|
||||
0x000002c2, 0x000000a7, 0x000400f6, 0x000001ba, 0x000001bb, 0x00000000, 0x000400fa, 0x000001be,
|
||||
0x000001b9, 0x000001ba, 0x000200f8, 0x000001b9, 0x00050041, 0x0000009a, 0x000001c1, 0x000000ab,
|
||||
0x000002c2, 0x0004003d, 0x00000009, 0x000001c2, 0x000001c1, 0x0008004f, 0x00000010, 0x000001c3,
|
||||
0x000001c2, 0x000001c2, 0x00000000, 0x00000001, 0x00000002, 0x00050083, 0x00000010, 0x000001c5,
|
||||
0x000001c3, 0x00000270, 0x00050094, 0x00000008, 0x000001c7, 0x000001c5, 0x000001b0, 0x00050088,
|
||||
0x00000008, 0x000001c9, 0x000001c7, 0x000001b6, 0x00050085, 0x00000008, 0x000001cb, 0x000001c9,
|
||||
0x000001ca, 0x0006000c, 0x00000008, 0x000001cc, 0x00000001, 0x00000001, 0x000001cb, 0x0008000c,
|
||||
0x00000008, 0x000001cd, 0x00000001, 0x0000002b, 0x000001cc, 0x0000004b, 0x000001ca, 0x0004006d,
|
||||
0x00000006, 0x000001ce, 0x000001cd, 0x000500aa, 0x00000088, 0x000001d1, 0x000001ce, 0x00000057,
|
||||
0x000300f7, 0x000001d4, 0x00000000, 0x000400fa, 0x000001d1, 0x000001d3, 0x000001d5, 0x000200f8,
|
||||
0x000001d3, 0x000200f9, 0x000001d4, 0x000200f8, 0x000001d5, 0x000500aa, 0x00000088, 0x000001d7,
|
||||
0x000001ce, 0x00000084, 0x000300f7, 0x000001da, 0x00000000, 0x000400fa, 0x000001d7, 0x000001d9,
|
||||
0x000001db, 0x000200f8, 0x000001d9, 0x000200f9, 0x000001da, 0x000200f8, 0x000001db, 0x00050080,
|
||||
0x00000006, 0x000001dd, 0x000001ce, 0x00000023, 0x000200f9, 0x000001da, 0x000200f8, 0x000001da,
|
||||
0x000700f5, 0x00000006, 0x000002d2, 0x00000023, 0x000001d9, 0x000001dd, 0x000001db, 0x000200f9,
|
||||
0x000001d4, 0x000200f8, 0x000001d4, 0x000700f5, 0x00000006, 0x000002d3, 0x00000057, 0x000001d3,
|
||||
0x000002d2, 0x000001da, 0x00050084, 0x00000006, 0x000001e2, 0x00000061, 0x000002c2, 0x000500c4,
|
||||
0x00000006, 0x000001e3, 0x000002d3, 0x000001e2, 0x000500c5, 0x00000006, 0x000001e5, 0x000002d0,
|
||||
0x000001e3, 0x000200f9, 0x000001bb, 0x000200f8, 0x000001bb, 0x00050080, 0x00000006, 0x000001e7,
|
||||
0x000002c2, 0x000000d0, 0x000200f9, 0x000001b8, 0x000200f8, 0x000001ba, 0x000200f9, 0x0000019b,
|
||||
0x000200f8, 0x0000019b, 0x000700f5, 0x00000006, 0x000002cf, 0x00000057, 0x0000017b, 0x000002d0,
|
||||
0x000001ba, 0x000700f5, 0x00000006, 0x000002cd, 0x0000025c, 0x0000017b, 0x00000312, 0x000001ba,
|
||||
0x000700f5, 0x00000006, 0x000002cb, 0x00000246, 0x0000017b, 0x00000313, 0x000001ba, 0x00050041,
|
||||
0x00000020, 0x000001ed, 0x0000001d, 0x000000d0, 0x0004003d, 0x00000006, 0x000001ee, 0x000001ed,
|
||||
0x00050051, 0x00000006, 0x000001f0, 0x00000080, 0x00000002, 0x00050041, 0x00000020, 0x000001f1,
|
||||
0x0000001d, 0x0000005e, 0x0004003d, 0x00000006, 0x000001f2, 0x000001f1, 0x00050084, 0x00000006,
|
||||
0x000001f3, 0x000001f0, 0x000001f2, 0x00050080, 0x00000006, 0x000001f4, 0x000001ee, 0x000001f3,
|
||||
0x00050051, 0x00000006, 0x000001f6, 0x00000080, 0x00000001, 0x00050084, 0x00000006, 0x000001f9,
|
||||
0x000001f6, 0x0000008d, 0x00050080, 0x00000006, 0x000001fa, 0x000001f4, 0x000001f9, 0x00050080,
|
||||
0x00000006, 0x000001fd, 0x000001fa, 0x0000008a, 0x00050051, 0x00000006, 0x000001ff, 0x000002c7,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x00000201, 0x000002c7, 0x00000001, 0x000500c4, 0x00000006,
|
||||
0x00000205, 0x000002cd, 0x00000204, 0x000500c5, 0x00000006, 0x00000206, 0x000002cb, 0x00000205,
|
||||
0x00070050, 0x000001e8, 0x00000208, 0x000001ff, 0x00000201, 0x00000206, 0x000002cf, 0x00060041,
|
||||
0x00000209, 0x0000020a, 0x000001ec, 0x00000030, 0x000001fd, 0x0003003e, 0x0000020a, 0x00000208,
|
||||
0x000200f9, 0x0000020d, 0x000200f8, 0x0000020d, 0x000100fd, 0x00010038,
|
||||
};
|
||||
364
runtime/boot.c
Normal file
364
runtime/boot.c
Normal file
@@ -0,0 +1,364 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/loader.c (lines 456-477) and services.c (lines 219-235) @ b75be8cf
|
||||
// (MIT, Copyright (c) 2026 MacNativeGames contributors) and LAS Recomp runtime/main.c (lines 16-35, 141-159) @
|
||||
// da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
||||
// The boot driver: the guest main thread runs the game's own nn::init::Start
|
||||
// with three host callbacks, the second of which runs DT_INIT of main, subsdk0 and sdk; Start then calls nnMain.
|
||||
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call, at the return address of
|
||||
// each in main (1.1.0, 1.2.1, 1.4.3):
|
||||
// SetFocusHandlingMode main+0xeb8c14 (1.2.1: +0xe7a954, 1.4.3: +0xbdb48c) in nnMain, every module's constructor
|
||||
// flag reads 2
|
||||
// nvnBootstrapLoader main+0xebcdec (1.2.1: +0xe7eb2c, 1.4.3: +0xbe0360) in the graphics bring-up; logs the
|
||||
// run statistics, exits 0
|
||||
// With --seconds S the run continues past that call; rt_frame_presented ends it once the world clock passed S.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "cheats.h"
|
||||
#include "fs.h"
|
||||
#include "gfx.h"
|
||||
#include "mods.h"
|
||||
#include "nvn_api.h"
|
||||
#include "platform/platform.h"
|
||||
#include "vk.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define SYM_INIT_START "_ZN2nn4init5StartEmmPFvvES2_S2_"
|
||||
// main: return addresses of nnMain's SetFocusHandlingMode call (1.1.0 and 1.2.1 from this port's analysis of main;
|
||||
// 1.4.3 from MacNativeGames' scripts/verify_boot.py) and of the first nvnBootstrapLoader call, per game version.
|
||||
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : RT_V110 ? 0xeb8c14ull : 0xe7a954ull)
|
||||
#define NVN_RET (RT_V143 ? 0xbe0360ull : RT_V110 ? 0xebcdecull : 0xe7eb2cull)
|
||||
#define CTOR_DONE 2 // a module's constructor flag after its DT_INIT ran the init_array
|
||||
|
||||
// The init_array sizes (1.1.0 and 1.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check
|
||||
// expects the same. The three tables carry the same rows, so cb_module_inits walks them by the size of one.
|
||||
typedef struct { const char *name; uint64_t entries; } InitCount;
|
||||
static const InitCount inits_110[] = {{"main", 10668}, {"subsdk0", 45}, {"sdk", 116}};
|
||||
static const InitCount inits_121[] = {{"main", 10673}, {"subsdk0", 45}, {"sdk", 116}};
|
||||
static const InitCount inits_143[] = {{"main", 10717}, {"subsdk0", 45}, {"sdk", 83}};
|
||||
#define expected_inits (RT_V143 ? inits_143 : RT_V110 ? inits_110 : inits_121)
|
||||
|
||||
static volatile int32_t inits_done, checkpoint_seen;
|
||||
uint64_t rt_guest_start_ns;
|
||||
|
||||
static const RtLoaded *module_named(const char *name) {
|
||||
for (int i = 0; i < ldr_count(); i++)
|
||||
if (!strcmp(ldr_module(i)->name, name)) return ldr_module(i);
|
||||
rt_fatal("boot: module %s is not loaded", name);
|
||||
}
|
||||
|
||||
static uint32_t ctor_state(const RtLoaded *m) { return LD32(m->ctor_flag); }
|
||||
|
||||
static const char *caller(const Cpu *c, char *buf, int n) { return ldr_describe(c->x[30] - 4, buf, n); }
|
||||
|
||||
// main.npdm META: the main thread's priority (+0xE), core (+0xF) and stack size (+0x1C). kernel.c builds the main
|
||||
// thread with the 1.2.1 values; a different npdm means a different game build.
|
||||
static void check_npdm(void) {
|
||||
size_t n;
|
||||
uint8_t *npdm = ldr_read_verified(rt_config()->exefs, "main.npdm", &n);
|
||||
if (n < 0x80 || memcmp(npdm, "META", 4)) rt_fatal("boot: main.npdm has no META header");
|
||||
uint32_t stack;
|
||||
memcpy(&stack, npdm + 0x1C, 4);
|
||||
int prio = npdm[0xE], core = npdm[0xF];
|
||||
free(npdm);
|
||||
if (prio != 44 || core != 0 || stack != 0x100000)
|
||||
rt_fatal("boot: main.npdm asks for priority %d, core %d, stack %#x; kernel.c builds 44, 0, 0x100000", prio,
|
||||
core, stack);
|
||||
}
|
||||
|
||||
// ---- the three callbacks of nn::init::Start (rtld's job on hardware)
|
||||
static void cb_loader_ready(Cpu *c) {
|
||||
(void)c;
|
||||
rt_log("boot: rtld callback: modules linked");
|
||||
}
|
||||
|
||||
// DT_INIT registers the module with musl (__nnmusl_init_dso) and runs its init_array; the constructor flag reads 2
|
||||
// afterwards (MacNativeGames loader.c:458-462).
|
||||
static void cb_module_inits(Cpu *c) {
|
||||
if (plat_cas32(&inits_done, 0, 0)) rt_fatal("boot: module constructors requested twice");
|
||||
for (unsigned k = 0; k < sizeof inits_121 / sizeof *inits_121; k++) {
|
||||
const RtLoaded *m = module_named(expected_inits[k].name);
|
||||
uint64_t entries = m->init_array_size / 8;
|
||||
if (!m->init) rt_fatal("boot: %s has no DT_INIT", m->name);
|
||||
rt_log("boot: ENTER %s DT_INIT (%llu init_array entries)", m->name, (unsigned long long)entries);
|
||||
rt_call(c, m->init, 0);
|
||||
uint32_t state = ctor_state(m);
|
||||
rt_logf("boot: DT_INIT %s state=%u (%llu)\n", m->name, state, (unsigned long long)entries);
|
||||
if (state != CTOR_DONE) rt_fatal("boot: %s DT_INIT left its constructor flag at %u, not 2", m->name, state);
|
||||
if (entries != expected_inits[k].entries)
|
||||
rt_fatal("boot: %s has %llu init_array entries, this game version has %llu", m->name, (unsigned long long)entries,
|
||||
(unsigned long long)expected_inits[k].entries);
|
||||
}
|
||||
plat_xchg32(&inits_done, 1);
|
||||
}
|
||||
|
||||
static void cb_module_finis(Cpu *c) {
|
||||
char lr[128];
|
||||
rt_fatal("boot: the game asked for module finalizers (from %s): the guest exited", caller(c, lr, sizeof lr));
|
||||
}
|
||||
|
||||
// ---- checkpoints (host.h)
|
||||
static void stats_line(void) {
|
||||
RtStats s = rt_stats();
|
||||
rt_logf("boot: passed (%llu SVCs, %llu threads, %llu host calls)\n", (unsigned long long)s.svcs,
|
||||
(unsigned long long)s.threads, (unsigned long long)s.host_calls);
|
||||
}
|
||||
|
||||
void rt_boot_event(const char *what, Cpu *c, const char *detail) {
|
||||
char at[128];
|
||||
const RtLoaded *main_mod = module_named("main");
|
||||
uint64_t ret = c->x[30];
|
||||
caller(c, at, sizeof at);
|
||||
if (!strcmp(what, "checkpoint")) {
|
||||
if (ret != main_mod->base + CHECKPOINT_RET)
|
||||
rt_fatal("boot: checkpoint %s reached from %s, expected main+%#llx", detail, at,
|
||||
(unsigned long long)CHECKPOINT_RET);
|
||||
if (!plat_cas32(&inits_done, 0, 0)) rt_fatal("boot: checkpoint %s before DT_INIT", detail);
|
||||
for (int i = 0; i < ldr_count(); i++)
|
||||
if (ctor_state(ldr_module(i)) != CTOR_DONE)
|
||||
rt_fatal("boot: checkpoint %s: %s constructor flag reads %u", detail, ldr_module(i)->name,
|
||||
ctor_state(ldr_module(i)));
|
||||
plat_xchg32(&checkpoint_seen, 1);
|
||||
rt_logf("boot: checkpoint %s from main+%#llx\n", detail, (unsigned long long)(ret - main_mod->base));
|
||||
return;
|
||||
}
|
||||
if (!strcmp(what, "nvn")) {
|
||||
if (ret != main_mod->base + NVN_RET)
|
||||
rt_fatal("boot: first NVN call %s(\"%s\") from %s, expected main+%#llx", "nvnBootstrapLoader", detail, at,
|
||||
(unsigned long long)NVN_RET);
|
||||
if (!plat_cas32(&checkpoint_seen, 0, 0))
|
||||
rt_fatal("boot: first NVN call before the SetFocusHandlingMode checkpoint");
|
||||
rt_logf("boot: first NVN call nvnBootstrapLoader(\"%s\") from main+%#llx\n", detail,
|
||||
(unsigned long long)(ret - main_mod->base));
|
||||
if (rt_config()->seconds) return; // --seconds: the run goes on into the world
|
||||
if (rt_trace & RT_TRACE_HOSTCALL) hostfn_report();
|
||||
stats_line();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
plat_exit(0); // other guest threads are still running; nothing of theirs needs an orderly end
|
||||
}
|
||||
rt_fatal("boot: unknown boot event %s (%s) from %s", what, detail ? detail : "", at);
|
||||
}
|
||||
|
||||
// ---- --seconds S
|
||||
// A PNG with stored (uncompressed) deflate blocks: no compression library, and the shot is only read by tools.
|
||||
// rt_write_png (declared in vk.h) is shared with the NX2NAVI_DUMP frame dump (vk_dump.c).
|
||||
static uint32_t crc32_update(uint32_t crc, const uint8_t *p, size_t n) {
|
||||
static uint32_t table[256];
|
||||
if (!table[1])
|
||||
for (uint32_t i = 0; i < 256; i++) {
|
||||
uint32_t c = i;
|
||||
for (int k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320u ^ (c >> 1) : c >> 1;
|
||||
table[i] = c;
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) crc = table[(crc ^ p[i]) & 0xFF] ^ (crc >> 8);
|
||||
return crc;
|
||||
}
|
||||
|
||||
static void png_chunk(FILE *f, const char *type, const uint8_t *data, uint32_t n) {
|
||||
uint8_t hdr[8] = {(uint8_t)(n >> 24), (uint8_t)(n >> 16), (uint8_t)(n >> 8), (uint8_t)n};
|
||||
memcpy(hdr + 4, type, 4);
|
||||
uint32_t crc = crc32_update(crc32_update(~0u, hdr + 4, 4), data, n) ^ ~0u;
|
||||
uint8_t tail[4] = {(uint8_t)(crc >> 24), (uint8_t)(crc >> 16), (uint8_t)(crc >> 8), (uint8_t)crc};
|
||||
fwrite(hdr, 1, 8, f);
|
||||
if (n) fwrite(data, 1, n, f);
|
||||
fwrite(tail, 1, 4, f);
|
||||
}
|
||||
|
||||
void rt_write_png(const char *path, const uint8_t *rgba, uint32_t w, uint32_t h) {
|
||||
uint64_t row = 1 + 4ull * w, raw_n = row * h;
|
||||
uint8_t *raw = malloc(raw_n);
|
||||
if (!raw) rt_fatal("boot: out of memory for the shot");
|
||||
for (uint32_t y = 0; y < h; y++) {
|
||||
raw[y * row] = 0; // filter: none
|
||||
memcpy(raw + y * row + 1, rgba + 4ull * w * y, 4ull * w);
|
||||
}
|
||||
uint64_t blocks = (raw_n + 65534) / 65535;
|
||||
uint8_t *z = malloc(2 + raw_n + 5 * blocks + 4);
|
||||
if (!z) rt_fatal("boot: out of memory for the shot");
|
||||
uint64_t zn = 0;
|
||||
z[zn++] = 0x78, z[zn++] = 0x01;
|
||||
uint32_t a = 1, b = 0;
|
||||
for (uint64_t off = 0; off < raw_n; off += 65535) {
|
||||
uint32_t k = raw_n - off > 65535 ? 65535 : (uint32_t)(raw_n - off);
|
||||
z[zn++] = off + k >= raw_n;
|
||||
z[zn++] = (uint8_t)k, z[zn++] = (uint8_t)(k >> 8), z[zn++] = (uint8_t)~k, z[zn++] = (uint8_t)(~k >> 8);
|
||||
memcpy(z + zn, raw + off, k);
|
||||
zn += k;
|
||||
for (uint32_t i = 0; i < k; i++) a = (a + raw[off + i]) % 65521, b = (b + a) % 65521;
|
||||
}
|
||||
uint32_t ad = b << 16 | a;
|
||||
z[zn++] = (uint8_t)(ad >> 24), z[zn++] = (uint8_t)(ad >> 16), z[zn++] = (uint8_t)(ad >> 8), z[zn++] = (uint8_t)ad;
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) rt_fatal("boot: cannot write %s", path);
|
||||
static const uint8_t sig[8] = {0x89, 'P', 'N', 'G', 13, 10, 26, 10};
|
||||
uint8_t ihdr[13] = {(uint8_t)(w >> 24), (uint8_t)(w >> 16), (uint8_t)(w >> 8), (uint8_t)w,
|
||||
(uint8_t)(h >> 24), (uint8_t)(h >> 16), (uint8_t)(h >> 8), (uint8_t)h, 8, 6, 0, 0, 0};
|
||||
fwrite(sig, 1, 8, f);
|
||||
png_chunk(f, "IHDR", ihdr, 13);
|
||||
png_chunk(f, "IDAT", z, (uint32_t)zn);
|
||||
png_chunk(f, "IEND", NULL, 0);
|
||||
if (fclose(f)) rt_fatal("boot: cannot write %s", path);
|
||||
free(z);
|
||||
free(raw);
|
||||
}
|
||||
|
||||
static void save_shot(const tex_t *t, const char *path) {
|
||||
int fmt = t->d.format;
|
||||
int bgra = fmt == NVN_FORMAT_BGRA8 || fmt == NVN_FORMAT_BGRA8_SRGB;
|
||||
if (!bgra && fmt != NVN_FORMAT_RGBA8 && fmt != NVN_FORMAT_RGBA8_SRGB)
|
||||
rt_fatal("boot: --shot supports 8-bit RGBA and BGRA targets, the presented texture has NVN format %#x", fmt);
|
||||
uint32_t w = t->d.width, h = t->d.height;
|
||||
uint8_t *px = malloc(4ull * w * h);
|
||||
if (!px) rt_fatal("boot: out of memory for the shot");
|
||||
view_t view;
|
||||
memset(&view, 0, sizeof view);
|
||||
NVNcopyRegion r = {.width = (int)w, .height = (int)h, .depth = 1};
|
||||
if (gfx_replay_async()) gfx_backend()->finish(); // the presented frame may still be queued for the replay
|
||||
gfx_backend()->read_texels(t, &view, &r, px, 4ull * w, 4ull * w * h);
|
||||
if (bgra)
|
||||
for (uint64_t i = 0; i < 4ull * w * h; i += 4) {
|
||||
uint8_t x = px[i];
|
||||
px[i] = px[i + 2];
|
||||
px[i + 2] = x;
|
||||
}
|
||||
rt_write_png(path, px, w, h);
|
||||
free(px);
|
||||
rt_logf("boot: shot %s (%ux%u)\n", path, w, h);
|
||||
}
|
||||
|
||||
// NX2NAVI_SHOTS=lo-hi:step [NX2NAVI_SHOTS_DIR=dir]: every step-th present in [lo, hi] saved as <dir>/f<n>.png (n the
|
||||
// present), box-filtered to 480x270; NX2NAVI_SHOTS=w<lo>-<hi>:<step> takes them by world-clock seconds instead, the first
|
||||
// present at or after each step (full size with NX2NAVI_SHOTS_FULL=1), for finding short glitches. Pipeline builds stay asynchronous, so the
|
||||
// pictures show what a player sees.
|
||||
static void series_shot(const tex_t *t, uint64_t n) {
|
||||
static int parsed;
|
||||
static unsigned long long lo, hi, step, next;
|
||||
static const char *dir;
|
||||
static int full, world;
|
||||
if (!parsed) {
|
||||
full = cfg_get("NX2NAVI_SHOTS_FULL") != NULL;
|
||||
const char *e = cfg_get("NX2NAVI_SHOTS");
|
||||
double wl, wh, ws;
|
||||
if (e && e[0] == 'w' && sscanf(e + 1, "%lf-%lf:%lf", &wl, &wh, &ws) == 3 && ws > 0) // world-clock seconds
|
||||
world = 1, lo = (unsigned long long)(wl * 1e6), hi = (unsigned long long)(wh * 1e6),
|
||||
step = (unsigned long long)(ws * 1e6), next = lo;
|
||||
else if (!e || sscanf(e, "%llu-%llu:%llu", &lo, &hi, &step) != 3 || !step) hi = 0;
|
||||
dir = cfg_get("NX2NAVI_SHOTS_DIR") ? cfg_get("NX2NAVI_SHOTS_DIR") : ".";
|
||||
if (hi) plat_file_mkdir_p(dir);
|
||||
parsed = 1;
|
||||
}
|
||||
if (world) { // the first present at or after each step of the world clock
|
||||
int64_t w = frame_step_world_us();
|
||||
if (w < 0 || (unsigned long long)w < next || next > hi) return;
|
||||
while (next <= (unsigned long long)w) next += step;
|
||||
} else if (n < lo || n > hi || (n - lo) % step) {
|
||||
return;
|
||||
}
|
||||
int fmt = t->d.format;
|
||||
int bgra = fmt == NVN_FORMAT_BGRA8 || fmt == NVN_FORMAT_BGRA8_SRGB;
|
||||
if (!bgra && fmt != NVN_FORMAT_RGBA8 && fmt != NVN_FORMAT_RGBA8_SRGB) return;
|
||||
uint32_t w = t->d.width, h = t->d.height, ow = full ? w : 480, oh = full ? h : 270;
|
||||
uint8_t *px = malloc(4ull * w * h), *out = malloc(4ull * ow * oh);
|
||||
if (!px || !out) rt_fatal("boot: out of memory for a NX2NAVI_SHOTS frame");
|
||||
view_t view;
|
||||
memset(&view, 0, sizeof view);
|
||||
NVNcopyRegion r = {.width = (int)w, .height = (int)h, .depth = 1};
|
||||
if (gfx_replay_async()) gfx_backend()->finish(); // the presented frame may still be queued for the replay
|
||||
gfx_backend()->read_texels(t, &view, &r, px, 4ull * w, 4ull * w * h);
|
||||
for (uint32_t y = 0; y < oh; y++)
|
||||
for (uint32_t x = 0; x < ow; x++) {
|
||||
uint32_t x0 = x * w / ow, x1 = (x + 1) * w / ow, y0 = y * h / oh, y1 = (y + 1) * h / oh;
|
||||
if (x1 <= x0) x1 = x0 + 1;
|
||||
if (y1 <= y0) y1 = y0 + 1;
|
||||
uint32_t sum[4] = {0}, k = 0;
|
||||
for (uint32_t sy = y0; sy < y1; sy++)
|
||||
for (uint32_t sx = x0; sx < x1; sx++, k++)
|
||||
for (int c = 0; c < 4; c++) sum[c] += px[4ull * (sy * (uint64_t)w + sx) + (uint32_t)c];
|
||||
uint8_t *o = out + 4ull * (y * ow + x);
|
||||
for (int c = 0; c < 4; c++) o[c] = (uint8_t)(sum[bgra && c != 3 ? 2 - c : c] / k);
|
||||
o[3] = 255;
|
||||
}
|
||||
char path[PLAT_PATH_MAX];
|
||||
snprintf(path, sizeof path, "%s/f%05llu.png", dir, (unsigned long long)n);
|
||||
rt_write_png(path, out, ow, oh);
|
||||
free(out);
|
||||
free(px);
|
||||
}
|
||||
|
||||
// A --seconds run ends at the first present after the world clock (game time since the save's first frame,
|
||||
// frame_step_world_us) passed S seconds, so a run covers the same game time at any frame rate; any other frame is
|
||||
// counted by the back end only.
|
||||
void rt_frame_presented(const tex_t *t, uint64_t n) {
|
||||
const RtConfig *cfg = rt_config();
|
||||
series_shot(t, n);
|
||||
int64_t w = cfg->seconds ? frame_step_world_us() : -1;
|
||||
if (w < 0 || (double)w < cfg->seconds * 1e6) return;
|
||||
if (cfg->shot) save_shot(t, cfg->shot);
|
||||
GfxStats g = gfx_stats();
|
||||
rt_logf("boot: frames %llu presented (nvn calls %llu, distinct %llu, unimplemented %llu, validation errors %llu, "
|
||||
"shader failures %llu, draws skipped %llu)\n", (unsigned long long)g.frames,
|
||||
(unsigned long long)g.nvn_calls, (unsigned long long)g.nvn_distinct, (unsigned long long)g.unimplemented,
|
||||
(unsigned long long)g.validation_errors, (unsigned long long)g.shader_failures,
|
||||
(unsigned long long)g.draws_skipped);
|
||||
stats_line();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
fs_save_flush();
|
||||
vk_pipeline_save(); // no-op without the Vulkan back end
|
||||
prof_write();
|
||||
fflush(stderr);
|
||||
plat_exit(0);
|
||||
}
|
||||
|
||||
// ---- steps 9-14
|
||||
static uint64_t cb_addr[3];
|
||||
|
||||
static void guest_main(Cpu *c) {
|
||||
uint32_t handle = kern_main_thread(&c);
|
||||
uint64_t start = ldr_symbol(SYM_INIT_START);
|
||||
if (!start) rt_fatal("boot: %s is not exported", SYM_INIT_START);
|
||||
char d[128];
|
||||
rt_log("boot: ENTER nn::init::Start at %s, main thread %#x", ldr_describe(start, d, sizeof d), handle);
|
||||
// nn::init::Start(main thread handle, arguments region, loader_ready, module_inits, module_finis)
|
||||
rt_call(c, start, 5, (uint64_t)handle, (uint64_t)0, cb_addr[0], cb_addr[1], cb_addr[2]);
|
||||
rt_fatal("boot: nn::init::Start returned");
|
||||
}
|
||||
|
||||
static void prewarm_main(void *arg) {
|
||||
(void)arg;
|
||||
vk_pipeline_prewarm();
|
||||
const char *prep = cfg_get("NX2NAVI_TEST_PREPARE_SHADERS"); // Task 2.3 test hook; Task 2.6 builds the preparation
|
||||
if (prep && !strcmp(prep, "1")) {
|
||||
int cpus = (int)plat_cpu_usable();
|
||||
gfx_shader_prepare_recorded(cpus, cpus / 2 > 16 ? 16 : cpus / 2 < 1 ? 1 : cpus / 2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void rt_boot(void) {
|
||||
check_npdm();
|
||||
if (!rt_config()->shot) vk_pipeline_async_enable(); // a --shot image must not depend on build timing
|
||||
// Shaders and pipelines recorded in earlier runs are made ready while the modules load; the guest waits for them.
|
||||
PlatThread *warm = plat_thread_new(prewarm_main, NULL, "prewarm");
|
||||
ldr_load(rt_config()->exefs);
|
||||
rt_patches_init();
|
||||
mods_exefs_switch(); // after the patch sets' settings: an enabled mod's ExeFS patches on
|
||||
frame_step_init();
|
||||
res_init();
|
||||
cheats_init(); // after the patch sets: a cheat's set follows its setting
|
||||
cb_addr[0] = hostfn_register("<rtld loader_ready>", cb_loader_ready);
|
||||
cb_addr[1] = hostfn_register("<rtld module_inits>", cb_module_inits);
|
||||
cb_addr[2] = hostfn_register("<rtld module_finis>", cb_module_finis);
|
||||
hostfn_seal();
|
||||
uint64_t held = plat_now_ms();
|
||||
if (warm) plat_thread_join(warm);
|
||||
else 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);
|
||||
rt_guest_start_ns = kern_now_ns();
|
||||
kern_run_on_guest_main(guest_main);
|
||||
rt_fatal("boot: the guest main thread ended");
|
||||
}
|
||||
159
runtime/camera_speed.c
Normal file
159
runtime/camera_speed.c
Normal file
@@ -0,0 +1,159 @@
|
||||
// Camera speed: NX2NAVI_STICK_SENS (0.1 to 3, default 1, read at start) multiplies the game's own right-stick camera speeds.
|
||||
// The game's Options menu offers five camera sensitivities, Very Low to Very High; their turn rates are the array
|
||||
// StickSensitivity {0.8, 1.2, 1.6, 2.0, 2.4} in Component/PlayerCameraParam/PlayerCamera.game__component__
|
||||
// PlayerCameraParam.bgyml inside Pack/Actor/PlayerCamera.pack.zs. romfs.c serves a copy of that pack with every entry
|
||||
// multiplied (cam_pack_build), so the setting raises the camera's top speed as well as its speed at a partial tilt, at
|
||||
// any of the five sensitivities. The mouse camera reaches the game as a right-stick deflection (input.c, mouse_camera),
|
||||
// which divides it by the same factor (cam_speed_applied), so NX2NAVI_MOUSE_SENS alone sets the mouse's speed.
|
||||
//
|
||||
// The pack is a SARC archive compressed with the dictionary pack.zsdic of Pack/ZsDic.pack.zs. A float in a BYML array
|
||||
// is stored in the array itself, so the multiplied values take the bytes of the old ones and no offset moves.
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
|
||||
#define MEMBER "Component/PlayerCameraParam/PlayerCamera.game__component__PlayerCameraParam.bgyml"
|
||||
#define MAX_RAW (64u << 20)
|
||||
|
||||
static float applied = 1.0f;
|
||||
|
||||
float cam_speed_wanted(void) {
|
||||
double v = cfg_double("NX2NAVI_STICK_SENS");
|
||||
return (float)(v < 0.1 ? 0.1 : v > 3 ? 3 : v);
|
||||
}
|
||||
|
||||
float cam_speed_applied(void) { return applied; }
|
||||
|
||||
static int u32_at(const uint8_t *p, size_t n, size_t off, uint32_t *v) {
|
||||
if (off > n || n - off < 4) return 0;
|
||||
memcpy(v, p + off, 4);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// SARC member `name` of the archive p[0..n): its start and size; 0 when it is not there.
|
||||
static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size) {
|
||||
uint32_t data;
|
||||
uint16_t head, count, bom;
|
||||
if (n < 0x14 || memcmp(p, "SARC", 4)) return 0;
|
||||
memcpy(&head, p + 4, 2);
|
||||
memcpy(&bom, p + 6, 2);
|
||||
if (bom != 0xfeff || !u32_at(p, n, 12, &data) || head + 12u > n || memcmp(p + head, "SFAT", 4)) return 0;
|
||||
memcpy(&count, p + head + 6, 2);
|
||||
size_t names = head + 12 + (size_t)count * 16;
|
||||
if (names + 8 > n || memcmp(p + names, "SFNT", 4)) return 0;
|
||||
names += 8;
|
||||
for (unsigned i = 0; i < count; i++) {
|
||||
uint32_t attr, lo, hi;
|
||||
size_t e = head + 12 + (size_t)i * 16;
|
||||
if (!u32_at(p, n, e + 4, &attr) || !u32_at(p, n, e + 8, &lo) || !u32_at(p, n, e + 12, &hi)) return 0;
|
||||
size_t s = names + (size_t)(attr & 0xffffff) * 4, len = strlen(name);
|
||||
if (!(attr >> 24) || s + len + 1 > n || memcmp(p + s, name, len + 1)) continue;
|
||||
if (lo > hi || data + (size_t)hi > n) return 0;
|
||||
*at = data + lo;
|
||||
*size = hi - lo;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The StickSensitivity floats of the BYML p[0..n), multiplied by k; their count, 0 when the file has no such array.
|
||||
static int scale_sensitivity(uint8_t *p, size_t n, float k, float was[8]) {
|
||||
uint32_t keys, root, head;
|
||||
if (n < 16 || memcmp(p, "YB", 2) || !u32_at(p, n, 4, &keys) || !u32_at(p, n, 12, &root) ||
|
||||
!u32_at(p, n, root, &head) || (head & 0xff) != 0xc1 || !u32_at(p, n, keys, &head) || (head & 0xff) != 0xc2)
|
||||
return 0;
|
||||
uint32_t nkeys = head >> 8;
|
||||
if (!u32_at(p, n, root, &head)) return 0;
|
||||
for (uint32_t i = 0; i < head >> 8; i++) {
|
||||
uint32_t e, at, arr, ah;
|
||||
if (!u32_at(p, n, root + 4 + 8 * (size_t)i, &e) || (e & 0xffffff) >= nkeys ||
|
||||
!u32_at(p, n, keys + 4 + 4 * (size_t)(e & 0xffffff), &at))
|
||||
return 0;
|
||||
const char *name = (const char *)p + keys + at;
|
||||
if (keys + at + sizeof "StickSensitivity" > n || strcmp(name, "StickSensitivity")) continue;
|
||||
if (e >> 24 != 0xc0 || !u32_at(p, n, root + 8 + 8 * (size_t)i, &arr) || !u32_at(p, n, arr, &ah) ||
|
||||
(ah & 0xff) != 0xc0)
|
||||
return 0;
|
||||
uint32_t count = ah >> 8;
|
||||
size_t vals = arr + 4 + ((count + 3) & ~3u);
|
||||
if (!count || count > 8 || vals + 4 * (size_t)count > n) return 0;
|
||||
for (uint32_t j = 0; j < count; j++)
|
||||
if (p[arr + 4 + j] != 0xd2) return 0;
|
||||
for (uint32_t j = 0; j < count; j++) {
|
||||
float v;
|
||||
memcpy(&v, p + vals + 4 * j, 4);
|
||||
was[j] = v;
|
||||
v *= k;
|
||||
memcpy(p + vals + 4 * j, &v, 4);
|
||||
}
|
||||
return (int)count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cam_pack_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, void **out, size_t *out_n) {
|
||||
float k = cam_speed_wanted();
|
||||
*out = NULL;
|
||||
if (k == 1.0f) return 0;
|
||||
int ok = 0;
|
||||
uint8_t *dics = NULL, *raw = NULL;
|
||||
ZSTD_DDict *dd = NULL;
|
||||
ZSTD_CDict *cd = NULL;
|
||||
void *z = NULL;
|
||||
// The dictionaries: Pack/ZsDic.pack.zs is a SARC compressed without one.
|
||||
unsigned long long dn = ZSTD_getFrameContentSize(zsdic, zsdic_n);
|
||||
size_t dic_at, dic_n;
|
||||
if (dn == ZSTD_CONTENTSIZE_UNKNOWN || dn == ZSTD_CONTENTSIZE_ERROR || dn > MAX_RAW || !(dics = malloc((size_t)dn)) ||
|
||||
ZSTD_decompress(dics, (size_t)dn, zsdic, zsdic_n) != dn ||
|
||||
!sarc_member(dics, (size_t)dn, "pack.zsdic", &dic_at, &dic_n)) {
|
||||
rt_log("camera speed: no pack.zsdic in Pack/ZsDic.pack.zs; the camera keeps its own speed");
|
||||
goto done;
|
||||
}
|
||||
unsigned long long rn = ZSTD_getFrameContentSize(in, n);
|
||||
unsigned id = ZSTD_getDictID_fromFrame(in, n);
|
||||
dd = ZSTD_createDDict(dics + dic_at, dic_n);
|
||||
if (!dd || id != ZSTD_getDictID_fromDDict(dd) || rn == ZSTD_CONTENTSIZE_UNKNOWN ||
|
||||
rn == ZSTD_CONTENTSIZE_ERROR || rn > MAX_RAW || !(raw = malloc((size_t)rn))) {
|
||||
rt_log("camera speed: Pack/Actor/PlayerCamera.pack.zs is not a pack.zsdic frame with a size; the camera keeps its "
|
||||
"own speed");
|
||||
goto done;
|
||||
}
|
||||
ZSTD_DCtx *dctx = ZSTD_createDCtx();
|
||||
size_t got = dctx ? ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, in, n, dd) : 0;
|
||||
ZSTD_freeDCtx(dctx);
|
||||
size_t at, size;
|
||||
float was[8];
|
||||
int count = !ZSTD_isError(got) && got == rn && sarc_member(raw, (size_t)rn, MEMBER, &at, &size)
|
||||
? scale_sensitivity(raw + at, size, k, was) : 0;
|
||||
if (!count) {
|
||||
rt_log("camera speed: no StickSensitivity array in %s; the camera keeps its own speed", MEMBER);
|
||||
goto done;
|
||||
}
|
||||
cd = ZSTD_createCDict(dics + dic_at, dic_n, 9);
|
||||
size_t cap = ZSTD_compressBound((size_t)rn), zn = 0;
|
||||
ZSTD_CCtx *cctx = cd ? ZSTD_createCCtx() : NULL;
|
||||
if (cctx && (z = malloc(cap))) zn = ZSTD_compress_usingCDict(cctx, z, cap, raw, (size_t)rn, cd);
|
||||
ZSTD_freeCCtx(cctx);
|
||||
if (!z || !zn || ZSTD_isError(zn)) goto done;
|
||||
*out = z;
|
||||
*out_n = zn;
|
||||
z = NULL;
|
||||
ok = 1;
|
||||
rt_log("camera speed: x%.2f, StickSensitivity %.2f %.2f %.2f %.2f %.2f -> %.2f %.2f %.2f %.2f %.2f", k, was[0],
|
||||
count > 1 ? was[1] : 0, count > 2 ? was[2] : 0, count > 3 ? was[3] : 0, count > 4 ? was[4] : 0, was[0] * k,
|
||||
count > 1 ? was[1] * k : 0, count > 2 ? was[2] * k : 0, count > 3 ? was[3] * k : 0,
|
||||
count > 4 ? was[4] * k : 0);
|
||||
done:
|
||||
free(z);
|
||||
free(raw);
|
||||
free(dics);
|
||||
ZSTD_freeDDict(dd);
|
||||
ZSTD_freeCDict(cd);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void cam_pack_served(void) { applied = cam_speed_wanted(); }
|
||||
710
runtime/cfg.c
Normal file
710
runtime/cfg.c
Normal file
@@ -0,0 +1,710 @@
|
||||
// nx2navi.ini: file-backed defaults for the NX2NAVI_* environment variables (docs/controls.md, nx2navi.ini.example).
|
||||
// Format: "key = value" lines, '#' or ';' at the start of a line begins a comment, [sections] only group. A key is a
|
||||
// variable name without "NX2NAVI_" (an optional NX2NAVI_ prefix is accepted), case-insensitive. A value is the rest of the
|
||||
// line, trimmed, optionally in double quotes; an empty value counts as unset. The environment wins over the file.
|
||||
//
|
||||
// Live settings: cfg_set changes a value in memory (readers see the new pointer; the old string is kept, so a pointer
|
||||
// cfg_get handed out stays valid) and cfg_save writes only the changed keys back, keeping the rest of the file.
|
||||
// Readers take no lock: an entry is complete before n_entries counts it, and value pointers are swapped atomically.
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define PREFIX "NX2NAVI_"
|
||||
#define PREFIX_LEN 5
|
||||
#define CFG_MAX_KEYS 512
|
||||
|
||||
typedef struct { char *key, *value; int dirty, removed; } CfgEntry; // key upper-case, without the prefix; value NULL after cfg_reset
|
||||
|
||||
static CfgEntry entries[CFG_MAX_KEYS];
|
||||
static int n_entries;
|
||||
static unsigned version;
|
||||
static PlatMutex set_lock = PLAT_MUTEX_INIT; // cfg_set, cfg_reset, cfg_save
|
||||
static int save_ok = 1; // the last cfg_save_to wrote (or had nothing to write)
|
||||
|
||||
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||
static const char *const known[] = {
|
||||
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_OFF", "AUDIO_PREFILL_MS",
|
||||
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
||||
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
|
||||
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
|
||||
"BIND_SHAKE",
|
||||
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
|
||||
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
|
||||
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES",
|
||||
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
|
||||
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
|
||||
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
|
||||
"MENU_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",
|
||||
"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",
|
||||
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
|
||||
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
|
||||
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
|
||||
"SWKBD_ANSWER",
|
||||
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
|
||||
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
|
||||
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
|
||||
"ZSTD_CHECK", "ZSTD_NATIVE",
|
||||
};
|
||||
|
||||
// The nx2navi.ini section a key cfg_set may write goes to when the file lacks it (nx2navi.ini.example's sections).
|
||||
static const struct { const char *key, *section; } sections[] = {
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"},
|
||||
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
||||
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
||||
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"COLOR_PRESET", "display"},
|
||||
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
|
||||
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
|
||||
{"HDR_UI_NITS", "display"}, {"HDR_HUE", "display"}, {"SHADOW_RES", "display"},
|
||||
{"DLAA", "display"}, {"DLSS", "display"}, {"DLSS_PRESET", "display"}, {"DLSS_CHAR_MASK", "display"}, {"FXAA", "display"},
|
||||
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
|
||||
{"DISCORD_RPC", "discord"}, {"DISCORD_APP_ID", "discord"}, {"DISCORD_IMAGE", "discord"},
|
||||
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
|
||||
{"PCSS", "display"},
|
||||
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
||||
{"STICK_SENS", "input"},
|
||||
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
|
||||
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
|
||||
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
|
||||
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
|
||||
{"BIND_RIGHT", "input"}, {"BIND_DOWN", "input"}, {"BIND_LSTICK_LEFT", "input"}, {"BIND_LSTICK_UP", "input"},
|
||||
{"BIND_LSTICK_RIGHT", "input"}, {"BIND_LSTICK_DOWN", "input"}, {"BIND_RSTICK_LEFT", "input"},
|
||||
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"}, {"BIND_SHAKE", "input"},
|
||||
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"FREE_CORES", "performance"},
|
||||
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
|
||||
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
|
||||
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
|
||||
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
|
||||
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
|
||||
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
|
||||
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
|
||||
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
|
||||
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_MERGE", "diagnostics"}, {"WB_EXACT", "diagnostics"},
|
||||
{"MV_SURVEY", "diagnostics"},
|
||||
};
|
||||
|
||||
// The settings the menu shows: the value in effect when the key is unset, and what the key accepts. The only place a
|
||||
// default lives: consumers read cfg_value / cfg_int / cfg_double / cfg_bool. A row with an empty default has no fixed
|
||||
// value (it depends on the machine); its consumer reads cfg_get and keeps an "unset" branch.
|
||||
#define LIVE 1
|
||||
#define RESTART 0
|
||||
static const CfgMeta metas[] = {
|
||||
// Screen
|
||||
{"FULLSCREEN", "0", "0|1", LIVE}, {"WINDOW_SIZE", "1920x1080", NULL, LIVE},
|
||||
{"VK_PRESENT", "mailbox", "mailbox|fifo|immediate", LIVE}, {"GPU_INDEX", "", NULL, RESTART},
|
||||
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
|
||||
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
|
||||
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
|
||||
{"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance|ultra_performance", LIVE},
|
||||
{"DLSS_PRESET", "default", "default|k|j", LIVE},
|
||||
{"DLSS_CHAR_MASK", "65", "0|35|65|100", LIVE}, {"FXAA", "1", "0|1", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
||||
{"ANISO", "game", "game|2|4|8|16", RESTART}, // vk_texture.c sampler_features
|
||||
// colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses
|
||||
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
|
||||
{"COLOR_CONTRAST", "100", "70..150", LIVE}, {"COLOR_SATURATION", "100", "50..200", LIVE},
|
||||
{"COLOR_GAMMA", "1", "0.8..1.2", LIVE},
|
||||
// HDR output (vk_hdr.c): on at start (the tonemap shader is swapped as shaders load); the nits apply at once
|
||||
{"HDR", "0", "0|1", RESTART}, {"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "80..500", LIVE},
|
||||
{"HDR_UI_NITS", "203", "80..500", LIVE}, {"HDR_HUE", "80", "0..100", LIVE},
|
||||
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
|
||||
{"SHADOW_RES", "game", "game|256|512|768|1024|1536|2048", RESTART},
|
||||
{"SHADOW_SCALE", "1", "1..4", RESTART},
|
||||
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
|
||||
{"PCSS", "0", "0..200", RESTART},
|
||||
// Game
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
|
||||
{"MODS_REBUILD", "auto", "auto|off", RESTART},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART},
|
||||
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR", RESTART},
|
||||
{"OPERATION_MODE", "docked", "docked|handheld", RESTART},
|
||||
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
|
||||
// Sound, Controls, Menu
|
||||
{"VOLUME", "100", "0..100", LIVE}, {"AUDIO_CHANNELS", "stereo", "auto|stereo|5.1", RESTART},
|
||||
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox", LIVE},
|
||||
{"PROMPTS", "nintendo", "nintendo|xbox|pc", RESTART},
|
||||
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", RESTART},
|
||||
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
|
||||
{"MOUSE_MODE", "direct", "stick|direct", LIVE},
|
||||
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
|
||||
{"WALK_KEY", "none", NULL, LIVE}, {"WALK_SCALE", "0.51", "0.2..0.9", LIVE},
|
||||
{"MENU_LANG", "", "en|ru|pt-BR|zh-Hans", LIVE},
|
||||
{"AMIIBO_DIR", "", NULL, LIVE},
|
||||
// Discord Rich Presence (discord_rpc.c): off by default, the Menu tab of the settings menu switches it.
|
||||
{"DISCORD_RPC", "0", "0|1", LIVE},
|
||||
{"DISCORD_APP_ID", "1567232599846293504", NULL, LIVE},
|
||||
{"DISCORD_IMAGE", "nx2navi_512x512", NULL, LIVE},
|
||||
// Advanced: scheduling
|
||||
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
|
||||
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
|
||||
// shaders and pipelines
|
||||
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "fullscreen", "strict|any|fullscreen", RESTART},
|
||||
{"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART},
|
||||
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "1", "0|1", RESTART},
|
||||
// textures and memory
|
||||
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
|
||||
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
|
||||
// render fixes
|
||||
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
|
||||
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
|
||||
{"DESC_TEMPLATE", "1", "0|1", RESTART},
|
||||
{"PUSH_SKIP", "1", "0|1", RESTART}, {"SPLIT_DRAWS", "64", "0..1000", RESTART},
|
||||
{"SPLIT_US", "1000", "0..20000", RESTART},
|
||||
// Vulkan
|
||||
{"VK_VALIDATION", "0", "0|1", RESTART}, {"VK_DYN_VERTEX", "", "0|1", RESTART},
|
||||
{"VK_DYN_A2C", "", "0|1", RESTART}, {"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
|
||||
// diagnostics
|
||||
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
|
||||
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_MERGE", "1", "0|1", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
|
||||
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
|
||||
{"INPUT_RECORD", "", NULL, RESTART},
|
||||
};
|
||||
|
||||
static int is_known(const char *key) {
|
||||
for (size_t i = 0; i < sizeof known / sizeof *known; i++)
|
||||
if (!strcmp(known[i], key)) return 1;
|
||||
return !strncmp(key, "CHEAT_", 6); // cheats.c and the cheat patch sets (tools/cheats.py)
|
||||
}
|
||||
|
||||
static void clear(void) {
|
||||
for (int i = 0; i < n_entries; i++) free(entries[i].key), free(entries[i].value);
|
||||
n_entries = 0;
|
||||
}
|
||||
|
||||
static char *trim(char *s) {
|
||||
while (*s == ' ' || *s == '\t') s++;
|
||||
char *e = s + strlen(s);
|
||||
while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r')) *--e = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
// The upper-case key without the prefix, in place.
|
||||
static char *norm_key(char *key) {
|
||||
for (char *p = key; *p; p++) *p = (char)toupper((unsigned char)*p);
|
||||
return strncmp(key, PREFIX, PREFIX_LEN) ? key : key + PREFIX_LEN;
|
||||
}
|
||||
|
||||
static void add(char *key, char *value, int line) {
|
||||
key = norm_key(key);
|
||||
size_t vl = strlen(value);
|
||||
if (vl >= 2 && value[0] == '"' && value[vl - 1] == '"') value[vl - 1] = 0, value++;
|
||||
if (!*key) return;
|
||||
if (!is_known(key)) rt_log("cfg: nx2navi.ini line %d: unknown key '%s'", line, key);
|
||||
if (!*value) return; // "key =" leaves the setting at its default
|
||||
for (int i = 0; i < n_entries; i++)
|
||||
if (!strcmp(entries[i].key, key)) { // a repeated key: the later line wins
|
||||
free(entries[i].value);
|
||||
entries[i].value = strdup(value);
|
||||
return;
|
||||
}
|
||||
if (n_entries == CFG_MAX_KEYS) return;
|
||||
entries[n_entries++] = (CfgEntry){strdup(key), strdup(value), 0, 0};
|
||||
}
|
||||
|
||||
// 0.1.9 had the Xbox prompts as a pad layout: pad_layout = xbox_prompts is pad_layout = nintendo with prompts = xbox.
|
||||
// Read as those two, which the menu shows and the next cfg_save writes; a prompts line of the file's own wins. The
|
||||
// value still works from the environment (input_pad_layout_pick, input_prompts_pick).
|
||||
static void migrate(void) {
|
||||
CfgEntry *layout = NULL, *prompts = NULL;
|
||||
for (int i = 0; i < n_entries; i++) {
|
||||
if (!strcmp(entries[i].key, "PAD_LAYOUT")) layout = &entries[i];
|
||||
if (!strcmp(entries[i].key, "PROMPTS")) prompts = &entries[i];
|
||||
}
|
||||
if (!layout || strcasecmp(layout->value, "xbox_prompts")) return;
|
||||
free(layout->value);
|
||||
layout->value = strdup("nintendo");
|
||||
layout->dirty = 1;
|
||||
if (!prompts && n_entries < CFG_MAX_KEYS) entries[n_entries++] = (CfgEntry){strdup("PROMPTS"), strdup("xbox"), 1, 0};
|
||||
rt_log("cfg: nx2navi.ini pad_layout = xbox_prompts is read as pad_layout = nintendo and prompts = %s",
|
||||
prompts ? prompts->value : "xbox");
|
||||
}
|
||||
|
||||
int cfg_load(const char *path) {
|
||||
clear();
|
||||
uint64_t size = 0;
|
||||
uint8_t *data = plat_file_read(path, &size);
|
||||
if (!data) return 0;
|
||||
char *p = (char *)data;
|
||||
if (size >= 3 && !memcmp(p, "\xEF\xBB\xBF", 3)) p += 3; // editors add a BOM
|
||||
for (int line = 1; *p; line++) {
|
||||
char *eol = strchr(p, '\n');
|
||||
if (eol) *eol = 0;
|
||||
char *s = trim(p);
|
||||
char *eq = strchr(s, '=');
|
||||
if (*s && *s != '#' && *s != ';' && *s != '[') {
|
||||
if (!eq) rt_log("cfg: nx2navi.ini line %d: no '=' in '%s'", line, s);
|
||||
else {
|
||||
*eq = 0;
|
||||
add(trim(s), trim(eq + 1), line);
|
||||
}
|
||||
}
|
||||
if (!eol) break;
|
||||
p = eol + 1;
|
||||
}
|
||||
free(data);
|
||||
migrate();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- device profiles: defaults for a known machine, below the environment and nx2navi.ini and above the table, so any
|
||||
// key the player sets wins and resetting a key in the menu returns to the profile's value. NX2NAVI_DEVICE_PROFILE = auto
|
||||
// (default: the Steam Deck when the firmware says so), desktop (none) or deck.
|
||||
//
|
||||
// The Steam Deck (LCD Jupiter, OLED Galileo; 1280x800, 16:10, 4 Zen 2 cores, RDNA 2 with 8 CUs sharing 16 GB): the game
|
||||
// at the screen's 1280x800 with its own 5/6 scene (the aspect patch gives the camera its 16:10, resolution.c, and the
|
||||
// romfs-patches-1280x800 overlay the HUD's);
|
||||
// fullscreen and vsync, as gamescope composes Game Mode; 40 fps, which divides 40 Hz (and 120 Hz on the OLED).
|
||||
// Starting points to replace with measured ones (docs/tester-guide.md, Steam Deck).
|
||||
static const struct { const char *key, *value; } DECK[] = {
|
||||
{"FULLSCREEN", "1"}, {"WINDOW_SIZE", "1280x800"}, {"OUTPUT_RES", "1280x800"}, {"RENDER_SCALE", "game"},
|
||||
{"VK_PRESENT", "fifo"}, {"FPS_LIMIT", "40"},
|
||||
};
|
||||
static int profile = -1; // 0 none, 1 Steam Deck
|
||||
|
||||
static int read_line(const char *path, char *out, size_t n) {
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return 0;
|
||||
int ok = fgets(out, (int)n, f) != NULL;
|
||||
fclose(f);
|
||||
out[ok ? strcspn(out, "\r\n") : 0] = 0;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The Steam Deck: Valve's board, product Jupiter or Galileo (/sys/class/dmi/id); SteamDeck=1, which Steam sets on it.
|
||||
static int on_steam_deck(void) {
|
||||
const char *env = getenv("SteamDeck");
|
||||
if (env && !strcmp(env, "1")) return 1;
|
||||
char vendor[64], product[64];
|
||||
return read_line("/sys/class/dmi/id/board_vendor", vendor, sizeof vendor) && !strcmp(vendor, "Valve") &&
|
||||
read_line("/sys/class/dmi/id/product_name", product, sizeof product) &&
|
||||
(!strcmp(product, "Jupiter") || !strcmp(product, "Galileo"));
|
||||
}
|
||||
|
||||
static const char *raw_get(const char *name);
|
||||
|
||||
static int profile_of(void) {
|
||||
int p = __atomic_load_n(&profile, __ATOMIC_ACQUIRE);
|
||||
if (p >= 0) return p;
|
||||
const char *set = raw_get(PREFIX "DEVICE_PROFILE");
|
||||
if (set && !strcmp(set, "deck")) p = 1;
|
||||
else if (set && *set && strcmp(set, "auto")) p = 0;
|
||||
else p = on_steam_deck();
|
||||
__atomic_store_n(&profile, p, __ATOMIC_RELEASE);
|
||||
return p;
|
||||
}
|
||||
|
||||
const char *cfg_device_profile(void) { return profile_of() ? "deck" : "desktop"; }
|
||||
|
||||
static const char *raw_get(const char *name) {
|
||||
const char *e = getenv(name);
|
||||
if (e || strncmp(name, PREFIX, PREFIX_LEN)) return e;
|
||||
int n = __atomic_load_n(&n_entries, __ATOMIC_ACQUIRE);
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!strcasecmp(entries[i].key, name + PREFIX_LEN)) return __atomic_load_n(&entries[i].value, __ATOMIC_ACQUIRE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *cfg_get(const char *name) {
|
||||
const char *v = raw_get(name);
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN) || !profile_of()) return v;
|
||||
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
|
||||
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Whether name's value comes from the player (the environment or nx2navi.ini), not a profile or the table.
|
||||
int cfg_is_set(const char *name) { return raw_get(name) != NULL; }
|
||||
|
||||
unsigned cfg_version(void) { return __atomic_load_n(&version, __ATOMIC_ACQUIRE); }
|
||||
|
||||
int cfg_from_env(const char *name) { return getenv(name) != NULL; }
|
||||
|
||||
void cfg_set(const char *name, const char *value) {
|
||||
if (strncmp(name, PREFIX, PREFIX_LEN) || !value) return;
|
||||
char key[64];
|
||||
snprintf(key, sizeof key, "%s", name + PREFIX_LEN);
|
||||
for (char *p = key; *p; p++) *p = (char)toupper((unsigned char)*p);
|
||||
plat_mutex_lock(&set_lock);
|
||||
int i = 0, n = n_entries;
|
||||
while (i < n && strcmp(entries[i].key, key)) i++;
|
||||
if (i < n) {
|
||||
if (!entries[i].value || strcmp(entries[i].value, value)) {
|
||||
__atomic_store_n(&entries[i].value, strdup(value), __ATOMIC_RELEASE); // the old string stays for readers
|
||||
entries[i].dirty = 1;
|
||||
}
|
||||
entries[i].removed = 0; // a value set after cfg_reset replaces the line instead of deleting it
|
||||
} else if (n < CFG_MAX_KEYS) {
|
||||
entries[n] = (CfgEntry){strdup(key), strdup(value), 1, 0};
|
||||
__atomic_store_n(&n_entries, n + 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
__atomic_add_fetch(&version, 1, __ATOMIC_RELEASE);
|
||||
plat_mutex_unlock(&set_lock);
|
||||
}
|
||||
|
||||
int cfg_reset(const char *const *keys, int n) {
|
||||
int done = 0;
|
||||
plat_mutex_lock(&set_lock);
|
||||
for (int k = 0; k < n; k++) {
|
||||
char key[64], var[80];
|
||||
snprintf(key, sizeof key, "%s", keys[k]);
|
||||
const char *name = norm_key(key);
|
||||
snprintf(var, sizeof var, PREFIX "%s", name);
|
||||
if (getenv(var)) continue; // the environment wins; the file line stays
|
||||
int i = 0;
|
||||
while (i < n_entries && strcmp(entries[i].key, name)) i++;
|
||||
if (i == n_entries) { // no value in memory ("key =" in the file): the line still has to go
|
||||
if (i == CFG_MAX_KEYS) continue;
|
||||
entries[i] = (CfgEntry){strdup(name), NULL, 0, 1};
|
||||
__atomic_store_n(&n_entries, i + 1, __ATOMIC_RELEASE);
|
||||
} else if (entries[i].value) {
|
||||
__atomic_store_n(&entries[i].value, NULL, __ATOMIC_RELEASE); // the old string stays for readers
|
||||
entries[i].dirty = 0;
|
||||
entries[i].removed = 1; // cfg_save_to deletes the key's lines
|
||||
done++;
|
||||
} else {
|
||||
entries[i].removed = 1;
|
||||
}
|
||||
}
|
||||
if (done) __atomic_add_fetch(&version, 1, __ATOMIC_RELEASE);
|
||||
plat_mutex_unlock(&set_lock);
|
||||
return done;
|
||||
}
|
||||
|
||||
int cfg_last_save_ok(void) { return __atomic_load_n(&save_ok, __ATOMIC_ACQUIRE); }
|
||||
|
||||
void cfg_each_active(void (*cb)(const char *key, const char *value, int from_env, void *ctx), void *ctx) {
|
||||
int n = __atomic_load_n(&n_entries, __ATOMIC_ACQUIRE);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char *v = __atomic_load_n(&entries[i].value, __ATOMIC_ACQUIRE);
|
||||
if (v) cb(entries[i].key, v, 0, ctx);
|
||||
}
|
||||
for (size_t i = 0; i < sizeof known / sizeof *known; i++) {
|
||||
char var[80];
|
||||
snprintf(var, sizeof var, PREFIX "%s", known[i]);
|
||||
const char *e = getenv(var);
|
||||
if (e) cb(known[i], e, 1, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
const CfgMeta *cfg_meta(const char *key) {
|
||||
char k[64];
|
||||
snprintf(k, sizeof k, "%s", key);
|
||||
const char *name = norm_key(k);
|
||||
for (size_t i = 0; i < sizeof metas / sizeof *metas; i++)
|
||||
if (!strcmp(metas[i].key, name)) return &metas[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const CfgMeta *cfg_meta_at(int i) { return i >= 0 && (size_t)i < sizeof metas / sizeof *metas ? &metas[i] : NULL; }
|
||||
|
||||
const char *cfg_value(const char *name) {
|
||||
const char *e = cfg_get(name);
|
||||
if (e && *e) return e;
|
||||
const CfgMeta *m = cfg_meta(name);
|
||||
if (!m) rt_fatal("cfg: %s has no row in the settings table", name);
|
||||
return m->def;
|
||||
}
|
||||
|
||||
// The row's default as a number, for a set value that does not parse.
|
||||
static double default_number(const char *name) {
|
||||
return strtod(cfg_meta(name)->def, NULL);
|
||||
}
|
||||
|
||||
double cfg_double(const char *name) {
|
||||
const char *v = cfg_value(name);
|
||||
char *end;
|
||||
double d = strtod(v, &end);
|
||||
return end == v || *end || d != d ? default_number(name) : d;
|
||||
}
|
||||
|
||||
int cfg_int(const char *name) {
|
||||
const char *v = cfg_value(name);
|
||||
char *end;
|
||||
long l = strtol(v, &end, 10);
|
||||
return end == v || *end ? (int)default_number(name) : (int)l;
|
||||
}
|
||||
|
||||
int cfg_bool(const char *name) { return strcmp(cfg_value(name), "0") != 0; }
|
||||
|
||||
// ---- cfg_save: the file as lines, each with its line ending
|
||||
typedef struct { const char *s; size_t n; } Line; // n includes the '\n' (and '\r') when present
|
||||
|
||||
static int line_count(const char *p, size_t size) {
|
||||
int n = 0;
|
||||
for (size_t i = 0; i < size; i++) n += p[i] == '\n';
|
||||
return n + 1;
|
||||
}
|
||||
|
||||
// The line's text without its ending, BOM and surrounding blanks, into out.
|
||||
static void line_text(const Line *l, int first, char *out, size_t cap) {
|
||||
size_t n = l->n;
|
||||
const char *s = l->s;
|
||||
if (first && n >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) s += 3, n -= 3;
|
||||
while (n && (s[n - 1] == '\n' || s[n - 1] == '\r')) n--;
|
||||
if (n >= cap) n = cap - 1;
|
||||
memcpy(out, s, n);
|
||||
out[n] = 0;
|
||||
char *t = trim(out);
|
||||
memmove(out, t, strlen(t) + 1);
|
||||
}
|
||||
|
||||
// The key of a "key = value" line (comment = 0) or of a commented "# key = value" line (comment = 1), normalised,
|
||||
// into key; 0 when the line is not one.
|
||||
static int line_key(const char *text, int comment, char *key, size_t cap) {
|
||||
if (comment) {
|
||||
if (*text != '#' && *text != ';') return 0;
|
||||
text++;
|
||||
while (*text == ' ' || *text == '\t') text++;
|
||||
}
|
||||
if (!*text || *text == '#' || *text == ';' || *text == '[') return 0;
|
||||
const char *eq = strchr(text, '=');
|
||||
if (!eq) return 0;
|
||||
size_t n = (size_t)(eq - text);
|
||||
while (n && (text[n - 1] == ' ' || text[n - 1] == '\t')) n--;
|
||||
if (!n || n >= cap) return 0;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (!isalnum((unsigned char)text[i]) && text[i] != '_') return 0;
|
||||
char tmp[128];
|
||||
memcpy(tmp, text, n);
|
||||
tmp[n] = 0;
|
||||
snprintf(key, cap, "%s", norm_key(tmp));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The section name of a "[name]" line, lower-case, into out; 0 when the line is not one.
|
||||
static int line_section(const char *text, char *out, size_t cap) {
|
||||
if (*text != '[') return 0;
|
||||
const char *e = strchr(text, ']');
|
||||
if (!e) return 0;
|
||||
char tmp[128];
|
||||
size_t n = (size_t)(e - text - 1);
|
||||
if (n >= sizeof tmp) n = sizeof tmp - 1;
|
||||
memcpy(tmp, text + 1, n);
|
||||
tmp[n] = 0;
|
||||
snprintf(out, cap, "%s", trim(tmp));
|
||||
for (char *p = out; *p; p++) *p = (char)tolower((unsigned char)*p);
|
||||
return 1;
|
||||
}
|
||||
|
||||
typedef struct { char *buf; size_t n, cap; } Out;
|
||||
static void put(Out *o, const char *s, size_t n) {
|
||||
if (o->n + n + 1 > o->cap) {
|
||||
o->cap = (o->n + n + 1) * 2;
|
||||
o->buf = realloc(o->buf, o->cap);
|
||||
if (!o->buf) rt_fatal("cfg: out of memory writing nx2navi.ini");
|
||||
}
|
||||
memcpy(o->buf + o->n, s, n);
|
||||
o->n += n;
|
||||
}
|
||||
static void puts_(Out *o, const char *s) { put(o, s, strlen(s)); }
|
||||
|
||||
static const char *section_of(const char *key) {
|
||||
for (size_t i = 0; i < sizeof sections / sizeof *sections; i++)
|
||||
if (!strcmp(sections[i].key, key)) return sections[i].section;
|
||||
if (!strncmp(key, "CHEAT_", 6)) return "cheats"; // cheats.c: one key per cheat of the cheat file
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// "key = value" + eol for entry e (key lower-case)
|
||||
static void put_entry(Out *o, const CfgEntry *e, const char *eol) {
|
||||
char k[64];
|
||||
snprintf(k, sizeof k, "%s", e->key);
|
||||
for (char *p = k; *p; p++) *p = (char)tolower((unsigned char)*p);
|
||||
puts_(o, k);
|
||||
puts_(o, " = ");
|
||||
puts_(o, e->value);
|
||||
puts_(o, eol);
|
||||
}
|
||||
|
||||
int cfg_save_to(const char *path) {
|
||||
plat_mutex_lock(&set_lock);
|
||||
int n = n_entries, any = 0;
|
||||
for (int i = 0; i < n; i++) any |= entries[i].dirty | entries[i].removed;
|
||||
if (!any) {
|
||||
__atomic_store_n(&save_ok, 1, __ATOMIC_RELEASE);
|
||||
plat_mutex_unlock(&set_lock);
|
||||
return 1;
|
||||
}
|
||||
uint64_t size = 0;
|
||||
uint8_t *data = plat_file_read(path, &size);
|
||||
if (!data && plat_file_exists(path) && plat_file_size(path) != 0) {
|
||||
// the file is there but unreadable (locked, transient error): rebuilding it from nothing would lose its lines
|
||||
rt_log("cfg: cannot read %s, nothing written", path);
|
||||
__atomic_store_n(&save_ok, 0, __ATOMIC_RELEASE);
|
||||
plat_mutex_unlock(&set_lock);
|
||||
return 0;
|
||||
}
|
||||
const char *text = data ? (const char *)data : "";
|
||||
int nl = data ? line_count(text, size) : 0;
|
||||
Line *lines = calloc((size_t)nl + 1, sizeof *lines);
|
||||
// after[i]: entries to write after line i (-1 = none); replace[i]: the entry whose value line i now carries
|
||||
int *replace = malloc(sizeof(int) * ((size_t)nl + 1)), *after_key = malloc(sizeof(int) * (size_t)(n + 1));
|
||||
char *drop = calloc((size_t)nl + 1, 1); // lines of a reset key
|
||||
if (!lines || !replace || !after_key || !drop) rt_fatal("cfg: out of memory writing nx2navi.ini");
|
||||
for (int i = 0, at = 0; i < nl; i++) {
|
||||
size_t start = (size_t)at;
|
||||
while ((uint64_t)at < size && text[at] != '\n') at++;
|
||||
if ((uint64_t)at < size) at++;
|
||||
lines[i] = (Line){text + start, (size_t)at - start};
|
||||
replace[i] = -1;
|
||||
}
|
||||
if (nl && !lines[nl - 1].n) nl--; // the text ended with a newline: no last empty line
|
||||
const char *eol = data && strstr(text, "\r\n") ? "\r\n" : "\n";
|
||||
char t[1024], k[128], sec[128];
|
||||
for (int e = 0; e < n; e++) {
|
||||
after_key[e] = -2; // -2 clean, -1 append in a new section at the end, >= 0 insert after that line
|
||||
if (entries[e].removed)
|
||||
for (int i = 0; i < nl; i++) {
|
||||
line_text(&lines[i], i == 0, t, sizeof t);
|
||||
if (line_key(t, 0, k, sizeof k) && !strcmp(k, entries[e].key)) drop[i] = 1;
|
||||
}
|
||||
if (!entries[e].dirty) continue;
|
||||
int last = -1;
|
||||
for (int i = 0; i < nl; i++) {
|
||||
line_text(&lines[i], i == 0, t, sizeof t);
|
||||
if (line_key(t, 0, k, sizeof k) && !strcmp(k, entries[e].key)) last = i;
|
||||
}
|
||||
if (last >= 0) {
|
||||
replace[last] = e;
|
||||
continue;
|
||||
}
|
||||
const char *want = section_of(entries[e].key);
|
||||
int in = 0, example = -1, tail = -1;
|
||||
for (int i = 0; i < nl && want; i++) {
|
||||
line_text(&lines[i], i == 0, t, sizeof t);
|
||||
if (line_section(t, sec, sizeof sec)) {
|
||||
in = !strcmp(sec, want);
|
||||
if (in) tail = i;
|
||||
continue;
|
||||
}
|
||||
if (!in) continue;
|
||||
if (*t) tail = i;
|
||||
if (line_key(t, 1, k, sizeof k) && !strcmp(k, entries[e].key)) example = i;
|
||||
}
|
||||
after_key[e] = example >= 0 ? example : tail;
|
||||
}
|
||||
Out o = {0};
|
||||
for (int i = 0; i < nl; i++) {
|
||||
if (drop[i]) {
|
||||
// the line leaves the file; keys inserted after it still need a line ending before them
|
||||
} else if (replace[i] >= 0) {
|
||||
// keep the line's indentation and key spelling, put the new value after " = "
|
||||
const char *s = lines[i].s;
|
||||
size_t len = lines[i].n;
|
||||
if (i == 0 && len >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) put(&o, s, 3), s += 3, len -= 3;
|
||||
const char *eq = memchr(s, '=', len);
|
||||
size_t kn = (size_t)(eq - s);
|
||||
while (kn && (s[kn - 1] == ' ' || s[kn - 1] == '\t')) kn--;
|
||||
put(&o, s, kn);
|
||||
puts_(&o, " = ");
|
||||
puts_(&o, entries[replace[i]].value);
|
||||
int had_eol = len && s[len - 1] == '\n';
|
||||
if (had_eol) puts_(&o, eol);
|
||||
} else {
|
||||
put(&o, lines[i].s, lines[i].n);
|
||||
}
|
||||
for (int e = 0; e < n; e++)
|
||||
if (after_key[e] == i) {
|
||||
if (o.n && o.buf[o.n - 1] != '\n') puts_(&o, eol);
|
||||
put_entry(&o, &entries[e], eol);
|
||||
}
|
||||
}
|
||||
// keys whose section the file lacks: one new section each, at the end
|
||||
for (int e = 0; e < n; e++) {
|
||||
if (after_key[e] != -1) continue;
|
||||
const char *want = section_of(entries[e].key);
|
||||
if (o.n && o.buf[o.n - 1] != '\n') puts_(&o, eol);
|
||||
if (want) {
|
||||
if (o.n) puts_(&o, eol);
|
||||
puts_(&o, "[");
|
||||
puts_(&o, want);
|
||||
puts_(&o, "]");
|
||||
puts_(&o, eol);
|
||||
}
|
||||
put_entry(&o, &entries[e], eol);
|
||||
// the following keys of the same new section go right after this one
|
||||
for (int f = e + 1; f < n && want; f++)
|
||||
if (after_key[f] == -1 && section_of(entries[f].key) && !strcmp(section_of(entries[f].key), want)) {
|
||||
put_entry(&o, &entries[f], eol);
|
||||
after_key[f] = -3;
|
||||
}
|
||||
}
|
||||
int ok = plat_file_write_atomic(path, o.buf ? o.buf : "", o.n);
|
||||
if (ok)
|
||||
for (int e = 0; e < n; e++) entries[e].dirty = entries[e].removed = 0;
|
||||
else
|
||||
rt_log("cfg: cannot write %s", path);
|
||||
__atomic_store_n(&save_ok, ok ? 1 : 0, __ATOMIC_RELEASE);
|
||||
free(o.buf);
|
||||
free(drop);
|
||||
free(after_key);
|
||||
free(replace);
|
||||
free(lines);
|
||||
free(data);
|
||||
plat_mutex_unlock(&set_lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static char ini_path[1024];
|
||||
|
||||
static int find_ini(void) {
|
||||
if (ini_path[0]) return 1;
|
||||
char path[1024];
|
||||
if (!plat_exe_path(path, sizeof path)) return 0;
|
||||
char *cut = plat_last_sep(path);
|
||||
if (!cut) cut = strrchr(path, '/');
|
||||
if (!cut || cut + 10 >= path + sizeof path) return 0;
|
||||
strcpy(cut + 1, "nx2navi.ini");
|
||||
snprintf(ini_path, sizeof ini_path, "%s", path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int cfg_save(void) {
|
||||
if (find_ini()) return cfg_save_to(ini_path);
|
||||
__atomic_store_n(&save_ok, 0, __ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void load_ini(void) {
|
||||
if (!find_ini()) return;
|
||||
if (!cfg_load(ini_path)) {
|
||||
rt_log("cfg: no settings file at %s", ini_path);
|
||||
return;
|
||||
}
|
||||
rt_log("cfg: read %s: %d keys", ini_path, n_entries);
|
||||
for (int i = 0; i < n_entries; i++) {
|
||||
char var[128];
|
||||
snprintf(var, sizeof var, PREFIX "%s", entries[i].key);
|
||||
if (getenv(var)) rt_log("cfg: %s is set in the environment and overrides nx2navi.ini", var);
|
||||
}
|
||||
}
|
||||
|
||||
void cfg_init(void) {
|
||||
load_ini();
|
||||
if (profile_of())
|
||||
rt_log("cfg: device profile Steam Deck: defaults for the keys nx2navi.ini does not set (NX2NAVI_DEVICE_PROFILE=desktop "
|
||||
"turns it off)");
|
||||
}
|
||||
62
runtime/cfg.h
Normal file
62
runtime/cfg.h
Normal file
@@ -0,0 +1,62 @@
|
||||
// Settings: the NX2NAVI_* environment variables, with nx2navi.ini next to nx2navi.exe behind them.
|
||||
#pragma once
|
||||
|
||||
// getenv for a setting: the environment variable when set, else the nx2navi.ini key (the name without "NX2NAVI_", any
|
||||
// case), else the device profile's default (cfg.c: the Steam Deck), else NULL. Same string semantics as getenv; the pointer stays valid for the process (a value cfg_set replaces
|
||||
// is kept). A name without the NX2NAVI_ prefix goes to getenv unchanged. Safe from any thread once cfg_init has returned.
|
||||
const char *cfg_get(const char *name);
|
||||
|
||||
// Reads nx2navi.ini once, from mem_init after the address space is claimed and before any setting is read. Logs the file, its key count, unknown keys and environment overrides.
|
||||
void cfg_init(void);
|
||||
|
||||
// Replaces the loaded keys with those of the file at path (tests). 1 when the file was read, 0 when missing.
|
||||
// Not thread-safe: nothing else may call cfg_get meanwhile.
|
||||
int cfg_load(const char *path);
|
||||
|
||||
// Live settings (the in-game menu). cfg_set stores value as the nx2navi.ini value of name (NX2NAVI_ prefixed), marks it for
|
||||
// cfg_save and bumps cfg_version. cfg_get returns it unless the environment sets name. Any thread.
|
||||
void cfg_set(const char *name, const char *value);
|
||||
// Bumped by every cfg_set. A consumer that keeps a parsed value compares this with the number it last saw (one atomic
|
||||
// load) and parses again when it changed.
|
||||
unsigned cfg_version(void);
|
||||
// 1 when the environment sets name: cfg_set cannot change what cfg_get returns (the menu greys the setting out).
|
||||
int cfg_from_env(const char *name);
|
||||
// 1 when the player set name (the environment or nx2navi.ini), 0 when cfg_get gives a device profile's default or NULL.
|
||||
int cfg_is_set(const char *name);
|
||||
// The device profile in use, "deck" or "desktop" (NX2NAVI_DEVICE_PROFILE, else what the machine is).
|
||||
const char *cfg_device_profile(void);
|
||||
// Writes the keys cfg_set changed since the last save into nx2navi.ini next to nx2navi.exe (cfg_save_to: the file at path).
|
||||
// Only those lines change: the last line of a changed key gets the new value, a missing key goes under its section
|
||||
// (after the commented example line of the key when the section has one; the section is added at the end of the file
|
||||
// when missing), comments and every other line stay byte for byte. 1 on success or with nothing to write.
|
||||
int cfg_save(void);
|
||||
int cfg_save_to(const char *path);
|
||||
|
||||
// Reset (the menu's "Reset all settings to defaults"). Removes the active line of each key (names without NX2NAVI_, any
|
||||
// case) from nx2navi.ini at the next cfg_save and its value from memory, so the consumer's own default applies again; the
|
||||
// commented example lines stay. Keys the environment sets are skipped. Bumps cfg_version. Returns how many it reset.
|
||||
int cfg_reset(const char *const *keys, int n);
|
||||
// 0 when the last cfg_save / cfg_save_to failed to write (the values stay set in memory, the next save tries again).
|
||||
int cfg_last_save_ok(void);
|
||||
// Every setting with a value, for a bug report ("Copy system info"): the nx2navi.ini and cfg_set keys (name without
|
||||
// NX2NAVI_, upper case; from_env 0), then the known NX2NAVI_ variables the environment sets (from_env 1).
|
||||
void cfg_each_active(void (*cb)(const char *key, const char *value, int from_env, void *ctx), void *ctx);
|
||||
|
||||
// What the menu knows about a setting. key: without NX2NAVI_, upper case. def: what the consumer uses when the key is
|
||||
// unset ("" = no fixed default, the consumer picks per machine). values: "0|1", "mailbox|fifo|immediate", a range
|
||||
// "0..300", or NULL for free text. live: applies at once; the others are read at start.
|
||||
typedef struct { const char *key, *def, *values; int live; } CfgMeta;
|
||||
// The row of key (prefix and case ignored); NULL for a key the menu does not show.
|
||||
const CfgMeta *cfg_meta(const char *key);
|
||||
// The i-th row (0-based, table order); NULL past the last.
|
||||
const CfgMeta *cfg_meta_at(int i);
|
||||
|
||||
// The setting's value for consumers: the environment or nx2navi.ini value, else the CfgMeta default. name has the NX2NAVI_
|
||||
// prefix, like cfg_get; a name without a row is a bug (rt_fatal). A row with an empty default has no fixed value: its
|
||||
// consumer reads cfg_get and keeps its own "unset" branch. An empty value counts as unset.
|
||||
const char *cfg_value(const char *name);
|
||||
// cfg_value parsed. A set value that does not parse (int: whole string; double: strtod) gives the default's number.
|
||||
// cfg_bool: "0" is false, anything else true.
|
||||
int cfg_int(const char *name);
|
||||
double cfg_double(const char *name);
|
||||
int cfg_bool(const char *name);
|
||||
475
runtime/cfg_probe.c
Normal file
475
runtime/cfg_probe.c
Normal file
@@ -0,0 +1,475 @@
|
||||
// --probe-cfg: nx2navi.ini parsing, the environment override and a missing file (cfg.c). Needs no game files.
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "input_map.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL cfg: %s\n", what); } } while (0)
|
||||
#define EQ(a, b) ((a) && !strcmp((a), (b)))
|
||||
|
||||
int probe_cfg(void) {
|
||||
char path[1024];
|
||||
if (!plat_exe_path(path, sizeof path)) return 1;
|
||||
char *cut = plat_last_sep(path);
|
||||
if (!cut) return 1;
|
||||
strcpy(cut + 1, "cfg-probe.ini");
|
||||
static const char ini[] =
|
||||
"\xEF\xBB\xBF# comment\r\n"
|
||||
"; another comment\r\n"
|
||||
"[frame rate]\r\n"
|
||||
" fps60 = 0 \r\n"
|
||||
"Realtime=0\r\n"
|
||||
"[Display]\r\n"
|
||||
"GPU_name = \"My GPU 1\"\r\n"
|
||||
"NX2NAVI_Locale = de-DE\r\n"
|
||||
"lang =\r\n"
|
||||
"trace = a\r\n"
|
||||
"trace = b\r\n"
|
||||
"no_such_setting = 1\r\n"
|
||||
"no equals sign\r\n"
|
||||
"last = no newline at the end";
|
||||
plat_setenv("NX2NAVI_LOCALE", NULL);
|
||||
plat_setenv("NX2NAVI_FPS60", NULL);
|
||||
CHECK(plat_file_write(path, ini, sizeof ini - 1), "write the probe ini");
|
||||
CHECK(cfg_load(path) == 1, "load the probe ini");
|
||||
CHECK(EQ(cfg_get("NX2NAVI_FPS60"), "0"), "value read, whitespace trimmed, lower-case key");
|
||||
CHECK(EQ(cfg_get("NX2NAVI_REALTIME"), "0"), "key without spaces, mixed case");
|
||||
CHECK(EQ(cfg_get("NX2NAVI_GPU_NAME"), "My GPU 1"), "quoted value, section ignored");
|
||||
CHECK(EQ(cfg_get("NX2NAVI_LOCALE"), "de-DE"), "NX2NAVI_ prefix accepted in the file");
|
||||
CHECK(cfg_get("NX2NAVI_LANG") == NULL, "empty value is unset");
|
||||
CHECK(EQ(cfg_get("NX2NAVI_TRACE"), "b"), "later line wins");
|
||||
CHECK(cfg_get("NX2NAVI_CLOTH_REALTIME") == NULL, "absent key is NULL");
|
||||
CHECK(cfg_get("NX2NAVI_comment") == NULL, "comment is not a key");
|
||||
CHECK(EQ(cfg_get("NX2NAVI_NO_SUCH_SETTING"), "1"), "unknown key is still readable");
|
||||
plat_setenv("NX2NAVI_FPS60", "1");
|
||||
CHECK(EQ(cfg_get("NX2NAVI_FPS60"), "1"), "environment overrides the file");
|
||||
plat_setenv("NX2NAVI_FPS60", NULL);
|
||||
CHECK(EQ(cfg_get("NX2NAVI_FPS60"), "0"), "file value returns when the variable is removed");
|
||||
plat_file_delete(path);
|
||||
CHECK(cfg_load(path) == 0, "missing file is not an error");
|
||||
CHECK(cfg_get("NX2NAVI_FPS60") == NULL, "missing file leaves no keys");
|
||||
|
||||
// cfg_set / cfg_save: only changed lines move, comments stay, a missing key goes under its section
|
||||
static const char before[] =
|
||||
"\xEF\xBB\xBF# my settings\r\n"
|
||||
"[frame rate]\r\n"
|
||||
" FPS60 = 1\r\n"
|
||||
"; keep this\r\n"
|
||||
"\r\n"
|
||||
"[input]\r\n"
|
||||
"# Gamepad motion controls (0 = off) and vibration (0 = off).\r\n"
|
||||
"# motion = 1\r\n"
|
||||
"# rumble = 1\r\n"
|
||||
"rumble_curve = 0.6\r\n"
|
||||
"\r\n"
|
||||
"[files]\r\n"
|
||||
"romfs_mods = C:\\mods";
|
||||
static const char after[] =
|
||||
"\xEF\xBB\xBF# my settings\r\n"
|
||||
"[frame rate]\r\n"
|
||||
" FPS60 = 0\r\n"
|
||||
"; keep this\r\n"
|
||||
"\r\n"
|
||||
"[input]\r\n"
|
||||
"# Gamepad motion controls (0 = off) and vibration (0 = off).\r\n"
|
||||
"# motion = 1\r\n"
|
||||
"motion = 0\r\n"
|
||||
"# rumble = 1\r\n"
|
||||
"rumble_curve = 0.6\r\n"
|
||||
"rumble_strength = 80\r\n"
|
||||
"\r\n"
|
||||
"[files]\r\n"
|
||||
"romfs_mods = C:\\mods\r\n"
|
||||
"\r\n"
|
||||
"[audio]\r\n"
|
||||
"volume = 40\r\n";
|
||||
plat_setenv("NX2NAVI_MOTION", NULL);
|
||||
plat_setenv("NX2NAVI_VOLUME", NULL);
|
||||
plat_setenv("NX2NAVI_RUMBLE_STRENGTH", NULL);
|
||||
CHECK(plat_file_write(path, before, sizeof before - 1), "write the save ini");
|
||||
CHECK(cfg_load(path) == 1, "load the save ini");
|
||||
unsigned v0 = cfg_version();
|
||||
cfg_set("NX2NAVI_FPS60", "0");
|
||||
cfg_set("NX2NAVI_MOTION", "0");
|
||||
cfg_set("NX2NAVI_RUMBLE_STRENGTH", "80");
|
||||
cfg_set("NX2NAVI_VOLUME", "40");
|
||||
CHECK(cfg_version() == v0 + 4, "cfg_set bumps the version");
|
||||
CHECK(EQ(cfg_get("NX2NAVI_FPS60"), "0") && EQ(cfg_get("NX2NAVI_VOLUME"), "40"), "cfg_set values are read back at once");
|
||||
CHECK(cfg_save_to(path) == 1, "cfg_save_to writes");
|
||||
uint64_t got_n = 0;
|
||||
uint8_t *got = plat_file_read(path, &got_n);
|
||||
CHECK(got && got_n == sizeof after - 1 && !memcmp(got, after, got_n),
|
||||
"saved file: changed line replaced, comments kept, missing keys under their sections");
|
||||
if (got && (got_n != sizeof after - 1 || memcmp(got, after, got_n))) fprintf(stderr, "---- saved:\n%s\n----\n", got);
|
||||
free(got);
|
||||
CHECK(cfg_save_to(path) == 1, "a save with nothing changed");
|
||||
got = plat_file_read(path, &got_n);
|
||||
CHECK(got && got_n == sizeof after - 1 && !memcmp(got, after, got_n), "a save with nothing changed keeps the file");
|
||||
free(got);
|
||||
CHECK(cfg_load(path) == 1 && EQ(cfg_get("NX2NAVI_FPS60"), "0") && EQ(cfg_get("NX2NAVI_MOTION"), "0") &&
|
||||
EQ(cfg_get("NX2NAVI_RUMBLE_STRENGTH"), "80") && EQ(cfg_get("NX2NAVI_VOLUME"), "40") &&
|
||||
EQ(cfg_get("NX2NAVI_ROMFS_MODS"), "C:\\mods"), "the saved file reads back");
|
||||
const char *held = cfg_get("NX2NAVI_VOLUME");
|
||||
plat_setenv("NX2NAVI_VOLUME", "70");
|
||||
cfg_set("NX2NAVI_VOLUME", "90");
|
||||
CHECK(cfg_from_env("NX2NAVI_VOLUME") && EQ(cfg_get("NX2NAVI_VOLUME"), "70"), "environment wins over cfg_set");
|
||||
CHECK(EQ(held, "40"), "a pointer cfg_get handed out stays valid after cfg_set");
|
||||
plat_setenv("NX2NAVI_VOLUME", NULL);
|
||||
CHECK(!cfg_from_env("NX2NAVI_VOLUME") && EQ(cfg_get("NX2NAVI_VOLUME"), "90"), "cfg_set value returns without the variable");
|
||||
plat_file_delete(path);
|
||||
CHECK(cfg_save_to(path) == 1, "cfg_save_to creates a missing file");
|
||||
got = plat_file_read(path, &got_n);
|
||||
CHECK(got && !strcmp((const char *)got, "[audio]\nvolume = 90\n"), "new file: the section and the key");
|
||||
free(got);
|
||||
|
||||
// Defaults table: the menu's "default" marks and Reset
|
||||
const CfgMeta *m = cfg_meta("FPS60");
|
||||
CHECK(m && EQ(m->def, "1") && m->live, "meta: fps60 defaults to 1 and is live");
|
||||
m = cfg_meta("ASYNC_PREEMPT");
|
||||
CHECK(m && EQ(m->values, "0|1|2") && !m->live, "meta: async_preempt takes 0|1|2 and needs a restart");
|
||||
m = cfg_meta("nx2navi_vk_present");
|
||||
CHECK(m && EQ(m->key, "VK_PRESENT") && EQ(m->values, "mailbox|fifo|immediate") && m->live,
|
||||
"meta: prefix and case are accepted");
|
||||
m = cfg_meta("RUMBLE_STRENGTH");
|
||||
CHECK(m && EQ(m->def, "130") && EQ(m->values, "0..300"), "meta: rumble strength 130 in 0..300");
|
||||
m = cfg_meta("PERF_OVERLAY");
|
||||
CHECK(m && EQ(m->def, "off") && EQ(m->values, "off|fps|full") && m->live, "meta: perf_overlay is live, off by default");
|
||||
CHECK(cfg_meta("BIND_A") == NULL && cfg_meta("FPS_COUNTER") == NULL && cfg_meta("NO_SUCH") == NULL,
|
||||
"meta: keys the menu does not show are NULL");
|
||||
|
||||
// cfg_reset: the key's active lines leave nx2navi.ini, the commented example stays
|
||||
static const char rbase[] = "[audio]\n# volume = 100\n[input]\nmotion = 0\n";
|
||||
static const char *const vol[] = {"VOLUME"};
|
||||
plat_setenv("NX2NAVI_VOLUME", NULL);
|
||||
CHECK(plat_file_write(path, rbase, sizeof rbase - 1), "write the reset ini");
|
||||
CHECK(cfg_load(path) == 1, "load the reset ini");
|
||||
cfg_set("NX2NAVI_VOLUME", "40");
|
||||
CHECK(cfg_save_to(path) == 1 && cfg_last_save_ok(), "reset: volume saved");
|
||||
got = plat_file_read(path, &got_n);
|
||||
CHECK(got && strstr((const char *)got, "\nvolume = 40\n"), "reset: the active line is in the file");
|
||||
free(got);
|
||||
unsigned v1 = cfg_version();
|
||||
CHECK(cfg_reset(vol, 1) == 1, "reset: one key reset");
|
||||
CHECK(cfg_version() > v1 && cfg_get("NX2NAVI_VOLUME") == NULL, "reset: the value is gone in memory, the version moved");
|
||||
CHECK(cfg_reset(vol, 1) == 0, "reset: a key already reset counts 0");
|
||||
CHECK(cfg_save_to(path) == 1, "reset: save");
|
||||
got = plat_file_read(path, &got_n);
|
||||
CHECK(got && got_n == sizeof rbase - 1 && !memcmp(got, rbase, got_n) && strstr((const char *)got, "# volume = 100"),
|
||||
"reset: no active volume line, the commented example and the other keys stay");
|
||||
if (got && (got_n != sizeof rbase - 1 || memcmp(got, rbase, got_n))) fprintf(stderr, "---- saved:\n%s\n----\n", got);
|
||||
free(got);
|
||||
cfg_set("NX2NAVI_VOLUME", "55");
|
||||
CHECK(cfg_reset(vol, 1) == 1 && cfg_get("NX2NAVI_VOLUME") == NULL, "reset: a value set after a save resets too");
|
||||
cfg_set("NX2NAVI_VOLUME", "60");
|
||||
CHECK(cfg_save_to(path) == 1 && EQ(cfg_get("NX2NAVI_VOLUME"), "60"), "reset then set before the save: the set wins");
|
||||
got = plat_file_read(path, &got_n);
|
||||
CHECK(got && strstr((const char *)got, "\nvolume = 60\n"), "reset then set: the new line is written");
|
||||
free(got);
|
||||
|
||||
// an active line with an empty value has no entry in memory; Reset still removes it from the file
|
||||
static const char rempty[] = "[audio]\n# volume = 100\nvolume =\n[input]\nmotion = 0\n";
|
||||
CHECK(plat_file_write(path, rempty, sizeof rempty - 1) && cfg_load(path) == 1 && cfg_get("NX2NAVI_VOLUME") == NULL,
|
||||
"reset: load an ini with an empty value");
|
||||
CHECK(cfg_reset(vol, 1) == 0 && cfg_save_to(path) == 1, "reset: an empty-valued key saves");
|
||||
got = plat_file_read(path, &got_n);
|
||||
CHECK(got && got_n == sizeof rbase - 1 && !memcmp(got, rbase, got_n), "reset: the empty-valued line is gone");
|
||||
free(got);
|
||||
cfg_set("NX2NAVI_VOLUME", "60");
|
||||
CHECK(cfg_save_to(path) == 1, "reset: volume 60 again for the environment case");
|
||||
|
||||
// a key the environment sets is skipped and keeps its file line
|
||||
CHECK(cfg_load(path) == 1 && EQ(cfg_get("NX2NAVI_VOLUME"), "60"), "reset: reload");
|
||||
plat_setenv("NX2NAVI_VOLUME", "70");
|
||||
CHECK(cfg_reset(vol, 1) == 0, "reset: an environment-set key is skipped");
|
||||
plat_setenv("NX2NAVI_VOLUME", NULL);
|
||||
CHECK(EQ(cfg_get("NX2NAVI_VOLUME"), "60"), "reset: the skipped key keeps its value");
|
||||
CHECK(cfg_save_to(path) == 1, "reset: save after a skip");
|
||||
got = plat_file_read(path, &got_n);
|
||||
CHECK(got && strstr((const char *)got, "\nvolume = 60\n"), "reset: the skipped key keeps its file line");
|
||||
free(got);
|
||||
|
||||
// a failed save: the call fails, cfg_last_save_ok says so, the value stays set and a later save works
|
||||
CHECK(cfg_last_save_ok() == 1, "save status: ok after a good save");
|
||||
cfg_set("NX2NAVI_VOLUME", "20");
|
||||
char bad[1100];
|
||||
snprintf(bad, sizeof bad, "%s", path);
|
||||
strcpy(plat_last_sep(bad) + 1, "no-such-dir/cfg-probe.ini");
|
||||
CHECK(cfg_save_to(bad) == 0 && cfg_last_save_ok() == 0 && EQ(cfg_get("NX2NAVI_VOLUME"), "20"),
|
||||
"save status: an unwritable path fails, the value stays");
|
||||
cfg_set("NX2NAVI_VOLUME", "21");
|
||||
CHECK(plat_file_write(path, rbase, sizeof rbase - 1), "write the read-only ini");
|
||||
CHECK(plat_file_set_readonly(path, 1), "set the file read-only");
|
||||
CHECK(cfg_save_to(path) == 0 && cfg_last_save_ok() == 0 && EQ(cfg_get("NX2NAVI_VOLUME"), "21"),
|
||||
"save status: a read-only file fails, cfg_get returns the new value");
|
||||
plat_file_set_readonly(path, 0);
|
||||
CHECK(cfg_save_to(path) == 1 && cfg_last_save_ok() == 1, "save status: the retry after the failure works");
|
||||
got = plat_file_read(path, &got_n);
|
||||
CHECK(got && strstr((const char *)got, "\nvolume = 21\n"), "save status: the retried save wrote the value");
|
||||
free(got);
|
||||
plat_file_delete(path);
|
||||
cfg_load(path);
|
||||
CHECK(rumble_motor(0, 0.6f, 1.3f) == 0 && rumble_motor(0.5f, 0.6f, 0) == 0, "rumble: zero amplitude or strength is silent");
|
||||
CHECK(rumble_motor(1, 0.6f, 3) == 65535 && rumble_motor(5, 1, 1) == 65535, "rumble: saturates at 65535");
|
||||
CHECK(rumble_motor(0.25f, 1, 1) > 16000 && rumble_motor(0.25f, 1, 1) < 16800, "rumble: curve 1, strength 100% is linear");
|
||||
CHECK(rumble_motor(0.1f, 0.6f, 1.3f) > rumble_motor(0.1f, 1, 1.3f), "rumble: curve lifts weak values");
|
||||
CHECK(rumble_motor(0.3f, 0.6f, 1.3f) > rumble_motor(0.3f, 0.6f, 1), "rumble: strength scales");
|
||||
// Keyboard and mouse bindings (input_map.c)
|
||||
uint8_t dn[KEY_COUNT] = {0};
|
||||
int32_t st[2];
|
||||
CHECK(input_map_buttons(dn) == 0, "keys: nothing held, no buttons");
|
||||
dn[KEY_MOUSE_L] = 1;
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_Y, "keys: left mouse button is Y");
|
||||
dn[KEY_MOUSE_R] = dn[KEY_Z] = 1;
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_Y | 1ull << B_ZL), "keys: right mouse button and Z both press ZL");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_X] = dn[KEY_TAB] = dn[KEY_M] = dn[KEY_LCTRL] = dn[KEY_3] = 1;
|
||||
CHECK(input_map_buttons(dn) == (1ull << B_ZR | 1ull << B_PLUS | 1ull << B_MINUS | 1ull << B_STICK_L | 1ull << B_RIGHT),
|
||||
"keys: X ZR, Tab Plus, M Minus, Left Ctrl stick click, 3 D-pad right");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_G] = 1;
|
||||
CHECK(input_map_buttons(dn) == 0 && input_map_held(dn, B_SHAKE) && !input_map_held(dn, B_A),
|
||||
"keys: G shakes and presses no button");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_W] = 1;
|
||||
input_map_stick(dn, 0, st);
|
||||
CHECK(st[0] == 0 && st[1] == 32767, "keys: W is left stick up");
|
||||
dn[KEY_A] = 1;
|
||||
input_map_stick(dn, 0, st);
|
||||
CHECK(st[0] == -23170 && st[1] == 23170, "keys: W A is a diagonal on the unit circle");
|
||||
dn[KEY_W] = dn[KEY_A] = dn[KEY_S] = 1;
|
||||
dn[KEY_D] = 1;
|
||||
input_map_stick(dn, 0, st);
|
||||
CHECK(st[0] == 0 && st[1] == 0, "keys: opposite keys cancel");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_LEFT] = 1;
|
||||
input_map_stick(dn, 1, st);
|
||||
CHECK(st[0] == -32767 && st[1] == 0, "keys: left arrow is right stick left");
|
||||
input_map_stick(dn, 0, st);
|
||||
CHECK(st[0] == 0 && st[1] == 0, "keys: arrows leave the left stick alone");
|
||||
input_mouse_stick(30, 0, 1.0 / 60, 1, st);
|
||||
CHECK(st[0] == 32767 * 1800 / 3000 && st[1] == 0, "mouse: 30 counts in 1/60 s is 60% right");
|
||||
input_mouse_stick(0, -10, 1.0 / 60, 1, st);
|
||||
CHECK(st[0] == 0 && st[1] > 0, "mouse: moving up is stick up");
|
||||
input_mouse_stick(30, 0, 1.0 / 30, 1, st);
|
||||
CHECK(st[0] == 32767 * 900 / 3000, "mouse: the same counts over twice the time are half the deflection");
|
||||
input_mouse_stick(15, 0, 1.0 / 60, 2, st);
|
||||
CHECK(st[0] == 32767 * 1800 / 3000, "mouse: doubling sensitivity halves the counts needed (15 counts at 2 = 30 at 1)");
|
||||
input_mouse_stick(1000, 1000, 1.0 / 60, 1, st);
|
||||
CHECK((int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768, "mouse: clamped to the unit circle");
|
||||
input_mouse_stick(30, 0, 1.0 / 60, 0, st);
|
||||
CHECK(st[0] == 0 && st[1] == 0, "mouse: sensitivity 0 is off");
|
||||
input_pad_stick(3000, -2000, 0.15f, st);
|
||||
CHECK(st[0] == 0 && st[1] == 0, "pad stick: an 11% drift inside a 15% dead zone reads centred");
|
||||
input_pad_stick(4000, -3000, 0, st);
|
||||
CHECK(st[0] == 4000 && st[1] == -3000, "pad stick: dead zone 0 passes the stick unchanged");
|
||||
input_pad_stick(32767, 0, 0.15f, st);
|
||||
CHECK(st[0] == 32767 && st[1] == 0, "pad stick: full deflection still reaches full");
|
||||
input_pad_stick(-32768, -32768, 0.15f, st);
|
||||
CHECK(st[0] < 0 && st[1] < 0 && st[0] >= -32767 && (int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768,
|
||||
"pad stick: a square gate's corner stays on the unit circle");
|
||||
input_pad_stick(0, 32767 / 2 + 1, 0.2f, st);
|
||||
CHECK(abs(st[1] - 32767 * 3 / 8) <= 3, "pad stick: half deflection past a 20% zone is (0.5 - 0.2) / 0.8 of full");
|
||||
plat_setenv("NX2NAVI_STICK_DEADZONE", "12");
|
||||
plat_setenv("NX2NAVI_LSTICK_DEADZONE", NULL);
|
||||
plat_setenv("NX2NAVI_RSTICK_DEADZONE", NULL);
|
||||
CHECK(fabsf(input_pad_deadzone(0) - 0.12f) < 1e-6f && fabsf(input_pad_deadzone(1) - 0.12f) < 1e-6f,
|
||||
"pad dead zone: both sticks follow stick_deadzone while lstick/rstick are unset");
|
||||
plat_setenv("NX2NAVI_RSTICK_DEADZONE", "20");
|
||||
CHECK(fabsf(input_pad_deadzone(0) - 0.12f) < 1e-6f && fabsf(input_pad_deadzone(1) - 0.20f) < 1e-6f,
|
||||
"pad dead zone: rstick_deadzone sets the right stick alone");
|
||||
plat_setenv("NX2NAVI_LSTICK_DEADZONE", "80");
|
||||
CHECK(fabsf(input_pad_deadzone(0) - 0.5f) < 1e-6f, "pad dead zone: clamped to 50%");
|
||||
plat_setenv("NX2NAVI_STICK_DEADZONE", NULL);
|
||||
plat_setenv("NX2NAVI_LSTICK_DEADZONE", NULL);
|
||||
plat_setenv("NX2NAVI_RSTICK_DEADZONE", NULL);
|
||||
CHECK(input_stick_percent(32767, 0) > 99.99 && input_stick_percent(32767, 0) < 100.01 &&
|
||||
input_stick_percent(0, 0) == 0 && fabs(input_stick_percent(3000, -4000) - 5000 * 100.0 / 32767) < 1e-9,
|
||||
"stick percent: an axis's end is 100%, the centre 0%, round in between");
|
||||
// Gamepad layouts (input_map.c)
|
||||
CHECK(input_pad_layout_pick("auto", 1) == PAD_LAYOUT_NINTENDO && input_pad_layout_pick(NULL, 0) == PAD_LAYOUT_XBOX,
|
||||
"pad layout: automatic is Nintendo for an east button labelled A, Xbox otherwise");
|
||||
CHECK(input_pad_layout_pick("Xbox", 1) == PAD_LAYOUT_XBOX && input_pad_layout_pick("nintendo", 0) == PAD_LAYOUT_NINTENDO,
|
||||
"pad layout: a set layout wins over the labels");
|
||||
CHECK(input_pad_layout_pick("xbox_prompts", 0) == PAD_LAYOUT_NINTENDO &&
|
||||
input_pad_layout_pick("Xbox_Prompts", 1) == PAD_LAYOUT_NINTENDO,
|
||||
"pad layout: 0.1.9's xbox_prompts keeps the Nintendo positions (only the prompts change, romfs.c overlay_init)");
|
||||
// Button prompts (input_map.c)
|
||||
CHECK(input_prompts_pick(NULL, "auto") == PROMPTS_NINTENDO && input_prompts_pick("", "xbox") == PROMPTS_NINTENDO &&
|
||||
input_prompts_pick("Xbox", "nintendo") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox") == PROMPTS_NINTENDO,
|
||||
"prompts: the game's own unless set to Xbox");
|
||||
CHECK(input_prompts_pick("PC", "nintendo") == PROMPTS_PC && input_prompts_pick("pc", "xbox_prompts") == PROMPTS_PC &&
|
||||
EQ(input_prompts_folder(PROMPTS_PC, "nintendo"), "romfs-patches-pc") &&
|
||||
EQ(input_prompts_folder(PROMPTS_PC, "xbox"), "romfs-patches-pc") &&
|
||||
EQ(input_prompts_folder(PROMPTS_PC, NULL), "romfs-patches-pc"),
|
||||
"prompts: pc shows the keyboard and mouse whatever the pad layout");
|
||||
CHECK(input_prompts_pick(NULL, "xbox_prompts") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox_prompts") == PROMPTS_NINTENDO,
|
||||
"prompts: 0.1.9's pad_layout = xbox_prompts means Xbox while prompts is unset");
|
||||
CHECK(!input_prompts_folder(PROMPTS_NINTENDO, "nintendo") && !input_prompts_folder(PROMPTS_NINTENDO, "xbox"),
|
||||
"prompts: the game's own need no folder");
|
||||
CHECK(EQ(input_prompts_folder(PROMPTS_XBOX, "nintendo"), "romfs-patches-xbox") &&
|
||||
EQ(input_prompts_folder(PROMPTS_XBOX, "xbox_prompts"), "romfs-patches-xbox") &&
|
||||
EQ(input_prompts_folder(PROMPTS_XBOX, "xbox"), "romfs-patches-xbox-label") &&
|
||||
EQ(input_prompts_folder(PROMPTS_XBOX, "auto"), "romfs-patches-xbox-label") &&
|
||||
EQ(input_prompts_folder(PROMPTS_XBOX, NULL), "romfs-patches-xbox-label"),
|
||||
"prompts: the face buttons by position for the Nintendo layout, by label for xbox and auto");
|
||||
// romfs-patches-xbox shows A as B (tests/romfs_prompts_test.py checks the folders' contents). It is right exactly
|
||||
// where the layout puts A on the east button, which an Xbox pad labels B.
|
||||
input_map_reset();
|
||||
static const char *const layouts[] = {"nintendo", "xbox", "auto", "xbox_prompts"};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int a_east = input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_EAST] = 1}, input_pad_layout_pick(layouts[i], 0)) == 1ull << B_A;
|
||||
CHECK(a_east == EQ(input_prompts_folder(PROMPTS_XBOX, layouts[i]), "romfs-patches-xbox"),
|
||||
"prompts: the folder names A as B exactly when the layout puts A on the button labelled B");
|
||||
}
|
||||
uint8_t pd[PAD_COUNT] = {0};
|
||||
pd[PAD_SOUTH] = pd[PAD_NORTH] = 1;
|
||||
CHECK(input_map_pad_buttons(pd, PAD_LAYOUT_NINTENDO) == (1ull << B_B | 1ull << B_X) &&
|
||||
input_map_pad_buttons(pd, PAD_LAYOUT_XBOX) == (1ull << B_A | 1ull << B_Y),
|
||||
"pad layout: south and north are B and X by position, A and Y on Xbox");
|
||||
pd[PAD_SOUTH] = pd[PAD_NORTH] = 0;
|
||||
pd[PAD_START] = pd[PAD_ZR] = 1;
|
||||
CHECK(input_map_pad_buttons(pd, PAD_LAYOUT_XBOX) == (1ull << B_PLUS | 1ull << B_ZR), "pad layout: Xbox keeps the other buttons");
|
||||
CHECK(input_map_bind("A", "pad:west") && input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_WEST] = 1}, PAD_LAYOUT_XBOX) ==
|
||||
(1ull << B_A | 1ull << B_X),
|
||||
"pad layout: a bind line adds its physical button in every layout");
|
||||
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_SOUTH] = 1}, PAD_LAYOUT_XBOX) == 0,
|
||||
"pad layout: a bind line replaces the layout's own button");
|
||||
input_map_reset();
|
||||
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_XBOX) == 0,
|
||||
"pad layout: a back paddle is unbound by default");
|
||||
CHECK(input_map_bind("ZL", "pad:default, pad:rpaddle1") &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_ZL] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL,
|
||||
"bind: pad:rpaddle1 adds a back paddle, pad:default keeps the trigger");
|
||||
input_map_reset();
|
||||
// Bind lines for stick directions and default parts; the remapping rows' edits (input_map.c)
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_I] = 1;
|
||||
CHECK(input_map_bind("LSTICK_UP", "key:i, pad:south") == 1, "bind: a stick direction takes keys, skips pad entries");
|
||||
input_map_stick(dn, 0, st);
|
||||
CHECK(st[1] == 32767, "bind: I moves forward after bind_LSTICK_UP = key:i");
|
||||
dn[KEY_I] = 0, dn[KEY_W] = 1;
|
||||
input_map_stick(dn, 0, st);
|
||||
CHECK(st[1] == 0, "bind: W no longer moves forward");
|
||||
CHECK(input_map_bind("A", "key:default, pad:north") == 1, "bind: key:default with a pad entry");
|
||||
memset(dn, 0, sizeof dn);
|
||||
dn[KEY_E] = 1;
|
||||
CHECK(input_map_buttons(dn) == 1ull << B_A &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_NORTH] = 1}, PAD_LAYOUT_XBOX) == (1ull << B_A | 1ull << B_Y),
|
||||
"bind: key:default keeps E, pad:north adds the top button in every layout");
|
||||
CHECK(input_map_bind("B", "key:j, pad:default") == 1 &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_EAST] = 1}, PAD_LAYOUT_XBOX) == 1ull << B_B &&
|
||||
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_SOUTH] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_B,
|
||||
"bind: pad:default keeps each layout's own button");
|
||||
input_map_reset();
|
||||
InputMap mp = input_map;
|
||||
char line[256];
|
||||
input_map_line(&mp, B_ZR, line, sizeof line);
|
||||
CHECK(!line[0], "edit: an action at its defaults needs no line");
|
||||
CHECK(input_map_set_key(&mp, B_A, 0, KEY_F) == B_ZR, "edit: F taken from ZR");
|
||||
input_map_line(&mp, B_A, line, sizeof line);
|
||||
CHECK(!strcmp(line, "key:f, pad:default"), "edit: A's line is key:f, pad:default");
|
||||
input_map_line(&mp, B_ZR, line, sizeof line);
|
||||
CHECK(!strcmp(line, "key:x, pad:default"), "edit: ZR keeps X");
|
||||
CHECK(input_map_set_key(&mp, B_ZL, 1, KEY_NONE) == -1, "edit: clearing a key takes from no one");
|
||||
uint8_t ks[8];
|
||||
CHECK(input_map_keys_of(&mp, B_ZL, ks, 8) == 1 && ks[0] == KEY_MOUSE_R, "edit: ZL keeps its first key only");
|
||||
mp = input_map;
|
||||
CHECK(input_map_set_pad(&mp, B_X, PAD_LAYOUT_XBOX, PAD_SOUTH) == B_A, "edit: south swaps X with A (Xbox layout)");
|
||||
uint8_t ps[8];
|
||||
CHECK(input_map_pads_of(&mp, B_A, PAD_LAYOUT_XBOX, ps, 8) == 1 && ps[0] == PAD_WEST, "edit: A takes X's west button");
|
||||
CHECK(input_map_pads_of(&mp, B_A, PAD_LAYOUT_NINTENDO, ps, 8) == 1 && ps[0] == PAD_WEST &&
|
||||
input_map_pads_of(&mp, B_B, PAD_LAYOUT_NINTENDO, ps, 8) == 1 && ps[0] == PAD_EAST,
|
||||
"edit: the face buttons are pinned in every layout after a gamepad edit");
|
||||
input_map_line(&mp, B_X, line, sizeof line);
|
||||
CHECK(!strcmp(line, "key:default, pad:south"), "edit: X's line is key:default, pad:south");
|
||||
input_map_set_pad(&mp, B_X, PAD_LAYOUT_XBOX, PAD_WEST); // and back
|
||||
input_map_line(&mp, B_A, line, sizeof line);
|
||||
CHECK(!line[0] && input_map_pads_of(&mp, B_A, PAD_LAYOUT_NINTENDO, ps, 8) == 1 && ps[0] == PAD_EAST,
|
||||
"edit: swapping back restores every layout's defaults");
|
||||
mp = input_map;
|
||||
input_map_set_key(&mp, B_SL_UP, 0, KEY_NONE);
|
||||
input_map_line(&mp, B_SL_UP, line, sizeof line);
|
||||
CHECK(!strcmp(line, "none"), "edit: a stick direction without keys is none");
|
||||
// defaults live in the CfgMeta table: cfg_value and the typed accessors fall back to it
|
||||
cfg_load("cfg-probe-missing.ini");
|
||||
plat_setenv("NX2NAVI_FPS60", NULL);
|
||||
plat_setenv("NX2NAVI_MOUSE_SENS", NULL);
|
||||
plat_setenv("NX2NAVI_FREE_CORES", NULL);
|
||||
CHECK(cfg_bool("NX2NAVI_FPS60") == 1, "default: unset FPS60 is on");
|
||||
CHECK(EQ(cfg_value("NX2NAVI_VK_PRESENT"), "mailbox") && cfg_int("NX2NAVI_SPLIT_DRAWS") == 64, "default: text and integer");
|
||||
plat_setenv("NX2NAVI_MOUSE_SENS", "2.5");
|
||||
CHECK(cfg_double("NX2NAVI_MOUSE_SENS") == 2.5, "double: the environment value");
|
||||
plat_setenv("NX2NAVI_MOUSE_SENS", "fast");
|
||||
CHECK(cfg_double("NX2NAVI_MOUSE_SENS") == 1.0, "double: a value that does not parse gives the default");
|
||||
plat_setenv("NX2NAVI_MOUSE_SENS", NULL);
|
||||
cfg_set("NX2NAVI_MOUSE_SENS", "3");
|
||||
CHECK(cfg_double("NX2NAVI_MOUSE_SENS") == 3.0 && cfg_int("NX2NAVI_MOUSE_SENS") == 3, "a cfg_set value beats the default");
|
||||
CHECK(cfg_bool("NX2NAVI_FREE_CORES") == 0, "free_cores unset: off");
|
||||
plat_setenv("NX2NAVI_FREE_CORES", "0");
|
||||
CHECK(cfg_bool("NX2NAVI_FREE_CORES") == 0, "free_cores 0 gives the same as unset");
|
||||
plat_setenv("NX2NAVI_FREE_CORES", "1");
|
||||
CHECK(cfg_bool("NX2NAVI_FREE_CORES") == 1, "free_cores 1: on");
|
||||
plat_setenv("NX2NAVI_FREE_CORES", NULL);
|
||||
// 0.1.9's pad_layout = xbox_prompts (cfg.c migrate): read as the Nintendo layout with the Xbox prompts, saved so
|
||||
plat_setenv("NX2NAVI_PAD_LAYOUT", NULL);
|
||||
plat_setenv("NX2NAVI_PROMPTS", NULL);
|
||||
static const char old_layout[] = "[input]\r\npad_layout = xbox_prompts\r\n";
|
||||
CHECK(plat_file_write(path, old_layout, sizeof old_layout - 1) && cfg_load(path) == 1 &&
|
||||
EQ(cfg_get("NX2NAVI_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("NX2NAVI_PROMPTS"), "xbox"),
|
||||
"0.1.9's pad_layout = xbox_prompts reads as nintendo with prompts = xbox");
|
||||
CHECK(cfg_save_to(path) == 1, "the read pad layout and prompts save");
|
||||
uint64_t saved_n = 0;
|
||||
char *saved = (char *)plat_file_read(path, &saved_n);
|
||||
CHECK(saved && strstr(saved, "pad_layout = nintendo") && strstr(saved, "prompts = xbox") && !strstr(saved, "xbox_prompts"),
|
||||
"the next save writes the pad layout and the prompts");
|
||||
free(saved);
|
||||
static const char old_layout_own[] = "[input]\r\npad_layout = xbox_prompts\r\nprompts = nintendo\r\n";
|
||||
CHECK(plat_file_write(path, old_layout_own, sizeof old_layout_own - 1) && cfg_load(path) == 1 &&
|
||||
EQ(cfg_get("NX2NAVI_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("NX2NAVI_PROMPTS"), "nintendo"),
|
||||
"a prompts line of the file's own wins over 0.1.9's pad_layout = xbox_prompts");
|
||||
plat_file_delete(path);
|
||||
cfg_load("cfg-probe-missing.ini");
|
||||
int listed = 0;
|
||||
for (int i = 0; cfg_meta_at(i); i++) {
|
||||
const CfgMeta *m = cfg_meta_at(i);
|
||||
if (!*m->def) continue;
|
||||
listed++;
|
||||
char name[96];
|
||||
snprintf(name, sizeof name, "NX2NAVI_%s", m->key);
|
||||
plat_setenv(name, "");
|
||||
CHECK(EQ(cfg_value(name), m->def), "cfg_value returns the table default");
|
||||
int ok = 0;
|
||||
if (!m->values) ok = 1;
|
||||
else if (strstr(m->values, "..")) {
|
||||
// strtod: ranges such as 0.2..0.9 have fractional ends
|
||||
double lo = strtod(m->values, NULL), hi = strtod(strstr(m->values, "..") + 2, NULL);
|
||||
double d = strtod(m->def, NULL);
|
||||
ok = d >= lo && d <= hi;
|
||||
} else {
|
||||
size_t dl = strlen(m->def);
|
||||
for (const char *v = m->values; *v;) {
|
||||
size_t n = strcspn(v, "|");
|
||||
ok |= n == dl && !strncmp(v, m->def, n);
|
||||
v += n + (v[n] == '|');
|
||||
}
|
||||
}
|
||||
if (!ok) fprintf(stderr, "FAIL cfg: default '%s' of %s is not one of its values '%s'\n", m->def, m->key, m->values), fails++;
|
||||
}
|
||||
CHECK(listed >= 30, "the defaults table was walked");
|
||||
// cfg_meta answers with the first row of a key: a second row would be dead, and Reset would visit the key twice
|
||||
for (int i = 0; cfg_meta_at(i); i++)
|
||||
for (int j = i + 1; cfg_meta_at(j); j++)
|
||||
if (!strcmp(cfg_meta_at(i)->key, cfg_meta_at(j)->key))
|
||||
fprintf(stderr, "FAIL cfg: %s has two rows in the defaults table\n", cfg_meta_at(i)->key), fails++;
|
||||
printf("probe-cfg: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
667
runtime/cheats.c
Normal file
667
runtime/cheats.c
Normal file
@@ -0,0 +1,667 @@
|
||||
// Cheats of the mods' Atmosphere cheat files (cheats.h). Each enabled mod the port can use (mods.h) may hold
|
||||
// <build id>.txt in its cheats folder, the build id the first 8 bytes of main's NSO build id, as Atmosphere and the
|
||||
// emulators name it; the file name's case does not matter. Each cheat's switch is the setting NX2NAVI_CHEAT_<hash>
|
||||
// (cheat_<hash> under [cheats] in nx2navi.ini), the hash FNV-1a 32 of "<mod folder name>/<cheat name>", so two mods'
|
||||
// cheats of one name keep their own switches.
|
||||
//
|
||||
// Code cheats (opcode 0 writes into main's code) cannot be applied here: the game's code is lifted C. tools/cheats.py
|
||||
// turns the code cheats of every mod into patch sets cheat_<hash> when the game is built and writes
|
||||
// build-cache\cheats.status next to nx2navi.exe, one line per cheat: name hash, content hash, kind, buttons, note
|
||||
// (tools/cheats.py has the format). A cheat whose status line is missing or has another content hash was added or
|
||||
// edited after the build: a data cheat runs anyway, a code cheat waits for the setup to run again.
|
||||
//
|
||||
// Data cheats run in a VM after Atmosphere's dmnt cheat VM (opcodes 0-A; the 0xC.. and 0xF.. extensions are not
|
||||
// supported), every presented frame while their switch is on, with registers cleared before each cheat. Memory is the
|
||||
// guest's, mapped at its own addresses: main's image is written with its pages made writable for the write (read-only
|
||||
// data included, as dmnt writes through the debugger), everything else after mem_query says the guest may read or write
|
||||
// it. Buttons are input_poll's HidNpadButton bits, the same as the cheat format's key masks; they read 0 while the menu
|
||||
// is open.
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "cheats.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "mods.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define SEP "\\"
|
||||
#else
|
||||
#define SEP "/"
|
||||
#endif
|
||||
|
||||
enum { STATUS_MAX = 160, OPS_PER_RUN = 1 << 20 };
|
||||
|
||||
typedef struct {
|
||||
CheatInfo info;
|
||||
uint32_t *words;
|
||||
int n;
|
||||
int dup; // 1 for the first cheat of its name in its file, 2 for the next ...
|
||||
uint32_t content; // FNV-1a 32 of its words as tools/cheats.py writes them
|
||||
int set; // patch set, -1 none
|
||||
volatile int32_t on;
|
||||
char note[STATUS_MAX];
|
||||
} Cheat;
|
||||
|
||||
static Cheat *g_cheats;
|
||||
static int g_n, g_cap, g_vm; // g_vm: some cheat needs cheats_frame
|
||||
static char g_file[64]; // "<build id>.txt"
|
||||
static char g_dir[PLAT_PATH_MAX]; // the folder of nx2navi.exe, with a trailing separator
|
||||
static int g_files; // mods whose file was read
|
||||
static uint64_t g_main, g_main_size, g_code_end; // main's image; its code with the padding up to .rodata
|
||||
|
||||
static uint32_t fnv(const char *s, size_t n) {
|
||||
uint32_t h = 0x811C9DC5u;
|
||||
for (size_t i = 0; i < n; i++) h = (h ^ (uint8_t)s[i]) * 0x01000193u;
|
||||
return h;
|
||||
}
|
||||
|
||||
// The folder of the executable, with a trailing separator.
|
||||
static int exe_dir(char *out, size_t n) {
|
||||
if (!plat_exe_path(out, n)) return 0;
|
||||
char *cut = plat_last_sep(out);
|
||||
if (!cut) return 0;
|
||||
cut[1] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int build_id(char out[17]) {
|
||||
char path[PLAT_PATH_MAX];
|
||||
snprintf(path, sizeof path, "%s/main", rt_config()->exefs);
|
||||
PlatFile *f = plat_file_open(path);
|
||||
if (!f) return 0;
|
||||
uint8_t head[0x48];
|
||||
int ok = plat_file_pread(f, head, sizeof head, 0) == sizeof head && !memcmp(head, "NSO0", 4);
|
||||
plat_file_close(f);
|
||||
for (int i = 0; ok && i < 8; i++) snprintf(out + 2 * i, 3, "%02X", head[0x40 + i]);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- opcodes
|
||||
|
||||
// Words of the opcode at w (left words remain), 0 when it is cut short.
|
||||
static int op_len(const uint32_t *w, int left) {
|
||||
uint32_t f = w[0];
|
||||
int t = f >> 28, width = (f >> 24) & 0xF, n;
|
||||
switch (t) {
|
||||
case 0: case 1: n = width == 8 ? 4 : 3; break;
|
||||
case 2: case 8: n = 1; break;
|
||||
case 3: n = width == 0 ? 2 : 1; break;
|
||||
case 4: case 6: n = 3; break;
|
||||
case 5: case 7: n = 2; break;
|
||||
case 9: n = 1 + (((f >> 8) & 0xF) ? (width == 8 ? 2 : 1) : 0); break;
|
||||
case 0xA: n = ((f >> 8) & 0xF) == 2 || ((f >> 8) & 0xF) == 4 || ((f >> 8) & 0xF) == 5 ? 2 : 1; break;
|
||||
default: n = 1; break;
|
||||
}
|
||||
return n <= left ? n : 0;
|
||||
}
|
||||
|
||||
// NULL when the VM runs the opcode, else why not (tools/cheats.py vm_ok).
|
||||
static const char *vm_unsupported(const uint32_t *w, char *buf, int n) {
|
||||
uint32_t f = w[0];
|
||||
int t = f >> 28, width = (f >> 24) & 0xF;
|
||||
if (t > 0xA || (t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
|
||||
snprintf(buf, n, "opcode %08X is not supported", f);
|
||||
return buf;
|
||||
}
|
||||
if ((t <= 1 || t == 5 || t == 6 || t == 7 || t == 9 || t == 0xA) && width != 1 && width != 2 && width != 4 &&
|
||||
width != 8) {
|
||||
snprintf(buf, n, "opcode %08X has a bad width", f);
|
||||
return buf;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The kind of a cheat the build did not see: heading, data, rebuild (it writes main's code) or unsupported.
|
||||
static CheatKind classify(Cheat *c) {
|
||||
int ops = 0, ends = 0;
|
||||
for (int i = 0; i < c->n;) {
|
||||
int len = op_len(c->words + i, c->n - i);
|
||||
if (!len) {
|
||||
snprintf(c->note, sizeof c->note, "opcode %08X is cut short", c->words[i]);
|
||||
return CHEAT_UNSUPPORTED;
|
||||
}
|
||||
uint32_t f = c->words[i];
|
||||
ops++;
|
||||
ends += f >> 28 == 2;
|
||||
if (f >> 28 == 0 && ((f >> 20) & 0xF) == 0) {
|
||||
uint64_t off = ((uint64_t)(f & 0xFF) << 32) | c->words[i + 1];
|
||||
if (off < g_code_end - g_main) return CHEAT_REBUILD;
|
||||
}
|
||||
if (vm_unsupported(c->words + i, c->note, sizeof c->note)) return CHEAT_UNSUPPORTED;
|
||||
i += len;
|
||||
}
|
||||
return ops == ends && ops <= 1 ? CHEAT_HEADING : CHEAT_DATA;
|
||||
}
|
||||
|
||||
// ---- guest memory
|
||||
|
||||
static uint64_t region(int m) {
|
||||
const RtGuestLayout *l = mem_layout();
|
||||
switch (m) {
|
||||
case 0: return g_main;
|
||||
case 1: return l->heap.base;
|
||||
case 2: return l->alias.base;
|
||||
case 3: return l->aslr.base;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int in_main(uint64_t a, unsigned n) { return a >= g_main && a + n <= g_main + g_main_size; }
|
||||
|
||||
static int guest_ok(uint64_t a, unsigned n, uint32_t perm) {
|
||||
if (in_main(a, n)) return 1;
|
||||
RtMemoryInfo mi;
|
||||
if (mem_query(a, &mi) || !mi.state || (mi.perm & perm) != perm) return 0;
|
||||
return a + n <= mi.base + mi.size;
|
||||
}
|
||||
|
||||
static int rd(uint64_t a, int width, uint64_t *v) {
|
||||
if (!a || !guest_ok(a, (unsigned)width, 1)) return 0;
|
||||
uint64_t x = 0;
|
||||
memcpy(&x, (const void *)(uintptr_t)a, (size_t)width);
|
||||
*v = x;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Writes n bytes into main's image a page at a time: a read-only page is writable for its part of the copy only.
|
||||
static void wr_main(uint64_t a, const uint8_t *from, int n) {
|
||||
while (n > 0) {
|
||||
uint64_t page = a & ~0xFFFull, end;
|
||||
int k = (int)(page + 0x1000 - a);
|
||||
if (k > n) k = n;
|
||||
int prot = plat_mem_state(page, page + 0x1000, &end);
|
||||
if (prot != PLAT_PROT_READ && prot != PLAT_PROT_RW) return;
|
||||
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return;
|
||||
memcpy((void *)(uintptr_t)a, from, (size_t)k);
|
||||
if (prot == PLAT_PROT_READ) plat_mem_protect(page, 0x1000, PLAT_PROT_READ);
|
||||
a += (uint64_t)k;
|
||||
from += k;
|
||||
n -= k;
|
||||
}
|
||||
}
|
||||
|
||||
static void wr(uint64_t a, int width, uint64_t v) {
|
||||
uint64_t now;
|
||||
if (!rd(a, width, &now) || !memcmp(&now, &v, (size_t)width)) return; // unchanged: no write, no protection change
|
||||
void *p = (void *)(uintptr_t)a;
|
||||
if (in_main(a, (unsigned)width)) {
|
||||
wr_main(a, (const uint8_t *)&v, width);
|
||||
} else if (guest_ok(a, (unsigned)width, 2)) {
|
||||
memcpy(p, &v, (size_t)width); // a frozen region thaws on the write (memory.c)
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t masked(int width, uint64_t v) { return width >= 8 ? v : v & ((1ull << (8 * width)) - 1); }
|
||||
|
||||
static uint64_t arith(int op, uint64_t a, uint64_t b) {
|
||||
switch (op) {
|
||||
case 0: return a + b;
|
||||
case 1: return a - b;
|
||||
case 2: return a * b;
|
||||
case 3: return b < 64 ? a << b : 0;
|
||||
case 4: return b < 64 ? a >> b : 0;
|
||||
case 5: return a & b;
|
||||
case 6: return a | b;
|
||||
case 7: return ~a;
|
||||
case 8: return a ^ b;
|
||||
default: return a; // 9: move
|
||||
}
|
||||
}
|
||||
|
||||
static int compare(int cond, uint64_t a, uint64_t b) {
|
||||
switch (cond) {
|
||||
case 1: return a > b;
|
||||
case 2: return a >= b;
|
||||
case 3: return a < b;
|
||||
case 4: return a <= b;
|
||||
case 5: return a == b;
|
||||
case 6: return a != b;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// From i (after a failed condition, or an else reached while running), the opcode to continue at: after the
|
||||
// matching else (else_ok) or end.
|
||||
static int skip(const uint32_t *w, int n, int i, int else_ok) {
|
||||
int depth = 0;
|
||||
while (i < n) {
|
||||
int len = op_len(w + i, n - i);
|
||||
if (!len) return n;
|
||||
uint32_t f = w[i];
|
||||
int t = f >> 28;
|
||||
i += len;
|
||||
if (t == 1 || t == 8) depth++;
|
||||
else if (t == 2 && ((f >> 24) & 0xF) == 0 && depth-- == 0) return i;
|
||||
else if (t == 2 && depth == 0 && else_ok) return i;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static uint64_t imm64(const uint32_t *w) { return (uint64_t)w[0] << 32 | w[1]; }
|
||||
|
||||
static void run(const Cheat *c, uint64_t keys) {
|
||||
uint64_t r[16] = {0};
|
||||
int loop[16] = {0};
|
||||
const uint32_t *w = c->words;
|
||||
int n = c->n, budget = OPS_PER_RUN;
|
||||
for (int i = 0; i < n && budget-- > 0;) {
|
||||
int len = op_len(w + i, n - i);
|
||||
if (!len) return;
|
||||
uint32_t f = w[i];
|
||||
int t = f >> 28, width = (f >> 24) & 0xF, x = (f >> 20) & 0xF, y = (f >> 16) & 0xF, z = (f >> 12) & 0xF;
|
||||
uint64_t rel = ((uint64_t)(f & 0xFF) << 32) | (len > 1 ? w[i + 1] : 0), v;
|
||||
switch (t) {
|
||||
case 0: // 0TMR00AA AAAAAAAA V..: store a value at region M + A + register R
|
||||
wr(region(x) + rel + r[y], width, width == 8 ? imm64(w + i + 2) : w[i + 2]);
|
||||
break;
|
||||
case 1: // 1TMC00AA AAAAAAAA V..: if (region M + A) <cond C> V
|
||||
if (!rd(region(x) + rel, width, &v) || !compare(y, v, width == 8 ? imm64(w + i + 2) : w[i + 2])) {
|
||||
i = skip(w, n, i + len, 1);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2: // 20000000 end, 21000000 else: an else reached while running skips to the end
|
||||
if (width == 1) {
|
||||
i = skip(w, n, i + len, 0);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 3: // 300R0000 VVVVVVVV loop start, 310R0000 loop end
|
||||
if (width == 0) {
|
||||
r[y] = w[i + 1];
|
||||
loop[y] = i + len;
|
||||
} else if (r[y] && --r[y]) {
|
||||
i = loop[y];
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 4: // 400R0000 V V: register R = V
|
||||
r[y] = imm64(w + i + 1);
|
||||
break;
|
||||
case 5: // 5TMRI0AA AAAAAAAA: R = (I ? R : region M) + A, read
|
||||
if (!rd(z ? r[y] + rel : region(x) + rel, width, &v)) return; // a broken pointer chain: nothing more
|
||||
r[y] = v;
|
||||
break;
|
||||
case 6: { // 6T0RIor0 V V: store V at R (+ register r); I: R += width
|
||||
uint64_t a = r[y] + (((f >> 8) & 0xF) ? r[(f >> 4) & 0xF] : 0);
|
||||
wr(a, width, imm64(w + i + 1));
|
||||
if (z) r[y] += (uint64_t)width;
|
||||
break;
|
||||
}
|
||||
case 7: // 7T0RC000 VVVVVVVV: R = R <op C> V
|
||||
r[y] = masked(width, arith(z, r[y], w[i + 1]));
|
||||
break;
|
||||
case 8: // 8kkkkkkk: if the buttons are held
|
||||
if ((keys & (f & 0x0FFFFFFF)) != (f & 0x0FFFFFFF)) {
|
||||
i = skip(w, n, i + len, 1);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 9: { // 9TCRS0s0 / 9TCRS100 V..: R = S <op C> (s or V)
|
||||
uint64_t b = (f >> 8) & 0xF ? (width == 8 ? imm64(w + i + 1) : w[i + 1]) : r[(f >> 4) & 0xF];
|
||||
r[y] = masked(width, arith(x, r[z], b));
|
||||
break;
|
||||
}
|
||||
case 0xA: { // ATSRIOxa (aaaaaaaa): store register S at an address of type O
|
||||
int o = (f >> 8) & 0xF, m = (f >> 4) & 0xF;
|
||||
uint64_t imm = ((uint64_t)(f & 0xF) << 32) | (len > 1 ? w[i + 1] : 0), a;
|
||||
switch (o) {
|
||||
case 0: a = r[y]; break;
|
||||
case 1: a = r[y] + r[m]; break;
|
||||
case 2: a = r[y] + imm; break;
|
||||
case 3: a = region(m) + r[y]; break;
|
||||
case 4: a = region(m) + imm; break;
|
||||
default: a = region(m) + r[y] + imm; break;
|
||||
}
|
||||
wr(a, width, r[x]);
|
||||
if (z) r[y] += (uint64_t)width;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the file
|
||||
|
||||
static char *read_text(const char *path) {
|
||||
uint64_t n = 0;
|
||||
char *s = (char *)plat_file_read(path, &n);
|
||||
if (s && n >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) memmove(s, s + 3, (size_t)n - 2); // a UTF-8 BOM
|
||||
return s;
|
||||
}
|
||||
|
||||
static void add_word(Cheat *c, uint32_t v, int *cap) {
|
||||
if (c->n == *cap) {
|
||||
*cap = *cap ? *cap * 2 : 16;
|
||||
c->words = (uint32_t *)realloc(c->words, sizeof *c->words * (size_t)*cap);
|
||||
if (!c->words) rt_fatal("cheats: out of memory");
|
||||
}
|
||||
c->words[c->n++] = v;
|
||||
}
|
||||
|
||||
// Appends the cheats of one mod's file; mod: the mod's folder name (kept).
|
||||
static void parse(char *s, const char *mod) {
|
||||
int first = g_n, wcap = 0;
|
||||
for (char *line = s; line && *line;) {
|
||||
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
||||
if (end) *end = 0;
|
||||
while (*line == ' ' || *line == '\t') line++;
|
||||
size_t len = strlen(line);
|
||||
while (len && (line[len - 1] == '\r' || line[len - 1] == ' ' || line[len - 1] == '\t')) line[--len] = 0;
|
||||
if (len >= 2 && (line[0] == '[' || line[0] == '{') && (line[len - 1] == ']' || line[len - 1] == '}')) {
|
||||
if (g_n == g_cap) {
|
||||
g_cap = g_cap ? g_cap * 2 : 32;
|
||||
g_cheats = (Cheat *)realloc(g_cheats, sizeof *g_cheats * (size_t)g_cap);
|
||||
if (!g_cheats) rt_fatal("cheats: out of memory");
|
||||
}
|
||||
Cheat *c = &g_cheats[g_n++];
|
||||
memset(c, 0, sizeof *c);
|
||||
line[len - 1] = 0;
|
||||
char *name = line + 1;
|
||||
while (*name == ' ' || *name == '\t') name++;
|
||||
size_t k = strlen(name);
|
||||
while (k && (name[k - 1] == ' ' || name[k - 1] == '\t')) name[--k] = 0;
|
||||
c->info.name = strdup(name);
|
||||
c->info.mod = mod;
|
||||
c->info.kind = line[0] == '{' ? CHEAT_MASTER : CHEAT_HEADING;
|
||||
c->set = -1;
|
||||
c->dup = 1; // a repeated name gets "#2", "#3" ... (tools/cheats.py parse)
|
||||
for (int j = first; j < g_n - 1; j++) c->dup += !strcmp(g_cheats[j].info.name, c->info.name);
|
||||
wcap = 0;
|
||||
} else if (g_n > first) {
|
||||
for (char *tok = strtok(line, " \t"); tok; tok = strtok(NULL, " \t")) {
|
||||
char *e;
|
||||
unsigned long v = strtoul(tok, &e, 16);
|
||||
if (e - tok == 8 && !*e) add_word(&g_cheats[g_n - 1], (uint32_t)v, &wcap);
|
||||
}
|
||||
}
|
||||
line = next;
|
||||
}
|
||||
}
|
||||
|
||||
// The file g_file in folder, its name in any case, into out; 0 when there is none.
|
||||
typedef struct {
|
||||
const char *dir;
|
||||
char *out;
|
||||
size_t cap;
|
||||
int found;
|
||||
} FindFile;
|
||||
static int find_cb(const char *name, int kind, uint64_t size, void *ctx) {
|
||||
(void)size;
|
||||
FindFile *f = (FindFile *)ctx;
|
||||
if (kind == PLAT_FS_FILE && !strcasecmp(name, g_file) &&
|
||||
snprintf(f->out, f->cap, "%s/%s", f->dir, name) < (int)f->cap)
|
||||
f->found = 1;
|
||||
return !f->found;
|
||||
}
|
||||
static int find_file(const char *folder, char *out, size_t cap) {
|
||||
FindFile f = {folder, out, cap, 0};
|
||||
plat_fs_list(folder, find_cb, &f);
|
||||
return f.found;
|
||||
}
|
||||
|
||||
// The status line of key, its fields split in place; 0 when none.
|
||||
static int status_of(char *status, const char *key, char **fields) {
|
||||
for (char *line = status; line && *line;) {
|
||||
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
||||
if (!strncmp(line, key, 8) && line[8] == '\t') {
|
||||
static char copy[512];
|
||||
size_t n = end ? (size_t)(end - line) : strlen(line);
|
||||
if (n >= sizeof copy) n = sizeof copy - 1;
|
||||
memcpy(copy, line, n);
|
||||
copy[n] = 0;
|
||||
if (n && copy[n - 1] == '\r') copy[n - 1] = 0;
|
||||
int k = 0;
|
||||
fields[k++] = copy;
|
||||
for (char *p = copy; *p && k < 5; p++)
|
||||
if (*p == '\t') *p = 0, fields[k++] = p + 1;
|
||||
while (k < 5) fields[k++] = (char *)"";
|
||||
return 1;
|
||||
}
|
||||
line = next;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The menu's font has no numero sign (U+2116), which cheat names often start with: they show "No" instead.
|
||||
static void plain_name(char *s) {
|
||||
for (char *p; (p = strstr(s, "\xE2\x84\x96")) != NULL;) {
|
||||
p[0] = 'N', p[1] = 'o';
|
||||
memmove(p + 2, p + 3, strlen(p + 3) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- rebuild: code cheats and ExeFS patches the build lacks
|
||||
// The setup writes build-cache\rebuild.txt (installer/steps.py write_rebuild_command), the command that starts its
|
||||
// window in rebuild mode on this install folder, one argument per line. When an enabled mod has code cheats this build
|
||||
// lacks (CHEAT_REBUILD) or ExeFS patch files it lacks (mods_exefs_pending), the game runs it and exits; the setup lifts
|
||||
// and compiles them in and starts the game again.
|
||||
// build-cache\rebuild.skip holds the key of the cheats a rebuild was started for: a rebuild that failed or was
|
||||
// cancelled leaves them unbuilt, and the same set does not start it again. NX2NAVI_MODS_REBUILD=off never starts it.
|
||||
// Scripted runs (--seconds, --boot) never start it.
|
||||
static void rebuild_if_needed(const char *dir) {
|
||||
uint32_t key = 0x811C9DC5u;
|
||||
int need = 0;
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
const Cheat *c = &g_cheats[i];
|
||||
if (c->info.kind != CHEAT_REBUILD) continue;
|
||||
need++;
|
||||
key = (key ^ fnv(c->info.key, strlen(c->info.key))) * 0x01000193u;
|
||||
key = (key ^ c->content) * 0x01000193u;
|
||||
}
|
||||
int patches = mods_exefs_pending(&key);
|
||||
if (!need && !patches) return;
|
||||
char cmd_path[PLAT_PATH_MAX + 64], skip_path[PLAT_PATH_MAX + 64], skip[16] = "", want[16];
|
||||
snprintf(cmd_path, sizeof cmd_path, "%sbuild-cache" SEP "rebuild.txt", dir);
|
||||
snprintf(skip_path, sizeof skip_path, "%sbuild-cache" SEP "rebuild.skip", dir);
|
||||
snprintf(want, sizeof want, "%08X", key);
|
||||
char *old = read_text(skip_path);
|
||||
if (old) snprintf(skip, sizeof skip, "%.8s", old);
|
||||
free(old);
|
||||
const char *why = !strcmp(cfg_value("NX2NAVI_MODS_REBUILD"), "off") ? "mods_rebuild = off"
|
||||
: rt_config()->seconds || rt_config()->boot ? "a scripted run"
|
||||
: !strcmp(skip, want) ? "the last rebuild for them failed or was cancelled"
|
||||
: NULL;
|
||||
char *cmd = why ? NULL : read_text(cmd_path);
|
||||
if (!why && !cmd) why = "no build-cache/rebuild.txt (a build without the setup)";
|
||||
if (why) {
|
||||
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; no rebuild: %s", need,
|
||||
patches, why);
|
||||
free(cmd);
|
||||
return;
|
||||
}
|
||||
const char *argv[32];
|
||||
int argc = 0;
|
||||
for (char *line = cmd; line && *line && argc < 31;) {
|
||||
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
|
||||
if (end) *end = 0;
|
||||
size_t n = strlen(line);
|
||||
if (n && line[n - 1] == '\r') line[n - 1] = 0;
|
||||
if (*line) argv[argc++] = line;
|
||||
line = next;
|
||||
}
|
||||
argv[argc] = NULL;
|
||||
if (argc < 2 || !plat_file_write(skip_path, want, strlen(want))) {
|
||||
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; cannot start the "
|
||||
"rebuild (%s)", need, patches, cmd_path);
|
||||
free(cmd);
|
||||
return;
|
||||
}
|
||||
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; starting the rebuild: %s",
|
||||
need, patches, argv[0]);
|
||||
int ok = plat_spawn(argv, dir);
|
||||
free(cmd);
|
||||
if (!ok) {
|
||||
rt_log("cheats: the rebuild did not start; the game starts with the code it has");
|
||||
return;
|
||||
}
|
||||
prof_write(); // plat_exit runs no atexit handlers
|
||||
plat_exit(0);
|
||||
}
|
||||
|
||||
void cheats_init(void) {
|
||||
const RtLoaded *m = ldr_count() ? ldr_module(0) : NULL;
|
||||
char dir[PLAT_PATH_MAX], bid[17], path[2400]; // path: a mod's cheats folder (ModInfo.cheats) + the file
|
||||
if (!m || !exe_dir(dir, sizeof dir) || !build_id(bid)) return;
|
||||
snprintf(g_dir, sizeof g_dir, "%s", dir);
|
||||
g_main = m->base;
|
||||
g_main_size = m->size;
|
||||
g_code_end = m->base + ((m->text_end + 0xFFF) & ~0xFFFull);
|
||||
snprintf(g_file, sizeof g_file, "%s.txt", bid);
|
||||
mods_scan();
|
||||
for (int k = 0; k < mods_count(); k++) { // the Mods tab's order, top first
|
||||
const ModInfo *mod = mods_at(k);
|
||||
if (!mod->enabled || mod->problem != MOD_OK || !mod->cheats[0] || !find_file(mod->cheats, path, sizeof path))
|
||||
continue;
|
||||
char *text = read_text(path);
|
||||
if (!text) {
|
||||
rt_log("cheats: %s: cannot read %s", mod->name, path);
|
||||
continue;
|
||||
}
|
||||
parse(text, strdup(mod->name));
|
||||
free(text);
|
||||
g_files++;
|
||||
}
|
||||
if (!g_files) {
|
||||
rt_log("cheats: no enabled mod holds cheats/%s", g_file);
|
||||
rebuild_if_needed(dir); // an enabled mod's ExeFS patches may still wait for it
|
||||
return;
|
||||
}
|
||||
snprintf(path, sizeof path, "%sbuild-cache" SEP "cheats.status", dir);
|
||||
char *status = read_text(path);
|
||||
int counts[CHEAT_UNSUPPORTED + 1] = {0}, seen_dups = 0;
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
Cheat *c = &g_cheats[i];
|
||||
char named[900];
|
||||
snprintf(named, sizeof named, c->dup > 1 ? "%s/%s#%d" : "%s/%s", c->info.mod, c->info.name, c->dup);
|
||||
seen_dups += c->dup > 1;
|
||||
char key[9], content[9];
|
||||
snprintf(key, sizeof key, "%08X", fnv(named, strlen(named)));
|
||||
size_t tn = (size_t)c->n * 9 + 1, tl = 0;
|
||||
char *t = (char *)malloc(tn);
|
||||
if (!t) rt_fatal("cheats: out of memory");
|
||||
t[0] = 0;
|
||||
for (int k = 0; k < c->n; k++) tl += (size_t)snprintf(t + tl, tn - tl, k ? " %08X" : "%08X", c->words[k]);
|
||||
c->content = fnv(t, tl);
|
||||
snprintf(content, sizeof content, "%08X", c->content);
|
||||
free(t);
|
||||
snprintf(c->info.key, sizeof c->info.key, "NX2NAVI_CHEAT_%s", key);
|
||||
int master = c->info.kind == CHEAT_MASTER;
|
||||
char *f[5];
|
||||
CheatKind kind;
|
||||
if (status && status_of(status, key, f) && !strcmp(f[1], content)) {
|
||||
const char *k = f[2];
|
||||
kind = !strcmp(k, "code") ? CHEAT_CODE : !strcmp(k, "key") ? CHEAT_KEY : !strcmp(k, "master") ? CHEAT_MASTER
|
||||
: !strcmp(k, "restore") ? CHEAT_RESTORE : !strcmp(k, "heading") ? CHEAT_HEADING
|
||||
: !strcmp(k, "data") ? CHEAT_DATA : CHEAT_UNSUPPORTED;
|
||||
c->info.buttons = (uint32_t)strtoul(f[3], NULL, 16);
|
||||
snprintf(c->note, sizeof c->note, "%s", f[4]);
|
||||
if (kind == CHEAT_CODE || kind == CHEAT_KEY || kind == CHEAT_MASTER) {
|
||||
char set[32];
|
||||
snprintf(set, sizeof set, "cheat_%08x", fnv(named, strlen(named)));
|
||||
c->set = rt_patch_set(set);
|
||||
if (c->set < 0 && kind != CHEAT_MASTER) kind = CHEAT_REBUILD;
|
||||
}
|
||||
} else {
|
||||
kind = classify(c);
|
||||
}
|
||||
if (master && kind == CHEAT_DATA) kind = CHEAT_MASTER; // a data master code: the VM, always on
|
||||
c->info.kind = kind;
|
||||
c->info.note = c->note;
|
||||
const char *v = cfg_get(c->info.key);
|
||||
c->on = kind == CHEAT_MASTER || (v && *v && strcmp(v, "0") != 0);
|
||||
if (kind == CHEAT_KEY && c->set >= 0) rt_patch_switch(c->set, 0); // on while the buttons are held
|
||||
if (kind == CHEAT_KEY || kind == CHEAT_DATA || (kind == CHEAT_MASTER && c->set < 0)) g_vm = 1;
|
||||
counts[kind]++;
|
||||
}
|
||||
free(status);
|
||||
rebuild_if_needed(dir);
|
||||
for (int i = 0; i < g_n; i++) plain_name((char *)g_cheats[i].info.name); // the hashes above read the file's bytes
|
||||
int on = 0;
|
||||
for (int i = 0; i < g_n; i++) on += g_cheats[i].on && g_cheats[i].info.kind != CHEAT_HEADING;
|
||||
rt_log("cheats: %s in %d mods: %d cheats, %d on; %d code, %d while buttons are held, %d data, %d master, "
|
||||
"%d restore, %d headings, %d wait for a rebuild, %d not supported%s",
|
||||
g_file, g_files, g_n, on, counts[CHEAT_CODE], counts[CHEAT_KEY], counts[CHEAT_DATA], counts[CHEAT_MASTER],
|
||||
counts[CHEAT_RESTORE], counts[CHEAT_HEADING], counts[CHEAT_REBUILD], counts[CHEAT_UNSUPPORTED],
|
||||
seen_dups ? " (repeated names numbered)" : "");
|
||||
}
|
||||
|
||||
int cheats_rebuild_pending(void) {
|
||||
char path[PLAT_PATH_MAX + 64];
|
||||
snprintf(path, sizeof path, "%sbuild-cache" SEP "rebuild.txt", g_dir);
|
||||
uint32_t key = 0;
|
||||
int need = mods_exefs_pending(&key);
|
||||
for (int i = 0; i < g_n && !need; i++) need = g_cheats[i].info.kind == CHEAT_REBUILD;
|
||||
return need && g_dir[0] && plat_file_exists(path);
|
||||
}
|
||||
|
||||
void cheats_rebuild_allow(void) {
|
||||
char path[PLAT_PATH_MAX + 64];
|
||||
snprintf(path, sizeof path, "%sbuild-cache" SEP "rebuild.skip", g_dir);
|
||||
plat_fs_unlink(path);
|
||||
}
|
||||
|
||||
int cheats_count(void) { return g_n; }
|
||||
|
||||
const CheatInfo *cheats_info(int i) { return i >= 0 && i < g_n ? &g_cheats[i].info : NULL; }
|
||||
|
||||
int cheats_on(int i) { return i >= 0 && i < g_n && g_cheats[i].on; }
|
||||
|
||||
const char *cheats_file(void) { return g_file; }
|
||||
|
||||
void cheats_set(int i, int on) {
|
||||
if (i < 0 || i >= g_n) return;
|
||||
Cheat *c = &g_cheats[i];
|
||||
if (c->info.kind != CHEAT_CODE && c->info.kind != CHEAT_KEY && c->info.kind != CHEAT_DATA) return;
|
||||
plat_xchg32(&c->on, on != 0);
|
||||
if (c->info.kind == CHEAT_CODE) rt_patch_switch(c->set, on); // CHEAT_KEY: cheats_frame follows the buttons
|
||||
cfg_set(c->info.key, on ? "1" : "0");
|
||||
rt_log("cheats: %s/%s %s", c->info.mod, c->info.name, on ? "on" : "off");
|
||||
}
|
||||
|
||||
void cheats_frame(uint64_t frame) {
|
||||
if (!g_vm || frame < 60) return; // the game polls its input (and starts SDL) before this
|
||||
uint64_t keys = 0;
|
||||
int32_t lx, ly, rx, ry;
|
||||
input_poll(&keys, &lx, &ly, &rx, &ry); // 0 while the menu is open
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
Cheat *c = &g_cheats[i];
|
||||
switch (c->info.kind) {
|
||||
case CHEAT_KEY:
|
||||
if (c->set >= 0) {
|
||||
int want = c->on && (keys & c->info.buttons) == c->info.buttons;
|
||||
if (rt_patch_enabled(c->set) != want) rt_patch_switch(c->set, want);
|
||||
}
|
||||
break;
|
||||
case CHEAT_DATA:
|
||||
if (c->on) run(c, keys);
|
||||
break;
|
||||
case CHEAT_MASTER:
|
||||
if (c->set < 0) run(c, keys);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char *cheats_buttons(uint32_t mask, char *buf, int n) {
|
||||
static const char *const names[] = {"A", "B", "X", "Y", "Left stick press", "Right stick press", "L", "R", "ZL",
|
||||
"ZR", "+", "-", "D-pad left", "D-pad up", "D-pad right", "D-pad down",
|
||||
"Left stick left", "Left stick up", "Left stick right", "Left stick down",
|
||||
"Right stick left", "Right stick up", "Right stick right", "Right stick down"};
|
||||
int k = 0;
|
||||
buf[0] = 0;
|
||||
for (int b = 0; b < 24; b++)
|
||||
if (mask & (1u << b)) k += snprintf(buf + k, (size_t)(n > k ? n - k : 0), "%s%s", k ? " + " : "", names[b]);
|
||||
return buf;
|
||||
}
|
||||
48
runtime/cheats.h
Normal file
48
runtime/cheats.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// Cheats of the mods' Atmosphere cheat files (cheats.c): mods\<mod>\cheats\<build id>.txt of each enabled mod,
|
||||
// switched from the menu's Cheats tab. A cheat that writes the game's instructions is a code patch set that tools/cheats.py built into the game
|
||||
// (switched with rt_patch_switch); one that writes data runs in the cheat VM every presented frame.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
CHEAT_HEADING, // no opcodes: a title or a group label
|
||||
CHEAT_CODE, // a patch set
|
||||
CHEAT_KEY, // a patch set on while the buttons are held
|
||||
CHEAT_DATA, // the cheat VM
|
||||
CHEAT_MASTER, // {master}: always on
|
||||
CHEAT_RESTORE, // writes the original instructions back: nothing to do
|
||||
CHEAT_REBUILD, // writes instructions, but the game was built without this version of it
|
||||
CHEAT_UNSUPPORTED, // note says why
|
||||
} CheatKind;
|
||||
|
||||
typedef struct {
|
||||
const char *name; // UTF-8, as in the file
|
||||
const char *mod; // the mod's folder name; cheats come grouped by mod, in the Mods tab's order
|
||||
const char *note; // why it is unsupported ("" otherwise)
|
||||
CheatKind kind;
|
||||
uint32_t buttons; // CHEAT_KEY: HidNpadButton mask
|
||||
char key[24]; // its setting, NX2NAVI_CHEAT_<hash of "<mod>/<name>">
|
||||
} CheatInfo;
|
||||
|
||||
void cheats_init(void); // after rt_patches_init: reads the mods' files and their build status
|
||||
int cheats_count(void);
|
||||
// 1 when a cheat of an enabled mod waits for a rebuild (CHEAT_REBUILD) and the setup left its rebuild command.
|
||||
int cheats_rebuild_pending(void);
|
||||
// The next start rebuilds again for cheats whose last rebuild failed or was cancelled (the menu restarts after it).
|
||||
void cheats_rebuild_allow(void);
|
||||
const CheatInfo *cheats_info(int i);
|
||||
int cheats_on(int i); // the player's switch (1 for a master cheat)
|
||||
void cheats_set(int i, int on); // switches at once and sets the cheat's setting (the menu saves nx2navi.ini)
|
||||
const char *cheats_file(void); // "<build id>.txt", the name the files have for this game version
|
||||
// Every presented frame (the present path): the data cheats' VM and the button state of CHEAT_KEY cheats.
|
||||
void cheats_frame(uint64_t frame);
|
||||
// The button names of a mask ("ZL + Right stick left"), into buf.
|
||||
const char *cheats_buttons(uint32_t mask, char *buf, int n);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
71
runtime/color_grade_spv.h
Normal file
71
runtime/color_grade_spv.h
Normal file
@@ -0,0 +1,71 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/color_grade.comp; do not edit.
|
||||
// source sha256 c93d6ac75667bdee5442d7e94fbf0bd6e7fe7250caa98974424aead10f554106
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t color_grade_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x00000073, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00060010, 0x00000004,
|
||||
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000000d, 0x0000000b, 0x0000001c,
|
||||
0x00030047, 0x00000015, 0x00000002, 0x00050048, 0x00000015, 0x00000000, 0x00000023, 0x00000000,
|
||||
0x00050048, 0x00000015, 0x00000001, 0x00000023, 0x00000008, 0x00050048, 0x00000015, 0x00000002,
|
||||
0x00000023, 0x00000010, 0x00040047, 0x00000029, 0x00000021, 0x00000000, 0x00040047, 0x00000029,
|
||||
0x00000022, 0x00000000, 0x00030047, 0x0000005d, 0x00000019, 0x00040047, 0x0000005d, 0x00000021,
|
||||
0x00000001, 0x00040047, 0x0000005d, 0x00000022, 0x00000000, 0x00040047, 0x0000006a, 0x0000000b,
|
||||
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
|
||||
0x00000020, 0x00000001, 0x00040017, 0x00000007, 0x00000006, 0x00000002, 0x00040015, 0x0000000a,
|
||||
0x00000020, 0x00000000, 0x00040017, 0x0000000b, 0x0000000a, 0x00000003, 0x00040020, 0x0000000c,
|
||||
0x00000001, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000001, 0x00040017, 0x0000000e,
|
||||
0x0000000a, 0x00000002, 0x00030016, 0x00000013, 0x00000020, 0x00040017, 0x00000014, 0x00000013,
|
||||
0x00000004, 0x0005001e, 0x00000015, 0x00000007, 0x00000007, 0x00000014, 0x00040020, 0x00000016,
|
||||
0x00000009, 0x00000015, 0x0004003b, 0x00000016, 0x00000017, 0x00000009, 0x0004002b, 0x00000006,
|
||||
0x00000018, 0x00000001, 0x00040020, 0x00000019, 0x00000009, 0x00000007, 0x00020014, 0x0000001c,
|
||||
0x00040017, 0x0000001d, 0x0000001c, 0x00000002, 0x00040017, 0x00000023, 0x00000013, 0x00000003,
|
||||
0x00090019, 0x00000026, 0x00000013, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
|
||||
0x00000000, 0x0003001b, 0x00000027, 0x00000026, 0x00040020, 0x00000028, 0x00000000, 0x00000027,
|
||||
0x0004003b, 0x00000028, 0x00000029, 0x00000000, 0x0004002b, 0x00000006, 0x0000002c, 0x00000000,
|
||||
0x0004002b, 0x00000006, 0x00000033, 0x00000002, 0x0004002b, 0x0000000a, 0x00000034, 0x00000000,
|
||||
0x00040020, 0x00000035, 0x00000009, 0x00000013, 0x0004002b, 0x00000013, 0x0000003c, 0x3f000000,
|
||||
0x0004002b, 0x0000000a, 0x0000003f, 0x00000001, 0x0004002b, 0x00000013, 0x00000046, 0x3e59b3d0,
|
||||
0x0004002b, 0x00000013, 0x00000047, 0x3f371759, 0x0004002b, 0x00000013, 0x00000048, 0x3d93dd98,
|
||||
0x0006002c, 0x00000023, 0x00000049, 0x00000046, 0x00000047, 0x00000048, 0x0004002b, 0x0000000a,
|
||||
0x0000004d, 0x00000002, 0x0004002b, 0x00000013, 0x00000053, 0x00000000, 0x0006002c, 0x00000023,
|
||||
0x00000054, 0x00000053, 0x00000053, 0x00000053, 0x0004002b, 0x0000000a, 0x00000056, 0x00000003,
|
||||
0x00090019, 0x0000005b, 0x00000013, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
|
||||
0x00000002, 0x00040020, 0x0000005c, 0x00000000, 0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d,
|
||||
0x00000000, 0x0004002b, 0x00000013, 0x00000061, 0x3f800000, 0x0004002b, 0x0000000a, 0x00000069,
|
||||
0x00000008, 0x0006002c, 0x0000000b, 0x0000006a, 0x00000069, 0x00000069, 0x0000003f, 0x0006002c,
|
||||
0x00000023, 0x00000071, 0x0000003c, 0x0000003c, 0x0000003c, 0x0006002c, 0x00000023, 0x00000072,
|
||||
0x00000061, 0x00000061, 0x00000061, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x000300f7, 0x0000006b, 0x00000000, 0x000300fb, 0x00000034, 0x0000006c,
|
||||
0x000200f8, 0x0000006c, 0x0004003d, 0x0000000b, 0x0000000f, 0x0000000d, 0x0007004f, 0x0000000e,
|
||||
0x00000010, 0x0000000f, 0x0000000f, 0x00000000, 0x00000001, 0x0004007c, 0x00000007, 0x00000011,
|
||||
0x00000010, 0x00050041, 0x00000019, 0x0000001a, 0x00000017, 0x00000018, 0x0004003d, 0x00000007,
|
||||
0x0000001b, 0x0000001a, 0x000500af, 0x0000001d, 0x0000001e, 0x00000011, 0x0000001b, 0x0004009a,
|
||||
0x0000001c, 0x0000001f, 0x0000001e, 0x000300f7, 0x00000021, 0x00000000, 0x000400fa, 0x0000001f,
|
||||
0x00000020, 0x00000021, 0x000200f8, 0x00000020, 0x000200f9, 0x0000006b, 0x000200f8, 0x00000021,
|
||||
0x0004003d, 0x00000027, 0x0000002a, 0x00000029, 0x00050041, 0x00000019, 0x0000002d, 0x00000017,
|
||||
0x0000002c, 0x0004003d, 0x00000007, 0x0000002e, 0x0000002d, 0x00050080, 0x00000007, 0x0000002f,
|
||||
0x00000011, 0x0000002e, 0x00040064, 0x00000026, 0x00000030, 0x0000002a, 0x0007005f, 0x00000014,
|
||||
0x00000031, 0x00000030, 0x0000002f, 0x00000002, 0x0000002c, 0x0008004f, 0x00000023, 0x00000032,
|
||||
0x00000031, 0x00000031, 0x00000000, 0x00000001, 0x00000002, 0x00060041, 0x00000035, 0x00000036,
|
||||
0x00000017, 0x00000033, 0x00000034, 0x0004003d, 0x00000013, 0x00000037, 0x00000036, 0x00060050,
|
||||
0x00000023, 0x00000039, 0x00000037, 0x00000037, 0x00000037, 0x00050081, 0x00000023, 0x0000003a,
|
||||
0x00000032, 0x00000039, 0x00050083, 0x00000023, 0x0000003e, 0x0000003a, 0x00000071, 0x00060041,
|
||||
0x00000035, 0x00000040, 0x00000017, 0x00000033, 0x0000003f, 0x0004003d, 0x00000013, 0x00000041,
|
||||
0x00000040, 0x0005008e, 0x00000023, 0x00000042, 0x0000003e, 0x00000041, 0x00050081, 0x00000023,
|
||||
0x00000044, 0x00000042, 0x00000071, 0x00050094, 0x00000013, 0x0000004a, 0x00000044, 0x00000049,
|
||||
0x00060050, 0x00000023, 0x0000004b, 0x0000004a, 0x0000004a, 0x0000004a, 0x00060041, 0x00000035,
|
||||
0x0000004e, 0x00000017, 0x00000033, 0x0000004d, 0x0004003d, 0x00000013, 0x0000004f, 0x0000004e,
|
||||
0x00060050, 0x00000023, 0x00000050, 0x0000004f, 0x0000004f, 0x0000004f, 0x0008000c, 0x00000023,
|
||||
0x00000051, 0x00000001, 0x0000002e, 0x0000004b, 0x00000044, 0x00000050, 0x0007000c, 0x00000023,
|
||||
0x00000055, 0x00000001, 0x00000028, 0x00000051, 0x00000054, 0x00060041, 0x00000035, 0x00000057,
|
||||
0x00000017, 0x00000033, 0x00000056, 0x0004003d, 0x00000013, 0x00000058, 0x00000057, 0x00060050,
|
||||
0x00000023, 0x00000059, 0x00000058, 0x00000058, 0x00000058, 0x0007000c, 0x00000023, 0x0000005a,
|
||||
0x00000001, 0x0000001a, 0x00000055, 0x00000059, 0x0004003d, 0x0000005b, 0x0000005e, 0x0000005d,
|
||||
0x0008000c, 0x00000023, 0x00000064, 0x00000001, 0x0000002b, 0x0000005a, 0x00000054, 0x00000072,
|
||||
0x00050051, 0x00000013, 0x00000065, 0x00000064, 0x00000000, 0x00050051, 0x00000013, 0x00000066,
|
||||
0x00000064, 0x00000001, 0x00050051, 0x00000013, 0x00000067, 0x00000064, 0x00000002, 0x00070050,
|
||||
0x00000014, 0x00000068, 0x00000065, 0x00000066, 0x00000067, 0x00000061, 0x00040063, 0x0000005e,
|
||||
0x00000011, 0x00000068, 0x000200f9, 0x0000006b, 0x000200f8, 0x0000006b, 0x000100fd, 0x00010038,
|
||||
};
|
||||
156
runtime/contact_shadow_spv.h
Normal file
156
runtime/contact_shadow_spv.h
Normal file
@@ -0,0 +1,156 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/contact_shadow.comp; do not edit.
|
||||
// source sha256 b452a1920e59c4d83b2c994b0c6573cc1e479f7032a1310a7432d11c5c2e9908
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t contact_shadow_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x000001b1, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000069,
|
||||
0x00060010, 0x00000004, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00030047, 0x00000027,
|
||||
0x00000002, 0x00050048, 0x00000027, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000027,
|
||||
0x00000001, 0x00000023, 0x00000010, 0x00050048, 0x00000027, 0x00000002, 0x00000023, 0x00000020,
|
||||
0x00050048, 0x00000027, 0x00000003, 0x00000023, 0x00000030, 0x00040047, 0x00000069, 0x0000000b,
|
||||
0x0000001c, 0x00040047, 0x00000088, 0x00000021, 0x00000000, 0x00040047, 0x00000088, 0x00000022,
|
||||
0x00000000, 0x00040047, 0x0000013f, 0x00000021, 0x00000001, 0x00040047, 0x0000013f, 0x00000022,
|
||||
0x00000000, 0x00040047, 0x0000015c, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021,
|
||||
0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006,
|
||||
0x00000002, 0x00040017, 0x0000000a, 0x00000006, 0x00000003, 0x0004002b, 0x00000006, 0x0000001b,
|
||||
0x40000000, 0x0004002b, 0x00000006, 0x0000001d, 0x3f800000, 0x00040015, 0x00000020, 0x00000020,
|
||||
0x00000000, 0x0004002b, 0x00000020, 0x00000021, 0x00000000, 0x00040017, 0x00000026, 0x00000006,
|
||||
0x00000004, 0x0006001e, 0x00000027, 0x00000026, 0x00000026, 0x00000026, 0x00000026, 0x00040020,
|
||||
0x00000028, 0x00000009, 0x00000027, 0x0004003b, 0x00000028, 0x00000029, 0x00000009, 0x00040015,
|
||||
0x0000002a, 0x00000020, 0x00000001, 0x0004002b, 0x0000002a, 0x0000002b, 0x00000000, 0x00040020,
|
||||
0x0000002c, 0x00000009, 0x00000006, 0x0004002b, 0x00000020, 0x00000030, 0x00000001, 0x0004002b,
|
||||
0x00000020, 0x0000003f, 0x00000002, 0x0004002b, 0x00000006, 0x00000043, 0x3f000000, 0x0004002b,
|
||||
0x00000006, 0x00000059, 0x4253ee82, 0x0004002b, 0x00000006, 0x0000005b, 0x3d897143, 0x0004002b,
|
||||
0x00000006, 0x0000005c, 0x3bbf4590, 0x0005002c, 0x00000007, 0x0000005d, 0x0000005b, 0x0000005c,
|
||||
0x00040017, 0x00000064, 0x0000002a, 0x00000002, 0x00040017, 0x00000067, 0x00000020, 0x00000003,
|
||||
0x00040020, 0x00000068, 0x00000001, 0x00000067, 0x0004003b, 0x00000068, 0x00000069, 0x00000001,
|
||||
0x00040017, 0x0000006a, 0x00000020, 0x00000002, 0x00020014, 0x0000006e, 0x0004002b, 0x0000002a,
|
||||
0x00000072, 0x00000002, 0x00090019, 0x00000085, 0x00000006, 0x00000001, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x00000086, 0x00000085, 0x00040020, 0x00000087,
|
||||
0x00000000, 0x00000086, 0x0004003b, 0x00000087, 0x00000088, 0x00000000, 0x0004002b, 0x00000006,
|
||||
0x0000008f, 0x00000000, 0x0004002b, 0x00000006, 0x00000092, 0x3f7fbe77, 0x0004002b, 0x00000006,
|
||||
0x0000009e, 0x42a00000, 0x00040020, 0x000000a8, 0x00000009, 0x00000026, 0x0004002b, 0x0000002a,
|
||||
0x000000b4, 0x00000001, 0x0004002b, 0x00000020, 0x000000b5, 0x00000003, 0x0004002b, 0x00000006,
|
||||
0x000000bb, 0x3ecccccd, 0x0004002b, 0x00000006, 0x000000bc, 0x40400000, 0x0004002b, 0x00000006,
|
||||
0x000000c0, 0x3e19999a, 0x0004002b, 0x00000006, 0x000000c2, 0x3c23d70a, 0x0004002b, 0x00000006,
|
||||
0x000000cb, 0x3e800000, 0x0004002b, 0x0000002a, 0x000000da, 0x00000010, 0x0004002b, 0x00000006,
|
||||
0x000000e6, 0xbd4ccccd, 0x0005002c, 0x00000007, 0x000000f0, 0x0000008f, 0x0000008f, 0x00040017,
|
||||
0x000000f1, 0x0000006e, 0x00000002, 0x0005002c, 0x00000007, 0x000000f8, 0x0000001d, 0x0000001d,
|
||||
0x0004002b, 0x00000006, 0x00000114, 0x3ca3d70a, 0x0004002b, 0x00000006, 0x00000116, 0x3b03126f,
|
||||
0x0004002b, 0x00000006, 0x0000012b, 0x42200000, 0x0004002b, 0x0000002a, 0x00000137, 0x00000003,
|
||||
0x00090019, 0x0000013d, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
|
||||
0x00000008, 0x00040020, 0x0000013e, 0x00000000, 0x0000013d, 0x0004003b, 0x0000013e, 0x0000013f,
|
||||
0x00000000, 0x0004002b, 0x00000020, 0x0000015b, 0x00000008, 0x0006002c, 0x00000067, 0x0000015c,
|
||||
0x0000015b, 0x0000015b, 0x00000030, 0x0005002c, 0x00000007, 0x000001ad, 0x00000043, 0x00000043,
|
||||
0x0004002b, 0x00000006, 0x000001af, 0x3dcccccd, 0x0004002b, 0x00000006, 0x000001b0, 0x3d800000,
|
||||
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x000300f7,
|
||||
0x0000015d, 0x00000000, 0x000300fb, 0x00000021, 0x0000015e, 0x000200f8, 0x0000015e, 0x0004003d,
|
||||
0x00000067, 0x0000006b, 0x00000069, 0x0007004f, 0x0000006a, 0x0000006c, 0x0000006b, 0x0000006b,
|
||||
0x00000000, 0x00000001, 0x0004007c, 0x00000064, 0x0000006d, 0x0000006c, 0x00050051, 0x0000002a,
|
||||
0x00000071, 0x0000006d, 0x00000000, 0x00060041, 0x0000002c, 0x00000073, 0x00000029, 0x00000072,
|
||||
0x00000021, 0x0004003d, 0x00000006, 0x00000074, 0x00000073, 0x0004006e, 0x0000002a, 0x00000075,
|
||||
0x00000074, 0x000500af, 0x0000006e, 0x00000076, 0x00000071, 0x00000075, 0x000400a8, 0x0000006e,
|
||||
0x00000077, 0x00000076, 0x000300f7, 0x00000079, 0x00000000, 0x000400fa, 0x00000077, 0x00000078,
|
||||
0x00000079, 0x000200f8, 0x00000078, 0x00050051, 0x0000002a, 0x0000007b, 0x0000006d, 0x00000001,
|
||||
0x00060041, 0x0000002c, 0x0000007c, 0x00000029, 0x00000072, 0x00000030, 0x0004003d, 0x00000006,
|
||||
0x0000007d, 0x0000007c, 0x0004006e, 0x0000002a, 0x0000007e, 0x0000007d, 0x000500af, 0x0000006e,
|
||||
0x0000007f, 0x0000007b, 0x0000007e, 0x000200f9, 0x00000079, 0x000200f8, 0x00000079, 0x000700f5,
|
||||
0x0000006e, 0x00000080, 0x00000076, 0x0000015e, 0x0000007f, 0x00000078, 0x000300f7, 0x00000082,
|
||||
0x00000000, 0x000400fa, 0x00000080, 0x00000081, 0x00000082, 0x000200f8, 0x00000081, 0x000200f9,
|
||||
0x0000015d, 0x000200f8, 0x00000082, 0x0004003d, 0x00000086, 0x00000089, 0x00000088, 0x00040064,
|
||||
0x00000085, 0x0000008b, 0x00000089, 0x0007005f, 0x00000026, 0x0000008c, 0x0000008b, 0x0000006d,
|
||||
0x00000002, 0x0000002b, 0x00050051, 0x00000006, 0x0000008d, 0x0000008c, 0x00000000, 0x000500bc,
|
||||
0x0000006e, 0x00000090, 0x0000008d, 0x0000008f, 0x000500be, 0x0000006e, 0x00000093, 0x0000008d,
|
||||
0x00000092, 0x000500a6, 0x0000006e, 0x00000094, 0x00000090, 0x00000093, 0x000300f7, 0x00000096,
|
||||
0x00000000, 0x000400fa, 0x00000094, 0x00000095, 0x00000096, 0x000200f8, 0x00000095, 0x000200f9,
|
||||
0x0000015d, 0x000200f8, 0x00000096, 0x00060041, 0x0000002c, 0x0000009a, 0x00000029, 0x0000002b,
|
||||
0x0000003f, 0x0004003d, 0x00000006, 0x0000009b, 0x0000009a, 0x00050085, 0x00000006, 0x0000009c,
|
||||
0x0000008d, 0x0000009b, 0x000500ba, 0x0000006e, 0x0000009f, 0x0000009c, 0x0000009e, 0x000300f7,
|
||||
0x000000a1, 0x00000000, 0x000400fa, 0x0000009f, 0x000000a0, 0x000000a1, 0x000200f8, 0x000000a0,
|
||||
0x000200f9, 0x0000015d, 0x000200f8, 0x000000a1, 0x0004006f, 0x00000007, 0x000000a5, 0x0000006d,
|
||||
0x00050081, 0x00000007, 0x000000a7, 0x000000a5, 0x000001ad, 0x00050041, 0x000000a8, 0x000000a9,
|
||||
0x00000029, 0x00000072, 0x0004003d, 0x00000026, 0x000000aa, 0x000000a9, 0x0007004f, 0x00000007,
|
||||
0x000000ab, 0x000000aa, 0x000000aa, 0x00000002, 0x00000003, 0x00050085, 0x00000007, 0x000000ac,
|
||||
0x000000a7, 0x000000ab, 0x0005008e, 0x00000007, 0x00000167, 0x000000ac, 0x0000001b, 0x00050083,
|
||||
0x00000007, 0x00000169, 0x00000167, 0x000000f8, 0x00050051, 0x00000006, 0x0000016b, 0x00000169,
|
||||
0x00000000, 0x00050085, 0x00000006, 0x0000016d, 0x0000016b, 0x0000009c, 0x00060041, 0x0000002c,
|
||||
0x0000016e, 0x00000029, 0x0000002b, 0x00000021, 0x0004003d, 0x00000006, 0x0000016f, 0x0000016e,
|
||||
0x00050088, 0x00000006, 0x00000170, 0x0000016d, 0x0000016f, 0x00050051, 0x00000006, 0x00000172,
|
||||
0x00000169, 0x00000001, 0x0004007f, 0x00000006, 0x00000173, 0x00000172, 0x00050085, 0x00000006,
|
||||
0x00000175, 0x00000173, 0x0000009c, 0x00060041, 0x0000002c, 0x00000176, 0x00000029, 0x0000002b,
|
||||
0x00000030, 0x0004003d, 0x00000006, 0x00000177, 0x00000176, 0x00050088, 0x00000006, 0x00000178,
|
||||
0x00000175, 0x00000177, 0x0004007f, 0x00000006, 0x0000017a, 0x0000009c, 0x00060050, 0x0000000a,
|
||||
0x0000017b, 0x00000170, 0x00000178, 0x0000017a, 0x00060041, 0x0000002c, 0x000000b6, 0x00000029,
|
||||
0x000000b4, 0x000000b5, 0x0004003d, 0x00000006, 0x000000b7, 0x000000b6, 0x00050085, 0x00000006,
|
||||
0x000000ba, 0x0000009c, 0x000001af, 0x0008000c, 0x00000006, 0x000000bd, 0x00000001, 0x0000002b,
|
||||
0x000000ba, 0x000000bb, 0x000000bc, 0x00050085, 0x00000006, 0x000000be, 0x000000b7, 0x000000bd,
|
||||
0x00050085, 0x00000006, 0x000000c3, 0x0000009c, 0x000000c2, 0x00050081, 0x00000006, 0x000000c4,
|
||||
0x000000c0, 0x000000c3, 0x00050085, 0x00000006, 0x000000c8, 0x000000be, 0x000001b0, 0x00050094,
|
||||
0x00000006, 0x0000017f, 0x000000a5, 0x0000005d, 0x0006000c, 0x00000006, 0x00000180, 0x00000001,
|
||||
0x0000000a, 0x0000017f, 0x00050085, 0x00000006, 0x00000181, 0x00000059, 0x00000180, 0x0006000c,
|
||||
0x00000006, 0x00000182, 0x00000001, 0x0000000a, 0x00000181, 0x00050081, 0x00000006, 0x000000d0,
|
||||
0x000000cb, 0x00000182, 0x00050085, 0x00000006, 0x000000d1, 0x000000c8, 0x000000d0, 0x000200f9,
|
||||
0x000000d4, 0x000200f8, 0x000000d4, 0x000700f5, 0x00000006, 0x000001a7, 0x000000d1, 0x000000a1,
|
||||
0x00000129, 0x000000d7, 0x000700f5, 0x0000002a, 0x000001a6, 0x0000002b, 0x000000a1, 0x00000126,
|
||||
0x000000d7, 0x000500b1, 0x0000006e, 0x000000db, 0x000001a6, 0x000000da, 0x000400f6, 0x000000d6,
|
||||
0x000000d7, 0x00000000, 0x000400fa, 0x000000db, 0x000000d5, 0x000000d6, 0x000200f8, 0x000000d5,
|
||||
0x00050041, 0x000000a8, 0x000000de, 0x00000029, 0x000000b4, 0x0004003d, 0x00000026, 0x000000df,
|
||||
0x000000de, 0x0008004f, 0x0000000a, 0x000000e0, 0x000000df, 0x000000df, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x0005008e, 0x0000000a, 0x000000e2, 0x000000e0, 0x000001a7, 0x00050081, 0x0000000a,
|
||||
0x000000e3, 0x0000017b, 0x000000e2, 0x00050051, 0x00000006, 0x000000e5, 0x000000e3, 0x00000002,
|
||||
0x000500be, 0x0000006e, 0x000000e7, 0x000000e5, 0x000000e6, 0x000300f7, 0x000000e9, 0x00000000,
|
||||
0x000400fa, 0x000000e7, 0x000000e8, 0x000000e9, 0x000200f8, 0x000000e8, 0x000200f9, 0x000000d6,
|
||||
0x000200f8, 0x000000e9, 0x0004007f, 0x00000006, 0x00000188, 0x000000e5, 0x00050051, 0x00000006,
|
||||
0x0000018a, 0x000000e3, 0x00000000, 0x00050085, 0x00000006, 0x0000018b, 0x00000043, 0x0000018a,
|
||||
0x00050085, 0x00000006, 0x0000018e, 0x0000018b, 0x0000016f, 0x00050088, 0x00000006, 0x00000190,
|
||||
0x0000018e, 0x00000188, 0x00050081, 0x00000006, 0x00000191, 0x00000043, 0x00000190, 0x00050051,
|
||||
0x00000006, 0x00000193, 0x000000e3, 0x00000001, 0x00050085, 0x00000006, 0x00000194, 0x00000043,
|
||||
0x00000193, 0x00050085, 0x00000006, 0x00000197, 0x00000194, 0x00000177, 0x00050088, 0x00000006,
|
||||
0x00000199, 0x00000197, 0x00000188, 0x00050083, 0x00000006, 0x0000019a, 0x00000043, 0x00000199,
|
||||
0x00050050, 0x00000007, 0x0000019b, 0x00000191, 0x0000019a, 0x000500b8, 0x000000f1, 0x000000f2,
|
||||
0x0000019b, 0x000000f0, 0x0004009a, 0x0000006e, 0x000000f3, 0x000000f2, 0x000400a8, 0x0000006e,
|
||||
0x000000f4, 0x000000f3, 0x000300f7, 0x000000f6, 0x00000000, 0x000400fa, 0x000000f4, 0x000000f5,
|
||||
0x000000f6, 0x000200f8, 0x000000f5, 0x000500ba, 0x000000f1, 0x000000f9, 0x0000019b, 0x000000f8,
|
||||
0x0004009a, 0x0000006e, 0x000000fa, 0x000000f9, 0x000200f9, 0x000000f6, 0x000200f8, 0x000000f6,
|
||||
0x000700f5, 0x0000006e, 0x000000fb, 0x000000f3, 0x000000e9, 0x000000fa, 0x000000f5, 0x000300f7,
|
||||
0x000000fd, 0x00000000, 0x000400fa, 0x000000fb, 0x000000fc, 0x000000fd, 0x000200f8, 0x000000fc,
|
||||
0x000200f9, 0x000000d6, 0x000200f8, 0x000000fd, 0x0004003d, 0x00000086, 0x00000100, 0x00000088,
|
||||
0x0007004f, 0x00000007, 0x00000104, 0x000000aa, 0x000000aa, 0x00000000, 0x00000001, 0x00050085,
|
||||
0x00000007, 0x00000105, 0x0000019b, 0x00000104, 0x0004006e, 0x00000064, 0x00000106, 0x00000105,
|
||||
0x00040064, 0x00000085, 0x00000107, 0x00000100, 0x0007005f, 0x00000026, 0x00000108, 0x00000107,
|
||||
0x00000106, 0x00000002, 0x0000002b, 0x00050051, 0x00000006, 0x00000109, 0x00000108, 0x00000000,
|
||||
0x00050085, 0x00000006, 0x0000010c, 0x00000109, 0x0000009b, 0x00050083, 0x00000006, 0x00000112,
|
||||
0x00000188, 0x0000010c, 0x00050085, 0x00000006, 0x00000117, 0x0000009c, 0x00000116, 0x00050081,
|
||||
0x00000006, 0x00000118, 0x00000114, 0x00000117, 0x000500ba, 0x0000006e, 0x00000119, 0x00000112,
|
||||
0x00000118, 0x000500b8, 0x0000006e, 0x0000011c, 0x00000112, 0x000000c4, 0x000500a7, 0x0000006e,
|
||||
0x0000011d, 0x00000119, 0x0000011c, 0x000300f7, 0x0000011f, 0x00000000, 0x000400fa, 0x0000011d,
|
||||
0x0000011e, 0x0000011f, 0x000200f8, 0x0000011e, 0x0004006f, 0x00000006, 0x00000121, 0x000001a6,
|
||||
0x00050085, 0x00000006, 0x00000122, 0x00000121, 0x000001b0, 0x00050083, 0x00000006, 0x00000123,
|
||||
0x0000001d, 0x00000122, 0x000200f9, 0x000000d6, 0x000200f8, 0x0000011f, 0x000200f9, 0x000000d7,
|
||||
0x000200f8, 0x000000d7, 0x00050080, 0x0000002a, 0x00000126, 0x000001a6, 0x000000b4, 0x00050081,
|
||||
0x00000006, 0x00000129, 0x000001a7, 0x000000c8, 0x000200f9, 0x000000d4, 0x000200f8, 0x000000d6,
|
||||
0x000b00f5, 0x00000006, 0x000001aa, 0x0000008f, 0x000000d4, 0x0000008f, 0x000000e8, 0x0000008f,
|
||||
0x000000fc, 0x00000123, 0x0000011e, 0x0008000c, 0x00000006, 0x0000012d, 0x00000001, 0x00000031,
|
||||
0x0000012b, 0x0000009e, 0x0000009c, 0x00050083, 0x00000006, 0x0000012e, 0x0000001d, 0x0000012d,
|
||||
0x00060041, 0x0000002c, 0x00000130, 0x00000029, 0x0000002b, 0x000000b5, 0x0004003d, 0x00000006,
|
||||
0x00000131, 0x00000130, 0x00050085, 0x00000006, 0x00000133, 0x00000131, 0x000001aa, 0x00050085,
|
||||
0x00000006, 0x00000135, 0x00000133, 0x0000012e, 0x00050083, 0x00000006, 0x00000136, 0x0000001d,
|
||||
0x00000135, 0x00060041, 0x0000002c, 0x00000138, 0x00000029, 0x00000137, 0x00000021, 0x0004003d,
|
||||
0x00000006, 0x00000139, 0x00000138, 0x000500ba, 0x0000006e, 0x0000013a, 0x00000139, 0x00000043,
|
||||
0x000300f7, 0x0000013c, 0x00000000, 0x000400fa, 0x0000013a, 0x0000013b, 0x0000013c, 0x000200f8,
|
||||
0x0000013b, 0x0004003d, 0x0000013d, 0x00000140, 0x0000013f, 0x00070050, 0x00000026, 0x00000143,
|
||||
0x00000136, 0x00000136, 0x00000136, 0x0000001d, 0x00040063, 0x00000140, 0x0000006d, 0x00000143,
|
||||
0x000200f9, 0x0000015d, 0x000200f8, 0x0000013c, 0x000500be, 0x0000006e, 0x00000146, 0x00000136,
|
||||
0x00000092, 0x000300f7, 0x00000148, 0x00000000, 0x000400fa, 0x00000146, 0x00000147, 0x00000148,
|
||||
0x000200f8, 0x00000147, 0x000200f9, 0x0000015d, 0x000200f8, 0x00000148, 0x0004003d, 0x0000013d,
|
||||
0x0000014c, 0x0000013f, 0x00050062, 0x00000026, 0x0000014e, 0x0000014c, 0x0000006d, 0x0004003d,
|
||||
0x0000013d, 0x0000014f, 0x0000013f, 0x0008004f, 0x0000000a, 0x00000152, 0x0000014e, 0x0000014e,
|
||||
0x00000000, 0x00000001, 0x00000002, 0x0005008e, 0x0000000a, 0x00000154, 0x00000152, 0x00000136,
|
||||
0x00050051, 0x00000006, 0x00000156, 0x0000014e, 0x00000003, 0x00050051, 0x00000006, 0x00000157,
|
||||
0x00000154, 0x00000000, 0x00050051, 0x00000006, 0x00000158, 0x00000154, 0x00000001, 0x00050051,
|
||||
0x00000006, 0x00000159, 0x00000154, 0x00000002, 0x00070050, 0x00000026, 0x0000015a, 0x00000157,
|
||||
0x00000158, 0x00000159, 0x00000156, 0x00040063, 0x0000014f, 0x0000006d, 0x0000015a, 0x000200f9,
|
||||
0x0000015d, 0x000200f8, 0x0000015d, 0x000100fd, 0x00010038,
|
||||
};
|
||||
361
runtime/cpu.h
Normal file
361
runtime/cpu.h
Normal file
@@ -0,0 +1,361 @@
|
||||
// Adapted from LAS Recomp runtime/cpu.h @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
||||
// Guest CPU state and helpers used by recompiled code.
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct __attribute__((aligned(16))) Cpu {
|
||||
uint64_t x[32]; // x0-x30 (x[31] unused; sp is separate)
|
||||
uint64_t sp; // 256
|
||||
uint64_t pc; // 264: target for indirect dispatch
|
||||
uint64_t nzcv; // 272: flags in bits 31..28
|
||||
uint64_t tpidr; // 280
|
||||
uint64_t tpidrro; // 288
|
||||
uint64_t excl_addr; // 296
|
||||
uint64_t excl_val; // 304
|
||||
void *thread; // 312: runtime thread object
|
||||
uint64_t fpcr; // 320: guest FPCR/FPSR (portable FP/SIMD lowering only; the asm path uses the host's)
|
||||
uint64_t fpsr; // 328
|
||||
uint64_t excl_val_hi; // 336: upper half of a 128-bit exclusive pair (LDXP/LDAXP of two X registers)
|
||||
uint8_t _pad[512 - 344];
|
||||
__uint128_t v[32]; // 512
|
||||
} Cpu;
|
||||
|
||||
_Static_assert(offsetof(Cpu, sp) == 256, "sp");
|
||||
_Static_assert(offsetof(Cpu, pc) == 264, "pc");
|
||||
_Static_assert(offsetof(Cpu, nzcv) == 272, "nzcv");
|
||||
_Static_assert(offsetof(Cpu, fpcr) == 320, "fpcr");
|
||||
_Static_assert(offsetof(Cpu, fpsr) == 328, "fpsr");
|
||||
_Static_assert(offsetof(Cpu, v) == 512, "v");
|
||||
|
||||
typedef void (*GuestFn)(Cpu *);
|
||||
|
||||
// ---- runtime services (runtime/*.c)
|
||||
void rt_dispatch(Cpu *c); // call function at c->pc
|
||||
void rt_spin_gate(Cpu *c, uint64_t head); // a pass of a spin wait loop (kernel.c, recomp/patches.py `spin`)
|
||||
void rt_svc(Cpu *c, uint32_t imm, uint64_t pc);
|
||||
void rt_brk(Cpu *c, uint32_t imm, uint64_t pc) __attribute__((noreturn));
|
||||
void rt_udf(Cpu *c, uint64_t pc) __attribute__((noreturn));
|
||||
uint64_t rt_ticks(void);
|
||||
|
||||
#if defined(__aarch64__)
|
||||
static inline uint64_t rt_get_fpcr(void) { uint64_t v; __asm__ volatile("mrs %0, fpcr" : "=r"(v)); return v; }
|
||||
static inline uint64_t rt_get_fpsr(void) { uint64_t v; __asm__ volatile("mrs %0, fpsr" : "=r"(v)); return v; }
|
||||
static inline void rt_set_fpcr(uint64_t v) { __asm__ volatile("msr fpcr, %0" :: "r"(v)); }
|
||||
static inline void rt_set_fpsr(uint64_t v) { __asm__ volatile("msr fpsr, %0" :: "r"(v)); }
|
||||
static inline void rt_fpcr_sync(const Cpu *c) { (void)c; }
|
||||
#else
|
||||
// Host FP state is not the guest's here: code lifted with --simd=c keeps FPCR/FPSR in Cpu (c->fpcr, zeroed
|
||||
// with the Cpu), so the host-side reset in thread start-up has nothing to do.
|
||||
static inline uint64_t rt_get_fpcr(void) { return 0; }
|
||||
static inline uint64_t rt_get_fpsr(void) { return 0; }
|
||||
static inline void rt_set_fpcr(uint64_t v) { (void)v; }
|
||||
static inline void rt_set_fpsr(uint64_t v) { (void)v; }
|
||||
// The MXCSR of a host thread running guest code mirrors c->fpcr: FTZ|DAZ = FPCR.FZ, RC = nearest (FPCR.RMode takes
|
||||
// the slow paths of runtime/simd.h, which switch RC around one operation). The FP helpers rely on it without checking,
|
||||
// so it is set wherever FPCR or the host FP context changes: MSR FPCR (recomp/lifter.py), rt_call (thread and fiber
|
||||
// entry, host code calling guest code), fiber resume (runtime/kernel.c fiber_resumed; fibers keep their own MXCSR).
|
||||
static inline void rt_fpcr_sync(const Cpu *c) {
|
||||
unsigned cur = __builtin_ia32_stmxcsr();
|
||||
unsigned want = (cur & ~0xE040u) | (c->fpcr & (1u << 24) ? 0x8040u : 0); // FTZ 0x8000, DAZ 0x40, RC 0x6000
|
||||
if (cur != want) __builtin_ia32_ldmxcsr(want);
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---- memory: guest addresses are host addresses
|
||||
typedef uint16_t __attribute__((aligned(1), may_alias)) u16u;
|
||||
typedef uint32_t __attribute__((aligned(1), may_alias)) u32u;
|
||||
typedef uint64_t __attribute__((aligned(1), may_alias)) u64u;
|
||||
typedef uint8_t __attribute__((may_alias)) u8a;
|
||||
|
||||
// Function-entry profiler (runtime/fnprof.c, LARECOMP_FNPROF): one predictable branch per call when off.
|
||||
extern volatile int g_fnprof;
|
||||
void fnprof_hit(uint64_t addr, uint64_t lr, uint64_t x0);
|
||||
#define FN_ENTER(a) do { if (__builtin_expect(g_fnprof, 0)) fnprof_hit(a, c->x[30], c->x[0]); } while (0)
|
||||
|
||||
#define LD8(a) (*(const u8a *)(uintptr_t)(a))
|
||||
#define LD16(a) (*(const u16u *)(uintptr_t)(a))
|
||||
#define LD32(a) (*(const u32u *)(uintptr_t)(a))
|
||||
#define LD64(a) (*(const u64u *)(uintptr_t)(a))
|
||||
#define ST8(a, v) (*(u8a *)(uintptr_t)(a) = (uint8_t)(v))
|
||||
#define ST16(a, v) (*(u16u *)(uintptr_t)(a) = (uint16_t)(v))
|
||||
#define ST32(a, v) (*(u32u *)(uintptr_t)(a) = (uint32_t)(v))
|
||||
#define ST64(a, v) (*(u64u *)(uintptr_t)(a) = (uint64_t)(v))
|
||||
|
||||
// ---- exclusive monitor (emulated with compare-and-swap)
|
||||
#define EXCL(N, T) \
|
||||
static inline uint64_t ldx##N(Cpu *c, uint64_t a) { \
|
||||
T v = __atomic_load_n((T *)(uintptr_t)a, __ATOMIC_ACQUIRE); \
|
||||
c->excl_addr = a; \
|
||||
c->excl_val = v; \
|
||||
return v; \
|
||||
} \
|
||||
static inline uint32_t stx##N(Cpu *c, uint64_t a, uint64_t v) { \
|
||||
if (c->excl_addr != a) return 1; \
|
||||
c->excl_addr = ~0ull; \
|
||||
T expected = (T)c->excl_val; \
|
||||
return !__atomic_compare_exchange_n((T *)(uintptr_t)a, &expected, (T)v, 0, \
|
||||
__ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); \
|
||||
} \
|
||||
static inline uint64_t lda##N(uint64_t a) { \
|
||||
return __atomic_load_n((T *)(uintptr_t)a, __ATOMIC_ACQUIRE); \
|
||||
} \
|
||||
static inline void stl##N(uint64_t a, uint64_t v) { \
|
||||
__atomic_store_n((T *)(uintptr_t)a, (T)v, __ATOMIC_RELEASE); \
|
||||
}
|
||||
EXCL(8, uint8_t)
|
||||
EXCL(16, uint16_t)
|
||||
EXCL(32, uint32_t)
|
||||
EXCL(64, uint64_t)
|
||||
|
||||
// Exclusive pair of two X registers: one 16-byte-aligned 128-bit access (cmpxchg16b; x86-64-v2 has CX16).
|
||||
// A pair of two W registers is a 64-bit exclusive and uses ldx64/stx64.
|
||||
static inline __uint128_t ldxp128(Cpu *c, uint64_t a) {
|
||||
__uint128_t v = __atomic_load_n((__uint128_t *)(uintptr_t)a, __ATOMIC_ACQUIRE);
|
||||
c->excl_addr = a;
|
||||
c->excl_val = (uint64_t)v;
|
||||
c->excl_val_hi = (uint64_t)(v >> 64);
|
||||
return v;
|
||||
}
|
||||
static inline uint32_t stxp128(Cpu *c, uint64_t a, uint64_t lo, uint64_t hi) {
|
||||
if (c->excl_addr != a) return 1;
|
||||
c->excl_addr = ~0ull;
|
||||
__uint128_t expected = ((__uint128_t)c->excl_val_hi << 64) | c->excl_val;
|
||||
__uint128_t v = ((__uint128_t)hi << 64) | lo;
|
||||
return !__atomic_compare_exchange_n((__uint128_t *)(uintptr_t)a, &expected, v, 0, __ATOMIC_SEQ_CST,
|
||||
__ATOMIC_SEQ_CST);
|
||||
}
|
||||
|
||||
// ---- LSE atomics (ARMv8.1; the A57 lacks them, the SDK carries them behind a CPU feature check).
|
||||
// Add, clear, xor, set and swap map to __atomic builtins in the lifter; min/max need a CAS loop. Return the old value.
|
||||
#define ATOMIC_MINMAX(N, T, S) \
|
||||
static inline T atomic_smax##N(uint64_t a, T v) { \
|
||||
T o = __atomic_load_n((T *)(uintptr_t)a, __ATOMIC_RELAXED); \
|
||||
while (!__atomic_compare_exchange_n((T *)(uintptr_t)a, &o, (S)o > (S)v ? o : v, 1, \
|
||||
__ATOMIC_SEQ_CST, __ATOMIC_RELAXED)) {} \
|
||||
return o; \
|
||||
} \
|
||||
static inline T atomic_smin##N(uint64_t a, T v) { \
|
||||
T o = __atomic_load_n((T *)(uintptr_t)a, __ATOMIC_RELAXED); \
|
||||
while (!__atomic_compare_exchange_n((T *)(uintptr_t)a, &o, (S)o < (S)v ? o : v, 1, \
|
||||
__ATOMIC_SEQ_CST, __ATOMIC_RELAXED)) {} \
|
||||
return o; \
|
||||
} \
|
||||
static inline T atomic_umax##N(uint64_t a, T v) { \
|
||||
T o = __atomic_load_n((T *)(uintptr_t)a, __ATOMIC_RELAXED); \
|
||||
while (!__atomic_compare_exchange_n((T *)(uintptr_t)a, &o, o > v ? o : v, 1, \
|
||||
__ATOMIC_SEQ_CST, __ATOMIC_RELAXED)) {} \
|
||||
return o; \
|
||||
} \
|
||||
static inline T atomic_umin##N(uint64_t a, T v) { \
|
||||
T o = __atomic_load_n((T *)(uintptr_t)a, __ATOMIC_RELAXED); \
|
||||
while (!__atomic_compare_exchange_n((T *)(uintptr_t)a, &o, o < v ? o : v, 1, \
|
||||
__ATOMIC_SEQ_CST, __ATOMIC_RELAXED)) {} \
|
||||
return o; \
|
||||
}
|
||||
ATOMIC_MINMAX(8, uint8_t, int8_t)
|
||||
ATOMIC_MINMAX(16, uint16_t, int16_t)
|
||||
ATOMIC_MINMAX(32, uint32_t, int32_t)
|
||||
ATOMIC_MINMAX(64, uint64_t, int64_t)
|
||||
|
||||
// ---- flags (packed NZCV in bits 31..28)
|
||||
#define FLAG_N(c) (((c)->nzcv >> 31) & 1)
|
||||
#define FLAG_Z(c) (((c)->nzcv >> 30) & 1)
|
||||
#define FLAG_C(c) (((c)->nzcv >> 29) & 1)
|
||||
#define FLAG_V(c) (((c)->nzcv >> 28) & 1)
|
||||
#define COND_EQ(c) (FLAG_Z(c))
|
||||
#define COND_NE(c) (!FLAG_Z(c))
|
||||
#define COND_CS(c) (FLAG_C(c))
|
||||
#define COND_CC(c) (!FLAG_C(c))
|
||||
#define COND_MI(c) (FLAG_N(c))
|
||||
#define COND_PL(c) (!FLAG_N(c))
|
||||
#define COND_VS(c) (FLAG_V(c))
|
||||
#define COND_VC(c) (!FLAG_V(c))
|
||||
#define COND_HI(c) (FLAG_C(c) && !FLAG_Z(c))
|
||||
#define COND_LS(c) (!(FLAG_C(c) && !FLAG_Z(c)))
|
||||
#define COND_GE(c) (FLAG_N(c) == FLAG_V(c))
|
||||
#define COND_LT(c) (FLAG_N(c) != FLAG_V(c))
|
||||
#define COND_GT(c) (!FLAG_Z(c) && FLAG_N(c) == FLAG_V(c))
|
||||
#define COND_LE(c) (!(!FLAG_Z(c) && FLAG_N(c) == FLAG_V(c)))
|
||||
|
||||
#define MKFLAGS(n, z, cy, v) \
|
||||
(((uint64_t)(n) << 31) | ((uint64_t)(z) << 30) | ((uint64_t)(cy) << 29) | ((uint64_t)(v) << 28))
|
||||
|
||||
static inline uint64_t adds64(Cpu *c, uint64_t a, uint64_t b) {
|
||||
uint64_t r = a + b;
|
||||
c->nzcv = MKFLAGS(r >> 63, r == 0, r < a, ((a ^ r) & (b ^ r)) >> 63);
|
||||
return r;
|
||||
}
|
||||
static inline uint64_t subs64(Cpu *c, uint64_t a, uint64_t b) {
|
||||
uint64_t r = a - b;
|
||||
c->nzcv = MKFLAGS(r >> 63, r == 0, a >= b, ((a ^ b) & (a ^ r)) >> 63);
|
||||
return r;
|
||||
}
|
||||
static inline uint32_t adds32(Cpu *c, uint32_t a, uint32_t b) {
|
||||
uint32_t r = a + b;
|
||||
c->nzcv = MKFLAGS(r >> 31, r == 0, r < a, ((a ^ r) & (b ^ r)) >> 31);
|
||||
return r;
|
||||
}
|
||||
static inline uint32_t subs32(Cpu *c, uint32_t a, uint32_t b) {
|
||||
uint32_t r = a - b;
|
||||
c->nzcv = MKFLAGS(r >> 31, r == 0, a >= b, ((a ^ b) & (a ^ r)) >> 31);
|
||||
return r;
|
||||
}
|
||||
static inline uint64_t logic64(Cpu *c, uint64_t r) { c->nzcv = MKFLAGS(r >> 63, r == 0, 0, 0); return r; }
|
||||
static inline uint32_t logic32(Cpu *c, uint32_t r) { c->nzcv = MKFLAGS(r >> 31, r == 0, 0, 0); return r; }
|
||||
|
||||
#define ADC_IMPL(N, S) \
|
||||
static inline uint##N##_t adc##N(Cpu *c, uint##N##_t a, uint##N##_t b) { \
|
||||
return a + b + (uint##N##_t)FLAG_C(c); \
|
||||
} \
|
||||
static inline uint##N##_t sbc##N(Cpu *c, uint##N##_t a, uint##N##_t b) { \
|
||||
return a - b - 1 + (uint##N##_t)FLAG_C(c); \
|
||||
} \
|
||||
static inline uint##N##_t adcs##N(Cpu *c, uint##N##_t a, uint##N##_t b) { \
|
||||
uint##N##_t cin = FLAG_C(c), r = a + b + cin; \
|
||||
int carry = cin ? r <= a : r < a; \
|
||||
c->nzcv = MKFLAGS(r >> (N - 1), r == 0, carry, ((a ^ r) & (b ^ r)) >> (N - 1)); \
|
||||
return r; \
|
||||
} \
|
||||
static inline uint##N##_t sbcs##N(Cpu *c, uint##N##_t a, uint##N##_t b) { \
|
||||
return adcs##N(c, a, (uint##N##_t)~b); \
|
||||
}
|
||||
ADC_IMPL(64, )
|
||||
ADC_IMPL(32, )
|
||||
|
||||
// ---- function-local flags (recomp/flags.py)
|
||||
// Generated functions keep NZCV in a local Flags F, never in c->nzcv. A definition of a known kind stores its
|
||||
// operands (a - b, a + b, or a logic result in a); fc_<kind>(&F, CC_xx) computes a condition from them, so a compare
|
||||
// and its branch become one host compare. F.nz holds the packed form for definitions only known packed (CCMP, FCMP,
|
||||
// MSR, ADCS) and for readers that several kinds of definition reach. Every helper is always inlined with a constant
|
||||
// condition, so F stays in registers and only the condition's own arithmetic remains.
|
||||
typedef struct { uint64_t a, b, nz; } Flags;
|
||||
enum { CC_EQ, CC_NE, CC_CS, CC_CC, CC_MI, CC_PL, CC_VS, CC_VC, CC_HI, CC_LS, CC_GE, CC_LT, CC_GT, CC_LE };
|
||||
#define CPU_FL static inline __attribute__((always_inline))
|
||||
|
||||
CPU_FL int fl_cond(int cc, int n, int z, int cy, int v) {
|
||||
switch (cc) {
|
||||
case CC_EQ: return z;
|
||||
case CC_NE: return !z;
|
||||
case CC_CS: return cy;
|
||||
case CC_CC: return !cy;
|
||||
case CC_MI: return n;
|
||||
case CC_PL: return !n;
|
||||
case CC_VS: return v;
|
||||
case CC_VC: return !v;
|
||||
case CC_HI: return cy && !z;
|
||||
case CC_LS: return !cy || z;
|
||||
case CC_GE: return n == v;
|
||||
case CC_LT: return n != v;
|
||||
case CC_GT: return !z && n == v;
|
||||
default: return z || n != v;
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract (SUBS, CMP): conditions as host compares of the operands.
|
||||
#define FL_SUB(N, U, S) \
|
||||
CPU_FL U fsub##N(Flags *f, U a, U b) { f->a = a; f->b = b; return (U)(a - b); } \
|
||||
CPU_FL int fc_sub##N(const Flags *f, int cc) { \
|
||||
U a = (U)f->a, b = (U)f->b, r = (U)(a - b); \
|
||||
switch (cc) { \
|
||||
case CC_EQ: return a == b; \
|
||||
case CC_NE: return a != b; \
|
||||
case CC_CS: return a >= b; \
|
||||
case CC_CC: return a < b; \
|
||||
case CC_MI: return (S)r < 0; \
|
||||
case CC_PL: return (S)r >= 0; \
|
||||
case CC_VS: return (S)((a ^ b) & (a ^ r)) < 0; \
|
||||
case CC_VC: return (S)((a ^ b) & (a ^ r)) >= 0; \
|
||||
case CC_HI: return a > b; \
|
||||
case CC_LS: return a <= b; \
|
||||
case CC_GE: return (S)a >= (S)b; \
|
||||
case CC_LT: return (S)a < (S)b; \
|
||||
case CC_GT: return (S)a > (S)b; \
|
||||
default: return (S)a <= (S)b; \
|
||||
} \
|
||||
} \
|
||||
CPU_FL uint64_t nz_sub##N(U a, U b) { \
|
||||
U r = (U)(a - b); \
|
||||
return MKFLAGS((S)r < 0, r == 0, a >= b, (S)((a ^ b) & (a ^ r)) < 0); \
|
||||
} \
|
||||
CPU_FL uint64_t fnz_sub##N(const Flags *f) { return nz_sub##N((U)f->a, (U)f->b); }
|
||||
FL_SUB(64, uint64_t, int64_t)
|
||||
FL_SUB(32, uint32_t, int32_t)
|
||||
|
||||
// Add (ADDS, CMN) and logic results (ANDS, TST, BICS): conditions from N, Z, C, V.
|
||||
#define FL_ADD(N, U, S) \
|
||||
CPU_FL U fadd##N(Flags *f, U a, U b) { f->a = a; f->b = b; return (U)(a + b); } \
|
||||
CPU_FL uint64_t nz_add##N(U a, U b) { \
|
||||
U r = (U)(a + b); \
|
||||
return MKFLAGS((S)r < 0, r == 0, r < a, (S)((a ^ r) & (b ^ r)) < 0); \
|
||||
} \
|
||||
CPU_FL int fc_add##N(const Flags *f, int cc) { \
|
||||
U a = (U)f->a, b = (U)f->b, r = (U)(a + b); \
|
||||
return fl_cond(cc, (S)r < 0, r == 0, r < a, (S)((a ^ r) & (b ^ r)) < 0); \
|
||||
} \
|
||||
CPU_FL uint64_t fnz_add##N(const Flags *f) { return nz_add##N((U)f->a, (U)f->b); } \
|
||||
CPU_FL U flogic##N(Flags *f, U r) { f->a = r; return r; } \
|
||||
CPU_FL int fc_logic##N(const Flags *f, int cc) { \
|
||||
U r = (U)f->a; \
|
||||
return fl_cond(cc, (S)r < 0, r == 0, 0, 0); \
|
||||
} \
|
||||
CPU_FL uint64_t fnz_logic##N(const Flags *f) { return MKFLAGS((S)f->a < 0, (U)f->a == 0, 0, 0); }
|
||||
FL_ADD(64, uint64_t, int64_t)
|
||||
FL_ADD(32, uint32_t, int32_t)
|
||||
|
||||
// Packed flags.
|
||||
CPU_FL int fc_nz(const Flags *f, int cc) {
|
||||
return fl_cond(cc, f->nz >> 31 & 1, f->nz >> 30 & 1, f->nz >> 29 & 1, f->nz >> 28 & 1);
|
||||
}
|
||||
CPU_FL uint64_t fnz_nz(const Flags *f) { return f->nz; }
|
||||
|
||||
// ADC, SBC, ADCS, SBCS with the carry in from F.
|
||||
#define FL_ADC(N, U) \
|
||||
CPU_FL U adc##N##f(int cin, U a, U b) { return (U)(a + b + (U)cin); } \
|
||||
CPU_FL U sbc##N##f(int cin, U a, U b) { return (U)(a - b - 1 + (U)cin); } \
|
||||
CPU_FL U adcs##N##f(Flags *f, int cin, U a, U b) { \
|
||||
U r = (U)(a + b + (U)cin); \
|
||||
int carry = cin ? r <= a : r < a; \
|
||||
f->nz = MKFLAGS(r >> (N - 1), r == 0, carry, ((a ^ r) & (b ^ r)) >> (N - 1)); \
|
||||
return r; \
|
||||
} \
|
||||
CPU_FL U sbcs##N##f(Flags *f, int cin, U a, U b) { return adcs##N##f(f, cin, a, (U)~b); }
|
||||
FL_ADC(64, uint64_t)
|
||||
FL_ADC(32, uint32_t)
|
||||
#undef CPU_FL
|
||||
|
||||
// ---- bit ops
|
||||
#define ROR64(v, r) ((uint64_t)(v) >> ((r) & 63) | (uint64_t)(v) << ((64 - ((r) & 63)) & 63))
|
||||
#define ROR32(v, r) ((uint32_t)(v) >> ((r) & 31) | (uint32_t)(v) << ((32 - ((r) & 31)) & 31))
|
||||
static inline uint64_t clz64(uint64_t v) { return v ? __builtin_clzll(v) : 64; }
|
||||
static inline uint32_t clz32(uint32_t v) { return v ? __builtin_clz(v) : 32; }
|
||||
static inline uint64_t cls64(uint64_t v) { return clz64(v ^ (uint64_t)((int64_t)v >> 1)) - 1; }
|
||||
static inline uint32_t cls32(uint32_t v) { return clz32(v ^ (uint32_t)((int32_t)v >> 1)) - 1; }
|
||||
static inline uint64_t rbit64(uint64_t v) { return __builtin_bitreverse64(v); }
|
||||
static inline uint32_t rbit32(uint32_t v) { return __builtin_bitreverse32(v); }
|
||||
static inline uint32_t rev16_32(uint32_t v) { return ((v & 0x00ff00ffu) << 8) | ((v >> 8) & 0x00ff00ffu); }
|
||||
static inline uint64_t rev16_64(uint64_t v) {
|
||||
return ((v & 0x00ff00ff00ff00ffull) << 8) | ((v >> 8) & 0x00ff00ff00ff00ffull);
|
||||
}
|
||||
static inline uint64_t rev32_64(uint64_t v) {
|
||||
return ((uint64_t)__builtin_bswap32((uint32_t)v)) | ((uint64_t)__builtin_bswap32((uint32_t)(v >> 32)) << 32);
|
||||
}
|
||||
static inline uint64_t udiv64(uint64_t a, uint64_t b) { return b ? a / b : 0; }
|
||||
static inline uint32_t udiv32(uint32_t a, uint32_t b) { return b ? a / b : 0; }
|
||||
static inline uint64_t sdiv64(uint64_t a, uint64_t b) {
|
||||
if (!b) return 0;
|
||||
if ((int64_t)a == INT64_MIN && (int64_t)b == -1) return a;
|
||||
return (uint64_t)((int64_t)a / (int64_t)b);
|
||||
}
|
||||
static inline uint32_t sdiv32(uint32_t a, uint32_t b) {
|
||||
if (!b) return 0;
|
||||
if ((int32_t)a == INT32_MIN && (int32_t)b == -1) return a;
|
||||
return (uint32_t)((int32_t)a / (int32_t)b);
|
||||
}
|
||||
|
||||
// Portable FP/SIMD helpers for code lifted with --simd=c (default off AArch64; gen.py includes simd.h itself
|
||||
// in that mode, so AArch64 hosts can build it too).
|
||||
#if !defined(__cplusplus) && !defined(__aarch64__)
|
||||
#include "simd.h"
|
||||
#endif
|
||||
259
runtime/discord_rpc.c
Normal file
259
runtime/discord_rpc.c
Normal file
@@ -0,0 +1,259 @@
|
||||
// Discord Rich Presence: "Playing NX2navi Recomp" with the elapsed play time in the player's Discord status, through
|
||||
// the local IPC of a running Discord client (platform.h's plat_ipc_*). Nothing leaves the machine but the activity
|
||||
// itself, and nothing is sent while NX2NAVI_DISCORD_RPC is off (the default: the status is information about the
|
||||
// player's session). NX2NAVI_DISCORD_APP_ID names the application whose name and artwork Discord shows — the default
|
||||
// is the project's own application, registered as "NX2navi Recomp"; a player who wants their own name or
|
||||
// art registers an application at discord.com/developers and puts its id in nx2navi.ini — and NX2NAVI_DISCORD_IMAGE the
|
||||
// key of its uploaded art asset.
|
||||
//
|
||||
// One host thread owns the connection and never holds a virtual core (kernel.c: host work runs without one). It
|
||||
// re-reads the settings when cfg_version changes (a menu change reaches it within half a second), connects to the
|
||||
// first of Discord's discord-ipc-0..9 pipes that answers, and writes the activity again every 60 s: that write is
|
||||
// the keep-alive, and one that fails closes the connection and tries again 15 s later. Discord clears the status by
|
||||
// itself when the process ends and the pipe closes, so there is no shutdown path. Every answer frame is read and
|
||||
// discarded: unread, they would fill the pipe's buffer and block the client's writes.
|
||||
//
|
||||
// The protocol is the documented IPC one (discord-rpc's src/rpc_connection.h): a frame is <opcode u32 little
|
||||
// endian><length u32><length bytes of JSON>, with Handshake 0, Frame 1, Close 2, Ping 3 and Pong 4. The handshake
|
||||
// (opcode 0) carries {"v":1,"client_id":...} and is answered by a Ready frame (opcode 1); every command,
|
||||
// SET_ACTIVITY here, is a frame (opcode 1) answered by one, and a Ping the client sends is echoed as a Pong. A
|
||||
// client that speaks another numbering (an early arRPC did) answers the handshake with a Close. --probe-discord
|
||||
// runs the same exchange once, without the game and without the setting being on.
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
enum { OP_HANDSHAKE = 0, OP_FRAME = 1, OP_CLOSE = 2, OP_PING = 3, OP_PONG = 4 };
|
||||
|
||||
#define APP_ID_CAP 24 // a Discord snowflake: at most 20 digits
|
||||
#define IMAGE_KEY_CAP 64
|
||||
#define ANSWER_CAP 8192 // the Ready frame carries the client's config; a few KiB
|
||||
|
||||
// A settings value that fits the JSON below without escaping: the app id is digits, an asset key letters, digits,
|
||||
// '_' and '-' (what Discord's developer portal accepts for an asset key).
|
||||
static int valid_app_id(const char *s) {
|
||||
size_t n = strlen(s);
|
||||
if (!n || n > 20) return 0;
|
||||
for (const char *p = s; *p; p++)
|
||||
if (*p < '0' || *p > '9') return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int valid_image_key(const char *s) {
|
||||
size_t n = strlen(s);
|
||||
if (!n || n >= IMAGE_KEY_CAP) return 0;
|
||||
for (const char *p = s; *p; p++)
|
||||
if ((*p < 'a' || *p > 'z') && (*p < 'A' || *p > 'Z') && (*p < '0' || *p > '9') && *p != '_' && *p != '-')
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The setting's value, or its default when the value does not fit the protocol (logged: the player sees why their
|
||||
// own app does not show). Called on the presence thread and by the probe, after cfg_version changed or once.
|
||||
static void read_setting(const char *name, char *out, size_t cap, int (*valid)(const char *)) {
|
||||
const char *v = cfg_value(name);
|
||||
if (valid(v)) {
|
||||
snprintf(out, cap, "%s", v);
|
||||
return;
|
||||
}
|
||||
snprintf(out, cap, "%s", cfg_meta(name)->def);
|
||||
rt_log("discord: %s does not fit what the Rich Presence protocol takes; using %s", name + strlen("NX2NAVI_"), out);
|
||||
}
|
||||
|
||||
static void le32(uint8_t *at, uint32_t v) {
|
||||
at[0] = (uint8_t)v, at[1] = (uint8_t)(v >> 8), at[2] = (uint8_t)(v >> 16), at[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
static int write_frame(PlatIpc *p, uint32_t op, const char *json) {
|
||||
uint8_t head[8];
|
||||
le32(head, op);
|
||||
le32(head + 4, (uint32_t)strlen(json));
|
||||
return plat_ipc_write(p, head, sizeof head) && plat_ipc_write(p, json, strlen(json));
|
||||
}
|
||||
|
||||
// One frame off the wire, its 8-byte header in as many reads as the peer made, then its payload; a payload longer
|
||||
// than cap-1 fills buf with its first cap-1 bytes and the rest is still consumed. *op gets the opcode; buf is
|
||||
// NUL-terminated. 1 on a whole frame, 0 when the deadline passed, -1 when the connection broke.
|
||||
static int read_one_frame(PlatIpc *p, uint32_t *op, char *buf, size_t cap, uint64_t deadline) {
|
||||
uint8_t head[8];
|
||||
size_t got = 0;
|
||||
while (got < sizeof head) {
|
||||
uint32_t left = deadline > plat_now_ms() ? (uint32_t)(deadline - plat_now_ms()) : 0;
|
||||
int n = plat_ipc_read(p, head + got, sizeof head - got, left);
|
||||
if (n < 0) return -1;
|
||||
if (!n) {
|
||||
if (plat_now_ms() >= deadline) return 0;
|
||||
continue;
|
||||
}
|
||||
got += (size_t)n;
|
||||
}
|
||||
*op = head[0] | head[1] << 8 | head[2] << 16 | (uint32_t)head[3] << 24;
|
||||
uint32_t len = head[4] | head[5] << 8 | head[6] << 16 | (uint32_t)head[7] << 24;
|
||||
size_t taken = 0;
|
||||
while (taken < len) {
|
||||
size_t want = len - taken > cap - 1 ? cap - 1 : len - taken;
|
||||
uint32_t left = deadline > plat_now_ms() ? (uint32_t)(deadline - plat_now_ms()) : 0;
|
||||
int n = plat_ipc_read(p, buf, want, left);
|
||||
if (n < 0) return -1;
|
||||
if (!n) {
|
||||
if (plat_now_ms() >= deadline) return 0;
|
||||
continue;
|
||||
}
|
||||
taken += (size_t)n;
|
||||
}
|
||||
buf[taken < cap - 1 ? taken : cap - 1] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The next frame that is not a Ping: a Ping the client sent to see we are alive is echoed as a Pong with its own
|
||||
// payload, a Close (the client told us it is done) counts as a broken connection so the caller connects again
|
||||
// later. Same return values as read_one_frame; the deadline covers the whole exchange.
|
||||
static int read_frame(PlatIpc *p, uint32_t *op, char *buf, size_t cap, uint32_t ms) {
|
||||
uint64_t deadline = plat_now_ms() + ms;
|
||||
for (;;) {
|
||||
int r = read_one_frame(p, op, buf, cap, deadline);
|
||||
if (r != 1) return r;
|
||||
if (*op == OP_PING) {
|
||||
uint8_t head[8];
|
||||
size_t n = strlen(buf);
|
||||
le32(head, OP_PONG);
|
||||
le32(head + 4, (uint32_t)n);
|
||||
if (!plat_ipc_write(p, head, sizeof head) || !plat_ipc_write(p, buf, n)) return -1;
|
||||
continue;
|
||||
}
|
||||
if (*op == OP_CLOSE) return -1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The first of Discord's pipes that completes a handshake. NULL when no client is running or none answered in time;
|
||||
// the caller tries again later.
|
||||
static PlatIpc *connect_client(const char *app_id) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
char name[32], json[160];
|
||||
snprintf(name, sizeof name, "discord-ipc-%d", i);
|
||||
PlatIpc *p = plat_ipc_connect(name);
|
||||
if (!p) continue;
|
||||
snprintf(json, sizeof json, "{\"v\":1,\"client_id\":\"%s\"}", app_id);
|
||||
if (!write_frame(p, OP_HANDSHAKE, json)) {
|
||||
plat_ipc_close(p);
|
||||
continue;
|
||||
}
|
||||
uint32_t op = 0;
|
||||
char answer[ANSWER_CAP];
|
||||
if (read_frame(p, &op, answer, sizeof answer, 2000) == 1 && op == OP_FRAME) return p; // the Ready event
|
||||
plat_ipc_close(p); // this pipe answered but not with a Ready frame in 2 s: the next may belong to a client
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static uint32_t next_nonce(void) {
|
||||
static volatile int32_t n;
|
||||
return (uint32_t)plat_inc32(&n);
|
||||
}
|
||||
|
||||
// The activity as one SET_ACTIVITY frame: the game update and the port version as the details line ("Game 1.4.3 ·
|
||||
// Recomp 0.3.7": the port version without its +commit suffix, the art's hover text keeps the full one; the dot goes
|
||||
// as a JSON escape), the process's start as the timestamp Discord counts the elapsed time from, and the application's
|
||||
// art asset. 1 when the client answered (a slow answer that ran out of the 2 s read is fine: the next drain takes
|
||||
// it), 0 when the connection broke.
|
||||
static int send_activity(PlatIpc *p, const char *image) {
|
||||
char json[512];
|
||||
int v = rt_game_version;
|
||||
uint64_t start = (plat_unix_time_ns() - plat_process_uptime_ns()) / 1000000000ull;
|
||||
int port_len = (int)strcspn(rt_port_version, "+");
|
||||
snprintf(json, sizeof json,
|
||||
"{\"cmd\":\"SET_ACTIVITY\",\"nonce\":\"%u\",\"args\":{\"pid\":%lu,\"activity\":{\"details\":\"Game "
|
||||
"%d.%d.%d \\u00b7 Recomp %.*s\",\"timestamps\":{\"start\":%llu},\"assets\":{\"large_image\":\"%s\","
|
||||
"\"large_text\":\"NX2navi Recomp %s\"}}}}",
|
||||
next_nonce(), (unsigned long)plat_process_id(), v / 100, v / 10 % 10, v % 10, port_len, rt_port_version,
|
||||
(unsigned long long)start, image, rt_port_version);
|
||||
if (!write_frame(p, OP_FRAME, json)) return 0;
|
||||
uint32_t op = 0;
|
||||
char answer[ANSWER_CAP];
|
||||
return read_frame(p, &op, answer, sizeof answer, 2000) >= 0;
|
||||
}
|
||||
|
||||
// ---- the presence thread
|
||||
|
||||
static void discord_main(void *arg) {
|
||||
(void)arg;
|
||||
unsigned seen = 0;
|
||||
char app_id[APP_ID_CAP], image[IMAGE_KEY_CAP];
|
||||
read_setting("NX2NAVI_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
|
||||
read_setting("NX2NAVI_DISCORD_IMAGE", image, sizeof image, valid_image_key);
|
||||
PlatIpc *ipc = NULL;
|
||||
uint64_t retry_at = 0, send_at = 0;
|
||||
int said_no_client = 0;
|
||||
for (;;) {
|
||||
unsigned v = cfg_version();
|
||||
if (v != seen) { // the settings changed (the menu writes them): pick the new ones up at once
|
||||
seen = v;
|
||||
read_setting("NX2NAVI_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
|
||||
read_setting("NX2NAVI_DISCORD_IMAGE", image, sizeof image, valid_image_key);
|
||||
if (ipc) plat_ipc_close(ipc), ipc = NULL; // the new app id needs a new handshake; the image is resent
|
||||
said_no_client = 0;
|
||||
}
|
||||
if (!cfg_bool("NX2NAVI_DISCORD_RPC")) {
|
||||
if (ipc) plat_ipc_close(ipc), ipc = NULL;
|
||||
plat_sleep_ms(500);
|
||||
continue;
|
||||
}
|
||||
if (!ipc && plat_now_ms() >= retry_at) {
|
||||
ipc = connect_client(app_id);
|
||||
if (!ipc) {
|
||||
retry_at = plat_now_ms() + 15000;
|
||||
if (!said_no_client)
|
||||
rt_log("discord: no client is running; trying again every 15 s while the setting is on");
|
||||
said_no_client = 1;
|
||||
} else {
|
||||
rt_log("discord: connected (app %s)", app_id);
|
||||
send_at = 0;
|
||||
said_no_client = 0;
|
||||
}
|
||||
}
|
||||
if (ipc && plat_now_ms() >= send_at) {
|
||||
if (send_activity(ipc, image)) {
|
||||
send_at = plat_now_ms() + 60000;
|
||||
} else {
|
||||
rt_log("discord: the connection broke; reconnecting");
|
||||
plat_ipc_close(ipc);
|
||||
ipc = NULL;
|
||||
retry_at = plat_now_ms() + 15000;
|
||||
}
|
||||
}
|
||||
plat_sleep_ms(500);
|
||||
}
|
||||
}
|
||||
|
||||
void discord_init(void) {
|
||||
if (!plat_thread_spawn(discord_main, NULL, "discord", 0)) rt_log("discord: cannot start the presence thread");
|
||||
}
|
||||
|
||||
// ---- --probe-discord: the exchange above without the game and the setting; needs no game files
|
||||
|
||||
int probe_discord(void) {
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL discord: %s\n", what); } } while (0)
|
||||
char app_id[APP_ID_CAP], image[IMAGE_KEY_CAP];
|
||||
read_setting("NX2NAVI_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
|
||||
read_setting("NX2NAVI_DISCORD_IMAGE", image, sizeof image, valid_image_key);
|
||||
PlatIpc *p = connect_client(app_id);
|
||||
if (!p) {
|
||||
fprintf(stderr, "discord: probe skipped: no client on discord-ipc-0..9\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, "discord: connected (app %s)\n", app_id);
|
||||
CHECK(send_activity(p, image), "SET_ACTIVITY written and answered");
|
||||
fprintf(stderr, "discord: activity set; the status shows it for ten seconds, then the connection closes\n");
|
||||
plat_sleep_ms(10000);
|
||||
plat_ipc_close(p);
|
||||
fprintf(stderr, "discord: probe %s\n", fails ? "FAILED" : "passed");
|
||||
return fails != 0;
|
||||
#undef CHECK
|
||||
}
|
||||
219
runtime/dispatch.c
Normal file
219
runtime/dispatch.c
Normal file
@@ -0,0 +1,219 @@
|
||||
// Indirect-branch dispatch: guest address -> lifted function, O(1).
|
||||
//
|
||||
// Layout of the table: one uint32_t slot per 4-byte guest instruction over [lo, hi), where lo is the lowest function
|
||||
// start of any module and hi is past the highest. A slot is 0 when no function starts there, else the host function's
|
||||
// address minus `anchor` (an address just below the lowest host function), so 4 bytes are enough as long as all lifted
|
||||
// code lies within 4 GiB, which a single PE image guarantees. Memory: 4 bytes per 4 guest bytes, i.e. the size of the
|
||||
// guest range covered. The gaps between code ranges (rodata, data, bss, alignment) are reserved but never written, so
|
||||
// the OS never backs them with RAM; resident memory is about the total .text size of the modules.
|
||||
//
|
||||
// The table is filled once by rt_dispatch_init and then made read-only, so any number of threads read it without
|
||||
// locks or per-thread caches. Host (HLE) functions at addresses outside the modules live in a small hash table that
|
||||
// is also fixed at init; they are looked up only after the flat table misses.
|
||||
#include "dispatch.h"
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
#define TABLE_SPAN_LIMIT (1ull << 30) // guest bytes covered by the table, and its size in bytes
|
||||
#define HOST_CAP 8192 // host function registry, power of two
|
||||
#define MAX_MODULES 16
|
||||
|
||||
// Hot data read by every rt_dispatch: one cache line.
|
||||
static struct __attribute__((aligned(64))) {
|
||||
const uint32_t *slot;
|
||||
uintptr_t anchor;
|
||||
uint64_t lo;
|
||||
uint64_t slots;
|
||||
} D;
|
||||
|
||||
static const RtModule *g_mods[MAX_MODULES]; // sorted by base
|
||||
static int g_nmods;
|
||||
static bool g_ready;
|
||||
static uint64_t g_table_bytes, g_functions;
|
||||
|
||||
static struct { uint64_t addr; GuestFn fn; } g_host[HOST_CAP];
|
||||
static uint64_t g_nhost;
|
||||
|
||||
void rt_fatal(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
rt_log_raw("FATAL: ", 7);
|
||||
rt_vlogf(fmt, ap);
|
||||
rt_log_raw("\n", 1);
|
||||
va_end(ap);
|
||||
// A failed allocation (malloc, realloc, VirtualAlloc) means the commit limit was reached: this line tells a leak of
|
||||
// ours (process private bytes near the limit) from a machine short of page file.
|
||||
double proc, used, limit;
|
||||
plat_commit_info(&proc, &used, &limit);
|
||||
rt_log("fatal: memory commit: this process %.0f MB, the system %.0f of %.0f MB", proc, used, limit);
|
||||
kern_free_cores_note();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_Exit(70);
|
||||
}
|
||||
|
||||
static const RtModule *module_at(uint64_t pc) {
|
||||
for (int i = 0; i < g_nmods; i++)
|
||||
if (pc - g_mods[i]->base < g_mods[i]->size) return g_mods[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *rt_guest_name(uint64_t pc, char *buf, int size) {
|
||||
const RtModule *m = module_at(pc);
|
||||
if (m) snprintf(buf, size, "%#llx (%s+%#llx)", (unsigned long long)pc, m->name, (unsigned long long)(pc - m->base));
|
||||
else snprintf(buf, size, "%#llx", (unsigned long long)pc);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static uint64_t host_hash(uint64_t addr) { return (addr >> 2) * 0x9E3779B97F4A7C15ull >> 51; } // 13 bits
|
||||
|
||||
static GuestFn host_lookup(uint64_t addr) {
|
||||
for (uint64_t h = host_hash(addr), n = 0; n < HOST_CAP; h = (h + 1) & (HOST_CAP - 1), n++) {
|
||||
if (g_host[h].addr == addr) return g_host[h].fn;
|
||||
if (!g_host[h].addr) return NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void rt_register_host(uint64_t addr, GuestFn fn) {
|
||||
if (g_ready) rt_fatal("rt_register_host(%#llx) after rt_dispatch_init", (unsigned long long)addr);
|
||||
if (!addr || (addr & 3) || !fn) rt_fatal("rt_register_host: bad entry %#llx", (unsigned long long)addr);
|
||||
if (g_nhost >= HOST_CAP / 2) rt_fatal("rt_register_host: more than %d host functions", HOST_CAP / 2);
|
||||
uint64_t h = host_hash(addr);
|
||||
while (g_host[h].addr && g_host[h].addr != addr) h = (h + 1) & (HOST_CAP - 1);
|
||||
if (g_host[h].addr) rt_fatal("rt_register_host: %#llx registered twice", (unsigned long long)addr);
|
||||
g_host[h].addr = addr;
|
||||
g_host[h].fn = fn;
|
||||
g_nhost++;
|
||||
}
|
||||
|
||||
static void *table_alloc(uint64_t bytes) {
|
||||
return plat_mem_alloc(bytes); // zero pages, backed on first write
|
||||
}
|
||||
|
||||
static void table_seal(void *p, uint64_t bytes) {
|
||||
if (!plat_mem_protect((uint64_t)(uintptr_t)p, bytes, PLAT_PROT_READ)) rt_fatal("dispatch: sealing the table failed");
|
||||
}
|
||||
|
||||
void rt_dispatch_init(const RtModule *mods, int count) {
|
||||
if (g_ready) rt_fatal("rt_dispatch_init called twice");
|
||||
if (count <= 0 || count > MAX_MODULES) rt_fatal("rt_dispatch_init: %d modules", count);
|
||||
|
||||
// Modules by base, checked for overlap.
|
||||
for (int i = 0; i < count; i++) {
|
||||
int j = g_nmods++;
|
||||
while (j > 0 && g_mods[j - 1]->base > mods[i].base) { g_mods[j] = g_mods[j - 1]; j--; }
|
||||
g_mods[j] = &mods[i];
|
||||
}
|
||||
for (int i = 1; i < g_nmods; i++)
|
||||
if (g_mods[i - 1]->base + g_mods[i - 1]->size > g_mods[i]->base)
|
||||
rt_fatal("dispatch: modules %s and %s overlap", g_mods[i - 1]->name, g_mods[i]->name);
|
||||
|
||||
// Function tables: sorted, aligned, inside their module. Range of guest starts and of host functions.
|
||||
uint64_t lo = UINT64_MAX, hi = 0;
|
||||
uintptr_t host_lo = UINTPTR_MAX, host_hi = 0;
|
||||
for (int i = 0; i < g_nmods; i++) {
|
||||
const RtModule *m = g_mods[i];
|
||||
for (uint64_t k = 0; k < m->count; k++) {
|
||||
uint64_t a = m->addrs[k];
|
||||
uintptr_t f = (uintptr_t)m->fns[k];
|
||||
if ((a & 3) || a - m->base >= m->size)
|
||||
rt_fatal("dispatch: %s_fn_addrs[%llu] = %#llx is misaligned or outside the module", m->name,
|
||||
(unsigned long long)k, (unsigned long long)a);
|
||||
if (k && a <= m->addrs[k - 1])
|
||||
rt_fatal("dispatch: %s_fn_addrs is not strictly ascending at [%llu] = %#llx", m->name,
|
||||
(unsigned long long)k, (unsigned long long)a);
|
||||
if (!f) rt_fatal("dispatch: %s_fn_ptrs[%llu] is NULL", m->name, (unsigned long long)k);
|
||||
if (f < host_lo) host_lo = f;
|
||||
if (f > host_hi) host_hi = f;
|
||||
}
|
||||
if (m->count) {
|
||||
if (m->addrs[0] < lo) lo = m->addrs[0];
|
||||
if (m->addrs[m->count - 1] + 4 > hi) hi = m->addrs[m->count - 1] + 4;
|
||||
}
|
||||
}
|
||||
if (hi <= lo) rt_fatal("dispatch: no functions in any module");
|
||||
if (hi - lo > TABLE_SPAN_LIMIT)
|
||||
rt_fatal("dispatch: modules span %#llx guest bytes, over the %#llx the flat table allows",
|
||||
(unsigned long long)(hi - lo), TABLE_SPAN_LIMIT);
|
||||
uintptr_t anchor = (host_lo & ~(uintptr_t)15) - 16;
|
||||
if (host_hi - anchor > UINT32_MAX)
|
||||
rt_fatal("dispatch: lifted code spans %#llx host bytes; slots hold 32-bit offsets (4 GiB)",
|
||||
(unsigned long long)(host_hi - anchor));
|
||||
|
||||
for (uint64_t h = 0; h < HOST_CAP; h++)
|
||||
if (g_host[h].addr && module_at(g_host[h].addr))
|
||||
rt_fatal("dispatch: host function at %#llx lies inside module %s", (unsigned long long)g_host[h].addr,
|
||||
module_at(g_host[h].addr)->name);
|
||||
|
||||
uint64_t slots = (hi - lo) >> 2, bytes = (slots * 4 + 0xFFFF) & ~0xFFFFull;
|
||||
uint32_t *slot = table_alloc(bytes);
|
||||
if (!slot) rt_fatal("dispatch: cannot allocate a %llu-byte table", (unsigned long long)bytes);
|
||||
for (int i = 0; i < g_nmods; i++) {
|
||||
const RtModule *m = g_mods[i];
|
||||
for (uint64_t k = 0; k < m->count; k++)
|
||||
slot[(m->addrs[k] - lo) >> 2] = (uint32_t)((uintptr_t)m->fns[k] - anchor);
|
||||
g_functions += m->count;
|
||||
}
|
||||
table_seal(slot, bytes);
|
||||
|
||||
g_table_bytes = bytes;
|
||||
D.slot = slot;
|
||||
D.anchor = anchor;
|
||||
D.lo = lo;
|
||||
D.slots = slots;
|
||||
__atomic_store_n(&g_ready, true, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
GuestFn rt_dispatch_lookup(uint64_t pc) {
|
||||
uint64_t i = __builtin_rotateright64(pc - D.lo, 2); // a misaligned pc lands far above D.slots
|
||||
if (i < D.slots && D.slot[i]) return (GuestFn)(D.anchor + D.slot[i]);
|
||||
return g_ready ? host_lookup(pc) : NULL;
|
||||
}
|
||||
|
||||
__attribute__((noinline, noreturn, cold)) static void dispatch_fail(Cpu *c) {
|
||||
char at[64], lr[64];
|
||||
uint64_t pc = c->pc;
|
||||
if (!g_ready) rt_fatal("rt_dispatch to %#llx before rt_dispatch_init", (unsigned long long)pc);
|
||||
const char *why = (pc & 3) ? "misaligned address"
|
||||
: module_at(pc) ? "not a function start"
|
||||
: "outside every module, no host function registered";
|
||||
rt_fatal("rt_dispatch: no function at guest pc %s: %s (lr %s)", rt_guest_name(pc, at, sizeof at), why,
|
||||
rt_guest_name(c->x[30], lr, sizeof lr));
|
||||
}
|
||||
|
||||
// Host functions, and the fatal error. A separate tail call keeps the fast path below free of a stack frame.
|
||||
__attribute__((noinline)) static void dispatch_slow(Cpu *c) {
|
||||
GuestFn f = g_ready ? host_lookup(c->pc) : NULL;
|
||||
if (!f) dispatch_fail(c);
|
||||
__attribute__((musttail)) return f(c);
|
||||
}
|
||||
|
||||
// A pending preempt request at an indirect call (a safe point: the guest state is all in Cpu). Out of line, so
|
||||
// rt_dispatch keeps no stack frame.
|
||||
static __attribute__((noinline)) void dispatch_preempt(Cpu *c) {
|
||||
kern_checkpoint();
|
||||
__attribute__((musttail)) return rt_dispatch(c);
|
||||
}
|
||||
|
||||
// Jump to the guest address in c->pc. Always a tail call, so chains of indirect jumps keep the host stack flat. A
|
||||
// pending preempt request costs two loads and a branch to check.
|
||||
void rt_dispatch(Cpu *c) {
|
||||
if (__builtin_expect(c->thread && kern_preempt_pending(c->thread), 0))
|
||||
__attribute__((musttail)) return dispatch_preempt(c);
|
||||
uint64_t i = __builtin_rotateright64(c->pc - D.lo, 2);
|
||||
uint32_t off = i < D.slots ? D.slot[i] : 0;
|
||||
if (__builtin_expect(!off, 0)) __attribute__((musttail)) return dispatch_slow(c);
|
||||
GuestFn f = (GuestFn)(D.anchor + off);
|
||||
__attribute__((musttail)) return f(c);
|
||||
}
|
||||
|
||||
RtDispatchStats rt_dispatch_stats(void) {
|
||||
return (RtDispatchStats){D.lo, D.lo + D.slots * 4, D.slots, g_table_bytes, g_functions, g_nhost};
|
||||
}
|
||||
53
runtime/dispatch.h
Normal file
53
runtime/dispatch.h
Normal file
@@ -0,0 +1,53 @@
|
||||
// Guest address -> host function lookup for indirect branches (rt_dispatch, declared in cpu.h).
|
||||
//
|
||||
// One flat table covers every module's code range: slot (pc - lo) >> 2 holds the host function for a guest
|
||||
// function start, as a 32-bit offset from an anchor address. Lookup is one range check and one load. The table is
|
||||
// built once by rt_dispatch_init and never written again, so threads read it without locks.
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "cpu.h"
|
||||
|
||||
// A lifted module: its place in the guest layout and the generator's sorted function table.
|
||||
typedef struct RtModule {
|
||||
const char *name;
|
||||
uint64_t base, size; // guest image (layout.h MOD_<NAME>_BASE / _SIZE)
|
||||
const uint64_t *addrs; // <mod>_fn_addrs: guest function starts, strictly ascending
|
||||
const GuestFn *fns; // <mod>_fn_ptrs: the lifted function for each start
|
||||
uint64_t count; // <mod>_fn_count
|
||||
} RtModule;
|
||||
|
||||
// The module glue that tools/build_game.py writes for each build (build/<version>/modules.c), from layout.json and
|
||||
// layout.h: every module of the layout, and the modules linked into this executable.
|
||||
typedef struct RtLayoutEntry { const char *name; uint64_t base, size; } RtLayoutEntry;
|
||||
extern const RtLayoutEntry rt_layout[];
|
||||
extern const int rt_layout_count;
|
||||
int rt_linked_modules(RtModule *out, int max);
|
||||
|
||||
// Register a host function at a guest address outside every module (HLE entry points). Allowed only before
|
||||
// rt_dispatch_init; the registry is fixed afterwards.
|
||||
void rt_register_host(uint64_t addr, GuestFn fn);
|
||||
|
||||
// Build the table from the modules (kept by pointer; they must outlive the process). Call it once, before any guest
|
||||
// thread starts: thread creation publishes the table to the threads. Fatal on a malformed table:
|
||||
// unsorted or duplicate starts, a start outside its module, overlapping modules, host code spread over 4 GiB.
|
||||
void rt_dispatch_init(const RtModule *mods, int count);
|
||||
|
||||
// The host function for a guest address, or NULL when nothing is there. Does not run guest code.
|
||||
GuestFn rt_dispatch_lookup(uint64_t pc);
|
||||
|
||||
// "0x7000001234 (main+0x1234)" for a guest address inside a module, else the bare address. Returns buf.
|
||||
const char *rt_guest_name(uint64_t pc, char *buf, int size);
|
||||
|
||||
typedef struct RtDispatchStats {
|
||||
uint64_t lo, hi; // guest range the table covers
|
||||
uint64_t slots; // (hi - lo) / 4
|
||||
uint64_t table_bytes; // reserved for the table
|
||||
uint64_t functions; // filled slots
|
||||
uint64_t host_fns; // rt_register_host entries
|
||||
} RtDispatchStats;
|
||||
RtDispatchStats rt_dispatch_stats(void);
|
||||
|
||||
// Runtime fatal error: message to stderr, then exit. Used by the dispatcher and the stubs.
|
||||
void rt_fatal(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2)));
|
||||
66
runtime/dlaa_mask_spv.h
Normal file
66
runtime/dlaa_mask_spv.h
Normal file
@@ -0,0 +1,66 @@
|
||||
// Generated by tools/make_dlaa_mask.py: DLSS's reactive mask pass (vk_dlaa.c mask_pass). Do not edit.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t dlaa_mask_spv[] = {
|
||||
0x07230203, 0x00010000, 0x00000000, 0x00000055, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000002, 0x6e69616d, 0x00000000, 0x0000001b,
|
||||
0x00060010, 0x00000002, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000001b,
|
||||
0x0000000b, 0x0000001c, 0x00040047, 0x00000018, 0x00000022, 0x00000000, 0x00040047, 0x00000018,
|
||||
0x00000021, 0x00000000, 0x00040047, 0x00000019, 0x00000022, 0x00000000, 0x00040047, 0x00000019,
|
||||
0x00000021, 0x00000001, 0x00040047, 0x0000001a, 0x00000022, 0x00000000, 0x00040047, 0x0000001a,
|
||||
0x00000021, 0x00000002, 0x00030047, 0x0000001a, 0x00000019, 0x00030047, 0x00000015, 0x00000002,
|
||||
0x00050048, 0x00000015, 0x00000000, 0x00000023, 0x00000000, 0x00020013, 0x00000003, 0x00030021,
|
||||
0x00000004, 0x00000003, 0x00020014, 0x00000005, 0x00030016, 0x00000006, 0x00000020, 0x00040015,
|
||||
0x00000007, 0x00000020, 0x00000001, 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x00040017,
|
||||
0x00000009, 0x00000006, 0x00000002, 0x00040017, 0x0000000a, 0x00000006, 0x00000003, 0x00040017,
|
||||
0x0000000b, 0x00000006, 0x00000004, 0x00040017, 0x0000000c, 0x00000007, 0x00000002, 0x00040017,
|
||||
0x0000000d, 0x00000008, 0x00000002, 0x00040017, 0x0000000e, 0x00000008, 0x00000003, 0x00090019,
|
||||
0x0000000f, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000,
|
||||
0x0003001b, 0x00000010, 0x0000000f, 0x00090019, 0x00000011, 0x00000006, 0x00000001, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000002, 0x0000000f, 0x00040020, 0x00000012, 0x00000000, 0x00000010,
|
||||
0x00040020, 0x00000013, 0x00000000, 0x00000011, 0x00040020, 0x00000014, 0x00000001, 0x0000000e,
|
||||
0x0003001e, 0x00000015, 0x0000000b, 0x00040020, 0x00000016, 0x00000009, 0x00000015, 0x00040020,
|
||||
0x00000017, 0x00000009, 0x0000000b, 0x0004002b, 0x00000007, 0x0000001d, 0x00000000, 0x0004002b,
|
||||
0x00000006, 0x0000001e, 0x00000000, 0x0004002b, 0x00000006, 0x0000001f, 0x3f800000, 0x0004002b,
|
||||
0x00000006, 0x00000020, 0x38d1b717, 0x0004003b, 0x00000012, 0x00000018, 0x00000000, 0x0004003b,
|
||||
0x00000012, 0x00000019, 0x00000000, 0x0004003b, 0x00000013, 0x0000001a, 0x00000000, 0x0004003b,
|
||||
0x00000014, 0x0000001b, 0x00000001, 0x0004003b, 0x00000016, 0x0000001c, 0x00000009, 0x00050036,
|
||||
0x00000003, 0x00000002, 0x00000000, 0x00000004, 0x000200f8, 0x00000021, 0x00050041, 0x00000017,
|
||||
0x00000024, 0x0000001c, 0x0000001d, 0x0004003d, 0x0000000b, 0x00000025, 0x00000024, 0x0004003d,
|
||||
0x0000000e, 0x00000026, 0x0000001b, 0x0007004f, 0x0000000d, 0x00000027, 0x00000026, 0x00000026,
|
||||
0x00000000, 0x00000001, 0x00050051, 0x00000008, 0x00000028, 0x00000026, 0x00000000, 0x00040070,
|
||||
0x00000006, 0x00000029, 0x00000028, 0x00050051, 0x00000008, 0x0000002a, 0x00000026, 0x00000001,
|
||||
0x00040070, 0x00000006, 0x0000002b, 0x0000002a, 0x00050051, 0x00000006, 0x0000002c, 0x00000025,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x0000002d, 0x00000025, 0x00000001, 0x000500b8, 0x00000005,
|
||||
0x0000002e, 0x00000029, 0x0000002c, 0x000500b8, 0x00000005, 0x0000002f, 0x0000002b, 0x0000002d,
|
||||
0x000500a7, 0x00000005, 0x00000030, 0x0000002e, 0x0000002f, 0x000300f7, 0x00000023, 0x00000000,
|
||||
0x000400fa, 0x00000030, 0x00000022, 0x00000023, 0x000200f8, 0x00000022, 0x0004007c, 0x0000000c,
|
||||
0x00000031, 0x00000027, 0x0004003d, 0x00000010, 0x00000032, 0x00000018, 0x00040064, 0x0000000f,
|
||||
0x00000033, 0x00000032, 0x0007005f, 0x0000000b, 0x00000034, 0x00000033, 0x00000031, 0x00000002,
|
||||
0x0000001d, 0x0008004f, 0x0000000a, 0x00000035, 0x00000034, 0x00000034, 0x00000000, 0x00000001,
|
||||
0x00000002, 0x0004003d, 0x00000010, 0x00000036, 0x00000019, 0x00040064, 0x0000000f, 0x00000037,
|
||||
0x00000036, 0x0007005f, 0x0000000b, 0x00000038, 0x00000037, 0x00000031, 0x00000002, 0x0000001d,
|
||||
0x0008004f, 0x0000000a, 0x00000039, 0x00000038, 0x00000038, 0x00000000, 0x00000001, 0x00000002,
|
||||
0x00050083, 0x0000000a, 0x0000003a, 0x00000039, 0x00000035, 0x0006000c, 0x0000000a, 0x0000003b,
|
||||
0x00000001, 0x00000004, 0x0000003a, 0x00050051, 0x00000006, 0x0000003c, 0x0000003b, 0x00000000,
|
||||
0x00050051, 0x00000006, 0x0000003d, 0x0000003b, 0x00000001, 0x00050051, 0x00000006, 0x0000003e,
|
||||
0x0000003b, 0x00000002, 0x0007000c, 0x00000006, 0x0000003f, 0x00000001, 0x00000028, 0x0000003c,
|
||||
0x0000003d, 0x0007000c, 0x00000006, 0x00000040, 0x00000001, 0x00000028, 0x0000003f, 0x0000003e,
|
||||
0x00050051, 0x00000006, 0x00000041, 0x00000039, 0x00000000, 0x00050051, 0x00000006, 0x00000042,
|
||||
0x00000039, 0x00000001, 0x00050051, 0x00000006, 0x00000043, 0x00000039, 0x00000002, 0x0007000c,
|
||||
0x00000006, 0x00000044, 0x00000001, 0x00000028, 0x00000041, 0x00000042, 0x0007000c, 0x00000006,
|
||||
0x00000045, 0x00000001, 0x00000028, 0x00000044, 0x00000043, 0x00050051, 0x00000006, 0x00000046,
|
||||
0x00000035, 0x00000000, 0x00050051, 0x00000006, 0x00000047, 0x00000035, 0x00000001, 0x00050051,
|
||||
0x00000006, 0x00000048, 0x00000035, 0x00000002, 0x0007000c, 0x00000006, 0x00000049, 0x00000001,
|
||||
0x00000028, 0x00000046, 0x00000047, 0x0007000c, 0x00000006, 0x0000004a, 0x00000001, 0x00000028,
|
||||
0x00000049, 0x00000048, 0x0007000c, 0x00000006, 0x0000004b, 0x00000001, 0x00000028, 0x00000045,
|
||||
0x0000004a, 0x0007000c, 0x00000006, 0x0000004c, 0x00000001, 0x00000028, 0x0000004b, 0x00000020,
|
||||
0x00050088, 0x00000006, 0x0000004d, 0x00000040, 0x0000004c, 0x00050051, 0x00000006, 0x0000004e,
|
||||
0x00000025, 0x00000002, 0x00050051, 0x00000006, 0x0000004f, 0x00000025, 0x00000003, 0x00050083,
|
||||
0x00000006, 0x00000050, 0x0000004d, 0x0000004e, 0x00050085, 0x00000006, 0x00000051, 0x00000050,
|
||||
0x0000004f, 0x0008000c, 0x00000006, 0x00000052, 0x00000001, 0x0000002b, 0x00000051, 0x0000001e,
|
||||
0x0000001f, 0x00070050, 0x0000000b, 0x00000053, 0x00000052, 0x00000052, 0x00000052, 0x00000052,
|
||||
0x0004003d, 0x00000011, 0x00000054, 0x0000001a, 0x00040063, 0x00000054, 0x00000031, 0x00000053,
|
||||
0x000200f9, 0x00000023, 0x000200f8, 0x00000023, 0x000100fd, 0x00010038
|
||||
};
|
||||
162
runtime/dlaa_mv_spv.h
Normal file
162
runtime/dlaa_mv_spv.h
Normal file
@@ -0,0 +1,162 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/dlaa_mv.comp; do not edit.
|
||||
// source sha256 e96a6d2d0cec26c47a76280363077c5982919bedc984f20ae9f1d185dc6ae674
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t dlaa_mv_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x0000019a, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
|
||||
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
|
||||
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000036,
|
||||
0x00060010, 0x00000004, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x00000036,
|
||||
0x0000000b, 0x0000001c, 0x00030047, 0x00000041, 0x00000002, 0x00050048, 0x00000041, 0x00000000,
|
||||
0x00000023, 0x00000000, 0x00050048, 0x00000041, 0x00000001, 0x00000023, 0x00000008, 0x00050048,
|
||||
0x00000041, 0x00000002, 0x00000023, 0x00000010, 0x00050048, 0x00000041, 0x00000003, 0x00000023,
|
||||
0x00000014, 0x00040047, 0x0000005d, 0x00000021, 0x00000000, 0x00040047, 0x0000005d, 0x00000022,
|
||||
0x00000000, 0x00040047, 0x00000086, 0x00000006, 0x00000010, 0x00030047, 0x00000087, 0x00000002,
|
||||
0x00050048, 0x00000087, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x00000089, 0x00000021,
|
||||
0x00000002, 0x00040047, 0x00000089, 0x00000022, 0x00000000, 0x00040047, 0x0000009d, 0x00000006,
|
||||
0x00000010, 0x00030047, 0x0000009e, 0x00000002, 0x00050048, 0x0000009e, 0x00000000, 0x00000023,
|
||||
0x00000000, 0x00040047, 0x000000a0, 0x00000021, 0x00000003, 0x00040047, 0x000000a0, 0x00000022,
|
||||
0x00000000, 0x00030047, 0x000000e7, 0x00000019, 0x00040047, 0x000000e7, 0x00000021, 0x00000001,
|
||||
0x00040047, 0x000000e7, 0x00000022, 0x00000000, 0x00040047, 0x000000f6, 0x00000021, 0x00000004,
|
||||
0x00040047, 0x000000f6, 0x00000022, 0x00000000, 0x00030047, 0x00000101, 0x00000019, 0x00040047,
|
||||
0x00000101, 0x00000021, 0x00000007, 0x00040047, 0x00000101, 0x00000022, 0x00000000, 0x00030047,
|
||||
0x0000012a, 0x00000018, 0x00040047, 0x0000012a, 0x00000021, 0x00000006, 0x00040047, 0x0000012a,
|
||||
0x00000022, 0x00000000, 0x00030047, 0x0000013b, 0x00000019, 0x00040047, 0x0000013b, 0x00000021,
|
||||
0x00000005, 0x00040047, 0x0000013b, 0x00000022, 0x00000000, 0x00040047, 0x00000146, 0x0000000b,
|
||||
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006,
|
||||
0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040018, 0x00000009, 0x00000007,
|
||||
0x00000004, 0x0004002b, 0x00000006, 0x00000015, 0x3f800000, 0x0004002b, 0x00000006, 0x00000016,
|
||||
0x00000000, 0x00040015, 0x0000002f, 0x00000020, 0x00000001, 0x00040017, 0x00000030, 0x0000002f,
|
||||
0x00000002, 0x00040015, 0x00000033, 0x00000020, 0x00000000, 0x00040017, 0x00000034, 0x00000033,
|
||||
0x00000003, 0x00040020, 0x00000035, 0x00000001, 0x00000034, 0x0004003b, 0x00000035, 0x00000036,
|
||||
0x00000001, 0x00040017, 0x00000037, 0x00000033, 0x00000002, 0x00020014, 0x0000003b, 0x0004002b,
|
||||
0x00000033, 0x0000003c, 0x00000000, 0x00040017, 0x00000040, 0x00000006, 0x00000002, 0x0006001e,
|
||||
0x00000041, 0x00000040, 0x00000040, 0x00000006, 0x00000006, 0x00040020, 0x00000042, 0x00000009,
|
||||
0x00000041, 0x0004003b, 0x00000042, 0x00000043, 0x00000009, 0x0004002b, 0x0000002f, 0x00000044,
|
||||
0x00000000, 0x00040020, 0x00000045, 0x00000009, 0x00000006, 0x0004002b, 0x00000033, 0x0000004d,
|
||||
0x00000001, 0x00090019, 0x0000005a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000001, 0x00000000, 0x0003001b, 0x0000005b, 0x0000005a, 0x00040020, 0x0000005c, 0x00000000,
|
||||
0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d, 0x00000000, 0x0004002b, 0x00000006, 0x00000067,
|
||||
0x3f000000, 0x0004002b, 0x0000002f, 0x0000006a, 0x00000001, 0x00040020, 0x0000006b, 0x00000009,
|
||||
0x00000040, 0x0004002b, 0x00000006, 0x00000075, 0x40000000, 0x0004002b, 0x00000033, 0x00000085,
|
||||
0x00000010, 0x0004001c, 0x00000086, 0x00000007, 0x00000085, 0x0003001e, 0x00000087, 0x00000086,
|
||||
0x00040020, 0x00000088, 0x00000002, 0x00000087, 0x0004003b, 0x00000088, 0x00000089, 0x00000002,
|
||||
0x0004002b, 0x0000002f, 0x0000008a, 0x00000003, 0x0004002b, 0x0000002f, 0x0000008b, 0x00000004,
|
||||
0x0004002b, 0x0000002f, 0x0000008c, 0x00000005, 0x0004002b, 0x0000002f, 0x0000008d, 0x00000006,
|
||||
0x00040020, 0x0000008f, 0x00000002, 0x00000007, 0x0004001c, 0x0000009d, 0x00000007, 0x00000085,
|
||||
0x0003001e, 0x0000009e, 0x0000009d, 0x00040020, 0x0000009f, 0x00000002, 0x0000009e, 0x0004003b,
|
||||
0x0000009f, 0x000000a0, 0x00000002, 0x0005002c, 0x00000040, 0x000000b4, 0x00000016, 0x00000016,
|
||||
0x0004002b, 0x00000006, 0x000000b9, 0x2b8cbccc, 0x0004002b, 0x00000006, 0x000000c7, 0x358637bd,
|
||||
0x00090019, 0x000000e5, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
|
||||
0x00000007, 0x00040020, 0x000000e6, 0x00000000, 0x000000e5, 0x0004003b, 0x000000e6, 0x000000e7,
|
||||
0x00000000, 0x0004002b, 0x0000002f, 0x000000ee, 0x00000002, 0x0004003b, 0x0000005c, 0x000000f6,
|
||||
0x00000000, 0x0004002b, 0x00000006, 0x000000fc, 0x3e808081, 0x00090019, 0x000000ff, 0x00000006,
|
||||
0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002, 0x0000000f, 0x00040020, 0x00000100,
|
||||
0x00000000, 0x000000ff, 0x0004003b, 0x00000100, 0x00000101, 0x00000000, 0x0004002b, 0x0000002f,
|
||||
0x00000119, 0xffffffff, 0x0004003b, 0x00000100, 0x0000012a, 0x00000000, 0x0005002c, 0x00000030,
|
||||
0x00000131, 0x00000044, 0x00000044, 0x0004003b, 0x00000100, 0x0000013b, 0x00000000, 0x0004002b,
|
||||
0x00000033, 0x00000145, 0x00000008, 0x0006002c, 0x00000034, 0x00000146, 0x00000145, 0x00000145,
|
||||
0x0000004d, 0x0005002c, 0x00000040, 0x00000197, 0x00000067, 0x00000067, 0x0005002c, 0x00000030,
|
||||
0x00000199, 0x0000006a, 0x0000006a, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
|
||||
0x000200f8, 0x00000005, 0x000300f7, 0x00000147, 0x00000000, 0x000300fb, 0x0000003c, 0x00000148,
|
||||
0x000200f8, 0x00000148, 0x0004003d, 0x00000034, 0x00000038, 0x00000036, 0x0007004f, 0x00000037,
|
||||
0x00000039, 0x00000038, 0x00000038, 0x00000000, 0x00000001, 0x0004007c, 0x00000030, 0x0000003a,
|
||||
0x00000039, 0x00050051, 0x0000002f, 0x0000003f, 0x0000003a, 0x00000000, 0x00060041, 0x00000045,
|
||||
0x00000046, 0x00000043, 0x00000044, 0x0000003c, 0x0004003d, 0x00000006, 0x00000047, 0x00000046,
|
||||
0x0004006e, 0x0000002f, 0x00000048, 0x00000047, 0x000500af, 0x0000003b, 0x00000049, 0x0000003f,
|
||||
0x00000048, 0x000400a8, 0x0000003b, 0x0000004a, 0x00000049, 0x000300f7, 0x0000004c, 0x00000000,
|
||||
0x000400fa, 0x0000004a, 0x0000004b, 0x0000004c, 0x000200f8, 0x0000004b, 0x00050051, 0x0000002f,
|
||||
0x0000004f, 0x0000003a, 0x00000001, 0x00060041, 0x00000045, 0x00000050, 0x00000043, 0x00000044,
|
||||
0x0000004d, 0x0004003d, 0x00000006, 0x00000051, 0x00000050, 0x0004006e, 0x0000002f, 0x00000052,
|
||||
0x00000051, 0x000500af, 0x0000003b, 0x00000053, 0x0000004f, 0x00000052, 0x000200f9, 0x0000004c,
|
||||
0x000200f8, 0x0000004c, 0x000700f5, 0x0000003b, 0x00000054, 0x00000049, 0x00000148, 0x00000053,
|
||||
0x0000004b, 0x000300f7, 0x00000056, 0x00000000, 0x000400fa, 0x00000054, 0x00000055, 0x00000056,
|
||||
0x000200f8, 0x00000055, 0x000200f9, 0x00000147, 0x000200f8, 0x00000056, 0x0004003d, 0x0000005b,
|
||||
0x0000005e, 0x0000005d, 0x00040064, 0x0000005a, 0x00000060, 0x0000005e, 0x0007005f, 0x00000007,
|
||||
0x00000061, 0x00000060, 0x0000003a, 0x00000002, 0x00000044, 0x00050051, 0x00000006, 0x00000062,
|
||||
0x00000061, 0x00000000, 0x0004006f, 0x00000040, 0x00000066, 0x0000003a, 0x00050081, 0x00000040,
|
||||
0x00000069, 0x00000066, 0x00000197, 0x00050041, 0x0000006b, 0x0000006c, 0x00000043, 0x0000006a,
|
||||
0x0004003d, 0x00000040, 0x0000006d, 0x0000006c, 0x00050083, 0x00000040, 0x0000006e, 0x00000069,
|
||||
0x0000006d, 0x00050051, 0x00000006, 0x00000071, 0x0000006e, 0x00000000, 0x00050088, 0x00000006,
|
||||
0x00000074, 0x00000071, 0x00000047, 0x00050085, 0x00000006, 0x00000076, 0x00000074, 0x00000075,
|
||||
0x00050083, 0x00000006, 0x00000077, 0x00000076, 0x00000015, 0x00050051, 0x00000006, 0x00000079,
|
||||
0x0000006e, 0x00000001, 0x00060041, 0x00000045, 0x0000007a, 0x00000043, 0x00000044, 0x0000004d,
|
||||
0x0004003d, 0x00000006, 0x0000007b, 0x0000007a, 0x00050088, 0x00000006, 0x0000007c, 0x00000079,
|
||||
0x0000007b, 0x00050085, 0x00000006, 0x0000007d, 0x0000007c, 0x00000075, 0x00050083, 0x00000006,
|
||||
0x0000007e, 0x00000015, 0x0000007d, 0x00050085, 0x00000006, 0x00000080, 0x00000062, 0x00000075,
|
||||
0x00050083, 0x00000006, 0x00000081, 0x00000080, 0x00000015, 0x00070050, 0x00000007, 0x00000082,
|
||||
0x00000077, 0x0000007e, 0x00000081, 0x00000015, 0x00060041, 0x0000008f, 0x00000090, 0x00000089,
|
||||
0x00000044, 0x0000008a, 0x0004003d, 0x00000007, 0x00000091, 0x00000090, 0x00060041, 0x0000008f,
|
||||
0x00000093, 0x00000089, 0x00000044, 0x0000008b, 0x0004003d, 0x00000007, 0x00000094, 0x00000093,
|
||||
0x00060041, 0x0000008f, 0x00000096, 0x00000089, 0x00000044, 0x0000008c, 0x0004003d, 0x00000007,
|
||||
0x00000097, 0x00000096, 0x00060041, 0x0000008f, 0x00000099, 0x00000089, 0x00000044, 0x0000008d,
|
||||
0x0004003d, 0x00000007, 0x0000009a, 0x00000099, 0x00070050, 0x00000009, 0x00000167, 0x00000091,
|
||||
0x00000094, 0x00000097, 0x0000009a, 0x00040054, 0x00000009, 0x00000168, 0x00000167, 0x00060041,
|
||||
0x0000008f, 0x000000a2, 0x000000a0, 0x00000044, 0x0000008a, 0x0004003d, 0x00000007, 0x000000a3,
|
||||
0x000000a2, 0x00060041, 0x0000008f, 0x000000a5, 0x000000a0, 0x00000044, 0x0000008b, 0x0004003d,
|
||||
0x00000007, 0x000000a6, 0x000000a5, 0x00060041, 0x0000008f, 0x000000a8, 0x000000a0, 0x00000044,
|
||||
0x0000008c, 0x0004003d, 0x00000007, 0x000000a9, 0x000000a8, 0x00060041, 0x0000008f, 0x000000ab,
|
||||
0x000000a0, 0x00000044, 0x0000008d, 0x0004003d, 0x00000007, 0x000000ac, 0x000000ab, 0x00070050,
|
||||
0x00000009, 0x00000183, 0x000000a3, 0x000000a6, 0x000000a9, 0x000000ac, 0x00040054, 0x00000009,
|
||||
0x00000184, 0x00000183, 0x0006000c, 0x00000009, 0x000000b0, 0x00000001, 0x00000022, 0x00000168,
|
||||
0x00050091, 0x00000007, 0x000000b2, 0x000000b0, 0x00000082, 0x00050051, 0x00000006, 0x000000b7,
|
||||
0x000000b2, 0x00000003, 0x0006000c, 0x00000006, 0x000000b8, 0x00000001, 0x00000004, 0x000000b7,
|
||||
0x000500ba, 0x0000003b, 0x000000ba, 0x000000b8, 0x000000b9, 0x000300f7, 0x000000bc, 0x00000000,
|
||||
0x000400fa, 0x000000ba, 0x000000bb, 0x000000bc, 0x000200f8, 0x000000bb, 0x00070050, 0x00000007,
|
||||
0x000000c2, 0x000000b7, 0x000000b7, 0x000000b7, 0x000000b7, 0x00050088, 0x00000007, 0x000000c3,
|
||||
0x000000b2, 0x000000c2, 0x00050091, 0x00000007, 0x000000c4, 0x00000184, 0x000000c3, 0x00050051,
|
||||
0x00000006, 0x000000c6, 0x000000c4, 0x00000003, 0x000500ba, 0x0000003b, 0x000000c8, 0x000000c6,
|
||||
0x000000c7, 0x000300f7, 0x000000ca, 0x00000000, 0x000400fa, 0x000000c8, 0x000000c9, 0x000000ca,
|
||||
0x000200f8, 0x000000c9, 0x0007004f, 0x00000040, 0x000000cd, 0x000000c4, 0x000000c4, 0x00000000,
|
||||
0x00000001, 0x00050050, 0x00000040, 0x000000d0, 0x000000c6, 0x000000c6, 0x00050088, 0x00000040,
|
||||
0x000000d1, 0x000000cd, 0x000000d0, 0x00050051, 0x00000006, 0x000000d4, 0x000000d1, 0x00000000,
|
||||
0x00050085, 0x00000006, 0x000000d5, 0x000000d4, 0x00000067, 0x00050081, 0x00000006, 0x000000d6,
|
||||
0x000000d5, 0x00000067, 0x00050085, 0x00000006, 0x000000d9, 0x000000d6, 0x00000047, 0x00050051,
|
||||
0x00000006, 0x000000db, 0x000000d1, 0x00000001, 0x00050085, 0x00000006, 0x000000dc, 0x000000db,
|
||||
0x00000067, 0x00050083, 0x00000006, 0x000000dd, 0x00000067, 0x000000dc, 0x00050085, 0x00000006,
|
||||
0x000000e0, 0x000000dd, 0x0000007b, 0x00050050, 0x00000040, 0x000000e1, 0x000000d9, 0x000000e0,
|
||||
0x00050083, 0x00000040, 0x000000e4, 0x000000e1, 0x0000006e, 0x000200f9, 0x000000ca, 0x000200f8,
|
||||
0x000000ca, 0x000700f5, 0x00000040, 0x00000190, 0x000000b4, 0x000000bb, 0x000000e4, 0x000000c9,
|
||||
0x000200f9, 0x000000bc, 0x000200f8, 0x000000bc, 0x000700f5, 0x00000040, 0x0000018f, 0x000000b4,
|
||||
0x00000056, 0x00000190, 0x000000ca, 0x0004003d, 0x000000e5, 0x000000e8, 0x000000e7, 0x00050051,
|
||||
0x00000006, 0x000000eb, 0x0000018f, 0x00000000, 0x00050051, 0x00000006, 0x000000ec, 0x0000018f,
|
||||
0x00000001, 0x00070050, 0x00000007, 0x000000ed, 0x000000eb, 0x000000ec, 0x00000016, 0x00000016,
|
||||
0x00040063, 0x000000e8, 0x0000003a, 0x000000ed, 0x00050041, 0x00000045, 0x000000ef, 0x00000043,
|
||||
0x000000ee, 0x0004003d, 0x00000006, 0x000000f0, 0x000000ef, 0x000500bc, 0x0000003b, 0x000000f1,
|
||||
0x000000f0, 0x00000016, 0x000300f7, 0x000000f3, 0x00000000, 0x000400fa, 0x000000f1, 0x000000f2,
|
||||
0x000000f3, 0x000200f8, 0x000000f2, 0x000200f9, 0x00000147, 0x000200f8, 0x000000f3, 0x0004003d,
|
||||
0x0000005b, 0x000000f7, 0x000000f6, 0x00040064, 0x0000005a, 0x000000f9, 0x000000f7, 0x0007005f,
|
||||
0x00000007, 0x000000fa, 0x000000f9, 0x0000003a, 0x00000002, 0x00000044, 0x00050051, 0x00000006,
|
||||
0x000000fb, 0x000000fa, 0x00000001, 0x000500ba, 0x0000003b, 0x000000fd, 0x000000fb, 0x000000fc,
|
||||
0x000600a9, 0x00000006, 0x000000fe, 0x000000fd, 0x00000015, 0x00000016, 0x0004003d, 0x000000ff,
|
||||
0x00000102, 0x00000101, 0x00070050, 0x00000007, 0x00000105, 0x000000fe, 0x000000fe, 0x000000fe,
|
||||
0x000000fe, 0x00040063, 0x00000102, 0x0000003a, 0x00000105, 0x00050041, 0x00000045, 0x00000107,
|
||||
0x00000043, 0x0000008a, 0x0004003d, 0x00000006, 0x00000108, 0x00000107, 0x000500ba, 0x0000003b,
|
||||
0x00000109, 0x00000108, 0x00000067, 0x000300f7, 0x0000010b, 0x00000000, 0x000400fa, 0x00000109,
|
||||
0x0000010a, 0x0000010b, 0x000200f8, 0x0000010a, 0x00050081, 0x00000040, 0x0000010f, 0x0000006e,
|
||||
0x0000018f, 0x0006000c, 0x00000040, 0x00000110, 0x00000001, 0x00000008, 0x0000010f, 0x0004006e,
|
||||
0x00000030, 0x00000111, 0x00000110, 0x00050041, 0x0000006b, 0x00000113, 0x00000043, 0x00000044,
|
||||
0x0004003d, 0x00000040, 0x00000114, 0x00000113, 0x0004006e, 0x00000030, 0x00000115, 0x00000114,
|
||||
0x00050082, 0x00000030, 0x00000117, 0x00000115, 0x00000199, 0x000200f9, 0x0000011a, 0x000200f8,
|
||||
0x0000011a, 0x000700f5, 0x00000006, 0x00000193, 0x00000016, 0x0000010a, 0x00000196, 0x0000011d,
|
||||
0x000700f5, 0x0000002f, 0x00000191, 0x00000119, 0x0000010a, 0x0000013a, 0x0000011d, 0x000500b3,
|
||||
0x0000003b, 0x00000120, 0x00000191, 0x0000006a, 0x000400f6, 0x0000011c, 0x0000011d, 0x00000000,
|
||||
0x000400fa, 0x00000120, 0x0000011b, 0x0000011c, 0x000200f8, 0x0000011b, 0x000200f9, 0x00000122,
|
||||
0x000200f8, 0x00000122, 0x000700f5, 0x00000006, 0x00000196, 0x00000193, 0x0000011b, 0x00000136,
|
||||
0x00000123, 0x000700f5, 0x0000002f, 0x00000194, 0x00000119, 0x0000011b, 0x00000138, 0x00000123,
|
||||
0x000500b3, 0x0000003b, 0x00000128, 0x00000194, 0x0000006a, 0x000400f6, 0x00000124, 0x00000123,
|
||||
0x00000000, 0x000400fa, 0x00000128, 0x00000123, 0x00000124, 0x000200f8, 0x00000123, 0x0004003d,
|
||||
0x000000ff, 0x0000012b, 0x0000012a, 0x00050050, 0x00000030, 0x0000012f, 0x00000194, 0x00000191,
|
||||
0x00050080, 0x00000030, 0x00000130, 0x00000111, 0x0000012f, 0x0008000c, 0x00000030, 0x00000133,
|
||||
0x00000001, 0x0000002d, 0x00000130, 0x00000131, 0x00000117, 0x00050062, 0x00000007, 0x00000134,
|
||||
0x0000012b, 0x00000133, 0x00050051, 0x00000006, 0x00000135, 0x00000134, 0x00000000, 0x0007000c,
|
||||
0x00000006, 0x00000136, 0x00000001, 0x00000028, 0x00000196, 0x00000135, 0x00050080, 0x0000002f,
|
||||
0x00000138, 0x00000194, 0x0000006a, 0x000200f9, 0x00000122, 0x000200f8, 0x00000124, 0x000200f9,
|
||||
0x0000011d, 0x000200f8, 0x0000011d, 0x00050080, 0x0000002f, 0x0000013a, 0x00000191, 0x0000006a,
|
||||
0x000200f9, 0x0000011a, 0x000200f8, 0x0000011c, 0x000200f9, 0x0000010b, 0x000200f8, 0x0000010b,
|
||||
0x000700f5, 0x00000006, 0x00000192, 0x00000016, 0x000000f3, 0x00000193, 0x0000011c, 0x0004003d,
|
||||
0x000000ff, 0x0000013c, 0x0000013b, 0x0007000c, 0x00000006, 0x00000140, 0x00000001, 0x00000028,
|
||||
0x000000fe, 0x00000192, 0x00050085, 0x00000006, 0x00000143, 0x00000140, 0x000000f0, 0x00070050,
|
||||
0x00000007, 0x00000144, 0x00000143, 0x00000143, 0x00000143, 0x00000143, 0x00040063, 0x0000013c,
|
||||
0x0000003a, 0x00000144, 0x000200f9, 0x00000147, 0x000200f8, 0x00000147, 0x000100fd, 0x00010038,
|
||||
};
|
||||
33
runtime/dpi_probe_win.c
Normal file
33
runtime/dpi_probe_win.c
Normal file
@@ -0,0 +1,33 @@
|
||||
// --probe-dpi: the process DPI awareness that nx2navi.manifest declares, and what the primary monitor reports under it.
|
||||
// A DPI-unaware process gets every window and monitor size scaled down by the display scale and its output stretched
|
||||
// back up by Windows; the probe fails unless the process is per-monitor aware (v2). Needs no game files and no GPU.
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "host.h"
|
||||
|
||||
static const char *awareness_name(DPI_AWARENESS_CONTEXT ctx) {
|
||||
if (AreDpiAwarenessContextsEqual(ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) return "per-monitor aware v2";
|
||||
switch (GetAwarenessFromDpiAwarenessContext(ctx)) {
|
||||
case DPI_AWARENESS_UNAWARE: return "unaware";
|
||||
case DPI_AWARENESS_SYSTEM_AWARE: return "system aware";
|
||||
case DPI_AWARENESS_PER_MONITOR_AWARE: return "per-monitor aware v1";
|
||||
default: return "invalid";
|
||||
}
|
||||
}
|
||||
|
||||
int probe_dpi(void) {
|
||||
DPI_AWARENESS_CONTEXT ctx = GetThreadDpiAwarenessContext();
|
||||
MONITORINFO mi = {sizeof mi};
|
||||
HMONITOR mon = MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
|
||||
int have = GetMonitorInfoW(mon, &mi);
|
||||
printf("dpi: awareness %s\n", awareness_name(ctx));
|
||||
printf("dpi: system dpi %u (scale %u%%)\n", GetDpiForSystem(), GetDpiForSystem() * 100 / 96);
|
||||
if (have)
|
||||
printf("dpi: primary monitor %ldx%ld at %ld,%ld; screen metrics %dx%d\n", mi.rcMonitor.right - mi.rcMonitor.left,
|
||||
mi.rcMonitor.bottom - mi.rcMonitor.top, mi.rcMonitor.left, mi.rcMonitor.top, GetSystemMetrics(SM_CXSCREEN),
|
||||
GetSystemMetrics(SM_CYSCREEN));
|
||||
int ok = AreDpiAwarenessContextsEqual(ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) && have;
|
||||
printf("probe-dpi: %s\n", ok ? "PASS" : "FAIL (the process is not per-monitor aware v2)");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
368
runtime/flight.c
Normal file
368
runtime/flight.c
Normal file
@@ -0,0 +1,368 @@
|
||||
// NX2NAVI_FLIGHT=1 flight recorder: a ring of the last NX2NAVI_FLIGHT_FRAMES (600) presents, one record each (interval,
|
||||
// render-thread blocked time per perf kind, event counts) and a 320x180 thumbnail each (gfx_present.c, blitted on the
|
||||
// GPU by the present thread). A dump goes to <dir>/<YYYYMMDD-HHMMSS>-f<frame>/: frames.tsv and f<frame>.png per
|
||||
// thumbnail, written by a background thread. Triggers: F8 while the game window has focus, a frame interval over
|
||||
// NX2NAVI_FLIGHT_SPIKE_MS (100; 0 off; at most one dump per 10 s), and NX2NAVI_FLIGHT_AT=<frame>[,<frame>...]. A dump holds
|
||||
// the frames up to FLIGHT_POST presents after the trigger, so the aftermath is in it too. tools/flight_view.py reads it.
|
||||
//
|
||||
// Records come from perf_present (prof_win.c) on the render thread; the counts are differences of the NX2NAVI_TRACE=perf
|
||||
// running totals and of vk_exec_frame. Off, the only cost is the flight_on branch in perf_present and vk_backend.c.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "flight.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "input.h"
|
||||
#include "vk.h"
|
||||
#include "vk_exec.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define FLIGHT_POST 30 // presents recorded after the trigger before the dump starts
|
||||
#define FLIGHT_SPIKE_GAP_NS 10000000000ull
|
||||
|
||||
// Record columns after frame, time and interval: blocked time per PERF_* kind (us), then the counts.
|
||||
enum { C_SUBMITS, C_PASSES, C_DRAWS, C_DRAWS_SEEN, C_CLEARS, C_COPIES, C_DISPATCHES, C_XLATED, C_PERF0, C_N = C_PERF0 + PERF_CNT_N };
|
||||
static const char *const blocked_names[PERF_N] = {"pace_ms", "fence_ms", "drain_ms", "pipe_ms", "xlate_ms", "tex_ms", "astc_ms"};
|
||||
static const char *const count_names[C_N] = {
|
||||
"submits", "passes", "draws", "draws_seen", "clears", "copies", "dispatches", "shaders_xlated",
|
||||
// PERF_CNT_* order (host.h)
|
||||
"pipe_bg_used", "draws_skip_pipe", "tex_up", "tex_reup", "draws_skip_shader", "pipe_waited", "oneshot_waited",
|
||||
"pipe_built", "tex_new", "tex_alias", "wb_done", "wb_skip"};
|
||||
_Static_assert(PERF_CNT_N == 12, "count_names follows the PERF_CNT_* list");
|
||||
|
||||
typedef struct {
|
||||
uint64_t frame, t_ns;
|
||||
uint32_t interval_us, blocked_us[PERF_N], n[C_N];
|
||||
} Rec;
|
||||
|
||||
typedef struct {
|
||||
int n; // records, oldest first
|
||||
uint64_t trig_frame, trig_interval_us;
|
||||
const char *why;
|
||||
char dir[PLAT_PATH_MAX];
|
||||
Rec r[];
|
||||
} DumpJob;
|
||||
|
||||
int flight_on;
|
||||
static int ring_n, thumbs_on;
|
||||
static Rec *ring;
|
||||
static uint64_t recorded; // records written so far
|
||||
static uint64_t spike_ns, t0_ns;
|
||||
static uint64_t dump_at, trig_frame, trig_interval_us, last_spike_ns;
|
||||
static const char *trig_why;
|
||||
static uint64_t at_frames[16];
|
||||
static int n_at;
|
||||
static char base_dir[PLAT_PATH_MAX];
|
||||
static uint64_t prev_cnt[PERF_CNT_N], prev_xlated;
|
||||
static VkExecFrame prev_exec;
|
||||
static volatile uint64_t cost_ns, cost_max_ns; // render-thread time in flight_frame, for the dump log line
|
||||
|
||||
static unsigned env_u(const char *name, unsigned def) {
|
||||
const char *e = cfg_get(name);
|
||||
return e && *e ? (unsigned)strtoul(e, NULL, 10) : def;
|
||||
}
|
||||
|
||||
void flight_init(void) {
|
||||
if (strcmp(cfg_value("NX2NAVI_FLIGHT"), "1")) return;
|
||||
ring_n = (int)env_u("NX2NAVI_FLIGHT_FRAMES", 600);
|
||||
if (ring_n < 64) ring_n = 64;
|
||||
if (ring_n > 7200) ring_n = 7200;
|
||||
if (!(ring = calloc((size_t)ring_n, sizeof *ring))) { rt_log("flight: no memory for %d records, off", ring_n); return; }
|
||||
spike_ns = (uint64_t)cfg_int("NX2NAVI_FLIGHT_SPIKE_MS") * 1000000ull;
|
||||
for (const char *p = cfg_get("NX2NAVI_FLIGHT_AT"); p && *p && n_at < 16;) {
|
||||
char *end;
|
||||
unsigned long long f = strtoull(p, &end, 10);
|
||||
if (end == p) break;
|
||||
at_frames[n_at++] = f;
|
||||
p = *end == ',' ? end + 1 : end;
|
||||
}
|
||||
const char *d = cfg_get("NX2NAVI_FLIGHT_DIR");
|
||||
char tmp[PLAT_PATH_MAX];
|
||||
if (d && *d) snprintf(tmp, sizeof tmp, "%s", d);
|
||||
else { // <exe dir>/../flight: build/flight for build/v121*/nx2navi.exe
|
||||
char *slash = plat_exe_path(tmp, sizeof tmp) ? plat_last_sep(tmp) : NULL;
|
||||
if (slash) snprintf(slash, sizeof tmp - (size_t)(slash - tmp), "/../flight");
|
||||
else snprintf(tmp, sizeof tmp, "flight");
|
||||
}
|
||||
if (!plat_full_path(tmp, base_dir, sizeof base_dir)) snprintf(base_dir, sizeof base_dir, "%s", tmp);
|
||||
thumbs_on = env_u("NX2NAVI_FLIGHT_THUMBS", 1) != 0;
|
||||
if (thumbs_on) vk_thumbs_enable(ring_n);
|
||||
flight_on = 1;
|
||||
rt_log("flight: on: last %d frames, %s; dump on F8, on a frame over %llu ms (one per 10 s)%s, to %s", ring_n,
|
||||
thumbs_on ? "thumbnails 320x180" : "no thumbnails", (unsigned long long)(spike_ns / 1000000),
|
||||
spike_ns ? "" : " (spike trigger off)", base_dir);
|
||||
if (thumbs_on)
|
||||
rt_log("flight: memory: records %llu KB, thumbnail ring %llu MB (host-visible)",
|
||||
(unsigned long long)((size_t)ring_n * sizeof *ring >> 10),
|
||||
(unsigned long long)((uint64_t)ring_n * FLIGHT_THUMB_W * FLIGHT_THUMB_H * 4 >> 20));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- PNG (RGB, Paeth rows, fixed-Huffman deflate)
|
||||
typedef struct { uint8_t *p; size_t n; uint64_t bits; int nb; } Bits;
|
||||
|
||||
static void put(Bits *b, uint32_t v, int n) {
|
||||
b->bits |= (uint64_t)v << b->nb;
|
||||
b->nb += n;
|
||||
while (b->nb >= 8) b->p[b->n++] = (uint8_t)b->bits, b->bits >>= 8, b->nb -= 8;
|
||||
}
|
||||
|
||||
static uint32_t rev(uint32_t v, int n) {
|
||||
uint32_t r = 0;
|
||||
while (n--) r = r << 1 | (v & 1), v >>= 1;
|
||||
return r;
|
||||
}
|
||||
|
||||
static void put_sym(Bits *b, int v) { // RFC 1951 3.2.6 fixed literal/length code
|
||||
if (v < 144) put(b, rev(0x30u + (uint32_t)v, 8), 8);
|
||||
else if (v < 256) put(b, rev(0x190u + (uint32_t)v - 144, 9), 9);
|
||||
else if (v < 280) put(b, rev((uint32_t)v - 256, 7), 7);
|
||||
else put(b, rev(0xC0u + (uint32_t)v - 280, 8), 8);
|
||||
}
|
||||
|
||||
static const uint16_t lbase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115,
|
||||
131, 163, 195, 227, 258};
|
||||
static const uint8_t lext[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
|
||||
static const uint16_t dbase[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025,
|
||||
1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
|
||||
static const uint8_t dext[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
|
||||
|
||||
#define HBITS 15
|
||||
// zlib stream of src: one fixed-Huffman block, greedy matches through a hash of the last position of each 3-byte key.
|
||||
static size_t deflate_fixed(const uint8_t *src, size_t n, uint8_t *out, int32_t *head) {
|
||||
Bits b = {out, 0, 0, 0};
|
||||
b.p[b.n++] = 0x78, b.p[b.n++] = 0x01;
|
||||
put(&b, 1, 1), put(&b, 1, 2);
|
||||
for (int i = 0; i < 1 << HBITS; i++) head[i] = -1;
|
||||
size_t i = 0;
|
||||
while (i < n) {
|
||||
size_t len = 0, dist = 0;
|
||||
if (i + 3 <= n) {
|
||||
uint32_t h = ((uint32_t)src[i] << 16 | (uint32_t)src[i + 1] << 8 | src[i + 2]) * 2654435761u >> (32 - HBITS);
|
||||
int32_t c = head[h];
|
||||
head[h] = (int32_t)i;
|
||||
if (c >= 0 && i - (size_t)c <= 32768) {
|
||||
size_t m = 0, lim = n - i < 258 ? n - i : 258;
|
||||
while (m < lim && src[(size_t)c + m] == src[i + m]) m++;
|
||||
if (m >= 3) len = m, dist = i - (size_t)c;
|
||||
}
|
||||
}
|
||||
if (!len) {
|
||||
put_sym(&b, src[i++]);
|
||||
continue;
|
||||
}
|
||||
int k = 28;
|
||||
while (lbase[k] > len) k--;
|
||||
put_sym(&b, 257 + k);
|
||||
if (lext[k]) put(&b, (uint32_t)(len - lbase[k]), lext[k]);
|
||||
int j = 29;
|
||||
while (dbase[j] > dist) j--;
|
||||
put(&b, rev((uint32_t)j, 5), 5);
|
||||
if (dext[j]) put(&b, (uint32_t)(dist - dbase[j]), dext[j]);
|
||||
for (size_t s = i + 1; s < i + len && s + 3 <= n; s++)
|
||||
head[((uint32_t)src[s] << 16 | (uint32_t)src[s + 1] << 8 | src[s + 2]) * 2654435761u >> (32 - HBITS)] = (int32_t)s;
|
||||
i += len;
|
||||
}
|
||||
put_sym(&b, 256);
|
||||
if (b.nb) put(&b, 0, 8 - b.nb);
|
||||
uint32_t a = 1, s2 = 0;
|
||||
for (size_t k = 0; k < n; k++) a = (a + src[k]) % 65521, s2 = (s2 + a) % 65521;
|
||||
uint32_t ad = s2 << 16 | a;
|
||||
b.p[b.n++] = (uint8_t)(ad >> 24), b.p[b.n++] = (uint8_t)(ad >> 16), b.p[b.n++] = (uint8_t)(ad >> 8), b.p[b.n++] = (uint8_t)ad;
|
||||
return b.n;
|
||||
}
|
||||
|
||||
static uint32_t crc_table[256];
|
||||
|
||||
static uint32_t crc(uint32_t c, const uint8_t *p, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) c = crc_table[(c ^ p[i]) & 0xFF] ^ (c >> 8);
|
||||
return c;
|
||||
}
|
||||
|
||||
static void chunk(FILE *f, const char *type, const uint8_t *data, uint32_t n) {
|
||||
uint8_t h[8] = {(uint8_t)(n >> 24), (uint8_t)(n >> 16), (uint8_t)(n >> 8), (uint8_t)n};
|
||||
memcpy(h + 4, type, 4);
|
||||
uint32_t c = crc(crc(~0u, h + 4, 4), data, n) ^ ~0u;
|
||||
uint8_t t[4] = {(uint8_t)(c >> 24), (uint8_t)(c >> 16), (uint8_t)(c >> 8), (uint8_t)c};
|
||||
fwrite(h, 1, 8, f);
|
||||
if (n) fwrite(data, 1, n, f);
|
||||
fwrite(t, 1, 4, f);
|
||||
}
|
||||
|
||||
static uint8_t paeth(int a, int b, int c) {
|
||||
int p = a + b - c, pa = abs(p - a), pb = abs(p - b), pc = abs(p - c);
|
||||
return (uint8_t)(pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
|
||||
}
|
||||
|
||||
typedef struct { uint8_t *raw, *z; int32_t *head; } PngBufs;
|
||||
|
||||
static int write_png(const char *path, const uint8_t *rgba, PngBufs *pb) {
|
||||
const uint32_t w = FLIGHT_THUMB_W, h = FLIGHT_THUMB_H, row = 1 + 3 * w;
|
||||
for (uint32_t y = 0; y < h; y++) {
|
||||
uint8_t *o = pb->raw + (size_t)y * row;
|
||||
o[0] = 4; // Paeth
|
||||
for (uint32_t x = 0; x < w; x++)
|
||||
for (int c = 0; c < 3; c++) {
|
||||
int v = rgba[4 * ((size_t)y * w + x) + (size_t)c];
|
||||
int a = x ? rgba[4 * ((size_t)y * w + x - 1) + (size_t)c] : 0;
|
||||
int up = y ? rgba[4 * ((size_t)(y - 1) * w + x) + (size_t)c] : 0;
|
||||
int ul = x && y ? rgba[4 * ((size_t)(y - 1) * w + x - 1) + (size_t)c] : 0;
|
||||
o[1 + 3 * x + (uint32_t)c] = (uint8_t)(v - paeth(a, up, ul));
|
||||
}
|
||||
}
|
||||
size_t zn = deflate_fixed(pb->raw, (size_t)row * h, pb->z, pb->head);
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return 0;
|
||||
static const uint8_t sig[8] = {0x89, 'P', 'N', 'G', 13, 10, 26, 10};
|
||||
uint8_t ihdr[13] = {0, 0, (uint8_t)(w >> 8), (uint8_t)w, 0, 0, (uint8_t)(h >> 8), (uint8_t)h, 8, 2, 0, 0, 0};
|
||||
fwrite(sig, 1, 8, f);
|
||||
chunk(f, "IHDR", ihdr, 13);
|
||||
chunk(f, "IDAT", pb->z, (uint32_t)zn);
|
||||
chunk(f, "IEND", NULL, 0);
|
||||
return fclose(f) == 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- dump thread
|
||||
static void dump_main(void *arg) {
|
||||
DumpJob *j = arg;
|
||||
uint64_t t_start = plat_now_ms();
|
||||
plat_thread_priority(PLAT_PRIO_BELOW_NORMAL);
|
||||
plat_file_mkdir_p(j->dir);
|
||||
if (!plat_dir_exists(j->dir)) {
|
||||
rt_log("flight: cannot create %s", j->dir);
|
||||
free(j);
|
||||
return;
|
||||
}
|
||||
uint8_t *have = calloc((size_t)j->n, 1);
|
||||
int n_thumbs = 0;
|
||||
if (thumbs_on && have) {
|
||||
if (!crc_table[1])
|
||||
for (uint32_t i = 0; i < 256; i++) {
|
||||
uint32_t c = i;
|
||||
for (int k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320u ^ (c >> 1) : c >> 1;
|
||||
crc_table[i] = c;
|
||||
}
|
||||
size_t raw_n = (size_t)(1 + 3 * FLIGHT_THUMB_W) * FLIGHT_THUMB_H;
|
||||
PngBufs pb = {malloc(raw_n), malloc(raw_n * 9 / 8 + 64), malloc(sizeof(int32_t) << HBITS)};
|
||||
uint8_t *px = malloc((size_t)FLIGHT_THUMB_W * FLIGHT_THUMB_H * 4);
|
||||
// Oldest first: each slot is copied out long before the presents that go on meanwhile reach it again. The newest
|
||||
// few complete only when their present fence has been waited for, so they get a second try.
|
||||
for (int pass = 0; pass < 2 && pb.raw && pb.z && pb.head && px; pass++) {
|
||||
if (pass) plat_sleep_ms(200);
|
||||
for (int i = pass ? (j->n > 8 ? j->n - 8 : 0) : 0; i < j->n; i++) {
|
||||
if (have[i] || !vk_thumbs_read(j->r[i].frame, px)) continue;
|
||||
char path[PLAT_PATH_MAX + 32];
|
||||
snprintf(path, sizeof path, "%s/f%06llu.png", j->dir, (unsigned long long)j->r[i].frame);
|
||||
if (write_png(path, px, &pb)) have[i] = 1, n_thumbs++;
|
||||
}
|
||||
}
|
||||
free(px), free(pb.raw), free(pb.z), free(pb.head);
|
||||
}
|
||||
char path[PLAT_PATH_MAX + 32];
|
||||
snprintf(path, sizeof path, "%s/frames.tsv", j->dir);
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
rt_log("flight: cannot write %s", path);
|
||||
free(have), free(j);
|
||||
return;
|
||||
}
|
||||
fprintf(f, "# nx2navi flight dump: %s at frame %llu, interval %.1f ms\n", j->why, (unsigned long long)j->trig_frame,
|
||||
j->trig_interval_us / 1000.0);
|
||||
fprintf(f, "frame\tt_ms\tinterval_ms");
|
||||
for (int k = 0; k < PERF_N; k++) fprintf(f, "\t%s", blocked_names[k]);
|
||||
for (int k = 0; k < C_N; k++) fprintf(f, "\t%s", count_names[k]);
|
||||
fprintf(f, "\tthumb\n");
|
||||
for (int i = 0; i < j->n; i++) {
|
||||
const Rec *r = &j->r[i];
|
||||
fprintf(f, "%llu\t%.1f\t%.2f", (unsigned long long)r->frame, r->t_ns / 1e6, r->interval_us / 1000.0);
|
||||
for (int k = 0; k < PERF_N; k++) fprintf(f, "\t%.2f", r->blocked_us[k] / 1000.0);
|
||||
for (int k = 0; k < C_N; k++) fprintf(f, "\t%u", r->n[k]);
|
||||
fprintf(f, "\t%d\n", have ? have[i] : 0);
|
||||
}
|
||||
fclose(f);
|
||||
rt_log("flight: dump %s: %s at frame %llu (%.1f ms), frames %llu-%llu, %d thumbnails, written in %llu ms; "
|
||||
"recorder %.1f us per frame (max %.0f us)", j->dir,
|
||||
j->why, (unsigned long long)j->trig_frame, j->trig_interval_us / 1000.0, (unsigned long long)j->r[0].frame,
|
||||
(unsigned long long)j->r[j->n - 1].frame, n_thumbs, (unsigned long long)(plat_now_ms() - t_start),
|
||||
recorded ? cost_ns / 1e3 / (double)recorded : 0.0, cost_max_ns / 1e3);
|
||||
free(have);
|
||||
free(j);
|
||||
return;
|
||||
}
|
||||
|
||||
static void start_dump(void) {
|
||||
int n = recorded < (uint64_t)ring_n ? (int)recorded : ring_n;
|
||||
DumpJob *j = n ? malloc(sizeof *j + sizeof(Rec) * (size_t)n) : NULL;
|
||||
if (!j) return;
|
||||
j->n = n;
|
||||
j->trig_frame = trig_frame;
|
||||
j->trig_interval_us = trig_interval_us;
|
||||
j->why = trig_why;
|
||||
for (int i = 0; i < n; i++) j->r[i] = ring[(recorded - (uint64_t)n + (uint64_t)i) % (uint64_t)ring_n];
|
||||
struct tm st;
|
||||
plat_local_time(&st);
|
||||
snprintf(j->dir, sizeof j->dir, "%s/%04d%02d%02d-%02d%02d%02d-f%llu", base_dir, st.tm_year + 1900, st.tm_mon + 1,
|
||||
st.tm_mday, st.tm_hour, st.tm_min, st.tm_sec, (unsigned long long)trig_frame);
|
||||
if (!plat_thread_spawn(dump_main, j, "flight dump", 0)) rt_log("flight: cannot start the dump thread"), free(j);
|
||||
}
|
||||
|
||||
static void trigger(uint64_t frame, uint64_t interval_ns, const char *why) {
|
||||
if (dump_at) return; // one dump pending at a time; its window covers this frame
|
||||
trig_frame = frame;
|
||||
trig_interval_us = interval_ns / 1000;
|
||||
trig_why = why;
|
||||
dump_at = frame + (FLIGHT_POST < ring_n / 4 ? FLIGHT_POST : (uint64_t)ring_n / 4);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- per frame (render thread)
|
||||
void flight_frame(uint64_t frame, uint64_t now_ns, uint64_t interval_ns, const uint64_t blocked_ns[],
|
||||
const volatile int64_t counts[]) {
|
||||
uint64_t c0 = kern_now_ns();
|
||||
if (!t0_ns) t0_ns = now_ns;
|
||||
Rec *r = &ring[recorded % (uint64_t)ring_n];
|
||||
r->frame = frame;
|
||||
r->t_ns = now_ns - t0_ns;
|
||||
r->interval_us = (uint32_t)(interval_ns / 1000);
|
||||
for (int k = 0; k < PERF_N; k++) r->blocked_us[k] = (uint32_t)(blocked_ns[k] / 1000);
|
||||
// vk_exec_frame grows until vk_exec_frame_log resets it, which NX2NAVI_TRACE=gfx does right after this call
|
||||
const VkExecFrame *x = &vk_exec_frame;
|
||||
int reset = (rt_trace & RT_TRACE_GFX) != 0;
|
||||
r->n[C_SUBMITS] = (uint32_t)(x->submits - (reset ? 0 : prev_exec.submits));
|
||||
r->n[C_PASSES] = (uint32_t)(x->passes - (reset ? 0 : prev_exec.passes));
|
||||
r->n[C_DRAWS] = (uint32_t)(x->draws - (reset ? 0 : prev_exec.draws));
|
||||
r->n[C_DRAWS_SEEN] = (uint32_t)(x->draws_seen - (reset ? 0 : prev_exec.draws_seen));
|
||||
r->n[C_CLEARS] = (uint32_t)(x->clears - (reset ? 0 : prev_exec.clears));
|
||||
r->n[C_COPIES] = (uint32_t)(x->copies - (reset ? 0 : prev_exec.copies));
|
||||
r->n[C_DISPATCHES] = (uint32_t)(x->dispatches - (reset ? 0 : prev_exec.dispatches));
|
||||
prev_exec = *x;
|
||||
uint64_t keys, xlated;
|
||||
gfx_shader_counts(&keys, &xlated);
|
||||
r->n[C_XLATED] = (uint32_t)(xlated - prev_xlated);
|
||||
prev_xlated = xlated;
|
||||
for (int k = 0; k < PERF_CNT_N; k++) {
|
||||
uint64_t v = (uint64_t)__atomic_load_n(&counts[k], __ATOMIC_RELAXED);
|
||||
r->n[C_PERF0 + k] = (uint32_t)(v - prev_cnt[k]);
|
||||
prev_cnt[k] = v;
|
||||
}
|
||||
recorded++;
|
||||
|
||||
static int key_was;
|
||||
int key = input_flight_key();
|
||||
if (key && !key_was) trigger(frame, interval_ns, "F8");
|
||||
key_was = key;
|
||||
for (int i = 0; i < n_at; i++)
|
||||
if (at_frames[i] == frame) trigger(frame, interval_ns, "NX2NAVI_FLIGHT_AT");
|
||||
if (spike_ns && interval_ns > spike_ns && recorded > 30 && (!last_spike_ns || now_ns - last_spike_ns >= FLIGHT_SPIKE_GAP_NS)) {
|
||||
if (!dump_at) last_spike_ns = now_ns;
|
||||
trigger(frame, interval_ns, "spike");
|
||||
}
|
||||
if (dump_at && frame >= dump_at) {
|
||||
start_dump();
|
||||
dump_at = 0;
|
||||
}
|
||||
uint64_t c = kern_now_ns() - c0;
|
||||
cost_ns += c;
|
||||
if (c > cost_max_ns) cost_max_ns = c;
|
||||
}
|
||||
22
runtime/flight.h
Normal file
22
runtime/flight.h
Normal file
@@ -0,0 +1,22 @@
|
||||
// NX2NAVI_FLIGHT=1 flight recorder (flight.c): the last frames' timing and event counts plus a thumbnail each, written to
|
||||
// disk on F8 or after a slow frame, so a glitch or stutter seen in play can be read after the fact.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#define FLIGHT_THUMB_W 320
|
||||
#define FLIGHT_THUMB_H 180
|
||||
|
||||
extern int flight_on; // NX2NAVI_FLIGHT=1, set once by flight_init before the first present
|
||||
|
||||
void flight_init(void); // prof_start calls it
|
||||
// perf_present, once per present on the render thread: the interval since the previous present, the render thread's
|
||||
// blocked time per PERF_* kind in this frame, and the running PERF_CNT_* totals.
|
||||
void flight_frame(uint64_t frame, uint64_t now_ns, uint64_t interval_ns, const uint64_t blocked_ns[],
|
||||
const volatile int64_t counts[]);
|
||||
|
||||
// gfx_present.c: the present thread blits every presented frame into a host-visible ring of n thumbnails.
|
||||
void vk_thumbs_enable(int n); // before the first present; n slots of FLIGHT_THUMB_W x FLIGHT_THUMB_H
|
||||
void vk_thumbs_frame(uint64_t frame); // the present thread names the frame of the vk_present_image call that follows
|
||||
// Copies frame's thumbnail as RGBA when the ring still holds it complete; 0 when it was never written, is still in
|
||||
// flight or was overwritten. Takes the present lock for one slot copy (about 20 us); any thread.
|
||||
int vk_thumbs_read(uint64_t frame, uint8_t *rgba);
|
||||
246
runtime/fma.c
Normal file
246
runtime/fma.c
Normal file
@@ -0,0 +1,246 @@
|
||||
// Correctly rounded fma/fmaf for x86-64 builds without FMA3: the -march=x86-64-v2 build (docs/building.md).
|
||||
//
|
||||
// runtime/simd.h lowers AArch64 FMADD/FMSUB and the RECPS/RSQRTS helpers to __builtin_elementwise_fma, which clang
|
||||
// turns into a call to fma/fmaf when the target has no FMA instruction. The C runtime is not a substitute: on CPUs
|
||||
// without FMA3 the UCRT's software fallback is documented as rounding incorrectly by up to half an ulp, while the FP
|
||||
// lowering needs the single rounding the ARM instruction gives. This file supplies it.
|
||||
//
|
||||
// Adapted from musl src/math/fma.c and src/math/fmaf.c (MIT, Copyright (c) 2005-2020 Rich Felker and contributors).
|
||||
// The result is rounded in the MXCSR rounding mode the caller set (simd.h simd_rm_enter), and MXCSR FTZ and DAZ are
|
||||
// honoured as the hardware FMA would, so runtime/simd.h's fast paths keep their assumptions: denormal inputs read as
|
||||
// zero and denormal results as zero under FPCR.FZ, without a trip through the slow path.
|
||||
//
|
||||
// The definitions exist only where the compiler would emit the calls. A -march=x86-64-v3 build defines __FMA__ and
|
||||
// emits vfmadd instead, so this file contributes nothing there.
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(__x86_64__) && !defined(__FMA__)
|
||||
|
||||
#include <float.h>
|
||||
#include <immintrin.h>
|
||||
|
||||
#define MXCSR_DAZ 0x0040u
|
||||
#define MXCSR_FTZ 0x8000u
|
||||
|
||||
static double fma_flush_d(double x) {
|
||||
union { double f; uint64_t i; } u = {x};
|
||||
if (!(u.i & 0x7ff0000000000000ull) && (u.i & 0x000fffffffffffffull)) u.i &= 0x8000000000000000ull;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static float fma_flush_f(float x) {
|
||||
union { float f; uint32_t i; } u = {x};
|
||||
if (!(u.i & 0x7f800000u) && (u.i & 0x007fffffu)) u.i &= 0x80000000u;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
// ---- float: the exact product fits in a double, so one extra term and a round-to-odd step decide the float
|
||||
// rounding. Halfway and tiny results are the only ones where rounding the double first can change the float result.
|
||||
float fmaf(float x, float y, float z) {
|
||||
unsigned csr = _mm_getcsr();
|
||||
if (csr & MXCSR_DAZ) x = fma_flush_f(x), y = fma_flush_f(y), z = fma_flush_f(z);
|
||||
double xy = (double)x * y; // exact
|
||||
union { double r; uint64_t i; } u = {xy + z};
|
||||
int e = u.i >> 52 & 0x7ff;
|
||||
int halfway = (u.i & 0x1fffffff) == 0x10000000;
|
||||
int tiny = e <= 0x3ff - 126 && e >= 0x3ff - 149;
|
||||
if (halfway || tiny) {
|
||||
if (e != 0x7ff) {
|
||||
// r+t is the exact sum in nearest rounding; otherwise t keeps its sign and whether it is zero, which
|
||||
// is all the round-to-odd step needs. An odd sticky bit makes the final conversion round once, in
|
||||
// whichever mode MXCSR asks for.
|
||||
int s = u.i >> 63;
|
||||
double t = s == (xy < z) ? xy - u.r + z : z - u.r + xy;
|
||||
if (t) {
|
||||
u.i -= s ^ (t < 0);
|
||||
u.i |= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
float r = (float)u.r;
|
||||
if (csr & MXCSR_FTZ) r = fma_flush_f(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- double: the significands are multiplied and aligned as integers, then converted back with one rounding.
|
||||
#define ASUINT64(x) ((union { double f; uint64_t i; }){x}).i
|
||||
#define ZEROINFNAN (0x7ff - 0x3ff - 52 - 1)
|
||||
|
||||
struct fma_num { uint64_t m; int e; int sign; };
|
||||
|
||||
static struct fma_num fma_normalize(double x) {
|
||||
uint64_t ix = ASUINT64(x);
|
||||
int e = ix >> 52;
|
||||
int sign = e & 0x800;
|
||||
e &= 0x7ff;
|
||||
if (!e) {
|
||||
ix = ASUINT64(x * 0x1p63);
|
||||
e = ix >> 52 & 0x7ff;
|
||||
e = e ? e - 63 : 0x800;
|
||||
}
|
||||
ix &= (1ull << 52) - 1;
|
||||
ix |= 1ull << 52;
|
||||
ix <<= 1;
|
||||
e -= 0x3ff + 52 + 1;
|
||||
return (struct fma_num){ix, e, sign};
|
||||
}
|
||||
|
||||
static void fma_mul(uint64_t *hi, uint64_t *lo, uint64_t x, uint64_t y) {
|
||||
uint64_t xlo = (uint32_t)x, xhi = x >> 32;
|
||||
uint64_t ylo = (uint32_t)y, yhi = y >> 32;
|
||||
uint64_t t1 = xlo * ylo;
|
||||
uint64_t t2 = xlo * yhi + xhi * ylo;
|
||||
uint64_t t3 = xhi * yhi;
|
||||
*lo = t1 + (t2 << 32);
|
||||
*hi = t3 + (t2 >> 32) + (t1 > *lo);
|
||||
}
|
||||
|
||||
// scalbn from musl src/math/scalbn.c (MIT): the fma's final scaling, kept here so the result does not depend on the
|
||||
// C runtime's version. Its multiplications honour MXCSR like any other host FP operation.
|
||||
static double fma_scalbn(double x, int n) {
|
||||
union { double f; uint64_t i; } u;
|
||||
double y = x;
|
||||
if (n > 1023) {
|
||||
y *= 0x1p1023;
|
||||
n -= 1023;
|
||||
if (n > 1023) {
|
||||
y *= 0x1p1023;
|
||||
n -= 1023;
|
||||
if (n > 1023) n = 1023;
|
||||
}
|
||||
} else if (n < -1022) {
|
||||
// Make sure the final n < -53, so the subnormal range rounds only once.
|
||||
y *= 0x1p-1022 * 0x1p53;
|
||||
n += 1022 - 53;
|
||||
if (n < -1022) {
|
||||
y *= 0x1p-1022 * 0x1p53;
|
||||
n += 1022 - 53;
|
||||
if (n < -1022) n = -1022;
|
||||
}
|
||||
}
|
||||
u.i = (uint64_t)(0x3ff + n) << 52;
|
||||
return y * u.f;
|
||||
}
|
||||
|
||||
double fma(double x, double y, double z) {
|
||||
#pragma STDC FENV_ACCESS ON
|
||||
unsigned csr = _mm_getcsr();
|
||||
if (csr & MXCSR_DAZ) x = fma_flush_d(x), y = fma_flush_d(y), z = fma_flush_d(z);
|
||||
|
||||
struct fma_num nx = fma_normalize(x), ny = fma_normalize(y), nz = fma_normalize(z);
|
||||
if (nx.e >= ZEROINFNAN || ny.e >= ZEROINFNAN) return x * y + z;
|
||||
if (nz.e >= ZEROINFNAN) {
|
||||
if (nz.e > ZEROINFNAN) return x * y; // z == 0
|
||||
return z;
|
||||
}
|
||||
|
||||
uint64_t rhi, rlo, zhi, zlo;
|
||||
fma_mul(&rhi, &rlo, nx.m, ny.m);
|
||||
|
||||
// Align the exponents: shift z left by kz and r right by kr with kz + kr == d.
|
||||
int e = nx.e + ny.e;
|
||||
int d = nz.e - e;
|
||||
if (d > 0) {
|
||||
if (d < 64) {
|
||||
zlo = nz.m << d;
|
||||
zhi = nz.m >> (64 - d);
|
||||
} else {
|
||||
zlo = 0;
|
||||
zhi = nz.m;
|
||||
e = nz.e - 64;
|
||||
d -= 64;
|
||||
if (d == 0) {
|
||||
} else if (d < 64) {
|
||||
rlo = rhi << (64 - d) | rlo >> d | !!(rlo << (64 - d));
|
||||
rhi = rhi >> d;
|
||||
} else {
|
||||
rlo = 1;
|
||||
rhi = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
zhi = 0;
|
||||
d = -d;
|
||||
if (d == 0) {
|
||||
zlo = nz.m;
|
||||
} else if (d < 64) {
|
||||
zlo = nz.m >> d | !!(nz.m << (64 - d));
|
||||
} else {
|
||||
zlo = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int sign = nx.sign ^ ny.sign;
|
||||
int samesign = !(sign ^ nz.sign);
|
||||
int nonzero = 1;
|
||||
if (samesign) {
|
||||
rlo += zlo;
|
||||
rhi += zhi + (rlo < zlo);
|
||||
} else {
|
||||
uint64_t t = rlo;
|
||||
rlo -= zlo;
|
||||
rhi = rhi - zhi - (t < rlo);
|
||||
if (rhi >> 63) {
|
||||
rlo = -rlo;
|
||||
rhi = -rhi - !!rlo;
|
||||
sign = !sign;
|
||||
}
|
||||
nonzero = !!rhi;
|
||||
}
|
||||
|
||||
// Keep the top 63 bits in rhi; the last bit is sticky.
|
||||
if (nonzero) {
|
||||
e += 64;
|
||||
d = __builtin_clzll(rhi) - 1;
|
||||
rhi = rhi << d | rlo >> (64 - d) | !!(rlo << d);
|
||||
} else if (rlo) {
|
||||
d = __builtin_clzll(rlo) - 1;
|
||||
if (d < 0)
|
||||
rhi = rlo >> 1 | (rlo & 1);
|
||||
else
|
||||
rhi = rlo << d;
|
||||
} else {
|
||||
return x * y + z; // exact +-0
|
||||
}
|
||||
e -= d;
|
||||
|
||||
int64_t i = rhi;
|
||||
if (sign) i = -i;
|
||||
double r = i; // |r| is in [0x1p62, 0x1p63]
|
||||
|
||||
if (e < -1022 - 62) {
|
||||
// The result is subnormal before rounding.
|
||||
if (e == -1022 - 63) {
|
||||
double c = 0x1p63;
|
||||
if (sign) c = -c;
|
||||
if (r == c) {
|
||||
// The smallest normal after rounding: whether it underflows depends on the architecture. A double
|
||||
// to float conversion imitates the before-rounding rule the guest has.
|
||||
float fltmin = 0x0.ffffff8p-63 * FLT_MIN * r;
|
||||
r = DBL_MIN / FLT_MIN * fltmin;
|
||||
return (csr & MXCSR_FTZ) ? fma_flush_d(r) : r;
|
||||
}
|
||||
// One bit is lost when scaled: add another top bit so the conversion rounds only once if inexact.
|
||||
if (rhi << 53) {
|
||||
i = rhi >> 1 | (rhi & 1) | 1ull << 62;
|
||||
if (sign) i = -i;
|
||||
r = i;
|
||||
r = 2 * r - c;
|
||||
// Raise underflow portably, so the compiler cannot optimize the expression away.
|
||||
double tiny = DBL_MIN / FLT_MIN * r;
|
||||
r += (double)(tiny * tiny) * (r - r);
|
||||
}
|
||||
} else {
|
||||
// Only round once when scaled.
|
||||
d = 10;
|
||||
i = (rhi >> d | !!(rhi << (64 - d))) << d;
|
||||
if (sign) i = -i;
|
||||
r = i;
|
||||
}
|
||||
}
|
||||
r = fma_scalbn(r, e);
|
||||
if (csr & MXCSR_FTZ) r = fma_flush_d(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
#endif
|
||||
18
runtime/folder_pick.h
Normal file
18
runtime/folder_pick.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// The host's own folder dialog, for menu pages that point at a folder (the Amiibo tab): folder_pick_win.cpp with Win32,
|
||||
// folder_pick_sdl.c with SDL. The menu has no text entry, so a folder is chosen here or by walking the page's list.
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// done gets the chosen folder (UTF-8) or NULL when the player cancelled or the host has no dialog.
|
||||
typedef void (*FolderPickDone)(const char *path, void *ctx);
|
||||
|
||||
// Opens the dialog at start (may be relative or missing) and returns at once; done runs later on another thread, once.
|
||||
// The caller keeps one dialog open at a time.
|
||||
void folder_pick(const char *start, FolderPickDone done, void *ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
42
runtime/folder_pick_sdl.c
Normal file
42
runtime/folder_pick_sdl.c
Normal file
@@ -0,0 +1,42 @@
|
||||
// The SDL front end of folder_pick.h: SDL_ShowOpenFolderDialog (the desktop portal, else zenity), started on the window
|
||||
// thread, which runs SDL's event loop that delivers the portal's answer.
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "folder_pick.h"
|
||||
#include "host.h"
|
||||
#include "input_sdl.h"
|
||||
|
||||
typedef struct {
|
||||
char start[PATH_MAX]; // absolute, or empty: the dialog's own default
|
||||
FolderPickDone done;
|
||||
void *ctx;
|
||||
} PickReq;
|
||||
|
||||
static void SDLCALL on_pick(void *userdata, const char *const *list, int filter) {
|
||||
(void)filter;
|
||||
PickReq *r = userdata;
|
||||
if (!list) rt_log("folder_pick: the folder dialog failed (%s)", SDL_GetError());
|
||||
r->done(list && list[0] ? list[0] : NULL, r->ctx);
|
||||
free(r);
|
||||
}
|
||||
|
||||
static void show(void *arg) {
|
||||
PickReq *r = arg;
|
||||
SDL_ShowOpenFolderDialog(on_pick, r, gfx_window_sdl(), r->start[0] ? r->start : NULL, false);
|
||||
}
|
||||
|
||||
void folder_pick(const char *start, FolderPickDone done, void *ctx) {
|
||||
PickReq *r = calloc(1, sizeof *r);
|
||||
if (!r) {
|
||||
done(NULL, ctx);
|
||||
return;
|
||||
}
|
||||
r->done = done;
|
||||
r->ctx = ctx;
|
||||
if (start && *start && !realpath(start, r->start)) r->start[0] = 0;
|
||||
gfx_window_sdl_run(show, r);
|
||||
}
|
||||
84
runtime/folder_pick_win.cpp
Normal file
84
runtime/folder_pick_win.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
// The Win32 front end of folder_pick.h: the shell's folder dialog (IFileOpenDialog with FOS_PICKFOLDERS), owned by the
|
||||
// game window, on a thread of its own so the present thread that draws the menu keeps running.
|
||||
#include <windows.h>
|
||||
#include <shobjidl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
|
||||
extern "C" {
|
||||
#include "folder_pick.h"
|
||||
#include "host.h"
|
||||
#include "input_win.h"
|
||||
#include "platform/platform.h"
|
||||
}
|
||||
|
||||
struct PickReq {
|
||||
std::string start;
|
||||
FolderPickDone done;
|
||||
void *ctx;
|
||||
};
|
||||
|
||||
static std::wstring wide(const std::string &utf8) {
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
|
||||
std::wstring w(n > 0 ? n : 1, L'\0');
|
||||
if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], n);
|
||||
w.resize(wcslen(w.c_str()));
|
||||
return w;
|
||||
}
|
||||
|
||||
static std::string utf8(const wchar_t *w) {
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
|
||||
std::string s(n > 0 ? n : 1, '\0');
|
||||
if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w, -1, &s[0], n, NULL, NULL);
|
||||
s.resize(strlen(s.c_str()));
|
||||
return s;
|
||||
}
|
||||
|
||||
// The dialog wants an absolute folder that exists; anything else leaves it at the shell's last folder.
|
||||
static void set_start(IFileOpenDialog *d, const std::string &start) {
|
||||
wchar_t full[MAX_PATH * 2];
|
||||
if (start.empty() || !GetFullPathNameW(wide(start).c_str(), MAX_PATH * 2, full, NULL)) return;
|
||||
IShellItem *item = NULL;
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(full, NULL, IID_PPV_ARGS(&item)))) {
|
||||
d->SetFolder(item);
|
||||
item->Release();
|
||||
}
|
||||
}
|
||||
|
||||
static void pick_main(void *arg) {
|
||||
PickReq *r = (PickReq *)arg;
|
||||
std::string chosen;
|
||||
HRESULT co = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||
IFileOpenDialog *d = NULL;
|
||||
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d));
|
||||
if (SUCCEEDED(hr)) {
|
||||
FILEOPENDIALOGOPTIONS opt = 0;
|
||||
d->GetOptions(&opt);
|
||||
d->SetOptions(opt | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
||||
set_start(d, r->start);
|
||||
hr = d->Show(gfx_window_handle());
|
||||
IShellItem *item = NULL;
|
||||
if (SUCCEEDED(hr) && SUCCEEDED(hr = d->GetResult(&item))) {
|
||||
PWSTR path = NULL;
|
||||
if (SUCCEEDED(hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
|
||||
chosen = utf8(path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
item->Release();
|
||||
}
|
||||
d->Release();
|
||||
}
|
||||
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_CANCELLED)) rt_log("folder_pick: the folder dialog failed (0x%08lx)", (unsigned long)hr);
|
||||
r->done(chosen.empty() ? NULL : chosen.c_str(), r->ctx);
|
||||
if (SUCCEEDED(co)) CoUninitialize();
|
||||
delete r;
|
||||
}
|
||||
|
||||
extern "C" void folder_pick(const char *start, FolderPickDone done, void *ctx) {
|
||||
PickReq *r = new PickReq{start ? start : "", done, ctx};
|
||||
if (!plat_thread_spawn(pick_main, r, "folder dialog", 0)) {
|
||||
rt_log("folder_pick: cannot start the dialog thread");
|
||||
done(NULL, ctx);
|
||||
delete r;
|
||||
}
|
||||
}
|
||||
292
runtime/frame_rate.c
Normal file
292
runtime/frame_rate.c
Normal file
@@ -0,0 +1,292 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/game_patches.c @ ebe621d (MIT, Copyright (c) 2026 MacNativeGames
|
||||
// contributors): runtime_frame_step_update (2338bb0: no 1/120 s floor) and the speed survey's player chain
|
||||
// (world_survey.c player_actor).
|
||||
// Frame rate: the frame limit and the real-time frame step, the host half of the patch sets fps60, realtime and cloth
|
||||
// (build tables patches-v121.txt, patches-v143.txt).
|
||||
//
|
||||
// NX2navi's adaptive present-interval controller (main+0x8f26c8) turns the main thread's measured work into a number of
|
||||
// 60 Hz ticks clamped to [minimum, maximum]; shipping data sets the minimum to 2 (30 fps), the set fps60 lowers it to
|
||||
// 1. The controller still raises the interval when a frame does not fit. Shipping code then steps the game by whole
|
||||
// vsyncs (VFR step main+0x7ede6c: ratio = interval * 0.5, seconds = interval / 60; global ratio at main+0x8f2394), so
|
||||
// game speed is right only when frames take exactly their interval. With the set realtime both read the patch page
|
||||
// instead: +0 ratio = seconds * 30, +4 seconds (1.4.3: +8 interval = seconds * 60, the producer's nominal interval
|
||||
// before its half-step coefficient), the moving average of the last 5 display intervals, at most 1/15 s
|
||||
// (slower frames slow the game down, as on the console) and at least 1/240 s, so a higher frame-rate cap only changes
|
||||
// the pacer (frame_step_cap_ns, NX2NAVI_FPS_LIMIT). The set cloth gives hair and cloth the same clock and needs realtime.
|
||||
//
|
||||
// The floor: with a shorter step (ratio below 0.125) 1.4.3 aborts within seconds of some save loads (issue #241, a
|
||||
// 280 Hz monitor): Havok's hkDefaultMemoryImpl reports "Out of memory" for the buffer the physics pre-solve (profiler
|
||||
// scope "TtPreSolve") resizes in main+0x1261630. Forced steps on the Depths save: 1/240 s plays, 1/244 s and shorter
|
||||
// abort; the field, sky island and fight saves play at 1/280 s, the town save aborts after about 800 frames. The
|
||||
// same short steps also crash a physics worker reading that buffer's contacts: an access violation in main+0x1292560
|
||||
// or its caller main+0x1286870 (narrowphase contact setup, reached from the grid task main+0x12f1ea0), reading a
|
||||
// Havok body's shape at body+0x78 as null or garbage (issues #29 and #92, 240 Hz monitors with fps_limit = display).
|
||||
// Steps drawn from 1/252..1/230 s crash the Depths load there in seconds; 1/240..1/230 s plays. The frame limit
|
||||
// stops at 240 too (LIMIT_MAX_HZ), so the floor only catches the average dipping under 1/240 s at that limit: a
|
||||
// present late by less than a period keeps the pacer's phase (nvn_api_obj.c, pace), so the next display interval is
|
||||
// shorter than the period. No limit makes the game run slower than the clock.
|
||||
//
|
||||
// The sample is the display time the pacer gives each present (nvnQueuePresentTexture), so the step follows the
|
||||
// frames as shown, pacing waits included. With the step on, the pacer ignores the game's interval and holds a frame
|
||||
// only to the frame limit (nvn_api_obj.c present_period): the controller's 2 or 3 vsyncs would only add latency. The
|
||||
// vsync event stays at 60 Hz whatever the limit: the game does not wait for it each frame (1.4.3 shows its title at 144
|
||||
// fps with the event at 60 Hz).
|
||||
// One 64-bit and one 32-bit store per present; the guest reads the floats on its own threads, and each stays finite
|
||||
// and in range whichever store it sees.
|
||||
//
|
||||
// The world clock (frame_step_world_us) sums the steps from the first present that finds the player actor away from
|
||||
// the origin, where the title screen keeps it: the end of the first save load. Recorded and scripted input
|
||||
// (input.c) runs on it, so a replay presses at the same game time at any frame rate and after a first load of any
|
||||
// length; a later load (fast travel) still shifts what follows by its length.
|
||||
//
|
||||
// NX2NAVI_TRACE=perf adds a line per 120 presents: fps, the game seconds stepped against wall seconds, and with
|
||||
// NX2NAVI_SPEED_PROBE=1 the player's position and ground speed (an independent check: Link runs at the same speed at
|
||||
// any frame rate when game time follows wall time).
|
||||
#include <math.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "layout.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
#define STEP_SAMPLES 5
|
||||
#define STEP_MAX_S (1.0 / 15.0) // longest step; a longer frame slows the game down
|
||||
#define STEP_RESET_S 0.25 // a gap this long (loading, a debugger) restarts the average
|
||||
#define STEP_FIRST_S (1.0 / 60.0)
|
||||
#define STEP_MIN_S (1.0 / 240.0) // shortest step; a shorter one makes the game's physics run out of memory
|
||||
|
||||
static _Atomic uint64_t *step_word; // patch page +0: ratio (low float), seconds (high float)
|
||||
static double step_now = STEP_FIRST_S; // the step the patch page holds: what the game steps each frame
|
||||
static _Atomic uint32_t *step_interval; // patch page +8: seconds * 60 (patches-v143.txt realtime)
|
||||
static uint64_t last_ns;
|
||||
static double samples[STEP_SAMPLES];
|
||||
static unsigned sample_count, sample_next;
|
||||
|
||||
static void step_store(float seconds) {
|
||||
step_now = seconds;
|
||||
float ratio = seconds * 30.0f, interval = seconds * 60.0f;
|
||||
uint32_t lo, hi, iv;
|
||||
memcpy(&lo, &ratio, 4);
|
||||
memcpy(&hi, &seconds, 4);
|
||||
memcpy(&iv, &interval, 4);
|
||||
atomic_store_explicit(step_word, (uint64_t)lo | (uint64_t)hi << 32, memory_order_release);
|
||||
atomic_store_explicit(step_interval, iv, memory_order_release);
|
||||
}
|
||||
|
||||
static void frame_limit_live(void);
|
||||
|
||||
void frame_step_init(void) {
|
||||
int fps60 = rt_patch_set("fps60"), realtime = rt_patch_set("realtime"), cloth = rt_patch_set("cloth");
|
||||
if (realtime < 0) {
|
||||
rt_log("frame rate: no patch table, shipping 30 fps whole-vsync steps");
|
||||
return;
|
||||
}
|
||||
if (!rt_patch_enabled(realtime) && rt_patch_enabled(cloth)) {
|
||||
rt_patch_disable(cloth);
|
||||
rt_log("frame rate: cloth clock off, it needs the real-time step (NX2NAVI_REALTIME=0)");
|
||||
}
|
||||
if (rt_patch_enabled(realtime)) {
|
||||
step_word = (_Atomic uint64_t *)rt_patch_page();
|
||||
step_interval = (_Atomic uint32_t *)((uint8_t *)rt_patch_page() + 8);
|
||||
step_store((float)STEP_FIRST_S);
|
||||
}
|
||||
frame_limit_live(); // NX2NAVI_FPS_LIMIT decides the set fps60 from the start
|
||||
rt_log("frame rate: present interval %s, %s", rt_patch_enabled(fps60) ? "1..3 (above 30 fps)" : "2..3 (shipping 30 fps)",
|
||||
step_word ? "real-time step (5-frame average of display intervals, at most 1/15 s)" : "whole-vsync steps");
|
||||
}
|
||||
|
||||
int frame_step_realtime(void) { return step_word != NULL; }
|
||||
|
||||
// The frame limit (menu, NX2NAVI_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 240, or display (the
|
||||
// refresh rate of the window's monitor, at most 240). A higher number gives 240, the rate of the shortest step the
|
||||
// game survives (STEP_MIN_S). Unset, the older NX2NAVI_FPS60=0 still means 30. A limit of 30 switches the set
|
||||
// fps60 off (the shipping minimum interval 2), every other limit switches it on; the controller reads its minimum
|
||||
// interval from the patched sites each frame. With the real-time step the pacer holds frames to the limit; above 60
|
||||
// only the pacer changes, as the step's floor is the limit's ceiling. Without it the game steps whole 60 Hz vsyncs and the pacer
|
||||
// follows the game's interval, so the limit stops at 60.
|
||||
#define LIMIT_MIN_HZ 30
|
||||
#define LIMIT_MAX_HZ 240 // 1 / STEP_MIN_S
|
||||
static int fps60_set = -2;
|
||||
static unsigned limit_seen;
|
||||
static int limit_read;
|
||||
static int limit_hz = -1; // 0: display; -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;
|
||||
char *end;
|
||||
long n = strtol(e, &end, 10);
|
||||
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);
|
||||
return 60;
|
||||
}
|
||||
|
||||
static void frame_limit_live(void) {
|
||||
unsigned v = cfg_version();
|
||||
if (limit_read && v == limit_seen) return;
|
||||
limit_read = 1;
|
||||
limit_seen = v;
|
||||
int hz = limit_parse();
|
||||
if (hz == limit_hz) return;
|
||||
limit_hz = hz;
|
||||
if (fps60_set == -2) fps60_set = rt_patch_set("fps60");
|
||||
if (fps60_set >= 0) rt_patch_switch(fps60_set, hz != 30);
|
||||
if ((hz > 60 || hz == 0) && !step_word)
|
||||
rt_log("frame rate: limits above 60 need the real-time step (NX2NAVI_REALTIME=1); 60 fps");
|
||||
else if (hz) rt_log("frame rate: limit %d fps", hz);
|
||||
else if (gfx_window_refresh_hz() > 0) rt_log("frame rate: limit at the monitor's refresh rate (%.3f Hz now)", gfx_window_refresh_hz());
|
||||
else rt_log("frame rate: limit at the monitor's refresh rate (60 until the window measures it)");
|
||||
}
|
||||
|
||||
uint64_t frame_step_cap_ns(void) {
|
||||
frame_limit_live();
|
||||
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;
|
||||
return (uint64_t)(1e9 / hz + 0.5);
|
||||
}
|
||||
|
||||
// The frame rate the limit gives, for the performance overlay: the setting, the monitor's rate for "display", 60 at
|
||||
// most without the real-time step; 0 before the first frame read the setting.
|
||||
double frame_step_limit_hz(void) {
|
||||
int hz = limit_hz;
|
||||
if (hz < 0) return 0;
|
||||
double r = hz ? hz : gfx_window_refresh_hz();
|
||||
if (r < LIMIT_MIN_HZ) r = 60;
|
||||
return !step_word && r > 60 ? 60 : r > LIMIT_MAX_HZ ? LIMIT_MAX_HZ : r;
|
||||
}
|
||||
|
||||
// ---- NX2NAVI_TRACE=perf speed lines
|
||||
static uint64_t win_t0, win_frames, win_hist[4]; // win_hist: frames by present interval 1, 2, 3+ ([0] unused)
|
||||
static double win_game;
|
||||
static int probe = -1;
|
||||
static float last_pos[3];
|
||||
static uint64_t last_pos_ns;
|
||||
|
||||
static int read_guest(uint64_t a, void *out, size_t n) { return a && plat_mem_read(a, out, n); }
|
||||
static uint64_t read_ptr(uint64_t a) {
|
||||
uint64_t p = 0;
|
||||
return read_guest(a, &p, 8) ? p : 0;
|
||||
}
|
||||
// 1.2.1: [main+0x4728538] (1.1.0: [main+0x4630a68], 1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13 lists the
|
||||
// resident actors; the one named "Player" has its position at +0x2b4 (MacNativeGames world_survey.c, game_version.h
|
||||
// NX2NAVI_GAME_V121/V143). The 1.1.0 slot was measured from its own dereference chain: 181 scene accesses in that build
|
||||
// read it, and one of them reproduces this walk (the 14-component check, component 13, the 0x70 stride and the name
|
||||
// compare) instruction for instruction.
|
||||
static int player_pos(float xyz[3]) {
|
||||
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : RT_V110 ? 0x4630a68 : 0x4728538)),
|
||||
scene = module ? read_ptr(module + 0x1e8) : 0;
|
||||
int32_t count = 0;
|
||||
if (!scene || !read_guest(scene + 0x50, &count, 4) || count < 14 || count > 256) return 0;
|
||||
uint64_t comps = read_ptr(scene + 0x58), resident = comps ? read_ptr(comps + 13 * 8) : 0;
|
||||
if (!resident || !read_guest(resident + 0x20, &count, 4) || count < 1 || count > 64) return 0;
|
||||
uint64_t links = read_ptr(resident + 0x28);
|
||||
for (int i = 0; links && i < count; i++) {
|
||||
uint64_t data = read_ptr(links + (uint64_t)i * 0x70 + 8), actor = data ? read_ptr(data + 0x40) : 0;
|
||||
uint64_t name = actor ? read_ptr(actor + 0x218) : 0;
|
||||
char text[7];
|
||||
if (name && read_guest(name, text, 7) && !memcmp(text, "Player", 7))
|
||||
return read_guest(actor + 0x2b4, xyz, 12) && isfinite(xyz[0]) && isfinite(xyz[2]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void speed_report(uint64_t now, double step, int interval) {
|
||||
if (!win_t0) {
|
||||
win_t0 = now;
|
||||
return;
|
||||
}
|
||||
win_frames++;
|
||||
win_game += step;
|
||||
win_hist[interval < 1 ? 1 : interval > 3 ? 3 : interval]++;
|
||||
if (win_frames < 120) return;
|
||||
if (probe < 0) {
|
||||
const char *e = cfg_get("NX2NAVI_SPEED_PROBE");
|
||||
probe = e && !strcmp(e, "1");
|
||||
}
|
||||
double wall = (double)(now - win_t0) * 1e-9;
|
||||
char pos[160] = "";
|
||||
float p[3];
|
||||
if (probe && player_pos(p)) {
|
||||
double d = last_pos_ns ? hypot(p[0] - last_pos[0], p[2] - last_pos[2]) : 0,
|
||||
s = last_pos_ns ? (double)(now - last_pos_ns) * 1e-9 : 0;
|
||||
snprintf(pos, sizeof pos, "; player %.1f %.1f %.1f, %.2f m in %.3f s = %.2f m/s", p[0], p[1], p[2], d, s,
|
||||
s > 0 ? d / s : 0);
|
||||
memcpy(last_pos, p, sizeof p);
|
||||
last_pos_ns = now;
|
||||
}
|
||||
if (step_word)
|
||||
rt_log("fps: %llu frames in %.3f s (%.1f fps), %.3f s game stepped, speed %.3f; interval 1/2/3+ %llu/%llu/%llu%s",
|
||||
(unsigned long long)win_frames, wall, (double)win_frames / wall, win_game, win_game / wall,
|
||||
(unsigned long long)win_hist[1], (unsigned long long)win_hist[2], (unsigned long long)win_hist[3], pos);
|
||||
else
|
||||
rt_log("fps: %llu frames in %.3f s (%.1f fps), whole-vsync steps; interval 1/2/3+ %llu/%llu/%llu%s",
|
||||
(unsigned long long)win_frames, wall, (double)win_frames / wall, (unsigned long long)win_hist[1],
|
||||
(unsigned long long)win_hist[2], (unsigned long long)win_hist[3], pos);
|
||||
win_t0 = now;
|
||||
win_frames = 0;
|
||||
win_game = 0;
|
||||
memset(win_hist, 0, sizeof win_hist);
|
||||
}
|
||||
|
||||
static _Atomic int64_t world_us = -1;
|
||||
static _Atomic uint64_t game_us; // game time stepped since the first present (frame_step_ticks)
|
||||
static uint64_t presents;
|
||||
|
||||
int64_t frame_step_world_us(void) { return atomic_load_explicit(&world_us, memory_order_relaxed); }
|
||||
uint64_t frame_step_ticks(void) { return atomic_load_explicit(&game_us, memory_order_relaxed) * 60 / 1000000; }
|
||||
|
||||
static void world_clock(double dt, double step) {
|
||||
presents++;
|
||||
int64_t w = atomic_load_explicit(&world_us, memory_order_relaxed);
|
||||
if (w < 0) {
|
||||
float p[3];
|
||||
if (!player_pos(p) || fabsf(p[0]) + fabsf(p[2]) < 1.0f) return; // a few guest reads, until the load ends
|
||||
rt_log("frame step: world clock starts at present %llu, %.2f s after the guest started (the player at %.1f %.1f %.1f)",
|
||||
(unsigned long long)presents, rt_guest_start_ns ? (kern_now_ns() - rt_guest_start_ns) / 1e9 : 0.0, p[0],
|
||||
p[1], p[2]);
|
||||
atomic_store_explicit(&world_us, 0, memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
if (!step_word) step = dt > STEP_MAX_S ? STEP_MAX_S : dt; // whole-vsync steps: the display time stands in
|
||||
atomic_store_explicit(&world_us, w + (int64_t)(step * 1e6 + 0.5), memory_order_relaxed);
|
||||
}
|
||||
|
||||
void frame_step_present(uint64_t display_ns, int interval) {
|
||||
frame_limit_live();
|
||||
double step = 0, dt = last_ns ? (double)(display_ns - last_ns) * 1e-9 : 0;
|
||||
if (step_word && last_ns) {
|
||||
if (dt > STEP_RESET_S || dt < 0.001) {
|
||||
sample_count = sample_next = 0;
|
||||
} else {
|
||||
if (dt > STEP_MAX_S) dt = STEP_MAX_S;
|
||||
samples[sample_next++ % STEP_SAMPLES] = dt;
|
||||
if (sample_count < STEP_SAMPLES) sample_count++;
|
||||
double sum = 0;
|
||||
for (unsigned i = 0; i < sample_count; i++) sum += samples[i];
|
||||
step = sum / sample_count;
|
||||
if (step < STEP_MIN_S) step = STEP_MIN_S;
|
||||
step_store((float)step);
|
||||
}
|
||||
}
|
||||
last_ns = display_ns;
|
||||
// The game clock of untimed script lines: what the game stepped this frame, the patch page's step with the
|
||||
// real-time step (kept through a reset), whole vsyncs without it.
|
||||
double game = step_word ? step_now : (interval > 0 ? interval : 1) / 60.0;
|
||||
atomic_store_explicit(&game_us, atomic_load_explicit(&game_us, memory_order_relaxed) + (uint64_t)(game * 1e6 + 0.5),
|
||||
memory_order_relaxed);
|
||||
world_clock(dt, step);
|
||||
if (rt_trace & RT_TRACE_PERF) speed_report(display_ns, step, interval);
|
||||
}
|
||||
60
runtime/fs.h
Normal file
60
runtime/fs.h
Normal file
@@ -0,0 +1,60 @@
|
||||
// Adapted from MacNativeGames games/<game>/runtime/fs_host.h @ b75be8cf (MIT, Copyright (c) 2026 MacNativeGames
|
||||
// contributors).
|
||||
// The nn::fs host layer: the read-only RomFS (romfs.c) and writable save, temporary and
|
||||
// cache storage under the profile directory (fs_host.c). Both register their nn::fs overrides with HOST_OVERRIDE;
|
||||
// this header is what the two files and the probe share.
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <wchar.h>
|
||||
|
||||
// ---- nn::fs results
|
||||
#define FS_R_PATH_NOT_FOUND 0x202u
|
||||
#define FS_R_ALREADY_EXISTS 0x402u
|
||||
#define FS_R_NOT_EMPTY 0x1002u
|
||||
#define FS_R_UNSUPPORTED_OPEN 0x6402u // RomFS: any open mode but read
|
||||
#define FS_R_INVALID 0xe402u // RomFS: bad handle, offset or argument
|
||||
#define FS_R_TARGET_NOT_FOUND 0x7d402u
|
||||
#define FS_R_INVALID_ARGUMENT 0x2ee202u
|
||||
#define FS_R_INVALID_PATH 0x2ee602u
|
||||
#define FS_R_OPEN_MODE 0x307402u
|
||||
#define FS_R_APPEND_REQUIRED 0x2f5e02u
|
||||
|
||||
enum { FS_OPEN_READ = 1, FS_OPEN_WRITE = 2, FS_OPEN_APPEND = 4 }; // nn::fs::OpenMode
|
||||
enum { FS_ENTRY_DIRECTORY = 0, FS_ENTRY_FILE = 1 }; // nn::fs::DirectoryEntryType
|
||||
|
||||
// ---- romfs.c
|
||||
// Handles 0x1_0000_0000 + j; host storage uses 0x2_0000_0000 + j (files) and 0x3_0000_0000 + j (directories).
|
||||
#define ROMFS_HANDLE_BASE 0x100000000ull
|
||||
int romfs_owns(const char *path); // path is on the mounted RomFS ("<mount>:...")
|
||||
int romfs_handle(uint64_t h); // h is a RomFS file handle
|
||||
uint32_t romfs_mount(const char *name); // nn::fs::MountRom; a second name is fatal
|
||||
uint32_t romfs_open(uint64_t *out, const char *path, int mode);
|
||||
uint32_t romfs_read(uint64_t *out, uint64_t h, int64_t offset, void *buffer, uint64_t size);
|
||||
uint32_t romfs_size(int64_t *out, uint64_t h);
|
||||
void romfs_close(uint64_t h);
|
||||
uint32_t romfs_entry_type(int *out, const char *path);
|
||||
uint64_t romfs_file_count(void); // files in the index (fs_init first)
|
||||
const char *romfs_entry(uint64_t i, uint64_t *offset, uint64_t *size); // path, image offset and size of index row i
|
||||
const char *romfs_image_path(void); // the romfs.bin path fs_init opened
|
||||
size_t romfs_overlay_count(void);
|
||||
int romfs_patches_loaded(void); // a romfs-patches manifest gave at least one file
|
||||
// The button prompts (input_prompts_folder): the folder loaded at start, NULL for the game's own prompts; whether a
|
||||
// folder had a manifest at start; and why the folder the settings asked for at start was not loaded (ROMFS_PROMPTS_OK
|
||||
// for any other folder: it was not asked for). romfs-patches-pc goes by that name also where romfs-patches-pc-<w>x<h>
|
||||
// stands in for it (romfs.c overlay_init).
|
||||
enum { ROMFS_PROMPTS_OK, ROMFS_PROMPTS_MISSING, ROMFS_PROMPTS_FAILED, ROMFS_PROMPTS_SHAPE, ROMFS_PROMPTS_MODS,
|
||||
ROMFS_PROMPTS_PATCHES };
|
||||
const char *romfs_prompts(void);
|
||||
int romfs_prompts_ready(const char *folder);
|
||||
int romfs_prompts_off(const char *folder);
|
||||
const char *romfs_overlay(size_t k, const char **file, uint64_t *size, int *is_added); // RomFS path of overlay k
|
||||
void fs_trace(const char *what, const char *path); // NX2NAVI_TRACE=fs: one line per open
|
||||
|
||||
// ---- fs_host.c
|
||||
void fs_host_set_profile(const char *dir); // profile directory; before the first storage mount only
|
||||
// (default rt_config()->profile)
|
||||
int fs_save_busy(void); // a save is being written: a save file open for writing,
|
||||
// or save data changed in the last 3 s and not committed
|
||||
void fs_save_flush(void); // wait for a save Commit still running in the background;
|
||||
// every exit path calls it before the process ends
|
||||
1791
runtime/fs_host.c
Normal file
1791
runtime/fs_host.c
Normal file
File diff suppressed because it is too large
Load Diff
1832
runtime/fsr_easu_spv.h
Normal file
1832
runtime/fsr_easu_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
424
runtime/fsr_rcas_spv.h
Normal file
424
runtime/fsr_rcas_spv.h
Normal file
@@ -0,0 +1,424 @@
|
||||
// Generated by tools/build_shaders.py from runtime/shaders/fsr_rcas.comp; do not edit.
|
||||
// source sha256 6ea46d4c41d9b70c0fffb44b34b812476b6aa6a840f86f941d106d4f05d48ce0
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
static const uint32_t fsr_rcas_spv[] = {
|
||||
0x07230203, 0x00010300, 0x000d000b, 0x00001186, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
|
||||
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
|
||||
0x0007000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000030c, 0x00000312, 0x00060010,
|
||||
0x00000004, 0x00000011, 0x00000040, 0x00000001, 0x00000001, 0x00040047, 0x000000b8, 0x00000021,
|
||||
0x00000000, 0x00040047, 0x000000b8, 0x00000022, 0x00000000, 0x00030047, 0x000002d3, 0x00000002,
|
||||
0x00050048, 0x000002d3, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x000002f9, 0x00000019,
|
||||
0x00040047, 0x000002f9, 0x00000021, 0x00000001, 0x00040047, 0x000002f9, 0x00000022, 0x00000000,
|
||||
0x00040047, 0x0000030c, 0x0000000b, 0x0000001b, 0x00040047, 0x00000312, 0x0000000b, 0x0000001a,
|
||||
0x00040047, 0x0000033e, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
|
||||
0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040015, 0x0000000c, 0x00000020, 0x00000000,
|
||||
0x00040017, 0x00000031, 0x0000000c, 0x00000002, 0x00040015, 0x00000036, 0x00000020, 0x00000001,
|
||||
0x00040017, 0x00000037, 0x00000036, 0x00000002, 0x00040017, 0x00000039, 0x00000006, 0x00000004,
|
||||
0x0004002b, 0x00000036, 0x00000069, 0x00000000, 0x0004002b, 0x00000006, 0x0000007d, 0x3f800000,
|
||||
0x0004002b, 0x00000006, 0x00000085, 0x00000000, 0x0004002b, 0x0000000c, 0x0000008e, 0x7ef19fff,
|
||||
0x0004002b, 0x00000006, 0x0000009a, 0x40000000, 0x0004002b, 0x0000000c, 0x000000a1, 0x00000001,
|
||||
0x00090019, 0x000000b5, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
|
||||
0x00000000, 0x0003001b, 0x000000b6, 0x000000b5, 0x00040020, 0x000000b7, 0x00000000, 0x000000b6,
|
||||
0x0004003b, 0x000000b7, 0x000000b8, 0x00000000, 0x0004002b, 0x0000000c, 0x000000c9, 0x00000000,
|
||||
0x0004002b, 0x0000000c, 0x000000ce, 0x00000002, 0x0004002b, 0x00000036, 0x000000d8, 0xffffffff,
|
||||
0x0005002c, 0x00000037, 0x000000d9, 0x00000069, 0x000000d8, 0x0005002c, 0x00000037, 0x000000e0,
|
||||
0x000000d8, 0x00000069, 0x0004002b, 0x00000036, 0x000000ec, 0x00000001, 0x0005002c, 0x00000037,
|
||||
0x000000ed, 0x000000ec, 0x00000069, 0x0005002c, 0x00000037, 0x000000f4, 0x00000069, 0x000000ec,
|
||||
0x0004002b, 0x00000006, 0x00000213, 0xc0800000, 0x0004002b, 0x00000006, 0x00000219, 0x40800000,
|
||||
0x0004002b, 0x00000006, 0x0000027a, 0xbe400000, 0x00020014, 0x000002d0, 0x0003001e, 0x000002d3,
|
||||
0x00000039, 0x00040020, 0x000002d4, 0x00000009, 0x000002d3, 0x0004003b, 0x000002d4, 0x000002d5,
|
||||
0x00000009, 0x00040020, 0x000002d6, 0x00000009, 0x00000006, 0x00090019, 0x000002f7, 0x00000006,
|
||||
0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002, 0x00000002, 0x00040020, 0x000002f8,
|
||||
0x00000000, 0x000002f7, 0x0004003b, 0x000002f8, 0x000002f9, 0x00000000, 0x00040017, 0x0000030a,
|
||||
0x0000000c, 0x00000003, 0x00040020, 0x0000030b, 0x00000001, 0x0000030a, 0x0004003b, 0x0000030b,
|
||||
0x0000030c, 0x00000001, 0x00040020, 0x0000030e, 0x00000001, 0x0000000c, 0x0004003b, 0x0000030b,
|
||||
0x00000312, 0x00000001, 0x0004002b, 0x0000000c, 0x00000315, 0x00000004, 0x0004002b, 0x0000000c,
|
||||
0x00000321, 0x00000008, 0x0004002b, 0x0000000c, 0x0000033d, 0x00000040, 0x0006002c, 0x0000030a,
|
||||
0x0000033e, 0x0000033d, 0x000000a1, 0x000000a1, 0x0004002b, 0x00000036, 0x0000115c, 0x00000003,
|
||||
0x0004002b, 0x00000006, 0x0000115d, 0x3e800000, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
|
||||
0x00000003, 0x000200f8, 0x00000005, 0x00060041, 0x000002d6, 0x00000305, 0x000002d5, 0x00000069,
|
||||
0x000000ce, 0x0004003d, 0x00000006, 0x00000306, 0x00000305, 0x0004007f, 0x00000006, 0x00000348,
|
||||
0x00000306, 0x0006000c, 0x00000006, 0x00000349, 0x00000001, 0x0000001d, 0x00000348, 0x00050041,
|
||||
0x0000030e, 0x0000030f, 0x0000030c, 0x000000c9, 0x0004003d, 0x0000000c, 0x00000310, 0x0000030f,
|
||||
0x000600cb, 0x0000000c, 0x0000036d, 0x00000310, 0x000000ec, 0x0000115c, 0x000600cb, 0x0000000c,
|
||||
0x00000375, 0x00000310, 0x0000115c, 0x0000115c, 0x000700c9, 0x0000000c, 0x0000037c, 0x00000375,
|
||||
0x00000310, 0x00000069, 0x000000ec, 0x00050050, 0x00000031, 0x00000365, 0x0000036d, 0x0000037c,
|
||||
0x00050041, 0x0000030e, 0x00000313, 0x00000312, 0x000000c9, 0x0004003d, 0x0000000c, 0x00000314,
|
||||
0x00000313, 0x000500c4, 0x0000000c, 0x00000316, 0x00000314, 0x00000315, 0x00050041, 0x0000030e,
|
||||
0x00000317, 0x00000312, 0x000000a1, 0x0004003d, 0x0000000c, 0x00000318, 0x00000317, 0x000500c4,
|
||||
0x0000000c, 0x00000319, 0x00000318, 0x00000315, 0x00050050, 0x00000031, 0x0000031a, 0x00000316,
|
||||
0x00000319, 0x00050080, 0x00000031, 0x0000031b, 0x00000365, 0x0000031a, 0x000300f7, 0x000003a9,
|
||||
0x00000000, 0x000300fb, 0x000000c9, 0x00000385, 0x000200f8, 0x00000385, 0x00050051, 0x0000000c,
|
||||
0x00000387, 0x0000031b, 0x00000000, 0x00060041, 0x000002d6, 0x00000388, 0x000002d5, 0x00000069,
|
||||
0x000000c9, 0x0004003d, 0x00000006, 0x00000389, 0x00000388, 0x0004006d, 0x0000000c, 0x0000038a,
|
||||
0x00000389, 0x000500ae, 0x000002d0, 0x0000038b, 0x00000387, 0x0000038a, 0x000400a8, 0x000002d0,
|
||||
0x0000038c, 0x0000038b, 0x000300f7, 0x00000394, 0x00000000, 0x000400fa, 0x0000038c, 0x0000038d,
|
||||
0x00000394, 0x000200f8, 0x0000038d, 0x00050051, 0x0000000c, 0x0000038f, 0x0000031b, 0x00000001,
|
||||
0x00060041, 0x000002d6, 0x00000390, 0x000002d5, 0x00000069, 0x000000a1, 0x0004003d, 0x00000006,
|
||||
0x00000391, 0x00000390, 0x0004006d, 0x0000000c, 0x00000392, 0x00000391, 0x000500ae, 0x000002d0,
|
||||
0x00000393, 0x0000038f, 0x00000392, 0x000200f9, 0x00000394, 0x000200f8, 0x00000394, 0x000700f5,
|
||||
0x000002d0, 0x00000395, 0x0000038b, 0x00000385, 0x00000393, 0x0000038d, 0x000300f7, 0x00000397,
|
||||
0x00000000, 0x000400fa, 0x00000395, 0x00000396, 0x00000397, 0x000200f8, 0x00000396, 0x000200f9,
|
||||
0x000003a9, 0x000200f8, 0x00000397, 0x0004007c, 0x00000037, 0x00000431, 0x0000031b, 0x00050080,
|
||||
0x00000037, 0x00000433, 0x00000431, 0x000000d9, 0x0004003d, 0x000000b6, 0x0000059d, 0x000000b8,
|
||||
0x00040064, 0x000000b5, 0x0000059f, 0x0000059d, 0x0007005f, 0x00000039, 0x000005a0, 0x0000059f,
|
||||
0x00000433, 0x00000002, 0x00000069, 0x00050080, 0x00000037, 0x00000437, 0x00000431, 0x000000e0,
|
||||
0x0004003d, 0x000000b6, 0x000005a3, 0x000000b8, 0x00040064, 0x000000b5, 0x000005a5, 0x000005a3,
|
||||
0x0007005f, 0x00000039, 0x000005a6, 0x000005a5, 0x00000437, 0x00000002, 0x00000069, 0x0004003d,
|
||||
0x000000b6, 0x000005a9, 0x000000b8, 0x00040064, 0x000000b5, 0x000005ab, 0x000005a9, 0x0007005f,
|
||||
0x00000039, 0x000005ac, 0x000005ab, 0x00000431, 0x00000002, 0x00000069, 0x00050080, 0x00000037,
|
||||
0x0000043e, 0x00000431, 0x000000ed, 0x0004003d, 0x000000b6, 0x000005af, 0x000000b8, 0x00040064,
|
||||
0x000000b5, 0x000005b1, 0x000005af, 0x0007005f, 0x00000039, 0x000005b2, 0x000005b1, 0x0000043e,
|
||||
0x00000002, 0x00000069, 0x00050080, 0x00000037, 0x00000442, 0x00000431, 0x000000f4, 0x0004003d,
|
||||
0x000000b6, 0x000005b5, 0x000000b8, 0x00040064, 0x000000b5, 0x000005b7, 0x000005b5, 0x0007005f,
|
||||
0x00000039, 0x000005b8, 0x000005b7, 0x00000442, 0x00000002, 0x00000069, 0x00050051, 0x00000006,
|
||||
0x00000446, 0x000005a0, 0x00000000, 0x00050051, 0x00000006, 0x00000448, 0x000005a0, 0x00000001,
|
||||
0x00050051, 0x00000006, 0x0000044a, 0x000005a0, 0x00000002, 0x00050051, 0x00000006, 0x0000044c,
|
||||
0x000005a6, 0x00000000, 0x00050051, 0x00000006, 0x0000044e, 0x000005a6, 0x00000001, 0x00050051,
|
||||
0x00000006, 0x00000450, 0x000005a6, 0x00000002, 0x00050051, 0x00000006, 0x00000452, 0x000005ac,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x00000454, 0x000005ac, 0x00000001, 0x00050051, 0x00000006,
|
||||
0x00000456, 0x000005ac, 0x00000002, 0x00050051, 0x00000006, 0x00000458, 0x000005b2, 0x00000000,
|
||||
0x00050051, 0x00000006, 0x0000045a, 0x000005b2, 0x00000001, 0x00050051, 0x00000006, 0x0000045c,
|
||||
0x000005b2, 0x00000002, 0x00050051, 0x00000006, 0x0000045e, 0x000005b8, 0x00000000, 0x00050051,
|
||||
0x00000006, 0x00000460, 0x000005b8, 0x00000001, 0x00050051, 0x00000006, 0x00000462, 0x000005b8,
|
||||
0x00000002, 0x0007000c, 0x00000006, 0x00000635, 0x00000001, 0x00000025, 0x0000044c, 0x00000458,
|
||||
0x0007000c, 0x00000006, 0x00000636, 0x00000001, 0x00000025, 0x00000446, 0x00000635, 0x0007000c,
|
||||
0x00000006, 0x000004e2, 0x00000001, 0x00000025, 0x00000636, 0x0000045e, 0x0007000c, 0x00000006,
|
||||
0x0000063c, 0x00000001, 0x00000025, 0x0000044e, 0x0000045a, 0x0007000c, 0x00000006, 0x0000063d,
|
||||
0x00000001, 0x00000025, 0x00000448, 0x0000063c, 0x0007000c, 0x00000006, 0x000004e8, 0x00000001,
|
||||
0x00000025, 0x0000063d, 0x00000460, 0x0007000c, 0x00000006, 0x00000643, 0x00000001, 0x00000025,
|
||||
0x00000450, 0x0000045c, 0x0007000c, 0x00000006, 0x00000644, 0x00000001, 0x00000025, 0x0000044a,
|
||||
0x00000643, 0x0007000c, 0x00000006, 0x000004ee, 0x00000001, 0x00000025, 0x00000644, 0x00000462,
|
||||
0x0007000c, 0x00000006, 0x0000064a, 0x00000001, 0x00000028, 0x0000044c, 0x00000458, 0x0007000c,
|
||||
0x00000006, 0x0000064b, 0x00000001, 0x00000028, 0x00000446, 0x0000064a, 0x0007000c, 0x00000006,
|
||||
0x000004f4, 0x00000001, 0x00000028, 0x0000064b, 0x0000045e, 0x0007000c, 0x00000006, 0x00000651,
|
||||
0x00000001, 0x00000028, 0x0000044e, 0x0000045a, 0x0007000c, 0x00000006, 0x00000652, 0x00000001,
|
||||
0x00000028, 0x00000448, 0x00000651, 0x0007000c, 0x00000006, 0x000004fa, 0x00000001, 0x00000028,
|
||||
0x00000652, 0x00000460, 0x0007000c, 0x00000006, 0x00000658, 0x00000001, 0x00000028, 0x00000450,
|
||||
0x0000045c, 0x0007000c, 0x00000006, 0x00000659, 0x00000001, 0x00000028, 0x0000044a, 0x00000658,
|
||||
0x0007000c, 0x00000006, 0x00000500, 0x00000001, 0x00000028, 0x00000659, 0x00000462, 0x0007000c,
|
||||
0x00000006, 0x00000503, 0x00000001, 0x00000025, 0x000004e2, 0x00000452, 0x00050088, 0x00000006,
|
||||
0x00000662, 0x0000115d, 0x000004f4, 0x00050085, 0x00000006, 0x00000508, 0x00000503, 0x00000662,
|
||||
0x0007000c, 0x00000006, 0x0000050b, 0x00000001, 0x00000025, 0x000004e8, 0x00000454, 0x00050088,
|
||||
0x00000006, 0x0000066e, 0x0000115d, 0x000004fa, 0x00050085, 0x00000006, 0x00000510, 0x0000050b,
|
||||
0x0000066e, 0x0007000c, 0x00000006, 0x00000513, 0x00000001, 0x00000025, 0x000004ee, 0x00000456,
|
||||
0x00050088, 0x00000006, 0x0000067a, 0x0000115d, 0x00000500, 0x00050085, 0x00000006, 0x00000518,
|
||||
0x00000513, 0x0000067a, 0x0007000c, 0x00000006, 0x0000051d, 0x00000001, 0x00000028, 0x000004f4,
|
||||
0x00000452, 0x00050083, 0x00000006, 0x0000051e, 0x0000007d, 0x0000051d, 0x00050085, 0x00000006,
|
||||
0x00000521, 0x00000219, 0x000004e2, 0x00050081, 0x00000006, 0x00000524, 0x00000521, 0x00000213,
|
||||
0x00050088, 0x00000006, 0x00000686, 0x0000007d, 0x00000524, 0x00050085, 0x00000006, 0x00000526,
|
||||
0x0000051e, 0x00000686, 0x0007000c, 0x00000006, 0x0000052b, 0x00000001, 0x00000028, 0x000004fa,
|
||||
0x00000454, 0x00050083, 0x00000006, 0x0000052c, 0x0000007d, 0x0000052b, 0x00050085, 0x00000006,
|
||||
0x0000052f, 0x00000219, 0x000004e8, 0x00050081, 0x00000006, 0x00000532, 0x0000052f, 0x00000213,
|
||||
0x00050088, 0x00000006, 0x00000692, 0x0000007d, 0x00000532, 0x00050085, 0x00000006, 0x00000534,
|
||||
0x0000052c, 0x00000692, 0x0007000c, 0x00000006, 0x00000539, 0x00000001, 0x00000028, 0x00000500,
|
||||
0x00000456, 0x00050083, 0x00000006, 0x0000053a, 0x0000007d, 0x00000539, 0x00050085, 0x00000006,
|
||||
0x0000053d, 0x00000219, 0x000004ee, 0x00050081, 0x00000006, 0x00000540, 0x0000053d, 0x00000213,
|
||||
0x00050088, 0x00000006, 0x0000069e, 0x0000007d, 0x00000540, 0x00050085, 0x00000006, 0x00000542,
|
||||
0x0000053a, 0x0000069e, 0x0004007f, 0x00000006, 0x00000544, 0x00000508, 0x0007000c, 0x00000006,
|
||||
0x00000546, 0x00000001, 0x00000028, 0x00000544, 0x00000526, 0x0004007f, 0x00000006, 0x00000548,
|
||||
0x00000510, 0x0007000c, 0x00000006, 0x0000054a, 0x00000001, 0x00000028, 0x00000548, 0x00000534,
|
||||
0x0004007f, 0x00000006, 0x0000054c, 0x00000518, 0x0007000c, 0x00000006, 0x0000054e, 0x00000001,
|
||||
0x00000028, 0x0000054c, 0x00000542, 0x0007000c, 0x00000006, 0x000006aa, 0x00000001, 0x00000028,
|
||||
0x0000054a, 0x0000054e, 0x0007000c, 0x00000006, 0x000006ab, 0x00000001, 0x00000028, 0x00000546,
|
||||
0x000006aa, 0x0007000c, 0x00000006, 0x00000555, 0x00000001, 0x00000025, 0x000006ab, 0x00000085,
|
||||
0x0007000c, 0x00000006, 0x00000556, 0x00000001, 0x00000028, 0x0000027a, 0x00000555, 0x00050085,
|
||||
0x00000006, 0x0000055a, 0x00000556, 0x00000349, 0x00050085, 0x00000006, 0x0000055d, 0x00000219,
|
||||
0x0000055a, 0x00050081, 0x00000006, 0x0000055f, 0x0000055d, 0x0000007d, 0x0004007c, 0x0000000c,
|
||||
0x000006bc, 0x0000055f, 0x00050082, 0x0000000c, 0x000006bd, 0x0000008e, 0x000006bc, 0x0004007c,
|
||||
0x00000006, 0x000006be, 0x000006bd, 0x0004007f, 0x00000006, 0x000006c1, 0x000006be, 0x00050085,
|
||||
0x00000006, 0x000006c3, 0x000006c1, 0x0000055f, 0x00050081, 0x00000006, 0x000006c5, 0x000006c3,
|
||||
0x0000009a, 0x00050085, 0x00000006, 0x000006c6, 0x000006be, 0x000006c5, 0x00050081, 0x00000006,
|
||||
0x0000115e, 0x00000446, 0x0000044c, 0x00050081, 0x00000006, 0x0000115f, 0x0000115e, 0x0000045e,
|
||||
0x00050081, 0x00000006, 0x00001160, 0x0000115f, 0x00000458, 0x00050085, 0x00000006, 0x0000056f,
|
||||
0x0000055a, 0x00001160, 0x00050081, 0x00000006, 0x00000571, 0x0000056f, 0x00000452, 0x00050085,
|
||||
0x00000006, 0x00000573, 0x00000571, 0x000006c6, 0x00050081, 0x00000006, 0x00001161, 0x00000448,
|
||||
0x0000044e, 0x00050081, 0x00000006, 0x00001162, 0x00001161, 0x00000460, 0x00050081, 0x00000006,
|
||||
0x00001163, 0x00001162, 0x0000045a, 0x00050085, 0x00000006, 0x00000582, 0x0000055a, 0x00001163,
|
||||
0x00050081, 0x00000006, 0x00000584, 0x00000582, 0x00000454, 0x00050085, 0x00000006, 0x00000586,
|
||||
0x00000584, 0x000006c6, 0x00050081, 0x00000006, 0x00001164, 0x0000044a, 0x00000450, 0x00050081,
|
||||
0x00000006, 0x00001165, 0x00001164, 0x00000462, 0x00050081, 0x00000006, 0x00001166, 0x00001165,
|
||||
0x0000045c, 0x00050085, 0x00000006, 0x00000595, 0x0000055a, 0x00001166, 0x00050081, 0x00000006,
|
||||
0x00000597, 0x00000595, 0x00000456, 0x00050085, 0x00000006, 0x00000599, 0x00000597, 0x000006c6,
|
||||
0x0004003d, 0x000002f7, 0x000003a1, 0x000002f9, 0x00070050, 0x00000039, 0x000003a8, 0x00000573,
|
||||
0x00000586, 0x00000599, 0x0000007d, 0x00040063, 0x000003a1, 0x00000431, 0x000003a8, 0x000200f9,
|
||||
0x000003a9, 0x000200f8, 0x000003a9, 0x00050080, 0x0000000c, 0x00000324, 0x00000387, 0x00000321,
|
||||
0x00060052, 0x00000031, 0x000010e2, 0x00000324, 0x0000031b, 0x00000000, 0x000300f7, 0x000006f9,
|
||||
0x00000000, 0x000300fb, 0x000000c9, 0x000006d5, 0x000200f8, 0x000006d5, 0x000500ae, 0x000002d0,
|
||||
0x000006db, 0x00000324, 0x0000038a, 0x000400a8, 0x000002d0, 0x000006dc, 0x000006db, 0x000300f7,
|
||||
0x000006e4, 0x00000000, 0x000400fa, 0x000006dc, 0x000006dd, 0x000006e4, 0x000200f8, 0x000006dd,
|
||||
0x00050051, 0x0000000c, 0x000006df, 0x0000031b, 0x00000001, 0x00060041, 0x000002d6, 0x000006e0,
|
||||
0x000002d5, 0x00000069, 0x000000a1, 0x0004003d, 0x00000006, 0x000006e1, 0x000006e0, 0x0004006d,
|
||||
0x0000000c, 0x000006e2, 0x000006e1, 0x000500ae, 0x000002d0, 0x000006e3, 0x000006df, 0x000006e2,
|
||||
0x000200f9, 0x000006e4, 0x000200f8, 0x000006e4, 0x000700f5, 0x000002d0, 0x000006e5, 0x000006db,
|
||||
0x000006d5, 0x000006e3, 0x000006dd, 0x000300f7, 0x000006e7, 0x00000000, 0x000400fa, 0x000006e5,
|
||||
0x000006e6, 0x000006e7, 0x000200f8, 0x000006e6, 0x000200f9, 0x000006f9, 0x000200f8, 0x000006e7,
|
||||
0x0004007c, 0x00000037, 0x00000781, 0x000010e2, 0x00050080, 0x00000037, 0x00000783, 0x00000781,
|
||||
0x000000d9, 0x0004003d, 0x000000b6, 0x000008ec, 0x000000b8, 0x00040064, 0x000000b5, 0x000008ee,
|
||||
0x000008ec, 0x0007005f, 0x00000039, 0x000008ef, 0x000008ee, 0x00000783, 0x00000002, 0x00000069,
|
||||
0x00050080, 0x00000037, 0x00000787, 0x00000781, 0x000000e0, 0x0004003d, 0x000000b6, 0x000008f2,
|
||||
0x000000b8, 0x00040064, 0x000000b5, 0x000008f4, 0x000008f2, 0x0007005f, 0x00000039, 0x000008f5,
|
||||
0x000008f4, 0x00000787, 0x00000002, 0x00000069, 0x0004003d, 0x000000b6, 0x000008f8, 0x000000b8,
|
||||
0x00040064, 0x000000b5, 0x000008fa, 0x000008f8, 0x0007005f, 0x00000039, 0x000008fb, 0x000008fa,
|
||||
0x00000781, 0x00000002, 0x00000069, 0x00050080, 0x00000037, 0x0000078e, 0x00000781, 0x000000ed,
|
||||
0x0004003d, 0x000000b6, 0x000008fe, 0x000000b8, 0x00040064, 0x000000b5, 0x00000900, 0x000008fe,
|
||||
0x0007005f, 0x00000039, 0x00000901, 0x00000900, 0x0000078e, 0x00000002, 0x00000069, 0x00050080,
|
||||
0x00000037, 0x00000792, 0x00000781, 0x000000f4, 0x0004003d, 0x000000b6, 0x00000904, 0x000000b8,
|
||||
0x00040064, 0x000000b5, 0x00000906, 0x00000904, 0x0007005f, 0x00000039, 0x00000907, 0x00000906,
|
||||
0x00000792, 0x00000002, 0x00000069, 0x00050051, 0x00000006, 0x00000796, 0x000008ef, 0x00000000,
|
||||
0x00050051, 0x00000006, 0x00000798, 0x000008ef, 0x00000001, 0x00050051, 0x00000006, 0x0000079a,
|
||||
0x000008ef, 0x00000002, 0x00050051, 0x00000006, 0x0000079c, 0x000008f5, 0x00000000, 0x00050051,
|
||||
0x00000006, 0x0000079e, 0x000008f5, 0x00000001, 0x00050051, 0x00000006, 0x000007a0, 0x000008f5,
|
||||
0x00000002, 0x00050051, 0x00000006, 0x000007a2, 0x000008fb, 0x00000000, 0x00050051, 0x00000006,
|
||||
0x000007a4, 0x000008fb, 0x00000001, 0x00050051, 0x00000006, 0x000007a6, 0x000008fb, 0x00000002,
|
||||
0x00050051, 0x00000006, 0x000007a8, 0x00000901, 0x00000000, 0x00050051, 0x00000006, 0x000007aa,
|
||||
0x00000901, 0x00000001, 0x00050051, 0x00000006, 0x000007ac, 0x00000901, 0x00000002, 0x00050051,
|
||||
0x00000006, 0x000007ae, 0x00000907, 0x00000000, 0x00050051, 0x00000006, 0x000007b0, 0x00000907,
|
||||
0x00000001, 0x00050051, 0x00000006, 0x000007b2, 0x00000907, 0x00000002, 0x0007000c, 0x00000006,
|
||||
0x00000984, 0x00000001, 0x00000025, 0x0000079c, 0x000007a8, 0x0007000c, 0x00000006, 0x00000985,
|
||||
0x00000001, 0x00000025, 0x00000796, 0x00000984, 0x0007000c, 0x00000006, 0x00000832, 0x00000001,
|
||||
0x00000025, 0x00000985, 0x000007ae, 0x0007000c, 0x00000006, 0x0000098b, 0x00000001, 0x00000025,
|
||||
0x0000079e, 0x000007aa, 0x0007000c, 0x00000006, 0x0000098c, 0x00000001, 0x00000025, 0x00000798,
|
||||
0x0000098b, 0x0007000c, 0x00000006, 0x00000838, 0x00000001, 0x00000025, 0x0000098c, 0x000007b0,
|
||||
0x0007000c, 0x00000006, 0x00000992, 0x00000001, 0x00000025, 0x000007a0, 0x000007ac, 0x0007000c,
|
||||
0x00000006, 0x00000993, 0x00000001, 0x00000025, 0x0000079a, 0x00000992, 0x0007000c, 0x00000006,
|
||||
0x0000083e, 0x00000001, 0x00000025, 0x00000993, 0x000007b2, 0x0007000c, 0x00000006, 0x00000999,
|
||||
0x00000001, 0x00000028, 0x0000079c, 0x000007a8, 0x0007000c, 0x00000006, 0x0000099a, 0x00000001,
|
||||
0x00000028, 0x00000796, 0x00000999, 0x0007000c, 0x00000006, 0x00000844, 0x00000001, 0x00000028,
|
||||
0x0000099a, 0x000007ae, 0x0007000c, 0x00000006, 0x000009a0, 0x00000001, 0x00000028, 0x0000079e,
|
||||
0x000007aa, 0x0007000c, 0x00000006, 0x000009a1, 0x00000001, 0x00000028, 0x00000798, 0x000009a0,
|
||||
0x0007000c, 0x00000006, 0x0000084a, 0x00000001, 0x00000028, 0x000009a1, 0x000007b0, 0x0007000c,
|
||||
0x00000006, 0x000009a7, 0x00000001, 0x00000028, 0x000007a0, 0x000007ac, 0x0007000c, 0x00000006,
|
||||
0x000009a8, 0x00000001, 0x00000028, 0x0000079a, 0x000009a7, 0x0007000c, 0x00000006, 0x00000850,
|
||||
0x00000001, 0x00000028, 0x000009a8, 0x000007b2, 0x0007000c, 0x00000006, 0x00000853, 0x00000001,
|
||||
0x00000025, 0x00000832, 0x000007a2, 0x00050088, 0x00000006, 0x000009b1, 0x0000115d, 0x00000844,
|
||||
0x00050085, 0x00000006, 0x00000858, 0x00000853, 0x000009b1, 0x0007000c, 0x00000006, 0x0000085b,
|
||||
0x00000001, 0x00000025, 0x00000838, 0x000007a4, 0x00050088, 0x00000006, 0x000009bd, 0x0000115d,
|
||||
0x0000084a, 0x00050085, 0x00000006, 0x00000860, 0x0000085b, 0x000009bd, 0x0007000c, 0x00000006,
|
||||
0x00000863, 0x00000001, 0x00000025, 0x0000083e, 0x000007a6, 0x00050088, 0x00000006, 0x000009c9,
|
||||
0x0000115d, 0x00000850, 0x00050085, 0x00000006, 0x00000868, 0x00000863, 0x000009c9, 0x0007000c,
|
||||
0x00000006, 0x0000086d, 0x00000001, 0x00000028, 0x00000844, 0x000007a2, 0x00050083, 0x00000006,
|
||||
0x0000086e, 0x0000007d, 0x0000086d, 0x00050085, 0x00000006, 0x00000871, 0x00000219, 0x00000832,
|
||||
0x00050081, 0x00000006, 0x00000874, 0x00000871, 0x00000213, 0x00050088, 0x00000006, 0x000009d5,
|
||||
0x0000007d, 0x00000874, 0x00050085, 0x00000006, 0x00000876, 0x0000086e, 0x000009d5, 0x0007000c,
|
||||
0x00000006, 0x0000087b, 0x00000001, 0x00000028, 0x0000084a, 0x000007a4, 0x00050083, 0x00000006,
|
||||
0x0000087c, 0x0000007d, 0x0000087b, 0x00050085, 0x00000006, 0x0000087f, 0x00000219, 0x00000838,
|
||||
0x00050081, 0x00000006, 0x00000882, 0x0000087f, 0x00000213, 0x00050088, 0x00000006, 0x000009e1,
|
||||
0x0000007d, 0x00000882, 0x00050085, 0x00000006, 0x00000884, 0x0000087c, 0x000009e1, 0x0007000c,
|
||||
0x00000006, 0x00000889, 0x00000001, 0x00000028, 0x00000850, 0x000007a6, 0x00050083, 0x00000006,
|
||||
0x0000088a, 0x0000007d, 0x00000889, 0x00050085, 0x00000006, 0x0000088d, 0x00000219, 0x0000083e,
|
||||
0x00050081, 0x00000006, 0x00000890, 0x0000088d, 0x00000213, 0x00050088, 0x00000006, 0x000009ed,
|
||||
0x0000007d, 0x00000890, 0x00050085, 0x00000006, 0x00000892, 0x0000088a, 0x000009ed, 0x0004007f,
|
||||
0x00000006, 0x00000894, 0x00000858, 0x0007000c, 0x00000006, 0x00000896, 0x00000001, 0x00000028,
|
||||
0x00000894, 0x00000876, 0x0004007f, 0x00000006, 0x00000898, 0x00000860, 0x0007000c, 0x00000006,
|
||||
0x0000089a, 0x00000001, 0x00000028, 0x00000898, 0x00000884, 0x0004007f, 0x00000006, 0x0000089c,
|
||||
0x00000868, 0x0007000c, 0x00000006, 0x0000089e, 0x00000001, 0x00000028, 0x0000089c, 0x00000892,
|
||||
0x0007000c, 0x00000006, 0x000009f9, 0x00000001, 0x00000028, 0x0000089a, 0x0000089e, 0x0007000c,
|
||||
0x00000006, 0x000009fa, 0x00000001, 0x00000028, 0x00000896, 0x000009f9, 0x0007000c, 0x00000006,
|
||||
0x000008a5, 0x00000001, 0x00000025, 0x000009fa, 0x00000085, 0x0007000c, 0x00000006, 0x000008a6,
|
||||
0x00000001, 0x00000028, 0x0000027a, 0x000008a5, 0x00050085, 0x00000006, 0x000008aa, 0x000008a6,
|
||||
0x00000349, 0x00050085, 0x00000006, 0x000008ad, 0x00000219, 0x000008aa, 0x00050081, 0x00000006,
|
||||
0x000008af, 0x000008ad, 0x0000007d, 0x0004007c, 0x0000000c, 0x00000a0b, 0x000008af, 0x00050082,
|
||||
0x0000000c, 0x00000a0c, 0x0000008e, 0x00000a0b, 0x0004007c, 0x00000006, 0x00000a0d, 0x00000a0c,
|
||||
0x0004007f, 0x00000006, 0x00000a10, 0x00000a0d, 0x00050085, 0x00000006, 0x00000a12, 0x00000a10,
|
||||
0x000008af, 0x00050081, 0x00000006, 0x00000a14, 0x00000a12, 0x0000009a, 0x00050085, 0x00000006,
|
||||
0x00000a15, 0x00000a0d, 0x00000a14, 0x00050081, 0x00000006, 0x00001168, 0x00000796, 0x0000079c,
|
||||
0x00050081, 0x00000006, 0x00001169, 0x00001168, 0x000007ae, 0x00050081, 0x00000006, 0x0000116a,
|
||||
0x00001169, 0x000007a8, 0x00050085, 0x00000006, 0x000008bf, 0x000008aa, 0x0000116a, 0x00050081,
|
||||
0x00000006, 0x000008c1, 0x000008bf, 0x000007a2, 0x00050085, 0x00000006, 0x000008c3, 0x000008c1,
|
||||
0x00000a15, 0x00050081, 0x00000006, 0x0000116b, 0x00000798, 0x0000079e, 0x00050081, 0x00000006,
|
||||
0x0000116c, 0x0000116b, 0x000007b0, 0x00050081, 0x00000006, 0x0000116d, 0x0000116c, 0x000007aa,
|
||||
0x00050085, 0x00000006, 0x000008d2, 0x000008aa, 0x0000116d, 0x00050081, 0x00000006, 0x000008d4,
|
||||
0x000008d2, 0x000007a4, 0x00050085, 0x00000006, 0x000008d6, 0x000008d4, 0x00000a15, 0x00050081,
|
||||
0x00000006, 0x0000116e, 0x0000079a, 0x000007a0, 0x00050081, 0x00000006, 0x0000116f, 0x0000116e,
|
||||
0x000007b2, 0x00050081, 0x00000006, 0x00001170, 0x0000116f, 0x000007ac, 0x00050085, 0x00000006,
|
||||
0x000008e5, 0x000008aa, 0x00001170, 0x00050081, 0x00000006, 0x000008e7, 0x000008e5, 0x000007a6,
|
||||
0x00050085, 0x00000006, 0x000008e9, 0x000008e7, 0x00000a15, 0x0004003d, 0x000002f7, 0x000006f1,
|
||||
0x000002f9, 0x00070050, 0x00000039, 0x000006f8, 0x000008c3, 0x000008d6, 0x000008e9, 0x0000007d,
|
||||
0x00040063, 0x000006f1, 0x00000781, 0x000006f8, 0x000200f9, 0x000006f9, 0x000200f8, 0x000006f9,
|
||||
0x00050051, 0x0000000c, 0x0000032c, 0x0000031b, 0x00000001, 0x00050080, 0x0000000c, 0x0000032d,
|
||||
0x0000032c, 0x00000321, 0x00050050, 0x00000031, 0x00001172, 0x00000324, 0x0000032d, 0x000300f7,
|
||||
0x00000a48, 0x00000000, 0x000300fb, 0x000000c9, 0x00000a24, 0x000200f8, 0x00000a24, 0x000300f7,
|
||||
0x00000a33, 0x00000000, 0x000400fa, 0x000006dc, 0x00000a2c, 0x00000a33, 0x000200f8, 0x00000a2c,
|
||||
0x00060041, 0x000002d6, 0x00000a2f, 0x000002d5, 0x00000069, 0x000000a1, 0x0004003d, 0x00000006,
|
||||
0x00000a30, 0x00000a2f, 0x0004006d, 0x0000000c, 0x00000a31, 0x00000a30, 0x000500ae, 0x000002d0,
|
||||
0x00000a32, 0x0000032d, 0x00000a31, 0x000200f9, 0x00000a33, 0x000200f8, 0x00000a33, 0x000700f5,
|
||||
0x000002d0, 0x00000a34, 0x000006db, 0x00000a24, 0x00000a32, 0x00000a2c, 0x000300f7, 0x00000a36,
|
||||
0x00000000, 0x000400fa, 0x00000a34, 0x00000a35, 0x00000a36, 0x000200f8, 0x00000a35, 0x000200f9,
|
||||
0x00000a48, 0x000200f8, 0x00000a36, 0x0004007c, 0x00000037, 0x00000ad0, 0x00001172, 0x00050080,
|
||||
0x00000037, 0x00000ad2, 0x00000ad0, 0x000000d9, 0x0004003d, 0x000000b6, 0x00000c3b, 0x000000b8,
|
||||
0x00040064, 0x000000b5, 0x00000c3d, 0x00000c3b, 0x0007005f, 0x00000039, 0x00000c3e, 0x00000c3d,
|
||||
0x00000ad2, 0x00000002, 0x00000069, 0x00050080, 0x00000037, 0x00000ad6, 0x00000ad0, 0x000000e0,
|
||||
0x0004003d, 0x000000b6, 0x00000c41, 0x000000b8, 0x00040064, 0x000000b5, 0x00000c43, 0x00000c41,
|
||||
0x0007005f, 0x00000039, 0x00000c44, 0x00000c43, 0x00000ad6, 0x00000002, 0x00000069, 0x0004003d,
|
||||
0x000000b6, 0x00000c47, 0x000000b8, 0x00040064, 0x000000b5, 0x00000c49, 0x00000c47, 0x0007005f,
|
||||
0x00000039, 0x00000c4a, 0x00000c49, 0x00000ad0, 0x00000002, 0x00000069, 0x00050080, 0x00000037,
|
||||
0x00000add, 0x00000ad0, 0x000000ed, 0x0004003d, 0x000000b6, 0x00000c4d, 0x000000b8, 0x00040064,
|
||||
0x000000b5, 0x00000c4f, 0x00000c4d, 0x0007005f, 0x00000039, 0x00000c50, 0x00000c4f, 0x00000add,
|
||||
0x00000002, 0x00000069, 0x00050080, 0x00000037, 0x00000ae1, 0x00000ad0, 0x000000f4, 0x0004003d,
|
||||
0x000000b6, 0x00000c53, 0x000000b8, 0x00040064, 0x000000b5, 0x00000c55, 0x00000c53, 0x0007005f,
|
||||
0x00000039, 0x00000c56, 0x00000c55, 0x00000ae1, 0x00000002, 0x00000069, 0x00050051, 0x00000006,
|
||||
0x00000ae5, 0x00000c3e, 0x00000000, 0x00050051, 0x00000006, 0x00000ae7, 0x00000c3e, 0x00000001,
|
||||
0x00050051, 0x00000006, 0x00000ae9, 0x00000c3e, 0x00000002, 0x00050051, 0x00000006, 0x00000aeb,
|
||||
0x00000c44, 0x00000000, 0x00050051, 0x00000006, 0x00000aed, 0x00000c44, 0x00000001, 0x00050051,
|
||||
0x00000006, 0x00000aef, 0x00000c44, 0x00000002, 0x00050051, 0x00000006, 0x00000af1, 0x00000c4a,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x00000af3, 0x00000c4a, 0x00000001, 0x00050051, 0x00000006,
|
||||
0x00000af5, 0x00000c4a, 0x00000002, 0x00050051, 0x00000006, 0x00000af7, 0x00000c50, 0x00000000,
|
||||
0x00050051, 0x00000006, 0x00000af9, 0x00000c50, 0x00000001, 0x00050051, 0x00000006, 0x00000afb,
|
||||
0x00000c50, 0x00000002, 0x00050051, 0x00000006, 0x00000afd, 0x00000c56, 0x00000000, 0x00050051,
|
||||
0x00000006, 0x00000aff, 0x00000c56, 0x00000001, 0x00050051, 0x00000006, 0x00000b01, 0x00000c56,
|
||||
0x00000002, 0x0007000c, 0x00000006, 0x00000cd3, 0x00000001, 0x00000025, 0x00000aeb, 0x00000af7,
|
||||
0x0007000c, 0x00000006, 0x00000cd4, 0x00000001, 0x00000025, 0x00000ae5, 0x00000cd3, 0x0007000c,
|
||||
0x00000006, 0x00000b81, 0x00000001, 0x00000025, 0x00000cd4, 0x00000afd, 0x0007000c, 0x00000006,
|
||||
0x00000cda, 0x00000001, 0x00000025, 0x00000aed, 0x00000af9, 0x0007000c, 0x00000006, 0x00000cdb,
|
||||
0x00000001, 0x00000025, 0x00000ae7, 0x00000cda, 0x0007000c, 0x00000006, 0x00000b87, 0x00000001,
|
||||
0x00000025, 0x00000cdb, 0x00000aff, 0x0007000c, 0x00000006, 0x00000ce1, 0x00000001, 0x00000025,
|
||||
0x00000aef, 0x00000afb, 0x0007000c, 0x00000006, 0x00000ce2, 0x00000001, 0x00000025, 0x00000ae9,
|
||||
0x00000ce1, 0x0007000c, 0x00000006, 0x00000b8d, 0x00000001, 0x00000025, 0x00000ce2, 0x00000b01,
|
||||
0x0007000c, 0x00000006, 0x00000ce8, 0x00000001, 0x00000028, 0x00000aeb, 0x00000af7, 0x0007000c,
|
||||
0x00000006, 0x00000ce9, 0x00000001, 0x00000028, 0x00000ae5, 0x00000ce8, 0x0007000c, 0x00000006,
|
||||
0x00000b93, 0x00000001, 0x00000028, 0x00000ce9, 0x00000afd, 0x0007000c, 0x00000006, 0x00000cef,
|
||||
0x00000001, 0x00000028, 0x00000aed, 0x00000af9, 0x0007000c, 0x00000006, 0x00000cf0, 0x00000001,
|
||||
0x00000028, 0x00000ae7, 0x00000cef, 0x0007000c, 0x00000006, 0x00000b99, 0x00000001, 0x00000028,
|
||||
0x00000cf0, 0x00000aff, 0x0007000c, 0x00000006, 0x00000cf6, 0x00000001, 0x00000028, 0x00000aef,
|
||||
0x00000afb, 0x0007000c, 0x00000006, 0x00000cf7, 0x00000001, 0x00000028, 0x00000ae9, 0x00000cf6,
|
||||
0x0007000c, 0x00000006, 0x00000b9f, 0x00000001, 0x00000028, 0x00000cf7, 0x00000b01, 0x0007000c,
|
||||
0x00000006, 0x00000ba2, 0x00000001, 0x00000025, 0x00000b81, 0x00000af1, 0x00050088, 0x00000006,
|
||||
0x00000d00, 0x0000115d, 0x00000b93, 0x00050085, 0x00000006, 0x00000ba7, 0x00000ba2, 0x00000d00,
|
||||
0x0007000c, 0x00000006, 0x00000baa, 0x00000001, 0x00000025, 0x00000b87, 0x00000af3, 0x00050088,
|
||||
0x00000006, 0x00000d0c, 0x0000115d, 0x00000b99, 0x00050085, 0x00000006, 0x00000baf, 0x00000baa,
|
||||
0x00000d0c, 0x0007000c, 0x00000006, 0x00000bb2, 0x00000001, 0x00000025, 0x00000b8d, 0x00000af5,
|
||||
0x00050088, 0x00000006, 0x00000d18, 0x0000115d, 0x00000b9f, 0x00050085, 0x00000006, 0x00000bb7,
|
||||
0x00000bb2, 0x00000d18, 0x0007000c, 0x00000006, 0x00000bbc, 0x00000001, 0x00000028, 0x00000b93,
|
||||
0x00000af1, 0x00050083, 0x00000006, 0x00000bbd, 0x0000007d, 0x00000bbc, 0x00050085, 0x00000006,
|
||||
0x00000bc0, 0x00000219, 0x00000b81, 0x00050081, 0x00000006, 0x00000bc3, 0x00000bc0, 0x00000213,
|
||||
0x00050088, 0x00000006, 0x00000d24, 0x0000007d, 0x00000bc3, 0x00050085, 0x00000006, 0x00000bc5,
|
||||
0x00000bbd, 0x00000d24, 0x0007000c, 0x00000006, 0x00000bca, 0x00000001, 0x00000028, 0x00000b99,
|
||||
0x00000af3, 0x00050083, 0x00000006, 0x00000bcb, 0x0000007d, 0x00000bca, 0x00050085, 0x00000006,
|
||||
0x00000bce, 0x00000219, 0x00000b87, 0x00050081, 0x00000006, 0x00000bd1, 0x00000bce, 0x00000213,
|
||||
0x00050088, 0x00000006, 0x00000d30, 0x0000007d, 0x00000bd1, 0x00050085, 0x00000006, 0x00000bd3,
|
||||
0x00000bcb, 0x00000d30, 0x0007000c, 0x00000006, 0x00000bd8, 0x00000001, 0x00000028, 0x00000b9f,
|
||||
0x00000af5, 0x00050083, 0x00000006, 0x00000bd9, 0x0000007d, 0x00000bd8, 0x00050085, 0x00000006,
|
||||
0x00000bdc, 0x00000219, 0x00000b8d, 0x00050081, 0x00000006, 0x00000bdf, 0x00000bdc, 0x00000213,
|
||||
0x00050088, 0x00000006, 0x00000d3c, 0x0000007d, 0x00000bdf, 0x00050085, 0x00000006, 0x00000be1,
|
||||
0x00000bd9, 0x00000d3c, 0x0004007f, 0x00000006, 0x00000be3, 0x00000ba7, 0x0007000c, 0x00000006,
|
||||
0x00000be5, 0x00000001, 0x00000028, 0x00000be3, 0x00000bc5, 0x0004007f, 0x00000006, 0x00000be7,
|
||||
0x00000baf, 0x0007000c, 0x00000006, 0x00000be9, 0x00000001, 0x00000028, 0x00000be7, 0x00000bd3,
|
||||
0x0004007f, 0x00000006, 0x00000beb, 0x00000bb7, 0x0007000c, 0x00000006, 0x00000bed, 0x00000001,
|
||||
0x00000028, 0x00000beb, 0x00000be1, 0x0007000c, 0x00000006, 0x00000d48, 0x00000001, 0x00000028,
|
||||
0x00000be9, 0x00000bed, 0x0007000c, 0x00000006, 0x00000d49, 0x00000001, 0x00000028, 0x00000be5,
|
||||
0x00000d48, 0x0007000c, 0x00000006, 0x00000bf4, 0x00000001, 0x00000025, 0x00000d49, 0x00000085,
|
||||
0x0007000c, 0x00000006, 0x00000bf5, 0x00000001, 0x00000028, 0x0000027a, 0x00000bf4, 0x00050085,
|
||||
0x00000006, 0x00000bf9, 0x00000bf5, 0x00000349, 0x00050085, 0x00000006, 0x00000bfc, 0x00000219,
|
||||
0x00000bf9, 0x00050081, 0x00000006, 0x00000bfe, 0x00000bfc, 0x0000007d, 0x0004007c, 0x0000000c,
|
||||
0x00000d5a, 0x00000bfe, 0x00050082, 0x0000000c, 0x00000d5b, 0x0000008e, 0x00000d5a, 0x0004007c,
|
||||
0x00000006, 0x00000d5c, 0x00000d5b, 0x0004007f, 0x00000006, 0x00000d5f, 0x00000d5c, 0x00050085,
|
||||
0x00000006, 0x00000d61, 0x00000d5f, 0x00000bfe, 0x00050081, 0x00000006, 0x00000d63, 0x00000d61,
|
||||
0x0000009a, 0x00050085, 0x00000006, 0x00000d64, 0x00000d5c, 0x00000d63, 0x00050081, 0x00000006,
|
||||
0x00001173, 0x00000ae5, 0x00000aeb, 0x00050081, 0x00000006, 0x00001174, 0x00001173, 0x00000afd,
|
||||
0x00050081, 0x00000006, 0x00001175, 0x00001174, 0x00000af7, 0x00050085, 0x00000006, 0x00000c0e,
|
||||
0x00000bf9, 0x00001175, 0x00050081, 0x00000006, 0x00000c10, 0x00000c0e, 0x00000af1, 0x00050085,
|
||||
0x00000006, 0x00000c12, 0x00000c10, 0x00000d64, 0x00050081, 0x00000006, 0x00001176, 0x00000ae7,
|
||||
0x00000aed, 0x00050081, 0x00000006, 0x00001177, 0x00001176, 0x00000aff, 0x00050081, 0x00000006,
|
||||
0x00001178, 0x00001177, 0x00000af9, 0x00050085, 0x00000006, 0x00000c21, 0x00000bf9, 0x00001178,
|
||||
0x00050081, 0x00000006, 0x00000c23, 0x00000c21, 0x00000af3, 0x00050085, 0x00000006, 0x00000c25,
|
||||
0x00000c23, 0x00000d64, 0x00050081, 0x00000006, 0x00001179, 0x00000ae9, 0x00000aef, 0x00050081,
|
||||
0x00000006, 0x0000117a, 0x00001179, 0x00000b01, 0x00050081, 0x00000006, 0x0000117b, 0x0000117a,
|
||||
0x00000afb, 0x00050085, 0x00000006, 0x00000c34, 0x00000bf9, 0x0000117b, 0x00050081, 0x00000006,
|
||||
0x00000c36, 0x00000c34, 0x00000af5, 0x00050085, 0x00000006, 0x00000c38, 0x00000c36, 0x00000d64,
|
||||
0x0004003d, 0x000002f7, 0x00000a40, 0x000002f9, 0x00070050, 0x00000039, 0x00000a47, 0x00000c12,
|
||||
0x00000c25, 0x00000c38, 0x0000007d, 0x00040063, 0x00000a40, 0x00000ad0, 0x00000a47, 0x000200f9,
|
||||
0x00000a48, 0x000200f8, 0x00000a48, 0x00060052, 0x00000031, 0x00001124, 0x00000387, 0x00001172,
|
||||
0x00000000, 0x000300f7, 0x00000d97, 0x00000000, 0x000300fb, 0x000000c9, 0x00000d73, 0x000200f8,
|
||||
0x00000d73, 0x000300f7, 0x00000d82, 0x00000000, 0x000400fa, 0x0000038c, 0x00000d7b, 0x00000d82,
|
||||
0x000200f8, 0x00000d7b, 0x00060041, 0x000002d6, 0x00000d7e, 0x000002d5, 0x00000069, 0x000000a1,
|
||||
0x0004003d, 0x00000006, 0x00000d7f, 0x00000d7e, 0x0004006d, 0x0000000c, 0x00000d80, 0x00000d7f,
|
||||
0x000500ae, 0x000002d0, 0x00000d81, 0x0000032d, 0x00000d80, 0x000200f9, 0x00000d82, 0x000200f8,
|
||||
0x00000d82, 0x000700f5, 0x000002d0, 0x00000d83, 0x0000038b, 0x00000d73, 0x00000d81, 0x00000d7b,
|
||||
0x000300f7, 0x00000d85, 0x00000000, 0x000400fa, 0x00000d83, 0x00000d84, 0x00000d85, 0x000200f8,
|
||||
0x00000d84, 0x000200f9, 0x00000d97, 0x000200f8, 0x00000d85, 0x0004007c, 0x00000037, 0x00000e1f,
|
||||
0x00001124, 0x00050080, 0x00000037, 0x00000e21, 0x00000e1f, 0x000000d9, 0x0004003d, 0x000000b6,
|
||||
0x00000f8a, 0x000000b8, 0x00040064, 0x000000b5, 0x00000f8c, 0x00000f8a, 0x0007005f, 0x00000039,
|
||||
0x00000f8d, 0x00000f8c, 0x00000e21, 0x00000002, 0x00000069, 0x00050080, 0x00000037, 0x00000e25,
|
||||
0x00000e1f, 0x000000e0, 0x0004003d, 0x000000b6, 0x00000f90, 0x000000b8, 0x00040064, 0x000000b5,
|
||||
0x00000f92, 0x00000f90, 0x0007005f, 0x00000039, 0x00000f93, 0x00000f92, 0x00000e25, 0x00000002,
|
||||
0x00000069, 0x0004003d, 0x000000b6, 0x00000f96, 0x000000b8, 0x00040064, 0x000000b5, 0x00000f98,
|
||||
0x00000f96, 0x0007005f, 0x00000039, 0x00000f99, 0x00000f98, 0x00000e1f, 0x00000002, 0x00000069,
|
||||
0x00050080, 0x00000037, 0x00000e2c, 0x00000e1f, 0x000000ed, 0x0004003d, 0x000000b6, 0x00000f9c,
|
||||
0x000000b8, 0x00040064, 0x000000b5, 0x00000f9e, 0x00000f9c, 0x0007005f, 0x00000039, 0x00000f9f,
|
||||
0x00000f9e, 0x00000e2c, 0x00000002, 0x00000069, 0x00050080, 0x00000037, 0x00000e30, 0x00000e1f,
|
||||
0x000000f4, 0x0004003d, 0x000000b6, 0x00000fa2, 0x000000b8, 0x00040064, 0x000000b5, 0x00000fa4,
|
||||
0x00000fa2, 0x0007005f, 0x00000039, 0x00000fa5, 0x00000fa4, 0x00000e30, 0x00000002, 0x00000069,
|
||||
0x00050051, 0x00000006, 0x00000e34, 0x00000f8d, 0x00000000, 0x00050051, 0x00000006, 0x00000e36,
|
||||
0x00000f8d, 0x00000001, 0x00050051, 0x00000006, 0x00000e38, 0x00000f8d, 0x00000002, 0x00050051,
|
||||
0x00000006, 0x00000e3a, 0x00000f93, 0x00000000, 0x00050051, 0x00000006, 0x00000e3c, 0x00000f93,
|
||||
0x00000001, 0x00050051, 0x00000006, 0x00000e3e, 0x00000f93, 0x00000002, 0x00050051, 0x00000006,
|
||||
0x00000e40, 0x00000f99, 0x00000000, 0x00050051, 0x00000006, 0x00000e42, 0x00000f99, 0x00000001,
|
||||
0x00050051, 0x00000006, 0x00000e44, 0x00000f99, 0x00000002, 0x00050051, 0x00000006, 0x00000e46,
|
||||
0x00000f9f, 0x00000000, 0x00050051, 0x00000006, 0x00000e48, 0x00000f9f, 0x00000001, 0x00050051,
|
||||
0x00000006, 0x00000e4a, 0x00000f9f, 0x00000002, 0x00050051, 0x00000006, 0x00000e4c, 0x00000fa5,
|
||||
0x00000000, 0x00050051, 0x00000006, 0x00000e4e, 0x00000fa5, 0x00000001, 0x00050051, 0x00000006,
|
||||
0x00000e50, 0x00000fa5, 0x00000002, 0x0007000c, 0x00000006, 0x00001022, 0x00000001, 0x00000025,
|
||||
0x00000e3a, 0x00000e46, 0x0007000c, 0x00000006, 0x00001023, 0x00000001, 0x00000025, 0x00000e34,
|
||||
0x00001022, 0x0007000c, 0x00000006, 0x00000ed0, 0x00000001, 0x00000025, 0x00001023, 0x00000e4c,
|
||||
0x0007000c, 0x00000006, 0x00001029, 0x00000001, 0x00000025, 0x00000e3c, 0x00000e48, 0x0007000c,
|
||||
0x00000006, 0x0000102a, 0x00000001, 0x00000025, 0x00000e36, 0x00001029, 0x0007000c, 0x00000006,
|
||||
0x00000ed6, 0x00000001, 0x00000025, 0x0000102a, 0x00000e4e, 0x0007000c, 0x00000006, 0x00001030,
|
||||
0x00000001, 0x00000025, 0x00000e3e, 0x00000e4a, 0x0007000c, 0x00000006, 0x00001031, 0x00000001,
|
||||
0x00000025, 0x00000e38, 0x00001030, 0x0007000c, 0x00000006, 0x00000edc, 0x00000001, 0x00000025,
|
||||
0x00001031, 0x00000e50, 0x0007000c, 0x00000006, 0x00001037, 0x00000001, 0x00000028, 0x00000e3a,
|
||||
0x00000e46, 0x0007000c, 0x00000006, 0x00001038, 0x00000001, 0x00000028, 0x00000e34, 0x00001037,
|
||||
0x0007000c, 0x00000006, 0x00000ee2, 0x00000001, 0x00000028, 0x00001038, 0x00000e4c, 0x0007000c,
|
||||
0x00000006, 0x0000103e, 0x00000001, 0x00000028, 0x00000e3c, 0x00000e48, 0x0007000c, 0x00000006,
|
||||
0x0000103f, 0x00000001, 0x00000028, 0x00000e36, 0x0000103e, 0x0007000c, 0x00000006, 0x00000ee8,
|
||||
0x00000001, 0x00000028, 0x0000103f, 0x00000e4e, 0x0007000c, 0x00000006, 0x00001045, 0x00000001,
|
||||
0x00000028, 0x00000e3e, 0x00000e4a, 0x0007000c, 0x00000006, 0x00001046, 0x00000001, 0x00000028,
|
||||
0x00000e38, 0x00001045, 0x0007000c, 0x00000006, 0x00000eee, 0x00000001, 0x00000028, 0x00001046,
|
||||
0x00000e50, 0x0007000c, 0x00000006, 0x00000ef1, 0x00000001, 0x00000025, 0x00000ed0, 0x00000e40,
|
||||
0x00050088, 0x00000006, 0x0000104f, 0x0000115d, 0x00000ee2, 0x00050085, 0x00000006, 0x00000ef6,
|
||||
0x00000ef1, 0x0000104f, 0x0007000c, 0x00000006, 0x00000ef9, 0x00000001, 0x00000025, 0x00000ed6,
|
||||
0x00000e42, 0x00050088, 0x00000006, 0x0000105b, 0x0000115d, 0x00000ee8, 0x00050085, 0x00000006,
|
||||
0x00000efe, 0x00000ef9, 0x0000105b, 0x0007000c, 0x00000006, 0x00000f01, 0x00000001, 0x00000025,
|
||||
0x00000edc, 0x00000e44, 0x00050088, 0x00000006, 0x00001067, 0x0000115d, 0x00000eee, 0x00050085,
|
||||
0x00000006, 0x00000f06, 0x00000f01, 0x00001067, 0x0007000c, 0x00000006, 0x00000f0b, 0x00000001,
|
||||
0x00000028, 0x00000ee2, 0x00000e40, 0x00050083, 0x00000006, 0x00000f0c, 0x0000007d, 0x00000f0b,
|
||||
0x00050085, 0x00000006, 0x00000f0f, 0x00000219, 0x00000ed0, 0x00050081, 0x00000006, 0x00000f12,
|
||||
0x00000f0f, 0x00000213, 0x00050088, 0x00000006, 0x00001073, 0x0000007d, 0x00000f12, 0x00050085,
|
||||
0x00000006, 0x00000f14, 0x00000f0c, 0x00001073, 0x0007000c, 0x00000006, 0x00000f19, 0x00000001,
|
||||
0x00000028, 0x00000ee8, 0x00000e42, 0x00050083, 0x00000006, 0x00000f1a, 0x0000007d, 0x00000f19,
|
||||
0x00050085, 0x00000006, 0x00000f1d, 0x00000219, 0x00000ed6, 0x00050081, 0x00000006, 0x00000f20,
|
||||
0x00000f1d, 0x00000213, 0x00050088, 0x00000006, 0x0000107f, 0x0000007d, 0x00000f20, 0x00050085,
|
||||
0x00000006, 0x00000f22, 0x00000f1a, 0x0000107f, 0x0007000c, 0x00000006, 0x00000f27, 0x00000001,
|
||||
0x00000028, 0x00000eee, 0x00000e44, 0x00050083, 0x00000006, 0x00000f28, 0x0000007d, 0x00000f27,
|
||||
0x00050085, 0x00000006, 0x00000f2b, 0x00000219, 0x00000edc, 0x00050081, 0x00000006, 0x00000f2e,
|
||||
0x00000f2b, 0x00000213, 0x00050088, 0x00000006, 0x0000108b, 0x0000007d, 0x00000f2e, 0x00050085,
|
||||
0x00000006, 0x00000f30, 0x00000f28, 0x0000108b, 0x0004007f, 0x00000006, 0x00000f32, 0x00000ef6,
|
||||
0x0007000c, 0x00000006, 0x00000f34, 0x00000001, 0x00000028, 0x00000f32, 0x00000f14, 0x0004007f,
|
||||
0x00000006, 0x00000f36, 0x00000efe, 0x0007000c, 0x00000006, 0x00000f38, 0x00000001, 0x00000028,
|
||||
0x00000f36, 0x00000f22, 0x0004007f, 0x00000006, 0x00000f3a, 0x00000f06, 0x0007000c, 0x00000006,
|
||||
0x00000f3c, 0x00000001, 0x00000028, 0x00000f3a, 0x00000f30, 0x0007000c, 0x00000006, 0x00001097,
|
||||
0x00000001, 0x00000028, 0x00000f38, 0x00000f3c, 0x0007000c, 0x00000006, 0x00001098, 0x00000001,
|
||||
0x00000028, 0x00000f34, 0x00001097, 0x0007000c, 0x00000006, 0x00000f43, 0x00000001, 0x00000025,
|
||||
0x00001098, 0x00000085, 0x0007000c, 0x00000006, 0x00000f44, 0x00000001, 0x00000028, 0x0000027a,
|
||||
0x00000f43, 0x00050085, 0x00000006, 0x00000f48, 0x00000f44, 0x00000349, 0x00050085, 0x00000006,
|
||||
0x00000f4b, 0x00000219, 0x00000f48, 0x00050081, 0x00000006, 0x00000f4d, 0x00000f4b, 0x0000007d,
|
||||
0x0004007c, 0x0000000c, 0x000010a9, 0x00000f4d, 0x00050082, 0x0000000c, 0x000010aa, 0x0000008e,
|
||||
0x000010a9, 0x0004007c, 0x00000006, 0x000010ab, 0x000010aa, 0x0004007f, 0x00000006, 0x000010ae,
|
||||
0x000010ab, 0x00050085, 0x00000006, 0x000010b0, 0x000010ae, 0x00000f4d, 0x00050081, 0x00000006,
|
||||
0x000010b2, 0x000010b0, 0x0000009a, 0x00050085, 0x00000006, 0x000010b3, 0x000010ab, 0x000010b2,
|
||||
0x00050081, 0x00000006, 0x0000117d, 0x00000e34, 0x00000e3a, 0x00050081, 0x00000006, 0x0000117e,
|
||||
0x0000117d, 0x00000e4c, 0x00050081, 0x00000006, 0x0000117f, 0x0000117e, 0x00000e46, 0x00050085,
|
||||
0x00000006, 0x00000f5d, 0x00000f48, 0x0000117f, 0x00050081, 0x00000006, 0x00000f5f, 0x00000f5d,
|
||||
0x00000e40, 0x00050085, 0x00000006, 0x00000f61, 0x00000f5f, 0x000010b3, 0x00050081, 0x00000006,
|
||||
0x00001180, 0x00000e36, 0x00000e3c, 0x00050081, 0x00000006, 0x00001181, 0x00001180, 0x00000e4e,
|
||||
0x00050081, 0x00000006, 0x00001182, 0x00001181, 0x00000e48, 0x00050085, 0x00000006, 0x00000f70,
|
||||
0x00000f48, 0x00001182, 0x00050081, 0x00000006, 0x00000f72, 0x00000f70, 0x00000e42, 0x00050085,
|
||||
0x00000006, 0x00000f74, 0x00000f72, 0x000010b3, 0x00050081, 0x00000006, 0x00001183, 0x00000e38,
|
||||
0x00000e3e, 0x00050081, 0x00000006, 0x00001184, 0x00001183, 0x00000e50, 0x00050081, 0x00000006,
|
||||
0x00001185, 0x00001184, 0x00000e4a, 0x00050085, 0x00000006, 0x00000f83, 0x00000f48, 0x00001185,
|
||||
0x00050081, 0x00000006, 0x00000f85, 0x00000f83, 0x00000e44, 0x00050085, 0x00000006, 0x00000f87,
|
||||
0x00000f85, 0x000010b3, 0x0004003d, 0x000002f7, 0x00000d8f, 0x000002f9, 0x00070050, 0x00000039,
|
||||
0x00000d96, 0x00000f61, 0x00000f74, 0x00000f87, 0x0000007d, 0x00040063, 0x00000d8f, 0x00000e1f,
|
||||
0x00000d96, 0x000200f9, 0x00000d97, 0x000200f8, 0x00000d97, 0x000100fd, 0x00010038,
|
||||
};
|
||||
147
runtime/gfx.h
Normal file
147
runtime/gfx.h
Normal file
@@ -0,0 +1,147 @@
|
||||
// Interface of the graphics layer: window, shader service, back ends and the NVN front end.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "host.h"
|
||||
#include "nvn_obj.h"
|
||||
|
||||
// ---- moved from ov.h
|
||||
enum { OE_MSG_FOCUS_STATE_CHANGED = 15, OE_MSG_OPERATION_MODE_CHANGED = 30, OE_MSG_PERFORMANCE_MODE_CHANGED = 31 };
|
||||
void oe_push_message(uint32_t message); // ov_system.c
|
||||
void oe_set_focus(int in_focus); // ov_system.c: GetCurrentFocusState; NX2NAVI_ASSUME_FOCUS pins 1
|
||||
typedef struct { uint64_t alloc, free, realloc, user; } OvNvAllocator;
|
||||
typedef struct { OvNvAllocator graphics, devtools; uint64_t memory, memory_size; int initialized; } OvNvState;
|
||||
const OvNvState *ov_nv_state(void); // ov_gfx.c
|
||||
|
||||
// ---- GPU address window for CPU-invisible pools (section 3.1)
|
||||
#define RT_GPU_VA_BASE 0x8000000000ull // 2^39, never mapped on the host
|
||||
#define RT_GPU_VA_SIZE (64ull << 30)
|
||||
|
||||
// ---- window, vsync, focus (gfx_present.c and the window back end, gfx_wsys.h; vk_device.c). Host threads only; never run guest code.
|
||||
void gfx_window_start(void); // window thread, Vulkan device, swapchain; idempotent
|
||||
void gfx_vsync_bind(uint64_t system_event); // nn::vi vsync event; signalled at 60 Hz
|
||||
uint64_t gfx_vsync_ticks(void); // ticks since start
|
||||
uint64_t gfx_vsync_time_ns(uint64_t tick); // kern_now_ns() time of a tick (pacing)
|
||||
double gfx_window_refresh_hz(void); // refresh rate of the window's monitor; 0 unknown
|
||||
int gfx_window_focused(void);
|
||||
int gfx_picture_rect(float out[4]); // where the last present put the picture, fractions of the swapchain (x, y, w, h); 0 before one
|
||||
|
||||
// ---- shader service client (gfx_shader.c)
|
||||
enum { GFX_STAGE_VS, GFX_STAGE_TCS, GFX_STAGE_TES, GFX_STAGE_GS, GFX_STAGE_FS, GFX_STAGE_CS };
|
||||
enum { GFX_TEX_1D = 1, GFX_TEX_1D_ARRAY, GFX_TEX_BUFFER, GFX_TEX_2D, GFX_TEX_2D_ARRAY, GFX_TEX_2D_MS,
|
||||
GFX_TEX_2D_MS_ARRAY, GFX_TEX_3D, GFX_TEX_CUBE, GFX_TEX_CUBE_ARRAY }; // gfx_shader.c tex_names order
|
||||
enum { GFX_TYPE_FLOAT, GFX_TYPE_SINT, GFX_TYPE_UINT };
|
||||
typedef struct { // the translator's answers (Program.cs Accessor)
|
||||
uint8_t tex_type[32], img_type[32], img_format[32]; // per c2 slot; 0 = not asked; types are GFX_TEX_*,
|
||||
// img_format is Ryujinx TextureFormat
|
||||
uint8_t attr_type[32], frag_out_type[8]; // GFX_TYPE_*
|
||||
uint16_t local[3]; uint32_t shared_bytes;
|
||||
uint8_t depth_minus_w;
|
||||
uint8_t mvprev; // VS: also write last frame's position (patch 0015,
|
||||
// vk_mvprev.c); packed as bit 7 of the depth_minus_w byte
|
||||
} GfxShaderAnswers;
|
||||
typedef struct GfxShader GfxShader; // SPIR-V words + parsed reflection; owned by the cache
|
||||
const GfxShader *gfx_shader_get(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
|
||||
const uint8_t *c1, const GfxShaderAnswers *a); // blocks; NULL on failure (counted)
|
||||
// As gfx_shader_get with c1 inside the code, but a shader in neither cache is queued to a background translation and
|
||||
// NULL returned with *pending = 1 until it is ready. A failed translation stays failed (*pending = 0).
|
||||
const GfxShader *gfx_shader_get_async(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
|
||||
const GfxShaderAnswers *a, int *pending);
|
||||
// As gfx_shader_get_async, but a shader queued for translation is moved to the front of the queue and waited for.
|
||||
// NULL when the translation failed.
|
||||
const GfxShader *gfx_shader_await(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
|
||||
const GfxShaderAnswers *a);
|
||||
uint64_t gfx_shader_async_done(void); // translations finished by the background worker
|
||||
void gfx_shader_counts(uint64_t *keys, uint64_t *translated); // distinct keys in memory, translations done (NX2NAVI_TRACE=perf)
|
||||
// Translator answers for this device (vk_caps.c): shaderFloat64, storage offset rounding, subgroup size per vs/fs/cs.
|
||||
void gfx_shader_set_host(int float64, uint32_t sbuf_align, const uint32_t subgroup[3], uint32_t gather_bits, int zero_locals);
|
||||
const uint8_t *gfx_shader_key(const GfxShader *s); // the 32-byte cache key (stable while the translator is)
|
||||
const uint8_t *gfx_shader_input_id(const GfxShader *s); // the 32-byte input id (stable across translators)
|
||||
// By input id from memory or disk; xlate 1 also translates it from its stored inputs. NULL if absent.
|
||||
const GfxShader *gfx_shader_load_input(const uint8_t iid[32], int xlate);
|
||||
// Boot prewarm: out[i] = gfx_shader_load_input(iids[i], 1) on `threads` threads with up to `procs` translators, then
|
||||
// every other stored input not cached under this translator is translated. Logs progress and the total.
|
||||
void gfx_shader_prewarm(const uint8_t (*iids)[32], size_t n, const GfxShader **out, int threads, int procs);
|
||||
// Answers met in play: from this call on, every (program, answers) a gfx_shader_get* call resolves is appended
|
||||
// to shader-answers.bin, and its program to shader-programs.bin, in the directory of `keys_path` (pipeline-keys.bin).
|
||||
void gfx_shader_record_at(const char *keys_path);
|
||||
void gfx_shader_record_flush(int force); // appends pending records; without force at most every 2 s
|
||||
// Translates every recorded (program, answers) on `threads` threads with up to `procs` translators (NX2NAVI_TEST_PREPARE_SHADERS).
|
||||
void gfx_shader_prepare_recorded(int threads, int procs);
|
||||
// nx2navi.exe --prepare-shaders: translates <dir>/seed-answers.bin + seed-programs.bin (tools/prepare_shaders.py) and
|
||||
// writes <dir>/shader-prepared.bin (stage, priority, input id per variant). 0 on success.
|
||||
int gfx_shader_prepare_seed(const char *dir, int threads, int procs);
|
||||
typedef struct { uint8_t iid[32]; uint8_t stage, prio; } GfxWarmEnt; // prio 0: met in play; 1..3: prepared
|
||||
size_t gfx_shader_warm_list(GfxWarmEnt **out); // malloc'd; recorded first, then prepared; needs recording on
|
||||
// Disk-cache load the caller owns (*owned 1: release it), or the memory cache's entry (*owned 0). NULL if not cached.
|
||||
const GfxShader *gfx_shader_load_private(const uint8_t iid[32], int *owned);
|
||||
void gfx_shader_release_private(const GfxShader *s);
|
||||
const uint32_t *gfx_shader_spirv(const GfxShader *s, uint64_t *words);
|
||||
// A shader of the runtime's own (SPIR-V and a .res.json text as the translator writes them), outside the cache and
|
||||
// never freed; its key is the sha256 of both. NULL when the SPIR-V is malformed.
|
||||
const GfxShader *gfx_shader_own(const uint32_t *spirv, uint64_t words, const char *res_json);
|
||||
// vk_hdr.c: with NX2NAVI_HDR=1, replaces a fragment shader's SPIR-V (malloc'd, *words long) by the HDR tonemap when it is
|
||||
// the game's tonemap; gfx_shader.c calls it for every fragment shader entering the cache.
|
||||
void vk_hdr_shader_swap(uint32_t **spirv, uint64_t *words);
|
||||
uint64_t gfx_shader_failures(void); // failed translations; feeds GfxStats.shader_failures
|
||||
// Reflection: parsed once per shader from the translator's .res.json and the SPIR-V before it enters the cache.
|
||||
enum { GFX_RES_UBO, GFX_RES_SSBO, GFX_RES_TEX, GFX_RES_IMG, GFX_RES_KINDS };
|
||||
typedef struct {
|
||||
uint16_t set, binding; // as the SPIR-V decorates them: set 0, bindings from 1 per shader (0: support buffer)
|
||||
int16_t slot; // UBO: c[slot]; SSBO: storage buffer index; TEX: c2 combined slot (handle at 0x20 + 8 slot)
|
||||
// or, separate, texture slot (0x168 + 8 slot); IMG: image slot (0x120 + 8 slot); -1: other
|
||||
uint16_t type; // TEX/IMG: Ryujinx SamplerType: 1 1D, 2 buffer, 3 2D, 4 3D, 5 cube | 0x100 array,
|
||||
// 0x200 multisample, 0x400 shadow
|
||||
uint8_t format; // IMG: declared format as a GfxShaderAnswers.img_format code, 0 unknown
|
||||
uint8_t flags; // SSBO: 1 written; TEX/IMG: Ryujinx TextureUsageFlags (4 image store, 8 coherent)
|
||||
int8_t cbuf_slot; // TEX/IMG: constant buffer of the handle (-1: bound texture buffer); SSBO: of its address
|
||||
uint16_t handle; // TEX/IMG: raw handle (word offset; separate: texture | sampler << 14); SSBO: address offset
|
||||
uint16_t count; // descriptors (array length)
|
||||
uint8_t separate; // TEX: texture and sampler handles are separate
|
||||
} GfxShaderRes;
|
||||
typedef struct { uint8_t type, comps, packed; } GfxShaderIo; // GFX_TYPE_*, components, 1 with a Component decoration
|
||||
int gfx_shader_stage(const GfxShader *s); // GFX_STAGE_VS, GFX_STAGE_FS or GFX_STAGE_CS
|
||||
int gfx_shader_resources(const GfxShader *s, int kind, const GfxShaderRes **res); // count of GFX_RES_* kind; array in *res
|
||||
uint32_t gfx_shader_slot_mask(const GfxShader *s); // texture/image slots whose type its translation read
|
||||
uint32_t gfx_shader_inputs(const GfxShader *s, GfxShaderIo io[32]); // Input Locations (VS attributes, FS varyings) as a mask
|
||||
uint32_t gfx_shader_outputs(const GfxShader *s, GfxShaderIo io[32]);
|
||||
uint32_t gfx_shader_mvprev_banks(const GfxShader *s); // a last-frame variant: the banks (bit n: cn) its position reads // Output Locations (VS varyings, FS colour targets)
|
||||
void gfx_shader_local_size(const GfxShader *s, uint32_t local[3]); // CS: the LocalSize execution mode; 1, 1, 1 otherwise
|
||||
|
||||
// ---- back end (gfx_null.c, vk_*.c). Called by the NVN front end (nvn_api_*.c, nvn_ext.c) on guest threads.
|
||||
typedef struct {
|
||||
const char *name;
|
||||
int (*pool_init)(pool_t *p); // 0 fails MemoryPoolInitialize
|
||||
void (*pool_fini)(pool_t *p);
|
||||
int (*pool_map_virtual)(pool_t *v, int n, const void *requests); // NVNmappingRequest[n], 0x28 bytes each
|
||||
void (*texture_register)(int id, const tex_t *t, const view_t *v);
|
||||
void (*sampler_register)(int id, const samp_par *s);
|
||||
void (*texture_release)(const tex_t *t);
|
||||
void (*program_release)(const prog_t *p);
|
||||
uint64_t (*submit)(const uint64_t *handles, int n); // replays; returns this submission's fence value
|
||||
int (*fence_wait)(uint64_t value, uint64_t timeout_ns); // 1 reached; caller has given its core up
|
||||
uint64_t (*fence_done)(void); // highest value whose side effects are written
|
||||
// Queues the present and returns at once; the host present goes out at at_ns (kern_now_ns clock), at once when that
|
||||
// has passed or at_ns is 0.
|
||||
void (*present)(const tex_t *t, const int crop[4], uint64_t at_ns);
|
||||
void (*finish)(void);
|
||||
void (*read_texels)(const tex_t *t, const view_t *v, const NVNcopyRegion *r, uint8_t *dst, uint64_t row, uint64_t img);
|
||||
void (*write_texels)(const tex_t *t, const view_t *v, const NVNcopyRegion *r, const uint8_t *src, uint64_t row, uint64_t img);
|
||||
void (*guest_written)(uint64_t addr, uint64_t size); // host wrote pool storage (3.2)
|
||||
void (*guest_read)(uint64_t addr, uint64_t size); // the CPU reads pool storage: GPU-written texels go there first
|
||||
// The CPU waited for fence: small render targets written by work up to it go to their storage (vk_texture.c
|
||||
// vk_texture_write_back_small).
|
||||
void (*cpu_synced)(uint64_t fence);
|
||||
} GfxBackend;
|
||||
extern const GfxBackend gfx_null_backend, gfx_vulkan_backend;
|
||||
const GfxBackend *gfx_backend(void); // NX2NAVI_GPU=null|vulkan (default vulkan)
|
||||
|
||||
// ---- NVN front end (nvn_procs.c)
|
||||
void nvn_procs_register(void); // hostfn_register every proc; before hostfn_seal (ov_gfx.c)
|
||||
uint64_t nvn_proc_address(const char *name); // synthetic host address of a proc, 0 for an unknown name
|
||||
|
||||
// ---- statistics and frames (nvn_api_*.c, vk_*.c; boot.c prints them)
|
||||
typedef struct { uint64_t frames, nvn_calls, nvn_distinct, unimplemented, validation_errors, shader_failures,
|
||||
draws_skipped; } GfxStats;
|
||||
GfxStats gfx_stats(void);
|
||||
void rt_frame_presented(const tex_t *t, uint64_t n); // boot.c: --frames N ends the run after the Nth present
|
||||
// (saves --shot first through read_texels)
|
||||
128
runtime/gfx_abi_probe.c
Normal file
128
runtime/gfx_abi_probe.c
Normal file
@@ -0,0 +1,128 @@
|
||||
// --probe-gfx-abi: every host object of nvn_obj.h fits the storage nvn.h reserves
|
||||
// for its NVN type, the layouts the game reads directly (NVNtextureView, the command buffer's inline fast-path
|
||||
// fields) sit where NX2NAVI's NVN 55 code expects them, and the GPU address window lies above every guest region.
|
||||
// The same facts are static assertions in nvn_obj.h; this mode prints the sizes so a change shows its margin.
|
||||
// Needs no game files.
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "gfx.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static int failures, checks;
|
||||
#define CHECK(cond, ...) do { checks++; if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
|
||||
|
||||
typedef struct { const char *host, *nvn; size_t size, reserved; } Fit;
|
||||
#define FIT(T, H) {#T, #H, sizeof(T), sizeof(H)}
|
||||
static const Fit fits[] = {
|
||||
FIT(dev_builder_t, NVNdeviceBuilder), FIT(dev_t_, NVNdevice),
|
||||
FIT(queue_builder_t, NVNqueueBuilder), FIT(queue_t, NVNqueue),
|
||||
FIT(pool_builder_t, NVNmemoryPoolBuilder), FIT(pool_t, NVNmemoryPool),
|
||||
FIT(dpool_t, NVNtexturePool), FIT(dpool_t, NVNsamplerPool),
|
||||
FIT(tex_builder_t, NVNtextureBuilder), FIT(tex_t, NVNtexture),
|
||||
FIT(view_t, NVNtextureView),
|
||||
FIT(samp_builder_t, NVNsamplerBuilder), FIT(samp_t, NVNsampler),
|
||||
FIT(prog_t, NVNprogram),
|
||||
FIT(sync_t, NVNsync),
|
||||
FIT(ev_builder_t, NVNeventBuilder), FIT(ev_t, NVNevent),
|
||||
FIT(win_builder_t, NVNwindowBuilder), FIT(win_t, NVNwindow),
|
||||
FIT(cmd_t, NVNcommandBuffer),
|
||||
FIT(color_st, NVNcolorState), FIT(chmask_st, NVNchannelMaskState),
|
||||
FIT(blend_st, NVNblendState), FIT(ms_st, NVNmultisampleState),
|
||||
FIT(poly_st, NVNpolygonState), FIT(ds_st, NVNdepthStencilState),
|
||||
FIT(va_st, NVNvertexAttribState), FIT(vs_st, NVNvertexStreamState),
|
||||
};
|
||||
|
||||
_Static_assert(sizeof(NVNtextureView) == 16, "NVNtextureView is 16 bytes in NX2NAVI's NVN 55");
|
||||
_Static_assert(offsetof(cmd_t, inline_cur) == 0x20 && offsetof(cmd_t, inline_end) == 0x28, "cmd_t inline fields");
|
||||
_Static_assert(RT_GPU_VA_BASE >= RT_ADDRESS_LIMIT, "GPU address window overlaps the guest address space");
|
||||
|
||||
// Write watch: a committed heap page and an alias page, written once, are reported once.
|
||||
static void check_write_watch(void) {
|
||||
const RtGuestLayout *g = mem_layout();
|
||||
uint64_t heap = 0, pages[8], n;
|
||||
CHECK(mem_set_heap_size(0x200000, &heap) == 0 && heap == g->heap.base, "SetHeapSize 2 MiB");
|
||||
mem_written_pages(heap, 0x200000, pages, 8); // drop whatever the commit itself marked
|
||||
volatile uint8_t *p = (volatile uint8_t *)(uintptr_t)heap;
|
||||
p[0x5000] = 1;
|
||||
n = mem_written_pages(heap, 0x200000, pages, 8);
|
||||
CHECK(n == 1 && pages[0] == heap + 0x5000, "heap write: %llu pages reported (want 1 at %#llx)",
|
||||
(unsigned long long)n, (unsigned long long)(heap + 0x5000));
|
||||
printf("write-watch: heap page written once: %llu reported at +%#llx\n", (unsigned long long)n,
|
||||
n ? (unsigned long long)(pages[0] - heap) : 0ull);
|
||||
n = mem_written_pages(heap, 0x200000, pages, 8);
|
||||
CHECK(n == 0, "second call reported %llu pages (want 0)", (unsigned long long)n);
|
||||
printf("write-watch: second call: %llu reported\n", (unsigned long long)n);
|
||||
p[0x7000] = 2;
|
||||
p[0x9000] = 3;
|
||||
p[0xA000] = 4;
|
||||
n = mem_written_pages(heap, 0x200000, pages, 2);
|
||||
uint64_t m = mem_written_pages(heap, 0x200000, pages + 2, 6);
|
||||
printf("write-watch: three pages written, max 2: %llu then %llu\n", (unsigned long long)n, (unsigned long long)m);
|
||||
CHECK(n == 2 && m == 1, "max 2 reported %llu, then %llu (want 2, 1)", (unsigned long long)n, (unsigned long long)m);
|
||||
|
||||
// Frozen region: read-only until the first write thaws it (the write lands and is reported); mem_thaw (a file
|
||||
// read) thaws it and holds it thawed; SetHeapSize below decommits it.
|
||||
mem_written_pages(heap, 0x200000, pages, 8);
|
||||
CHECK(mem_freeze(heap) == 1 && mem_frozen(heap), "freeze of an idle committed 2 MiB region");
|
||||
uint64_t end;
|
||||
CHECK(plat_mem_state(heap, heap + 1, &end) == PLAT_PROT_READ, "frozen region is not read-only");
|
||||
p[0x3000] = 7;
|
||||
CHECK(!mem_frozen(heap) && p[0x3000] == 7, "a write into a frozen region did not thaw it");
|
||||
n = mem_written_pages(heap, 0x200000, pages, 8);
|
||||
CHECK(n == 1 && pages[0] == heap + 0x3000, "write after a thaw: %llu pages reported (want 1)", (unsigned long long)n);
|
||||
CHECK(mem_freeze(heap) == 1, "refreeze after a write thaw");
|
||||
mem_thaw(heap + 0x6000, 0x1000);
|
||||
CHECK(!mem_frozen(heap) && mem_freeze(heap) == 0, "mem_thaw did not thaw and hold the region");
|
||||
p[0x6000] = 8;
|
||||
printf("write-watch: frozen region thawed by a write (reported once) and by mem_thaw (held)\n");
|
||||
|
||||
uint64_t a = g->alias.base + 0x10000;
|
||||
CHECK(mem_map_physical(a, 0x4000) == 0, "MapPhysicalMemory");
|
||||
mem_written_pages(a, 0x4000, pages, 8);
|
||||
*(volatile uint8_t *)(uintptr_t)(a + 0x1000) = 1;
|
||||
n = mem_written_pages(a, 0x4000, pages, 8);
|
||||
CHECK(n == 1 && pages[0] == a + 0x1000, "alias write: %llu pages reported (want 1)", (unsigned long long)n);
|
||||
CHECK(mem_written_pages(a, 0x4000, pages, 8) == 0, "alias: second call reported pages");
|
||||
|
||||
// A region with reserved holes freezes its committed pages only, and a write thaws them without committing a hole.
|
||||
uint64_t ra = a & ~0x1fffffull;
|
||||
CHECK(mem_freeze(ra) == 1 && mem_frozen(ra), "freeze of a 2 MiB region with reserved holes");
|
||||
CHECK(plat_mem_state(a, a + 1, &end) == PLAT_PROT_READ, "its committed pages are not read-only");
|
||||
*(volatile uint8_t *)(uintptr_t)(a + 0x2000) = 9;
|
||||
CHECK(!mem_frozen(ra) && *(volatile uint8_t *)(uintptr_t)(a + 0x2000) == 9, "a write did not thaw the region with holes");
|
||||
CHECK(plat_mem_state(a, a + 0x10000, &end) == PLAT_PROT_RW && end == a + 0x4000, "the thawed pages are not read-write");
|
||||
CHECK(plat_mem_state(a + 0x4000, a + 0x4001, &end) == PLAT_MEM_RESERVED, "a hole got committed");
|
||||
printf("write-watch: region with reserved holes frozen and thawed by a write\n");
|
||||
CHECK(mem_unmap_physical(a, 0x4000) == 0, "UnmapPhysicalMemory");
|
||||
CHECK(mem_written_pages(RT_KERNEL_BASE, 0x1000, pages, 8) == UINT64_MAX, "kernel region is not write-watched");
|
||||
CHECK(mem_set_heap_size(0, &heap) == 0, "SetHeapSize 0");
|
||||
printf("write-watch: alias page written once: reported once, then not\n");
|
||||
}
|
||||
|
||||
int probe_gfx_abi(void) {
|
||||
size_t tightest = (size_t)-1;
|
||||
const char *tight = "";
|
||||
for (size_t i = 0; i < sizeof fits / sizeof *fits; i++) {
|
||||
const Fit *f = &fits[i];
|
||||
CHECK(f->size <= f->reserved, "%s is %zu bytes, %s reserves %zu", f->host, f->size, f->nvn, f->reserved);
|
||||
if (f->reserved - f->size < tightest) tightest = f->reserved - f->size, tight = f->host;
|
||||
}
|
||||
CHECK(sizeof(NVNtextureView) == 16 && sizeof(view_t) == 16, "NVNtextureView %zu bytes, view_t %zu (want 16)",
|
||||
sizeof(NVNtextureView), sizeof(view_t));
|
||||
CHECK(offsetof(cmd_t, guest_flags) == 0x10, "cmd_t.guest_flags at %#zx (want 0x10)", offsetof(cmd_t, guest_flags));
|
||||
CHECK(offsetof(cmd_t, inline_cur) == 0x20, "cmd_t.inline_cur at %#zx (want 0x20)", offsetof(cmd_t, inline_cur));
|
||||
CHECK(offsetof(cmd_t, inline_end) == 0x28, "cmd_t.inline_end at %#zx (want 0x28)", offsetof(cmd_t, inline_end));
|
||||
CHECK(sizeof(samp_par) == 44, "samp_par is %zu bytes (want 44)", sizeof(samp_par));
|
||||
CHECK(RT_GPU_VA_BASE >= RT_ADDRESS_LIMIT && RT_GPU_VA_SIZE > 0, "GPU address window %#llx overlaps the guest space",
|
||||
RT_GPU_VA_BASE);
|
||||
check_write_watch();
|
||||
printf("gfx-abi: %d layouts fit (tightest %s, %zu bytes spare), cmd_t inline fields at +0x20/+0x28, "
|
||||
"NVNtextureView %zu bytes\n", (int)(sizeof fits / sizeof *fits), tight, tightest, sizeof(NVNtextureView));
|
||||
if (failures) {
|
||||
printf("probe-gfx-abi: FAILED (%d of %d checks)\n", failures, checks);
|
||||
return 1;
|
||||
}
|
||||
printf("probe-gfx-abi: passed\n");
|
||||
return 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user