Skip to content

Commit d6c4f36

Browse files
authored
NEW: Improve performance when reading control values (#1569)
1 parent d3afc62 commit d6c4f36

35 files changed

+1386
-122
lines changed

Assets/Tests/InputSystem/APIVerificationTests.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ private static bool IsTypeWhichCanHavePublicFields(TypeReference type)
8181
resolved.Interfaces.Any(i => i.InterfaceType.FullName == typeof(IInputEventTypeInfo).FullName) ||
8282

8383
// serializable types may depend on the field names to match serialized data (eg. Json)
84-
resolved.Attributes.HasFlag(TypeAttributes.Serializable)
84+
resolved.Attributes.HasFlag(TypeAttributes.Serializable) ||
85+
86+
// These types need to use fields because they are returned as ref readonly from InputAction.value and we
87+
// want to avoid defensive copies being created for every property access. Also, we can't use the types
88+
// Bone and Eyes here because they don't exist on some platforms
89+
resolved.Name == "Bone" || resolved.Name == "Eyes"
8590
)
8691
return true;
8792

@@ -517,22 +522,27 @@ private static string FilterIgnoredChanges(string line)
517522
}
518523
}
519524

525+
/// <summary>
526+
/// Use a scoped exclusion property to exclude members of a type from API verification when the member's names are not
527+
/// unique in the entire project and you don't want to exclude the unrelated members. This type will scope the exlusion
528+
/// to just a particular namespace and type.
529+
/// </summary>
520530
internal readonly struct ScopedExclusion
521531
{
522-
public ScopedExclusion(string version, string ns, string type, string method)
532+
public ScopedExclusion(string version, string ns, string type, params string[] members)
523533
{
524534
Version = version;
525535
Namespace = ns;
526536
Type = type;
527-
Method = method;
537+
Members = members;
528538
}
529539

530540
public string Version { get; }
531541
public string Namespace { get; }
532542
public string Type { get; }
533-
public string Method { get; }
543+
public string[] Members { get; }
534544

535-
public bool IsMatch(List<string> scopeStack, string method)
545+
public bool IsMatch(List<string> scopeStack, string member)
536546
{
537547
var namespaceScope = string.Empty;
538548
var typeScope = string.Empty;
@@ -545,7 +555,7 @@ public bool IsMatch(List<string> scopeStack, string method)
545555
typeScope = scopeStack[i].Trim();
546556
}
547557

548-
return namespaceScope == Namespace && typeScope == Type && method.Trim() == Method;
558+
return namespaceScope == Namespace && typeScope == Type && Members.Contains(member.Trim());
549559
}
550560
}
551561

@@ -554,7 +564,7 @@ public class ScopedExclusionPropertyAttribute : PropertyAttribute
554564
{
555565
public const string ScopedExclusions = "ScopedExclusions";
556566

557-
public ScopedExclusionPropertyAttribute(string version, string ns, string type, string method)
567+
public ScopedExclusionPropertyAttribute(string version, string ns, string type, params string[] method)
558568
{
559569
Properties.Add(ScopedExclusions, new ScopedExclusion(version, ns, type, method));
560570
}

Assets/Tests/InputSystem/CorePerformanceTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,8 @@ public void Performance_OptimizedControls_ReadingMousePosition100kTimes(Optimize
560560
{
561561
var useOptimizedControls = testSetup == OptimizedControlsTest.OptimizedControls;
562562
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, useOptimizedControls);
563+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, useOptimizedControls);
564+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, false);
563565

564566
var mouse = InputSystem.AddDevice<Mouse>();
565567
Assert.That(mouse.position.x.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatFloat : InputStateBlock.FormatInvalid));
@@ -586,6 +588,8 @@ public void Performance_OptimizedControls_ReadingPose4kTimes(OptimizedControlsTe
586588
{
587589
var useOptimizedControls = testSetup == OptimizedControlsTest.OptimizedControls;
588590
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, useOptimizedControls);
591+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, useOptimizedControls);
592+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, false);
589593

590594
runtime.ReportNewInputDevice(XRTests.PoseDeviceState.CreateDeviceDescription().ToJson());
591595

Assets/Tests/InputSystem/CoreTests_Controls.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,93 @@ public void Controls_CanCheckWhetherControlIsAtDefaultValue_IgnoringNoise()
436436
}
437437
}
438438

439+
[Test]
440+
[Category("Controls")]
441+
public unsafe void Controls_ValueIsReadFromStateMemoryOnlyWhenControlHasBeenMarkedAsStale()
442+
{
443+
// disable paranoid checks because this test is consciously writing to state memory directly
444+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, false);
445+
446+
var gamepad = InputSystem.AddDevice<Gamepad>();
447+
448+
// read the value once initially so it gets cached
449+
var value = gamepad.leftTrigger.value;
450+
451+
// Note that we have to write a different value here (0.5) to the one we write below, otherwise the
452+
// state comparison during the state update won't see any difference between the values and won't
453+
// mark the control as stale
454+
gamepad.leftTrigger.WriteValueIntoState(0.5f, gamepad.currentStatePtr);
455+
456+
// because we wrote the state manually into the current state ptr, the stale flag is not set, so calling
457+
// value should return whatever was previously cached (0 by default).
458+
Assert.That(gamepad.leftTrigger.value, Is.EqualTo(0));
459+
460+
// calling ApplyParameterChanges should recursively invalidate cached values
461+
gamepad.ApplyParameterChanges();
462+
Assert.That(gamepad.leftTrigger.value, Is.EqualTo(0.5f));
463+
464+
InputSystem.QueueStateEvent(gamepad, new GamepadState {leftTrigger = 0.75f});
465+
InputSystem.Update();
466+
467+
// but this time, we updated state through the system which *does* set the stale flag on controls that
468+
// have changed.
469+
Assert.That(gamepad.leftTrigger.value, Is.EqualTo(0.75f));
470+
}
471+
472+
[Test]
473+
[Category("Controls")]
474+
public void Controls_ValueCachingWorksAcrossEntireDeviceMemoryRange()
475+
{
476+
var keyboard = InputSystem.AddDevice<Keyboard>();
477+
478+
// read all values to initially mark everything as cached
479+
foreach (var control in keyboard.allControls)
480+
{
481+
var v = ((ButtonControl)control).value;
482+
}
483+
484+
foreach (var control in keyboard.allControls)
485+
{
486+
Assert.That(control.m_CachedValueIsStale, Is.False);
487+
}
488+
489+
var keyboardState = new KeyboardState((Key[])Enum.GetValues(typeof(Key)));
490+
InputSystem.QueueStateEvent(keyboard, keyboardState);
491+
InputSystem.Update();
492+
493+
foreach (var control in keyboard.allControls)
494+
{
495+
if (control == keyboard.imeSelected) // not a real key
496+
continue;
497+
498+
Assert.That(control.m_CachedValueIsStale, Is.True);
499+
}
500+
}
501+
502+
[Test]
503+
[Category("Controls")]
504+
public void Controls_ValueIsSetToDefaultStateOnInitialization()
505+
{
506+
var json = @"
507+
{
508+
""name"" : ""CustomGamepad"",
509+
""extend"" : ""Gamepad"",
510+
""controls"" : [
511+
{
512+
""name"" : ""rightTrigger"",
513+
""defaultState"" : ""0.5""
514+
}
515+
]
516+
}
517+
";
518+
519+
InputSystem.RegisterLayout(json);
520+
var gamepad = InputDevice.Build<Gamepad>("CustomGamepad");
521+
InputSystem.AddDevice(gamepad);
522+
523+
Assert.That(gamepad.rightTrigger.value, Is.EqualTo(0.5f));
524+
}
525+
439526
[Test]
440527
[Category("Controls")]
441528
public unsafe void Controls_CanWriteValueFromObjectIntoState()

Packages/com.unity.inputsystem/CHANGELOG.md

100644100755
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ however, it has to be formatted properly to pass verification tests.
1515
- Added `InputSettings.shortcutKeysConsumeInput`. This allows programmatic access to opt-in to the enhanced shortcut key behaviour ([case ISXB-254](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-254))).
1616
- Significantly optimized cost of `ReadValue`/`ReadUnprocessedValueFromState`/`WriteValueIntoState` for some control types. Optimization is opt-in for now, please call `InputSystem.settings.SetInternalFeatureFlag("USE_OPTIMIZED_CONTROLS", true);` in your project to enable it. You can observe which controls are optimized by looking at new optimized column in device debugger. You will need to call a new `InputControl.ApplyParameterChanges()` method if the code is changing `AxisControl` fields after initial setup is done.
1717
- Added the ability to change the origin positioning and movement behaviour of the OnScreenStick (`OnScreenStick.cs`) via the new `behaviour` property. This currently supports three modes of operation, two of which are new in addition to the previous behaviour. Based on the user contribution from [eblabs](https://github.com/eblabs) in [#658](https://github.com/Unity-Technologies/InputSystem/pull/658).
18+
- Significantly optimized cost of `InputAction.ReadValue` and `InputControl.ReadValue` calls by introducing caching behaviour to input controls. Input controls now keep track of whether their underlying state has been changed and only read the value from the underlying state and apply processors when absolutely necessary. Optimization is opt-in for now, please call `InputSystem.settings.SetInternalFeatureFlag("USE_READ_VALUE_CACHING", true);` in your project to enable it. If there are issues try enabling `InputSystem.settings.SetInternalFeatureFlag("PARANOID_READ_VALUE_CACHING_CHECKS", true);` and check in the console if there are any errors regarding caching.
1819

1920
### Fixed
2021
- Fixed composite bindings incorrectly getting a control scheme assigned when pasting into input asset editor with a control scheme selected.
@@ -51,6 +52,7 @@ however, it has to be formatted properly to pass verification tests.
5152
- Fixed an issue where PlayerInput behavior description was not updated when changing action assset ([case ISXB-286](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-286)).
5253

5354
### Changed
55+
- Readded OnDisable() member to MultiplayerEventSystem which was previously removed from the API
5456
- Improved performance of HID descriptor parsing by moving json parsing to a simple custom predicitve parser instead of relying on Unity's json parsing. This should improve domain reload times when there are many HID devices connected to a machine.
5557

5658
## [1.4.2] - 2022-08-12

Packages/com.unity.inputsystem/Documentation~/Controls.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* [Actuation](#control-actuation)
99
* [Noisy Controls](#noisy-controls)
1010
* [Synthetic Controls](#synthetic-controls)
11+
* [Performance Optimization](#performance-optimization)
1112

1213
An Input Control represents a source of values. These values can be of any structured or primitive type. The only requirement is that the type is [blittable](https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types).
1314

@@ -229,3 +230,33 @@ A synthetic Control is a Control that doesn't correspond to an actual physical c
229230
Whether a given Control is synthetic is indicated by its [`InputControl.synthetic`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_synthetic) property.
230231

231232
The system considers synthetic Controls for [interactive rebinding](ActionBindings.md#interactive-rebinding) but always favors non-synthetic Controls. If both a synthetic and a non-synthetic Control that are a potential match exist, the non-synthetic Control wins by default. This makes it possible to interactively bind to `<Gamepad>/leftStick/left`, for example, but also makes it possible to bind to `<Gamepad>/leftStickPress` without getting interference from the synthetic buttons on the stick.
233+
234+
## Performance Optimization
235+
236+
### Avoiding defensive copies
237+
238+
Use [`InputControl<T>.value`](../api/UnityEngine.InputSystem.InputControl-1.html#UnityEngine_InputSystem_InputControl_1_value) instead of [`InputControl<T>.ReadValue`](../api/UnityEngine.InputSystem.InputControl-1.html#UnityEngine_InputSystem_InputControl_1_ReadValue) to avoid creating a copy of the control state on every call, as the former returns the value as `ref readonly` while the latter always makes a copy. Note that this optimization only applies if the call site assigns the return value to a variable that has been declared 'ref readonly'. Otherwise a copy will be made as before. Additionally, be aware of defensive copies that can be allocated by the compiler when it is unable to determine that it can safely use the readonly reference i.e. if it can't determine that the reference won't be changed, it will create a defensive copy for you. For more details, see https://learn.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code#use-ref-readonly-return-statements.
239+
240+
241+
### Control Value Caching
242+
243+
When the 'USE_READ_VALUE_CACHING' internal feature flag is set, the Input System will switch to an optimized path for reading control values. This path efficiently marks controls as 'stale' when they have been actuated and subsequent calls to [`InputControl<T>.ReadValue`](../api/UnityEngine.InputSystem.InputControl-1.html#UnityEngine_InputSystem_InputControl_1_ReadValue) will only apply control processing when absolutely necessary. Control processing in this case can mean any hard-coded processing that might exist on the control, such as with [`AxisControl`](../api/UnityEngine.InputSystem.Controls.AxisControl.html) which has built-in inversion, normalisation, scaling etc, or any processors that have been applied to the controls' [processor stack](Processors.md#processors-on-controls). This can have a significant positive impact on performance, especially when using complex composite input actions with many composite parts, such as a movement input action that could be bound to W, A, S, and D on the keyboard, two gamepad sticks and a DPad.
244+
245+
This feature is not enabled by default as it can result in the following minor behavioural changes:
246+
* Some control processors use global state. Without cached value optimizations, it is possible to read the control value, change the global state, read the control value again, and get a new value due to the fact that the control processor runs on every call. With cached value optimizations, reading the control value will only ever return a new value if the physical control has been actuated. Changing the global state of a control processor will have no effect otherwise.
247+
* Writing to device state using low-level APIs like [`InputControl<T>.WriteValueIntoState`](../api/UnityEngine.InputSystem.InputControl-1.html#UnityEngine_InputSystem_InputControl_1_WriteValueIntoState__0_System_Void__) does not set the stale flag and subsequent calls to [`InputControl<T>.value`](../api/UnityEngine.InputSystem.InputControl-1.html#UnityEngine_InputSystem_InputControl_1_value) will not reflect those changes.
248+
* After changing properties on [`AxisControl`](../api/UnityEngine.InputSystem.Controls.AxisControl.html) the [`ApplyParameterChanges`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_ApplyParameterChanges) has to be called to invalidate cached value.
249+
250+
Processors that need to run on every read can set their respective caching policy to EvaluateOnEveryRead. That will disable caching on controls that are using such processor.
251+
252+
If there are any non-obvious inconsistencies, 'PARANOID_READ_VALUE_CACHING_CHECKS' internal feature flag can be enabled to compare cached and uncached value on every read and log an error if they don't match.
253+
254+
### Optimized control read value
255+
256+
When the 'USE_OPTIMIZED_CONTROLS' internal feature flag is set, the Input System will use faster way to use state memory for some controls instances.
257+
258+
Most controls are flexible with regards to memory representation, like [`AxisControl`](../api/UnityEngine.InputSystem.Controls.AxisControl.html) can be one bit, multiple bits, a float, etc, or in [`Vector2Control`](../api/UnityEngine.InputSystem.Controls.Vector2Control.html) where x and y can have different memory representation. Yet for most controls there are common memory representation patterns, for example [`AxisControl`](../api/UnityEngine.InputSystem.Controls.AxisControl.html) are floats or single bytes, or some [`Vector2Control`](../api/UnityEngine.InputSystem.Controls.Vector2Control.html) are two consequitive floats in memory. If a control is matching a common representation we can bypass reading children control and cast memory directly to the common representation. For example if [`Vector2Control`](../api/UnityEngine.InputSystem.Controls.Vector2Control.html) is two consequitive floats in memory we can bypass reading `x` and `y` separately and just cast whole state memory to `Vector2`, this only works if `x` and `y` don't need any processing applied to them.
259+
260+
Optimized controls compute a potential memory representation in [`InputControl.CalculateOptimizedControlDataType()`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_CalculateOptimizedControlDataType), store it [`InputControl.optimizedControlDataType`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_optimizedControlDataType) and then inside [`ReadUnprocessedValueFromState`](../api/UnityEngine.InputSystem.InputControl-1.html#UnityEngine_InputSystem_InputControl_1_ReadUnprocessedValueFromState_) used it to decide to cast memory directly instead of reading every children control on it's own to reconstruct the controls state.
261+
262+
[`InputControl.ApplyParameterChanges()`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_ApplyParameterChanges) should be called after changes to ensure [`InputControl.optimizedControlDataType`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_optimizedControlDataType) is updated to the correct value when configuration changes after [`InputControl.FinishSetup()`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_FinishSetup_) was called, like value of [`AxisControl.invert`](../api/UnityEngine.InputSystem.Controls.AxisControl.html#UnityEngine_InputSystem_Controls_AxisControl_invert) flips or other cases.

Packages/com.unity.inputsystem/InputSystem/Actions/InputActionRebindingExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2355,7 +2355,7 @@ private unsafe void OnEvent(InputEventPtr eventPtr, InputDevice device)
23552355
{
23562356
// Haven't seen this control changing actuation yet. Record its current actuation as its
23572357
// starting actuation and ignore the control if we haven't reached our actuation threshold yet.
2358-
startingMagnitude = control.EvaluateMagnitude();
2358+
startingMagnitude = control.magnitude;
23592359
m_StartingActuations.Add(control, startingMagnitude);
23602360
}
23612361

0 commit comments

Comments
 (0)