Add support for "none" controller mode: unplug virtual joystick and allow alternative controllers

This commit is contained in:
iChris4 committed 2026-09-26 21:53:48 +02:00
1 parent 171702bef4
commit c536f0087d
12 files changed
+212 -120

No files matched your search

+13 -3
View File
@@ -246,8 +246,7 @@ Touch controllers. The port is served through KPAD like a Bluetooth remote
| Right trigger | B | | Right trigger | B |
| Right B | C (look behind) | | Right B | C (look behind) |
| Right stick up / down | 1 / 2 | | Right stick up / down | 1 / 2 |
| Left X | − | | Left X or left menu | + |
| Left menu | + |
| Left stick | Nunchuk stick | | Left stick | Nunchuk stick |
| Left trigger | Z | | Left trigger | Z |
| Left Y | Settings panel (not a Wii button) | | 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 | | Left controller motion | Nunchuk accelerometer |
Analog inputs count as pressed past half travel. The grips, right stick left / right and the left 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 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 game's Wii Remote rumble vibrates both controllers, subject to the ordinary controller-vibration
switch. 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 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. 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 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
raycast and debounce, the picture placement and the button profile without a headset. raycast and debounce, the picture placement and the button profile without a headset.
@@ -302,7 +302,7 @@ class SettingsPage(
section(R.string.section_controls) { section(R.string.section_controls) {
choice( choice(
R.string.controls_mode, R.string.controls_mode_helper, 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) }, read = { stringIndex(it, "vr", "controller_mode", CONTROLLER_MODES) },
write = { c, index -> c.setString("vr", "controller_mode", CONTROLLER_MODES[index]) }, 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_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_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_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_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))
@@ -759,7 +759,7 @@ class SettingsPage(
val PERFORMANCE_LEVELS = listOf("boost", "sustained_high", "sustained_low", "power_savings", "default") val PERFORMANCE_LEVELS = listOf("boost", "sustained_high", "sustained_low", "power_savings", "default")
// runtime_config.h's kVrFoveationLevels, the default ("off") first. // runtime_config.h's kVrFoveationLevels, the default ("off") first.
val FOVEATION_LEVELS = listOf("off", "low", "medium", "high") 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 INTERPOLATION_FPS = listOf(0L, 1L, 72L, 90L, 120L)
val RESOLUTIONS = listOf(1.0, 1.5, 2.0, 3.0, 4.0) 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) val SUPPORTED_RESOLUTIONS = listOf(0.0, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0)
+4 -3
View File
@@ -588,9 +588,10 @@
<!-- Controls tab --> <!-- Controls tab -->
<string name="section_controls">Controllers</string> <string name="section_controls">Controllers</string>
<string name="controls_mode">VR controllers</string> <string name="controls_mode">VR controllers</string>
<string name="controls_mode_helper">What the Touch controllers are to the game.</string> <string name="controls_mode_helper">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.</string>
<string name="controls_mode_wii_remote">Wii Remote + Nunchuk</string> <string name="controls_mode_wii_remote">Wii Remote + Nunchuk</string>
<string name="controls_mode_gamepad">Gamepad</string> <string name="controls_mode_gamepad">Gamepad</string>
<string name="controls_mode_none">None</string>
<string name="controls_rumble">Vibration</string> <string name="controls_rumble">Vibration</string>
<string name="controls_rumble_helper">The game\'s rumble vibrates the controllers.</string> <string name="controls_rumble_helper">The game\'s rumble vibrates the controllers.</string>
<string name="section_controls_mapping">Wii Remote + Nunchuk buttons</string> <string name="section_controls_mapping">Wii Remote + Nunchuk buttons</string>
@@ -600,8 +601,8 @@
<string name="controls_map_b_value">Right trigger</string> <string name="controls_map_b_value">Right trigger</string>
<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_plus">+</string>
<string name="controls_map_minus_plus_value">Left X / left menu</string> <string name="controls_map_plus_value">Left X or 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>
+3 -1
View File
@@ -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: 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 → A/B → South/East, X/Y → West/North, index triggers → trigger axes, grips →
shoulders, thumbsticks → sticks (clicks → stick buttons), left menu → Start, 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`. suggested for `oculus/touch_controller` and `khr/simple_controller`.
The manifest declares hand tracking (`horizonos.permission.HAND_TRACKING`, the The manifest declares hand tracking (`horizonos.permission.HAND_TRACKING`, the
+5 -4
View File
@@ -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 // 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 // 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 constexpr const char* kVrControllerModeDefault = "wii_remote";
inline bool IsSupportedVrControllerMode(std::string_view value) { 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 // SDL scancode name, spelled the way SDL_GetScancodeName produces it. An
// empty string leaves the recenter hotkey unbound, menu button only. // 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" "# 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" "# 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" "# the virtual screen) plus a Nunchuk (left hand); \"gamepad\" is one\n"
"# ordinary controller read as a GameCube pad. Changeable live from\n" "# ordinary controller read as a GameCube pad; \"none\" leaves the game\n"
"# the F10 menu.\n" "# to other controllers. Changeable live from the F10 menu.\n"
"controller_mode = \"wii_remote\"\n" "controller_mode = \"wii_remote\"\n"
"# VR interpolation: 0 = Off, 1 = Auto, or 72/90/120 FPS. Live.\n" "# VR interpolation: 0 = Off, 1 = Auto, or 72/90/120 FPS. Live.\n"
"frame_interpolation_fps = 0\n" "frame_interpolation_fps = 0\n"
+10 -8
View File
@@ -57,6 +57,11 @@ struct OpenXRPointerScreen {
// left / right thumbstick-> left / right stick axes, clicks -> stick buttons // left / right thumbstick-> left / right stick axes, clicks -> stick buttons
// left menu -> Start // 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 // 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.
// //
@@ -90,7 +95,8 @@ struct OpenXRPointerScreen {
// run on the XR pacing thread. The Wii Remote bridge is internally locked, so // 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 // the game thread may read it concurrently, and the virtual gamepad is only
// published here: OpenXRApplyVirtualGamepad() performs the SDL writes on the // 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 { class OpenXRInput final {
public: public:
explicit OpenXRInput(OpenXRLogCallback logger = {}); explicit OpenXRInput(OpenXRLogCallback logger = {});
@@ -131,7 +137,6 @@ public:
void ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration); void ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration);
bool IsCreated() const noexcept { return m_created; } 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; } const std::string& LastError() const noexcept { return m_last_error; }
private: private:
@@ -143,8 +148,6 @@ private:
void DestroyPoseSpaces(); void DestroyPoseSpaces();
void LoadInputClock(); void LoadInputClock();
XrTime InputSampleTime(XrTime predicted_display_time) const; XrTime InputSampleTime(XrTime predicted_display_time) const;
bool AttachVirtualGamepad();
void DetachVirtualGamepad();
// `withheld` publishes a remote at rest with nothing held and no pointer, // `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. // while still tracking motion so releasing it does not read as a jolt.
void PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen& screen, void PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen& screen,
@@ -197,8 +200,6 @@ private:
bool m_panel_select_held = false; bool m_panel_select_held = false;
std::array<float, 2> m_horizon{1.0f, 0.0f}; std::array<float, 2> m_horizon{1.0f, 0.0f};
bool m_haptics_active[kHands]{}; 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; ClickToggle m_first_person_click;
SteeringWheel m_wheel; SteeringWheel m_wheel;
WheelReferenceLatch m_wheel_reference; WheelReferenceLatch m_wheel_reference;
@@ -262,8 +263,9 @@ private:
std::string m_last_error; std::string m_last_error;
}; };
// Game thread: writes the gamepad the pacing thread last published, if any. // Game thread: plugs the virtual gamepad in or out as the controller mode asks,
// Does nothing when no OpenXR controllers are attached. // then writes the gamepad the pacing thread last published, if any. Does
// nothing when no OpenXR controllers are attached.
void OpenXRApplyVirtualGamepad() noexcept; void OpenXRApplyVirtualGamepad() noexcept;
} // namespace mkw::vr } // namespace mkw::vr
+4 -3
View File
@@ -77,10 +77,11 @@ struct Frame {
}; };
// Whether the button that opens and closes the panel is held. As a Wii Remote // 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 // that is left Y, which has no Wii button, and with the controllers nothing to
// both thumbsticks clicked together stand in for it. // 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<HandInputs, 2>& hands, OpenXRControllerMode mode) noexcept { inline bool ToggleHeld(const std::array<HandInputs, 2>& hands, OpenXRControllerMode mode) noexcept {
if (mode == OpenXRControllerMode::WiiRemote) { if (mode != OpenXRControllerMode::Gamepad) {
return hands[0].secondary; return hands[0].secondary;
} }
return hands[0].thumbstick_click && hands[1].thumbstick_click; return hands[0].thumbstick_click && hands[1].thumbstick_click;
+8 -4
View File
@@ -24,6 +24,9 @@ enum class OpenXRControllerMode : uint8_t {
WiiRemote, WiiRemote,
// One ordinary gamepad, read through PAD as a GameCube controller. // One ordinary gamepad, read through PAD as a GameCube controller.
Gamepad, 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 { struct OpenXRWiiRemoteSample {
@@ -40,7 +43,7 @@ struct OpenXRWiiRemoteSample {
float distance_meters = 0.0f; 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. // configuration both go through it.
void OpenXRSetControllerMode(OpenXRControllerMode mode) noexcept; void OpenXRSetControllerMode(OpenXRControllerMode mode) noexcept;
OpenXRControllerMode OpenXRGetControllerMode() noexcept; OpenXRControllerMode OpenXRGetControllerMode() noexcept;
@@ -343,8 +346,10 @@ struct HandInputs {
// Adapted from DolphinXR's default "OpenXR Wii Remote" profile // Adapted from DolphinXR's default "OpenXR Wii Remote" profile
// (Data/Sys/Profiles/Wiimote): // (Data/Sys/Profiles/Wiimote):
// right A -> A, right trigger -> B, right B -> C, right stick up/down -> 1/2, // 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. // 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). // 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 // 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. // 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.secondary, kButtonC);
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(left.primary, kButtonMinus); press(left.primary || left.menu, kButtonPlus);
press(left.menu, kButtonPlus);
press(left.trigger > kPressThreshold, kButtonZ); press(left.trigger > kPressThreshold, kButtonZ);
return hold; return hold;
} }
+9 -6
View File
@@ -203,10 +203,11 @@ int g_vrMirrorView = [] {
}(); }();
// Config spellings and menu labels for the VR controllers, index-matched to // Config spellings and menu labels for the VR controllers, index-matched to
// mkw::vr::OpenXRControllerMode. // mkw::vr::OpenXRControllerMode.
constexpr std::array<const char*, 2> kVrControllerModeNames{"wii_remote", "gamepad"}; constexpr std::array<const char*, 3> kVrControllerModeNames{"wii_remote", "gamepad", "none"};
constexpr std::array<const char*, 2> kVrControllerModeLabels{"Wii Remote + Nunchuk", "Gamepad"}; constexpr std::array<const char*, 3> kVrControllerModeLabels{"Wii Remote + Nunchuk", "Gamepad", "None"};
static_assert(static_cast<int>(mkw::vr::OpenXRControllerMode::WiiRemote) == 0); static_assert(static_cast<int>(mkw::vr::OpenXRControllerMode::WiiRemote) == 0);
static_assert(static_cast<int>(mkw::vr::OpenXRControllerMode::Gamepad) == 1); static_assert(static_cast<int>(mkw::vr::OpenXRControllerMode::Gamepad) == 1);
static_assert(static_cast<int>(mkw::vr::OpenXRControllerMode::None) == 2);
int g_vrControllerMode = [] { int g_vrControllerMode = [] {
const std::string mode = RuntimeConfigFile::VrControllerMode(); const std::string mode = RuntimeConfigFile::VrControllerMode();
for (size_t i = 0; i < kVrControllerModeNames.size(); ++i) { 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 " "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, B = C (look behind), stick up/down = 1/2\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" " 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" "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."); "Applies immediately; the game sees the controller change as a reconnection.");
} }
if (mkw::vr::OpenXRIsRunning() && 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("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.", ImGui::TextDisabled("%s to close. The game does not see the controllers meanwhile.",
mkw::vr::OpenXRGetControllerMode() == mkw::vr::OpenXRControllerMode::WiiRemote mkw::vr::OpenXRGetControllerMode() == mkw::vr::OpenXRControllerMode::Gamepad
? "Press left Y or Menu" ? "Click both thumbsticks or press Menu"
: "Click both thumbsticks or press Menu"); : "Press left Y or 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)()) {
+132 -84
View File
@@ -22,6 +22,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <atomic>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
@@ -49,8 +50,15 @@ namespace {
// slot that passes. So it leaves the gamepad here, and the game thread writes // slot that passes. So it leaves the gamepad here, and the game thread writes
// it to SDL where it already polls controllers. // it to SDL where it already polls controllers.
// //
// m_sdl is the lock the SDL work runs under; the pacing thread only ever takes // The relay also owns the virtual joystick. Attaching and detaching it take the
// m_state, and only for the copy. Both are taken in that order. // 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 { class VirtualGamepadRelay {
public: public:
struct Pad { struct Pad {
@@ -58,18 +66,29 @@ public:
std::array<bool, SDL_GAMEPAD_BUTTON_COUNT> buttons{}; std::array<bool, SDL_GAMEPAD_BUTTON_COUNT> 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); 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; 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); std::scoped_lock lock(m_sdl, m_state);
m_joystick = nullptr; m_open = false;
Detach();
m_pending = false; 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. // Pacing thread.
void Publish(const Pad& pad) { void Publish(const Pad& pad) {
std::lock_guard lock(m_state); std::lock_guard lock(m_state);
@@ -77,35 +96,106 @@ public:
m_pending = true; 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. // joystick underneath the writes.
void Apply() { void Apply() {
std::lock_guard sdl(m_sdl); std::lock_guard sdl(m_sdl);
FollowControllerMode();
Pad pad; Pad pad;
SDL_Joystick* joystick = nullptr;
{ {
std::lock_guard lock(m_state); std::lock_guard lock(m_state);
if (!m_pending || m_joystick == nullptr) { if (!m_pending || m_joystick == nullptr) {
return; return;
} }
pad = m_pad; pad = m_pad;
joystick = m_joystick;
m_pending = false; m_pending = false;
} }
for (int axis = 0; axis < SDL_GAMEPAD_AXIS_COUNT; ++axis) { for (int axis = 0; axis < SDL_GAMEPAD_AXIS_COUNT; ++axis) {
SDL_SetJoystickVirtualAxis(joystick, static_cast<SDL_GamepadAxis>(axis), SDL_SetJoystickVirtualAxis(m_joystick, static_cast<SDL_GamepadAxis>(axis),
pad.axes[static_cast<size_t>(axis)]); pad.axes[static_cast<size_t>(axis)]);
} }
for (int button = 0; button < SDL_GAMEPAD_BUTTON_COUNT; ++button) { for (int button = 0; button < SDL_GAMEPAD_BUTTON_COUNT; ++button) {
SDL_SetJoystickVirtualButton(joystick, static_cast<SDL_GamepadButton>(button), SDL_SetJoystickVirtualButton(m_joystick, static_cast<SDL_GamepadButton>(button),
pad.buttons[static_cast<size_t>(button)]); pad.buttons[static_cast<size_t>(button)]);
} }
} }
private: 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_sdl;
std::mutex m_state; std::mutex m_state;
OpenXRLogCallback m_logger;
bool m_open = false;
bool m_attach_failed = false;
SDL_Joystick* m_joystick = nullptr; SDL_Joystick* m_joystick = nullptr;
std::atomic<uint32_t> m_id{0};
Pad m_pad; Pad m_pad;
bool m_pending = false; bool m_pending = false;
}; };
@@ -273,13 +363,18 @@ bool OpenXRInput::Create(OpenXRRuntime& runtime) {
CreatePoseSpaces(); CreatePoseSpaces();
LoadInputClock(); LoadInputClock();
LoadHandTracking(); LoadHandTracking();
if (!AttachVirtualGamepad()) { Relay().Open(m_logger);
Log(OpenXRLogLevel::Warning, switch (OpenXRGetControllerMode()) {
"SDL refused the virtual gamepad; OpenXR controllers will not reach the game"); 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; return true;
} }
@@ -739,54 +834,6 @@ void OpenXRApplyVirtualGamepad() noexcept {
Relay().Apply(); 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<SDL_Joystick*>(m_joystick));
m_joystick = nullptr;
}
if (m_joystick_id != 0) {
SDL_DetachVirtualJoystick(m_joystick_id);
m_joystick_id = 0;
}
}
void OpenXRInput::Destroy() { void OpenXRInput::Destroy() {
// The game must stop reading a remote whose controllers are going away. // The game must stop reading a remote whose controllers are going away.
OpenXRWithdrawWiiRemote(); OpenXRWithdrawWiiRemote();
@@ -794,7 +841,7 @@ void OpenXRInput::Destroy() {
if (m_created) { if (m_created) {
StopRumble(); StopRumble();
} }
DetachVirtualGamepad(); Relay().Close();
DestroyHandTrackers(); DestroyHandTrackers();
m_create_hand_tracker = nullptr; m_create_hand_tracker = nullptr;
m_destroy_hand_tracker = nullptr; m_destroy_hand_tracker = nullptr;
@@ -842,7 +889,7 @@ void OpenXRInput::Idle() {
} }
m_pointer.Reset(); m_pointer.Reset();
m_horizon = {1.0f, 0.0f}; 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. // The panel stays as it was; only what the controllers were holding is forgotten.
m_panel_controls.Reset(); m_panel_controls.Reset();
m_last_input_time = 0; m_last_input_time = 0;
@@ -855,9 +902,7 @@ void OpenXRInput::Idle() {
ResetDriving(); ResetDriving();
StopRumble(); StopRumble();
// Nothing stays held on the gamepad either while input is away. // 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, 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. // 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 = const OpenXRControllerMode mode = OpenXRGetControllerMode();
m_panel_controls.Update(panel_hands, open, dt_seconds, 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. // 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) {
@@ -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 // 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. // 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__) #if defined(__ANDROID__)
// Bare hands in the cockpit. While one of them holds the wheel it holds the // 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 // 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. // pointer cannot tell the two apart: MKW keeps it on while driving.
const bool cockpit_hands = 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<bool, kHands> bare_held{}; std::array<bool, kHands> bare_held{};
for (uint32_t hand = 0; hand < kHands; ++hand) { for (uint32_t hand = 0; hand < kHands; ++hand) {
bare_held[hand] = cockpit_hands && m_bare_latch[hand].Bare() && m_wheel_held[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], flick[hand] = {m_bare_latch[hand].Tracked(), m_wheel_held[hand],
m_joint_frame.seat_from_joint[hand][hand_tracking::kPalm][7]}; 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; m_flick_start = input_time;
} }
} else { } else {
@@ -1084,14 +1132,14 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
m_injected_flick_held = injected_flick; m_injected_flick_held = injected_flick;
#endif #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<wii_remote::HandInputs, kHands> kIdleHands{}; static const std::array<wii_remote::HandInputs, kHands> 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& left = game_hands[0];
const wii_remote::HandInputs& right = game_hands[1]; 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; VirtualGamepadRelay::Pad pad;
// OpenXR thumbsticks report +Y up; SDL gamepads report +Y down. // OpenXR thumbsticks report +Y up; SDL gamepads report +Y down.
pad.axes[SDL_GAMEPAD_AXIS_LEFTX] = ToAxis(left.stick_x); 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); Relay().Publish(pad);
} }
PublishWiiRemote(input_time, screen, game_hands, panel.withheld ? 0u : InjectedWiiRemoteButtons(), PublishWiiRemote(input_time, screen, game_hands, withheld ? 0u : InjectedWiiRemoteButtons(),
panel.withheld); withheld);
UpdateRumble(); UpdateRumble();
} }
@@ -1215,7 +1263,7 @@ void OpenXRInput::PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen&
sample.nunchuk_acc = OpenXRWiiRemoteSample{}.nunchuk_acc; sample.nunchuk_acc = OpenXRWiiRemoteSample{}.nunchuk_acc;
m_pointer.Reset(); m_pointer.Reset();
m_flick_start = 0; m_flick_start = 0;
OpenXRPublishWiiRemote(m_joystick_id, sample); OpenXRPublishWiiRemote(Relay().JoystickId(), sample);
return; return;
} }
if (m_flick_start != 0) { 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"); Log(OpenXRLogLevel::Info, "OpenXR Wii Remote pointer reached the virtual screen");
} }
} }
OpenXRPublishWiiRemote(m_joystick_id, sample); OpenXRPublishWiiRemote(Relay().JoystickId(), sample);
} }
void OpenXRInput::ResetDriving() { void OpenXRInput::ResetDriving() {
@@ -1427,7 +1475,7 @@ void OpenXRInput::UpdateDriving(XrTime display_time, const driving::SeatFrame& s
} }
void OpenXRInput::UpdateRumble() { void OpenXRInput::UpdateRumble() {
if (!OpenXRWiiRemoteRumbleRequested() || !OpenXRWiiRemoteOwnsGamepad(m_joystick_id)) { if (!OpenXRWiiRemoteRumbleRequested() || !OpenXRWiiRemoteOwnsGamepad(Relay().JoystickId())) {
StopRumble(); StopRumble();
return; return;
} }
+16
View File
@@ -34,6 +34,7 @@ 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 kWiiRemote = OpenXRControllerMode::WiiRemote;
constexpr OpenXRControllerMode kGamepad = OpenXRControllerMode::Gamepad; constexpr OpenXRControllerMode kGamepad = OpenXRControllerMode::Gamepad;
constexpr OpenXRControllerMode kNone = OpenXRControllerMode::None;
std::array<HandInputs, 2> Released() { std::array<HandInputs, 2> Released() {
return {}; return {};
@@ -103,6 +104,20 @@ void ChordOpensAndClosesOnceAsAGamepad() {
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 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() { void MenuClosesAndItsPressStaysOutOfTheGame() {
Controls controls; Controls controls;
bool open = false; bool open = false;
@@ -213,6 +228,7 @@ void BridgeAccumulatesWheelUntilTaken() {
int main() { int main() {
LeftYOpensAndClosesOnceAsAWiiRemote(); LeftYOpensAndClosesOnceAsAWiiRemote();
ChordOpensAndClosesOnceAsAGamepad(); ChordOpensAndClosesOnceAsAGamepad();
LeftYOpensThePanelWithoutAGameController();
MenuClosesAndItsPressStaysOutOfTheGame(); MenuClosesAndItsPressStaysOutOfTheGame();
SelectWaitsForAReleaseAndFollowsTheTrigger(); SelectWaitsForAReleaseAndFollowsTheTrigger();
ThumbstickScrolls(); ThumbstickScrolls();
+5 -1
View File
@@ -259,8 +259,12 @@ void TestButtons() {
right = {}; right = {};
left.primary = true; left.primary = true;
Check(RemoteButtons(left, right) == kButtonPlus, "left X is +, and nothing presses -");
left = {};
left.menu = true; 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 = {};
left.trigger = 0.7f; left.trigger = 0.7f;
Check(RemoteButtons(left, right) == kButtonZ, "left trigger is Z"); Check(RemoteButtons(left, right) == kButtonZ, "left trigger is Z");