diff --git a/OPENXR.md b/OPENXR.md
index 650e796..2bd8601 100644
--- a/OPENXR.md
+++ b/OPENXR.md
@@ -417,7 +417,8 @@ copied array, those that bound one outside the window it was set for, the matche
closest position matrix was from the expected one; the first such line with a bound draw also prints
both matrices.
-**Hand steering.** `hand_steering = true` (off by default; also in WheelWizard's OpenXR VR settings)
+**Hand steering.** `hand_steering = true` (off by default; also in WheelWizard's OpenXR VR settings
+and the Quest launcher's Settings > VR, beside the seat)
lets you take hold of the wheel or handlebar with the tracked controllers. Squeeze a grip near it:
past 55 % squeeze, within `wheel_grab_distance` metres of its plane (default 0.35) and near the rim,
or near a bar end, scaled by `wheel_grab_assist`. Once taken, only letting go of the grip releases
@@ -441,7 +442,8 @@ aside while it drives.
**Hands and the separate wheel.** Hands are drawn while hand steering is on: the runtime's own hand
mesh where it offers one (`XR_EXT_hand_tracking` and `XR_FB_hand_tracking_mesh`, requested only when
-hand steering is on at launch), otherwise procedural gloves that curl with the squeeze. They and the
+hand steering is on at launch), otherwise procedural gloves that curl with the squeeze. The Quest
+build declares no hand-tracking permission, so it always draws the gloves. They and the
separate VR wheel or handlebar travel with the stereo packet in metres in the seated frame, and each
eye draws them inside the scene's pass just before the first 2D-layer draw, depth-tested with the
world's own depth mapping, so the kart and the track hide them and the HUD cannot
@@ -764,9 +766,11 @@ ends, including mid-frame flushes, so live setting changes cannot invalidate pen
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 cockpit seat, the turning wheel and hand steering have not yet been validated in a headset on
- this build: the native wheel's match against the race camera's view, bikes and Quacker, and the
- Quest. Hand steering needs analog grips (Touch); the simple controller profile cannot grab.
+- The cockpit has only been seen in one kart race on a Quest 3 (2026-09-22): the race camera's view
+ matched the scene's exactly and the kart's own wheel animated (228 draws a frame), but the driver's
+ eye was never calibrated, and the fallback placed the wheel centre about 13 cm above eye level.
+ Bikes and Quacker, the PC, and hand steering itself are unvalidated. Hand steering needs analog
+ grips (Touch); the simple controller profile cannot grab.
- The headset settings panel 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.
diff --git a/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt b/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
index 17ce290..9b5859a 100644
--- a/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
+++ b/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
@@ -115,6 +115,8 @@ class SettingsPage(
private fun buildVr() {
val firstPerson = { c: TomlConfig -> c.bool("vr", "first_person") ?: false }
+ // The steering wheel and hand steering belong to the cockpit seat.
+ val cockpit = { c: TomlConfig -> firstPerson(c) && stringIndex(c, "vr", "first_person_seat", SEATS) == 0 }
section(R.string.section_vr_camera) {
choice(
R.string.vr_camera, R.string.vr_camera_helper,
@@ -151,6 +153,20 @@ class SettingsPage(
},
enabledIf = firstPerson,
)
+ choice(
+ R.string.vr_seat, R.string.vr_seat_helper,
+ listOf(R.string.vr_seat_cockpit, R.string.vr_seat_custom),
+ read = { stringIndex(it, "vr", "first_person_seat", SEATS) },
+ write = { c, index -> c.setString("vr", "first_person_seat", SEATS[index]) },
+ enabledIf = firstPerson,
+ )
+ // heurazy's grab-and-turn wheel: runtime_config.h's kVrHandSteeringDefault is off.
+ toggle(
+ R.string.vr_hand_steering, R.string.vr_hand_steering_helper,
+ read = { it.bool("vr", "hand_steering") ?: false },
+ write = { c, value -> c.setBool("vr", "hand_steering", value) },
+ enabledIf = cockpit,
+ )
slider(
R.string.vr_lean_back, R.string.vr_lean_back_helper, -45.0, 45.0, 1.0,
read = { number(it, "vr", "lean_back_degrees", -45.0, 45.0, 0.0) },
@@ -366,6 +382,7 @@ class SettingsPage(
}
section(R.string.section_about_credits) {
info(R.string.about_credit_title_vr, activity.getString(R.string.about_credit_vr), stacked = true)
+ info(R.string.about_credit_title_hand_steering, activity.getString(R.string.about_credit_hand_steering), stacked = true)
info(R.string.about_credit_title_wiicompiled, activity.getString(R.string.about_credit_wiicompiled), stacked = true)
info(R.string.about_credit_title_retro_rewind, activity.getString(R.string.about_credit_retro_rewind), stacked = true)
info(R.string.about_credit_title_wheel_wizard, activity.getString(R.string.about_credit_wheel_wizard), stacked = true)
@@ -686,6 +703,8 @@ class SettingsPage(
private companion object {
val ROTATIONS = listOf("yaw", "yaw_pitch", "full")
+ // The runtime's default ("cockpit") first.
+ val SEATS = listOf("cockpit", "custom")
// The runtime's default ("boost") first: an absent key reads as index 0.
val PERFORMANCE_LEVELS = listOf("boost", "sustained_high", "sustained_low", "power_savings", "default")
val CONTROLLER_MODES = listOf("wii_remote", "gamepad")
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 5b85355..ddecc3b 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -141,6 +141,7 @@
WiiCompiled OpenXR VR
Mario Kart Wii recompiled to native code, with an OpenXR renderer, running on this headset. No game code and no game data come with it: each game is built from the disc you own.
Meta Quest port and the OpenXR VR renderer by iChris4.
+ The cockpit\'s turning steering wheel and hand steering by heurazy, from mario-kart-wii-VR-port (GPL v3).
WiiCompiled, the static recompilation this is built on, by patchzyy and the static recompilation community.
Retro Rewind by ZPL and team, the mod distribution this app can download and play.
Wheel Wizard by Patchzy and WantToBeeMe, the PC launcher this panel is modelled on.
@@ -153,6 +154,7 @@
GNU General Public License v3. Not affiliated with, endorsed by, or associated with Nintendo. Mario Kart Wii is a trademark of Nintendo.
Game kit
Meta Quest and VR
+ Steering wheel and hand steering
WiiCompiled
Retro Rewind
Wheel Wizard
@@ -184,6 +186,12 @@
Nothing
Driver
Driver and kart
+ Seat
+ The cockpit puts you at the driver\'s eyes, life-size, with the steering wheel or handlebar turning within reach. Custom uses the head offsets from the in-headset settings.
+ Cockpit
+ Custom
+ Hand steering
+ In the cockpit, squeeze a grip near the steering wheel or handlebar to grab it, and turn it to steer. Releasing both grips gives steering back to the stick. Hand steering by heurazy.
Lean back angle
Tilts the race view back for playing reclined. 0 applies no tilt.
diff --git a/docs/quest-port.md b/docs/quest-port.md
index 990ceb2..eb4613d 100644
--- a/docs/quest-port.md
+++ b/docs/quest-port.md
@@ -183,7 +183,8 @@ a 2D Horizon OS panel modelled on the PC launcher, WheelWizard VR, and using its
palette. **Home** has the Play button and reports a missing or incomplete `DATA`
(the check is the runtime's own `IsDvdDataRoot`: `files/` and `sys/fst.bin`).
**Settings** edits `Config.toml` in tabs: VR (camera, rotation, driver hiding,
-lean back, render scale, VR interpolation, virtual screen size and distance),
+seat, hand steering, lean back, render scale, VR interpolation, virtual screen
+size and distance),
Graphics (resolution, widescreen, bloom, shader stutter), Controls (controller
mode, vibration, the Wii Remote mapping), Audio, and About (paths, OpenXR
logging). The launch-time geometry (`render_scale`, `hud_distance_meters`,