mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-18 14:01:03 +02:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b35243244f | ||
|
|
55d921a118 | ||
|
|
9be271d49a | ||
|
|
6a3441b616 | ||
|
|
58b6d81ed1 | ||
|
|
f7ca5ecbd6 | ||
|
|
c5f9ca3e2d | ||
|
|
8e9e905b85 | ||
|
|
03db602a05 | ||
|
|
77333709ae | ||
|
|
cfd434fac8 | ||
|
|
93df677470 | ||
|
|
8f725001de | ||
|
|
e49ec771c3 | ||
|
|
db8afbb3a9 | ||
|
|
755fb0e332 | ||
|
|
f2f4f081c8 | ||
|
|
897899a5f9 | ||
|
|
c8220c0c11 | ||
|
|
c91c1b289c | ||
|
|
a23fd087ee | ||
|
|
4407013d61 | ||
|
|
20ef08b3b8 | ||
|
|
b361ecd54c | ||
|
|
c12b6b7d99 | ||
|
|
5bf1904ca8 | ||
|
|
a0f97d307d | ||
|
|
491d39503e | ||
|
|
a132a82952 | ||
|
|
cfb9623e83 | ||
|
|
35e045f9f7 | ||
|
|
504ea792da | ||
|
|
f87732d0ad | ||
|
|
152c4f5f38 | ||
|
|
794b715d31 | ||
|
|
a532ffa6f3 | ||
|
|
ced586d159 | ||
|
|
f038958c6f | ||
|
|
94bfc555c8 | ||
|
|
00034b17fc | ||
|
|
6b0251e6d6 | ||
|
|
89e1dee867 | ||
|
|
92fb438c24 | ||
|
|
56b853d5ef | ||
|
|
9bad7123de | ||
|
|
16c4e703dd | ||
|
|
1a42dfc382 | ||
|
|
e223ad511d | ||
|
|
06861dbb9a | ||
|
|
3a027d71dc | ||
|
|
0a3bcc99bd |
No files matched your search
@@ -12,7 +12,7 @@ on:
|
||||
tag:
|
||||
description: Release tag, frame-<something>; docs/releases/<tag>.md holds its notes
|
||||
required: true
|
||||
default: frame-beta-1
|
||||
default: frame-beta-2
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
|
||||
+11
-1
@@ -39,9 +39,19 @@ That's ok, but rules apply:
|
||||
game (hardware comparison, logs, whatever fits).
|
||||
- Review feedback. It's about the code, not about you ;).
|
||||
|
||||
## Porting from other projects
|
||||
|
||||
This fork takes fixes from upstream WiiCompiled and other forks. When you port one:
|
||||
|
||||
- name the source repository and commit in the commit message ("Ported from owner/repo abc1234");
|
||||
- keep the original author on the commit when the change is taken as is, and say what you changed
|
||||
when it is adapted;
|
||||
- add a line to [`CREDITS.md`](CREDITS.md), and check the source's license is compatible with
|
||||
GPL v3.0.
|
||||
|
||||
## Bug reports
|
||||
|
||||
See the FAQ in the [README](README.md)
|
||||
See [Reporting problems](README.md#reporting-problems) in the README.
|
||||
|
||||
## A note on related projects
|
||||
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Credits
|
||||
|
||||
WiiCompiled VR for the Steam Frame is the work of many people. This fork adds the SteamOS build,
|
||||
the install and diagnostics scripts and the Frame-specific rendering, but most of what you play
|
||||
was written elsewhere, and a good part of this fork's own fixes were ported from other people's
|
||||
projects. This page lists where everything came from.
|
||||
|
||||
Every project below is GPL-3.0 licensed like this one unless it says otherwise. Ported commits name
|
||||
their source repository and commit in their messages, and keep the original author where the
|
||||
change was taken as is, so `git log` is the full record. Bundled libraries and their licenses are
|
||||
in [`THIRD-PARTY-NOTICES.md`](THIRD-PARTY-NOTICES.md).
|
||||
|
||||
## The projects this fork is built on
|
||||
|
||||
- **[WiiCompiled](https://github.com/patchzyy/Wiicompiled)** by patchzyy and its contributors: the
|
||||
static recompilation of Mario Kart Wii itself, the translator, the runtime and the Aurora-based
|
||||
renderer.
|
||||
- **[WiiCompiled OpenXR VR](https://github.com/iChris4/Wiicompiled_VR)** by iChris4: the OpenXR VR
|
||||
renderer, the Quest app, the headset settings panel, foveation and the first-person cockpit. This
|
||||
fork contains all of its `openxr-work` branch up to 2026-10-01.
|
||||
- **[mario-kart-wii-VR-port](https://github.com/heurazy/mario-kart-wii-VR-port)** by heurazy: the
|
||||
cockpit's turning steering wheel, hand steering and USB wheel support, which came in through
|
||||
WiiCompiled OpenXR VR.
|
||||
- **[DolphinXR](https://github.com/iChris4/dolphinXR)**: the OpenXR Wii Remote input design and its
|
||||
default button profile, which the controller code (`runtime/src/vr/openxr_input.cpp`) and the
|
||||
Steam Frame controller profile's input paths are adapted from.
|
||||
|
||||
## Ported into this fork
|
||||
|
||||
### From WiiCompiled (upstream)
|
||||
|
||||
Cherry-picked or adapted from [patchzyy/Wiicompiled](https://github.com/patchzyy/Wiicompiled) after
|
||||
this fork's line split from it:
|
||||
|
||||
| Change | Author | Upstream |
|
||||
| --- | --- | --- |
|
||||
| TLS through mbed TLS on Linux and Android, which online play (Retro Rewind WFC) needs there | dorPXP, with patchzyy | #144 |
|
||||
| NAND moves across mount points on Linux (saves on a different drive or the SD card) | Daan Vervacke | #212 |
|
||||
| Controller mapping platform, player index sync, and socket send errors | Michael G ([DarthMDev](https://github.com/DarthMDev)) | #251 |
|
||||
| `bltl` translation | patchzyy | #254 |
|
||||
| The KartPad batch: KD and NCD kept while offline, RFL alarm interrupt context, shared LR continuation dispatch, pipeline cache size checks, pipeline waiter wake-ups, packed three-byte vertex reads, draw merging bounds, GEN_MODE decode, EFB copy and readback fixes, graphics startup errors, and the staging buffer wait | patchzyy, adapting Chris Sotraidis's [KartPad](https://github.com/chrissotraidis/wiicompiled) fixes | #244 |
|
||||
|
||||
### From other WiiCompiled forks
|
||||
|
||||
| Change | Author | Source |
|
||||
| --- | --- | --- |
|
||||
| Scene-load crash fix (`GXClearVtxDesc` keeps the position descriptor) | heurazy | [Wiicompiled_VR-PLUS](https://github.com/heurazy/Wiicompiled_VR-PLUS) c2b2500 |
|
||||
| `[vr] adaptive_resolution` | heurazy | Wiicompiled_VR-PLUS, adapted |
|
||||
| Building and installing Retro Rewind from its own update server (`steam-frame-install.sh --retro-rewind`) | heurazy | Wiicompiled_VR-PLUS, adapted |
|
||||
| Faster disc reads: bulk DVD DMA copies, Yaz0 (SZS) decoding through host buffers, per-thread disc file handles | Chris Sotraidis | [KartPad](https://github.com/chrissotraidis/wiicompiled) ec14a0d, 97cdc9a, affabbd |
|
||||
| No debug labels in the ImGui passes in normal builds (an Adreno driver crash) | Chris Sotraidis | KartPad 420b828, extended to this fork's extra passes |
|
||||
| A TLS session that failed a write is kept instead of its socket being deleted | Michael G ([DarthMDev](https://github.com/DarthMDev)) | [DarthMDev/Wiicompiled](https://github.com/DarthMDev/Wiicompiled) a2ecc0f, fcf8646 (upstream PR #258) |
|
||||
| `Config.toml` saved through a temporary file and a rename | BlackAndBlue95 | [Strikers-WiiCompiled](https://github.com/BlackAndBlue95/Strikers-WiiCompiled) 435daf3 |
|
||||
| No depth writes while the Z compare is off (the Stormship lightning) | BlackAndBlue95 | Strikers-WiiCompiled 369ec99 |
|
||||
| NEON forms of the AX audio mix kernels | rooklz | [rooklz/Wiicompiled](https://github.com/rooklz/Wiicompiled) 4346362 |
|
||||
| Emptied WC24 bootstrap files are refilled | nx-mod | [wiicompiled-nx](https://github.com/nx-mod/wiicompiled-nx) d0149e6, adapted |
|
||||
| Guest memory access checks folded away on 4 KiB-page builds | nx-mod's finding | wiicompiled-nx 18ef16f, done here as a compile-time constant |
|
||||
|
||||
## Tools and references
|
||||
|
||||
- **[aurora](https://github.com/encounter/aurora)** by Luke Street: the GX rendering and windowing
|
||||
backend the whole graphics layer sits on (MIT).
|
||||
- **[Dawn](https://dawn.googlesource.com/dawn)**: Google's WebGPU implementation, under Aurora.
|
||||
- **[OpenXR](https://www.khronos.org/openxr/)** by Khronos: the VR API.
|
||||
- **[Mbed TLS](https://github.com/Mbed-TLS/mbedtls)**: TLS for online play on Linux and Android.
|
||||
- **[Dolphin Emulator](https://github.com/dolphin-emu/dolphin)**: a reference for Wii hardware
|
||||
behaviour, and the source of the DSP coefficient ROM, the WiiConnect24 bootstrap tree and the
|
||||
Riivolution parser bundled with the runtime.
|
||||
- **[nod](https://github.com/encounter/nod)** by Luke Street: `nodtool`, the disc image extractor.
|
||||
- **[Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind)** by ZPL and team: the mod
|
||||
distribution this project supports.
|
||||
- **[Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard)**: the mod manager the PC build
|
||||
integrates with, and the model for how the install script fetches Retro Rewind.
|
||||
- **[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.
|
||||
- **Inkwreck**: the logo.
|
||||
- Everyone in the static recompilation community.
|
||||
|
||||
If something of yours is in this fork and missing here, or credited wrongly, please open an issue.
|
||||
@@ -1,5 +1,9 @@
|
||||
# Windows VR distribution
|
||||
|
||||
This page covers upstream's Windows releases, kept for the PC build. The Steam Frame releases
|
||||
(`frame-*` tags) are source only and are published by `.github/workflows/frame-release.yml` with
|
||||
the notes in `docs/releases/<tag>.md`; see the [README](README.md#quick-start).
|
||||
|
||||
Distribute only `WiiCompiled-Setup.exe` and its checksum from
|
||||
[iChris4/Wiicompiled_VR](https://github.com/iChris4/Wiicompiled_VR/releases), and
|
||||
`WheelWizardVRWindows.exe` from [iChris4/WheelWizard_VR](https://github.com/iChris4/WheelWizard_VR/releases).
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Vulkan hooks through which the OpenXR runtime creates Dawn's own instance and device (the
|
||||
# same-device OpenXR backend, runtime/src/vr/openxr_vulkan_win32.cpp) and fragment density maps for
|
||||
# foveated rendering. The stock prebuilt package has neither. This is the Linux counterpart of
|
||||
# android/Build-QuestDawn.ps1, for the Steam Frame's native SteamOS build (docs/steam-frame.md).
|
||||
# android/Build-QuestDawn.ps1, for the Steam Frame's native SteamOS build (README.md, Quick start).
|
||||
#
|
||||
# Launcher/build-dawn-linux.sh [--work-dir DIR] [--cc PATH --cxx PATH] [--cmake PATH]
|
||||
# [--ninja PATH] [--python PATH] [--jobs N] [--force]
|
||||
|
||||
Executable
+372
@@ -0,0 +1,372 @@
|
||||
#!/usr/bin/env bash
|
||||
# Collects what a Steam Frame problem report needs into one .tar.gz on this machine: the game's run
|
||||
# logs and crash files, Config.toml, the install, SteamVR's logs and OpenXR runtime, the GPU and
|
||||
# Vulkan driver, and the system's state (SteamOS version, memory, thermals, kernel log, core dumps).
|
||||
# It only reads on the Frame, apart from a temporary folder it removes again.
|
||||
#
|
||||
# Launcher/frame-diagnostics.sh --frame HOST [options]
|
||||
#
|
||||
# Or without downloading anything first:
|
||||
#
|
||||
# curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/openxr-work/Launcher/frame-diagnostics.sh \
|
||||
# | bash -s -- --frame steamos@<frame-ip>
|
||||
#
|
||||
# Options:
|
||||
# --frame HOST the Frame: an SSH destination (steamos@<frame-ip>, or an ~/.ssh/config host such
|
||||
# as Frame Control's "frame"), or "local" when running on the Frame (default frame)
|
||||
# --runs N how many of the newest run folders to take (default 3; 0 takes them all)
|
||||
# --game-dir DIR where the game is installed, relative to the Frame's home or absolute
|
||||
# (default devkit-game/WiiCompiled)
|
||||
# --output DIR where the bundle goes (default: the current folder)
|
||||
# --with-memory also take the guest memory snapshot (mem1.bin) a crash writes
|
||||
# --with-core also take the newest WiiCompiled core dump, when systemd-coredump kept one (large)
|
||||
# -h, --help
|
||||
#
|
||||
# The bundle holds summary.txt first: the version, which startup steps the newest run reached, its
|
||||
# crash files, the end of its log and its last frame-pacing lines. The disc is never copied.
|
||||
set -euo pipefail
|
||||
|
||||
# Everything is in main, so bash has read the whole script before running any of it: when it comes
|
||||
# through curl | bash, a step that reads stdin cannot eat the rest. (Not indented, for the heredocs.)
|
||||
main() {
|
||||
say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
||||
note() { printf ' %s\n' "$*"; }
|
||||
fail() {
|
||||
printf '\nframe-diagnostics.sh: error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
usage() { awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$script_path"; }
|
||||
|
||||
script_path=${BASH_SOURCE[0]:-}
|
||||
|
||||
frame=frame
|
||||
runs=3
|
||||
game_dir=devkit-game/WiiCompiled
|
||||
output_dir=.
|
||||
with_memory=no
|
||||
with_core=no
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--frame) frame=$2; shift 2 ;;
|
||||
--runs) runs=$2; shift 2 ;;
|
||||
--game-dir) game_dir=$2; shift 2 ;;
|
||||
--output) output_dir=$2; shift 2 ;;
|
||||
--with-memory) with_memory=yes; shift ;;
|
||||
--with-core) with_core=yes; shift ;;
|
||||
-h|--help)
|
||||
if [[ -n "$script_path" && -f "$script_path" ]]; then usage; else echo "See the comment at the top of the script."; fi
|
||||
exit 0 ;;
|
||||
*) fail "unknown argument: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
[[ "$runs" =~ ^[0-9]+$ ]] || fail "--runs must be a number"
|
||||
# The game folder reaches the Frame as one word of an SSH command line.
|
||||
[[ "$game_dir" =~ ^[A-Za-z0-9._/~-]+$ ]] || fail "--game-dir may only hold letters, digits and . _ / ~ -"
|
||||
mkdir -p "$output_dir"
|
||||
output_dir=$(cd "$output_dir" && pwd)
|
||||
|
||||
stamp=$(date +%Y%m%d-%H%M%S)
|
||||
bundle="$output_dir/frame-diagnostics-$stamp.tar.gz"
|
||||
partial="$bundle.partial"
|
||||
trap 'rm -f "$partial"' EXIT
|
||||
|
||||
if [[ "$frame" == local ]]; then
|
||||
say "Collecting on this machine"
|
||||
on_frame() { bash -s -- "$@"; }
|
||||
else
|
||||
say "Collecting from $frame over SSH"
|
||||
on_frame() { ssh "$frame" bash -s -- "$@"; }
|
||||
fi
|
||||
|
||||
# The collector runs on the Frame and writes the bundle to stdout; its progress goes to stderr.
|
||||
if ! on_frame "$runs" "$game_dir" "$with_memory" "$with_core" "$stamp" <<<"$collector" >"$partial"; then
|
||||
fail "collecting stopped; the output above says why. If SSH could not connect, turn on Developer
|
||||
Mode and set a user password on the Frame (Steam Settings > System), then check the address."
|
||||
fi
|
||||
gzip -t "$partial" 2>/dev/null || fail "the Frame sent back something that is not a bundle; the output above says why"
|
||||
mv "$partial" "$bundle"
|
||||
|
||||
say "Done"
|
||||
note "$bundle ($(du -h "$bundle" | cut -f1))"
|
||||
summary=$(tar -xzOf "$bundle" "frame-diagnostics-$stamp/summary.txt" 2>/dev/null || true)
|
||||
if [[ -n "$summary" ]]; then
|
||||
# The crash files' heads stay in the bundle; the rest is printed.
|
||||
printf '\n%s\n' "$summary" | awk '/^--- crash_/ { exit } { print " " $0 }'
|
||||
fi
|
||||
note ""
|
||||
note "Logs can hold your user name and paths; look through the bundle before posting it publicly."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Runs on the Frame: bash -s -- RUNS GAME_DIR WITH_MEMORY WITH_CORE STAMP
|
||||
read -r -d '' collector <<'COLLECTOR' || true
|
||||
set -uo pipefail
|
||||
runs=$1 game_dir=$2 with_memory=$3 with_core=$4 stamp=$5
|
||||
log() { printf ' %s\n' "$*" >&2; }
|
||||
[[ "$game_dir" == /* ]] || game_dir="$HOME/${game_dir#\~/}"
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/frame-diagnostics.XXXXXX") || exit 1
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
root="$tmp/frame-diagnostics-$stamp"
|
||||
mkdir -p "$root"/{system,gpu,game,steamvr}
|
||||
|
||||
# run FILE COMMAND...: the command's output in FILE under a header naming it; a missing tool or a
|
||||
# hung one is noted rather than stopping the collection.
|
||||
run() {
|
||||
local file=$root/$1; shift
|
||||
{
|
||||
printf '$ %s\n' "$*"
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "(not installed)"
|
||||
else
|
||||
timeout 30 "$@" 2>&1
|
||||
local status=$?
|
||||
(( status == 0 )) || echo "(exit status $status)"
|
||||
fi
|
||||
echo
|
||||
} >>"$file"
|
||||
}
|
||||
# grab SOURCE DEST: a copy of a file or folder, if it exists.
|
||||
grab() {
|
||||
[[ -e "$1" ]] || return 0
|
||||
mkdir -p "$(dirname "$2")"
|
||||
cp -R "$1" "$2" 2>/dev/null || log "could not copy $1"
|
||||
}
|
||||
# grab_tail SOURCE DEST: the last 4 MB of a log that can grow without bound.
|
||||
grab_tail() {
|
||||
[[ -f "$1" ]] || return 0
|
||||
mkdir -p "$(dirname "$2")"
|
||||
tail -c 4194304 "$1" >"$2" 2>/dev/null || log "could not copy $1"
|
||||
}
|
||||
|
||||
# --- System -------------------------------------------------------------------------------------
|
||||
log "system"
|
||||
run system/os.txt cat /etc/os-release
|
||||
run system/os.txt uname -a
|
||||
run system/os.txt uptime
|
||||
run system/os.txt date --iso-8601=seconds
|
||||
run system/hardware.txt lscpu
|
||||
run system/hardware.txt free -m
|
||||
run system/hardware.txt cat /proc/meminfo
|
||||
run system/hardware.txt cat /proc/pressure/memory
|
||||
run system/storage.txt df -h "$HOME" /tmp /run/media
|
||||
run system/storage.txt lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS,LABEL
|
||||
run system/storage.txt findmnt -t ext4,btrfs,exfat,vfat,f2fs
|
||||
{
|
||||
echo "zone type temp(milli-C)"
|
||||
for zone in /sys/class/thermal/thermal_zone*; do
|
||||
[[ -r "$zone/temp" ]] && echo "${zone##*/} $(cat "$zone/type" 2>/dev/null) $(cat "$zone/temp" 2>/dev/null)"
|
||||
done
|
||||
echo
|
||||
echo "cpu cur/min/max(kHz) governor"
|
||||
for cpu in /sys/devices/system/cpu/cpu[0-9]*/cpufreq; do
|
||||
[[ -r "$cpu/scaling_cur_freq" ]] || continue
|
||||
echo "$(basename "$(dirname "$cpu")") $(cat "$cpu/scaling_cur_freq")/$(cat "$cpu/scaling_min_freq")/$(cat "$cpu/scaling_max_freq") $(cat "$cpu/scaling_governor" 2>/dev/null)"
|
||||
done
|
||||
echo
|
||||
for supply in /sys/class/power_supply/*; do
|
||||
[[ -r "$supply/uevent" ]] && { echo "${supply##*/}:"; sed 's/^/ /' "$supply/uevent"; }
|
||||
done
|
||||
} >"$root/system/thermal-power.txt" 2>&1
|
||||
run system/sensors.txt sensors
|
||||
run system/processes.txt ps -eo pid,ppid,etime,pcpu,pmem,rss,args --sort=-pcpu
|
||||
# The kernel log holds GPU hangs and out-of-memory kills; the previous boot's covers a crash that
|
||||
# took the headset down with it.
|
||||
run system/kernel-this-boot.txt journalctl -k -b 0 --no-pager -n 5000
|
||||
run system/kernel-previous-boot.txt journalctl -k -b -1 --no-pager -n 5000
|
||||
run system/kernel-dmesg.txt dmesg
|
||||
run system/user-journal.txt journalctl --user -b 0 --no-pager -n 3000
|
||||
run system/coredumps.txt coredumpctl list --no-pager --since=-7d
|
||||
run system/coredumps.txt coredumpctl info --no-pager WiiCompiled
|
||||
|
||||
# --- GPU and Vulkan -----------------------------------------------------------------------------
|
||||
log "GPU and Vulkan"
|
||||
run gpu/vulkaninfo-summary.txt vulkaninfo --summary
|
||||
run gpu/vulkaninfo.txt vulkaninfo
|
||||
run gpu/eglinfo.txt eglinfo -B
|
||||
{
|
||||
for card in /sys/class/drm/card*; do
|
||||
[[ -e "$card/device/uevent" && "$card" != *-* ]] || continue
|
||||
echo "${card##*/}:"; sed 's/^/ /' "$card/device/uevent"
|
||||
for f in "$card"/device/devfreq/*/{cur_freq,max_freq,governor}; do
|
||||
[[ -r "$f" ]] && echo " ${f#"$card"/device/}: $(cat "$f")"
|
||||
done
|
||||
done
|
||||
for f in /sys/kernel/debug/dri/*/name; do [[ -r "$f" ]] && cat "$f"; done
|
||||
} >"$root/gpu/drm.txt" 2>&1
|
||||
for dir in /usr/share/vulkan/{icd.d,implicit_layer.d,explicit_layer.d} /etc/vulkan/{icd.d,implicit_layer.d} \
|
||||
"$HOME/.local/share/vulkan/implicit_layer.d"; do
|
||||
[[ -d "$dir" ]] && { echo "== $dir"; ls -l "$dir"; }
|
||||
done >"$root/gpu/vulkan-manifests.txt" 2>&1
|
||||
|
||||
# --- The game -----------------------------------------------------------------------------------
|
||||
log "the game in $game_dir"
|
||||
# Where the game keeps Config.toml and Logs: UserData beside a portable.txt at or above the
|
||||
# executable (four levels up at most), otherwise $XDG_DATA_HOME/WiiCompiled.
|
||||
data_dir=${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled
|
||||
search=$game_dir
|
||||
for _ in 0 1 2 3 4; do
|
||||
if [[ -f "$search/portable.txt" ]]; then data_dir=$search/UserData; break; fi
|
||||
[[ "$search" == / ]] && break
|
||||
search=$(dirname "$search")
|
||||
done
|
||||
{
|
||||
echo "game dir: $game_dir"
|
||||
echo "data dir: $data_dir"
|
||||
echo
|
||||
ls -la "$game_dir" 2>&1
|
||||
echo
|
||||
if [[ -f "$game_dir/WiiCompiled" ]]; then
|
||||
file "$game_dir/WiiCompiled" 2>&1
|
||||
sha256sum "$game_dir/WiiCompiled" 2>&1
|
||||
stat -c 'modified %y' "$game_dir/WiiCompiled" 2>&1
|
||||
echo
|
||||
echo "libraries it loads:"
|
||||
ldd "$game_dir/WiiCompiled" 2>&1
|
||||
else
|
||||
echo "(no WiiCompiled executable here)"
|
||||
fi
|
||||
echo
|
||||
ls -la "$data_dir" 2>&1
|
||||
} >"$root/game/install.txt"
|
||||
grab "$game_dir/build-fingerprint.json" "$root/game/build-fingerprint.json"
|
||||
grab "$data_dir/Config.toml" "$root/game/Config.toml"
|
||||
grab "$data_dir/gamecontrollerdb.txt" "$root/game/gamecontrollerdb.txt"
|
||||
grab_tail "$data_dir/wii_accel_trace.csv" "$root/game/wii_accel_trace.csv"
|
||||
grab_tail "$HOME/wiicompiled-frame/build.log" "$root/game/build.log"
|
||||
|
||||
# The disc the config points at: is it there, whole, and the right release? Never copied.
|
||||
dvd_root=$(sed -n 's/^[[:space:]]*dvd_root[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$data_dir/Config.toml" 2>/dev/null | head -n 1)
|
||||
{
|
||||
echo "dvd_root: ${dvd_root:-(not set in Config.toml)}"
|
||||
if [[ -n "$dvd_root" ]]; then
|
||||
if [[ -d "$dvd_root" ]]; then
|
||||
for f in sys/boot.bin sys/main.dol files/rel/StaticR.rel; do
|
||||
[[ -f "$dvd_root/$f" ]] && echo "$f: present" || echo "$f: MISSING"
|
||||
done
|
||||
[[ -f "$dvd_root/sys/boot.bin" ]] && echo "game ID: $(head -c 6 "$dvd_root/sys/boot.bin" | tr -dc 'A-Z0-9') (RMCP01 expected)"
|
||||
echo "size: $(du -sh "$dvd_root" 2>/dev/null | cut -f1)"
|
||||
findmnt -T "$dvd_root" 2>&1
|
||||
else
|
||||
echo "MISSING: the folder does not exist (an SD card that is not mounted?)"
|
||||
fi
|
||||
fi
|
||||
} >"$root/game/disc.txt"
|
||||
|
||||
# The newest run folders (Logs/<product>_<epochSeconds>_pid<pid>/), console.log and crash files.
|
||||
logs_dir=$data_dir/Logs
|
||||
mapfile -t run_dirs < <(ls -1dt "$logs_dir"/*/ 2>/dev/null | sed 's:/$::')
|
||||
(( runs > 0 && ${#run_dirs[@]} > runs )) && run_dirs=("${run_dirs[@]:0:runs}")
|
||||
log "${#run_dirs[@]} run folder(s) from $logs_dir"
|
||||
ls -lt "$logs_dir" >"$root/game/all-runs.txt" 2>&1
|
||||
for dir in "${run_dirs[@]}"; do
|
||||
dest=$root/game/Logs/${dir##*/}
|
||||
mkdir -p "$dest"
|
||||
for f in "$dir"/*; do
|
||||
[[ -f "$f" ]] || continue
|
||||
[[ "${f##*/}" == mem1.bin && "$with_memory" != yes ]] && continue
|
||||
cp "$f" "$dest/" 2>/dev/null || log "could not copy $f"
|
||||
done
|
||||
done
|
||||
|
||||
if [[ "$with_core" == yes ]] && command -v coredumpctl >/dev/null 2>&1; then
|
||||
log "the newest core dump"
|
||||
timeout 120 coredumpctl dump --no-pager -o "$root/system/WiiCompiled.core" WiiCompiled >/dev/null 2>&1 ||
|
||||
log "no core dump was available"
|
||||
fi
|
||||
|
||||
# --- SteamVR and OpenXR -------------------------------------------------------------------------
|
||||
log "SteamVR and OpenXR"
|
||||
steam_dir=""
|
||||
for candidate in "$HOME/.local/share/Steam" "$HOME/.steam/steam" "$HOME/.steam/root"; do
|
||||
[[ -d "$candidate/logs" ]] && { steam_dir=$(readlink -f "$candidate"); break; }
|
||||
done
|
||||
if [[ -n "$steam_dir" ]]; then
|
||||
for f in "$steam_dir"/logs/vr*.txt "$steam_dir"/logs/compat_log.txt "$steam_dir"/logs/console_log.txt \
|
||||
"$steam_dir"/logs/content_log.txt; do
|
||||
[[ -f "$f" ]] && grab_tail "$f" "$root/steamvr/logs/${f##*/}"
|
||||
done
|
||||
grab "$steam_dir/config/steamvr.vrsettings" "$root/steamvr/steamvr.vrsettings"
|
||||
ls -la "$steam_dir/logs" >"$root/steamvr/logs-listing.txt" 2>&1
|
||||
else
|
||||
echo "no Steam folder with logs found" >"$root/steamvr/logs-listing.txt"
|
||||
fi
|
||||
for f in "${XDG_CONFIG_HOME:-$HOME/.config}/openxr/1/active_runtime.json" /etc/xdg/openxr/1/active_runtime.json \
|
||||
/usr/share/openxr/1/active_runtime.json; do
|
||||
[[ -f "$f" ]] && { echo "== $f"; cat "$f"; echo; }
|
||||
done >"$root/steamvr/openxr-runtime.txt" 2>&1
|
||||
pgrep -a 'vrserver|vrcompositor|vrmonitor|WiiCompiled' >"$root/steamvr/vr-processes.txt" 2>&1 ||
|
||||
echo "none running" >>"$root/steamvr/vr-processes.txt"
|
||||
|
||||
# --- Summary ------------------------------------------------------------------------------------
|
||||
newest=${run_dirs[0]:-}
|
||||
{
|
||||
echo "WiiCompiled VR diagnostics, $(date --iso-8601=seconds)"
|
||||
echo "SteamOS: $(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-unknown} ${VERSION_ID:-} build ${BUILD_ID:-?}")"
|
||||
echo "kernel: $(uname -r) uptime: $(uptime -p 2>/dev/null)"
|
||||
echo "memory: $(free -m 2>/dev/null | awk '/^Mem:/ { print $7 " MB available of " $2 " MB" }')"
|
||||
vk=$root/gpu/vulkaninfo-summary.txt
|
||||
gpu=$(sed -n 's/^[[:space:]]*deviceName[[:space:]]*=[[:space:]]*//p' "$vk" | head -n 1)
|
||||
driver=$(sed -n 's/^[[:space:]]*driverInfo[[:space:]]*=[[:space:]]*//p' "$vk" | head -n 1)
|
||||
echo "GPU: ${gpu:-unknown (no vulkaninfo; see gpu/)}${driver:+, driver $driver}"
|
||||
echo "game: $([[ -x "$game_dir/WiiCompiled" ]] && echo "installed in $game_dir" || echo "NOT FOUND in $game_dir")"
|
||||
echo "disc: $(head -n 1 "$root/game/disc.txt" | sed 's/^dvd_root: //')$(grep -q MISSING "$root/game/disc.txt" && echo ' (PROBLEM: see game/disc.txt)')"
|
||||
echo "SteamVR running: $(pgrep -x vrserver >/dev/null 2>&1 && echo yes || echo no)"
|
||||
echo "game running: $(pgrep -x WiiCompiled >/dev/null 2>&1 && echo "yes (the newest run's log is still being written)" || echo no)"
|
||||
echo
|
||||
if [[ -z "$newest" ]]; then
|
||||
echo "No run folders in $logs_dir: the game has not started on this Frame (or logs elsewhere)."
|
||||
else
|
||||
console=$newest/console.log
|
||||
echo "newest run: ${newest##*/} ($(stat -c %y "$newest" 2>/dev/null | cut -d. -f1))"
|
||||
version=$(head -n 1 "$console" 2>/dev/null | sed 's/^\[runtime\] //')
|
||||
# Builds without a build-fingerprint.json beside the executable log no version; its date
|
||||
# still tells builds apart.
|
||||
[[ "$version" == *"version unknown"* && -f "$game_dir/WiiCompiled" ]] &&
|
||||
version="$version (executable built $(stat -c %y "$game_dir/WiiCompiled" | cut -d. -f1))"
|
||||
echo "version: $version"
|
||||
echo
|
||||
echo "startup steps (README, Troubleshooting); the first one missing is where it stopped:"
|
||||
while IFS='|' read -r label pattern; do
|
||||
if grep -qF -- "$pattern" "$console" 2>/dev/null; then mark="ok "; else mark="MISSING"; fi
|
||||
echo " $mark $label"
|
||||
done <<'STEPS'
|
||||
OpenXR initialized|OpenXR initialized: runtime
|
||||
Dawn creates its device through the runtime|Dawn will create its device through the runtime
|
||||
Fragment density maps enabled|Fragment density maps: enabled
|
||||
Vulkan swapchains ready|OpenXR Vulkan swapchains ready
|
||||
Refresh rate requested|display refresh rate
|
||||
Session focused|OpenXR session state -> FOCUSED
|
||||
Controller profile bound|OpenXR interaction profiles:
|
||||
Eye gaze available|OpenXR eye gaze: available
|
||||
Eye gaze tracking|OpenXR eye gaze: tracking
|
||||
STEPS
|
||||
grep -qF "requires a Dawn built with Aurora's patches" "$console" 2>/dev/null &&
|
||||
echo " NOTE: built without --dawn-package (Dawn lacks Aurora's patches)"
|
||||
echo
|
||||
crashes=$(cd "$newest" && ls crash_*.txt 2>/dev/null | tr '\n' ' ')
|
||||
echo "crash files: ${crashes:-none}"
|
||||
echo "lines mentioning error, fail, fatal or warn in console.log: $(grep -ciE 'error|fail|fatal|warn' "$console" 2>/dev/null)"
|
||||
echo
|
||||
echo "--- last 40 lines of console.log ---"
|
||||
tail -n 40 "$console" 2>/dev/null
|
||||
echo "--- end of console.log ---"
|
||||
echo
|
||||
echo "--- last frame-pacing lines ([diagnostics] openxr_logging = true) ---"
|
||||
pacing=$(grep -h 'xr-diag\] 1.0' "$console" 2>/dev/null | tail -n 5)
|
||||
echo "${pacing:-(none: this run had openxr_logging off)}"
|
||||
for f in "$newest"/crash_*.txt; do
|
||||
[[ -f "$f" ]] || continue
|
||||
echo
|
||||
echo "--- ${f##*/} (first 60 lines) ---"
|
||||
head -n 60 "$f"
|
||||
done
|
||||
fi
|
||||
} >"$root/summary.txt" 2>&1
|
||||
|
||||
log "packing"
|
||||
tar -C "$tmp" -czf - "frame-diagnostics-$stamp"
|
||||
COLLECTOR
|
||||
|
||||
main "$@" </dev/null
|
||||
@@ -99,7 +99,7 @@ Usage: local-build.sh --output-dir DIR [options]
|
||||
--dawn-package DIR A Dawn built with Aurora's patches (Launcher/build-dawn-linux.sh's
|
||||
WORK_DIR/package); build it with the same --cc/--cxx
|
||||
--headset NAME steam_frame: the Steam Frame's native SteamOS build and its defaults
|
||||
(docs/steam-frame.md); empty for any other PC headset
|
||||
(README.md); empty for any other PC headset
|
||||
--cpu NAME AArch64 -mcpu target (default: cortex-x4 with --headset steam_frame,
|
||||
else native)
|
||||
EOF
|
||||
@@ -503,7 +503,9 @@ publish_built_product() {
|
||||
local exe=$build/$target
|
||||
assert_file "$exe" "Locally compiled game executable"
|
||||
cp -f "$exe" "$destination/$target"
|
||||
for name in dsp_coef.bin initial_pipeline_cache.db; do
|
||||
# cacert.pem is the TLS root bundle the mbed TLS path looks up beside the executable
|
||||
# (runtime/src/hle/net/network_ssl.cpp); without it HTTPS fails at runtime.
|
||||
for name in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
|
||||
[[ -f "$build/$name" ]] && cp -f "$build/$name" "$destination/"
|
||||
done
|
||||
[[ -d "$build/wii_bootstrap" ]] && cp -rf "$build/wii_bootstrap" "$destination/"
|
||||
|
||||
@@ -27,7 +27,7 @@ done
|
||||
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
|
||||
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
|
||||
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
|
||||
|
||||
app="$output_dir/$product.app"
|
||||
macos="$app/Contents/MacOS"
|
||||
@@ -52,7 +52,7 @@ cat > "$app/Contents/Info.plist" <<EOF
|
||||
</dict></plist>
|
||||
EOF
|
||||
ditto "$build_dir/$product" "$macos/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
|
||||
ditto "$build_dir/$asset" "$resources/$asset"
|
||||
ln -s "../Resources/$asset" "$macos/$asset"
|
||||
done
|
||||
|
||||
Executable
+926
@@ -0,0 +1,926 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds WiiCompiled VR for the Steam Frame from a release and your own disc, and installs it on the
|
||||
# Frame. Run it again with --update to update: it fetches the newest release and rebuilds only what
|
||||
# changed, with the options it was installed with.
|
||||
#
|
||||
# Launcher/steam-frame-install.sh --disc PATH [--frame HOST] [options]
|
||||
# Launcher/steam-frame-install.sh --update
|
||||
#
|
||||
# Or without downloading anything first:
|
||||
#
|
||||
# curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/openxr-work/Launcher/steam-frame-install.sh \
|
||||
# | bash -s -- --disc "/path/to/Mario Kart Wii.wbfs" --frame steamos@<frame-ip>
|
||||
#
|
||||
# It runs on an x86_64 or ARM64 Linux PC with podman (or docker), or on the Frame itself. The game
|
||||
# is built in a Debian ARM64 container, under qemu emulation on x86_64; the first build takes a few
|
||||
# hours there, mostly compiling Dawn, and later ones reuse it.
|
||||
#
|
||||
# Options:
|
||||
# --disc PATH your clean PAL RMCP01 disc: an ISO, WBFS or RVZ image (or WIA, CISO, GCZ, NFS,
|
||||
# TGC), a .zip or .7z holding one, or an extracted disc folder holding sys/ and
|
||||
# files/. Needed for the first build only.
|
||||
# --frame HOST where to install: an SSH destination (steamos@<frame-ip>, or an ~/.ssh/config
|
||||
# host such as Frame Control's "frame"), or "local" when running on the Frame.
|
||||
# Without it the game is only built.
|
||||
# --work-dir DIR where the toolchain, Dawn, the build and the extracted disc live
|
||||
# (default ~/wiicompiled-frame; about 20 GB)
|
||||
# --release TAG the release to build (default: the newest one). Ignored with --source.
|
||||
# --source DIR build this source tree instead of a release (default when the script runs
|
||||
# from inside one)
|
||||
# --jobs N parallel compiles (default: a quarter of the memory in GB; under emulation each
|
||||
# compile needs a lot of it)
|
||||
# --frame-disc DIR where the extracted disc goes on the Frame: absolute, or relative to the
|
||||
# Frame's home (default wiicompiled/disc; with --frame local, the disc folder
|
||||
# the build used)
|
||||
# --retro-rewind also build Retro Rewind and install it beside the base game. Its pack (about
|
||||
# 4 GB) comes from Retro Rewind's own update server, as Wheel Wizard and the
|
||||
# Quest app fetch it, and later runs apply only the updates published since.
|
||||
# --retro-rewind-pack DIR
|
||||
# use this RetroRewind6 folder instead of downloading one (implies
|
||||
# --retro-rewind); it is copied into the work dir and never changed
|
||||
# --update update an earlier install: its options are reused (any given here win), and
|
||||
# nothing is built when the Frame already has the newest release and Retro
|
||||
# Rewind pack. Follows releases unless it was installed with --source.
|
||||
# --check only say whether an update is available; nothing is changed
|
||||
# -h, --help
|
||||
#
|
||||
# Installed with --frame local, the game can update itself: Settings > Updates in the game checks
|
||||
# for a new release, and its Update button runs this script with --update in the background,
|
||||
# through a systemd user service this script sets up. The tab shows the update's progress while the
|
||||
# game stays open, and a game closed meanwhile is opened again when the update is done.
|
||||
set -euo pipefail
|
||||
|
||||
# Everything is in main, so bash has read the whole script before running any of it: when it comes
|
||||
# through curl | bash, a step that reads stdin cannot eat the rest. (Not indented, for the heredocs.)
|
||||
main() {
|
||||
# What the error path below reads, set before anything can fail into it.
|
||||
mode=install
|
||||
steam_game_id=""
|
||||
trap 'on_error "$LINENO"' ERR
|
||||
|
||||
repo=mitch030504/Wiicompiled_VR_Frame
|
||||
game_id=WiiCompiled
|
||||
image=docker.io/library/debian:trixie
|
||||
container=wiicompiled-frame-build
|
||||
nodtool_version=v2.0.0-alpha.10
|
||||
# Where the game keeps its settings, and where an update it starts reports back to it. Resolved as
|
||||
# the game resolves it (RuntimeConfigFile::ApplicationDataDirectory).
|
||||
game_data="${XDG_DATA_HOME:-$HOME/.local/share}/$game_id"
|
||||
|
||||
say() {
|
||||
printf '\n\033[1m==> %s\033[0m\n' "$*"
|
||||
status running "$*"
|
||||
}
|
||||
note() { printf ' %s\n' "$*"; }
|
||||
fail() {
|
||||
printf '\nsteam-frame-install.sh: error: %s\n' "$*" >&2
|
||||
finish_failed "${1%%$'\n'*}"
|
||||
}
|
||||
on_error() {
|
||||
printf '\nsteam-frame-install.sh: stopped by a failed step (line %s); the output above says why.\n' "$1" >&2
|
||||
finish_failed "a step failed (line $1)"
|
||||
}
|
||||
finish_failed() {
|
||||
trap - ERR
|
||||
status failed "$1"
|
||||
relaunch
|
||||
exit 1
|
||||
}
|
||||
usage() { awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$script_path"; }
|
||||
|
||||
# The game's Updates tab reads this: "<state>\t<unix time>\t<text>", state running, available,
|
||||
# uptodate, done or failed. Written only when the game asked for this run
|
||||
# (WIICOMPILED_UPDATE_STATUS names the file).
|
||||
status() {
|
||||
local file=${WIICOMPILED_UPDATE_STATUS:-}
|
||||
[[ -n "$file" ]] || return 0
|
||||
{ printf '%s\t%s\t%s\n' "$1" "$(date +%s)" "$2" > "$file.new" && mv -f "$file.new" "$file"; } 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Progress goes to that file rather than to Steam notifications: on the Frame, SteamOS's
|
||||
# steam_notif_daemon accepts a notify-send and Steam then shows nothing, even when given the
|
||||
# notification directly with steam -ifrunning (tried on a Frame, 2026-10).
|
||||
# An update the game started ends by opening the game again if it was closed meanwhile, which is
|
||||
# how its result gets seen; a game still open shows the result in its Updates tab.
|
||||
game_running() { pgrep -f "devkit-game/($game_id|RetroRewind)/" >/dev/null 2>&1; }
|
||||
relaunch() {
|
||||
[[ -n "$steam_game_id" ]] || return 0
|
||||
command -v steam >/dev/null 2>&1 || return 0
|
||||
if game_running; then return 0; fi
|
||||
note "opening $game_id again"
|
||||
timeout 60 steam "steam://rungameid/$steam_game_id" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
script_path=${BASH_SOURCE[0]:-}
|
||||
script_dir=""
|
||||
if [[ -n "$script_path" && -f "$script_path" ]]; then
|
||||
script_dir=$(cd "$(dirname "$script_path")" && pwd)
|
||||
script_path="$script_dir/$(basename "$script_path")"
|
||||
fi
|
||||
|
||||
disc=""
|
||||
frame=""
|
||||
work_dir="$HOME/wiicompiled-frame"
|
||||
release=""
|
||||
source_dir=""
|
||||
jobs=""
|
||||
frame_disc=""
|
||||
retro_rewind=0
|
||||
retro_pack=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--disc) disc=$2; shift 2 ;;
|
||||
--frame) frame=$2; shift 2 ;;
|
||||
--work-dir) work_dir=$2; shift 2 ;;
|
||||
--release) release=$2; shift 2 ;;
|
||||
--source) source_dir=$2; shift 2 ;;
|
||||
--jobs) jobs=$2; shift 2 ;;
|
||||
--frame-disc) frame_disc=$2; shift 2 ;;
|
||||
--retro-rewind) retro_rewind=1; shift ;;
|
||||
--retro-rewind-pack) retro_rewind=1; retro_pack=$2; shift 2 ;;
|
||||
--update) mode=update; shift ;;
|
||||
--check) mode=check; shift ;;
|
||||
-h|--help)
|
||||
if [[ -n "$script_dir" ]]; then usage; else echo "See the comment at the top of the script."; fi
|
||||
exit 0 ;;
|
||||
*) fail "unknown argument: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
# What --update reuses: the options given, before defaults fill the rest in.
|
||||
jobs_given=$jobs
|
||||
source_given=$source_dir
|
||||
|
||||
mkdir -p "$work_dir"
|
||||
work_dir=$(cd "$work_dir" && pwd)
|
||||
|
||||
# The game's Update button leaves a request file; the service that runs this script moves it here.
|
||||
if [[ -n "${WIICOMPILED_UPDATE_REQUEST:-}" && -f "$WIICOMPILED_UPDATE_REQUEST" ]]; then
|
||||
steam_game_id=$(sed -n 's/^steam_game_id=\([0-9]*\)$/\1/p' "$WIICOMPILED_UPDATE_REQUEST" | head -n 1)
|
||||
rm -f "$WIICOMPILED_UPDATE_REQUEST"
|
||||
fi
|
||||
|
||||
if [[ "$mode" != install && -f "$work_dir/install.conf" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
frame) [[ -n "$frame" ]] || frame=$value ;;
|
||||
frame_disc) [[ -n "$frame_disc" ]] || frame_disc=$value ;;
|
||||
jobs) [[ -n "$jobs" ]] || jobs=$value ;;
|
||||
source) [[ -n "$source_dir" ]] || source_dir=$value ;;
|
||||
retro_rewind) if [[ "$value" == 1 ]]; then retro_rewind=1; fi ;;
|
||||
retro_rewind_pack) [[ -n "$retro_pack" ]] || retro_pack=$value ;;
|
||||
esac
|
||||
done < "$work_dir/install.conf"
|
||||
jobs_given=$jobs
|
||||
source_given=$source_dir
|
||||
fi
|
||||
save_settings() {
|
||||
cat > "$work_dir/install.conf" <<EOF
|
||||
# Written by steam-frame-install.sh after each install: the options --update reuses.
|
||||
frame=$frame
|
||||
frame_disc=$frame_disc
|
||||
jobs=$jobs_given
|
||||
source=$source_given
|
||||
retro_rewind=$retro_rewind
|
||||
retro_rewind_pack=$retro_pack
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Talking to the Frame, and the versions involved. Up here because --update and --check read them
|
||||
# before anything is built.
|
||||
# One SSH connection for every step, so a password is asked for once.
|
||||
ssh_opts=(-o ControlMaster=auto -o "ControlPath=${XDG_RUNTIME_DIR:-/tmp}/wiicompiled-ssh-%C" -o ControlPersist=600)
|
||||
on_frame() {
|
||||
# Runs a bash script, given on stdin, on the Frame; its arguments follow.
|
||||
if [[ "$frame" == local ]]; then bash -s -- "$@"; else ssh "${ssh_opts[@]}" "$frame" bash -s -- "$@"; fi
|
||||
}
|
||||
read_on_frame() {
|
||||
# Prints the contents of $1 on the Frame, with its whitespace removed, or nothing.
|
||||
on_frame "$1" <<'EOF'
|
||||
[[ "$1" = /* ]] && f=$1 || f="$HOME/$1"
|
||||
tr -d '[:space:]' 2>/dev/null < "$f" || true
|
||||
EOF
|
||||
}
|
||||
frame_game_dir="devkit-game/$game_id"
|
||||
|
||||
newest_release() {
|
||||
curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=1" |
|
||||
sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1
|
||||
}
|
||||
|
||||
# Retro Rewind's own versioning, as its update server publishes it.
|
||||
rr_server=https://update.rwfc.net/RetroRewind/
|
||||
rr_version_ok() { [[ "$1" =~ ^[0-9]+(\.[0-9]+)+$ ]]; }
|
||||
rr_newer() {
|
||||
# True when version $1 is newer than $2 (dotted numbers, as 6.12.7).
|
||||
[[ "$1" != "$2" && "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n 1)" == "$1" ]]
|
||||
}
|
||||
rr_fetch() { curl -fsSL --retry 2 "$1"; }
|
||||
rr_published_version() {
|
||||
rr_fetch "${rr_server}RetroRewindVersion.txt" |
|
||||
awk 'NF >= 4 && $1 ~ /^[0-9]+(\.[0-9]+)+$/ { print $1 }' | sort -V | tail -n 1
|
||||
}
|
||||
# Where the pack sits on the Frame: beside the disc, as the install below puts it.
|
||||
frame_pack_path_for() {
|
||||
local disc=${1#\~/}
|
||||
[[ -n "$disc" ]] || disc=wiicompiled/disc
|
||||
if [[ "$disc" == */* ]]; then printf '%s/RetroRewind6\n' "$(dirname "$disc")"; else printf 'RetroRewind6\n'; fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# --update and --check: what the Frame has against what is published. Nothing is built when the
|
||||
# Frame is already current, so an update that has nothing to do costs one web request.
|
||||
if [[ "$mode" != install ]]; then
|
||||
[[ -n "$frame" ]] || fail "--$mode updates an install made by this script, and there is none in
|
||||
$work_dir. Install first (see --help), or pass --work-dir and --frame."
|
||||
say "Checking for an update"
|
||||
reasons=()
|
||||
if [[ -n "$source_dir" ]]; then
|
||||
# A source install has no published version to compare against; it rebuilds what changed.
|
||||
reasons+=("it was installed from the source tree in $source_dir, which is rebuilt as it is")
|
||||
else
|
||||
if [[ -z "$release" ]]; then
|
||||
release=$(newest_release) ||
|
||||
fail "could not reach github.com to ask for the newest release. Check this machine's
|
||||
internet connection, then try again."
|
||||
[[ -n "$release" ]] || fail "could not find the newest release on github.com/$repo"
|
||||
fi
|
||||
frame_installed_release=$(read_on_frame "$frame_game_dir/.release-tag") ||
|
||||
fail "cannot reach $frame over SSH to ask which release it has."
|
||||
if [[ "$frame_installed_release" == "$release" ]]; then
|
||||
note "the Frame has release $release"
|
||||
elif [[ -z "$frame_installed_release" ]]; then
|
||||
reasons+=("the Frame does not say which release it has; release $release would be installed")
|
||||
else
|
||||
reasons+=("release $release (the Frame has $frame_installed_release)")
|
||||
fi
|
||||
fi
|
||||
if (( retro_rewind )) && [[ -z "$retro_pack" ]]; then
|
||||
rr_published=$(rr_published_version) ||
|
||||
fail "could not reach Retro Rewind's update server (${rr_server})"
|
||||
rr_installed=$(read_on_frame "$(frame_pack_path_for "$frame_disc")/version.txt")
|
||||
if ! rr_version_ok "$rr_installed"; then
|
||||
reasons+=("the Frame does not say which Retro Rewind pack it has")
|
||||
elif rr_newer "$rr_published" "$rr_installed"; then
|
||||
reasons+=("Retro Rewind $rr_published (the Frame has $rr_installed)")
|
||||
else
|
||||
note "the Frame has Retro Rewind $rr_installed"
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( ${#reasons[@]} == 0 )); then
|
||||
say "Up to date"
|
||||
status uptodate "Up to date${release:+ (}${release}${release:+)}"
|
||||
relaunch
|
||||
exit 0
|
||||
fi
|
||||
note "an update is available:"
|
||||
for line in "${reasons[@]}"; do note " $line"; done
|
||||
if [[ "$mode" == check ]]; then
|
||||
status available "${reasons[0]}"
|
||||
exit 0
|
||||
fi
|
||||
status running "Starting the update${release:+ to }$release"
|
||||
fi
|
||||
|
||||
host_arch=$(uname -m)
|
||||
case "$host_arch" in
|
||||
x86_64|aarch64) ;;
|
||||
*) fail "this machine is $host_arch; the build needs an x86_64 or ARM64 Linux machine" ;;
|
||||
esac
|
||||
[[ "$(uname -s)" == Linux ]] || fail "run this on Linux (the build uses a Linux container)"
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
say "Checking the container runtime"
|
||||
runtime=""
|
||||
for candidate in podman docker; do
|
||||
if command -v "$candidate" >/dev/null 2>&1; then runtime=$candidate; break; fi
|
||||
done
|
||||
[[ -n "$runtime" ]] || fail "neither podman nor docker is installed.
|
||||
Arch, CachyOS: sudo pacman -S --needed podman qemu-user-static qemu-user-static-binfmt
|
||||
Debian, Ubuntu: sudo apt install podman qemu-user-static binfmt-support
|
||||
Fedora: sudo dnf install podman qemu-user-static
|
||||
SteamOS (the Frame) already has podman."
|
||||
note "using $runtime"
|
||||
for tool in curl tar; do
|
||||
command -v "$tool" >/dev/null 2>&1 || fail "'$tool' is not installed"
|
||||
done
|
||||
|
||||
platform_args=()
|
||||
if [[ "$host_arch" == x86_64 ]]; then
|
||||
platform_args=(--platform linux/arm64)
|
||||
say "Checking ARM64 emulation"
|
||||
seen=$("$runtime" run --rm "${platform_args[@]}" "$image" uname -m 2>&1 | tail -n 1 || true)
|
||||
case "$seen" in
|
||||
aarch64) note "ARM64 programs run under qemu" ;;
|
||||
Linux)
|
||||
fail "qemu is registered without the P (preserve argv[0]) flag its build needs, so every
|
||||
program loses its first argument (uname -m printed 'Linux'). Register it again with flags POCF;
|
||||
the README's 'Other ways to build' shows how." ;;
|
||||
*)
|
||||
fail "ARM64 programs do not run in containers here ($seen).
|
||||
Arch, CachyOS: sudo pacman -S --needed qemu-user-static qemu-user-static-binfmt
|
||||
sudo systemctl restart systemd-binfmt
|
||||
Debian, Ubuntu: sudo apt install qemu-user-static binfmt-support
|
||||
Fedora: sudo dnf install qemu-user-static && sudo systemctl restart systemd-binfmt
|
||||
Then run this script again. On hosts whose binfmt_misc is per container (Unraid), see the
|
||||
README's 'Other ways to build'." ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ -z "$jobs" ]]; then
|
||||
mem_gb=$(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo) / 1024 / 1024 ))
|
||||
jobs=$(( mem_gb / 4 ))
|
||||
(( jobs < 1 )) && jobs=1
|
||||
(( jobs > $(nproc) )) && jobs=$(nproc)
|
||||
fi
|
||||
[[ "$jobs" =~ ^[1-9][0-9]*$ ]] || fail "--jobs must be a positive number"
|
||||
note "$jobs parallel compiles"
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# The source: the tree this script is in, an explicit --source, or a release kept in the work dir.
|
||||
if [[ -z "$source_dir" && -z "$release" && -n "$script_dir" && -f "$script_dir/../runtime/CMakeLists.txt" ]]; then
|
||||
source_dir=$(cd "$script_dir/.." && pwd)
|
||||
fi
|
||||
if [[ -n "$source_dir" ]]; then
|
||||
source_dir=$(cd "$source_dir" && pwd)
|
||||
[[ -f "$source_dir/Launcher/local-build.sh" ]] || fail "$source_dir is not a WiiCompiled VR source tree"
|
||||
say "Building the source in $source_dir"
|
||||
else
|
||||
if [[ -z "$release" ]]; then
|
||||
release=$(curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=1" |
|
||||
sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1)
|
||||
[[ -n "$release" ]] || fail "could not find the newest release on github.com/$repo (try --release TAG)"
|
||||
fi
|
||||
source_dir="$work_dir/source"
|
||||
have=$(cat "$source_dir/.release-tag" 2>/dev/null || true)
|
||||
if [[ "$have" == "$release" ]]; then
|
||||
say "Release $release is already unpacked"
|
||||
else
|
||||
say "Downloading release $release"
|
||||
rm -rf "$work_dir/source-new"
|
||||
mkdir -p "$work_dir/source-new" "$source_dir"
|
||||
curl -fL --progress-bar "https://github.com/$repo/archive/refs/tags/$release.tar.gz" |
|
||||
tar -xz --strip-components=1 -C "$work_dir/source-new" ||
|
||||
fail "could not download release $release; the Releases page on github.com/$repo lists them"
|
||||
# Only files whose content changed are copied, stamped with the current time, so the build
|
||||
# recompiles exactly those; the release's own file dates can be older than the last build.
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -rcE "$work_dir/source-new/" "$source_dir/"
|
||||
else
|
||||
(cd "$work_dir/source-new" && find . -type f -print0) |
|
||||
while IFS= read -r -d '' file; do
|
||||
if ! cmp -s "$work_dir/source-new/$file" "$source_dir/$file"; then
|
||||
mkdir -p "$source_dir/$(dirname "$file")"
|
||||
cp "$work_dir/source-new/$file" "$source_dir/$file"
|
||||
touch "$source_dir/$file"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
rm -rf "$work_dir/source-new"
|
||||
printf '%s\n' "$release" > "$source_dir/.release-tag"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# The disc: the two files the translation reads, and the extracted disc the game reads at run time.
|
||||
check_disc_id() {
|
||||
# Fails unless the extracted disc in $1 is the release the translation is made for.
|
||||
local id
|
||||
if [[ ! -f "$1/sys/boot.bin" ]]; then
|
||||
note "could not check the disc's game ID: $1/sys/boot.bin is missing"
|
||||
return 0
|
||||
fi
|
||||
id=$(head -c 6 "$1/sys/boot.bin" | tr -dc 'A-Z0-9')
|
||||
[[ "$id" == RMCP01 ]] && return 0
|
||||
# An extraction of the wrong disc is not kept, so the next run extracts the one it is given.
|
||||
[[ "$1" == "$work_dir/disc" ]] && rm -rf "$1"
|
||||
case "$id" in
|
||||
RMC?01) fail "this is the $id release of Mario Kart Wii. The translation is made for the PAL
|
||||
release, RMCP01; other regions' code differs and is not supported." ;;
|
||||
*) fail "this disc's game ID is ${id:-unreadable}, not Mario Kart Wii PAL (RMCP01)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_nodtool() {
|
||||
nodtool="$work_dir/nodtool-$nodtool_version"
|
||||
if [[ ! -x "$nodtool" ]]; then
|
||||
curl -fL --progress-bar -o "$nodtool.partial" \
|
||||
"https://github.com/encounter/nod/releases/download/$nodtool_version/nodtool-linux-$host_arch"
|
||||
chmod +x "$nodtool.partial"
|
||||
mv "$nodtool.partial" "$nodtool"
|
||||
fi
|
||||
}
|
||||
|
||||
archive_kind() {
|
||||
# Prints zip or 7z when $1 is such an archive, judged by its first bytes rather than its name.
|
||||
case "$(od -An -tx1 -N6 "$1" | tr -d ' \n')" in
|
||||
504b0304*|504b0506*|504b0708*) echo zip ;;
|
||||
377abcaf271c) echo 7z ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
unpack_archive() {
|
||||
# unpack_archive ARCHIVE DEST KIND, with whichever unpacker this machine has.
|
||||
local archive=$1 dest=$2 kind=$3 tool
|
||||
for tool in 7zz 7z 7za; do
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
"$tool" x -y "-o$dest" "$archive" >/dev/null
|
||||
return
|
||||
fi
|
||||
done
|
||||
if command -v bsdtar >/dev/null 2>&1; then
|
||||
bsdtar -xf "$archive" -C "$dest"
|
||||
return
|
||||
fi
|
||||
if [[ "$kind" == zip ]] && command -v unzip >/dev/null 2>&1; then
|
||||
unzip -qo "$archive" -d "$dest"
|
||||
return
|
||||
fi
|
||||
if [[ "$kind" == zip ]] && command -v python3 >/dev/null 2>&1; then
|
||||
python3 -m zipfile -e "$archive" "$dest"
|
||||
return
|
||||
fi
|
||||
# Nothing here unpacks it: bsdtar in a container of this machine's own architecture.
|
||||
note "no 7-Zip or bsdtar on this machine; unpacking in a container"
|
||||
local platform=linux/amd64
|
||||
[[ "$host_arch" == aarch64 ]] && platform=linux/arm64
|
||||
"$runtime" run --rm --platform "$platform" -e OWNER="$(id -u):$(id -g)" \
|
||||
-v "$archive:/archive:ro,z" -v "$dest:/out:z" "$image" bash -c '
|
||||
apt-get update -qq && apt-get install -y -qq --no-install-recommends libarchive-tools >/dev/null &&
|
||||
bsdtar -xf /archive -C /out && chown -R "$OWNER" /out'
|
||||
}
|
||||
|
||||
find_disc_image() {
|
||||
# Prints the disc image nodtool reads under $1, largest first, preferring Mario Kart Wii PAL;
|
||||
# another disc is printed if there is no RMCP01, so the ID check names what it is.
|
||||
local file id first=""
|
||||
while IFS= read -r -d '' file; do
|
||||
id=$("$nodtool" --no-color info "$file" 2>/dev/null |
|
||||
sed -n 's/^Game ID: *\([A-Z0-9]\{6\}\).*/\1/p' | head -n 1) || true
|
||||
[[ -n "$id" ]] || continue
|
||||
if [[ "$id" == RMCP01 ]]; then
|
||||
printf '%s\n' "$file"
|
||||
return 0
|
||||
fi
|
||||
[[ -n "$first" ]] || first=$file
|
||||
done < <(find "$1" -type f -printf '%s\t%p\0' | sort -zrn | cut -zf2-)
|
||||
[[ -n "$first" ]] || return 1
|
||||
printf '%s\n' "$first"
|
||||
}
|
||||
|
||||
disc_dir="$work_dir/disc"
|
||||
if [[ -n "$disc" ]]; then
|
||||
[[ -e "$disc" ]] || fail "no disc at $disc"
|
||||
if [[ -d "$disc" ]]; then
|
||||
disc_dir=$(cd "$disc" && pwd)
|
||||
# Some tools put the game's partition in a subfolder (DATA/, for example).
|
||||
if [[ ! -f "$disc_dir/sys/main.dol" ]]; then
|
||||
for candidate in "$disc_dir"/*/; do
|
||||
if [[ -f "$candidate/sys/main.dol" && -f "$candidate/files/rel/StaticR.rel" ]]; then
|
||||
disc_dir=${candidate%/}
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[[ -f "$disc_dir/sys/main.dol" && -f "$disc_dir/files/rel/StaticR.rel" ]] ||
|
||||
fail "$disc is not an extracted disc: it needs sys/main.dol and files/rel/StaticR.rel"
|
||||
elif [[ ! -f "$disc_dir/sys/main.dol" ]]; then
|
||||
get_nodtool
|
||||
disc_image=$disc
|
||||
if kind=$(archive_kind "$disc"); then
|
||||
say "Unpacking the $kind archive (a minute or two)"
|
||||
rm -rf "$work_dir/archive"
|
||||
mkdir -p "$work_dir/archive"
|
||||
unpack_archive "$disc" "$work_dir/archive" "$kind" || {
|
||||
rm -rf "$work_dir/archive"
|
||||
fail "could not unpack $disc (a damaged or password-protected archive?)"
|
||||
}
|
||||
disc_image=$(find_disc_image "$work_dir/archive") || {
|
||||
rm -rf "$work_dir/archive"
|
||||
fail "there is no disc image nodtool reads inside $disc. It reads ISO, WBFS, RVZ, WIA,
|
||||
CISO, GCZ, NFS and TGC images; an archive inside the archive has to be unpacked by hand."
|
||||
}
|
||||
note "found ${disc_image#"$work_dir/archive/"} inside"
|
||||
fi
|
||||
say "Extracting your disc (a minute or two)"
|
||||
rm -rf "$disc_dir.partial"
|
||||
# nodtool tells the format from the file's contents, whatever it is named.
|
||||
"$nodtool" extract -q "$disc_image" "$disc_dir.partial" ||
|
||||
fail "nodtool could not read $disc. It reads ISO, WBFS, RVZ, WIA, CISO, GCZ, NFS and TGC
|
||||
images, and those inside a .zip or .7z."
|
||||
mv "$disc_dir.partial" "$disc_dir"
|
||||
rm -rf "$work_dir/archive"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -f "$disc_dir/sys/main.dol" || ! -f "$disc_dir/files/rel/StaticR.rel" ]]; then
|
||||
if [[ -f "$source_dir/Assets/main.dol" && -f "$source_dir/Assets/StaticR.rel" ]]; then
|
||||
disc_dir=""
|
||||
else
|
||||
fail "no disc yet: pass --disc with your disc image or an extracted disc folder"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$disc_dir" ]]; then
|
||||
check_disc_id "$disc_dir"
|
||||
note "disc: $disc_dir"
|
||||
fi
|
||||
mkdir -p "$source_dir/Assets"
|
||||
if [[ -n "$disc_dir" ]]; then
|
||||
cp "$disc_dir/sys/main.dol" "$disc_dir/files/rel/StaticR.rel" "$source_dir/Assets/"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Retro Rewind: its pack, as its update server publishes it, and the Retro-WFC payload its online
|
||||
# play runs. Same steps as the Quest app (android/.../RetroRewindPack.kt and GameBuild.kt).
|
||||
# Its server, versions and comparisons are up with the other version helpers.
|
||||
rr_pack="$work_dir/RetroRewind6"
|
||||
rr_payload="$work_dir/retro-wfc/binary/payload.RMCPD00.bin"
|
||||
|
||||
rr_unpack_update() {
|
||||
# rr_unpack_update URL DEST: downloads a published zip and lays its RetroRewind6/ tree over DEST.
|
||||
# Only that tree is kept; the Riivolution XML beside it belongs to a Wii setup.
|
||||
local url=${1/http:\/\/update.rwfc.net:8000\//https://update.rwfc.net/} dest=$2
|
||||
[[ "$url" == https://* ]] || fail "Retro Rewind's server gave a download that is not https: $url"
|
||||
rm -rf "$work_dir/rr-download" "$work_dir/rr-download.zip"
|
||||
mkdir -p "$work_dir/rr-download"
|
||||
curl -fL --retry 2 --progress-bar -o "$work_dir/rr-download.zip" "$url" ||
|
||||
fail "could not download $url"
|
||||
unpack_archive "$work_dir/rr-download.zip" "$work_dir/rr-download" zip ||
|
||||
fail "could not unpack $url"
|
||||
[[ -d "$work_dir/rr-download/RetroRewind6" ]] || fail "$url holds no RetroRewind6 folder"
|
||||
mkdir -p "$dest"
|
||||
cp -a "$work_dir/rr-download/RetroRewind6/." "$dest/"
|
||||
rm -rf "$work_dir/rr-download" "$work_dir/rr-download.zip"
|
||||
}
|
||||
|
||||
if (( retro_rewind )); then
|
||||
if [[ -n "$retro_pack" ]]; then
|
||||
[[ -f "$retro_pack/Binaries/Code.pul" ]] ||
|
||||
fail "$retro_pack is not a RetroRewind6 folder: it needs Binaries/Code.pul"
|
||||
say "Copying your Retro Rewind pack"
|
||||
rm -rf "$rr_pack.partial"
|
||||
cp -a "$retro_pack" "$rr_pack.partial"
|
||||
rm -rf "$rr_pack" && mv "$rr_pack.partial" "$rr_pack"
|
||||
else
|
||||
say "Checking Retro Rewind's published version"
|
||||
feed=$(rr_fetch "${rr_server}RetroRewindVersion.txt") ||
|
||||
fail "could not reach Retro Rewind's update server (${rr_server})"
|
||||
# One update per line: <version> <url> <path> <description>, oldest first.
|
||||
updates=$(awk 'NF >= 4 && $1 ~ /^[0-9]+(\.[0-9]+)+$/ { print $1, $2 }' <<<"$feed" | sort -V -k1,1)
|
||||
[[ -n "$updates" ]] || fail "Retro Rewind's version list is empty"
|
||||
latest=$(tail -n 1 <<<"$updates" | cut -d' ' -f1)
|
||||
installed=$(tr -d '[:space:]' 2>/dev/null < "$rr_pack/version.txt" || true)
|
||||
rr_version_ok "$installed" && [[ -f "$rr_pack/Binaries/Code.pul" ]] || installed=""
|
||||
if [[ -z "$installed" ]]; then
|
||||
say "Downloading Retro Rewind (about 4 GB)"
|
||||
base=$(rr_fetch "${rr_server}RetroRewindInstall.txt" | tr -d '[:space:]') ||
|
||||
fail "Retro Rewind's server did not say where its download is"
|
||||
rm -rf "$rr_pack.partial"
|
||||
rr_unpack_update "$base" "$rr_pack.partial" </dev/null
|
||||
[[ -f "$rr_pack.partial/Binaries/Code.pul" && -f "$rr_pack.partial/version.txt" ]] ||
|
||||
fail "Retro Rewind's download did not contain the pack"
|
||||
rm -rf "$rr_pack" && mv "$rr_pack.partial" "$rr_pack"
|
||||
installed=$(tr -d '[:space:]' < "$rr_pack/version.txt")
|
||||
fi
|
||||
if rr_newer "$latest" "$installed"; then
|
||||
deletions=$(rr_fetch "${rr_server}RetroRewindDelete.txt") ||
|
||||
fail "could not read Retro Rewind's deletion list"
|
||||
previous=$installed
|
||||
while read -r version url; do
|
||||
rr_newer "$version" "$previous" || continue
|
||||
say "Applying Retro Rewind $version"
|
||||
rr_unpack_update "$url" "$rr_pack" </dev/null
|
||||
# Each update's deletions follow it, before the next update can put a file back.
|
||||
# Only paths inside the pack are touched, and none that climb out of it.
|
||||
while read -r dversion dpath; do
|
||||
rr_version_ok "$dversion" || continue
|
||||
rr_newer "$dversion" "$previous" || continue
|
||||
rr_newer "$dversion" "$version" && continue
|
||||
dpath=${dpath//\\//}
|
||||
dpath=${dpath#/}
|
||||
[[ "$dpath" == RetroRewind6/?* && "/$dpath/" != */../* ]] || continue
|
||||
rm -rf "${rr_pack:?}/${dpath#RetroRewind6/}"
|
||||
done <<<"$deletions"
|
||||
previous=$version
|
||||
done <<<"$updates"
|
||||
# Written last, so an interrupted update runs again next time.
|
||||
printf '%s\n' "$latest" > "$rr_pack/version.txt"
|
||||
fi
|
||||
note "Retro Rewind $(tr -d '[:space:]' < "$rr_pack/version.txt")"
|
||||
fi
|
||||
|
||||
# translate-mod builds the payload into the game; without it, going online jumps into code that
|
||||
# was never translated. Fetched on every build, as the Quest app does.
|
||||
say "Downloading the Retro-WFC payload for online play"
|
||||
mkdir -p "$(dirname "$rr_payload")"
|
||||
if curl -fsS --retry 2 --max-filesize 16777216 -H 'Accept-Encoding: identity' \
|
||||
-o "$rr_payload.partial" 'https://rwfc.net/api/wfc/payload?g=RMCPD00'; then
|
||||
mv "$rr_payload.partial" "$rr_payload"
|
||||
elif [[ -f "$rr_payload" ]]; then
|
||||
rm -f "$rr_payload.partial"
|
||||
note "rwfc.net did not answer; using the payload from the last build"
|
||||
else
|
||||
fail "Retro Rewind's online play needs the Retro-WFC payload from rwfc.net, which could not be
|
||||
downloaded. Check this machine's internet connection, then run the script again."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
say "Preparing the ARM64 build container"
|
||||
cat > "$work_dir/container-build.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Written by steam-frame-install.sh and run inside its build container.
|
||||
set -euo pipefail
|
||||
if [[ ! -f /var/lib/wiicompiled-build-deps ]]; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq --no-install-recommends \
|
||||
ca-certificates curl git python3 xz-utils unzip file pkg-config g++ binutils libicu-dev \
|
||||
zlib1g-dev libvulkan-dev libx11-dev libx11-xcb-dev libxcb1-dev libxext-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev libxkbcommon-dev \
|
||||
libwayland-dev wayland-protocols libdecor-0-dev libegl-dev libgl-dev libgles-dev libdrm-dev \
|
||||
libgbm-dev libasound2-dev libpulse-dev libpipewire-0.3-dev libudev-dev libdbus-1-dev \
|
||||
libusb-1.0-0-dev >/dev/null
|
||||
touch /var/lib/wiicompiled-build-deps
|
||||
fi
|
||||
cd /src
|
||||
Launcher/prepare-portable-tools.sh --arch aarch64 --destination /work/tools
|
||||
T=/work/tools/toolchain-aarch64/bin
|
||||
if [[ ! -x /work/dotnet/dotnet ]]; then
|
||||
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0 --install-dir /work/dotnet
|
||||
fi
|
||||
echo "== Dawn (built once; reused until a release changes its patches)"
|
||||
Launcher/build-dawn-linux.sh --work-dir /work/dawn --cc "$T/clang" --cxx "$T/clang++" \
|
||||
--cmake "$T/cmake" --ninja "$T/ninja" --jobs "$JOBS"
|
||||
echo "== The game"
|
||||
products=(--output-dir /work/out)
|
||||
if [[ "$RETRO_REWIND" == 1 ]]; then
|
||||
# Both products from one translation: the base game in out/, Retro Rewind in out-retro-rewind/.
|
||||
products=(--profile both --output-dir /work/out-retro-rewind --base-output-dir /work/out
|
||||
--retro-rewind-package-dir /work/RetroRewind6 --retro-wfc-offline-dir /work/retro-wfc)
|
||||
fi
|
||||
Launcher/local-build.sh "${products[@]}" --cc "$T/clang" --cxx "$T/clang++" --fuse-ld lld \
|
||||
--cmake "$T/cmake" --ninja "$T/ninja" --dotnet /work/dotnet/dotnet --parallel "$JOBS" \
|
||||
--openxr --dawn-package /work/dawn/package --headset steam_frame
|
||||
EOF
|
||||
chmod +x "$work_dir/container-build.sh"
|
||||
|
||||
# One long-lived container, so its packages are installed once; recreated if its folders changed.
|
||||
want_mounts="$work_dir=/work;$source_dir=/src;"
|
||||
if "$runtime" container inspect "$container" >/dev/null 2>&1; then
|
||||
mounts=$("$runtime" container inspect -f '{{range .Mounts}}{{.Source}}={{.Destination}};{{end}}' "$container")
|
||||
if [[ "$mounts" != *"$work_dir=/work;"* || "$mounts" != *"$source_dir=/src;"* ]]; then
|
||||
note "its folders changed; recreating it"
|
||||
"$runtime" rm -f "$container" >/dev/null
|
||||
fi
|
||||
fi
|
||||
if ! "$runtime" container inspect "$container" >/dev/null 2>&1; then
|
||||
"$runtime" run -d --name "$container" "${platform_args[@]}" \
|
||||
-v "$work_dir:/work:z" -v "$source_dir:/src:z" "$image" sleep infinity >/dev/null
|
||||
fi
|
||||
"$runtime" start "$container" >/dev/null
|
||||
note "container $container (mounts ${want_mounts//;/ })"
|
||||
|
||||
build_progress() {
|
||||
# Passes the build's output on, and keeps the game's status file at the stage it is on and how
|
||||
# far through it ninja is, from the "[done/total]" lines it prints.
|
||||
local line stage=Building percent last=-1
|
||||
while IFS= read -r line; do
|
||||
printf '%s\n' "$line"
|
||||
[[ -n "${WIICOMPILED_UPDATE_STATUS:-}" ]] || continue
|
||||
if [[ "$line" == "== "* ]]; then
|
||||
stage=${line#== }
|
||||
stage=${stage%% (*}
|
||||
last=-1
|
||||
status running "$stage"
|
||||
elif [[ "$line" =~ ^\[([0-9]+)/([0-9]+)\] ]] && (( BASH_REMATCH[2] > 0 )); then
|
||||
percent=$(( BASH_REMATCH[1] * 100 / BASH_REMATCH[2] ))
|
||||
if (( percent != last )); then
|
||||
last=$percent
|
||||
status running "$stage, $percent% built"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
say "Building (the first time takes hours under emulation; a log is in $work_dir/build.log)"
|
||||
if ! "$runtime" exec -e JOBS="$jobs" -e RETRO_REWIND="$retro_rewind" "$container" bash /work/container-build.sh 2>&1 |
|
||||
tee "$work_dir/build.log" | build_progress; then
|
||||
fail "the build stopped; the end of $work_dir/build.log says why. Run the script again to resume.
|
||||
A machine that froze ran out of memory: pass a lower --jobs."
|
||||
fi
|
||||
[[ -x "$work_dir/out/$game_id" ]] || fail "the build finished without $work_dir/out/$game_id"
|
||||
note "built $work_dir/out/$game_id"
|
||||
if (( retro_rewind )); then
|
||||
[[ -x "$work_dir/out-retro-rewind/RetroRewind" ]] ||
|
||||
fail "the build finished without $work_dir/out-retro-rewind/RetroRewind"
|
||||
note "built $work_dir/out-retro-rewind/RetroRewind"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
if [[ -z "$frame" ]]; then
|
||||
say "Done"
|
||||
note "The game is in $work_dir/out. Run the script again with --frame steamos@<frame-ip> to install it."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
say "Installing on the Frame"
|
||||
# on_frame and the SSH options it uses are up with the version helpers.
|
||||
copy_to_frame() {
|
||||
# copy_to_frame SOURCE DEST: DEST is absolute or relative to the Frame's home. A DEST ending in
|
||||
# / is an existing folder SOURCE goes into; otherwise SOURCE is copied to that new name.
|
||||
local source=$1 dest=$2
|
||||
if [[ "$frame" == local ]]; then
|
||||
[[ "$dest" == /* ]] || dest="$HOME/$dest"
|
||||
cp -R "$source" "$dest"
|
||||
elif [[ "$frame_rsync" == yes ]]; then
|
||||
# rsync copies a folder without a trailing slash into DEST rather than as DEST.
|
||||
[[ -d "$source" && "$dest" != */ ]] && source="$source/"
|
||||
rsync -a -e "ssh ${ssh_opts[*]}" "$source" "$frame:$dest"
|
||||
else
|
||||
scp -rq "${ssh_opts[@]}" "$source" "$frame:$dest"
|
||||
fi
|
||||
}
|
||||
|
||||
on_frame <<<'true' || fail "cannot reach $frame over SSH. Turn on Developer Mode and set a user password on the
|
||||
Frame (Steam Settings > System), then check the address."
|
||||
frame_rsync=no
|
||||
if [[ "$frame" != local ]] && command -v rsync >/dev/null 2>&1 &&
|
||||
[[ "$(on_frame <<<'command -v rsync >/dev/null && echo yes || true')" == yes ]]; then
|
||||
frame_rsync=yes
|
||||
fi
|
||||
|
||||
install_game() {
|
||||
# install_game ID OUT: the built game in OUT goes to ~/devkit-game/ID on the Frame.
|
||||
local id=$1 out=$2 dir="devkit-game/$1"
|
||||
on_frame "$dir" <<'EOF'
|
||||
mkdir -p "$HOME/$1"
|
||||
EOF
|
||||
note "copying $id to ~/$dir"
|
||||
(cd "$out" && find . -mindepth 1 -maxdepth 1 ! -name "$id" -print0) |
|
||||
while IFS= read -r -d '' item; do copy_to_frame "$out/${item#./}" "$dir/"; done
|
||||
# The executable goes in under a new name and is moved into place: that works while it runs.
|
||||
copy_to_frame "$out/$id" "$dir/$id.new"
|
||||
on_frame "$dir" "$id" <<'EOF'
|
||||
cd "$HOME/$1" && mv -f "$2.new" "$2" && chmod -R u=rwX,go=rX . && chmod 755 "$2"
|
||||
EOF
|
||||
# Which release is on the Frame, for --check and the game's Updates tab. A source build has no
|
||||
# release to name, and the stamp is removed so neither claims a release this is not.
|
||||
on_frame "$dir" "$release" <<'EOF'
|
||||
cd "$HOME/$1" && if [[ -n "$2" ]]; then printf '%s\n' "$2" > .release-tag; else rm -f .release-tag; fi
|
||||
EOF
|
||||
}
|
||||
install_game "$game_id" "$work_dir/out"
|
||||
if (( retro_rewind )); then install_game RetroRewind "$work_dir/out-retro-rewind"; fi
|
||||
|
||||
# The Frame's disc folder, absolute or relative to its home.
|
||||
if [[ -z "$frame_disc" ]]; then
|
||||
if [[ "$frame" == local && -n "$disc_dir" ]]; then frame_disc=$disc_dir; else frame_disc=wiicompiled/disc; fi
|
||||
fi
|
||||
frame_disc_path=${frame_disc#\~/}
|
||||
has_disc=$(on_frame "$frame_disc_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
[[ -f "$d/sys/main.dol" ]] && echo yes || true
|
||||
EOF
|
||||
)
|
||||
if [[ "$has_disc" != yes ]]; then
|
||||
[[ -n "$disc_dir" ]] || fail "the Frame has no disc at $frame_disc yet: pass --disc"
|
||||
note "copying the extracted disc to $frame_disc (a few GB)"
|
||||
on_frame "$frame_disc_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
mkdir -p "$(dirname "$d")" && rm -rf "$d.partial"
|
||||
EOF
|
||||
copy_to_frame "$disc_dir" "$frame_disc_path.partial"
|
||||
on_frame "$frame_disc_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
rm -rf "$d" && mv "$d.partial" "$d"
|
||||
EOF
|
||||
fi
|
||||
|
||||
if (( retro_rewind )); then
|
||||
# The pack goes beside the disc; copied again only when its version changed.
|
||||
frame_pack_path=$(frame_pack_path_for "$frame_disc_path")
|
||||
frame_pack_version=$(read_on_frame "$frame_pack_path/version.txt")
|
||||
local_pack_version=$(tr -d '[:space:]' 2>/dev/null < "$rr_pack/version.txt" || true)
|
||||
if [[ -z "$frame_pack_version" || "$frame_pack_version" != "$local_pack_version" ]]; then
|
||||
note "copying the Retro Rewind pack to $frame_pack_path (about 4 GB)"
|
||||
on_frame "$frame_pack_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
mkdir -p "$(dirname "$d")" && rm -rf "$d.partial"
|
||||
EOF
|
||||
copy_to_frame "$rr_pack" "$frame_pack_path.partial"
|
||||
on_frame "$frame_pack_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
rm -rf "$d" && mv "$d.partial" "$d"
|
||||
EOF
|
||||
fi
|
||||
note "pointing Retro Rewind at its pack"
|
||||
on_frame "$frame_pack_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
config="$HOME/.local/share/WiiCompiled/Config.toml"
|
||||
mkdir -p "$(dirname "$config")"
|
||||
if [[ ! -f "$config" ]]; then
|
||||
printf '[paths]\nretro_rewind_root = "%s"\n' "$d" > "$config"
|
||||
elif ! grep -q '^retro_rewind_root *=' "$config"; then
|
||||
if grep -q '^\[paths\]' "$config"; then
|
||||
sed -i "/^\[paths\]/a retro_rewind_root = \"$d\"" "$config"
|
||||
else
|
||||
printf '\n[paths]\nretro_rewind_root = "%s"\n' "$d" >> "$config"
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
fi
|
||||
|
||||
note "pointing the game at the disc"
|
||||
on_frame "$frame_disc_path" <<'EOF'
|
||||
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
||||
config="$HOME/.local/share/WiiCompiled/Config.toml"
|
||||
mkdir -p "$(dirname "$config")"
|
||||
if [[ ! -f "$config" ]]; then
|
||||
printf '[paths]\ndvd_root = "%s"\n' "$d" > "$config"
|
||||
elif ! grep -q '^dvd_root *=' "$config"; then
|
||||
if grep -q '^\[paths\]' "$config"; then
|
||||
sed -i "/^\[paths\]/a dvd_root = \"$d\"" "$config"
|
||||
else
|
||||
printf '\n[paths]\ndvd_root = "%s"\n' "$d" >> "$config"
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Steam's library: through Valve's devkit tools when Frame Control or the Devkit Client put them on
|
||||
# the Frame, as a Steam Linux Runtime ARM64 title (Steam starts an ARM64 program natively).
|
||||
# Registering again is harmless and refreshes the entry, as Valve's Devkit Client does each upload.
|
||||
register_game() {
|
||||
# register_game ID OUT: adds ~/devkit-game/ID to Steam's library, or says how to.
|
||||
local id=$1 out=$2 dir="devkit-game/$1" registered
|
||||
registered=$(on_frame "$dir" "$id" <<'EOF'
|
||||
if [[ ! -f "$HOME/devkit-utils/steam-client-create-shortcut" ]]; then echo no-tools; exit 0; fi
|
||||
parms=$(printf '{"gameid": "%s", "directory": "%s", "argv": ["%s"], "env": {}, "settings": {"steam_play": "0", "compat_tool": "SteamLinuxRuntime_4-arm64"}, "clear_settings": true, "force_appid": "", "lepton_args": ""}' "$2" "$HOME/$1" "$2")
|
||||
reply=$(python3 "$HOME/devkit-utils/steam-client-create-shortcut" --parms "$parms" 2>/dev/null | tail -n 1)
|
||||
case "$reply" in *'"success"'*) echo added ;; *) echo "failed $reply" ;; esac
|
||||
EOF
|
||||
)
|
||||
case "$registered" in
|
||||
added) note "$id is in your Steam library" ;;
|
||||
no-tools|failed*)
|
||||
[[ "$registered" == failed* ]] && note "Steam did not take the shortcut: ${registered#failed }"
|
||||
note "Add $id to your Steam library once: in Frame Control, Send to Frame the folder"
|
||||
note " $out (name it $id), or on the Frame in Desktop Mode, Steam >"
|
||||
note " Add a Non-Steam Game > ~/$dir/$id. Later runs of this script keep it updated." ;;
|
||||
esac
|
||||
}
|
||||
register_game "$game_id" "$work_dir/out"
|
||||
if (( retro_rewind )); then register_game RetroRewind "$work_dir/out-retro-rewind"; fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Updating from inside the game. Only an install on the Frame itself can do it, since that is the
|
||||
# only one with the build here; installed from a PC, the game's Updates tab says to update there.
|
||||
if [[ "$frame" == local ]]; then
|
||||
note "setting up the game's Update button"
|
||||
update_script="$work_dir/steam-frame-install.sh"
|
||||
# A copy, so an update never runs from the source tree it is about to overwrite.
|
||||
cp -f "$source_dir/Launcher/steam-frame-install.sh" "$update_script"
|
||||
chmod +x "$update_script"
|
||||
mkdir -p "$game_data" "$HOME/.config/systemd/user"
|
||||
cat > "$game_data/update.conf" <<EOF
|
||||
# Written by steam-frame-install.sh: what the game's Updates tab runs. Remove this file to take the
|
||||
# Update button away, and the tab goes back to saying how to update from a PC.
|
||||
script=$update_script
|
||||
work_dir=$work_dir
|
||||
service=wiicompiled-update.service
|
||||
EOF
|
||||
# Its own service, so the update outlives the game that started it: Steam stops the game's own
|
||||
# process group when the game closes.
|
||||
cat > "$HOME/.config/systemd/user/wiicompiled-update.service" <<EOF
|
||||
[Unit]
|
||||
Description=WiiCompiled VR update
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=WIICOMPILED_UPDATE_STATUS=$game_data/update-status
|
||||
Environment=WIICOMPILED_UPDATE_REQUEST=$game_data/update-request
|
||||
# Below the game in line for the processor and the storage, so it can keep running meanwhile.
|
||||
Nice=19
|
||||
IOSchedulingClass=idle
|
||||
ExecStart=$update_script --update --work-dir "$work_dir"
|
||||
EOF
|
||||
systemctl --user daemon-reload >/dev/null 2>&1 ||
|
||||
note "(systemd did not reload, so the Update button will not work until the Frame restarts)"
|
||||
fi
|
||||
|
||||
save_settings
|
||||
|
||||
say "Done"
|
||||
status done "Updated${release:+ to }${release}"
|
||||
if [[ "$mode" == update ]]; then
|
||||
note "Updated${release:+ to }$release."
|
||||
else
|
||||
note "Start $game_id from your library in the headset. Settings: left shoulder button, VR tab."
|
||||
fi
|
||||
if (( retro_rewind )); then note "RetroRewind is beside it, and shares its settings."; fi
|
||||
relaunch
|
||||
}
|
||||
|
||||
main "$@" </dev/null
|
||||
@@ -147,6 +147,15 @@ headroom. It is live: **F10 → VR → Render resolution** (also on the headset
|
||||
in percent, applies it when the slider is let go, and saves it. Below the slider, *Each eye* gives
|
||||
the left eye's size now and, while they differ, the size the slider's value gives.
|
||||
|
||||
`adaptive_resolution` (default off; *Adaptive resolution (experimental)* on the VR tab) lowers the
|
||||
race's eye resolution in 10% steps, down to 70% of `render_scale`, while new eye frames fall below
|
||||
85% of the game's 60 FPS, and raises it again after three seconds on time. The eyes render into the
|
||||
top-left corner of the full swapchain image and the projection layer shows only that corner, so the
|
||||
compositor scales them up and no swapchain is rebuilt; each step does rebuild the foveation maps.
|
||||
`console.log` records every change as `OpenXR: adaptive resolution`. It is offered on the Quest and
|
||||
the Linux (Steam Frame) build, not on Windows, whose bridges copy whole eyes. Adapted from
|
||||
heurazy's [Wiicompiled_VR-PLUS](https://github.com/heurazy/Wiicompiled_VR-PLUS).
|
||||
|
||||
A new scale never interrupts the picture. Each backend keeps two swapchain pairs, one on display and
|
||||
one Aurora writes next, and rebuilds only the second, at the start of the frame that writes it; the
|
||||
other follows a frame later, once it is the one written. The new swapchains are created before the
|
||||
@@ -287,7 +296,7 @@ The Steam Frame's controllers get their own profile where the runtime offers it
|
||||
(`XR_VALVE_frame_controller_interaction`, `/interaction_profiles/valve/frame_controller_valve`):
|
||||
right A, B, trigger and stick as above, left View as the left menu (+), the left shoulder as left Y
|
||||
(the settings panel), and the left D-pad as the Wii Remote's D-pad (the gamepad's D-pad in
|
||||
`"gamepad"` mode). The table is in `docs/steam-frame.md`.
|
||||
`"gamepad"` mode). The table is in the README, Controls.
|
||||
|
||||
**Motion.** Each XR frame the aim and grip poses are located at the measured current time
|
||||
(`XR_KHR_win32_convert_performance_counter_time`, `XR_KHR_convert_timespec_time` on Android), not
|
||||
@@ -1097,8 +1106,8 @@ turned into tangents of each eye's view (`vr/eye_gaze.h`), which `AuroraStereoFr
|
||||
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`.
|
||||
tracker, the setting off) the map is the forward one above, unchanged. The README's How it works
|
||||
covers the Steam Frame.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
@@ -1282,8 +1291,8 @@ custom DLL's ABI through three borrowed-image copy/readback cycles; run it with
|
||||
| Windows D3D12 | Implemented: same-adapter, same-device asynchronous OpenXR submission. |
|
||||
| Windows Vulkan | Implemented, opt-in (`video.graphics_api = "vulkan"`): the runtime creates Dawn's Vulkan instance and device through `XR_KHR_vulkan_enable2`, eyes are copied on the same queue, and Dawn's device guard is held around the four queue-touching OpenXR calls. Needs the custom Dawn from `Launcher/Build-DawnVulkan.ps1`. Raced on SteamVR/PSVR2 at the headset's full rate; other runtimes unexercised. See [Windows Vulkan](#windows-vulkan). |
|
||||
| Android Vulkan (Meta Quest) | Implemented and running on a Quest 3: the OpenXR side owns its own Vulkan device (`XR_KHR_vulkan_enable2`, `XR_KHR_vulkan_enable` fallback) and shares eyes with Dawn through `AHardwareBuffer`s ordered by sync-fd fences. Controllers arrive through OpenXR actions as a virtual SDL gamepad. See `docs/quest-port.md`. |
|
||||
| Android Vulkan (Steam Frame) | The same backend in the `steamFrame` flavour, for SteamVR's Android runtime under Lepton: Frame controller profile, 120 Hz request, eye-tracked foveation. Blocked on the headset: Lepton's Turnip exposes no AHardwareBuffer or fd external memory, so the eyes cannot cross devices. The native build below is the supported route. See `docs/steam-frame.md`. |
|
||||
| Linux Vulkan (Steam Frame, SteamOS) | Implemented, not yet run on the headset: the Windows Vulkan design compiled for Linux, so the runtime creates Dawn's own instance and device and the eyes are copied on Dawn's queue with no sharing. Adds the Frame controller profile, 120 Hz and gaze-centred foveation. Needs a Dawn built with Aurora's patches (`Launcher/build-dawn-linux.sh`), then `Launcher/local-build.sh --openxr --dawn-package <dir> --headset steam_frame`. See `docs/steam-frame.md`. |
|
||||
| Android Vulkan (Steam Frame) | The same backend in the `steamFrame` flavour, for SteamVR's Android runtime under Lepton: Frame controller profile, 120 Hz request, eye-tracked foveation. Blocked on the headset: Lepton's Turnip exposes no AHardwareBuffer or fd external memory, so the eyes cannot cross devices. The native build below is the supported route. See the README, The Android flavour. |
|
||||
| Linux Vulkan (Steam Frame, SteamOS) | Implemented and played on a Steam Frame (beta): the Windows Vulkan design compiled for Linux, so the runtime creates Dawn's own instance and device and the eyes are copied on Dawn's queue with no sharing. Adds the Frame controller profile, 120 Hz and gaze-centred foveation. Needs a Dawn built with Aurora's patches (`Launcher/build-dawn-linux.sh`), then `Launcher/local-build.sh --openxr --dawn-package <dir> --headset steam_frame`. See the README, Quick start. |
|
||||
| Other platforms | Not wired yet. |
|
||||
|
||||
Both bindings share `openxr_integration.cpp`: the pacing thread, policy evaluation, the
|
||||
|
||||
@@ -29,129 +29,402 @@ C++ and compiled for the Frame's ARM64 CPU, and it renders through SteamVR's Ope
|
||||
|
||||
---
|
||||
|
||||
## What this fork adds
|
||||
## Quick start
|
||||
|
||||
Everything below is in [`docs/steam-frame.md`](docs/steam-frame.md), with the reasoning and the
|
||||
readings it is based on.
|
||||
This is the whole way from your disc to playing in the headset. An install script does the work: it
|
||||
downloads the newest [release](https://github.com/mitch030504/Wiicompiled_VR_Frame/releases),
|
||||
extracts your disc, builds the game in an ARM64 container on your PC, copies it and the disc to the
|
||||
Frame, and adds it to your Steam library. Releases are source only: the game is always built from
|
||||
your own disc, so there is nothing ready-built to download. The first build takes a few hours, most
|
||||
of it compiling Dawn under emulation; later builds reuse it.
|
||||
|
||||
- **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`.
|
||||
**You need:**
|
||||
- a Steam Frame with Developer Mode on (Steam Settings → System → Enable Developer Mode, then set a
|
||||
user password), on the same network as your PC;
|
||||
- your own clean PAL `RMCP01` disc of Mario Kart Wii: an ISO, WBFS or RVZ image (WIA, CISO, GCZ
|
||||
and NFS work too), as is or in a `.zip` or `.7z`, or a folder you extracted it to;
|
||||
- an x86_64 Linux PC with about 20 GB free and 16 GB of memory or more.
|
||||
|
||||
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
|
||||
|
||||
- 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 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.
|
||||
The PC commands below work in bash, zsh and fish.
|
||||
|
||||
> [!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. 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.
|
||||
|
||||
## Building and installing
|
||||
### 1. Set up ARM64 emulation on the PC
|
||||
|
||||
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:
|
||||
```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
|
||||
```
|
||||
|
||||
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:
|
||||
The last command must print `aarch64`. On Debian or Ubuntu install `podman qemu-user-static
|
||||
binfmt-support` instead. If podman complains about subordinate ids, run
|
||||
`sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER` and log in again.
|
||||
Docker works too, in place of podman.
|
||||
|
||||
```toml
|
||||
[paths]
|
||||
dvd_root = "/home/steamos/wiicompiled/disc"
|
||||
```
|
||||
### 2. Run the installer
|
||||
|
||||
### Recommended settings
|
||||
With the path to your disc image and your Frame's address:
|
||||
|
||||
All of them are in the headset's settings panel (left shoulder button, **VR** tab) as well as in
|
||||
`Config.toml`:
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/openxr-work/Launcher/steam-frame-install.sh \
|
||||
| bash -s -- --disc "/path/to/Mario Kart Wii.wbfs" --frame steamos@<frame-ip>
|
||||
```
|
||||
|
||||
- `--disc` takes the image whatever its name or extension; the format is read from the file
|
||||
itself. A `.zip` or `.7z` is unpacked first (with 7-Zip, bsdtar or unzip if you have one, in a
|
||||
container otherwise), and the disc image inside it is found and used. It checks the game ID
|
||||
before building and stops on any disc other than PAL `RMCP01`.
|
||||
- It asks for the Frame's password when it gets there. If you use
|
||||
[Frame Control](https://github.com/saphid/frame-control), pass `--frame frame` instead: its SSH
|
||||
key answers to that name.
|
||||
- Everything it builds lives in `~/wiicompiled-frame` (`--work-dir` to change that): the toolchain,
|
||||
Dawn, the source, the extracted disc and the build log, `build.log`.
|
||||
- It runs as many compiles at once as fit in memory (a quarter of your memory in GB). If the machine
|
||||
still freezes, run it again with a lower `--jobs`, such as `--jobs 2`.
|
||||
- If it stops for any reason, the same command picks up where it left off.
|
||||
- On the Frame, the game goes to `~/devkit-game/WiiCompiled/` and the disc to `~/wiicompiled/disc`.
|
||||
It is added to your Steam library through Valve's devkit tools in `~/devkit-utils`, which Frame
|
||||
Control and Valve's Devkit Client put there. Without them, the script says how to add it once
|
||||
yourself. Steam must be running on the Frame for this step.
|
||||
- Add `--retro-rewind` to also build [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind).
|
||||
The script downloads its pack (about 4 GB) from Retro Rewind's own update server, as Wheel Wizard
|
||||
does, and later runs apply only the updates published since. It also fetches the Retro-WFC
|
||||
payload online play needs. On the Frame, Retro Rewind goes to `~/devkit-game/RetroRewind/` and
|
||||
its pack to `~/wiicompiled/RetroRewind6`, and it shares WiiCompiled's settings.
|
||||
`--retro-rewind-pack DIR` uses a RetroRewind6 folder you already have instead.
|
||||
- `--help` lists every option. From a downloaded release, run `Launcher/steam-frame-install.sh`
|
||||
with the same options: it then builds that release's source.
|
||||
|
||||
### 3. Play
|
||||
|
||||
Start **WiiCompiled** from the library in the headset. Open the settings panel with the left
|
||||
shoulder button, go to the **VR** tab and set the [recommended settings](#recommended-settings).
|
||||
|
||||
### Updating
|
||||
|
||||
```bash
|
||||
Launcher/steam-frame-install.sh --update
|
||||
```
|
||||
|
||||
`--update` reuses the options the install was made with, so there is nothing to repeat: it reads
|
||||
them from `install.conf` in the work dir, and any option you do pass wins over the saved one. It
|
||||
downloads the newest release, puts only the files that changed over the old source, rebuilds what
|
||||
they touch, and replaces the game on the Frame. Dawn is only rebuilt when a release changes its
|
||||
patches (the release notes say so), so an update usually takes minutes. When the Frame already has
|
||||
the newest release, and the newest Retro Rewind pack if you use one, it says so and builds nothing.
|
||||
|
||||
`--check` only reports whether an update is available. `--release <tag>` builds a given release
|
||||
instead. Running the whole install command again works as it always did.
|
||||
|
||||
### Updating from inside the game
|
||||
|
||||
An install built on the Frame itself (`--frame local`, [below](#other-ways-to-build)) can update
|
||||
itself: the settings panel grows an **Updates** tab with the release you are running and a **Check
|
||||
for updates** button, and when there is one, **Update now** builds it in the background. The tab
|
||||
shows each step and how far the build is while you keep playing (the game may stutter while it
|
||||
compiles), and says when to restart the game to use the update.
|
||||
|
||||
The update runs as a systemd user service (`wiicompiled-update.service`), behind the game in line
|
||||
for the processor, so it carries on if you close the game, and then opens the game again when it is
|
||||
done. Keep the Frame on its charger for it. Installed from a PC, the tab says so and the update is
|
||||
run there. Progress is not shown as Steam notifications: on the Frame, Steam receives them from
|
||||
SteamOS's notification service but does not display them.
|
||||
|
||||
## Recommended settings
|
||||
|
||||
All of these are in the headset's settings panel (left shoulder button, **VR** tab) and in
|
||||
`~/.local/share/WiiCompiled/Config.toml`. Quit the game before editing the file; it writes its
|
||||
settings back when it closes.
|
||||
|
||||
| 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. |
|
||||
| `[vr] render_scale` | `1.25` | Scales SteamVR's recommended eye size, 1728x1728 on the Frame: 1.25 is the panels' native 2160x2160, which the Frame renders in 9 to 11 ms a frame 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 game's rate and fills refreshes itself. |
|
||||
| `[vr] frame_interpolation_fps` | `0` | Rendering in-between frames needs 120 eye pairs a second, which made things worse on the Frame. |
|
||||
| `[vr] adaptive_resolution` | `false` (default) | Experimental and untested on the Frame. When on, races drop to as little as 70% of `render_scale` while new frames fall behind 60 FPS, and climb back once they keep up. Each step rebuilds the foveation maps; `console.log` records every change as `OpenXR: adaptive resolution`. |
|
||||
| `[video] resolution_multiplier` | `2` | The game's own frame, which the eyes are made from. 4x is far too heavy for the Frame's GPU. |
|
||||
|
||||
Keep SteamVR's own refresh rate at 120 Hz. Motion Smoothing makes no difference here.
|
||||
Keep SteamVR's refresh rate at 120 Hz. Motion Smoothing makes no difference to this game.
|
||||
|
||||
## Controls
|
||||
|
||||
The Frame's controllers are bound through their own profile, so the left D-pad works:
|
||||
|
||||
| Frame controller | Wii Remote mode | Gamepad mode |
|
||||
| --- | --- | --- |
|
||||
| Right A | A | South (A) |
|
||||
| Right B | C (look behind) | East (B) |
|
||||
| Right trigger | B | Right trigger |
|
||||
| Right stick up / down | 1 / 2 | Right stick |
|
||||
| Left View | + (pause) | Start |
|
||||
| Left shoulder | Settings panel | North (Y) |
|
||||
| Left D-pad | Wii Remote D-pad | D-pad |
|
||||
| Left stick, left trigger | Nunchuk stick, Z | Left stick, left trigger |
|
||||
| Grips, stick clicks, motion, aim | as on Quest Touch ([`OPENXR.md`](OPENXR.md), Controllers) | as on Touch |
|
||||
| Right X, Y, menu and shoulder | unbound | unbound |
|
||||
|
||||
## 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.
|
||||
Both eyes get the same frames, so a doubling in one eye points at foveation: the Frame's driver
|
||||
(Turnip) draws a foveated screen tile at lower resolution and scales it back up, and each eye's
|
||||
density maps change with its gaze. Try foveation **Off** if it bothers you, and report whether it
|
||||
helped.
|
||||
- **Foveation follows the right eye less well** than the left. 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.
|
||||
- **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.
|
||||
- **The Quest app's `steamFrame` flavour** cannot show a picture in the Frame's Android layer
|
||||
([below](#the-android-flavour)). Use the native build.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Everything at once.** From the PC, `Launcher/frame-diagnostics.sh --frame steamos@<frame-ip>` (or
|
||||
`--frame frame` with Frame Control) gathers the newest runs' logs and crash files, `Config.toml`,
|
||||
SteamVR's logs, the GPU and Vulkan driver, and the system's state into one `.tar.gz`. Its
|
||||
`summary.txt`, also printed at the end, gives the startup steps below that the newest run reached.
|
||||
`--help` lists the options.
|
||||
|
||||
**Logs.** Each run gets a folder under `~/.local/share/WiiCompiled/Logs/` on the Frame, holding
|
||||
`console.log` and, after a crash, `crash_sigsegv.txt`. A working start logs, in order:
|
||||
|
||||
1. `OpenXR initialized: runtime 'SteamVR/OpenXR'`;
|
||||
2. `Dawn will create its device through the runtime`;
|
||||
3. `Fragment density maps: enabled`;
|
||||
4. `OpenXR Vulkan swapchains ready ... same-queue native eye copies`;
|
||||
5. `display refresh rate 120 Hz requested`, then `OpenXR session state -> FOCUSED`;
|
||||
6. `OpenXR interaction profiles: left /interaction_profiles/valve/frame_controller_valve`;
|
||||
7. `OpenXR eye gaze: available`, then `tracking`.
|
||||
|
||||
The first one missing says where it stopped. `Linux Vulkan OpenXR requires a Dawn built with
|
||||
Aurora's patches` means the game was built without `--dawn-package`.
|
||||
|
||||
**Smoothness.** With `[diagnostics] openxr_logging = true`, the log gets a pacing line every second:
|
||||
|
||||
```bash
|
||||
ssh frame 'cd ~/.local/share/WiiCompiled/Logs && d=$(ls -t | head -1) && grep -h "xr-diag\] 1.0" "$d/console.log" | tail -5'
|
||||
```
|
||||
|
||||
A healthy race reads `predicted-rate=120.0Hz`, `new=60 repeat=60` and `late=0`. Fewer than 60 `new`
|
||||
frames means the GPU is over budget: lower `render_scale` or `resolution_multiplier`.
|
||||
|
||||
**Crashes.** `console.log` then names the faulting thread and gives its pc and a backtrace as
|
||||
`module+offset`. On the build machine,
|
||||
`addr2line -f -C -e ~/wiicompiled-frame/source/native-build/WiiCompiled 0x<offset>` turns an offset
|
||||
in `WiiCompiled` into a function: the executable is not stripped. (Built by hand, the executable is
|
||||
in `~/wiicompiled/Wiicompiled_VR_Frame/native-build/`.)
|
||||
|
||||
**Building.** The install script's log is `~/wiicompiled-frame/build.log`; its end says why a build
|
||||
stopped. A build that freezes the machine has run out of memory: lower `--jobs` (or, by hand,
|
||||
`--parallel`). A missing CMake package means installing its `-dev` package in the container and
|
||||
running the same command again.
|
||||
|
||||
## Building by hand
|
||||
|
||||
These are the steps the install script runs, for when you want to see or change each one. Set up
|
||||
emulation as in [Quick start](#quick-start) step 1 first.
|
||||
|
||||
### 1. Download the release and extract your disc
|
||||
|
||||
Take the newest release from the [Releases](https://github.com/mitch030504/Wiicompiled_VR_Frame/releases)
|
||||
page; the commands use `frame-beta-1`, so put the newest release's tag in its place:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/wiicompiled/Wiicompiled_VR_Frame; cd ~/wiicompiled
|
||||
curl -fL https://github.com/mitch030504/Wiicompiled_VR_Frame/archive/refs/tags/frame-beta-1.tar.gz \
|
||||
| tar -xz --strip-components=1 -C Wiicompiled_VR_Frame
|
||||
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/
|
||||
```
|
||||
|
||||
`disc-extract` must hold `sys/` and `files/` directly; keep it, the game reads it when it runs.
|
||||
|
||||
### 2. Build, inside an ARM64 container
|
||||
|
||||
```bash
|
||||
podman run -it --name wiicompiled-frame --platform linux/arm64 -v ~/wiicompiled:/work docker.io/library/debian:trixie bash
|
||||
```
|
||||
|
||||
Then, in the container's bash prompt:
|
||||
|
||||
```bash
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl git python3 xz-utils unzip file pkg-config g++ binutils libicu-dev zlib1g-dev \
|
||||
libvulkan-dev libx11-dev libx11-xcb-dev libxcb1-dev libxext-dev libxrandr-dev libxinerama-dev \
|
||||
libxcursor-dev libxi-dev libxss-dev libxtst-dev libxkbcommon-dev libwayland-dev wayland-protocols \
|
||||
libdecor-0-dev libegl-dev libgl-dev libgles-dev libdrm-dev libgbm-dev libasound2-dev libpulse-dev \
|
||||
libpipewire-0.3-dev libudev-dev libdbus-1-dev libusb-1.0-0-dev
|
||||
cd /work/Wiicompiled_VR_Frame
|
||||
Launcher/prepare-portable-tools.sh --arch aarch64 --destination /work/tools # clang 22, CMake, Ninja
|
||||
T=/work/tools/toolchain-aarch64/bin
|
||||
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0 --install-dir /work/dotnet
|
||||
Launcher/build-dawn-linux.sh --work-dir /work/dawn --cc $T/clang --cxx $T/clang++ \
|
||||
--cmake $T/cmake --ninja $T/ninja --jobs 4 2>&1 | tee -a /work/dawn.log
|
||||
Launcher/local-build.sh --output-dir /work/out --cc $T/clang --cxx $T/clang++ --fuse-ld lld \
|
||||
--cmake $T/cmake --ninja $T/ninja --dotnet /work/dotnet/dotnet --parallel 4 \
|
||||
--openxr --dawn-package /work/dawn/package --headset steam_frame 2>&1 | tee -a /work/game.log
|
||||
exit
|
||||
```
|
||||
|
||||
- Dawn is done when it prints `Patched Dawn for Linux ready`, the game when it prints
|
||||
`MKWCBUILD:OUTPUT=/work/out`.
|
||||
- `--jobs 4` and `--parallel 4` suit 16 GB of memory. Compiles take more memory under emulation, and
|
||||
16 at once froze a 16 GB laptop. Use 8 with 32 GB.
|
||||
- If it stops, `podman start -ai wiicompiled-frame` gets you back in. Run `cd /work/Wiicompiled_VR_Frame;
|
||||
T=/work/tools/toolchain-aarch64/bin`, then the step that stopped: both scripts resume.
|
||||
|
||||
### 3. Install on the Frame
|
||||
|
||||
The easiest way is [Frame Control](https://github.com/saphid/frame-control): set up its connection
|
||||
to the Frame, then drag `~/wiicompiled/out` onto **Send to Frame**. Name it `WiiCompiled` and keep
|
||||
**Launches** on `WiiCompiled`. It adds the game to your Steam library, in `~/devkit-game/WiiCompiled/`
|
||||
on the Frame.
|
||||
|
||||
Then copy the disc over and, on a first install, tell the game where it is (Frame Control's SSH
|
||||
key answers to `frame`; use `steamos@<frame-ip>` otherwise):
|
||||
|
||||
```bash
|
||||
ssh frame mkdir -p wiicompiled
|
||||
scp -r ~/wiicompiled/disc-extract frame:wiicompiled/disc
|
||||
ssh frame 'mkdir -p ~/.local/share/WiiCompiled && printf "[paths]\ndvd_root = \"/home/steamos/wiicompiled/disc\"\n" > ~/.local/share/WiiCompiled/Config.toml'
|
||||
```
|
||||
|
||||
Without Frame Control, `scp -r ~/wiicompiled/out frame:wiicompiled/` and start
|
||||
`~/wiicompiled/out/WiiCompiled` from a terminal in the Frame's Desktop Mode, with SteamVR running.
|
||||
|
||||
### Updating by hand
|
||||
|
||||
When a new release comes out, put its changed files over the old source, rebuild, and replace only
|
||||
the executable. `rsync -c` copies just the files whose content changed and stamps them with the
|
||||
current time, so the build recompiles exactly those. Unpacking over the source would restore each
|
||||
file's commit date, which can be older than the last build, and changes would be skipped. Your
|
||||
`Assets/` and build folders stay. Install `rsync` if your system lacks it, and put the new release's
|
||||
tag in place of `frame-beta-2`:
|
||||
|
||||
```bash
|
||||
cd ~/wiicompiled
|
||||
mkdir -p release-new
|
||||
curl -fL https://github.com/mitch030504/Wiicompiled_VR_Frame/archive/refs/tags/frame-beta-2.tar.gz \
|
||||
| tar -xz --strip-components=1 -C release-new
|
||||
rsync -rcE release-new/ Wiicompiled_VR_Frame/
|
||||
rm -rf release-new
|
||||
podman start -ai wiicompiled-frame
|
||||
# in the container: cd /work/Wiicompiled_VR_Frame; T=/work/tools/toolchain-aarch64/bin, then the
|
||||
# build-dawn-linux.sh and local-build.sh lines from step 2, then exit. Dawn only rebuilds when the
|
||||
# release changed its patches (the release notes say so); otherwise both finish quickly.
|
||||
scp ~/wiicompiled/out/WiiCompiled frame:devkit-game/WiiCompiled/WiiCompiled.new
|
||||
ssh frame 'cd ~/devkit-game/WiiCompiled && chmod 755 WiiCompiled.new && mv -f WiiCompiled.new WiiCompiled'
|
||||
```
|
||||
|
||||
The executable is copied under a new name and moved into place, which works even while an old copy
|
||||
is open.
|
||||
|
||||
## Other ways to build
|
||||
|
||||
**On the Frame itself.** SteamOS's root file system is read-only, but it ships podman, and the
|
||||
install script runs there too. Over SSH (`ssh steamos@<frame-ip>`), or in a Desktop Mode terminal:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/openxr-work/Launcher/steam-frame-install.sh \
|
||||
| bash -s -- --disc "/path/to/Mario Kart Wii.wbfs" --frame local
|
||||
```
|
||||
|
||||
It is native ARM64, so no emulation, but the Frame has less memory and cooling than a PC; keep it
|
||||
on its charger. The game then reads the disc straight from `~/wiicompiled-frame/disc`, and this is
|
||||
the install that can [update itself from inside the game](#updating-from-inside-the-game). By hand, the
|
||||
steps under [Building by hand](#building-by-hand) work the same in a container started without
|
||||
`--platform linux/arm64`, with `nodtool-linux-aarch64` in place of `nodtool-linux-x86_64`.
|
||||
|
||||
**On a stronger machine** (a server, for example). The script works with Docker as well as podman,
|
||||
and runs much faster with more cores and memory. A machine that cannot reach the Frame builds
|
||||
without `--frame`; give it more compiles at once if it has the memory, `--jobs 8` for 32 GB:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/openxr-work/Launcher/steam-frame-install.sh \
|
||||
| bash -s -- --disc "/path/to/Mario Kart Wii.wbfs" --jobs 8
|
||||
```
|
||||
|
||||
Run it in `tmux` or `screen` so a closed SSH session does not stop it. Copy `~/wiicompiled-frame/out`
|
||||
and `~/wiicompiled-frame/disc` back to the PC, send `out` to the Frame with Frame Control and copy
|
||||
the disc over as in [Install on the Frame](#3-install-on-the-frame).
|
||||
|
||||
On some hosts, for example Unraid 7 with kernel 6.18, `binfmt_misc` registrations are per container.
|
||||
`tonistiigi/binfmt --install arm64` then reports success, but Debian answers `exec format error`.
|
||||
Register qemu on the host instead, with the `P` flag the tonistiigi build of qemu expects. Without it,
|
||||
every program loses its first argument, and `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 repeat this after a reboot.
|
||||
|
||||
## How it works
|
||||
|
||||
- **Same-device rendering.** SteamVR's OpenXR runtime creates the Vulkan instance and device that
|
||||
Dawn (Aurora's WebGPU layer) renders with, through Aurora's patches to Dawn
|
||||
(`aurora-main/patches/dawn`). Each eye is copied into SteamVR's swapchain on Dawn's own queue, so
|
||||
nothing is shared between GPU devices. This is the PC's Vulkan backend
|
||||
(`runtime/src/vr/openxr_vulkan_win32.cpp`), compiled for Linux. Dawn links statically, so the
|
||||
patched Dawn is built once (`Launcher/build-dawn-linux.sh`). Its `aurora-dawn.json` declares the
|
||||
Vulkan hook and density map ABIs that Aurora compiles against.
|
||||
- **Headset only.** The game neither shows nor finishes rendering a desktop window, and is compiled
|
||||
for the Frame's Cortex-X4 cores (`-mcpu=cortex-x4`, overridable with `--cpu`).
|
||||
- **Tuned for ARM64.** The Frame's kernel uses 4 KiB memory pages, so the `steam_frame` build drops
|
||||
the page-size check from every translated memory access; it checks the page size once at startup
|
||||
and refuses to start if it is ever different. Audio mixing uses NEON on ARM64.
|
||||
- **Every refresh from the game.** The game draws 60 frames a second. When it handed SteamVR only
|
||||
those, SteamVR ran it at half rate and made up every other refresh itself, even with Motion
|
||||
Smoothing off. With `[vr] repeat_frames`, the pacing thread waits for the next frame until 1.5 ms
|
||||
before SteamVR's next wake, then resubmits the last frame at the pose it was rendered for.
|
||||
SteamVR turns that to the current head pose.
|
||||
- **Eye-tracked foveation.** `XR_EXT_eye_gaze_interaction` gives the gaze, which
|
||||
`runtime/include/vr/eye_gaze.h` turns into each eye's view. Aurora picks a fragment density map
|
||||
centred on the gaze, snapped to cells of about 3 degrees (`aurora-main/lib/gfx/foveation.hpp`),
|
||||
and keeps up to 128 per eye. Each map has a memory block of its own: Turnip reads a map through a
|
||||
host mapping, and Dawn's buffer uploads unmap the shared blocks they sub-allocate from. That
|
||||
crashed the game when a race restarted, until each map got its own block.
|
||||
- **120 Hz.** `[vr] refresh_rate` (default 120 on the Frame, `0` keeps the headset's own) is asked of
|
||||
SteamVR through `XR_FB_display_refresh_rate`. SteamVR only offers the rate set in its own settings.
|
||||
|
||||
### The Android flavour
|
||||
|
||||
The Quest app also has a `steamFrame` flavour (`android/Build-Quest.ps1 -Headset frame`), for the
|
||||
Frame's Android layer, Lepton. It builds and starts in VR there, but cannot show a picture. The
|
||||
Android backend hands each eye from Dawn's device to its own OpenXR device through
|
||||
`VK_ANDROID_external_memory_android_hardware_buffer` and sync fds, and Lepton's Turnip has neither,
|
||||
nor `VK_KHR_external_memory_fd`. The native build needs no sharing, which is why it is the one to
|
||||
play. [`docs/quest-port.md`](docs/quest-port.md) covers the Android 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`;
|
||||
- the archive [`frame-diagnostics.sh`](#troubleshooting) makes, or at least the run's `console.log`
|
||||
(and `crash_sigsegv.txt` after a crash) from `~/.local/share/WiiCompiled/Logs/` on the Frame;
|
||||
- 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:
|
||||
its README covers it in full. On top of that it carries fixes taken from
|
||||
[WiiCompiled](https://github.com/patchzyy/Wiicompiled) itself and from other WiiCompiled forks, such
|
||||
as Linux TLS for online play, faster disc reads, safer settings saves and NEON audio mixing; each is
|
||||
listed with its author in [`CREDITS.md`](CREDITS.md). 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
|
||||
@@ -193,13 +466,20 @@ sharing.
|
||||
|
||||
**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.
|
||||
it needs an OpenXR runtime with `XR_KHR_vulkan_enable2`. Untested. The generic build also works on
|
||||
kernels with memory pages larger than 4 KiB, which the `steam_frame` build refuses.
|
||||
|
||||
## AI usage
|
||||
AI coding tools were used during development of this project.
|
||||
All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ).
|
||||
|
||||
## Credits
|
||||
|
||||
This fork stands on other people's work: WiiCompiled by patchzyy, WiiCompiled OpenXR VR by iChris4,
|
||||
and fixes ported from heurazy's Wiicompiled_VR-PLUS, Chris Sotraidis's KartPad, DarthMDev,
|
||||
BlackAndBlue95's Strikers-WiiCompiled, rooklz and nx-mod's wiicompiled-nx. [`CREDITS.md`](CREDITS.md)
|
||||
says what came from whom. In short:
|
||||
|
||||
- **inkwreck** - making the logo
|
||||
- **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this
|
||||
project's whole graphics layer sits on. MIT licensed.
|
||||
@@ -220,8 +500,8 @@ All translated output is verified against real hardware behavior and most import
|
||||
- **[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.
|
||||
- **[DolphinXR](https://github.com/iChris4/dolphinXR)** - the OpenXR Wii Remote input design, and
|
||||
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.
|
||||
|
||||
+18
-3
@@ -88,6 +88,19 @@ wheel geometry and guest reads in `runtime/src/vr/mkw_vr_first_person.cpp` and
|
||||
(`runtime/include/physical_wheel.h`, `runtime/src/physical_wheel.cpp` and their test). The files
|
||||
carry that attribution in their headers.
|
||||
|
||||
### Mozilla CA certificate bundle - MPL-2.0
|
||||
|
||||
The root certificates Mbed TLS verifies servers against on Linux, macOS and Android
|
||||
(`runtime/assets/certs/cacert.pem`), copied next to the built game. Extracted from Mozilla's
|
||||
`certdata.txt` and redistributed unmodified as curl's CA bundle.
|
||||
Source: <https://curl.se/docs/caextract.html>
|
||||
|
||||
### Code ported from other WiiCompiled forks - GPL-3.0-or-later
|
||||
|
||||
Fixes taken from other GPL-3.0 forks of WiiCompiled (heurazy's Wiicompiled_VR-PLUS, KartPad,
|
||||
DarthMDev, Strikers-WiiCompiled, rooklz and wiicompiled-nx) are listed with their authors and
|
||||
source commits in [`CREDITS.md`](CREDITS.md).
|
||||
|
||||
### pugixml - MIT
|
||||
|
||||
Copyright (c) 2006-2025 Arseny Kapoulkine.
|
||||
@@ -131,14 +144,16 @@ Source: <https://github.com/higan-emu/libco>. Full license text:
|
||||
|
||||
## Fetched at build time and redistributed in release builds
|
||||
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
|
||||
downloads them, and release installers carry the resulting binaries. Their license texts are
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt`,
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`, and (for Mbed TLS) `runtime/CMakeLists.txt`. They are
|
||||
not stored in this repository; the build downloads them - each fetch is pinned to an exact version
|
||||
with a checked SHA-256 - and links or redistributes the resulting binaries. Their license texts are
|
||||
included in the installer's `licenses/` folder. The Windows installer bundles the pinned source
|
||||
trees themselves (fetched by `Launcher/Prepare-Dependencies.ps1`) so end-user builds run offline.
|
||||
|
||||
| Component | Version | License | Upstream |
|
||||
| --- | --- | --- | --- |
|
||||
| Mbed TLS | 3.6.7 | Apache-2.0 / GPL-2.0-or-later | <https://github.com/Mbed-TLS/mbedtls> |
|
||||
| Dawn (WebGPU) | `v20260603.191052` prebuilt | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
|
||||
| Tint (part of Dawn) | with Dawn | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
|
||||
| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | <https://github.com/microsoft/DirectXShaderCompiler> |
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Standalone Android/OpenXR build of the Mario Kart Wii recompilation for Quest 1,
|
||||
Quest 2, Quest 3, Quest 3S and Quest Pro, and, as the `steamFrame` flavour, for Valve's Steam
|
||||
Frame under Lepton ([docs/steam-frame.md](../docs/steam-frame.md)). The full design, build
|
||||
Frame under Lepton (the [README](../README.md#the-android-flavour); the Frame is played with the native SteamOS build). The full design, build
|
||||
walkthrough and current status live in [docs/quest-port.md](../docs/quest-port.md); this directory only
|
||||
holds the Gradle project, its helper scripts, the game kit tooling
|
||||
(`QuestGameKit.psm1`, `Build-QuestGame.ps1`), the on-headset build toolchain
|
||||
|
||||
@@ -28,6 +28,7 @@ val prepareRuntimeResources by tasks.registering(Copy::class) {
|
||||
from(File(assets, "wii")) { into("runtime_resources/wii_bootstrap") }
|
||||
from(File(assets, "dsp/dsp_coef.bin")) { into("runtime_resources") }
|
||||
from(File(assets, "pipeline/initial_pipeline_cache.db")) { into("runtime_resources") }
|
||||
from(File(assets, "certs/cacert.pem")) { into("runtime_resources") }
|
||||
into(runtimeResources)
|
||||
}
|
||||
|
||||
|
||||
@@ -295,12 +295,20 @@ typedef struct {
|
||||
bool xrHeadsetOnly;
|
||||
} AuroraConfig;
|
||||
|
||||
typedef enum {
|
||||
AURORA_INITIALIZATION_SUCCESS = 0,
|
||||
AURORA_INITIALIZATION_GRAPHICS_UNAVAILABLE = 1,
|
||||
} AuroraInitializationStatus;
|
||||
|
||||
typedef struct {
|
||||
AuroraBackend backend;
|
||||
const char* userPath;
|
||||
const char* cachePath;
|
||||
SDL_Window* window;
|
||||
AuroraWindowSize windowSize;
|
||||
AuroraInitializationStatus initializationStatus;
|
||||
// On failure, owned by SDL on the calling thread. Copy before another SDL call.
|
||||
const char* initializationError;
|
||||
} AuroraInfo;
|
||||
|
||||
AuroraInfo aurora_initialize(int argc, char* argv[], const AuroraConfig* config);
|
||||
|
||||
@@ -1229,15 +1229,23 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
|
||||
const AuroraBackend requestedBackend = config.desiredBackend;
|
||||
AuroraBackend selectedBackend = requestedBackend;
|
||||
bool windowCreated = false;
|
||||
std::string firstGraphicsError;
|
||||
const auto rememberGraphicsError = [&] {
|
||||
if (firstGraphicsError.empty() && SDL_GetError()[0] != '\0') {
|
||||
firstGraphicsError = SDL_GetError();
|
||||
}
|
||||
};
|
||||
if (selectedBackend != BACKEND_AUTO) {
|
||||
Log.info("Requested graphics backend: {}", backend_name(selectedBackend));
|
||||
if (window::create_window(selectedBackend)) {
|
||||
if (webgpu::initialize(selectedBackend)) {
|
||||
windowCreated = true;
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
window::destroy_window();
|
||||
}
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
Log.error("Failed to create a window for backend {}: {}", backend_name(selectedBackend), SDL_GetError());
|
||||
}
|
||||
if (!windowCreated) {
|
||||
@@ -1254,18 +1262,28 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
|
||||
for (const auto backendType : PreferredBackendOrder) {
|
||||
selectedBackend = backendType;
|
||||
if (!window::create_window(selectedBackend)) {
|
||||
rememberGraphicsError();
|
||||
continue;
|
||||
}
|
||||
if (webgpu::initialize(selectedBackend)) {
|
||||
windowCreated = true;
|
||||
break;
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
window::destroy_window();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT(windowCreated, "Error creating window: {}", SDL_GetError());
|
||||
if (!windowCreated) {
|
||||
if (firstGraphicsError.empty()) firstGraphicsError = "No supported graphics backend is available";
|
||||
SDL_SetError("%s", firstGraphicsError.c_str());
|
||||
Log.error("Graphics initialization failed: {}", firstGraphicsError);
|
||||
return {
|
||||
.initializationStatus = AURORA_INITIALIZATION_GRAPHICS_UNAVAILABLE,
|
||||
.initializationError = SDL_GetError(),
|
||||
};
|
||||
}
|
||||
if (requestedBackend != BACKEND_AUTO && selectedBackend != requestedBackend) {
|
||||
Log.error(
|
||||
"Graphics backend fallback in effect: video.graphics_api requested {}, "
|
||||
|
||||
@@ -399,19 +399,20 @@ const char* PADGetNameForControllerIndex(const u32 idx) {
|
||||
}
|
||||
|
||||
void PADSetPortForIndex(const u32 idx, const u32 port) {
|
||||
if (port >= PAD_MAX_CONTROLLERS) return;
|
||||
const auto* ctrl = __PADGetControllerForIndex(idx);
|
||||
if (ctrl == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int32_t oldPort = SDL_GetGamepadPlayerIndex(ctrl->m_controller);
|
||||
const int32_t oldPort = aurora::input::player_index(ctrl->m_index);
|
||||
if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) {
|
||||
SDL_SetGamepadPlayerIndex(dest->m_controller, -1);
|
||||
aurora::input::set_player_index(dest->m_index, -1);
|
||||
}
|
||||
if (oldPort >= 0 && oldPort != port) {
|
||||
aurora::input::persist_controller_for_player(oldPort, nullptr);
|
||||
}
|
||||
SDL_SetGamepadPlayerIndex(ctrl->m_controller, static_cast<Sint32>(port));
|
||||
aurora::input::set_player_index(ctrl->m_index, static_cast<Sint32>(port));
|
||||
aurora::input::persist_controller_for_player(port, ctrl);
|
||||
}
|
||||
|
||||
@@ -437,7 +438,7 @@ void PADClearPort(const u32 port) {
|
||||
if (ctrl == nullptr) {
|
||||
return;
|
||||
}
|
||||
SDL_SetGamepadPlayerIndex(ctrl->m_controller, -1);
|
||||
aurora::input::set_player_index(ctrl->m_index, -1);
|
||||
}
|
||||
|
||||
// Secondary bindings live only in memory; the runtime re-applies them from its
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
|
||||
namespace aurora::vi {
|
||||
std::optional<GXRenderModeObj> g_renderMode;
|
||||
namespace {
|
||||
std::atomic<float> g_presentAspectCorrection{1.f};
|
||||
std::mutex g_renderModeMutex;
|
||||
|
||||
float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept {
|
||||
if (rm.viWidth == 0 || rm.viHeight == 0) {
|
||||
@@ -29,9 +31,8 @@ float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept {
|
||||
const float verticalFill = static_cast<float>(rm.viHeight) / nominalActiveHeight;
|
||||
return horizontalFill / verticalFill;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Vec2<uint32_t> render_mode_size() noexcept {
|
||||
Vec2<uint32_t> render_mode_size_locked() noexcept {
|
||||
if (!g_renderMode) {
|
||||
return {640, 528};
|
||||
}
|
||||
@@ -40,18 +41,31 @@ Vec2<uint32_t> render_mode_size() noexcept {
|
||||
return {std::max<uint32_t>(g_renderMode->fbWidth, 640), std::max<uint32_t>(g_renderMode->efbHeight, 528)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Vec2<uint32_t> render_mode_size() noexcept {
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
return render_mode_size_locked();
|
||||
}
|
||||
|
||||
void configure(const GXRenderModeObj* rm) noexcept {
|
||||
const auto oldSize = render_mode_size();
|
||||
if (rm == nullptr) {
|
||||
g_renderMode.reset();
|
||||
} else {
|
||||
g_renderMode = *rm;
|
||||
g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release);
|
||||
bool sizeChanged = false;
|
||||
{
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
const auto oldSize = render_mode_size_locked();
|
||||
if (rm == nullptr) {
|
||||
g_renderMode.reset();
|
||||
} else {
|
||||
g_renderMode = *rm;
|
||||
g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release);
|
||||
}
|
||||
if (rm == nullptr) {
|
||||
g_presentAspectCorrection.store(1.f, std::memory_order_release);
|
||||
}
|
||||
sizeChanged = render_mode_size_locked() != oldSize;
|
||||
}
|
||||
if (rm == nullptr) {
|
||||
g_presentAspectCorrection.store(1.f, std::memory_order_release);
|
||||
}
|
||||
if (render_mode_size() != oldSize) {
|
||||
// Never hold the mode lock across a resize request or a renderer callback.
|
||||
if (sizeChanged) {
|
||||
window::request_frame_buffer_resize();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +75,7 @@ Vec2<uint32_t> configured_fb_size() noexcept {
|
||||
}
|
||||
|
||||
Vec2<uint32_t> visible_fb_size() noexcept {
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
if (!g_renderMode) {
|
||||
return {640, 528};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "common.hpp"
|
||||
#include "staging_map.hpp"
|
||||
#include "../gx/shader_info.hpp"
|
||||
|
||||
#include "clear.hpp"
|
||||
@@ -139,12 +140,7 @@ wgpu::Buffer g_storageBuffer;
|
||||
constexpr size_t FrameSlotCount = 3;
|
||||
static std::array<wgpu::Buffer, FrameSlotCount> g_stagingBuffers;
|
||||
static size_t currentStagingBuffer = 0;
|
||||
enum class BufferMapState {
|
||||
Unmapped,
|
||||
Mapping,
|
||||
Mapped,
|
||||
};
|
||||
static std::atomic s_mappingState{BufferMapState::Unmapped};
|
||||
static StagingMapState s_mappingState;
|
||||
static wgpu::Limits g_cachedLimits;
|
||||
// Advanced once per logical frame in the seal prologue, under the renderer GPU mutex and with the
|
||||
// producer blocked, so every later reader sees a value that no longer moves.
|
||||
@@ -405,7 +401,7 @@ static size_t g_recordingSnapshotSlot = 0;
|
||||
static TextureHandle new_resolve_source_snapshot(wgpu::Extent3D size, wgpu::TextureFormat format) noexcept {
|
||||
const wgpu::TextureDescriptor textureDescriptor{
|
||||
.label = "GX Copy Source Snapshot",
|
||||
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst,
|
||||
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst,
|
||||
.dimension = wgpu::TextureDimension::e2D,
|
||||
.size = size,
|
||||
.format = format,
|
||||
@@ -1048,7 +1044,7 @@ void initialize() {
|
||||
label.c_str());
|
||||
}
|
||||
currentStagingBuffer = 0;
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
map_staging_buffer();
|
||||
|
||||
{
|
||||
@@ -1160,6 +1156,8 @@ void shutdown() {
|
||||
g_uniformBuffer = {};
|
||||
g_indexBuffer = {};
|
||||
g_storageBuffer = {};
|
||||
// Invalidate outstanding callbacks before releasing their buffers.
|
||||
s_mappingState.reset();
|
||||
g_stagingBuffers.fill({});
|
||||
for (auto& pool : g_resolveSourceSnapshotPools) {
|
||||
pool.entry.reset();
|
||||
@@ -1178,27 +1176,25 @@ void shutdown() {
|
||||
g_inOffscreen = false;
|
||||
g_frameIndex = UINT32_MAX;
|
||||
currentStagingBuffer = 0;
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
}
|
||||
|
||||
void map_staging_buffer() {
|
||||
auto expected = BufferMapState::Unmapped;
|
||||
if (!s_mappingState.compare_exchange_strong(expected, BufferMapState::Mapping, std::memory_order_acq_rel,
|
||||
std::memory_order_acquire)) {
|
||||
const auto generation = s_mappingState.request();
|
||||
if (generation == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_stagingBuffers[currentStagingBuffer].MapAsync(
|
||||
wgpu::MapMode::Write, 0, StagingBufferSize, wgpu::CallbackMode::AllowSpontaneous,
|
||||
[](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
[generation](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
const auto result = status == wgpu::MapAsyncStatus::Success ? BufferMapState::Mapped : BufferMapState::Unmapped;
|
||||
if (!s_mappingState.complete(generation, result)) return;
|
||||
if (status == wgpu::MapAsyncStatus::CallbackCancelled || status == wgpu::MapAsyncStatus::Aborted) {
|
||||
Log.warn("Buffer mapping {}: {}", magic_enum::enum_name(status), message);
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
ASSERT(status == wgpu::MapAsyncStatus::Success, "Buffer mapping failed: {} {}", magic_enum::enum_name(status),
|
||||
message);
|
||||
s_mappingState.store(BufferMapState::Mapped, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1208,7 +1204,7 @@ static bool begin_frame_impl(bool clearEfb) {
|
||||
ZoneScopedN("Wait for buffer map");
|
||||
map_staging_buffer();
|
||||
while (true) {
|
||||
const auto mappingState = s_mappingState.load(std::memory_order_acquire);
|
||||
const auto mappingState = s_mappingState.state();
|
||||
if (mappingState == BufferMapState::Mapped) {
|
||||
break;
|
||||
}
|
||||
@@ -1224,6 +1220,9 @@ static bool begin_frame_impl(bool clearEfb) {
|
||||
return false;
|
||||
}
|
||||
g_instance.ProcessEvents();
|
||||
webgpu::fail_if_device_lost();
|
||||
// Sleep until the map callback lands (or 1 ms passes) instead of spinning a core on ProcessEvents.
|
||||
s_mappingState.wait_for_progress();
|
||||
}
|
||||
}
|
||||
g_recordingSnapshotSlot = currentStagingBuffer;
|
||||
@@ -1296,12 +1295,12 @@ void abort_frame() noexcept {
|
||||
g_textureUploads.clear();
|
||||
g_textureUpload.release();
|
||||
}
|
||||
if (s_mappingState.load(std::memory_order_acquire) == BufferMapState::Mapped) {
|
||||
if (s_mappingState.state() == BufferMapState::Mapped) {
|
||||
// Pending interpolation tasks hold raw pointers into the mapped staging
|
||||
// range; they must be dropped before the buffer is unmapped and rotated.
|
||||
gx::drop_pending_frame_interpolation_uniforms();
|
||||
g_stagingBuffers[currentStagingBuffer].Unmap();
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size();
|
||||
map_staging_buffer();
|
||||
}
|
||||
@@ -1741,7 +1740,7 @@ static bool end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame,
|
||||
g_uniformUploadDestination = nullptr;
|
||||
}
|
||||
g_stagingBuffers[currentStagingBuffer].Unmap();
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
g_stats.drawCallCount = g_drawCallCount;
|
||||
g_stats.mergedDrawCallCount = g_mergedDrawCallCount;
|
||||
g_stats.lastVertSize = writeBuffer(g_verts, g_vertexBuffer, VertexBufferSize, "Vertex");
|
||||
|
||||
@@ -109,8 +109,9 @@ void ensure_native_texture(PendingCopy& pending, TextureHandle* cache = nullptr)
|
||||
*cache = pending.nativeTexture;
|
||||
}
|
||||
}
|
||||
// The shared blit shader clamps Y to flags.z/w; preserve the full source.
|
||||
const std::array nativeBlitUniform{
|
||||
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
|
||||
};
|
||||
pending.nativeBlitUniform = push_uniform(nativeBlitUniform);
|
||||
}
|
||||
|
||||
@@ -426,6 +426,7 @@ static PendingPipeline* touch_pending_pipeline(PipelineRef hash, bool prioritize
|
||||
|
||||
g_priorityPipelines.emplace_back(std::move(*backgroundIt));
|
||||
g_backgroundPipelines.erase(backgroundIt);
|
||||
g_pipelineCv.notify_all();
|
||||
return &g_priorityPipelines.back();
|
||||
}
|
||||
|
||||
@@ -566,7 +567,8 @@ static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& con
|
||||
}
|
||||
|
||||
if (notifyWorker) {
|
||||
g_pipelineCv.notify_one();
|
||||
// Compiler workers and renderer waiters share this condition variable.
|
||||
g_pipelineCv.notify_all();
|
||||
}
|
||||
if (notifyWaiters) {
|
||||
g_pipelineCv.notify_all();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
namespace aurora::gfx {
|
||||
|
||||
enum class BufferMapState { Unmapped, Mapping, Mapped };
|
||||
|
||||
// The renderer owns request/reset; Dawn may complete a request on another thread.
|
||||
// An old callback must never publish readiness for a different staging slot.
|
||||
class StagingMapState {
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable changed_;
|
||||
uint64_t generation_ = 0;
|
||||
BufferMapState state_ = BufferMapState::Unmapped;
|
||||
|
||||
public:
|
||||
uint64_t request() {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (state_ != BufferMapState::Unmapped) return 0;
|
||||
state_ = BufferMapState::Mapping;
|
||||
return ++generation_;
|
||||
}
|
||||
|
||||
bool complete(uint64_t generation, BufferMapState state) {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (generation != generation_ || state_ != BufferMapState::Mapping) return false;
|
||||
state_ = state;
|
||||
}
|
||||
changed_.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
++generation_;
|
||||
state_ = BufferMapState::Unmapped;
|
||||
}
|
||||
changed_.notify_all();
|
||||
}
|
||||
|
||||
BufferMapState state() const {
|
||||
std::lock_guard lock(mutex_);
|
||||
return state_;
|
||||
}
|
||||
|
||||
void wait_for_progress() {
|
||||
std::unique_lock lock(mutex_);
|
||||
// ProcessEvents is still serviced between waits for implementations that
|
||||
// need it. A spontaneous completion wakes immediately, without polling.
|
||||
changed_.wait_for(lock, std::chrono::milliseconds(1), [&] { return state_ != BufferMapState::Mapping; });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace aurora::gfx
|
||||
@@ -422,7 +422,7 @@ static wgpu::BindGroupLayout g_depthBindGroupLayout;
|
||||
static wgpu::Sampler g_nearestSampler;
|
||||
static wgpu::Sampler g_linearSampler;
|
||||
static absl::flat_hash_map<GXTexFmt, wgpu::RenderPipeline> g_pipelines;
|
||||
static wgpu::RenderPipeline g_blitPipeline;
|
||||
static absl::flat_hash_map<wgpu::TextureFormat, wgpu::RenderPipeline> g_blitPipelines;
|
||||
static wgpu::RenderPipeline g_quest1BlitPipeline;
|
||||
|
||||
static bool quest1_simple_blit() noexcept {
|
||||
@@ -549,9 +549,15 @@ void initialize() {
|
||||
};
|
||||
g_depthBindGroupLayout = g_device.CreateBindGroupLayout(&depthBindGroupLayoutDescriptor);
|
||||
|
||||
g_blitPipeline = create_pipeline(
|
||||
{GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"},
|
||||
ShaderPreamble, g_bindGroupLayout);
|
||||
// Native RAM readback uses RGBA even when the EFB/surface uses BGRA.
|
||||
// Build both variants here; frame workers only read the completed map.
|
||||
for (const auto format : {wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureFormat::BGRA8Unorm,
|
||||
webgpu::g_graphicsConfig.surfaceConfiguration.format}) {
|
||||
if (format != wgpu::TextureFormat::Undefined && !g_blitPipelines.contains(format)) {
|
||||
g_blitPipelines[format] = create_pipeline({GX_TF_RGBA8, FragPassthrough, format, "TexCopyConv Blit"},
|
||||
ShaderPreamble, g_bindGroupLayout);
|
||||
}
|
||||
}
|
||||
g_quest1BlitPipeline = create_pipeline(
|
||||
{GX_TF_RGBA8, {}, webgpu::g_graphicsConfig.surfaceConfiguration.format, "Quest 1 Simple TexCopy Blit"},
|
||||
SimpleBlitShader, g_bindGroupLayout);
|
||||
@@ -585,7 +591,7 @@ void initialize() {
|
||||
|
||||
void shutdown() {
|
||||
g_pipelines.clear();
|
||||
g_blitPipeline = {};
|
||||
g_blitPipelines.clear();
|
||||
g_quest1BlitPipeline = {};
|
||||
g_bindGroupLayout = {};
|
||||
g_depthBindGroupLayout = {};
|
||||
@@ -678,7 +684,15 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
||||
}
|
||||
|
||||
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
||||
execute(cmd, req, quest1_simple_blit() ? g_quest1BlitPipeline : g_blitPipeline);
|
||||
if (quest1_simple_blit()) {
|
||||
execute(cmd, req, g_quest1BlitPipeline);
|
||||
return;
|
||||
}
|
||||
const auto it = g_blitPipelines.find(req.dst->format);
|
||||
if (it == g_blitPipelines.end()) {
|
||||
Log.fatal("Unsupported blit destination format {}", static_cast<int>(req.dst->format));
|
||||
}
|
||||
execute(cmd, req, it->second);
|
||||
}
|
||||
|
||||
} // namespace aurora::gfx::tex_copy_conv
|
||||
@@ -33,10 +33,11 @@ using IndexBuffer = std::vector<u16>;
|
||||
static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount) {
|
||||
size_t writePos = 0;
|
||||
if (prim == GX_QUADS) {
|
||||
// Retain the existing incomplete-quad behavior: every started group emits a complete six-index quad.
|
||||
buf.resize(((static_cast<u32>(vtxCount) + 3u) / 4u) * 6u);
|
||||
// GX renders a three-vertex remainder as a triangle. One/two are ignored.
|
||||
const u32 completeVertices = static_cast<u32>(vtxCount) & ~3u;
|
||||
buf.resize((completeVertices / 4u) * 6u + (vtxCount % 4u == 3u ? 3u : 0u));
|
||||
|
||||
for (u16 v = 0; v < vtxCount; v += 4) {
|
||||
for (u32 v = 0; v < completeVertices; v += 4) {
|
||||
const u16 idx0 = v;
|
||||
const u16 idx1 = static_cast<u16>(v + 1);
|
||||
const u16 idx2 = static_cast<u16>(v + 2);
|
||||
@@ -48,15 +49,21 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
buf[writePos++] = idx3;
|
||||
buf[writePos++] = idx0;
|
||||
}
|
||||
if (vtxCount % 4u == 3u) {
|
||||
buf[writePos++] = static_cast<u16>(completeVertices);
|
||||
buf[writePos++] = static_cast<u16>(completeVertices + 1u);
|
||||
buf[writePos++] = static_cast<u16>(completeVertices + 2u);
|
||||
}
|
||||
} else if (prim == GX_TRIANGLES) {
|
||||
buf.resize(vtxCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
const u32 completeVertices = (static_cast<u32>(vtxCount) / 3u) * 3u;
|
||||
buf.resize(completeVertices);
|
||||
for (u32 v = 0; v < completeVertices; ++v) {
|
||||
buf[writePos++] = v;
|
||||
}
|
||||
} else if (prim == GX_TRIANGLEFAN) {
|
||||
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
|
||||
const u32 indexCount = vtxCount < 3 ? 0u : (static_cast<u32>(vtxCount) - 2u) * 3u;
|
||||
buf.resize(indexCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
for (u32 v = 0; indexCount != 0 && v < vtxCount; ++v) {
|
||||
if (v < 3) {
|
||||
buf[writePos++] = v;
|
||||
continue;
|
||||
@@ -66,9 +73,9 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
buf[writePos++] = v;
|
||||
}
|
||||
} else if (prim == GX_TRIANGLESTRIP) {
|
||||
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
|
||||
const u32 indexCount = vtxCount < 3 ? 0u : (static_cast<u32>(vtxCount) - 2u) * 3u;
|
||||
buf.resize(indexCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
for (u32 v = 0; indexCount != 0 && v < vtxCount; ++v) {
|
||||
if (v < 3) {
|
||||
buf[writePos++] = v;
|
||||
continue;
|
||||
@@ -91,6 +98,13 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
return static_cast<u32>(writePos);
|
||||
}
|
||||
|
||||
// Empty/incomplete draws consume FIFO bytes but cannot produce a primitive.
|
||||
static bool has_complete_primitive(GXPrimitive prim, u16 count) {
|
||||
if (prim == GX_POINTS) return count >= 1;
|
||||
if (prim == GX_LINES || prim == GX_LINESTRIP) return count >= 2;
|
||||
return count >= 3;
|
||||
}
|
||||
|
||||
// GX FIFO opcodes - use CP_ prefix to avoid clashing with GXCommandList.h macros
|
||||
static constexpr u8 CP_CMD_NOP = GX_NOP;
|
||||
static constexpr u8 CP_CMD_LOAD_CP_REG = GX_LOAD_CP_REG;
|
||||
@@ -552,6 +566,10 @@ void process(const u8* data, u32 size, bool bigEndian) {
|
||||
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
|
||||
g_gxState.arrays[i].cachedRange = {};
|
||||
}
|
||||
// A merged draw retains its previous array uploads. Force a new draw so
|
||||
// handle_draw_unmerged observes the invalidation and uploads fresh data.
|
||||
// Pipeline configuration itself did not change.
|
||||
g_gxState.stateDirty = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1926,6 +1944,10 @@ static u32 calculate_last_vtx_size(GXVtxFmt fmt) {
|
||||
|
||||
g_gxState.lastVtxFmt = fmt;
|
||||
g_gxState.lastVtxSize = vtxSize;
|
||||
// The format is selected by the draw opcode, without a register write.
|
||||
// Even equal-stride formats may decode bytes differently, so do not merge
|
||||
// into a draw using the previous format's shader and uniform layout.
|
||||
g_gxState.stateDirty = true;
|
||||
|
||||
return vtxSize;
|
||||
}
|
||||
@@ -2366,6 +2388,8 @@ bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, ui
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!has_complete_primitive(prim, vtxCount)) return true;
|
||||
|
||||
// This entry point bypasses process(), so it owns the renderer lock itself.
|
||||
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
|
||||
if (model_array_hidden()) return true;
|
||||
@@ -2414,7 +2438,7 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
|
||||
return false;
|
||||
}
|
||||
|
||||
if (model_array_hidden()) {
|
||||
if (!has_complete_primitive(prim, vtxCount) || model_array_hidden()) {
|
||||
pos += totalVtxBytes;
|
||||
return true;
|
||||
}
|
||||
@@ -2436,9 +2460,12 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
|
||||
// Only if the previous draw call was a single instance draw (no lines/points handling), and only into a draw
|
||||
// that resolved the same animated array: the merged whole renders through that draw's binding. Anything the
|
||||
// decision cache cannot vouch for (a command it was not recorded against) stays unmerged.
|
||||
// Expanded lines/points have different vertex interpretation even with one instance.
|
||||
// Triangle-list output has no restart index; index 65535 is usable.
|
||||
// Overflow would address earlier vertices instead of the appended geometry.
|
||||
if (lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS &&
|
||||
!lastDraw->uniformReplayLayout.vertexMotion.enabled &&
|
||||
lastDraw->instanceCount == 1 &&
|
||||
!lastDraw->uniformReplayLayout.vertexMotion.enabled && !lastDraw->expandedPrimitive &&
|
||||
lastDraw->instanceCount == 1 && uint64_t(lastDraw->vtxCount) + vtxCount <= 65536u &&
|
||||
(nativeWheelArrays.empty() ||
|
||||
(nativeWheelLastDrawCommand == lastDraw && nativeWheelLastDecision == nativeWheel)))
|
||||
LIKELY {
|
||||
@@ -2615,6 +2642,7 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g
|
||||
.vtxCount = vtxCount,
|
||||
.indexCount = numIndices,
|
||||
.instanceCount = instanceCount,
|
||||
.expandedPrimitive = prim == GX_LINES || prim == GX_LINESTRIP || prim == GX_POINTS,
|
||||
.bindGroups = bindGroups,
|
||||
.dstAlpha = pipelineState.dstAlpha,
|
||||
.screenRect = screen_rect(prim, fmt, vertices, vtxCount, vtxStride),
|
||||
|
||||
@@ -1910,7 +1910,8 @@ void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXV
|
||||
config.pixelFmt = g_gxState.pixelFmt;
|
||||
config.dstAlpha = effective_dst_alpha(g_gxState.pixelFmt, g_gxState.alphaUpdate, g_gxState.dstAlpha);
|
||||
config.depthCompare = g_gxState.depthCompare;
|
||||
config.depthUpdate = g_gxState.depthUpdate;
|
||||
// As on the hardware (and in Dolphin): with the Z compare off, the Z buffer isn't updated either.
|
||||
config.depthUpdate = g_gxState.depthCompare && g_gxState.depthUpdate;
|
||||
config.alphaUpdate = effective_alpha_update(g_gxState.pixelFmt, g_gxState.alphaUpdate);
|
||||
config.colorUpdate = g_gxState.colorUpdate;
|
||||
}
|
||||
|
||||
@@ -439,6 +439,8 @@ struct GXState {
|
||||
u32 pipelineStateGeneration = next_gx_state_epoch();
|
||||
std::array<u32, 0x100> bpRegCache = [] {
|
||||
std::array<u32, 0x100> regs{};
|
||||
// Force the first GEN_MODE decode without changing its masked reset value.
|
||||
regs[0x00] = 0xFF000000;
|
||||
regs[0xFE] = 0x00FFFFFF;
|
||||
return regs;
|
||||
}();
|
||||
|
||||
@@ -29,6 +29,7 @@ struct DrawData {
|
||||
uint32_t vtxCount;
|
||||
uint32_t indexCount;
|
||||
uint32_t instanceCount;
|
||||
bool expandedPrimitive;
|
||||
GXBindGroups bindGroups;
|
||||
uint32_t dstAlpha;
|
||||
// Valid only for simple orthographic rectangles/lines (textured or not).
|
||||
|
||||
@@ -559,6 +559,12 @@ static AlphaCompareExpr alpha_compare(GXCompare comp, u8 ref) {
|
||||
static inline std::string vtx_attr(const ShaderConfig& config, GXAttr attr) {
|
||||
const auto type = config.attrs[attr].attrType;
|
||||
if (type == GX_NONE) {
|
||||
if (attr == GX_VA_POS) {
|
||||
// Some scene transitions submit a vertex stream with no position attribute.
|
||||
// The resulting primitive has no useful geometry, but shader generation
|
||||
// must not abort the entire game while the next scene is loading.
|
||||
return "vec3f(0.0)"s;
|
||||
}
|
||||
if (attr == GX_VA_PNMTXIDX) {
|
||||
return "ubuf.current_pnmtx";
|
||||
}
|
||||
@@ -1733,8 +1739,22 @@ fn load_u16(p: ptr<storage, array<u32>>, byte_off: u32, le: bool) -> u32 {{
|
||||
return bswap16(raw, le);
|
||||
}}
|
||||
|
||||
fn load_u24_raw(p: ptr<storage, array<u32>>, byte_off: u32) -> u32 {{
|
||||
let word_idx = byte_off >> 2u;
|
||||
let sub = byte_off & 3u;
|
||||
let word = p[word_idx];
|
||||
// Three bytes at offsets zero or one fit entirely in this word. Do not
|
||||
// access the next word: this attribute may end at the binding boundary.
|
||||
if (sub <= 1u) {{
|
||||
return (word >> (sub * 8u)) & 0x00FFFFFFu;
|
||||
}}
|
||||
let next = p[word_idx + 1u];
|
||||
let shift = sub * 8u;
|
||||
return ((word >> shift) | (next << (32u - shift))) & 0x00FFFFFFu;
|
||||
}}
|
||||
|
||||
fn load_u24(p: ptr<storage, array<u32>>, byte_off: u32, le: bool) -> u32 {{
|
||||
let raw = load_u32_raw(p, byte_off) & 0x00FFFFFFu;
|
||||
let raw = load_u24_raw(p, byte_off);
|
||||
if (le) {{
|
||||
return raw;
|
||||
}}
|
||||
@@ -1774,7 +1794,7 @@ fn raw_fetch_u8_2(p: ptr<storage, array<u32>>, byte_off: u32) -> vec2u {{
|
||||
}}
|
||||
|
||||
fn raw_fetch_u8_3(p: ptr<storage, array<u32>>, byte_off: u32) -> vec3u {{
|
||||
let raw = load_u32_raw(p, byte_off);
|
||||
let raw = load_u24_raw(p, byte_off);
|
||||
return vec3u(
|
||||
extractBits(raw, 0u, 8u),
|
||||
extractBits(raw, 8u, 8u),
|
||||
|
||||
@@ -85,18 +85,30 @@ void initialize() noexcept {
|
||||
|
||||
void shutdown() noexcept {
|
||||
ZoneScoped;
|
||||
if (g_useSdlRenderer) {
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
} else {
|
||||
ImGui_ImplWGPU_Shutdown();
|
||||
// Startup can fail before either backend initializes. A context alone does
|
||||
// not mean its renderer/platform backend owns resources to release.
|
||||
if (ImGui::GetCurrentContext() != nullptr) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.BackendRendererUserData != nullptr) {
|
||||
if (g_useSdlRenderer) {
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
} else {
|
||||
ImGui_ImplWGPU_Shutdown();
|
||||
}
|
||||
}
|
||||
if (io.BackendPlatformUserData != nullptr) {
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
}
|
||||
ImGui::DestroyContext();
|
||||
}
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
for (const auto& texture : g_sdlTextures) {
|
||||
SDL_DestroyTexture(texture);
|
||||
}
|
||||
g_sdlTextures.clear();
|
||||
g_wgpuTextures.clear();
|
||||
g_useSdlRenderer = false;
|
||||
g_scale = 0.f;
|
||||
g_frameDataBuilt = true;
|
||||
}
|
||||
|
||||
void process_event(const SDL_Event& event) noexcept {
|
||||
@@ -209,9 +221,13 @@ void render(const wgpu::RenderPassEncoder& pass) noexcept {
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(data, renderer);
|
||||
SDL_RenderPresent(renderer);
|
||||
} else {
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PushDebugGroup("Aurora: Dear Imgui");
|
||||
#endif
|
||||
ImGui_ImplWGPU_RenderDrawData(data, pass.Get());
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PopDebugGroup();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,9 +291,13 @@ void render(const wgpu::RenderPassEncoder& pass, const ImDrawData* data) noexcep
|
||||
if (g_useSdlRenderer || data == nullptr) {
|
||||
return;
|
||||
}
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PushDebugGroup("Aurora: Dear Imgui");
|
||||
#endif
|
||||
ImGui_ImplWGPU_RenderDrawData(const_cast<ImDrawData*>(data), pass.Get());
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PopDebugGroup();
|
||||
#endif
|
||||
}
|
||||
|
||||
StereoOverlay latch_stereo_overlay() noexcept {
|
||||
@@ -291,9 +311,13 @@ bool render_draw_data(const wgpu::RenderPassEncoder& pass, ImDrawData* data) noe
|
||||
if (g_useSdlRenderer || data == nullptr || ImGui::GetCurrentContext() == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PushDebugGroup("Aurora: Dear Imgui headset panel");
|
||||
#endif
|
||||
ImGui_ImplWGPU_RenderDrawData(data, pass.Get());
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
pass.PopDebugGroup();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -414,6 +414,29 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
g_GameControllers[instance] = controller;
|
||||
ensure_player_index(g_GameControllers[instance]);
|
||||
apply_port_preferences();
|
||||
#if defined(SDL_PLATFORM_MACOS)
|
||||
// First-use convenience only: never override a saved assignment or None.
|
||||
if (g_portPreferences[0].state == PortPreferenceState::Unset) {
|
||||
bool hasOtherPortPreference = false;
|
||||
for (size_t port = 1; port < g_portPreferences.size(); ++port) {
|
||||
if (g_portPreferences[port].state == PortPreferenceState::Controller &&
|
||||
identity_match(g_portPreferences[port].identity, controller_identity(g_GameControllers[instance])) !=
|
||||
IdentityMatch::None) {
|
||||
hasOtherPortPreference = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasOtherPortPreference) {
|
||||
const auto* p0 = get_controller_for_player(0);
|
||||
if (p0 == nullptr) {
|
||||
assign_player_index(g_GameControllers[instance], 0);
|
||||
persist_controller_for_player(0, &g_GameControllers[instance]);
|
||||
} else if (p0 == &g_GameControllers[instance]) {
|
||||
persist_controller_for_player(0, &g_GameControllers[instance]);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -627,12 +627,16 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
g_adapter = std::move(adapter);
|
||||
} else {
|
||||
Log.warn("Adapter request failed: {}", message);
|
||||
const std::string_view reason{message};
|
||||
SDL_SetError("Graphics adapter unavailable: %.*s",
|
||||
static_cast<int>(std::min<size_t>(reason.size(), 512)), reason.data());
|
||||
}
|
||||
});
|
||||
const auto status = g_instance.WaitAny(future, 5000000000);
|
||||
if (status != wgpu::WaitStatus::Success) {
|
||||
Log.error("Failed to create {} adapter: {}", magic_enum::enum_name(backend),
|
||||
magic_enum::enum_name(status));
|
||||
SDL_SetError("Graphics adapter request did not complete within its startup deadline");
|
||||
return false;
|
||||
}
|
||||
if (!g_adapter) {
|
||||
@@ -845,11 +849,16 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
g_device = std::move(device);
|
||||
} else {
|
||||
Log.warn("Device request failed: {}", message);
|
||||
const std::string_view reason{message};
|
||||
SDL_SetError("Graphics device unavailable: %.*s",
|
||||
static_cast<int>(std::min<size_t>(reason.size(), 512)),
|
||||
reason.data());
|
||||
}
|
||||
});
|
||||
const auto status = g_instance.WaitAny(future, 5000000000);
|
||||
if (status != wgpu::WaitStatus::Success) {
|
||||
Log.error("Failed to create device: {}", magic_enum::enum_name(status));
|
||||
SDL_SetError("Graphics device request did not complete within its startup deadline");
|
||||
return false;
|
||||
}
|
||||
if (!g_device) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
@@ -313,8 +314,33 @@ static size_t load_from_database(const XXH128_hash_t& keyHash, void* value, size
|
||||
if (ret == SQLITE_ROW) {
|
||||
// Hit
|
||||
const auto foundPtr = sqlite3_column_blob(load_stmt, 0);
|
||||
foundSize = sqlite3_column_int64(load_stmt, 1);
|
||||
const bool compressed = sqlite3_column_int(load_stmt, 2) != 0;
|
||||
const auto declaredSize = sqlite3_column_int64(load_stmt, 1);
|
||||
const auto storedSize = sqlite3_column_bytes(load_stmt, 0);
|
||||
const auto compression = sqlite3_column_int(load_stmt, 2);
|
||||
const bool compressed = compression == 1;
|
||||
// Dawn asks for the size before allocating its destination. Validate here,
|
||||
// not only during the copy: corrupt metadata must become a cache miss.
|
||||
bool valid = declaredSize > 0 &&
|
||||
static_cast<uint64_t>(declaredSize) <= std::numeric_limits<size_t>::max() &&
|
||||
foundPtr != nullptr && storedSize > 0 && (compression == 0 || compression == 1);
|
||||
if (valid && compressed) {
|
||||
#if defined(AURORA_CACHE_USE_ZSTD)
|
||||
// Our writer uses ZSTD_compress, which records the original content size.
|
||||
const auto frameSize = ZSTD_getFrameContentSize(foundPtr, static_cast<size_t>(storedSize));
|
||||
valid = frameSize != ZSTD_CONTENTSIZE_ERROR && frameSize != ZSTD_CONTENTSIZE_UNKNOWN &&
|
||||
frameSize == static_cast<uint64_t>(declaredSize);
|
||||
#else
|
||||
valid = false;
|
||||
#endif
|
||||
} else if (valid) {
|
||||
valid = declaredSize == storedSize;
|
||||
}
|
||||
if (!valid) {
|
||||
Log.error("Ignoring cache entry with inconsistent size or compression metadata");
|
||||
check(sqlite3_reset(load_stmt));
|
||||
return 0;
|
||||
}
|
||||
foundSize = static_cast<size_t>(declaredSize);
|
||||
if (value == nullptr) {
|
||||
g_hits.fetch_add(1, std::memory_order_relaxed);
|
||||
} else {
|
||||
|
||||
@@ -150,8 +150,11 @@ public:
|
||||
for (uint32_t i = 0; i < images; ++i) {
|
||||
const auto& eye = sources[i];
|
||||
const auto& target = targets[i];
|
||||
if (!eye.texture || eye.size.width != target.width || eye.size.height != target.height ||
|
||||
!CopyCompatible(VkFormat(eye.format), target.dxgiFormat)) return false;
|
||||
// An eye smaller than its target goes in the target's top-left corner, which is all the
|
||||
// projection layer shows of it; the panel and the virtual screen are always full size.
|
||||
const bool fits = i < count ? eye.size.width <= target.width && eye.size.height <= target.height
|
||||
: eye.size.width == target.width && eye.size.height == target.height;
|
||||
if (!eye.texture || !fits || !CopyCompatible(VkFormat(eye.format), target.dxgiFormat)) return false;
|
||||
const uint64_t image = reinterpret_cast<uintptr_t>(target.resource);
|
||||
auto it = std::find_if(imports.begin(), imports.end(), [&](const Import& entry) { return entry.image == image; });
|
||||
if (it == imports.end()) {
|
||||
@@ -179,7 +182,7 @@ public:
|
||||
wgpu::TexelCopyTextureInfo source, destination;
|
||||
source.texture = *sources[i].texture;
|
||||
destination.texture = active[i];
|
||||
wgpu::Extent3D size{targets[i].width, targets[i].height, 1};
|
||||
wgpu::Extent3D size{sources[i].size.width, sources[i].size.height, 1};
|
||||
encoder.CopyTextureToTexture(&source, &destination, &size);
|
||||
}
|
||||
encoded = true;
|
||||
|
||||
@@ -64,6 +64,7 @@ if (AURORA_ENABLE_GX)
|
||||
add_executable(gx_fifo_tests
|
||||
gx_fifo_test.cpp
|
||||
gx_test_stubs.cpp
|
||||
renderer_regression_test.cpp
|
||||
stereo_replay_test.cpp
|
||||
stereo_interpolation_test.cpp
|
||||
stereo_mirror_test.cpp
|
||||
|
||||
@@ -1328,12 +1328,35 @@ TEST(TevRegisterLivenessContract, PacksOneUniformWhenBothHalvesNeedInitialValue)
|
||||
auto config = baseline;
|
||||
config.tevStages[0].colorPass.a = GX_CC_C0;
|
||||
config.tevStages[0].alphaPass.a = GX_CA_A0;
|
||||
config.tevStages[0].colorPass.b = GX_CC_KONST;
|
||||
config.tevStages[0].kcSel = GX_TEV_KCSEL_K0;
|
||||
|
||||
const auto baselineInfo = aurora::gx::build_shader_info(baseline);
|
||||
const auto info = aurora::gx::build_shader_info(config);
|
||||
EXPECT_TRUE(info.loadsTevRegRgb.test(GX_TEVREG0));
|
||||
EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0));
|
||||
EXPECT_EQ(info.uniformSize, baselineInfo.uniformSize + sizeof(aurora::Vec4<float>));
|
||||
// The final allocation is alignment-rounded, so adding one register need
|
||||
// not increase it. Verify actual packing with a distinct following K color.
|
||||
const auto savedReg = g_gxState.colorRegs[GX_TEVREG0];
|
||||
const auto savedKColor = g_gxState.kcolors[GX_KCOLOR0];
|
||||
g_gxState.colorRegs[GX_TEVREG0] = {11.f, 22.f, 33.f, 44.f};
|
||||
g_gxState.kcolors[GX_KCOLOR0] = {55.f, 66.f, 77.f, 88.f};
|
||||
EXPECT_TRUE(info.sampledKColors.test(GX_KCOLOR0));
|
||||
aurora::gfx::testing::reset_uniform_allocations();
|
||||
aurora::gx::build_uniform(info, 0, {}, {}, false);
|
||||
const auto expectedReg = g_gxState.colorRegs[GX_TEVREG0];
|
||||
const auto expectedKColor = g_gxState.kcolors[GX_KCOLOR0];
|
||||
g_gxState.colorRegs[GX_TEVREG0] = savedReg;
|
||||
g_gxState.kcolors[GX_KCOLOR0] = savedKColor;
|
||||
const auto& bytes = aurora::gfx::testing::uniform_allocation(0);
|
||||
const auto* reg = reinterpret_cast<const uint8_t*>(&expectedReg);
|
||||
const auto found = std::search(bytes.begin(), bytes.end(), reg, reg + sizeof(aurora::Vec4<float>));
|
||||
ASSERT_NE(found, bytes.end());
|
||||
const size_t offset = static_cast<size_t>(found - bytes.begin());
|
||||
ASSERT_LE(offset + 2 * sizeof(aurora::Vec4<float>), bytes.size());
|
||||
EXPECT_EQ(std::memcmp(bytes.data() + offset + sizeof(aurora::Vec4<float>), &expectedKColor,
|
||||
sizeof(aurora::Vec4<float>)),
|
||||
0);
|
||||
aurora::gfx::testing::reset_uniform_allocations();
|
||||
}
|
||||
|
||||
// BP registers (direct FIFO writes, no dirty state flush needed)
|
||||
@@ -1390,6 +1413,51 @@ TEST_F(GXFifoTest, BlendMode_Logic) {
|
||||
EXPECT_EQ(g_gxState.blendOp, GX_LO_XOR);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_FirstZeroWriteDecodesAndRepeatDeduplicates) {
|
||||
reset_gx_state();
|
||||
const auto before = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
EXPECT_EQ(g_gxState.numChans, 0u);
|
||||
EXPECT_EQ(g_gxState.numTexGens, 0u);
|
||||
EXPECT_EQ(g_gxState.numIndStages, 0u);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
|
||||
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
|
||||
const auto decoded = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.pipelineStateGeneration, decoded);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_FirstMaskedWritePreservesZeroResetBits) {
|
||||
for (const u32 mask : {0u, 1u << 10}) {
|
||||
reset_gx_state();
|
||||
const auto before = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0xFE, mask));
|
||||
decode_fifo(bp_cmd(0, 0xFFFFFF));
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], mask);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0xFE], 0xFFFFFFu);
|
||||
EXPECT_EQ(g_gxState.numTevStages, mask ? 2u : 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_ColdSingleStageApiSetupDecodes) {
|
||||
reset_gx_state();
|
||||
GXSetNumTevStages(1);
|
||||
GXSetNumTexGens(0);
|
||||
GXSetNumChans(0);
|
||||
GXSetCullMode(GX_CULL_NONE);
|
||||
const auto bytes = flush_and_capture();
|
||||
decode_fifo(bytes);
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, BpMask_AppliesOnlyToNextWrite) {
|
||||
std::vector<u8> bytes;
|
||||
auto mask = bp_cmd(0xFE, 1u << 19);
|
||||
@@ -2861,6 +2929,7 @@ TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
|
||||
const auto decodeAndReadIndices = [&](GXPrimitive primitive, u16 count) {
|
||||
std::vector<u8> fifo;
|
||||
append_test_draw(fifo, primitive, count);
|
||||
aurora::gfx::testing::reset_vertex_push_record();
|
||||
decode_fifo(fifo);
|
||||
return aurora::gfx::testing::last_pushed_indices();
|
||||
};
|
||||
@@ -2871,7 +2940,7 @@ TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
|
||||
g_gxState.stateDirty = true;
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 5), (std::vector<u16>{0, 1, 2, 0, 2, 3, 0, 3, 4}));
|
||||
g_gxState.stateDirty = true;
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 2), (std::vector<u16>{0, 1}));
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 2), (std::vector<u16>{}));
|
||||
g_gxState.stateDirty = true;
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLESTRIP, 6), (std::vector<u16>{0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5}));
|
||||
g_gxState.stateDirty = true;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "gx_test_common.hpp"
|
||||
#include "gfx/staging_map.hpp"
|
||||
#include "gx/pipeline.hpp"
|
||||
|
||||
#include <thread>
|
||||
|
||||
using aurora::gx::g_gxState;
|
||||
|
||||
namespace {
|
||||
std::vector<u8> draw(GXPrimitive primitive, u16 count, GXVtxFmt format = GX_VTXFMT0) {
|
||||
std::vector<u8> bytes{static_cast<u8>(primitive | format), static_cast<u8>(count >> 8), static_cast<u8>(count)};
|
||||
bytes.resize(3 + count);
|
||||
return bytes;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(GXFifoTest, MaximumQuadCountTerminatesWithoutOutOfRangeIndices) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
for (const u16 count : {65532, 65533, 65534, 65535}) {
|
||||
g_gxState.stateDirty = true;
|
||||
decode_fifo(draw(GX_QUADS, count));
|
||||
const auto& indices = aurora::gfx::testing::last_pushed_indices();
|
||||
ASSERT_EQ(indices.size(), (count / 4) * 6 + (count % 4 == 3 ? 3 : 0));
|
||||
for (const auto index : indices)
|
||||
ASSERT_LT(index, count);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, IncompletePrimitivesNeverJoinAcrossDraws) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 4));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
decode_fifo(draw(GX_TRIANGLES, 5));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{4, 5, 6}));
|
||||
const auto before = aurora::gfx::testing::last_pushed_indices();
|
||||
decode_fifo(draw(GX_TRIANGLEFAN, 2));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), before);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, MergeStopsBeforeSixteenBitIndexOverflow) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 65535));
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, VertexCacheInvalidationBreaksDrawMerging) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
decode_fifo({GX_CMD_INVL_VC});
|
||||
EXPECT_TRUE(g_gxState.stateDirty);
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, EqualStrideVertexFormatChangeBreaksDrawMerging) {
|
||||
aurora::gfx::testing::use_real_vertex_format_helpers(true);
|
||||
g_gxState.vtxDesc[GX_VA_POS] = GX_DIRECT;
|
||||
for (const auto format : {GX_VTXFMT0, GX_VTXFMT1}) {
|
||||
g_gxState.vtxFmts[format].attrs[GX_VA_POS].cnt = GX_POS_XY;
|
||||
g_gxState.vtxFmts[format].attrs[GX_VA_POS].type = GX_U8;
|
||||
}
|
||||
g_gxState.vtxFmts[GX_VTXFMT1].attrs[GX_VA_POS].frac = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
for (const auto format : {GX_VTXFMT0, GX_VTXFMT1}) {
|
||||
auto bytes = draw(GX_TRIANGLES, 3, format);
|
||||
bytes.resize(9);
|
||||
decode_fifo(bytes);
|
||||
}
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, SingleExpandedPrimitiveCannotMergeWithTriangles) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_POINTS, 1));
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
}
|
||||
|
||||
TEST(StagingMapping, RetiredCallbacksCannotPublishAnotherBuffersReadiness) {
|
||||
using namespace aurora::gfx;
|
||||
StagingMapState state;
|
||||
const auto old = state.request();
|
||||
EXPECT_EQ(state.request(), 0u);
|
||||
state.reset();
|
||||
const auto current = state.request();
|
||||
EXPECT_FALSE(state.complete(old, BufferMapState::Mapped));
|
||||
EXPECT_FALSE(state.complete(old, BufferMapState::Unmapped));
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapping);
|
||||
EXPECT_TRUE(state.complete(current, BufferMapState::Mapped));
|
||||
EXPECT_FALSE(state.complete(current, BufferMapState::Unmapped));
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapped);
|
||||
}
|
||||
|
||||
TEST(StagingMapping, AsyncCompletionWakesWaiters) {
|
||||
using namespace aurora::gfx;
|
||||
StagingMapState state;
|
||||
const auto generation = state.request();
|
||||
std::thread callback([&] {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
state.complete(generation, BufferMapState::Mapped);
|
||||
});
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (state.state() == BufferMapState::Mapping && std::chrono::steady_clock::now() < deadline)
|
||||
state.wait_for_progress();
|
||||
callback.join();
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapped);
|
||||
}
|
||||
+2
-2
@@ -823,7 +823,7 @@ Android facts this design rests on, all measured on a Quest 3:
|
||||
application ID and storage, but select the appropriate CPU baseline (one
|
||||
`headsetCpus` map in `app/build.gradle.kts`), manifest and launcher
|
||||
behavior. `steamFrame` is Valve's Steam Frame under Lepton, SteamOS's
|
||||
Android layer; see `docs/steam-frame.md`.
|
||||
Android layer, where it cannot show a picture; see the README, The Android flavour.
|
||||
`app/src/main/cpp/CMakeLists.txt` adds the repository's `runtime/` as a
|
||||
subdirectory with those Android choices and builds both game kit probes
|
||||
(the Retro Rewind one only when the translation includes the mod), which
|
||||
@@ -852,7 +852,7 @@ powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset quest1
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Install # your game, against that kit, into Import (or WheelWizard VR's Build for Quest)
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install # the mod and its pack (needs translate-mod output with --retro-wfc-payload)
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Headset quest1 -Install # game package from the Quest 1 kit
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset frame # Steam Frame flavour (docs/steam-frame.md)
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset frame # Steam Frame flavour (README, The Android flavour)
|
||||
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
The second 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 one is about installing: one
|
||||
command now takes you from your disc to the game in your Steam library.
|
||||
|
||||
**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.
|
||||
|
||||
## Install in one command
|
||||
|
||||
Set up ARM64 emulation on an x86_64 Linux PC (the README's
|
||||
[Quick start](https://github.com/mitch030504/Wiicompiled_VR_Frame#quick-start), step 1), then:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/openxr-work/Launcher/steam-frame-install.sh \
|
||||
| bash -s -- --disc "/path/to/Mario Kart Wii.wbfs" --frame steamos@<frame-ip>
|
||||
```
|
||||
|
||||
`Launcher/steam-frame-install.sh` (also in this release's source) does the rest:
|
||||
|
||||
- downloads the newest release and builds it in an ARM64 container (podman or docker), with as many
|
||||
compiles at once as your memory allows;
|
||||
- takes your disc as an ISO, WBFS or RVZ image (WIA, CISO, GCZ and NFS too), inside a `.zip` or
|
||||
`.7z` if you like, or as an extracted folder. It checks the disc is PAL `RMCP01` before building;
|
||||
- copies the game and the disc to the Frame over SSH, points the game at the disc, and adds it to
|
||||
your Steam library (through Valve's devkit tools, which Frame Control puts on the Frame);
|
||||
- updates: run the same command again and only what changed is rebuilt and replaced.
|
||||
|
||||
With [Frame Control](https://github.com/saphid/frame-control) set up, `--frame frame` works.
|
||||
`--frame local` builds and installs on the Frame itself; leaving `--frame` out only builds.
|
||||
`--help` lists every option.
|
||||
|
||||
## Also new
|
||||
|
||||
- The README is now the one guide, opening with the Quick start; the manual steps remain under
|
||||
"Building by hand". `docs/steam-frame.md` is gone (it stays readable at the `frame-beta-1` tag).
|
||||
|
||||
## Updating from frame-beta-1
|
||||
|
||||
Nothing in the game or in Dawn changed, so a working frame-beta-1 install needs no rebuild. To move
|
||||
to the installer anyway, run the command above: it keeps its own work folder
|
||||
(`~/wiicompiled-frame`) and builds from scratch there the first time, then installs over your
|
||||
existing `~/devkit-game/WiiCompiled/`. A disc already at `~/wiicompiled/disc` on the Frame and an
|
||||
existing `dvd_root` setting are left as they are.
|
||||
|
||||
## Known issues
|
||||
|
||||
Unchanged from frame-beta-1:
|
||||
|
||||
- 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.
|
||||
|
||||
The installer is new and was tested with synthetic discs and stand-ins for the container and the
|
||||
Frame; reports of how it does on real machines are welcome.
|
||||
|
||||
## 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/` (and, for the installer,
|
||||
`~/wiicompiled-frame/build.log`) are welcome as issues.
|
||||
@@ -0,0 +1,81 @@
|
||||
The third 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 one brings in fixes from upstream
|
||||
WiiCompiled and from other WiiCompiled forks, Retro Rewind in the installer, and a diagnostics
|
||||
script. Thanks to everyone whose work is in it; [`CREDITS.md`](https://github.com/mitch030504/Wiicompiled_VR_Frame/blob/openxr-work/CREDITS.md) lists who wrote
|
||||
what.
|
||||
|
||||
**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.
|
||||
|
||||
## New
|
||||
|
||||
- **Retro Rewind in the installer.** `steam-frame-install.sh --retro-rewind` downloads the Retro
|
||||
Rewind pack from its own update server, builds Retro Rewind beside the base game and adds it to
|
||||
your Steam library. `--retro-rewind-pack DIR` uses a pack you already have. Online play works on
|
||||
Linux now that TLS does (below). From heurazy's Wiicompiled_VR-PLUS.
|
||||
- **Adaptive resolution** (`[vr] adaptive_resolution`, off by default): lowers the race's eye
|
||||
resolution down to 70% while frames fall behind, and raises it again once they keep up.
|
||||
Experimental and not yet tried in a race on the Frame. From Wiicompiled_VR-PLUS.
|
||||
- **Updating, from the PC or from inside the game.** `steam-frame-install.sh --update` reuses the
|
||||
options the install was made with and builds nothing when the Frame already has the newest
|
||||
release (and Retro Rewind pack); `--check` only reports. An install built on the Frame itself
|
||||
(`--frame local`) gets an **Updates** tab in the settings panel: it checks for a new release and
|
||||
builds it in the background while the game stays open, showing each step and how far the build
|
||||
is. Close the game meanwhile and the update reopens it when done. Steam notifications were tried
|
||||
for this and do not show on the Frame.
|
||||
- **`Launcher/frame-diagnostics.sh`** gathers the logs, crash files, settings, SteamVR logs and
|
||||
system state from the Frame into one archive, with a summary of how far startup got. Attach it
|
||||
to bug reports.
|
||||
|
||||
## Fixed
|
||||
|
||||
- A crash while a scene loads (`GXClearVtxDesc`). From Wiicompiled_VR-PLUS.
|
||||
- TLS on Linux, through Mbed TLS, so online services can connect at all (upstream #144), and a
|
||||
timed-out TLS write no longer frees a session the game still holds (DarthMDev).
|
||||
- Saves on a different drive or the SD card can be moved (upstream #212); the game keeps its local
|
||||
Wii identity services while offline, and emptied WiiConnect24 files are refilled, so it no longer
|
||||
reports a save error for them (upstream #244, nx-mod).
|
||||
- `Config.toml` is saved through a temporary file, so a crash mid-save can't empty it
|
||||
(BlackAndBlue95).
|
||||
- Renderer fixes from upstream's KartPad batch (#244): draws no longer read past their own vertices,
|
||||
three-byte vertex attributes, GEN_MODE, EFB copies and readback, pipeline cache rows and pipeline
|
||||
compile wake-ups. Waiting on the GPU now sleeps instead of holding a CPU core busy.
|
||||
- Effects no longer vanish while the world is darkened, such as Stormship's lightning
|
||||
(BlackAndBlue95).
|
||||
- Controller player indices and socket errors (upstream #251), and a `bltl` translation (#254).
|
||||
|
||||
## Faster
|
||||
|
||||
- Disc reads and SZS decompression take bulk copies and keep file handles open per thread
|
||||
(KartPad).
|
||||
- Audio mixing uses NEON (rooklz).
|
||||
- Translated memory accesses no longer check the page size each time, since the Frame uses 4 KiB
|
||||
pages; the game checks once at startup (from nx-mod's finding).
|
||||
|
||||
## Updating from frame-beta-2
|
||||
|
||||
Run the install command again; you can leave out `--disc`. Dawn's patches did not change, so it is
|
||||
not rebuilt. Most of the game is, since the translator changed as well as the runtime and renderer,
|
||||
so expect a longer update than usual. The build now also downloads Mbed TLS. Add `--retro-rewind` to get Retro Rewind too.
|
||||
|
||||
frame-beta-2's script has no `--update`, so this one update is the full install command; it saves
|
||||
its options, and from then on `steam-frame-install.sh --update` is enough. To update from inside
|
||||
the game later, make this install on the Frame itself with `--frame local`.
|
||||
|
||||
## Known issues
|
||||
|
||||
Unchanged from frame-beta-2:
|
||||
|
||||
- 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/`, or the archive
|
||||
`frame-diagnostics.sh` makes, are welcome as issues.
|
||||
@@ -1,548 +0,0 @@
|
||||
# WiiCompiled VR on the Steam Frame
|
||||
|
||||
Valve's Steam Frame runs SteamOS on a Snapdragon 8 Gen 3 (Cortex-X4, A720 and A520 cores, Adreno 750),
|
||||
with 2160x2160 panels per eye at 72 to 144 Hz, eye tracking, and SteamVR as its OpenXR runtime. A game
|
||||
can run on it two ways, and this project has both:
|
||||
|
||||
- **Natively on SteamOS** (Linux ARM64), with SteamVR's own OpenXR runtime. This is the Frame's build:
|
||||
[The native SteamOS build](#the-native-steamos-build) says how to make and run it.
|
||||
- **As an Android app in Lepton**, SteamOS's Android layer, as a third flavour of the Quest app
|
||||
(`steamFrame`). It is built, but it cannot show a picture there: Lepton's Vulkan driver has no
|
||||
external memory or sync fd extensions, and the Quest backend's two-device eye handoff needs them
|
||||
(see [What the Frame reported](#what-the-frame-reported)).
|
||||
|
||||
Most of what this document describes is shared by both: the Frame controller profile, the 120 Hz
|
||||
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: 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
|
||||
|
||||
The backend is the PC's same-device Vulkan backend (`openxr_vulkan_win32.cpp`, on Linux too): the
|
||||
OpenXR runtime creates Dawn's own Vulkan instance and device through Aurora's patches to Dawn, and
|
||||
each eye is copied into SteamVR's swapchain on Dawn's queue, so nothing is shared between devices.
|
||||
On Linux, Dawn links statically, so the patched Dawn is built once on the build machine
|
||||
(`Launcher/build-dawn-linux.sh`); `aurora-dawn.json` in its package declares the Vulkan hook and
|
||||
density map ABIs, and only against such a package does Aurora compile the bridge
|
||||
(`AURORA_DAWN_VULKAN_HOOKS`) and the density maps (`AURORA_DAWN_FDM`). Against a stock Dawn the
|
||||
build still links, and VR falls back to the desktop.
|
||||
|
||||
What the Frame build changes, beyond the Android flavour's settings:
|
||||
|
||||
- `-mcpu=cortex-x4` (`MKW_LINUX_CPU`, which `--cpu` overrides).
|
||||
- `AuroraConfig::xrHeadsetOnly`: Aurora neither presents the desktop window nor renders it past the
|
||||
last pass the eyes sample, as on Android (4 to 6 ms of a 12 ms GPU frame on a Quest 3).
|
||||
- Fragment density maps are asked for on Linux as on the Quest; `AURORA_FDM=0` or `1` overrides the
|
||||
settings, as `debug.wiicompiled.fdm` does there.
|
||||
- Controller motion uses `XR_KHR_convert_timespec_time` when SteamVR offers it.
|
||||
|
||||
### Building it on the Frame
|
||||
|
||||
SteamOS's root file system is read-only, so the build runs in a Debian container on the Frame,
|
||||
started with the `podman` SteamOS already ships. Over SSH (`ssh steamos@<frame-ip>`):
|
||||
|
||||
```bash
|
||||
mkdir -p ~/wiicompiled && cd ~/wiicompiled
|
||||
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
|
||||
```
|
||||
|
||||
Inside the container (`podman start -ai wiicompiled-build` gets back into it later):
|
||||
|
||||
```bash
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl git python3 xz-utils unzip file pkg-config g++ binutils libicu-dev zlib1g-dev \
|
||||
libvulkan-dev libx11-dev libx11-xcb-dev libxcb1-dev libxext-dev libxrandr-dev libxinerama-dev \
|
||||
libxcursor-dev libxi-dev libxss-dev libxtst-dev libxkbcommon-dev libwayland-dev wayland-protocols \
|
||||
libdecor-0-dev libegl-dev libgl-dev libgles-dev libdrm-dev libgbm-dev libasound2-dev libpulse-dev \
|
||||
libpipewire-0.3-dev libudev-dev libdbus-1-dev libusb-1.0-0-dev
|
||||
cd /work/Wiicompiled_VR_Frame
|
||||
Launcher/prepare-portable-tools.sh --arch aarch64 --destination /work/tools # clang 22, CMake, Ninja
|
||||
T=/work/tools/toolchain-aarch64/bin
|
||||
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0 --install-dir /work/dotnet
|
||||
Launcher/build-dawn-linux.sh --work-dir /work/dawn --cc $T/clang --cxx $T/clang++ --cmake $T/cmake --ninja $T/ninja
|
||||
```
|
||||
|
||||
Then the game. `local-build.sh` translates your own disc, so it needs `main.dol` and `StaticR.rel` from
|
||||
your extracted PAL `RMCP01` disc in `Assets/` (the extracted disc's `sys/main.dol` and
|
||||
`files/rel/StaticR.rel`; `translator/README.md` explains):
|
||||
|
||||
```bash
|
||||
mkdir -p Assets && cp <DATA>/sys/main.dol <DATA>/files/rel/StaticR.rel Assets/
|
||||
Launcher/local-build.sh --output-dir /work/out --cc $T/clang --cxx $T/clang++ --fuse-ld lld \
|
||||
--cmake $T/cmake --ninja $T/ninja --dotnet /work/dotnet/dotnet \
|
||||
--openxr --dawn-package /work/dawn/package --headset steam_frame
|
||||
```
|
||||
|
||||
The game lands in `~/wiicompiled/out` on the Frame. Debian trixie's C library is not newer than
|
||||
SteamOS's, so the binary runs on SteamOS outside the container. If CMake reports a missing package,
|
||||
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/`;
|
||||
`/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:
|
||||
|
||||
1. `OpenXR runtime offers N extensions: ...`, and `OpenXR initialized: runtime 'SteamVR/OpenXR'`;
|
||||
2. `OpenXR Vulkan requirements: ... Dawn will create its device through the runtime`;
|
||||
3. `Fragment density maps: enabled` (the patched Dawn and Turnip's density maps);
|
||||
4. `OpenXR Vulkan swapchains ready ... same-queue native eye copies`;
|
||||
5. `display refresh rate 120 Hz requested`, the session reaching `FOCUSED`, and
|
||||
`OpenXR interaction profiles: left /interaction_profiles/valve/frame_controller_valve`;
|
||||
6. `OpenXR eye gaze: available`, then `tracking`.
|
||||
|
||||
`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`
|
||||
flavour of the Quest app. Its controller, refresh rate and foveation work is shared with the native
|
||||
build; its launch and manifest are Lepton's.
|
||||
|
||||
### What the flavour changes
|
||||
|
||||
| | Quest flavours | `steamFrame` |
|
||||
| --- | --- | --- |
|
||||
| CPU target (`kit.json` `androidCpu`) | `cortex-a77` (`kryo` on Quest 1) | `cortex-x4` |
|
||||
| `MKW_HEADSET` | empty | `steam_frame` (defines `MKW_HEADSET_STEAM_FRAME`) |
|
||||
| Library entry | `LauncherActivity` (Quest 1: `QuestActivity`) | `FrameEntryActivity` |
|
||||
| Horizon OS manifest entries | present | removed |
|
||||
| `[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
|
||||
fingerprint: a game built for a Quest is refused on the Frame and the other way round, by the same
|
||||
checks that keep Quest 1 and modern Quest games apart.
|
||||
|
||||
**CPU.** Every core of the 8 Gen 3 implements ARMv9.2, so the products target the Cortex-X4 with
|
||||
its whole feature set. That includes SVE and SVE2, which clang auto-vectorises with (a simple loop
|
||||
compiled with `-O3` used SVE registers ten times). Phones with this chip do not expose SVE, but the
|
||||
Frame's kernel does: `/proc/cpuinfo` lists `sve`, `sve2`, `svei8mm`, `svebf16` and the SVE2 crypto
|
||||
extensions, on SteamOS and inside Lepton alike. A build for a device without SVE would need
|
||||
`cortex-x4+nosve`. The flavour-to-CPU map lives once in
|
||||
`android/app/build.gradle.kts` (`headsetCpus`), which the kit export also reads now instead of
|
||||
guessing from the variant name.
|
||||
|
||||
**Launch under Lepton.** Lepton starts the one real activity that is both `MAIN` and `LAUNCHER`, and
|
||||
runs the app in VR when that activity carries a VR category; it ignores `activity-alias` entries.
|
||||
Quest builds put `LAUNCHER` on the 2D panel (or, on Quest 1, add an alias), so neither works there.
|
||||
`src/steamFrame/AndroidManifest.xml` makes `FrameEntryActivity` the only `MAIN`/`LAUNCHER` activity,
|
||||
with `org.khronos.openxr.intent.category.IMMERSIVE_HMD` and `com.oculus.intent.category.VR`. It
|
||||
shows nothing: it always opens the setup panel (`LauncherActivity`), and when the selected game can
|
||||
start as it is (game files, a game built for this kit, Retro Rewind's pack, and no enabled mods
|
||||
still to copy into the pack), it opens `QuestActivity` on top of it. The headset therefore goes
|
||||
straight into VR, and quitting the game returns to the panel for setup, imports and mods.
|
||||
`adb logcat -s WiiCompiledLauncher` shows which way it went.
|
||||
|
||||
The manifest also removes Horizon OS's own entries (`com.oculus.supportedDevices`, `focusaware`,
|
||||
`trade_cpu_for_gpu_amount`, the passthrough feature and the hand tracking permissions and feature)
|
||||
and keeps the Khronos broker queries and the `OPENXR_SYSTEM` permission, which any Android OpenXR
|
||||
runtime needs.
|
||||
|
||||
## Controllers
|
||||
|
||||
With `XR_VALVE_frame_controller_interaction` the runtime offers the Frame controller's own profile,
|
||||
`/interaction_profiles/valve/frame_controller_valve`. Without it SteamVR presents the controllers as
|
||||
Touch controllers, which loses the left D-pad. `openxr_input.cpp` suggests it after Touch and the
|
||||
simple controller. Each hand has a thumbstick, trigger, grip and shoulder button. The right hand has
|
||||
A, B, X, Y and a menu button; the left hand has a D-pad and a View button. Binding paths follow
|
||||
DolphinXR's port (iChris4/dolphinXR#9). Windows asks for the same extension, so a Frame streaming
|
||||
from a PC through SteamVR gets the D-pad too.
|
||||
|
||||
| Frame controller | Wii Remote mode | Gamepad mode |
|
||||
| --- | --- | --- |
|
||||
| Right A | A | South (A) |
|
||||
| Right B | C (look behind) | East (B) |
|
||||
| Right trigger | B | Right trigger |
|
||||
| Right stick up / down | 1 / 2 | Right stick |
|
||||
| Left View | + (pause) | Start |
|
||||
| Left shoulder | Settings panel (Touch's left Y) | North (Y) |
|
||||
| Left D-pad | Wii Remote D-pad | D-pad |
|
||||
| Left stick, left trigger | Nunchuk stick, Z | Left stick, left trigger |
|
||||
| Grips, stick clicks, motion, aim | as on Touch (`OPENXR.md`, Controllers) | as on Touch |
|
||||
| Right X, Y, menu and shoulder | unbound | unbound |
|
||||
|
||||
The four D-pad actions are new and also reach the virtual gamepad's D-pad; Touch leaves them unbound,
|
||||
so nothing changes on a Quest.
|
||||
|
||||
## Refresh rate
|
||||
|
||||
`[vr] refresh_rate` (Hz, `0` = the headset's own rate) is asked of the runtime through
|
||||
`XR_FB_display_refresh_rate` each time the session starts running and whenever the setting changes.
|
||||
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`) 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
|
||||
changes. The setting is in the headset panel and in the launcher (VR → Headset); other headsets
|
||||
offer it as well, at their own default of `0`.
|
||||
|
||||
## Eye-tracked foveation
|
||||
|
||||
`[vr] eye_tracked_foveation` (default on for the Frame) moves the foveation level's full-density
|
||||
region to where the player looks:
|
||||
|
||||
1. At launch the runtime asks for `XR_EXT_eye_gaze_interaction`. If the system reports an eye tracker,
|
||||
`OpenXRInput` binds the gaze pose (`/user/eyes_ext/input/gaze_ext/pose`) and locates it for each
|
||||
packet's display time, in the space the eye views are located in.
|
||||
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`). 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 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. 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
|
||||
gaze` for each eye's first gaze map. Foveation pays only when an eye is pixel-bound
|
||||
(`OPENXR.md`, Foveated rendering), which the Frame's larger eyes make more likely.
|
||||
|
||||
If the Frame's driver offers `VK_QCOM_fragment_density_map_offset` or
|
||||
`VK_EXT_fragment_density_map_offset`, shifting one map per pass would replace switching between
|
||||
maps. That needs the Dawn patch to create the eye textures with the offset flag, so it waits for
|
||||
the device's extension list.
|
||||
|
||||
## Building and installing
|
||||
|
||||
On the Windows build host described in `docs/quest-port.md`:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset frame # the steamFrame APK; checks kit.json says cortex-x4
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Headset frame -Product base -Data <DATA> # a .wcgame for the Frame's kit
|
||||
```
|
||||
|
||||
The APK lands in `android/app/build/outputs/apk/steamFrame/<configuration>`. WheelWizard VR's
|
||||
"Build for Quest" builds a Frame game unchanged once it is given the Frame APK (Setup takes the
|
||||
headset from the APK's kit). WheelWizard itself still needs a Steam Frame choice that fetches that
|
||||
APK from the release, published as `…-SteamFrame.apk`.
|
||||
|
||||
Lepton opens an adb port (5555 and up) for each running Android instance, reachable over the
|
||||
network. With an Android app running on the Frame, `adb connect <frame-ip>:5555` reaches it from the
|
||||
build PC. How the APK reaches the Steam library (adb into a Lepton instance, frame-control, or
|
||||
Steam's own sideloading) is to be confirmed on the device.
|
||||
|
||||
## What the Frame reported
|
||||
|
||||
Read on 2026-10-04 from a Steam Frame running SteamOS (`holo`), kernel 6.18.0, with the commands
|
||||
below:
|
||||
|
||||
| Reading | SteamOS | Lepton |
|
||||
| --- | --- | --- |
|
||||
| Page size | 4096 | 4096 |
|
||||
| CPU | 8 cores; `sve sve2 svei8mm svebf16 sveaes svepmull svebitperm svesha3 svesm4 i8mm bf16 bti paca pacg ...` | the same |
|
||||
| Android | — | 11 (API 30, LineageOS), `ro.product.model` Lepton, device `lepton_arm64_only`, platform `waydroid`, `ro.steam.running_in_app_container=true` |
|
||||
| Vulkan driver | Turnip, Mesa 26.3.0-devel, Vulkan 1.4.362: `VK_EXT_fragment_density_map` (non-subsampled images, not dynamic), `VK_EXT_fragment_density_map_offset` and `VK_QCOM_fragment_density_map_offset`, `VK_KHR_external_{memory,semaphore,fence}_fd`, `VK_EXT_external_memory_dma_buf`, `VK_EXT_queue_family_foreign`, `VK_KHR_dynamic_rendering`; Valve's `fdm_injection` and `rpo` Vulkan layers | `ro.hardware.vulkan=freedreno`: the same Mesa 26.3.0-devel Turnip built for Android (`vulkan.pastel.so` is also present, not selected). `/dev/kgsl-3d0` is the DRM render node |
|
||||
| OpenXR runtime | SteamVR, `bin/linuxarm64/vrclient.so` (`~/.config/openxr/1/active_runtime.json`); SteamOS ships the SDK headers, `libopenxr_loader.a` and `openxr.pc` | `/vendor/etc/openxr/1/active_runtime.json`, from the host's `/usr/share/guestos/android/vendor/etc/openxr`; no runtime broker package |
|
||||
| Implicit OpenXR layers | `XrApiLayer_VALVE_fdm_injection` (also listed as explicit) | `XrApiLayer_VALVE_fdm_injection` |
|
||||
|
||||
What follows from them:
|
||||
|
||||
- **Fast memory path.** 4 KB pages keep the translated code's flat memory path. A 16 KB kernel
|
||||
would have sent it through the checked path.
|
||||
- **CPU target.** The Frame exposes SVE, so the build targets the whole `cortex-x4` (above).
|
||||
- **Android version.** API 30 meets the app's minimum of 29.
|
||||
- **Driver workarounds.** Lepton is a Waydroid container, and its Vulkan driver is Turnip. The
|
||||
Adreno workarounds in `docs/quest-port.md` were found on Qualcomm's own driver. The vertex padding
|
||||
stays on (it is correct either way); `debug.wiicompiled.vtxpad 0` can check whether Turnip needs it.
|
||||
- **Foveation.** The host's Turnip has density maps for non-subsampled images through dynamic
|
||||
rendering, what the Dawn patch needs, and both density map offset extensions, which would let
|
||||
eye-tracked foveation shift one map instead of switching maps.
|
||||
- **No buffer sharing between devices in Lepton.** The Vulkan Hardware Capability Viewer (4.03,
|
||||
the last release for Android 11), run inside Lepton, reports Turnip `26.2.99` (Vulkan 1.4.362,
|
||||
display name "Valve Lepton") with `VK_EXT_fragment_density_map`, both density map offset
|
||||
extensions, `VK_VALVE_fragment_density_map_layered`, `VK_KHR_timeline_semaphore` and the
|
||||
maintenance extensions, but **no** `VK_ANDROID_external_memory_android_hardware_buffer`,
|
||||
`VK_KHR_external_memory_fd`, `VK_KHR_external_semaphore_fd` or `VK_KHR_external_fence_fd`. The
|
||||
Quest backend (`openxr_vulkan.cpp`) hands each eye from Dawn's device to its own OpenXR device
|
||||
through exactly those, so it cannot present under Lepton. What can: binding Dawn's own device to
|
||||
the session, as the Windows Vulkan backend (`openxr_vulkan_win32.cpp`) does, so the eyes are
|
||||
copied into the swapchain on Dawn's queue with no sharing at all. That backend is also the core
|
||||
of a native SteamOS build ([above](#the-native-steamos-build)).
|
||||
- **Finding the runtime.** An app inside Lepton reaches SteamVR's OpenXR runtime through the system
|
||||
runtime file, not a broker. The Khronos loader the game links statically (`DYNAMIC_LOADER OFF`)
|
||||
tries the runtime brokers first, then reads `/{product,odm,oem,vendor,system}/etc/openxr/1/active_runtime.json`,
|
||||
so no app change is needed; the manifest's broker queries are simply unused here. Walkabout Mini
|
||||
Golf, an Android VR game, runs in the same Lepton.
|
||||
- **Valve's foveation layer.** `XrApiLayer_VALVE_fdm_injection` is implicit, so it wraps every
|
||||
Android OpenXR app. By its name it adds fragment density maps to apps' own render passes. This
|
||||
game draws its eyes on Dawn's device and only copies them into the swapchain on the OpenXR
|
||||
device, so the layer has no render pass of the game's to change; the game's own maps
|
||||
(eye-tracked foveation, above) do that. If the layer gets in the way,
|
||||
`DISABLE_VULKAN_FDM_INJECTION_LAYER` turns it off (its manifest loads
|
||||
`libVkLayer_VALVE_fdm_injection.so`; the runtime itself is
|
||||
`/data/steamvr/runtime/bin/androidarm64/vrclient.so`).
|
||||
|
||||
### SteamVR's Android OpenXR extensions
|
||||
|
||||
Walkabout Mini Golf, a Unity game in the same Lepton, logs what the runtime offers (`adb logcat -d |
|
||||
grep -F '[XR]'`). Its extensions:
|
||||
|
||||
```
|
||||
XR_EXT_active_action_set_priority XR_EXT_debug_utils XR_EXT_dpad_binding XR_EXT_eye_gaze_interaction
|
||||
XR_EXT_frame_composition_report XR_EXT_frame_synthesis XR_EXT_hand_interaction XR_EXT_hand_joints_motion_range
|
||||
XR_EXT_hand_tracking XR_EXT_hand_tracking_data_source XR_EXT_hp_mixed_reality_controller
|
||||
XR_EXT_interaction_profile_battery_state_display XR_EXT_interaction_render_model XR_EXT_local_floor
|
||||
XR_EXT_palm_pose XR_EXT_performance_settings XR_EXT_render_model XR_EXT_user_presence XR_EXT_uuid
|
||||
XR_EXT_view_configuration_views_change XR_FB_display_refresh_rate XR_FB_foveation
|
||||
XR_FB_foveation_configuration XR_FB_foveation_vulkan XR_FB_space_warp XR_FB_swapchain_update_state
|
||||
XR_HTC_vive_cosmos_controller_interaction XR_HTC_vive_focus3_controller_interaction
|
||||
XR_HTC_vive_wrist_tracker_interaction XR_HTCX_vive_tracker_interaction XR_KHR_android_create_instance
|
||||
XR_KHR_binding_modification XR_KHR_composition_layer_depth XR_KHR_generic_controller XR_KHR_locate_spaces
|
||||
XR_KHR_opengl_enable XR_KHR_opengl_es_enable XR_KHR_visibility_mask XR_KHR_vulkan_enable
|
||||
XR_KHR_vulkan_enable2 XR_META_foveation_eye_tracked XR_META_performance_metrics
|
||||
XR_META_recommended_layer_resolution XR_META_vulkan_swapchain_create_info XR_MND_headless
|
||||
XR_MNDX_egl_enable XR_VALVE_analog_threshold XR_VALVE_app_space_delta_pose
|
||||
XR_VALVE_frame_controller_interaction XR_VALVE_timing_utils
|
||||
```
|
||||
|
||||
Environment blend modes `OPAQUE` and `ALPHA_BLEND`; reference spaces `LOCAL`, `STAGE` and `VIEW`.
|
||||
What this build asks for and gets:
|
||||
|
||||
| Extension | Offered | What it means here |
|
||||
| --- | --- | --- |
|
||||
| `XR_KHR_android_create_instance`, `XR_KHR_vulkan_enable2` | yes | The Android backend's instance and Vulkan binding |
|
||||
| `XR_VALVE_frame_controller_interaction` | yes | The Frame controller profile and its D-pad |
|
||||
| `XR_FB_display_refresh_rate` | yes | `[vr] refresh_rate` (120 Hz) can be requested |
|
||||
| `XR_EXT_eye_gaze_interaction` | yes | Eye-tracked foveation |
|
||||
| `XR_EXT_performance_settings` | yes | `performance_level` |
|
||||
| `XR_EXT_hand_tracking`, `XR_EXT_hand_tracking_data_source` | yes | Tracked hands; `XR_FB_hand_tracking_mesh` and `_aim` are not offered, so the cockpit draws its procedural gloves and bare hands get no pinch gestures |
|
||||
| `XR_KHR_convert_timespec_time` | **no** | VR frame interpolation is unavailable, and controller motion is sampled at the frame's display time rather than the current time |
|
||||
| `XR_KHR_android_thread_settings` | no | Thread hints are skipped (logged as refused) |
|
||||
| `XR_FB_passthrough` | no | As expected; the build does not ask for it |
|
||||
|
||||
Later candidates the runtime offers: `ALPHA_BLEND` could bring back the room around the menu screen
|
||||
without `XR_FB_passthrough`, `XR_KHR_visibility_mask` would skip the pixels the lenses never show,
|
||||
and `XR_FB_foveation` with `XR_META_foveation_eye_tracked` only shapes render passes into the
|
||||
runtime's swapchain images, which this game's eyes reach by copy, so it does not apply.
|
||||
|
||||
## Device checklist
|
||||
|
||||
Information to collect first, from any PC over Lepton's adb port (the commands work in bash, zsh and
|
||||
fish; in PowerShell only the adb lines do), with an Android app running on the Frame:
|
||||
|
||||
```sh
|
||||
adb connect <frame-ip>:5555
|
||||
adb -s <frame-ip>:5555 shell 'getprop ro.build.version.release; getprop ro.build.version.sdk; getprop ro.product.manufacturer; getprop ro.product.model; getprop ro.product.device; getprop ro.hardware.vulkan; getprop ro.board.platform'
|
||||
adb -s <frame-ip>:5555 shell 'uname -a; getconf PAGE_SIZE; grep -m1 Features /proc/cpuinfo; grep -c processor /proc/cpuinfo'
|
||||
adb -s <frame-ip>:5555 shell cmd gpu vkjson > frame-vkjson.json
|
||||
adb -s <frame-ip>:5555 shell "pm list packages | grep -i -E 'xr|valve|steam|khronos|openxr'"
|
||||
# The parts of frame-vkjson.json that matter:
|
||||
grep -oE '"(deviceName|driverName|driverInfo|apiVersion|driverVersion)": *[^,]*' frame-vkjson.json | sort -u
|
||||
grep -oE '"extensionName": *"[^"]*"' frame-vkjson.json | grep -iE 'hardware_buffer|external_semaphore|external_fence|external_memory|fragment_density|shading_rate|dynamic_rendering' | sort -u
|
||||
```
|
||||
|
||||
and on the Frame itself (Desktop Mode, Konsole):
|
||||
|
||||
```bash
|
||||
uname -a; getconf PAGESIZE; grep -m1 Features /proc/cpuinfo; head -5 /etc/os-release
|
||||
cat ~/.config/openxr/1/active_runtime.json 2>/dev/null; ls /usr/share/openxr/1/ /etc/xdg/openxr/1/ 2>/dev/null
|
||||
```
|
||||
|
||||
What each answers:
|
||||
|
||||
- `vkjson`: whether Lepton's Vulkan driver has what the Android bridge needs:
|
||||
- `VK_ANDROID_external_memory_android_hardware_buffer`;
|
||||
- `VK_KHR_external_semaphore_fd` and `VK_KHR_external_fence_fd` with sync fd handles.
|
||||
|
||||
Without these the APK route cannot present at all, and the native route becomes the way. It also
|
||||
shows `VK_EXT_fragment_density_map` (foveation), the density map offset extensions, and which
|
||||
driver Lepton uses.
|
||||
- The CPU features line: no `sve`, as the CPU target assumes.
|
||||
- The page size: on a 16 KB-page kernel the runtime finds out at launch and routes translated memory
|
||||
accesses through the checked path (`guest_flat_memory.h`, `RequiresCheckedAccess`), which works but
|
||||
is slower. That is worth knowing before measuring.
|
||||
- The Android version: the app needs API 29 (Android 10) or newer.
|
||||
|
||||
Then, on the first launch, the session log (`Logs/<product>_<stamp>_pid<pid>/console.log` next to
|
||||
`DATA`) and `adb logcat -s SDL WiiCompiledQuest WiiCompiledLauncher` should show, in order:
|
||||
|
||||
1. `OpenXR Android loader initialized`;
|
||||
2. `OpenXR runtime offers N extensions: ...`, the whole list SteamVR's Android runtime has;
|
||||
3. `OpenXR initialized: runtime '...'` with SteamVR's name;
|
||||
4. the negotiated Vulkan binding, then `OpenXR Vulkan swapchains ready`;
|
||||
5. `display refresh rate 120 Hz requested` (or `refused`, with the rates on offer);
|
||||
6. the session reaching `FOCUSED`;
|
||||
7. `OpenXR interaction profiles: left /interaction_profiles/valve/frame_controller_valve, right ...`;
|
||||
8. `OpenXR eye gaze: available`, then `tracking`;
|
||||
9. `presentation=virtual-screen` for the menus, and `first immersive packet consumed` on race entry.
|
||||
|
||||
Then, in the game:
|
||||
|
||||
- the D-pad does tricks, the left shoulder opens the settings panel, and View pauses;
|
||||
- `debug.wiicompiled.fpslog 1` on a race start shows the pacing summary and the GPU time per pass
|
||||
with foveation off, fixed and following the gaze;
|
||||
- `render_scale` starts at 0.8, the Quest's value. The runtime's recommended eye size (logged at
|
||||
startup) and those measurements decide whether the Frame keeps it.
|
||||
|
||||
A black headset with a working mirror points at the AHardwareBuffer copy, as on the Quest
|
||||
(`docs/quest-port.md`, Validation status).
|
||||
|
||||
Open questions only the device can answer:
|
||||
|
||||
- Whether Lepton's driver supports the AHardwareBuffer and sync fd bridge.
|
||||
- Whether Lepton shows the 2D setup panel while the app runs in VR mode. If it does not, the game
|
||||
still starts directly once a `.wcgame` with the game files has been imported, but importing needs
|
||||
the panel.
|
||||
- Whether SteamVR grants 120 Hz.
|
||||
- Whether the gaze needs an Android permission under Lepton.
|
||||
- Whether "Build on this headset" can run its toolchain through `/system/bin/linker64` inside
|
||||
Lepton. A game built on the PC does not depend on it.
|
||||
@@ -144,6 +144,43 @@ if(NOT MKW_NATIVE_PREBUILT_DIR)
|
||||
set_target_properties(mkw_cryptopp PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
# TLS for non-Windows guest network HLE (runtime/src/hle/net/network_ssl.cpp) - the Windows path
|
||||
# uses Schannel (a Windows-only OS API), which has no equivalent on Linux/Android, so this project
|
||||
# needs its own TLS library there. mbed TLS was chosen over OpenSSL specifically because it cross-
|
||||
# compiles cleanly for Android with nothing beyond a plain C toolchain (no perl/asm build-script
|
||||
# dependency the way OpenSSL's build has), matching how this project already prefers toolchain-
|
||||
# simple libraries (see Crypto++ above, similarly stripped of ASM/SIMD for portability).
|
||||
# Fetched at build time from a pinned upstream release tarball with a checked SHA-256, the same way
|
||||
# aurora-main's own dependencies (SDL, zlib, etc.) are pulled in - not committed as a vendored
|
||||
# source tree, so the repository ships the compiled dependency rather than ~280 tracked upstream
|
||||
# files. Bump MKW_MBEDTLS_VERSION/MKW_MBEDTLS_SHA256 together when updating; the hash comes from
|
||||
# upstream's own signed `mbedtls-<version>-sha256sum.txt` release asset.
|
||||
#
|
||||
# The alias exists on every platform so the link lines in cmake/PublicProducts.cmake stay
|
||||
# platform-independent, but it is only populated where network_ssl.cpp actually compiles the mbed
|
||||
# TLS path (`#ifndef _WIN32`). Windows keeps Schannel and must not fetch anything: its builds run
|
||||
# with FETCHCONTENT_FULLY_DISCONNECTED=ON against the offline dependency set prepared by
|
||||
# Launcher/Prepare-Dependencies.ps1, so an unconditional fetch would fail a clean configure there
|
||||
# and would also add a dependency Windows never links.
|
||||
add_library(mkw_mbedtls INTERFACE)
|
||||
add_library(mkw::mbedtls ALIAS mkw_mbedtls)
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
include(FetchContent)
|
||||
set(MKW_MBEDTLS_VERSION "3.6.7")
|
||||
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
|
||||
FetchContent_Declare(mkw_mbedtls_upstream
|
||||
URL "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MKW_MBEDTLS_VERSION}/mbedtls-${MKW_MBEDTLS_VERSION}.tar.bz2"
|
||||
URL_HASH SHA256=${MKW_MBEDTLS_SHA256})
|
||||
# Subproject mode already defaults ENABLE_TESTING off and skips codegen (GEN_FILES), but
|
||||
# ENABLE_PROGRAMS defaults on and installation/package-config isn't wanted for a linked-in copy.
|
||||
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
|
||||
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(mkw_mbedtls_upstream)
|
||||
target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto)
|
||||
endif()
|
||||
|
||||
set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING
|
||||
"Cap on concurrently compiling translated shard TUs via a Ninja job pool (0 = uncapped). \
|
||||
Scheduling only - never affects output bytes, so it is deliberately outside the canonical flag fingerprint.")
|
||||
@@ -460,11 +497,21 @@ target_include_directories(mkw_vr_wii_remote_tests PRIVATE "${CMAKE_CURRENT_LIST
|
||||
target_compile_features(mkw_vr_wii_remote_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_wii_remote_tests COMMAND mkw_vr_wii_remote_tests)
|
||||
# Eye-tracked foveation: the gaze in each eye's view (vr/eye_gaze.h).
|
||||
add_executable(mkw_vr_adaptive_resolution_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_adaptive_resolution_tests.cpp")
|
||||
target_include_directories(mkw_vr_adaptive_resolution_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_adaptive_resolution_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_adaptive_resolution_tests COMMAND mkw_vr_adaptive_resolution_tests)
|
||||
add_executable(mkw_vr_eye_gaze_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_eye_gaze_tests.cpp")
|
||||
target_include_directories(mkw_vr_eye_gaze_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_eye_gaze_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_eye_gaze_tests COMMAND mkw_vr_eye_gaze_tests)
|
||||
|
||||
# The AX mix kernels' AVX2 and NEON forms against their scalar reference loops.
|
||||
add_executable(mkw_ax_mix_kernels_tests "${CMAKE_CURRENT_LIST_DIR}/tests/ax_mix_kernels_tests.cpp")
|
||||
target_include_directories(mkw_ax_mix_kernels_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_ax_mix_kernels_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_ax_mix_kernels_tests COMMAND mkw_ax_mix_kernels_tests)
|
||||
|
||||
# The in-headset settings panel's controller chord, release latch, selection,
|
||||
# scrolling and canvas mapping, plus the thread bridge they publish through.
|
||||
add_executable(mkw_vr_settings_panel_tests tests/vr_settings_panel_tests.cpp src/vr/openxr_settings_panel.cpp)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -110,7 +110,7 @@ target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_ENABLE_OPENXR)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE ${MKW_OPENXR_TARGET})
|
||||
endif()
|
||||
@@ -276,10 +276,10 @@ function(mkw_configure_product target)
|
||||
mkw_bound_translated_compiles(${target})
|
||||
if(MKW_PRODUCT_WITHOUT_GAME)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
else()
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
@@ -379,6 +379,21 @@ function(mkw_configure_product target)
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_INITIAL_PIPELINE_CACHE}"
|
||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
||||
|
||||
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
|
||||
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
|
||||
# Schannel, mbed TLS does not ship one itself. Not SHA256-pinned like the DSP ROM above: unlike
|
||||
# a fixed hardware ROM, this bundle is expected to be refreshed periodically as CAs rotate.
|
||||
# Windows gets its trust store from Schannel, so only the platforms that actually build the
|
||||
# mbed TLS path need the bundle beside the executable.
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
set(MKW_CA_CERTIFICATE_BUNDLE "${MKW_RUNTIME_SOURCE_DIR}/assets/certs/cacert.pem")
|
||||
if(NOT EXISTS "${MKW_CA_CERTIFICATE_BUNDLE}")
|
||||
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Android ships each product as the shared library SDLActivity loads; the base
|
||||
|
||||
@@ -71,10 +71,15 @@ bool IsActive();
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
// not become a hot-path load. On AArch64 the page size is a kernel
|
||||
// configuration (4/16/64 KiB), so it is probed at runtime, except in the native
|
||||
// Steam Frame build: that build only runs on the headset, whose SteamOS kernel
|
||||
// uses 4 KiB pages, and Initialize() refuses to start on any other page size.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#elif defined(MKW_HEADSET_STEAM_FRAME) && !defined(__ANDROID__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#define MKW_GUEST_FLAT_VERIFY_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
@@ -294,14 +295,57 @@ inline bool ReadExact(const std::filesystem::path& hostPath,
|
||||
HostReadFailure& failure) {
|
||||
failure = HostReadFailure::None;
|
||||
|
||||
std::ifstream file(hostPath, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
failure = HostReadFailure::MissingFile;
|
||||
return false;
|
||||
// DVD callbacks run on the guest thread, often reading the same stream
|
||||
// file many times per second. Reuse a bounded set of handles without
|
||||
// sharing seek positions with another host thread.
|
||||
struct OpenFile {
|
||||
std::filesystem::path path;
|
||||
std::ifstream stream;
|
||||
std::streamoff size = 0;
|
||||
std::filesystem::file_time_type modified{};
|
||||
uint64_t lastUse = 0;
|
||||
};
|
||||
static thread_local std::vector<OpenFile> openFiles;
|
||||
static thread_local uint64_t useCount = 0;
|
||||
constexpr size_t kMaxOpenFiles = 8;
|
||||
|
||||
auto found = std::find_if(openFiles.begin(), openFiles.end(),
|
||||
[&](const OpenFile& entry) { return entry.path == hostPath; });
|
||||
if (found != openFiles.end()) {
|
||||
std::error_code ec;
|
||||
const auto modified = std::filesystem::last_write_time(hostPath, ec);
|
||||
if (ec || modified != found->modified) {
|
||||
openFiles.erase(found);
|
||||
found = openFiles.end();
|
||||
}
|
||||
}
|
||||
|
||||
file.seekg(0, std::ios::end);
|
||||
const std::streamoff fileSize = file.tellg();
|
||||
if (found == openFiles.end()) {
|
||||
std::ifstream file(hostPath, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
failure = HostReadFailure::MissingFile;
|
||||
return false;
|
||||
}
|
||||
file.seekg(0, std::ios::end);
|
||||
const std::streamoff size = file.tellg();
|
||||
if (size < 0) {
|
||||
failure = HostReadFailure::BadOffset;
|
||||
return false;
|
||||
}
|
||||
std::error_code ec;
|
||||
const auto modified = std::filesystem::last_write_time(hostPath, ec);
|
||||
if (openFiles.size() == kMaxOpenFiles) {
|
||||
const auto oldest = std::min_element(openFiles.begin(), openFiles.end(),
|
||||
[](const OpenFile& a, const OpenFile& b) { return a.lastUse < b.lastUse; });
|
||||
openFiles.erase(oldest);
|
||||
}
|
||||
openFiles.push_back({hostPath, std::move(file), size,
|
||||
ec ? std::filesystem::file_time_type{} : modified, 0});
|
||||
found = std::prev(openFiles.end());
|
||||
}
|
||||
found->lastUse = ++useCount;
|
||||
|
||||
const std::streamoff fileSize = found->size;
|
||||
if (fileSize < 0 ||
|
||||
offset > static_cast<uint64_t>(std::numeric_limits<std::streamoff>::max()) ||
|
||||
offset >= static_cast<uint64_t>(fileSize)) {
|
||||
@@ -316,17 +360,20 @@ inline bool ReadExact(const std::filesystem::path& hostPath,
|
||||
}
|
||||
|
||||
std::vector<uint8_t> staged(length);
|
||||
file.seekg(static_cast<std::streamoff>(offset), std::ios::beg);
|
||||
if (!file) {
|
||||
found->stream.clear();
|
||||
found->stream.seekg(static_cast<std::streamoff>(offset), std::ios::beg);
|
||||
if (!found->stream) {
|
||||
failure = HostReadFailure::BadOffset;
|
||||
openFiles.erase(found);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length != 0) {
|
||||
file.read(reinterpret_cast<char*>(staged.data()),
|
||||
static_cast<std::streamsize>(length));
|
||||
if (file.gcount() != static_cast<std::streamsize>(length)) {
|
||||
found->stream.read(reinterpret_cast<char*>(staged.data()),
|
||||
static_cast<std::streamsize>(length));
|
||||
if (found->stream.gcount() != static_cast<std::streamsize>(length)) {
|
||||
failure = HostReadFailure::ShortRead;
|
||||
openFiles.erase(found);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,19 +86,37 @@ inline bool CopyBootstrapFile(const std::filesystem::path& sourceRoot,
|
||||
std::error_code& ec) {
|
||||
const auto source = sourceRoot / relativePath;
|
||||
const auto destination = destinationRoot / relativePath;
|
||||
auto copyOptions = std::filesystem::copy_options::none;
|
||||
if (std::filesystem::exists(destination, ec)) {
|
||||
return !ec;
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
// Keep whatever the player has, except an empty file where the payload has content. WC24
|
||||
// rejects a zero-length download or friend list outright and MKW reports that as a save
|
||||
// error; since seeding only ran for missing files, such a file stayed broken for good.
|
||||
const auto existingSize = std::filesystem::file_size(destination, ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
std::error_code sourceError;
|
||||
const auto sourceSize = std::filesystem::file_size(source, sourceError);
|
||||
if (existingSize != 0 || sourceError || sourceSize == 0) {
|
||||
return true;
|
||||
}
|
||||
copyOptions = std::filesystem::copy_options::overwrite_existing;
|
||||
} else if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(destination.parent_path(), ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
std::filesystem::copy_file(source, destination, std::filesystem::copy_options::none, ec);
|
||||
std::filesystem::copy_file(source, destination, copyOptions, ec);
|
||||
return !ec;
|
||||
}
|
||||
|
||||
// Create these WC24 files only for a new profile; never overwrite user data.
|
||||
// Create these WC24 files for a new profile, or refill one left empty; never overwrite user data.
|
||||
constexpr std::string_view kBootstrapFiles[] = {
|
||||
"shared2/wc24/misc.bin",
|
||||
"shared2/wc24/nwc24dl.bin",
|
||||
|
||||
@@ -101,6 +101,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<std::string> vrFoveation;
|
||||
std::optional<bool> vrEyeTrackedFoveation;
|
||||
std::optional<bool> vrRepeatFrames;
|
||||
std::optional<bool> vrAdaptiveResolution;
|
||||
std::optional<std::string> vrRecenterKey;
|
||||
std::optional<float> vrLeanBackDegrees;
|
||||
// F10 > Diagnostics: OpenXR pacing and presentation logging in console.log.
|
||||
@@ -607,6 +608,10 @@ inline void EnsureConfigFile() {
|
||||
"# 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"
|
||||
"# Lower the race's eye resolution, down to 70% of render_scale, while\n"
|
||||
"# new frames fall behind the game's 60 FPS, and raise it again once\n"
|
||||
"# they keep up. Experimental; the Steam Frame and the Quest. Live.\n"
|
||||
"adaptive_resolution = false\n"
|
||||
"render_scale = " MKW_VR_RENDER_SCALE_DEFAULT_TEXT "\n"
|
||||
"world_units_per_meter = 500.0\n"
|
||||
"hud_distance_meters = 2.0\n"
|
||||
@@ -928,6 +933,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
}
|
||||
config.vrEyeTrackedFoveation = FindConfigValue<bool>(document, "vr", "eye_tracked_foveation");
|
||||
config.vrRepeatFrames = FindConfigValue<bool>(document, "vr", "repeat_frames");
|
||||
config.vrAdaptiveResolution = FindConfigValue<bool>(document, "vr", "adaptive_resolution");
|
||||
if (auto value = FindConfigValue<std::string>(document, "vr", "mirror_view");
|
||||
value && IsSupportedVrMirrorView(*value)) {
|
||||
config.vrMirrorView = *value;
|
||||
@@ -1058,14 +1064,24 @@ inline const std::optional<std::string>& ControllerButton(size_t index) {
|
||||
// user-specific paths. This is used by the in-game F10 settings bar.
|
||||
inline bool WriteSetting(std::string_view section, std::string_view key, std::string_view value) {
|
||||
const auto path = ResolveConfigPath();
|
||||
std::ifstream input(path);
|
||||
std::vector<std::string> lines;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
std::error_code existsError;
|
||||
if (std::filesystem::exists(path, existsError)) {
|
||||
// A file that's there but can't be read is not an empty one: rewriting it from nothing
|
||||
// would throw away every other setting.
|
||||
std::ifstream input(path);
|
||||
std::string line;
|
||||
while (input && std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
lines.push_back(std::move(line));
|
||||
}
|
||||
if (!input.eof()) {
|
||||
std::cerr << "[runtime-config] Unable to read " << PathToUtf8(path) << "; not saving "
|
||||
<< key << std::endl;
|
||||
return false;
|
||||
}
|
||||
lines.push_back(std::move(line));
|
||||
}
|
||||
|
||||
const std::string normalizedSection = Trim(section);
|
||||
@@ -1118,19 +1134,34 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
|
||||
}
|
||||
}
|
||||
|
||||
// Written beside the file and renamed over it, so a crash or a full disk mid-write leaves the
|
||||
// old file whole rather than a truncated one.
|
||||
std::error_code ec;
|
||||
if (path.has_parent_path()) {
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
}
|
||||
std::ofstream output(path, std::ios::trunc);
|
||||
if (!output) {
|
||||
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
|
||||
std::filesystem::path temporary = path;
|
||||
temporary += ".tmp";
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::trunc);
|
||||
for (const auto& outputLine : lines) {
|
||||
output << outputLine << '\n';
|
||||
}
|
||||
output.close();
|
||||
if (!output) {
|
||||
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(temporary) << std::endl;
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::filesystem::rename(temporary, path, ec);
|
||||
if (ec) {
|
||||
std::cerr << "[runtime-config] Unable to replace " << PathToUtf8(path) << ": " << ec.message()
|
||||
<< std::endl;
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
for (const auto& outputLine : lines) {
|
||||
output << outputLine << '\n';
|
||||
}
|
||||
return static_cast<bool>(output);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string FormatString(std::string_view value) {
|
||||
@@ -1345,6 +1376,11 @@ inline bool SetVrRepeatFrames(bool value) {
|
||||
return WriteSetting("vr", "repeat_frames", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrAdaptiveResolution(bool value) {
|
||||
Mutable().vrAdaptiveResolution = value;
|
||||
return WriteSetting("vr", "adaptive_resolution", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrEyeTrackedFoveation(bool value) {
|
||||
Mutable().vrEyeTrackedFoveation = value;
|
||||
return WriteSetting("vr", "eye_tracked_foveation", value ? "true" : "false");
|
||||
@@ -1880,6 +1916,10 @@ inline bool VrRepeatFrames(bool fallback = kVrRepeatFramesDefault) {
|
||||
return Get().vrRepeatFrames.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool VrAdaptiveResolution(bool fallback = false) {
|
||||
return Get().vrAdaptiveResolution.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool VrEyeTrackedFoveation(bool fallback = kVrEyeTrackedFoveationDefault) {
|
||||
return Get().vrEyeTrackedFoveation.value_or(fallback);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// Settings > Updates: whether a newer release of the game exists, and, on a Frame that holds its own
|
||||
// build, updating to it without leaving the headset.
|
||||
//
|
||||
// Both answers come from Launcher/steam-frame-install.sh, which already knows how to compare
|
||||
// versions and how to build: `--check` reports, `--update` installs. The update reports its progress
|
||||
// through a status file rather than Steam notifications, which the Frame's Steam does not show.
|
||||
// The install writes update.conf beside the player's configuration when it put a build on this
|
||||
// machine, so the Update button appears only where it can actually work; installed from a PC, the
|
||||
// tab says to update there.
|
||||
namespace update_check {
|
||||
|
||||
enum class State {
|
||||
// This install cannot update itself: it was built on a PC, which is where updates are run.
|
||||
PcInstall,
|
||||
// It can, and nothing has been asked of it yet this session.
|
||||
Idle,
|
||||
Checking,
|
||||
UpToDate,
|
||||
Available,
|
||||
// An update is running beside the game, as a service of its own: it carries on if the game is
|
||||
// closed, and then opens it again when it is done.
|
||||
Updating,
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct Status {
|
||||
State state = State::PcInstall;
|
||||
// The release this build came from, or empty when it was built from a source tree.
|
||||
std::string installedRelease;
|
||||
// One line under the state: which release is available, what failed, or what step is running.
|
||||
std::string detail;
|
||||
// An update has installed a different build since this one started: it takes effect when the
|
||||
// game is started again.
|
||||
bool restartNeeded = false;
|
||||
};
|
||||
|
||||
// What the Updates tab draws. Cheap enough to call every frame: a check or update in flight is
|
||||
// answered from memory, and otherwise the files behind it are re-read at most once a second.
|
||||
Status Current();
|
||||
|
||||
// Whether there is an Updates tab to show at all: this install can update itself, or it at least
|
||||
// knows which release it is running. Neither is true of a build that came from somewhere else.
|
||||
inline bool Relevant(const Status& status) {
|
||||
return status.state != State::PcInstall || !status.installedRelease.empty();
|
||||
}
|
||||
|
||||
// Asks the install script whether a newer release exists, in the background. Returns at once.
|
||||
void StartCheck();
|
||||
|
||||
// Starts the update in the background: its own systemd user service runs the install script, which
|
||||
// reports each step to the file Current() reads, so the game can stay open and show the progress.
|
||||
// On false, `error` says why, for display.
|
||||
bool StartUpdate(std::string& error);
|
||||
|
||||
} // namespace update_check
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
// Adaptive resolution for the immersive eyes ([vr] adaptive_resolution), adapted from
|
||||
// heurazy/Wiicompiled_VR-PLUS. Once a second the pacing thread reports how many new eye frames
|
||||
// reached the headset and how many it should have had. A second well short of the target lowers
|
||||
// the eyes' scale by a step; three seconds in a row at the target raise it again. The steps are
|
||||
// slow so a single spike never resizes the eyes, which rebuilds their foveation maps.
|
||||
//
|
||||
// The eyes are rendered at the scaled size into a corner of the full swapchain image, and the
|
||||
// projection layer shows that corner, so the compositor does the upscaling and the swapchains are
|
||||
// never recreated. Nothing here depends on OpenXR (tests/vr_adaptive_resolution_tests.cpp).
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
class AdaptiveResolution {
|
||||
public:
|
||||
static constexpr float kMinimumScale = 0.7f;
|
||||
static constexpr float kStep = 0.1f;
|
||||
// Below this share of the target a second counts as short, at or above the other as on time.
|
||||
static constexpr float kShortShare = 0.85f;
|
||||
static constexpr float kOnTimeShare = 0.97f;
|
||||
static constexpr unsigned kOnTimeSecondsToRaise = 3;
|
||||
|
||||
float Scale() const noexcept { return scale_; }
|
||||
|
||||
// One second's measurement: `fps` new eye frames against `target_fps`. Off resets to full size.
|
||||
float Observe(float fps, float target_fps, bool enabled) noexcept {
|
||||
if (!enabled) {
|
||||
scale_ = 1.0f;
|
||||
on_time_seconds_ = 0;
|
||||
return scale_;
|
||||
}
|
||||
if (!(target_fps > 0.0f) || !(fps > 0.0f)) {
|
||||
return scale_;
|
||||
}
|
||||
if (fps < target_fps * kShortShare) {
|
||||
scale_ = std::max(kMinimumScale, scale_ - kStep);
|
||||
on_time_seconds_ = 0;
|
||||
} else if (fps >= target_fps * kOnTimeShare) {
|
||||
if (++on_time_seconds_ >= kOnTimeSecondsToRaise) {
|
||||
scale_ = std::min(1.0f, scale_ + kStep);
|
||||
on_time_seconds_ = 0;
|
||||
}
|
||||
} else {
|
||||
on_time_seconds_ = 0;
|
||||
}
|
||||
// Whole steps only, so float drift never leaves the eyes a pixel off full size.
|
||||
scale_ = static_cast<float>(static_cast<int>(scale_ / kStep + 0.5f)) * kStep;
|
||||
return scale_;
|
||||
}
|
||||
|
||||
// The scaled size of an eye `full` pixels across, never below 2.
|
||||
static unsigned Scaled(unsigned full, float scale) noexcept {
|
||||
if (scale >= 1.0f) {
|
||||
return full;
|
||||
}
|
||||
return std::max(2u, static_cast<unsigned>(static_cast<float>(full) * scale));
|
||||
}
|
||||
|
||||
private:
|
||||
float scale_ = 1.0f;
|
||||
unsigned on_time_seconds_ = 0;
|
||||
};
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
#include <SDL3/SDL_platform.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -146,7 +147,7 @@ std::string BuildMappingString() {
|
||||
mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ",";
|
||||
}
|
||||
}
|
||||
mapping += "platform:Windows,";
|
||||
mapping += std::string("platform:") + SDL_GetPlatform() + ",";
|
||||
return mapping;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "memory.h"
|
||||
@@ -61,8 +62,9 @@ constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
|
||||
constexpr size_t kHostPageSize = 0x1000;
|
||||
|
||||
// Only hosts that can expose a page larger than 4 KiB need to discover their
|
||||
// size at runtime; see RequiresCheckedAccess() in guest_flat_memory.h.
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
// size at runtime, or check it when the build assumes 4 KiB; see
|
||||
// RequiresCheckedAccess() in guest_flat_memory.h.
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE) || defined(MKW_GUEST_FLAT_VERIFY_PAGE_SIZE)
|
||||
size_t HostPageSize()
|
||||
{
|
||||
const long size = sysconf(_SC_PAGESIZE);
|
||||
@@ -521,6 +523,13 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
g_requiresCheckedAccess = HostPageSize() > kGuestPageSize;
|
||||
#elif defined(MKW_GUEST_FLAT_VERIFY_PAGE_SIZE)
|
||||
if (HostPageSize() != kGuestPageSize) {
|
||||
throw std::runtime_error(
|
||||
"This is a Steam Frame build, which assumes the headset's 4 KiB memory pages, but this "
|
||||
"kernel uses " + std::to_string(HostPageSize()) + "-byte pages. Build without "
|
||||
"--headset steam_frame for this device.");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (g_initialized) {
|
||||
|
||||
@@ -12,8 +12,16 @@
|
||||
#if defined(__AVX2__)
|
||||
#include <immintrin.h>
|
||||
#define MKW_AX_MIX_AVX2 1
|
||||
#define MKW_AX_MIX_NEON 0
|
||||
#elif defined(__aarch64__)
|
||||
// Advanced SIMD is architectural on AArch64: the NEON forms below are the arm64 counterparts of
|
||||
// the AVX2 kernels, processing eight 16-bit samples per step as two int32x4 halves.
|
||||
#include <arm_neon.h>
|
||||
#define MKW_AX_MIX_AVX2 0
|
||||
#define MKW_AX_MIX_NEON 1
|
||||
#else
|
||||
#define MKW_AX_MIX_AVX2 0
|
||||
#define MKW_AX_MIX_NEON 0
|
||||
#endif
|
||||
|
||||
namespace AxMixKernels {
|
||||
@@ -99,10 +107,63 @@ inline uint16_t MixAddRampAvx2(int32_t* out, const int16_t* input, uint32_t coun
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MKW_AX_MIX_NEON
|
||||
inline uint16_t MixAddRampNeon(int32_t* out, const int16_t* input, uint32_t count,
|
||||
uint16_t volume, uint16_t delta, int16_t& dpop) {
|
||||
uint32_t i = 0;
|
||||
int16_t last = dpop;
|
||||
if (count >= 8) {
|
||||
// Same ramp model as the AVX2 form: volume + k*delta stays inside int32 for every count
|
||||
// the AX mix uses, and the 16-bit wrap is applied only where the scalar loop applies it.
|
||||
const int32x4_t lanesLo = {0, 1, 2, 3};
|
||||
const int32x4_t lanesHi = {4, 5, 6, 7};
|
||||
const int32x4_t deltaVec = vdupq_n_s32(static_cast<int32_t>(delta));
|
||||
const int32x4_t wrapMask = vdupq_n_s32(0xFFFF);
|
||||
const int32x4_t blockStep = vdupq_n_s32(static_cast<int32_t>(delta) * 8);
|
||||
const int32x4_t base = vdupq_n_s32(static_cast<int32_t>(volume));
|
||||
int32x4_t rampLo = vmlaq_s32(base, lanesLo, deltaVec);
|
||||
int32x4_t rampHi = vmlaq_s32(base, lanesHi, deltaVec);
|
||||
int32x4_t lastBlock = vdupq_n_s32(0);
|
||||
for (; i + 8 <= count; i += 8) {
|
||||
const int16x8_t samples = vld1q_s16(input + i);
|
||||
const int32x4_t sLo = vmovl_s16(vget_low_s16(samples));
|
||||
const int32x4_t sHi = vmovl_s16(vget_high_s16(samples));
|
||||
// int16 x uint16 fits in int32: the low 32-bit product is exact. Signed 32 -> 16
|
||||
// saturation after the >> 15 is exactly clamp(-0x8000, 0x7fff).
|
||||
int32x4_t scaledLo = vshrq_n_s32(vmulq_s32(sLo, vandq_s32(rampLo, wrapMask)), 15);
|
||||
int32x4_t scaledHi = vshrq_n_s32(vmulq_s32(sHi, vandq_s32(rampHi, wrapMask)), 15);
|
||||
scaledLo = vmovl_s16(vqmovn_s32(scaledLo));
|
||||
scaledHi = vmovl_s16(vqmovn_s32(scaledHi));
|
||||
vst1q_s32(out + i, vaddq_s32(vld1q_s32(out + i), scaledLo));
|
||||
vst1q_s32(out + i + 4, vaddq_s32(vld1q_s32(out + i + 4), scaledHi));
|
||||
lastBlock = scaledHi;
|
||||
rampLo = vaddq_s32(rampLo, blockStep);
|
||||
rampHi = vaddq_s32(rampHi, blockStep);
|
||||
}
|
||||
if (i != 0) {
|
||||
last = static_cast<int16_t>(vgetq_lane_s32(lastBlock, 3));
|
||||
volume = static_cast<uint16_t>(static_cast<uint32_t>(vgetq_lane_s32(rampLo, 0)));
|
||||
}
|
||||
}
|
||||
for (; i < count; ++i) {
|
||||
const int32_t scaled =
|
||||
(static_cast<int32_t>(input[i]) * static_cast<int32_t>(volume)) >> 15;
|
||||
const int16_t sample = ClampToS16(scaled);
|
||||
out[i] += sample;
|
||||
volume = static_cast<uint16_t>(volume + delta);
|
||||
last = sample;
|
||||
}
|
||||
dpop = last;
|
||||
return volume;
|
||||
}
|
||||
#endif
|
||||
|
||||
inline uint16_t MixAddRamp(int32_t* out, const int16_t* input, uint32_t count,
|
||||
uint16_t volume, uint16_t delta, int16_t& dpop) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
return MixAddRampAvx2(out, input, count, volume, delta, dpop);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
return MixAddRampNeon(out, input, count, volume, delta, dpop);
|
||||
#else
|
||||
return MixAddRampScalar(out, input, count, volume, delta, dpop);
|
||||
#endif
|
||||
@@ -162,10 +223,50 @@ inline uint16_t ScaleRampAvx2(int16_t* samples, uint32_t count, uint16_t volume,
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MKW_AX_MIX_NEON
|
||||
inline uint16_t ScaleRampNeon(int16_t* samples, uint32_t count, uint16_t volume,
|
||||
uint16_t delta) {
|
||||
uint32_t i = 0;
|
||||
if (count >= 8) {
|
||||
const int32x4_t lanesLo = {0, 1, 2, 3};
|
||||
const int32x4_t lanesHi = {4, 5, 6, 7};
|
||||
const int32x4_t deltaVec = vdupq_n_s32(static_cast<int32_t>(delta));
|
||||
const int32x4_t wrapMask = vdupq_n_s32(0xFFFF);
|
||||
const int32x4_t blockStep = vdupq_n_s32(static_cast<int32_t>(delta) * 8);
|
||||
const int32x4_t base = vdupq_n_s32(static_cast<int32_t>(volume));
|
||||
int32x4_t rampLo = vmlaq_s32(base, lanesLo, deltaVec);
|
||||
int32x4_t rampHi = vmlaq_s32(base, lanesHi, deltaVec);
|
||||
for (; i + 8 <= count; i += 8) {
|
||||
const int16x8_t block = vld1q_s16(samples + i);
|
||||
const int32x4_t scaledLo = vshrq_n_s32(
|
||||
vmulq_s32(vmovl_s16(vget_low_s16(block)), vandq_s32(rampLo, wrapMask)), 15);
|
||||
const int32x4_t scaledHi = vshrq_n_s32(
|
||||
vmulq_s32(vmovl_s16(vget_high_s16(block)), vandq_s32(rampHi, wrapMask)), 15);
|
||||
// Signed 32 -> 16 saturation is exactly clamp(-0x8000, 0x7fff).
|
||||
vst1q_s16(samples + i, vcombine_s16(vqmovn_s32(scaledLo), vqmovn_s32(scaledHi)));
|
||||
rampLo = vaddq_s32(rampLo, blockStep);
|
||||
rampHi = vaddq_s32(rampHi, blockStep);
|
||||
}
|
||||
if (i != 0) {
|
||||
volume = static_cast<uint16_t>(static_cast<uint32_t>(vgetq_lane_s32(rampLo, 0)));
|
||||
}
|
||||
}
|
||||
for (; i < count; ++i) {
|
||||
const int32_t scaled =
|
||||
(static_cast<int32_t>(samples[i]) * static_cast<int32_t>(volume)) >> 15;
|
||||
samples[i] = ClampToS16(scaled);
|
||||
volume = static_cast<uint16_t>(volume + delta);
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
#endif
|
||||
|
||||
inline uint16_t ScaleRamp(int16_t* samples, uint32_t count, uint16_t volume,
|
||||
uint16_t delta) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
return ScaleRampAvx2(samples, count, volume, delta);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
return ScaleRampNeon(samples, count, volume, delta);
|
||||
#else
|
||||
return ScaleRampScalar(samples, count, volume, delta);
|
||||
#endif
|
||||
@@ -214,10 +315,34 @@ inline void MixAccumRamp32Avx2(int32_t* dst, const int32_t* src, const uint16_t*
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MKW_AX_MIX_NEON
|
||||
inline void MixAccumRamp32Neon(int32_t* dst, const int32_t* src, const uint16_t* ramp,
|
||||
uint32_t count) {
|
||||
uint32_t i = 0;
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
const int32x4_t source = vld1q_s32(src + i);
|
||||
// The ramp is unsigned 16-bit, so it is a non-negative int32 and the widening signed
|
||||
// multiply produces the exact 64-bit product (|product| < 2^47); the arithmetic >> 15
|
||||
// then narrows to the same bits the scalar (int32)(p >> 15) keeps.
|
||||
const int32x4_t gain = vreinterpretq_s32_u32(vmovl_u16(vld1_u16(ramp + i)));
|
||||
const int64x2_t lo = vshrq_n_s64(vmull_s32(vget_low_s32(source), vget_low_s32(gain)), 15);
|
||||
const int64x2_t hi = vshrq_n_s64(vmull_high_s32(source, gain), 15);
|
||||
const int32x4_t result = vcombine_s32(vmovn_s64(lo), vmovn_s64(hi));
|
||||
vst1q_s32(dst + i, vaddq_s32(vld1q_s32(dst + i), result));
|
||||
}
|
||||
for (; i < count; ++i) {
|
||||
dst[i] += static_cast<int32_t>(
|
||||
(static_cast<int64_t>(src[i]) * static_cast<int64_t>(ramp[i])) >> 15);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
inline void MixAccumRamp32(int32_t* dst, const int32_t* src, const uint16_t* ramp,
|
||||
uint32_t count) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
MixAccumRamp32Avx2(dst, src, ramp, count);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
MixAccumRamp32Neon(dst, src, ramp, count);
|
||||
#else
|
||||
MixAccumRamp32Scalar(dst, src, ramp, count);
|
||||
#endif
|
||||
@@ -270,9 +395,32 @@ inline void LoadBigEndian32Avx2(int32_t* dst, const uint8_t* src, size_t count)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MKW_AX_MIX_NEON
|
||||
// rev32 on byte lanes is the whole byte swap; four words per step.
|
||||
inline void StoreBigEndian32Neon(uint8_t* dst, const int32_t* src, size_t count) {
|
||||
size_t i = 0;
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
const uint8x16_t value = vreinterpretq_u8_s32(vld1q_s32(src + i));
|
||||
vst1q_u8(dst + i * sizeof(uint32_t), vrev32q_u8(value));
|
||||
}
|
||||
StoreBigEndian32Scalar(dst + i * sizeof(uint32_t), src + i, count - i);
|
||||
}
|
||||
|
||||
inline void LoadBigEndian32Neon(int32_t* dst, const uint8_t* src, size_t count) {
|
||||
size_t i = 0;
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
const uint8x16_t value = vld1q_u8(src + i * sizeof(uint32_t));
|
||||
vst1q_s32(dst + i, vreinterpretq_s32_u8(vrev32q_u8(value)));
|
||||
}
|
||||
LoadBigEndian32Scalar(dst + i, src + i * sizeof(uint32_t), count - i);
|
||||
}
|
||||
#endif
|
||||
|
||||
inline void StoreBigEndian32(uint8_t* dst, const int32_t* src, size_t count) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
StoreBigEndian32Avx2(dst, src, count);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
StoreBigEndian32Neon(dst, src, count);
|
||||
#else
|
||||
StoreBigEndian32Scalar(dst, src, count);
|
||||
#endif
|
||||
@@ -281,6 +429,8 @@ inline void StoreBigEndian32(uint8_t* dst, const int32_t* src, size_t count) {
|
||||
inline void LoadBigEndian32(int32_t* dst, const uint8_t* src, size_t count) {
|
||||
#if MKW_AX_MIX_AVX2
|
||||
LoadBigEndian32Avx2(dst, src, count);
|
||||
#elif MKW_AX_MIX_NEON
|
||||
LoadBigEndian32Neon(dst, src, count);
|
||||
#else
|
||||
LoadBigEndian32Scalar(dst, src, count);
|
||||
#endif
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
#include "hle_stubs.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include "memory.h"
|
||||
#include "recomp_mod_loader.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
extern "C" void GxNotifyGuestRamDmaWrite(uint32_t addr, uint32_t size);
|
||||
|
||||
// Native because a crafted Yaz0 run writes past the caller's buffer
|
||||
// (github.com/vabold/szsHaxx)
|
||||
// https://github.com/vabold/Kinoko/blob/main/source/egg/core/Decomp.cc
|
||||
|
||||
extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
{
|
||||
const uint32_t expandSize = (static_cast<uint32_t>(MemoryInline::FlatRead8(src + 4)) << 24) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 5)) << 16) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 6)) << 8) |
|
||||
static_cast<uint32_t>(MemoryInline::FlatRead8(src + 7));
|
||||
|
||||
template <typename Access>
|
||||
static uint32_t DecodeSZS(uint32_t src, uint32_t expandSize, Access& access) {
|
||||
uint32_t srcIdx = 16;
|
||||
uint32_t dstIdx = 0;
|
||||
uint32_t mask = 0;
|
||||
@@ -22,15 +21,15 @@ extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
|
||||
while (static_cast<int32_t>(dstIdx) < static_cast<int32_t>(expandSize)) {
|
||||
if (mask == 0) {
|
||||
flags = MemoryInline::FlatRead8(src + srcIdx++);
|
||||
flags = access.ReadSource(srcIdx++);
|
||||
mask = 0x80;
|
||||
}
|
||||
|
||||
if ((flags & mask) != 0) {
|
||||
MemoryInline::FlatWrite8(dst + dstIdx++, MemoryInline::FlatRead8(src + srcIdx++));
|
||||
access.WriteOutput(dstIdx++, access.ReadSource(srcIdx++));
|
||||
} else {
|
||||
const uint32_t high = MemoryInline::FlatRead8(src + srcIdx);
|
||||
const uint32_t low = MemoryInline::FlatRead8(src + srcIdx + 1);
|
||||
const uint32_t high = access.ReadSource(srcIdx);
|
||||
const uint32_t low = access.ReadSource(srcIdx + 1);
|
||||
srcIdx += 2;
|
||||
|
||||
const uint32_t rep = (high << 8) | low;
|
||||
@@ -48,7 +47,7 @@ extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
uint32_t count = rep >> 12;
|
||||
count = count != 0
|
||||
? count + 2
|
||||
: static_cast<uint32_t>(MemoryInline::FlatRead8(src + srcIdx++)) + 18;
|
||||
: static_cast<uint32_t>(access.ReadSource(srcIdx++)) + 18;
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (dstIdx >= expandSize) {
|
||||
@@ -59,7 +58,7 @@ extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
"The game stopped decoding a malformed Yaz0 file.");
|
||||
std::abort();
|
||||
}
|
||||
MemoryInline::FlatWrite8(dst + dstIdx++, MemoryInline::FlatRead8(dst + copyIdx++));
|
||||
access.WriteOutput(dstIdx++, access.ReadOutput(copyIdx++));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +68,61 @@ extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
return expandSize;
|
||||
}
|
||||
|
||||
struct GuestSZSAccess {
|
||||
uint32_t src;
|
||||
uint32_t dst;
|
||||
|
||||
uint8_t ReadSource(uint32_t offset) const { return MemoryInline::FlatRead8(src + offset); }
|
||||
uint8_t ReadOutput(uint32_t offset) const { return MemoryInline::FlatRead8(dst + offset); }
|
||||
void WriteOutput(uint32_t offset, uint8_t value) const {
|
||||
MemoryInline::FlatWrite8(dst + offset, value);
|
||||
}
|
||||
};
|
||||
|
||||
struct HostSZSAccess {
|
||||
const uint8_t* src;
|
||||
uint8_t* dst;
|
||||
|
||||
uint8_t ReadSource(uint32_t offset) const { return src[offset]; }
|
||||
uint8_t ReadOutput(uint32_t offset) const { return dst[offset]; }
|
||||
void WriteOutput(uint32_t offset, uint8_t value) const { dst[offset] = value; }
|
||||
};
|
||||
|
||||
static bool HasDeferredReadPages(uint32_t address, size_t length) {
|
||||
const uint32_t first = address >> MemoryInline::kPageShift;
|
||||
const uint32_t last = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(address) + length - 1) >> MemoryInline::kPageShift);
|
||||
for (uint32_t page = first; page <= last; ++page) {
|
||||
if (MemoryInline::g_deferredReadCoveredPages[page] != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
{
|
||||
const uint32_t expandSize = (static_cast<uint32_t>(MemoryInline::FlatRead8(src + 4)) << 24) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 5)) << 16) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 6)) << 8) |
|
||||
static_cast<uint32_t>(MemoryInline::FlatRead8(src + 7));
|
||||
|
||||
// A token produces at least one byte; all-literal data is the largest
|
||||
// valid input (one flag per eight output bytes). Three extra bytes cover
|
||||
// the final malformed run before its output-overrun check aborts.
|
||||
const size_t maxSourceBytes = 16ull + expandSize + (static_cast<uint64_t>(expandSize) + 7) / 8 + 3;
|
||||
if (expandSize != 0 && expandSize <= static_cast<uint32_t>(std::numeric_limits<int32_t>::max()) &&
|
||||
!GuestFlat::RequiresCheckedAccess() &&
|
||||
Memory::Contains(src, maxSourceBytes) && Memory::Contains(dst, expandSize) &&
|
||||
!HasDeferredReadPages(src, maxSourceBytes) && !HasDeferredReadPages(dst, expandSize) &&
|
||||
!RecompMod::ExecutableWriteGuardMayHit(dst, expandSize)) {
|
||||
HostSZSAccess access{Memory::GetPointer(src, maxSourceBytes), Memory::GetPointer(dst, expandSize)};
|
||||
const uint32_t decoded = DecodeSZS(src, expandSize, access);
|
||||
GxNotifyGuestRamDmaWrite(dst, decoded);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
GuestSZSAccess access{src, dst};
|
||||
return DecodeSZS(src, expandSize, access);
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE(80218C2C, EGG_Decomp_decodeSZS_80218c2c, uint32_t,
|
||||
(uint32_t src, uint32_t dst), (src, dst));
|
||||
@@ -41,6 +41,11 @@ static void GX__ClearVtxDesc_8016dc34_gx() {
|
||||
changed |= g_hleGxState.vtxDesc[i] != GX_NONE;
|
||||
g_hleGxState.vtxDesc[i]=GX_NONE;
|
||||
}
|
||||
// Nintendo's GXClearVtxDesc retains a direct position descriptor. Aurora's
|
||||
// GXClearVtxDesc does the same; keep the HLE mirror in sync so the next
|
||||
// draw does not publish GX_VA_POS=GX_NONE and abort shader generation.
|
||||
changed |= g_hleGxState.vtxDesc[GX_VA_POS] != GX_DIRECT;
|
||||
g_hleGxState.vtxDesc[GX_VA_POS] = GX_DIRECT;
|
||||
if (changed) g_hleGxState.InvalidateVtxLayoutHash();
|
||||
// GXClearVtxDesc resets descriptors only; array base/stride state persists.
|
||||
GXClearVtxDesc();
|
||||
@@ -49,6 +54,7 @@ extern "C" void GX__ClearVtxDesc_8016dc34() {
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
g_gxGameVertexState.vtxDesc[i] = GX_NONE;
|
||||
}
|
||||
g_gxGameVertexState.vtxDesc[GX_VA_POS] = GX_DIRECT;
|
||||
GxThread::Post(&GX__ClearVtxDesc_8016dc34_gx);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(8016dc34, GX__ClearVtxDesc_8016dc34, (), ());
|
||||
|
||||
@@ -648,9 +648,10 @@ extern "C" int32_t Network_HLE_OpenDevice(const char* path, uint32_t mode) {
|
||||
if (!path) {
|
||||
return -101;
|
||||
}
|
||||
if (!RuntimeConfigFile::NetworkEnabled(true)) {
|
||||
// The guest opens several /dev/net nodes at boot and retries; report the
|
||||
// reason online will not work exactly once.
|
||||
const bool isIpTop = std::strcmp(path, "/dev/net/ip/top") == 0;
|
||||
const bool isSsl = std::strcmp(path, "/dev/net/ssl") == 0;
|
||||
// KD and NCD provide local identity/configuration services even offline.
|
||||
if ((isIpTop || isSsl) && !RuntimeConfigFile::NetworkEnabled(true)) {
|
||||
static bool reported = false;
|
||||
if (!reported) {
|
||||
reported = true;
|
||||
@@ -665,10 +666,10 @@ extern "C" int32_t Network_HLE_OpenDevice(const char* path, uint32_t mode) {
|
||||
kind = DeviceKind::KdTime;
|
||||
} else if (std::strcmp(path, "/dev/net/ncd/manage") == 0) {
|
||||
kind = DeviceKind::NcdManage;
|
||||
} else if (std::strcmp(path, "/dev/net/ip/top") == 0) {
|
||||
} else if (isIpTop) {
|
||||
kind = DeviceKind::IpTop;
|
||||
EnsureSocketRuntime();
|
||||
} else if (std::strcmp(path, "/dev/net/ssl") == 0) {
|
||||
} else if (isSsl) {
|
||||
kind = DeviceKind::Ssl;
|
||||
EnsureSocketRuntime();
|
||||
} else {
|
||||
|
||||
@@ -242,6 +242,7 @@ void WritePollResults(uint32_t outAddress,
|
||||
const std::vector<NetworkPollContract::CopiedDescriptor>& descriptors);
|
||||
|
||||
// network_socket.cpp
|
||||
int32_t DeleteWiiSocket(uint32_t fd);
|
||||
void CleanupAllWiiSockets();
|
||||
sockaddr_in ReadWiiSockAddr(uint32_t addr);
|
||||
int32_t HandleIpTopIoctl(uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf,
|
||||
|
||||
@@ -21,7 +21,7 @@ static int32_t NewWiiSocket(uint32_t af, uint32_t type, uint32_t protocol) {
|
||||
return wiiFd;
|
||||
}
|
||||
|
||||
static int32_t DeleteWiiSocket(uint32_t fd) {
|
||||
int32_t DeleteWiiSocket(uint32_t fd) {
|
||||
WiiSocket* s = GetWiiSocket(fd);
|
||||
if (!s) {
|
||||
return -SO_EBADF;
|
||||
@@ -467,7 +467,8 @@ int32_t HandleIpTopIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const s
|
||||
const int ret = sendto(s->native, reinterpret_cast<const char*>(sendData), static_cast<int>(sendSize),
|
||||
static_cast<int>(flags), destPtr, destLen);
|
||||
const int hostError = ret < 0 ? NativeLastError() : 0;
|
||||
int32_t result = SocketResult(ret);
|
||||
// Diagnostics may change the native error; use the send result captured above.
|
||||
int32_t result = ret >= 0 ? SocketResult(ret) : SocketErrorResult(hostError);
|
||||
if (patchedWrite && ret == static_cast<int>(sendSize)) {
|
||||
result = static_cast<int32_t>(in[0].size);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
#include "network_internal.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/error.h>
|
||||
#include <mbedtls/net_sockets.h>
|
||||
#include <mbedtls/ssl.h>
|
||||
#include <mbedtls/x509_crt.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#endif
|
||||
|
||||
namespace NetworkHle {
|
||||
|
||||
@@ -38,6 +54,9 @@ constexpr int kMaxSslSessions = 4;
|
||||
struct SslSession {
|
||||
bool active = false;
|
||||
bool handshaked = false;
|
||||
// A failed POSIX TLS write cannot be resumed with a new guest buffer.
|
||||
// Keep the slot and socket ownership intact until explicit teardown.
|
||||
bool failed = false;
|
||||
bool plaintextWfc = false;
|
||||
uint32_t socketFd = UINT32_MAX;
|
||||
NativeSocket native = kInvalidSocket;
|
||||
@@ -56,6 +75,11 @@ struct SslSession {
|
||||
CredHandle cred{};
|
||||
CtxtHandle context{};
|
||||
SecPkgContext_StreamSizes sizes{};
|
||||
#else
|
||||
bool haveSsl = false;
|
||||
mbedtls_ssl_context sslContext{};
|
||||
mbedtls_ssl_config sslConfig{};
|
||||
mbedtls_net_context netContext{};
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -539,26 +563,298 @@ static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
|
||||
return copied == 0 ? SSL_ERR_ZERO : static_cast<int32_t>(copied);
|
||||
}
|
||||
#else
|
||||
// Windows gets TLS for free from the OS (Schannel, above) - mbed TLS is this project's own
|
||||
// vendored equivalent for everywhere else (runtime/third_party/mbedtls, see runtime/CMakeLists.txt
|
||||
// for why mbed TLS specifically). The CA chain and RNG are expensive to set up (parsing ~150 root
|
||||
// certificates, seeding entropy) and read-only once built, so they're shared process-wide instead
|
||||
// of being redone per SSL session.
|
||||
static bool g_mbedtlsCaLoaded = false;
|
||||
static mbedtls_x509_crt g_mbedtlsCaChain;
|
||||
static mbedtls_entropy_context g_mbedtlsEntropy;
|
||||
static mbedtls_ctr_drbg_context g_mbedtlsCtrDrbg;
|
||||
|
||||
static ssize_t SendSslSocket(NativeSocket socket, const uint8_t* data, size_t size) {
|
||||
#ifdef __APPLE__
|
||||
const int noSigPipe = 1;
|
||||
if (setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return send(socket, data, size, 0);
|
||||
#else
|
||||
return send(socket, data, size, MSG_NOSIGNAL);
|
||||
#endif
|
||||
}
|
||||
|
||||
static int MbedtlsSend(void* context, const unsigned char* data, size_t size) {
|
||||
const auto* net = static_cast<mbedtls_net_context*>(context);
|
||||
const ssize_t result = SendSslSocket(net->fd, data, size);
|
||||
if (result >= 0) {
|
||||
return static_cast<int>(result);
|
||||
}
|
||||
if (errno == EINTR) {
|
||||
return MBEDTLS_ERR_SSL_WANT_WRITE;
|
||||
}
|
||||
if (errno == EPIPE || errno == ECONNRESET) {
|
||||
return MBEDTLS_ERR_NET_CONN_RESET;
|
||||
}
|
||||
return MBEDTLS_ERR_NET_SEND_FAILED;
|
||||
}
|
||||
|
||||
static int MbedtlsRecv(void* context, unsigned char* data, size_t size) {
|
||||
const int result = mbedtls_net_recv(context, data, size);
|
||||
// Blocking socket timeouts must leave the TLS session retryable.
|
||||
if (result == MBEDTLS_ERR_NET_RECV_FAILED && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
return MBEDTLS_ERR_SSL_WANT_READ;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Mirrors ax_mix.cpp's FindDspCoefficientRom exactly - same three places a bundled asset can live
|
||||
// depending on platform and how the binary was launched (next to the desktop executable, the
|
||||
// Android app's own data directory, or a source-tree checkout during development).
|
||||
static std::optional<std::filesystem::path> FindCaCertificateBundle() {
|
||||
if (const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
|
||||
const auto adjacent = *executableDirectory / "cacert.pem";
|
||||
if (std::filesystem::is_regular_file(adjacent)) {
|
||||
return adjacent;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
const auto androidAsset = RuntimeConfigFile::ApplicationDataDirectory() / "cacert.pem";
|
||||
if (std::filesystem::is_regular_file(androidAsset)) {
|
||||
return androidAsset;
|
||||
}
|
||||
#endif
|
||||
|
||||
for (auto base = std::filesystem::current_path(); !base.empty();) {
|
||||
const auto sourceTreeAsset = base / "runtime" / "assets" / "certs" / "cacert.pem";
|
||||
if (std::filesystem::is_regular_file(sourceTreeAsset)) {
|
||||
return sourceTreeAsset;
|
||||
}
|
||||
const auto parent = base.parent_path();
|
||||
if (parent == base) {
|
||||
break;
|
||||
}
|
||||
base = parent;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Lazy, once-per-process: the first real SSL use pays for parsing the CA bundle and seeding the
|
||||
// RNG, every session after that reuses the result. Returns false (logging once) if the bundle is
|
||||
// missing or unparseable - callers treat that as a normal handshake failure, not a crash, since a
|
||||
// missing TLS root store shouldn't take down gameplay that never touches the network.
|
||||
static bool EnsureMbedtlsGlobalsInitialized() {
|
||||
static const bool initialized = [] {
|
||||
mbedtls_x509_crt_init(&g_mbedtlsCaChain);
|
||||
mbedtls_entropy_init(&g_mbedtlsEntropy);
|
||||
mbedtls_ctr_drbg_init(&g_mbedtlsCtrDrbg);
|
||||
|
||||
const char* personalization = "wiicompiled_ssl";
|
||||
if (mbedtls_ctr_drbg_seed(&g_mbedtlsCtrDrbg, mbedtls_entropy_func, &g_mbedtlsEntropy,
|
||||
reinterpret_cast<const unsigned char*>(personalization),
|
||||
std::strlen(personalization)) != 0) {
|
||||
NetFail("ssl: failed to seed TLS random number generator");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bundle = FindCaCertificateBundle();
|
||||
if (!bundle) {
|
||||
NetFail("ssl: missing TLS root CA bundle (cacert.pem) - HTTPS connections will fail");
|
||||
return false;
|
||||
}
|
||||
const int parseRet = mbedtls_x509_crt_parse_file(&g_mbedtlsCaChain, bundle->string().c_str());
|
||||
if (parseRet < 0) {
|
||||
char errorBuffer[256];
|
||||
mbedtls_strerror(parseRet, errorBuffer, sizeof(errorBuffer));
|
||||
NetFail("ssl: failed to parse CA bundle %s: %s", bundle->string().c_str(), errorBuffer);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
g_mbedtlsCaLoaded = initialized;
|
||||
return initialized;
|
||||
}
|
||||
|
||||
// Builds this session's mbed TLS handshake state exactly once - a second call (e.g. the handshake
|
||||
// re-running after DOHANDSHAKE was already satisfied) is a no-op via ssl.haveSsl.
|
||||
static int32_t EnsureMbedtlsSession(SslSession& ssl) {
|
||||
if (ssl.haveSsl) {
|
||||
return SSL_OK;
|
||||
}
|
||||
if (!EnsureMbedtlsGlobalsInitialized()) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
|
||||
mbedtls_ssl_init(&ssl.sslContext);
|
||||
mbedtls_ssl_config_init(&ssl.sslConfig);
|
||||
if (mbedtls_ssl_config_defaults(&ssl.sslConfig, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
|
||||
MBEDTLS_SSL_PRESET_DEFAULT) != 0) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
// Real certificate validation, matching Schannel's SCH_CRED_AUTO_CRED_VALIDATION on the
|
||||
// Windows side above - a self-signed or wrong-hostname certificate must fail the handshake,
|
||||
// not just get logged.
|
||||
mbedtls_ssl_conf_authmode(&ssl.sslConfig, MBEDTLS_SSL_VERIFY_REQUIRED);
|
||||
mbedtls_ssl_conf_ca_chain(&ssl.sslConfig, &g_mbedtlsCaChain, nullptr);
|
||||
mbedtls_ssl_conf_rng(&ssl.sslConfig, mbedtls_ctr_drbg_random, &g_mbedtlsCtrDrbg);
|
||||
if (mbedtls_ssl_setup(&ssl.sslContext, &ssl.sslConfig) != 0) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
// The hostname drives both SNI (which certificate the server presents) and the CN/SAN check
|
||||
// mbedtls_ssl_conf_authmode enforces above - required, not optional, same reasoning as the
|
||||
// Windows path's own "refuse an empty hostname" check just above SslHandshakeImpl.
|
||||
mbedtls_ssl_set_hostname(&ssl.sslContext, ssl.hostname.c_str());
|
||||
|
||||
ssl.netContext.fd = static_cast<int>(ssl.native);
|
||||
mbedtls_ssl_set_bio(&ssl.sslContext, &ssl.netContext, MbedtlsSend, MbedtlsRecv, nullptr);
|
||||
|
||||
ssl.haveSsl = true;
|
||||
return SSL_OK;
|
||||
}
|
||||
|
||||
static void ClearSslSession(SslSession& ssl) {
|
||||
if (ssl.haveSsl) {
|
||||
mbedtls_ssl_free(&ssl.sslContext);
|
||||
mbedtls_ssl_config_free(&ssl.sslConfig);
|
||||
}
|
||||
ssl = {};
|
||||
}
|
||||
|
||||
static int32_t SslHandshakeImpl(SslSession&) {
|
||||
static int32_t SslHandshakeImpl(SslSession& ssl) {
|
||||
if (ssl.plaintextWfc) {
|
||||
ssl.handshaked = true;
|
||||
return SSL_OK;
|
||||
}
|
||||
if (ssl.handshaked) {
|
||||
return SSL_OK;
|
||||
}
|
||||
if (ssl.native == kInvalidSocket) {
|
||||
return SSL_ERR_SYSCALL;
|
||||
}
|
||||
// mbed TLS can authenticate a certificate chain without authenticating a server identity when
|
||||
// no hostname is set - refuse that ambiguous mode, matching the Windows path's own check.
|
||||
if (ssl.hostname.empty()) {
|
||||
return SSL_ERR_VCOMMONNAME;
|
||||
}
|
||||
|
||||
const int32_t setupRet = EnsureMbedtlsSession(ssl);
|
||||
if (setupRet != SSL_OK) {
|
||||
return setupRet;
|
||||
}
|
||||
|
||||
// Receive timeouts are retryable, but the handshake must still terminate.
|
||||
const auto handshakeDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
|
||||
int handshakeRet;
|
||||
while ((handshakeRet = mbedtls_ssl_handshake(&ssl.sslContext)) != 0) {
|
||||
if (handshakeRet == MBEDTLS_ERR_SSL_WANT_READ || handshakeRet == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||
if (std::chrono::steady_clock::now() >= handshakeDeadline) {
|
||||
NetFail("ssl handshake TIMED OUT host=%s", ssl.hostname.c_str());
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
char errorBuffer[256];
|
||||
mbedtls_strerror(handshakeRet, errorBuffer, sizeof(errorBuffer));
|
||||
NetFail("ssl handshake FAILED host=%s mbedtls_err=%s", ssl.hostname.c_str(), errorBuffer);
|
||||
return handshakeRet == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED ? SSL_ERR_VCOMMONNAME : SSL_ERR_FAILED;
|
||||
}
|
||||
ssl.handshaked = true;
|
||||
return SSL_OK;
|
||||
}
|
||||
|
||||
static int32_t FailSslWrite(SslSession& ssl) {
|
||||
ssl.failed = true;
|
||||
// Stop transport I/O without deleting the guest descriptor or freeing a
|
||||
// session still referenced by the IOCTLV_NET_SSL_WRITE caller.
|
||||
::shutdown(ssl.native, SHUT_RDWR);
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
|
||||
static int32_t SslWrite(SslSession&, const uint8_t*, uint32_t) {
|
||||
return SSL_ERR_FAILED;
|
||||
static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
|
||||
if (!data || size == 0) {
|
||||
return SSL_ERR_ZERO;
|
||||
}
|
||||
const int32_t handshakeRet = SslHandshake(ssl);
|
||||
if (handshakeRet != SSL_OK) {
|
||||
return handshakeRet;
|
||||
}
|
||||
|
||||
if (ssl.plaintextWfc) {
|
||||
uint32_t total = 0;
|
||||
while (total < size) {
|
||||
const ssize_t sent = SendSslSocket(ssl.native, data + total, size - total);
|
||||
if (sent <= 0) {
|
||||
return SSL_ERR_SYSCALL;
|
||||
}
|
||||
total += static_cast<uint32_t>(sent);
|
||||
}
|
||||
return static_cast<int32_t>(total);
|
||||
}
|
||||
|
||||
// mbed TLS is allowed to write fewer bytes than requested in one call (e.g. when size exceeds
|
||||
// one TLS record) - the caller must resend the remainder starting from where it left off, so
|
||||
// loop here until every byte is actually written rather than returning the first partial count.
|
||||
uint32_t totalWritten = 0;
|
||||
const auto writeDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(15);
|
||||
while (totalWritten < size) {
|
||||
const int ret = mbedtls_ssl_write(&ssl.sslContext, data + totalWritten, size - totalWritten);
|
||||
if (ret > 0) {
|
||||
totalWritten += static_cast<uint32_t>(ret);
|
||||
continue;
|
||||
}
|
||||
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||
if (std::chrono::steady_clock::now() >= writeDeadline) {
|
||||
return FailSslWrite(ssl);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return FailSslWrite(ssl);
|
||||
}
|
||||
return static_cast<int32_t>(totalWritten);
|
||||
}
|
||||
|
||||
static int32_t SslRead(SslSession&, uint8_t*, uint32_t) {
|
||||
return SSL_ERR_FAILED;
|
||||
static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
|
||||
if (!out || size == 0) {
|
||||
return SSL_ERR_ZERO;
|
||||
}
|
||||
const int32_t handshakeRet = SslHandshake(ssl);
|
||||
if (handshakeRet != SSL_OK) {
|
||||
return handshakeRet;
|
||||
}
|
||||
|
||||
if (ssl.plaintextWfc) {
|
||||
const ssize_t ret = recv(ssl.native, out, size, 0);
|
||||
if (ret == 0) {
|
||||
return SSL_ERR_ZERO;
|
||||
}
|
||||
if (ret < 0) {
|
||||
return SSL_ERR_RAGAIN;
|
||||
}
|
||||
return static_cast<int32_t>(ret);
|
||||
}
|
||||
|
||||
const int ret = mbedtls_ssl_read(&ssl.sslContext, out, size);
|
||||
if (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
|
||||
return SSL_ERR_ZERO;
|
||||
}
|
||||
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||
return SSL_ERR_RAGAIN;
|
||||
}
|
||||
if (ret < 0) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
// The handshake runs on every SSL read/write, so a failure repeats for as long
|
||||
// as the session lives; report only the first one.
|
||||
static int32_t SslHandshake(SslSession& ssl) {
|
||||
if (ssl.failed) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
const int32_t result = SslHandshakeImpl(ssl);
|
||||
if (result != SSL_OK && !ssl.loggedHandshakeFail) {
|
||||
ssl.loggedHandshakeFail = true;
|
||||
@@ -640,6 +936,14 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
|
||||
const int timeoutMs = 15000;
|
||||
setsockopt(socket->native, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
|
||||
setsockopt(socket->native, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
|
||||
#else
|
||||
// Match the Windows 15s timeout so a peer that accepts the TCP connection but stalls
|
||||
// during the TLS handshake or a later read/write can't hang this thread forever. POSIX
|
||||
// takes a struct timeval here, not a plain millisecond count like Windows does.
|
||||
struct timeval timeout {};
|
||||
timeout.tv_sec = 15;
|
||||
setsockopt(socket->native, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(socket->native, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
#endif
|
||||
WriteSslReturn(in, SSL_OK);
|
||||
return 0;
|
||||
@@ -656,6 +960,11 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
|
||||
WriteSslReturn(in, SSL_ERR_ID);
|
||||
return 0;
|
||||
}
|
||||
// Reject before NAS buffering can acknowledge data on a failed session.
|
||||
if (g_sslSessions[sslId].failed) {
|
||||
WriteSslReturn(in, SSL_ERR_FAILED);
|
||||
return 0;
|
||||
}
|
||||
if (out.size() < 2 || !out[1].address) {
|
||||
WriteSslReturn(in, SSL_ERR_FAILED);
|
||||
return 0;
|
||||
|
||||
@@ -222,7 +222,6 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
|
||||
throw;
|
||||
}
|
||||
DecrementSchedulerDisableCount();
|
||||
RunDeferredReschedule(cpu);
|
||||
}
|
||||
}
|
||||
} catch (const ::Memory::AccessViolation& e) {
|
||||
@@ -233,7 +232,7 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
|
||||
// Host DNS workers never touch guest memory. Commit their output here on
|
||||
// the scheduler thread, waking synchronous IOS waiters or queuing async IOS
|
||||
// callbacks before the callback drain below.
|
||||
bool completionNeedsReschedule = false;
|
||||
bool completionNeedsReschedule = handledAny;
|
||||
if (Network_HLE_ProcessCompletions(cpu)) {
|
||||
handledAny = true;
|
||||
completionNeedsReschedule = true;
|
||||
@@ -446,15 +445,18 @@ PPC_NATIVE_OVERRIDE_VOID(801A08E0, OS__SetPeriodicAlarm_801a08e0, (CpuContext* c
|
||||
// returning 0 when the manager pointer (0x80386298) is null.
|
||||
extern "C" uint32_t RFLiIsWorking_HLE_800bd860()
|
||||
{
|
||||
// Pump alarms/callbacks on the current guest thread when available. Using a
|
||||
// detached persistent context here can leave the busy loop waiting on work
|
||||
// that completed on the wrong scheduling context.
|
||||
CpuContext* cpu = TryGetCpuContext();
|
||||
if (!cpu) {
|
||||
cpu = &GetPersistentCpuContext();
|
||||
}
|
||||
// Alarm callbacks interrupt the caller; keep their register writes private.
|
||||
GuestInterruptCallbackContext interrupt;
|
||||
CpuContext* cpu = interrupt.get();
|
||||
EnsureSda1Base(cpu);
|
||||
ProcessAlarmQueue(cpu, 32);
|
||||
IncrementSchedulerDisableCount();
|
||||
try {
|
||||
ProcessAlarmQueue(cpu, 32);
|
||||
} catch (...) {
|
||||
DecrementSchedulerDisableCount();
|
||||
throw;
|
||||
}
|
||||
DecrementSchedulerDisableCount();
|
||||
|
||||
// Now return the actual "working" status
|
||||
constexpr uint32_t kRflManagerPtrAddr = 0x80386298u;
|
||||
|
||||
@@ -104,10 +104,15 @@ extern "C" void DVDInit_8015EA1C();
|
||||
extern "C" uint32_t g_dvdFstReservedBase;
|
||||
extern "C" uint32_t g_dvdFstReservedSize;
|
||||
|
||||
// Byte-wise copy into guest RAM plus the DMA notification the GX caches need.
|
||||
// DVD DMA normally targets ordinary RAM. Keep scalar writes for executable
|
||||
// ranges so the translated-code write guard still sees every modified byte.
|
||||
static void CopyToGuestAsDma(uint32_t dest, const uint8_t* data, size_t size) {
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
Memory::Write8(dest + static_cast<uint32_t>(i), data[i]);
|
||||
if (size != 0 && !RecompMod::ExecutableWriteGuardMayHit(dest, size)) {
|
||||
std::memcpy(Memory::GetPointer(dest, size), data, size);
|
||||
} else {
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
Memory::Write8(dest + static_cast<uint32_t>(i), data[i]);
|
||||
}
|
||||
}
|
||||
GxNotifyGuestRamDmaWrite(dest, static_cast<uint32_t>(size));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
#include "nand_internal.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <linux/fs.h>
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
// Local helpers
|
||||
// ============================================================================
|
||||
@@ -44,6 +52,32 @@ static FileHandle* ResolveNandFileHandle(const char* who, uint32_t fileInfoPtr)
|
||||
// The synchronous RVL NAND* library
|
||||
// ============================================================================
|
||||
|
||||
static bool RenameNoReplace(const std::filesystem::path& from,
|
||||
const std::filesystem::path& to,
|
||||
std::error_code& error) {
|
||||
#ifdef _WIN32
|
||||
if (MoveFileExW(from.c_str(), to.c_str(), MOVEFILE_WRITE_THROUGH)) {
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
error = std::error_code(static_cast<int>(GetLastError()), std::system_category());
|
||||
return false;
|
||||
#elif defined(__linux__)
|
||||
const int result = syscall(SYS_renameat2, AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE);
|
||||
if (result == 0) {
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
error = std::error_code(errno, std::generic_category());
|
||||
return false;
|
||||
#else
|
||||
(void)from;
|
||||
(void)to;
|
||||
error = std::make_error_code(std::errc::operation_not_supported);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" int32_t NANDInit_HLE(void) {
|
||||
// Initialize ISFS
|
||||
ISFS_OpenLib_Initialize(&GetPersistentCpuContext());
|
||||
@@ -348,6 +382,11 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
|
||||
PPC_NATIVE_OVERRIDE(8019BBE0, NANDCreateDir_HLE, int32_t, (uint32_t pathPtr, uint32_t perm, uint32_t attr), (pathPtr, perm, attr));
|
||||
|
||||
extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
|
||||
// A cross-mount move is implemented as several host operations. Keep two
|
||||
// guest moves from interleaving those operations and corrupting recovery.
|
||||
static std::mutex moveMutex;
|
||||
std::lock_guard<std::mutex> lock(moveMutex);
|
||||
|
||||
const char* srcPath = srcPathPtr ? (const char*)Memory::GetPointer(srcPathPtr) : nullptr;
|
||||
const char* dstPath = dstPathPtr ? (const char*)Memory::GetPointer(dstPathPtr) : nullptr;
|
||||
|
||||
@@ -383,6 +422,112 @@ extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
|
||||
return NAND_RESULT_OK;
|
||||
}
|
||||
|
||||
// Flatpak can expose the managed NAND and an external Riivolution save
|
||||
// directory as separate mounts. Linux cannot rename across mounts, but
|
||||
// nandMove must still work for files such as banner.bin. Preserve the
|
||||
// operation's semantics with a copy followed by source removal.
|
||||
if (ec == std::errc::cross_device_link) {
|
||||
static std::atomic<uint64_t> moveSequence{0};
|
||||
#ifdef _WIN32
|
||||
const auto processId = GetCurrentProcessId();
|
||||
#else
|
||||
const auto processId = getpid();
|
||||
#endif
|
||||
std::filesystem::path scratchHost;
|
||||
std::error_code scratchEc;
|
||||
for (unsigned attempt = 0; attempt < 128; ++attempt) {
|
||||
const auto name = ".nandmove-" + std::to_string(processId) + "-" +
|
||||
std::to_string(moveSequence.fetch_add(1)) + "-" +
|
||||
std::to_string(attempt);
|
||||
const auto candidate = dstDirectoryHost / name;
|
||||
scratchEc.clear();
|
||||
if (std::filesystem::create_directory(candidate, scratchEc)) {
|
||||
scratchHost = candidate;
|
||||
break;
|
||||
}
|
||||
if (scratchEc && scratchEc != std::errc::file_exists) {
|
||||
LogNandError("NANDMove", "failed to claim temporary directory '%s': %s",
|
||||
HostPathText(candidate).c_str(), scratchEc.message().c_str());
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
}
|
||||
if (scratchHost.empty()) {
|
||||
LogNandError("NANDMove", "could not claim a unique temporary directory");
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
const bool sourceIsDirectory = IsDirectory(srcHost);
|
||||
const std::filesystem::path tempHost = scratchHost / srcName;
|
||||
const auto cleanupScratch = [&]() {
|
||||
std::error_code cleanupEc;
|
||||
std::filesystem::remove_all(scratchHost, cleanupEc);
|
||||
if (cleanupEc) {
|
||||
LogNandError("NANDMove", "failed to clean up temporary directory '%s': %s",
|
||||
HostPathText(scratchHost).c_str(), cleanupEc.message().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
std::error_code copyEc;
|
||||
if (sourceIsDirectory) {
|
||||
std::filesystem::copy(srcHost, tempHost,
|
||||
std::filesystem::copy_options::recursive, copyEc);
|
||||
} else {
|
||||
std::filesystem::copy_file(srcHost, tempHost, copyEc);
|
||||
}
|
||||
|
||||
if (copyEc) {
|
||||
LogNandError("NANDMove", "cross-mount copy failed: %s", copyEc.message().c_str());
|
||||
cleanupScratch();
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
std::error_code publishEc;
|
||||
if (sourceIsDirectory) {
|
||||
RenameNoReplace(tempHost, dstHost, publishEc);
|
||||
} else {
|
||||
// link(2) and CreateHardLink do not replace an existing destination,
|
||||
// unlike rename(2) on POSIX. Both paths are already on the target
|
||||
// filesystem, so the link is a no-replace publication operation.
|
||||
std::filesystem::create_hard_link(tempHost, dstHost, publishEc);
|
||||
}
|
||||
if (publishEc) {
|
||||
LogNandError("NANDMove", "failed to publish cross-mount copy: %s",
|
||||
publishEc.message().c_str());
|
||||
cleanupScratch();
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
cleanupScratch();
|
||||
|
||||
std::error_code removeEc;
|
||||
std::filesystem::remove_all(srcHost, removeEc);
|
||||
if (!removeEc) {
|
||||
LogNandWarning("NANDMove", "used copy/remove fallback across mounts");
|
||||
return NAND_RESULT_OK;
|
||||
}
|
||||
|
||||
// Keep the source as the authoritative copy when cleanup fails. The
|
||||
// destination was published atomically on its own mount; regular files
|
||||
// are rolled back below, while directories keep the complete copy when
|
||||
// their source removal was only partial. Cross-mount moves cannot
|
||||
// provide crash-atomicity, so this is best effort.
|
||||
LogNandError("NANDMove", "copy succeeded but source removal failed: %s",
|
||||
removeEc.message().c_str());
|
||||
if (sourceIsDirectory) {
|
||||
// remove_all may have removed only part of a directory tree. Keep
|
||||
// the complete published copy rather than rolling it back to a
|
||||
// partially deleted source.
|
||||
LogNandWarning("NANDMove", "preserving published directory copy after partial source removal");
|
||||
} else {
|
||||
std::error_code rollbackEc;
|
||||
std::filesystem::remove_all(dstHost, rollbackEc);
|
||||
if (rollbackEc) {
|
||||
LogNandError("NANDMove", "failed to roll back destination '%s': %s",
|
||||
HostPathText(dstHost).c_str(), rollbackEc.message().c_str());
|
||||
}
|
||||
}
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
LogNandError("NANDMove", "FAILED error=%d message='%s'", ec.value(), ec.message().c_str());
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -1551,6 +1551,10 @@ int RuntimeMain(int argc, char** argv) {
|
||||
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
|
||||
|
||||
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
|
||||
if (auroraInfo.initializationStatus != AURORA_INITIALIZATION_SUCCESS) {
|
||||
throw std::runtime_error(auroraInfo.initializationError != nullptr
|
||||
? auroraInfo.initializationError : "No supported graphics backend is available");
|
||||
}
|
||||
if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend
|
||||
<< "\" is not available on this system; aurora fell back to \""
|
||||
|
||||
@@ -48,7 +48,7 @@ std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
|
||||
return (ec ? std::filesystem::path(path) : resolved).parent_path();
|
||||
#elif defined(__ANDROID__)
|
||||
// Bundled read-only runtime resources (wii_bootstrap/, dsp_coef.bin,
|
||||
// initial_pipeline_cache.db) unpacked from the APK by the activity. Stands
|
||||
// initial_pipeline_cache.db, cacert.pem) unpacked from the APK by the activity. Stands
|
||||
// in for the executable directory the desktop builds look next to.
|
||||
if (const char* resources = std::getenv("MKW_ANDROID_RESOURCES_DIR"); resources && *resources) {
|
||||
return std::filesystem::path(resources);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "update_check.h"
|
||||
#include "vr/camera_toggle.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
@@ -159,6 +160,7 @@ int g_vrFoveation = static_cast<int>(RuntimeConfigFile::VrFoveationLevelIndex(Ru
|
||||
bool g_vrEyeTrackedFoveation = RuntimeConfigFile::VrEyeTrackedFoveation();
|
||||
#endif
|
||||
bool g_vrRepeatFrames = RuntimeConfigFile::VrRepeatFrames();
|
||||
bool g_vrAdaptiveResolution = RuntimeConfigFile::VrAdaptiveResolution();
|
||||
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.
|
||||
@@ -1568,6 +1570,18 @@ void DrawVrSettings() {
|
||||
"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 !defined(_WIN32)
|
||||
// Only the Vulkan backends show part of an eye image; the PC's copy whole eyes.
|
||||
if (ImGui::Checkbox("Adaptive resolution (experimental)", &g_vrAdaptiveResolution)) {
|
||||
RuntimeConfigFile::SetVrAdaptiveResolution(g_vrAdaptiveResolution);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"During races, lowers the eyes' resolution a step at a time, down to 70%%, while new "
|
||||
"frames fall behind the game's 60 FPS, and raises it again once they keep up. Each change "
|
||||
"rebuilds the foveation maps. Applies immediately.");
|
||||
}
|
||||
#endif
|
||||
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)];
|
||||
@@ -2081,6 +2095,107 @@ void DrawDiagnosticsSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
bool g_updatePromptOpen = false;
|
||||
std::string g_updateError;
|
||||
|
||||
void DrawUpdatePrompt() {
|
||||
constexpr const char* kTitle = "Update";
|
||||
if (g_updatePromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle);
|
||||
if (!ImGui::BeginPopupModal(kTitle, &g_updatePromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) return;
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||
ImGui::TextUnformatted("The update is built here on the Frame, in the background, which takes a "
|
||||
"while: keep the Frame on its charger. You can keep playing meanwhile, "
|
||||
"though the game may stutter while it compiles. Close the game whenever "
|
||||
"you like: the update carries on and opens it again when it is done.");
|
||||
ImGui::PopTextWrapPos();
|
||||
if (ImGui::Button("Start the update", ImVec2(Scaled(180.0f), 0.0f))) {
|
||||
update_check::StartUpdate(g_updateError);
|
||||
g_updatePromptOpen = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(Scaled(120.0f), 0.0f))) g_updatePromptOpen = false;
|
||||
if (!g_updatePromptOpen) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void DrawUpdateSettings() {
|
||||
const update_check::Status status = update_check::Current();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||
if (status.installedRelease.empty()) {
|
||||
ImGui::TextUnformatted("This build came from a source tree rather than a release.");
|
||||
} else {
|
||||
ImGui::Text("Release %s", status.installedRelease.c_str());
|
||||
}
|
||||
|
||||
const bool busy = status.state == update_check::State::Checking ||
|
||||
status.state == update_check::State::Updating;
|
||||
switch (status.state) {
|
||||
case update_check::State::PcInstall:
|
||||
ImGui::TextDisabled("The PC that built this installs its updates. Run the installer there "
|
||||
"again with --update.");
|
||||
break;
|
||||
case update_check::State::Idle:
|
||||
break;
|
||||
case update_check::State::Checking:
|
||||
ImGui::TextDisabled("Looking for a newer release...");
|
||||
break;
|
||||
case update_check::State::UpToDate:
|
||||
ImGui::TextDisabled("%s", status.detail.c_str());
|
||||
break;
|
||||
case update_check::State::Available:
|
||||
ImGui::TextUnformatted(status.detail.c_str());
|
||||
break;
|
||||
case update_check::State::Updating:
|
||||
ImGui::Text("Updating: %s", status.detail.c_str());
|
||||
ImGui::TextDisabled("Keep playing, or close the game: the update carries on and opens it "
|
||||
"again when it is done.");
|
||||
break;
|
||||
case update_check::State::Failed:
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", status.detail.c_str());
|
||||
break;
|
||||
}
|
||||
if (!g_updateError.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", g_updateError.c_str());
|
||||
}
|
||||
if (status.restartNeeded && status.state != update_check::State::Updating) {
|
||||
// The update replaced the build on disk; this process is still the old one.
|
||||
ImGui::TextUnformatted("The update is installed. Quit and start the game again to use it.");
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
if (status.state == update_check::State::PcInstall) {
|
||||
return;
|
||||
}
|
||||
if (status.restartNeeded && status.state != update_check::State::Updating) {
|
||||
if (ImGui::Button("Quit")) g_exitPromptOpen = true;
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::BeginDisabled(busy);
|
||||
if (ImGui::Button(status.state == update_check::State::Idle ? "Check for updates" : "Check again")) {
|
||||
g_updateError.clear();
|
||||
update_check::StartCheck();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Asks github.com for the newest release, and Retro Rewind's server for\n"
|
||||
"its newest pack when this install has one. Nothing is downloaded\n"
|
||||
"until you choose to update.");
|
||||
}
|
||||
if (status.state == update_check::State::Available) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Update now")) {
|
||||
g_updateError.clear();
|
||||
g_updatePromptOpen = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Builds the update here on the Frame, in the background. The game\n"
|
||||
"can stay open and shows the progress here.");
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
// The prompt itself is drawn at the top level, as the exit prompt is: the tab lives both in the
|
||||
// headset's panel and in a desktop menu, and a modal must outlive whichever opened it.
|
||||
}
|
||||
|
||||
void DrawFpsOverlay() {
|
||||
AuroraPresentTiming presentTiming{};
|
||||
aurora_get_present_timing(&presentTiming);
|
||||
@@ -2278,6 +2393,11 @@ void DrawTopBar() {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (update_check::Relevant(update_check::Current()) && ImGui::BeginMenu("Updates")) {
|
||||
DrawUpdateSettings();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + style.FramePadding.x * 2.0f;
|
||||
const float exitWidth = ImGui::CalcTextSize("X").x + style.FramePadding.x * 2.0f;
|
||||
@@ -2466,6 +2586,9 @@ void DrawVrSettingsPanelWindow() {
|
||||
tab("Controllers", DrawControllerSettings);
|
||||
tab("Audio", DrawAudioSettings);
|
||||
tab("Diagnostics", DrawDiagnosticsSettings);
|
||||
if (update_check::Relevant(update_check::Current())) {
|
||||
tab("Updates", DrawUpdateSettings);
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
@@ -2674,6 +2797,7 @@ void Draw() noexcept {
|
||||
DrawFpsOverlay();
|
||||
DrawTopBar();
|
||||
DrawExitPrompt();
|
||||
DrawUpdatePrompt();
|
||||
controller_mapping_wizard::Draw();
|
||||
// The wizard captures raw presses; keep them out of the game.
|
||||
const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active;
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "update_check.h"
|
||||
|
||||
#include "runtime_config.h"
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
// Updating itself is the Frame's build, installed by Launcher/steam-frame-install.sh: a desktop
|
||||
// Linux game that holds its own build directory. Everywhere else the tab only reports the release
|
||||
// it is running, which needs none of the machinery below.
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#define MKW_UPDATE_CHECK_SUPPORTED 1
|
||||
#include <sys/wait.h>
|
||||
#else
|
||||
#define MKW_UPDATE_CHECK_SUPPORTED 0
|
||||
#endif
|
||||
|
||||
namespace update_check {
|
||||
namespace {
|
||||
|
||||
// Written by Launcher/steam-frame-install.sh when it built on this machine, naming itself and the
|
||||
// service that runs it. Its absence is what tells the tab this install is updated from a PC.
|
||||
constexpr std::string_view kConfigFileName = "update.conf";
|
||||
// An update's progress, and a check's answer. Separate files, so a check never overwrites the
|
||||
// result of the update that opened the game.
|
||||
constexpr std::string_view kStatusFileName = "update-status";
|
||||
constexpr std::string_view kCheckStatusFileName = "update-status-check";
|
||||
// What the Update button leaves for the update to pick up.
|
||||
constexpr std::string_view kRequestFileName = "update-request";
|
||||
// Stamped beside the executable by the install; absent for a build from a source tree.
|
||||
constexpr std::string_view kReleaseTagFileName = ".release-tag";
|
||||
|
||||
// A finished update's status belongs on screen when the game opens right afterwards, which is what
|
||||
// the update does. Older than this it is history, and the tab starts clean.
|
||||
constexpr auto kStatusLifetime = std::chrono::hours(1);
|
||||
// A running update rewrites its status at every step and every percent of its build, so one that
|
||||
// has said nothing for this long was stopped without a chance to say so (the Frame lost power).
|
||||
constexpr auto kRunningStatusLifetime = std::chrono::hours(2);
|
||||
// The tab is drawn every frame; the files behind it are not read anywhere near that often.
|
||||
constexpr auto kReadInterval = std::chrono::seconds(1);
|
||||
|
||||
std::string Trimmed(std::string_view text) {
|
||||
const auto first = text.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
return std::string(text.substr(first, text.find_last_not_of(" \t\r\n") - first + 1));
|
||||
}
|
||||
|
||||
std::string ReadFirstLine(const std::filesystem::path& file) {
|
||||
std::ifstream stream(file);
|
||||
std::string line;
|
||||
if (!stream || !std::getline(stream, line)) {
|
||||
return {};
|
||||
}
|
||||
return Trimmed(line);
|
||||
}
|
||||
|
||||
struct Config {
|
||||
std::filesystem::path script;
|
||||
// The install's work directory, which holds the build the update reuses. The script defaults to
|
||||
// the same place, but an install given --work-dir does not live there.
|
||||
std::filesystem::path workDir;
|
||||
std::string service;
|
||||
|
||||
bool Usable() const { return !script.empty() && !service.empty(); }
|
||||
};
|
||||
|
||||
Config ReadConfig() {
|
||||
Config config;
|
||||
#if !MKW_UPDATE_CHECK_SUPPORTED
|
||||
return config;
|
||||
#else
|
||||
std::ifstream stream(RuntimeConfigFile::ApplicationDataDirectory() / kConfigFileName);
|
||||
if (!stream) {
|
||||
return config;
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
const auto separator = line.find('=');
|
||||
if (line.empty() || line.front() == '#' || separator == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
const std::string key = Trimmed(std::string_view(line).substr(0, separator));
|
||||
const std::string value = Trimmed(std::string_view(line).substr(separator + 1));
|
||||
if (key == "script") {
|
||||
config.script = value;
|
||||
} else if (key == "work_dir") {
|
||||
config.workDir = value;
|
||||
} else if (key == "service") {
|
||||
config.service = value;
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
if (!config.script.empty() && !std::filesystem::is_regular_file(config.script, ec)) {
|
||||
// The work directory was removed, so there is nothing here to update from any more.
|
||||
config.script.clear();
|
||||
}
|
||||
return config;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string InstalledRelease() {
|
||||
if (const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
|
||||
return ReadFirstLine(*executableDirectory / kReleaseTagFileName);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// What this session has been told, which outranks the files until the game is next started: a check
|
||||
// or update it asked for, and what came back.
|
||||
std::mutex g_mutex;
|
||||
bool g_sessionStateSet = false;
|
||||
State g_sessionState = State::Idle;
|
||||
std::string g_sessionDetail;
|
||||
// What was last read from disk, and when, so drawing the tab does not re-read it every frame.
|
||||
std::chrono::steady_clock::time_point g_lastRead;
|
||||
bool g_haveRead = false;
|
||||
bool g_haveFileState = false;
|
||||
State g_fileState = State::Idle;
|
||||
std::string g_fileDetail;
|
||||
bool g_canUpdate = false;
|
||||
// The release this process was started from, read once: an update replaces the stamp on disk while
|
||||
// the old build is still the one running. The stamp as it is now tells whether that has happened.
|
||||
std::string g_runningRelease;
|
||||
bool g_haveRunningRelease = false;
|
||||
std::string g_releaseOnDisk;
|
||||
|
||||
void SetSessionState(State state, std::string detail) {
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_sessionStateSet = true;
|
||||
g_sessionState = state;
|
||||
g_sessionDetail = std::move(detail);
|
||||
}
|
||||
|
||||
std::string ShellQuoted(const std::string& text) {
|
||||
std::string quoted = "'";
|
||||
for (const char character : text) {
|
||||
if (character == '\'') {
|
||||
quoted += "'\\''";
|
||||
} else {
|
||||
quoted += character;
|
||||
}
|
||||
}
|
||||
quoted += '\'';
|
||||
return quoted;
|
||||
}
|
||||
|
||||
// Runs `command`, returning its exit status and keeping the last line of its output for a message.
|
||||
int Run(const std::string& command, std::string& lastLine) {
|
||||
lastLine.clear();
|
||||
#if !MKW_UPDATE_CHECK_SUPPORTED
|
||||
(void)command;
|
||||
return -1;
|
||||
#else
|
||||
FILE* pipe = popen(command.c_str(), "r");
|
||||
if (pipe == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
std::array<char, 512> buffer{};
|
||||
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
|
||||
if (const std::string line = Trimmed(buffer.data()); !line.empty()) {
|
||||
lastLine = line;
|
||||
}
|
||||
}
|
||||
const int status = pclose(pipe);
|
||||
if (status == -1 || !WIFEXITED(status)) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(status);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reads a "<state>\t<unix time>\t<text>" line the script wrote. False when there is none, it is too
|
||||
// old to be about now, or it cannot be understood.
|
||||
bool ReadStatusFile(const std::filesystem::path& file, State& state, std::string& detail) {
|
||||
const std::string line = ReadFirstLine(file);
|
||||
if (line.empty()) {
|
||||
return false;
|
||||
}
|
||||
const auto firstTab = line.find('\t');
|
||||
const auto secondTab = line.find('\t', firstTab + 1);
|
||||
if (firstTab == std::string::npos || secondTab == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
const std::string written = line.substr(0, firstTab);
|
||||
long long seconds = 0;
|
||||
const char* const secondsBegin = line.data() + firstTab + 1;
|
||||
if (std::from_chars(secondsBegin, line.data() + secondTab, seconds).ec != std::errc{}) {
|
||||
return false;
|
||||
}
|
||||
const auto age = std::chrono::system_clock::now() -
|
||||
std::chrono::system_clock::time_point(std::chrono::seconds(seconds));
|
||||
const auto lifetime = written == "running" ? kRunningStatusLifetime : kStatusLifetime;
|
||||
if (age > lifetime || age < -lifetime) {
|
||||
return false;
|
||||
}
|
||||
if (written == "running") {
|
||||
state = State::Updating;
|
||||
} else if (written == "available") {
|
||||
state = State::Available;
|
||||
} else if (written == "uptodate" || written == "done") {
|
||||
state = State::UpToDate;
|
||||
} else if (written == "failed") {
|
||||
state = State::Failed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
detail = line.substr(secondTab + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status Current() {
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
// Drawn every frame, so the files behind it are read on a timer instead.
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!g_haveRead || now - g_lastRead > kReadInterval) {
|
||||
g_releaseOnDisk = InstalledRelease();
|
||||
if (!g_haveRunningRelease) {
|
||||
g_runningRelease = g_releaseOnDisk;
|
||||
g_haveRunningRelease = true;
|
||||
}
|
||||
g_lastRead = now;
|
||||
g_haveRead = true;
|
||||
g_canUpdate = ReadConfig().Usable();
|
||||
g_fileState = State::Idle;
|
||||
g_fileDetail.clear();
|
||||
g_haveFileState = ReadStatusFile(RuntimeConfigFile::ApplicationDataDirectory() / kStatusFileName,
|
||||
g_fileState, g_fileDetail);
|
||||
}
|
||||
|
||||
Status status;
|
||||
status.installedRelease = g_runningRelease;
|
||||
status.restartNeeded = g_releaseOnDisk != g_runningRelease;
|
||||
if (!g_canUpdate) {
|
||||
status.state = State::PcInstall;
|
||||
return status;
|
||||
}
|
||||
// An update's own reports outrank what this session last said: one started here (the session
|
||||
// says Updating until the update writes its first step), or one this game was reopened after.
|
||||
const bool followUpdate = !g_sessionStateSet || g_sessionState == State::Updating;
|
||||
if (followUpdate && g_haveFileState) {
|
||||
status.state = g_fileState;
|
||||
status.detail = g_fileDetail;
|
||||
} else if (g_sessionStateSet) {
|
||||
status.state = g_sessionState;
|
||||
status.detail = g_sessionDetail;
|
||||
} else {
|
||||
status.state = State::Idle;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void StartCheck() {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
if (g_sessionStateSet && (g_sessionState == State::Checking || g_sessionState == State::Updating)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const Config config = ReadConfig();
|
||||
if (!config.Usable()) {
|
||||
return;
|
||||
}
|
||||
SetSessionState(State::Checking, "Asking github.com for the newest release");
|
||||
|
||||
// Detached: it talks to the network and must not hold up a frame, and there is no shutdown
|
||||
// ordering to manage because it touches nothing the game owns.
|
||||
std::thread([config] {
|
||||
const auto statusFile = RuntimeConfigFile::ApplicationDataDirectory() / kCheckStatusFileName;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(statusFile, ec);
|
||||
std::string command = "WIICOMPILED_UPDATE_STATUS=" + ShellQuoted(statusFile.string()) + ' ' +
|
||||
ShellQuoted(config.script.string()) + " --check";
|
||||
if (!config.workDir.empty()) {
|
||||
command += " --work-dir " + ShellQuoted(config.workDir.string());
|
||||
}
|
||||
command += " 2>&1";
|
||||
std::string lastLine;
|
||||
const int exitStatus = Run(command, lastLine);
|
||||
|
||||
State state = State::Failed;
|
||||
std::string detail;
|
||||
if (ReadStatusFile(statusFile, state, detail)) {
|
||||
SetSessionState(state, detail);
|
||||
} else if (exitStatus == 0) {
|
||||
// It ran and said nothing this can read, which is still an answer: ask again later.
|
||||
SetSessionState(State::Idle, "The check said nothing this version understands");
|
||||
} else {
|
||||
std::cerr << "[update] the check failed (exit " << exitStatus << "): " << lastLine << std::endl;
|
||||
SetSessionState(State::Failed,
|
||||
lastLine.empty() ? "The check could not be run" : lastLine);
|
||||
}
|
||||
std::filesystem::remove(statusFile, ec);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
bool StartUpdate(std::string& error) {
|
||||
error.clear();
|
||||
const Config config = ReadConfig();
|
||||
if (!config.Usable()) {
|
||||
error = "This build was installed from a PC, so the update is run there.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto dataDirectory = RuntimeConfigFile::ApplicationDataDirectory();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dataDirectory, ec);
|
||||
// How the update opens the game again when it is done. Steam names the running title; without
|
||||
// one (a build started from a terminal) the update still runs and the game is started by hand.
|
||||
std::string steamGameId;
|
||||
for (const char* variable : {"SteamGameId", "SteamAppId"}) {
|
||||
if (const char* value = std::getenv(variable); value != nullptr && *value != '\0') {
|
||||
steamGameId = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::ofstream request(dataDirectory / kRequestFileName, std::ios::trunc);
|
||||
if (!request) {
|
||||
error = "Could not write to " + dataDirectory.string();
|
||||
return false;
|
||||
}
|
||||
request << "# Written by the game's Update button; the update removes it.\n";
|
||||
if (!steamGameId.empty()) {
|
||||
request << "steam_game_id=" << steamGameId << '\n';
|
||||
}
|
||||
}
|
||||
// The previous update's result must not be read as this one's before it has written anything.
|
||||
std::filesystem::remove(dataDirectory / kStatusFileName, ec);
|
||||
// The update must outlive the game if the player closes it meanwhile, and Steam ends the game's
|
||||
// own processes when it closes, so it runs as a service of its own. --no-block: started, not
|
||||
// waited for.
|
||||
std::string lastLine;
|
||||
const std::string command =
|
||||
"systemctl --user start --no-block " + ShellQuoted(config.service) + " 2>&1";
|
||||
if (const int exitStatus = Run(command, lastLine); exitStatus != 0) {
|
||||
std::filesystem::remove(dataDirectory / kRequestFileName, ec);
|
||||
std::cerr << "[update] could not start " << config.service << " (exit " << exitStatus
|
||||
<< "): " << lastLine << std::endl;
|
||||
error = lastLine.empty() ? "Could not start the update service" : lastLine;
|
||||
return false;
|
||||
}
|
||||
SetSessionState(State::Updating, "Starting the update");
|
||||
// Read the files again at once, rather than show the removed status for up to a second.
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_haveRead = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace update_check
|
||||
|
||||
#undef MKW_UPDATE_CHECK_SUPPORTED
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "runtime_config.h"
|
||||
#include "gx_thread.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/adaptive_resolution.h"
|
||||
#include "vr/eye_gaze.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
@@ -111,6 +112,15 @@ inline constexpr bool kWindowShapedEyesSupported = true;
|
||||
inline constexpr bool kWindowShapedEyesSupported = false;
|
||||
#endif
|
||||
|
||||
// Whether the immersive eyes can be rendered smaller than their swapchain images for
|
||||
// [vr] adaptive_resolution: the backend copies them into a corner of the image and its projection
|
||||
// layer shows that corner. The Vulkan backends do; the PC's D3D12 one copies whole eyes.
|
||||
#if defined(_WIN32)
|
||||
inline constexpr bool kScaledEyesSupported = false;
|
||||
#else
|
||||
inline constexpr bool kScaledEyesSupported = true;
|
||||
#endif
|
||||
|
||||
struct Quaternion {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
@@ -1506,6 +1516,15 @@ private:
|
||||
destination.eyes[eye].height = source.render_height[eye];
|
||||
}
|
||||
}
|
||||
// The backend shows the part of each image this size fills, so the field of view is kept.
|
||||
if (const float scale = adaptive_scale_.load(std::memory_order_relaxed); kScaledEyesSupported && scale < 1.0f) {
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
source.render_width[eye] = AdaptiveResolution::Scaled(source.render_width[eye], scale);
|
||||
source.render_height[eye] = AdaptiveResolution::Scaled(source.render_height[eye], scale);
|
||||
destination.eyes[eye].width = source.render_width[eye];
|
||||
destination.eyes[eye].height = source.render_height[eye];
|
||||
}
|
||||
}
|
||||
// Read once so both eyes are built from the same angle even if the
|
||||
// settings slider moves between them.
|
||||
const float lean_back_radians =
|
||||
@@ -1879,7 +1898,21 @@ private:
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const float elapsed = std::chrono::duration<float>(now - timing_start_).count();
|
||||
if (elapsed >= 1.0f) {
|
||||
rendered_fps_.store(static_cast<float>(timing_submissions_) / elapsed, std::memory_order_relaxed);
|
||||
const float fps = static_cast<float>(timing_submissions_) / elapsed;
|
||||
rendered_fps_.store(fps, std::memory_order_relaxed);
|
||||
// Measured during races only: menus and loading screens run below 60 on their own.
|
||||
// The target is the game's 60 FPS, or the headset's rate when frames are interpolated.
|
||||
const bool adaptive = kScaledEyesSupported && RuntimeConfigFile::VrAdaptiveResolution();
|
||||
if (!adaptive || last_immersive_) {
|
||||
const float target = aurora_get_stereo_frame_interpolation() ? hz : std::min(hz, 60.0f);
|
||||
const float previous = adaptive_resolution_.Scale();
|
||||
const float scale = adaptive_resolution_.Observe(fps, target, adaptive);
|
||||
adaptive_scale_.store(scale, std::memory_order_relaxed);
|
||||
if (scale != previous) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: adaptive resolution " << static_cast<int>(scale * 100.0f + 0.5f)
|
||||
<< "% (" << fps << " of " << target << " FPS)" << std::endl;
|
||||
}
|
||||
}
|
||||
timing_start_ = now;
|
||||
timing_submissions_ = 0;
|
||||
}
|
||||
@@ -2031,6 +2064,9 @@ private:
|
||||
std::atomic<float> rendered_fps_{0};
|
||||
std::chrono::steady_clock::time_point timing_start_ = std::chrono::steady_clock::now();
|
||||
uint32_t timing_submissions_ = 0;
|
||||
// [vr] adaptive_resolution: measured on the pacing thread, read where eyes are published.
|
||||
AdaptiveResolution adaptive_resolution_;
|
||||
std::atomic<float> adaptive_scale_{1.0f};
|
||||
PFN_xrGetDisplayRefreshRateFB get_display_refresh_rate_ = nullptr;
|
||||
PFN_xrEnumerateDisplayRefreshRatesFB enumerate_refresh_rates_ = nullptr;
|
||||
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate_ = nullptr;
|
||||
|
||||
@@ -744,10 +744,11 @@ public:
|
||||
views[eye].pose.position = frame.xr_frame.views[eye].pose.position;
|
||||
views[eye].fov = frame.xr_frame.views[eye].fov;
|
||||
views[eye].subImage.swapchain = retained_swapchains_[eye].handle;
|
||||
// The eyes may fill only a corner of the image ([vr] adaptive_resolution).
|
||||
views[eye].subImage.imageRect = {
|
||||
{0, 0},
|
||||
{static_cast<int32_t>(retained_swapchains_[eye].width),
|
||||
static_cast<int32_t>(retained_swapchains_[eye].height)}};
|
||||
{static_cast<int32_t>(std::min(frame.render_width[eye], retained_swapchains_[eye].width)),
|
||||
static_cast<int32_t>(std::min(frame.render_height[eye], retained_swapchains_[eye].height))}};
|
||||
views[eye].subImage.imageArrayIndex = 0;
|
||||
}
|
||||
XrCompositionLayerProjection projection{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// The AX mix kernels' vector forms (AVX2 on x86-64, NEON on arm64) must be bit-exact with the
|
||||
// scalar reference loops they replace. Random blocks at every tail length, plus the ramp and
|
||||
// clamp extremes; on a build with neither vector form this compares the scalar loops with
|
||||
// themselves.
|
||||
|
||||
#include "../src/hle/audio/ax_mix_kernels.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void Fail(const char* kernel, uint32_t count, uint32_t volume, uint32_t delta) {
|
||||
if (++g_failures <= 10) {
|
||||
std::cerr << kernel << " differs from the scalar loop: count " << count << ", volume "
|
||||
<< volume << ", delta " << delta << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::vector<T> RandomBlock(std::mt19937& rng, uint32_t count, int64_t low, int64_t high) {
|
||||
std::uniform_int_distribution<int64_t> dist(low, high);
|
||||
std::vector<T> block(count);
|
||||
for (auto& value : block) {
|
||||
value = static_cast<T>(dist(rng));
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
void CheckRamps(std::mt19937& rng, uint32_t count, uint16_t volume, uint16_t delta) {
|
||||
const auto input = RandomBlock<int16_t>(rng, count, INT16_MIN, INT16_MAX);
|
||||
const auto bus = RandomBlock<int32_t>(rng, count, -(1 << 24), 1 << 24);
|
||||
|
||||
auto expectedOut = bus;
|
||||
auto actualOut = bus;
|
||||
int16_t expectedDpop = 1234;
|
||||
int16_t actualDpop = 1234;
|
||||
const uint16_t expectedVolume =
|
||||
AxMixKernels::MixAddRampScalar(expectedOut.data(), input.data(), count, volume, delta, expectedDpop);
|
||||
const uint16_t actualVolume =
|
||||
AxMixKernels::MixAddRamp(actualOut.data(), input.data(), count, volume, delta, actualDpop);
|
||||
if (expectedOut != actualOut || expectedDpop != actualDpop || expectedVolume != actualVolume) {
|
||||
Fail("MixAddRamp", count, volume, delta);
|
||||
}
|
||||
|
||||
auto expectedSamples = input;
|
||||
auto actualSamples = input;
|
||||
const uint16_t expectedScaled = AxMixKernels::ScaleRampScalar(expectedSamples.data(), count, volume, delta);
|
||||
const uint16_t actualScaled = AxMixKernels::ScaleRamp(actualSamples.data(), count, volume, delta);
|
||||
if (expectedSamples != actualSamples || expectedScaled != actualScaled) {
|
||||
Fail("ScaleRamp", count, volume, delta);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckAccumAndMarshal(std::mt19937& rng, uint32_t count) {
|
||||
const auto src = RandomBlock<int32_t>(rng, count, INT32_MIN, INT32_MAX);
|
||||
const auto ramp = RandomBlock<uint16_t>(rng, count, 0, UINT16_MAX);
|
||||
const auto bus = RandomBlock<int32_t>(rng, count, -(1 << 24), 1 << 24);
|
||||
auto expected = bus;
|
||||
auto actual = bus;
|
||||
AxMixKernels::MixAccumRamp32Scalar(expected.data(), src.data(), ramp.data(), count);
|
||||
AxMixKernels::MixAccumRamp32(actual.data(), src.data(), ramp.data(), count);
|
||||
if (expected != actual) {
|
||||
Fail("MixAccumRamp32", count, 0, 0);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> expectedBytes(count * 4);
|
||||
std::vector<uint8_t> actualBytes(count * 4);
|
||||
AxMixKernels::StoreBigEndian32Scalar(expectedBytes.data(), src.data(), count);
|
||||
AxMixKernels::StoreBigEndian32(actualBytes.data(), src.data(), count);
|
||||
if (expectedBytes != actualBytes) {
|
||||
Fail("StoreBigEndian32", count, 0, 0);
|
||||
}
|
||||
|
||||
std::vector<int32_t> expectedWords(count);
|
||||
std::vector<int32_t> actualWords(count);
|
||||
AxMixKernels::LoadBigEndian32Scalar(expectedWords.data(), expectedBytes.data(), count);
|
||||
AxMixKernels::LoadBigEndian32(actualWords.data(), expectedBytes.data(), count);
|
||||
if (expectedWords != actualWords || expectedWords != src) {
|
||||
Fail("LoadBigEndian32", count, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
std::mt19937 rng(0x41584D58u);
|
||||
std::uniform_int_distribution<uint32_t> any16(0, UINT16_MAX);
|
||||
const uint16_t edges[] = {0, 1, 0x7FFF, 0x8000, 0x8001, 0xFFFE, 0xFFFF};
|
||||
|
||||
// Every tail length around the vector widths, and the AX frame sizes (96 per 3 ms frame,
|
||||
// 160 at the 5 ms subframe the AXWii list can use).
|
||||
for (uint32_t count = 0; count <= 40; ++count) {
|
||||
for (uint16_t volume : edges) {
|
||||
for (uint16_t delta : edges) {
|
||||
CheckRamps(rng, count, volume, delta);
|
||||
}
|
||||
}
|
||||
for (int trial = 0; trial < 64; ++trial) {
|
||||
CheckRamps(rng, count, static_cast<uint16_t>(any16(rng)), static_cast<uint16_t>(any16(rng)));
|
||||
}
|
||||
CheckAccumAndMarshal(rng, count);
|
||||
}
|
||||
for (uint32_t count : {96u, 160u, 255u}) {
|
||||
for (int trial = 0; trial < 256; ++trial) {
|
||||
CheckRamps(rng, count, static_cast<uint16_t>(any16(rng)), static_cast<uint16_t>(any16(rng)));
|
||||
CheckAccumAndMarshal(rng, count);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " mismatches\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "ax mix kernels match the scalar loops (avx2 " << MKW_AX_MIX_AVX2 << ", neon "
|
||||
<< MKW_AX_MIX_NEON << ")\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "vr/adaptive_resolution.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
using mkw::vr::AdaptiveResolution;
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool condition, const char* what) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "vr_adaptive_resolution_tests: %s\n", what);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckNear(float value, float expected, const char* what) {
|
||||
Check(std::fabs(value - expected) <= 1.0e-5f, what);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
{
|
||||
AdaptiveResolution adaptive;
|
||||
CheckNear(adaptive.Observe(40.0f, 60.0f, false), 1.0f, "off stays at full size");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 1.0f, "on time at full size stays there");
|
||||
}
|
||||
{
|
||||
AdaptiveResolution adaptive;
|
||||
CheckNear(adaptive.Observe(45.0f, 60.0f, true), 0.9f, "a short second lowers a step");
|
||||
CheckNear(adaptive.Observe(45.0f, 60.0f, true), 0.8f, "another lowers another");
|
||||
CheckNear(adaptive.Observe(45.0f, 60.0f, true), 0.7f, "and another");
|
||||
CheckNear(adaptive.Observe(10.0f, 60.0f, true), 0.7f, "never below the minimum");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.7f, "one second on time is not enough");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.7f, "two are not enough");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.8f, "three raise a step");
|
||||
CheckNear(adaptive.Observe(55.0f, 60.0f, true), 0.8f, "between the thresholds holds");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.8f, "and restarts the on-time count");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.8f, "still counting");
|
||||
CheckNear(adaptive.Observe(60.0f, 60.0f, true), 0.9f, "three more raise again");
|
||||
for (int second = 0; second < 30; ++second) adaptive.Observe(60.0f, 60.0f, true);
|
||||
Check(adaptive.Scale() == 1.0f, "back at exactly full size");
|
||||
CheckNear(adaptive.Observe(0.0f, 60.0f, true), 1.0f, "a second with no measurement changes nothing");
|
||||
CheckNear(adaptive.Observe(30.0f, 0.0f, true), 1.0f, "nor one with no target");
|
||||
adaptive.Observe(30.0f, 60.0f, true);
|
||||
CheckNear(adaptive.Observe(30.0f, 60.0f, false), 1.0f, "turning it off restores full size");
|
||||
}
|
||||
Check(AdaptiveResolution::Scaled(2064, 1.0f) == 2064, "full size is unchanged");
|
||||
Check(AdaptiveResolution::Scaled(2000, 0.7f) == 1400, "scaled size");
|
||||
Check(AdaptiveResolution::Scaled(2, 0.7f) == 2, "never below two pixels");
|
||||
std::puts("vr_adaptive_resolution_tests: ok");
|
||||
return 0;
|
||||
}
|
||||
@@ -84,6 +84,9 @@ int main() {
|
||||
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());
|
||||
// [vr] adaptive_resolution: off unless set.
|
||||
Require(Parse("[vr]\nadaptive_resolution = true\n").vrAdaptiveResolution == true);
|
||||
Require(!Parse("[vr]\n").vrAdaptiveResolution.has_value());
|
||||
#if defined(MKW_HEADSET_STEAM_FRAME)
|
||||
Require(RuntimeConfigFile::kVrRepeatFramesDefault);
|
||||
Require(std::string_view(MKW_VR_REPEAT_FRAMES_DEFAULT_TOML) == "true");
|
||||
|
||||
@@ -27,7 +27,8 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> stateFreeAbiContracts,
|
||||
IReadOnlyDictionary<uint, string> stateFreeCallSymbols,
|
||||
IReadOnlyDictionary<GuestStateFreeCallSiteKey, GuestStateFreeCallVariant> stateFreeCallSiteVariants,
|
||||
IReadOnlySet<uint> modOverridableCallTargets)
|
||||
IReadOnlySet<uint> modOverridableCallTargets,
|
||||
bool shareLrContinuationDispatch)
|
||||
{
|
||||
if (ins is IrPhi)
|
||||
{
|
||||
@@ -410,11 +411,18 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
fallbackPad = IndentPad(indent + 1);
|
||||
}
|
||||
|
||||
EmitLocalLrContinuationDispatch(sb, fallbackPad, labelNames);
|
||||
sb.AppendLine($"{fallbackPad}if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {{");
|
||||
sb.AppendLine($"{fallbackPad} InvokeIndirectCpu(ctx->lr, ctx);");
|
||||
sb.AppendLine($"{fallbackPad}}}");
|
||||
sb.AppendLine($"{fallbackPad}return;");
|
||||
if (shareLrContinuationDispatch)
|
||||
{
|
||||
sb.AppendLine($"{fallbackPad}goto lr_continuation_dispatch;");
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitLocalLrContinuationDispatch(sb, fallbackPad, labelNames);
|
||||
sb.AppendLine($"{fallbackPad}if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {{");
|
||||
sb.AppendLine($"{fallbackPad} InvokeIndirectCpu(ctx->lr, ctx);");
|
||||
sb.AppendLine($"{fallbackPad}}}");
|
||||
sb.AppendLine($"{fallbackPad}return;");
|
||||
}
|
||||
if (localFallthroughLr.HasValue)
|
||||
{
|
||||
sb.AppendLine($"{pad}}}");
|
||||
|
||||
@@ -241,12 +241,14 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
|
||||
var labelNames = func.Blocks.ToDictionary(b => b.Label, b => SanitizeLabel(b.Label), StringComparer.OrdinalIgnoreCase);
|
||||
var instructionContinuationLabels = new Dictionary<uint, string>();
|
||||
var needsInstructionContinuationLabels = func.Blocks
|
||||
var continuationCallCount = func.Blocks
|
||||
.SelectMany(static block => block.Instructions)
|
||||
.OfType<IrCall>()
|
||||
.Any(call =>
|
||||
.Count(call =>
|
||||
TryParseAddress(call.Target, out var target) &&
|
||||
(nonReturningCallTargets.Contains(target) || lrContinuationCallTargets.Contains(target)));
|
||||
var needsInstructionContinuationLabels = continuationCallCount > 0;
|
||||
var shareLrContinuationDispatch = continuationCallCount > 1;
|
||||
if (needsInstructionContinuationLabels)
|
||||
{
|
||||
foreach (var trace in func.Blocks.SelectMany(static block => block.Instructions).OfType<IrTracePpc>())
|
||||
@@ -405,7 +407,7 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
_activeGpuFifoBurstSlot = gpuFifoBurstPlan.Slot(block.Label, i);
|
||||
try
|
||||
{
|
||||
EmitInstruction(block.Label, directCallOrdinal, block.Instructions[i], body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase, localFallthroughLr, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets);
|
||||
EmitInstruction(block.Label, directCallOrdinal, block.Instructions[i], body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase, localFallthroughLr, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets, shareLrContinuationDispatch);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -423,7 +425,7 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
switch (term)
|
||||
{
|
||||
case IrUndefined undef:
|
||||
EmitInstruction(block.Label, -1, undef, body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase: false, localFallthroughLr: null, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets);
|
||||
EmitInstruction(block.Label, -1, undef, body, bufferBaseLength, 1, cfg, labelNames, types, signature, localPaired, _guestAbiProvider, knownConstants, localConstants, linkedAddressRemap, nonReturningCallTargets, lrContinuationCallTargets, stackFacts, inlineGuestThunkStackBase: false, localFallthroughLr: null, guestAbiContracts, stateFreeAbiContracts, stateFreeCallSymbols, stateFreeCallSiteVariants, modOverridableCallTargets, shareLrContinuationDispatch);
|
||||
AppendFlush(body, " ");
|
||||
body.AppendLine(" return;");
|
||||
break;
|
||||
@@ -509,6 +511,18 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
body.AppendLine();
|
||||
}
|
||||
|
||||
if (shareLrContinuationDispatch)
|
||||
{
|
||||
// Every call site has already reloaded the callee's state.
|
||||
// Keep the complete local target set, but emit it only once.
|
||||
body.AppendLine(" return;");
|
||||
body.AppendLine("[[maybe_unused]] lr_continuation_dispatch:");
|
||||
EmitLocalLrContinuationDispatch(body, " ", labelNames);
|
||||
body.AppendLine(" if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {");
|
||||
body.AppendLine(" InvokeIndirectCpu(ctx->lr, ctx);");
|
||||
body.AppendLine(" }");
|
||||
body.AppendLine(" return;");
|
||||
}
|
||||
body.Append('}');
|
||||
// The residency discovery pass only exists for its side effects
|
||||
// on the recorder; materializing its text costs a full copy of
|
||||
|
||||
@@ -1942,6 +1942,22 @@ public sealed partial class PpcLifter
|
||||
new IrCall(string.Empty, TargetLabel(ins, validAddresses, preferFallthrough: false), blArgs)
|
||||
};
|
||||
|
||||
case "bltl":
|
||||
{
|
||||
var crField = "cr0";
|
||||
if (ops.Count > 0 && ops[0] is PpcConditionRegisterOperand crOp)
|
||||
{
|
||||
crField = NormalizeRegister(crOp.Name);
|
||||
}
|
||||
|
||||
return new IrInstruction[]
|
||||
{
|
||||
new IrAssign("lr", IrValue.Imm((int)ins.EndAddress)),
|
||||
new IrBranch("blt", TargetLabel(ins, validAddresses, preferFallthrough: false),
|
||||
$"0x{ins.EndAddress:X8}", crField)
|
||||
};
|
||||
}
|
||||
|
||||
case "bcl":
|
||||
{
|
||||
var rawInstr = ReadRawInstruction(ins);
|
||||
|
||||
@@ -14,6 +14,23 @@ public class PpcLifterAdditionalCoverageTests
|
||||
private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands)
|
||||
=> PpcInstruction.Synthetic(0x80000000, raw, mnemonic, operands);
|
||||
|
||||
[Fact]
|
||||
public void LinkedConditionalBranchSetsLrBeforeEitherPath()
|
||||
{
|
||||
var branch = PpcDecoder.Decode(0x80004394, 0x41800029);
|
||||
var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir;
|
||||
|
||||
Assert.Equal("bltl", branch.Mnemonic);
|
||||
var lr = Assert.IsType<IrAssign>(ir[0]);
|
||||
Assert.Equal("lr", lr.Destination);
|
||||
Assert.Equal(unchecked((int)0x80004398u), lr.Value.Constant);
|
||||
|
||||
var decision = Assert.IsType<IrBranch>(ir[1]);
|
||||
Assert.Equal("blt", decision.Condition);
|
||||
Assert.Equal("0x800043BC", decision.TrueLabel);
|
||||
Assert.Equal("0x80004398", decision.FalseLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiftsAdditionalNonDotArithmeticAndLogicalForms()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.CodeGen;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Representation;
|
||||
using Xunit;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public class SharedLrContinuationCodeGenTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(2)]
|
||||
[InlineData(20)]
|
||||
public void MultipleHooksShareOneCompleteDispatchAndKeepNormalFallthrough(int callCount)
|
||||
{
|
||||
var instructions = new List<IrInstruction>();
|
||||
for (var i = 0; i < callCount; i++)
|
||||
{
|
||||
instructions.Add(new IrAssign("lr", IrValue.Imm(unchecked((int)(0x80001004u + (uint)i * 4u)))));
|
||||
instructions.Add(new IrCall(string.Empty, "0x81800000", Array.Empty<IrValue>()));
|
||||
}
|
||||
instructions.Add(new IrAssign("r3", IrValue.Imm(8)));
|
||||
instructions.Add(new IrReturn(null));
|
||||
var function = new IrFunction("shared_lr_continuation", "0x80001000", new[]
|
||||
{
|
||||
new IrBasicBlock("0x80001000", instructions),
|
||||
new IrBasicBlock("0x80001100", new IrInstruction[]
|
||||
{
|
||||
new IrAssign("r3", IrValue.Imm(1)), new IrReturn(null)
|
||||
})
|
||||
});
|
||||
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
|
||||
{
|
||||
["lr"] = ValueRepresentation.UInt32, ["r3"] = ValueRepresentation.UInt32
|
||||
});
|
||||
var code = new CxxLinearCodeGenerator().Emit(0x80001000,
|
||||
new SsaTransformer().Convert(function),
|
||||
new FunctionAbiClassification("shared_lr_continuation", ValueRepresentation.Void), types,
|
||||
lrContinuationCallTargets: new HashSet<uint> { 0x81800000u });
|
||||
|
||||
Assert.Equal(1, code.Split("switch (ctx->lr)").Length - 1);
|
||||
Assert.Equal(callCount, code.Split("goto lr_continuation_dispatch;").Length - 1);
|
||||
Assert.Equal(callCount, code.Split("if (ctx->lr != ").Length - 1);
|
||||
Assert.Contains("case 0x80001100u:", code);
|
||||
Assert.Contains("goto loc_80001100;", code);
|
||||
Assert.Contains("InvokeIndirectCpu(ctx->lr, ctx);", code);
|
||||
var sharedLabel = code.IndexOf("lr_continuation_dispatch:", StringComparison.Ordinal);
|
||||
Assert.True(code.IndexOf("r3 = 8;", StringComparison.Ordinal) < sharedLabel);
|
||||
Assert.True(code.IndexOf("case 0x80001100u:", StringComparison.Ordinal) > sharedLabel);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user