A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Text;
4using UnityEngine.InputSystem.Controls;
5using UnityEngine.InputSystem.LowLevel;
6using UnityEngine.InputSystem.Utilities;
7
8////TODO: add ability to add to existing arrays rather than creating per-device arrays
9
10////TODO: the next step here is to write a code generator that generates code for a given layout that when
11//// executed, does what InputDeviceBuilder does but without the use of reflection and much more quickly
12
13////REVIEW: it probably makes sense to have an initial phase where we process the initial set of
14//// device discoveries from native and keep the layout cache around instead of throwing
15//// it away after the creation of every single device; best approach may be to just
16//// reuse the same InputDeviceBuilder instance over and over
17
18////TODO: ensure that things are aligned properly for ARM; should that be done on the reading side or in the state layouts?
19//// (make sure that alignment works the same on *all* platforms; otherwise editor will not be able to process events from players properly)
20
21namespace UnityEngine.InputSystem.Layouts
22{
23 /// <summary>
24 /// Turns a device layout into an actual <see cref="InputDevice"/> instance.
25 /// </summary>
26 /// <remarks>
27 /// Ultimately produces a device but can also be used to query the control setup described
28 /// by a layout.
29 ///
30 /// Can be used both to create control hierarchies from scratch as well as to re-create or
31 /// change existing hierarchies.
32 ///
33 /// InputDeviceBuilder is the only way to create control hierarchies. InputControls cannot be
34 /// <c>new</c>'d directly.
35 ///
36 /// Also computes a final state layout when setup is finished.
37 ///
38 /// Note that InputDeviceBuilders generate garbage. They are meant to be used for initialization only. Don't
39 /// use them during normal gameplay.
40 ///
41 /// Running an *existing* device through another control build is a *destructive* operation.
42 /// Existing controls may be reused while at the same time the hierarchy and even the device instance
43 /// itself may change.
44 /// </remarks>
45 internal struct InputDeviceBuilder : IDisposable
46 {
47 public void Setup(InternedString layout, InternedString variants,
48 InputDeviceDescription deviceDescription = default)
49 {
50 m_LayoutCacheRef = InputControlLayout.CacheRef();
51
52 InstantiateLayout(layout, variants, new InternedString(), null);
53 FinalizeControlHierarchy();
54
55 m_StateOffsetToControlMap.Sort();
56
57 m_Device.m_Description = deviceDescription;
58 m_Device.m_StateOffsetToControlMap = m_StateOffsetToControlMap.ToArray();
59
60 m_Device.CallFinishSetupRecursive();
61 }
62
63 // Complete the setup and return the full control hierarchy setup
64 // with its device root.
65 public InputDevice Finish()
66 {
67 var device = m_Device;
68
69 // Set up the list of just ButtonControls to quickly update press state.
70 var i = 0;
71 foreach (var control in device.allControls)
72 {
73 if (control.isButton)
74 ++i;
75 }
76
77 device.m_ButtonControlsCheckingPressState = new List<ButtonControl>(i);
78 #if UNITY_2020_1_OR_NEWER
79 device.m_UpdatedButtons = new HashSet<int>(i);
80 #else
81 // 2019 is too old to support setting HashSet capacity
82 device.m_UpdatedButtons = new HashSet<int>();
83 #endif
84
85 // Kill off our state.
86 Reset();
87
88 return device;
89 }
90
91 public void Dispose()
92 {
93 m_LayoutCacheRef.Dispose();
94 }
95
96 private InputDevice m_Device;
97
98 // Make sure the global layout cache sticks around for at least as long
99 // as the device builder so that we don't load layouts over and over.
100 private InputControlLayout.CacheRefInstance m_LayoutCacheRef;
101
102 // Table mapping (lower-cased) control paths to control layouts that contain
103 // overrides for the control at the given path.
104 private Dictionary<string, InputControlLayout.ControlItem> m_ChildControlOverrides;
105
106 private List<uint> m_StateOffsetToControlMap;
107
108 private StringBuilder m_StringBuilder;
109
110 // Reset the setup in a way where it can be reused for another setup.
111 // Should retain allocations that can be reused.
112 private void Reset()
113 {
114 m_Device = null;
115 m_ChildControlOverrides?.Clear();
116 m_StateOffsetToControlMap?.Clear();
117 // Leave the cache in place so we can reuse them in another setup path.
118 }
119
120 private InputControl InstantiateLayout(InternedString layout, InternedString variants, InternedString name, InputControl parent)
121 {
122 // Look up layout by name.
123 var layoutInstance = FindOrLoadLayout(layout);
124
125 // Create control hierarchy.
126 return InstantiateLayout(layoutInstance, variants, name, parent);
127 }
128
129 private InputControl InstantiateLayout(InputControlLayout layout, InternedString variants, InternedString name,
130 InputControl parent)
131 {
132 Debug.Assert(layout.type != null, "Layout has no type set on it");
133
134 // No, so create a new control.
135 var controlObject = Activator.CreateInstance(layout.type);
136 if (!(controlObject is InputControl control))
137 {
138 throw new InvalidOperationException(
139 $"Type '{layout.type.Name}' referenced by layout '{layout.name}' is not an InputControl");
140 }
141
142 // If it's a device, perform some extra work specific to the control
143 // hierarchy root.
144 if (control is InputDevice controlAsDevice)
145 {
146 if (parent != null)
147 throw new InvalidOperationException(
148 $"Cannot instantiate device layout '{layout.name}' as child of '{parent.path}'; devices must be added at root");
149
150 m_Device = controlAsDevice;
151 m_Device.m_StateBlock.byteOffset = 0;
152 m_Device.m_StateBlock.bitOffset = 0;
153 m_Device.m_StateBlock.format = layout.stateFormat;
154
155 // If we have an existing device, we'll start the various control arrays
156 // from scratch. Note that all the controls still refer to the existing
157 // arrays and so we can iterate children, for example, just fine while
158 // we are rebuilding the control hierarchy.
159 m_Device.m_AliasesForEachControl = null;
160 m_Device.m_ChildrenForEachControl = null;
161 m_Device.m_UpdatedButtons = null;
162 m_Device.m_UsagesForEachControl = null;
163 m_Device.m_UsageToControl = null;
164
165 if (layout.m_UpdateBeforeRender == true)
166 m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.UpdateBeforeRender;
167 if (layout.canRunInBackground != null)
168 {
169 m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.CanRunInBackgroundHasBeenQueried;
170 if (layout.canRunInBackground == true)
171 m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.CanRunInBackground;
172 }
173 }
174 else if (parent == null)
175 {
176 // Someone did "new InputDeviceBuilder(...)" with a control layout.
177 // We don't support creating control hierarchies without a device at the root.
178 throw new InvalidOperationException(
179 $"Toplevel layout used with InputDeviceBuilder must be a device layout; '{layout.name}' is a control layout");
180 }
181
182 // Name defaults to name of layout.
183 if (name.IsEmpty())
184 {
185 name = layout.name;
186
187 // If there's a namespace in the layout name, snip it out.
188 var indexOfLastColon = name.ToString().LastIndexOf(':');
189 if (indexOfLastColon != -1)
190 name = new InternedString(name.ToString().Substring(indexOfLastColon + 1));
191 }
192
193 // Make sure name does not contain any slashes.
194 if (name.ToString().IndexOf(InputControlPath.Separator) != -1)
195 name = new InternedString(name.ToString().CleanSlashes());
196
197 // Variant defaults to variants of layout.
198 if (variants.IsEmpty())
199 {
200 variants = layout.variants;
201
202 if (variants.IsEmpty())
203 variants = InputControlLayout.DefaultVariant;
204 }
205
206 control.m_Name = name;
207 control.m_DisplayNameFromLayout = layout.m_DisplayName; // No short display names at layout roots.
208 control.m_Layout = layout.name;
209 control.m_Variants = variants;
210 control.m_Parent = parent;
211 control.m_Device = m_Device;
212
213 // this has to be done down here instead of in the device block above because the state for the
214 // device needs to be set up before setting noisy or it will throw because the device's m_Device
215 // hasn't been set yet. Yes, a device's m_Device is itself.
216 if (control is InputDevice)
217 control.noisy = layout.isNoisy;
218
219 // Create children and configure their settings from our
220 // layout values.
221 var haveChildrenUsingStateFromOtherControl = false;
222 try
223 {
224 // Pass list of existing control on to function as we may have decided to not
225 // actually reuse the existing control (and thus control.m_ChildrenReadOnly will
226 // now be blank) but still want crawling down the hierarchy to preserve existing
227 // controls where possible.
228 AddChildControls(layout, variants, control,
229 ref haveChildrenUsingStateFromOtherControl);
230 }
231 catch
232 {
233 ////TODO: remove control from collection and rethrow
234 throw;
235 }
236
237 // Come up with a layout for our state.
238 ComputeStateLayout(control);
239
240 // Finally, if we have child controls that take their state blocks from other
241 // controls, assign them their blocks now.
242 if (haveChildrenUsingStateFromOtherControl)
243 {
244 var controls = layout.m_Controls;
245 for (var i = 0; i < controls.Length; ++i)
246 {
247 ref var item = ref controls[i];
248 if (string.IsNullOrEmpty(item.useStateFrom))
249 continue;
250 ApplyUseStateFrom(control, ref item, layout);
251 }
252 }
253
254 return control;
255 }
256
257 private const uint kSizeForControlUsingStateFromOtherControl = InputStateBlock.InvalidOffset;
258
259 private void AddChildControls(InputControlLayout layout, InternedString variants, InputControl parent,
260 ref bool haveChildrenUsingStateFromOtherControls)
261 {
262 var controlLayouts = layout.m_Controls;
263 if (controlLayouts == null)
264 return;
265
266 // Find out how many direct children we will add.
267 var childCount = 0;
268 var haveControlLayoutWithPath = false;
269 for (var i = 0; i < controlLayouts.Length; ++i)
270 {
271 // Skip if variants don't match.
272 if (!controlLayouts[i].variants.IsEmpty() &&
273 !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayouts[i].variants,
274 variants, InputControlLayout.VariantSeparator[0]))
275 continue;
276
277 ////REVIEW: I'm not sure this is good enough. ATM if you have a control layout with
278 //// name "foo" and one with name "foo/bar", then the latter is taken as an override
279 //// but the former isn't. However, whether it has a slash in the path or not shouldn't
280 //// matter. If a control layout of the same name already exists, it should be
281 //// considered an override, if not, it shouldn't.
282 // Not a new child if it's a layout reaching in to the hierarchy to modify
283 // an existing child.
284 if (controlLayouts[i].isModifyingExistingControl)
285 {
286 if (controlLayouts[i].isArray)
287 throw new NotSupportedException(
288 $"Control '{controlLayouts[i].name}' in layout '{layout.name}' is modifying the child of another control but is marked as an array");
289
290 haveControlLayoutWithPath = true;
291 InsertChildControlOverride(parent, ref controlLayouts[i]);
292 continue;
293 }
294
295 if (controlLayouts[i].isArray)
296 childCount += controlLayouts[i].arraySize;
297 else
298 ++childCount;
299 }
300
301 // Nothing to do if there's no children.
302 if (childCount == 0)
303 {
304 parent.m_ChildCount = default;
305 parent.m_ChildStartIndex = default;
306 haveChildrenUsingStateFromOtherControls = false;
307 return;
308 }
309
310 // Add room for us in the device's child array.
311 var firstChildIndex = ArrayHelpers.GrowBy(ref m_Device.m_ChildrenForEachControl, childCount);
312
313 // Add controls from all control layouts except the ones that have
314 // paths in them.
315 var childIndex = firstChildIndex;
316 for (var i = 0; i < controlLayouts.Length; ++i)
317 {
318 var controlLayout = controlLayouts[i];
319
320 // Skip control layouts that don't add controls but rather modify child
321 // controls of other controls added by the layout. We do a second pass
322 // to apply their settings.
323 if (controlLayout.isModifyingExistingControl)
324 continue;
325
326 // If the control is part of a variant, skip it if it isn't in the variants we're
327 // looking for.
328 if (!controlLayout.variants.IsEmpty() &&
329 !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayout.variants,
330 variants, InputControlLayout.VariantSeparator[0]))
331 continue;
332
333 // If it's an array, add a control for each array element.
334 if (controlLayout.isArray)
335 {
336 for (var n = 0; n < controlLayout.arraySize; ++n)
337 {
338 var name = controlLayout.name + n;
339 var control = AddChildControl(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls,
340 controlLayout, childIndex, nameOverride: name);
341 ++childIndex;
342
343 // Adjust offset, if the control uses explicit offsets.
344 if (control.m_StateBlock.byteOffset != InputStateBlock.InvalidOffset)
345 control.m_StateBlock.byteOffset += (uint)n * control.m_StateBlock.alignedSizeInBytes;
346 }
347 }
348 else
349 {
350 AddChildControl(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls,
351 controlLayout, childIndex);
352 ++childIndex;
353 }
354 }
355
356 parent.m_ChildCount = childCount;
357 parent.m_ChildStartIndex = firstChildIndex;
358
359 ////REVIEW: there's probably a better way to do this based on m_ChildControlOverrides
360 // We apply all overrides through m_ChildControlOverrides. However, there may be a control item
361 // that *adds* a child control to another existing control. This will look the same as overriding
362 // properties on a child control just that in this case the child control doesn't exist.
363 //
364 // Go through all the controls and check for ones that need to be added.
365 if (haveControlLayoutWithPath)
366 {
367 for (var i = 0; i < controlLayouts.Length; ++i)
368 {
369 var controlLayout = controlLayouts[i];
370 if (!controlLayout.isModifyingExistingControl)
371 continue;
372
373 // If the control is part of a variants, skip it if it isn't the variants we're
374 // looking for.
375 if (!controlLayout.variants.IsEmpty() &&
376 !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayouts[i].variants,
377 variants, InputControlLayout.VariantSeparator[0]))
378 continue;
379
380 AddChildControlIfMissing(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls,
381 ref controlLayout);
382 }
383 }
384 }
385
386 private InputControl AddChildControl(InputControlLayout layout, InternedString variants, InputControl parent,
387 ref bool haveChildrenUsingStateFromOtherControls,
388 InputControlLayout.ControlItem controlItem,
389 int childIndex, string nameOverride = null)
390 {
391 var name = nameOverride != null ? new InternedString(nameOverride) : controlItem.name;
392
393 ////REVIEW: can we check this in InputControlLayout instead?
394 if (string.IsNullOrEmpty(controlItem.layout))
395 throw new InvalidOperationException($"Layout has not been set on control '{controlItem.name}' in '{layout.name}'");
396
397 // See if there is an override for the control.
398 if (m_ChildControlOverrides != null)
399 {
400 var pathLowerCase = ChildControlOverridePath(parent, name);
401 if (m_ChildControlOverrides.TryGetValue(pathLowerCase, out var controlOverride))
402 controlItem = controlOverride.Merge(controlItem);
403 }
404
405 // Get name of layout to use for control.
406 var layoutName = controlItem.layout;
407
408 // Create control.
409 InputControl control;
410 try
411 {
412 control = InstantiateLayout(layoutName, variants, name, parent);
413 }
414 catch (InputControlLayout.LayoutNotFoundException exception)
415 {
416 // Throw better exception that gives more info.
417 throw new InputControlLayout.LayoutNotFoundException(
418 $"Cannot find layout '{exception.layout}' used in control '{name}' of layout '{layout.name}'",
419 exception);
420 }
421
422 // Add to array.
423 // NOTE: AddChildControls and InstantiateLayout take care of growing the array and making
424 // room for the immediate children of each control.
425 m_Device.m_ChildrenForEachControl[childIndex] = control;
426
427 // Set flags and misc things.
428 control.noisy = controlItem.isNoisy;
429 control.synthetic = controlItem.isSynthetic;
430 control.usesStateFromOtherControl = !string.IsNullOrEmpty(controlItem.useStateFrom);
431 control.dontReset = (control.noisy || controlItem.dontReset) && !control.usesStateFromOtherControl; // Imply dontReset for noisy controls.
432 if (control.noisy)
433 m_Device.noisy = true;
434 control.isButton = control is ButtonControl;
435 if (control.dontReset)
436 m_Device.hasDontResetControls = true;
437
438 // Remember the display names from the layout. We later do a proper pass once we have
439 // the full hierarchy to set final names.
440 control.m_DisplayNameFromLayout = controlItem.displayName;
441 control.m_ShortDisplayNameFromLayout = controlItem.shortDisplayName;
442
443 // Set default value.
444 control.m_DefaultState = controlItem.defaultState;
445 if (!control.m_DefaultState.isEmpty)
446 m_Device.hasControlsWithDefaultState = true;
447
448 // Set min and max value. Don't just overwrite here as the control's constructor may
449 // have set a default value.
450 if (!controlItem.minValue.isEmpty)
451 control.m_MinValue = controlItem.minValue;
452 if (!controlItem.maxValue.isEmpty)
453 control.m_MaxValue = controlItem.maxValue;
454
455 // Pass state block config on to control.
456 if (!control.usesStateFromOtherControl)
457 {
458 control.m_StateBlock.byteOffset = controlItem.offset;
459 control.m_StateBlock.bitOffset = controlItem.bit;
460 if (controlItem.sizeInBits != 0)
461 control.m_StateBlock.sizeInBits = controlItem.sizeInBits;
462 if (controlItem.format != 0)
463 SetFormat(control, controlItem);
464 }
465 else
466 {
467 // Mark controls that don't have state blocks of their own but rather get their
468 // blocks from other controls by setting their state size to InvalidOffset.
469 control.m_StateBlock.sizeInBits = kSizeForControlUsingStateFromOtherControl;
470 haveChildrenUsingStateFromOtherControls = true;
471 }
472
473 ////REVIEW: the constant appending to m_UsagesForEachControl and m_AliasesForEachControl may lead to a lot
474 //// of successive re-allocations
475
476 // Add usages.
477 var usages = controlItem.usages;
478 if (usages.Count > 0)
479 {
480 var usageCount = usages.Count;
481 var usageIndex =
482 ArrayHelpers.AppendToImmutable(ref m_Device.m_UsagesForEachControl, usages.m_Array);
483 control.m_UsageStartIndex = usageIndex;
484 control.m_UsageCount = usageCount;
485
486 ArrayHelpers.GrowBy(ref m_Device.m_UsageToControl, usageCount);
487 for (var n = 0; n < usageCount; ++n)
488 m_Device.m_UsageToControl[usageIndex + n] = control;
489 }
490
491 // Add aliases.
492 if (controlItem.aliases.Count > 0)
493 {
494 var aliasCount = controlItem.aliases.Count;
495 var aliasIndex =
496 ArrayHelpers.AppendToImmutable(ref m_Device.m_AliasesForEachControl, controlItem.aliases.m_Array);
497 control.m_AliasStartIndex = aliasIndex;
498 control.m_AliasCount = aliasCount;
499 }
500
501 // Set parameters.
502 if (controlItem.parameters.Count > 0)
503 NamedValue.ApplyAllToObject(control, controlItem.parameters);
504
505 // Add processors.
506 if (controlItem.processors.Count > 0)
507 AddProcessors(control, ref controlItem, layout.name);
508
509 return control;
510 }
511
512 private void InsertChildControlOverride(InputControl parent, ref InputControlLayout.ControlItem controlItem)
513 {
514 if (m_ChildControlOverrides == null)
515 m_ChildControlOverrides = new Dictionary<string, InputControlLayout.ControlItem>();
516
517 // See if there are existing overrides for the control.
518 var pathLowerCase = ChildControlOverridePath(parent, controlItem.name);
519 if (!m_ChildControlOverrides.TryGetValue(pathLowerCase, out var existingOverrides))
520 {
521 // So, so just insert our overrides and we're done.
522 m_ChildControlOverrides[pathLowerCase] = controlItem;
523 return;
524 }
525
526 // Yes, there's existing overrides so we have to merge.
527 // NOTE: The existing override's properties take precedence here. This is because
528 // the override has been established from higher up in the layout hierarchy.
529 existingOverrides = existingOverrides.Merge(controlItem);
530 m_ChildControlOverrides[pathLowerCase] = existingOverrides;
531 }
532
533 private string ChildControlOverridePath(InputControl parent, InternedString controlName)
534 {
535 var pathLowerCase = controlName.ToLower();
536 for (var current = parent; current != m_Device; current = current.m_Parent)
537 pathLowerCase = $"{current.m_Name.ToLower()}/{pathLowerCase}";
538 return pathLowerCase;
539 }
540
541 private void AddChildControlIfMissing(InputControlLayout layout, InternedString variants, InputControl parent,
542 ref bool haveChildrenUsingStateFromOtherControls,
543 ref InputControlLayout.ControlItem controlItem)
544 {
545 ////TODO: support arrays (we may modify an entire array in bulk)
546
547 // Find the child control.
548 var child = InputControlPath.TryFindChild(parent, controlItem.name);
549 if (child != null)
550 return;
551
552 // We're adding a child somewhere in the existing hierarchy. This is a tricky
553 // case as we have to potentially shift indices around in the hierarchy to make
554 // room for the new control.
555
556 ////TODO: this path does not support recovering existing controls? does it matter?
557
558 child = InsertChildControl(layout, variants, parent,
559 ref haveChildrenUsingStateFromOtherControls, ref controlItem);
560
561 // Apply layout change.
562 if (!ReferenceEquals(child.parent, parent))
563 ComputeStateLayout(child.parent);
564 }
565
566 private InputControl InsertChildControl(InputControlLayout layout, InternedString variant, InputControl parent,
567 ref bool haveChildrenUsingStateFromOtherControls,
568 ref InputControlLayout.ControlItem controlItem)
569 {
570 var path = controlItem.name.ToString();
571
572 // First we need to find the immediate parent from the given path.
573 var indexOfSlash = path.LastIndexOf('/');
574 if (indexOfSlash == -1)
575 throw new InvalidOperationException("InsertChildControl has to be called with a slash-separated path");
576 Debug.Assert(indexOfSlash != 0, "Could not find slash in path");
577 var immediateParentPath = path.Substring(0, indexOfSlash);
578 var immediateParent = InputControlPath.TryFindChild(parent, immediateParentPath);
579 if (immediateParent == null)
580 throw new InvalidOperationException(
581 $"Cannot find parent '{immediateParentPath}' of control '{controlItem.name}' in layout '{layout.name}'");
582
583 var controlName = path.Substring(indexOfSlash + 1);
584 if (controlName.Length == 0)
585 throw new InvalidOperationException(
586 $"Path cannot end in '/' (control '{controlItem.name}' in layout '{layout.name}')");
587
588 // Make room in the device's child array.
589 var childStartIndex = immediateParent.m_ChildStartIndex;
590 if (childStartIndex == default)
591 {
592 // First child of parent.
593 childStartIndex = m_Device.m_ChildrenForEachControl.LengthSafe();
594 immediateParent.m_ChildStartIndex = childStartIndex;
595 }
596 var childIndex = childStartIndex + immediateParent.m_ChildCount;
597 ShiftChildIndicesInHierarchyOneUp(m_Device, childIndex, immediateParent);
598 ArrayHelpers.InsertAt(ref m_Device.m_ChildrenForEachControl, childIndex, null);
599 ++immediateParent.m_ChildCount;
600
601 // Insert the child.
602 // NOTE: This may *add several* controls depending on the layout of the control we are inserting.
603 // The children will be appended to the child array.
604 var control = AddChildControl(layout, variant, immediateParent,
605 ref haveChildrenUsingStateFromOtherControls, controlItem, childIndex, controlName);
606
607 return control;
608 }
609
610 private static void ApplyUseStateFrom(InputControl parent, ref InputControlLayout.ControlItem controlItem, InputControlLayout layout)
611 {
612 var child = InputControlPath.TryFindChild(parent, controlItem.name);
613 Debug.Assert(child != null, "Could not find child control which should be present at this point");
614
615 // Find the referenced control.
616 var referencedControl = InputControlPath.TryFindChild(parent, controlItem.useStateFrom);
617 if (referencedControl == null)
618 throw new InvalidOperationException(
619 $"Cannot find control '{controlItem.useStateFrom}' referenced in 'useStateFrom' of control '{controlItem.name}' in layout '{layout.name}'");
620
621 // Copy its state settings.
622 child.m_StateBlock = referencedControl.m_StateBlock;
623 child.usesStateFromOtherControl = true;
624 child.dontReset = referencedControl.dontReset;
625
626 // At this point, all byteOffsets are relative to parents so we need to
627 // walk up the referenced control's parent chain and add offsets until
628 // we are at the same level that we are at.
629 if (child.parent != referencedControl.parent)
630 for (var parentInChain = referencedControl.parent; parentInChain != parent; parentInChain = parentInChain.parent)
631 child.m_StateBlock.byteOffset += parentInChain.m_StateBlock.byteOffset;
632 }
633
634 private static void ShiftChildIndicesInHierarchyOneUp(InputDevice device, int startIndex, InputControl exceptControl)
635 {
636 var controls = device.m_ChildrenForEachControl;
637 var count = controls.Length;
638 for (var i = 0; i < count; ++i)
639 {
640 var control = controls[i];
641 if (control != null && control != exceptControl && control.m_ChildStartIndex >= startIndex)
642 ++control.m_ChildStartIndex;
643 }
644 }
645
646 // NOTE: We can only do this once we've initialized the names on the parent control. I.e. it has to be
647 // done in the second pass we do over the control hierarchy.
648 private void SetDisplayName(InputControl control, string longDisplayNameFromLayout, string shortDisplayNameFromLayout, bool shortName)
649 {
650 var displayNameFromLayout = shortName ? shortDisplayNameFromLayout : longDisplayNameFromLayout;
651
652 // Display name may not be set in layout.
653 if (string.IsNullOrEmpty(displayNameFromLayout))
654 {
655 // For short names, we leave it unassigned if there's nothing in the layout
656 // except if it's a nested control where the parent has a short name.
657 if (shortName)
658 {
659 if (control.parent != null && control.parent != control.device)
660 {
661 if (m_StringBuilder == null)
662 m_StringBuilder = new StringBuilder();
663 m_StringBuilder.Length = 0;
664 AddParentDisplayNameRecursive(control.parent, m_StringBuilder, true);
665 if (m_StringBuilder.Length == 0)
666 {
667 control.m_ShortDisplayNameFromLayout = null;
668 return;
669 }
670
671 if (!string.IsNullOrEmpty(longDisplayNameFromLayout))
672 m_StringBuilder.Append(longDisplayNameFromLayout);
673 else
674 m_StringBuilder.Append(control.name);
675 control.m_ShortDisplayNameFromLayout = m_StringBuilder.ToString();
676 return;
677 }
678
679 control.m_ShortDisplayNameFromLayout = null;
680 return;
681 }
682
683 ////REVIEW: automatically uppercase or prettify this?
684 // For long names, we default to the control's name.
685 displayNameFromLayout = control.name;
686 }
687
688 // If it's a nested control, synthesize a path that includes parents.
689 if (control.parent != null && control.parent != control.device)
690 {
691 if (m_StringBuilder == null)
692 m_StringBuilder = new StringBuilder();
693 m_StringBuilder.Length = 0;
694 AddParentDisplayNameRecursive(control.parent, m_StringBuilder, shortName);
695 m_StringBuilder.Append(displayNameFromLayout);
696 displayNameFromLayout = m_StringBuilder.ToString();
697 }
698
699 // Assign.
700 if (shortName)
701 control.m_ShortDisplayNameFromLayout = displayNameFromLayout;
702 else
703 control.m_DisplayNameFromLayout = displayNameFromLayout;
704 }
705
706 private static void AddParentDisplayNameRecursive(InputControl control, StringBuilder stringBuilder,
707 bool shortName)
708 {
709 if (control.parent != null && control.parent != control.device)
710 AddParentDisplayNameRecursive(control.parent, stringBuilder, shortName);
711
712 if (shortName)
713 {
714 var text = control.shortDisplayName;
715 if (string.IsNullOrEmpty(text))
716 text = control.displayName;
717
718 stringBuilder.Append(text);
719 }
720 else
721 {
722 stringBuilder.Append(control.displayName);
723 }
724 stringBuilder.Append(' ');
725 }
726
727 private static void AddProcessors(InputControl control, ref InputControlLayout.ControlItem controlItem, string layoutName)
728 {
729 var processorCount = controlItem.processors.Count;
730 for (var n = 0; n < processorCount; ++n)
731 {
732 var name = controlItem.processors[n].name;
733 var type = InputProcessor.s_Processors.LookupTypeRegistration(name);
734 if (type == null)
735 throw new InvalidOperationException(
736 $"Cannot find processor '{name}' referenced by control '{controlItem.name}' in layout '{layoutName}'");
737
738 var processor = Activator.CreateInstance(type);
739
740 var parameters = controlItem.processors[n].parameters;
741 if (parameters.Count > 0)
742 NamedValue.ApplyAllToObject(processor, parameters);
743
744 control.AddProcessor(processor);
745 }
746 }
747
748 private static void SetFormat(InputControl control, InputControlLayout.ControlItem controlItem)
749 {
750 control.m_StateBlock.format = controlItem.format;
751 if (controlItem.sizeInBits == 0)
752 {
753 var primitiveFormatSize = InputStateBlock.GetSizeOfPrimitiveFormatInBits(controlItem.format);
754 if (primitiveFormatSize != -1)
755 control.m_StateBlock.sizeInBits = (uint)primitiveFormatSize;
756 }
757 }
758
759 private static InputControlLayout FindOrLoadLayout(string name)
760 {
761 Debug.Assert(InputControlLayout.s_CacheInstanceRef > 0, "Should have acquired layout cache reference");
762 return InputControlLayout.cache.FindOrLoadLayout(name);
763 }
764
765 private static void ComputeStateLayout(InputControl control)
766 {
767 var children = control.children;
768
769 // If the control has a format but no size specified and the format is a
770 // primitive format, just set the size automatically.
771 if (control.m_StateBlock.sizeInBits == 0 && control.m_StateBlock.format != 0)
772 {
773 var sizeInBits = InputStateBlock.GetSizeOfPrimitiveFormatInBits(control.m_StateBlock.format);
774 if (sizeInBits != -1)
775 control.m_StateBlock.sizeInBits = (uint)sizeInBits;
776 }
777
778 // If state size is not set, it means it's computed from the size of the
779 // children so make sure we actually have children.
780 if (control.m_StateBlock.sizeInBits == 0 && children.Count == 0)
781 {
782 throw new InvalidOperationException(
783 $"Control '{control.path}' with layout '{control.layout}' has no size set and has no children to compute size from");
784 }
785
786 // If there's no children, our job is done.
787 if (children.Count == 0)
788 return;
789
790 // First deal with children that want fixed offsets. All the other ones
791 // will get appended to the end.
792 var firstUnfixedByteOffset = 0u;
793 foreach (var child in children)
794 {
795 Debug.Assert(child.m_StateBlock.sizeInBits != 0, "Size of state block not set on child");
796
797 // Skip children using state from other controls.
798 if (child.m_StateBlock.sizeInBits == kSizeForControlUsingStateFromOtherControl)
799 continue;
800
801 // Make sure the child has a valid size set on it.
802 var childSizeInBits = child.m_StateBlock.sizeInBits;
803 if (childSizeInBits == 0 || childSizeInBits == InputStateBlock.InvalidOffset)
804 throw new InvalidOperationException(
805 $"Child '{child.name}' of '{control.name}' has no size set!");
806
807 // Skip children that don't have fixed offsets.
808 if (child.m_StateBlock.byteOffset == InputStateBlock.InvalidOffset ||
809 child.m_StateBlock.byteOffset == InputStateBlock.AutomaticOffset)
810 continue;
811
812 // At this point, if the child has no valid bit offset, put it at #0 now.
813 if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset)
814 child.m_StateBlock.bitOffset = 0;
815
816 // See if the control bumps our fixed layout size.
817 var endOffset =
818 MemoryHelpers.ComputeFollowingByteOffset(child.m_StateBlock.byteOffset, child.m_StateBlock.bitOffset + childSizeInBits);
819 if (endOffset > firstUnfixedByteOffset)
820 firstUnfixedByteOffset = endOffset;
821 }
822
823 ////TODO: this doesn't support mixed automatic and fixed layouting *within* bitfields;
824 //// I think it's okay not to support that but we should at least detect it
825
826 // Now assign an offset to every control that wants an
827 // automatic offset. For bitfields, we need to delay advancing byte
828 // offsets until we've seen all bits in the fields.
829 // NOTE: Bit addressing controls using automatic offsets *must* be consecutive.
830 var runningByteOffset = firstUnfixedByteOffset;
831 InputControl firstBitAddressingChild = null;
832 var bitfieldSizeInBits = 0u;
833 foreach (var child in children)
834 {
835 // Skip children with fixed offsets.
836 if (child.m_StateBlock.byteOffset != InputStateBlock.InvalidOffset &&
837 child.m_StateBlock.byteOffset != InputStateBlock.AutomaticOffset)
838 continue;
839
840 // Skip children using state from other controls.
841 if (child.m_StateBlock.sizeInBits == kSizeForControlUsingStateFromOtherControl)
842 continue;
843
844 // See if it's a bit addressing control.
845 var isBitAddressingChild = (child.m_StateBlock.sizeInBits % 8) != 0;
846 if (isBitAddressingChild)
847 {
848 // Remember start of bitfield group.
849 if (firstBitAddressingChild == null)
850 firstBitAddressingChild = child;
851
852 // Keep a running count of the size of the bitfield.
853 if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset ||
854 child.m_StateBlock.bitOffset == InputStateBlock.AutomaticOffset)
855 {
856 // Put child at current bit offset.
857 child.m_StateBlock.bitOffset = bitfieldSizeInBits;
858
859 bitfieldSizeInBits += child.m_StateBlock.sizeInBits;
860 }
861 else
862 {
863 // Child already has bit offset. Keep it but make sure we're accounting for it
864 // in the bitfield size.
865 var lastBit = child.m_StateBlock.bitOffset + child.m_StateBlock.sizeInBits;
866 if (lastBit > bitfieldSizeInBits)
867 bitfieldSizeInBits = lastBit;
868 }
869 }
870 else
871 {
872 // Terminate bitfield group (if there was one).
873 if (firstBitAddressingChild != null)
874 {
875 runningByteOffset = MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, bitfieldSizeInBits);
876 firstBitAddressingChild = null;
877 }
878
879 if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset)
880 child.m_StateBlock.bitOffset = 0;
881
882 // Conform to memory addressing constraints of CPU architecture. If we don't do
883 // this, ARMs will end up choking on misaligned memory accesses.
884 runningByteOffset = MemoryHelpers.AlignNatural(runningByteOffset, child.m_StateBlock.alignedSizeInBytes);
885 }
886
887 ////FIXME: seems like this should take bitOffset into account
888 child.m_StateBlock.byteOffset = runningByteOffset;
889
890 if (!isBitAddressingChild)
891 runningByteOffset =
892 MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, child.m_StateBlock.sizeInBits);
893 }
894
895 // Compute total size.
896 // If we ended on a bitfield, account for its size.
897 if (firstBitAddressingChild != null)
898 runningByteOffset = MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, bitfieldSizeInBits);
899 var totalSizeInBytes = runningByteOffset;
900
901 // Set size. We force all parents to the combined size of their children.
902 control.m_StateBlock.sizeInBits = totalSizeInBytes * 8;
903 }
904
905 private void FinalizeControlHierarchy()
906 {
907 if (m_StateOffsetToControlMap == null)
908 m_StateOffsetToControlMap = new List<uint>();
909
910 if (m_Device.allControls.Count > (1U << InputDevice.kControlIndexBits))
911 throw new NotSupportedException($"Device '{m_Device}' exceeds maximum supported control count of {1U << InputDevice.kControlIndexBits} (has {m_Device.allControls.Count} controls)");
912
913 var rootNode = new InputDevice.ControlBitRangeNode((ushort)(m_Device.m_StateBlock.sizeInBits - 1));
914 m_Device.m_ControlTreeNodes = new InputDevice.ControlBitRangeNode[1];
915 m_Device.m_ControlTreeNodes[0] = rootNode;
916
917 var controlIndiciesNextFreeIndex = 0;
918 // Device is not in m_ChildrenForEachControl so use index -1.
919 FinalizeControlHierarchyRecursive(m_Device, -1, m_Device.m_ChildrenForEachControl, false, false, ref controlIndiciesNextFreeIndex);
920 }
921
922 private void FinalizeControlHierarchyRecursive(InputControl control, int controlIndex, InputControl[] allControls, bool noisy, bool dontReset, ref int controlIndiciesNextFreeIndex)
923 {
924 // Make sure we're staying within limits on state offsets and sizes.
925 if (control.m_ChildCount == 0)
926 {
927 if (control.m_StateBlock.effectiveBitOffset >= (1U << InputDevice.kStateOffsetBits))
928 throw new NotSupportedException($"Control '{control}' exceeds maximum supported state bit offset of {(1U << InputDevice.kStateOffsetBits) - 1} (bit offset {control.stateBlock.effectiveBitOffset})");
929 if (control.m_StateBlock.sizeInBits >= (1U << InputDevice.kStateSizeBits))
930 throw new NotSupportedException($"Control '{control}' exceeds maximum supported state bit size of {(1U << InputDevice.kStateSizeBits) - 1} (bit offset {control.stateBlock.sizeInBits})");
931 }
932
933 // Construct control bit range tree
934 if (control != m_Device)
935 InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[0], control, ref controlIndiciesNextFreeIndex, 0);
936
937 // Add all leaf controls to state offset mapping.
938 if (control.m_ChildCount == 0)
939 m_StateOffsetToControlMap.Add(
940 InputDevice.EncodeStateOffsetToControlMapEntry((uint)controlIndex, control.m_StateBlock.effectiveBitOffset, control.m_StateBlock.sizeInBits));
941
942 // Set final display names. This may overwrite the ones supplied by the layout so temporarily
943 // store the values here.
944 var displayNameFromLayout = control.m_DisplayNameFromLayout;
945 var shortDisplayNameFromLayout = control.m_ShortDisplayNameFromLayout;
946 SetDisplayName(control, displayNameFromLayout, shortDisplayNameFromLayout, false);
947 SetDisplayName(control, displayNameFromLayout, shortDisplayNameFromLayout, true);
948
949 if (control != control.device)
950 {
951 if (noisy)
952 control.noisy = true;
953 else
954 noisy = control.noisy;
955
956 if (dontReset)
957 control.dontReset = true;
958 else
959 dontReset = control.dontReset;
960 }
961
962 // Recurse into children. Also bake our state offset into our children.
963 var ourOffset = control.m_StateBlock.byteOffset;
964 var childCount = control.m_ChildCount;
965 var childStartIndex = control.m_ChildStartIndex;
966 for (var i = 0; i < childCount; ++i)
967 {
968 var childIndex = childStartIndex + i;
969 var child = allControls[childIndex];
970 child.m_StateBlock.byteOffset += ourOffset;
971
972 FinalizeControlHierarchyRecursive(child, childIndex, allControls, noisy, dontReset, ref controlIndiciesNextFreeIndex);
973 }
974
975 control.isSetupFinished = true;
976 }
977
978 private void InsertControlBitRangeNode(ref InputDevice.ControlBitRangeNode parent, InputControl control, ref int controlIndiciesNextFreeIndex, ushort startOffset)
979 {
980 InputDevice.ControlBitRangeNode leftNode;
981 InputDevice.ControlBitRangeNode rightNode;
982
983 // we don't recalculate mid-points for nodes that have already been created
984 if (parent.leftChildIndex == -1)
985 {
986 var midPoint = GetBestMidPoint(parent, startOffset);
987 leftNode = new InputDevice.ControlBitRangeNode(midPoint);
988 rightNode = new InputDevice.ControlBitRangeNode(parent.endBitOffset);
989 AddChildren(ref parent, leftNode, rightNode);
990 }
991 else
992 {
993 leftNode = m_Device.m_ControlTreeNodes[parent.leftChildIndex];
994 rightNode = m_Device.m_ControlTreeNodes[parent.leftChildIndex + 1];
995 }
996
997
998 // if the control starts in the left node and ends in the right, add a pointer to both nodes and return
999 if (control.m_StateBlock.effectiveBitOffset < leftNode.endBitOffset &&
1000 control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits > leftNode.endBitOffset)
1001 {
1002 AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex);
1003 AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex + 1);
1004 return;
1005 }
1006
1007 // if it exactly fits one of the nodes, add a pointer to just that node and return
1008 if (control.m_StateBlock.effectiveBitOffset == startOffset &&
1009 control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits == leftNode.endBitOffset)
1010 {
1011 AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex);
1012 return;
1013 }
1014
1015 if (control.m_StateBlock.effectiveBitOffset == leftNode.endBitOffset &&
1016 control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits == rightNode.endBitOffset)
1017 {
1018 AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex + 1);
1019 return;
1020 }
1021
1022 // otherwise, if the node ends in the left node, recurse left
1023 if (control.m_StateBlock.effectiveBitOffset < leftNode.endBitOffset)
1024 InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[parent.leftChildIndex], control,
1025 ref controlIndiciesNextFreeIndex, startOffset);
1026 else
1027 InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[parent.leftChildIndex + 1], control,
1028 ref controlIndiciesNextFreeIndex, leftNode.endBitOffset);
1029 }
1030
1031 private ushort GetBestMidPoint(InputDevice.ControlBitRangeNode parent, ushort startOffset)
1032 {
1033 // find the absolute mid-point, rounded up
1034 var absoluteMidPoint = (ushort)(startOffset + ((parent.endBitOffset - startOffset - 1) / 2 + 1));
1035 var closestControlEndPointToMidPoint = ushort.MaxValue;
1036 var closestControlStartPointToMidPoint = ushort.MaxValue;
1037
1038 // go through all controls and find the start and end offsets that are closest to the absolute mid-point
1039 foreach (var control in m_Device.m_ChildrenForEachControl)
1040 {
1041 var stateBlock = control.m_StateBlock;
1042
1043 // don't consider controls that end before the start of the parent range, or start after
1044 // the end of the parent range
1045 if (stateBlock.effectiveBitOffset + stateBlock.sizeInBits - 1 < startOffset ||
1046 stateBlock.effectiveBitOffset >= parent.endBitOffset)
1047 continue;
1048
1049 // don't consider controls that are larger than the parent range
1050 if (stateBlock.sizeInBits > parent.endBitOffset - startOffset)
1051 continue;
1052
1053 // don't consider controls that start or end on the same boundary as the parent
1054 if (stateBlock.effectiveBitOffset == startOffset ||
1055 stateBlock.effectiveBitOffset + stateBlock.sizeInBits == parent.endBitOffset)
1056 continue;
1057
1058 if (Math.Abs(stateBlock.effectiveBitOffset + stateBlock.sizeInBits - (int)absoluteMidPoint) <
1059 Math.Abs(closestControlEndPointToMidPoint - absoluteMidPoint) &&
1060 stateBlock.effectiveBitOffset + stateBlock.sizeInBits < parent.endBitOffset)
1061 {
1062 closestControlEndPointToMidPoint = (ushort)(stateBlock.effectiveBitOffset + stateBlock.sizeInBits);
1063 }
1064
1065 if (Math.Abs(stateBlock.effectiveBitOffset - (int)absoluteMidPoint) <
1066 Math.Abs(closestControlStartPointToMidPoint - absoluteMidPoint) &&
1067 stateBlock.effectiveBitOffset >= startOffset)
1068 {
1069 closestControlStartPointToMidPoint = (ushort)stateBlock.effectiveBitOffset;
1070 }
1071 }
1072
1073 var absoluteMidPointCollisions = 0;
1074 var controlStartMidPointCollisions = 0;
1075 var controlEndMidPointCollisions = 0;
1076
1077 // figure out which of the possible midpoints intersects the fewest controls. The one with the fewest
1078 // is the best one because it means fewer controls will be added to this node.
1079 foreach (var control in m_Device.m_ChildrenForEachControl)
1080 {
1081 if (closestControlStartPointToMidPoint != ushort.MaxValue &&
1082 closestControlStartPointToMidPoint > control.m_StateBlock.effectiveBitOffset &&
1083 closestControlStartPointToMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits)
1084 controlStartMidPointCollisions++;
1085
1086 if (closestControlEndPointToMidPoint != ushort.MaxValue &&
1087 closestControlEndPointToMidPoint > control.m_StateBlock.effectiveBitOffset &&
1088 closestControlEndPointToMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits)
1089 controlEndMidPointCollisions++;
1090
1091 if (absoluteMidPoint > control.m_StateBlock.effectiveBitOffset &&
1092 absoluteMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits)
1093 absoluteMidPointCollisions++;
1094 }
1095
1096 if (closestControlEndPointToMidPoint != ushort.MaxValue &&
1097 controlEndMidPointCollisions <= controlStartMidPointCollisions &&
1098 controlEndMidPointCollisions <= absoluteMidPointCollisions)
1099 {
1100 Debug.Assert(closestControlEndPointToMidPoint >= startOffset && closestControlEndPointToMidPoint <= startOffset + parent.endBitOffset);
1101 return closestControlEndPointToMidPoint;
1102 }
1103
1104 if (closestControlStartPointToMidPoint != ushort.MaxValue &&
1105 controlStartMidPointCollisions <= controlEndMidPointCollisions &&
1106 controlStartMidPointCollisions <= absoluteMidPointCollisions)
1107 {
1108 Debug.Assert(closestControlStartPointToMidPoint >= startOffset && closestControlStartPointToMidPoint <= startOffset + parent.endBitOffset);
1109 return closestControlStartPointToMidPoint;
1110 }
1111
1112 Debug.Assert(absoluteMidPoint >= startOffset && absoluteMidPoint <= startOffset + parent.endBitOffset);
1113 return absoluteMidPoint;
1114 }
1115
1116 private void AddControlToNode(InputControl control, ref int controlIndiciesNextFreeIndex, int nodeIndex)
1117 {
1118 Debug.Assert(m_Device.m_ControlTreeNodes[nodeIndex].controlCount < 255,
1119 "Control bit range nodes can address maximum of 255 controls.");
1120
1121 ref var node = ref m_Device.m_ControlTreeNodes[nodeIndex];
1122 var leafControlStartIndex = node.controlStartIndex;
1123 if (node.controlCount == 0)
1124 {
1125 node.controlStartIndex = (ushort)controlIndiciesNextFreeIndex;
1126 leafControlStartIndex = node.controlStartIndex;
1127 }
1128
1129 ArrayHelpers.InsertAt(ref m_Device.m_ControlTreeIndices,
1130 node.controlStartIndex + node.controlCount,
1131 GetControlIndex(control));
1132 ++node.controlCount;
1133 ++controlIndiciesNextFreeIndex;
1134
1135 // bump up all the start indicies for nodes that have a start index larger than the one we just inserted into
1136 for (var i = 0; i < m_Device.m_ControlTreeNodes.Length; i++)
1137 {
1138 if (m_Device.m_ControlTreeNodes[i].controlCount == 0 ||
1139 m_Device.m_ControlTreeNodes[i].controlStartIndex <= leafControlStartIndex)
1140 continue;
1141
1142 ++m_Device.m_ControlTreeNodes[i].controlStartIndex;
1143 }
1144 }
1145
1146 private void AddChildren(ref InputDevice.ControlBitRangeNode parent, InputDevice.ControlBitRangeNode left, InputDevice.ControlBitRangeNode right)
1147 {
1148 // if this node has a child start index, its already in the tree
1149 if (parent.leftChildIndex != -1)
1150 return;
1151
1152 var startIndex = m_Device.m_ControlTreeNodes.Length;
1153 parent.leftChildIndex = (short)startIndex;
1154 Array.Resize(ref m_Device.m_ControlTreeNodes, startIndex + 2);
1155 m_Device.m_ControlTreeNodes[startIndex] = left;
1156 m_Device.m_ControlTreeNodes[startIndex + 1] = right;
1157 }
1158
1159 private ushort GetControlIndex(InputControl control)
1160 {
1161 for (var i = 0; i < m_Device.m_ChildrenForEachControl.Length; i++)
1162 {
1163 if (control == m_Device.m_ChildrenForEachControl[i])
1164 return (ushort)i;
1165 }
1166
1167 throw new InvalidOperationException($"InputDeviceBuilder error. Couldn't find control {control}.");
1168 }
1169
1170 private static InputDeviceBuilder s_Instance;
1171 private static int s_InstanceRef;
1172
1173 internal static ref InputDeviceBuilder instance
1174 {
1175 get
1176 {
1177 Debug.Assert(s_InstanceRef > 0, "Must hold an instance reference");
1178 return ref s_Instance;
1179 }
1180 }
1181
1182 internal static RefInstance Ref()
1183 {
1184 Debug.Assert(s_Instance.m_Device == null,
1185 "InputDeviceBuilder is already in use! Cannot use the builder recursively");
1186 ++s_InstanceRef;
1187 return new RefInstance();
1188 }
1189
1190 // Helper that allows setting up an InputDeviceBuilder such that it will either be created
1191 // locally and temporarily or, if one already exists globally, reused.
1192 internal struct RefInstance : IDisposable
1193 {
1194 public void Dispose()
1195 {
1196 --s_InstanceRef;
1197 if (s_InstanceRef <= 0)
1198 {
1199 s_Instance.Dispose();
1200 s_Instance = default;
1201 s_InstanceRef = 0;
1202 }
1203 else
1204 // Make sure we reset when there is an exception.
1205 s_Instance.Reset();
1206 }
1207 }
1208 }
1209}