From ce9904b9feb7f8b24c70c2b9314d2bf73d19ddb4 Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Fri, 12 Jun 2026 12:45:17 +0200 Subject: [PATCH 1/2] Add test and fix for multiple touchscreen input bug. --- .../TouchscreenMultiDisplayTests.cs | 84 +++++++++++++++++++ .../TouchscreenMultiDisplayTests.cs.meta | 2 + .../Runtime/Devices/Touchscreen.cs | 6 +- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs create mode 100644 Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs.meta diff --git a/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs b/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs new file mode 100644 index 0000000000..e6c440ac0f --- /dev/null +++ b/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs @@ -0,0 +1,84 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.LowLevel; +using TouchPhase = UnityEngine.InputSystem.TouchPhase; + +// Tests covering touch tracking across multiple physical touchscreen monitors. +// Regression coverage for IN-108611: touches from one screen incorrectly matching +// ongoing touches from another screen when both screens report the same touchId. +[TestFixture] +internal class TouchscreenMultiDisplayTests : CoreTestsFixture +{ + // When two physical touchscreens both have an active touch with the same touchId, + // a Move event from screen 2 must update screen 2's touch slot, not screen 1's. + // + // Failure mode (pre-fix): OnStateEvent matches ongoing touches by touchId alone, + // so screen 2's Move event finds screen 1's slot first (both have touchId=1) and + // incorrectly updates it, leaving screen 1's position changed and screen 2 stale. + [Test] + [Category("Devices")] + public void Devices_TouchMoveOnSecondDisplay_DoesNotUpdateTouchOnFirstDisplay() + { + var device = InputSystem.AddDevice(); + + // Finger down on display 0 (touchId=1). + InputSystem.QueueStateEvent(device, new TouchState + { + phase = TouchPhase.Began, + touchId = 1, + position = new Vector2(100, 100), + displayIndex = 0, + }); + + // Finger down on display 1 — same touchId, different screen. + InputSystem.QueueStateEvent(device, new TouchState + { + phase = TouchPhase.Began, + touchId = 1, + position = new Vector2(200, 200), + displayIndex = 1, + }); + + InputSystem.Update(); + + // Both touches should be allocated to separate slots. + Assert.That(device.touches[0].phase.ReadValue(), Is.EqualTo(TouchPhase.Began)); + Assert.That(device.touches[1].phase.ReadValue(), Is.EqualTo(TouchPhase.Began)); + + var display0SlotIndex = -1; + var display1SlotIndex = -1; + for (var i = 0; i < 2; i++) + { + var displayIdx = device.touches[i].displayIndex.ReadValue(); + if (displayIdx == 0) display0SlotIndex = i; + else if (displayIdx == 1) display1SlotIndex = i; + } + + Assert.That(display0SlotIndex, Is.Not.EqualTo(-1), "No touch slot found for display 0"); + Assert.That(display1SlotIndex, Is.Not.EqualTo(-1), "No touch slot found for display 1"); + + var display0PositionBefore = device.touches[display0SlotIndex].position.ReadValue(); + + // Swipe on display 1 (same touchId=1 as display 0's held touch). + InputSystem.QueueStateEvent(device, new TouchState + { + phase = TouchPhase.Moved, + touchId = 1, + position = new Vector2(300, 300), + displayIndex = 1, + }); + + InputSystem.Update(); + + // Display 0's touch must be unchanged. + Assert.That(device.touches[display0SlotIndex].position.ReadValue(), Is.EqualTo(display0PositionBefore), + "Touch on display 0 was incorrectly updated by a Move event from display 1"); + + // Display 1's touch must reflect the new position. + Assert.That(device.touches[display1SlotIndex].position.ReadValue(), Is.EqualTo(new Vector2(300, 300)), + "Touch on display 1 was not updated by its own Move event"); + + Assert.That(device.touches[display1SlotIndex].phase.ReadValue(), Is.EqualTo(TouchPhase.Moved)); + } +} diff --git a/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs.meta b/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs.meta new file mode 100644 index 0000000000..a9587bb70a --- /dev/null +++ b/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a5c39c3a4a0654c28a5b7754add41d2a \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Touchscreen.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Touchscreen.cs index 7a82e6a177..d90811dcd4 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Touchscreen.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Touchscreen.cs @@ -694,7 +694,7 @@ protected override void FinishSetup() var touchId = newTouchState.touchId; for (var i = 0; i < touchControlCount; ++i) { - if (currentTouchState[i].touchId == touchId) + if (currentTouchState[i].touchId == touchId && currentTouchState[i].displayIndex == newTouchState.displayIndex) { // Preserve primary touch state. var isPrimaryTouch = currentTouchState[i].isPrimaryTouch; @@ -915,7 +915,7 @@ unsafe bool IInputStateCallbackReceiver.GetStateOffsetForEvent(InputControl cont for (var i = 0; i < touchControlCount; ++i) { var touch = ¤tTouchState[i]; - if (touch->touchId == eventTouchId || (!touch->isInProgress && eventTouchPhase.IsActive())) + if ((touch->touchId == eventTouchId && touch->displayIndex == eventTouchState->displayIndex) || (!touch->isInProgress && eventTouchPhase.IsActive())) { offset = primaryTouch.m_StateBlock.byteOffset + primaryTouch.m_StateBlock.alignedSizeInBytes - m_StateBlock.byteOffset + (uint)(i * UnsafeUtility.SizeOf()); @@ -1002,7 +1002,7 @@ internal static unsafe bool MergeForward(InputEventPtr currentEventPtr, InputEve var currentState = (TouchState*)currentEvent->state; var nextState = (TouchState*)nextEvent->state; - if (currentState->touchId != nextState->touchId || currentState->phaseId != nextState->phaseId || currentState->flags != nextState->flags) + if (currentState->touchId != nextState->touchId || currentState->phaseId != nextState->phaseId || currentState->flags != nextState->flags || currentState->displayIndex != nextState->displayIndex) return false; nextState->delta += currentState->delta; From 1adb45696df5ee77904a7c163547e4a6efe463eb Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Fri, 12 Jun 2026 13:11:10 +0200 Subject: [PATCH 2/2] Add fix for regression reported by u-pr. --- .../Runtime/Plugins/EnhancedTouch/TouchSimulation.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/EnhancedTouch/TouchSimulation.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/EnhancedTouch/TouchSimulation.cs index d7bbc15ccd..d3ad485357 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/EnhancedTouch/TouchSimulation.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/EnhancedTouch/TouchSimulation.cs @@ -253,6 +253,9 @@ protected void OnEnable() if (m_TouchIds == null) m_TouchIds = new int[simulatedTouchscreen.touches.Count]; + if (m_TouchDisplayIndices == null) + m_TouchDisplayIndices = new byte[simulatedTouchscreen.touches.Count]; + foreach (var device in InputSystem.devices) OnDeviceChange(device, InputDeviceChange.Added); @@ -306,10 +309,12 @@ private unsafe void UpdateTouch(int touchIndex, int pointerIndex, TouchPhase pha touch.startPosition = position; touch.touchId = ++m_LastTouchId; m_TouchIds[touchIndex] = m_LastTouchId; + m_TouchDisplayIndices[touchIndex] = displayIndex; } else { touch.touchId = m_TouchIds[touchIndex]; + touch.displayIndex = m_TouchDisplayIndices[touchIndex]; } //NOTE: Processing these events still happen in the current frame. @@ -327,6 +332,7 @@ private unsafe void UpdateTouch(int touchIndex, int pointerIndex, TouchPhase pha [NonSerialized] private int[] m_CurrentDisplayIndices; [NonSerialized] private ButtonControl[] m_Touches; [NonSerialized] private int[] m_TouchIds; + [NonSerialized] private byte[] m_TouchDisplayIndices; [NonSerialized] private int m_LastTouchId; [NonSerialized] private Action m_OnDeviceChange;