mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 06:00:25 +02:00
Implement OpenXR Wii Remote support
This commit is contained in:
1 parent
75f0f5f26d
commit
19485b9e8b
18 files changed
+1620
-91
No files matched your search
@@ -29,6 +29,7 @@ Standalone launches remain opt-in. `Config.toml` is created with the following d
|
||||
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
|
||||
@@ -124,6 +125,72 @@ 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`
|
||||
@@ -309,9 +376,10 @@ ends, including mid-frame flushes, so live setting changes cannot invalidate pen
|
||||
## Current limitations
|
||||
|
||||
- Only the project's supported PAL `RMCP01` translation has race instrumentation addresses.
|
||||
- Wii Remote pointer/motion emulation from tracked controllers is not implemented. OpenXR action
|
||||
bindings exist only on the Android build, where they present the Touch controllers as one
|
||||
ordinary gamepad; on Windows use the existing game-controller input path.
|
||||
- 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.
|
||||
|
||||
@@ -111,6 +111,15 @@ bool aurora_get_stereo_skip_copy_clears();
|
||||
void aurora_set_stereo_hud_screen(bool enabled, float width, float distance);
|
||||
bool aurora_get_stereo_hud_screen_enabled();
|
||||
|
||||
// Aspect ratios behind the most recent headset frame's 2D content, for mapping a
|
||||
// point on a virtual screen back onto the game picture (the VR Wii Remote
|
||||
// pointer). `pictureAspect` is the game picture's width over height, which the
|
||||
// immersive HUD screen's height follows; `snapshotAspect` is the desktop
|
||||
// presentation image's, which the virtual-screen eye texture letterboxes and
|
||||
// the picture is letterboxed inside. False until a headset frame was encoded.
|
||||
// Safe to call from any thread.
|
||||
bool aurora_get_stereo_screen_aspects(float* pictureAspect, float* snapshotAspect);
|
||||
|
||||
// What the desktop window shows while a headset is being fed. NORMAL leaves the
|
||||
// ordinary mono presentation untouched, the eye views mirror what the headset is
|
||||
// actually displaying, and NONE presents a black window. Live, and only
|
||||
|
||||
@@ -1495,6 +1495,30 @@ void draw_mirror_eye(const wgpu::RenderPassEncoder& pass, uint32_t eyeIndex, con
|
||||
pass.Draw(3);
|
||||
}
|
||||
|
||||
// Read by aurora_get_stereo_screen_aspects from the XR input thread.
|
||||
std::atomic<float> g_stereoPictureAspect{0.f};
|
||||
std::atomic<float> g_stereoSnapshotAspect{0.f};
|
||||
|
||||
// Records the geometry a headset frame's 2D content was laid out with: the picture aspect exactly as
|
||||
// encode_presentation_snapshot fits it into the snapshot (and, for immersive replay, as
|
||||
// stereo_hud_screen sizes the HUD screen), and the snapshot's own aspect.
|
||||
void publish_stereo_screen_aspects(const webgpu::PresentSource& presentSource, const wgpu::Extent3D& snapshotSize,
|
||||
bool immersiveReplay) noexcept {
|
||||
float picture = 0.f;
|
||||
if (!window::get_present_aspect_ratio(picture) || !(picture > 0.f)) {
|
||||
if (immersiveReplay) {
|
||||
picture = 16.f / 9.f;
|
||||
} else if (presentSource.size.width != 0 && presentSource.size.height != 0) {
|
||||
picture = static_cast<float>(presentSource.size.width) / static_cast<float>(presentSource.size.height);
|
||||
}
|
||||
}
|
||||
const float snapshot = snapshotSize.width != 0 && snapshotSize.height != 0
|
||||
? static_cast<float>(snapshotSize.width) / static_cast<float>(snapshotSize.height)
|
||||
: 0.f;
|
||||
g_stereoPictureAspect.store(picture, std::memory_order_relaxed);
|
||||
g_stereoSnapshotAspect.store(snapshot, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// `presentSource` is latched in the seal prologue: by the time this encodes, the producer's next
|
||||
// gfx::begin_frame() may already have cleared the display-copy override.
|
||||
void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder, const webgpu::PresentSource& presentSource,
|
||||
@@ -1962,6 +1986,9 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
const bool virtualScreenNeedsMono = stereoOutput && !immersiveReplay;
|
||||
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true,
|
||||
virtualScreenNeedsMono ? MirrorPlan::Mono : mirrorPlan);
|
||||
if (stereoOutput) {
|
||||
publish_stereo_screen_aspects(ctx.presentSource, finalImage->texture.size, immersiveReplay);
|
||||
}
|
||||
|
||||
if (virtualScreenNeedsMono) {
|
||||
// Use the completed mono snapshot so virtual-screen XR includes ImGui at
|
||||
@@ -2595,6 +2622,16 @@ void aurora_set_stereo_hud_screen(bool enabled, float width, float distance) {
|
||||
aurora::gfx::set_stereo_hud_screen(enabled, width, distance);
|
||||
}
|
||||
bool aurora_get_stereo_hud_screen_enabled() { return aurora::gfx::get_stereo_hud_screen_enabled(); }
|
||||
bool aurora_get_stereo_screen_aspects(float* pictureAspect, float* snapshotAspect) {
|
||||
const float picture = aurora::g_stereoPictureAspect.load(std::memory_order_relaxed);
|
||||
const float snapshot = aurora::g_stereoSnapshotAspect.load(std::memory_order_relaxed);
|
||||
if (pictureAspect == nullptr || snapshotAspect == nullptr || !(picture > 0.f) || !(snapshot > 0.f)) {
|
||||
return false;
|
||||
}
|
||||
*pictureAspect = picture;
|
||||
*snapshotAspect = snapshot;
|
||||
return true;
|
||||
}
|
||||
void aurora_set_stereo_mirror_view(AuroraStereoMirrorView view) { aurora::gfx::set_stereo_mirror_view(view); }
|
||||
AuroraStereoMirrorView aurora_get_stereo_mirror_view() { return aurora::gfx::get_stereo_mirror_view(); }
|
||||
void aurora_set_background_input(bool value) {
|
||||
|
||||
+10
-7
@@ -89,12 +89,15 @@ the bridge needs.
|
||||
|
||||
Quest Touch controllers are not HID gamepads, so `openxr_input.cpp` syncs an
|
||||
OpenXR action set on the pacing thread and feeds a virtual SDL joystick
|
||||
(`SDL_AttachVirtualJoystick`, type gamepad). Aurora opens it like any pad and
|
||||
assigns it to player 1; every existing binding, dead zone and overlay setting
|
||||
applies. Mapping: A/B → South/East, X/Y → West/North, index triggers → trigger
|
||||
axes, grips → shoulders, thumbsticks → sticks (clicks → stick buttons), left
|
||||
menu → Start. Bindings are suggested for `oculus/touch_controller` and
|
||||
`khr/simple_controller`.
|
||||
(`SDL_AttachVirtualJoystick`, type gamepad) that Aurora assigns to player 1.
|
||||
By default (`[vr] controller_mode = "wii_remote"`) that port is then served
|
||||
through KPAD as a Wii Remote with a Nunchuk, with motion and an IR pointer
|
||||
aimed at the virtual screen; see "Controllers" in `OPENXR.md` for the mapping
|
||||
and the geometry. With `controller_mode = "gamepad"` it stays an ordinary pad:
|
||||
A/B → South/East, X/Y → West/North, index triggers → trigger axes, grips →
|
||||
shoulders, thumbsticks → sticks (clicks → stick buttons), left menu → Start,
|
||||
and every existing binding, dead zone and overlay setting applies. Bindings are
|
||||
suggested for `oculus/touch_controller` and `khr/simple_controller`.
|
||||
|
||||
### Android platform glue
|
||||
|
||||
@@ -253,7 +256,7 @@ the app:
|
||||
| --- | --- |
|
||||
| `debug.wiicompiled.vtxpad 0` | Turns the stride padding off, to re-check a driver update |
|
||||
| `debug.wiicompiled.validation 1` | Keeps WebGPU validation and robustness on in release builds |
|
||||
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes |
|
||||
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist |
|
||||
| `debug.wiicompiled.fpslog 1` | Logs the game's rendered frame rate every 5 s. The compositor's `VrApi` log line gives headset FPS, `GPU%`, `CPU%` and app GPU time (`App=`) |
|
||||
|
||||
The injector makes headset tests possible with nobody wearing the headset.
|
||||
|
||||
@@ -397,6 +397,13 @@ target_include_directories(mkw_vr_first_person_tests PRIVATE "${CMAKE_CURRENT_LI
|
||||
target_compile_features(mkw_vr_first_person_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_first_person_tests COMMAND mkw_vr_first_person_tests)
|
||||
|
||||
# The VR controllers' Wii Remote presentation (accelerometer frame, pointer
|
||||
# raycast, debounce, button profile) is header-only for the same reason.
|
||||
add_executable(mkw_vr_wii_remote_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_wii_remote_tests.cpp")
|
||||
target_include_directories(mkw_vr_wii_remote_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_wii_remote_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_wii_remote_tests COMMAND mkw_vr_wii_remote_tests)
|
||||
|
||||
# Resolve the real local-kart pointer walk against synthetic offline/online rosters.
|
||||
add_executable(mkw_vr_player_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_player_tests.cpp")
|
||||
target_include_directories(mkw_vr_player_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
|
||||
@@ -58,6 +58,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> vrStopAtDisplayCopy;
|
||||
std::optional<bool> vrSkipCopyClears;
|
||||
std::optional<std::string> vrMirrorView;
|
||||
std::optional<std::string> vrControllerMode;
|
||||
std::optional<uint32_t> vrFrameInterpolationFps;
|
||||
std::optional<bool> vrFirstPerson;
|
||||
std::optional<float> vrFirstPersonUnitsPerMeter;
|
||||
@@ -183,6 +184,14 @@ inline constexpr const char* kVrMirrorViewDefault = "normal";
|
||||
inline bool IsSupportedVrMirrorView(std::string_view value) {
|
||||
return value == "normal" || value == "both" || value == "left" || value == "right" || value == "none";
|
||||
}
|
||||
// What the tracked VR controllers are to the game: "wii_remote" is a Wii
|
||||
// Remote with a Nunchuk (motion and pointer included), "gamepad" one ordinary
|
||||
// controller read as a GameCube pad. Matches mkw::vr::OpenXRControllerMode.
|
||||
inline constexpr const char* kVrControllerModeDefault = "wii_remote";
|
||||
|
||||
inline bool IsSupportedVrControllerMode(std::string_view value) {
|
||||
return value == "wii_remote" || value == "gamepad";
|
||||
}
|
||||
// SDL scancode name, spelled the way SDL_GetScancodeName produces it. An
|
||||
// empty string leaves the recenter hotkey unbound, menu button only.
|
||||
inline constexpr std::string_view kVrRecenterKeyDefault = "F9";
|
||||
@@ -393,6 +402,12 @@ inline void EnsureConfigFile() {
|
||||
"# \"right\" mirror the headset's eyes, and \"none\" blacks the window\n"
|
||||
"# out. Changeable live from the F10 menu.\n"
|
||||
"mirror_view = \"normal\"\n"
|
||||
"# What the headset's controllers are to the game: \"wii_remote\"\n"
|
||||
"# is a Wii Remote (right hand, with motion and a pointer aimed at\n"
|
||||
"# the virtual screen) plus a Nunchuk (left hand); \"gamepad\" is one\n"
|
||||
"# ordinary controller read as a GameCube pad. Changeable live from\n"
|
||||
"# the F10 menu.\n"
|
||||
"controller_mode = \"wii_remote\"\n"
|
||||
"# VR interpolation: 0 = Off, 1 = Auto, or 72/90/120 FPS. Live.\n"
|
||||
"frame_interpolation_fps = 0\n"
|
||||
"render_scale = 1.0\n"
|
||||
@@ -655,6 +670,10 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
value && IsSupportedVrMirrorView(*value)) {
|
||||
config.vrMirrorView = *value;
|
||||
}
|
||||
if (auto value = FindConfigValue<std::string>(document, "vr", "controller_mode");
|
||||
value && IsSupportedVrControllerMode(*value)) {
|
||||
config.vrControllerMode = *value;
|
||||
}
|
||||
config.vrFrameInterpolationFps = FindConfigValue<uint32_t>(document, "vr", "frame_interpolation_fps");
|
||||
if (!config.vrFrameInterpolationFps) {
|
||||
// Migrate the initial experimental checkbox to Auto.
|
||||
@@ -962,6 +981,14 @@ inline bool SetVrMirrorView(std::string value) {
|
||||
return WriteSetting("vr", "mirror_view", FormatString(value));
|
||||
}
|
||||
|
||||
inline bool SetVrControllerMode(std::string value) {
|
||||
if (!IsSupportedVrControllerMode(value)) {
|
||||
return false;
|
||||
}
|
||||
Mutable().vrControllerMode = value;
|
||||
return WriteSetting("vr", "controller_mode", FormatString(value));
|
||||
}
|
||||
|
||||
inline bool SetVrFrameInterpolationFps(uint32_t value) {
|
||||
value = mkw::vr::NormalizeFrameInterpolationFps(value);
|
||||
Mutable().vrFrameInterpolationFps = value;
|
||||
@@ -1302,6 +1329,11 @@ inline std::string VrMirrorView(std::string fallback = kVrMirrorViewDefault) {
|
||||
return value && IsSupportedVrMirrorView(*value) ? *value : std::move(fallback);
|
||||
}
|
||||
|
||||
inline std::string VrControllerMode(std::string fallback = kVrControllerModeDefault) {
|
||||
const auto& value = Get().vrControllerMode;
|
||||
return value && IsSupportedVrControllerMode(*value) ? *value : std::move(fallback);
|
||||
}
|
||||
|
||||
inline uint32_t VrFrameInterpolationFps() {
|
||||
return mkw::vr::NormalizeFrameInterpolationFps(Get().vrFrameInterpolationFps.value_or(0));
|
||||
}
|
||||
|
||||
@@ -5,25 +5,45 @@
|
||||
#if defined(MKW_ENABLE_OPENXR)
|
||||
|
||||
#include "vr/openxr_runtime.h"
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
// OpenXR action-based controller input, surfaced to the rest of the runtime as
|
||||
// one ordinary SDL gamepad.
|
||||
// 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. Rather than adding a second input path through the PAD/WPAD
|
||||
// HLE, 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, maps and assigns to player 1 exactly like a physical
|
||||
// pad. Every binding the settings overlay already offers keeps working.
|
||||
// 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:
|
||||
//
|
||||
// Mapping (Oculus Touch profile; the same actions are also bound for
|
||||
// khr/simple_controller so an unknown runtime still gets A/menu):
|
||||
// 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 follow 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
|
||||
@@ -31,12 +51,14 @@ namespace mkw::vr {
|
||||
// left / right thumbstick-> left / right stick axes, clicks -> stick buttons
|
||||
// left menu -> Start
|
||||
//
|
||||
// Lifetime: OpenXRInputCreate after the session exists (attaches the action
|
||||
// set, which OpenXR permits once per session), OpenXRInputSync once per
|
||||
// xrWaitFrame when the session is focused, OpenXRInputDestroy before the
|
||||
// session is destroyed. All three run on the XR pacing thread; SDL's virtual
|
||||
// joystick setters are internally locked, so the game thread may read the pad
|
||||
// concurrently.
|
||||
// Both are bound for the Oculus Touch profile; khr/simple_controller gets
|
||||
// select/menu and the poses so an unknown runtime still offers something.
|
||||
//
|
||||
// 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; SDL's virtual joystick setters and the Wii
|
||||
// Remote bridge are internally locked, so the game thread may read concurrently.
|
||||
class OpenXRInput final {
|
||||
public:
|
||||
explicit OpenXRInput(OpenXRLogCallback logger = {});
|
||||
@@ -51,9 +73,14 @@ public:
|
||||
bool Create(OpenXRRuntime& runtime);
|
||||
void Destroy();
|
||||
|
||||
// xrSyncActions + state reads, then publishes to the virtual gamepad.
|
||||
// predicted_display_time is the frame's XrTime for pose-based lookups.
|
||||
void Sync(XrTime predicted_display_time);
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
@@ -63,10 +90,20 @@ public:
|
||||
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();
|
||||
void PublishWiiRemote(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
|
||||
const std::array<wii_remote::HandInputs, kHands>& hands, uint32_t injected_buttons);
|
||||
void UpdateRumble();
|
||||
void StopRumble();
|
||||
bool Check(XrResult result, const char* operation);
|
||||
void Log(OpenXRLogLevel level, const std::string& message) const noexcept;
|
||||
|
||||
@@ -80,12 +117,24 @@ private:
|
||||
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[2]{};
|
||||
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;
|
||||
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*
|
||||
bool m_created = false;
|
||||
bool m_logged_sync_failure = false;
|
||||
bool m_logged_pointer = false;
|
||||
std::string m_last_error;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
// The tracked VR controllers presented to the game as a Wii Remote with a
|
||||
// Nunchuk, the way DolphinXR's "OpenXR Wii Remote" source does it: the right
|
||||
// controller is the remote (buttons, accelerometer and IR pointer), the left
|
||||
// one is the Nunchuk (stick, C/Z and its own accelerometer).
|
||||
//
|
||||
// The OpenXR pacing thread builds one OpenXRWiiRemoteSample per XR frame and
|
||||
// publishes it here; the KPAD/WPAD HLE on the guest thread reads the latest one
|
||||
// whenever the game polls. Nothing in this header depends on OpenXR, so the
|
||||
// guest side compiles (and simply never sees a remote) in builds without it.
|
||||
|
||||
enum class OpenXRControllerMode : uint8_t {
|
||||
// Wii Remote + Nunchuk through KPAD, with motion and pointing.
|
||||
WiiRemote,
|
||||
// One ordinary gamepad, read through PAD as a GameCube controller.
|
||||
Gamepad,
|
||||
};
|
||||
|
||||
struct OpenXRWiiRemoteSample {
|
||||
uint32_t hold = 0; // WPAD_BUTTON_* bits, Nunchuk C/Z included
|
||||
std::array<float, 3> acc{0.0f, -1.0f, 0.0f}; // remote accelerometer in g, KPAD frame
|
||||
std::array<float, 2> stick{}; // Nunchuk stick, -1..1, +y up
|
||||
std::array<float, 3> nunchuk_acc{0.0f, -1.0f, 0.0f};
|
||||
// IR pointer in KPADStatus terms: pos is -1..1 across the game picture with
|
||||
// +y down, horizon is the remote's x axis on the screen ((1, 0) held level,
|
||||
// (0, 1) rolled a quarter turn clockwise), distance in metres.
|
||||
bool pointer_valid = false;
|
||||
std::array<float, 2> pointer{};
|
||||
std::array<float, 2> horizon{1.0f, 0.0f};
|
||||
float distance_meters = 0.0f;
|
||||
};
|
||||
|
||||
// Live switch between the two presentations; the settings bar and the launch
|
||||
// configuration both go through it.
|
||||
void OpenXRSetControllerMode(OpenXRControllerMode mode) noexcept;
|
||||
OpenXRControllerMode OpenXRGetControllerMode() noexcept;
|
||||
|
||||
// Guest side. True when `sdl_joystick_id` is the OpenXR virtual gamepad and the
|
||||
// controllers are currently presented as a Wii Remote.
|
||||
bool OpenXRWiiRemoteOwnsGamepad(uint32_t sdl_joystick_id) noexcept;
|
||||
// Latest published sample; false before the first one or after withdrawal.
|
||||
bool OpenXRReadWiiRemote(OpenXRWiiRemoteSample& sample) noexcept;
|
||||
// WPADControlMotor for the emulated remote.
|
||||
void OpenXRSetWiiRemoteRumble(bool active) noexcept;
|
||||
|
||||
// XR side.
|
||||
void OpenXRPublishWiiRemote(uint32_t sdl_joystick_id, const OpenXRWiiRemoteSample& sample) noexcept;
|
||||
void OpenXRWithdrawWiiRemote() noexcept;
|
||||
bool OpenXRWiiRemoteRumbleRequested() noexcept;
|
||||
|
||||
// The geometry and signal conditioning behind a sample, kept free of OpenXR
|
||||
// types so it can be checked headlessly (tests/vr_wii_remote_tests.cpp).
|
||||
//
|
||||
// Conventions are OpenXR's: right-handed, +Y up, metres. A controller's aim
|
||||
// pose points down its -Z axis with +X to the right and +Y up; a screen faces
|
||||
// its +Z axis with +X to the right and +Y up across the picture.
|
||||
namespace wii_remote {
|
||||
|
||||
// WPAD_BUTTON_* bits as KPADStatus.hold carries them.
|
||||
inline constexpr uint32_t kButtonLeft = 0x0001, kButtonRight = 0x0002, kButtonDown = 0x0004,
|
||||
kButtonUp = 0x0008, kButtonPlus = 0x0010, kButtonTwo = 0x0100,
|
||||
kButtonOne = 0x0200, kButtonB = 0x0400, kButtonA = 0x0800,
|
||||
kButtonMinus = 0x1000, kButtonZ = 0x2000, kButtonC = 0x4000,
|
||||
kButtonHome = 0x8000;
|
||||
|
||||
inline constexpr float kStandardGravity = 9.80665f;
|
||||
// The remote's ADXL330 saturates a little past +-3 g.
|
||||
inline constexpr float kAccelRangeG = 3.6f;
|
||||
// Analog inputs count as a press past this, like Dolphin's button threshold.
|
||||
inline constexpr float kPressThreshold = 0.5f;
|
||||
// How far past the picture's edge (in half extents) the pointer is still
|
||||
// reported. A real remote's camera (42 x 31.5 degrees) keeps seeing the sensor
|
||||
// bar well beyond the screen, so it does not drop the cursor at the border.
|
||||
inline constexpr float kPointerMarginU = 1.9f;
|
||||
inline constexpr float kPointerMarginV = 1.5f;
|
||||
// An excursion past those margins, or a lost hit, must last this long before
|
||||
// the pointer is hidden: pose spikes during fast wrist motion otherwise drop it.
|
||||
inline constexpr int64_t kPointerHideDelayNs = 100'000'000;
|
||||
|
||||
using Vec3 = std::array<float, 3>;
|
||||
using Quat = std::array<float, 4>; // x, y, z, w
|
||||
|
||||
struct Pose {
|
||||
Vec3 position{};
|
||||
Quat orientation{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
};
|
||||
|
||||
inline float Dot(const Vec3& a, const Vec3& b) noexcept {
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
}
|
||||
|
||||
// q * v * conjugate(q) for a unit quaternion.
|
||||
inline Vec3 Rotate(const Quat& q, const Vec3& v) noexcept {
|
||||
const Vec3 t{2.0f * (q[1] * v[2] - q[2] * v[1]), 2.0f * (q[2] * v[0] - q[0] * v[2]),
|
||||
2.0f * (q[0] * v[1] - q[1] * v[0])};
|
||||
return {v[0] + q[3] * t[0] + (q[1] * t[2] - q[2] * t[1]),
|
||||
v[1] + q[3] * t[1] + (q[2] * t[0] - q[0] * t[2]),
|
||||
v[2] + q[3] * t[2] + (q[0] * t[1] - q[1] * t[0])};
|
||||
}
|
||||
|
||||
inline Quat Conjugate(const Quat& q) noexcept {
|
||||
return {-q[0], -q[1], -q[2], q[3]};
|
||||
}
|
||||
|
||||
// A flat rectangle: the part of a virtual screen the game's picture covers.
|
||||
struct Screen {
|
||||
Pose pose;
|
||||
float half_width = 0.0f;
|
||||
float half_height = 0.0f;
|
||||
};
|
||||
|
||||
struct ScreenHit {
|
||||
bool valid = false;
|
||||
float u = 0.0f; // -1..1 across the picture, +right; beyond +-1 off the edge
|
||||
float v = 0.0f; // -1..1, +up
|
||||
float distance_meters = 0.0f;
|
||||
};
|
||||
|
||||
// Where the aim ray meets the screen's plane, the same absolute mapping as
|
||||
// DolphinXR's ComputeVirtualScreenHit: aiming at a point puts the pointer
|
||||
// there, with nothing to recenter.
|
||||
inline ScreenHit RaycastScreen(const Pose& aim, const Screen& screen) noexcept {
|
||||
ScreenHit hit{};
|
||||
if (!(screen.half_width > 0.0f) || !(screen.half_height > 0.0f)) {
|
||||
return hit;
|
||||
}
|
||||
const Quat inverse = Conjugate(screen.pose.orientation);
|
||||
const Vec3 offset{aim.position[0] - screen.pose.position[0], aim.position[1] - screen.pose.position[1],
|
||||
aim.position[2] - screen.pose.position[2]};
|
||||
const Vec3 origin = Rotate(inverse, offset);
|
||||
const Vec3 direction = Rotate(inverse, Rotate(aim.orientation, {0.0f, 0.0f, -1.0f}));
|
||||
// Only from in front of the picture, and only towards it.
|
||||
if (!(origin[2] > 0.0f) || !(direction[2] < -1.0e-6f)) {
|
||||
return hit;
|
||||
}
|
||||
const float t = -origin[2] / direction[2];
|
||||
hit.valid = true;
|
||||
hit.u = (origin[0] + t * direction[0]) / screen.half_width;
|
||||
hit.v = (origin[1] + t * direction[1]) / screen.half_height;
|
||||
// Perpendicular distance: rotating the controller must not move it.
|
||||
hit.distance_meters = origin[2];
|
||||
return hit;
|
||||
}
|
||||
|
||||
// KPADStatus.pos for a hit: the SDK's pointer runs from (-1, -1) at the
|
||||
// picture's top left to (1, 1) at its bottom right.
|
||||
inline std::array<float, 2> KpadPosition(const ScreenHit& hit) noexcept {
|
||||
return {hit.u, -hit.v};
|
||||
}
|
||||
|
||||
// KPADStatus.horizon: the remote's right axis as it lies on the screen, in the
|
||||
// pointer's +y-down frame.
|
||||
inline std::array<float, 2> Horizon(const Pose& aim, const Screen& screen) noexcept {
|
||||
const Vec3 right = Rotate(Conjugate(screen.pose.orientation), Rotate(aim.orientation, {1.0f, 0.0f, 0.0f}));
|
||||
const float length = std::sqrt(right[0] * right[0] + right[1] * right[1]);
|
||||
if (!(length > 1.0e-3f)) {
|
||||
return {1.0f, 0.0f};
|
||||
}
|
||||
return {right[0] / length, -right[1] / length};
|
||||
}
|
||||
|
||||
// KPAD accelerometer reading for a controller whose aim orientation is
|
||||
// `orientation` while it accelerates at `world_acceleration` (m/s^2).
|
||||
//
|
||||
// An accelerometer measures specific force, acceleration minus gravity, so a
|
||||
// remote at rest reads 1 g upwards. KPAD's frame is x right across the face, y
|
||||
// through the back of the remote and z towards the player (Wii axes
|
||||
// (-x, -z, y)), which on an aim pose is (x, -y, z): at rest, level, that is
|
||||
// (0, -1, 0), and DolphinXR's (-x, z, y) Wii-frame mapping lands on the same.
|
||||
inline Vec3 KpadAcceleration(const Quat& orientation, const Vec3& world_acceleration) noexcept {
|
||||
const Vec3 specific_force{world_acceleration[0], world_acceleration[1] + kStandardGravity,
|
||||
world_acceleration[2]};
|
||||
const Vec3 local = Rotate(Conjugate(orientation), specific_force);
|
||||
const auto axis = [](float value) {
|
||||
return std::clamp(value / kStandardGravity, -kAccelRangeG, kAccelRangeG);
|
||||
};
|
||||
return {axis(local[0]), axis(-local[1]), axis(local[2])};
|
||||
}
|
||||
|
||||
// Differentiates a controller's linear velocity into the acceleration its
|
||||
// accelerometer would add to gravity, mirroring DolphinXR's
|
||||
// OpenXRVelocityHistory: the runtime's velocity is averaged with one derived
|
||||
// from the pose, because some runtimes smooth theirs heavily and a flick loses
|
||||
// its peak. Time is XrTime nanoseconds, so wall-clock jitter never enters dt.
|
||||
class MotionTracker {
|
||||
public:
|
||||
// `orientation` is the aim pose, `position`/`velocity` the grip's. Returns
|
||||
// the KPAD reading; with no orientation it repeats the last one.
|
||||
Vec3 Update(const Quat* orientation, const Vec3* position, const Vec3* velocity, int64_t time_ns) noexcept {
|
||||
if (orientation == nullptr) {
|
||||
Reset();
|
||||
return m_last;
|
||||
}
|
||||
const float dt = m_has_position ? static_cast<float>(time_ns - m_time_ns) * 1.0e-9f : 0.0f;
|
||||
const bool dt_usable = dt > 0.001f;
|
||||
|
||||
bool have_velocity = velocity != nullptr;
|
||||
Vec3 current = have_velocity ? *velocity : Vec3{};
|
||||
if (position != nullptr && m_has_position && dt_usable) {
|
||||
const Vec3 from_pose{((*position)[0] - m_position[0]) / dt, ((*position)[1] - m_position[1]) / dt,
|
||||
((*position)[2] - m_position[2]) / dt};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
current[i] = have_velocity ? 0.5f * (current[i] + from_pose[i]) : from_pose[i];
|
||||
}
|
||||
have_velocity = true;
|
||||
}
|
||||
|
||||
Vec3 acceleration{};
|
||||
if (have_velocity && m_has_velocity && dt_usable) {
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
acceleration[i] = (current[i] - m_velocity[i]) / dt;
|
||||
}
|
||||
}
|
||||
|
||||
if (position != nullptr) {
|
||||
m_position = *position;
|
||||
m_time_ns = time_ns;
|
||||
m_has_position = true;
|
||||
} else {
|
||||
m_has_position = false;
|
||||
m_has_velocity = false;
|
||||
}
|
||||
if (have_velocity) {
|
||||
m_velocity = current;
|
||||
m_has_velocity = true;
|
||||
} else if (!m_has_position) {
|
||||
m_has_velocity = false;
|
||||
}
|
||||
|
||||
m_last = KpadAcceleration(*orientation, acceleration);
|
||||
return m_last;
|
||||
}
|
||||
|
||||
void Reset() noexcept {
|
||||
m_has_position = false;
|
||||
m_has_velocity = false;
|
||||
}
|
||||
|
||||
// Back to a remote lying still, for when the controllers go idle.
|
||||
void Rest() noexcept {
|
||||
Reset();
|
||||
m_last = {0.0f, -1.0f, 0.0f};
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_has_position = false;
|
||||
bool m_has_velocity = false;
|
||||
Vec3 m_position{};
|
||||
Vec3 m_velocity{};
|
||||
int64_t m_time_ns = 0;
|
||||
Vec3 m_last{0.0f, -1.0f, 0.0f};
|
||||
};
|
||||
|
||||
// Hides the pointer the way a real remote loses the sensor bar, without
|
||||
// dropping it on every tracking hiccup: brief excursions and lost hits hold or
|
||||
// pin the last position, and only a sustained one hides it.
|
||||
class PointerFilter {
|
||||
public:
|
||||
ScreenHit Update(const ScreenHit& hit, int64_t time_ns) noexcept {
|
||||
const bool on_screen = hit.valid && std::fabs(hit.u) <= kPointerMarginU &&
|
||||
std::fabs(hit.v) <= kPointerMarginV;
|
||||
if (on_screen) {
|
||||
m_off_screen = false;
|
||||
m_held = hit;
|
||||
return hit;
|
||||
}
|
||||
if (!m_off_screen) {
|
||||
m_off_screen = true;
|
||||
m_off_since_ns = time_ns;
|
||||
}
|
||||
if (!m_held.valid || time_ns - m_off_since_ns >= kPointerHideDelayNs) {
|
||||
m_held.valid = false;
|
||||
return {};
|
||||
}
|
||||
if (!hit.valid) {
|
||||
return m_held;
|
||||
}
|
||||
ScreenHit pinned = hit;
|
||||
pinned.u = std::clamp(hit.u, -kPointerMarginU, kPointerMarginU);
|
||||
pinned.v = std::clamp(hit.v, -kPointerMarginV, kPointerMarginV);
|
||||
return pinned;
|
||||
}
|
||||
|
||||
void Reset() noexcept {
|
||||
m_held = {};
|
||||
m_off_screen = false;
|
||||
}
|
||||
|
||||
private:
|
||||
ScreenHit m_held{};
|
||||
bool m_off_screen = false;
|
||||
int64_t m_off_since_ns = 0;
|
||||
};
|
||||
|
||||
// The part of an aspect-ratio-preserving fit a `content` aspect takes inside a
|
||||
// `container` aspect, as fractions of the container's width and height.
|
||||
inline std::array<float, 2> FitFraction(float content_aspect, float container_aspect) noexcept {
|
||||
if (!(content_aspect > 0.0f) || !(container_aspect > 0.0f)) {
|
||||
return {1.0f, 1.0f};
|
||||
}
|
||||
return content_aspect >= container_aspect ? std::array<float, 2>{1.0f, container_aspect / content_aspect}
|
||||
: std::array<float, 2>{content_aspect / container_aspect, 1.0f};
|
||||
}
|
||||
|
||||
// Half extents, in metres, of the game picture on the menu quad. The quad is
|
||||
// `quad_width` across with the eye texture's aspect; Aurora fits the desktop
|
||||
// snapshot into that texture and the game picture into the snapshot, both
|
||||
// letterboxed, so a 4:3 picture in a 16:9 window keeps its pillarboxes.
|
||||
inline std::array<float, 2> MenuPictureHalfExtents(float quad_width, float eye_aspect, float snapshot_aspect,
|
||||
float picture_aspect) noexcept {
|
||||
const float quad_half_width = 0.5f * quad_width;
|
||||
const float quad_half_height = eye_aspect > 0.0f ? quad_half_width / eye_aspect : quad_half_width;
|
||||
const std::array<float, 2> snapshot = FitFraction(snapshot_aspect, eye_aspect);
|
||||
const std::array<float, 2> picture = FitFraction(picture_aspect, snapshot_aspect);
|
||||
return {quad_half_width * snapshot[0] * picture[0], quad_half_height * snapshot[1] * picture[1]};
|
||||
}
|
||||
|
||||
// One controller's digital and analog inputs.
|
||||
struct HandInputs {
|
||||
bool primary = false; // A / X
|
||||
bool secondary = false; // B / Y
|
||||
bool menu = false;
|
||||
bool thumbstick_click = false;
|
||||
float trigger = 0.0f;
|
||||
float squeeze = 0.0f;
|
||||
float stick_x = 0.0f;
|
||||
float stick_y = 0.0f; // +up
|
||||
};
|
||||
|
||||
// DolphinXR's default "OpenXR Wii Remote" profile (Data/Sys/Profiles/Wiimote):
|
||||
// right A -> A, right trigger -> B, right stick up/down -> 1/2,
|
||||
// right stick left/right -> -/+, left menu -> HOME,
|
||||
// left grip -> C, left trigger -> Z, left stick -> Nunchuk stick.
|
||||
inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) noexcept {
|
||||
uint32_t hold = 0;
|
||||
const auto press = [&hold](bool held, uint32_t bit) {
|
||||
if (held) {
|
||||
hold |= bit;
|
||||
}
|
||||
};
|
||||
press(right.primary, kButtonA);
|
||||
press(right.trigger > kPressThreshold, kButtonB);
|
||||
press(right.stick_y > kPressThreshold, kButtonOne);
|
||||
press(right.stick_y < -kPressThreshold, kButtonTwo);
|
||||
press(right.stick_x < -kPressThreshold, kButtonMinus);
|
||||
press(right.stick_x > kPressThreshold, kButtonPlus);
|
||||
press(left.menu, kButtonHome);
|
||||
press(left.squeeze > kPressThreshold, kButtonC);
|
||||
press(left.trigger > kPressThreshold, kButtonZ);
|
||||
return hold;
|
||||
}
|
||||
|
||||
// The left thumbstick as the Nunchuk's, kept inside its circular gate.
|
||||
inline std::array<float, 2> NunchukStick(const HandInputs& left) noexcept {
|
||||
float x = left.stick_x;
|
||||
float y = left.stick_y;
|
||||
const float length = std::sqrt(x * x + y * y);
|
||||
if (length > 1.0f) {
|
||||
x /= length;
|
||||
y /= length;
|
||||
}
|
||||
return {x, y};
|
||||
}
|
||||
|
||||
} // namespace wii_remote
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -17,6 +17,11 @@ struct PADStatus;
|
||||
// extension in or out mid-game switches control scheme like on the console. Only
|
||||
// the Wii U Pro Controller, which has no Wii-era equivalent, goes through
|
||||
// aurora's PAD layer as a GameCube pad.
|
||||
//
|
||||
// The OpenXR build's tracked controllers join the same path: while they are
|
||||
// presented as a Wii Remote (vr/openxr_wii_remote.h), the virtual gamepad they
|
||||
// feed is a Wii Remote with a Nunchuk here, and its samples, IR pointer
|
||||
// included, come from the headset instead of SDL.
|
||||
namespace WiiRemoteInput {
|
||||
|
||||
enum class Kind : uint8_t {
|
||||
@@ -52,6 +57,13 @@ struct KpadSample {
|
||||
int16_t clRStickRaw[2] = {};
|
||||
uint8_t clTriggerL = 0; // 0..255; SDL only exposes the digital click
|
||||
uint8_t clTriggerR = 0;
|
||||
// IR pointer, only from the VR controllers (a Bluetooth remote's camera
|
||||
// data does not reach SDL). pos is -1..1 across the game picture with +y
|
||||
// down, horizon the remote's x axis on screen, dist in metres.
|
||||
bool dpdValid = false;
|
||||
float pos[2] = {};
|
||||
float horizon[2] = {1.0f, 0.0f};
|
||||
float dist = 0.0f;
|
||||
};
|
||||
|
||||
// What the game should see on `chan`: the controller SDL has there right now,
|
||||
@@ -63,6 +75,8 @@ Kind EffectiveKind(uint32_t chan);
|
||||
// True when the game reads `chan` through KPAD: a Wii Remote alone, with a
|
||||
// Nunchuk or with a Classic Controller (live or within the swap grace period).
|
||||
bool IsRemoteChannel(uint32_t chan);
|
||||
// True when the remote on `chan` is the VR controllers (see above).
|
||||
bool IsVrControllerChannel(uint32_t chan);
|
||||
// Reads the current state of the remote on `chan`; false when IsRemoteChannel
|
||||
// is false. During the swap grace period the sample is neutral.
|
||||
bool ReadKpadSample(uint32_t chan, KpadSample& sample);
|
||||
|
||||
@@ -7,19 +7,30 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
// KPAD HLE fed by a real Bluetooth Wii Remote. The game calls KPADRead once per
|
||||
// frame with room for 16 KPADStatus entries and only looks at entry 0; with a
|
||||
// Classic Controller it also calls KPADGetUnifiedWpadStatus for the raw
|
||||
// WPADCLStatus (buttons, sticks and triggers of the extension).
|
||||
// KPAD HLE fed by a real Bluetooth Wii Remote, or by VR controllers standing in
|
||||
// for one. The game calls KPADRead once per frame with room for 16 KPADStatus
|
||||
// entries and only looks at entry 0; with a Classic Controller it also calls
|
||||
// KPADGetUnifiedWpadStatus for the raw WPADCLStatus (buttons, sticks and
|
||||
// triggers of the extension). Of the pointer, Input::WiiController::UpdateImpl
|
||||
// reads pos, horizon and dist, and only while dpd_valid_fg is positive.
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kKpadStatusSize = 0x84;
|
||||
|
||||
// KPADStatus field offsets (RVL SDK).
|
||||
constexpr uint32_t kHold = 0x00, kTrig = 0x04, kRelease = 0x08, kAcc = 0x0C, kAccValue = 0x18,
|
||||
kAccSpeed = 0x1C, kPos = 0x20, kAccVertical = 0x54, kDevType = 0x5C, kWpadErr = 0x5D,
|
||||
kDpdValidFg = 0x5E, kDataFormat = 0x5F, kFsStick = 0x60, kFsAcc = 0x68, kFsAccValue = 0x74,
|
||||
kFsAccSpeed = 0x78;
|
||||
kAccSpeed = 0x1C, kPos = 0x20, kVec = 0x28, kSpeed = 0x30, kHorizon = 0x34, kHoriVec = 0x3C,
|
||||
kHoriSpeed = 0x44, kDist = 0x48, kDistVec = 0x4C, kDistSpeed = 0x50, kAccVertical = 0x54,
|
||||
kDevType = 0x5C, kWpadErr = 0x5D, kDpdValidFg = 0x5E, kDataFormat = 0x5F, kFsStick = 0x60,
|
||||
kFsAcc = 0x68, kFsAccValue = 0x74, kFsAccSpeed = 0x78;
|
||||
|
||||
// KPAD's per-channel work area (PAL RMCP01): KPADInitEx sets the pointer
|
||||
// switch to 1 for every channel, and KPADEnableDpd / KPADDisableDpd, which the
|
||||
// game inlines into Input::WiiController::TogglePointer, flip it. The SDK
|
||||
// reports no pointer while it is off.
|
||||
constexpr uint32_t kKpadWorkBase = 0x803457E0, kKpadWorkSize = 0x538, kKpadDpdEnabled = 0x520;
|
||||
// dpd_valid_fg for a pointer computed from both sensor-bar dots.
|
||||
constexpr uint8_t kDpdValidTwoDots = 2;
|
||||
// KPADStatus.ex_status.cl (KPADEXStatus, Classic Controller view).
|
||||
constexpr uint32_t kClHold = 0x60, kClTrig = 0x64, kClRelease = 0x68, kClLStick = 0x6C, kClRStick = 0x74,
|
||||
kClLTrigger = 0x7C, kClRTrigger = 0x80;
|
||||
@@ -52,6 +63,10 @@ struct ChannelState {
|
||||
uint32_t prevClHold = 0;
|
||||
float prevAcc[3] = {0.0f, -1.0f, 0.0f};
|
||||
float prevFsAcc[3] = {0.0f, -1.0f, 0.0f};
|
||||
bool prevDpdValid = false;
|
||||
float prevPos[2] = {};
|
||||
float prevHorizon[2] = {1.0f, 0.0f};
|
||||
float prevDist = 0.0f;
|
||||
};
|
||||
|
||||
std::array<ChannelState, 4> g_channels{};
|
||||
@@ -74,6 +89,17 @@ void WriteVec3(uint32_t addr, const float* v) {
|
||||
Memory::WriteFloat32(addr + 8, v[2]);
|
||||
}
|
||||
|
||||
// Writes two big-endian floats to guest memory.
|
||||
void WriteVec2(uint32_t addr, const float* v) {
|
||||
Memory::WriteFloat32(addr, v[0]);
|
||||
Memory::WriteFloat32(addr + 4, v[1]);
|
||||
}
|
||||
|
||||
// Whether the game has the pointer switched on for `chan` (see kKpadDpdEnabled).
|
||||
bool DpdEnabled(uint32_t chan) {
|
||||
return Memory::Read8(kKpadWorkBase + chan * kKpadWorkSize + kKpadDpdEnabled) != 0;
|
||||
}
|
||||
|
||||
// Zeroes `count` consecutive floats in guest memory.
|
||||
void WriteZeroFloats(uint32_t addr, uint32_t count) {
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
@@ -107,10 +133,44 @@ int32_t WriteStatus(uint32_t chan, uint32_t addr, const WiiRemoteInput::KpadSamp
|
||||
Memory::WriteFloat32(addr + kAccSpeed, Distance(sample->acc, state.prevAcc));
|
||||
for (int i = 0; i < 3; ++i) state.prevAcc[i] = sample->acc[i];
|
||||
|
||||
// No IR pointer: pos .. acc_vertical zeroed and dpd_valid_fg clear, which
|
||||
// the game treats as "pointing away from the screen".
|
||||
// IR pointer. Without one, pos .. acc_vertical are zeroed and dpd_valid_fg
|
||||
// is clear, which the game treats as "pointing away from the screen".
|
||||
// vec, hori_vec and dist_vec are the frame-to-frame changes, the speeds
|
||||
// their lengths, as the SDK derives them. acc_vertical stays zero.
|
||||
if (sample->dpdValid && DpdEnabled(chan)) {
|
||||
if (!state.prevDpdValid) {
|
||||
state.prevPos[0] = sample->pos[0];
|
||||
state.prevPos[1] = sample->pos[1];
|
||||
state.prevHorizon[0] = sample->horizon[0];
|
||||
state.prevHorizon[1] = sample->horizon[1];
|
||||
state.prevDist = sample->dist;
|
||||
}
|
||||
const float vec[2] = {sample->pos[0] - state.prevPos[0], sample->pos[1] - state.prevPos[1]};
|
||||
const float horiVec[2] = {sample->horizon[0] - state.prevHorizon[0],
|
||||
sample->horizon[1] - state.prevHorizon[1]};
|
||||
const float distVec = sample->dist - state.prevDist;
|
||||
WriteVec2(addr + kPos, sample->pos);
|
||||
WriteVec2(addr + kVec, vec);
|
||||
Memory::WriteFloat32(addr + kSpeed, std::hypot(vec[0], vec[1]));
|
||||
WriteVec2(addr + kHorizon, sample->horizon);
|
||||
WriteVec2(addr + kHoriVec, horiVec);
|
||||
Memory::WriteFloat32(addr + kHoriSpeed, std::hypot(horiVec[0], horiVec[1]));
|
||||
Memory::WriteFloat32(addr + kDist, sample->dist);
|
||||
Memory::WriteFloat32(addr + kDistVec, distVec);
|
||||
Memory::WriteFloat32(addr + kDistSpeed, std::fabs(distVec));
|
||||
WriteZeroFloats(addr + kAccVertical, 2);
|
||||
Memory::Write8(addr + kDpdValidFg, kDpdValidTwoDots);
|
||||
state.prevDpdValid = true;
|
||||
state.prevPos[0] = sample->pos[0];
|
||||
state.prevPos[1] = sample->pos[1];
|
||||
state.prevHorizon[0] = sample->horizon[0];
|
||||
state.prevHorizon[1] = sample->horizon[1];
|
||||
state.prevDist = sample->dist;
|
||||
} else {
|
||||
WriteZeroFloats(addr + kPos, (kAccVertical + 8 - kPos) / 4);
|
||||
Memory::Write8(addr + kDpdValidFg, 0);
|
||||
state.prevDpdValid = false;
|
||||
}
|
||||
|
||||
const uint8_t devType = sample->hasClassic ? kDevClassic : sample->hasNunchuk ? kDevFreestyle : kDevCore;
|
||||
const uint8_t dataFormat =
|
||||
@@ -170,7 +230,9 @@ void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample)
|
||||
Memory::Write16(addr + kUAccX, RawAcc(-sample->acc[0]));
|
||||
Memory::Write16(addr + kUAccY, RawAcc(sample->acc[2]));
|
||||
Memory::Write16(addr + kUAccZ, RawAcc(-sample->acc[1]));
|
||||
// No IR: every DPDObject invalid (x/y at the sensor's out-of-range value).
|
||||
// No raw IR: every DPDObject invalid (x/y at the sensor's out-of-range
|
||||
// value). The VR pointer is only synthesised at the KPADStatus level, which
|
||||
// is where the game reads it; no camera dots are invented for it here.
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
Memory::Write16(addr + kUObj + i * 8, 0x3FF);
|
||||
Memory::Write16(addr + kUObj + i * 8 + 2, 0x3FF);
|
||||
|
||||
@@ -94,6 +94,12 @@ extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled)
|
||||
g_rumbleEnabled.store(enabled, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// The same switch, for the Wii Remote motor the VR controllers stand in for.
|
||||
extern "C" bool PAD_HLE_RumbleEnabled()
|
||||
{
|
||||
return g_rumbleEnabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
extern "C" uint32_t PAD__Init_HLE()
|
||||
{
|
||||
return PADInit() ? 1u : 0u;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
|
||||
extern "C" bool PAD_HLE_RumbleEnabled();
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kDefaultWorkMemSize = 0x20000;
|
||||
constexpr uint32_t kWpadMotorRumble = 1; // WPAD_MOTOR_RUMBLE; WPAD_MOTOR_STOP is 0
|
||||
constexpr uint8_t kDefaultDpdSensitivity = 3;
|
||||
constexpr int32_t kStatusOk = 0;
|
||||
|
||||
@@ -141,10 +144,13 @@ extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr)
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0990, WPADProbe_HLE, int32_t, (uint32_t chan, uint32_t typePtr), (chan, typePtr));
|
||||
|
||||
// WPADControlMotor: a Bluetooth remote's motor is not driven, but VR controllers
|
||||
// standing in for a remote rumble both hands while the game holds it on.
|
||||
extern "C" void WPADControlMotor_HLE(uint32_t chan, uint32_t command)
|
||||
{
|
||||
(void)chan;
|
||||
(void)command;
|
||||
if (chan < WpadContract::kChannelCount && WiiRemoteInput::IsVrControllerChannel(chan)) {
|
||||
mkw::vr::OpenXRSetWiiRemoteRumble(command == kWpadMotorRumble && PAD_HLE_RumbleEnabled());
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0EC4, WPADControlMotor_HLE, void, (uint32_t chan, uint32_t command), (chan, command));
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
#include "vr/openxr_integration.h"
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -144,6 +145,21 @@ int g_vrMirrorView = [] {
|
||||
}
|
||||
return 0;
|
||||
}();
|
||||
// Config spellings and menu labels for the VR controllers, index-matched to
|
||||
// mkw::vr::OpenXRControllerMode.
|
||||
constexpr std::array<const char*, 2> kVrControllerModeNames{"wii_remote", "gamepad"};
|
||||
constexpr std::array<const char*, 2> kVrControllerModeLabels{"Wii Remote + Nunchuk", "Gamepad"};
|
||||
static_assert(static_cast<int>(mkw::vr::OpenXRControllerMode::WiiRemote) == 0);
|
||||
static_assert(static_cast<int>(mkw::vr::OpenXRControllerMode::Gamepad) == 1);
|
||||
int g_vrControllerMode = [] {
|
||||
const std::string mode = RuntimeConfigFile::VrControllerMode();
|
||||
for (size_t i = 0; i < kVrControllerModeNames.size(); ++i) {
|
||||
if (mode == kVrControllerModeNames[i]) {
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}();
|
||||
constexpr std::array<const char*, 3> kVrFirstPersonRotationNames{"yaw", "yaw_pitch", "full"};
|
||||
int g_vrFirstPersonRotation = [] {
|
||||
const std::string mode = RuntimeConfigFile::VrFirstPersonRotation();
|
||||
@@ -542,6 +558,7 @@ void DrawRumbleSettings() {
|
||||
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD,
|
||||
};
|
||||
PADControlAllMotors(stopAll.data());
|
||||
mkw::vr::OpenXRSetWiiRemoteRumble(false);
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
@@ -936,6 +953,33 @@ void DrawVrSettings() {
|
||||
"and None leaves the window black. Menus reach the headset as a screen showing this "
|
||||
"same desktop image, so the eye choices only differ from Normal during a race.");
|
||||
}
|
||||
if (ImGui::Combo("VR controllers", &g_vrControllerMode, kVrControllerModeLabels.data(),
|
||||
static_cast<int>(kVrControllerModeLabels.size()))) {
|
||||
mkw::vr::OpenXRSetControllerMode(static_cast<mkw::vr::OpenXRControllerMode>(g_vrControllerMode));
|
||||
RuntimeConfigFile::SetVrControllerMode(kVrControllerModeNames[static_cast<size_t>(g_vrControllerMode)]);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
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, stick left/right = -/+\n"
|
||||
" Left: stick = Nunchuk stick, trigger = Z, grip = C, menu = HOME\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.");
|
||||
}
|
||||
if (mkw::vr::OpenXRIsRunning() &&
|
||||
mkw::vr::OpenXRGetControllerMode() == mkw::vr::OpenXRControllerMode::WiiRemote) {
|
||||
mkw::vr::OpenXRWiiRemoteSample remote;
|
||||
if (mkw::vr::OpenXRReadWiiRemote(remote)) {
|
||||
if (remote.pointer_valid) {
|
||||
ImGui::TextDisabled("Pointer %+.2f %+.2f | Remote %+.2f %+.2f %+.2f g", remote.pointer[0],
|
||||
remote.pointer[1], remote.acc[0], remote.acc[1], remote.acc[2]);
|
||||
} else {
|
||||
ImGui::TextDisabled("Pointer off screen | Remote %+.2f %+.2f %+.2f g", remote.acc[0],
|
||||
remote.acc[1], remote.acc[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::Combo("VR frame interpolation (experimental)", &g_vrFrameInterpolationMode,
|
||||
kVrInterpolationLabels.data(), static_cast<int>(kVrInterpolationLabels.size()))) {
|
||||
@@ -1398,6 +1442,7 @@ void InitializeRuntimeSettings() noexcept {
|
||||
aurora_set_stereo_stop_at_display_copy(g_vrStopAtDisplayCopy);
|
||||
aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears);
|
||||
aurora_set_stereo_mirror_view(static_cast<AuroraStereoMirrorView>(g_vrMirrorView));
|
||||
mkw::vr::OpenXRSetControllerMode(static_cast<mkw::vr::OpenXRControllerMode>(g_vrControllerMode));
|
||||
ApplyVrHudVirtualScreen();
|
||||
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
|
||||
+334
-42
@@ -2,6 +2,13 @@
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR)
|
||||
|
||||
#if defined(_WIN32)
|
||||
#if !defined(NOMINMAX)
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#include "vr/openxr_input.h"
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
@@ -13,11 +20,14 @@
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <sys/system_properties.h>
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
namespace mkw::vr {
|
||||
@@ -28,8 +38,10 @@ namespace {
|
||||
// a menu can be reached without someone wearing the headset:
|
||||
// adb shell setprop debug.wiicompiled.inject <sequence>:<button>
|
||||
// A new sequence number holds the button for kInjectHoldFrames XR frames.
|
||||
// Buttons: a, b, x, y, start, up, down, left, right. The property is unset in
|
||||
// normal use, so this costs one property read every few frames.
|
||||
// Buttons: a, b, x, y, start, up, down, left, right, and for the Wii Remote
|
||||
// presentation also home, c and z (x/y/start press 1/2/+ there, and the
|
||||
// directions push the Nunchuk stick). The property is unset in normal use, so
|
||||
// this costs one property read every few frames.
|
||||
constexpr uint32_t kInjectHoldFrames = 12;
|
||||
constexpr uint32_t kInjectPollFrames = 4;
|
||||
|
||||
@@ -77,13 +89,23 @@ bool Injected(const char* button) {
|
||||
const InjectedPress& press = Injection();
|
||||
return press.frames_left > 0 && press.button == button;
|
||||
}
|
||||
|
||||
using ConvertNowToXrTime = XrResult(XRAPI_PTR*)(XrInstance, const struct timespec*, XrTime*);
|
||||
#else
|
||||
void PollInjection() {}
|
||||
bool Injected(const char*) { return false; }
|
||||
|
||||
#if defined(_WIN32)
|
||||
using ConvertNowToXrTime = XrResult(XRAPI_PTR*)(XrInstance, const LARGE_INTEGER*, XrTime*);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
constexpr uint32_t kHandCount = 2;
|
||||
|
||||
// Re-sent every frame while the game holds the motor on, so a rumble whose stop
|
||||
// never arrives (or a stalled pacing thread) dies out on its own.
|
||||
constexpr XrDuration kRumblePulseNs = 50'000'000;
|
||||
|
||||
struct Binding {
|
||||
XrAction* action;
|
||||
const char* path;
|
||||
@@ -100,6 +122,30 @@ Sint16 ToTrigger(float value) noexcept {
|
||||
return static_cast<Sint16>(std::lround(clamped * 32767.0f));
|
||||
}
|
||||
|
||||
wii_remote::Pose ToWiiRemotePose(const XrPosef& pose) noexcept {
|
||||
return {{pose.position.x, pose.position.y, pose.position.z},
|
||||
{pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w}};
|
||||
}
|
||||
|
||||
// The adb injection's buttons as the Wii Remote presentation's WPAD bits.
|
||||
uint32_t InjectedWiiRemoteButtons() {
|
||||
uint32_t hold = 0;
|
||||
const auto press = [&hold](const char* name, uint32_t bit) {
|
||||
if (Injected(name)) {
|
||||
hold |= bit;
|
||||
}
|
||||
};
|
||||
press("a", wii_remote::kButtonA);
|
||||
press("b", wii_remote::kButtonB);
|
||||
press("x", wii_remote::kButtonOne);
|
||||
press("y", wii_remote::kButtonTwo);
|
||||
press("start", wii_remote::kButtonPlus);
|
||||
press("home", wii_remote::kButtonHome);
|
||||
press("c", wii_remote::kButtonC);
|
||||
press("z", wii_remote::kButtonZ);
|
||||
return hold;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OpenXRInput::OpenXRInput(OpenXRLogCallback logger) : m_logger(std::move(logger)) {}
|
||||
@@ -139,11 +185,15 @@ bool OpenXRInput::Create(OpenXRRuntime& runtime) {
|
||||
return false;
|
||||
}
|
||||
m_created = true;
|
||||
CreatePoseSpaces();
|
||||
LoadInputClock();
|
||||
if (!AttachVirtualGamepad()) {
|
||||
Log(OpenXRLogLevel::Warning,
|
||||
"SDL refused the virtual gamepad; OpenXR controllers will not reach the game");
|
||||
}
|
||||
Log(OpenXRLogLevel::Info, "OpenXR controller actions attached");
|
||||
Log(OpenXRLogLevel::Info, OpenXRGetControllerMode() == OpenXRControllerMode::WiiRemote
|
||||
? "OpenXR controller actions attached (Wii Remote + Nunchuk)"
|
||||
: "OpenXR controller actions attached (gamepad)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -162,7 +212,7 @@ bool OpenXRInput::CreateActions() {
|
||||
const char* localized;
|
||||
XrActionType type;
|
||||
};
|
||||
const std::array<Spec, 8> specs{{
|
||||
const std::array<Spec, 10> specs{{
|
||||
{&m_thumbstick, "thumbstick", "Thumbstick", XR_ACTION_TYPE_VECTOR2F_INPUT},
|
||||
{&m_thumbstick_click, "thumbstick_click", "Thumbstick Click", XR_ACTION_TYPE_BOOLEAN_INPUT},
|
||||
{&m_trigger, "trigger", "Trigger", XR_ACTION_TYPE_FLOAT_INPUT},
|
||||
@@ -170,6 +220,8 @@ bool OpenXRInput::CreateActions() {
|
||||
{&m_button_primary, "button_primary", "A / X", XR_ACTION_TYPE_BOOLEAN_INPUT},
|
||||
{&m_button_secondary, "button_secondary", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT},
|
||||
{&m_menu, "menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT},
|
||||
{&m_aim_pose, "aim_pose", "Pointer", XR_ACTION_TYPE_POSE_INPUT},
|
||||
{&m_grip_pose, "grip_pose", "Motion", XR_ACTION_TYPE_POSE_INPUT},
|
||||
{&m_haptic, "haptic", "Haptic", XR_ACTION_TYPE_VIBRATION_OUTPUT},
|
||||
}};
|
||||
for (const Spec& spec : specs) {
|
||||
@@ -235,17 +287,26 @@ bool OpenXRInput::SuggestBindings() {
|
||||
{&m_button_secondary, "/user/hand/left/input/y/click"},
|
||||
{&m_button_secondary, "/user/hand/right/input/b/click"},
|
||||
{&m_menu, "/user/hand/left/input/menu/click"},
|
||||
{&m_aim_pose, "/user/hand/left/input/aim/pose"},
|
||||
{&m_aim_pose, "/user/hand/right/input/aim/pose"},
|
||||
{&m_grip_pose, "/user/hand/left/input/grip/pose"},
|
||||
{&m_grip_pose, "/user/hand/right/input/grip/pose"},
|
||||
{&m_haptic, "/user/hand/left/output/haptic"},
|
||||
{&m_haptic, "/user/hand/right/output/haptic"},
|
||||
};
|
||||
if (!suggest("/interaction_profiles/oculus/touch_controller", touch, true)) {
|
||||
return false;
|
||||
}
|
||||
// Minimal fallback so an unfamiliar runtime still offers a select and a menu.
|
||||
// Minimal fallback so an unfamiliar runtime still offers a select, a menu
|
||||
// and something to point with.
|
||||
const std::vector<Binding> simple{
|
||||
{&m_button_primary, "/user/hand/right/input/select/click"},
|
||||
{&m_button_secondary, "/user/hand/left/input/select/click"},
|
||||
{&m_menu, "/user/hand/left/input/menu/click"},
|
||||
{&m_aim_pose, "/user/hand/left/input/aim/pose"},
|
||||
{&m_aim_pose, "/user/hand/right/input/aim/pose"},
|
||||
{&m_grip_pose, "/user/hand/left/input/grip/pose"},
|
||||
{&m_grip_pose, "/user/hand/right/input/grip/pose"},
|
||||
{&m_haptic, "/user/hand/left/output/haptic"},
|
||||
{&m_haptic, "/user/hand/right/output/haptic"},
|
||||
};
|
||||
@@ -253,6 +314,92 @@ bool OpenXRInput::SuggestBindings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenXRInput::CreatePoseSpaces() {
|
||||
bool logged = false;
|
||||
for (uint32_t hand = 0; hand < kHandCount; ++hand) {
|
||||
for (auto [action, spaces] : {std::pair{m_aim_pose, m_aim_spaces}, std::pair{m_grip_pose, m_grip_spaces}}) {
|
||||
XrActionSpaceCreateInfo info{XR_TYPE_ACTION_SPACE_CREATE_INFO};
|
||||
info.action = action;
|
||||
info.subactionPath = m_hand_paths[hand];
|
||||
info.poseInActionSpace.orientation.w = 1.0f;
|
||||
const XrResult result = xrCreateActionSpace(m_runtime->Session(), &info, &spaces[hand]);
|
||||
m_runtime->ObserveResult(result);
|
||||
if (XR_FAILED(result)) {
|
||||
spaces[hand] = XR_NULL_HANDLE;
|
||||
if (!logged) {
|
||||
logged = true;
|
||||
std::ostringstream message;
|
||||
message << "xrCreateActionSpace failed (" << result
|
||||
<< "); the Wii Remote will have no motion or pointer";
|
||||
Log(OpenXRLogLevel::Warning, message.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpenXRInput::DestroyPoseSpaces() {
|
||||
for (uint32_t hand = 0; hand < kHandCount; ++hand) {
|
||||
for (XrSpace* space : {&m_aim_spaces[hand], &m_grip_spaces[hand]}) {
|
||||
if (*space != XR_NULL_HANDLE) {
|
||||
xrDestroySpace(*space);
|
||||
*space = XR_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poses for input are located at the measured current time, not the frame's
|
||||
// predicted display time: that lies tens of milliseconds ahead, and the runtime
|
||||
// extrapolates a fast wrist turn that far past where the hand really is, which
|
||||
// sprays the pointer and invents acceleration (DolphinXR's fast-motion fix).
|
||||
void OpenXRInput::LoadInputClock() {
|
||||
const auto& extensions = m_runtime->EnabledExtensions();
|
||||
const auto enabled = [&](const char* name) {
|
||||
return std::find(extensions.begin(), extensions.end(), name) != extensions.end();
|
||||
};
|
||||
PFN_xrVoidFunction function = nullptr;
|
||||
#if defined(_WIN32)
|
||||
if (enabled("XR_KHR_win32_convert_performance_counter_time")) {
|
||||
m_runtime->GetInstanceProcAddress("xrConvertWin32PerformanceCounterToTimeKHR", &function);
|
||||
}
|
||||
#elif defined(__ANDROID__)
|
||||
if (enabled("XR_KHR_convert_timespec_time")) {
|
||||
m_runtime->GetInstanceProcAddress("xrConvertTimespecTimeToTimeKHR", &function);
|
||||
}
|
||||
#else
|
||||
(void)enabled;
|
||||
#endif
|
||||
m_convert_now_to_xr_time = function;
|
||||
if (m_convert_now_to_xr_time == nullptr) {
|
||||
Log(OpenXRLogLevel::Info,
|
||||
"OpenXR offers no clock conversion; controller motion is sampled at display time");
|
||||
}
|
||||
}
|
||||
|
||||
XrTime OpenXRInput::InputSampleTime(XrTime predicted_display_time) const {
|
||||
if (m_convert_now_to_xr_time == nullptr) {
|
||||
return predicted_display_time;
|
||||
}
|
||||
XrTime now = 0;
|
||||
#if defined(_WIN32)
|
||||
LARGE_INTEGER counter{};
|
||||
if (QueryPerformanceCounter(&counter) == 0 ||
|
||||
XR_FAILED(reinterpret_cast<ConvertNowToXrTime>(m_convert_now_to_xr_time)(m_runtime->Instance(),
|
||||
&counter, &now))) {
|
||||
return predicted_display_time;
|
||||
}
|
||||
#elif defined(__ANDROID__)
|
||||
timespec spec{};
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &spec) != 0 ||
|
||||
XR_FAILED(reinterpret_cast<ConvertNowToXrTime>(m_convert_now_to_xr_time)(m_runtime->Instance(),
|
||||
&spec, &now))) {
|
||||
return predicted_display_time;
|
||||
}
|
||||
#endif
|
||||
return now > 0 ? (std::min)(predicted_display_time, now) : predicted_display_time;
|
||||
}
|
||||
|
||||
bool OpenXRInput::AttachVirtualGamepad() {
|
||||
SDL_VirtualJoystickDesc desc;
|
||||
SDL_INIT_INTERFACE(&desc);
|
||||
@@ -299,7 +446,13 @@ void OpenXRInput::DetachVirtualGamepad() {
|
||||
}
|
||||
|
||||
void OpenXRInput::Destroy() {
|
||||
// The game must stop reading a remote whose controllers are going away.
|
||||
OpenXRWithdrawWiiRemote();
|
||||
if (m_created) {
|
||||
StopRumble();
|
||||
}
|
||||
DetachVirtualGamepad();
|
||||
DestroyPoseSpaces();
|
||||
if (m_action_set != XR_NULL_HANDLE) {
|
||||
// Destroying the set destroys every action created from it.
|
||||
xrDestroyActionSet(m_action_set);
|
||||
@@ -307,13 +460,47 @@ void OpenXRInput::Destroy() {
|
||||
}
|
||||
m_thumbstick = m_thumbstick_click = m_trigger = m_squeeze = XR_NULL_HANDLE;
|
||||
m_button_primary = m_button_secondary = m_menu = m_haptic = XR_NULL_HANDLE;
|
||||
m_aim_pose = m_grip_pose = XR_NULL_HANDLE;
|
||||
m_hand_paths[0] = m_hand_paths[1] = XR_NULL_PATH;
|
||||
m_convert_now_to_xr_time = nullptr;
|
||||
for (auto& motion : m_motion) {
|
||||
motion.Rest();
|
||||
}
|
||||
m_pointer.Reset();
|
||||
m_horizon = {1.0f, 0.0f};
|
||||
m_created = false;
|
||||
m_runtime = nullptr;
|
||||
}
|
||||
|
||||
void OpenXRInput::Sync(XrTime) {
|
||||
if (!m_created || m_runtime == nullptr || !m_runtime->IsSessionFocused()) {
|
||||
void OpenXRInput::Idle() {
|
||||
if (!m_created) {
|
||||
return;
|
||||
}
|
||||
for (auto& motion : m_motion) {
|
||||
motion.Rest();
|
||||
}
|
||||
m_pointer.Reset();
|
||||
m_horizon = {1.0f, 0.0f};
|
||||
OpenXRPublishWiiRemote(m_joystick_id, OpenXRWiiRemoteSample{});
|
||||
StopRumble();
|
||||
// Nothing stays held on the gamepad either while input is away.
|
||||
if (m_joystick != nullptr) {
|
||||
auto* joystick = static_cast<SDL_Joystick*>(m_joystick);
|
||||
for (int axis = 0; axis < SDL_GAMEPAD_AXIS_COUNT; ++axis) {
|
||||
SDL_SetJoystickVirtualAxis(joystick, axis, 0);
|
||||
}
|
||||
for (int button = 0; button < SDL_GAMEPAD_BUTTON_COUNT; ++button) {
|
||||
SDL_SetJoystickVirtualButton(joystick, button, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen) {
|
||||
if (!m_created || m_runtime == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!m_runtime->IsSessionFocused()) {
|
||||
Idle();
|
||||
return;
|
||||
}
|
||||
XrActiveActionSet active{m_action_set, XR_NULL_PATH};
|
||||
@@ -329,12 +516,9 @@ void OpenXRInput::Sync(XrTime) {
|
||||
message << "xrSyncActions failed (" << result << ')';
|
||||
Log(OpenXRLogLevel::Warning, message.str());
|
||||
}
|
||||
Idle();
|
||||
return;
|
||||
}
|
||||
if (m_joystick == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto* joystick = static_cast<SDL_Joystick*>(m_joystick);
|
||||
|
||||
const auto boolean = [&](XrAction action, uint32_t hand) {
|
||||
XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
@@ -367,39 +551,147 @@ void OpenXRInput::Sync(XrTime) {
|
||||
return state.currentState;
|
||||
};
|
||||
|
||||
PollInjection();
|
||||
XrVector2f left = vector(m_thumbstick, 0);
|
||||
const XrVector2f right = vector(m_thumbstick, 1);
|
||||
if (Injected("up")) {
|
||||
left.y = 1.0f;
|
||||
} else if (Injected("down")) {
|
||||
left.y = -1.0f;
|
||||
} else if (Injected("left")) {
|
||||
left.x = -1.0f;
|
||||
} else if (Injected("right")) {
|
||||
left.x = 1.0f;
|
||||
std::array<wii_remote::HandInputs, kHands> hands{};
|
||||
for (uint32_t hand = 0; hand < kHands; ++hand) {
|
||||
wii_remote::HandInputs& inputs = hands[hand];
|
||||
inputs.primary = boolean(m_button_primary, hand);
|
||||
inputs.secondary = boolean(m_button_secondary, hand);
|
||||
inputs.menu = boolean(m_menu, hand);
|
||||
inputs.thumbstick_click = boolean(m_thumbstick_click, hand);
|
||||
inputs.trigger = scalar(m_trigger, hand);
|
||||
inputs.squeeze = scalar(m_squeeze, hand);
|
||||
const XrVector2f stick = vector(m_thumbstick, hand);
|
||||
inputs.stick_x = stick.x;
|
||||
inputs.stick_y = stick.y;
|
||||
}
|
||||
// OpenXR thumbsticks report +Y up; SDL gamepads report +Y down.
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFTX, ToAxis(left.x));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFTY, ToAxis(-left.y));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHTX, ToAxis(right.x));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHTY, ToAxis(-right.y));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, ToTrigger(scalar(m_trigger, 0)));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, ToTrigger(scalar(m_trigger, 1)));
|
||||
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_SOUTH,
|
||||
boolean(m_button_primary, 1) || Injected("a"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_EAST,
|
||||
boolean(m_button_secondary, 1) || Injected("b"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_WEST,
|
||||
boolean(m_button_primary, 0) || Injected("x"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_NORTH,
|
||||
boolean(m_button_secondary, 0) || Injected("y"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_START, boolean(m_menu, 0) || Injected("start"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, boolean(m_thumbstick_click, 0));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, boolean(m_thumbstick_click, 1));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, scalar(m_squeeze, 0) > 0.5f);
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, scalar(m_squeeze, 1) > 0.5f);
|
||||
PollInjection();
|
||||
wii_remote::HandInputs& left = hands[0];
|
||||
const wii_remote::HandInputs& right = hands[1];
|
||||
if (Injected("up")) {
|
||||
left.stick_y = 1.0f;
|
||||
} else if (Injected("down")) {
|
||||
left.stick_y = -1.0f;
|
||||
} else if (Injected("left")) {
|
||||
left.stick_x = -1.0f;
|
||||
} else if (Injected("right")) {
|
||||
left.stick_x = 1.0f;
|
||||
}
|
||||
|
||||
if (m_joystick != nullptr) {
|
||||
auto* joystick = static_cast<SDL_Joystick*>(m_joystick);
|
||||
// OpenXR thumbsticks report +Y up; SDL gamepads report +Y down.
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFTX, ToAxis(left.stick_x));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFTY, ToAxis(-left.stick_y));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHTX, ToAxis(right.stick_x));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHTY, ToAxis(-right.stick_y));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, ToTrigger(left.trigger));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, ToTrigger(right.trigger));
|
||||
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_SOUTH, right.primary || Injected("a"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_EAST, right.secondary || Injected("b"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_WEST, left.primary || Injected("x"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_NORTH, left.secondary || Injected("y"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_START, left.menu || Injected("start"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, left.thumbstick_click);
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, right.thumbstick_click);
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, left.squeeze > 0.5f);
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, right.squeeze > 0.5f);
|
||||
}
|
||||
|
||||
PublishWiiRemote(predicted_display_time, screen, hands, InjectedWiiRemoteButtons());
|
||||
UpdateRumble();
|
||||
}
|
||||
|
||||
void OpenXRInput::PublishWiiRemote(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
|
||||
const std::array<wii_remote::HandInputs, kHands>& hands,
|
||||
uint32_t injected_buttons) {
|
||||
constexpr XrSpaceLocationFlags kPoseValid =
|
||||
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
||||
const XrTime input_time = InputSampleTime(predicted_display_time);
|
||||
|
||||
OpenXRWiiRemoteSample sample{};
|
||||
sample.hold = wii_remote::RemoteButtons(hands[0], hands[1]) | injected_buttons;
|
||||
sample.stick = wii_remote::NunchukStick(hands[0]);
|
||||
|
||||
// Left is the Nunchuk, right is the remote.
|
||||
std::array<wii_remote::Pose, kHands> aims{};
|
||||
std::array<bool, kHands> aim_valid{};
|
||||
for (uint32_t hand = 0; hand < kHands; ++hand) {
|
||||
if (m_aim_spaces[hand] != XR_NULL_HANDLE) {
|
||||
XrSpaceLocation location{XR_TYPE_SPACE_LOCATION};
|
||||
if (XR_SUCCEEDED(xrLocateSpace(m_aim_spaces[hand], m_runtime->AppSpace(), input_time, &location)) &&
|
||||
(location.locationFlags & kPoseValid) == kPoseValid) {
|
||||
aims[hand] = ToWiiRemotePose(location.pose);
|
||||
aim_valid[hand] = true;
|
||||
}
|
||||
}
|
||||
wii_remote::Vec3 grip_position{};
|
||||
wii_remote::Vec3 grip_velocity{};
|
||||
bool position_valid = false;
|
||||
bool velocity_valid = false;
|
||||
if (m_grip_spaces[hand] != XR_NULL_HANDLE) {
|
||||
XrSpaceVelocity velocity{XR_TYPE_SPACE_VELOCITY};
|
||||
XrSpaceLocation location{XR_TYPE_SPACE_LOCATION};
|
||||
location.next = &velocity;
|
||||
if (XR_SUCCEEDED(xrLocateSpace(m_grip_spaces[hand], m_runtime->AppSpace(), input_time, &location))) {
|
||||
position_valid = (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0;
|
||||
velocity_valid = (velocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT) != 0;
|
||||
grip_position = {location.pose.position.x, location.pose.position.y, location.pose.position.z};
|
||||
grip_velocity = {velocity.linearVelocity.x, velocity.linearVelocity.y, velocity.linearVelocity.z};
|
||||
}
|
||||
}
|
||||
const wii_remote::Vec3 acc =
|
||||
m_motion[hand].Update(aim_valid[hand] ? &aims[hand].orientation : nullptr,
|
||||
position_valid ? &grip_position : nullptr,
|
||||
velocity_valid ? &grip_velocity : nullptr, input_time);
|
||||
(hand == 0 ? sample.nunchuk_acc : sample.acc) = acc;
|
||||
}
|
||||
|
||||
wii_remote::Screen target{};
|
||||
wii_remote::ScreenHit hit{};
|
||||
if (screen.valid && aim_valid[1]) {
|
||||
target.pose = ToWiiRemotePose(screen.pose);
|
||||
target.half_width = screen.half_width_meters;
|
||||
target.half_height = screen.half_height_meters;
|
||||
hit = wii_remote::RaycastScreen(aims[1], target);
|
||||
}
|
||||
if (screen.valid && aim_valid[1]) {
|
||||
m_horizon = wii_remote::Horizon(aims[1], target);
|
||||
}
|
||||
const wii_remote::ScreenHit pointer = m_pointer.Update(hit, input_time);
|
||||
if (pointer.valid) {
|
||||
sample.pointer_valid = true;
|
||||
sample.pointer = wii_remote::KpadPosition(pointer);
|
||||
// Held with the position through a tracking blip.
|
||||
sample.horizon = m_horizon;
|
||||
sample.distance_meters = pointer.distance_meters;
|
||||
if (!m_logged_pointer) {
|
||||
m_logged_pointer = true;
|
||||
Log(OpenXRLogLevel::Info, "OpenXR Wii Remote pointer reached the virtual screen");
|
||||
}
|
||||
}
|
||||
OpenXRPublishWiiRemote(m_joystick_id, sample);
|
||||
}
|
||||
|
||||
void OpenXRInput::UpdateRumble() {
|
||||
if (!OpenXRWiiRemoteRumbleRequested() || !OpenXRWiiRemoteOwnsGamepad(m_joystick_id)) {
|
||||
StopRumble();
|
||||
return;
|
||||
}
|
||||
for (uint32_t hand = 0; hand < kHandCount; ++hand) {
|
||||
ApplyHaptic(hand, 1.0f, kRumblePulseNs);
|
||||
m_haptics_active[hand] = true;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenXRInput::StopRumble() {
|
||||
for (uint32_t hand = 0; hand < kHandCount; ++hand) {
|
||||
if (m_haptics_active[hand]) {
|
||||
ApplyHaptic(hand, 0.0f, 0);
|
||||
m_haptics_active[hand] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpenXRInput::ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration) {
|
||||
|
||||
@@ -595,6 +595,9 @@ private:
|
||||
break;
|
||||
}
|
||||
if (!session_active) {
|
||||
if (input_ != nullptr) {
|
||||
input_->Idle();
|
||||
}
|
||||
SetInterpolationActive(false);
|
||||
interpolation_pacing_.Reset();
|
||||
rendered_fps_.store(0, std::memory_order_relaxed);
|
||||
@@ -652,13 +655,15 @@ private:
|
||||
}
|
||||
|
||||
UpdateFrameTiming(frame.xr_frame);
|
||||
if (input_ != nullptr) {
|
||||
input_->Sync(frame.xr_frame.predicted_display_time);
|
||||
}
|
||||
// Both of these read this frame's located head pose and must run
|
||||
// before FinishFrame submits a layer built from it.
|
||||
ServiceRecenterRequest();
|
||||
UpdateVirtualScreenPose(frame);
|
||||
if (input_ != nullptr) {
|
||||
// After the screen is placed, so the pointer aims at this
|
||||
// frame's screen rather than the previous one's.
|
||||
input_->Sync(frame.xr_frame.predicted_display_time, PointerScreen(frame, policy, immersive));
|
||||
}
|
||||
|
||||
if (!frame.expects_gpu_submission) {
|
||||
if (!backend_->FinishFrame(frame, false)) {
|
||||
@@ -840,6 +845,89 @@ private:
|
||||
frame.presentation.quad_pose = virtual_screen_pose_;
|
||||
}
|
||||
|
||||
// The rectangle the game picture covers on the screen this frame shows, in
|
||||
// the application space, for the Wii Remote pointer to aim at.
|
||||
//
|
||||
// Menus: the quad layer UpdateVirtualScreenPose placed (or its head-locked
|
||||
// fallback), sized like the backends size it: hud_width_meters across with
|
||||
// the eye texture's aspect, the desktop snapshot letterboxed into it and the
|
||||
// picture into the snapshot.
|
||||
//
|
||||
// Races: the 2D layer's screen, which Aurora hangs hud_distance_meters
|
||||
// ahead in the recorded centre-eye space. ViewFromBase maps a point p of
|
||||
// that space (in metres) to base + lean * p in the application space, so the
|
||||
// screen sits at base + lean * (0, 0, -distance), turned by the lean, its
|
||||
// height following the picture aspect as stereo_hud_screen's does. With the
|
||||
// 2D layer stretched across the eyes there is no screen to point at.
|
||||
OpenXRPointerScreen PointerScreen(const OpenXRBackendFrame& frame, const MkwVRPolicySnapshot& policy,
|
||||
bool immersive) const noexcept {
|
||||
OpenXRPointerScreen screen{};
|
||||
float picture_aspect = 0.0f;
|
||||
float snapshot_aspect = 0.0f;
|
||||
if (!aurora_get_stereo_screen_aspects(&picture_aspect, &snapshot_aspect)) {
|
||||
return screen;
|
||||
}
|
||||
const OpenXRFrame& xr_frame = frame.xr_frame;
|
||||
const bool views_usable = xr_frame.views_valid &&
|
||||
(xr_frame.view_state_flags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0;
|
||||
const bool position_usable =
|
||||
views_usable && (xr_frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
|
||||
|
||||
if (immersive) {
|
||||
const float distance = policy.config.hud_distance_meters;
|
||||
const float width = policy.config.hud_width_meters;
|
||||
if (!aurora_get_stereo_hud_screen_enabled() || !(distance > 0.0f) || !(width > 0.0f)) {
|
||||
return screen;
|
||||
}
|
||||
std::array<float, 3> base{};
|
||||
if (base_position_valid_ && last_immersive_) {
|
||||
base = base_position_;
|
||||
} else if (position_usable) {
|
||||
// BuildPublishedFrame latches exactly this for the frame.
|
||||
base = CenterPosition(xr_frame);
|
||||
} else {
|
||||
return screen;
|
||||
}
|
||||
const float half_angle =
|
||||
0.5f * lean_back_degrees_.load(std::memory_order_relaxed) * kDegreesToRadians;
|
||||
const Quaternion lean{std::sin(half_angle), 0.0f, 0.0f, std::cos(half_angle)};
|
||||
const std::array<float, 3> ahead = Rotate(lean, {0.0f, 0.0f, -distance});
|
||||
screen.pose.orientation = {lean.x, lean.y, lean.z, lean.w};
|
||||
screen.pose.position = {base[0] + ahead[0], base[1] + ahead[1], base[2] + ahead[2]};
|
||||
screen.half_width_meters = 0.5f * width;
|
||||
screen.half_height_meters = screen.half_width_meters / picture_aspect;
|
||||
screen.valid = true;
|
||||
return screen;
|
||||
}
|
||||
|
||||
if (frame.presentation.mode != OpenXRFrameMode::VirtualScreen || frame.render_width[0] == 0 ||
|
||||
frame.render_height[0] == 0) {
|
||||
return screen;
|
||||
}
|
||||
if (frame.presentation.quad_anchored) {
|
||||
screen.pose = frame.presentation.quad_pose;
|
||||
} else if (position_usable) {
|
||||
// Head-locked in the view space: straight ahead of the head.
|
||||
const auto& head = xr_frame.views[0].pose.orientation;
|
||||
const Quaternion orientation = Normalize({head.x, head.y, head.z, head.w});
|
||||
const std::array<float, 3> center = CenterPosition(xr_frame);
|
||||
const std::array<float, 3> ahead = Rotate(
|
||||
orientation, {0.0f, 0.0f, -std::max(0.25f, frame.presentation.quad_distance_meters)});
|
||||
screen.pose.orientation = {orientation.x, orientation.y, orientation.z, orientation.w};
|
||||
screen.pose.position = {center[0] + ahead[0], center[1] + ahead[1], center[2] + ahead[2]};
|
||||
} else {
|
||||
return screen;
|
||||
}
|
||||
const float eye_aspect =
|
||||
static_cast<float>(frame.render_width[0]) / static_cast<float>(frame.render_height[0]);
|
||||
const std::array<float, 2> extents = wii_remote::MenuPictureHalfExtents(
|
||||
std::max(0.25f, frame.presentation.quad_width_meters), eye_aspect, snapshot_aspect, picture_aspect);
|
||||
screen.half_width_meters = extents[0];
|
||||
screen.half_height_meters = extents[1];
|
||||
screen.valid = true;
|
||||
return screen;
|
||||
}
|
||||
|
||||
void ApplyPendingReferenceSpaceChange(const OpenXRFrame& frame) noexcept {
|
||||
if (runtime_->ConsumeAppSpaceChangesThrough(frame.predicted_display_time)) {
|
||||
ResetTrackingOrigin();
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
namespace mkw::vr {
|
||||
// Named rather than anonymous: runtime sources are unity-built in groups, and
|
||||
// anonymous namespaces from other VR files would share this one's scope.
|
||||
namespace wii_remote_bridge {
|
||||
|
||||
struct PublishedSample {
|
||||
std::mutex mutex;
|
||||
OpenXRWiiRemoteSample sample{};
|
||||
bool available = false;
|
||||
};
|
||||
|
||||
PublishedSample& Published() {
|
||||
static PublishedSample published;
|
||||
return published;
|
||||
}
|
||||
|
||||
std::atomic<OpenXRControllerMode> g_mode{OpenXRControllerMode::WiiRemote};
|
||||
// SDL_JoystickID of the virtual gamepad the samples belong to; 0 when none.
|
||||
std::atomic<uint32_t> g_joystick_id{0};
|
||||
std::atomic<bool> g_rumble{false};
|
||||
|
||||
} // namespace wii_remote_bridge
|
||||
|
||||
void OpenXRSetControllerMode(OpenXRControllerMode mode) noexcept {
|
||||
wii_remote_bridge::g_mode.store(mode, std::memory_order_relaxed);
|
||||
if (mode != OpenXRControllerMode::WiiRemote) {
|
||||
// The game stops addressing the remote's motor once it is gone.
|
||||
wii_remote_bridge::g_rumble.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
OpenXRControllerMode OpenXRGetControllerMode() noexcept {
|
||||
return wii_remote_bridge::g_mode.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool OpenXRWiiRemoteOwnsGamepad(uint32_t sdl_joystick_id) noexcept {
|
||||
return sdl_joystick_id != 0 && OpenXRGetControllerMode() == OpenXRControllerMode::WiiRemote &&
|
||||
wii_remote_bridge::g_joystick_id.load(std::memory_order_relaxed) == sdl_joystick_id;
|
||||
}
|
||||
|
||||
bool OpenXRReadWiiRemote(OpenXRWiiRemoteSample& sample) noexcept {
|
||||
auto& published = wii_remote_bridge::Published();
|
||||
std::lock_guard lock(published.mutex);
|
||||
if (!published.available) {
|
||||
return false;
|
||||
}
|
||||
sample = published.sample;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenXRSetWiiRemoteRumble(bool active) noexcept {
|
||||
wii_remote_bridge::g_rumble.store(active, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void OpenXRPublishWiiRemote(uint32_t sdl_joystick_id, const OpenXRWiiRemoteSample& sample) noexcept {
|
||||
auto& published = wii_remote_bridge::Published();
|
||||
{
|
||||
std::lock_guard lock(published.mutex);
|
||||
published.sample = sample;
|
||||
published.available = true;
|
||||
}
|
||||
wii_remote_bridge::g_joystick_id.store(sdl_joystick_id, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void OpenXRWithdrawWiiRemote() noexcept {
|
||||
wii_remote_bridge::g_joystick_id.store(0, std::memory_order_relaxed);
|
||||
wii_remote_bridge::g_rumble.store(false, std::memory_order_relaxed);
|
||||
auto& published = wii_remote_bridge::Published();
|
||||
std::lock_guard lock(published.mutex);
|
||||
published.available = false;
|
||||
}
|
||||
|
||||
bool OpenXRWiiRemoteRumbleRequested() noexcept {
|
||||
return OpenXRGetControllerMode() == OpenXRControllerMode::WiiRemote &&
|
||||
wii_remote_bridge::g_rumble.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
@@ -530,12 +531,28 @@ Kind KindForName(const char* name) {
|
||||
return Kind::Remote;
|
||||
}
|
||||
|
||||
// True for the OpenXR virtual gamepad while it stands in for a Wii Remote.
|
||||
static bool IsVrControllerGamepad(SDL_Gamepad* gamepad) {
|
||||
return gamepad != nullptr && mkw::vr::OpenXRWiiRemoteOwnsGamepad(SDL_GetGamepadID(gamepad));
|
||||
}
|
||||
|
||||
// Kind of an SDL gamepad: the VR controllers are a remote with a Nunchuk, every
|
||||
// other device is classified by the name SDL gives it.
|
||||
static Kind KindForGamepad(SDL_Gamepad* gamepad) {
|
||||
if (gamepad == nullptr) return Kind::NotWii;
|
||||
if (IsVrControllerGamepad(gamepad)) return Kind::RemoteWithNunchuk;
|
||||
return KindForName(SDL_GetGamepadName(gamepad));
|
||||
}
|
||||
|
||||
// Kind of the SDL gamepad assigned to a game port, NotWii when empty.
|
||||
Kind KindForPort(uint32_t port) {
|
||||
if (port >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(port));
|
||||
if (gamepad == nullptr) return Kind::NotWii;
|
||||
return KindForName(SDL_GetGamepadName(gamepad));
|
||||
return KindForGamepad(SDL_GetGamepadFromPlayerIndex(static_cast<int>(port)));
|
||||
}
|
||||
|
||||
// True when the port's remote is the VR controllers.
|
||||
bool IsVrControllerChannel(uint32_t chan) {
|
||||
return chan < PAD_MAX_CONTROLLERS && IsVrControllerGamepad(SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan)));
|
||||
}
|
||||
|
||||
// Human-readable name of a Kind for the settings overlay.
|
||||
@@ -561,7 +578,7 @@ Kind EffectiveKind(uint32_t chan) {
|
||||
if (chan >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
|
||||
PortMemory& memory = g_ports[chan];
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
|
||||
const Kind live = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
|
||||
const Kind live = KindForGamepad(gamepad);
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
if (live != Kind::NotWii) {
|
||||
memory.lastKind = live;
|
||||
@@ -613,13 +630,47 @@ int16_t ClassicStickRaw(Sint16 axis, bool invert) {
|
||||
return static_cast<int16_t>(std::clamp(std::lround(value * 512.0f), -512L, 511L));
|
||||
}
|
||||
|
||||
// The VR controllers' latest sample as a remote with a Nunchuk. Before the XR
|
||||
// thread has published one the remote is simply at rest.
|
||||
static void ReadVrControllerSample(uint32_t chan, KpadSample& sample) {
|
||||
mkw::vr::OpenXRWiiRemoteSample vr;
|
||||
if (!mkw::vr::OpenXRReadWiiRemote(vr)) {
|
||||
FillGraceSample(chan, Kind::RemoteWithNunchuk, sample);
|
||||
return;
|
||||
}
|
||||
sample = {};
|
||||
sample.hold = vr.hold;
|
||||
sample.hasNunchuk = true;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
sample.acc[i] = vr.acc[i];
|
||||
sample.nunchukAcc[i] = vr.nunchuk_acc[i];
|
||||
}
|
||||
sample.stick[0] = vr.stick[0];
|
||||
sample.stick[1] = vr.stick[1];
|
||||
sample.dpdValid = vr.pointer_valid;
|
||||
if (vr.pointer_valid) {
|
||||
sample.pos[0] = vr.pointer[0];
|
||||
sample.pos[1] = vr.pointer[1];
|
||||
sample.horizon[0] = vr.horizon[0];
|
||||
sample.horizon[1] = vr.horizon[1];
|
||||
sample.dist = vr.distance_meters;
|
||||
}
|
||||
// Carried over should the port briefly lose its gamepad.
|
||||
g_lastAcc[chan].valid = true;
|
||||
for (int i = 0; i < 3; ++i) g_lastAcc[chan].acc[i] = sample.acc[i];
|
||||
}
|
||||
|
||||
// Samples buttons, accelerometers and the extension of the remote on a port.
|
||||
bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
if (chan >= PAD_MAX_CONTROLLERS) {
|
||||
return false;
|
||||
}
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
|
||||
const Kind kind = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
|
||||
if (IsVrControllerGamepad(gamepad)) {
|
||||
ReadVrControllerSample(chan, sample);
|
||||
return true;
|
||||
}
|
||||
const Kind kind = KindForGamepad(gamepad);
|
||||
if (!IsKpadKind(kind)) {
|
||||
const Kind remembered = EffectiveKind(chan);
|
||||
if (!IsKpadKind(remembered)) {
|
||||
@@ -751,6 +802,11 @@ void StartAccelCalibration(uint32_t chan) {
|
||||
FinishAccelCalibration("No Wii Remote on this port.");
|
||||
return;
|
||||
}
|
||||
if (IsVrControllerChannel(chan)) {
|
||||
// Their motion comes from headset tracking, which has no zero-point bias.
|
||||
FinishAccelCalibration("The VR controllers need no calibration.");
|
||||
return;
|
||||
}
|
||||
g_calibration = {};
|
||||
g_calibration.active = true;
|
||||
g_calibration.chan = chan;
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The VR controllers' Wii Remote presentation, tested without a headset: the
|
||||
// KPAD accelerometer frame, the absolute pointer against a virtual screen, the
|
||||
// picture's place on the menu quad, the off-screen debounce and the button
|
||||
// profile. Every expectation is stated in the Wii's own terms, so a sign error
|
||||
// in the geometry shows up as the wrong button face, tilt or cursor edge.
|
||||
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace mkw::vr::wii_remote;
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void Check(bool condition, const char* what) {
|
||||
if (!condition) {
|
||||
++g_failures;
|
||||
std::cerr << "FAILED: " << what << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void CheckNear(float actual, float expected, const char* what, float tolerance = 1.0e-3f) {
|
||||
if (!(std::fabs(actual - expected) <= tolerance)) {
|
||||
++g_failures;
|
||||
std::cerr << "FAILED: " << what << " (expected " << expected << ", got " << actual << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float kHalfTurn = 3.14159265f;
|
||||
constexpr float kQuarterTurn = 0.5f * kHalfTurn;
|
||||
|
||||
Quat AxisAngle(float x, float y, float z, float radians) {
|
||||
const float s = std::sin(0.5f * radians);
|
||||
return {x * s, y * s, z * s, std::cos(0.5f * radians)};
|
||||
}
|
||||
|
||||
// Aim poses as a player holds the controller. Forward is -Z, up +Y, right +X.
|
||||
constexpr Quat kLevel{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
Quat PitchedUp(float radians) { return AxisAngle(1.0f, 0.0f, 0.0f, radians); }
|
||||
Quat YawedLeft(float radians) { return AxisAngle(0.0f, 1.0f, 0.0f, radians); }
|
||||
// Seen from behind the controller, looking where it points.
|
||||
Quat RolledClockwise(float radians) { return AxisAngle(0.0f, 0.0f, 1.0f, -radians); }
|
||||
|
||||
void TestRestingAccelerometer() {
|
||||
const Vec3 zero{};
|
||||
// KPAD's rest reading with the buttons up.
|
||||
Vec3 acc = KpadAcceleration(kLevel, zero);
|
||||
CheckNear(acc[0], 0.0f, "level remote: x");
|
||||
CheckNear(acc[1], -1.0f, "level remote: gravity through the back of the remote");
|
||||
CheckNear(acc[2], 0.0f, "level remote: z");
|
||||
|
||||
// Pointing at the floor, the remote's back end (KPAD +z, towards the
|
||||
// player) is the side facing up.
|
||||
acc = KpadAcceleration(PitchedUp(-kQuarterTurn), zero);
|
||||
CheckNear(acc[1], 0.0f, "pointing down: y");
|
||||
CheckNear(acc[2], 1.0f, "pointing down: gravity along z");
|
||||
|
||||
// Pointing at the ceiling it is the tip that faces up.
|
||||
acc = KpadAcceleration(PitchedUp(kQuarterTurn), zero);
|
||||
CheckNear(acc[2], -1.0f, "pointing up: gravity along -z");
|
||||
|
||||
// Rolled a quarter turn clockwise the remote's left edge (KPAD -x) faces up.
|
||||
acc = KpadAcceleration(RolledClockwise(kQuarterTurn), zero);
|
||||
CheckNear(acc[0], -1.0f, "rolled clockwise: gravity along -x");
|
||||
CheckNear(acc[1], 0.0f, "rolled clockwise: y");
|
||||
|
||||
// Buttons facing the floor.
|
||||
acc = KpadAcceleration(RolledClockwise(kHalfTurn), zero);
|
||||
CheckNear(acc[1], 1.0f, "upside down: gravity along +y");
|
||||
}
|
||||
|
||||
void TestAccelerometerRange() {
|
||||
// A 10 g upward jolt saturates like the ADXL330 instead of reporting 11 g.
|
||||
const Vec3 acc = KpadAcceleration(kLevel, {0.0f, 10.0f * kStandardGravity, 0.0f});
|
||||
CheckNear(acc[1], -kAccelRangeG, "saturates at the sensor's range");
|
||||
}
|
||||
|
||||
void TestMotionTracker() {
|
||||
MotionTracker tracker;
|
||||
const Quat orientation = kLevel;
|
||||
// Rising at 1 g: p = a t^2 / 2, v = a t, sampled at 90 Hz.
|
||||
const float a = kStandardGravity;
|
||||
const int64_t step_ns = 11'111'111;
|
||||
Vec3 acc{};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
const float t = static_cast<float>(i) * static_cast<float>(step_ns) * 1.0e-9f;
|
||||
const Vec3 position{0.0f, 0.5f * a * t * t, 0.0f};
|
||||
const Vec3 velocity{0.0f, a * t, 0.0f};
|
||||
acc = tracker.Update(&orientation, &position, &velocity, i * step_ns);
|
||||
if (i == 0) {
|
||||
CheckNear(acc[1], -1.0f, "first sample has no history: gravity only");
|
||||
}
|
||||
}
|
||||
// Gravity plus the climb: 2 g through the back of the remote.
|
||||
CheckNear(acc[1], -2.0f, "steady 1 g climb reads 2 g", 1.0e-2f);
|
||||
|
||||
// Holding still again settles back to rest.
|
||||
const Vec3 still_position{0.0f, 1.0f, 0.0f};
|
||||
const Vec3 still_velocity{};
|
||||
tracker.Reset();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
acc = tracker.Update(&orientation, &still_position, &still_velocity, (10 + i) * step_ns);
|
||||
}
|
||||
CheckNear(acc[1], -1.0f, "at rest after a reset", 1.0e-3f);
|
||||
|
||||
// Losing tracking repeats the last reading instead of inventing one.
|
||||
const Vec3 held = tracker.Update(nullptr, nullptr, nullptr, 20 * step_ns);
|
||||
CheckNear(held[1], acc[1], "untracked controller holds its last reading");
|
||||
}
|
||||
|
||||
Screen ScreenAhead(float distance, float half_width, float half_height) {
|
||||
Screen screen;
|
||||
screen.pose.position = {0.0f, 0.0f, -distance};
|
||||
screen.half_width = half_width;
|
||||
screen.half_height = half_height;
|
||||
return screen;
|
||||
}
|
||||
|
||||
void TestRaycast() {
|
||||
const Screen screen = ScreenAhead(2.0f, 1.2f, 0.675f);
|
||||
|
||||
Pose aim;
|
||||
ScreenHit hit = RaycastScreen(aim, screen);
|
||||
Check(hit.valid, "aiming at the screen's centre hits");
|
||||
CheckNear(hit.u, 0.0f, "centre: u");
|
||||
CheckNear(hit.v, 0.0f, "centre: v");
|
||||
CheckNear(hit.distance_meters, 2.0f, "centre: distance");
|
||||
|
||||
// Absolute: sliding the hand to the right edge puts the pointer there.
|
||||
aim.position = {1.2f, 0.0f, 0.0f};
|
||||
hit = RaycastScreen(aim, screen);
|
||||
CheckNear(hit.u, 1.0f, "hand at the right edge: u");
|
||||
|
||||
// Tilting up to the top edge: KPAD's y is -1 at the top.
|
||||
aim.position = {};
|
||||
aim.orientation = PitchedUp(std::atan(0.675f / 2.0f));
|
||||
hit = RaycastScreen(aim, screen);
|
||||
CheckNear(hit.v, 1.0f, "tilted to the top edge: v");
|
||||
CheckNear(KpadPosition(hit)[1], -1.0f, "top edge is KPAD y -1");
|
||||
|
||||
// Turning left moves the pointer left.
|
||||
aim.orientation = YawedLeft(std::atan(0.6f / 2.0f));
|
||||
hit = RaycastScreen(aim, screen);
|
||||
CheckNear(hit.u, -0.5f, "turned left: u");
|
||||
CheckNear(KpadPosition(hit)[0], -0.5f, "turned left: KPAD x");
|
||||
|
||||
// Rotating the controller does not change the distance.
|
||||
CheckNear(hit.distance_meters, 2.0f, "distance is perpendicular");
|
||||
|
||||
// Pointing away, or standing behind the screen, is no hit at all.
|
||||
aim.orientation = YawedLeft(kHalfTurn);
|
||||
Check(!RaycastScreen(aim, screen).valid, "pointing away misses");
|
||||
aim.orientation = kLevel;
|
||||
aim.position = {0.0f, 0.0f, -3.0f};
|
||||
Check(!RaycastScreen(aim, screen).valid, "behind the screen misses");
|
||||
|
||||
// A screen off to the side, facing the player: only the pose matters.
|
||||
Screen side;
|
||||
side.pose.position = {-2.0f, 0.0f, 0.0f};
|
||||
side.pose.orientation = YawedLeft(kQuarterTurn); // its +Z faces +X, back at the player
|
||||
side.half_width = 1.0f;
|
||||
side.half_height = 1.0f;
|
||||
Pose towards_side;
|
||||
towards_side.orientation = YawedLeft(kQuarterTurn); // aim -Z becomes -X
|
||||
hit = RaycastScreen(towards_side, side);
|
||||
Check(hit.valid, "turned towards a side screen hits");
|
||||
CheckNear(hit.u, 0.0f, "side screen: u");
|
||||
towards_side.position = {0.0f, 0.5f, 0.0f};
|
||||
hit = RaycastScreen(towards_side, side);
|
||||
CheckNear(hit.v, 0.5f, "side screen: raised hand raises the pointer");
|
||||
}
|
||||
|
||||
void TestHorizon() {
|
||||
const Screen screen = ScreenAhead(2.0f, 1.0f, 1.0f);
|
||||
Pose aim;
|
||||
std::array<float, 2> horizon = Horizon(aim, screen);
|
||||
CheckNear(horizon[0], 1.0f, "level remote: horizon x");
|
||||
CheckNear(horizon[1], 0.0f, "level remote: horizon y");
|
||||
// The SDK's (0, 1) for a quarter turn clockwise.
|
||||
aim.orientation = RolledClockwise(kQuarterTurn);
|
||||
horizon = Horizon(aim, screen);
|
||||
CheckNear(horizon[0], 0.0f, "rolled clockwise: horizon x");
|
||||
CheckNear(horizon[1], 1.0f, "rolled clockwise: horizon y");
|
||||
}
|
||||
|
||||
void TestPointerFilter() {
|
||||
PointerFilter filter;
|
||||
const int64_t ms = 1'000'000;
|
||||
ScreenHit on{true, 0.25f, -0.5f, 1.5f};
|
||||
ScreenHit result = filter.Update(on, 0);
|
||||
Check(result.valid && result.u == 0.25f, "on screen passes through");
|
||||
|
||||
// A tracking blip holds the last position.
|
||||
result = filter.Update(ScreenHit{}, 10 * ms);
|
||||
Check(result.valid && result.u == 0.25f, "lost hit holds the pointer");
|
||||
// An excursion past the margin pins at the margin.
|
||||
result = filter.Update(ScreenHit{true, 3.0f, 0.0f, 1.5f}, 50 * ms);
|
||||
Check(result.valid, "short excursion stays visible");
|
||||
CheckNear(result.u, kPointerMarginU, "short excursion pins at the margin");
|
||||
// Sustained, it is hidden like a remote that lost the sensor bar.
|
||||
result = filter.Update(ScreenHit{true, 3.0f, 0.0f, 1.5f}, 110 * ms);
|
||||
Check(!result.valid, "sustained excursion hides the pointer");
|
||||
result = filter.Update(ScreenHit{}, 120 * ms);
|
||||
Check(!result.valid, "stays hidden");
|
||||
// Coming back shows it straight away.
|
||||
result = filter.Update(on, 130 * ms);
|
||||
Check(result.valid, "returning to the screen shows it again");
|
||||
|
||||
// Never having been on screen, a lost hit is simply no pointer.
|
||||
PointerFilter fresh;
|
||||
Check(!fresh.Update(ScreenHit{}, 0).valid, "no pointer before the first hit");
|
||||
// Just inside the margins still counts as on screen.
|
||||
Check(fresh.Update(ScreenHit{true, 1.8f, -1.4f, 1.0f}, ms).valid, "inside the margins is tracked");
|
||||
}
|
||||
|
||||
void TestMenuPicture() {
|
||||
// 16:9 game in a 16:9 window on a square eye texture: full width.
|
||||
std::array<float, 2> extents = MenuPictureHalfExtents(2.4f, 1.0f, 16.0f / 9.0f, 16.0f / 9.0f);
|
||||
CheckNear(extents[0], 1.2f, "16:9 picture: half width");
|
||||
CheckNear(extents[1], 0.675f, "16:9 picture: half height");
|
||||
// 4:3 game pillarboxed inside that window.
|
||||
extents = MenuPictureHalfExtents(2.4f, 1.0f, 16.0f / 9.0f, 4.0f / 3.0f);
|
||||
CheckNear(extents[0], 0.9f, "4:3 picture in 16:9 window: half width");
|
||||
CheckNear(extents[1], 0.675f, "4:3 picture in 16:9 window: half height");
|
||||
// A wide eye texture pillarboxes a square snapshot by width instead.
|
||||
extents = MenuPictureHalfExtents(2.0f, 2.0f, 1.0f, 1.0f);
|
||||
CheckNear(extents[0], 0.5f, "square picture on a wide quad: half width");
|
||||
CheckNear(extents[1], 0.5f, "square picture on a wide quad: half height");
|
||||
}
|
||||
|
||||
void TestButtons() {
|
||||
HandInputs left;
|
||||
HandInputs right;
|
||||
Check(RemoteButtons(left, right) == 0, "nothing held");
|
||||
|
||||
right.primary = true;
|
||||
right.trigger = 0.6f;
|
||||
Check(RemoteButtons(left, right) == (kButtonA | kButtonB), "right A and trigger are A and B");
|
||||
|
||||
right = {};
|
||||
right.stick_y = 0.9f;
|
||||
Check(RemoteButtons(left, right) == kButtonOne, "right stick up is 1");
|
||||
right.stick_y = -0.9f;
|
||||
Check(RemoteButtons(left, right) == kButtonTwo, "right stick down is 2");
|
||||
right.stick_y = 0.0f;
|
||||
right.stick_x = -0.9f;
|
||||
Check(RemoteButtons(left, right) == kButtonMinus, "right stick left is -");
|
||||
right.stick_x = 0.9f;
|
||||
Check(RemoteButtons(left, right) == kButtonPlus, "right stick right is +");
|
||||
right.stick_x = 0.3f;
|
||||
Check(RemoteButtons(left, right) == 0, "a light push is no press");
|
||||
|
||||
right = {};
|
||||
left.menu = true;
|
||||
left.squeeze = 0.8f;
|
||||
left.trigger = 0.7f;
|
||||
Check(RemoteButtons(left, right) == (kButtonHome | kButtonC | kButtonZ), "left menu, grip, trigger are HOME, C, Z");
|
||||
// Left X and Y have no Wii button in the profile.
|
||||
left = {};
|
||||
left.primary = true;
|
||||
left.secondary = true;
|
||||
Check(RemoteButtons(left, right) == 0, "left X/Y are unbound");
|
||||
|
||||
left.stick_x = 1.0f;
|
||||
left.stick_y = 1.0f;
|
||||
const std::array<float, 2> stick = NunchukStick(left);
|
||||
CheckNear(std::hypot(stick[0], stick[1]), 1.0f, "diagonal stays inside the gate");
|
||||
CheckNear(stick[0], stick[1], "diagonal keeps its direction");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestRestingAccelerometer();
|
||||
TestAccelerometerRange();
|
||||
TestMotionTracker();
|
||||
TestRaycast();
|
||||
TestHorizon();
|
||||
TestPointerFilter();
|
||||
TestMenuPicture();
|
||||
TestButtons();
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " check(s) failed\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "vr_wii_remote_tests: all checks passed\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user