Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Devices /InputDeviceBuilder.cs
| using System; | |
| using System.Collections.Generic; | |
| using System.Text; | |
| using UnityEngine.InputSystem.Controls; | |
| using UnityEngine.InputSystem.LowLevel; | |
| using UnityEngine.InputSystem.Utilities; | |
| ////TODO: add ability to add to existing arrays rather than creating per-device arrays | |
| ////TODO: the next step here is to write a code generator that generates code for a given layout that when | |
| //// executed, does what InputDeviceBuilder does but without the use of reflection and much more quickly | |
| ////REVIEW: it probably makes sense to have an initial phase where we process the initial set of | |
| //// device discoveries from native and keep the layout cache around instead of throwing | |
| //// it away after the creation of every single device; best approach may be to just | |
| //// reuse the same InputDeviceBuilder instance over and over | |
| ////TODO: ensure that things are aligned properly for ARM; should that be done on the reading side or in the state layouts? | |
| //// (make sure that alignment works the same on *all* platforms; otherwise editor will not be able to process events from players properly) | |
| namespace UnityEngine.InputSystem.Layouts | |
| { | |
| /// <summary> | |
| /// Turns a device layout into an actual <see cref="InputDevice"/> instance. | |
| /// </summary> | |
| /// <remarks> | |
| /// Ultimately produces a device but can also be used to query the control setup described | |
| /// by a layout. | |
| /// | |
| /// Can be used both to create control hierarchies from scratch as well as to re-create or | |
| /// change existing hierarchies. | |
| /// | |
| /// InputDeviceBuilder is the only way to create control hierarchies. InputControls cannot be | |
| /// <c>new</c>'d directly. | |
| /// | |
| /// Also computes a final state layout when setup is finished. | |
| /// | |
| /// Note that InputDeviceBuilders generate garbage. They are meant to be used for initialization only. Don't | |
| /// use them during normal gameplay. | |
| /// | |
| /// Running an *existing* device through another control build is a *destructive* operation. | |
| /// Existing controls may be reused while at the same time the hierarchy and even the device instance | |
| /// itself may change. | |
| /// </remarks> | |
| internal struct InputDeviceBuilder : IDisposable | |
| { | |
| public void Setup(InternedString layout, InternedString variants, | |
| InputDeviceDescription deviceDescription = default) | |
| { | |
| m_LayoutCacheRef = InputControlLayout.CacheRef(); | |
| InstantiateLayout(layout, variants, new InternedString(), null); | |
| FinalizeControlHierarchy(); | |
| m_StateOffsetToControlMap.Sort(); | |
| m_Device.m_Description = deviceDescription; | |
| m_Device.m_StateOffsetToControlMap = m_StateOffsetToControlMap.ToArray(); | |
| m_Device.CallFinishSetupRecursive(); | |
| } | |
| // Complete the setup and return the full control hierarchy setup | |
| // with its device root. | |
| public InputDevice Finish() | |
| { | |
| var device = m_Device; | |
| // Kill off our state. | |
| Reset(); | |
| return device; | |
| } | |
| public void Dispose() | |
| { | |
| m_LayoutCacheRef.Dispose(); | |
| } | |
| private InputDevice m_Device; | |
| // Make sure the global layout cache sticks around for at least as long | |
| // as the device builder so that we don't load layouts over and over. | |
| private InputControlLayout.CacheRefInstance m_LayoutCacheRef; | |
| // Table mapping (lower-cased) control paths to control layouts that contain | |
| // overrides for the control at the given path. | |
| private Dictionary<string, InputControlLayout.ControlItem> m_ChildControlOverrides; | |
| private List<uint> m_StateOffsetToControlMap; | |
| private StringBuilder m_StringBuilder; | |
| // Reset the setup in a way where it can be reused for another setup. | |
| // Should retain allocations that can be reused. | |
| private void Reset() | |
| { | |
| m_Device = null; | |
| m_ChildControlOverrides?.Clear(); | |
| m_StateOffsetToControlMap?.Clear(); | |
| // Leave the cache in place so we can reuse them in another setup path. | |
| } | |
| private InputControl InstantiateLayout(InternedString layout, InternedString variants, InternedString name, InputControl parent) | |
| { | |
| // Look up layout by name. | |
| var layoutInstance = FindOrLoadLayout(layout); | |
| // Create control hierarchy. | |
| return InstantiateLayout(layoutInstance, variants, name, parent); | |
| } | |
| private InputControl InstantiateLayout(InputControlLayout layout, InternedString variants, InternedString name, | |
| InputControl parent) | |
| { | |
| Debug.Assert(layout.type != null, "Layout has no type set on it"); | |
| // No, so create a new control. | |
| var controlObject = Activator.CreateInstance(layout.type); | |
| if (!(controlObject is InputControl control)) | |
| { | |
| throw new InvalidOperationException( | |
| $"Type '{layout.type.Name}' referenced by layout '{layout.name}' is not an InputControl"); | |
| } | |
| // If it's a device, perform some extra work specific to the control | |
| // hierarchy root. | |
| if (control is InputDevice controlAsDevice) | |
| { | |
| if (parent != null) | |
| throw new InvalidOperationException( | |
| $"Cannot instantiate device layout '{layout.name}' as child of '{parent.path}'; devices must be added at root"); | |
| m_Device = controlAsDevice; | |
| m_Device.m_StateBlock.byteOffset = 0; | |
| m_Device.m_StateBlock.bitOffset = 0; | |
| m_Device.m_StateBlock.format = layout.stateFormat; | |
| // If we have an existing device, we'll start the various control arrays | |
| // from scratch. Note that all the controls still refer to the existing | |
| // arrays and so we can iterate children, for example, just fine while | |
| // we are rebuilding the control hierarchy. | |
| m_Device.m_AliasesForEachControl = null; | |
| m_Device.m_ChildrenForEachControl = null; | |
| m_Device.m_UsagesForEachControl = null; | |
| m_Device.m_UsageToControl = null; | |
| if (layout.m_UpdateBeforeRender == true) | |
| m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.UpdateBeforeRender; | |
| if (layout.canRunInBackground != null) | |
| { | |
| m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.CanRunInBackgroundHasBeenQueried; | |
| if (layout.canRunInBackground == true) | |
| m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.CanRunInBackground; | |
| } | |
| } | |
| else if (parent == null) | |
| { | |
| // Someone did "new InputDeviceBuilder(...)" with a control layout. | |
| // We don't support creating control hierarchies without a device at the root. | |
| throw new InvalidOperationException( | |
| $"Toplevel layout used with InputDeviceBuilder must be a device layout; '{layout.name}' is a control layout"); | |
| } | |
| // Name defaults to name of layout. | |
| if (name.IsEmpty()) | |
| { | |
| name = layout.name; | |
| // If there's a namespace in the layout name, snip it out. | |
| var indexOfLastColon = name.ToString().LastIndexOf(':'); | |
| if (indexOfLastColon != -1) | |
| name = new InternedString(name.ToString().Substring(indexOfLastColon + 1)); | |
| } | |
| // Make sure name does not contain any slashes. | |
| if (name.ToString().IndexOf(InputControlPath.Separator) != -1) | |
| name = new InternedString(name.ToString().CleanSlashes()); | |
| // Variant defaults to variants of layout. | |
| if (variants.IsEmpty()) | |
| { | |
| variants = layout.variants; | |
| if (variants.IsEmpty()) | |
| variants = InputControlLayout.DefaultVariant; | |
| } | |
| control.m_Name = name; | |
| control.m_DisplayNameFromLayout = layout.m_DisplayName; // No short display names at layout roots. | |
| control.m_Layout = layout.name; | |
| control.m_Variants = variants; | |
| control.m_Parent = parent; | |
| control.m_Device = m_Device; | |
| // this has to be done down here instead of in the device block above because the state for the | |
| // device needs to be set up before setting noisy or it will throw because the device's m_Device | |
| // hasn't been set yet. Yes, a device's m_Device is itself. | |
| if (control is InputDevice) | |
| control.noisy = layout.isNoisy; | |
| // Create children and configure their settings from our | |
| // layout values. | |
| var haveChildrenUsingStateFromOtherControl = false; | |
| try | |
| { | |
| // Pass list of existing control on to function as we may have decided to not | |
| // actually reuse the existing control (and thus control.m_ChildrenReadOnly will | |
| // now be blank) but still want crawling down the hierarchy to preserve existing | |
| // controls where possible. | |
| AddChildControls(layout, variants, control, | |
| ref haveChildrenUsingStateFromOtherControl); | |
| } | |
| catch | |
| { | |
| ////TODO: remove control from collection and rethrow | |
| throw; | |
| } | |
| // Come up with a layout for our state. | |
| ComputeStateLayout(control); | |
| // Finally, if we have child controls that take their state blocks from other | |
| // controls, assign them their blocks now. | |
| if (haveChildrenUsingStateFromOtherControl) | |
| { | |
| var controls = layout.m_Controls; | |
| for (var i = 0; i < controls.Length; ++i) | |
| { | |
| ref var item = ref controls[i]; | |
| if (string.IsNullOrEmpty(item.useStateFrom)) | |
| continue; | |
| ApplyUseStateFrom(control, ref item, layout); | |
| } | |
| } | |
| return control; | |
| } | |
| private const uint kSizeForControlUsingStateFromOtherControl = InputStateBlock.InvalidOffset; | |
| private void AddChildControls(InputControlLayout layout, InternedString variants, InputControl parent, | |
| ref bool haveChildrenUsingStateFromOtherControls) | |
| { | |
| var controlLayouts = layout.m_Controls; | |
| if (controlLayouts == null) | |
| return; | |
| // Find out how many direct children we will add. | |
| var childCount = 0; | |
| var haveControlLayoutWithPath = false; | |
| for (var i = 0; i < controlLayouts.Length; ++i) | |
| { | |
| // Skip if variants don't match. | |
| if (!controlLayouts[i].variants.IsEmpty() && | |
| !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayouts[i].variants, | |
| variants, InputControlLayout.VariantSeparator[0])) | |
| continue; | |
| ////REVIEW: I'm not sure this is good enough. ATM if you have a control layout with | |
| //// name "foo" and one with name "foo/bar", then the latter is taken as an override | |
| //// but the former isn't. However, whether it has a slash in the path or not shouldn't | |
| //// matter. If a control layout of the same name already exists, it should be | |
| //// considered an override, if not, it shouldn't. | |
| // Not a new child if it's a layout reaching in to the hierarchy to modify | |
| // an existing child. | |
| if (controlLayouts[i].isModifyingExistingControl) | |
| { | |
| if (controlLayouts[i].isArray) | |
| throw new NotSupportedException( | |
| $"Control '{controlLayouts[i].name}' in layout '{layout.name}' is modifying the child of another control but is marked as an array"); | |
| haveControlLayoutWithPath = true; | |
| InsertChildControlOverride(parent, ref controlLayouts[i]); | |
| continue; | |
| } | |
| if (controlLayouts[i].isArray) | |
| childCount += controlLayouts[i].arraySize; | |
| else | |
| ++childCount; | |
| } | |
| // Nothing to do if there's no children. | |
| if (childCount == 0) | |
| { | |
| parent.m_ChildCount = default; | |
| parent.m_ChildStartIndex = default; | |
| haveChildrenUsingStateFromOtherControls = false; | |
| return; | |
| } | |
| // Add room for us in the device's child array. | |
| var firstChildIndex = ArrayHelpers.GrowBy(ref m_Device.m_ChildrenForEachControl, childCount); | |
| // Add controls from all control layouts except the ones that have | |
| // paths in them. | |
| var childIndex = firstChildIndex; | |
| for (var i = 0; i < controlLayouts.Length; ++i) | |
| { | |
| var controlLayout = controlLayouts[i]; | |
| // Skip control layouts that don't add controls but rather modify child | |
| // controls of other controls added by the layout. We do a second pass | |
| // to apply their settings. | |
| if (controlLayout.isModifyingExistingControl) | |
| continue; | |
| // If the control is part of a variant, skip it if it isn't in the variants we're | |
| // looking for. | |
| if (!controlLayout.variants.IsEmpty() && | |
| !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayout.variants, | |
| variants, InputControlLayout.VariantSeparator[0])) | |
| continue; | |
| // If it's an array, add a control for each array element. | |
| if (controlLayout.isArray) | |
| { | |
| for (var n = 0; n < controlLayout.arraySize; ++n) | |
| { | |
| var name = controlLayout.name + n; | |
| var control = AddChildControl(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls, | |
| controlLayout, childIndex, nameOverride: name); | |
| ++childIndex; | |
| // Adjust offset, if the control uses explicit offsets. | |
| if (control.m_StateBlock.byteOffset != InputStateBlock.InvalidOffset) | |
| control.m_StateBlock.byteOffset += (uint)n * control.m_StateBlock.alignedSizeInBytes; | |
| } | |
| } | |
| else | |
| { | |
| AddChildControl(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls, | |
| controlLayout, childIndex); | |
| ++childIndex; | |
| } | |
| } | |
| parent.m_ChildCount = childCount; | |
| parent.m_ChildStartIndex = firstChildIndex; | |
| ////REVIEW: there's probably a better way to do this based on m_ChildControlOverrides | |
| // We apply all overrides through m_ChildControlOverrides. However, there may be a control item | |
| // that *adds* a child control to another existing control. This will look the same as overriding | |
| // properties on a child control just that in this case the child control doesn't exist. | |
| // | |
| // Go through all the controls and check for ones that need to be added. | |
| if (haveControlLayoutWithPath) | |
| { | |
| for (var i = 0; i < controlLayouts.Length; ++i) | |
| { | |
| var controlLayout = controlLayouts[i]; | |
| if (!controlLayout.isModifyingExistingControl) | |
| continue; | |
| // If the control is part of a variants, skip it if it isn't the variants we're | |
| // looking for. | |
| if (!controlLayout.variants.IsEmpty() && | |
| !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayouts[i].variants, | |
| variants, InputControlLayout.VariantSeparator[0])) | |
| continue; | |
| AddChildControlIfMissing(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls, | |
| ref controlLayout); | |
| } | |
| } | |
| } | |
| private InputControl AddChildControl(InputControlLayout layout, InternedString variants, InputControl parent, | |
| ref bool haveChildrenUsingStateFromOtherControls, | |
| InputControlLayout.ControlItem controlItem, | |
| int childIndex, string nameOverride = null) | |
| { | |
| var name = nameOverride != null ? new InternedString(nameOverride) : controlItem.name; | |
| ////REVIEW: can we check this in InputControlLayout instead? | |
| if (string.IsNullOrEmpty(controlItem.layout)) | |
| throw new InvalidOperationException($"Layout has not been set on control '{controlItem.name}' in '{layout.name}'"); | |
| // See if there is an override for the control. | |
| if (m_ChildControlOverrides != null) | |
| { | |
| var pathLowerCase = ChildControlOverridePath(parent, name); | |
| if (m_ChildControlOverrides.TryGetValue(pathLowerCase, out var controlOverride)) | |
| controlItem = controlOverride.Merge(controlItem); | |
| } | |
| // Get name of layout to use for control. | |
| var layoutName = controlItem.layout; | |
| // Create control. | |
| InputControl control; | |
| try | |
| { | |
| control = InstantiateLayout(layoutName, variants, name, parent); | |
| } | |
| catch (InputControlLayout.LayoutNotFoundException exception) | |
| { | |
| // Throw better exception that gives more info. | |
| throw new InputControlLayout.LayoutNotFoundException( | |
| $"Cannot find layout '{exception.layout}' used in control '{name}' of layout '{layout.name}'", | |
| exception); | |
| } | |
| // Add to array. | |
| // NOTE: AddChildControls and InstantiateLayout take care of growing the array and making | |
| // room for the immediate children of each control. | |
| m_Device.m_ChildrenForEachControl[childIndex] = control; | |
| // Set flags and misc things. | |
| control.noisy = controlItem.isNoisy; | |
| control.synthetic = controlItem.isSynthetic; | |
| control.usesStateFromOtherControl = !string.IsNullOrEmpty(controlItem.useStateFrom); | |
| control.dontReset = (control.noisy || controlItem.dontReset) && !control.usesStateFromOtherControl; // Imply dontReset for noisy controls. | |
| if (control.noisy) | |
| m_Device.noisy = true; | |
| control.isButton = control is ButtonControl; | |
| if (control.dontReset) | |
| m_Device.hasDontResetControls = true; | |
| // Remember the display names from the layout. We later do a proper pass once we have | |
| // the full hierarchy to set final names. | |
| control.m_DisplayNameFromLayout = controlItem.displayName; | |
| control.m_ShortDisplayNameFromLayout = controlItem.shortDisplayName; | |
| // Set default value. | |
| control.m_DefaultState = controlItem.defaultState; | |
| if (!control.m_DefaultState.isEmpty) | |
| m_Device.hasControlsWithDefaultState = true; | |
| // Set min and max value. Don't just overwrite here as the control's constructor may | |
| // have set a default value. | |
| if (!controlItem.minValue.isEmpty) | |
| control.m_MinValue = controlItem.minValue; | |
| if (!controlItem.maxValue.isEmpty) | |
| control.m_MaxValue = controlItem.maxValue; | |
| // Pass state block config on to control. | |
| if (!control.usesStateFromOtherControl) | |
| { | |
| control.m_StateBlock.byteOffset = controlItem.offset; | |
| control.m_StateBlock.bitOffset = controlItem.bit; | |
| if (controlItem.sizeInBits != 0) | |
| control.m_StateBlock.sizeInBits = controlItem.sizeInBits; | |
| if (controlItem.format != 0) | |
| SetFormat(control, controlItem); | |
| } | |
| else | |
| { | |
| // Mark controls that don't have state blocks of their own but rather get their | |
| // blocks from other controls by setting their state size to InvalidOffset. | |
| control.m_StateBlock.sizeInBits = kSizeForControlUsingStateFromOtherControl; | |
| haveChildrenUsingStateFromOtherControls = true; | |
| } | |
| ////REVIEW: the constant appending to m_UsagesForEachControl and m_AliasesForEachControl may lead to a lot | |
| //// of successive re-allocations | |
| // Add usages. | |
| var usages = controlItem.usages; | |
| if (usages.Count > 0) | |
| { | |
| var usageCount = usages.Count; | |
| var usageIndex = | |
| ArrayHelpers.AppendToImmutable(ref m_Device.m_UsagesForEachControl, usages.m_Array); | |
| control.m_UsageStartIndex = usageIndex; | |
| control.m_UsageCount = usageCount; | |
| ArrayHelpers.GrowBy(ref m_Device.m_UsageToControl, usageCount); | |
| for (var n = 0; n < usageCount; ++n) | |
| m_Device.m_UsageToControl[usageIndex + n] = control; | |
| } | |
| // Add aliases. | |
| if (controlItem.aliases.Count > 0) | |
| { | |
| var aliasCount = controlItem.aliases.Count; | |
| var aliasIndex = | |
| ArrayHelpers.AppendToImmutable(ref m_Device.m_AliasesForEachControl, controlItem.aliases.m_Array); | |
| control.m_AliasStartIndex = aliasIndex; | |
| control.m_AliasCount = aliasCount; | |
| } | |
| // Set parameters. | |
| if (controlItem.parameters.Count > 0) | |
| NamedValue.ApplyAllToObject(control, controlItem.parameters); | |
| // Add processors. | |
| if (controlItem.processors.Count > 0) | |
| AddProcessors(control, ref controlItem, layout.name); | |
| return control; | |
| } | |
| private void InsertChildControlOverride(InputControl parent, ref InputControlLayout.ControlItem controlItem) | |
| { | |
| if (m_ChildControlOverrides == null) | |
| m_ChildControlOverrides = new Dictionary<string, InputControlLayout.ControlItem>(); | |
| // See if there are existing overrides for the control. | |
| var pathLowerCase = ChildControlOverridePath(parent, controlItem.name); | |
| if (!m_ChildControlOverrides.TryGetValue(pathLowerCase, out var existingOverrides)) | |
| { | |
| // So, so just insert our overrides and we're done. | |
| m_ChildControlOverrides[pathLowerCase] = controlItem; | |
| return; | |
| } | |
| // Yes, there's existing overrides so we have to merge. | |
| // NOTE: The existing override's properties take precedence here. This is because | |
| // the override has been established from higher up in the layout hierarchy. | |
| existingOverrides = existingOverrides.Merge(controlItem); | |
| m_ChildControlOverrides[pathLowerCase] = existingOverrides; | |
| } | |
| private string ChildControlOverridePath(InputControl parent, InternedString controlName) | |
| { | |
| var pathLowerCase = controlName.ToLower(); | |
| for (var current = parent; current != m_Device; current = current.m_Parent) | |
| pathLowerCase = $"{current.m_Name.ToLower()}/{pathLowerCase}"; | |
| return pathLowerCase; | |
| } | |
| private void AddChildControlIfMissing(InputControlLayout layout, InternedString variants, InputControl parent, | |
| ref bool haveChildrenUsingStateFromOtherControls, | |
| ref InputControlLayout.ControlItem controlItem) | |
| { | |
| ////TODO: support arrays (we may modify an entire array in bulk) | |
| // Find the child control. | |
| var child = InputControlPath.TryFindChild(parent, controlItem.name); | |
| if (child != null) | |
| return; | |
| // We're adding a child somewhere in the existing hierarchy. This is a tricky | |
| // case as we have to potentially shift indices around in the hierarchy to make | |
| // room for the new control. | |
| ////TODO: this path does not support recovering existing controls? does it matter? | |
| child = InsertChildControl(layout, variants, parent, | |
| ref haveChildrenUsingStateFromOtherControls, ref controlItem); | |
| // Apply layout change. | |
| if (!ReferenceEquals(child.parent, parent)) | |
| ComputeStateLayout(child.parent); | |
| } | |
| private InputControl InsertChildControl(InputControlLayout layout, InternedString variant, InputControl parent, | |
| ref bool haveChildrenUsingStateFromOtherControls, | |
| ref InputControlLayout.ControlItem controlItem) | |
| { | |
| var path = controlItem.name.ToString(); | |
| // First we need to find the immediate parent from the given path. | |
| var indexOfSlash = path.LastIndexOf('/'); | |
| if (indexOfSlash == -1) | |
| throw new InvalidOperationException("InsertChildControl has to be called with a slash-separated path"); | |
| Debug.Assert(indexOfSlash != 0, "Could not find slash in path"); | |
| var immediateParentPath = path.Substring(0, indexOfSlash); | |
| var immediateParent = InputControlPath.TryFindChild(parent, immediateParentPath); | |
| if (immediateParent == null) | |
| throw new InvalidOperationException( | |
| $"Cannot find parent '{immediateParentPath}' of control '{controlItem.name}' in layout '{layout.name}'"); | |
| var controlName = path.Substring(indexOfSlash + 1); | |
| if (controlName.Length == 0) | |
| throw new InvalidOperationException( | |
| $"Path cannot end in '/' (control '{controlItem.name}' in layout '{layout.name}')"); | |
| // Make room in the device's child array. | |
| var childStartIndex = immediateParent.m_ChildStartIndex; | |
| if (childStartIndex == default) | |
| { | |
| // First child of parent. | |
| childStartIndex = m_Device.m_ChildrenForEachControl.LengthSafe(); | |
| immediateParent.m_ChildStartIndex = childStartIndex; | |
| } | |
| var childIndex = childStartIndex + immediateParent.m_ChildCount; | |
| ShiftChildIndicesInHierarchyOneUp(m_Device, childIndex, immediateParent); | |
| ArrayHelpers.InsertAt(ref m_Device.m_ChildrenForEachControl, childIndex, null); | |
| ++immediateParent.m_ChildCount; | |
| // Insert the child. | |
| // NOTE: This may *add several* controls depending on the layout of the control we are inserting. | |
| // The children will be appended to the child array. | |
| var control = AddChildControl(layout, variant, immediateParent, | |
| ref haveChildrenUsingStateFromOtherControls, controlItem, childIndex, controlName); | |
| return control; | |
| } | |
| private static void ApplyUseStateFrom(InputControl parent, ref InputControlLayout.ControlItem controlItem, InputControlLayout layout) | |
| { | |
| var child = InputControlPath.TryFindChild(parent, controlItem.name); | |
| Debug.Assert(child != null, "Could not find child control which should be present at this point"); | |
| // Find the referenced control. | |
| var referencedControl = InputControlPath.TryFindChild(parent, controlItem.useStateFrom); | |
| if (referencedControl == null) | |
| throw new InvalidOperationException( | |
| $"Cannot find control '{controlItem.useStateFrom}' referenced in 'useStateFrom' of control '{controlItem.name}' in layout '{layout.name}'"); | |
| // Copy its state settings. | |
| child.m_StateBlock = referencedControl.m_StateBlock; | |
| child.usesStateFromOtherControl = true; | |
| child.dontReset = referencedControl.dontReset; | |
| // At this point, all byteOffsets are relative to parents so we need to | |
| // walk up the referenced control's parent chain and add offsets until | |
| // we are at the same level that we are at. | |
| if (child.parent != referencedControl.parent) | |
| for (var parentInChain = referencedControl.parent; parentInChain != parent; parentInChain = parentInChain.parent) | |
| child.m_StateBlock.byteOffset += parentInChain.m_StateBlock.byteOffset; | |
| } | |
| private static void ShiftChildIndicesInHierarchyOneUp(InputDevice device, int startIndex, InputControl exceptControl) | |
| { | |
| var controls = device.m_ChildrenForEachControl; | |
| var count = controls.Length; | |
| for (var i = 0; i < count; ++i) | |
| { | |
| var control = controls[i]; | |
| if (control != null && control != exceptControl && control.m_ChildStartIndex >= startIndex) | |
| ++control.m_ChildStartIndex; | |
| } | |
| } | |
| // NOTE: We can only do this once we've initialized the names on the parent control. I.e. it has to be | |
| // done in the second pass we do over the control hierarchy. | |
| private void SetDisplayName(InputControl control, string longDisplayNameFromLayout, string shortDisplayNameFromLayout, bool shortName) | |
| { | |
| var displayNameFromLayout = shortName ? shortDisplayNameFromLayout : longDisplayNameFromLayout; | |
| // Display name may not be set in layout. | |
| if (string.IsNullOrEmpty(displayNameFromLayout)) | |
| { | |
| // For short names, we leave it unassigned if there's nothing in the layout | |
| // except if it's a nested control where the parent has a short name. | |
| if (shortName) | |
| { | |
| if (control.parent != null && control.parent != control.device) | |
| { | |
| if (m_StringBuilder == null) | |
| m_StringBuilder = new StringBuilder(); | |
| m_StringBuilder.Length = 0; | |
| AddParentDisplayNameRecursive(control.parent, m_StringBuilder, true); | |
| if (m_StringBuilder.Length == 0) | |
| { | |
| control.m_ShortDisplayNameFromLayout = null; | |
| return; | |
| } | |
| if (!string.IsNullOrEmpty(longDisplayNameFromLayout)) | |
| m_StringBuilder.Append(longDisplayNameFromLayout); | |
| else | |
| m_StringBuilder.Append(control.name); | |
| control.m_ShortDisplayNameFromLayout = m_StringBuilder.ToString(); | |
| return; | |
| } | |
| control.m_ShortDisplayNameFromLayout = null; | |
| return; | |
| } | |
| ////REVIEW: automatically uppercase or prettify this? | |
| // For long names, we default to the control's name. | |
| displayNameFromLayout = control.name; | |
| } | |
| // If it's a nested control, synthesize a path that includes parents. | |
| if (control.parent != null && control.parent != control.device) | |
| { | |
| if (m_StringBuilder == null) | |
| m_StringBuilder = new StringBuilder(); | |
| m_StringBuilder.Length = 0; | |
| AddParentDisplayNameRecursive(control.parent, m_StringBuilder, shortName); | |
| m_StringBuilder.Append(displayNameFromLayout); | |
| displayNameFromLayout = m_StringBuilder.ToString(); | |
| } | |
| // Assign. | |
| if (shortName) | |
| control.m_ShortDisplayNameFromLayout = displayNameFromLayout; | |
| else | |
| control.m_DisplayNameFromLayout = displayNameFromLayout; | |
| } | |
| private static void AddParentDisplayNameRecursive(InputControl control, StringBuilder stringBuilder, | |
| bool shortName) | |
| { | |
| if (control.parent != null && control.parent != control.device) | |
| AddParentDisplayNameRecursive(control.parent, stringBuilder, shortName); | |
| if (shortName) | |
| { | |
| var text = control.shortDisplayName; | |
| if (string.IsNullOrEmpty(text)) | |
| text = control.displayName; | |
| stringBuilder.Append(text); | |
| } | |
| else | |
| { | |
| stringBuilder.Append(control.displayName); | |
| } | |
| stringBuilder.Append(' '); | |
| } | |
| private static void AddProcessors(InputControl control, ref InputControlLayout.ControlItem controlItem, string layoutName) | |
| { | |
| var processorCount = controlItem.processors.Count; | |
| for (var n = 0; n < processorCount; ++n) | |
| { | |
| var name = controlItem.processors[n].name; | |
| var type = InputProcessor.s_Processors.LookupTypeRegistration(name); | |
| if (type == null) | |
| throw new InvalidOperationException( | |
| $"Cannot find processor '{name}' referenced by control '{controlItem.name}' in layout '{layoutName}'"); | |
| var processor = Activator.CreateInstance(type); | |
| var parameters = controlItem.processors[n].parameters; | |
| if (parameters.Count > 0) | |
| NamedValue.ApplyAllToObject(processor, parameters); | |
| control.AddProcessor(processor); | |
| } | |
| } | |
| private static void SetFormat(InputControl control, InputControlLayout.ControlItem controlItem) | |
| { | |
| control.m_StateBlock.format = controlItem.format; | |
| if (controlItem.sizeInBits == 0) | |
| { | |
| var primitiveFormatSize = InputStateBlock.GetSizeOfPrimitiveFormatInBits(controlItem.format); | |
| if (primitiveFormatSize != -1) | |
| control.m_StateBlock.sizeInBits = (uint)primitiveFormatSize; | |
| } | |
| } | |
| private static InputControlLayout FindOrLoadLayout(string name) | |
| { | |
| Debug.Assert(InputControlLayout.s_CacheInstanceRef > 0, "Should have acquired layout cache reference"); | |
| return InputControlLayout.cache.FindOrLoadLayout(name); | |
| } | |
| private static void ComputeStateLayout(InputControl control) | |
| { | |
| var children = control.children; | |
| // If the control has a format but no size specified and the format is a | |
| // primitive format, just set the size automatically. | |
| if (control.m_StateBlock.sizeInBits == 0 && control.m_StateBlock.format != 0) | |
| { | |
| var sizeInBits = InputStateBlock.GetSizeOfPrimitiveFormatInBits(control.m_StateBlock.format); | |
| if (sizeInBits != -1) | |
| control.m_StateBlock.sizeInBits = (uint)sizeInBits; | |
| } | |
| // If state size is not set, it means it's computed from the size of the | |
| // children so make sure we actually have children. | |
| if (control.m_StateBlock.sizeInBits == 0 && children.Count == 0) | |
| { | |
| throw new InvalidOperationException( | |
| $"Control '{control.path}' with layout '{control.layout}' has no size set and has no children to compute size from"); | |
| } | |
| // If there's no children, our job is done. | |
| if (children.Count == 0) | |
| return; | |
| // First deal with children that want fixed offsets. All the other ones | |
| // will get appended to the end. | |
| var firstUnfixedByteOffset = 0u; | |
| foreach (var child in children) | |
| { | |
| Debug.Assert(child.m_StateBlock.sizeInBits != 0, "Size of state block not set on child"); | |
| // Skip children using state from other controls. | |
| if (child.m_StateBlock.sizeInBits == kSizeForControlUsingStateFromOtherControl) | |
| continue; | |
| // Make sure the child has a valid size set on it. | |
| var childSizeInBits = child.m_StateBlock.sizeInBits; | |
| if (childSizeInBits == 0 || childSizeInBits == InputStateBlock.InvalidOffset) | |
| throw new InvalidOperationException( | |
| $"Child '{child.name}' of '{control.name}' has no size set!"); | |
| // Skip children that don't have fixed offsets. | |
| if (child.m_StateBlock.byteOffset == InputStateBlock.InvalidOffset || | |
| child.m_StateBlock.byteOffset == InputStateBlock.AutomaticOffset) | |
| continue; | |
| // At this point, if the child has no valid bit offset, put it at #0 now. | |
| if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset) | |
| child.m_StateBlock.bitOffset = 0; | |
| // See if the control bumps our fixed layout size. | |
| var endOffset = | |
| MemoryHelpers.ComputeFollowingByteOffset(child.m_StateBlock.byteOffset, child.m_StateBlock.bitOffset + childSizeInBits); | |
| if (endOffset > firstUnfixedByteOffset) | |
| firstUnfixedByteOffset = endOffset; | |
| } | |
| ////TODO: this doesn't support mixed automatic and fixed layouting *within* bitfields; | |
| //// I think it's okay not to support that but we should at least detect it | |
| // Now assign an offset to every control that wants an | |
| // automatic offset. For bitfields, we need to delay advancing byte | |
| // offsets until we've seen all bits in the fields. | |
| // NOTE: Bit addressing controls using automatic offsets *must* be consecutive. | |
| var runningByteOffset = firstUnfixedByteOffset; | |
| InputControl firstBitAddressingChild = null; | |
| var bitfieldSizeInBits = 0u; | |
| foreach (var child in children) | |
| { | |
| // Skip children with fixed offsets. | |
| if (child.m_StateBlock.byteOffset != InputStateBlock.InvalidOffset && | |
| child.m_StateBlock.byteOffset != InputStateBlock.AutomaticOffset) | |
| continue; | |
| // Skip children using state from other controls. | |
| if (child.m_StateBlock.sizeInBits == kSizeForControlUsingStateFromOtherControl) | |
| continue; | |
| // See if it's a bit addressing control. | |
| var isBitAddressingChild = (child.m_StateBlock.sizeInBits % 8) != 0; | |
| if (isBitAddressingChild) | |
| { | |
| // Remember start of bitfield group. | |
| if (firstBitAddressingChild == null) | |
| firstBitAddressingChild = child; | |
| // Keep a running count of the size of the bitfield. | |
| if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset || | |
| child.m_StateBlock.bitOffset == InputStateBlock.AutomaticOffset) | |
| { | |
| // Put child at current bit offset. | |
| child.m_StateBlock.bitOffset = bitfieldSizeInBits; | |
| bitfieldSizeInBits += child.m_StateBlock.sizeInBits; | |
| } | |
| else | |
| { | |
| // Child already has bit offset. Keep it but make sure we're accounting for it | |
| // in the bitfield size. | |
| var lastBit = child.m_StateBlock.bitOffset + child.m_StateBlock.sizeInBits; | |
| if (lastBit > bitfieldSizeInBits) | |
| bitfieldSizeInBits = lastBit; | |
| } | |
| } | |
| else | |
| { | |
| // Terminate bitfield group (if there was one). | |
| if (firstBitAddressingChild != null) | |
| { | |
| runningByteOffset = MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, bitfieldSizeInBits); | |
| firstBitAddressingChild = null; | |
| } | |
| if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset) | |
| child.m_StateBlock.bitOffset = 0; | |
| // Conform to memory addressing constraints of CPU architecture. If we don't do | |
| // this, ARMs will end up choking on misaligned memory accesses. | |
| runningByteOffset = MemoryHelpers.AlignNatural(runningByteOffset, child.m_StateBlock.alignedSizeInBytes); | |
| } | |
| ////FIXME: seems like this should take bitOffset into account | |
| child.m_StateBlock.byteOffset = runningByteOffset; | |
| if (!isBitAddressingChild) | |
| runningByteOffset = | |
| MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, child.m_StateBlock.sizeInBits); | |
| } | |
| // Compute total size. | |
| // If we ended on a bitfield, account for its size. | |
| if (firstBitAddressingChild != null) | |
| runningByteOffset = MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, bitfieldSizeInBits); | |
| var totalSizeInBytes = runningByteOffset; | |
| // Set size. We force all parents to the combined size of their children. | |
| control.m_StateBlock.sizeInBits = totalSizeInBytes * 8; | |
| } | |
| private void FinalizeControlHierarchy() | |
| { | |
| if (m_StateOffsetToControlMap == null) | |
| m_StateOffsetToControlMap = new List<uint>(); | |
| if (m_Device.allControls.Count > (1U << InputDevice.kControlIndexBits)) | |
| throw new NotSupportedException($"Device '{m_Device}' exceeds maximum supported control count of {1U << InputDevice.kControlIndexBits} (has {m_Device.allControls.Count} controls)"); | |
| var rootNode = new InputDevice.ControlBitRangeNode((ushort)(m_Device.m_StateBlock.sizeInBits - 1)); | |
| m_Device.m_ControlTreeNodes = new InputDevice.ControlBitRangeNode[1]; | |
| m_Device.m_ControlTreeNodes[0] = rootNode; | |
| var controlIndiciesNextFreeIndex = 0; | |
| // Device is not in m_ChildrenForEachControl so use index -1. | |
| FinalizeControlHierarchyRecursive(m_Device, -1, m_Device.m_ChildrenForEachControl, false, false, ref controlIndiciesNextFreeIndex); | |
| } | |
| private void FinalizeControlHierarchyRecursive(InputControl control, int controlIndex, InputControl[] allControls, bool noisy, bool dontReset, ref int controlIndiciesNextFreeIndex) | |
| { | |
| // Make sure we're staying within limits on state offsets and sizes. | |
| if (control.m_ChildCount == 0) | |
| { | |
| if (control.m_StateBlock.effectiveBitOffset >= (1U << InputDevice.kStateOffsetBits)) | |
| throw new NotSupportedException($"Control '{control}' exceeds maximum supported state bit offset of {(1U << InputDevice.kStateOffsetBits) - 1} (bit offset {control.stateBlock.effectiveBitOffset})"); | |
| if (control.m_StateBlock.sizeInBits >= (1U << InputDevice.kStateSizeBits)) | |
| throw new NotSupportedException($"Control '{control}' exceeds maximum supported state bit size of {(1U << InputDevice.kStateSizeBits) - 1} (bit offset {control.stateBlock.sizeInBits})"); | |
| } | |
| // Construct control bit range tree | |
| if (control != m_Device) | |
| InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[0], control, ref controlIndiciesNextFreeIndex, 0); | |
| // Add all leaf controls to state offset mapping. | |
| if (control.m_ChildCount == 0) | |
| m_StateOffsetToControlMap.Add( | |
| InputDevice.EncodeStateOffsetToControlMapEntry((uint)controlIndex, control.m_StateBlock.effectiveBitOffset, control.m_StateBlock.sizeInBits)); | |
| // Set final display names. This may overwrite the ones supplied by the layout so temporarily | |
| // store the values here. | |
| var displayNameFromLayout = control.m_DisplayNameFromLayout; | |
| var shortDisplayNameFromLayout = control.m_ShortDisplayNameFromLayout; | |
| SetDisplayName(control, displayNameFromLayout, shortDisplayNameFromLayout, false); | |
| SetDisplayName(control, displayNameFromLayout, shortDisplayNameFromLayout, true); | |
| if (control != control.device) | |
| { | |
| if (noisy) | |
| control.noisy = true; | |
| else | |
| noisy = control.noisy; | |
| if (dontReset) | |
| control.dontReset = true; | |
| else | |
| dontReset = control.dontReset; | |
| } | |
| // Recurse into children. Also bake our state offset into our children. | |
| var ourOffset = control.m_StateBlock.byteOffset; | |
| var childCount = control.m_ChildCount; | |
| var childStartIndex = control.m_ChildStartIndex; | |
| for (var i = 0; i < childCount; ++i) | |
| { | |
| var childIndex = childStartIndex + i; | |
| var child = allControls[childIndex]; | |
| child.m_StateBlock.byteOffset += ourOffset; | |
| FinalizeControlHierarchyRecursive(child, childIndex, allControls, noisy, dontReset, ref controlIndiciesNextFreeIndex); | |
| } | |
| control.isSetupFinished = true; | |
| } | |
| private void InsertControlBitRangeNode(ref InputDevice.ControlBitRangeNode parent, InputControl control, ref int controlIndiciesNextFreeIndex, ushort startOffset) | |
| { | |
| InputDevice.ControlBitRangeNode leftNode; | |
| InputDevice.ControlBitRangeNode rightNode; | |
| // we don't recalculate mid-points for nodes that have already been created | |
| if (parent.leftChildIndex == -1) | |
| { | |
| var midPoint = GetBestMidPoint(parent, startOffset); | |
| leftNode = new InputDevice.ControlBitRangeNode(midPoint); | |
| rightNode = new InputDevice.ControlBitRangeNode(parent.endBitOffset); | |
| AddChildren(ref parent, leftNode, rightNode); | |
| } | |
| else | |
| { | |
| leftNode = m_Device.m_ControlTreeNodes[parent.leftChildIndex]; | |
| rightNode = m_Device.m_ControlTreeNodes[parent.leftChildIndex + 1]; | |
| } | |
| // if the control starts in the left node and ends in the right, add a pointer to both nodes and return | |
| if (control.m_StateBlock.effectiveBitOffset < leftNode.endBitOffset && | |
| control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits > leftNode.endBitOffset) | |
| { | |
| AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex); | |
| AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex + 1); | |
| return; | |
| } | |
| // if it exactly fits one of the nodes, add a pointer to just that node and return | |
| if (control.m_StateBlock.effectiveBitOffset == startOffset && | |
| control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits == leftNode.endBitOffset) | |
| { | |
| AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex); | |
| return; | |
| } | |
| if (control.m_StateBlock.effectiveBitOffset == leftNode.endBitOffset && | |
| control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits == rightNode.endBitOffset) | |
| { | |
| AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex + 1); | |
| return; | |
| } | |
| // otherwise, if the node ends in the left node, recurse left | |
| if (control.m_StateBlock.effectiveBitOffset < leftNode.endBitOffset) | |
| InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[parent.leftChildIndex], control, | |
| ref controlIndiciesNextFreeIndex, startOffset); | |
| else | |
| InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[parent.leftChildIndex + 1], control, | |
| ref controlIndiciesNextFreeIndex, leftNode.endBitOffset); | |
| } | |
| private ushort GetBestMidPoint(InputDevice.ControlBitRangeNode parent, ushort startOffset) | |
| { | |
| // find the absolute mid-point, rounded up | |
| var absoluteMidPoint = (ushort)(startOffset + ((parent.endBitOffset - startOffset - 1) / 2 + 1)); | |
| var closestControlEndPointToMidPoint = ushort.MaxValue; | |
| var closestControlStartPointToMidPoint = ushort.MaxValue; | |
| // go through all controls and find the start and end offsets that are closest to the absolute mid-point | |
| foreach (var control in m_Device.m_ChildrenForEachControl) | |
| { | |
| var stateBlock = control.m_StateBlock; | |
| // don't consider controls that end before the start of the parent range, or start after | |
| // the end of the parent range | |
| if (stateBlock.effectiveBitOffset + stateBlock.sizeInBits - 1 < startOffset || | |
| stateBlock.effectiveBitOffset >= parent.endBitOffset) | |
| continue; | |
| // don't consider controls that are larger than the parent range | |
| if (stateBlock.sizeInBits > parent.endBitOffset - startOffset) | |
| continue; | |
| // don't consider controls that start or end on the same boundary as the parent | |
| if (stateBlock.effectiveBitOffset == startOffset || | |
| stateBlock.effectiveBitOffset + stateBlock.sizeInBits == parent.endBitOffset) | |
| continue; | |
| if (Math.Abs(stateBlock.effectiveBitOffset + stateBlock.sizeInBits - (int)absoluteMidPoint) < | |
| Math.Abs(closestControlEndPointToMidPoint - absoluteMidPoint) && | |
| stateBlock.effectiveBitOffset + stateBlock.sizeInBits < parent.endBitOffset) | |
| { | |
| closestControlEndPointToMidPoint = (ushort)(stateBlock.effectiveBitOffset + stateBlock.sizeInBits); | |
| } | |
| if (Math.Abs(stateBlock.effectiveBitOffset - (int)absoluteMidPoint) < | |
| Math.Abs(closestControlStartPointToMidPoint - absoluteMidPoint) && | |
| stateBlock.effectiveBitOffset >= startOffset) | |
| { | |
| closestControlStartPointToMidPoint = (ushort)stateBlock.effectiveBitOffset; | |
| } | |
| } | |
| var absoluteMidPointCollisions = 0; | |
| var controlStartMidPointCollisions = 0; | |
| var controlEndMidPointCollisions = 0; | |
| // figure out which of the possible midpoints intersects the fewest controls. The one with the fewest | |
| // is the best one because it means fewer controls will be added to this node. | |
| foreach (var control in m_Device.m_ChildrenForEachControl) | |
| { | |
| if (closestControlStartPointToMidPoint != ushort.MaxValue && | |
| closestControlStartPointToMidPoint > control.m_StateBlock.effectiveBitOffset && | |
| closestControlStartPointToMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits) | |
| controlStartMidPointCollisions++; | |
| if (closestControlEndPointToMidPoint != ushort.MaxValue && | |
| closestControlEndPointToMidPoint > control.m_StateBlock.effectiveBitOffset && | |
| closestControlEndPointToMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits) | |
| controlEndMidPointCollisions++; | |
| if (absoluteMidPoint > control.m_StateBlock.effectiveBitOffset && | |
| absoluteMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits) | |
| absoluteMidPointCollisions++; | |
| } | |
| if (closestControlEndPointToMidPoint != ushort.MaxValue && | |
| controlEndMidPointCollisions <= controlStartMidPointCollisions && | |
| controlEndMidPointCollisions <= absoluteMidPointCollisions) | |
| { | |
| Debug.Assert(closestControlEndPointToMidPoint >= startOffset && closestControlEndPointToMidPoint <= startOffset + parent.endBitOffset); | |
| return closestControlEndPointToMidPoint; | |
| } | |
| if (closestControlStartPointToMidPoint != ushort.MaxValue && | |
| controlStartMidPointCollisions <= controlEndMidPointCollisions && | |
| controlStartMidPointCollisions <= absoluteMidPointCollisions) | |
| { | |
| Debug.Assert(closestControlStartPointToMidPoint >= startOffset && closestControlStartPointToMidPoint <= startOffset + parent.endBitOffset); | |
| return closestControlStartPointToMidPoint; | |
| } | |
| Debug.Assert(absoluteMidPoint >= startOffset && absoluteMidPoint <= startOffset + parent.endBitOffset); | |
| return absoluteMidPoint; | |
| } | |
| private void AddControlToNode(InputControl control, ref int controlIndiciesNextFreeIndex, int nodeIndex) | |
| { | |
| Debug.Assert(m_Device.m_ControlTreeNodes[nodeIndex].controlCount < 255, | |
| "Control bit range nodes can address maximum of 255 controls."); | |
| ref var node = ref m_Device.m_ControlTreeNodes[nodeIndex]; | |
| var leafControlStartIndex = node.controlStartIndex; | |
| if (node.controlCount == 0) | |
| { | |
| node.controlStartIndex = (ushort)controlIndiciesNextFreeIndex; | |
| leafControlStartIndex = node.controlStartIndex; | |
| } | |
| ArrayHelpers.InsertAt(ref m_Device.m_ControlTreeIndices, | |
| node.controlStartIndex + node.controlCount, | |
| GetControlIndex(control)); | |
| ++node.controlCount; | |
| ++controlIndiciesNextFreeIndex; | |
| // bump up all the start indicies for nodes that have a start index larger than the one we just inserted into | |
| for (var i = 0; i < m_Device.m_ControlTreeNodes.Length; i++) | |
| { | |
| if (m_Device.m_ControlTreeNodes[i].controlCount == 0 || | |
| m_Device.m_ControlTreeNodes[i].controlStartIndex <= leafControlStartIndex) | |
| continue; | |
| ++m_Device.m_ControlTreeNodes[i].controlStartIndex; | |
| } | |
| } | |
| private void AddChildren(ref InputDevice.ControlBitRangeNode parent, InputDevice.ControlBitRangeNode left, InputDevice.ControlBitRangeNode right) | |
| { | |
| // if this node has a child start index, its already in the tree | |
| if (parent.leftChildIndex != -1) | |
| return; | |
| var startIndex = m_Device.m_ControlTreeNodes.Length; | |
| parent.leftChildIndex = (short)startIndex; | |
| Array.Resize(ref m_Device.m_ControlTreeNodes, startIndex + 2); | |
| m_Device.m_ControlTreeNodes[startIndex] = left; | |
| m_Device.m_ControlTreeNodes[startIndex + 1] = right; | |
| } | |
| private ushort GetControlIndex(InputControl control) | |
| { | |
| for (var i = 0; i < m_Device.m_ChildrenForEachControl.Length; i++) | |
| { | |
| if (control == m_Device.m_ChildrenForEachControl[i]) | |
| return (ushort)i; | |
| } | |
| throw new InvalidOperationException($"InputDeviceBuilder error. Couldn't find control {control}."); | |
| } | |
| private static InputDeviceBuilder s_Instance; | |
| private static int s_InstanceRef; | |
| internal static ref InputDeviceBuilder instance | |
| { | |
| get | |
| { | |
| Debug.Assert(s_InstanceRef > 0, "Must hold an instance reference"); | |
| return ref s_Instance; | |
| } | |
| } | |
| internal static RefInstance Ref() | |
| { | |
| Debug.Assert(s_Instance.m_Device == null, | |
| "InputDeviceBuilder is already in use! Cannot use the builder recursively"); | |
| ++s_InstanceRef; | |
| return new RefInstance(); | |
| } | |
| // Helper that allows setting up an InputDeviceBuilder such that it will either be created | |
| // locally and temporarily or, if one already exists globally, reused. | |
| internal struct RefInstance : IDisposable | |
| { | |
| public void Dispose() | |
| { | |
| --s_InstanceRef; | |
| if (s_InstanceRef <= 0) | |
| { | |
| s_Instance.Dispose(); | |
| s_Instance = default; | |
| s_InstanceRef = 0; | |
| } | |
| else | |
| // Make sure we reset when there is an exception. | |
| s_Instance.Reset(); | |
| } | |
| } | |
| } | |
| } | |