Skip to content

Commit efdcd78

Browse files
author
Dmytro Ivanov
authored
FIX: Filtering out axis jitter on Switch Pro / DualShock 4 / DualSense gamepads connected via HID (#1611)
1 parent 460161c commit efdcd78

File tree

6 files changed

+330
-4
lines changed

6 files changed

+330
-4
lines changed

Assets/Tests/InputSystem/Plugins/DualShockTests.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,5 +262,149 @@ public void Devices_CanSetLightBarColorAndMotorSpeedsOnDualShockHID()
262262
Assert.That(receivedCommand.Value.blueColor, Is.EqualTo((byte)(0.5f * 255)));
263263
}
264264

265+
[Test]
266+
[Category("Devices")]
267+
public void Devices_DualSense_AxisJitter_DoesntMakeDeviceCurrent()
268+
{
269+
var device1 = InputSystem.AddDevice<DualSenseGamepadHID>();
270+
var device2 = InputSystem.AddDevice<DualSenseGamepadHID>();
271+
Assert.That(Gamepad.current, Is.EqualTo(device2));
272+
273+
// queuing state that is with-in axis dead zone doesn't make device current
274+
InputSystem.QueueStateEvent(device1,
275+
new DualSenseHIDInputReport
276+
{
277+
leftStickX = DualSenseGamepadHID.JitterMaskLow,
278+
leftStickY = DualSenseGamepadHID.JitterMaskHigh,
279+
rightStickX = DualSenseGamepadHID.JitterMaskHigh,
280+
rightStickY = DualSenseGamepadHID.JitterMaskLow,
281+
buttons0 = 8 // default dpad is at 8
282+
});
283+
InputSystem.Update();
284+
Assert.That(Gamepad.current, Is.EqualTo(device2));
285+
286+
// queuing state that is outside of dead zone makes device current
287+
InputSystem.QueueStateEvent(device1,
288+
new DualSenseHIDInputReport
289+
{
290+
leftStickX = DualSenseGamepadHID.JitterMaskLow - 1,
291+
leftStickY = DualSenseGamepadHID.JitterMaskHigh,
292+
rightStickX = DualSenseGamepadHID.JitterMaskHigh,
293+
rightStickY = DualSenseGamepadHID.JitterMaskLow,
294+
buttons0 = 8 // default dpad is at 8
295+
});
296+
InputSystem.Update();
297+
Assert.That(Gamepad.current, Is.EqualTo(device1));
298+
299+
// reset test
300+
device2.MakeCurrent();
301+
Assert.That(Gamepad.current, Is.EqualTo(device2));
302+
303+
// queuing state with button change makes device current
304+
InputSystem.QueueStateEvent(device1,
305+
new DualSenseHIDInputReport
306+
{
307+
leftStickX = DualSenseGamepadHID.JitterMaskLow,
308+
leftStickY = DualSenseGamepadHID.JitterMaskHigh,
309+
rightStickX = DualSenseGamepadHID.JitterMaskHigh,
310+
rightStickY = DualSenseGamepadHID.JitterMaskLow,
311+
buttons1 = 1,
312+
buttons0 = 8 // default dpad is at 8
313+
});
314+
InputSystem.Update();
315+
Assert.That(Gamepad.current, Is.EqualTo(device1));
316+
317+
// reset test
318+
device2.MakeCurrent();
319+
Assert.That(Gamepad.current, Is.EqualTo(device2));
320+
321+
// queuing state with trigger change makes device current
322+
InputSystem.QueueStateEvent(device1,
323+
new DualSenseHIDInputReport
324+
{
325+
leftStickX = DualSenseGamepadHID.JitterMaskLow,
326+
leftStickY = DualSenseGamepadHID.JitterMaskHigh,
327+
rightStickX = DualSenseGamepadHID.JitterMaskHigh,
328+
rightStickY = DualSenseGamepadHID.JitterMaskLow,
329+
buttons1 = 1,
330+
leftTrigger = 1,
331+
buttons0 = 8 // default dpad is at 8
332+
});
333+
InputSystem.Update();
334+
Assert.That(Gamepad.current, Is.EqualTo(device1));
335+
}
336+
337+
[Test]
338+
[Category("Devices")]
339+
public void Devices_DualShock4_AxisJitter_DoesntMakeDeviceCurrent()
340+
{
341+
var device1 = InputSystem.AddDevice<DualShock4GamepadHID>();
342+
var device2 = InputSystem.AddDevice<DualShock4GamepadHID>();
343+
Assert.That(Gamepad.current, Is.EqualTo(device2));
344+
345+
// queuing state that is with-in axis dead zone doesn't make device current
346+
InputSystem.QueueStateEvent(device1,
347+
new DualShock4HIDInputReport()
348+
{
349+
leftStickX = DualShock4GamepadHID.JitterMaskLow,
350+
leftStickY = DualShock4GamepadHID.JitterMaskHigh,
351+
rightStickX = DualShock4GamepadHID.JitterMaskHigh,
352+
rightStickY = DualShock4GamepadHID.JitterMaskLow,
353+
buttons1 = 8 // default dpad is at 8
354+
});
355+
InputSystem.Update();
356+
Assert.That(Gamepad.current, Is.EqualTo(device2));
357+
358+
// queuing state that is outside of dead zone makes device current
359+
InputSystem.QueueStateEvent(device1,
360+
new DualShock4HIDInputReport
361+
{
362+
leftStickX = DualShock4GamepadHID.JitterMaskLow - 1,
363+
leftStickY = DualShock4GamepadHID.JitterMaskHigh,
364+
rightStickX = DualShock4GamepadHID.JitterMaskHigh,
365+
rightStickY = DualShock4GamepadHID.JitterMaskLow,
366+
buttons1 = 8 // default dpad is at 8
367+
});
368+
InputSystem.Update();
369+
Assert.That(Gamepad.current, Is.EqualTo(device1));
370+
371+
// reset test
372+
device2.MakeCurrent();
373+
Assert.That(Gamepad.current, Is.EqualTo(device2));
374+
375+
// queuing state with button change makes device current
376+
InputSystem.QueueStateEvent(device1,
377+
new DualShock4HIDInputReport
378+
{
379+
leftStickX = DualShock4GamepadHID.JitterMaskLow,
380+
leftStickY = DualShock4GamepadHID.JitterMaskHigh,
381+
rightStickX = DualShock4GamepadHID.JitterMaskHigh,
382+
rightStickY = DualShock4GamepadHID.JitterMaskLow,
383+
buttons2 = 1,
384+
buttons1 = 8 // default dpad is at 8
385+
});
386+
InputSystem.Update();
387+
Assert.That(Gamepad.current, Is.EqualTo(device1));
388+
389+
// reset test
390+
device2.MakeCurrent();
391+
Assert.That(Gamepad.current, Is.EqualTo(device2));
392+
393+
// queuing state with trigger change makes device current
394+
InputSystem.QueueStateEvent(device1,
395+
new DualShock4HIDInputReport
396+
{
397+
leftStickX = DualShock4GamepadHID.JitterMaskLow,
398+
leftStickY = DualShock4GamepadHID.JitterMaskHigh,
399+
rightStickX = DualShock4GamepadHID.JitterMaskHigh,
400+
rightStickY = DualShock4GamepadHID.JitterMaskLow,
401+
buttons2 = 1,
402+
leftTrigger = 1,
403+
buttons1 = 8 // default dpad is at 8
404+
});
405+
InputSystem.Update();
406+
Assert.That(Gamepad.current, Is.EqualTo(device1));
407+
}
408+
265409
#endif
266410
}

Assets/Tests/InputSystem/SwitchTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,55 @@ private static SwitchProControllerHIDInputState StateWithButton(SwitchProControl
8080
}.WithButton(button);
8181
}
8282

83+
[Test]
84+
[Category("Devices")]
85+
public void Devices_SwitchPro_AxisJitter_DoesntMakeDeviceCurrent()
86+
{
87+
var device1 = InputSystem.AddDevice<SwitchProControllerHID>();
88+
var device2 = InputSystem.AddDevice<SwitchProControllerHID>();
89+
Assert.That(Gamepad.current, Is.EqualTo(device2));
90+
91+
// queuing state that is with-in axis dead zone doesn't make device current
92+
InputSystem.QueueStateEvent(device1,
93+
new SwitchProControllerHIDInputState
94+
{
95+
leftStickX = SwitchProControllerHID.JitterMaskLow,
96+
leftStickY = SwitchProControllerHID.JitterMaskHigh,
97+
rightStickX = SwitchProControllerHID.JitterMaskHigh,
98+
rightStickY = SwitchProControllerHID.JitterMaskLow,
99+
});
100+
InputSystem.Update();
101+
Assert.That(Gamepad.current, Is.EqualTo(device2));
102+
103+
// queuing state that is outside of dead zone makes device current
104+
InputSystem.QueueStateEvent(device1,
105+
new SwitchProControllerHIDInputState
106+
{
107+
leftStickX = SwitchProControllerHID.JitterMaskLow - 1,
108+
leftStickY = SwitchProControllerHID.JitterMaskHigh,
109+
rightStickX = SwitchProControllerHID.JitterMaskHigh,
110+
rightStickY = SwitchProControllerHID.JitterMaskLow,
111+
});
112+
InputSystem.Update();
113+
Assert.That(Gamepad.current, Is.EqualTo(device1));
114+
115+
// reset test
116+
device2.MakeCurrent();
117+
Assert.That(Gamepad.current, Is.EqualTo(device2));
118+
119+
// queuing state with button change makes device current
120+
InputSystem.QueueStateEvent(device1,
121+
new SwitchProControllerHIDInputState
122+
{
123+
leftStickX = SwitchProControllerHID.JitterMaskLow,
124+
leftStickY = SwitchProControllerHID.JitterMaskHigh,
125+
rightStickX = SwitchProControllerHID.JitterMaskHigh,
126+
rightStickY = SwitchProControllerHID.JitterMaskLow,
127+
}.WithButton(SwitchProControllerHIDInputState.Button.A));
128+
InputSystem.Update();
129+
Assert.That(Gamepad.current, Is.EqualTo(device1));
130+
}
131+
83132
[Test]
84133
[Category("Devices")]
85134
[TestCase(0x0f0d, 0x0092)]

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ however, it has to be formatted properly to pass verification tests.
1717
### Fixed
1818
- Fixed composite bindings incorrectly getting a control scheme assigned when pasting into input asset editor with a control scheme selected.
1919
- Fixed an issue on PS5 where device disconnected events that happen while the app is in the background are missed causing orphaned devices to hang around forever and exceptions when the same device is added again ([case UUM-7842](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-6744)).
20+
- Fixed Switch Pro, DualShock 4, DualSense gamepads becoming current on PC/macOS when no controls are changing ([case ISXB-223](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-223))).
2021

2122
### Actions
2223
- Extended input action code generator (`InputActionCodeGenerator.cs`) to support optional registration and unregistration of callbacks for multiple callback instances via `AddCallbacks(...)` and `RemoveCallbacks(...)` part of the generated code. Contribution by [Ramobo](https://github.com/Ramobo) in [#889](https://github.com/Unity-Technologies/InputSystem/pull/889).

Packages/com.unity.inputsystem/InputSystem/InputManager.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3172,9 +3172,12 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev
31723172
var haveChangedStateOtherThanNoise = true;
31733173
if (deviceIsStateCallbackReceiver)
31743174
{
3175+
m_ShouldMakeCurrentlyUpdatingDeviceCurrent = true;
31753176
// NOTE: We leave it to the device to make sure the event has the right format. This allows the
31763177
// device to handle multiple different incoming formats.
31773178
((IInputStateCallbackReceiver)device).OnStateEvent(eventPtr);
3179+
3180+
haveChangedStateOtherThanNoise = m_ShouldMakeCurrentlyUpdatingDeviceCurrent;
31783181
}
31793182
else
31803183
{
@@ -3314,6 +3317,15 @@ private void InvokeAfterUpdateCallback(InputUpdateType updateType)
33143317
"InputSystem.onAfterUpdate");
33153318
}
33163319

3320+
private bool m_ShouldMakeCurrentlyUpdatingDeviceCurrent;
3321+
3322+
// This is a dirty hot fix to expose entropy from device back to input manager to make a choice if we want to make device current or not.
3323+
// A proper fix would be to change IInputStateCallbackReceiver.OnStateEvent to return bool to make device current or not.
3324+
internal void DontMakeCurrentlyUpdatingDeviceCurrent()
3325+
{
3326+
m_ShouldMakeCurrentlyUpdatingDeviceCurrent = false;
3327+
}
3328+
33173329
internal unsafe bool UpdateState(InputDevice device, InputEvent* eventPtr, InputUpdateType updateType)
33183330
{
33193331
Debug.Assert(eventPtr != null, "Received NULL event ptr");

Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ namespace UnityEngine.InputSystem.DualShock
334334
/// PS5 DualSense controller that is interfaced to a HID backend.
335335
/// </summary>
336336
[InputControlLayout(stateType = typeof(DualSenseHIDInputReport), displayName = "DualSense HID")]
337-
public class DualSenseGamepadHID : DualShockGamepad, IEventMerger, IEventPreProcessor
337+
public class DualSenseGamepadHID : DualShockGamepad, IEventMerger, IEventPreProcessor, IInputStateCallbackReceiver
338338
{
339339
// Gamepad might send 3 types of input reports:
340340
// - Minimal report, first byte is 0x01, observed size is 78, also can be 10
@@ -516,6 +516,9 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr)
516516
return eventPtr.type != DeltaStateEvent.Type; // only skip delta state events
517517

518518
var stateEvent = StateEvent.FromUnchecked(eventPtr);
519+
if (stateEvent->stateFormat == DualSenseHIDInputReport.Format)
520+
return true; // if someone queued DSVS directly, just use as-is
521+
519522
var size = stateEvent->stateSizeInBytes;
520523
if (stateEvent->stateFormat != DualSenseHIDGenericInputReport.Format || size < sizeof(DualSenseHIDInputReport))
521524
return false; // skip unrecognized state events otherwise they will corrupt control states
@@ -549,6 +552,50 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr)
549552
return false; // skip unrecognized reportId
550553
}
551554

555+
public void OnNextUpdate()
556+
{
557+
}
558+
559+
// filter out three lower bits as jitter noise
560+
internal const byte JitterMaskLow = 0b01111000;
561+
internal const byte JitterMaskHigh = 0b10000111;
562+
563+
public unsafe void OnStateEvent(InputEventPtr eventPtr)
564+
{
565+
if (eventPtr.type == StateEvent.Type && eventPtr.stateFormat == DualSenseHIDInputReport.Format)
566+
{
567+
var currentState = (DualSenseHIDInputReport*)((byte*)currentStatePtr + m_StateBlock.byteOffset);
568+
var newState = (DualSenseHIDInputReport*)StateEvent.FromUnchecked(eventPtr)->state;
569+
570+
var actuated =
571+
// we need to make device current if axes are outside of deadzone specifying hardware jitter of sticks around zero point
572+
newState->leftStickX<JitterMaskLow
573+
|| newState->leftStickX> JitterMaskHigh
574+
|| newState->leftStickY<JitterMaskLow
575+
|| newState->leftStickY> JitterMaskHigh
576+
|| newState->rightStickX<JitterMaskLow
577+
|| newState->rightStickX> JitterMaskHigh
578+
|| newState->rightStickY<JitterMaskLow
579+
|| newState->rightStickY> JitterMaskHigh
580+
// we need to make device current if triggers or buttons state change
581+
|| newState->leftTrigger != currentState->leftTrigger
582+
|| newState->rightTrigger != currentState->rightTrigger
583+
|| newState->buttons0 != currentState->buttons0
584+
|| newState->buttons1 != currentState->buttons1
585+
|| newState->buttons2 != currentState->buttons2;
586+
587+
if (!actuated)
588+
InputSystem.s_Manager.DontMakeCurrentlyUpdatingDeviceCurrent();
589+
}
590+
591+
InputState.Change(this, eventPtr);
592+
}
593+
594+
public bool GetStateOffsetForEvent(InputControl control, InputEventPtr eventPtr, ref uint offset)
595+
{
596+
return false;
597+
}
598+
552599
[StructLayout(LayoutKind.Explicit)]
553600
internal struct DualSenseHIDGenericInputReport
554601
{
@@ -665,7 +712,7 @@ public DualSenseHIDInputReport ToHIDInputReport()
665712
/// PS4 DualShock controller that is interfaced to a HID backend.
666713
/// </summary>
667714
[InputControlLayout(stateType = typeof(DualShock4HIDInputReport), hideInUI = true, isNoisy = true)]
668-
public class DualShock4GamepadHID : DualShockGamepad, IEventPreProcessor
715+
public class DualShock4GamepadHID : DualShockGamepad, IEventPreProcessor, IInputStateCallbackReceiver
669716
{
670717
public ButtonControl leftTriggerButton { get; protected set; }
671718
public ButtonControl rightTriggerButton { get; protected set; }
@@ -791,8 +838,10 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr)
791838
return eventPtr.type != DeltaStateEvent.Type; // only skip delta state events
792839

793840
var stateEvent = StateEvent.FromUnchecked(eventPtr);
794-
var size = stateEvent->stateSizeInBytes;
841+
if (stateEvent->stateFormat == DualShock4HIDInputReport.Format)
842+
return true; // if someone queued D4VS directly, just use as-is
795843

844+
var size = stateEvent->stateSizeInBytes;
796845
if (stateEvent->stateFormat != DualShock4HIDGenericInputReport.Format || size < sizeof(DualShock4HIDGenericInputReport))
797846
return false; // skip unrecognized state events otherwise they will corrupt control states
798847

@@ -838,6 +887,50 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr)
838887
}
839888
}
840889

890+
public void OnNextUpdate()
891+
{
892+
}
893+
894+
// filter out three lower bits as jitter noise
895+
internal const byte JitterMaskLow = 0b01111000;
896+
internal const byte JitterMaskHigh = 0b10000111;
897+
898+
public unsafe void OnStateEvent(InputEventPtr eventPtr)
899+
{
900+
if (eventPtr.type == StateEvent.Type && eventPtr.stateFormat == DualShock4HIDInputReport.Format)
901+
{
902+
var currentState = (DualShock4HIDInputReport*)((byte*)currentStatePtr + m_StateBlock.byteOffset);
903+
var newState = (DualShock4HIDInputReport*)StateEvent.FromUnchecked(eventPtr)->state;
904+
905+
var actuatedOrChanged =
906+
// we need to make device current if axes are outside of deadzone specifying hardware jitter of sticks around zero point
907+
newState->leftStickX<JitterMaskLow
908+
|| newState->leftStickX> JitterMaskHigh
909+
|| newState->leftStickY<JitterMaskLow
910+
|| newState->leftStickY> JitterMaskHigh
911+
|| newState->rightStickX<JitterMaskLow
912+
|| newState->rightStickX> JitterMaskHigh
913+
|| newState->rightStickY<JitterMaskLow
914+
|| newState->rightStickY> JitterMaskHigh
915+
// we need to make device current if triggers or buttons state change
916+
|| newState->leftTrigger != currentState->leftTrigger
917+
|| newState->rightTrigger != currentState->rightTrigger
918+
|| newState->buttons1 != currentState->buttons1
919+
|| newState->buttons2 != currentState->buttons2
920+
|| newState->buttons3 != currentState->buttons3;
921+
922+
if (!actuatedOrChanged)
923+
InputSystem.s_Manager.DontMakeCurrentlyUpdatingDeviceCurrent();
924+
}
925+
926+
InputState.Change(this, eventPtr);
927+
}
928+
929+
public bool GetStateOffsetForEvent(InputControl control, InputEventPtr eventPtr, ref uint offset)
930+
{
931+
return false;
932+
}
933+
841934
[StructLayout(LayoutKind.Explicit)]
842935
internal struct DualShock4HIDGenericInputReport
843936
{

0 commit comments

Comments
 (0)