Skip to content

Commit 6808810

Browse files
authored
CHANGE: Improve performance of HID descriptor parsing and device matching (#1579)
1 parent 8ff4ac3 commit 6808810

5 files changed

Lines changed: 324 additions & 1 deletion

File tree

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ however, it has to be formatted properly to pass verification tests.
1616
- Fixed an issue where Input Action name would not display correctly in Inspector if serialized as `[SerializedProperty]` within a class not derived from `MonoBehavior` ([case ISXB-124](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-124).
1717
- Fix an issue where users could end up with the wrong device assignments when using the InputUser API directly and removing a user ([case ISXB-274](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-231)).
1818

19+
### Changed
20+
- Improved performance of HID descriptor parsing by moving json parsing to a simple custom predicitve parser instead of relying on Unity's json parsing. This should improve domain reload times when there are many HID devices connected to a machine.
21+
1922
## [1.4.2] - 2022-08-12
2023

2124
### Changed

Packages/com.unity.inputsystem/InputSystem/Devices/InputDeviceMatcher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ private static bool MatchSingleProperty(object pattern, string value)
348348
{
349349
// String match.
350350
if (pattern is string str)
351-
return string.Compare(str, value, StringComparison.InvariantCultureIgnoreCase) == 0;
351+
return string.Compare(str, value, StringComparison.OrdinalIgnoreCase) == 0;
352352

353353
// Regex match.
354354
if (pattern is Regex regex)

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

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
using Unity.Collections.LowLevel.Unsafe;
99
using UnityEngine.InputSystem.Layouts;
1010
using UnityEngine.Scripting;
11+
#if UNITY_2021_2_OR_NEWER
12+
using UnityEngine.Pool;
13+
#endif
1114

1215
// HID support is currently broken in 32-bit Windows standalone players. Consider 32bit Windows players unsupported for now.
1316
#if UNITY_STANDALONE_WIN && !UNITY_64
@@ -974,7 +977,165 @@ public string ToJson()
974977

975978
public static HIDDeviceDescriptor FromJson(string json)
976979
{
980+
#if UNITY_2021_2_OR_NEWER
981+
try
982+
{
983+
// HID descriptors, when formatted correctly, are always json strings with no whitespace and a
984+
// predictable order of elements, so we can try and use this simple predictive parser to extract
985+
// the data. If for any reason the data is not formatted correctly, we'll automatically fall back
986+
// to Unity's default json parser.
987+
var descriptor = new HIDDeviceDescriptor();
988+
989+
var jsonSpan = json.AsSpan();
990+
var parser = new PredictiveParser();
991+
parser.ExpectSingleChar(jsonSpan, '{');
992+
993+
parser.AcceptString(jsonSpan, out _);
994+
parser.ExpectSingleChar(jsonSpan, ':');
995+
descriptor.vendorId = parser.ExpectInt(jsonSpan);
996+
parser.AcceptSingleChar(jsonSpan, ',');
997+
998+
parser.AcceptString(jsonSpan, out _);
999+
parser.ExpectSingleChar(jsonSpan, ':');
1000+
descriptor.productId = parser.ExpectInt(jsonSpan);
1001+
parser.AcceptSingleChar(jsonSpan, ',');
1002+
1003+
parser.AcceptString(jsonSpan, out _);
1004+
parser.ExpectSingleChar(jsonSpan, ':');
1005+
descriptor.usage = parser.ExpectInt(jsonSpan);
1006+
parser.AcceptSingleChar(jsonSpan, ',');
1007+
1008+
parser.AcceptString(jsonSpan, out _);
1009+
parser.ExpectSingleChar(jsonSpan, ':');
1010+
descriptor.usagePage = (UsagePage)parser.ExpectInt(jsonSpan);
1011+
parser.AcceptSingleChar(jsonSpan, ',');
1012+
1013+
parser.AcceptString(jsonSpan, out _);
1014+
parser.ExpectSingleChar(jsonSpan, ':');
1015+
descriptor.inputReportSize = parser.ExpectInt(jsonSpan);
1016+
parser.AcceptSingleChar(jsonSpan, ',');
1017+
1018+
parser.AcceptString(jsonSpan, out _);
1019+
parser.ExpectSingleChar(jsonSpan, ':');
1020+
descriptor.outputReportSize = parser.ExpectInt(jsonSpan);
1021+
parser.AcceptSingleChar(jsonSpan, ',');
1022+
1023+
parser.AcceptString(jsonSpan, out _);
1024+
parser.ExpectSingleChar(jsonSpan, ':');
1025+
descriptor.featureReportSize = parser.ExpectInt(jsonSpan);
1026+
parser.AcceptSingleChar(jsonSpan, ',');
1027+
1028+
// elements
1029+
parser.AcceptString(jsonSpan, out var key);
1030+
if (key.ToString() != "elements") return descriptor;
1031+
1032+
parser.ExpectSingleChar(jsonSpan, ':');
1033+
parser.ExpectSingleChar(jsonSpan, '[');
1034+
1035+
using var pool = ListPool<HIDElementDescriptor>.Get(out var elements);
1036+
while (!parser.AcceptSingleChar(jsonSpan, ']'))
1037+
{
1038+
parser.AcceptSingleChar(jsonSpan, ',');
1039+
parser.ExpectSingleChar(jsonSpan, '{');
1040+
1041+
HIDElementDescriptor elementDesc = default;
1042+
1043+
1044+
parser.AcceptSingleChar(jsonSpan, '}');
1045+
parser.AcceptSingleChar(jsonSpan, ',');
1046+
1047+
// usage
1048+
parser.ExpectString(jsonSpan);
1049+
parser.ExpectSingleChar(jsonSpan, ':');
1050+
elementDesc.usage = parser.ExpectInt(jsonSpan);
1051+
parser.AcceptSingleChar(jsonSpan, ',');
1052+
1053+
parser.ExpectString(jsonSpan);
1054+
parser.ExpectSingleChar(jsonSpan, ':');
1055+
elementDesc.usagePage = (UsagePage)parser.ExpectInt(jsonSpan);
1056+
parser.AcceptSingleChar(jsonSpan, ',');
1057+
1058+
parser.ExpectString(jsonSpan);
1059+
parser.ExpectSingleChar(jsonSpan, ':');
1060+
elementDesc.unit = parser.ExpectInt(jsonSpan);
1061+
parser.AcceptSingleChar(jsonSpan, ',');
1062+
1063+
parser.ExpectString(jsonSpan);
1064+
parser.ExpectSingleChar(jsonSpan, ':');
1065+
elementDesc.unitExponent = parser.ExpectInt(jsonSpan);
1066+
parser.AcceptSingleChar(jsonSpan, ',');
1067+
1068+
parser.ExpectString(jsonSpan);
1069+
parser.ExpectSingleChar(jsonSpan, ':');
1070+
elementDesc.logicalMin = parser.ExpectInt(jsonSpan);
1071+
parser.AcceptSingleChar(jsonSpan, ',');
1072+
1073+
parser.ExpectString(jsonSpan);
1074+
parser.ExpectSingleChar(jsonSpan, ':');
1075+
elementDesc.logicalMax = parser.ExpectInt(jsonSpan);
1076+
parser.AcceptSingleChar(jsonSpan, ',');
1077+
1078+
parser.ExpectString(jsonSpan);
1079+
parser.ExpectSingleChar(jsonSpan, ':');
1080+
elementDesc.physicalMin = parser.ExpectInt(jsonSpan);
1081+
parser.AcceptSingleChar(jsonSpan, ',');
1082+
1083+
parser.ExpectString(jsonSpan);
1084+
parser.ExpectSingleChar(jsonSpan, ':');
1085+
elementDesc.physicalMax = parser.ExpectInt(jsonSpan);
1086+
parser.AcceptSingleChar(jsonSpan, ',');
1087+
1088+
parser.ExpectString(jsonSpan);
1089+
parser.ExpectSingleChar(jsonSpan, ':');
1090+
elementDesc.collectionIndex = parser.ExpectInt(jsonSpan);
1091+
parser.AcceptSingleChar(jsonSpan, ',');
1092+
1093+
parser.ExpectString(jsonSpan);
1094+
parser.ExpectSingleChar(jsonSpan, ':');
1095+
elementDesc.reportType = (HIDReportType)parser.ExpectInt(jsonSpan);
1096+
parser.AcceptSingleChar(jsonSpan, ',');
1097+
1098+
parser.ExpectString(jsonSpan);
1099+
parser.ExpectSingleChar(jsonSpan, ':');
1100+
elementDesc.reportId = parser.ExpectInt(jsonSpan);
1101+
parser.AcceptSingleChar(jsonSpan, ',');
1102+
1103+
// reportCount. We don't store this one
1104+
parser.ExpectString(jsonSpan);
1105+
parser.ExpectSingleChar(jsonSpan, ':');
1106+
parser.AcceptInt(jsonSpan);
1107+
parser.AcceptSingleChar(jsonSpan, ',');
1108+
1109+
parser.ExpectString(jsonSpan);
1110+
parser.ExpectSingleChar(jsonSpan, ':');
1111+
elementDesc.reportSizeInBits = parser.ExpectInt(jsonSpan);
1112+
parser.AcceptSingleChar(jsonSpan, ',');
1113+
1114+
parser.ExpectString(jsonSpan);
1115+
parser.ExpectSingleChar(jsonSpan, ':');
1116+
elementDesc.reportOffsetInBits = parser.ExpectInt(jsonSpan);
1117+
parser.AcceptSingleChar(jsonSpan, ',');
1118+
1119+
parser.ExpectString(jsonSpan);
1120+
parser.ExpectSingleChar(jsonSpan, ':');
1121+
elementDesc.flags = (HIDElementFlags)parser.ExpectInt(jsonSpan);
1122+
1123+
parser.ExpectSingleChar(jsonSpan, '}');
1124+
1125+
elements.Add(elementDesc);
1126+
}
1127+
descriptor.elements = elements.ToArray();
1128+
1129+
return descriptor;
1130+
}
1131+
catch (Exception)
1132+
{
1133+
Debug.LogWarning($"Couldn't parse HID descriptor with fast parser. Using fallback");
1134+
return JsonUtility.FromJson<HIDDeviceDescriptor>(json);
1135+
}
1136+
#else
9771137
return JsonUtility.FromJson<HIDDeviceDescriptor>(json);
1138+
#endif
9781139
}
9791140
}
9801141

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using System;
2+
3+
namespace UnityEngine.InputSystem.Utilities
4+
{
5+
// this parser uses Span<T> so it's only available from later unity versions
6+
#if UNITY_2021_2_OR_NEWER
7+
internal struct PredictiveParser
8+
{
9+
public void ExpectSingleChar(ReadOnlySpan<char> str, char c)
10+
{
11+
if (str[m_Position] != c)
12+
throw new InvalidOperationException($"Expected a '{c}' character at position {m_Position} in : {str.ToString()}");
13+
14+
++m_Position;
15+
}
16+
17+
public int ExpectInt(ReadOnlySpan<char> str)
18+
{
19+
int pos = m_Position;
20+
21+
int sign = 1;
22+
if (str[pos] == '-')
23+
{
24+
sign = -1;
25+
++pos;
26+
}
27+
28+
int value = 0;
29+
for (;;)
30+
{
31+
var n = str[pos];
32+
if (n >= '0' && n <= '9')
33+
{
34+
value *= 10;
35+
value += n - '0';
36+
++pos;
37+
}
38+
else
39+
break;
40+
}
41+
42+
if (m_Position == pos)
43+
throw new InvalidOperationException($"Expected an int at position {m_Position} in {str.ToString()}");
44+
45+
m_Position = pos;
46+
return value * sign;
47+
}
48+
49+
public ReadOnlySpan<char> ExpectString(ReadOnlySpan<char> str)
50+
{
51+
var startPos = m_Position;
52+
if (str[startPos] != '\"')
53+
throw new InvalidOperationException($"Expected a '\"' character at position {m_Position} in {str.ToString()}");
54+
55+
++m_Position;
56+
57+
for (;;)
58+
{
59+
var c = str[m_Position];
60+
c |= ' ';
61+
if (c >= 'a' && c <= 'z')
62+
{
63+
++m_Position;
64+
continue;
65+
}
66+
67+
break;
68+
}
69+
70+
// if the first non-alpha character is not a quote, throw
71+
if (str[m_Position] != '\"')
72+
throw new InvalidOperationException($"Expected a closing '\"' character at position {m_Position} in string: {str.ToString()}");
73+
74+
if (m_Position - startPos == 1)
75+
return ReadOnlySpan<char>.Empty;
76+
77+
var output = str.Slice(startPos + 1, m_Position - startPos - 1);
78+
79+
++m_Position;
80+
return output;
81+
}
82+
83+
public bool AcceptSingleChar(ReadOnlySpan<char> str, char c)
84+
{
85+
if (str[m_Position] != c)
86+
return false;
87+
88+
m_Position++;
89+
return true;
90+
}
91+
92+
public bool AcceptString(ReadOnlySpan<char> input, out ReadOnlySpan<char> output)
93+
{
94+
output = default;
95+
var startPos = m_Position;
96+
var endPos = startPos;
97+
if (input[endPos] != '\"')
98+
return false;
99+
100+
++endPos;
101+
102+
for (;;)
103+
{
104+
var c = input[endPos];
105+
c |= ' ';
106+
if (c >= 'a' && c <= 'z')
107+
{
108+
++endPos;
109+
continue;
110+
}
111+
112+
break;
113+
}
114+
115+
// if the first non-alpha character is not a quote, throw
116+
if (input[endPos] != '\"')
117+
return false;
118+
119+
// empty string?
120+
if (m_Position - startPos == 1)
121+
output = ReadOnlySpan<char>.Empty;
122+
else
123+
output = input.Slice(startPos + 1, endPos - startPos - 1);
124+
125+
m_Position = endPos + 1;
126+
return true;
127+
}
128+
129+
public void AcceptInt(ReadOnlySpan<char> str)
130+
{
131+
// skip negative sign
132+
if (str[m_Position] == '-')
133+
++m_Position;
134+
135+
for (;;)
136+
{
137+
var n = str[m_Position];
138+
if (n >= '0' && n <= '9')
139+
++m_Position;
140+
else
141+
break;
142+
}
143+
}
144+
145+
private int m_Position;
146+
}
147+
#endif
148+
}

Packages/com.unity.inputsystem/InputSystem/Utilities/PredictiveParser.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)