Skip to content

FIX: handled events still triggering actions when switching PlayerInput#2340

Open
AswinRajGopal wants to merge 6 commits intodevelopfrom
isxb-1097/handled-event-triggers-fix
Open

FIX: handled events still triggering actions when switching PlayerInput#2340
AswinRajGopal wants to merge 6 commits intodevelopfrom
isxb-1097/handled-event-triggers-fix

Conversation

@AswinRajGopal
Copy link
Collaborator

@AswinRajGopal AswinRajGopal commented Feb 3, 2026

Description

Bug: https://issuetracker.unity3d.com/issues/setting-inputeventptr-dot-handled-to-true-does-not-prevent-actions-from-triggering-when-changing-devices
When InputUser.onUnpairedDeviceUsed marks eventPtr.handled = true, we expect no action to fire from that same
button press.
But on devices like DualSense, a single press can generate multiple input events. The handled flag only stops the
one event, and another event in the same press still triggers the action.

Root cause
The action system is driven by state change events. We can see different eventIds for the handled event vs the
action‑triggering event, so the action was coming from a separate input event.

Fix

  • While dispatching InputSystem.onEvent, stop calling remaining listeners once handled becomes true.
  • When an event is handled (policy = SuppressStateUpdates), temporarily suppress action processing for that device
    for the rest of the update (and next update).
  • In InputActionState, skip processing control changes if the device is suppressed.

Testing status & QA

Manually verified using repro project + dualsense controller.

Overall Product Risks

  • Complexity: Low
  • Halo Effect: Low

Comments to reviewers

  • Only applies when policy is SuppressStateUpdates and a listener explicitly sets handled = true.
  • Normal input flow remains unchanged, however documentation update seems necessary.
  • This simply makes “handled” cover the entire physical press, even if it arrives as multiple events.
  • handled + no state update leaves the button “unpressed” in system state, and gamepad polling makes
    the next event look like a new press which is now prevented.

Checklist

Before review:

  • Changelog entry added.
    • Explains the change in Changed, Fixed, Added sections.
    • For API change contains an example snippet and/or migration example.
    • JIRA ticket linked, example (case %%). If it is a private issue, just add the case ID without a link.
    • Jira port for the next release set as "Resolved".
  • Tests added/changed, if applicable.
    • Functional tests Area_CanDoX, Area_CanDoX_EvenIfYIsTheCase, Area_WhenIDoX_AndYHappens_ThisIsTheResult.
    • Performance tests.
    • Integration tests.
  • Docs for new/changed API's.
    • Xmldoc cross references are set correctly.
    • Added explanation how the API works.
    • Usage code examples added.
    • The manual is updated, if needed.

During merge:

  • Commit message for squash-merge is prefixed with one of the list:
    • NEW: ___.
    • FIX: ___.
    • DOCS: ___.
    • CHANGE: ___.
    • RELEASE: 1.1.0-preview.3.

@u-pr
Copy link
Contributor

u-pr bot commented Feb 3, 2026

PR Reviewer Guide 🔍

(Review updated until commit 94564dc)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪

The PR involves a targeted fix within the InputSystem's event processing loop and state management. The changes are logical, well-scoped, and include appropriate tests.
🏅 Score: 95

The solution effectively addresses the issue of duplicate action triggers from handled events by implementing a suppression window. The code is clean, efficient, and well-tested. The logic correctly handles frame transitions to prevent 'phantom' presses.
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected
  • Update review

🤖 Helpful? Please react with 👍/👎 | Questions❓Please reach out in Slack #ask-u-pr

@u-pr
Copy link
Contributor

u-pr bot commented Feb 3, 2026

PR Code Suggestions ✨


🤖 Helpful? Please react with 👍/👎 | Questions❓Please reach out in Slack #ask-u-pr

@codecov-github-com
Copy link

codecov-github-com bot commented Feb 3, 2026

Codecov Report

All modified and coverable lines are covered by tests ✅

@@           Coverage Diff            @@
##           develop    #2340   +/-   ##
========================================
  Coverage    77.95%   77.96%           
========================================
  Files          476      476           
  Lines        97453    97423   -30     
========================================
- Hits         75971    75952   -19     
+ Misses       21482    21471   -11     
Files with missing lines Coverage Δ
Assets/Tests/InputSystem/CoreTests_Events.cs 98.49% <ø> (-0.01%) ⬇️
...nputsystem/InputSystem/Actions/InputActionState.cs 92.81% <ø> (ø)
.../com.unity.inputsystem/InputSystem/InputManager.cs 91.74% <ø> (-0.03%) ⬇️
...putsystem/InputSystem/Utilities/DelegateHelpers.cs 62.77% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Pauliusd01 Pauliusd01 requested review from K-Tone and Pauliusd01 and removed request for UnityAnthony February 5, 2026 07:43
@u-pr
Copy link
Contributor

u-pr bot commented Feb 5, 2026

Test Plan

  • Test plan approved by PR author
  • Test execution in progress
  • Test execution complete
  • Ready for merge

Summary of Changes & Risk Assessment

Summary of Changes

This PR fixes a bug where physical button presses that generate multiple input events (common on controllers like DualSense) would still trigger actions even if the first event was marked as handled. The fix introduces a device-specific suppression window that prevents InputActionState from processing changes for a device for the current and subsequent update steps once an event is handled under the SuppressStateUpdates policy.

Risk Assessment

  • High Risk Areas: None identified.
  • Medium Risk Areas: Core event dispatching loop in InputManager, action triggering logic in InputActionState.
  • Low Risk Areas: Utilities (DelegateHelpers), Changelog.

Test Scenarios

Functional Testing

  • Verify Multi-Event Suppression: Using the new test case Events_HandledFirstEvent_DoesNotTriggerAction_OnSecondEventSameUpdate in CoreTests_Events.cs:L1260, ensure that when two events are queued and the first is marked handled, the action performed count remains 0.
  • Policy Requirement Check: Verify that suppression only occurs when InputSystem.settings.inputEventHandledPolicy is set to SuppressStateUpdates.
  • Listener Termination: Confirm that InputSystem.onEvent stops calling subsequent listeners immediately after one listener sets eventPtr.handled = true (via DelegateHelpers.cs:L110).

Regression Testing

  • Standard Input Flow: Verify that normal button presses (where no listener marks the event as handled) still trigger actions as expected across different device types (Keyboard, Mouse, Gamepad).
  • Device Reconnection: Ensure that the suppression array m_SuppressActionsForDeviceUpdate in InputManager handles device indices correctly if a device is removed and a new one is added (checking for potential out-of-bounds or stale suppression).
  • Existing Suppression Tests: Run existing tests in CoreTests_Events.cs that were modified (e.g., Events_ShouldRespectHandledPolicyUponUpdateAndSuppressedPressTransitions) to ensure the updated expected values are correct.
🔍 Regression Deep Dive (additional risks identified)
  • Performance (Memory Allocation): The array m_SuppressActionsForDeviceUpdate is resized in InputManager.cs:L3906. Verify no excessive allocations occur during normal gameplay with many devices.
  • Cross-Update Suppression: Verify the logic in InputManager.cs:L3921 (lastUpdate + 1 == currentUpdate) correctly clears suppression after the 1-frame window.

Edge Cases

  • Null Device Events: Verify SuppressActionsForDevice(device) handles null device scenarios gracefully without crashing (referenced in InputManager.cs:L3901).
  • Simultaneous Multi-Device Handling: Test handling events from two different devices in the same frame; ensure suppression only applies to the device that sent the handled event.

💡 This test plan updates automatically when /test_plan is run on new commits. If you have any feedback, please reach out in #ai-qa


🤖 Helpful? Please react with 👍/👎 | Questions❓Please reach out in Slack #ask-u-pr

@ekcoh ekcoh self-requested a review February 5, 2026 08:31
@u-pr
Copy link
Contributor

u-pr bot commented Feb 5, 2026

Persistent review updated to latest commit 94564dc

if (m_InputEventHandledPolicy == InputEventHandledPolicy.SuppressStateUpdates &&
currentEventReadPtr->handled)
if (DelegateHelpers.InvokeCallbacksSafeUntilHandled(ref m_EventListeners, currentEventPtr, device, k_InputOnEventMarker, "InputSystem.onEvent",
m_InputEventHandledPolicy == InputEventHandledPolicy.SuppressStateUpdates))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we saying here that if a single listener handles the event, we don't let other listeners process the event? Should this be part of the listener logic instead? Otherwise it's a unclear for the user why was their method hooked on InputSystem.onEvent not being triggered 🤔

Comment on lines +1341 to +1355
new int[] { 0, 0, 0, 0, 1}, // started
new int[] { 0, 0, 0, 0, 1}, // performed
new int[] {0, 0, 0, 0, 0})] // cancelled
// Press event is not detected in step 1/2 with default interaction
[TestCase(InputEventHandledPolicy.SuppressActionEventNotifications,
null,
new int[] { 0, 0, 0, 0, 1},
new int[] { 0, 0, 0, 0, 1},
new int[] {0, 0, 0, 1, 1})]
// Press event is detected in step 2 (false positive) with explicit press interaction
// Press event is suppressed in step 1/2 with explicit press interaction
[TestCase(InputEventHandledPolicy.SuppressStateUpdates,
"press",
new int[] { 0, 0, 1, 1, 2},
new int[] { 0, 0, 1, 1, 2},
new int[] {0, 0, 0, 1, 1})]
new int[] { 0, 0, 0, 0, 1},
new int[] { 0, 0, 0, 0, 1},
new int[] {0, 0, 0, 0, 0})]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a behavior change. What is the reason to change this?

Comment on lines +3899 to +3922
private void SuppressActionsForDevice(InputDevice device)
{
if (m_InputEventHandledPolicy != InputEventHandledPolicy.SuppressStateUpdates || device == null)
return;

var deviceIndex = device.m_DeviceIndex;
if (m_SuppressActionsForDeviceUpdate == null || deviceIndex >= m_SuppressActionsForDeviceUpdate.Length)
Array.Resize(ref m_SuppressActionsForDeviceUpdate, deviceIndex + 1);
m_SuppressActionsForDeviceUpdate[deviceIndex] = InputUpdate.s_UpdateStepCount;
}

internal bool ShouldSuppressActionsForDevice(InputDevice device)
{
if (m_InputEventHandledPolicy != InputEventHandledPolicy.SuppressStateUpdates || device == null)
return false;

var deviceIndex = device.m_DeviceIndex;
if (m_SuppressActionsForDeviceUpdate == null || deviceIndex >= m_SuppressActionsForDeviceUpdate.Length)
return false;

var lastUpdate = (int)m_SuppressActionsForDeviceUpdate[deviceIndex];
var currentUpdate = (int)InputUpdate.s_UpdateStepCount;
return lastUpdate == currentUpdate || lastUpdate + 1 == currentUpdate;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure why this code is needed.. Can you clarify what is this doing and why we need it?

Copy link
Collaborator

@jfreire-unity jfreire-unity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, left some comments for clarification

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants