Skip to content

Commit d3f7053

Browse files
FIX: USE_OPTIMIZED_CONTROLS feature flag reduces performance in built players (#1906)
* Check optimization valid state in Editor only * Update documentation * Update documentation with performance impact on PlayMode * Add READ_VALUE_CACHING performance tests These tests add a use case of performance cost and improvement when state changes are done to composite controls vs float controls * Update documentation for control value caching Updated this docs to be more clear about the optimization impact of caching control values. * Redo added READ_VALUE_CACHING performance tests Previous tests were not very accurate as the performance only shows if there are actions with bindings in the controls we are reading from. * Rephrase documentation changes
1 parent 7f4ceee commit d3f7053

File tree

4 files changed

+125
-11
lines changed

4 files changed

+125
-11
lines changed

Assets/Tests/InputSystem/CorePerformanceTests.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,89 @@ void CallUpdate()
766766
}
767767
}
768768

769+
[Test, Performance]
770+
[Category("Performance")]
771+
[TestCase(OptimizationTestType.NoOptimization)]
772+
[TestCase(OptimizationTestType.ReadValueCaching)]
773+
// These tests show the performance of the ReadValueCaching optimization when there are state changes per frame on
774+
// gamepad controls and there are composite actions that read from controls.
775+
// Currently, there is a positive performance impact by using ReadValueCaching when reading from controls which have
776+
// composite bindings.
777+
public void Performance_OptimizedControls_EvaluateStaleControlReadsWhenGamepadStateChanges(OptimizationTestType testType)
778+
{
779+
SetInternalFeatureFlagsFromTestType(testType);
780+
781+
var gamepad = InputSystem.AddDevice<Gamepad>();
782+
783+
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
784+
// Disable the project wide actions actions to avoid performance impact.
785+
InputSystem.actions.Disable();
786+
#endif
787+
788+
Measure.Method(() =>
789+
{
790+
MethodToMeasure(gamepad);
791+
}).SampleGroup("ReadValueCaching Expected With WORSE Performance")
792+
.MeasurementCount(100)
793+
.WarmupCount(5)
794+
.Run();
795+
796+
// Create composite actions to show the performance improvement when using ReadValueCaching.
797+
798+
var leftStickCompositeAction = new InputAction("LeftStickComposite", InputActionType.Value);
799+
leftStickCompositeAction.AddCompositeBinding("2DVector")
800+
.With("Up", "<Gamepad>/leftStick/up")
801+
.With("Down", "<Gamepad>/leftStick/down")
802+
.With("Left", "<Gamepad>/leftStick/left")
803+
.With("Right", "<Gamepad>/leftStick/right");
804+
805+
806+
var rightStickCompositeAction = new InputAction("RightStickComposite", InputActionType.Value);
807+
rightStickCompositeAction.AddCompositeBinding("2DVector")
808+
.With("Up", "<Gamepad>/rightStick/up")
809+
.With("Down", "<Gamepad>/rightStick/down")
810+
.With("Left", "<Gamepad>/rightStick/left")
811+
.With("Right", "<Gamepad>/rightStick/right");
812+
813+
leftStickCompositeAction.Enable();
814+
rightStickCompositeAction.Enable();
815+
816+
Measure.Method(() =>
817+
{
818+
MethodToMeasure(gamepad);
819+
}).SampleGroup("ReadValueCaching Expected With BETTER Performance")
820+
.MeasurementCount(100)
821+
.WarmupCount(5)
822+
.Run();
823+
824+
825+
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
826+
// Re-enable the project wide actions actions.
827+
InputSystem.actions.Enable();
828+
#endif
829+
return;
830+
831+
void MethodToMeasure(Gamepad gamepad)
832+
{
833+
var value2d = Vector2.zero;
834+
835+
for (var i = 0; i < 1000; ++i)
836+
{
837+
// Make sure state changes are different from previous state so that we mark the controls as
838+
// stale.
839+
InputSystem.QueueStateEvent(gamepad,
840+
new GamepadState
841+
{
842+
leftStick = new Vector2(i / 1000f, i / 1000f),
843+
rightStick = new Vector2(i / 1000f, i / 1200f)
844+
});
845+
InputSystem.Update();
846+
847+
value2d = gamepad.leftStick.value;
848+
}
849+
}
850+
}
851+
769852
#if ENABLE_VR
770853
[Test, Performance]
771854
[Category("Performance")]

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,18 @@ Use [`InputControl<T>.value`](../api/UnityEngine.InputSystem.InputControl-1.html
243243

244244
### Control Value Caching
245245

246-
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.
246+
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. Subsequent calls to [`InputControl<T>.ReadValue`](../api/UnityEngine.InputSystem.InputControl-1.html#UnityEngine_InputSystem_InputControl_1_ReadValue) will only apply control processing when there have been changes to that control or in case of control processing. 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).
247+
> Note: Performance improvements **are currently not guaranteed** for all use cases. Even though this performance path marks controls as "stale" in an efficient way, it still has an overhead which can degrade performance in some cases.
247248
248-
This feature is not enabled by default as it can result in the following minor behavioural changes:
249+
A positive performance impact has been seen when:
250+
- Reading from controls that do not change frequently.
251+
- In case the controls change every frame, are being read and have actions bound to them as well, e.g. on a Gamepad, reading `leftStick`, `leftStick.x` and `leftStick.left` for example when there's a action with composite bindings setup.
252+
253+
On the other hand, it is likely to have a negative performance impact when:
254+
- No control reads are performed for a control, and there are a lot of changes for that particular control.
255+
- Reading from controls that change frequently that have no actions bound to those controls.
256+
257+
Moreover, this feature is not enabled by default as it can result in the following minor behavioural changes:
249258
* 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.
250259
* 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.
251260
* 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.
@@ -256,10 +265,21 @@ If there are any non-obvious inconsistencies, 'PARANOID_READ_VALUE_CACHING_CHECK
256265

257266
### Optimized control read value
258267

259-
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.
268+
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. This is very specific optimization and should be used with caution.
269+
270+
> __Please note__: This optimization has a performance impact on `PlayMode` as we do extra checks to ensure that the controls have the correct memory representation during development. Don't be alarmed if you see a performance drop in `PlayMode` when using this optimization as it's expected at this stage.
271+
272+
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.
273+
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.
274+
If a control matches a common representation we can bypass reading its children control and cast the memory directly to the common representation. For example if [`Vector2Control`](../api/UnityEngine.InputSystem.Controls.Vector2Control.html) is two consecutive floats in memory we can bypass reading `x` and `y` separately and just cast the state memory to `Vector2`.
260275

261-
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.
276+
> __Please note__: This optimization only works if the controls don't need any processing applied to them, such as `invert`, `clamp`, `normalize`, `scale` or any other processor. If any of these are applied to the control, **there won't be any optimization applied** and the control will be read as usual.
262277
263-
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.
278+
Also, [`InputControl.ApplyParameterChanges()`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_ApplyParameterChanges) **must be explicitly called** in specific changes to ensure [`InputControl.optimizedControlDataType`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_optimizedControlDataType) is updated to the correct memory representation. Make sure to call it when:
279+
* Configuration changes after [`InputControl.FinishSetup()`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_FinishSetup_) is called.
280+
* Changing parameters such [`AxisControl.invert`](../api/UnityEngine.InputSystem.Controls.AxisControl.html#UnityEngine_InputSystem_Controls_AxisControl_invert), [`AxisControl.clamp`](../api/UnityEngine.InputSystem.Controls.AxisControl.html#UnityEngine_InputSystem_Controls_AxisControl_clamp), [`AxisControl.normalize`](../api/UnityEngine.InputSystem.Controls.AxisControl.html#UnityEngine_InputSystem_Controls_AxisControl_normalize), [`AxisControl.scale`](../api/UnityEngine.InputSystem.Controls.AxisControl.html#UnityEngine_InputSystem_Controls_AxisControl_scale) or changing processors. The memory representation needs to be recalculated after these changes so that we know that the control is not optimized anymore. Otherwise, the control will be read with wrong values.
264281

265-
[`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.
282+
The optimized controls work as follows:
283+
* A potential memory representation is set using [`InputControl.CalculateOptimizedControlDataType()`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_CalculateOptimizedControlDataType)
284+
* Its memory representation is stored in [`InputControl.optimizedControlDataType`](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_optimizedControlDataType)
285+
* Finally, [`ReadUnprocessedValueFromState`](../api/UnityEngine.InputSystem.InputControl-1.html#UnityEngine_InputSystem_InputControl_1_ReadUnprocessedValueFromState_) uses the optimized memory representation to decide if it should cast to memory directly instead of reading every children control on it's own to reconstruct the controls state.

Packages/com.unity.inputsystem/InputSystem/Controls/InputControl.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -952,8 +952,9 @@ internal void SetOptimizedControlDataTypeRecursively()
952952
// This is mainly to AxisControl fields being public and capable of changing at any time even if we were not anticipated such a usage pattern.
953953
// Also it's not clear if InputControl.stateBlock.format can potentially change at any time, likely not.
954954
[MethodImpl(MethodImplOptions.AggressiveInlining)]
955-
// Only do this check in development builds and editor in hope that it will be sufficient to catch any misuse during development.
956-
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
955+
// Only do this check in and editor in hope that it will be sufficient to catch any misuse during development.
956+
// It is not done in debug builds because it has a performance cost and it will show up when profiled.
957+
[Conditional("UNITY_EDITOR")]
957958
internal void EnsureOptimizationTypeHasNotChanged()
958959
{
959960
if (!InputSettings.optimizedControlsFeatureEnabled)
@@ -969,7 +970,7 @@ internal void EnsureOptimizationTypeHasNotChanged()
969970
"after the changes to the control to fix this error.");
970971

971972
// Automatically fix the issue
972-
// Note this function is only executed in editor and development builds
973+
// Note this function is only executed in the editor
973974
m_OptimizedControlDataType = currentOptimizedControlDataType;
974975
}
975976

Packages/com.unity.inputsystem/InputSystem/InputManager.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2972,8 +2972,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev
29722972
InputUpdate.OnUpdate(updateType);
29732973

29742974
// Ensure optimized controls are in valid state
2975-
foreach (var device in devices)
2976-
device.EnsureOptimizationTypeHasNotChanged();
2975+
CheckAllDevicesOptimizedControlsHaveValidState();
29772976

29782977
var shouldProcessActionTimeouts = updateType.IsPlayerUpdate() && gameIsPlaying;
29792978

@@ -3494,6 +3493,17 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev
34943493
m_CurrentUpdate = default;
34953494
}
34963495

3496+
// Only do this check in editor in hope that it will be sufficient to catch any misuse during development.
3497+
[Conditional("UNITY_EDITOR")]
3498+
void CheckAllDevicesOptimizedControlsHaveValidState()
3499+
{
3500+
if (!InputSettings.optimizedControlsFeatureEnabled)
3501+
return;
3502+
3503+
foreach (var device in devices)
3504+
device.EnsureOptimizationTypeHasNotChanged();
3505+
}
3506+
34973507
private void InvokeAfterUpdateCallback(InputUpdateType updateType)
34983508
{
34993509
// don't invoke the after update callback if this is an editor update and the game is playing. We

0 commit comments

Comments
 (0)