Files
mitch030504--Wiicompiled_VR…/runtime/include/vr/openxr_input.h
T
iChris4andClaude Opus 5.5 5a38377f61 Drive with bare hands in the cockpit
- With tracked hands on, a hand driving khr/simple_controller with camera-tracked joints is bare:
  latched through wheel_tracking_grace (Meta drops the select action while a hand is lost),
  cleared as soon as a controller's squeeze is back. Its palm joint stands in for the grip and a
  grasp from the middle, ring and little fingers' flexion (0 below 1.2 rad, 1 from 3.0) for the
  squeeze, so closing a hand on the rim takes hold under the wheel's own press and release. The
  grasp never reaches the game's buttons.
- In a cockpit race a bare hand holding the wheel holds the gas (A / South) and a pinch from a free
  bare hand uses an item (Z / L) once the hand has been off the wheel for 0.15 s. Only while the
  game has the remote's pointer off: KPAD publishes the game's pointer switch for the VR remote,
  so the pause menu and the results keep a right pinch as A. The pacing thread logs each change,
  to confirm on the headset that MKW turns the pointer off while driving.
- A held bare hand keeps its last joints drawn through a short loss; bare hands get no haptics.
  The headset panel's readout shows grasp, hold and pinch. Docs: OPENXR.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 00:08:25 +02:00

260 lines
12 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#if defined(MKW_ENABLE_OPENXR)
#include "vr/camera_toggle.h"
#include "vr/openxr_driving.h"
#include "vr/openxr_hand_tracking.h"
#include "vr/openxr_runtime.h"
#include "vr/openxr_settings_panel.h"
#include "vr/openxr_wii_remote.h"
#include "vr/steering_wheel.h"
#include <array>
#include <cstdint>
#include <string>
namespace mkw::vr {
// The rectangle the game's picture occupies on whichever virtual screen is
// showing it, in the application reference space: the target the Wii Remote
// pointer is aimed at. The pose faces +Z with +X right and +Y up across the
// picture. Invalid when no screen can be pointed at (for instance a race with
// its 2D layer left stretched across the eyes).
struct OpenXRPointerScreen {
bool valid = false;
XrPosef pose{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
float half_width_meters = 0.0f;
float half_height_meters = 0.0f;
};
// OpenXR action-based controller input.
//
// Quest Touch controllers are not visible to SDL's joystick layer (the OS does
// not expose them as HID gamepads), so a standalone headset build would have no
// input at all. This module syncs an OpenXR action set on the pacing thread and
// feeds a virtual SDL joystick (SDL_AttachVirtualJoystick) that Aurora's
// existing controller code opens and assigns to a port exactly like a physical
// pad. What the game then sees on that port depends on OpenXRControllerMode:
//
// Wii Remote (default): the port is served through KPAD as a Wii Remote with a
// Nunchuk, like DolphinXR's OpenXR Wii Remote. Every XR frame the aim and grip
// poses are located at the measured current time, turned into both
// accelerometers and the IR pointer (the right aim ray against the virtual
// screen the renderer is showing), and published through openxr_wii_remote.h.
// Buttons are adapted from DolphinXR's "OpenXR Wii Remote" profile (see
// RemoteButtons).
// The game's rumble drives both controllers' haptics.
//
// Gamepad: the virtual joystick is read through PAD as a GameCube controller,
// and every binding the settings overlay offers applies:
// right A / B -> gamepad South / East (GameCube A / B)
// left X / Y -> gamepad West / North (GameCube X / Y)
// index triggers -> left / right trigger axes
// grip squeezes -> left / right shoulder buttons
// left / right thumbstick-> left / right stick axes, clicks -> stick buttons
// left menu -> Start
//
// Both are bound for the Oculus Touch profile; khr/simple_controller gets
// select/menu and the poses so an unknown runtime still offers something.
//
// In the first-person cockpit (openxr_driving.h) the grip poses are located in
// 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 (a shoulder on the gamepad; as a Wii Remote the grips are unbound,
// so a hand on the wheel cannot hold down a button).
//
// Tracked hands ([vr] hand_tracking, with hand steering): two hand trackers
// (XR_EXT_hand_tracking) located every frame give the cockpit the hands' own
// joints, from the controllers' touch sensors while they are held and from the
// cameras once they are put down. On the Quest a hand that drives
// khr/simple_controller instead of the Touch profile has bare-hand buttons
// (openxr_hand_tracking.h): with the option off it presses nothing but the menu
// gesture, so the manifest's hand-tracking permission changes nothing for
// players who leave it off.
//
// A right-thumbstick click on its own toggles the first-person camera, as its
// F10 checkbox does (first_person_toggle_click).
//
// Left Y (both thumbsticks clicked together as a gamepad) 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
// panel button, 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
// run on the XR pacing thread. The Wii Remote bridge is internally locked, so
// the game thread may read it concurrently, and the virtual gamepad is only
// published here: OpenXRApplyVirtualGamepad() performs the SDL writes on the
// game thread, keeping SDL's joystick lock off this thread entirely.
class OpenXRInput final {
public:
explicit OpenXRInput(OpenXRLogCallback logger = {});
~OpenXRInput();
OpenXRInput(const OpenXRInput&) = delete;
OpenXRInput& operator=(const OpenXRInput&) = delete;
// Creates the action set/actions/spaces and attaches them to the session.
// Returns false (with LastError set) if the runtime rejects the action set;
// the caller continues without controller input rather than failing VR.
bool Create(OpenXRRuntime& runtime);
void Destroy();
// 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 Wii Remote pointer can land this frame, and
// settings_panel where the settings panel is (its whole rectangle). seat is
// the immersive seated frame, invalid outside an immersive race.
void Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
const OpenXRPointerScreen& settings_panel, const driving::SeatFrame& seat);
// The cockpit state the last Sync published (also OpenXRReadDriving()).
const DrivingSnapshot& Driving() const noexcept { return m_driving; }
// The tracked hands' joints in the seated frame, as the last Sync located
// them; read on the pacing thread only.
const hand_tracking::HandJointFrame& HandJoints() const noexcept { return m_joint_frame; }
// A hand's tracker while tracked hands keep one, else XR_NULL_HANDLE.
XrHandTrackerEXT HandTracker(uint32_t hand) const noexcept {
return hand < kHands ? m_hand_trackers[hand] : XR_NULL_HANDLE;
}
// Publishes a remote with nothing held, at rest and not pointing, and stops
// the haptics, for frames without focused input.
void Idle();
// Rumble for the given hand (0 = left, 1 = right); amplitude 0..1.
void ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration);
bool IsCreated() const noexcept { return m_created; }
bool HasVirtualGamepad() const noexcept { return m_joystick_id != 0; }
const std::string& LastError() const noexcept { return m_last_error; }
private:
static constexpr uint32_t kHands = 2;
bool CreateActions();
bool SuggestBindings();
void CreatePoseSpaces();
void DestroyPoseSpaces();
void LoadInputClock();
XrTime InputSampleTime(XrTime predicted_display_time) const;
bool AttachVirtualGamepad();
void DetachVirtualGamepad();
// `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);
// Hand steering: locates the grips in the seated frame, runs the wheel and
// hands its steering to `hands` before the game sees them.
void UpdateDriving(XrTime display_time, const driving::SeatFrame& seat,
std::array<wii_remote::HandInputs, kHands>& hands, bool withheld);
void ResetDriving();
// Tracked hands: the extension's functions at Create, the trackers as the
// settings ask for them, and both hands located for `time`, their joints
// in `seat` when it is valid.
void LoadHandTracking();
void UpdateHandTrackers();
void DestroyHandTrackers();
void LocateHands(XrTime time, const driving::SeatFrame& seat);
void LogInteractionProfiles();
void UpdateRumble();
void StopRumble();
bool Check(XrResult result, const char* operation);
void Log(OpenXRLogLevel level, const std::string& message) const noexcept;
OpenXRLogCallback m_logger;
OpenXRRuntime* m_runtime = nullptr;
XrActionSet m_action_set = XR_NULL_HANDLE;
XrAction m_thumbstick = XR_NULL_HANDLE;
XrAction m_thumbstick_click = XR_NULL_HANDLE;
XrAction m_trigger = XR_NULL_HANDLE;
XrAction m_squeeze = XR_NULL_HANDLE;
XrAction m_button_primary = XR_NULL_HANDLE; // A / X
XrAction m_button_secondary = XR_NULL_HANDLE; // B / Y
XrAction m_menu = XR_NULL_HANDLE;
XrAction m_aim_pose = XR_NULL_HANDLE;
XrAction m_grip_pose = XR_NULL_HANDLE;
XrAction m_haptic = XR_NULL_HANDLE;
XrPath m_hand_paths[kHands]{};
XrSpace m_aim_spaces[kHands]{};
XrSpace m_grip_spaces[kHands]{};
// xrConvertWin32PerformanceCounterToTimeKHR / xrConvertTimespecTimeToTimeKHR,
// when the runtime offers them; the input time falls back to display time.
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
void* m_joystick = nullptr; // SDL_Joystick*
ClickToggle m_first_person_click;
SteeringWheel m_wheel;
WheelReferenceLatch m_wheel_reference;
driving::WheelVisual m_wheel_visual;
std::array<bool, kHands> m_wheel_held{};
XrTime m_wheel_time = 0;
bool m_wheel_uses_geometry = false;
bool m_wheel_bike = false;
DrivingSnapshot m_driving{};
// Tracked hands.
struct TrackedHand {
bool active = false; // joints located this frame
bool seated = false; // and written into m_joint_frame, in the seated frame
hand_tracking::Source source = hand_tracking::Source::None;
hand_tracking::JointPositions positions{}; // application space
// XR_FB_hand_tracking_aim: the runtime's own pinch and menu gesture.
bool aim_valid = false;
bool aim_pinching = false;
bool aim_menu = false;
bool aim_system_gesture = false;
};
PFN_xrCreateHandTrackerEXT m_create_hand_tracker = nullptr;
PFN_xrDestroyHandTrackerEXT m_destroy_hand_tracker = nullptr;
PFN_xrLocateHandJointsEXT m_locate_hand_joints = nullptr;
bool m_hand_data_source = false; // XR_EXT_hand_tracking_data_source
bool m_hand_aim = false; // XR_FB_hand_tracking_aim
XrHandTrackerEXT m_hand_trackers[kHands]{};
bool m_hand_trackers_failed = false;
bool m_logged_hand_restart = false;
std::array<TrackedHand, kHands> m_tracked_hands{};
hand_tracking::HandJointFrame m_joint_frame{};
std::array<hand_tracking::Source, kHands> m_logged_sources{hand_tracking::Source::None,
hand_tracking::Source::None};
// Per frame, from the actions: a controller is in the hand (its squeeze is
// bound), and the hand drives simple_controller (Android only).
std::array<bool, kHands> m_squeeze_active{};
std::array<bool, kHands> m_hand_driven{};
std::array<bool, kHands> m_pinch{};
// Bare-hand driving: each hand's bare latch (camera joints, last grasp) and
// item pinch gate, and the game's pointer switch as last logged.
std::array<hand_tracking::BareLatch, kHands> m_bare_latch{};
std::array<hand_tracking::PinchGate, kHands> m_pinch_gate{};
int m_logged_game_pointer = -2;
uint64_t m_profile_serial = 0;
bool m_created = false;
bool m_logged_sync_failure = false;
bool m_logged_pointer = false;
std::string m_last_error;
};
// Game thread: writes the gamepad the pacing thread last published, if any.
// Does nothing when no OpenXR controllers are attached.
void OpenXRApplyVirtualGamepad() noexcept;
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR)