diff --git a/OPENXR.md b/OPENXR.md
index 73ab9e8..57adc64 100644
--- a/OPENXR.md
+++ b/OPENXR.md
@@ -246,8 +246,7 @@ Touch controllers. The port is served through KPAD like a Bluetooth remote
| Right trigger | B |
| Right B | C (look behind) |
| Right stick up / down | 1 / 2 |
-| Left X | − |
-| Left menu | + |
+| Left X or left menu | + |
| Left stick | Nunchuk stick |
| Left trigger | Z |
| Left Y | Settings panel (not a Wii button) |
@@ -257,7 +256,9 @@ Touch controllers. The port is served through KPAD like a Bluetooth remote
| Left controller motion | Nunchuk accelerometer |
Analog inputs count as pressed past half travel. The grips, right stick left / right and the left
-stick click press no Wii button, and nothing presses HOME. C sits on right B rather than a grip
+stick click press no Wii button, and nothing presses − or HOME: Mario Kart Wii never reads −. Left
+X presses + as well as the left menu because the PlayStation VR2's controllers give no usable left
+menu, so pause would otherwise be out of reach there. C sits on right B rather than a grip
because hand steering holds a grip down for a whole corner, and C is the game's look-behind. The
game's Wii Remote rumble vibrates both controllers, subject to the ordinary controller-vibration
switch.
@@ -312,6 +313,15 @@ A/B → South/East, X/Y → West/North, index triggers → trigger axes, grips
applies. Left Y is GameCube Y here, so clicking both thumbsticks together opens the settings panel
instead. The right thumbstick click on its own still toggles the first-person camera.
+`"none"` makes the controllers nothing to the game, for playing with another controller (a desktop
+gamepad, a USB wheel or a Bluetooth Wii Remote). The virtual gamepad is unplugged, so it takes no
+port and the other controllers keep theirs; the game sees a switch to or from `"none"` as that
+controller disconnecting or connecting. The plugging follows the mode on the game thread, where the
+gamepad is already written, so the pacing thread never waits on SDL's joystick lock for it. The
+controllers still open the settings panel with left Y, point at it and toggle the first-person camera
+with a right thumbstick click; they press no game button, drive no Wii Remote, cannot take hold of
+the cockpit's wheel, and the game's rumble does not reach them.
+
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.
diff --git a/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt b/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
index f66d04a..63c9e61 100644
--- a/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
+++ b/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
@@ -302,7 +302,7 @@ class SettingsPage(
section(R.string.section_controls) {
choice(
R.string.controls_mode, R.string.controls_mode_helper,
- listOf(R.string.controls_mode_wii_remote, R.string.controls_mode_gamepad),
+ listOf(R.string.controls_mode_wii_remote, R.string.controls_mode_gamepad, R.string.controls_mode_none),
read = { stringIndex(it, "vr", "controller_mode", CONTROLLER_MODES) },
write = { c, index -> c.setString("vr", "controller_mode", CONTROLLER_MODES[index]) },
)
@@ -316,7 +316,7 @@ class SettingsPage(
info(R.string.controls_map_a, activity.getString(R.string.controls_map_a_value))
info(R.string.controls_map_b, activity.getString(R.string.controls_map_b_value))
info(R.string.controls_map_12, activity.getString(R.string.controls_map_12_value))
- info(R.string.controls_map_minus_plus, activity.getString(R.string.controls_map_minus_plus_value))
+ info(R.string.controls_map_plus, activity.getString(R.string.controls_map_plus_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_c, activity.getString(R.string.controls_map_c_value))
@@ -759,7 +759,7 @@ class SettingsPage(
val PERFORMANCE_LEVELS = listOf("boost", "sustained_high", "sustained_low", "power_savings", "default")
// runtime_config.h's kVrFoveationLevels, the default ("off") first.
val FOVEATION_LEVELS = listOf("off", "low", "medium", "high")
- val CONTROLLER_MODES = listOf("wii_remote", "gamepad")
+ val CONTROLLER_MODES = listOf("wii_remote", "gamepad", "none")
val INTERPOLATION_FPS = listOf(0L, 1L, 72L, 90L, 120L)
val RESOLUTIONS = listOf(1.0, 1.5, 2.0, 3.0, 4.0)
val SUPPORTED_RESOLUTIONS = listOf(0.0, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0)
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 89ec519..eb1aa05 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -588,9 +588,10 @@
Controllers
VR controllers
- What the Touch controllers are to the game.
+ What the Touch controllers are to the game. None leaves it to another controller, such as a Bluetooth gamepad; left Y still opens the settings panel.
Wii Remote + Nunchuk
Gamepad
+ None
Vibration
The game\'s rumble vibrates the controllers.
Wii Remote + Nunchuk buttons
@@ -600,8 +601,8 @@
Right trigger
1 / 2
Right stick up / down
- − / +
- Left X / left menu
+ +
+ Left X or left menu
Nunchuk stick
Left stick
Z
diff --git a/docs/quest-port.md b/docs/quest-port.md
index d135c3e..d3b0dc2 100644
--- a/docs/quest-port.md
+++ b/docs/quest-port.md
@@ -233,7 +233,9 @@ 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
+and every existing binding, dead zone and overlay setting applies. With
+`controller_mode = "none"` the virtual joystick is unplugged, leaving the ports
+to a Bluetooth gamepad; left Y still opens the settings panel. Bindings are
suggested for `oculus/touch_controller` and `khr/simple_controller`.
The manifest declares hand tracking (`horizonos.permission.HAND_TRACKING`, the
diff --git a/runtime/include/runtime_config.h b/runtime/include/runtime_config.h
index 7b993e0..bb76315 100644
--- a/runtime/include/runtime_config.h
+++ b/runtime/include/runtime_config.h
@@ -285,11 +285,12 @@ inline bool IsSupportedVrMirrorView(std::string_view value) {
}
// 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.
+// controller read as a GameCube pad, "none" nothing at all (they only open the
+// settings panel). Matches mkw::vr::OpenXRControllerMode.
inline constexpr const char* kVrControllerModeDefault = "wii_remote";
inline bool IsSupportedVrControllerMode(std::string_view value) {
- return value == "wii_remote" || value == "gamepad";
+ return value == "wii_remote" || value == "gamepad" || value == "none";
}
// SDL scancode name, spelled the way SDL_GetScancodeName produces it. An
// empty string leaves the recenter hotkey unbound, menu button only.
@@ -510,8 +511,8 @@ inline void EnsureConfigFile() {
"# 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"
+ "# ordinary controller read as a GameCube pad; \"none\" leaves the game\n"
+ "# to other controllers. Changeable live from 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"
diff --git a/runtime/include/vr/openxr_input.h b/runtime/include/vr/openxr_input.h
index c29c076..17e14f7 100644
--- a/runtime/include/vr/openxr_input.h
+++ b/runtime/include/vr/openxr_input.h
@@ -57,6 +57,11 @@ struct OpenXRPointerScreen {
// left / right thumbstick-> left / right stick axes, clicks -> stick buttons
// left menu -> Start
//
+// None: the virtual joystick is unplugged, so the controllers hold no port and
+// another controller (a desktop gamepad, a Bluetooth remote) plays in their
+// place. The game sees them idle, the way it does while the settings panel has
+// them; they still open the panel and toggle the first-person camera.
+//
// Both are bound for the Oculus Touch profile; khr/simple_controller gets
// select/menu and the poses so an unknown runtime still offers something.
//
@@ -90,7 +95,8 @@ struct OpenXRPointerScreen {
// run on the XR pacing thread. The Wii Remote bridge is internally locked, so
// the game thread may read it concurrently, and the virtual gamepad is only
// published here: OpenXRApplyVirtualGamepad() performs the SDL writes on the
-// game thread, keeping SDL's joystick lock off this thread entirely.
+// game thread, and plugs the joystick in or out as the controller mode
+// changes, keeping SDL's joystick lock off this thread during a session.
class OpenXRInput final {
public:
explicit OpenXRInput(OpenXRLogCallback logger = {});
@@ -131,7 +137,6 @@ public:
void ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration);
bool IsCreated() const noexcept { return m_created; }
- bool HasVirtualGamepad() const noexcept { return m_joystick_id != 0; }
const std::string& LastError() const noexcept { return m_last_error; }
private:
@@ -143,8 +148,6 @@ private:
void DestroyPoseSpaces();
void LoadInputClock();
XrTime InputSampleTime(XrTime predicted_display_time) const;
- bool AttachVirtualGamepad();
- void DetachVirtualGamepad();
// `withheld` publishes a remote at rest with nothing held and no pointer,
// while still tracking motion so releasing it does not read as a jolt.
void PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen& screen,
@@ -197,8 +200,6 @@ private:
bool m_panel_select_held = false;
std::array m_horizon{1.0f, 0.0f};
bool m_haptics_active[kHands]{};
- uint32_t m_joystick_id = 0; // SDL_JoystickID; 0 when detached
- void* m_joystick = nullptr; // SDL_Joystick*
ClickToggle m_first_person_click;
SteeringWheel m_wheel;
WheelReferenceLatch m_wheel_reference;
@@ -262,8 +263,9 @@ private:
std::string m_last_error;
};
-// Game thread: writes the gamepad the pacing thread last published, if any.
-// Does nothing when no OpenXR controllers are attached.
+// Game thread: plugs the virtual gamepad in or out as the controller mode asks,
+// then writes the gamepad the pacing thread last published, if any. Does
+// nothing when no OpenXR controllers are attached.
void OpenXRApplyVirtualGamepad() noexcept;
} // namespace mkw::vr
diff --git a/runtime/include/vr/openxr_settings_panel.h b/runtime/include/vr/openxr_settings_panel.h
index d0d038c..c5a8e10 100644
--- a/runtime/include/vr/openxr_settings_panel.h
+++ b/runtime/include/vr/openxr_settings_panel.h
@@ -77,10 +77,11 @@ struct Frame {
};
// 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.
+// that is left Y, which has no Wii button, and with the controllers nothing to
+// the game it is left Y too; as a gamepad left Y is GameCube Y, so both
+// thumbsticks clicked together stand in for it.
inline bool ToggleHeld(const std::array& hands, OpenXRControllerMode mode) noexcept {
- if (mode == OpenXRControllerMode::WiiRemote) {
+ if (mode != OpenXRControllerMode::Gamepad) {
return hands[0].secondary;
}
return hands[0].thumbstick_click && hands[1].thumbstick_click;
diff --git a/runtime/include/vr/openxr_wii_remote.h b/runtime/include/vr/openxr_wii_remote.h
index ffd28d3..666b2f6 100644
--- a/runtime/include/vr/openxr_wii_remote.h
+++ b/runtime/include/vr/openxr_wii_remote.h
@@ -24,6 +24,9 @@ enum class OpenXRControllerMode : uint8_t {
WiiRemote,
// One ordinary gamepad, read through PAD as a GameCube controller.
Gamepad,
+ // Nothing to the game: the virtual gamepad is unplugged, so the controllers
+ // hold no port. They still open the settings panel.
+ None,
};
struct OpenXRWiiRemoteSample {
@@ -40,7 +43,7 @@ struct OpenXRWiiRemoteSample {
float distance_meters = 0.0f;
};
-// Live switch between the two presentations; the settings bar and the launch
+// Live switch between the presentations; the settings bar and the launch
// configuration both go through it.
void OpenXRSetControllerMode(OpenXRControllerMode mode) noexcept;
OpenXRControllerMode OpenXRGetControllerMode() noexcept;
@@ -343,8 +346,10 @@ struct HandInputs {
// Adapted from DolphinXR's default "OpenXR Wii Remote" profile
// (Data/Sys/Profiles/Wiimote):
// right A -> A, right trigger -> B, right B -> C, right stick up/down -> 1/2,
-// left X -> -, left menu -> +,
+// left X and left menu -> +,
// left trigger -> Z, left stick -> Nunchuk stick.
+// + is on left X as well because the PlayStation VR2's controllers give no
+// usable left menu, and - has no button because Mario Kart Wii never reads it.
// HOME has no button; left Y opens the settings panel (openxr_settings_panel.h).
// The grips press nothing: they take hold of the wheel (openxr_driving.h), and C
// is the game's look-behind, which a hand on the wheel would otherwise hold down.
@@ -360,8 +365,7 @@ inline uint32_t RemoteButtons(const HandInputs& left, const HandInputs& right) n
press(right.secondary, kButtonC);
press(right.stick_y > kPressThreshold, kButtonOne);
press(right.stick_y < -kPressThreshold, kButtonTwo);
- press(left.primary, kButtonMinus);
- press(left.menu, kButtonPlus);
+ press(left.primary || left.menu, kButtonPlus);
press(left.trigger > kPressThreshold, kButtonZ);
return hold;
}
diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp
index f423206..2fbf47d 100644
--- a/runtime/src/settings_overlay.cpp
+++ b/runtime/src/settings_overlay.cpp
@@ -203,10 +203,11 @@ int g_vrMirrorView = [] {
}();
// Config spellings and menu labels for the VR controllers, index-matched to
// mkw::vr::OpenXRControllerMode.
-constexpr std::array kVrControllerModeNames{"wii_remote", "gamepad"};
-constexpr std::array kVrControllerModeLabels{"Wii Remote + Nunchuk", "Gamepad"};
+constexpr std::array kVrControllerModeNames{"wii_remote", "gamepad", "none"};
+constexpr std::array kVrControllerModeLabels{"Wii Remote + Nunchuk", "Gamepad", "None"};
static_assert(static_cast(mkw::vr::OpenXRControllerMode::WiiRemote) == 0);
static_assert(static_cast(mkw::vr::OpenXRControllerMode::Gamepad) == 1);
+static_assert(static_cast(mkw::vr::OpenXRControllerMode::None) == 2);
int g_vrControllerMode = [] {
const std::string mode = RuntimeConfigFile::VrControllerMode();
for (size_t i = 0; i < kVrControllerModeNames.size(); ++i) {
@@ -878,9 +879,11 @@ void DrawVrControllerSettings() {
"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, B = C (look behind), stick up/down = 1/2\n"
- " Left: stick = Nunchuk stick, trigger = Z, X = -, menu = +, Y = settings panel\n"
+ " Left: stick = Nunchuk stick, trigger = Z, X or menu = +, Y = settings panel\n"
" The grips press nothing; they take hold of the wheel with hand steering.\n"
"Gamepad: both controllers are one ordinary controller, read as a GameCube pad.\n"
+ "None: the game does not see the VR controllers at all and they take no controller port,\n"
+ "so a desktop gamepad or a Wii Remote plays instead. Left Y still opens the settings panel.\n"
"Applies immediately; the game sees the controller change as a reconnection.");
}
if (mkw::vr::OpenXRIsRunning() &&
@@ -2322,9 +2325,9 @@ void DrawVrSettingsPanelWindow() {
}
ImGui::TextDisabled("Aim and pull a trigger to change a setting, push a thumbstick to scroll.");
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");
+ mkw::vr::OpenXRGetControllerMode() == mkw::vr::OpenXRControllerMode::Gamepad
+ ? "Click both thumbsticks or press Menu"
+ : "Press left Y or Menu");
ImGui::Separator();
if (ImGui::BeginTabBar("Settings")) {
const auto tab = [](const char* label, void (*draw)()) {
diff --git a/runtime/src/vr/openxr_input.cpp b/runtime/src/vr/openxr_input.cpp
index a1213c3..8dce000 100644
--- a/runtime/src/vr/openxr_input.cpp
+++ b/runtime/src/vr/openxr_input.cpp
@@ -22,6 +22,7 @@
#include
#include
+#include
#include
#include
#include
@@ -49,8 +50,15 @@ namespace {
// slot that passes. So it leaves the gamepad here, and the game thread writes
// it to SDL where it already polls controllers.
//
-// m_sdl is the lock the SDL work runs under; the pacing thread only ever takes
-// m_state, and only for the copy. Both are taken in that order.
+// The relay also owns the virtual joystick. Attaching and detaching it take the
+// same lock, so a live switch to or from the None controller mode (which
+// unplugs it, so the VR controllers hold no port) is followed on the game
+// thread too; only the input's creation and destruction plug it in or out
+// where they run.
+//
+// m_sdl is the lock the SDL work, and the joystick, are under; the pacing
+// thread only ever takes m_state, and only for the copy. Both are taken in that
+// order.
class VirtualGamepadRelay {
public:
struct Pad {
@@ -58,18 +66,29 @@ public:
std::array buttons{};
};
- void Attach(SDL_Joystick* joystick) {
+ // The controllers' input exists: plugs the joystick in unless the mode is
+ // None. `logger` reports a joystick SDL refuses, from whichever thread.
+ void Open(OpenXRLogCallback logger) {
std::scoped_lock lock(m_sdl, m_state);
- m_joystick = joystick;
+ m_logger = std::move(logger);
+ m_open = true;
+ m_attach_failed = false;
m_pending = false;
+ FollowControllerMode();
}
- void Detach() {
+ // The controllers' input is going away: unplugs the joystick.
+ void Close() {
std::scoped_lock lock(m_sdl, m_state);
- m_joystick = nullptr;
+ m_open = false;
+ Detach();
m_pending = false;
+ m_logger = {};
}
+ // The plugged-in joystick's SDL_JoystickID, 0 while unplugged.
+ uint32_t JoystickId() const noexcept { return m_id.load(std::memory_order_relaxed); }
+
// Pacing thread.
void Publish(const Pad& pad) {
std::lock_guard lock(m_state);
@@ -77,35 +96,106 @@ public:
m_pending = true;
}
- // Game thread. Holding m_sdl here is what keeps Detach from closing the
+ // Game thread. Holding m_sdl here is what keeps Close from closing the
// joystick underneath the writes.
void Apply() {
std::lock_guard sdl(m_sdl);
+ FollowControllerMode();
Pad pad;
- SDL_Joystick* joystick = nullptr;
{
std::lock_guard lock(m_state);
if (!m_pending || m_joystick == nullptr) {
return;
}
pad = m_pad;
- joystick = m_joystick;
m_pending = false;
}
for (int axis = 0; axis < SDL_GAMEPAD_AXIS_COUNT; ++axis) {
- SDL_SetJoystickVirtualAxis(joystick, static_cast(axis),
+ SDL_SetJoystickVirtualAxis(m_joystick, static_cast(axis),
pad.axes[static_cast(axis)]);
}
for (int button = 0; button < SDL_GAMEPAD_BUTTON_COUNT; ++button) {
- SDL_SetJoystickVirtualButton(joystick, static_cast(button),
+ SDL_SetJoystickVirtualButton(m_joystick, static_cast(button),
pad.buttons[static_cast(button)]);
}
}
private:
+ // m_sdl held. A joystick SDL refused is not asked for again until the mode
+ // unplugs it or the input is created anew.
+ void FollowControllerMode() {
+ if (!m_open) {
+ return;
+ }
+ if (OpenXRGetControllerMode() == OpenXRControllerMode::None) {
+ m_attach_failed = false;
+ Detach();
+ } else if (m_joystick == nullptr && !m_attach_failed) {
+ m_attach_failed = !Attach();
+ }
+ }
+
+ bool Attach() {
+ SDL_VirtualJoystickDesc desc;
+ SDL_INIT_INTERFACE(&desc);
+ desc.type = SDL_JOYSTICK_TYPE_GAMEPAD;
+ desc.naxes = SDL_GAMEPAD_AXIS_COUNT;
+ desc.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
+ desc.button_mask = (1u << SDL_GAMEPAD_BUTTON_SOUTH) | (1u << SDL_GAMEPAD_BUTTON_EAST) |
+ (1u << SDL_GAMEPAD_BUTTON_WEST) | (1u << SDL_GAMEPAD_BUTTON_NORTH) |
+ (1u << SDL_GAMEPAD_BUTTON_START) | (1u << SDL_GAMEPAD_BUTTON_LEFT_STICK) |
+ (1u << SDL_GAMEPAD_BUTTON_RIGHT_STICK) |
+ (1u << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) |
+ (1u << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
+ desc.axis_mask = (1u << SDL_GAMEPAD_AXIS_LEFTX) | (1u << SDL_GAMEPAD_AXIS_LEFTY) |
+ (1u << SDL_GAMEPAD_AXIS_RIGHTX) | (1u << SDL_GAMEPAD_AXIS_RIGHTY) |
+ (1u << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1u << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
+ desc.name = "OpenXR Touch Controllers";
+ const SDL_JoystickID id = SDL_AttachVirtualJoystick(&desc);
+ if (id == 0) {
+ Refused("SDL_AttachVirtualJoystick");
+ return false;
+ }
+ SDL_Joystick* joystick = SDL_OpenJoystick(id);
+ if (joystick == nullptr) {
+ Refused("SDL_OpenJoystick");
+ SDL_DetachVirtualJoystick(id);
+ return false;
+ }
+ m_joystick = joystick;
+ m_id.store(id, std::memory_order_relaxed);
+ return true;
+ }
+
+ void Detach() {
+ const SDL_JoystickID id = m_id.exchange(0, std::memory_order_relaxed);
+ if (m_joystick != nullptr) {
+ SDL_CloseJoystick(m_joystick);
+ m_joystick = nullptr;
+ }
+ if (id != 0) {
+ SDL_DetachVirtualJoystick(id);
+ }
+ }
+
+ void Refused(const char* operation) const {
+ if (!m_logger) {
+ return;
+ }
+ try {
+ m_logger(OpenXRLogLevel::Warning, std::string(operation) + " failed: " + SDL_GetError() +
+ "; OpenXR controllers will not reach the game");
+ } catch (...) {
+ }
+ }
+
std::mutex m_sdl;
std::mutex m_state;
+ OpenXRLogCallback m_logger;
+ bool m_open = false;
+ bool m_attach_failed = false;
SDL_Joystick* m_joystick = nullptr;
+ std::atomic m_id{0};
Pad m_pad;
bool m_pending = false;
};
@@ -273,13 +363,18 @@ bool OpenXRInput::Create(OpenXRRuntime& runtime) {
CreatePoseSpaces();
LoadInputClock();
LoadHandTracking();
- if (!AttachVirtualGamepad()) {
- Log(OpenXRLogLevel::Warning,
- "SDL refused the virtual gamepad; OpenXR controllers will not reach the game");
+ Relay().Open(m_logger);
+ switch (OpenXRGetControllerMode()) {
+ case OpenXRControllerMode::WiiRemote:
+ Log(OpenXRLogLevel::Info, "OpenXR controller actions attached (Wii Remote + Nunchuk)");
+ break;
+ case OpenXRControllerMode::Gamepad:
+ Log(OpenXRLogLevel::Info, "OpenXR controller actions attached (gamepad)");
+ break;
+ case OpenXRControllerMode::None:
+ Log(OpenXRLogLevel::Info, "OpenXR controller actions attached (none: the game does not see them)");
+ break;
}
- Log(OpenXRLogLevel::Info, OpenXRGetControllerMode() == OpenXRControllerMode::WiiRemote
- ? "OpenXR controller actions attached (Wii Remote + Nunchuk)"
- : "OpenXR controller actions attached (gamepad)");
return true;
}
@@ -739,54 +834,6 @@ void OpenXRApplyVirtualGamepad() noexcept {
Relay().Apply();
}
-bool OpenXRInput::AttachVirtualGamepad() {
- SDL_VirtualJoystickDesc desc;
- SDL_INIT_INTERFACE(&desc);
- desc.type = SDL_JOYSTICK_TYPE_GAMEPAD;
- desc.naxes = SDL_GAMEPAD_AXIS_COUNT;
- desc.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
- desc.button_mask = (1u << SDL_GAMEPAD_BUTTON_SOUTH) | (1u << SDL_GAMEPAD_BUTTON_EAST) |
- (1u << SDL_GAMEPAD_BUTTON_WEST) | (1u << SDL_GAMEPAD_BUTTON_NORTH) |
- (1u << SDL_GAMEPAD_BUTTON_START) | (1u << SDL_GAMEPAD_BUTTON_LEFT_STICK) |
- (1u << SDL_GAMEPAD_BUTTON_RIGHT_STICK) |
- (1u << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER) |
- (1u << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
- desc.axis_mask = (1u << SDL_GAMEPAD_AXIS_LEFTX) | (1u << SDL_GAMEPAD_AXIS_LEFTY) |
- (1u << SDL_GAMEPAD_AXIS_RIGHTX) | (1u << SDL_GAMEPAD_AXIS_RIGHTY) |
- (1u << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1u << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
- desc.name = "OpenXR Touch Controllers";
- const SDL_JoystickID id = SDL_AttachVirtualJoystick(&desc);
- if (id == 0) {
- m_last_error = std::string("SDL_AttachVirtualJoystick failed: ") + SDL_GetError();
- Log(OpenXRLogLevel::Warning, m_last_error);
- return false;
- }
- SDL_Joystick* joystick = SDL_OpenJoystick(id);
- if (joystick == nullptr) {
- m_last_error = std::string("SDL_OpenJoystick failed: ") + SDL_GetError();
- Log(OpenXRLogLevel::Warning, m_last_error);
- SDL_DetachVirtualJoystick(id);
- return false;
- }
- m_joystick_id = id;
- m_joystick = joystick;
- Relay().Attach(joystick);
- return true;
-}
-
-void OpenXRInput::DetachVirtualGamepad() {
- // Before the handle goes: the game thread may be writing through it.
- Relay().Detach();
- if (m_joystick != nullptr) {
- SDL_CloseJoystick(static_cast(m_joystick));
- m_joystick = nullptr;
- }
- if (m_joystick_id != 0) {
- SDL_DetachVirtualJoystick(m_joystick_id);
- m_joystick_id = 0;
- }
-}
-
void OpenXRInput::Destroy() {
// The game must stop reading a remote whose controllers are going away.
OpenXRWithdrawWiiRemote();
@@ -794,7 +841,7 @@ void OpenXRInput::Destroy() {
if (m_created) {
StopRumble();
}
- DetachVirtualGamepad();
+ Relay().Close();
DestroyHandTrackers();
m_create_hand_tracker = nullptr;
m_destroy_hand_tracker = nullptr;
@@ -842,7 +889,7 @@ void OpenXRInput::Idle() {
}
m_pointer.Reset();
m_horizon = {1.0f, 0.0f};
- OpenXRPublishWiiRemote(m_joystick_id, OpenXRWiiRemoteSample{});
+ OpenXRPublishWiiRemote(Relay().JoystickId(), OpenXRWiiRemoteSample{});
// The panel stays as it was; only what the controllers were holding is forgotten.
m_panel_controls.Reset();
m_last_input_time = 0;
@@ -855,9 +902,7 @@ void OpenXRInput::Idle() {
ResetDriving();
StopRumble();
// Nothing stays held on the gamepad either while input is away.
- if (m_joystick != nullptr) {
- Relay().Publish({});
- }
+ Relay().Publish({});
}
void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen& screen,
@@ -1015,8 +1060,11 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
// is written back.
const bool was_open = OpenXRSettingsPanelOpen();
bool open = was_open;
- const settings_panel::Frame panel =
- m_panel_controls.Update(panel_hands, open, dt_seconds, OpenXRGetControllerMode());
+ const OpenXRControllerMode mode = OpenXRGetControllerMode();
+ const settings_panel::Frame panel = m_panel_controls.Update(panel_hands, open, dt_seconds, mode);
+ // While the panel has the controllers, and always when they are nothing to
+ // the game, the game sees them idle.
+ const bool withheld = panel.withheld || mode == OpenXRControllerMode::None;
// Pointer first: the game thread reads it as soon as it sees the panel open.
PublishSettingsPanel(input_time, settings_panel, panel);
if (open != was_open) {
@@ -1036,7 +1084,7 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
// The cockpit's wheel before the game reads the controllers: a held wheel
// steers through the left stick and keeps its grips from the game.
- UpdateDriving(predicted_display_time, seat, hands, panel.withheld);
+ UpdateDriving(predicted_display_time, seat, hands, withheld);
#if defined(__ANDROID__)
// Bare hands in the cockpit. While one of them holds the wheel it holds the
@@ -1044,7 +1092,7 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
// pause menu, the results, coasting) a right pinch stays A. The game's own
// pointer cannot tell the two apart: MKW keeps it on while driving.
const bool cockpit_hands =
- hand_tracking_on && m_driving.cockpit_active && m_driving.hand_steering && !panel.withheld;
+ hand_tracking_on && m_driving.cockpit_active && m_driving.hand_steering && !withheld;
std::array bare_held{};
for (uint32_t hand = 0; hand < kHands; ++hand) {
bare_held[hand] = cockpit_hands && m_bare_latch[hand].Bare() && m_wheel_held[hand];
@@ -1069,7 +1117,7 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
flick[hand] = {m_bare_latch[hand].Tracked(), m_wheel_held[hand],
m_joint_frame.seat_from_joint[hand][hand_tracking::kPalm][7]};
}
- if (m_flick.Update(flick, dt_seconds) && OpenXRGetControllerMode() == OpenXRControllerMode::WiiRemote) {
+ if (m_flick.Update(flick, dt_seconds) && mode == OpenXRControllerMode::WiiRemote) {
m_flick_start = input_time;
}
} else {
@@ -1084,14 +1132,14 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
m_injected_flick_held = injected_flick;
#endif
- // While the panel has the controllers, the game sees them idle.
+ // What the game sees of the controllers: nothing held while they are withheld.
static const std::array kIdleHands{};
- const auto& game_hands = panel.withheld ? kIdleHands : hands;
+ const auto& game_hands = withheld ? kIdleHands : hands;
const wii_remote::HandInputs& left = game_hands[0];
const wii_remote::HandInputs& right = game_hands[1];
- const auto injected = [&panel](const char* button) { return !panel.withheld && Injected(button); };
+ const auto injected = [withheld](const char* button) { return !withheld && Injected(button); };
- if (m_joystick != nullptr) {
+ if (Relay().JoystickId() != 0) {
VirtualGamepadRelay::Pad pad;
// OpenXR thumbsticks report +Y up; SDL gamepads report +Y down.
pad.axes[SDL_GAMEPAD_AXIS_LEFTX] = ToAxis(left.stick_x);
@@ -1113,8 +1161,8 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
Relay().Publish(pad);
}
- PublishWiiRemote(input_time, screen, game_hands, panel.withheld ? 0u : InjectedWiiRemoteButtons(),
- panel.withheld);
+ PublishWiiRemote(input_time, screen, game_hands, withheld ? 0u : InjectedWiiRemoteButtons(),
+ withheld);
UpdateRumble();
}
@@ -1215,7 +1263,7 @@ void OpenXRInput::PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen&
sample.nunchuk_acc = OpenXRWiiRemoteSample{}.nunchuk_acc;
m_pointer.Reset();
m_flick_start = 0;
- OpenXRPublishWiiRemote(m_joystick_id, sample);
+ OpenXRPublishWiiRemote(Relay().JoystickId(), sample);
return;
}
if (m_flick_start != 0) {
@@ -1252,7 +1300,7 @@ void OpenXRInput::PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen&
Log(OpenXRLogLevel::Info, "OpenXR Wii Remote pointer reached the virtual screen");
}
}
- OpenXRPublishWiiRemote(m_joystick_id, sample);
+ OpenXRPublishWiiRemote(Relay().JoystickId(), sample);
}
void OpenXRInput::ResetDriving() {
@@ -1427,7 +1475,7 @@ void OpenXRInput::UpdateDriving(XrTime display_time, const driving::SeatFrame& s
}
void OpenXRInput::UpdateRumble() {
- if (!OpenXRWiiRemoteRumbleRequested() || !OpenXRWiiRemoteOwnsGamepad(m_joystick_id)) {
+ if (!OpenXRWiiRemoteRumbleRequested() || !OpenXRWiiRemoteOwnsGamepad(Relay().JoystickId())) {
StopRumble();
return;
}
diff --git a/runtime/tests/vr_settings_panel_tests.cpp b/runtime/tests/vr_settings_panel_tests.cpp
index 60219aa..429c785 100644
--- a/runtime/tests/vr_settings_panel_tests.cpp
+++ b/runtime/tests/vr_settings_panel_tests.cpp
@@ -34,6 +34,7 @@ void CheckNear(float actual, float expected, const char* what, float tolerance =
constexpr float kDt = 1.0f / 90.0f;
constexpr OpenXRControllerMode kWiiRemote = OpenXRControllerMode::WiiRemote;
constexpr OpenXRControllerMode kGamepad = OpenXRControllerMode::Gamepad;
+constexpr OpenXRControllerMode kNone = OpenXRControllerMode::None;
std::array Released() {
return {};
@@ -103,6 +104,20 @@ void ChordOpensAndClosesOnceAsAGamepad() {
Check(!frame.withheld, "the game gets the controllers back once everything is released");
}
+void LeftYOpensThePanelWithoutAGameController() {
+ Controls controls;
+ bool open = false;
+
+ Frame frame = controls.Update(Chord(), open, kDt, kNone);
+ Check(!open, "with no game controller the thumbstick chord does not open the panel");
+ controls.Update(Released(), open, kDt, kNone);
+ frame = controls.Update(LeftY(), open, kDt, kNone);
+ Check(open && frame.open, "with no game controller left Y opens the panel");
+ controls.Update(Released(), open, kDt, kNone);
+ frame = controls.Update(LeftY(), open, kDt, kNone);
+ Check(!open && !frame.open, "and closes it again");
+}
+
void MenuClosesAndItsPressStaysOutOfTheGame() {
Controls controls;
bool open = false;
@@ -213,6 +228,7 @@ void BridgeAccumulatesWheelUntilTaken() {
int main() {
LeftYOpensAndClosesOnceAsAWiiRemote();
ChordOpensAndClosesOnceAsAGamepad();
+ LeftYOpensThePanelWithoutAGameController();
MenuClosesAndItsPressStaysOutOfTheGame();
SelectWaitsForAReleaseAndFollowsTheTrigger();
ThumbstickScrolls();
diff --git a/runtime/tests/vr_wii_remote_tests.cpp b/runtime/tests/vr_wii_remote_tests.cpp
index 6e7a702..7ffe66d 100644
--- a/runtime/tests/vr_wii_remote_tests.cpp
+++ b/runtime/tests/vr_wii_remote_tests.cpp
@@ -259,8 +259,12 @@ void TestButtons() {
right = {};
left.primary = true;
+ Check(RemoteButtons(left, right) == kButtonPlus, "left X is +, and nothing presses -");
+ left = {};
left.menu = true;
- Check(RemoteButtons(left, right) == (kButtonMinus | kButtonPlus), "left X and menu are - and +");
+ Check(RemoteButtons(left, right) == kButtonPlus, "left menu is + too");
+ left.primary = true;
+ Check(RemoteButtons(left, right) == kButtonPlus, "left X and menu together are one +");
left = {};
left.trigger = 0.7f;
Check(RemoteButtons(left, right) == kButtonZ, "left trigger is Z");