Skip to content

Commit a0ed434

Browse files
FIX: Incorrect HID Stick values for LogicalMinimum with negative values (ISXB-1735) (#2246)
Co-authored-by: Copilot <[email protected]>
1 parent 95870f5 commit a0ed434

File tree

4 files changed

+134
-26
lines changed

4 files changed

+134
-26
lines changed

Assets/Tests/InputSystem/Plugins/HIDTests.cs

Lines changed: 127 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -218,27 +218,9 @@ public void Devices_CanCreateGenericHID_FromDeviceWithBinaryReportDescriptor()
218218

219219
// The HID report descriptor is fetched from the device via an IOCTL.
220220
var deviceId = runtime.AllocateDeviceId();
221-
unsafe
222-
{
223-
runtime.SetDeviceCommandCallback(deviceId,
224-
(id, commandPtr) =>
225-
{
226-
if (commandPtr->type == HID.QueryHIDReportDescriptorSizeDeviceCommandType)
227-
return reportDescriptor.Length;
228221

229-
if (commandPtr->type == HID.QueryHIDReportDescriptorDeviceCommandType
230-
&& commandPtr->payloadSizeInBytes >= reportDescriptor.Length)
231-
{
232-
fixed(byte* ptr = reportDescriptor)
233-
{
234-
UnsafeUtility.MemCpy(commandPtr->payloadPtr, ptr, reportDescriptor.Length);
235-
return reportDescriptor.Length;
236-
}
237-
}
222+
SetDeviceCommandCallbackToReturnReportDescriptor(deviceId, reportDescriptor);
238223

239-
return InputDeviceCommand.GenericFailure;
240-
});
241-
}
242224
// Report device.
243225
runtime.ReportNewInputDevice(
244226
new InputDeviceDescription
@@ -309,6 +291,130 @@ public void Devices_CanCreateGenericHID_FromDeviceWithBinaryReportDescriptor()
309291
////TODO: check hat switch
310292
}
311293

294+
// This is used to mock out the IOCTL the HID device driver would use to return
295+
// the report descriptor and its size.
296+
unsafe void SetDeviceCommandCallbackToReturnReportDescriptor(int deviceId, byte[] reportDescriptor)
297+
{
298+
runtime.SetDeviceCommandCallback(deviceId,
299+
(id, commandPtr) =>
300+
{
301+
if (commandPtr == null)
302+
return InputDeviceCommand.GenericFailure;
303+
304+
if (commandPtr->type == HID.QueryHIDReportDescriptorSizeDeviceCommandType)
305+
return reportDescriptor.Length;
306+
307+
if (commandPtr->type == HID.QueryHIDReportDescriptorDeviceCommandType
308+
&& commandPtr->payloadSizeInBytes >= reportDescriptor.Length)
309+
{
310+
fixed(byte* ptr = reportDescriptor)
311+
{
312+
UnsafeUtility.MemCpy(commandPtr->payloadPtr, ptr, reportDescriptor.Length);
313+
return reportDescriptor.Length;
314+
}
315+
}
316+
317+
return InputDeviceCommand.GenericFailure;
318+
});
319+
}
320+
321+
[Test]
322+
[Category("HID Devices")]
323+
// These descriptor values were generated with the Microsoft HID Authoring descriptor tool in
324+
// https://github.com/microsoft/hidtools for the expected value:
325+
// Logical min 0, logical max 65535
326+
[TestCase(16, new byte[] {0x16, 0x00, 0x00}, new byte[] { 0x27, 0xFF, 0xFF, 0x00, 0x00 }, 0, 65535)]
327+
// Logical min -32768, logical max 32767
328+
[TestCase(16, new byte[] {0x16, 0x00, 0x80}, new byte[] {0x26, 0xFF, 0x7F}, -32768, 32767)]
329+
// Logical min 0, logical max 255
330+
[TestCase(8, new byte[] {0x15, 00}, new byte[] {0x26, 0xFF, 0x00}, 0, 255)]
331+
// Logical min -128, logical max 127
332+
[TestCase(8, new byte[] {0x15, 0x80}, new byte[] {0x25, 0x7F}, -128, 127)]
333+
// Logical min -16, logical max 15 (below 8 bit boundary)
334+
[TestCase(5, new byte[] {0x15, 0xF0}, new byte[] {0x25, 0x0F}, -16, 15)]
335+
// Logical min 0, logical max 31 (below 8 bit boundary)
336+
[TestCase(5, new byte[] {0x15, 0x00}, new byte[] {0x25, 0x1F}, 0, 31)]
337+
// Logical min -4096, logical max 4095 (crosses byte boundary)
338+
[TestCase(13, new byte[] {0x16, 0x00, 0xF0}, new byte[] {0x26, 0xFF, 0x0F}, -4096, 4095)]
339+
// Logical min 0, logical max 8191 (crosses byte boundary)
340+
[TestCase(13, new byte[] {0x15, 0x00}, new byte[] {0x26, 0xFF, 0x1F}, 0, 8191)]
341+
// Logical min 0, logical max 16777215 (24 bit)
342+
[TestCase(24, new byte[] {0x15, 0x00}, new byte[] {0x27, 0xFF, 0xFF, 0xFF, 0x00}, 0, 16777215)]
343+
// Logical min -8388608, logical max 8388607 (24 bit)
344+
[TestCase(24, new byte[] {0x17, 0x00, 0x00, 0x80, 0xFF}, new byte[] {0x27, 0xFF, 0xFF, 0x7F, 0x00}, -8388608, 8388607)]
345+
// Logical min -72, logical max -35
346+
[TestCase(8, new byte[] {0x15, 0xB8}, new byte[] {0x25, 0xDD}, -72, -35)]
347+
// Logical min 30, logical max 78
348+
[TestCase(8, new byte[] {0x15, 0x1E}, new byte[] {0x25, 0x4E}, 30, 78)]
349+
350+
public void Devices_CanParseHIDDescriptor_WithSignedLogicalMinAndMaxValues(byte reportSizeBits, byte[] logicalMinBytes, byte[] logicalMaxBytes, int logicalMinExpected, int logicalMaxExpected)
351+
{
352+
// Dynamically create HID report descriptor for one X-axis with parameterized logical min/max
353+
var reportDescriptorStart = new byte[]
354+
{
355+
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
356+
0x09, 0x05, // Usage (Game Pad)
357+
0xA1, 0x01, // Collection (Application)
358+
0x85, 0x01, // Report ID (1)
359+
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
360+
0x09, 0x30, // Usage (X)
361+
};
362+
363+
var reportDescriptorEnd = new byte[]
364+
{
365+
0x75, reportSizeBits, // Report Size (8)
366+
0x95, 0x01, // Report Count (1)
367+
0x81, 0x02, // Input (Data,Var,Abs)
368+
0xC0, // End Collection
369+
};
370+
371+
// Concatenate to form final descriptor based on test parameters where logical min/max bytes
372+
// are inserted in the middle.
373+
var reportDescriptor = reportDescriptorStart.Concat(logicalMinBytes).
374+
Concat(logicalMaxBytes).
375+
Concat(reportDescriptorEnd).
376+
ToArray();
377+
378+
// The HID report descriptor is fetched from the device via an IOCTL.
379+
var deviceId = runtime.AllocateDeviceId();
380+
381+
// Callback to return the desired report descriptor.
382+
SetDeviceCommandCallbackToReturnReportDescriptor(deviceId, reportDescriptor);
383+
384+
// Report device.
385+
runtime.ReportNewInputDevice(
386+
new InputDeviceDescription
387+
{
388+
interfaceName = HID.kHIDInterface,
389+
manufacturer = "TestLogicalMinMaxParsing",
390+
product = "TestHID",
391+
capabilities = new HID.HIDDeviceDescriptor
392+
{
393+
vendorId = 0x321,
394+
productId = 0x432
395+
}.ToJson()
396+
}.ToJson(), deviceId);
397+
398+
InputSystem.Update();
399+
400+
var device = InputSystem.GetDeviceById(deviceId);
401+
Assert.That(device, Is.Not.Null);
402+
403+
var parsedDescriptor = JsonUtility.FromJson<HID.HIDDeviceDescriptor>(device.description.capabilities);
404+
405+
// Check we parsed the values as expected
406+
foreach (var element in parsedDescriptor.elements)
407+
{
408+
if (element.usage == (int)HID.GenericDesktop.X)
409+
{
410+
Assert.That(element.logicalMin, Is.EqualTo(logicalMinExpected));
411+
Assert.That(element.logicalMax, Is.EqualTo(logicalMaxExpected));
412+
}
413+
else
414+
Assert.Fail("Could not find X element in descriptor");
415+
}
416+
}
417+
312418
[Test]
313419
[Category("Devices")]
314420
public void Devices_CanCreateGenericHID_FromDeviceWithParsedReportDescriptor()
@@ -1026,7 +1132,7 @@ public void Devices_GenericHIDConvertsXAndYUsagesToStickControl()
10261132
}
10271133

10281134
[StructLayout(LayoutKind.Explicit)]
1029-
struct SimpleJoystickLayout : IInputStateTypeInfo
1135+
struct SimpleJoystickLayoutWithStick : IInputStateTypeInfo
10301136
{
10311137
[FieldOffset(0)] public byte reportId;
10321138
[FieldOffset(1)] public ushort x;
@@ -1069,7 +1175,7 @@ public void Devices_GenericHIDXAndYDrivesStickControl()
10691175
Assert.That(device, Is.TypeOf<Joystick>());
10701176
Assert.That(device["Stick"], Is.TypeOf<StickControl>());
10711177

1072-
InputSystem.QueueStateEvent(device, new SimpleJoystickLayout { reportId = 1, x = ushort.MaxValue, y = ushort.MinValue });
1178+
InputSystem.QueueStateEvent(device, new SimpleJoystickLayoutWithStick { reportId = 1, x = ushort.MaxValue, y = ushort.MinValue });
10731179
InputSystem.Update();
10741180

10751181
Assert.That(device["stick"].ReadValueAsObject(),

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ however, it has to be formatted properly to pass verification tests.
3030
- Fixed an issue where `InputActionReference.Create(null)` returns `null` instead "of a reference object to the given action" as documented behaviour for the API. (ISXB-1693)
3131
- Fixed an issue where `InputActionReference.Set(null)` does not update the cached input action reference nor the `ScriptableObject.name` leading to incorrect action being returned and reference showing up as the path to the previously assigned action in Inspector. (ISXB-1694)
3232
- Fixed an issue where Inspector field of type `InputActionReference` isn't reflecting broken reference in case action is deleted from asset, action map is deleted from asset, or the asset itself is deleted. (ISXB-1701)
33+
- Fixed HID parsing not handling logical minimum and maximum values correctly when they are negative. This applies for platforms that parse HID descriptors in the package, e.g. macOS at the moment.
34+
- Fix usage of correct data format for stick axes in HID Layout Builder ([User contribution](https://github.com/Unity-Technologies/InputSystem/pull/2245))
3335

3436
## [1.15.0] - 2025-10-03
3537

Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HID.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ public InputControlLayout Build()
385385
var yElementParameters = yElement.DetermineParameters();
386386

387387
builder.AddControl(stickName + "/x")
388-
.WithFormat(xElement.isSigned ? InputStateBlock.FormatSBit : InputStateBlock.FormatBit)
388+
.WithFormat(xElement.DetermineFormat())
389389
.WithByteOffset((uint)(xElement.reportOffsetInBits / 8 - byteOffset))
390390
.WithBitOffset((uint)(xElement.reportOffsetInBits % 8))
391391
.WithSizeInBits((uint)xElement.reportSizeInBits)
@@ -394,7 +394,7 @@ public InputControlLayout Build()
394394
.WithProcessors(xElement.DetermineProcessors());
395395

396396
builder.AddControl(stickName + "/y")
397-
.WithFormat(yElement.isSigned ? InputStateBlock.FormatSBit : InputStateBlock.FormatBit)
397+
.WithFormat(yElement.DetermineFormat())
398398
.WithByteOffset((uint)(yElement.reportOffsetInBits / 8 - byteOffset))
399399
.WithBitOffset((uint)(yElement.reportOffsetInBits % 8))
400400
.WithSizeInBits((uint)yElement.reportSizeInBits)

Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HIDParser.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ private unsafe static int ReadData(int itemSize, byte* currentPtr, byte* endPtr)
267267
{
268268
if (currentPtr >= endPtr)
269269
return 0;
270-
return *currentPtr;
270+
return (sbyte)*currentPtr;
271271
}
272272

273273
// Read short.
@@ -277,7 +277,7 @@ private unsafe static int ReadData(int itemSize, byte* currentPtr, byte* endPtr)
277277
return 0;
278278
var data1 = *currentPtr;
279279
var data2 = *(currentPtr + 1);
280-
return (data2 << 8) | data1;
280+
return (short)((data2 << 8) | data1);
281281
}
282282

283283
// Read int.
@@ -291,7 +291,7 @@ private unsafe static int ReadData(int itemSize, byte* currentPtr, byte* endPtr)
291291
var data3 = *(currentPtr + 2);
292292
var data4 = *(currentPtr + 3);
293293

294-
return (data4 << 24) | (data3 << 24) | (data2 << 8) | data1;
294+
return (data4 << 24) | (data3 << 16) | (data2 << 8) | data1;
295295
}
296296

297297
Debug.Assert(false, "Should not reach here");

0 commit comments

Comments
 (0)