Merge pull request #2 from mitch030504/claude/peaceful-keller-2ek99b

Steam Frame beta: device fixes, 120 Hz pacing, README and release
This commit is contained in:
mitch030504 authored and GitHub committed 2026-10-05 08:18:41 +02:00
commit 26c8305c80
18 files changed
+582 -246

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
name: Steam Frame release
# Publishes a GitHub pre-release with the notes in docs/releases/<tag>.md, either for a pushed
# frame-<something> tag or, run by hand (Actions > Steam Frame release > Run workflow), for a tag
# GitHub creates on the commit the run is for. Source only: the game is built from the player's own
# disc and nothing built from it is distributed (README, Requirements). The v* tags stay with
# package.yml, which packages upstream's Windows installer.
on:
push:
tags: ['frame-*']
workflow_dispatch:
inputs:
tag:
description: Release tag, frame-<something>; docs/releases/<tag>.md holds its notes
required: true
default: frame-beta-1
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Publish the pre-release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
REPOSITORY: ${{ github.repository }}
DISPATCHED: ${{ github.event_name == 'workflow_dispatch' }}
run: |
case "$TAG" in
frame-*) ;;
*) echo "::error::Steam Frame release tags start with frame-, not '$TAG'"; exit 1 ;;
esac
notes="docs/releases/${TAG}.md"
if [ ! -f "$notes" ]; then
echo "::error::No release notes at $notes"
exit 1
fi
if [ "$DISPATCHED" = "true" ]; then
# GitHub creates the tag on this run's commit.
where=(--target "$GITHUB_SHA")
else
where=(--verify-tag)
fi
gh release create "$TAG" --repo "$REPOSITORY" "${where[@]}" --prerelease \
--title "WiiCompiled VR for the Steam Frame ${TAG#frame-}" --notes-file "$notes"
+1 -1
View File
@@ -50,7 +50,7 @@ foreach ($relative in $sourceFiles | Sort-Object -Unique) {
# Keep this list explicit: a new unresolved guest dependency must fail the test.
[uint32]$entry = 0x80001000L
[uint32[]]$guestCallbacks = @(
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
0x801284B4L, 0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL,
0x8056A470L, 0x8056A580L, 0x805A6C58L
)
+8 -2
View File
@@ -41,6 +41,7 @@ mirror_view = "normal"
controller_mode = "wii_remote"
frame_interpolation_fps = 0
refresh_rate = 0
repeat_frames = false
render_scale = 1.0
world_units_per_meter = 500.0
hud_distance_meters = 2.0
@@ -231,6 +232,10 @@ Frame) to leave the headset's own. The game renders 60 frames a second, so 120 H
for exactly two refreshes. A rate the runtime does not list, or declines, is logged and leaves its
own; setting `0` again restores the rate the session started at. It is live from F10 / the headset
panel (*Headset refresh rate*) and the Quest launcher; runtimes without the extension ignore it.
`repeat_frames` (default off, on for the Steam Frame) submits the last frame again, with the poses it
was rendered for, on each refresh the game has no new frame for, so a runtime sees the app at the
display's rate and does not halve it and fill refreshes itself (SteamVR on the Frame did, doubling the
HUD while the head turned). Render-first pacing only; live from F10 / the headset panel.
## Controllers
@@ -1088,8 +1093,9 @@ did (39 to 41.5 FPS).
With `eye_tracked_foveation`, a runtime that offers `XR_EXT_eye_gaze_interaction` and reports an eye
tracker (the Steam Frame's SteamVR) has its gaze pose located for each packet's display time and
turned into tangents of each eye's view (`vr/eye_gaze.h`), which `AuroraStereoFrame` carries as
`gaze`/`gazeValid`. Aurora centres the level's rings on the gaze snapped to a cell of two map texels
(about 3 degrees), keeping up to 32 maps per eye, one per cell looked at, and binds a new one once
`gaze`/`gazeValid`. Aurora centres the level's rings, each widened by 8 degrees to cover the lag
and error of tracking, on the gaze snapped to a cell of two map texels (about 3 degrees), keeping up
to 128 maps per eye, one per cell looked at, and binds a new one once
its upload completes, the previous map staying bound meanwhile. Without a tracked gaze (a blink, no
tracker, the setting off) the map is the forward one above, unchanged. Details and the Steam Frame
checks are in `docs/steam-frame.md`.
+163 -219
View File
@@ -1,263 +1,199 @@
<img width="4190" height="2464" alt="Mario Kart WiiCompiled VR logo (logo by Inkwreck)" src="docs/images/wiicompiled-vr-logo.png" />
# WiiCompiled OpenXR VR
# WiiCompiled VR for the Steam Frame
<p align="center">
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Windows 10 / 11, x64" src="https://img.shields.io/badge/Windows-10%20%2F%2011%20%C2%B7%20x64-0078D4"></a>
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Linux, x64 / ARM64" src="https://img.shields.io/badge/Linux-x64%20%2F%20ARM64-FCC624?logo=linux&amp;logoColor=white"></a>
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="macOS 14+, Apple Silicon" src="https://img.shields.io/badge/macOS-14%2B%20%C2%B7%20Apple%20Silicon-0A84FF?logo=apple&amp;logoColor=white"></a>
</p>
<p align="center">
<a href="#building-from-source"><img alt="PowerPC static recompilation" src="https://img.shields.io/badge/PowerPC-static%20recompilation-FF9F0A"></a>
<a href="#retro-rewind"><img alt="Retro Rewind supported" src="https://img.shields.io/badge/Retro%20Rewind-supported-FF375F"></a>
<a href="https://github.com/TeamWheelWizard/WheelWizard/releases"><img alt="Install with Wheel Wizard" src="https://img.shields.io/badge/install%20with-Wheel%20Wizard-8B5CF6"></a>
<img alt="Steam Frame, SteamOS ARM64" src="https://img.shields.io/badge/Steam%20Frame-SteamOS%20%C2%B7%20ARM64-1A9FFF?logo=steam&amp;logoColor=white">
<a href="https://github.com/mitch030504/Wiicompiled_VR_Frame/releases"><img alt="Status: beta" src="https://img.shields.io/badge/status-beta-FF9F0A"></a>
<a href="https://github.com/iChris4/Wiicompiled_VR"><img alt="Fork of WiiCompiled OpenXR VR" src="https://img.shields.io/badge/fork%20of-WiiCompiled%20OpenXR%20VR-8B5CF6"></a>
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&amp;logoColor=white"></a>
</p>
A native PC port of Mario Kart Wii, made with static recompilation.
There's no emulator in the loop, no interpreter, no JIT, no PowerPC
anywhere at runtime.
Mario Kart Wii in VR on Valve's Steam Frame, running natively on SteamOS: a fork of
[WiiCompiled OpenXR VR](https://github.com/iChris4/Wiicompiled_VR) (itself built on
[WiiCompiled](https://github.com/patchzyy/Wiicompiled)), the static recompilation of Mario Kart Wii
to native code. There is no emulator, interpreter or JIT at runtime: your own disc is translated to
C++ and compiled for the Frame's ARM64 CPU, and it renders through SteamVR's OpenXR runtime.
> [!IMPORTANT]
> There is no Nintendo code, no assets and no game data anywhere in this project or its releases.
> You need your own legally dumped copy of the PAL version of the game. Setup only ships the
> toolchain, the translation runs on your machine against your disc image, and nothing ever gets
> uploaded.
> There is no Nintendo code, no assets and no game data anywhere in this project. You need your
> own legally dumped copy of the PAL version of the game; the translation runs on your machine
> against your disc image, and nothing is uploaded.
[Download WheelWizard VR](https://github.com/iChris4/WheelWizard_VR/releases/latest)
> [!WARNING]
> **Beta.** It runs on a Steam Frame: SteamVR, both eyes at the panels' 2160x2160, the Frame's
> controllers, 120 Hz with every game frame shown, and foveation that follows your eyes. It is not
> finished: images still double in races and on the HUD, and the foveation tracks the right eye
> less well than the left (see [Known issues](#known-issues)). Reports with the run log move it
> forward (see [Reporting problems](#reporting-problems)).
---
## What it does
## What this fork adds
**Unlocked framerate with interpolation.**
The original game is hard-locked to 60 fps. The runtime can generate interpolated frames in between, so on a
120/144 Hz monitor things genuinely look smoother.
Everything below is in [`docs/steam-frame.md`](docs/steam-frame.md), with the reasoning and the
readings it is based on.
> [!WARNING]
> Interpolation is experimental right now and will show artifacts in specific scenarios.
- **A native SteamOS build.** The game links SteamVR's OpenXR runtime directly and lets it create
the GPU device it renders with, so each eye is copied straight into the headset's swapchain with
no second device and no sharing between them. It runs fullscreen in the headset only; there is
no desktop window to draw.
- **Built for the Frame's Snapdragon 8 Gen 3**: compiled with `-mcpu=cortex-x4`.
- **The Frame's controllers.** Its own interaction profile is bound, so beside what the Quest Touch
layout already does, the left D-pad is the Wii Remote's D-pad (tricks, menus), the left View
button pauses and the left shoulder opens the settings panel.
- **120 Hz.** The headset is asked for 120 Hz, exactly two display refreshes per game frame at the
game's 60 FPS, so motion is even. `[vr] refresh_rate` changes it.
- **Every refresh from the game.** SteamVR halved the app's rate and filled every other refresh
itself. The game now submits its last frame again, at the pose it was rendered for, on each
refresh it has no new frame for (`[vr] repeat_frames`), so SteamVR runs it at the full 120 Hz.
- **Eye-tracked foveation.** Variable-resolution rendering whose sharp centre follows your gaze
through the Frame's eye tracking, instead of staying fixed straight ahead. `[vr] foveation` sets
the strength and `[vr] eye_tracked_foveation` turns the tracking off.
- **Standalone defaults**: the game's own object culling and medium foveation, as on the Quest.
The render scale defaults to 0.8; on the Frame 1.0 is SteamVR's recommended 1728x1728 and **1.25
is the panels' native 2160x2160**, which the Frame renders with time to spare.
- **Crash diagnostics on Linux.** A native crash logs the faulting thread, its pc and a backtrace,
which is how a crash on race restart was traced to the density maps sharing memory Dawn unmaps.
- **Build tooling.** `Launcher/build-dawn-linux.sh` builds the patched Dawn (Aurora's WebGPU
layer) the VR backend needs, and `Launcher/local-build.sh` gained `--openxr`, `--dawn-package`,
`--headset steam_frame` and `--cpu`.
**Any aspect ratio you want.**
Drag the window bigger, wider, whatever, the camera adjusts
live.
**Native rendering via aurora.**
The graphics layer is built on
[aurora](https://github.com/encounter/aurora). Aurora is a source-level GameCube & Wii compatibility layer.
**High internal resolution.**
Play at several times the console's resolution.
**Experimental OpenXR VR.**
Windows builds can render through an OpenXR runtime on D3D12, or on Vulkan with a custom Dawn
build, without CPU readback. Menus and
unsupported scenes appear as a head-locked virtual screen; a validated single-camera race switches
to immersive stereo rendering. VR is opt-in and falls back to the normal desktop renderer if the
runtime or headset is unavailable. In first person you sit in the cockpit, where the steering wheel
or handlebar turns with your steering, and hand steering by heurazy lets you grab it with the
tracked controllers and turn it. On a Quest the hands can follow the headset's own hand tracking.
A native SteamOS build for the Steam Frame (not yet tested on the headset) adds the Frame controllers'
D-pad, a 120 Hz display for the game's 60 FPS, and foveation that follows your eyes; see
[`docs/steam-frame.md`](docs/steam-frame.md).
See [`OPENXR.md`](OPENXR.md) for setup, configuration, and the current limitations.
**Music ducking.**
Start playing something else, Spotify, a YouTube video, and
the game automatically mutes its own music until the other audio stops. Optional, if you'd
rather it didn't. All audio that shows in your display media controls on your windows pc fall under this.
**An in-game settings bar.**
Press **F10** while the game window has focus:
- Internal resolution
- FPS counter
- Controller assignment for all four ports
- Full per-controller button mapping, including the bumpers
- Dolphin-syntax input expressions and GCPadNew.ini import
- Controller vibration on/off
- Volume, instant mute, and the music ducking toggle
Everything you change is saved to `Config.toml` on the spot and restored next launch.
**Dolphin-compatible input expressions.**
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
and the same functions.
A Dolphin `GCPadNew.ini` can be imported directly from the F10 bar.
**Vibration toggle.**
Force feedback can be turned off for every port at once.
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
adapter must be switched to the WinUSB driver once (Zadig).
**Real Wii Remotes over Bluetooth.**
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
PIN empty)
Known limitations of the Wii Remote path:
- No IR pointer yet: menus are navigated with the D-pad and A (the game treats the remote as
pointing away from the screen).
- Battery level is not reported to the game and the remote's speaker is not implemented.
- Only the Wii Remote's own accelerometer is calibrated; the Nunchuk's uses SDL's fixed zero point.
- The Classic Controller's L/R triggers reach the game as digital (full pull on click): SDL does not
expose their analog travel.
- Turn the Wii Remote support off in that menu if you use a Mayflash DolphinBar, which already
presents the remote as a regular gamepad.
**USB steering wheels and pedals.**
Ported from heurazy's [mario-kart-wii-VR-port](https://github.com/heurazy/mario-kart-wii-VR-port).
Open **F10 > Controllers > USB wheel and pedals (player 1)**; it is also in the headset's settings
panel. Pick the steering device and axis and record full left, full right and centre, then each
pedal's released and fully pressed positions. Assign the right paddle to drift and the left paddle to
items; trick, confirm, pause and back are optional. Any wheel SDL sees as a joystick works this way,
with no gamepad mapping: separate USB pedals, reversed axes and combined pedal axes (select the same
axis for both pedals) all calibrate the same. The settings are saved in `PhysicalWheel.toml` beside
`Config.toml`.
The wheel is player 1's GameCube controller. Press its confirm button at the title screen so the game
uses a GameCube controller; its D-pad, confirm and back then work the menus. In a race it owns
steering and the pedals. The brake pedal brakes, then reverses, and beats the accelerator and drift.
In VR, the cockpit's wheel turns with it and hand steering steps aside. Setting the VR controllers to
**Gamepad** keeps them for menus, pause and item aiming alongside the wheel. Light vibration is
optional, off by default, capped at 15 % and follows the game's own rumble. No centering spring or
steering force is requested.
Logitech wheels (G29, G920, G923, G27, G25, Driving Force GT, PRO Racing Wheel) are recognised by SDL
as wheels and marked "(wheel)" in the device list. This has not been tried on a physical wheel yet:
- Install Logitech G HUB (Logitech Gaming Software for a G27 or G25). Without the driver a Logitech
wheel starts in a compatibility mode, typically with a smaller rotation range and both pedals on
one axis. A G920 or G923 for Xbox also starts as an Xbox controller, which the game would read as
an ordinary pad.
- Set a G29's mode switch to PS3 on PC.
- Full lock is wherever you record full left and right. Recording them a quarter turn each way
(90°) matches the VR cockpit's wheel, or lower the operating range in G HUB.
- A Driving Force Shifter's gears reach the game as buttons of the wheel and can be assigned like
any other. A gear stays pressed while it is engaged: on the item button it keeps the item held
behind you until you shift back to neutral. The clutch is not used.
- Turn on the centering spring in G HUB if you want the wheel to self-centre.
The Steam Frame also runs Android apps through its Lepton layer, and the Quest app gained a
`steamFrame` flavour for it, but it cannot show a picture there: Lepton's graphics driver lacks
the memory-sharing extensions the Android backend needs. The native build is the way to play.
## Requirements
- Windows 10 or 11, 64-bit
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB)
- This fork's packaged release supports Windows x64. Other platforms are not release targets.
- A Steam Frame with Developer Mode on and SSH set up (Steam Settings → System → Enable Developer
Mode, then set a user password). [Frame Control](https://github.com/saphid/frame-control) makes the
rest easier: it sets up an SSH key and installs the game into your Steam library.
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
> [!NOTE]
> GPU/CPU minimums are set by driver support and D3D12/Vulkan feature requirements, not by the game's actual demands.
Only the clean PAL revision will work. Anything else (other
regions, patched executables) is rejected outright.
GCZ, CISO, WBFS, WIA and RVZ can all be extracted. Other regions and patched executables are
rejected.
- Somewhere to build, all of them running an ARM64 Debian container:
- **an x86_64 Linux PC** with podman and qemu (emulated, so slow: the first build takes hours);
- **a stronger Linux machine or server** with Docker, the same way and faster;
- **the Frame itself**, in a podman container there.
- About 20 GB of free disk space on the build machine for the toolchain, Dawn and the game.
> [!NOTE]
> Nobody here will tell you where to get the game. Dumping your own disc is on you, and links to
> game files won't be provided or tolerated.
> game files won't be provided or tolerated. For the same reason there is no ready-built game to
> download: releases hold the source, and the game is always built from your own disc.
## Installing
## Building and installing
Use [WheelWizard VR](https://github.com/iChris4/WheelWizard_VR/releases/latest). Select your clean PAL
`RMCP01` image in Settings, then open **Settings → Other → WiiCompiled (beta)** and enable
**Enable WiiCompiled OpenXR VR (beta)**. Press Install on Home. Installation builds both Base game
and Retro Rewind locally using the bundled toolchain; a developer toolchain is not required.
The commands are in [`docs/steam-frame.md`](docs/steam-frame.md): [on a Linux
PC](docs/steam-frame.md#building-it-on-a-linux-pc), [with Docker on another
machine](docs/steam-frame.md#building-it-with-docker-on-another-machine) or [on the
Frame](docs/steam-frame.md#building-it-on-the-frame). In short:
Home lets you choose **Base game** or **Retro Rewind**. The normal WiiCompiled switch selects the
original backend; turning both switches off selects Dolphin. Only one recompilation switch can
be enabled at a time. VR uses a separate `RecompVR` installation beside the normal `Recomp` folder.
Saves and Miis use the normal installation's effective NAND; Retro Rewind retains its separate
XML-directed saves and ghosts. Graphics, VR preferences, caches, and compiled binaries stay separate.
Uninstalling either backend in WheelWizard VR preserves configuration and shared progress.
1. Extract your disc with [nodtool](https://github.com/encounter/nod) and copy `sys/main.dol` and
`files/rel/StaticR.rel` into `Assets/`. Keep the extracted disc: the game reads it at run time.
2. Start a Debian trixie ARM64 container and install the build packages, the bundled clang 22,
CMake and Ninja (`Launcher/prepare-portable-tools.sh --arch aarch64`), and .NET 8.
3. Build the patched Dawn once: `Launcher/build-dawn-linux.sh`. Under emulation give it `--jobs 4`
on a 16 GB machine; more parallel jobs can run it out of memory.
4. Build the game: `Launcher/local-build.sh ... --openxr --dawn-package <dawn>/package --headset
steam_frame`, with `--parallel` sized to the machine's memory (8 for 32 GB).
5. Copy the extracted disc to the Frame, and either send the `out` folder to the Frame with
[Frame Control](docs/steam-frame.md#installing-it-with-frame-control), which adds it to your
Steam library, or copy it over with `scp`.
6. Write `~/.local/share/WiiCompiled/Config.toml` on the Frame with your disc's path, then start
the game from the library in the headset:
Managed VR launches enable OpenXR with D3D12; the Vulkan binding is opt-in through
`video.graphics_api` (see [OPENXR.md](OPENXR.md)). If the runtime or headset is unavailable, the game
continues on the desktop and displays the failure briefly; **F10 → VR** retains the explanation.
See [OpenXR configuration](OPENXR.md) and [distribution and validation](DISTRIBUTION.md).
```toml
[paths]
dvd_root = "/home/steamos/wiicompiled/disc"
```
### Recommended settings
> [!CAUTION]
> Only take builds from this repository's
> [Releases](https://github.com/iChris4/Wiicompiled_VR/releases) page. If someone's sharing an
> installer through Discord or some random download site, don't touch it!!
All of them are in the headset's settings panel (left shoulder button, **VR** tab) as well as in
`Config.toml`:
| Setting | Value | Why |
| --- | --- | --- |
| `[vr] render_scale` | `1.25` | The panels' native 2160x2160 per eye. |
| `[vr] foveation` | `medium` | `off` costs the most GPU time. |
| `[vr] repeat_frames` | `true` (default) | Keeps SteamVR at 120 Hz. |
| `[vr] frame_interpolation_fps` | `0` | Interpolation made things worse on the Frame. |
| `[video] resolution_multiplier` | `2` | The game's own frame; 4x is too heavy for the Frame's GPU. |
Keep SteamVR's own refresh rate at 120 Hz. Motion Smoothing makes no difference here.
## Known issues
- **Doubled images** in races and on the HUD, worst while racing, sometimes in the right eye only.
The suspect is foveation on the Frame's graphics driver. If it bothers you, try foveation **Off**
in the panel (it costs GPU time), and report whether it helped.
- **Foveation follows the right eye less well** than the left, and its sharp area may feel small
on Low and Medium.
- **VR frame interpolation** is not recommended on the Frame.
- **The Quest app's `steamFrame` flavour** (an Android build for the Frame's Lepton layer) cannot
show a picture: Lepton's graphics driver lacks the memory sharing it needs. Use the native build.
## Reporting problems
Open an issue on this repository with:
- what you did and what you saw (which eye, where in the picture, racing or menus);
- the run log: each run has a folder under `~/.local/share/WiiCompiled/Logs/` on the Frame, with
`console.log` and, after a crash, `crash_sigsegv.txt`;
- or, for a build problem, the last lines of the failing step.
[Running it](docs/steam-frame.md#running-it) lists the log lines a working start shows, in order;
the first one missing says where it stopped.
Problems that also happen on a PC or a Quest belong upstream, in
[WiiCompiled OpenXR VR](https://github.com/iChris4/Wiicompiled_VR).
## From upstream
The fork keeps everything [WiiCompiled OpenXR VR](https://github.com/iChris4/Wiicompiled_VR) does;
its README covers it in full. In the headset that means:
- Menus and unsupported scenes on a head-locked virtual screen, and races in immersive stereo.
- A first-person cockpit whose steering wheel or handlebar turns with your steering, and hand
steering by heurazy: grab the wheel with the tracked controllers and turn it.
- The settings panel in the headset (render scale, foveation, refresh rate, controls), saved to
`Config.toml` on the spot.
- Physics identical to the original game, proven by ghosts that sync across Wii, Dolphin and
WiiCompiled.
- [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind) as its own statically translated
profile.
The PC (Windows D3D12 and Vulkan) and Meta Quest builds are still here and unchanged; see
[`OPENXR.md`](OPENXR.md) and [`docs/quest-port.md`](docs/quest-port.md). For those, use upstream's
[WheelWizard VR](https://github.com/iChris4/WheelWizard_VR/releases/latest) instead of this fork.
## A note on related projects
WiiCompiled, Wheel Wizard, Retro rewind and other related projects are developed
**independently** and each has its **own** contribution rules and all have their own
rules. What applies here does not automatically apply there,
and vice versa. Check each project's own CONTRIBUTING and README files.
## Retro Rewind
[Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind), ZPL's Mario Kart Wii mod distribution,
can be built as its **own static profile**: instead of applying `Code.pul` as runtime patches,
the Kamek/Pulsar code is statically translated together with the base game into a separate native
executable.
Wheel Wizard drives this too.
## Building from source
Owning the game is still required even if you compile everything yourself.
You'll need: .NET 8 SDK, CMake, Ninja, and LLVM/Clang (the shipped build uses LLVM-MinGW targeting
`x86-64-v3`).
Build the translator:
```powershell
dotnet build translator/Translator.sln -c Release
```
The default test suite needs no binaries and no host C++ compiler, so you can hack on the
translator without any game data around.
For everything beyond that, feeding in your own `main.dol`/`StaticR.rel`, running the
translation, generating the manifest and build graph, and compiling, see [`translator/README.md`](translator/README.md).
For a step-by-step guide on compiling both WiiCompiled and Retro Rewind from source on macOS (Apple Silicon), see the [macOS Build Guide](docs/building-macos.md).
WiiCompiled, WiiCompiled OpenXR VR, Wheel Wizard, Retro Rewind and this fork are developed
**independently**, each with its **own** rules. What applies here does not automatically apply
there, and vice versa. Check each project's own CONTRIBUTING and README files.
## FAQ
**Is this an emulator?**
No. Everything is compiled to native code before you ever press play. At runtime there's nothing
emulating a Wii CPU or GPU.
No. Everything is compiled to native ARM64 code before you press play. At runtime nothing emulates
a Wii CPU or GPU.
**Do you provide the game?**
No. Don't ask. Nothing in this repo or any release contains Nintendo code or assets.
**Why does setup take so long?**
Because we **don't** ship the translated binary, most other recomp projects do, but we
don't want to risk it right now, setup has to run a static recompiler over the whole game
and then throw a C++ compiler at the result. It's a **one-time cost** on your machine.
**Do you provide the game, or a ready-built binary?**
No. Nothing in this repo contains Nintendo code or assets, and the translated game is never
shipped: it is built from your own disc, a one-time cost on your machine.
**Which game version works?**
Clean PAL `RMCP01`. Other regions and modified executables are **rejected**. Translating
them against the wrong manifest would give you a subtly broken game that's miserable to debug for us.
Clean PAL `RMCP01`. Other regions and modified executables are **rejected**.
**Can I recompile other GameCube/Wii games with it?**
The translator itself handles DOLs and RELs generically, see
`projects/examples/generic-dol.yml`. The catch is that a *playable* port also needs a runtime:
audio, input, GX, everything the game touches.
**Why not install the Quest APK on the Frame?**
The Frame runs it in Lepton, whose graphics driver cannot share images between the two GPU devices
the Android backend uses, so it shows nothing. The native build uses one device and needs no
sharing.
**The game crashed / stopped with an error.**
Errors are deliberately loud instead of quietly swallowed. Send a report along with the run log
from `%LOCALAPPDATA%\WiiCompiled\Logs`.
**Will you fix original bugs?**
Not in the base game, behavior identical to real hardware is the goal. Only report things where this port differs
from the original game. As for Retro Rewind, some base-game behavior **is** patched, so if it differs from the
base game, that's normal. If Retro Rewind behavior differs between Dolphin/Wii and WiiCompiled, open an issue on GitHub.
**How accurate are the physics?**
100% - this is proven by in-game ghosts. Since ghosts are replay files based on inputs rather
than tracked positions, matching ghosts prove the physics match across Dolphin/Wii/WiiCompiled.
**Is it done?**
Not fully. The game is in a state where everything should be playable and the physics do match
100% with the original game, but compatibility, rendering, networking and performance are all
actively being worked on. If you do find an issue, we strongly encourage you to open one on
GitHub so we can take a look at it.
**Can it run on other SteamOS or Linux ARM64 devices?**
Leave out `--headset steam_frame` and pass `--cpu` for your CPU to get a generic Linux VR build;
it needs an OpenXR runtime with `XR_KHR_vulkan_enable2`. Untested.
## AI usage
AI coding tools were used during development of this project.
@@ -271,6 +207,9 @@ All translated output is verified against real hardware behavior and most import
aurora's Direct3D, Vulkan and OpenGL backends.
- **[OpenXR](https://www.khronos.org/openxr/)** - the Khronos cross-platform API used by the
experimental VR renderer.
- **[WiiCompiled OpenXR VR](https://github.com/iChris4/Wiicompiled_VR)** by iChris4 and
**[WiiCompiled](https://github.com/patchzyy/Wiicompiled)** by patchzyy - the projects this fork
is built on.
- **heurazy** - the VR cockpit's turning steering wheel and hand steering, ported from
**[mario-kart-wii-VR-port](https://github.com/heurazy/mario-kart-wii-VR-port)** (GPL-3.0).
- **[Dolphin Emulator](https://github.com/dolphin-emu/dolphin)** - an invaluable reference for Wii
@@ -280,6 +219,11 @@ All translated output is verified against real hardware behavior and most import
distribution this project supports.
- **[Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard)** - the mod manager this
project integrates with as a launch backend.
- **[nod](https://github.com/encounter/nod)** - nodtool, the disc image extractor.
- **[DolphinXR](https://github.com/iChris4/dolphinXR)** - the Steam Frame controller profile's
input paths.
- **[Frame Control](https://github.com/saphid/frame-control)** by saphid - installing the game into
the Frame's Steam library, and its notes on how the Frame's software fits together.
- Everyone in the static recompilation community.
Bundled third-party components and their licenses live in
+5 -1
View File
@@ -185,7 +185,11 @@ elseif (_aurora_dawn_provider STREQUAL "package")
endif ()
endif ()
endif ()
message(STATUS "aurora: Fetching prebuilt Dawn package from ${AURORA_DAWN_PACKAGE_URL}")
if (FETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT)
message(STATUS "aurora: Using the local Dawn package ${FETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT}")
else ()
message(STATUS "aurora: Fetching prebuilt Dawn package from ${AURORA_DAWN_PACKAGE_URL}")
endif ()
set(_dawn_prebuilt_hash_argument "")
if (AURORA_DAWN_PACKAGE_URL_HASH)
+3 -2
View File
@@ -615,8 +615,9 @@ struct EyeDensityMap {
uint64_t map = 0;
uint64_t lastUse = 0;
};
// The gaze cells' maps an eye keeps: a few glances' worth, each 2 bytes per 32x32 pixels.
constexpr size_t kEyeDensityMapCacheSize = 32;
// The gaze cells' maps an eye keeps, each 2 bytes per 32x32 pixels in a memory block of its own:
// most of the cells a session's glances reach, so a glance rarely waits for a map to upload.
constexpr size_t kEyeDensityMapCacheSize = 128;
struct StereoEyeTarget {
webgpu::TextureWithSampler color;
+12 -3
View File
@@ -138,8 +138,16 @@ inline Gaze cell_gaze(uint32_t eyeWidth, uint32_t eyeHeight, uint32_t texel, con
.tanY = fov.tanUp + (fov.tanDown - fov.tanUp) * v};
}
inline uint8_t density(Level level, float eccentricity) noexcept {
const Rings ring = rings(level);
// Eye-tracked maps widen both rings by this much. The full-density region has to cover where the eye
// lands after a saccade until the next game frame's map is bound, the tracker's error, and the
// snapping of the gaze to cells and of the density to the driver's tiles (Turnip shades a whole
// render-pass bin at one density).
inline constexpr float kTrackedMarginDegrees = 8.0f;
inline uint8_t density(Level level, float eccentricity, float margin = 0.0f) noexcept {
Rings ring = rings(level);
ring.full += margin;
ring.half += margin;
if (eccentricity < ring.full) {
return kFullDensity;
}
@@ -176,7 +184,8 @@ inline void build(uint32_t eyeWidth, uint32_t eyeHeight, uint32_t texel, const E
static_cast<float>(eyeWidth);
const float tanX = fov.tanLeft + (fov.tanRight - fov.tanLeft) * u;
const uint8_t value =
density(level, forward ? eccentricity_degrees(tanX, tanY) : angle_from_gaze_degrees(tanX, tanY, gaze));
forward ? density(level, eccentricity_degrees(tanX, tanY))
: density(level, angle_from_gaze_degrees(tanX, tanY, gaze), kTrackedMarginDegrees);
uint8_t* texelBytes = &map.rg8[(static_cast<size_t>(y) * map.width + x) * 2];
texelBytes[0] = value;
texelBytes[1] = value;
+6 -2
View File
@@ -93,8 +93,12 @@ MaybeError AuroraFdmUpload(Device* device, uint32_t width, uint32_t height, cons
VkMemoryRequirements requirements;
device->fn.GetImageMemoryRequirements(device->GetVkDevice(), map.image, &requirements);
DAWN_TRY_ASSIGN(map.memory,
device->GetResourceMemoryAllocator()->Allocate(requirements, MemoryKind::DeviceLocal));
// A memory block of its own. Turnip reads a map through a host mapping of its memory, taken
// when the image is bound, and Dawn's buffer uploads map and unmap the shared blocks they
// sub-allocate from (Buffer::MapMemoryAndPerformOperation). An unmap there pulled the mapping
// from under a map sharing the block, and the next render pass read freed memory.
DAWN_TRY_ASSIGN(map.memory, device->GetResourceMemoryAllocator()->Allocate(
requirements, MemoryKind::DeviceLocal, /*forceDisableSubAllocation=*/true));
DAWN_TRY(CheckVkSuccess(device->fn.BindImageMemory(device->GetVkDevice(), map.image,
ToBackend(map.memory.GetResourceHeap())->GetMemory(),
map.memory.GetOffset()),
+3 -2
View File
@@ -199,8 +199,9 @@ TEST(Foveation, TheForwardGazeKeepsTheFixedMap) {
TEST(Foveation, TheFullDensityRegionFollowsTheGaze) {
const EyeFov fov = left_eye();
// Down and to the right, well off the forward direction.
const Gaze gaze{.tanX = std::tan(20.0f * kDegrees), .tanY = std::tan(-15.0f * kDegrees)};
// Down and to the right, off the forward direction, with the widened full-density region still
// inside the eye so its centre is not pulled in by the edge.
const Gaze gaze{.tanX = std::tan(12.0f * kDegrees), .tanY = std::tan(-10.0f * kDegrees)};
Map map;
foveation::build(1344, 1408, 32, fov, Level::High, map, gaze);
double sumX = 0.0;
+43
View File
@@ -0,0 +1,43 @@
The first beta of WiiCompiled VR for the Steam Frame: Mario Kart Wii, statically recompiled to native
ARM64 code, running in VR on SteamOS through SteamVR.
**This release is source only.** The game is always built from your own clean PAL `RMCP01` disc,
and nothing built from it may be distributed, so there is no ready-built game here. Follow
[Building and installing](https://github.com/mitch030504/Wiicompiled_VR_Frame#building-and-installing)
in the README; the full commands are in
[`docs/steam-frame.md`](https://github.com/mitch030504/Wiicompiled_VR_Frame/blob/frame-beta-1/docs/steam-frame.md).
## What works on the Frame
- Starts in VR from the Steam library (installed with Frame Control) with SteamVR's OpenXR runtime.
The runtime creates the GPU device the game renders with, so the eyes go straight into the
headset's swapchain.
- Both eyes at the panels' native 2160x2160 (`render_scale = 1.25`), 120 Hz, every game frame
shown: SteamVR reports 120 Hz with 60 new frames and 60 repeats a second and no late frames.
- The Frame's own controllers: left D-pad as the Wii Remote's D-pad, View to pause, left shoulder
for the settings panel.
- Fragment density map foveation that follows your eyes through the Frame's eye tracking.
## Fixed during device testing
- A crash on race restart inside the Frame's Vulkan driver: density maps shared memory that Dawn
unmaps after buffer uploads. Each map now has its own memory block (Dawn patch, so rebuild Dawn).
- SteamVR halving the game's rate and filling every other refresh itself:
`[vr] repeat_frames` (on by default for the Frame) resubmits the last frame for those refreshes.
- Native crashes on Linux now log the faulting thread, pc and backtrace.
- The eye-tracked foveation region is 8 degrees wider and each eye keeps 128 maps.
## Known issues
- Images double in races and on the HUD, sometimes in the right eye only. Foveation is the main
suspect; try foveation Off if it bothers you, and report the result.
- Foveation follows the right eye less well than the left.
- VR frame interpolation is not recommended on the Frame.
- The Android (`steamFrame`) flavour cannot show a picture in Lepton; use the native build.
## Recommended settings
`render_scale = 1.25`, `foveation = "medium"`, `repeat_frames = true`, `frame_interpolation_fps = 0`
under `[vr]`, and `resolution_multiplier = 2` under `[video]`. SteamVR at 120 Hz.
Reports with the run log from `~/.local/share/WiiCompiled/Logs/` are welcome as issues.
+154 -12
View File
@@ -15,8 +15,14 @@ Most of what this document describes is shared by both: the Frame controller pro
request, eye-tracked foveation and the Frame's defaults. The native build gets them through
`MKW_HEADSET=steam_frame` (`MKW_HEADSET_STEAM_FRAME`), as the Android flavour does.
**Status: not yet run on a Steam Frame.** The native build's VR code compiles and the unit tests
pass; building it on the Frame and the device checks are still to do.
**Status: beta, played on a Steam Frame.** The native build starts in VR under SteamVR, renders
both eyes on the device Dawn shares with the runtime, binds the Frame's controllers, runs at 120 Hz
with every game frame shown (`new=60 repeat=60` a second, no late frames, at `render_scale = 1.25`,
the panels' 2160x2160), and follows the eyes with its foveation. Two device runs found and fixed a
crash on race restart (see [Eye-tracked foveation](#eye-tracked-foveation)) and SteamVR halving the
app's rate (see [Refresh rate](#refresh-rate)). Still open, in [Known issues](#known-issues): doubled
images in races and on the HUD, sometimes in one eye only, and foveation that follows the right eye
less well than the left.
## The native SteamOS build
@@ -45,7 +51,7 @@ started with the `podman` SteamOS already ships. Over SSH (`ssh steamos@<frame-i
```bash
mkdir -p ~/wiicompiled && cd ~/wiicompiled
git clone -b claude/peaceful-keller-2ek99b https://github.com/mitch030504/Wiicompiled_VR_Frame.git
git clone https://github.com/mitch030504/Wiicompiled_VR_Frame.git
podman run -it --name wiicompiled-build -v ~/wiicompiled:/work:Z docker.io/library/debian:trixie bash
```
@@ -81,10 +87,102 @@ SteamOS's, so the binary runs on SteamOS outside the container. If CMake reports
install its `-dev` package in the container and run the same command again; both scripts resume
where they stopped.
### Building it on a Linux PC
The same container runs on an x86_64 Linux PC as an emulated ARM64 one, which spares the Frame's
storage and battery; the result is copied over. Emulation makes it several times slower: the
first Dawn build takes hours. On the PC (these commands also work in fish):
```bash
sudo pacman -S --needed podman qemu-user-static qemu-user-static-binfmt # Arch, CachyOS
sudo systemctl restart systemd-binfmt
podman run --rm --platform linux/arm64 docker.io/library/debian:trixie uname -m # prints aarch64
```
Other distributions name the packages differently (on Debian and Ubuntu: `podman
qemu-user-static binfmt-support`). If rootless podman complains about subordinate ids, run
`sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER` and log in again.
Extract your disc image with [nodtool](https://github.com/encounter/nod), the extractor the
installer uses, and put the two files the build reads into `Assets/`:
```bash
mkdir -p ~/wiicompiled; cd ~/wiicompiled
git clone https://github.com/mitch030504/Wiicompiled_VR_Frame.git
curl -fL -o nodtool https://github.com/encounter/nod/releases/download/v2.0.0-alpha.10/nodtool-linux-x86_64
chmod +x nodtool
./nodtool extract "/path/to/Mario Kart Wii.wbfs" disc-extract
mkdir -p Wiicompiled_VR_Frame/Assets
cp disc-extract/sys/main.dol disc-extract/files/rel/StaticR.rel Wiicompiled_VR_Frame/Assets/
podman run -it --name wiicompiled-frame --platform linux/arm64 -v ~/wiicompiled:/work docker.io/library/debian:trixie bash
```
Inside the container, run the commands of [Building it on the Frame](#building-it-on-the-frame)
from `apt-get` on, skipping the `cp` into `Assets/`, which is done. `podman start -ai
wiicompiled-frame` gets back into it; set `T` again before resuming. Then copy the game and the
extracted disc to the Frame:
```bash
ssh steamos@<frame-ip> mkdir -p wiicompiled
scp -r ~/wiicompiled/out steamos@<frame-ip>:wiicompiled/
scp -r ~/wiicompiled/disc-extract steamos@<frame-ip>:wiicompiled/disc
```
### Building it with Docker on another machine
A faster x86_64 machine helps most with the game itself (the translated code is a few thousand large
files). Docker works as podman does, with one difference on hosts whose `binfmt_misc` registrations
are per container (Unraid 7 with kernel 6.18): `tonistiigi/binfmt --install arm64` then succeeds but
only inside its own container, and Debian answers `exec format error`. Register the emulator on the
host instead, with the `P` flag the tonistiigi build of qemu expects (without it every program loses
its first argument: `uname -m` prints `Linux`):
```bash
docker create --name qemu-src tonistiigi/binfmt
docker cp qemu-src:/usr/bin/qemu-aarch64 /usr/local/bin/qemu-aarch64
docker rm qemu-src
echo ':qemu-aarch64:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00:\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/local/bin/qemu-aarch64:POCF' > /proc/sys/fs/binfmt_misc/register
docker run --rm --platform linux/arm64 debian:trixie uname -m # aarch64
```
Unraid keeps `/usr/local/bin` in memory, so this is repeated after a reboot. Copy the work directory
over, without Dawn's build tree (the package is what the game links), and run the build detached so
that a closed SSH session does not stop it:
```bash
rsync -a --exclude dawn/build --exclude 'dawn/dawn-*' --exclude disc-extract \
~/wiicompiled/ root@<server>:/mnt/user/appdata/wiicompiled/ # from the PC
docker run -d --name wiicompiled-frame --platform linux/arm64 \
-v /mnt/user/appdata/wiicompiled:/work debian:trixie \
bash -c 'bash /work/build-game.sh >> /work/game.log 2>&1'
```
`build-game.sh` holds the `apt-get` line of [Building it on the Frame](#building-it-on-the-frame)
and the `local-build.sh` command, with `--parallel 8` for 32 GB of memory (compiles take more memory
under emulation: Dawn's build at 16 jobs froze a 16 GB laptop, and finished at 4). `docker start
wiicompiled-frame` runs it again after a change: the translation is reused and only what changed
is compiled.
### Installing it with Frame Control
[Frame Control](https://github.com/saphid/frame-control) adds a folder to the Frame's Steam library as
a Devkit Game: drop the `out` folder on **Send to Frame** and keep **Launches** on `WiiCompiled`.
It picks `SteamLinuxRuntime_4-arm64` for an ARM64 program, but Steam starts such a title natively,
which this build needs (it uses the system's `libpng16`, `libstdc++` and `libz`). The game then lands
in `~/devkit-game/<name>/` and starts from the library inside the headset, where SteamVR is already
running. An update only replaces the executable; copy it under another name and move it into place,
which also works while an old copy is open:
```bash
scp WiiCompiled frame:devkit-game/WiiCompiled/WiiCompiled.new
ssh frame 'cd ~/devkit-game/WiiCompiled && chmod 755 WiiCompiled.new && mv -f WiiCompiled.new WiiCompiled'
```
### Running it
The game reads its `Config.toml` from `~/.local/share/WiiCompiled/` on SteamOS (it is created on the first start): set
`[paths] dvd_root` there to your extracted disc (the directory holding `sys/` and `files/`). Start
`[paths] dvd_root` there to your extracted disc (the directory holding `sys/` and `files/`;
`/home/steamos/wiicompiled/disc` when it was copied as above). Start
SteamVR on the Frame, then start `~/wiicompiled/out/WiiCompiled`, from Desktop Mode or as a
non-Steam game added to the library. The run log is in `Logs/` next to `Config.toml`; it should show,
in order:
@@ -100,6 +198,35 @@ in order:
`Linux Vulkan OpenXR requires a Dawn built with Aurora's patches` means the build used a stock Dawn:
check that `--dawn-package` pointed at `build-dawn-linux.sh`'s `package` directory.
The game only writes a `Config.toml` when there is none, so a file holding just `[paths]` and
`dvd_root` can be written before the first start. Each run gets its own folder under `Logs/`; a
native crash leaves `crash_sigsegv.txt` there and, in `console.log`, the faulting thread, its pc and
lr and a backtrace as module + offset, which `addr2line -f -C -e native-build/WiiCompiled <offset>`
turns into functions on the build machine (the executable is not stripped).
### Settings that matter on the Frame
| Setting | Recommended | Why |
| --- | --- | --- |
| `[vr] render_scale` | `1.25` | Scales SteamVR's recommended eye size, 1728x1728 on the Frame; 1.25 is the panels' 2160x2160. The eye passes took 9 to 11 ms a game frame there, with medium foveation. |
| `[vr] foveation` | `medium` | `off` shades every pixel and costs the most. See [Known issues](#known-issues) if images double. |
| `[vr] repeat_frames` | `true` (default) | Without it SteamVR halves the app's rate and fills refreshes itself. |
| `[vr] frame_interpolation_fps` | `0` | Rendering in-between frames needs 120 eye pairs a second; at these resolutions it made things worse. |
| `[video] resolution_multiplier` | `2` | The game's own frame, which the eyes are made from, at 2x the Wii's. 4x is far too heavy for the Adreno 750. |
## Known issues
- **Doubled images.** Images double in races and on the HUD, worst while racing, at first in the
right eye only and later in both. Both eyes get the same frames and repeats, so a one-eyed doubling
is not the 60 FPS cadence. The suspect is foveation: Turnip draws a coarse bin at lower resolution
and scales it back up, and each eye's density maps change with its gaze. Comparing foveation off,
on without eye tracking, and on with it is the next test.
- **Foveation and the right eye.** The full-density region follows the left eye better than the
right. Each eye already gets its own gaze direction; convergence on near content (the HUD screen at
2 m, the cockpit) is not yet corrected for.
- **VR frame interpolation** is not recommended on the Frame (see the table above).
- **The Android flavour** cannot show a picture in Lepton (below).
## The Android flavour in Lepton
Everything from here to [Building and installing](#building-and-installing) is the `steamFrame`
@@ -117,6 +244,7 @@ build; its launch and manifest are Lepton's.
| `[vr] refresh_rate` default | `0` (the headset's own) | `120` |
| `[vr] passthrough` | default on (`XR_FB_passthrough`) | not asked for, default off, setting hidden |
| `[vr] eye_tracked_foveation` default | off | on |
| `[vr] repeat_frames` default | off | on |
The application ID stays `org.wiicompiled.quest`, so the storage paths in `docs/quest-port.md` hold
as they are. The kit's CPU string differs from the Quest ones, which gives the Frame its own kit
@@ -181,9 +309,19 @@ so nothing changes on a Quest.
The request uses the runtime's own value within half a hertz of the setting (runtimes report 119.98
for 120). Setting it back to `0` restores the rate the session started at. The game renders 60 frames
a second, so at 120 Hz each frame shows for exactly two refreshes. At 72 or 90 Hz some frames show
for one refresh and others for two, which judders. The Frame starts at 120. Render-first pacing
(`docs/quest-port.md`) already waits for each sealed game frame, so on the Frame the pacing summary
should read about 60 `skipped-slots` a second with no `late` cycles.
for one refresh and others for two, which judders. The Frame starts at 120.
Render-first pacing (`docs/quest-port.md`) submits a frame once the game has sealed one, 60 times a
second. On the Frame, SteamVR answered that by running the app at half rate (the pacing summary read
`predicted-rate=60.0Hz`) and filling every other refresh itself, even with Motion Smoothing off, which
doubled the HUD and the menu screen while the head turned. `[vr] repeat_frames` (default on for the
Frame, off elsewhere, live in the headset panel's VR tab) therefore submits the retained layer, with
the poses it was rendered for, on every refresh the next eyes are not ready for: the pacing thread
waits for them until 1.5 ms before the runtime's next wake (a period after the last xrWaitFrame
returned) and otherwise spends that refresh on a keep-alive cycle, which xrWaitFrame paces. The
summary should then read `predicted-rate=120.0Hz`, about 60 `keepalive` a second and 60 `new`
layers. A first version waited only a millisecond, so every packet also spent a refresh on a repeat
it did not need, the next packet missed the game's next frame, and `new` fell to about 35 a second.
Lepton may decline the request (frame-control found SteamVR keeping its own rate there). The session
log then says `display refresh rate 120 Hz refused` with the rates it offers, and nothing else
@@ -201,13 +339,17 @@ region to where the player looks:
2. `vr/eye_gaze.h` turns the gaze into tangents of each eye's own view, which may be canted.
`AuroraStereoFrame` carries them as `gaze` and `gazeValid`, appended after its existing fields.
3. Aurora snaps the gaze to a cell of two map texels (64 pixels, about 3 degrees) and builds that
cell's density map with the level's rings centred on the gaze (`gfx/foveation.hpp`).
cell's density map with the level's rings centred on the gaze (`gfx/foveation.hpp`). Both rings
are 8 degrees wider than the fixed map's: the full-density region has to cover where a saccade
lands until the next game frame's map is bound, the tracker's error, and Turnip shading a whole
render-pass bin at one density.
Each eye keeps up to 32 maps, one per cell looked at, so a glance back reuses its map instead of
uploading a new one. A new map is bound once its upload has completed, and until then the eye keeps
Each eye keeps up to 128 maps, one per cell looked at, so a glance back reuses its map instead of
uploading a new one. Each map has a memory block of its own: Turnip reads a map through a host
mapping of its memory, and Dawn's buffer uploads unmap the shared blocks they sub-allocate from. A new map is bound once its upload has completed, and until then the eye keeps
the map it had. A blink, lost tracking or the setting turned off returns to the map centred on the
forward direction, which is byte-identical to the fixed foveation map. No change to the Dawn patch
was needed: maps stay immutable, and the patch already lets a view be rebound to another map.
forward direction, which is byte-identical to the fixed foveation map. Maps stay immutable, and the
Dawn patch lets a view be rebound to another map.
The session log reports `OpenXR eye gaze: available` (or that the runtime has no eye tracker),
`OpenXR eye gaze: tracking` at the first tracked sample, and `eye foveation medium following the
+27
View File
@@ -100,6 +100,7 @@ struct RuntimeUserConfig {
std::optional<std::string> vrPerformanceLevel;
std::optional<std::string> vrFoveation;
std::optional<bool> vrEyeTrackedFoveation;
std::optional<bool> vrRepeatFrames;
std::optional<std::string> vrRecenterKey;
std::optional<float> vrLeanBackDegrees;
// F10 > Diagnostics: OpenXR pacing and presentation logging in console.log.
@@ -338,6 +339,19 @@ inline constexpr bool kVrEyeTrackedFoveationDefault = true;
inline constexpr bool kVrEyeTrackedFoveationDefault = false;
#endif
// Repeated frames: while the game has no new frame for a display refresh, the last one is submitted
// again with the head pose it was rendered for, which the compositor turns to the current one. A
// runtime then sees the app at the display's rate instead of at the game's 60 FPS, and does not
// halve the app's rate and fill every other refresh itself: SteamVR's filled frames double the HUD
// and the menu screen while the head turns. On by default on the Steam Frame. Live.
#if defined(MKW_HEADSET_STEAM_FRAME)
inline constexpr bool kVrRepeatFramesDefault = true;
#define MKW_VR_REPEAT_FRAMES_DEFAULT_TOML "true"
#else
inline constexpr bool kVrRepeatFramesDefault = false;
#define MKW_VR_REPEAT_FRAMES_DEFAULT_TOML "false"
#endif
// The level aurora_set_stereo_foveation takes; anything unknown is off.
inline uint32_t VrFoveationLevelIndex(std::string_view value) {
const auto it = std::find(kVrFoveationLevels.begin(), kVrFoveationLevels.end(), value);
@@ -590,6 +604,9 @@ inline void EnsureConfigFile() {
"# at 60, so 120 shows every frame twice. Only runtimes that let apps\n"
"# choose (XR_FB_display_refresh_rate) take it. Live.\n"
"refresh_rate = " MKW_VR_REFRESH_RATE_DEFAULT_TEXT "\n"
"# Submit the last frame again for each refresh the game has no new\n"
"# frame for, so the runtime does not fill those refreshes itself. Live.\n"
"repeat_frames = " MKW_VR_REPEAT_FRAMES_DEFAULT_TOML "\n"
"render_scale = " MKW_VR_RENDER_SCALE_DEFAULT_TEXT "\n"
"world_units_per_meter = 500.0\n"
"hud_distance_meters = 2.0\n"
@@ -910,6 +927,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.vrFoveation = *value;
}
config.vrEyeTrackedFoveation = FindConfigValue<bool>(document, "vr", "eye_tracked_foveation");
config.vrRepeatFrames = FindConfigValue<bool>(document, "vr", "repeat_frames");
if (auto value = FindConfigValue<std::string>(document, "vr", "mirror_view");
value && IsSupportedVrMirrorView(*value)) {
config.vrMirrorView = *value;
@@ -1322,6 +1340,11 @@ inline bool SetVrFoveation(std::string value) {
return WriteSetting("vr", "foveation", FormatString(value));
}
inline bool SetVrRepeatFrames(bool value) {
Mutable().vrRepeatFrames = value;
return WriteSetting("vr", "repeat_frames", value ? "true" : "false");
}
inline bool SetVrEyeTrackedFoveation(bool value) {
Mutable().vrEyeTrackedFoveation = value;
return WriteSetting("vr", "eye_tracked_foveation", value ? "true" : "false");
@@ -1853,6 +1876,10 @@ inline std::string VrFoveation(std::string fallback = kVrFoveationDefault) {
return value && IsSupportedVrFoveation(*value) ? *value : std::move(fallback);
}
inline bool VrRepeatFrames(bool fallback = kVrRepeatFramesDefault) {
return Get().vrRepeatFrames.value_or(fallback);
}
inline bool VrEyeTrackedFoveation(bool fallback = kVrEyeTrackedFoveationDefault) {
return Get().vrEyeTrackedFoveation.value_or(fallback);
}
+7
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <functional>
@@ -192,6 +193,10 @@ public:
// BeginFrame and EndFrame using the same token. LocateViews is optional when
// should_render is false; it is otherwise normally called after BeginFrame.
OpenXRFrameStatus WaitFrame(OpenXRFrame& frame);
// When the last xrWaitFrame returned and the display period it predicted (0 before the first).
// The runtime wakes the app about once a period, so the next wake is due a period after it.
std::chrono::steady_clock::time_point LastWaitFrameReturn() const noexcept { return m_last_wait_return; }
XrDuration LastWaitFramePeriod() const noexcept { return m_last_wait_period; }
bool BeginFrame(const OpenXRFrame& frame);
bool LocateViews(OpenXRFrame& frame);
// Locates the views for `display_time` outside the frame protocol, for a
@@ -312,6 +317,8 @@ private:
uint64_t m_next_frame_serial = 1;
uint64_t m_active_frame_serial = 0;
XrTime m_active_frame_display_time = 0;
std::chrono::steady_clock::time_point m_last_wait_return{};
XrDuration m_last_wait_period = 0;
OpenXRRuntimeInfo m_runtime_info;
std::array<OpenXRViewConfiguration, kOpenXREyeCount> m_view_configuration{};
+54
View File
@@ -49,6 +49,14 @@
#include <ucontext.h>
#endif
#include <unistd.h>
#if defined(__GLIBC__)
// The host side of a native crash on desktop Linux: the faulting thread, its pc and lr, and a
// backtrace, each as module + offset for addr2line against the unstripped executable.
#include <dlfcn.h>
#include <execinfo.h>
#include <pthread.h>
#include <ucontext.h>
#endif
#endif
#include "abi_bridge.h"
@@ -766,6 +774,23 @@ void SetRuntimeExitCodeImpl(int code) {
namespace {
#if defined(__GLIBC__)
// One host code address as module + offset (what `addr2line -f -C -e <module> <offset>` takes),
// with the nearest exported symbol when there is one.
void LogHostAddress(const char* label, int index, const void* address) {
Dl_info info{};
if (address != nullptr && dladdr(address, &info) != 0 && info.dli_fname != nullptr) {
const char* slash = std::strrchr(info.dli_fname, '/');
const char* module = slash != nullptr ? slash + 1 : info.dli_fname;
const auto offset = reinterpret_cast<uintptr_t>(address) - reinterpret_cast<uintptr_t>(info.dli_fbase);
RT_LOGF(RT_TAG_RUNTIME, "%s%d %p %s+0x%zx%s%s\n", label, index, address, module, static_cast<size_t>(offset),
info.dli_sname != nullptr ? " " : "", info.dli_sname != nullptr ? info.dli_sname : "");
} else {
RT_LOGF(RT_TAG_RUNTIME, "%s%d %p\n", label, index, address);
}
}
#endif
void DumpHostStackTrace() {
#if defined(_WIN32)
static std::atomic_flag s_inProgress = ATOMIC_FLAG_INIT;
@@ -776,6 +801,19 @@ void DumpHostStackTrace() {
std::fputs(trace.c_str(), stderr);
std::fflush(stderr);
s_inProgress.clear();
#elif defined(__GLIBC__)
static std::atomic_flag s_inProgress = ATOMIC_FLAG_INIT;
if (s_inProgress.test_and_set()) {
return;
}
std::array<void*, 64> frames{};
const int count = backtrace(frames.data(), static_cast<int>(frames.size()));
RT_LOGF(RT_TAG_RUNTIME, "Host stack trace (%d frames):\n", count);
for (int i = 0; i < count; ++i) {
LogHostAddress(" #", i, frames[static_cast<size_t>(i)]);
}
std::fflush(stderr);
s_inProgress.clear();
#else
RT_LOGF(RT_TAG_RUNTIME, "Host stack trace unavailable on this platform\n");
std::fflush(stderr);
@@ -1231,6 +1269,22 @@ void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack "
"diagnostics.";
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
#if defined(__GLIBC__)
{
char threadName[32] = "?";
pthread_getname_np(pthread_self(), threadName, sizeof(threadName));
RT_LOGF(RT_TAG_RUNTIME, "Faulting host thread: '%s' (tid %ld)\n", threadName, static_cast<long>(gettid()));
if (ucontextVoid != nullptr) {
const auto* uc = static_cast<const ucontext_t*>(ucontextVoid);
#if defined(__aarch64__)
LogHostAddress("Faulting pc #", 0, reinterpret_cast<const void*>(uc->uc_mcontext.pc));
LogHostAddress("Faulting lr #", 0, reinterpret_cast<const void*>(uc->uc_mcontext.regs[30]));
#elif defined(__x86_64__)
LogHostAddress("Faulting pc #", 0, reinterpret_cast<const void*>(uc->uc_mcontext.gregs[REG_RIP]));
#endif
}
}
#endif
DumpHostStackTrace();
WriteFatalLogImpl(sig == SIGBUS ? "sigbus" : "sigsegv");
+11
View File
@@ -158,6 +158,7 @@ static_assert(kVrFoveationLabels.size() == RuntimeConfigFile::kVrFoveationLevels
int g_vrFoveation = static_cast<int>(RuntimeConfigFile::VrFoveationLevelIndex(RuntimeConfigFile::VrFoveation()));
bool g_vrEyeTrackedFoveation = RuntimeConfigFile::VrEyeTrackedFoveation();
#endif
bool g_vrRepeatFrames = RuntimeConfigFile::VrRepeatFrames();
bool g_vrFirstPerson = RuntimeConfigFile::VrFirstPerson(false);
bool g_vrFirstPersonToggleClick = RuntimeConfigFile::VrFirstPersonToggleClick();
// Set from any thread by the right-thumbstick click, applied on the game thread.
@@ -1557,6 +1558,16 @@ void DrawVrSettings() {
"immediately.");
}
}
if (ImGui::Checkbox("Repeat frames at the headset's rate", &g_vrRepeatFrames)) {
RuntimeConfigFile::SetVrRepeatFrames(g_vrRepeatFrames);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Shows the last frame again, turned to where you now look, on each refresh the game has "
"no new frame for. The headset's runtime then does not drop the game to half its rate and "
"fill the gaps itself, which doubles the HUD and the menu screen as you turn your head "
"(SteamVR on the Steam Frame). Applies immediately.");
}
if (ImGui::Combo("VR frame interpolation (experimental)", &g_vrFrameInterpolationMode,
kVrInterpolationLabels.data(), static_cast<int>(kVrInterpolationLabels.size()))) {
const auto target = kVrInterpolationFps[static_cast<size_t>(g_vrFrameInterpolationMode)];
+22 -2
View File
@@ -710,6 +710,11 @@ private:
// Skipped eye copies tolerated back to back before the session is given up: a few seconds
// at the headset's refresh rate.
static constexpr uint32_t kMaxConsecutiveSkips = 300;
// With [vr] repeat_frames, render-first pacing waits for the eyes until this long before the
// runtime's next wake, then spends that refresh on the retained layer; before the first wake,
// it waits this many milliseconds. The repeat's xrWaitFrame does the actual pacing.
static constexpr std::chrono::microseconds kRepeatFrameMargin{1500};
static constexpr uint32_t kRepeatFramePollMs = 1;
static float ClampRenderScale(float scale) noexcept {
return std::clamp(scale, RuntimeConfigFile::kVrRenderScaleMin, RuntimeConfigFile::kVrRenderScaleMax);
@@ -1335,13 +1340,28 @@ private:
aurora_notify_stereo_frame();
// Aurora renders the eyes at its next seal. Meanwhile the compositor keeps showing the
// retained layer; a 50 ms stall repeats it explicitly and withdraws the packet.
// retained layer; a 50 ms stall repeats it explicitly and withdraws the packet. With
// [vr] repeat_frames the retained layer is also submitted for every display refresh the
// eyes are not ready for, so the runtime sees the app at the display's rate rather than the
// game's and never fills refreshes in itself. The eyes are waited for until shortly before
// the runtime's next wake: a repeat begun earlier would spend a refresh the new eyes could
// still have made, and hold back the next packet past the game's next frame.
const bool repeat_frames = RuntimeConfigFile::VrRepeatFrames();
const auto wait_ms = [&]() -> uint32_t {
if (!repeat_frames) return 50;
const XrDuration period = runtime_->LastWaitFramePeriod();
if (period <= 0) return kRepeatFramePollMs;
const auto wake = runtime_->LastWaitFrameReturn() + std::chrono::nanoseconds(period);
const auto left = std::chrono::duration_cast<std::chrono::milliseconds>(
wake - kRepeatFrameMargin - std::chrono::steady_clock::now());
return static_cast<uint32_t>(std::clamp<int64_t>(left.count(), 0, 50));
};
OpenXRSubmissionStatus submission = OpenXRSubmissionStatus::Timeout;
bool canceled_before_encode = false;
const auto cancel_after = std::chrono::steady_clock::now() + std::chrono::milliseconds(50);
while (!stop_.load(std::memory_order_acquire) && submission == OpenXRSubmissionStatus::Timeout) {
submission = diagnostics::Measure(diagnostics::Stage::SubmissionWait, [&] {
return backend_->WaitForSubmission(packet, 50);
return backend_->WaitForSubmission(packet, wait_ms());
});
if (submission == OpenXRSubmissionStatus::Timeout) {
if (std::chrono::steady_clock::now() >= cancel_after) {
+2
View File
@@ -710,6 +710,8 @@ OpenXRFrameStatus OpenXRRuntime::WaitFrame(OpenXRFrame& frame) {
return OpenXRFrameStatus::Error;
}
diagnostics::OnWaitFrame(wait_timer, state.predictedDisplayTime, state.predictedDisplayPeriod);
m_last_wait_return = std::chrono::steady_clock::now();
m_last_wait_period = state.predictedDisplayPeriod;
frame = {};
frame.serial = m_next_frame_serial++;
+13
View File
@@ -79,6 +79,19 @@ int main() {
Require(!RuntimeConfigFile::kVrEyeTrackedFoveationDefault);
#endif
// [vr] repeat_frames: the retained layer fills the refreshes the game has no frame for.
Require(Parse("[vr]\nrepeat_frames = true\n").vrRepeatFrames == true);
Require(Parse("[vr]\nrepeat_frames = false\n").vrRepeatFrames == false);
Require(!Parse("[vr]\nrepeat_frames = 1\n").vrRepeatFrames.has_value());
Require(!Parse("[vr]\n").vrRepeatFrames.has_value());
#if defined(MKW_HEADSET_STEAM_FRAME)
Require(RuntimeConfigFile::kVrRepeatFramesDefault);
Require(std::string_view(MKW_VR_REPEAT_FRAMES_DEFAULT_TOML) == "true");
#else
Require(!RuntimeConfigFile::kVrRepeatFramesDefault);
Require(std::string_view(MKW_VR_REPEAT_FRAMES_DEFAULT_TOML) == "false");
#endif
// [vr] passthrough: Horizon OS's room view, which the Steam Frame build does not offer.
Require(Parse("[vr]\npassthrough = false\n").vrPassthrough == false);
Require(!Parse("[vr]\n").vrPassthrough.has_value());