Implement OpenXR Settings Panel and Stereo Overlay

This commit is contained in:
iChris4 committed 2026-09-17 04:32:22 +02:00
1 parent f222eedf6b
commit 1346e53cd6
22 files changed
+1466 -101

No files matched your search

+20 -4
View File
@@ -5,6 +5,7 @@
#if defined(MKW_ENABLE_OPENXR)
#include "vr/openxr_runtime.h"
#include "vr/openxr_settings_panel.h"
#include "vr/openxr_wii_remote.h"
#include <array>
@@ -54,6 +55,11 @@ struct OpenXRPointerScreen {
// Both are bound for the Oculus Touch profile; khr/simple_controller gets
// select/menu and the poses so an unknown runtime still offers something.
//
// Clicking both thumbsticks opens the in-headset settings panel
// (openxr_settings_panel.h). While it is open, and until every button has been
// released after it closes, the game sees idle controllers: the chord, the
// pointer and the triggers belong to the panel.
//
// Lifetime: Create after the session exists (attaches the action set, which
// OpenXR permits once per session), Sync once per xrWaitFrame, Idle while the
// session is not running, Destroy before the session is destroyed. All of them
@@ -75,8 +81,10 @@ public:
// xrSyncActions + state reads, then publishes to the virtual gamepad and
// the Wii Remote bridge. predicted_display_time is the frame's XrTime;
// screen is where the pointer can land this frame.
void Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen);
// screen is where the Wii Remote pointer can land this frame, and
// settings_panel where the settings panel is (its whole rectangle).
void Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
const OpenXRPointerScreen& settings_panel);
// Publishes a remote with nothing held, at rest and not pointing, and stops
// the haptics, for frames without focused input.
@@ -100,8 +108,13 @@ private:
XrTime InputSampleTime(XrTime predicted_display_time) const;
bool AttachVirtualGamepad();
void DetachVirtualGamepad();
void PublishWiiRemote(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
const std::array<wii_remote::HandInputs, kHands>& hands, uint32_t injected_buttons);
// `withheld` publishes a remote at rest with nothing held and no pointer,
// while still tracking motion so releasing it does not read as a jolt.
void PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen& screen,
const std::array<wii_remote::HandInputs, kHands>& hands, uint32_t injected_buttons,
bool withheld);
void PublishSettingsPanel(XrTime input_time, const OpenXRPointerScreen& panel,
const settings_panel::Frame& frame);
void UpdateRumble();
void StopRumble();
bool Check(XrResult result, const char* operation);
@@ -128,6 +141,9 @@ private:
PFN_xrVoidFunction m_convert_now_to_xr_time = nullptr;
wii_remote::MotionTracker m_motion[kHands];
wii_remote::PointerFilter m_pointer;
settings_panel::Controls m_panel_controls;
XrTime m_last_input_time = 0;
bool m_panel_select_held = false;
std::array<float, 2> m_horizon{1.0f, 0.0f};
bool m_haptics_active[kHands]{};
uint32_t m_joystick_id = 0; // SDL_JoystickID; 0 when detached
+165
View File
@@ -0,0 +1,165 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "vr/openxr_wii_remote.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
namespace mkw::vr {
// The settings panel shown inside the headset: the F10 settings, drawn by the
// settings overlay with an ImGui context of its own and laid over the eyes by
// Aurora (aurora_imgui_set_stereo_overlay). It sits on the virtual screen, the
// menu quad or the race's 2D screen, and is operated with the tracked
// controllers, which are withheld from the game while it is open.
//
// The XR pacing thread owns the controllers: it opens and closes the panel from
// the controller chord, aims the pointer and publishes it here. The game thread
// draws the panel, may close it (its Close button) or open it (the F10 bar), and
// reads the pointer once per presented frame. Nothing in this header depends on
// OpenXR, so the settings overlay compiles the same in builds without it.
// The panel's canvas in ImGui pixels, drawn at twice the desktop menu's scale.
inline constexpr float kSettingsPanelWidthPixels = 1440.0f;
inline constexpr float kSettingsPanelHeightPixels = 1080.0f;
inline constexpr float kSettingsPanelUiScale = 2.0f;
// Its width as a fraction of the virtual screen's.
inline constexpr float kSettingsPanelWidthFraction = 0.75f;
struct OpenXRSettingsPanelPointer {
bool valid = false;
float x = 0.0f; // canvas pixels, +x right
float y = 0.0f; // canvas pixels, +y down
bool select = false;
float wheel = 0.0f; // ImGui wheel steps since the last read, +up
};
// Either thread.
void OpenXRSetSettingsPanelOpen(bool open) noexcept;
bool OpenXRSettingsPanelOpen() noexcept;
// XR thread: this frame's pointer, and wheel steps to add to what is pending.
void OpenXRPublishSettingsPanelPointer(bool valid, float x, float y, bool select, float wheel) noexcept;
// Game thread: the latest pointer, taking the wheel steps accumulated since the last call.
OpenXRSettingsPanelPointer OpenXRTakeSettingsPanelPointer() noexcept;
namespace settings_panel {
using wii_remote::HandInputs;
// Thumbstick deflection below this does not scroll.
inline constexpr float kScrollDeadzone = 0.25f;
// Wheel steps per second at full deflection. ImGui scrolls about five lines a step.
inline constexpr float kScrollStepsPerSecond = 8.0f;
// The panel's half extents, in metres, on a virtual screen `screen_width` metres across.
inline std::array<float, 2> HalfExtents(float screen_width) noexcept {
const float half_width = 0.5f * screen_width * kSettingsPanelWidthFraction;
return {half_width, half_width * kSettingsPanelHeightPixels / kSettingsPanelWidthPixels};
}
// A hit on the panel (-1..1 across it, +v up) in canvas pixels.
inline std::array<float, 2> CanvasPoint(const wii_remote::ScreenHit& hit) noexcept {
return {0.5f * (hit.u + 1.0f) * kSettingsPanelWidthPixels, 0.5f * (1.0f - hit.v) * kSettingsPanelHeightPixels};
}
// What the controllers do this frame.
struct Frame {
bool open = false; // the panel is showing
bool withheld = false; // the game must not see the controllers
uint32_t pointing_hand = 1;
bool select = false;
float wheel = 0.0f;
};
// The controller side of the panel, one Update per XR frame:
//
// - Clicking both thumbsticks together opens or closes it. Nothing else opens
// it from the controllers: every other button already means something to the
// game.
// - While it is open, the left menu button also closes it, both triggers and
// the A / X buttons select, the thumbsticks scroll, and the hand whose trigger
// was pulled last is the one pointing.
// - The game gets the controllers back only once everything is released, so the
// press that closed the panel never lands in the game as well.
class Controls {
public:
// `open` is the shared flag: the chord flips it, and the game thread may
// have changed it since the last frame. `dt_seconds` is the time since the
// previous frame.
Frame Update(const std::array<HandInputs, 2>& hands, bool& open, float dt_seconds) noexcept {
const bool chord = hands[0].thumbstick_click && hands[1].thumbstick_click;
if (chord && !m_chord_held) {
open = !open;
}
m_chord_held = chord;
if (open && hands[0].menu && !m_menu_held && m_was_open) {
open = false;
}
m_menu_held = hands[0].menu;
for (uint32_t hand = 0; hand < 2; ++hand) {
const bool pulled = hands[hand].trigger > wii_remote::kPressThreshold;
if (pulled && !m_trigger_held[hand]) {
m_pointing_hand = hand;
}
m_trigger_held[hand] = pulled;
}
if (open != m_was_open) {
// Whatever is held across the change belongs to that change.
m_release_pending = true;
}
if (m_release_pending && !AnyHeld(hands)) {
m_release_pending = false;
}
m_was_open = open;
Frame frame{};
frame.open = open;
frame.withheld = open || m_release_pending;
frame.pointing_hand = m_pointing_hand;
if (!open) {
return frame;
}
// Opening held nothing that selects, but a trigger still down from the
// game must not click the first thing under the pointer.
frame.select = !m_release_pending && (m_trigger_held[0] || m_trigger_held[1] || hands[0].primary ||
hands[1].primary);
const float stick = std::fabs(hands[1].stick_y) >= std::fabs(hands[0].stick_y) ? hands[1].stick_y
: hands[0].stick_y;
if (std::fabs(stick) > kScrollDeadzone) {
const float magnitude = (std::fabs(stick) - kScrollDeadzone) / (1.0f - kScrollDeadzone);
frame.wheel = std::copysign(magnitude, stick) * kScrollStepsPerSecond * std::clamp(dt_seconds, 0.0f, 0.1f);
}
return frame;
}
void Reset() noexcept { *this = Controls{}; }
static bool AnyHeld(const std::array<HandInputs, 2>& hands) noexcept {
for (const HandInputs& hand : hands) {
if (hand.primary || hand.secondary || hand.menu || hand.thumbstick_click ||
hand.trigger > wii_remote::kPressThreshold || hand.squeeze > wii_remote::kPressThreshold) {
return true;
}
}
return false;
}
private:
bool m_chord_held = false;
bool m_menu_held = false;
std::array<bool, 2> m_trigger_held{};
bool m_was_open = false;
bool m_release_pending = false;
uint32_t m_pointing_hand = 1;
};
} // namespace settings_panel
} // namespace mkw::vr