Skip to content

Commit 83cfb4a

Browse files
authored
CHANGE: Merging touch events (#1392)
1 parent 6bea4e8 commit 83cfb4a

File tree

12 files changed

+241
-38
lines changed

12 files changed

+241
-38
lines changed

Assets/Tests/InputSystem/APIVerificationTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,10 @@ public class TrackedPoseDriver : UnityEngine.MonoBehaviour
794794
[Property("Exclusions", @"1.0.0
795795
public static event System.Action<UnityEngine.InputSystem.LowLevel.InputEventPtr, UnityEngine.InputSystem.InputDevice> onEvent;
796796
")]
797+
// Mouse and Touchscreen implement internal IEventMerger interface
798+
[Property("Exclusions", @"1.0.0
799+
public class Touchscreen : UnityEngine.InputSystem.Pointer, UnityEngine.InputSystem.LowLevel.IInputStateCallbackReceiver
800+
")]
797801
[ScopedExclusionProperty("1.0.0", "UnityEngine.InputSystem.Editor", "public sealed class InputControlPathEditor : System.IDisposable", "public void OnGUI(UnityEngine.Rect rect);")]
798802
public void API_MinorVersionsHaveNoBreakingChanges()
799803
{

Assets/Tests/InputSystem/CoreTests_Events.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2328,8 +2328,39 @@ public void Events_CanQueueEventsFromWithinEventProcessing_WithEventMergingSetTo
23282328
Assert.That(mouse.position.ReadValue(), Is.EqualTo(new Vector2(numMouseEventsQueued, numMouseEventsQueued * 2)));
23292329
Assert.That(mouse.delta.ReadValue(), Is.EqualTo(new Vector2(numMouseEventsQueued, numMouseEventsQueued)));
23302330
Assert.That(numMouseEventsReceived, Is.EqualTo(mergeRedundantEvents ? 1 : numMouseEventsQueued));
2331+
}
2332+
2333+
[Test]
2334+
[Category("Events")]
2335+
public void Events_QueuedToDifferentDevices_AreNotMergedTogether()
2336+
{
2337+
var mouse1 = InputSystem.AddDevice<Mouse>();
2338+
var mouse2 = InputSystem.AddDevice<Mouse>();
2339+
2340+
var numMouse1EventsReceived = 0;
2341+
var numMouse2EventsReceived = 0;
2342+
InputSystem.onEvent +=
2343+
(eventPtr, device) =>
2344+
{
2345+
if (device == mouse1)
2346+
++numMouse1EventsReceived;
2347+
if (device == mouse2)
2348+
++numMouse2EventsReceived;
2349+
};
2350+
2351+
InputSystem.QueueStateEvent(mouse1, new MouseState { position = new Vector2(1, 2)});
2352+
InputSystem.QueueStateEvent(mouse1, new MouseState { position = new Vector2(2, 3)});
2353+
InputSystem.QueueStateEvent(mouse2, new MouseState { position = new Vector2(3, 4)});
2354+
InputSystem.QueueStateEvent(mouse2, new MouseState { position = new Vector2(4, 5)});
2355+
InputSystem.QueueStateEvent(mouse2, new MouseState { position = new Vector2(5, 6)});
2356+
2357+
InputSystem.Update();
2358+
2359+
Assert.That(mouse1.position.ReadValue(), Is.EqualTo(new Vector2(2, 3)));
2360+
Assert.That(mouse2.position.ReadValue(), Is.EqualTo(new Vector2(5, 6)));
23312361

2332-
InputSystem.settings.disableRedundantEventsMerging = false;
2362+
Assert.That(numMouse1EventsReceived, Is.EqualTo(1));
2363+
Assert.That(numMouse2EventsReceived, Is.EqualTo(1));
23332364
}
23342365

23352366
[Test]

Assets/Tests/InputSystem/Plugins/EnhancedTouchTests.cs

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -441,19 +441,40 @@ public void EnhancedTouch_DeltasInActiveTouchesAccumulateAndReset()
441441
// Thus we don't want accumulation and resetting (which again are frame-to-frame kind of mechanics).
442442
[Test]
443443
[Category("EnhancedTouch")]
444-
public void EnhancedTouch_DeltasInTouchHistoryDoNotAccumulateAndReset()
444+
[TestCase(false)]
445+
[TestCase(true)]
446+
public void EnhancedTouch_DeltasInTouchHistoryDoNotAccumulateAndReset_WithEventMergingSetTo(bool mergeRedundantEvents)
445447
{
448+
InputSystem.settings.disableRedundantEventsMerging = !mergeRedundantEvents;
449+
446450
BeginTouch(1, new Vector2(0.123f, 0.234f), queueEventOnly: true);
447451
MoveTouch(1, new Vector2(0.234f, 0.345f), queueEventOnly: true);
448452
MoveTouch(1, new Vector2(0.345f, 0.456f), queueEventOnly: true);
453+
MoveTouch(1, new Vector2(0.456f, 0.567f), queueEventOnly: true);
449454

450455
InputSystem.Update();
451456

452-
Assert.That(Touch.activeFingers[0].touchHistory[0].delta,
453-
Is.EqualTo(new Vector2(0.111f, 0.111f)).Using(Vector2EqualityComparer.Instance));
454-
Assert.That(Touch.activeFingers[0].touchHistory[1].delta,
455-
Is.EqualTo(new Vector2(0.111f, 0.111f)).Using(Vector2EqualityComparer.Instance));
456-
Assert.That(Touch.activeFingers[0].touchHistory[2].delta,
457+
Assert.That(Touch.activeFingers[0].touchHistory.Count, Is.EqualTo(mergeRedundantEvents ? 3 : 4));
458+
459+
if (mergeRedundantEvents)
460+
{
461+
// Event merging adds deltas inside
462+
Assert.That(Touch.activeFingers[0].touchHistory[0].delta,
463+
Is.EqualTo(new Vector2(0.222f, 0.222f)).Using(Vector2EqualityComparer.Instance));
464+
Assert.That(Touch.activeFingers[0].touchHistory[1].delta,
465+
Is.EqualTo(new Vector2(0.111f, 0.111f)).Using(Vector2EqualityComparer.Instance));
466+
}
467+
else
468+
{
469+
Assert.That(Touch.activeFingers[0].touchHistory[0].delta,
470+
Is.EqualTo(new Vector2(0.222f, 0.222f)).Using(Vector2EqualityComparer.Instance));
471+
Assert.That(Touch.activeFingers[0].touchHistory[1].delta,
472+
Is.EqualTo(new Vector2(0.111f, 0.111f)).Using(Vector2EqualityComparer.Instance));
473+
Assert.That(Touch.activeFingers[0].touchHistory[2].delta,
474+
Is.EqualTo(new Vector2(0.111f, 0.111f)).Using(Vector2EqualityComparer.Instance));
475+
}
476+
477+
Assert.That(Touch.activeFingers[0].touchHistory.Last().delta,
457478
Is.EqualTo(new Vector2()).Using(Vector2EqualityComparer.Instance));
458479
}
459480

@@ -501,8 +522,12 @@ public void EnhancedTouch_CanGetStartPositionAndTimeOfTouch()
501522

502523
[Test]
503524
[Category("EnhancedTouch")]
504-
public void EnhancedTouch_CanAccessHistoryOfTouch()
525+
[TestCase(false)]
526+
[TestCase(true)]
527+
public void EnhancedTouch_CanAccessHistoryOfTouch_WithEventMergingSetTo(bool mergeRedundantEvents)
505528
{
529+
InputSystem.settings.disableRedundantEventsMerging = !mergeRedundantEvents;
530+
506531
// Noise. This one shouldn't show up in the history.
507532
BeginTouch(2, new Vector2(0.111f, 0.222f), queueEventOnly: true);
508533
EndTouch(2, new Vector2(0.111f, 0.222f), queueEventOnly: true);
@@ -514,6 +539,7 @@ public void EnhancedTouch_CanAccessHistoryOfTouch()
514539
runtime.currentTime = 0.987;
515540
MoveTouch(1, new Vector2(0.234f, 0.345f), queueEventOnly: true);
516541
MoveTouch(1, new Vector2(0.345f, 0.456f), queueEventOnly: true);
542+
MoveTouch(1, new Vector2(0.456f, 0.567f), queueEventOnly: true);
517543
BeginTouch(3, new Vector2(0.666f, 0.666f), queueEventOnly: true);
518544
BeginTouch(4, new Vector2(0.777f, 0.777f), queueEventOnly: true);
519545
EndTouch(4, new Vector2(0.888f, 0.888f), queueEventOnly: true);
@@ -523,18 +549,22 @@ public void EnhancedTouch_CanAccessHistoryOfTouch()
523549
Assert.That(Touch.activeTouches, Has.Count.EqualTo(3));
524550

525551
Assert.That(Touch.activeTouches[0].touchId, Is.EqualTo(1));
526-
Assert.That(Touch.activeTouches[0].history, Has.Count.EqualTo(2));
552+
Assert.That(Touch.activeTouches[0].history, Has.Count.EqualTo(mergeRedundantEvents ? 2 : 3));
527553
Assert.That(Touch.activeTouches[0].history, Has.All.Property("finger").SameAs(Touch.activeTouches[0].finger));
528-
Assert.That(Touch.activeTouches[0].history[0].phase, Is.EqualTo(TouchPhase.Moved));
529-
Assert.That(Touch.activeTouches[0].history[1].phase, Is.EqualTo(TouchPhase.Began));
530-
Assert.That(Touch.activeTouches[0].history[0].time, Is.EqualTo(0.987));
531-
Assert.That(Touch.activeTouches[0].history[1].time, Is.EqualTo(0.876));
532-
Assert.That(Touch.activeTouches[0].history[0].startTime, Is.EqualTo(0.876));
533-
Assert.That(Touch.activeTouches[0].history[1].startTime, Is.EqualTo(0.876));
534-
Assert.That(Touch.activeTouches[0].history[0].startScreenPosition,
535-
Is.EqualTo(new Vector2(0.123f, 0.234f)).Using(Vector2EqualityComparer.Instance));
536-
Assert.That(Touch.activeTouches[0].history[1].startScreenPosition,
554+
var beganIndex = mergeRedundantEvents ? 1 : 2;
555+
Assert.That(Touch.activeTouches[0].history[beganIndex].phase, Is.EqualTo(TouchPhase.Began));
556+
Assert.That(Touch.activeTouches[0].history[beganIndex].time, Is.EqualTo(0.876));
557+
Assert.That(Touch.activeTouches[0].history[beganIndex].startTime, Is.EqualTo(0.876));
558+
Assert.That(Touch.activeTouches[0].history[beganIndex].startScreenPosition,
537559
Is.EqualTo(new Vector2(0.123f, 0.234f)).Using(Vector2EqualityComparer.Instance));
560+
for (int index = 0; index < (mergeRedundantEvents ? 1 : 2); ++index)
561+
{
562+
Assert.That(Touch.activeTouches[0].history[index].phase, Is.EqualTo(TouchPhase.Moved));
563+
Assert.That(Touch.activeTouches[0].history[index].time, Is.EqualTo(0.987));
564+
Assert.That(Touch.activeTouches[0].history[index].startTime, Is.EqualTo(0.876));
565+
Assert.That(Touch.activeTouches[0].history[index].startScreenPosition,
566+
Is.EqualTo(new Vector2(0.123f, 0.234f)).Using(Vector2EqualityComparer.Instance));
567+
}
538568

539569
Assert.That(Touch.activeTouches[1].touchId, Is.EqualTo(3));
540570
Assert.That(Touch.activeTouches[1].history, Is.Empty);

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ however, it has to be formatted properly to pass verification tests.
3030
- Fixed InvalidOperationException when opening a preset created from a .inputactions asset ([case 1199544](https://issuetracker.unity3d.com/issues/input-system-properties-are-not-visible-and-invalidoperationexception-is-thrown-on-selecting-inputactionimporter-preset-asset)).
3131
- Fixed a problem arising when combining InputSystemUIInputModule and PlayInput with SendMessage or BroadcastMessage callback behavior on the same game object or hierarchy which is an ambiguous input setup. This fix eliminates callbacks into InputSystemUIInputModule. Related to ([1343712](https://issuetracker.unity3d.com/issues/input-system-ui-components-lags-when-using-input-system-ui-input-module-together-with-player-input-component)).
3232
- Fixed inconsistent usage of `ENABLE_PROFILER` define together with `Profiler.BeginSample`/`Profiler.EndSample` by removing `ENABLE_PROFILER` macro check because `BeginSample`/`EndSample` are already conditional with `[Conditional("ENABLE_PROFILER")]` ([case 1350139](https://issuetracker.unity3d.com/issues/inconsistent-enable-profiler-scripting-defines-in-inputmanager-dot-cs-when-using-profiler-dot-beginssample-and-profiler-dot-endsample)).
33-
- Remediated majority of performance issues with high frequency mice (>=1kHz poll rates) in release mode by merging consecutive mouse move events together ([case 1281266](https://issuetracker.unity3d.com/issues/many-input-events-when-using-1000hz-mouse)).
33+
- Remediated majority of performance issues with high frequency mice (>=1kHz poll rates) in release mode by merging consecutive mouse move events together ([case 1281266](https://issuetracker.unity3d.com/issues/many-input-events-when-using-1000hz-mouse)), see the events documentation for more information.
3434
- Fixed `InputEventTrace` replays skipping over empty frames and thus causing playback to happen too fast.
3535
- Fixed `"Pointer should have exited all objects before being removed"` error when changing screen orientation on mobile.
3636
- Controls such as mouse positions are no longer reset when focus is lost.
@@ -85,6 +85,7 @@ however, it has to be formatted properly to pass verification tests.
8585
}
8686
");
8787
```
88+
- Improved performance of `Touchscreen` by merging consecutive touch move events together. See the events documentation for more information.
8889

8990
#### Actions
9091

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [Reading state events](#reading-state-events)
1010
* [Creating events](#creating-events)
1111
* [Capturing events](#capturing-events)
12+
* [Merging of events](#merging-of-events)
1213

1314
The Input System is event-driven. All input is delivered as events, and you can generate custom input by injecting events. You can also observe all source input by listening in on the events flowing through the system.
1415

@@ -231,3 +232,45 @@ controller.PlayAllFramesOnyByOne();
231232
// Replay events in a way that tries to simulate original event timing.
232233
controller.PlayAllEventsAccordingToTimestamps();
233234
```
235+
236+
## Merging of events
237+
238+
Input system uses event mering to reduce amount of events required to be processed.
239+
This greatly improves performance when working with high refresh rate devices like 8000 Hz mice, touchscreens and others.
240+
241+
For example let's take a stream of 7 mouse events coming in the same update:
242+
243+
```
244+
245+
Mouse Mouse Mouse Mouse Mouse Mouse Mouse
246+
Event no1 Event no2 Event no3 Event no4 Event no5 Event no6 Event no7
247+
Time 1 Time 2 Time 3 Time 4 Time 5 Time 6 Time 7
248+
Pos(10,20) Pos(12,21) Pos(13,23) Pos(14,24) Pos(16,25) Pos(17,27) Pos(18,28)
249+
Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1)
250+
BtnLeft(0) BtnLeft(0) BtnLeft(0) BtnLeft(1) BtnLeft(1) BtnLeft(1) BtnLeft(1)
251+
```
252+
253+
To reduce workload we can skip events that are not encoding button state changes:
254+
255+
```
256+
Mouse Mouse Mouse
257+
Time 3 Time 4 Time 7
258+
Event no3 Event no4 Event no7
259+
Pos(13,23) Pos(14,24) Pos(18,28)
260+
Delta(3,3) Delta(1,1) Delta(4,4)
261+
BtnLeft(0) BtnLeft(1) BtnLeft(1)
262+
```
263+
264+
In that case we combine no1, no2, no3 together into no3 and accumulate the delta,
265+
then we keep no4 because it stores the transition from button unpressed to button pressed,
266+
and it's important to keep the exact timestamp of such transition.
267+
Later we combine no5, no6, no7 together into no7 because it is the last event in the update.
268+
269+
Currently this approach is implemented for:
270+
- `FastMouse`, combines events unless `buttons` or `clickCount` differ in `MouseState`.
271+
- `Touchscreen`, combines events unless `touchId`, `phaseId` or `flags` differ in `TouchState`.
272+
273+
You can disable merging of events by:
274+
```
275+
InputSystem.settings.disableRedundantEventsMerging = true;
276+
```

Packages/com.unity.inputsystem/InputSystem/Devices/IEventMerger.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace UnityEngine.InputSystem
44
{
55
internal interface IEventMerger
66
{
7-
bool MergeForward(InputEventPtr currentEvent, InputEventPtr nextEvent);
7+
bool MergeForward(InputEventPtr currentEventPtr, InputEventPtr nextEventPtr);
88
}
99
}

Packages/com.unity.inputsystem/InputSystem/Devices/InputDevice.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -620,14 +620,15 @@ internal enum DeviceFlags
620620
HasStateCallbacks = 1 << 1,
621621
HasControlsWithDefaultState = 1 << 2,
622622
HasDontResetControls = 1 << 3,
623+
HasEventMerger = 1 << 4,
623624

624-
Remote = 1 << 4, // It's a local mirror of a device from a remote player connection.
625-
Native = 1 << 5, // It's a device created from data surfaced by NativeInputRuntime.
625+
Remote = 1 << 5, // It's a local mirror of a device from a remote player connection.
626+
Native = 1 << 6, // It's a device created from data surfaced by NativeInputRuntime.
626627

627-
DisabledInFrontend = 1 << 6, // Explicitly disabled on the managed side.
628-
DisabledInRuntime = 1 << 7, // Disabled in the native runtime.
629-
DisabledWhileInBackground = 1 << 8, // Disabled while the player is running in the background.
630-
DisabledStateHasBeenQueriedFromRuntime = 1 << 9, // Whether we have fetched the current enable/disable state from the runtime.
628+
DisabledInFrontend = 1 << 7, // Explicitly disabled on the managed side.
629+
DisabledInRuntime = 1 << 8, // Disabled in the native runtime.
630+
DisabledWhileInBackground = 1 << 9, // Disabled while the player is running in the background.
631+
DisabledStateHasBeenQueriedFromRuntime = 1 << 10, // Whether we have fetched the current enable/disable state from the runtime.
631632

632633
CanRunInBackground = 1 << 11,
633634
CanRunInBackgroundHasBeenQueried = 1 << 12,
@@ -756,7 +757,7 @@ internal bool hasControlsWithDefaultState
756757

757758
internal bool hasDontResetControls
758759
{
759-
get => (m_DeviceFlags & DeviceFlags.HasDontResetControls) != 0;
760+
get => (m_DeviceFlags & DeviceFlags.HasDontResetControls) == DeviceFlags.HasDontResetControls;
760761
set
761762
{
762763
if (value)
@@ -768,7 +769,7 @@ internal bool hasDontResetControls
768769

769770
internal bool hasStateCallbacks
770771
{
771-
get => (m_DeviceFlags & DeviceFlags.HasStateCallbacks) != 0;
772+
get => (m_DeviceFlags & DeviceFlags.HasStateCallbacks) == DeviceFlags.HasStateCallbacks;
772773
set
773774
{
774775
if (value)
@@ -778,6 +779,18 @@ internal bool hasStateCallbacks
778779
}
779780
}
780781

782+
internal bool hasEventMerger
783+
{
784+
get => (m_DeviceFlags & DeviceFlags.HasEventMerger) == DeviceFlags.HasEventMerger;
785+
set
786+
{
787+
if (value)
788+
m_DeviceFlags |= DeviceFlags.HasEventMerger;
789+
else
790+
m_DeviceFlags &= ~DeviceFlags.HasEventMerger;
791+
}
792+
}
793+
781794
internal void AddDeviceUsage(InternedString usage)
782795
{
783796
var controlUsageCount = m_UsageToControl.LengthSafe();

Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastMouse.partial.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ void IInputStateCallbackReceiver.OnStateEvent(InputEventPtr eventPtr)
4949
OnStateEvent(eventPtr);
5050
}
5151

52-
public unsafe bool MergeForward(InputEventPtr currentEventPtr, InputEventPtr nextEventPtr)
52+
internal static unsafe bool MergeForward(InputEventPtr currentEventPtr, InputEventPtr nextEventPtr)
5353
{
5454
if (currentEventPtr.type != StateEvent.Type || nextEventPtr.type != StateEvent.Type)
5555
return false;
@@ -71,5 +71,10 @@ public unsafe bool MergeForward(InputEventPtr currentEventPtr, InputEventPtr nex
7171
nextState->scroll += currentState->scroll;
7272
return true;
7373
}
74+
75+
bool IEventMerger.MergeForward(InputEventPtr currentEventPtr, InputEventPtr nextEventPtr)
76+
{
77+
return MergeForward(currentEventPtr, nextEventPtr);
78+
}
7479
}
7580
}

Packages/com.unity.inputsystem/InputSystem/Devices/Touchscreen.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ public enum TouchPhase
489489
/// </remarks>
490490
[InputControlLayout(stateType = typeof(TouchscreenState), isGenericTypeOfDevice = true)]
491491
[Scripting.Preserve]
492-
public class Touchscreen : Pointer, IInputStateCallbackReceiver
492+
public class Touchscreen : Pointer, IInputStateCallbackReceiver, IEventMerger
493493
{
494494
/// <summary>
495495
/// Synthetic control that has the data for the touch that is deemed the "primary" touch at the moment.
@@ -953,6 +953,33 @@ unsafe bool IInputStateCallbackReceiver.GetStateOffsetForEvent(InputControl cont
953953
return true;
954954
}
955955

956+
internal static unsafe bool MergeForward(InputEventPtr currentEventPtr, InputEventPtr nextEventPtr)
957+
{
958+
if (currentEventPtr.type != StateEvent.Type || nextEventPtr.type != StateEvent.Type)
959+
return false;
960+
961+
var currentEvent = StateEvent.FromUnchecked(currentEventPtr);
962+
var nextEvent = StateEvent.FromUnchecked(nextEventPtr);
963+
964+
if (currentEvent->stateFormat != TouchState.Format || nextEvent->stateFormat != TouchState.Format)
965+
return false;
966+
967+
var currentState = (TouchState*)currentEvent->state;
968+
var nextState = (TouchState*)nextEvent->state;
969+
970+
if (currentState->touchId != nextState->touchId || currentState->phaseId != nextState->phaseId || currentState->flags != nextState->flags)
971+
return false;
972+
973+
nextState->delta += currentState->delta;
974+
975+
return true;
976+
}
977+
978+
bool IEventMerger.MergeForward(InputEventPtr currentEventPtr, InputEventPtr nextEventPtr)
979+
{
980+
return MergeForward(currentEventPtr, nextEventPtr);
981+
}
982+
956983
// We can only detect taps on touch *release*. At which point it acts like a button that triggers and releases
957984
// in one operation.
958985
private static void TriggerTap(TouchControl control, ref TouchState state, InputEventPtr eventPtr)

0 commit comments

Comments
 (0)