Skip to content

Commit fbb9034

Browse files
Dmytro Ivanovduckets
andauthored
NEW: Hot path optimization for ReadValue/WriteValueIntoState for most used controls (requires opt-in) (#1606)
Co-authored-by: Ben Pitt <[email protected]>
1 parent efdcd78 commit fbb9034

File tree

19 files changed

+576
-68
lines changed

19 files changed

+576
-68
lines changed

Assets/Tests/InputSystem/CorePerformanceTests.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,4 +531,66 @@ public void TODO_CanSaveAndRestoreSystemInLessThan10Milliseconds() // Currently
531531
}
532532

533533
#endif
534+
535+
internal enum OptimizedControlsTest
536+
{
537+
OptimizedControls,
538+
NormalControls
539+
}
540+
541+
[Test, Performance]
542+
[Category("Performance")]
543+
[TestCase(OptimizedControlsTest.OptimizedControls)]
544+
[TestCase(OptimizedControlsTest.NormalControls)]
545+
public void Performance_OptimizedControls_ReadingMousePosition100kTimes(OptimizedControlsTest testSetup)
546+
{
547+
var useOptimizedControls = testSetup == OptimizedControlsTest.OptimizedControls;
548+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, useOptimizedControls);
549+
550+
var mouse = InputSystem.AddDevice<Mouse>();
551+
Assert.That(mouse.position.x.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatFloat : InputStateBlock.FormatInvalid));
552+
Assert.That(mouse.position.y.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatFloat : InputStateBlock.FormatInvalid));
553+
Assert.That(mouse.position.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatVector2 : InputStateBlock.FormatInvalid));
554+
555+
Measure.Method(() =>
556+
{
557+
var pos = new Vector2();
558+
for (var i = 0; i < 100000; ++i)
559+
pos += mouse.position.ReadValue();
560+
})
561+
.MeasurementCount(100)
562+
.WarmupCount(5)
563+
.Run();
564+
}
565+
566+
#if ENABLE_VR
567+
[Test, Performance]
568+
[Category("Performance")]
569+
[TestCase(OptimizedControlsTest.OptimizedControls)]
570+
[TestCase(OptimizedControlsTest.NormalControls)]
571+
public void Performance_OptimizedControls_ReadingPose4kTimes(OptimizedControlsTest testSetup)
572+
{
573+
var useOptimizedControls = testSetup == OptimizedControlsTest.OptimizedControls;
574+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, useOptimizedControls);
575+
576+
runtime.ReportNewInputDevice(XRTests.PoseDeviceState.CreateDeviceDescription().ToJson());
577+
578+
InputSystem.Update();
579+
580+
var device = InputSystem.devices[0];
581+
582+
var poseControl = device["posecontrol"] as UnityEngine.InputSystem.XR.PoseControl;
583+
Assert.That(poseControl.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatPose : InputStateBlock.FormatInvalid));
584+
585+
Measure.Method(() =>
586+
{
587+
for (var i = 0; i < 4000; ++i)
588+
poseControl.ReadValue();
589+
})
590+
.MeasurementCount(100)
591+
.WarmupCount(5)
592+
.Run();
593+
}
594+
595+
#endif
534596
}

Assets/Tests/InputSystem/CoreTests_Controls.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,4 +1378,45 @@ public void Controls_TouchControlStateCorrespondsToTouchState()
13781378
Assert.That(UnsafeUtility.SizeOf<TouchState>(), Is.EqualTo(TouchState.kSizeInBytes));
13791379
Assert.That(touchscreen.touches[0].stateBlock.alignedSizeInBytes, Is.EqualTo(TouchState.kSizeInBytes));
13801380
}
1381+
1382+
[Test]
1383+
[Category("Controls")]
1384+
public void Controls_OptimizedControls_TrivialControlsAreOptimized()
1385+
{
1386+
var mouse = InputSystem.AddDevice<Mouse>();
1387+
1388+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, false);
1389+
Assert.That(mouse.position.x.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1390+
Assert.That(mouse.position.y.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1391+
Assert.That(mouse.position.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1392+
Assert.That(mouse.leftButton.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1393+
1394+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, true);
1395+
Assert.That(mouse.position.x.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatFloat));
1396+
Assert.That(mouse.position.y.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatFloat));
1397+
Assert.That(mouse.position.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatVector2));
1398+
Assert.That(mouse.leftButton.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1399+
1400+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, false);
1401+
Assert.That(mouse.position.x.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1402+
Assert.That(mouse.position.y.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1403+
Assert.That(mouse.position.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1404+
Assert.That(mouse.leftButton.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1405+
}
1406+
1407+
[Test]
1408+
[Category("Controls")]
1409+
public void Controls_OptimizedControls_ParentChangesOptimization_IfChildIsNoLongerOptimized()
1410+
{
1411+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, true);
1412+
1413+
var mouse = InputSystem.AddDevice<Mouse>();
1414+
1415+
mouse.position.x.invert = true;
1416+
mouse.position.x.ApplyParameterChanges();
1417+
1418+
Assert.That(mouse.position.x.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1419+
Assert.That(mouse.position.y.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatFloat));
1420+
Assert.That(mouse.position.optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatInvalid));
1421+
}
13811422
}

Assets/Tests/InputSystem/Plugins/XRTests.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ public FourCC format
10211021
}
10221022

10231023
[StructLayout(LayoutKind.Explicit)]
1024-
unsafe struct PoseDeviceState : IInputStateTypeInfo
1024+
internal unsafe struct PoseDeviceState : IInputStateTypeInfo
10251025
{
10261026
[FieldOffset(0)] public byte isTracked;
10271027
[FieldOffset(4)] public uint trackingState;
@@ -1123,5 +1123,20 @@ public void Controls_XRAxisControls_AreClampedToOneMagnitude()
11231123
Assert.That((device["Vector2/x"] as AxisControl).EvaluateMagnitude(), Is.EqualTo(1f).Within(0.0001f));
11241124
Assert.That((device["Vector2/y"] as AxisControl).EvaluateMagnitude(), Is.EqualTo(1f).Within(0.0001f));
11251125
}
1126+
1127+
[Test]
1128+
[Category("Controls")]
1129+
public void Controls_OptimizedControls_PoseControl_IsOptimized()
1130+
{
1131+
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, true);
1132+
1133+
runtime.ReportNewInputDevice(PoseDeviceState.CreateDeviceDescription().ToJson());
1134+
1135+
InputSystem.Update();
1136+
1137+
var device = InputSystem.devices[0];
1138+
1139+
Assert.That((device["posecontrol"] as PoseControl).optimizedControlDataType, Is.EqualTo(InputStateBlock.FormatPose));
1140+
}
11261141
}
11271142
#endif

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ however, it has to be formatted properly to pass verification tests.
1313
### Added
1414
- Added support for reading Tracking State in [TrackedPoseDriver](xref:UnityEngine.InputSystem.XR.TrackedPoseDriver) to constrain whether the input pose is applied to the Transform. This should be used when the device supports valid flags for the position and rotation values, which is the case for XR poses.
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))).
16+
- 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.
1617

1718
### Fixed
1819
- Fixed composite bindings incorrectly getting a control scheme assigned when pasting into input asset editor with a control scheme selected.

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

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
using System.Runtime.CompilerServices;
12
using UnityEngine.InputSystem.LowLevel;
23
using UnityEngine.InputSystem.Processors;
4+
using UnityEngine.InputSystem.Utilities;
35

46
////REVIEW: change 'clampToConstant' to simply 'clampToMin'?
57

8+
////TODO: if AxisControl fields where properties, we wouldn't need ApplyParameterChanges, maybe it's ok breaking change?
9+
610
namespace UnityEngine.InputSystem.Controls
711
{
812
/// <summary>
@@ -177,6 +181,7 @@ public enum Clamp
177181
/// <seealso cref="normalize"/>
178182
/// <seealso cref="scale"/>
179183
/// <seealso cref="invert"/>
184+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
180185
protected float Preprocess(float value)
181186
{
182187
if (scale)
@@ -234,16 +239,37 @@ protected override void FinishSetup()
234239
/// <inheritdoc />
235240
public override unsafe float ReadUnprocessedValueFromState(void* statePtr)
236241
{
237-
var value = stateBlock.ReadFloat(statePtr);
238-
////REVIEW: this isn't very raw
239-
return Preprocess(value);
242+
switch (m_OptimizedControlDataType)
243+
{
244+
case InputStateBlock.kFormatFloat:
245+
return *(float*)((byte*)statePtr + m_StateBlock.m_ByteOffset);
246+
case InputStateBlock.kFormatByte:
247+
return *((byte*)statePtr + m_StateBlock.m_ByteOffset) != 0 ? 1.0f : 0.0f;
248+
default:
249+
{
250+
var value = stateBlock.ReadFloat(statePtr);
251+
////REVIEW: this isn't very raw
252+
return Preprocess(value);
253+
}
254+
}
240255
}
241256

242257
/// <inheritdoc />
243258
public override unsafe void WriteValueIntoState(float value, void* statePtr)
244259
{
245-
value = Unpreprocess(value);
246-
stateBlock.WriteFloat(statePtr, value);
260+
switch (m_OptimizedControlDataType)
261+
{
262+
case InputStateBlock.kFormatFloat:
263+
*(float*)((byte*)statePtr + m_StateBlock.m_ByteOffset) = value;
264+
break;
265+
case InputStateBlock.kFormatByte:
266+
*((byte*)statePtr + m_StateBlock.m_ByteOffset) = (byte)(value >= 0.5f ? 1 : 0);
267+
break;
268+
default:
269+
value = Unpreprocess(value);
270+
stateBlock.WriteFloat(statePtr, value);
271+
break;
272+
}
247273
}
248274

249275
/// <inheritdoc />
@@ -277,5 +303,28 @@ public override unsafe float EvaluateMagnitude(void* statePtr)
277303

278304
return NormalizeProcessor.Normalize(value, min, max, 0);
279305
}
306+
307+
protected override FourCC CalculateOptimizedControlDataType()
308+
{
309+
var noProcessingNeeded =
310+
clamp == Clamp.None &&
311+
invert == false &&
312+
normalize == false &&
313+
scale == false;
314+
315+
if (noProcessingNeeded &&
316+
m_StateBlock.format == InputStateBlock.FormatFloat &&
317+
m_StateBlock.sizeInBits == 32 &&
318+
m_StateBlock.bitOffset == 0)
319+
return InputStateBlock.FormatFloat;
320+
if (noProcessingNeeded &&
321+
m_StateBlock.format == InputStateBlock.FormatBit &&
322+
// has to be 8, otherwise we might be mapping to a state which only represents first bit in the byte, while other bits might map to some other controls
323+
// like in the mouse where LMB/RMB map to the same byte, just LMB maps to first bit and RMB maps to second bit
324+
m_StateBlock.sizeInBits == 8 &&
325+
m_StateBlock.bitOffset == 0)
326+
return InputStateBlock.FormatByte;
327+
return InputStateBlock.FormatInvalid;
328+
}
280329
}
281330
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Runtime.CompilerServices;
12
using UnityEngine.InputSystem.LowLevel;
23
using UnityEngine.Scripting;
34

@@ -75,6 +76,7 @@ public ButtonControl()
7576
/// <returns>True if <paramref name="value"/> crosses the threshold to be considered pressed.</returns>
7677
/// <seealso cref="pressPoint"/>
7778
/// <seealso cref="InputSettings.defaultButtonPressPoint"/>
79+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
7880
public new bool IsValueConsideredPressed(float value)
7981
{
8082
return value >= pressPointOrDefault;

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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.Runtime.CompilerServices;
45
using UnityEngine.InputSystem.Controls;
56
using UnityEngine.InputSystem.LowLevel;
67
using UnityEngine.InputSystem.Utilities;
@@ -853,6 +854,114 @@ protected internal uint stateOffsetRelativeToDeviceRoot
853854
internal PrimitiveValue m_MinValue;
854855
internal PrimitiveValue m_MaxValue;
855856

857+
internal FourCC m_OptimizedControlDataType;
858+
859+
/// <summary>
860+
/// For some types of control you can safely read/write state memory directly
861+
/// which is much faster than calling ReadUnprocessedValueFromState/WriteValueIntoState.
862+
/// This method returns a type that you can use for reading/writing the control directly,
863+
/// or it returns InputStateBlock.kFormatInvalid if it's not possible for this type of control.
864+
/// </summary>
865+
/// <remarks>
866+
/// For example, AxisControl might be a "float" in state memory, and if no processing is applied during reading (e.g. no invert/scale/etc),
867+
/// then you could read it as float in memory directly without calling ReadUnprocessedValueFromState, which is faster.
868+
/// Additionally, if you have a Vector3Control which uses 3 AxisControls as consecutive floats in memory,
869+
/// you can cast the Vector3Control state memory directly to Vector3 without calling ReadUnprocessedValueFromState on x/y/z axes.
870+
///
871+
/// The value returned for any given control is computed automatically by the Input System, when the control's setup configuration changes. <see cref="InputControl.CalculateOptimizedControlDataType"/>
872+
/// There are some parameter changes which don't trigger a configuration change (such as the clamp, invert, normalize, and scale parameters on AxisControl),
873+
/// so if you modify these, the optimized data type is not automatically updated. In this situation, you should manually update it by calling <see cref="InputControl.ApplyParameterChanges"/>.
874+
/// </remarks>
875+
public FourCC optimizedControlDataType => m_OptimizedControlDataType;
876+
877+
/// <summary>
878+
/// Calculates and returns a optimized data type that can represent a control's value in memory directly.
879+
/// The value then is cached in <see cref="InputControl.optimizedControlDataType"/>.
880+
/// This method is for internal use only, you should not call this from your own code.
881+
/// </summary>
882+
protected virtual FourCC CalculateOptimizedControlDataType()
883+
{
884+
return InputStateBlock.kFormatInvalid;
885+
}
886+
887+
/// <summary>
888+
/// Apply built-in parameters changes (e.g. <see cref="AxisControl.invert"/>, others) and recompute <see cref="InputControl.optimizedControlDataType"/> for impacted controls.
889+
/// </summary>
890+
/// <remarks>
891+
/// </remarks>
892+
public void ApplyParameterChanges()
893+
{
894+
// First we go through all children of our own hierarchy
895+
SetOptimizedControlDataTypeRecursively();
896+
897+
// Then we go through all parents up to the root, because our own change might influence their optimization status
898+
// e.g. let's say we have a tree where root is Vector3 and children are three AxisControl
899+
// And user is calling this method on AxisControl which goes from Float to NotOptimized.
900+
// Then we need to also transition Vector3 to NotOptimized as well.
901+
902+
var currentParent = parent;
903+
while (currentParent != null)
904+
{
905+
currentParent.SetOptimizedControlDataType();
906+
currentParent = currentParent.parent;
907+
}
908+
}
909+
910+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
911+
private void SetOptimizedControlDataType()
912+
{
913+
// setting check need to be inline so we clear optimizations if setting is disabled after the fact
914+
m_OptimizedControlDataType = InputSettings.optimizedControlsFeatureEnabled
915+
? CalculateOptimizedControlDataType()
916+
: (FourCC)InputStateBlock.kFormatInvalid;
917+
}
918+
919+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
920+
internal void SetOptimizedControlDataTypeRecursively()
921+
{
922+
// Need to go depth-first because CalculateOptimizedControlDataType might depend on computed values of children
923+
if (m_ChildCount > 0)
924+
{
925+
foreach (var inputControl in children)
926+
inputControl.SetOptimizedControlDataTypeRecursively();
927+
}
928+
929+
SetOptimizedControlDataType();
930+
}
931+
932+
// This function exists to warn users to start using ApplyParameterChanges for edge cases that were previously not intentionally supported,
933+
// where control properties suddenly change underneath us without us anticipating that.
934+
// 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.
935+
// Also it's not clear if InputControl.stateBlock.format can potentially change at any time, likely not.
936+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
937+
// Only do this check in development builds and editor in hope that it will be sufficient to catch any misuse during development.
938+
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
939+
internal void EnsureOptimizationTypeHasNotChanged()
940+
{
941+
if (!InputSettings.optimizedControlsFeatureEnabled)
942+
return;
943+
944+
var currentOptimizedControlDataType = CalculateOptimizedControlDataType();
945+
if (currentOptimizedControlDataType != optimizedControlDataType)
946+
{
947+
Debug.LogError(
948+
$"Control '{name}' / '{path}' suddenly changed optimization state due to either format " +
949+
$"change or control parameters change (was '{optimizedControlDataType}' but became '{currentOptimizedControlDataType}'), " +
950+
"this hinders control hot path optimization, please call control.ApplyParameterChanges() " +
951+
"after the changes to the control to fix this error.");
952+
953+
// Automatically fix the issue
954+
// Note this function is only executed in editor and development builds
955+
m_OptimizedControlDataType = currentOptimizedControlDataType;
956+
}
957+
958+
if (m_ChildCount > 0)
959+
{
960+
foreach (var inputControl in children)
961+
inputControl.EnsureOptimizationTypeHasNotChanged();
962+
}
963+
}
964+
856965
[Flags]
857966
internal enum ControlFlags
858967
{
@@ -935,6 +1044,7 @@ internal void CallFinishSetupRecursive()
9351044
for (var i = 0; i < list.Count; ++i)
9361045
list[i].CallFinishSetupRecursive();
9371046
FinishSetup();
1047+
SetOptimizedControlDataTypeRecursively();
9381048
}
9391049

9401050
internal string MakeChildPath(string path)
@@ -1162,6 +1272,7 @@ public override unsafe bool CompareValue(void* firstStatePtr, void* secondStateP
11621272
return UnsafeUtility.MemCmp(firstValuePtr, secondValuePtr, UnsafeUtility.SizeOf<TValue>()) != 0;
11631273
}
11641274

1275+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
11651276
public TValue ProcessValue(TValue value)
11661277
{
11671278
if (m_ProcessorStack.length > 0)

0 commit comments

Comments
 (0)