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");