diff --git a/Launcher/build-dawn-linux.sh b/Launcher/build-dawn-linux.sh new file mode 100755 index 0000000..4885c96 --- /dev/null +++ b/Launcher/build-dawn-linux.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# Builds the pinned Dawn for desktop Linux with Aurora's patches (aurora-main/patches/dawn): the +# 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). +# +# Launcher/build-dawn-linux.sh [--work-dir DIR] [--cc PATH --cxx PATH] [--cmake PATH] +# [--ninja PATH] [--python PATH] [--jobs N] [--force] +# +# It builds for the machine it runs on (the Frame's aarch64, in a container there). Pass the same +# --cc/--cxx as Launcher/local-build.sh gets: the archive is static and carries C++ objects, so it +# must be built against the same C++ standard library as the game. +# +# The result is an install tree in the stock package's layout under WORK_DIR/package, with an +# aurora-dawn.json declaring the two ABIs (AuroraVulkanAbi, AuroraFdmAbi) that +# aurora-main/cmake/AuroraDawnProvider.cmake reads. local-build.sh --dawn-package takes it. A later +# run with the same inputs reuses it. +# +# Prerequisites: git, python3, curl, tar, CMake 3.25+, Ninja, a C/C++ compiler, and the X11, XCB and +# Wayland development headers Dawn's Vulkan surfaces need. The first build compiles all of Dawn and +# Tint and takes a while. +set -euo pipefail + +fail() { + echo "build-dawn-linux.sh: error: $*" >&2 + exit 1 +} + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo=$(cd "$script_dir/.." && pwd) +work_dir="$repo/.scratch/linux-dawn" +cc_bin=${CC:-cc} +cxx_bin=${CXX:-c++} +cmake_bin=cmake +ninja_bin=ninja +python_bin=python3 +jobs=$(nproc) +force=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --work-dir) work_dir=$2; shift 2 ;; + --cc) cc_bin=$2; shift 2 ;; + --cxx) cxx_bin=$2; shift 2 ;; + --cmake) cmake_bin=$2; shift 2 ;; + --ninja) ninja_bin=$2; shift 2 ;; + --python) python_bin=$2; shift 2 ;; + --jobs) jobs=$2; shift 2 ;; + --force) force=1; shift ;; + -h|--help) sed -n '2,24p' "$0"; exit 0 ;; + *) fail "unknown argument: $1" ;; + esac +done + +for tool in "$cc_bin" "$cxx_bin" "$cmake_bin" "$ninja_bin" "$python_bin" git curl tar sha256sum; do + command -v "$tool" >/dev/null 2>&1 || fail "required tool '$tool' was not found" +done +mkdir -p "$work_dir" +work_dir=$(cd "$work_dir" && pwd) +cc_path=$(command -v "$cc_bin") +cxx_path=$(command -v "$cxx_bin") + +# The revision the stock packages, the Windows Vulkan DLL (Launcher/Build-DawnVulkan.ps1) and the +# Quest's Dawn (android/Build-QuestDawn.ps1) are built from. +revision=13abc3bc8ea2d3c2050f9e77a12d012108ceee24 +archive_hash=713bea5b92d4f6c5175752fd7cbf1c3c5ce36598ff5dd98685d8a1216614ebba +flags=( + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + -DDAWN_FETCH_DEPENDENCIES=ON + -DDAWN_BUILD_MONOLITHIC_LIBRARY=STATIC + -DBUILD_SHARED_LIBS=OFF + -DDAWN_ENABLE_INSTALL=ON + -DDAWN_BUILD_SAMPLES=OFF + -DDAWN_BUILD_TESTS=OFF + -DDAWN_BUILD_BENCHMARKS=OFF + -DDAWN_USE_GLFW=OFF + -DDAWN_USE_WAYLAND=ON + -DDAWN_ENABLE_DESKTOP_GL=OFF + -DDAWN_ENABLE_OPENGLES=OFF + -DTINT_BUILD_TESTS=OFF + -DTINT_BUILD_CMD_TOOLS=OFF + -DTINT_BUILD_IR_BINARY=OFF + -DDAWN_BUILD_PROTOBUF=OFF +) + +patch_dir="$repo/aurora-main/patches/dawn" +patch_files=( + "$patch_dir/apply.py" + "$patch_dir/aurora_vulkan_hooks.h" + "$patch_dir/aurora_vulkan_interop.inc" + "$patch_dir/aurora_fdm.h" + "$patch_dir/aurora_fdm.inc" + "$repo/aurora-main/include/aurora/dawn_vulkan_abi.h" + "$repo/aurora-main/include/aurora/dawn_fdm_abi.h" +) +# Hashed with LF line endings and each file's name, as Build-QuestDawn.ps1 does. +patch_hash=$(for file in "${patch_files[@]}"; do + printf '%s\n' "$(basename "$file")" + tr -d '\r' < "$file" +done | sha256sum | awk '{print $1}') +compiler_id=$("$cxx_path" --version 2>/dev/null | head -n 1 | tr -d '"\\') +cache_key=$(printf '%s|%s|%s|%s|%s|%s' "$revision" "$archive_hash" "$patch_hash" "$(uname -m)" \ + "$compiler_id" "${flags[*]}" | sha256sum | awk '{print $1}') + +package="$work_dir/package" +manifest="$package/aurora-dawn.json" +library="$package/lib/libwebgpu_dawn.a" +[[ -f "$library" ]] || library="$package/lib64/libwebgpu_dawn.a" +if [[ "$force" -eq 0 && -f "$manifest" && -f "$library" ]]; then + library_hash=$(sha256sum "$library" | awk '{print $1}') + if grep -q "\"CacheKey\": \"$cache_key\"" "$manifest" && + grep -q "\"ArchiveSha256\": \"$library_hash\"" "$manifest"; then + echo "Patched Dawn for Linux is up to date: $package" + exit 0 + fi +fi + +archive="$work_dir/dawn-source.tar.gz" +if [[ ! -f "$archive" ]]; then + curl -fL --retry 3 -o "$archive.partial" "https://github.com/google/dawn/archive/$revision.tar.gz" + mv "$archive.partial" "$archive" +fi +[[ "$(sha256sum "$archive" | awk '{print $1}')" == "$archive_hash" ]] || + fail "Dawn source archive does not match the pinned SHA-256; delete $archive and try again" + +# The patches are applied to pristine sources: src/ is extracted again whenever the patches changed, +# while third_party/, which DAWN_FETCH_DEPENDENCIES fills, is kept. +source="$work_dir/dawn-$revision" +patch_marker="$source/aurora-patches.sha256" +if [[ ! -f "$patch_marker" || "$(cat "$patch_marker")" != "$patch_hash" ]]; then + if [[ -d "$source" ]]; then + rm -rf "$source/src" + tar -xzf "$archive" -C "$work_dir" "dawn-$revision/src" + else + tar -xzf "$archive" -C "$work_dir" + fi + "$python_bin" "$patch_dir/apply.py" "$source" + printf '%s' "$patch_hash" > "$patch_marker" +fi + +build="$work_dir/build" +"$cmake_bin" -S "$source" -B "$build" -G Ninja \ + -DCMAKE_MAKE_PROGRAM="$(command -v "$ninja_bin")" \ + -DCMAKE_C_COMPILER="$cc_path" -DCMAKE_CXX_COMPILER="$cxx_path" \ + -DPython3_EXECUTABLE="$(command -v "$python_bin")" \ + "${flags[@]}" -DCMAKE_INSTALL_PREFIX="$package" +"$cmake_bin" --build "$build" --parallel "$jobs" +rm -rf "$package" +"$cmake_bin" --install "$build" +library="$package/lib/libwebgpu_dawn.a" +[[ -f "$library" ]] || library="$package/lib64/libwebgpu_dawn.a" +[[ -f "$library" ]] || fail "Dawn archive missing under $package" +# As the stock package: debug info would only make the archive several times larger. +strip_bin=$(dirname "$cc_path")/llvm-strip +[[ -x "$strip_bin" ]] || strip_bin=strip +"$strip_bin" --strip-debug "$library" + +cat > "$manifest" < --headset steam_frame`. See `docs/steam-frame.md`. | | Other platforms | Not wired yet. | Both bindings share `openxr_integration.cpp`: the pacing thread, policy evaluation, the diff --git a/README.md b/README.md index 25db3ea..a7142a7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ to immersive stereo rendering. VR is opt-in and falls back to the normal desktop runtime or headset is unavailable. In first person you sit in the cockpit, where the steering wheel or handlebar turns with your steering, and hand steering by heurazy lets you grab it with the tracked controllers and turn it. On a Quest the hands can follow the headset's own hand tracking. -A Steam Frame build of the Android app (not yet tested on the headset) adds the Frame controllers' +A native SteamOS build for the Steam Frame (not yet tested on the headset) adds the Frame controllers' D-pad, a 120 Hz display for the game's 60 FPS, and foveation that follows your eyes; see [`docs/steam-frame.md`](docs/steam-frame.md). See [`OPENXR.md`](OPENXR.md) for setup, configuration, and the current limitations. diff --git a/docs/steam-frame.md b/docs/steam-frame.md index c12bfce..6261b1e 100644 --- a/docs/steam-frame.md +++ b/docs/steam-frame.md @@ -1,23 +1,112 @@ # 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. It runs -Android apps through Lepton, SteamOS's Android layer, where SteamVR provides an Android OpenXR runtime -(OpenXR 1.0, through the Khronos loader's runtime broker). The Steam Frame build is therefore a third -flavour of the Quest app, `steamFrame`: everything in `docs/quest-port.md` below the app shell (the -Vulkan backend, the game kit, `.wcgame` packages, the on-headset build) applies unchanged, and this -document covers what differs. +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: -A native SteamOS ARM64 build is a separate, later piece of work: Linux has no OpenXR graphics backend -yet (see [A native SteamOS build](#a-native-steamos-build)). +- **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)). -**Status: not yet run on a Steam Frame, and the Android backend as it stands cannot present under -Lepton**: Lepton's Vulkan driver has no external memory or sync fd extensions, which the Quest -backend's two-device design needs (see [What the Frame reported](#what-the-frame-reported)). The -flavour, controller, refresh rate and foveation work below stays valid; the eye handoff has to move -to a single shared device first. +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. -## What the flavour changes +**Status: not yet run on a Steam Frame.** The native build's VR code compiles and the unit tests +pass; building it on the Frame and the device checks are still to do. + +## 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@`): + +```bash +mkdir -p ~/wiicompiled && cd ~/wiicompiled +git clone -b claude/peaceful-keller-2ek99b 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 /sys/main.dol /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. + +### Running it + +The game reads its `Config.toml` from `~/.local/share/WiiCompiled/` on SteamOS (it is created on the first start): set +`[paths] dvd_root` there to your extracted disc (the directory holding `sys/` and `files/`). Start +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 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` | | --- | --- | --- | @@ -185,7 +274,7 @@ What follows from them: 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 ([below](#a-native-steamos-build)). + 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`, @@ -315,26 +404,3 @@ Open questions only the device can answer: - 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. - -## A native SteamOS build - -Valve recommends native Linux ARM64 builds for the Frame, and one would avoid Lepton and the -two-device copy. The pieces: - -- **Backend.** The Windows Vulkan backend (`openxr_vulkan_win32.cpp`), where OpenXR creates Dawn's - own device and eyes are copied on Dawn's queue, ports almost as it is. Only its `_WIN32` guards are - platform-specific. -- **Interop.** Aurora's `vulkan_win32_interop.cpp` finds the patched Dawn's exports with - `GetModuleHandleW`. A static Linux Dawn would reference them directly, and `aurora_core.cmake` - compiles that file on Windows only. -- **Dawn.** A linux-aarch64 Dawn built with `aurora-main/patches/dawn` (the hook ABI and density - maps), as `android/Build-QuestDawn.ps1` already does for Android. -- **`openxr_integration.cpp`.** It needs a Linux branch asking for `XR_KHR_vulkan_enable2` and - `XR_KHR_convert_timespec_time`. Today its `#else` is Android's and requires - `XR_KHR_android_create_instance`. -- **Controller timing.** `openxr_input.cpp` needs a `__linux__` branch for the input clock. -- **CPU target.** An `MKW_LINUX_CPU` knob in place of `-mcpu=native`, for cross-builds. -- **Android-gated fixes.** The Adreno vertex padding, `headset_owns_display` and the - `last_pass_feeding_replay` saving are gated on `__ANDROID__`. They would follow the GPU or the - headset instead. -- **Packaging.** SteamOS packaging, and a way to build the player's game for it.