HID device with multiple reports

Hi @Rene-Damm ,

I’m still trying to get the custom HID stuff to work using InputSystem.onFindLayoutForDevice and InputSystem.RegisterLayoutBuilder(). I’ve duplicated lots of stuff from your HID class to my own namespace because the auto-generated layout is already pretty close to what I want. All those little helpers like HIDElementDescriptor.DetermineFormat() are great. Shame they’re tucked away behind internal…!

The device sends its data within multiple reports, this is correctly set per element inside the HIDDeviceDescriptor. When adding new controls via AddControl() though, there is no way to define which report it belongs to, only the other properties like e.g. offset and size are used.

So my question is: Is there any way to make a device work sending multiple reports using the InputSystem.onFindLayoutForDevice and InputSystem.RegisterLayoutBuilder() approach?

What I had working before was using IInputStateCallbackReceiver and manually copying the reports to a state struct containing the combined state. But that approach is limited because you need to have different state structs for different device types. Hence, I tried to make it work by reading the HIDDeviceDescriptor and generate the layouts on the fly.

Try “PreProcessEvent” similar to what I did here InputSystem/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs at develop · Unity-Technologies/InputSystem · GitHub

Meaning there is no way to “just” let the layout system handle the reportId (as it does with offsets and sizes)? I want to clarify that before moving on :slight_smile:

Thanks for linking your implementation, it gave a few good pointers. So, looking at the code: If I understand correctly there are three different reports regarding e.g. the DualSense device (Minimal, USB, BT) you’re unifying to DualSenseHIDInputReport before sending them downstream. And you know all of your expected device layouts (DualShock3, DualShock4 etc).

This is different from my use-case in two dimensions:

  1. You have several reports all containing the full state but in different (byte-)layouts. I have several reports that I need to combine to one full state.
  2. You know your expected device layouts and can layout your structs to match it. I do not know every device type – they all share the basic stuff but one may have 1 button, the other may have 49 buttons.

Because of the unknown-devices-factor I wanted to use the InputControlLayout.Builder but if I have to declare the state struct beforehand so I have a target struct to merge my state reports into, I’m at a loss how to do this.

Hmm ok, maybe I could just put the merged events into an anonymous buffer instead of a struct and move the control’s offsets to match the merged buffer.

I’d like to fiddle around with the data like you did in your linked DualShock code but it uses internal interfaces! Both IEventPreProcessor (the impl you linked) and IEventMerger are internal:confused:

Is your device sends the same report type, just you have many different report types? Or it’s one device, sending different report types?

Can you realistically build a layout of all possible controls?

are internal

Ah right, sorry about that, I needed them to do DS/Switch support, maybe we do need to make them public.

It’s the worst of all: Multiple devices sending different amounts of reports with different sizes and layouts :slight_smile:

Yes, at least I think so. They are all made of two axis, but different button count and report layout.

Forgive my frustration back then, there were just so many roadblocks.
Using IEventPreProcessor would not have worked either way because you can only inject same or smaller sized reports with it – but I needed to up the size a bit. I was able to do what I wanted using IInputStateCallbackReceiver.OnStateEvent() merging the multiple reports into one.

To account for different report sizes I’ve reserved a “big” empty struct:

[StructLayout(Size = 99)]
internal struct MergedReports {}

I’d rather use a dynamically sized buffer but I have to use the struct because I need to call InputState.Change() and it only accepts structs. This is rather limiting in this particular case.

I think how I would approach the problem in general is something along the lines of

// State struct that entirely ignores report formats and
// just goes only by the controls that should sit on the device.
public struct MyDeviceState : IInputStateTypeInfo
{
    // Say the device has two buttons contained in two
    // separate reports.
    [InputControl(layout = "Button")]
    public float buttonA;
    [InputControl(layout = "Button")]
    public float buttonB;
}

[StructLayout(LayoutKind.Explicit)]
internal struct MyDeviceReport
{
    [FieldOffset(0)] public byte reportId;

    // Could define two separate structs for this but easier to just
    // fake a "union" through explicit offsets.
    [FieldOffset(1)] public byte button1;
    [FieldOffset(1)] public byte button2;
}

[InputControlLayout(stateType = typeof(MyDeviceState))]
public class MyDevice : InputDevice, IInputStateCallbackReceiver
{
    public ButtonControl button1 { get; private set; }
    public ButtonControl button2 { get; private set; }

    public void OnStateEvent(InputEventPtr eventPtr)
    {
        var report = StateEvent.GetState<MyDeviceReport>(eventPtr);
        switch (report.reportId)
        {
            case 1:
                InputState.Change(button1, 255f / report.button1);
                break;
            case 2:
                InputState.Change(button2, 255f / report.button2);
                break;
        }
    }
}

If none of the reports actually share any control it’s also possible to just define a state struct for each report and then define one state struct that embeds all the other ones. And OnStateEvent would just do InputState.Change on the specific nested state struct that corresponds to the received report.

For the sake of completeness, that’d be the approach with nested state structs.

public struct MyDeviceStateA : IInputStateTypeInfo
{
    [InputControl(name = "ButtonA", layout = "Button")]
    public byte button;
}

public struct MyDeviceStateB : IInputStateTypeInfo
{
    [InputControl(name = "ButtonB", layout = "Button")]
    public byte button;
}

public struct MyDeviceState : IInputStateTypeInfo
{
    public MyDeviceStateA stateA;
    public MyDeviceStateB stateB;
}

[InputControlLayout(stateType = typeof(MyDeviceState))]
public MyDevice : InputDevice, IInputStateCallbackReceiver
{
    public unsafe void OnStateEvent(InputEventPtr eventPtr)
    {
        var reportIdPtr = (byte*)StateEvent.From(eventPtr)->state;
        var statePtr = reportIdPtr + 1;
        var currentState = *(MyDeviceState*)currentStatePtr;
        switch (*reportIdPtr)
        {
            case 1:
                currentState.stateA = *(MyDeviceStateA*)statePtr;
                break;
            case 2:
                currentState.stateB = *(MyDeviceStateB*)statePtr;
                break;
        }
        InputState.Change(this, currentState);
    }
}

Not terribly great that it requires creating a full state to pass to InputState.Change but workable.

Thanks for your effort, but it’s not really applicable to my use case.

But I changed my code to directly write to the currentStatePtr now too. With that I learned the buffer does not neccessarily have the size of the struct but is calculated (Pseudo-code-guess: stateBufferSize = byteAlign(lastElement.offset + lastElement.size)).

Here’s my current implementation, maybe you can have a quick look?

Regarding your code sample above: Didn’t you forget to offset the pointer by stateBlock.byteOffset?