Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 117 additions & 21 deletions Assets/Tests/InputSystem/Plugins/HIDTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,27 +218,9 @@

// The HID report descriptor is fetched from the device via an IOCTL.
var deviceId = runtime.AllocateDeviceId();
unsafe
{
runtime.SetDeviceCommandCallback(deviceId,
(id, commandPtr) =>
{
if (commandPtr->type == HID.QueryHIDReportDescriptorSizeDeviceCommandType)
return reportDescriptor.Length;

if (commandPtr->type == HID.QueryHIDReportDescriptorDeviceCommandType
&& commandPtr->payloadSizeInBytes >= reportDescriptor.Length)
{
fixed(byte* ptr = reportDescriptor)
{
UnsafeUtility.MemCpy(commandPtr->payloadPtr, ptr, reportDescriptor.Length);
return reportDescriptor.Length;
}
}
SetDeviceCommandCallbackToReturnReportDescriptor(deviceId, reportDescriptor);

return InputDeviceCommand.GenericFailure;
});
}
// Report device.
runtime.ReportNewInputDevice(
new InputDeviceDescription
Expand Down Expand Up @@ -309,6 +291,120 @@
////TODO: check hat switch
}

// This is used to mock out the IOCTL the HID device driver would use to return
// the report descriptor and its size.
unsafe void SetDeviceCommandCallbackToReturnReportDescriptor(int deviceId, byte[] reportDescriptor)
{
runtime.SetDeviceCommandCallback(deviceId,
(id, commandPtr) =>
{
if (commandPtr->type == HID.QueryHIDReportDescriptorSizeDeviceCommandType)
return reportDescriptor.Length;

if (commandPtr->type == HID.QueryHIDReportDescriptorDeviceCommandType
&& commandPtr->payloadSizeInBytes >= reportDescriptor.Length)
{
fixed(byte* ptr = reportDescriptor)
{
UnsafeUtility.MemCpy(commandPtr->payloadPtr, ptr, reportDescriptor.Length);
return reportDescriptor.Length;
}
}

return InputDeviceCommand.GenericFailure;
});
}

[Test]
[Category("HID Devices")]

// These descriptor values were generated with the Microsoft HID Authoring descriptor tool in
// https://github.com/microsoft/hidtools for the expexted values.
// Logical min 0, logical max 65535
[TestCase(16, new byte[] {0x16, 0x00, 0x00}, new byte[] { 0x27, 0xFF, 0xFF, 0x00, 0x00 }, 0, 65535, 0.01f)]
// Logical min -32768, logical max 32767
[TestCase(16, new byte[] {0x16, 0x00, 0x80}, new byte[] {0x26, 0xFF, 0x7F}, -32768, 32767, 0.01f)]
// Logical min 0, logical max 255
[TestCase(8, new byte[] {0x15, 00}, new byte[] {0x26, 0xFF, 0x00}, 0, 255, 0.01f)]
// Logical min -128, logical max 127
[TestCase(8, new byte[] {0x15, 0x80}, new byte[] {0x25, 0x7F}, -128, 127, 0.01f)]
// Logical min -16, logical max 15 (below 8 bit boundary)
[TestCase(5, new byte[] {0x15, 0xF0}, new byte[] {0x25, 0x0F}, -16, 15, 0)]
// Logical min 0, logical max 31 (below 8 bit boundary)
[TestCase(5, new byte[] {0x15, 0x00}, new byte[] {0x25, 0x1F}, 0, 31, 0)]
public void Devices_CanParseHIDDescritpor_WithSignedLogicalMinAndMaxSticks(byte reportSizeBits, byte[] logicalMinBytes, byte[] logicalMaxBytes, int logicalMinExpected, int logicalMaxExpected, float errorMargin)
{
// Dynamically create HID report descriptor for two analog sticks with parameterized logical min/max

var reportDescriptorStart = new byte[]
{
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x05, // Usage (Game Pad)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x30, // Usage (X)
};

var reportDescriptorEnd = new byte[]
{
0x75, reportSizeBits, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data,Var,Abs)
0xC0, // End Collection
};

// Concatenate to form final descriptor based on test parameters where logical min/max bytes
// are inserted in the middle.
var reportDescriptor = reportDescriptorStart.Concat(logicalMinBytes).
Concat(logicalMaxBytes).
Concat(reportDescriptorEnd).
ToArray();

// The HID report descriptor is fetched from the device via an IOCTL.
var deviceId = runtime.AllocateDeviceId();

// Callback to return the desired report descriptor.
SetDeviceCommandCallbackToReturnReportDescriptor(deviceId, reportDescriptor);

// Report device.
runtime.ReportNewInputDevice(
new InputDeviceDescription
{
interfaceName = HID.kHIDInterface,
manufacturer = "TestLogicalMinMaxParsing",
product = "TestHID",
capabilities = new HID.HIDDeviceDescriptor
{
vendorId = 0x321,
productId = 0x432
}.ToJson()
}.ToJson(), deviceId);

InputSystem.Update();

var device = (Joystick)InputSystem.GetDeviceById(deviceId);
Assert.That(device, Is.Not.Null);
Assert.That(device, Is.TypeOf<Joystick>());

var parsedDescriptor = JsonUtility.FromJson<HID.HIDDeviceDescriptor>(device.description.capabilities);

// Check we parsed the values as expected
foreach (var element in parsedDescriptor.elements)
{
if (element.usage == (int)HID.GenericDesktop.X)
{
Assert.That(element.logicalMin, Is.EqualTo(logicalMinExpected));
Assert.That(element.logicalMax, Is.EqualTo(logicalMaxExpected));
}
else
Assert.Fail("Could not find X and Y elements in descriptor");

Check warning on line 401 in Assets/Tests/InputSystem/Plugins/HIDTests.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

Assets/Tests/InputSystem/Plugins/HIDTests.cs#L401

Added line #L401 was not covered by tests
}

// Stick vector 2 should be centered at (0,0) when initialized
Assert.That(device.stick.ReadValue(), Is.EqualTo(new Vector2(0f, 0f)).Using(Vector2EqualityComparer.Instance));
}

[Test]
[Category("Devices")]
public void Devices_CanCreateGenericHID_FromDeviceWithParsedReportDescriptor()
Expand Down Expand Up @@ -1026,7 +1122,7 @@
}

[StructLayout(LayoutKind.Explicit)]
struct SimpleJoystickLayout : IInputStateTypeInfo
struct SimpleJoystickLayoutWithStick : IInputStateTypeInfo
{
[FieldOffset(0)] public byte reportId;
[FieldOffset(1)] public ushort x;
Expand Down Expand Up @@ -1069,7 +1165,7 @@
Assert.That(device, Is.TypeOf<Joystick>());
Assert.That(device["Stick"], Is.TypeOf<StickControl>());

InputSystem.QueueStateEvent(device, new SimpleJoystickLayout { reportId = 1, x = ushort.MaxValue, y = ushort.MinValue });
InputSystem.QueueStateEvent(device, new SimpleJoystickLayoutWithStick { reportId = 1, x = ushort.MaxValue, y = ushort.MinValue });
InputSystem.Update();

Assert.That(device["stick"].ReadValueAsObject(),
Expand Down
4 changes: 2 additions & 2 deletions Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ public InputControlLayout Build()
var yElementParameters = yElement.DetermineParameters();

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

builder.AddControl(stickName + "/y")
.WithFormat(yElement.isSigned ? InputStateBlock.FormatSBit : InputStateBlock.FormatBit)
.WithFormat(yElement.DetermineFormat())
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was included from #2245

Copy link
Collaborator

@ekcoh ekcoh Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it required to always determine format for any control? Also buttons support this (but I cannot recall ever seeing it in practise), but definitely e.g. triggers go into same territory

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and we also did it before. Not sure why assigned it the SBit/Bit format before but we do read multiple "bits" if we had them (e.g. in stateBlock.ReadFloat()). So it might have been to deal with cases where we could use like "10 bits" to define an axis. I'll have a look and test this as well to see if there are any other changes needed. Thanks.

.WithByteOffset((uint)(yElement.reportOffsetInBits / 8 - byteOffset))
.WithBitOffset((uint)(yElement.reportOffsetInBits % 8))
.WithSizeInBits((uint)yElement.reportSizeInBits)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ private unsafe static int ReadData(int itemSize, byte* currentPtr, byte* endPtr)
{
if (currentPtr >= endPtr)
return 0;
return *currentPtr;
return (sbyte)*currentPtr;
}

// Read short.
Expand All @@ -277,7 +277,7 @@ private unsafe static int ReadData(int itemSize, byte* currentPtr, byte* endPtr)
return 0;
var data1 = *currentPtr;
var data2 = *(currentPtr + 1);
return (data2 << 8) | data1;
return (short)((data2 << 8) | data1);
}

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

return (data4 << 24) | (data3 << 24) | (data2 << 8) | data1;
return (data4 << 24) | (data3 << 16) | (data2 << 8) | data1;
}

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