Files
mitch030504--Wiicompiled_VR…/OPENXR.md
T

517 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
For managed installation, use [WheelWizard VR](https://github.com/iChris4/WheelWizard_VR/releases/latest)
and enable **Settings → Other → WiiCompiled (beta) → Enable WiiCompiled OpenXR VR (beta)**.
The launcher sets `vr.enabled=true`, `vr.required=false`, and `video.graphics_api="d3d12"` before
each VR launch, preserving other preferences. Its portable configuration lives at
`RecompVR/UserData/Config.toml` beneath WheelWizard's data folder. Normal graphics settings remain
in `Recomp/UserData/Config.toml`. Both backends use the normal installation's effective NAND.
Standalone launches start in VR too: this is the VR build, and `required = false` makes a failed
headset startup fall back to the desktop renderer rather than stop the game. `Config.toml` is
created with the following defaults, and a configuration that never mentions `enabled` reads the
same way:
```toml
[vr]
enabled = true
required = false
mirror_view = "normal"
controller_mode = "wii_remote"
frame_interpolation_fps = 0
render_scale = 1.0
world_units_per_meter = 500.0
hud_distance_meters = 2.0
hud_width_meters = 2.4
hud_virtual_screen = true
stop_at_display_copy = true
skip_copy_clears = true
first_person = false
first_person_units_per_meter = 30.0
first_person_head_up_meters = 3.0
first_person_head_forward_meters = 0.0
first_person_head_right_meters = 0.0
first_person_hide_driver = true
first_person_hidden_model = 0
first_person_rotation = "yaw"
performance_level = "boost"
```
To play this installation on the desktop instead, set `enabled = false`, 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. A temporary
notification explains the failure; the message remains available under **F10 → VR**. Set it to
`true` only when a failed VR startup should stop the game with an error.
`mirror_view` chooses what the desktop window shows while the headset is running: `"normal"`
keeps the ordinary desktop view, `"both"`, `"left"` and `"right"` mirror the headset's eyes, and
`"none"` blacks the window out. It is live and can be changed from the F10 settings bar, where it
sits directly under the enable switch as *Desktop view*. Menus reach the headset as a virtual
screen carrying the desktop image itself, so there is no separate eye view to mirror there and the
three eye choices show that same image; only `"none"` differs. The F10 bar is drawn over whichever
image is chosen, so the setting can always be changed back.
Eye mirror modes retain the last eye image when a desktop frame has no new XR packet, so they
do not alternate with the normal camera. `"none"` also stays black between XR packets.
## Local multiplayer
During 2-, 3-, and 4-player races, the headset replays Player 1's world in immersive stereo
with head tracking. Keep **F10 > VR > Desktop view** set to **Normal** (`mirror_view = "normal"`)
for the original desktop split-screen layout. No extra multiplayer switch is required.
Menus continue to use the virtual screen.
Only headset replay filters the other players' viewports and expands Player 1 to each eye.
The desktop split-screen partition (the game's `partition_line` layout, one-pixel textured
picture panes on the split boundaries) and full masks of the other panes are omitted from
the eyes; the desktop image keeps them.
Player-local HUD viewports follow Player 1; shared orthographic overlays keep their full-screen
layout on the virtual screen. Framebuffer effects that sample the desktop split-screen image
are omitted from multiplayer eyes, since those textures contain the other cameras too.
The local-screen count is sealed with each frame, including retained VR interpolation frames,
and a layout change invalidates older XR packets.
Multiplayer uses Player 1's game camera. The optional first-person relocation and model hiding
remain single-player-only: guest model visibility changes would also affect the desktop players.
The opt-in `stereo_multiplayer_smoke` D3D12 test reads back both eye images and the desktop EFB
for 1/2/3/4/1-screen transitions with VR interpolation on and off. Actual headset racing still
needs visual validation for course effects, HUD layout, pause/resume, and scene transitions.
**F10 > VR > VR frame interpolation (experimental)** offers **Off, Auto, 72, 90, 120** and
applies immediately. `frame_interpolation_fps` stores `0` for Off (the default), `1` for Auto,
or the selected rate. The earlier `frame_interpolation = true` checkbox migrates to Auto.
Auto renders at the headset's display deadlines; the numbered choices cap the rate of new
stereo frames. They do not change the headset's physical refresh setting. For VDXR with Virtual
Desktop set to 90 Hz, select Auto or 90. The menu shows both the detected headset rate and the
rate of newly rendered VR frames, excluding repeated images. Menus and other virtual-screen
scenes continue at the game's rate; assess interpolation during an immersive race.
VR interpolation is independent of **Graphics > Race frame interpolation**. The simulation,
physics, audio and VI remain at 60 Hz. Scene motion is delayed by one game frame (about 16.7 ms)
to interpolate between known transforms; each rendered eye pair uses a fresh predicted head
pose. This needs enough GPU headroom to render both eyes at the target rate, and carries the
desktop interpolator's experimental artifacts, especially for unmatched or changing geometry.
Refresh detection uses `XR_FB_display_refresh_rate` when available and the OpenXR predicted
display period otherwise. Interpolation requires `XR_KHR_win32_convert_performance_counter_time`
to relate those display deadlines to the game's clock; the menu reports if it is unavailable.
The old Eager Frame Heartbeat option has been removed and existing `eager_frame_heartbeat`
settings are ignored. Completed rendering wakes the XR thread immediately. A 50 ms keep-alive
still protects pauses and window dragging without eager repeats during rendering.
`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` and `hud_width_meters` place and size the virtual screen. They are read at
launch and govern both the menu screen and the in-race 2D screen, so 2D content keeps its place
across the transition. `hud_virtual_screen` decides whether the race's 2D layer uses that screen;
it is live and can be flipped from the F10 settings bar.
`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.
`first_person` and the `first_person_*` values are the first-person camera described below. All
four are live and are also exposed in the F10 settings bar.
`performance_level` is the level asked of the runtime through `XR_EXT_performance_settings` for
its CPU and GPU domains: `boost`, `sustained_high`, `sustained_low`, `power_savings`, or
`default` to leave the runtime's own choice. Standalone headsets clock their cores by this
request (see `docs/quest-port.md`); desktop runtimes rarely offer the extension, and the setting
then does nothing. It is read at launch, and the session log records whether the runtime accepted
it and any later performance notification (a thermal or rendering warning).
## Controllers
The headset's tracked controllers reach the game through an OpenXR action set synced on the pacing
thread (`runtime/src/vr/openxr_input.cpp`), which feeds a virtual SDL gamepad that Aurora assigns
to a port like any other. `controller_mode` decides what the game finds on that port, and is live
from **F10 > VR > VR controllers**; the game sees a change as a controller reconnection.
`"wii_remote"`, the default, presents them as a Wii Remote with a Nunchuk, the way DolphinXR's
OpenXR Wii Remote does, with buttons adapted from its default `OpenXR Wii Remote` profile for the
Touch controllers. The port is served through KPAD like a Bluetooth remote
(`wii_remote_input.cpp`), so `WPADProbe` reports a Nunchuk and the game runs its own Wii Remote + Nunchuk control scheme:
| Controller | Wii |
| --- | --- |
| Right A | A |
| Right trigger | B |
| Right stick up / down | 1 / 2 |
| Left X | − |
| Left menu | + |
| Left stick | Nunchuk stick |
| Left trigger | Z |
| Left grip | C |
| Left Y | Settings panel (not a Wii button) |
| Right controller motion and aim | Wii Remote accelerometer and pointer |
| Left controller motion | Nunchuk accelerometer |
Analog inputs count as pressed past half travel. Right B, right stick left / right and the stick
clicks are unbound, and no controller button presses HOME. The game's Wii Remote rumble vibrates
both controllers, subject to the ordinary controller-vibration switch.
**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
the predicted display time, whose extrapolation sprays fast wrist motion. The grip's linear
velocity, averaged with one derived from its position, is differentiated over XrTime into
acceleration; gravity is added and the result is expressed in the aim pose's frame. KPAD's
accelerometer axes are the aim pose's `(x, -y, z)` in g: a level controller reads `(0, -1, 0)`,
pointing at the floor `(0, 0, 1)`. Readings saturate at ±3.6 g like the remote's sensor, and a
controller that loses tracking repeats its last reading. The game's own motion detection (tricks,
wheelies) then works on these readings as it would on a remote's.
**Pointer.** The pointer is absolute, as in DolphinXR: the right controller's aim ray is intersected
with the screen the renderer is showing, and the point it meets is where the cursor goes, so there
is nothing to recenter. On the menu screen that is the quad layer, `hud_width_meters` across with
the eye texture's aspect, and the pointer spans the game picture inside it (Aurora letterboxes the
desktop image into the quad and the picture into the desktop image, so a 4:3 picture keeps its
pillarboxes). During a race it is the 2D layer's screen, `hud_distance_meters` ahead of the latched
race origin and turned by the lean-back angle, with the picture's aspect. With
`hud_virtual_screen = false` the race's 2D layer has no fixed place and the pointer is off. The
game's own pointer switch (`KPADEnableDpd` / `KPADDisableDpd`) is honoured as well.
The hit becomes KPAD's `pos` (−1..1 across the picture, +y down), `horizon` (the controller's roll
on the screen) and `dist` (perpendicular distance in metres, so rotating the controller does not
change it). Like a real remote's camera, the pointer keeps tracking up to 1.9 half-widths and 1.5
half-heights past the picture's centre; a lost hit or an excursion beyond that holds or pins the
cursor for 100 ms before it disappears, so tracking spikes during fast motion do not drop it.
Raw IR camera dots in `KPADGetUnifiedWpadStatus` stay invalid; the game reads the pointer from
`KPADStatus`.
**Settings in the headset.** Left Y opens the settings panel described below; while it is open the
controllers operate the panel and the game sees them idle.
`"gamepad"` keeps the controllers one ordinary gamepad read through PAD as a GameCube controller:
A/B → South/East, X/Y → West/North, index triggers → trigger axes, grips → shoulders, thumbsticks
→ sticks (clicks → stick buttons), left menu → Start. Every binding in the F10 controller menu
applies. Left Y is GameCube Y here, so clicking both thumbsticks together opens the settings panel
instead.
Bindings are suggested for `oculus/touch_controller` (Quest 2, 3 and Pro) and
`khr/simple_controller`. `mkw_vr_wii_remote_tests` checks the accelerometer frame, the pointer
raycast and debounce, the picture placement and the button profile without a headset.
## Settings in the headset
The F10 settings bar is only visible on the desktop window, so the same settings are also offered on
a panel inside the headset, in menus and during an immersive race alike, including on the Quest.
**Press left Y** to open it, and again to close it (with `controller_mode = "gamepad"`, **click both
thumbsticks together** instead); the left controller's menu button and the panel's *Close* button
also close it. It can be opened from the desktop as well, with
**F10 → VR → Show these settings in the headset**.
The panel has the F10 bar's menus as tabs (VR, Graphics, Controllers, Audio, Diagnostics) and a
*Recenter view* button. Aim a controller at it: the cursor goes where you aim, a trigger (or A / X)
selects and drags sliders, and a thumbstick scrolls. Whichever hand last pulled its trigger does the
pointing. Changes apply exactly as they do from the F10 bar, and the two stay in step.
While the panel is open, and until every button has been released after it closes, the game sees
the VR controllers idle: no buttons, no pointer and a remote at rest. Nothing reaches the game from
the panel button, the trigger that clicked *Close*, or the menu press that closed the panel. The
game is not paused, so a race carries on while you change settings. Other controllers (keyboard, desktop
gamepads, Bluetooth remotes) are not affected.
The panel sits centred on the virtual screen, three quarters of its width across (1.8 m with the
default `hud_width_meters`). On a menu that is the anchored menu quad; in a race it is the 2D layer's
screen, `hud_distance_meters` ahead of the latched race origin and turned by the lean-back angle,
whether or not `hud_virtual_screen` places the HUD there. **Recenter view** brings both back in front
of you.
How it is drawn: `settings_overlay.cpp` builds the panel with a second Dear ImGui context of its own,
a 1440 × 1080 canvas at twice the desktop menu's scale with its own font atlas, fed by the pointer
that `openxr_input.cpp` publishes through `vr/openxr_settings_panel.h`. Aurora renders that draw data
into a panel texture once per sealed frame and lays it over each eye after the eye is finished
(`aurora-main/lib/stereo_overlay.cpp`): through the eye's frustum and `viewFromCenter` onto the
screen rectangle for an immersive eye (including headset-rate interpolated eyes, which reuse the
texture), and as a centred rectangle on a virtual-screen eye image. The eye images the OpenXR
backends already submit carry it, so no extra swapchain or composition layer is involved. The
ImGui backend keeps a single projection uniform, so the panel's pass is submitted on its own command
buffer before the desktop's ImGui pass of the same frame is recorded.
`mkw_vr_settings_panel_tests` covers the panel button in both controller modes, the release latch,
selection, scrolling and the canvas mapping; `gx_fifo_tests` covers where the panel lands in each eye.
## The first-person camera
By default the headset sits where Mario Kart's own chase camera sits, and `world_units_per_meter`
of 500 presents the race as a small diorama on a table. Turning on `first_person` moves the camera
to the local driver's head instead, and switches the world scale to
`first_person_units_per_meter`, whose default of 30 is what makes the race read life-size from the
seat. It is a matter of taste rather than a property of the game, so the F10 bar exposes it.
The kart is selected through the game's local-screen-to-racer mapping, including online races
where your racer is not slot zero. First person requires a locally controlled racer; spectating
another racer keeps the game's own camera.
The game's own transforms are never modified. Each guest frame the runtime reads the race camera's
view matrix and the player kart's physics pose and derives one affine transform from the recorded
view space into the space to render from. That transform is published with the sealed frame, and
the renderer composes it onto every perspective draw's model-view matrix, alongside the headset's
own per-eye delta. The kart's *physics* pose is used deliberately, not the animated model: an
animated frame would bob and lurch the camera.
`first_person_rotation` decides where the view's orientation comes from, mirroring DolphinXR's
camera-anchor modes. `"yaw"`, the default, keeps the horizon level through a chase-camera tilt or a
banked corner. `"yaw_pitch"` adds the kart's climb, so a slope or a wheelie tips the view while a
banked corner still never rolls it. `"full"` takes the kart's whole orientation, banking included.
All three are the same construction from a forward and an up axis, differing only in which pair
they take: pairing a forward with world up is what removes roll. The headset always adds free look
on top of whichever is chosen, and only the translation onto the head is common to all three.
The head's place in the kart is `first_person_head_up_meters` and its two companions, measured in
the kart's own frame; the F10 sliders exist because the comfortable value is a matter of taste and
is best judged from inside the headset.
The mode engages only in a single-screen race, the same content that already qualifies for
immersive stereo. Menus, split-screen, and the virtual-screen fallback are unaffected, and so is
the desktop mirror, which keeps showing the game's ordinary third-person view. If the kart or
camera cannot be read the camera stays where the game put it rather than guessing.
Your own driver sits exactly where your eyes are, so their head would fill the view.
`first_person_hide_driver` removes it. The game applies one draw byte across every model of a kart
and to its body, so clearing it outright takes the vehicle along with the driver;
`first_person_hidden_model` names a single model to hide instead. On PAL `RMCP01` a kart carries two
models and index `0` is the driver, which is the default: the character goes and the vehicle stays.
`-1` restores the blunt behaviour and hides everything. An index the kart does not have hides
nothing, and the log reports how many it has when the mode engages. The F10 bar presents this as
two toggles, "Hide driver" and "Hide driver and kart", alongside a button that restores every
first-person default. Both settings touch your own kart
only, so the other racers are untouched, and the original values are restored when first person
stops or the race ends. This is the one place the first-person camera modifies the game rather than
only reading it.
One limitation is worth knowing: Mario Kart still culls the scene from its own chase camera, so a
wide head turn in first person can reveal the edge of what the game decided to draw. As with the
rest of the race instrumentation, the object offsets this reads are specific to the project's
supported PAL `RMCP01` translation.
## 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, while the game's 2D layer goes on a fixed virtual screen
(see below). 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.
With VR interpolation enabled, Aurora retains each sealed race's command stream and matched
previous/current transform uniforms. New OpenXR packets wake the frame worker between game
frames. It interpolates at the requested display time, then applies that packet's head pose and
the scene anchor to both eyes. Native offscreen effects and the 2D HUD retain their game-frame
updates. A mid-frame EFB readback invalidates retained GPU data; a policy-tag mismatch rejects
the replay. Missing matches use current transforms, and stalls clamp at the last known pose
instead of extrapolating. The ordinary desktop interpolation settings remain independent.
The D3D12 pacing thread retains the last completed projection or virtual-screen layer and
resubmits it during stalls, including while moving the desktop window,
pausing, or minimizing. The scene freezes until rendering resumes; the compositor can still
reproject the retained image for head movement. Repeated layers keep their original render poses
and field of view, paired with the new compositor display time. Two pairs of eye swapchains keep
the retained image separate from pending or canceled rendering (at the cost of additional GPU
memory). Unencoded packets can be withdrawn after 50 ms; encoded work retains its images while
the pacing thread continues submitting the last completed layer. A stall alone no longer requests
desktop fallback after 250 ms.
Before the first valid image, when OpenXR requests no rendering, or after a session/reference-space
change invalidates the retained content, frames can still have no layers. Actual runtime or GPU
submission failures retain the safe teardown path. This does not detect black images rendered by
the game itself, and cannot keep submitting if the entire process or XR runtime is suspended.
All OpenXR session and swapchain calls remain on their owning thread.
## The race's 2D layer
The minimap, race position, item roulette, lap times and the rest of the game's orthographic layer
would otherwise be stretched across each eye's entire field of view. With `hud_virtual_screen` on
they are instead placed on a rectangle fixed in the recorded camera's own frame, `hud_distance_meters`
ahead of it and `hud_width_meters` across, its height following the aspect ratio the game is
presenting at. The screen stays where the camera puts it, so looking around moves the view across it
rather than dragging it along.
An orthographic GX projection is affine, so the draw's clip position is already its position on the
flat frame. Replay folds three further steps into that same projection matrix, one per eye: the
draw viewport into full-frame coordinates, the frame position onto the screen rectangle, and the
screen through that eye's view and OpenXR frustum. The draw's own position matrices are left alone.
Depth uses the equivalent of DolphinXR's Exact Screen Depth path. A replay-only shader variant
carries the draw's original GX depth through a flat-interpolated value and explicitly writes it at
the fragment, including the draw's recorded viewport depth range. The reprojected geometry itself
is parked at mid-depth for clipping. This avoids the view-dependent perspective-divide rounding
that otherwise breaks equal-depth `LEQUAL` ordering and causes overlapping menu/HUD elements to
z-fight.
Two classes of draw are deliberately left on their recorded transforms: native framebuffer effects
(bloom and the rest of the post-processing chain, recognised by sampling a freshly produced,
reduced or blended-back EFB copy), which belong to the rendered image rather than to the game's 2D
layer, and any draw whose matrix is not actually affine. Retained one-shot EFB bakes such as Mario
Kart Wii's minimap are treated as game art and remain eligible for the screen. A reprojected 2D draw
uses the full eye viewport and scissor because its recorded rectangle no longer describes where it
ended up; its original viewport is folded into the projection instead.
## Diagnostics
**F10 > Diagnostics** holds two bug-report aids.
**OpenXR diagnostic logging** is off by default. When it is off, each hook on the pacing thread is
one atomic test. It applies immediately and is remembered as:
```toml
[diagnostics]
openxr_logging = false
```
When it is on, `console.log` receives lines tagged `[runtime] [xr-diag]`
(`runtime/src/vr/openxr_diagnostics.cpp`). They cover both the D3D12 and the Vulkan backend.
- **Session description.** Written when logging starts and again for every new OpenXR session. It
gives the runtime and system names and versions, vendor id, tracking support, backend, reference
space, blend mode, enabled extensions, recommended and maximum eye sizes, `render_scale`, swapchain
sizes, display period, and the VR frame interpolation setting.
- **View geometry.** Written on the first located views and again whenever they change by more
than 0.5° or 0.5 mm. It gives per-eye FOV half-angles, the eye cant (the angle between the two
eyes' forward axes: 0 for parallel displays, non-zero for canted ones such as Pimax without
parallel projections), and the IPD.
- **A one-second summary.** Timings are `median/worst` in milliseconds; for `end-margin`, worst is
the minimum.
| Field | Meaning |
| --- | --- |
| `Hz`, `cycles` | Display rate from the predicted display period; compositor cycles (xrWaitFrame/xrEndFrame pairs, repeats included). |
| `skipped-slots` | Display slots the predicted display time jumped over: the runtime throttled or dropped frames. |
| `late` | Frames whose xrEndFrame came after their predicted display time (needs `XR_KHR_win32_convert_performance_counter_time` or `XR_KHR_convert_timespec_time`). |
| `layers new/repeat/empty` | Cycles ending with a newly rendered layer, the retained layer again, or no layer at all (black). |
| `discarded`, `layer-rejected` | Retained layers dropped by a session or reference-space change; rendered layers not submitted (invalid pose or views, failed release). |
| `wait-frame`, `open`, `end-call` | Time blocked in xrWaitFrame, from xrBeginFrame to xrEndFrame, and inside xrEndFrame. |
| `end-margin`, `end-gap` | Predicted display time minus the xrEndFrame time; interval between xrEndFrame calls. |
| `pickup`, `render` | Stereo packet published until Aurora's frame worker takes it (without interpolation this includes waiting for the next 60 Hz game frame); taken until the eye copy is submitted. |
| `acquire`, `release` | Swapchain image acquire+wait and release. |
| `keepalive` | Retained-layer repeats while Aurora was still encoding past the 50 ms keep-alive. |
| `packet-unused`, `packet-rejected`, `submit-failed` | Packets no game frame took within 50 ms; packets Aurora took but rendered mono (content tag or transform check); failed stereo copies. |
| `interp-skip` | Cycles the VR interpolation rate cap chose not to render. |
| `frames immersive/screen` | Cycles per presentation mode; `not-rendered` counts cycles without views to render. |
| `no-orientation`, `no-position` | Cycles whose head orientation or position was not valid. |
| `suppressed` | Event lines dropped by the rate limit. |
- **Event lines.** At most 8 per second; the rest are counted in `suppressed`. They report late
frames, skipped display slots, stalls (more than 2.5 display periods, and at least 25 ms, between
xrEndFrame calls), empty frames and their reason, discarded retained layers, rejected layers,
withdrawn or rejected packets, failed submissions, and head-tracking loss and recovery.
Reference-space change events are never rate-limited.
- **Presentation changes.** While logging is on, every `[mkw-vr] presentation=` transition is
logged, not just the first 16.
**Export Logs** opens the system folder picker. It then creates a
`WiiCompiled-logs-YYYYMMDD-HHMMSS` folder at the chosen location, containing:
- `Logs/`: every retained run folder, the current session included. The runtime prunes run
folders after four days.
- `Config.toml`.
- `export-info.txt`: the export time, the exporting process id (whose run folder ends in `_pid<id>`),
and the OpenXR state.
The current `console.log` is copied through a shared-read stream while it is still being written.
The copy runs on SDL's dialog thread (`runtime/src/log_export.cpp`), and the outcome is shown under
the button. `mkw_openxr_diagnostics_tests` and `mkw_log_export_tests` cover both without a headset.
## Backend status
| Backend | Status |
| --- | --- |
| Windows D3D12 | Implemented: same-adapter, same-device asynchronous OpenXR submission. |
| 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`. |
| Linux Vulkan | Not wired. The pinned Dawn package does not expose a native Vulkan device, and the AHardwareBuffer bridge is Android-only; a dma-buf/opaque-fd variant of the same design would cover desktop Linux. |
| Other platforms | Not wired yet. |
Both bindings share `openxr_integration.cpp`: the pacing thread, policy evaluation, the
retained-layer protocol and the head-pose maths are compiled once against the neutral types in
`vr/openxr_backend.h`, and only the backend class differs per platform.
### Interpolation validation
Probe-sized EFB readbacks retain completed pixels in host memory and publish them only inside
the next compatible `GXCopyTex` call. GPU completion callbacks must not write to guest RAM:
a race restart can reuse a freed probe buffer for `RaceCamera`, and a late 4x4 Z24X8 tile then
turns its rotation fields into NaNs and triggers `triangular.h` / `PPCHalt`.
The optional Windows GPU test `efb_ram_lifetime_smoke` exercises that allocation reuse and
format/size changes. It fails with the former callback write and passes with deferred publication.
The GX tests cover retained transform endpoints with desktop interpolation off and continuous
sampling at 72/90/120 Hz. `mkw_frame_interpolation_pacing_tests` covers fixed-rate scheduling,
live changes, stalls and configuration migration; `mkw_openxr_replay_tests` exercises swapchain
ownership and retained-layer submission without a headset.
For a Windows GPU check, configure Aurora with its tests enabled and
`AURORA_GPU_SMOKE_TESTS=ON`, then build/run `stereo_frame_worker_smoke`. This feeds the actual
renderer a 60 Hz GX stream and an independent 90 Hz stereo provider. The development check
produced 359 new stereo submissions in 4 seconds (89.7 FPS). This verifies submission cadence,
not full-race performance or visual quality on a headset. Pass a draw count, for example
`stereo_frame_worker_smoke 2000`, to stress uniform preparation and renderer/producer overlap;
`stereo_frame_worker_smoke 2000 0` checks native stereo with interpolation Off.
Use `stereo_frame_worker_smoke 1000 1 1` to exercise ten-matrix palettes and their
larger uniform history, or `stereo_frame_worker_smoke 1000 2 1` to switch interpolation
On/Off during recording. The test compositor discards obsolete ticks and uses
high-resolution waits on Windows, keeping missed ticks from accumulating into bursts.
It pre-warms the next game frame like the runtime and excludes the first 60 frames
from timing so shader compilation and initial resource allocation do not skew steady-state results.
The test checks that the producer stays above 55 FPS as well as checking headset submissions;
replaying an old scene more often must not hide a slowed simulation. Validate actual races in VDXR at 90 Hz with
Auto/90 selected, including race entry/exit, first person, recentering and pauses.
Stereo uniform calculations use cached CPU memory, followed by a single write into the upload
buffer. Reading or modifying matrices directly in D3D12 upload memory can be extremely slow,
especially with many character draws; see Microsoft's [Map guidance](https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12resource-map).
Retained interpolation reserves eye ranges at seal time and fills them once at the headset sample
time. The frame worker always releases the producer after sealing, so eye encoding overlaps the
next game frame whether or not interpolation is on. It used to publish that phase only after the
encode unless interpolation was enabled, and the producer's first GX drain of every frame then
waited for the previous frame's whole encode and submit (3 to 4.5 ms per frame on a Quest 3).
When VR interpolation is enabled at batch start, uniform recording also uses cached CPU
memory. Matching and history capture read that buffer, then the used prefix is copied to
the mapped upload buffer before unmapping. The backing choice stays fixed until the batch
ends, including mid-frame flushes, so live setting changes cannot invalidate pending tasks.
## Current limitations
- Only the project's supported PAL `RMCP01` translation has race instrumentation addresses.
- The tracked controllers are always Player 1's Wii Remote; there is no left-handed swap, and only
the Touch and simple controller profiles have suggested bindings. The Wii Remote presentation
still needs headset validation: cursor direction and roll, trick/wheelie motion, rumble strength
and the HOME Menu.
- The Quest build (`android/`, `docs/quest-port.md`) runs on a Quest 3 through menus and races.
Lifecycle events and performance (about 43 game FPS) are still open. Apple visionOS packaging
is not implemented.
- Scene-specific comfort options, culling fixes and replay/spectator classification are future work.
- The headset settings panel is drawn into the eye images rather than submitted as its own quad
layer, so its text is resampled once more than a compositor layer's would be. It has no laser
beam, only the cursor on the panel itself, and text fields cannot be typed into without a keyboard.
- 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.