Search Unity

New Input System problem - RX2SIM Game Controller: Every second frame is zero

Discussion in 'Input System' started by zulu79, Apr 19, 2020.

  1. zulu79

    zulu79

    Joined:
    Oct 8, 2019
    Posts:
    14
    Hello,

    I am new to Unity and I wanted to test the new input system, because the user should be able to select from an input device, if multiple are connected, what I couldn't figure out in the old system.

    For some reason the new input system sets for my device - RX2SIM Game Controller - in every second frame almost all axis values to zero. What might be the reason for this? The RX2SIM Game Controller works with the old input system and other programs without issues. My second device, an old Sony Gamepad works also without problems. System is Windows 10 + Unity 2019.3.2f1, 2020.2.0a7 + Input System Version 1.0.0-preview.7

    I overwritten in a test the HID Fallback, according to this HID Support page, but so far without success. Maybe I should focus on this?

    Any help would be appreciated.

    Unity_NewInputSystem_Problem.png
     
  2. zulu79

    zulu79

    Joined:
    Oct 8, 2019
    Posts:
    14
    The problem seems to be, that my device sends not all information in one frame, but in two successive. The two frames are marked, i think, by the "Report ID", as shown in the HID Descriptor. I guess, that Unity does not understand, that the 8 axis channels + 8 buttons are not sent in one frame, but in two.

    - Axis 0..3 in first frame
    - Axis 4..7 and 8 buttons in second frame

    Therefore Unity's interpreted signals are changing every output frame between the values of the two data frames.

    Other devices (see blue marked one in figure as example), which send all information in one frame are working normally.

    Is there a way to say Unity to take the Report ID into account, or maybe another solution?

    Unity_NewInputSystem_Problem_002.png

    Raw data test
    Unity_NewInputSystem_Problem_003.png
     
  3. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    The existing HID support is indeed ill-equipped to handle this automatically. ATM it only works with a single report format.

    It is possible to support this with some custom code. Essentially you need to make your device an IInputStateCallbackReceiver. This will bypass some of the default state handling in the system and give you control over rolling your own.

    Prepare for some frightening looking code... :)

    Code (CSharp):
    1.     public struct ReportFormat1
    2.     {
    3.         //axis 0..3
    4.     }
    5.  
    6.     public struct ReportFormat2
    7.     {
    8.         //axis 4..7
    9.         //buttons
    10.     }
    11.  
    12.     public struct RX2SIMState : IInputStateTypeInfo
    13.     {
    14.         public FourCC format => new FourCC('R', 'X', 'S', 'M');
    15.  
    16.         [InputControl(name = "axis0", layout = "Axis", offset = 2, ...)]
    17.         [InputControl(name = "axis1", layout = "Axis", offset = 4, ...)]
    18.         //etc
    19.         public ReportFormat1 report1;
    20.         [InputControl(name = "axis4", layout = "Axis", offset = 10, ...)] // Put after sizeof(report1).
    21.         [InputControl(name = "axis7", layout = "Axis", offset = 12, ...)]
    22.         //etc
    23.         public ReportFormat2 report2;
    24.     }
    25.  
    26.     [InputControlLayout(stateType = typeof(RX2SIMState))]
    27.     public class RX2SIM : InputDevice, IInputStateCallbackReceiver
    28.     {
    29.         public void OnNextUpdate()
    30.         {
    31.         }
    32.  
    33.         public unsafe void OnStateEvent(InputEventPtr eventPtr)
    34.         {
    35.             // Refuse delta events.
    36.             if (eventPtr.IsA<DeltaStateEvent>())
    37.                 return;
    38.  
    39.             var stateEventPtr = StateEvent.From(eventPtr);
    40.             if (stateEventPtr->stateFormat != new FourCC('H', 'I', 'D'))
    41.                 return;
    42.  
    43.             var reportPtr = (byte*)stateEventPtr->state;
    44.             var reportId = *reportPtr;
    45.             var reportStatePtr = (reportPtr + 1); // or wherever the actual report starts.
    46.          
    47.             // We have two options here. We can either use InputState.Change with a DeltaStateEvent that we set up
    48.             // from the event we have received (and simply update either report1 or report2 only) or we can merge
    49.             // our current state with the state we have just received. The latter is simpler so we do that here.
    50.  
    51.             var newState = default(RX2SIMState);
    52.             // Can opt to only copy the state that we won't override. We don't bother here.
    53.             UnsafeUtility.MemCpy(&newState, (byte*)currentStatePtr + stateBlock.byteOffset, sizeof(RX2SIMState));
    54.  
    55.             if (reportId == 1)
    56.                 UnsafeUtility.MemCpy(&newState.report1, reportStatePtr, sizeof(ReportFormat1));
    57.             else if (reportId == 2)
    58.                 UnsafeUtility.MemCpy(&newState.report2, reportStatePtr, sizeof(ReportFormat2));
    59.          
    60.             // Apply the state change. Don't simply MemCpy over currentStatePtr as that will lead to various
    61.             // malfunctions. The system needs to do the memcpy itself.
    62.             InputState.Change(this, newState, eventPtr: eventPtr);
    63.         }
    64.  
    65.         public bool GetStateOffsetForEvent(InputControl control, InputEventPtr eventPtr, ref uint offset)
    66.         {
    67.             // If this isn't implemented, some stuff like auto-switching won't work correctly.
    68.             // If you want to implement this, return a state offset corresponding to the control in the given input event.
    69.             return false;
    70.         }
    71.     }
    72.  
    I assume that the debugger will get confused over this when looking at individual events but the outcome on the device should be correct.
     
  4. Fenrisul

    Fenrisul

    Joined:
    Jan 2, 2010
    Posts:
    618
    Oof lol... there's no good reason why RX2SIM had to send in two reports... None. My giant HOTAS Warthog controllers send a single report...
     
  5. zulu79

    zulu79

    Joined:
    Oct 8, 2019
    Posts:
    14
    @Fenrisul
    That's something I can't change.

    @Rene-Damm
    Thank you very much for the detailed explanation!

    Your suggestion with "IInputStateCallbackReceiver" is comletely new to me. Meanwhile I found a solution that works fine. It is partially similar to your suggestion. It is mainly based on Unity's HID-Support help page. Based on this I use InputDeviceMatcher to overwrite the default HID setting for the RX2SIM device with my custom one, where I store the "reportId" as a Control-Layer of type "Integer" ("reportId_workaround_as_integer").

    The InputSystem.onEvent is used then to process all events manually, where I can easyly assign both data frame's axis information to the correct axis (and handle all other devices too). I'm very happy with this solution. I tried the worklfow with Input Actions too, but in the code below everything is in one file and is thus more compact.

    Code (CSharp):
    1.  
    2. public partial class Game_Main : MonoBehaviour
    3. {
    4.     // used axis channels in game
    5.     float[] input_channel_from_event_proccessing = new float[8];
    6.  
    7.     // for Unity workaround with this device
    8.     public RX2SIM_Game_Controller RX2SIM_Game_Controller_;
    9.  
    10.  
    11.  
    12.     // ############################################################################
    13.     // on event
    14.     // ############################################################################
    15.     private void OnEnable()
    16.     {
    17.         InputSystem.onEvent += (eventPtr, device) => IO_Proccess_Input(eventPtr, device);
    18.     }
    19.     private void OnDisable()
    20.     {
    21.         InputSystem.onEvent -= (eventPtr, device) => IO_Proccess_Input(eventPtr, device);
    22.     }
    23.     // ############################################################################
    24.  
    25.  
    26.  
    27.  
    28.  
    29.     // ############################################################################
    30.     // Input System Event
    31.     // ############################################################################
    32.     void IO_Proccess_Input(InputEventPtr eventPtr, InputDevice device)
    33.     {
    34.         // Ignore anything that isn't a state event.
    35.         if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
    36.             return;
    37.  
    38.         // proccess selected device
    39.         if (device.name.Contains(connected_input_devices_names[selected_input_device_id]))
    40.         {
    41.             // if device is a gamepad
    42.             if (connected_input_devices_type[selected_input_device_id].Contains("Gamepad"))
    43.             {
    44.                 for (int i = 0; i < 8; i++) input_channel_from_event_proccessing[i] = 0;
    45.  
    46.                 foreach (var each_control in ((Gamepad)device).allControls)
    47.                 {
    48.                     if (each_control.displayName.Contains("Left Stick X"))
    49.                         ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[0]);
    50.                     if (each_control.displayName.Contains("Left Stick Y"))
    51.                         ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[1]);
    52.                     if (each_control.displayName.Contains("Right Stick X"))
    53.                         ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[2]);
    54.                     if (each_control.displayName.Contains("Right Stick Y"))
    55.                         ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[3]);
    56.                     if (each_control.displayName.Contains("Left Trigger"))
    57.                         ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[4]);
    58.                     if (each_control.displayName.Contains("Right Trigger"))
    59.                         ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[5]);
    60.                 }
    61.                 input_channel_from_event_proccessing[6] = 0;
    62.                 input_channel_from_event_proccessing[7] = 0;
    63.  
    64.                 // Can handle events yourself, for example, and then stop them from further processing by marking them as handled.
    65.                 eventPtr.handled = true;
    66.             }
    67.  
    68.             // if device is a joystick
    69.             if (connected_input_devices_type[selected_input_device_id].Contains("Joystick"))
    70.             {
    71.                 // process standard Joysticks
    72.                 if (!device.description.product.Contains("RX2SIM Game Controller"))
    73.                 {
    74.                     for (int i = 0; i < 8; i++) input_channel_from_event_proccessing[i] = 0;
    75.  
    76.                     foreach (var each_control in ((Joystick)device).allControls)
    77.                     {
    78.                         if (each_control.displayName.Contains("Stick X"))
    79.                             ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[0]);
    80.                         if (each_control.displayName.Contains("Stick Y"))
    81.                             ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[1]);
    82.                         if (each_control.displayName.Contains("Rz"))
    83.                             ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[2]);
    84.                         if (each_control.displayName.Contains("Z"))
    85.                             ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[3]);
    86.                         if (each_control.displayName.Contains("Rx"))
    87.                             ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[4]);
    88.                         if (each_control.displayName.Contains("Ry"))
    89.                             ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[5]);
    90.                     }
    91.                     input_channel_from_event_proccessing[6] = 0;
    92.                     input_channel_from_event_proccessing[7] = 0;
    93.                 }
    94.                 else // workaround for not functioning "RX2SIM Game Controller" device
    95.                 {
    96.                     int reportId_workaround_as_integer = 0;
    97.  
    98.                     foreach (var each_control in ((RX2SIM_Game_Controller)device).allControls)
    99.                     {
    100.                         // get reportId
    101.                         if (each_control.name.Contains("reportId_workaround_as_integer"))
    102.                             ((IntegerControl)each_control).ReadValueFromEvent(eventPtr, out reportId_workaround_as_integer);
    103.  
    104.                         // process first data frame wih axes 0..3
    105.                         if (reportId_workaround_as_integer == 1)
    106.                         {
    107.                             for (int i = 0; i < 4; i++)
    108.                             {
    109.                                 if (each_control.name.Contains("axis" + i.ToString()))
    110.                                 {
    111.                                     ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[i]);
    112.                                     input_channel_from_event_proccessing[i] = (input_channel_from_event_proccessing[i] - 0.5f) * 2f;
    113.                                 }
    114.                             }
    115.                         }
    116.                         // process second data frame wih axes 4..7 (following 8 buttons are not considered yet)
    117.                         if (reportId_workaround_as_integer == 2)
    118.                         {
    119.                             for (int i = 4; i < 8; i++)
    120.                             {
    121.                                 if (each_control.name.Contains("axis" + (i - 4).ToString()))
    122.                                 {
    123.                                     ((AxisControl)each_control).ReadValueFromEvent(eventPtr, out input_channel_from_event_proccessing[i]);
    124.                                     input_channel_from_event_proccessing[i] = (input_channel_from_event_proccessing[i] - 0.5f) * 2f;
    125.                                 }
    126.                             }
    127.                         }
    128.                     }
    129.  
    130.                 }
    131.  
    132.                 // Can handle events yourself, for example, and then stop them from further processing by marking them as handled.
    133.                 eventPtr.handled = true;
    134.             }
    135.         }
    136.     }
    137.     // ############################################################################
    138.  
    139.  
    140.  
    141.     // ############################################################################
    142.     // Awake
    143.     // ############################################################################
    144.     void Awake()
    145.     {
    146.         RX2SIM_Game_Controller_ = new RX2SIM_Game_Controller();
    147.     }
    148.     // ############################################################################
    149.  
    150.  
    151.  
    152. }
    153.  
    154.  
    155.  
    156.  
    157.  
    158.  
    159.  
    160.  
    161.  
    162.  
    163.  
    164.  
    165. // ############################################################################
    166. // RX2SIM Game Controller - Workaround
    167. // ############################################################################
    168. // We receive data as raw HID input reports. This struct
    169. // describes the raw binary format of such a report.
    170. public struct RX2SIM_Game_Controller_Device_State : IInputStateTypeInfo
    171. {
    172.     // Because all HID input reports are tagged with the 'HID ' FourCC,
    173.     // this is the format we need to use for this state struct.
    174.     public FourCC format => new FourCC('H', 'I', 'D');
    175.  
    176.     // HID input reports can start with an 8-bit report ID. It depends on the device
    177.     // whether this is present or not. On the RX2SIM Game Controller, it is
    178.     // present and in the two frames it has the value 1 or 2.
    179.     [InputControl(name = "reportId_workaround_as_integer", format = "BIT", layout = "Integer", displayName = "Button reportId1 workaround", bit = 0, offset = 0, sizeInBits = 2)]
    180.     public byte reportId_workaround_as_integer;
    181.  
    182.     // https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/UnityEngine.InputSystem.Layouts.InputControlAttribute.html
    183.     [InputControl(name = "axis0", format = "BIT", layout = "Axis", displayName = "Axis 0", bit = 0, offset = 1, sizeInBits = 12)] // axis-X
    184.     public float axis0;
    185.  
    186.     [InputControl(name = "axis1", format = "BIT", layout = "Axis", displayName = "Axis 1", bit = 4, offset = 2, sizeInBits = 12)] // axis-Y
    187.     public float axis1;
    188.  
    189.     [InputControl(name = "axis2", format = "BIT", layout = "Axis", displayName = "Axis 2", bit = 0, offset = 4, sizeInBits = 12)] // axis-Z
    190.     public float axis2;
    191.  
    192.     [InputControl(name = "axis3", format = "BIT", layout = "Axis", displayName = "Axis 3", bit = 4, offset = 5, sizeInBits = 12)] // axis-Rx
    193.     public float axis3;
    194.  
    195.     //[InputControl(name = "axis4", format = "BIT", layout = "Axis", displayName = "Axis 4", bit = 0, offset = 8, sizeInBits = 12)] // axis-Ry
    196.     //public float axis4;
    197.  
    198.     //[InputControl(name = "axis5", format = "BIT", layout = "Axis", displayName = "Axis 5", bit = 4, offset = 9, sizeInBits = 12)] // axis-Rz
    199.     //public float axis5;
    200.  
    201.     //[InputControl(name = "axis6", format = "BIT", layout = "Axis", displayName = "Axis 6", bit = 0, offset = 11, sizeInBits = 12)] // Slider
    202.     //public float axis6;
    203.  
    204.     //[InputControl(name = "axis7", format = "BIT", layout = "Axis", displayName = "Axis 7", bit = 4, offset = 12, sizeInBits = 12)] // Dial
    205.     //public float axis7;
    206.  
    207.     //[InputControl(name = "button1", format = "BIT", layout = "Button", displayName = "Button 1", bit = 0, offset = 14, sizeInBits = 1)]
    208.     //[InputControl(name = "button2", format = "BIT", layout = "Button", displayName = "Button 2", bit = 1, offset = 14, sizeInBits = 1)]
    209.     //[InputControl(name = "button3", format = "BIT", layout = "Button", displayName = "Button 3", bit = 2, offset = 14, sizeInBits = 1)]
    210.     //[InputControl(name = "button4", format = "BIT", layout = "Button", displayName = "Button 4", bit = 3, offset = 14, sizeInBits = 1)]
    211.     //[InputControl(name = "button5", format = "BIT", layout = "Button", displayName = "Button 5", bit = 4, offset = 14, sizeInBits = 1)]
    212.     //[InputControl(name = "button6", format = "BIT", layout = "Button", displayName = "Button 6", bit = 5, offset = 14, sizeInBits = 1)]
    213.     //[InputControl(name = "button7", format = "BIT", layout = "Button", displayName = "Button 7", bit = 6, offset = 14, sizeInBits = 1)]
    214.     //[InputControl(name = "button8", format = "BIT", layout = "Button", displayName = "Button 8", bit = 7, offset = 14, sizeInBits = 1)]
    215.     //[FieldOffset(10)]
    216.     //public byte buttons1;
    217. }
    218.  
    219.  
    220. // ############################################################################
    221. // Using InputControlLayoutAttribute, we tell the system about the state
    222. // struct we created, which includes where to find all the InputControl
    223. // attributes that we placed on there.This is how the Input System knows
    224. // what controls to create and how to configure them.
    225. // ############################################################################
    226. [InputControlLayout(stateType = typeof(RX2SIM_Game_Controller_Device_State))]
    227. #if UNITY_EDITOR
    228. [InitializeOnLoad] // Make sure static constructor is called during startup.
    229. #endif
    230. public class RX2SIM_Game_Controller : Joystick // Gamepad // Joystick // InputDevice
    231. {
    232.     static RX2SIM_Game_Controller()
    233.     {
    234.         //// This is one way to match the Device.
    235.         InputSystem.RegisterLayout<RX2SIM_Game_Controller>(
    236.             matches: new InputDeviceMatcher()
    237.                 .WithInterface("HID")
    238.                 .WithManufacturer("RCWARE")
    239.                 .WithProduct("RX2SIM Game Controller"));
    240.  
    241.         //// Alternatively, you can also match by PID and VID, which is generally
    242.         //// more reliable for HIDs.
    243.         //InputSystem.RegisterLayout<RX2SIM_Game_Controller>(
    244.         //    matches: new InputDeviceMatcher()
    245.         //        .WithInterface("HID")
    246.         //        .WithCapability("vendorId", 1155) // RCWARE
    247.         //        .WithCapability("productId", 41195)); // RX2SIM Game Controller
    248.  
    249.     }
    250.  
    251.     //// In the Player, to trigger the calling of the static constructor,
    252.     //// create an empty method annotated with RuntimeInitializeOnLoadMethod.
    253.     [RuntimeInitializeOnLoadMethod]
    254.     static void Init() { }
    255. }
    256. // ############################################################################
    257.