mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 05:00:27 +02:00
458 lines
30 KiB
Markdown
458 lines
30 KiB
Markdown
# 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 remain opt-in. `Config.toml` is created with the following defaults:
|
||
|
||
```toml
|
||
[vr]
|
||
enabled = false
|
||
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"
|
||
```
|
||
|
||
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. 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.
|
||
|
||
## 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, including 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 |
|
||
| Right stick left / right | − / + |
|
||
| Left stick | Nunchuk stick |
|
||
| Left trigger | Z |
|
||
| Left grip | C |
|
||
| Left menu | HOME |
|
||
| 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, left X/Y and the stick clicks are unbound,
|
||
as in DolphinXR's profile. 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`.
|
||
|
||
`"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.
|
||
|
||
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.
|
||
|
||
## 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. VR interpolation also releases the producer after sealing so eye encoding can overlap the
|
||
next game frame, as it does with desktop interpolation.
|
||
|
||
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, 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.
|