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.
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
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:
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.
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…
It’s the worst of all: Multiple devices sending different amounts of reports with different sizes and layouts
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:
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.
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)).