Refactor OpenXR controller input handling and update settings panel interactions

This commit is contained in:
iChris4 committed 2026-09-19 15:28:20 +02:00
1 parent 60c443654f
commit e681294709
11 files changed
+163 -95

No files matched your search

+20 -17
View File
@@ -143,26 +143,27 @@ to a port like any other. `controller_mode` decides what the game finds on that
from **F10 > VR > VR controllers**; the game sees a change as a controller reconnection. 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 `"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 OpenXR Wii Remote does, with buttons adapted from its default `OpenXR Wii Remote` profile for the
controllers. The port is served through KPAD like a Bluetooth remote (`wii_remote_input.cpp`), so Touch controllers. The port is served through KPAD like a Bluetooth remote
`WPADProbe` reports a Nunchuk and the game runs its own Wii Remote + Nunchuk control scheme: (`wii_remote_input.cpp`), so `WPADProbe` reports a Nunchuk and the game runs its own Wii Remote + Nunchuk control scheme:
| Controller | Wii | | Controller | Wii |
| --- | --- | | --- | --- |
| Right A | A | | Right A | A |
| Right trigger | B | | Right trigger | B |
| Right stick up / down | 1 / 2 | | Right stick up / down | 1 / 2 |
| Right stick left / right | − / + | | Left X | − |
| Left menu | + |
| Left stick | Nunchuk stick | | Left stick | Nunchuk stick |
| Left trigger | Z | | Left trigger | Z |
| Left grip | C | | Left grip | C |
| Left menu | HOME | | Left Y | Settings panel (not a Wii button) |
| Right controller motion and aim | Wii Remote accelerometer and pointer | | Right controller motion and aim | Wii Remote accelerometer and pointer |
| Left controller motion | Nunchuk accelerometer | | Left controller motion | Nunchuk accelerometer |
Analog inputs count as pressed past half travel. Right B, left X/Y and the stick clicks are unbound, Analog inputs count as pressed past half travel. Right B, right stick left / right and the stick
as in DolphinXR's profile. The game's Wii Remote rumble vibrates both controllers, subject to the clicks are unbound, and no controller button presses HOME. The game's Wii Remote rumble vibrates
ordinary controller-vibration switch. 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 **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 (`XR_KHR_win32_convert_performance_counter_time`, `XR_KHR_convert_timespec_time` on Android), not
@@ -192,13 +193,14 @@ cursor for 100 ms before it disappears, so tracking spikes during fast motion do
Raw IR camera dots in `KPADGetUnifiedWpadStatus` stay invalid; the game reads the pointer from Raw IR camera dots in `KPADGetUnifiedWpadStatus` stay invalid; the game reads the pointer from
`KPADStatus`. `KPADStatus`.
**Settings in the headset.** Clicking both thumbsticks together opens the settings panel described **Settings in the headset.** Left Y opens the settings panel described below; while it is open the
below; while it is open the controllers operate the panel and the game sees them idle. controllers operate the panel and the game sees them idle.
`"gamepad"` keeps the controllers one ordinary gamepad read through PAD as a GameCube controller: `"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 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 → sticks (clicks → stick buttons), left menu → Start. Every binding in the F10 controller menu
applies. applies. Left Y is GameCube Y here, so clicking both thumbsticks together opens the settings panel
instead.
Bindings are suggested for `oculus/touch_controller` (Quest 2, 3 and Pro) and 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 `khr/simple_controller`. `mkw_vr_wii_remote_tests` checks the accelerometer frame, the pointer
@@ -208,8 +210,9 @@ raycast and debounce, the picture placement and the button profile without a hea
The F10 settings bar is only visible on the desktop window, so the same settings are also offered on The F10 settings bar is only visible on the desktop window, so the same settings are also offered on
a panel inside the headset, in menus and during an immersive race alike, including on the Quest. a panel inside the headset, in menus and during an immersive race alike, including on the Quest.
**Click both thumbsticks together** to open it, and again to close it; the left controller's menu **Press left Y** to open it, and again to close it (with `controller_mode = "gamepad"`, **click both
button and the panel's *Close* button also close it. It can be opened from the desktop as well, with thumbsticks together** instead); the left controller's menu button and the panel's *Close* button
also close it. It can be opened from the desktop as well, with
**F10 → VR → Show these settings in the headset**. **F10 → VR → Show these settings in the headset**.
The panel has the F10 bar's menus as tabs (VR, Graphics, Controllers, Audio, Diagnostics) and a The panel has the F10 bar's menus as tabs (VR, Graphics, Controllers, Audio, Diagnostics) and a
@@ -219,8 +222,8 @@ pointing. Changes apply exactly as they do from the F10 bar, and the two stay in
While the panel is open, and until every button has been released after it closes, the game sees While the panel is open, and until every button has been released after it closes, the game sees
the VR controllers idle: no buttons, no pointer and a remote at rest. Nothing reaches the game from the VR controllers idle: no buttons, no pointer and a remote at rest. Nothing reaches the game from
the chord, the trigger that clicked *Close*, or the menu press that closed the panel. The game is the panel button, the trigger that clicked *Close*, or the menu press that closed the panel. The
not paused, so a race carries on while you change settings. Other controllers (keyboard, desktop game is not paused, so a race carries on while you change settings. Other controllers (keyboard, desktop
gamepads, Bluetooth remotes) are not affected. gamepads, Bluetooth remotes) are not affected.
The panel sits centred on the virtual screen, three quarters of its width across (1.8 m with the The panel sits centred on the virtual screen, three quarters of its width across (1.8 m with the
@@ -240,8 +243,8 @@ backends already submit carry it, so no extra swapchain or composition layer is
ImGui backend keeps a single projection uniform, so the panel's pass is submitted on its own command ImGui backend keeps a single projection uniform, so the panel's pass is submitted on its own command
buffer before the desktop's ImGui pass of the same frame is recorded. buffer before the desktop's ImGui pass of the same frame is recorded.
`mkw_vr_settings_panel_tests` covers the chord, the release latch, selection, scrolling and the `mkw_vr_settings_panel_tests` covers the panel button in both controller modes, the release latch,
canvas mapping; `gx_fifo_tests` covers where the panel lands in each eye. selection, scrolling and the canvas mapping; `gx_fifo_tests` covers where the panel lands in each eye.
## The first-person camera ## The first-person camera
@@ -252,7 +252,6 @@ class SettingsPage(
info(R.string.controls_map_stick, activity.getString(R.string.controls_map_stick_value)) info(R.string.controls_map_stick, activity.getString(R.string.controls_map_stick_value))
info(R.string.controls_map_z, activity.getString(R.string.controls_map_z_value)) info(R.string.controls_map_z, activity.getString(R.string.controls_map_z_value))
info(R.string.controls_map_c, activity.getString(R.string.controls_map_c_value)) info(R.string.controls_map_c, activity.getString(R.string.controls_map_c_value))
info(R.string.controls_map_home, activity.getString(R.string.controls_map_home_value))
info(R.string.controls_map_pointer, activity.getString(R.string.controls_map_pointer_value)) info(R.string.controls_map_pointer, activity.getString(R.string.controls_map_pointer_value))
info(R.string.controls_map_panel, activity.getString(R.string.controls_map_panel_value)) info(R.string.controls_map_panel, activity.getString(R.string.controls_map_panel_value))
} }
+3 -5
View File
@@ -130,7 +130,7 @@
<string name="about_credit_title_dolphin">Dolphin</string> <string name="about_credit_title_dolphin">Dolphin</string>
<string name="about_credit_title_logo">Logo</string> <string name="about_credit_title_logo">Logo</string>
<string name="about_licence_title">GPL v3</string> <string name="about_licence_title">GPL v3</string>
<string name="settings_apply_note">Changes apply the next time the game starts. While playing, click both thumbsticks together to open the in-headset settings.</string> <string name="settings_apply_note">Changes apply the next time the game starts. While playing, press left Y to open the in-headset settings (click both thumbsticks together with Gamepad controllers).</string>
<string name="settings_running_note">The game is running, so these changes wait until it is closed and started again.</string> <string name="settings_running_note">The game is running, so these changes wait until it is closed and started again.</string>
<string name="settings_write_failed">Could not save Config.toml.</string> <string name="settings_write_failed">Could not save Config.toml.</string>
<string name="settings_custom_value">Custom (%1$s)</string> <string name="settings_custom_value">Custom (%1$s)</string>
@@ -205,19 +205,17 @@
<string name="controls_map_12">1 / 2</string> <string name="controls_map_12">1 / 2</string>
<string name="controls_map_12_value">Right stick up / down</string> <string name="controls_map_12_value">Right stick up / down</string>
<string name="controls_map_minus_plus">− / +</string> <string name="controls_map_minus_plus">− / +</string>
<string name="controls_map_minus_plus_value">Right stick left / right</string> <string name="controls_map_minus_plus_value">Left X / left menu</string>
<string name="controls_map_stick">Nunchuk stick</string> <string name="controls_map_stick">Nunchuk stick</string>
<string name="controls_map_stick_value">Left stick</string> <string name="controls_map_stick_value">Left stick</string>
<string name="controls_map_z">Z</string> <string name="controls_map_z">Z</string>
<string name="controls_map_z_value">Left trigger</string> <string name="controls_map_z_value">Left trigger</string>
<string name="controls_map_c">C</string> <string name="controls_map_c">C</string>
<string name="controls_map_c_value">Left grip</string> <string name="controls_map_c_value">Left grip</string>
<string name="controls_map_home">HOME</string>
<string name="controls_map_home_value">Left menu</string>
<string name="controls_map_pointer">Pointer and motion</string> <string name="controls_map_pointer">Pointer and motion</string>
<string name="controls_map_pointer_value">Aim and move the right controller</string> <string name="controls_map_pointer_value">Aim and move the right controller</string>
<string name="controls_map_panel">Settings panel</string> <string name="controls_map_panel">Settings panel</string>
<string name="controls_map_panel_value">Click both thumbsticks</string> <string name="controls_map_panel_value">Left Y</string>
<!-- Audio tab --> <!-- Audio tab -->
<string name="section_audio">Volume</string> <string name="section_audio">Volume</string>
+1 -1
View File
@@ -581,7 +581,7 @@ the app:
| --- | --- | | --- | --- |
| `debug.wiicompiled.vtxpad 0` | Turns the stride padding off, to re-check a driver update | | `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.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. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist. `panel` clicks both thumbsticks, opening or closing the settings panel (see `OPENXR.md`) | | `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. `panel` presses the settings panel's button (left Y, or both thumbsticks as a gamepad), opening or closing it (see `OPENXR.md`) |
| `debug.wiicompiled.fpslog 1` | Logs the game's rendered frame rate every 5 s, with per-frame averages of the producer's waits for the frame worker's DONE and SEALED phases and of the worker's seal, permit wait, prepare and encode stretches. The compositor's `VrApi` log line gives headset FPS, `GPU%`, `CPU%`, clock levels and app GPU time (`App=`) | | `debug.wiicompiled.fpslog 1` | Logs the game's rendered frame rate every 5 s, with per-frame averages of the producer's waits for the frame worker's DONE and SEALED phases and of the worker's seal, permit wait, prepare and encode stretches. The compositor's `VrApi` log line gives headset FPS, `GPU%`, `CPU%`, clock levels and app GPU time (`App=`) |
The injector makes headset tests possible with nobody wearing the headset. The injector makes headset tests possible with nobody wearing the headset.
+6 -5
View File
@@ -40,7 +40,8 @@ struct OpenXRPointerScreen {
// poses are located at the measured current time, turned into both // poses are located at the measured current time, turned into both
// accelerometers and the IR pointer (the right aim ray against the virtual // accelerometers and the IR pointer (the right aim ray against the virtual
// screen the renderer is showing), and published through openxr_wii_remote.h. // screen the renderer is showing), and published through openxr_wii_remote.h.
// Buttons follow DolphinXR's "OpenXR Wii Remote" profile (see RemoteButtons). // Buttons are adapted from DolphinXR's "OpenXR Wii Remote" profile (see
// RemoteButtons).
// The game's rumble drives both controllers' haptics. // The game's rumble drives both controllers' haptics.
// //
// Gamepad: the virtual joystick is read through PAD as a GameCube controller, // Gamepad: the virtual joystick is read through PAD as a GameCube controller,
@@ -55,10 +56,10 @@ struct OpenXRPointerScreen {
// Both are bound for the Oculus Touch profile; khr/simple_controller gets // Both are bound for the Oculus Touch profile; khr/simple_controller gets
// select/menu and the poses so an unknown runtime still offers something. // select/menu and the poses so an unknown runtime still offers something.
// //
// Clicking both thumbsticks opens the in-headset settings panel // Left Y (both thumbsticks clicked together as a gamepad) opens the in-headset
// (openxr_settings_panel.h). While it is open, and until every button has been // settings panel (openxr_settings_panel.h). While it is open, and until every
// released after it closes, the game sees idle controllers: the chord, the // button has been released after it closes, the game sees idle controllers: the
// pointer and the triggers belong to the panel. // panel button, the pointer and the triggers belong to the panel.
// //
// Lifetime: Create after the session exists (attaches the action set, which // Lifetime: Create after the session exists (attaches the action set, which
// OpenXR permits once per session), Sync once per xrWaitFrame, Idle while the // OpenXR permits once per session), Sync once per xrWaitFrame, Idle while the
+26 -14
View File
@@ -18,10 +18,11 @@ namespace mkw::vr {
// controllers, which are withheld from the game while it is open. // controllers, which are withheld from the game while it is open.
// //
// The XR pacing thread owns the controllers: it opens and closes the panel from // The XR pacing thread owns the controllers: it opens and closes the panel from
// the controller chord, aims the pointer and publishes it here. The game thread // the controllers' panel button, aims the pointer and publishes it here. The
// draws the panel, may close it (its Close button) or open it (the F10 bar), and // game thread draws the panel, may close it (its Close button) or open it (the
// reads the pointer once per presented frame. Nothing in this header depends on // F10 bar), and reads the pointer once per presented frame. Nothing in this
// OpenXR, so the settings overlay compiles the same in builds without it. // header depends on OpenXR, so the settings overlay compiles the same in builds
// without it.
// The panel's canvas in ImGui pixels, drawn at twice the desktop menu's scale. // The panel's canvas in ImGui pixels, drawn at twice the desktop menu's scale.
inline constexpr float kSettingsPanelWidthPixels = 1440.0f; inline constexpr float kSettingsPanelWidthPixels = 1440.0f;
@@ -75,10 +76,20 @@ struct Frame {
float wheel = 0.0f; float wheel = 0.0f;
}; };
// Whether the button that opens and closes the panel is held. As a Wii Remote
// that is left Y, which has no Wii button; as a gamepad left Y is GameCube Y, so
// both thumbsticks clicked together stand in for it.
inline bool ToggleHeld(const std::array<HandInputs, 2>& hands, OpenXRControllerMode mode) noexcept {
if (mode == OpenXRControllerMode::WiiRemote) {
return hands[0].secondary;
}
return hands[0].thumbstick_click && hands[1].thumbstick_click;
}
// The controller side of the panel, one Update per XR frame: // The controller side of the panel, one Update per XR frame:
// //
// - Clicking both thumbsticks together opens or closes it. Nothing else opens // - The panel button (ToggleHeld) opens or closes it. Nothing else opens it
// it from the controllers: every other button already means something to the // from the controllers: every other button already means something to the
// game. // game.
// - While it is open, the left menu button also closes it, both triggers and // - While it is open, the left menu button also closes it, both triggers and
// the A / X buttons select, the thumbsticks scroll, and the hand whose trigger // the A / X buttons select, the thumbsticks scroll, and the hand whose trigger
@@ -87,15 +98,16 @@ struct Frame {
// press that closed the panel never lands in the game as well. // press that closed the panel never lands in the game as well.
class Controls { class Controls {
public: public:
// `open` is the shared flag: the chord flips it, and the game thread may // `open` is the shared flag: the panel button flips it, and the game thread
// have changed it since the last frame. `dt_seconds` is the time since the // may have changed it since the last frame. `dt_seconds` is the time since
// previous frame. // the previous frame; `mode` is how the game sees the controllers.
Frame Update(const std::array<HandInputs, 2>& hands, bool& open, float dt_seconds) noexcept { Frame Update(const std::array<HandInputs, 2>& hands, bool& open, float dt_seconds,
const bool chord = hands[0].thumbstick_click && hands[1].thumbstick_click; OpenXRControllerMode mode) noexcept {
if (chord && !m_chord_held) { const bool toggle = ToggleHeld(hands, mode);
if (toggle && !m_toggle_held) {
open = !open; open = !open;
} }
m_chord_held = chord; m_toggle_held = toggle;
if (open && hands[0].menu && !m_menu_held && m_was_open) { if (open && hands[0].menu && !m_menu_held && m_was_open) {
open = false; open = false;
@@ -152,7 +164,7 @@ public:
} }
private: private:
bool m_chord_held = false; bool m_toggle_held = false;
bool m_menu_held = false; bool m_menu_held = false;
std::array<bool, 2> m_trigger_held{}; std::array<bool, 2> m_trigger_held{};
bool m_was_open = false; bool m_was_open = false;
+6 -5
View File
@@ -337,10 +337,12 @@ struct HandInputs {
float stick_y = 0.0f; // +up float stick_y = 0.0f; // +up
}; };
// DolphinXR's default "OpenXR Wii Remote" profile (Data/Sys/Profiles/Wiimote): // Adapted from DolphinXR's default "OpenXR Wii Remote" profile
// (Data/Sys/Profiles/Wiimote):
// right A -> A, right trigger -> B, right stick up/down -> 1/2, // right A -> A, right trigger -> B, right stick up/down -> 1/2,
// right stick left/right -> -/+, left menu -> HOME, // left X -> -, left menu -> +,
// left grip -> C, left trigger -> Z, left stick -> Nunchuk stick. // left grip -> C, left trigger -> Z, left stick -> Nunchuk stick.
// HOME has no button; left Y opens the settings panel (openxr_settings_panel.h).
inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) noexcept { inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) noexcept {
uint32_t hold = 0; uint32_t hold = 0;
const auto press = [&hold](bool held, uint32_t bit) { const auto press = [&hold](bool held, uint32_t bit) {
@@ -352,9 +354,8 @@ inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) n
press(right.trigger > kPressThreshold, kButtonB); press(right.trigger > kPressThreshold, kButtonB);
press(right.stick_y > kPressThreshold, kButtonOne); press(right.stick_y > kPressThreshold, kButtonOne);
press(right.stick_y < -kPressThreshold, kButtonTwo); press(right.stick_y < -kPressThreshold, kButtonTwo);
press(right.stick_x < -kPressThreshold, kButtonMinus); press(left.primary, kButtonMinus);
press(right.stick_x > kPressThreshold, kButtonPlus); press(left.menu, kButtonPlus);
press(left.menu, kButtonHome);
press(left.squeeze > kPressThreshold, kButtonC); press(left.squeeze > kPressThreshold, kButtonC);
press(left.trigger > kPressThreshold, kButtonZ); press(left.trigger > kPressThreshold, kButtonZ);
return hold; return hold;
+8 -4
View File
@@ -1240,7 +1240,8 @@ void DrawVrSettings() {
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip( ImGui::SetTooltip(
"Opens these settings on a panel in front of you, in menus and races alike.\n" "Opens these settings on a panel in front of you, in menus and races alike.\n"
"In the headset, clicking both thumbsticks together opens and closes it too.\n" "In the headset, left Y opens and closes it too (with Gamepad VR controllers,\n"
"click both thumbsticks together instead).\n"
"Aim at it and pull a trigger to change a setting; push a thumbstick to scroll.\n" "Aim at it and pull a trigger to change a setting; push a thumbstick to scroll.\n"
"While it is open the game does not see the VR controllers."); "While it is open the game does not see the VR controllers.");
} }
@@ -1253,8 +1254,8 @@ void DrawVrSettings() {
ImGui::SetTooltip( ImGui::SetTooltip(
"Wii Remote + Nunchuk: the right controller is a Wii Remote, with motion and a pointer " "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" "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" " Right: A = A, trigger = B, stick up/down = 1/2\n"
" Left: stick = Nunchuk stick, trigger = Z, grip = C, menu = HOME\n" " Left: stick = Nunchuk stick, trigger = Z, grip = C, X = -, menu = +, Y = settings panel\n"
"Gamepad: both controllers are one ordinary controller, read as a GameCube pad.\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."); "Applies immediately; the game sees the controller change as a reconnection.");
} }
@@ -1967,7 +1968,10 @@ void DrawVrSettingsPanelWindow() {
mkw::vr::OpenXRSetSettingsPanelOpen(false); mkw::vr::OpenXRSetSettingsPanelOpen(false);
} }
ImGui::TextDisabled("Aim and pull a trigger to change a setting, push a thumbstick to scroll."); ImGui::TextDisabled("Aim and pull a trigger to change a setting, push a thumbstick to scroll.");
ImGui::TextDisabled("Click both thumbsticks or press Menu to close. The game does not see the controllers meanwhile."); ImGui::TextDisabled("%s to close. The game does not see the controllers meanwhile.",
mkw::vr::OpenXRGetControllerMode() == mkw::vr::OpenXRControllerMode::WiiRemote
? "Press left Y or Menu"
: "Click both thumbsticks or press Menu");
ImGui::Separator(); ImGui::Separator();
if (ImGui::BeginTabBar("Settings")) { if (ImGui::BeginTabBar("Settings")) {
const auto tab = [](const char* label, void (*draw)()) { const auto tab = [](const char* label, void (*draw)()) {
+8 -4
View File
@@ -40,9 +40,10 @@ namespace {
// A new sequence number holds the button for kInjectHoldFrames XR frames. // A new sequence number holds the button for kInjectHoldFrames XR frames.
// Buttons: a, b, x, y, start, up, down, left, right, and for the Wii Remote // 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 // presentation also home, c and z (x/y/start press 1/2/+ there, and the
// directions push the Nunchuk stick). `panel` clicks both thumbsticks, opening // directions push the Nunchuk stick). `panel` presses the settings panel's
// or closing the settings panel, where `a` then selects. The property is unset // button (left Y, or both thumbsticks for a gamepad), opening or closing it,
// in normal use, so this costs one property read every few frames. // where `a` then selects. The property is unset in normal use, so this costs
// one property read every few frames.
constexpr uint32_t kInjectHoldFrames = 12; constexpr uint32_t kInjectHoldFrames = 12;
constexpr uint32_t kInjectPollFrames = 4; constexpr uint32_t kInjectPollFrames = 4;
@@ -594,6 +595,8 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
m_last_input_time = input_time; m_last_input_time = input_time;
std::array<wii_remote::HandInputs, kHands> panel_hands = hands; std::array<wii_remote::HandInputs, kHands> panel_hands = hands;
if (Injected("panel")) { if (Injected("panel")) {
// The panel button in either controller mode.
panel_hands[0].secondary = true;
panel_hands[0].thumbstick_click = true; panel_hands[0].thumbstick_click = true;
panel_hands[1].thumbstick_click = true; panel_hands[1].thumbstick_click = true;
} }
@@ -604,7 +607,8 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
// is written back. // is written back.
const bool was_open = OpenXRSettingsPanelOpen(); const bool was_open = OpenXRSettingsPanelOpen();
bool open = was_open; bool open = was_open;
const settings_panel::Frame panel = m_panel_controls.Update(panel_hands, open, dt_seconds); const settings_panel::Frame panel =
m_panel_controls.Update(panel_hands, open, dt_seconds, OpenXRGetControllerMode());
// Pointer first: the game thread reads it as soon as it sees the panel open. // Pointer first: the game thread reads it as soon as it sees the panel open.
PublishSettingsPanel(input_time, settings_panel, panel); PublishSettingsPanel(input_time, settings_panel, panel);
if (open != was_open) { if (open != was_open) {
+68 -31
View File
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// //
// The in-headset settings panel's controller handling, tested without a // The in-headset settings panel's controller handling, tested without a
// headset: the thumbstick chord that opens and closes it, the release latch that // headset: the button that opens and closes it in each controller mode, the
// keeps a closing press out of the game, selection, scrolling, and where a hit // release latch that keeps a closing press out of the game, selection,
// lands on the panel's canvas. // scrolling, and where a hit lands on the panel's canvas.
#include "vr/openxr_settings_panel.h" #include "vr/openxr_settings_panel.h"
@@ -32,6 +32,8 @@ void CheckNear(float actual, float expected, const char* what, float tolerance =
} }
constexpr float kDt = 1.0f / 90.0f; constexpr float kDt = 1.0f / 90.0f;
constexpr OpenXRControllerMode kWiiRemote = OpenXRControllerMode::WiiRemote;
constexpr OpenXRControllerMode kGamepad = OpenXRControllerMode::Gamepad;
std::array<HandInputs, 2> Released() { std::array<HandInputs, 2> Released() {
return {}; return {};
@@ -44,46 +46,80 @@ std::array<HandInputs, 2> Chord() {
return hands; return hands;
} }
void ChordOpensAndClosesOnce() { std::array<HandInputs, 2> LeftY() {
std::array<HandInputs, 2> hands{};
hands[0].secondary = true;
return hands;
}
void LeftYOpensAndClosesOnceAsAWiiRemote() {
Controls controls; Controls controls;
bool open = false; bool open = false;
auto right_b = Released();
right_b[1].secondary = true;
Frame frame = controls.Update(right_b, open, kDt, kWiiRemote);
Check(!open && !frame.withheld, "right B neither opens nor withholds");
frame = controls.Update(Chord(), open, kDt, kWiiRemote);
Check(!open && !frame.withheld, "as a Wii Remote the thumbstick chord does not open the panel");
controls.Update(Released(), open, kDt, kWiiRemote);
frame = controls.Update(LeftY(), open, kDt, kWiiRemote);
Check(open && frame.open && frame.withheld, "left Y opens the panel");
frame = controls.Update(LeftY(), open, kDt, kWiiRemote);
Check(open, "holding left Y does not toggle again");
frame = controls.Update(Released(), open, kDt, kWiiRemote);
Check(open && frame.withheld, "the panel stays open and keeps the controllers after left Y is released");
frame = controls.Update(LeftY(), open, kDt, kWiiRemote);
Check(!open && !frame.open, "left Y closes the panel again");
Check(frame.withheld, "the closing press is still withheld from the game");
frame = controls.Update(Released(), open, kDt, kWiiRemote);
Check(!frame.withheld, "the game gets the controllers back once everything is released");
}
void ChordOpensAndClosesOnceAsAGamepad() {
Controls controls;
bool open = false;
Frame frame = controls.Update(LeftY(), open, kDt, kGamepad);
Check(!open && !frame.withheld, "as a gamepad left Y is GameCube Y, not the panel");
auto one = Released(); auto one = Released();
one[0].thumbstick_click = true; one[0].thumbstick_click = true;
Frame frame = controls.Update(one, open, kDt); frame = controls.Update(one, open, kDt, kGamepad);
Check(!open && !frame.withheld, "one thumbstick click alone neither opens nor withholds"); Check(!open && !frame.withheld, "one thumbstick click alone neither opens nor withholds");
frame = controls.Update(Chord(), open, kDt); frame = controls.Update(Chord(), open, kDt, kGamepad);
Check(open && frame.open && frame.withheld, "clicking both thumbsticks opens the panel"); Check(open && frame.open && frame.withheld, "clicking both thumbsticks opens the panel");
frame = controls.Update(Chord(), open, kDt); frame = controls.Update(Chord(), open, kDt, kGamepad);
Check(open, "holding the chord does not toggle again"); Check(open, "holding the chord does not toggle again");
frame = controls.Update(Released(), open, kDt); frame = controls.Update(Released(), open, kDt, kGamepad);
Check(open && frame.withheld, "the panel stays open and keeps the controllers after the chord is released"); Check(open && frame.withheld, "the panel stays open and keeps the controllers after the chord is released");
frame = controls.Update(Chord(), open, kDt); frame = controls.Update(Chord(), open, kDt, kGamepad);
Check(!open && !frame.open, "the chord closes the panel again"); Check(!open && !frame.open, "the chord closes the panel again");
Check(frame.withheld, "the closing chord is still withheld from the game"); Check(frame.withheld, "the closing chord is still withheld from the game");
frame = controls.Update(Released(), open, kDt); frame = controls.Update(Released(), open, kDt, kGamepad);
Check(!frame.withheld, "the game gets the controllers back once everything is released"); Check(!frame.withheld, "the game gets the controllers back once everything is released");
} }
void MenuClosesAndItsPressStaysOutOfTheGame() { void MenuClosesAndItsPressStaysOutOfTheGame() {
Controls controls; Controls controls;
bool open = false; bool open = false;
controls.Update(Chord(), open, kDt); controls.Update(LeftY(), open, kDt, kWiiRemote);
controls.Update(Released(), open, kDt); controls.Update(Released(), open, kDt, kWiiRemote);
auto menu = Released(); auto menu = Released();
menu[0].menu = true; menu[0].menu = true;
Frame frame = controls.Update(menu, open, kDt); Frame frame = controls.Update(menu, open, kDt, kWiiRemote);
Check(!open, "the left menu button closes an open panel"); Check(!open, "the left menu button closes an open panel");
frame = controls.Update(menu, open, kDt); frame = controls.Update(menu, open, kDt, kWiiRemote);
Check(frame.withheld, "HOME held across the close does not reach the game"); Check(frame.withheld, "+ held across the close does not reach the game");
Check(!open, "a held menu button does not reopen the panel"); Check(!open, "a held menu button does not reopen the panel");
frame = controls.Update(Released(), open, kDt); frame = controls.Update(Released(), open, kDt, kWiiRemote);
Check(!frame.withheld, "released, the controllers go back to the game"); Check(!frame.withheld, "released, the controllers go back to the game");
frame = controls.Update(menu, open, kDt); frame = controls.Update(menu, open, kDt, kWiiRemote);
Check(!open && !frame.withheld, "with the panel closed the menu button is the game's"); Check(!open && !frame.withheld, "with the panel closed the menu button is the game's");
} }
@@ -93,49 +129,49 @@ void SelectWaitsForAReleaseAndFollowsTheTrigger() {
// Opened from the settings bar while a trigger is held for the game. // Opened from the settings bar while a trigger is held for the game.
auto trigger = Released(); auto trigger = Released();
trigger[1].trigger = 1.0f; trigger[1].trigger = 1.0f;
controls.Update(trigger, open, kDt); controls.Update(trigger, open, kDt, kWiiRemote);
open = true; open = true;
Frame frame = controls.Update(trigger, open, kDt); Frame frame = controls.Update(trigger, open, kDt, kWiiRemote);
Check(frame.open && !frame.select, "a trigger held from before the panel opened does not click"); Check(frame.open && !frame.select, "a trigger held from before the panel opened does not click");
frame = controls.Update(Released(), open, kDt); frame = controls.Update(Released(), open, kDt, kWiiRemote);
Check(!frame.select, "nothing held, nothing selected"); Check(!frame.select, "nothing held, nothing selected");
frame = controls.Update(trigger, open, kDt); frame = controls.Update(trigger, open, kDt, kWiiRemote);
Check(frame.select && frame.pointing_hand == 1, "a fresh right trigger selects and points with the right hand"); Check(frame.select && frame.pointing_hand == 1, "a fresh right trigger selects and points with the right hand");
auto left = Released(); auto left = Released();
left[0].trigger = 0.9f; left[0].trigger = 0.9f;
controls.Update(Released(), open, kDt); controls.Update(Released(), open, kDt, kWiiRemote);
frame = controls.Update(left, open, kDt); frame = controls.Update(left, open, kDt, kWiiRemote);
Check(frame.select && frame.pointing_hand == 0, "pulling the left trigger hands the pointer to the left hand"); Check(frame.select && frame.pointing_hand == 0, "pulling the left trigger hands the pointer to the left hand");
auto button = Released(); auto button = Released();
button[1].primary = true; button[1].primary = true;
controls.Update(Released(), open, kDt); controls.Update(Released(), open, kDt, kWiiRemote);
frame = controls.Update(button, open, kDt); frame = controls.Update(button, open, kDt, kWiiRemote);
Check(frame.select && frame.pointing_hand == 0, "A selects without moving the pointer to another hand"); Check(frame.select && frame.pointing_hand == 0, "A selects without moving the pointer to another hand");
} }
void ThumbstickScrolls() { void ThumbstickScrolls() {
Controls controls; Controls controls;
bool open = true; bool open = true;
controls.Update(Released(), open, kDt); controls.Update(Released(), open, kDt, kWiiRemote);
auto small = Released(); auto small = Released();
small[1].stick_y = 0.2f; small[1].stick_y = 0.2f;
Check(controls.Update(small, open, kDt).wheel == 0.0f, "a resting thumbstick does not scroll"); Check(controls.Update(small, open, kDt, kWiiRemote).wheel == 0.0f, "a resting thumbstick does not scroll");
auto up = Released(); auto up = Released();
up[1].stick_y = 1.0f; up[1].stick_y = 1.0f;
CheckNear(controls.Update(up, open, 0.5f).wheel, kScrollStepsPerSecond * 0.1f, CheckNear(controls.Update(up, open, 0.5f, kWiiRemote).wheel, kScrollStepsPerSecond * 0.1f,
"full deflection scrolls up at the full rate, with a long frame clamped"); "full deflection scrolls up at the full rate, with a long frame clamped");
auto down = Released(); auto down = Released();
down[0].stick_y = -1.0f; down[0].stick_y = -1.0f;
down[1].stick_y = 0.3f; down[1].stick_y = 0.3f;
Check(controls.Update(down, open, kDt).wheel < 0.0f, "the more deflected stick decides the direction"); Check(controls.Update(down, open, kDt, kWiiRemote).wheel < 0.0f, "the more deflected stick decides the direction");
bool closed = false; bool closed = false;
Controls idle; Controls idle;
Check(idle.Update(up, closed, kDt).wheel == 0.0f, "a closed panel does not scroll"); Check(idle.Update(up, closed, kDt, kWiiRemote).wheel == 0.0f, "a closed panel does not scroll");
} }
void HitsMapOntoTheCanvas() { void HitsMapOntoTheCanvas() {
@@ -175,7 +211,8 @@ void BridgeAccumulatesWheelUntilTaken() {
} // namespace } // namespace
int main() { int main() {
ChordOpensAndClosesOnce(); LeftYOpensAndClosesOnceAsAWiiRemote();
ChordOpensAndClosesOnceAsAGamepad();
MenuClosesAndItsPressStaysOutOfTheGame(); MenuClosesAndItsPressStaysOutOfTheGame();
SelectWaitsForAReleaseAndFollowsTheTrigger(); SelectWaitsForAReleaseAndFollowsTheTrigger();
ThumbstickScrolls(); ThumbstickScrolls();
+17 -8
View File
@@ -247,24 +247,33 @@ void TestButtons() {
Check(RemoteButtons(left, right) == kButtonOne, "right stick up is 1"); Check(RemoteButtons(left, right) == kButtonOne, "right stick up is 1");
right.stick_y = -0.9f; right.stick_y = -0.9f;
Check(RemoteButtons(left, right) == kButtonTwo, "right stick down is 2"); Check(RemoteButtons(left, right) == kButtonTwo, "right stick down is 2");
right.stick_y = 0.3f;
Check(RemoteButtons(left, right) == 0, "a light push is no press");
right.stick_y = 0.0f; right.stick_y = 0.0f;
right.stick_x = -0.9f; right.stick_x = -0.9f;
Check(RemoteButtons(left, right) == kButtonMinus, "right stick left is -"); Check(RemoteButtons(left, right) == 0, "right stick left is unbound");
right.stick_x = 0.9f; right.stick_x = 0.9f;
Check(RemoteButtons(left, right) == kButtonPlus, "right stick right is +"); Check(RemoteButtons(left, right) == 0, "right stick right is unbound");
right.stick_x = 0.3f;
Check(RemoteButtons(left, right) == 0, "a light push is no press");
right = {}; right = {};
left.primary = true;
left.menu = true; left.menu = true;
Check(RemoteButtons(left, right) == (kButtonMinus | kButtonPlus), "left X and menu are - and +");
left = {};
left.squeeze = 0.8f; left.squeeze = 0.8f;
left.trigger = 0.7f; left.trigger = 0.7f;
Check(RemoteButtons(left, right) == (kButtonHome | kButtonC | kButtonZ), "left menu, grip, trigger are HOME, C, Z"); Check(RemoteButtons(left, right) == (kButtonC | kButtonZ), "left grip and trigger are C and Z");
// Left X and Y have no Wii button in the profile. // Left Y is the settings panel's button, and nothing presses HOME.
left = {}; left = {};
left.primary = true;
left.secondary = true; left.secondary = true;
Check(RemoteButtons(left, right) == 0, "left X/Y are unbound"); right.secondary = true;
right.thumbstick_click = true;
left.thumbstick_click = true;
Check(RemoteButtons(left, right) == 0, "left Y, right B and the stick clicks are unbound");
left = {};
right = {};
left.menu = true;
Check((RemoteButtons(left, right) & kButtonHome) == 0, "left menu is no longer HOME");
left.stick_x = 1.0f; left.stick_x = 1.0f;
left.stick_y = 1.0f; left.stick_y = 1.0f;