From 81d1e4077c6f15de4178272099ebcd3149a3417b Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Mon, 13 Apr 2026 13:42:12 +0100 Subject: [PATCH 01/10] Skip dualsense output report when connected via BT --- .../InputSystem/Plugins/DualShock/DualShockGamepadHID.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs index eda2f4c037..373d0205a7 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs @@ -351,6 +351,7 @@ public class DualSenseGamepadHID : DualShockGamepad, IEventMerger, IEventPreProc private float? m_HighFrequenceyMotorSpeed; protected Color? m_LightBarColor; private byte outputSequenceId; + private bool m_IsBluetooth; protected override void FinishSetup() { @@ -419,6 +420,9 @@ public override void SetMotorSpeeds(float lowFrequency, float highFrequency) /// for the respective documentation regarding setting rumble and light bar color. public bool SetMotorSpeedsAndLightBarColor(float? lowFrequency, float? highFrequency, Color? color) { + if (m_IsBluetooth) + return false; + var lf = lowFrequency.HasValue ? lowFrequency.Value : 0; var hf = highFrequency.HasValue ? highFrequency.Value : 0; var c = color.HasValue ? color.Value : Color.black; @@ -526,6 +530,7 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) var genericReport = (DualSenseHIDGenericInputReport*)stateEvent->state; if (genericReport->reportId == DualSenseHIDUSBInputReport.ExpectedReportId) { + m_IsBluetooth = false; if (stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize1 || stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2) { @@ -543,6 +548,7 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) } else if (genericReport->reportId == DualSenseHIDBluetoothInputReport.ExpectedReportId) { + m_IsBluetooth = true; var data = ((DualSenseHIDBluetoothInputReport*)stateEvent->state)->ToHIDInputReport(); *((DualSenseHIDInputReport*)stateEvent->state) = data; stateEvent->stateFormat = DualSenseHIDInputReport.Format; From 45569f553e6c301b1b9e910b2b21ee5f1ffd7982 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Mon, 13 Apr 2026 14:00:10 +0100 Subject: [PATCH 02/10] Update CHANGELOG.md --- Packages/com.unity.inputsystem/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 2813c56348..015c019556 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed +- Fixed DualSense controller sending garbage HID output data when connected over Bluetooth, which caused significant FPS drops. Haptic output is now skipped when connected via Bluetooth. [ISXB-1477](https://jira.unity3d.com/browse/ISXB-1477) - Fixed a `NullReferenceException` thrown when removing all action maps [UUM-137116](https://jira.unity3d.com/browse/UUM-137116) - Simplified default setting messaging by consolidating repetitive messages into a single HelpBox. - Fixed a `NullPointerReferenceException` thrown in `InputManagerStateMonitors.FireStateChangeNotifications` logging by adding validation [UUM-136095]. From bd94aa408bf96694382a8f94297f63717944e938 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Thu, 23 Apr 2026 19:11:21 +0100 Subject: [PATCH 03/10] implements crc32 protocol --- .../Plugins/DualShock/DualShockGamepadHID.cs | 114 +++++++++++++++--- 1 file changed, 98 insertions(+), 16 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs index 373d0205a7..4777317978 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs @@ -93,17 +93,22 @@ internal struct DualSenseHIDUSBOutputReport : IInputDeviceCommandInfo public static FourCC Type => new FourCC('H', 'I', 'D', 'O'); public FourCC typeStatic => Type; - internal const int kSize = InputDeviceCommand.BaseCommandSize + 48; + // Fixed wire size for report 0x02: 1 byte report ID + 47 byte payload. + // The HID descriptor's outputReportSize cannot be used here: DualSense advertises a much larger + // max output size (~547 bytes) as the maximum across all vendor-defined feature reports, which + // would make the native HID backend write too many bytes and cause the request to be rejected. + internal const int kReportSize = 48; + internal const int kSize = InputDeviceCommand.BaseCommandSize + kReportSize; [FieldOffset(0)] public InputDeviceCommand baseCommand; [FieldOffset(InputDeviceCommand.BaseCommandSize + 0)] public byte reportId; [FieldOffset(InputDeviceCommand.BaseCommandSize + 1)] public DualSenseHIDOutputReportPayload payload; - public static DualSenseHIDUSBOutputReport Create(DualSenseHIDOutputReportPayload payload, int outputReportSize) + public static DualSenseHIDUSBOutputReport Create(DualSenseHIDOutputReportPayload payload) { return new DualSenseHIDUSBOutputReport { - baseCommand = new InputDeviceCommand(Type, InputDeviceCommand.kBaseCommandSize + outputReportSize), + baseCommand = new InputDeviceCommand(Type, InputDeviceCommand.kBaseCommandSize + kReportSize), reportId = 2, payload = payload }; @@ -116,7 +121,12 @@ internal struct DualSenseHIDBluetoothOutputReport : IInputDeviceCommandInfo public static FourCC Type => new FourCC('H', 'I', 'D', 'O'); public FourCC typeStatic => Type; - internal const int kSize = InputDeviceCommand.BaseCommandSize + 78; + // Fixed wire size for report 0x31: 1 byte report ID + 2 byte tag + 47 byte payload + 24 byte + // unused fields + 4 byte CRC32. See note on DualSenseHIDUSBOutputReport about why we don't + // use hidDescriptor.outputReportSize here. + internal const int kReportSize = 78; + internal const int kCrcInputLength = 74; // Everything before the trailing CRC32. + internal const int kSize = InputDeviceCommand.BaseCommandSize + kReportSize; [FieldOffset(0)] public InputDeviceCommand baseCommand; [FieldOffset(InputDeviceCommand.BaseCommandSize + 0)] public byte reportId; @@ -125,24 +135,55 @@ internal struct DualSenseHIDBluetoothOutputReport : IInputDeviceCommandInfo [FieldOffset(InputDeviceCommand.BaseCommandSize + 3)] public DualSenseHIDOutputReportPayload payload; [FieldOffset(InputDeviceCommand.BaseCommandSize + 74)] public uint crc32; - [FieldOffset(InputDeviceCommand.BaseCommandSize + 0)] public unsafe fixed byte rawData[74]; + [FieldOffset(InputDeviceCommand.BaseCommandSize + 0)] public unsafe fixed byte rawData[kCrcInputLength]; + + // CRC32 seed prepended to the report before the CRC is computed. Matches SDL / Linux PS5 HID driver. + // See PS_OUTPUT_CRC32_SEED in SDL's SDL_hidapi_ps5.c and linux/drivers/hid/hid-playstation.c. + private const byte k_OutputCrc32Seed = 0xA2; - public static DualSenseHIDBluetoothOutputReport Create(DualSenseHIDOutputReportPayload payload, byte outputSequenceId, int outputReportSize) + public static unsafe DualSenseHIDBluetoothOutputReport Create(DualSenseHIDOutputReportPayload payload, byte outputSequenceId) { var report = new DualSenseHIDBluetoothOutputReport { - baseCommand = new InputDeviceCommand(Type, InputDeviceCommand.kBaseCommandSize + outputReportSize), + baseCommand = new InputDeviceCommand(Type, InputDeviceCommand.kBaseCommandSize + kReportSize), reportId = 0x31, tag1 = (byte)((outputSequenceId & 0xf) << 4), tag2 = 0x10, payload = payload }; - ////FIXME: Calculate crc32 correctly + // DualSense BT output reports require a trailing CRC32 computed over a one-byte HIDP header (0xA2) + // followed by the 74 leading bytes of the report (reportId + tags + payload + unused fields). + var crc = DualSenseCrc32.Compute(k_OutputCrc32Seed, report.rawData, kCrcInputLength); + report.crc32 = crc; + return report; } } + internal static class DualSenseCrc32 + { + // Standard zlib / IEEE 802.3 CRC32: reflected polynomial 0xEDB88320, init 0xFFFFFFFF, xor-out 0xFFFFFFFF. + // Matches SDL_crc32 used for DualSense Bluetooth output reports. + public static unsafe uint Compute(byte seed, byte* data, int length) + { + uint crc = 0xFFFFFFFFu; + crc = UpdateByte(crc, seed); + for (var i = 0; i < length; i++) + crc = UpdateByte(crc, data[i]); + return ~crc; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint UpdateByte(uint crc, byte b) + { + crc ^= b; + for (var i = 0; i < 8; i++) + crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320u : crc >> 1; + return crc; + } + } + /// /// Structure of HID input reports for PS4 DualShock 4 controllers. /// @@ -352,6 +393,7 @@ public class DualSenseGamepadHID : DualShockGamepad, IEventMerger, IEventPreProc protected Color? m_LightBarColor; private byte outputSequenceId; private bool m_IsBluetooth; + private byte m_LastLoggedReportId; protected override void FinishSetup() { @@ -359,6 +401,26 @@ protected override void FinishSetup() rightTriggerButton = GetChildControl("rightTriggerButton"); playStationButton = GetChildControl("systemButton"); + // Infer transport from the HID descriptor: a Bluetooth-paired DualSense advertises an + // output report with ID 0x31, a USB-connected one does not. This is more reliable than + // looking at the first input report because Windows delivers the device in "simple mode" + // until we send a 0x31 output, so inputs initially arrive as reportId=0x01. + var elements = hidDescriptor.elements; + if (elements != null) + { + for (var i = 0; i < elements.Length; i++) + { + var element = elements[i]; + if (element.reportType == UnityEngine.InputSystem.HID.HID.HIDReportType.Output && + element.reportId == DualSenseHIDBluetoothInputReport.ExpectedReportId) + { + m_IsBluetooth = true; + break; + } + } + } + Debug.Log($"[DualSense] FinishSetup: m_IsBluetooth={m_IsBluetooth}, hidDescriptor.outputReportSize={hidDescriptor.outputReportSize}, elementCount={(elements?.Length ?? 0)}"); + base.FinishSetup(); } @@ -420,9 +482,6 @@ public override void SetMotorSpeeds(float lowFrequency, float highFrequency) /// for the respective documentation regarding setting rumble and light bar color. public bool SetMotorSpeedsAndLightBarColor(float? lowFrequency, float? highFrequency, Color? color) { - if (m_IsBluetooth) - return false; - var lf = lowFrequency.HasValue ? lowFrequency.Value : 0; var hf = highFrequency.HasValue ? highFrequency.Value : 0; var c = color.HasValue ? color.Value : Color.black; @@ -441,10 +500,23 @@ public bool SetMotorSpeedsAndLightBarColor(float? lowFrequency, float? highFrequ blueColor = (byte)NumberHelpers.NormalizedFloatToUInt(c.b, byte.MinValue, byte.MaxValue) }; - ////FIXME: Bluetooth reports are not working - //var command = DualSenseHIDBluetoothOutputReport.Create(payload, ++outputSequenceId); - var command = DualSenseHIDUSBOutputReport.Create(payload, hidDescriptor.outputReportSize); - return ExecuteCommand(ref command) >= 0; + // Output report format differs by transport: USB uses report ID 0x02 (48 bytes), + // Bluetooth uses report ID 0x31 (78 bytes, including a trailing CRC32). Sending the USB + // report over Bluetooth makes the device drop frames and ignore haptics (ISXB-1477). + if (m_IsBluetooth) + { + var command = DualSenseHIDBluetoothOutputReport.Create(payload, ++outputSequenceId); + var result = ExecuteCommand(ref command); + Debug.Log($"[DualSense] BT output sent. seq={outputSequenceId}, result={result}"); + return result >= 0; + } + else + { + var command = DualSenseHIDUSBOutputReport.Create(payload); + var result = ExecuteCommand(ref command); + Debug.Log($"[DualSense] USB output sent. result={result}"); + return result >= 0; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -528,9 +600,19 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) return false; // skip unrecognized state events otherwise they will corrupt control states var genericReport = (DualSenseHIDGenericInputReport*)stateEvent->state; + if (genericReport->reportId != m_LastLoggedReportId) + { + m_LastLoggedReportId = genericReport->reportId; + Debug.Log($"[DualSense] PreProcessEvent: reportId=0x{genericReport->reportId:X2}, size={stateEvent->stateSizeInBytes}, m_IsBluetooth(before)={m_IsBluetooth}"); + } if (genericReport->reportId == DualSenseHIDUSBInputReport.ExpectedReportId) { - m_IsBluetooth = false; + // A 78-byte report with reportId=0x01 is the Windows Bluetooth "simple mode" variant: + // the BT HID wrapper forces a 78-byte frame but the device has not yet been switched + // into enhanced reporting. Only clear m_IsBluetooth for a true USB-sized packet, + // otherwise we'd send the wrong output report format and lose haptics. + if (stateEvent->stateSizeInBytes != DualSenseHIDMinimalInputReport.ExpectedSize2) + m_IsBluetooth = false; if (stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize1 || stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2) { From 06e7be1cc24b1175a5e695d8590e3c78352a8f96 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Thu, 23 Apr 2026 19:11:26 +0100 Subject: [PATCH 04/10] Update CHANGELOG.md --- Packages/com.unity.inputsystem/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 015c019556..80782d79b8 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed -- Fixed DualSense controller sending garbage HID output data when connected over Bluetooth, which caused significant FPS drops. Haptic output is now skipped when connected via Bluetooth. [ISXB-1477](https://jira.unity3d.com/browse/ISXB-1477) +- Fixed DualSense controller sending garbage HID output data when connected over Bluetooth, which caused significant FPS drops and prevented haptics from working. The Bluetooth output report (ID `0x31`, 78 bytes, with CRC32 trailer) is now used when the controller is connected via Bluetooth, so rumble and light bar updates work on both USB and Bluetooth. [ISXB-1477](https://jira.unity3d.com/browse/ISXB-1477) - Fixed a `NullReferenceException` thrown when removing all action maps [UUM-137116](https://jira.unity3d.com/browse/UUM-137116) - Simplified default setting messaging by consolidating repetitive messages into a single HelpBox. - Fixed a `NullPointerReferenceException` thrown in `InputManagerStateMonitors.FireStateChangeNotifications` logging by adding validation [UUM-136095]. From 96fc4c3d4159873eb972344989189fe95bba779a Mon Sep 17 00:00:00 2001 From: Morgan Hoarau <122548697+MorganHoarau@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:53:21 +0100 Subject: [PATCH 05/10] Cleanup implementation for rumble --- .../Plugins/DualShock/DualShockGamepadHID.cs | 56 ++++--------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs index 4777317978..e90d4fcc24 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs @@ -82,6 +82,7 @@ internal struct DualSenseHIDOutputReportPayload [FieldOffset(1)] public byte enableFlags2; [FieldOffset(2)] public byte highFrequencyMotorSpeed; [FieldOffset(3)] public byte lowFrequencyMotorSpeed; + [FieldOffset(38)] public byte validFlag2; [FieldOffset(44)] public byte redColor; [FieldOffset(45)] public byte greenColor; [FieldOffset(46)] public byte blueColor; @@ -93,10 +94,6 @@ internal struct DualSenseHIDUSBOutputReport : IInputDeviceCommandInfo public static FourCC Type => new FourCC('H', 'I', 'D', 'O'); public FourCC typeStatic => Type; - // Fixed wire size for report 0x02: 1 byte report ID + 47 byte payload. - // The HID descriptor's outputReportSize cannot be used here: DualSense advertises a much larger - // max output size (~547 bytes) as the maximum across all vendor-defined feature reports, which - // would make the native HID backend write too many bytes and cause the request to be rejected. internal const int kReportSize = 48; internal const int kSize = InputDeviceCommand.BaseCommandSize + kReportSize; @@ -121,11 +118,8 @@ internal struct DualSenseHIDBluetoothOutputReport : IInputDeviceCommandInfo public static FourCC Type => new FourCC('H', 'I', 'D', 'O'); public FourCC typeStatic => Type; - // Fixed wire size for report 0x31: 1 byte report ID + 2 byte tag + 47 byte payload + 24 byte - // unused fields + 4 byte CRC32. See note on DualSenseHIDUSBOutputReport about why we don't - // use hidDescriptor.outputReportSize here. internal const int kReportSize = 78; - internal const int kCrcInputLength = 74; // Everything before the trailing CRC32. + internal const int kCrcInputLength = 74; internal const int kSize = InputDeviceCommand.BaseCommandSize + kReportSize; [FieldOffset(0)] public InputDeviceCommand baseCommand; @@ -137,8 +131,7 @@ internal struct DualSenseHIDBluetoothOutputReport : IInputDeviceCommandInfo [FieldOffset(InputDeviceCommand.BaseCommandSize + 0)] public unsafe fixed byte rawData[kCrcInputLength]; - // CRC32 seed prepended to the report before the CRC is computed. Matches SDL / Linux PS5 HID driver. - // See PS_OUTPUT_CRC32_SEED in SDL's SDL_hidapi_ps5.c and linux/drivers/hid/hid-playstation.c. + // CRC32 seed prepended to the report data before computing the checksum. private const byte k_OutputCrc32Seed = 0xA2; public static unsafe DualSenseHIDBluetoothOutputReport Create(DualSenseHIDOutputReportPayload payload, byte outputSequenceId) @@ -152,8 +145,6 @@ public static unsafe DualSenseHIDBluetoothOutputReport Create(DualSenseHIDOutput payload = payload }; - // DualSense BT output reports require a trailing CRC32 computed over a one-byte HIDP header (0xA2) - // followed by the 74 leading bytes of the report (reportId + tags + payload + unused fields). var crc = DualSenseCrc32.Compute(k_OutputCrc32Seed, report.rawData, kCrcInputLength); report.crc32 = crc; @@ -163,8 +154,7 @@ public static unsafe DualSenseHIDBluetoothOutputReport Create(DualSenseHIDOutput internal static class DualSenseCrc32 { - // Standard zlib / IEEE 802.3 CRC32: reflected polynomial 0xEDB88320, init 0xFFFFFFFF, xor-out 0xFFFFFFFF. - // Matches SDL_crc32 used for DualSense Bluetooth output reports. + // Standard zlib / IEEE 802.3 CRC32 (reflected polynomial 0xEDB88320). public static unsafe uint Compute(byte seed, byte* data, int length) { uint crc = 0xFFFFFFFFu; @@ -393,7 +383,6 @@ public class DualSenseGamepadHID : DualShockGamepad, IEventMerger, IEventPreProc protected Color? m_LightBarColor; private byte outputSequenceId; private bool m_IsBluetooth; - private byte m_LastLoggedReportId; protected override void FinishSetup() { @@ -401,10 +390,6 @@ protected override void FinishSetup() rightTriggerButton = GetChildControl("rightTriggerButton"); playStationButton = GetChildControl("systemButton"); - // Infer transport from the HID descriptor: a Bluetooth-paired DualSense advertises an - // output report with ID 0x31, a USB-connected one does not. This is more reliable than - // looking at the first input report because Windows delivers the device in "simple mode" - // until we send a 0x31 output, so inputs initially arrive as reportId=0x01. var elements = hidDescriptor.elements; if (elements != null) { @@ -419,7 +404,6 @@ protected override void FinishSetup() } } } - Debug.Log($"[DualSense] FinishSetup: m_IsBluetooth={m_IsBluetooth}, hidDescriptor.outputReportSize={hidDescriptor.outputReportSize}, elementCount={(elements?.Length ?? 0)}"); base.FinishSetup(); } @@ -484,38 +468,24 @@ public bool SetMotorSpeedsAndLightBarColor(float? lowFrequency, float? highFrequ { var lf = lowFrequency.HasValue ? lowFrequency.Value : 0; var hf = highFrequency.HasValue ? highFrequency.Value : 0; - var c = color.HasValue ? color.Value : Color.black; - // DualSense differs a bit from DualShock 4 because all effects need to be set at a same time, - // otherwise setting just a color would disable motor rumble. var payload = new DualSenseHIDOutputReportPayload { enableFlags1 = 0x1 | // Enable motor rumble. 0x2, // Disable haptics. - enableFlags2 = 0x4, // Enable LEDs color. lowFrequencyMotorSpeed = (byte)NumberHelpers.NormalizedFloatToUInt(lf, byte.MinValue, byte.MaxValue), highFrequencyMotorSpeed = (byte)NumberHelpers.NormalizedFloatToUInt(hf, byte.MinValue, byte.MaxValue), - redColor = (byte)NumberHelpers.NormalizedFloatToUInt(c.r, byte.MinValue, byte.MaxValue), - greenColor = (byte)NumberHelpers.NormalizedFloatToUInt(c.g, byte.MinValue, byte.MaxValue), - blueColor = (byte)NumberHelpers.NormalizedFloatToUInt(c.b, byte.MinValue, byte.MaxValue) }; - // Output report format differs by transport: USB uses report ID 0x02 (48 bytes), - // Bluetooth uses report ID 0x31 (78 bytes, including a trailing CRC32). Sending the USB - // report over Bluetooth makes the device drop frames and ignore haptics (ISXB-1477). if (m_IsBluetooth) { var command = DualSenseHIDBluetoothOutputReport.Create(payload, ++outputSequenceId); - var result = ExecuteCommand(ref command); - Debug.Log($"[DualSense] BT output sent. seq={outputSequenceId}, result={result}"); - return result >= 0; + return ExecuteCommand(ref command) >= 0; } else { var command = DualSenseHIDUSBOutputReport.Create(payload); - var result = ExecuteCommand(ref command); - Debug.Log($"[DualSense] USB output sent. result={result}"); - return result >= 0; + return ExecuteCommand(ref command) >= 0; } } @@ -600,18 +570,12 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) return false; // skip unrecognized state events otherwise they will corrupt control states var genericReport = (DualSenseHIDGenericInputReport*)stateEvent->state; - if (genericReport->reportId != m_LastLoggedReportId) - { - m_LastLoggedReportId = genericReport->reportId; - Debug.Log($"[DualSense] PreProcessEvent: reportId=0x{genericReport->reportId:X2}, size={stateEvent->stateSizeInBytes}, m_IsBluetooth(before)={m_IsBluetooth}"); - } if (genericReport->reportId == DualSenseHIDUSBInputReport.ExpectedReportId) { - // A 78-byte report with reportId=0x01 is the Windows Bluetooth "simple mode" variant: - // the BT HID wrapper forces a 78-byte frame but the device has not yet been switched - // into enhanced reporting. Only clear m_IsBluetooth for a true USB-sized packet, - // otherwise we'd send the wrong output report format and lose haptics. - if (stateEvent->stateSizeInBytes != DualSenseHIDMinimalInputReport.ExpectedSize2) + // 78-byte frame with reportId=0x01 is Bluetooth "simple mode". + if (stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2) + m_IsBluetooth = true; + else m_IsBluetooth = false; if (stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize1 || stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2) From 6420d4ce53fb4fa6f5c2930111781da81c32e8b4 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau <122548697+MorganHoarau@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:54:56 +0100 Subject: [PATCH 06/10] Fixes dualsense LED over bluetooth --- .../Plugins/DualShock/DualShockGamepadHID.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs index e90d4fcc24..2af817ee7d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs @@ -83,6 +83,7 @@ internal struct DualSenseHIDOutputReportPayload [FieldOffset(2)] public byte highFrequencyMotorSpeed; [FieldOffset(3)] public byte lowFrequencyMotorSpeed; [FieldOffset(38)] public byte validFlag2; + [FieldOffset(41)] public byte lightbarSetup; [FieldOffset(44)] public byte redColor; [FieldOffset(45)] public byte greenColor; [FieldOffset(46)] public byte blueColor; @@ -383,6 +384,9 @@ public class DualSenseGamepadHID : DualShockGamepad, IEventMerger, IEventPreProc protected Color? m_LightBarColor; private byte outputSequenceId; private bool m_IsBluetooth; + private bool m_LedResetSent; + private DualSenseHIDOutputReportPayload? m_PendingBtPayload; + private int m_PendingBtPayloadDelay; protected override void FinishSetup() { @@ -404,10 +408,25 @@ protected override void FinishSetup() } } } + m_LedResetSent = false; base.FinishSetup(); } + private void SendBluetoothLedReset() + { + if (m_LedResetSent) + return; + + var resetPayload = new DualSenseHIDOutputReportPayload + { + enableFlags2 = 0x08, + }; + var command = DualSenseHIDBluetoothOutputReport.Create(resetPayload, ++outputSequenceId); + ExecuteCommand(ref command); + m_LedResetSent = true; + } + public override void PauseHaptics() { if (!m_LowFrequencyMotorSpeed.HasValue && !m_HighFrequenceyMotorSpeed.HasValue) @@ -477,8 +496,25 @@ public bool SetMotorSpeedsAndLightBarColor(float? lowFrequency, float? highFrequ highFrequencyMotorSpeed = (byte)NumberHelpers.NormalizedFloatToUInt(hf, byte.MinValue, byte.MaxValue), }; + if (color.HasValue) + { + payload.enableFlags2 = 0x4; + payload.redColor = (byte)NumberHelpers.NormalizedFloatToUInt(color.Value.r, byte.MinValue, byte.MaxValue); + payload.greenColor = (byte)NumberHelpers.NormalizedFloatToUInt(color.Value.g, byte.MinValue, byte.MaxValue); + payload.blueColor = (byte)NumberHelpers.NormalizedFloatToUInt(color.Value.b, byte.MinValue, byte.MaxValue); + } + if (m_IsBluetooth) { + // LED reset and color must be in separate reports with a multi-frame gap. + if (color.HasValue && !m_LedResetSent) + { + SendBluetoothLedReset(); + m_PendingBtPayload = payload; + m_PendingBtPayloadDelay = 3; + return true; + } + var command = DualSenseHIDBluetoothOutputReport.Create(payload, ++outputSequenceId); return ExecuteCommand(ref command) >= 0; } @@ -606,6 +642,18 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) public void OnNextUpdate() { + if (m_PendingBtPayload.HasValue && m_IsBluetooth) + { + if (m_PendingBtPayloadDelay > 0) + { + m_PendingBtPayloadDelay--; + return; + } + + var command = DualSenseHIDBluetoothOutputReport.Create(m_PendingBtPayload.Value, ++outputSequenceId); + ExecuteCommand(ref command); + m_PendingBtPayload = null; + } } // filter out three lower bits as jitter noise From eea8b55bec6d79bd3dd21ff51208bb8f0f11f36e Mon Sep 17 00:00:00 2001 From: Morgan Hoarau <122548697+MorganHoarau@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:27:39 +0100 Subject: [PATCH 07/10] Update documentation --- .../com.unity.inputsystem/Documentation~/SupportedDevices.md | 4 ++-- .../Runtime/Plugins/DualShock/IDualShockHaptics.cs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Packages/com.unity.inputsystem/Documentation~/SupportedDevices.md b/Packages/com.unity.inputsystem/Documentation~/SupportedDevices.md index 3f6f0af710..cdba0d7bda 100644 --- a/Packages/com.unity.inputsystem/Documentation~/SupportedDevices.md +++ b/Packages/com.unity.inputsystem/Documentation~/SupportedDevices.md @@ -40,13 +40,13 @@ Support for the following Devices doesn't require specialized support of particu > 2. WebGL support varies between browsers, Devices, and operating systems. > 3. XInput controllers on macOS currently require the installation of the [Xbox Controller Driver for macOS](https://github.com/360Controller/360Controller). This driver only supports only USB connections, and doesn't support wireless dongles. However, the latest generation of Xbox One controllers natively support Bluetooth, and are natively supported on Macs as HIDs without any additional drivers when connected via Bluetooth. > 4. This includes any XInput-compatible Device. -> 5. Unity doesn't support motor rumble and light bar color over Bluetooth. Unity doesn't support the gyro or accelerometer on PS4/PS5 controllers on platforms other than the PlayStation consoles. Unity also doesn't support the DualShock 4 USB Wireless Adapter. On UWP, only USB connection is supported, motor rumble and light bar are not working correctly. +> 5. Unity doesn't support motor rumble and light bar color over Bluetooth for DualShock 4 controllers. For DualSense (PS5) Bluetooth support, refer to note 11. Unity doesn't support the gyro or accelerometer on PS4/PS5 controllers on platforms other than the PlayStation consoles. Unity also doesn't support the DualShock 4 USB Wireless Adapter. On UWP, only USB connection is supported, motor rumble and light bar are not working correctly. > 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 supports PS4 controllers on Android devices running [Android 10 or higher](https://playstation.com/en-us/support/hardware/ps4-pair-dualshock-4-wireless-with-sony-xperia-and-android). > 9. Unity supports PS5 controllers on Android devices running [Android 12 or higher](https://playstation.com/en-gb/support/hardware/pair-dualsense-controller-bluetooth/). > 10. Switch Joy-Cons are not currently supported on Windows and Mac. Some of official accessories are supported on Windows and Mac: "Hori Co HORIPAD for Nintendo Switch", "HORI Pokken Tournament DX Pro Pad", "HORI Wireless Switch Pad", "HORI Real Arcade Pro V Hayabusa in Switch Mode", "PowerA NSW Fusion Wired FightPad", "PowerA NSW Fusion Pro Controller (USB only)", "PDP Wired Fight Pad Pro: Mario", "PDP Faceoff Wired Pro Controller for Nintendo Switch", "PDP Faceoff Deluxe Wired Pro Controller for Nintendo Switch", "PDP Afterglow Wireless Switch Controller", "PDP Rockcandy Wired Controller". -> 11. PS5 DualSense is supported on Windows, macOS, and Linux via USB HID. Linux support begins with Unity Editor 6000.4 (refer to [PS5 controller support on Linux](#ps5-controller-support-on-linux)). On all platforms, setting motor rumble and light bar color when connected over Bluetooth is currently not supported. +> 11. PS5 DualSense is supported on Windows, macOS, and Linux via USB HID. Linux support begins with Unity Editor 6000.4 (refer to [PS5 controller support on Linux](#ps5-controller-support-on-linux)). Motor rumble and light bar color over Bluetooth are supported. Light bar color changes persist on the controller after the application exits. > 12. SteelSeries Nimbus+ supported via HID on macOS. > - On UWP only USB connection is supported, motor rumble and light bar are not working correctly. > - On Android it's expected to be working from Android 12. diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/IDualShockHaptics.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/IDualShockHaptics.cs index 918a0fba2e..2936cc7d5f 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/IDualShockHaptics.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/IDualShockHaptics.cs @@ -12,6 +12,10 @@ public interface IDualShockHaptics : IDualMotorRumble /// /// Color to use for the light bar. Alpha component is ignored. Also, /// RBG values are clamped into [0..1] range. + /// + /// The light bar color persists on the controller hardware after the + /// application exits and is not automatically restored to the default color. + /// void SetLightBarColor(Color color); } } From 252f547d9636531edb87547f4aebb3c0fa91c495 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau <122548697+MorganHoarau@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:10:57 +0100 Subject: [PATCH 08/10] Cleanup implementation --- .../Plugins/DualShock/DualShockGamepadHID.cs | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs index 2af817ee7d..9fde09f99a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs @@ -82,8 +82,6 @@ internal struct DualSenseHIDOutputReportPayload [FieldOffset(1)] public byte enableFlags2; [FieldOffset(2)] public byte highFrequencyMotorSpeed; [FieldOffset(3)] public byte lowFrequencyMotorSpeed; - [FieldOffset(38)] public byte validFlag2; - [FieldOffset(41)] public byte lightbarSetup; [FieldOffset(44)] public byte redColor; [FieldOffset(45)] public byte greenColor; [FieldOffset(46)] public byte blueColor; @@ -388,6 +386,10 @@ public class DualSenseGamepadHID : DualShockGamepad, IEventMerger, IEventPreProc private DualSenseHIDOutputReportPayload? m_PendingBtPayload; private int m_PendingBtPayloadDelay; + // The Bluetooth LED reset and the color report must be sent as separate reports. + // A delay between them is also required to let the firmware process the reset first. + private const int k_BluetoothLedResetDelayFrames = 3; + protected override void FinishSetup() { leftTriggerButton = GetChildControl("leftTriggerButton"); @@ -420,7 +422,7 @@ private void SendBluetoothLedReset() var resetPayload = new DualSenseHIDOutputReportPayload { - enableFlags2 = 0x08, + enableFlags2 = 0x08, // Release firmware control of the light bar so the app can drive it. }; var command = DualSenseHIDBluetoothOutputReport.Create(resetPayload, ++outputSequenceId); ExecuteCommand(ref command); @@ -506,23 +508,22 @@ public bool SetMotorSpeedsAndLightBarColor(float? lowFrequency, float? highFrequ if (m_IsBluetooth) { - // LED reset and color must be in separate reports with a multi-frame gap. + // The light bar reset and color must be sent as separate reports, so defer the + // color payload and let OnNextUpdate send it once the reset has taken effect. if (color.HasValue && !m_LedResetSent) { SendBluetoothLedReset(); m_PendingBtPayload = payload; - m_PendingBtPayloadDelay = 3; + m_PendingBtPayloadDelay = k_BluetoothLedResetDelayFrames; return true; } - var command = DualSenseHIDBluetoothOutputReport.Create(payload, ++outputSequenceId); - return ExecuteCommand(ref command) >= 0; - } - else - { - var command = DualSenseHIDUSBOutputReport.Create(payload); - return ExecuteCommand(ref command) >= 0; + var btCommand = DualSenseHIDBluetoothOutputReport.Create(payload, ++outputSequenceId); + return ExecuteCommand(ref btCommand) >= 0; } + + var command = DualSenseHIDUSBOutputReport.Create(payload); + return ExecuteCommand(ref command) >= 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -608,11 +609,8 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) var genericReport = (DualSenseHIDGenericInputReport*)stateEvent->state; if (genericReport->reportId == DualSenseHIDUSBInputReport.ExpectedReportId) { - // 78-byte frame with reportId=0x01 is Bluetooth "simple mode". - if (stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2) - m_IsBluetooth = true; - else - m_IsBluetooth = false; + // A 78-byte frame with reportId=0x01 is Bluetooth "simple mode". + m_IsBluetooth = stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2; if (stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize1 || stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2) { From 9eb2086228c21b6680e97446640efacaaf3019f0 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau <122548697+MorganHoarau@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:06:11 +0100 Subject: [PATCH 09/10] Gate DualSense Bluetooth light bar reset on connection-animation window Also applies minor improvements like code name convention. --- .../Plugins/DualShock/DualShockGamepadHID.cs | 136 +++++++++++++----- 1 file changed, 98 insertions(+), 38 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs index 9fde09f99a..9270c5cbaa 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/DualShock/DualShockGamepadHID.cs @@ -7,6 +7,7 @@ using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.DualShock.LowLevel; using UnityEngine.InputSystem.Utilities; +using HIDReportType = UnityEngine.InputSystem.HID.HID.HIDReportType; ////TODO: figure out sensor formats and add support for acceleration, angularVelocity, and orientation (also add to base layout then) @@ -380,15 +381,35 @@ public class DualSenseGamepadHID : DualShockGamepad, IEventMerger, IEventPreProc private float? m_LowFrequencyMotorSpeed; private float? m_HighFrequenceyMotorSpeed; protected Color? m_LightBarColor; - private byte outputSequenceId; + private byte m_OutputSequenceId; private bool m_IsBluetooth; + + // Over Bluetooth the firmware drives the light bar during (and shortly after) the connection + // animation. The app takes over by sending a one-time reset report, but only once the animation + // has finished: a reset sent during it wedges the firmware until the app is restarted. + private enum BluetoothLedState + { + FirmwareControlled, // initial; light bar driven by firmware, reset not sent + WaitingToSend, // a color was requested; waiting out the animation, then reset -> color + AppControlled // color sent; app owns the light bar + } + private BluetoothLedState m_BtLedState; private bool m_LedResetSent; - private DualSenseHIDOutputReportPayload? m_PendingBtPayload; - private int m_PendingBtPayloadDelay; + private int m_LedResetGapFrames; + + // Event time (seconds) of the first and most recent Bluetooth report. The elapsed time between + // them gates the LED reset; timing it from detection lets the wait overlap with app start-up. + private double m_BtConnectTime = -1.0; + private double m_LastBtEventTime; + + // Wait this long after the controller is first seen before releasing firmware control of the + // light bar, to let the Bluetooth connection animation finish. The animation runs for under + // 5 seconds; 6 leaves a safety margin. + private const double k_BluetoothLedAnimationSeconds = 6.0; - // The Bluetooth LED reset and the color report must be sent as separate reports. - // A delay between them is also required to let the firmware process the reset first. - private const int k_BluetoothLedResetDelayFrames = 3; + // The reset and the color must be separate reports, with a short gap for the firmware to + // process the reset before the color is applied. + private const int k_BluetoothLedResetGapFrames = 3; protected override void FinishSetup() { @@ -402,7 +423,7 @@ protected override void FinishSetup() for (var i = 0; i < elements.Length; i++) { var element = elements[i]; - if (element.reportType == UnityEngine.InputSystem.HID.HID.HIDReportType.Output && + if (element.reportType == HIDReportType.Output && element.reportId == DualSenseHIDBluetoothInputReport.ExpectedReportId) { m_IsBluetooth = true; @@ -411,10 +432,19 @@ protected override void FinishSetup() } } m_LedResetSent = false; + m_BtLedState = BluetoothLedState.FirmwareControlled; + m_BtConnectTime = -1.0; base.FinishSetup(); } + private void UpdateBluetoothClock(double eventTime) + { + if (m_BtConnectTime < 0.0) + m_BtConnectTime = eventTime; + m_LastBtEventTime = eventTime; + } + private void SendBluetoothLedReset() { if (m_LedResetSent) @@ -424,7 +454,7 @@ private void SendBluetoothLedReset() { enableFlags2 = 0x08, // Release firmware control of the light bar so the app can drive it. }; - var command = DualSenseHIDBluetoothOutputReport.Create(resetPayload, ++outputSequenceId); + var command = DualSenseHIDBluetoothOutputReport.Create(resetPayload, ++m_OutputSequenceId); ExecuteCommand(ref command); m_LedResetSent = true; } @@ -487,9 +517,40 @@ public override void SetMotorSpeeds(float lowFrequency, float highFrequency) /// for the respective documentation regarding setting rumble and light bar color. public bool SetMotorSpeedsAndLightBarColor(float? lowFrequency, float? highFrequency, Color? color) { - var lf = lowFrequency.HasValue ? lowFrequency.Value : 0; - var hf = highFrequency.HasValue ? highFrequency.Value : 0; + var payload = BuildOutputPayload(lowFrequency, highFrequency, color); + + if (!m_IsBluetooth) + { + var command = DualSenseHIDUSBOutputReport.Create(payload); + return ExecuteCommand(ref command) >= 0; + } + + // Bluetooth: the light bar can only be driven once firmware control has been released, and + // that reset is gated on the connection-animation window (see OnNextUpdate). Until the light + // bar is app-controlled, remember the latest requested color and send only rumble inline so + // rumble stays responsive while the LED waits. Overwriting keeps the latest color and avoids + // out-of-order or double sends if more requests arrive before the reset completes. + if (color.HasValue && m_BtLedState != BluetoothLedState.AppControlled) + { + m_LightBarColor = color; + m_BtLedState = BluetoothLedState.WaitingToSend; + + var rumbleOnly = BuildOutputPayload(lowFrequency, highFrequency, null); + var rumbleCommand = DualSenseHIDBluetoothOutputReport.Create(rumbleOnly, ++m_OutputSequenceId); + return ExecuteCommand(ref rumbleCommand) >= 0; + } + + var btCommand = DualSenseHIDBluetoothOutputReport.Create(payload, ++m_OutputSequenceId); + return ExecuteCommand(ref btCommand) >= 0; + } + + private static DualSenseHIDOutputReportPayload BuildOutputPayload(float? lowFrequency, float? highFrequency, Color? color) + { + var lf = lowFrequency ?? 0f; + var hf = highFrequency ?? 0f; + // All effects must be set in a single report. Sending just a color with the rumble + // flags cleared would disable rumble, so the rumble flags are always included here. var payload = new DualSenseHIDOutputReportPayload { enableFlags1 = 0x1 | // Enable motor rumble. @@ -500,30 +561,13 @@ public bool SetMotorSpeedsAndLightBarColor(float? lowFrequency, float? highFrequ if (color.HasValue) { - payload.enableFlags2 = 0x4; + payload.enableFlags2 = 0x4; // Enable LED color. payload.redColor = (byte)NumberHelpers.NormalizedFloatToUInt(color.Value.r, byte.MinValue, byte.MaxValue); payload.greenColor = (byte)NumberHelpers.NormalizedFloatToUInt(color.Value.g, byte.MinValue, byte.MaxValue); payload.blueColor = (byte)NumberHelpers.NormalizedFloatToUInt(color.Value.b, byte.MinValue, byte.MaxValue); } - if (m_IsBluetooth) - { - // The light bar reset and color must be sent as separate reports, so defer the - // color payload and let OnNextUpdate send it once the reset has taken effect. - if (color.HasValue && !m_LedResetSent) - { - SendBluetoothLedReset(); - m_PendingBtPayload = payload; - m_PendingBtPayloadDelay = k_BluetoothLedResetDelayFrames; - return true; - } - - var btCommand = DualSenseHIDBluetoothOutputReport.Create(payload, ++outputSequenceId); - return ExecuteCommand(ref btCommand) >= 0; - } - - var command = DualSenseHIDUSBOutputReport.Create(payload); - return ExecuteCommand(ref command) >= 0; + return payload; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -611,6 +655,8 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) { // A 78-byte frame with reportId=0x01 is Bluetooth "simple mode". m_IsBluetooth = stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2; + if (m_IsBluetooth) + UpdateBluetoothClock(eventPtr.time); if (stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize1 || stateEvent->stateSizeInBytes == DualSenseHIDMinimalInputReport.ExpectedSize2) { @@ -629,6 +675,7 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) else if (genericReport->reportId == DualSenseHIDBluetoothInputReport.ExpectedReportId) { m_IsBluetooth = true; + UpdateBluetoothClock(eventPtr.time); var data = ((DualSenseHIDBluetoothInputReport*)stateEvent->state)->ToHIDInputReport(); *((DualSenseHIDInputReport*)stateEvent->state) = data; stateEvent->stateFormat = DualSenseHIDInputReport.Format; @@ -640,18 +687,31 @@ unsafe bool IEventPreProcessor.PreProcessEvent(InputEventPtr eventPtr) public void OnNextUpdate() { - if (m_PendingBtPayload.HasValue && m_IsBluetooth) + if (!m_IsBluetooth || m_BtLedState != BluetoothLedState.WaitingToSend) + return; + + // Wait out the connection animation (timed from when the controller was first seen) before + // releasing firmware control: a reset sent during the animation wedges the firmware. + if (m_BtConnectTime < 0.0 || (m_LastBtEventTime - m_BtConnectTime) < k_BluetoothLedAnimationSeconds) + return; + + if (!m_LedResetSent) { - if (m_PendingBtPayloadDelay > 0) - { - m_PendingBtPayloadDelay--; - return; - } + SendBluetoothLedReset(); + m_LedResetGapFrames = k_BluetoothLedResetGapFrames; + return; + } - var command = DualSenseHIDBluetoothOutputReport.Create(m_PendingBtPayload.Value, ++outputSequenceId); - ExecuteCommand(ref command); - m_PendingBtPayload = null; + if (m_LedResetGapFrames > 0) + { + m_LedResetGapFrames--; + return; } + + var payload = BuildOutputPayload(m_LowFrequencyMotorSpeed, m_HighFrequenceyMotorSpeed, m_LightBarColor); + var command = DualSenseHIDBluetoothOutputReport.Create(payload, ++m_OutputSequenceId); + ExecuteCommand(ref command); + m_BtLedState = BluetoothLedState.AppControlled; } // filter out three lower bits as jitter noise From 6333cb896d5870c836af933d6bb1255c881bf601 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau <122548697+MorganHoarau@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:49:48 +0100 Subject: [PATCH 10/10] Update CHANGELOG.md --- Packages/com.unity.inputsystem/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 0c4ff33650..c917944129 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixed an incorrect ArraysHelper.HaveDuplicateReferences implementation that didn't use its arguments right [ISXB-1792] (https://github.com/Unity-Technologies/InputSystem/pull/2376) - Fixed `InputAction.IsPressed`, `WasPressedThisFrame`, and `WasReleasedThisFrame` using a `ButtonControl`'s `pressPoint` when a binding also had an explicit `PressInteraction` with its own `pressPoint`, which could make those APIs disagree with the interaction's press and release behavior. Action-level press APIs now follow the interaction threshold when both are set explicitly. - Fixed `IndexOutOfRangeException` in `InputDeviceBuilder` when connecting an HID gamepad whose report descriptor declares a hat switch with Report Size 8 (e.g. ESP32-BLE-Gamepad). The HID layer now anchors the hat's directional sub-controls to the hat's own byte instead of letting the layout system auto-allocate a fresh byte for each [UUM-143659](https://jira.unity3d.com/browse/UUM-143659). -- Fixed DualSense controller sending garbage HID output data when connected over Bluetooth, which caused significant FPS drops and prevented haptics from working. The Bluetooth output report (ID `0x31`, 78 bytes, with CRC32 trailer) is now used when the controller is connected via Bluetooth, so rumble and light bar updates work on both USB and Bluetooth. [ISXB-1477](https://jira.unity3d.com/browse/ISXB-1477) +- Fixed DualSense rumble and light bar color not working, and causing frame rate drops, when the controller is connected over Bluetooth. [ISXB-1477](https://jira.unity3d.com/browse/ISXB-1477) ### Changed - Action-level `IsPressed`, `WasPressedThisFrame`, and `WasReleasedThisFrame` for bindings to `Vector2Control` / `StickControl` no longer consult a per-control `pressPoint` on the vector (that field was removed). Use a `Press` interaction to set a custom threshold, or rely on `defaultButtonPressPoint`.