mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
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:
18 files changed
+582
-246
No files matched your search
@@ -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"
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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&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&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&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&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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user