diff --git a/Assets/Tests/InputSystem/APIVerificationTests.cs b/Assets/Tests/InputSystem/APIVerificationTests.cs index 3d856a6d3d..cd18ffa2d0 100644 --- a/Assets/Tests/InputSystem/APIVerificationTests.cs +++ b/Assets/Tests/InputSystem/APIVerificationTests.cs @@ -778,6 +778,10 @@ public class Touchscreen : UnityEngine.InputSystem.Pointer, UnityEngine.InputSys [Property("Exclusions", @"1.0.0 public bool filterNoiseOnCurrent { get; set; } ")] + // SwitchProControllerHID inherited from IInputStateCallbackReceiver and IEventPreProcessor, both are internal interfaces + [Property("Exclusions", @"1.0.0 + public class SwitchProControllerHID : UnityEngine.InputSystem.Gamepad + ")] public void API_MinorVersionsHaveNoBreakingChanges() { var currentVersion = CoreTests.PackageJson.ReadVersion(); diff --git a/Assets/Tests/InputSystem/SwitchTests.cs b/Assets/Tests/InputSystem/SwitchTests.cs index e09761b32d..09e5047d93 100644 --- a/Assets/Tests/InputSystem/SwitchTests.cs +++ b/Assets/Tests/InputSystem/SwitchTests.cs @@ -8,6 +8,7 @@ using UnityEngine.InputSystem.HID; using UnityEngine.InputSystem.Switch.LowLevel; using UnityEngine.InputSystem.Processors; +using UnityEngine.TestTools.Utils; internal class SwitchTests : CoreTestsFixture { @@ -35,31 +36,48 @@ public void Devices_SupportsHIDNpad() InputSystem.QueueStateEvent(controller, new SwitchProControllerHIDInputState { - leftStickX = 0x1000, - leftStickY = 0x1000, - rightStickX = 0x7fff, - rightStickY = 0xefff, + leftStickX = 0x10, + leftStickY = 0x10, + rightStickX = 0x80, + rightStickY = 0xf2, }); InputSystem.Update(); var leftStickDeadzone = controller.leftStick.TryGetProcessor(); var rightStickDeadzone = controller.rightStick.TryGetProcessor(); - Assert.That(Vector2.Distance(controller.leftStick.ReadValue(), leftStickDeadzone.Process(new Vector2(-1.0f, 1.0f))), Is.LessThan(0.0001f)); - Assert.That(Vector2.Distance(controller.rightStick.ReadValue(), rightStickDeadzone.Process(new Vector2(0.0f, -1.0f))), Is.LessThan(0.0001f)); + var currentLeft = controller.leftStick.ReadValue(); + var expectedLeft = leftStickDeadzone.Process(new Vector2(-1.0f, 1.0f)); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.A), controller.buttonEast); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.B), controller.buttonSouth); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.X), controller.buttonNorth); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.Y), controller.buttonWest); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.StickL), controller.leftStickButton); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.StickR), controller.rightStickButton); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.L), controller.leftShoulder); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.R), controller.rightShoulder); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.ZL), controller.leftTrigger); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.ZR), controller.rightTrigger); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.Plus), controller.startButton); - AssertButtonPress(controller, new SwitchProControllerHIDInputState().WithButton(SwitchProControllerHIDInputState.Button.Minus), controller.selectButton); + var currentRight = controller.rightStick.ReadValue(); + var expectedRight = rightStickDeadzone.Process(new Vector2(0.0f, -1.0f)); + + Assert.That(currentLeft, Is.EqualTo(expectedLeft).Using(Vector2EqualityComparer.Instance)); + Assert.That(currentRight, Is.EqualTo(expectedRight).Using(new Vector2EqualityComparer(0.01f))); + + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.A), controller.buttonEast); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.B), controller.buttonSouth); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.X), controller.buttonNorth); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Y), controller.buttonWest); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.StickL), controller.leftStickButton); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.StickR), controller.rightStickButton); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.L), controller.leftShoulder); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.R), controller.rightShoulder); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.ZL), controller.leftTrigger); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.ZR), controller.rightTrigger); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Plus), controller.startButton); + AssertButtonPress(controller, StateWithButton(SwitchProControllerHIDInputState.Button.Minus), controller.selectButton); + } + + private static SwitchProControllerHIDInputState StateWithButton(SwitchProControllerHIDInputState.Button button) + { + return new SwitchProControllerHIDInputState + { + leftStickX = 0x7f, + leftStickY = 0x7f, + rightStickX = 0x7f, + rightStickY = 0x7f, + }.WithButton(button); } #endif diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 4c58979687..d9d0478dbe 100755 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -12,6 +12,8 @@ however, it has to be formatted properly to pass verification tests. ### Fixed +- Fixed Switch Pro controller not working correctly in different scenarios ([case 1369091](https://issuetracker.unity3d.com/issues/nintendo-switch-pro-controller-output-garbage), [case 1190216](https://issuetracker.unity3d.com/issues/inputsystem-windows-switch-pro-controller-only-works-when-connected-via-bluetooth-but-not-via-usb), case 1314869). + #### Actions * Fixed `InputAction.GetTimeoutCompletionPercentage` jumping to 100% completion early ([case 1377009](https://issuetracker.unity3d.com/issues/gettimeoutcompletionpercentage-returns-1-after-0-dot-1s-when-hold-action-was-started-even-though-it-is-not-performed-yet)). diff --git a/Packages/com.unity.inputsystem/Documentation~/SupportedDevices.md b/Packages/com.unity.inputsystem/Documentation~/SupportedDevices.md index eb5936d0a8..4e0d1856ab 100644 --- a/Packages/com.unity.inputsystem/Documentation~/SupportedDevices.md +++ b/Packages/com.unity.inputsystem/Documentation~/SupportedDevices.md @@ -43,7 +43,7 @@ On UWP only USB connection is supported, motor rumble and lightbar are not worki >6. Unity supports Made for iOS (Mfi) certified controllers on iOS. Xbox One and PS4 controllers are only supported on iOS 13 or higher. >7. Consoles are supported using separate packages. You need to install these packages in your Project to enable console support. >8. Unity officially supports PS4 controllers only on [Android 10 or higher](https://playstation.com/en-us/support/hardware/ps4-pair-dualshock-4-wireless-with-sony-xperia-and-android). ->9. Switch Joy-Cons are not currently supported on Windows and Mac. Also, Switch Pro controllers are supported only when connected via Bluetooth but not when connected via wired USB. +>9. Switch Joy-Cons are not currently supported on Windows and Mac. >10. PS5 DualSense is supported on Windows and macOS via USB HID, though setting motor rumble and lightbar color when connected over Bluetooth is currently not supported. On UWP only USB connection is supported, motor rumble and lightbar are not working correctly. On Android it's expected to be working from Android 12. diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs index 3bd4730896..28dd3d7fab 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs @@ -1,5 +1,6 @@ #if UNITY_EDITOR || UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_WSA || PACKAGE_DOCS_GENERATION using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.Layouts; @@ -15,71 +16,79 @@ namespace UnityEngine.InputSystem.Switch.LowLevel /// /// Structure of HID input reports for Switch Pro controllers. /// - [StructLayout(LayoutKind.Explicit, Size = 20)] + [StructLayout(LayoutKind.Explicit, Size = 7)] internal struct SwitchProControllerHIDInputState : IInputStateTypeInfo { - public FourCC format => new FourCC('H', 'I', 'D'); - - [InputControl(name = "dpad", format = "BIT", layout = "Dpad", bit = 24, sizeInBits = 4, defaultState = 8)] - [InputControl(name = "dpad/up", format = "BIT", layout = "DiscreteButton", parameters = "minValue=7,maxValue=1,nullValue=8,wrapAtValue=7", bit = 24, sizeInBits = 4)] - [InputControl(name = "dpad/right", format = "BIT", layout = "DiscreteButton", parameters = "minValue=1,maxValue=3", bit = 24, sizeInBits = 4)] - [InputControl(name = "dpad/down", format = "BIT", layout = "DiscreteButton", parameters = "minValue=3,maxValue=5", bit = 24, sizeInBits = 4)] - [InputControl(name = "dpad/left", format = "BIT", layout = "DiscreteButton", parameters = "minValue=5, maxValue=7", bit = 24, sizeInBits = 4)] - [InputControl(name = "buttonNorth", displayName = "X", shortDisplayName = "X", bit = (uint)Button.North)] - [InputControl(name = "buttonSouth", displayName = "B", shortDisplayName = "B", bit = (uint)Button.South, usage = "Back")] - [InputControl(name = "buttonWest", displayName = "Y", shortDisplayName = "Y", bit = (uint)Button.West, usage = "SecondaryAction")] - [InputControl(name = "buttonEast", displayName = "A", shortDisplayName = "A", bit = (uint)Button.East, usage = "PrimaryAction")] - [InputControl(name = "leftStickPress", displayName = "Left Stick", bit = (uint)Button.StickL)] - [InputControl(name = "rightStickPress", displayName = "Right Stick", bit = (uint)Button.StickR)] + public static FourCC Format = new FourCC('S', 'P', 'V', 'S'); // Switch Pro Virtual State + + public FourCC format => Format; + + [InputControl(name = "leftStick", layout = "Stick", format = "VC2B")] + [InputControl(name = "leftStick/x", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")] + [InputControl(name = "leftStick/left", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")] + [InputControl(name = "leftStick/right", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85")] + [InputControl(name = "leftStick/y", offset = 1, format = "BYTE", parameters = "invert,normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")] + [InputControl(name = "leftStick/up", offset = 1, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")] + [InputControl(name = "leftStick/down", offset = 1, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85,invert=false")] + [FieldOffset(0)] public byte leftStickX; + [FieldOffset(1)] public byte leftStickY; + + [InputControl(name = "rightStick", layout = "Stick", format = "VC2B")] + [InputControl(name = "rightStick/x", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")] + [InputControl(name = "rightStick/left", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0,clampMax=0.5,invert")] + [InputControl(name = "rightStick/right", offset = 0, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1")] + [InputControl(name = "rightStick/y", offset = 1, format = "BYTE", parameters = "invert,normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")] + [InputControl(name = "rightStick/up", offset = 1, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")] + [InputControl(name = "rightStick/down", offset = 1, format = "BYTE", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85,invert=false")] + [FieldOffset(2)] public byte rightStickX; + [FieldOffset(3)] public byte rightStickY; + + [InputControl(name = "dpad", format = "BIT", bit = 0, sizeInBits = 4)] + [InputControl(name = "dpad/up", bit = (int)Button.Up)] + [InputControl(name = "dpad/right", bit = (int)Button.Right)] + [InputControl(name = "dpad/down", bit = (int)Button.Down)] + [InputControl(name = "dpad/left", bit = (int)Button.Left)] + [InputControl(name = "buttonWest", displayName = "Y", shortDisplayName = "Y", bit = (int)Button.Y, usage = "SecondaryAction")] + [InputControl(name = "buttonNorth", displayName = "X", shortDisplayName = "X", bit = (int)Button.X)] + [InputControl(name = "buttonSouth", displayName = "B", shortDisplayName = "B", bit = (int)Button.B, usage = "Back")] + [InputControl(name = "buttonEast", displayName = "A", shortDisplayName = "A", bit = (int)Button.A, usage = "PrimaryAction")] [InputControl(name = "leftShoulder", displayName = "L", shortDisplayName = "L", bit = (uint)Button.L)] [InputControl(name = "rightShoulder", displayName = "R", shortDisplayName = "R", bit = (uint)Button.R)] + [InputControl(name = "leftStickPress", displayName = "Left Stick", bit = (uint)Button.StickL)] + [InputControl(name = "rightStickPress", displayName = "Right Stick", bit = (uint)Button.StickR)] [InputControl(name = "leftTrigger", displayName = "ZL", shortDisplayName = "ZL", format = "BIT", bit = (uint)Button.ZL)] [InputControl(name = "rightTrigger", displayName = "ZR", shortDisplayName = "ZR", format = "BIT", bit = (uint)Button.ZR)] [InputControl(name = "start", displayName = "Plus", bit = (uint)Button.Plus, usage = "Menu")] [InputControl(name = "select", displayName = "Minus", bit = (uint)Button.Minus)] - [FieldOffset(0)] - public uint buttons; - - [InputControl(name = "leftStick", format = "VC2S", layout = "Stick")] - [InputControl(name = "leftStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")] - [InputControl(name = "leftStick/left", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")] - [InputControl(name = "leftStick/right", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85")] - [InputControl(name = "leftStick/y", offset = 2, format = "USHT", parameters = "invert,normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")] - [InputControl(name = "leftStick/up", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")] - [InputControl(name = "leftStick/down", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85,invert=false")] - [FieldOffset(4)] public ushort leftStickX; - [FieldOffset(6)] public ushort leftStickY; - - [InputControl(name = "rightStick", format = "VC2S", layout = "Stick")] - [InputControl(name = "rightStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")] - [InputControl(name = "rightStick/left", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0,clampMax=0.5,invert")] - [InputControl(name = "rightStick/right", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1")] - [InputControl(name = "rightStick/y", offset = 2, format = "USHT", parameters = "invert,normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")] - [InputControl(name = "rightStick/up", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")] - [InputControl(name = "rightStick/down", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85,invert=false")] - [FieldOffset(8)] public ushort rightStickX; - [FieldOffset(10)] public ushort rightStickY; - - public float leftTrigger => ((buttons & (1U << (int)Button.ZL)) != 0) ? 1f : 0f; - - public float rightTrigger => ((buttons & (1U << (int)Button.ZR)) != 0) ? 1f : 0f; + [FieldOffset(4)] public ushort buttons1; + + [InputControl(name = "capture", layout = "Button", displayName = "Capture", bit = (uint)Button.Capture - 16)] + [InputControl(name = "home", layout = "Button", displayName = "Home", bit = (uint)Button.Home - 16)] + [FieldOffset(6)] public byte buttons2; public enum Button { - North = 11, - South = 8, - West = 10, - East = 9, + Up = 0, + Right = 1, + Down = 2, + Left = 3, + + West = 4, + North = 5, + South = 6, + East = 7, - StickL = 18, - StickR = 19, - L = 12, - R = 13, + L = 8, + R = 9, + StickL = 10, + StickR = 11, - ZL = 14, - ZR = 15, - Plus = 17, - Minus = 16, + ZL = 12, + ZR = 13, + Plus = 14, + Minus = 15, + Capture = 16, + Home = 17, X = North, B = South, @@ -87,22 +96,46 @@ public enum Button A = East, } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SwitchProControllerHIDInputState WithButton(Button button, bool value = true) { - Debug.Assert((int)button < 32, $"Expected button < 32, so we fit into the 32 bit wide bitmask"); - var bit = 1U << (int)button; - if (value) - buttons |= bit; - else - buttons &= ~bit; - // dpad default state - buttons |= 8 << 24; - leftStickX = 0x8000; - leftStickY = 0x8000; - rightStickX = 0x8000; - rightStickY = 0x8000; + Set(button, value); return this; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Button button, bool state) + { + Debug.Assert((int)button < 18, $"Expected button < 18"); + if ((int)button < 16) + { + var bit = (ushort)(1U << (int)button); + if (state) + buttons1 = (ushort)(buttons1 | bit); + else + buttons1 &= (ushort)~bit; + } + else if ((int)button < 18) + { + var bit = (byte)(1U << ((int)button - 16)); + if (state) + buttons2 = (byte)(buttons2 | bit); + else + buttons2 &= (byte)~bit; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Press(Button button) + { + Set(button, true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Release(Button button) + { + Set(button, false); + } } #endif } @@ -113,12 +146,372 @@ namespace UnityEngine.InputSystem.Switch /// /// A Nintendo Switch Pro controller connected to a desktop mac/windows PC using the HID interface. /// - /// - /// This controller currently only works when connected via Bluetooth but not when connected over USB. - /// [InputControlLayout(stateType = typeof(SwitchProControllerHIDInputState), displayName = "Switch Pro Controller")] - public class SwitchProControllerHID : Gamepad + public class SwitchProControllerHID : Gamepad, IInputStateCallbackReceiver, IEventPreProcessor { + [InputControl(name = "capture", displayName = "Capture")] + public ButtonControl captureButton { get; protected set; } + + [InputControl(name = "home", displayName = "Home")] + public ButtonControl homeButton { get; protected set; } + + protected override void OnAdded() + { + base.OnAdded(); + + captureButton = GetChildControl("capture"); + homeButton = GetChildControl("home"); + + HandshakeRestart(); + } + + private static readonly SwitchMagicOutputReport.CommandIdType[] s_HandshakeSequence = new[] + { + SwitchMagicOutputReport.CommandIdType.Status, + SwitchMagicOutputReport.CommandIdType.Handshake, + SwitchMagicOutputReport.CommandIdType.Highspeed, + SwitchMagicOutputReport.CommandIdType.Handshake, + SwitchMagicOutputReport.CommandIdType.ForceUSB + ////TODO: Should we add a step to revert back to simple interface? + //// Because currently full reports don't work in old input system. + }; + + private int m_HandshakeStepIndex; + private double m_HandshakeTimer; + + private void HandshakeRestart() + { + // Delay first command issue until some time into the future + m_HandshakeStepIndex = -1; + m_HandshakeTimer = InputRuntime.s_Instance.currentTime; + } + + private void HandshakeTick() + { + const double handshakeRestartTimeout = 2.0; + const double handshakeNextStepTimeout = 0.1; + + var currentTime = InputRuntime.s_Instance.currentTime; + + // There were no events for last few seconds, restart handshake + if (currentTime >= m_LastUpdateTimeInternal + handshakeRestartTimeout && + currentTime >= m_HandshakeTimer + handshakeRestartTimeout) + m_HandshakeStepIndex = 0; + // If handshake is complete, ignore the tick. + else if (m_HandshakeStepIndex + 1 >= s_HandshakeSequence.Length) + return; + // If we timeout, proceed to next step after some time is elapsed. + else if (currentTime > m_HandshakeTimer + handshakeNextStepTimeout) + m_HandshakeStepIndex++; + // If we haven't timed out on handshake step, skip the tick. + else + return; + + m_HandshakeTimer = currentTime; + + var command = s_HandshakeSequence[m_HandshakeStepIndex]; + + // Native backend rejects one of the commands based on size of descriptor. + // So just report both at a same time. + ////TODO: fix this. + var commandBt = SwitchMagicOutputHIDBluetooth.Create(command); + if (ExecuteCommand(ref commandBt) > 0) + return; + + var commandUsb = SwitchMagicOutputHIDUSB.Create(command); + ExecuteCommand(ref commandUsb); + } + + public void OnNextUpdate() + { + HandshakeTick(); + } + + public void OnStateEvent(InputEventPtr eventPtr) + { + InputState.Change(this, eventPtr); + } + + public bool GetStateOffsetForEvent(InputControl control, InputEventPtr eventPtr, ref uint offset) + { + return false; + } + + public unsafe bool PreProcessEvent(InputEventPtr eventPtr) + { + if (eventPtr.type == DeltaStateEvent.Type) + // if someone queued delta state SPVS directly, just use as-is + // otherwise skip all delta state events + return DeltaStateEvent.FromUnchecked(eventPtr)->stateFormat == SwitchProControllerHIDInputState.Format; + + // use all other non-state/non-delta-state events + if (eventPtr.type != StateEvent.Type) + return true; + + var stateEvent = StateEvent.FromUnchecked(eventPtr); + var size = stateEvent->stateSizeInBytes; + + if (stateEvent->stateFormat == SwitchProControllerHIDInputState.Format) + return true; // if someone queued SPVS directly, just use as-is + + if (stateEvent->stateFormat != SwitchHIDGenericInputReport.Format || size < sizeof(SwitchHIDGenericInputReport)) + return false; // skip unrecognized state events otherwise they will corrupt control states + + var genericReport = (SwitchHIDGenericInputReport*)stateEvent->state; + if (genericReport->reportId == SwitchSimpleInputReport.ExpectedReportId && size >= SwitchSimpleInputReport.kSize) + { + var data = ((SwitchSimpleInputReport*)stateEvent->state)->ToHIDInputReport(); + *((SwitchProControllerHIDInputState*)stateEvent->state) = data; + stateEvent->stateFormat = SwitchProControllerHIDInputState.Format; + return true; + } + else if (genericReport->reportId == SwitchFullInputReport.ExpectedReportId && size >= SwitchFullInputReport.kSize) + { + var data = ((SwitchFullInputReport*)stateEvent->state)->ToHIDInputReport(); + *((SwitchProControllerHIDInputState*)stateEvent->state) = data; + stateEvent->stateFormat = SwitchProControllerHIDInputState.Format; + return true; + } + else + return false; // skip unrecognized reportId + } + + [StructLayout(LayoutKind.Explicit, Size = kSize)] + private struct SwitchSimpleInputReport + { + public const int kSize = 12; + public const byte ExpectedReportId = 0x3f; + + [FieldOffset(0)] public byte reportId; + [FieldOffset(1)] public byte buttons0; + [FieldOffset(2)] public byte buttons1; + [FieldOffset(3)] public byte hat; + [FieldOffset(4)] public ushort leftX; + [FieldOffset(6)] public ushort leftY; + [FieldOffset(8)] public ushort rightX; + [FieldOffset(10)] public ushort rightY; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SwitchProControllerHIDInputState ToHIDInputReport() + { + var leftXByte = (byte)NumberHelpers.RemapUIntBitsToNormalizeFloatToUIntBits(leftX, 16, 8); + var leftYByte = (byte)NumberHelpers.RemapUIntBitsToNormalizeFloatToUIntBits(leftY, 16, 8); + var rightXByte = (byte)NumberHelpers.RemapUIntBitsToNormalizeFloatToUIntBits(rightX, 16, 8); + var rightYByte = (byte)NumberHelpers.RemapUIntBitsToNormalizeFloatToUIntBits(rightY, 16, 8); + + var state = new SwitchProControllerHIDInputState + { + leftStickX = leftXByte, + leftStickY = leftYByte, + rightStickX = rightXByte, + rightStickY = rightYByte + }; + + state.Set(SwitchProControllerHIDInputState.Button.B, (buttons0 & 0x01) != 0); + state.Set(SwitchProControllerHIDInputState.Button.A, (buttons0 & 0x02) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Y, (buttons0 & 0x04) != 0); + state.Set(SwitchProControllerHIDInputState.Button.X, (buttons0 & 0x08) != 0); + state.Set(SwitchProControllerHIDInputState.Button.L, (buttons0 & 0x10) != 0); + state.Set(SwitchProControllerHIDInputState.Button.R, (buttons0 & 0x20) != 0); + state.Set(SwitchProControllerHIDInputState.Button.ZL, (buttons0 & 0x40) != 0); + state.Set(SwitchProControllerHIDInputState.Button.ZR, (buttons0 & 0x80) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Minus, (buttons1 & 0x01) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Plus, (buttons1 & 0x02) != 0); + state.Set(SwitchProControllerHIDInputState.Button.StickL, (buttons1 & 0x04) != 0); + state.Set(SwitchProControllerHIDInputState.Button.StickR, (buttons1 & 0x08) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Home, (buttons1 & 0x10) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Capture, (buttons1 & 0x20) != 0); + + var left = false; + var up = false; + var right = false; + var down = false; + + switch (hat) + { + case 0: + up = true; + break; + case 1: + up = true; + right = true; + break; + case 2: + right = true; + break; + case 3: + down = true; + right = true; + break; + case 4: + down = true; + break; + case 5: + down = true; + left = true; + break; + case 6: + left = true; + break; + case 7: + up = true; + left = true; + break; + } + + state.Set(SwitchProControllerHIDInputState.Button.Left, left); + state.Set(SwitchProControllerHIDInputState.Button.Up, up); + state.Set(SwitchProControllerHIDInputState.Button.Right, right); + state.Set(SwitchProControllerHIDInputState.Button.Down, down); + + return state; + } + } + + [StructLayout(LayoutKind.Explicit, Size = kSize)] + private struct SwitchFullInputReport + { + public const int kSize = 25; + public const byte ExpectedReportId = 0x30; + + [FieldOffset(0)] public byte reportId; + [FieldOffset(3)] public byte buttons0; + [FieldOffset(4)] public byte buttons1; + [FieldOffset(5)] public byte buttons2; + [FieldOffset(6)] public byte left0; + [FieldOffset(7)] public byte left1; + [FieldOffset(8)] public byte left2; + [FieldOffset(9)] public byte right0; + [FieldOffset(10)] public byte right1; + [FieldOffset(11)] public byte right2; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SwitchProControllerHIDInputState ToHIDInputReport() + { + ////TODO: calibration curve + + var leftXRaw = (uint)(left0 | ((left1 & 0x0F) << 8)); + var leftYRaw = (uint)(((left1 & 0xF0) >> 4) | (left2 << 4)); + var rightXRaw = (uint)(right0 | ((right1 & 0x0F) << 8)); + var rightYRaw = (uint)(((right1 & 0xF0) >> 4) | (right2 << 4)); + + var leftXByte = (byte)NumberHelpers.RemapUIntBitsToNormalizeFloatToUIntBits(leftXRaw, 12, 8); + var leftYByte = (byte)(0xff - (byte)NumberHelpers.RemapUIntBitsToNormalizeFloatToUIntBits(leftYRaw, 12, 8)); + var rightXByte = (byte)NumberHelpers.RemapUIntBitsToNormalizeFloatToUIntBits(rightXRaw, 12, 8); + var rightYByte = (byte)(0xff - (byte)NumberHelpers.RemapUIntBitsToNormalizeFloatToUIntBits(rightYRaw, 12, 8)); + + var state = new SwitchProControllerHIDInputState + { + leftStickX = leftXByte, + leftStickY = leftYByte, + rightStickX = rightXByte, + rightStickY = rightYByte + }; + + state.Set(SwitchProControllerHIDInputState.Button.Y, (buttons0 & 0x01) != 0); + state.Set(SwitchProControllerHIDInputState.Button.X, (buttons0 & 0x02) != 0); + state.Set(SwitchProControllerHIDInputState.Button.B, (buttons0 & 0x04) != 0); + state.Set(SwitchProControllerHIDInputState.Button.A, (buttons0 & 0x08) != 0); + state.Set(SwitchProControllerHIDInputState.Button.R, (buttons0 & 0x40) != 0); + state.Set(SwitchProControllerHIDInputState.Button.ZR, (buttons0 & 0x80) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Minus, (buttons1 & 0x01) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Plus, (buttons1 & 0x02) != 0); + state.Set(SwitchProControllerHIDInputState.Button.StickR, (buttons1 & 0x04) != 0); + state.Set(SwitchProControllerHIDInputState.Button.StickL, (buttons1 & 0x08) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Home, (buttons1 & 0x10) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Capture, (buttons1 & 0x20) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Down, (buttons2 & 0x01) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Up, (buttons2 & 0x02) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Right, (buttons2 & 0x04) != 0); + state.Set(SwitchProControllerHIDInputState.Button.Left, (buttons2 & 0x08) != 0); + state.Set(SwitchProControllerHIDInputState.Button.L, (buttons2 & 0x40) != 0); + state.Set(SwitchProControllerHIDInputState.Button.ZL, (buttons2 & 0x80) != 0); + + return state; + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct SwitchHIDGenericInputReport + { + public static FourCC Format => new FourCC('H', 'I', 'D'); + + [FieldOffset(0)] public byte reportId; + } + + [StructLayout(LayoutKind.Explicit, Size = kSize)] + internal struct SwitchMagicOutputReport + { + public const int kSize = 49; + + public const byte ExpectedReplyInputReportId = 0x81; + + [FieldOffset(0)] public byte reportType; + [FieldOffset(1)] public byte commandId; + + internal enum ReportType + { + Magic = 0x80 + } + + public enum CommandIdType + { + Status = 0x01, + Handshake = 0x02, + Highspeed = 0x03, + ForceUSB = 0x04, + } + } + + [StructLayout(LayoutKind.Explicit, Size = kSize)] + internal struct SwitchMagicOutputHIDBluetooth : IInputDeviceCommandInfo + { + public static FourCC Type => new FourCC('H', 'I', 'D', 'O'); + public FourCC typeStatic => Type; + + public const int kSize = InputDeviceCommand.kBaseCommandSize + 49; + + [FieldOffset(0)] public InputDeviceCommand baseCommand; + [FieldOffset(InputDeviceCommand.kBaseCommandSize + 0)] public SwitchMagicOutputReport report; + + public static SwitchMagicOutputHIDBluetooth Create(SwitchMagicOutputReport.CommandIdType type) + { + return new SwitchMagicOutputHIDBluetooth + { + baseCommand = new InputDeviceCommand(Type, kSize), + report = new SwitchMagicOutputReport + { + reportType = (byte)SwitchMagicOutputReport.ReportType.Magic, + commandId = (byte)type + } + }; + } + } + + [StructLayout(LayoutKind.Explicit, Size = kSize)] + internal struct SwitchMagicOutputHIDUSB : IInputDeviceCommandInfo + { + public static FourCC Type => new FourCC('H', 'I', 'D', 'O'); + public FourCC typeStatic => Type; + + public const int kSize = InputDeviceCommand.kBaseCommandSize + 64; + + [FieldOffset(0)] public InputDeviceCommand baseCommand; + [FieldOffset(InputDeviceCommand.kBaseCommandSize + 0)] public SwitchMagicOutputReport report; + + public static SwitchMagicOutputHIDUSB Create(SwitchMagicOutputReport.CommandIdType type) + { + return new SwitchMagicOutputHIDUSB + { + baseCommand = new InputDeviceCommand(Type, kSize), + report = new SwitchMagicOutputReport + { + reportType = (byte)SwitchMagicOutputReport.ReportType.Magic, + commandId = (byte)type + } + }; + } + } } #endif } diff --git a/Packages/com.unity.inputsystem/InputSystem/Utilities/NumberHelpers.cs b/Packages/com.unity.inputsystem/InputSystem/Utilities/NumberHelpers.cs index 50ef99e3b1..68c164ef0a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Utilities/NumberHelpers.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Utilities/NumberHelpers.cs @@ -83,5 +83,15 @@ public static uint NormalizedFloatToUInt(float value, uint uintMinValue, uint ui return uintMaxValue; return (uint)(value * ((double)uintMaxValue - uintMinValue) + uintMinValue); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RemapUIntBitsToNormalizeFloatToUIntBits(uint value, uint inBitSize, uint outBitSize) + { + var inMaxValue = (uint)((1UL << (int)inBitSize) - 1); + var outMaxValue = (uint)((1UL << (int)outBitSize) - 1); + + var normFloat = UIntToNormalizedFloat(value, 0, inMaxValue); + return NormalizedFloatToUInt(normFloat, 0, outMaxValue); + } } }