Add Flat Screen mode for VR races and update settings accordingly

This commit is contained in:
iChris4 committed 2026-09-23 17:15:53 +02:00
1 parent 2a89fac79d
commit 2a4686191e
16 files changed
+161 -50

No files matched your search

+21 -2
View File
@@ -57,6 +57,7 @@ struct RuntimeUserConfig {
std::optional<float> vrHudDistanceMeters;
std::optional<float> vrHudWidthMeters;
std::optional<bool> vrHudVirtualScreen;
std::optional<bool> vrFlatScreen;
std::optional<bool> vrPassthrough;
std::optional<bool> vrStopAtDisplayCopy;
std::optional<bool> vrSkipCopyClears;
@@ -496,6 +497,11 @@ inline void EnsureConfigFile() {
"# Changeable live from the F10 menu; the two sizes above place\n"
"# that screen and the menu screen alike and are read at launch.\n"
"hud_virtual_screen = true\n"
"# Flat Screen mode keeps races on that same screen, as the\n"
"# menus are, instead of all around you: no stereo race view, no\n"
"# first-person camera or hand steering. Changeable live from the\n"
"# F10 menu.\n"
"flat_screen = false\n"
"# EFB replay controls for the per-eye views, changeable live\n"
"# from the F10 menu. stop_at_display_copy ends each eye at the\n"
"# frame's final GXCopyDisp; skip_copy_clears drops the EFB\n"
@@ -747,6 +753,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.vrHudWidthMeters = *value;
}
config.vrHudVirtualScreen = FindConfigValue<bool>(document, "vr", "hud_virtual_screen");
config.vrFlatScreen = FindConfigValue<bool>(document, "vr", "flat_screen");
config.vrPassthrough = FindConfigValue<bool>(document, "vr", "passthrough");
config.vrStopAtDisplayCopy = FindConfigValue<bool>(document, "vr", "stop_at_display_copy");
config.vrSkipCopyClears = FindConfigValue<bool>(document, "vr", "skip_copy_clears");
@@ -1061,6 +1068,11 @@ inline bool SetVrHudVirtualScreen(bool value) {
return WriteSetting("vr", "hud_virtual_screen", value ? "true" : "false");
}
inline bool SetVrFlatScreen(bool value) {
Mutable().vrFlatScreen = value;
return WriteSetting("vr", "flat_screen", value ? "true" : "false");
}
inline bool SetVrPassthrough(bool value) {
Mutable().vrPassthrough = value;
return WriteSetting("vr", "passthrough", value ? "true" : "false");
@@ -1518,9 +1530,16 @@ inline bool VrHudVirtualScreen(bool fallback = true) {
return Get().vrHudVirtualScreen.value_or(fallback);
}
// Races on the flat virtual screen the menus use, instead of immersive
// stereo. The launcher's Settings page shows the same default.
inline bool VrFlatScreen(bool fallback = false) {
return Get().vrFlatScreen.value_or(fallback);
}
// The room, through the headset's cameras, around the menu screen and every
// other virtual screen (never in a race). Only the Quest offers it; the
// launcher's Settings page shows the same default.
// other virtual screen, a Flat Screen race included (never an immersive
// race). Only the Quest offers it; the launcher's Settings page shows the same
// default.
inline bool VrPassthrough(bool fallback = true) {
return Get().vrPassthrough.value_or(fallback);
}
+4
View File
@@ -71,6 +71,7 @@ inline constexpr uint32_t kMkwVRRequiredImmersiveBindings =
struct MkwVRPolicyConfig {
bool enabled = false;
// False is Flat Screen mode: races stay on the virtual screen the menus use.
bool immersive_races = true;
float world_units_per_meter = 500.0f;
float hud_distance_meters = 2.0f;
@@ -151,6 +152,9 @@ void MkwVRPolicySetFirstPersonEngaged(bool engaged) noexcept;
// MkwVRPolicyConfigure so the F10 slider can retune it during a race without
// republishing (and revalidating) the whole configuration.
void MkwVRPolicySetFirstPersonUnitsPerMeter(float units_per_meter) noexcept;
// Flat Screen mode, live from the F10 menu. Unlike the scale above this changes
// which content is safe to present, so it advances the safety generation.
void MkwVRPolicySetImmersiveRaces(bool enabled) noexcept;
MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept;
// This classifier is deliberately structural rather than heuristic: future
+5 -4
View File
@@ -79,10 +79,11 @@ inline float MaxWheelAngle(bool bike, const WheelTuning& tuning) noexcept {
return clamped * 0.01745329252f;
}
// Grips only grab. While a hand holds the wheel its squeeze is released for the
// game, where it would press C on the Nunchuk or a shoulder on the gamepad, and
// the wheel replaces the left stick's X axis, which both controller modes steer
// with. The stick's Y axis keeps aiming items forwards and backwards.
// Grips only grab. As a Wii Remote they press nothing at all (C, the game's
// look-behind, is right B); as a gamepad they are the shoulders, so a holding
// hand's squeeze is released for the game. The wheel replaces the left stick's
// X axis, which both controller modes steer with, and the stick's Y axis keeps
// aiming items forwards and backwards.
inline void ApplyHandSteering(std::array<wii_remote::HandInputs, 2>& hands, const WheelState& wheel) noexcept {
for (size_t hand = 0; hand < hands.size(); ++hand) {
if (wheel.held[hand]) {
+2 -1
View File
@@ -63,7 +63,8 @@ struct OpenXRPointerScreen {
// the seated frame every frame. With hand steering on, a squeezed grip near the
// steering wheel or handlebar takes hold of it; while held, the wheel replaces
// the left stick's X axis in both presentations and that grip no longer reaches
// the game (C on the Nunchuk, a shoulder on the gamepad).
// the game (a shoulder on the gamepad; as a Wii Remote the grips are unbound,
// so a hand on the wheel cannot hold down a button).
//
// A right-thumbstick click on its own toggles the first-person camera, as its
// F10 checkbox does (first_person_toggle_click).
+5 -3
View File
@@ -342,10 +342,12 @@ struct HandInputs {
// Adapted from DolphinXR's default "OpenXR Wii Remote" profile
// (Data/Sys/Profiles/Wiimote):
// right A -> A, right trigger -> B, right stick up/down -> 1/2,
// right A -> A, right trigger -> B, right B -> C, right stick up/down -> 1/2,
// left X -> -, left menu -> +,
// left grip -> C, left trigger -> Z, left stick -> Nunchuk stick.
// left trigger -> Z, left stick -> Nunchuk stick.
// HOME has no button; left Y opens the settings panel (openxr_settings_panel.h).
// The grips press nothing: they take hold of the wheel (openxr_driving.h), and C
// is the game's look-behind, which a hand on the wheel would otherwise hold down.
inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) noexcept {
uint32_t hold = 0;
const auto press = [&hold](bool held, uint32_t bit) {
@@ -355,11 +357,11 @@ inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) n
};
press(right.primary, kButtonA);
press(right.trigger > kPressThreshold, kButtonB);
press(right.secondary, kButtonC);
press(right.stick_y > kPressThreshold, kButtonOne);
press(right.stick_y < -kPressThreshold, kButtonTwo);
press(left.primary, kButtonMinus);
press(left.menu, kButtonPlus);
press(left.squeeze > kPressThreshold, kButtonC);
press(left.trigger > kPressThreshold, kButtonZ);
return hold;
}
+30 -6
View File
@@ -141,6 +141,7 @@ bool g_vrEnabled = RuntimeConfigFile::VrEnabled(true);
bool g_vrStopAtDisplayCopy = RuntimeConfigFile::VrStopAtDisplayCopy(true);
bool g_vrSkipCopyClears = RuntimeConfigFile::VrSkipCopyClears(true);
bool g_vrHudVirtualScreen = RuntimeConfigFile::VrHudVirtualScreen(true);
bool g_vrFlatScreen = RuntimeConfigFile::VrFlatScreen();
#if defined(__ANDROID__)
bool g_vrPassthrough = RuntimeConfigFile::VrPassthrough();
#endif
@@ -1354,8 +1355,9 @@ void DrawVrSettings() {
ImGui::SetTooltip(
"Wii Remote + Nunchuk: the right controller is a Wii Remote, with motion and a pointer "
"that lands where you aim on the virtual screen; the left one is the Nunchuk.\n"
" Right: A = A, trigger = B, stick up/down = 1/2\n"
" Left: stick = Nunchuk stick, trigger = Z, grip = C, X = -, menu = +, Y = settings panel\n"
" Right: A = A, trigger = B, B = C (look behind), stick up/down = 1/2\n"
" Left: stick = Nunchuk stick, trigger = Z, X = -, menu = +, Y = settings panel\n"
" The grips press nothing; they take hold of the wheel with hand steering.\n"
"Gamepad: both controllers are one ordinary controller, read as a GameCube pad.\n"
"Applies immediately; the game sees the controller change as a reconnection.");
}
@@ -1424,11 +1426,13 @@ void DrawVrSettings() {
ImGui::PopTextWrapPos();
ImGui::Separator();
ImGui::Text("VR 2D layer");
ImGui::BeginDisabled(g_vrFlatScreen);
if (ImGui::Checkbox("2D layer on a virtual screen", &g_vrHudVirtualScreen)) {
ApplyVrHudVirtualScreen();
RuntimeConfigFile::SetVrHudVirtualScreen(g_vrHudVirtualScreen);
}
if (ImGui::IsItemHovered()) {
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip(
"Puts the minimap, race position, item roulette and the rest of the race HUD on a "
"screen fixed ahead of the kart camera. Turn off to leave them stretched across "
@@ -1467,13 +1471,15 @@ void DrawVrSettings() {
ImGui::SetTooltip("Click to rebind. Esc cancels, Backspace unbinds.");
}
}
ImGui::BeginDisabled(g_vrFlatScreen);
if (ImGui::SliderFloat("Lean back angle (deg)", &g_vrLeanBackDegrees,
-RuntimeConfigFile::kVrLeanBackDegreesLimit,
RuntimeConfigFile::kVrLeanBackDegreesLimit, "%.1f")) {
mkw::vr::OpenXRSetLeanBackDegrees(g_vrLeanBackDegrees);
RuntimeConfigFile::SetVrLeanBackDegrees(g_vrLeanBackDegrees);
}
if (ImGui::IsItemHovered()) {
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip(
"Tilts the game camera back with you when you play reclined, so set it to "
"roughly how far back your seat is and the track comes back in front of you "
@@ -1489,12 +1495,26 @@ void DrawVrSettings() {
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Shows your room through the headset's cameras around the menu screen and every "
"other screen outside a race, instead of black. Races stay fully virtual. "
"other screen outside an immersive race, instead of black. Immersive races stay "
"fully virtual; the Flat Screen race has the room around it too. "
"Applies immediately.");
}
#endif
ImGui::Separator();
ImGui::Text("VR camera");
if (ImGui::Checkbox("Flat Screen mode", &g_vrFlatScreen)) {
RuntimeConfigFile::SetVrFlatScreen(g_vrFlatScreen);
mkw::vr::MkwVRPolicySetImmersiveRaces(!g_vrFlatScreen);
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Plays races on the same flat screen as the menus, through the game's own camera, "
"instead of all around you in stereo. The first-person camera, hand steering and "
"the race view settings do not apply while it is on. Applies immediately.");
}
// Everything below shapes the immersive race view, which Flat Screen mode replaces.
ImGui::BeginDisabled(g_vrFlatScreen);
if (ImGui::Checkbox("First-person camera", &g_vrFirstPerson)) {
RuntimeConfigFile::SetVrFirstPerson(g_vrFirstPerson);
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
@@ -1643,11 +1663,15 @@ void DrawVrSettings() {
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
ApplyVrHudVirtualScreen();
}
ImGui::EndDisabled();
}
// The right-thumbstick click: flips the first-person camera exactly as its
// checkbox does. Game thread.
// checkbox does, so it does nothing in Flat Screen mode either. Game thread.
void ToggleFirstPersonCamera() {
if (g_vrFlatScreen) {
return;
}
g_vrFirstPerson = !g_vrFirstPerson;
RuntimeConfigFile::SetVrFirstPerson(g_vrFirstPerson);
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
+5 -2
View File
@@ -1213,8 +1213,11 @@ void MkwVRFirstPersonApplyConfiguredSettings() noexcept {
const FirstPersonRotation rotation = mode == "full" ? FirstPersonRotation::Full
: mode == "yaw_pitch" ? FirstPersonRotation::YawPitch
: FirstPersonRotation::YawOnly;
MkwVRFirstPersonConfigure(RuntimeConfigFile::VrFirstPerson(false), offsets, units_per_meter,
rotation);
// Flat Screen mode shows the race on the menu screen through the game's own
// camera, where a hidden driver would only be missing from the kart.
MkwVRFirstPersonConfigure(RuntimeConfigFile::VrFirstPerson(false) &&
!RuntimeConfigFile::VrFlatScreen(),
offsets, units_per_meter, rotation);
MkwVRPolicySetFirstPersonUnitsPerMeter(units_per_meter);
const bool cockpit = RuntimeConfigFile::VrFirstPersonSeat() != "custom";
const float cockpit_units = RuntimeConfigFile::VrCockpitUnitsPerMeter();
+5
View File
@@ -257,6 +257,11 @@ void MkwVRPolicySetFirstPersonUnitsPerMeter(float units_per_meter) noexcept {
}
}
void MkwVRPolicySetImmersiveRaces(bool enabled) noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
ApplyPolicyMutation([&] { g_policy.config.immersive_races = enabled; });
}
MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
MkwVRPolicySnapshot snapshot;
+12 -9
View File
@@ -67,7 +67,7 @@ void ConfigurePolicy(bool enabled) noexcept {
MkwVRPolicyReset();
MkwVRPolicyConfig config{};
config.enabled = enabled;
config.immersive_races = true;
config.immersive_races = !RuntimeConfigFile::VrFlatScreen();
config.world_units_per_meter = RuntimeConfigFile::VrWorldUnitsPerMeter(500.0f);
config.hud_distance_meters = RuntimeConfigFile::VrHudDistanceMeters(2.0f);
config.hud_width_meters = RuntimeConfigFile::VrHudWidthMeters(2.4f);
@@ -753,7 +753,7 @@ private:
bool fatal = false;
uint32_t consecutive_skips = 0;
bool store_gate_set = false;
bool store_gate_immersive = false;
bool store_gate_racing = false;
bool presentation_logged = false;
VRPresentationMode logged_presentation = VRPresentationMode::Desktop;
uint32_t presentation_log_count = 0;
@@ -840,8 +840,9 @@ private:
aurora_get_stereo_screen_aspects(&picture_aspect, &snapshot_aspect)) {
presentation.quad_content_aspect = snapshot_aspect;
}
// The room around the menu screen and every other virtual screen; a race is
// fully virtual, and the cameras are paused for it.
// The room around the menu screen and every other virtual screen, a
// Flat Screen race included; an immersive race is fully virtual, and the
// cameras are paused for it.
presentation.passthrough = !immersive && passthrough_.load(std::memory_order_relaxed);
// The settings panel gets a compositor layer of its own while it is
// open, and Aurora leaves it out of the eyes. A backend that could
@@ -852,12 +853,14 @@ private:
// Pipeline caches are stored where their stall is invisible: once when a race ends,
// and by the compiler itself while the headset shows the virtual screen. Never
// mid-race.
if (!store_gate_set || immersive != store_gate_immersive) {
const bool left_race = store_gate_set && store_gate_immersive && !immersive;
// mid-race, and a race on the virtual screen (Flat Screen mode) is still a race.
const bool racing = immersive || (!policy.config.immersive_races &&
policy.scene.mode == VRSceneMode::Race);
if (!store_gate_set || racing != store_gate_racing) {
const bool left_race = store_gate_set && store_gate_racing && !racing;
store_gate_set = true;
store_gate_immersive = immersive;
aurora_set_pipeline_cache_idle_store(!immersive);
store_gate_racing = racing;
aurora_set_pipeline_cache_idle_store(!racing);
if (left_race) {
aurora_store_pipeline_caches();
}
+1 -1
View File
@@ -51,7 +51,7 @@ void TestHandOff() {
CheckNear(hands[0].stick_x, 0.6f, "a held wheel steers through the left stick");
CheckNear(hands[0].stick_y, 0.8f, "the stick keeps aiming items");
CheckNear(hands[0].squeeze, 1.0f, "a free hand's grip still reaches the game");
CheckNear(hands[1].squeeze, 0.0f, "a holding grip does not press C or a shoulder");
CheckNear(hands[1].squeeze, 0.0f, "a holding grip does not press a shoulder");
wheel.held = {true, true};
wheel.steering = 3.0f;
+16
View File
@@ -73,6 +73,22 @@ int main() {
check(MkwVRPolicyGetSnapshot().presentation == VRPresentationMode::VirtualScreen, "race entry needs fresh camera");
MkwVRPolicyPublishRaceCamera(camera);
check(MkwVRPolicyGetSnapshot().presentation == VRPresentationMode::ImmersiveRace, "race resumes with fresh camera");
{
const uint64_t immersive_tag = MkwVRPolicyGetSnapshot().content_tag;
MkwVRPolicySetFirstPersonEngaged(true);
MkwVRPolicySetImmersiveRaces(false);
const auto flat = MkwVRPolicyGetSnapshot();
check(flat.presentation == VRPresentationMode::VirtualScreen, "Flat Screen mode keeps races on the screen");
check(flat.content_tag != immersive_tag, "Flat Screen mode must invalidate immersive packets");
check(!flat.first_person_engaged, "Flat Screen mode never engages first person");
check(MkwVRPolicyRouteDraw(VRDrawClass::PerspectiveWorld, flat) == VRDrawRoute::VirtualScreen,
"Flat Screen mode routes the race world to the screen");
MkwVRPolicySetFirstPersonEngaged(false);
MkwVRPolicySetImmersiveRaces(true);
const auto resumed = MkwVRPolicyGetSnapshot();
check(resumed.presentation == VRPresentationMode::ImmersiveRace, "leaving Flat Screen mode resumes the race");
check(resumed.content_tag != flat.content_tag, "leaving Flat Screen mode must invalidate screen packets");
}
MkwVRPolicySetSessionActive(false);
check(MkwVRPolicyGetSnapshot().presentation == VRPresentationMode::Desktop, "inactive session uses desktop");
return failures == 0 ? 0 : 1;
+9 -6
View File
@@ -240,7 +240,9 @@ void TestButtons() {
right.primary = true;
right.trigger = 0.6f;
Check(RemoteButtons(left, right) == (kButtonA | kButtonB), "right A and trigger are A and B");
right.secondary = true;
Check(RemoteButtons(left, right) == (kButtonA | kButtonB | kButtonC),
"right A, trigger and B are A, B and C");
right = {};
right.stick_y = 0.9f;
@@ -260,16 +262,17 @@ void TestButtons() {
left.menu = true;
Check(RemoteButtons(left, right) == (kButtonMinus | kButtonPlus), "left X and menu are - and +");
left = {};
left.squeeze = 0.8f;
left.trigger = 0.7f;
Check(RemoteButtons(left, right) == (kButtonC | kButtonZ), "left grip and trigger are C and Z");
// Left Y is the settings panel's button, and nothing presses HOME.
Check(RemoteButtons(left, right) == kButtonZ, "left trigger is Z");
// Left Y is the settings panel's button, the grips take hold of the steering
// wheel, and nothing presses HOME.
left = {};
left.secondary = true;
right.secondary = true;
left.squeeze = 0.8f;
right.squeeze = 0.8f;
right.thumbstick_click = true;
left.thumbstick_click = true;
Check(RemoteButtons(left, right) == 0, "left Y, right B and the stick clicks are unbound");
Check(RemoteButtons(left, right) == 0, "left Y, the grips and the stick clicks are unbound");
left = {};
right = {};
left.menu = true;