diff --git a/.gitignore b/.gitignore
index 1c69c0a..10356c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,7 @@ project.lock.json
/tools/
/TEMP/
/tmp/
+/.scratch/
/test_output/
*.log
output.txt
diff --git a/Launcher/Build-Installer.ps1 b/Launcher/Build-Installer.ps1
index 41662b7..f547d4c 100644
--- a/Launcher/Build-Installer.ps1
+++ b/Launcher/Build-Installer.ps1
@@ -98,7 +98,7 @@ Assert-Directory $dependencySources 'Pinned offline dependency sources'
# to compile (launcher/Prepare-NativePrebuilt.ps1).
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
# refuses to call a toolkit complete unless every one of these directories is present.
-$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
+$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','openxr','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
# The precompiled archives are only interchangeable with what the user's machine
# compiles if both came from this toolchain and this flag set, so a stale package
@@ -225,6 +225,7 @@ Write-Host '[3/6] Writing manifests and third-party license inventory...'
Copy-Item (Join-Path $portableTools 'README-LICENSES.txt') (Join-Path $payloadRoot 'licenses\Portable-build-tools.txt')
Copy-Item (Join-Path $repoRoot 'aurora-main\LICENSE') (Join-Path $payloadRoot 'licenses\Aurora-LICENSE.txt')
Copy-Item (Join-Path $dependencySources 'cppwinrt\LICENSE.txt') (Join-Path $payloadRoot 'licenses\CppWinRT-LICENSE.txt')
+Copy-Item (Join-Path $dependencySources 'openxr\LICENSE') (Join-Path $payloadRoot 'licenses\OpenXR-SDK-LICENSE.txt')
# The precompiled aurora/third-party archives are built from the very sources
# already shipped under build-workspace\Dependencies and aurora-main, so they add
# no third-party component and therefore no new license obligation.
diff --git a/Launcher/LocalBuild.ps1 b/Launcher/LocalBuild.ps1
index b4538be..cf8c399 100644
--- a/Launcher/LocalBuild.ps1
+++ b/Launcher/LocalBuild.ps1
@@ -189,12 +189,13 @@ $pins = Get-MkwProjectPins $project
# (the real RAM guard, capping concurrent clang compiles of memory-hungry translated TUs via the Ninja
# MKW_TRANSLATED_COMPILE_JOBS pool), and $globalJobs (Ninja's overall parallelism for everything else).
# An explicit -Parallel pins all three.
-$memoryGiB = [math]::Max(1, [math]::Floor((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB))
if ($Parallel -gt 0) {
$translatorThreads = $Parallel
$translatedJobs = $Parallel
$globalJobs = $Parallel
} else {
+ $memoryGiB = [math]::Max(1, [math]::Floor(
+ (Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB))
$translatorThreads = [math]::Max(1, [math]::Min([Environment]::ProcessorCount, 16))
$translatedJobs = [math]::Max(1, [math]::Min([Environment]::ProcessorCount,
[math]::Floor($memoryGiB / 2)))
diff --git a/Launcher/WiiCompiled.Setup.Windows/InstalledLayout.cs b/Launcher/WiiCompiled.Setup.Windows/InstalledLayout.cs
index 598343e..03521d6 100644
--- a/Launcher/WiiCompiled.Setup.Windows/InstalledLayout.cs
+++ b/Launcher/WiiCompiled.Setup.Windows/InstalledLayout.cs
@@ -27,7 +27,7 @@ internal static class InstalledLayout
public static readonly string[] DependencyNames =
[
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "libusb", "native_prebuilt",
- "png", "SDL", "sqlite3", "tracy", "xxhash", "zlib", "zstd"
+ "openxr", "png", "SDL", "sqlite3", "tracy", "xxhash", "zlib", "zstd"
];
}
diff --git a/OPENXR.md b/OPENXR.md
new file mode 100644
index 0000000..0d08d01
--- /dev/null
+++ b/OPENXR.md
@@ -0,0 +1,95 @@
+# Experimental OpenXR VR
+
+WiiCompiled has an opt-in OpenXR rendering path. The first functional backend is Windows D3D12.
+It asks the OpenXR runtime for the required GPU before Aurora creates Dawn, then copies each eye
+on that same D3D12 device and queue into the acquired OpenXR swapchain images. Eye submission
+stays on the GPU; there is no CPU texture readback and no second graphics device.
+
+This is an experimental renderer, not yet a release-ready VR mode.
+
+## Requirements
+
+- A Windows OpenXR runtime selected as the system's active runtime.
+- A connected headset supported by that runtime.
+- A D3D12-capable GPU and driver accepted by both OpenXR and Dawn.
+- A build made with `MKW_ENABLE_OPENXR=ON`, which defaults on for Windows and off elsewhere while
+ the Vulkan bridge remains capability-gated.
+
+OpenXR remains disabled until requested in `Config.toml`. The file is next to the installed game
+configuration and is created with the following defaults:
+
+```toml
+[vr]
+enabled = false
+required = false
+render_scale = 1.0
+world_units_per_meter = 500.0
+hud_distance_meters = 2.0
+stop_at_display_copy = true
+skip_copy_clears = true
+```
+
+Set `enabled = true`, close the game completely, and start it again. These settings are read only
+at launch. The in-game F10 settings bar also exposes the enable switch, but a restart is still
+required.
+
+`required = false` is the safe default: an absent runtime, disconnected headset, unsupported GPU,
+or graphics-binding failure is logged and the game continues in ordinary desktop mode. Set it to
+`true` only when a failed VR startup should stop the game with an error.
+
+`render_scale` scales the per-eye size recommended by the OpenXR runtime.
+`world_units_per_meter` controls the scale of headset translation in the game world.
+`hud_distance_meters` controls the distance of the head-locked virtual screen.
+`stop_at_display_copy` ends eye replay at the final `GXCopyDisp`, matching the frame shown on the
+desktop. `skip_copy_clears` independently suppresses the EFB reset performed after a copy. Both
+default on and can be changed live from the F10 settings bar for diagnostics.
+
+## Presentation policy
+
+The runtime deliberately fails safe instead of guessing which Mario Kart camera is active:
+
+- Menus, loading screens, unclassified scenes, and multiplayer render on a head-locked virtual
+ screen.
+- A PAL `RMCP01` race scene switches to immersive stereo only after translated-code observers
+ confirm exactly one distinct race camera for the current GX frame.
+- Leaving the race or observing zero or multiple cameras immediately returns presentation to the
+ virtual screen. Session/runtime loss safely tears down XR and continues on the desktop mirror.
+
+Aurora records the original GX frame once and replays it for both OpenXR eyes. Perspective GX draws
+receive asymmetric headset projections; orthographic and unclassified draws retain their original
+GX transforms during immersive replay. Menus and unsafe whole scenes use the virtual-screen path.
+Head pose is sampled by the OpenXR pacing thread, while Aurora's frame worker consumes a
+short-lived immutable stereo packet. Each sealed GX frame and immersive packet carry the same
+policy-generation tag; a mismatch is rendered in mono and the acquired XR frame is canceled, so an
+asynchronous menu/race transition cannot replay race transforms over unsafe content. A pause or
+minimized window can withdraw an unencoded packet and end that compositor frame without layers; an
+encoded-work stall requests teardown at Aurora's next safe producer boundary. All OpenXR session
+and swapchain calls remain on their owning thread.
+
+## Backend status
+
+| Backend | Status |
+| --- | --- |
+| Windows D3D12 | Implemented: same-adapter, same-device asynchronous OpenXR submission. |
+| Linux Vulkan | Capability-gated scaffold. The pinned Dawn package does not expose the complete native Vulkan instance/device/queue context needed for safe same-device OpenXR interop, so the runtime logs the limitation and falls back to desktop rendering. |
+| Other platforms | Not wired yet. |
+
+The Vulkan path intentionally does not create an unrelated Vulkan device or use a CPU readback as
+a workaround. It accepts a future explicit Dawn native context, including external queue locking,
+so it can be enabled once Aurora exposes those handles safely.
+
+## Current limitations
+
+- Only the project's supported PAL `RMCP01` translation has race instrumentation addresses.
+- Motion-controller/Wii Remote emulation and OpenXR action bindings are not implemented yet; use
+ the existing game-controller input path.
+- Dedicated Quest, Android, and Apple visionOS packaging is not implemented. The static recompilation
+ architecture avoids a runtime JIT, but each platform still needs an Aurora graphics bridge,
+ windowing/lifecycle work, and packaging.
+- Scene-specific comfort options, culling fixes, replay/spectator classification, and a broader VR
+ settings UI beyond the current enable/replay controls are future work.
+- The desktop window remains available as a mirror/fallback.
+
+OpenXR diagnostics are written to the normal run log under
+`%LOCALAPPDATA%\WiiCompiled\Logs`. Search for `OpenXR` when reporting a startup or submission
+failure.
diff --git a/README.md b/README.md
index 8dd8426..0895bcc 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,13 @@ The graphics layer is built on
**High internal resolution.**
Play at several times the console's resolution.
+**Experimental OpenXR VR.**
+Windows builds can render through a D3D12 OpenXR runtime without CPU readback. Menus and
+unsupported scenes appear as a head-locked virtual screen; a validated single-camera race switches
+to immersive stereo rendering. VR is opt-in and falls back to the normal desktop renderer if the
+runtime or headset is unavailable. See [`OPENXR.md`](OPENXR.md) for setup, configuration, and the
+current limitations.
+
**Music ducking.**
Start playing something else, Spotify, a YouTube video, and
the game automatically mutes its own music until the other audio stops. Optional, if you'd
@@ -178,6 +185,8 @@ All translated output is verified against real hardware behavior and most import
project's whole graphics layer sits on. MIT licensed.
- **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering
aurora's Direct3D, Vulkan and OpenGL backends.
+- **[OpenXR](https://www.khronos.org/openxr/)** - the Khronos cross-platform API used by the
+ experimental VR renderer.
- **[Dolphin Emulator](https://github.com/dolphin-emu/dolphin)** - an invaluable reference for Wii
hardware behavior during development, plus the source of the free DSP coefficient ROM and the
unmodified default WiiConnect24 bootstrap tree bundled with the runtime.
diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md
index 6214087..c297c89 100644
--- a/THIRD-PARTY-NOTICES.md
+++ b/THIRD-PARTY-NOTICES.md
@@ -141,6 +141,7 @@ included in the installer's `licenses/` folder.
| SQLite | 3.51.3 amalgamation | Public domain | |
| Tracy Profiler | pinned commit | BSD-3-Clause | |
| C++/WinRT | - | MIT (Microsoft) | |
+| OpenXR-SDK | 1.1.61 | Apache-2.0 | |
| nodtool (disc image extraction) | v2.0.0-alpha.10 | MIT OR Apache-2.0 | |
### Dual-licensed components - elections made by this project