From 6cff6036e0882aeb7420a682caf2aa385c6b98c8 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Tue, 14 Apr 2026 15:27:47 +0100 Subject: [PATCH 01/14] Add InputManager isReplayActive flag Bypasses several game focused related logic gates to let replay input event reach playmode session. --- .../InputSystem/Runtime/InputManager.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs index 4be52778df..9dbac13300 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs @@ -492,8 +492,18 @@ public bool runPlayerUpdatesInEditMode set => m_RunPlayerUpdatesInEditMode = value; } + /// + /// Number of active instances currently replaying. + /// When greater than zero, focus-based gating is bypassed so that replayed events reach the game + /// regardless of Game View focus. This affects event routing (A), disabled-device discard (B), + /// and UI module processing (C). See ISXB-1319. + /// + internal int m_ActiveReplayCount; + + internal bool isReplayActive => m_ActiveReplayCount > 0; #endif // UNITY_EDITOR + private bool gameIsPlaying => #if UNITY_EDITOR (m_Runtime.isInPlayMode && !m_Runtime.isEditorPaused) || m_RunPlayerUpdatesInEditMode; @@ -504,7 +514,7 @@ public bool runPlayerUpdatesInEditMode private bool gameHasFocus => #if UNITY_EDITOR - m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus; + m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus || isReplayActive; #else applicationHasFocus || gameShouldGetInputRegardlessOfFocus; #endif @@ -3372,7 +3382,9 @@ private unsafe void ProcessEventBuffer(InputUpdateType updateType, ref InputEven // If device is disabled, we let the event through only in certain cases. // Removal and configuration change events should always be processed. - if (device != null && !device.enabled && + // During replay, allow events through for devices disabled due to background + // focus loss — the replay intentionally re-injects events for those devices. + if (device != null && !device.enabled && !isReplayActive && currentEventType != DeviceRemoveEvent.Type && currentEventType != DeviceConfigurationEvent.Type && (device.m_DeviceFlags & (InputDevice.DeviceFlags.DisabledInRuntime | @@ -3411,7 +3423,6 @@ private unsafe void ProcessEventBuffer(InputUpdateType updateType, ref InputEven #endif if (!shouldProcess) { - // Skip event if PreProcessEvent considers it to be irrelevant. m_InputEventStream.Advance(false); continue; } From 1a6a6a29fe2f43c93b78472154ac879e780eeb54 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Tue, 14 Apr 2026 15:28:49 +0100 Subject: [PATCH 02/14] Links ReplayController to InputManager Bit of logics to track replay controllers and bridge with InputManager. --- .../Runtime/Events/InputEventTrace.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index c8b3a40413..16e4056481 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1072,6 +1072,8 @@ public class ReplayController : IDisposable private double m_StartTimeAsPerRuntime; private int m_AllEventsByTimeIndex = 0; private List m_AllEventsByTime; + private bool m_ReplayBypassActive; + private Action m_ClearReplayBypassCallback; internal ReplayController(InputEventTrace trace) { @@ -1088,12 +1090,52 @@ public void Dispose() { InputSystem.onBeforeUpdate -= OnBeginFrame; finished = true; + EndReplayBypass(); foreach (var device in m_CreatedDevices) InputSystem.RemoveDevice(device); m_CreatedDevices = default; } + // Signals InputManager to treat events as if game view has focus, bypassing + // editor focus routing that would otherwise defer pointer/keyboard events to + // editor updates where they reach the editor UI instead of the game. + private void BeginReplayBypass() + { + if (!m_ReplayBypassActive) + { + m_ReplayBypassActive = true; + ++InputSystem.s_Manager.m_ActiveReplayCount; + } + } + + // Schedules the bypass to be cleared after the current OnUpdate finishes processing + // events (via onAfterUpdate). This ensures events already queued in the native buffer + // are still processed with the bypass active before it is removed. + private void ScheduleEndReplayBypass() + { + if (!m_ReplayBypassActive) + return; + + if (m_ClearReplayBypassCallback == null) + m_ClearReplayBypassCallback = EndReplayBypass; + InputSystem.onAfterUpdate += m_ClearReplayBypassCallback; + } + + private void EndReplayBypass() + { + if (m_ClearReplayBypassCallback != null) + { + InputSystem.onAfterUpdate -= m_ClearReplayBypassCallback; + m_ClearReplayBypassCallback = null; + } + if (m_ReplayBypassActive) + { + m_ReplayBypassActive = false; + --InputSystem.s_Manager.m_ActiveReplayCount; + } + } + /// /// Replay events recorded from on device . /// @@ -1249,6 +1291,7 @@ public ReplayController Rewind() public ReplayController PlayAllFramesOneByOne() { finished = false; + BeginReplayBypass(); InputSystem.onBeforeUpdate += OnBeginFrame; return this; } @@ -1267,6 +1310,7 @@ public ReplayController PlayAllFramesOneByOne() public ReplayController PlayAllEvents() { finished = false; + BeginReplayBypass(); try { while (MoveNext(true, out var eventPtr)) @@ -1311,6 +1355,7 @@ public ReplayController PlayAllEventsAccordingToTimestamps() // Start playback. finished = false; + BeginReplayBypass(); m_StartTimeAsPerFirstEvent = -1; m_AllEventsByTimeIndex = -1; InputSystem.onBeforeUpdate += OnBeginFrame; @@ -1381,6 +1426,9 @@ private void Finished() { finished = true; InputSystem.onBeforeUpdate -= OnBeginFrame; + // Schedule bypass removal for after the next OnUpdate, so any events already + // queued into the native buffer this frame are still processed with the bypass active. + ScheduleEndReplayBypass(); m_OnFinished?.Invoke(); } From d7fa732721ceefd5c5ee9f6a6a269b0d1b969724 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Tue, 14 Apr 2026 15:29:21 +0100 Subject: [PATCH 03/14] Bypass early out UIInputModule processing Uses InputManager.isReplayActive flag --- .../Runtime/Plugins/UI/InputSystemUIInputModule.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs index 0108a76cec..0395f15fcc 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs @@ -997,7 +997,11 @@ private bool shouldIgnoreFocus // if running in the background is enabled, we already have rules in place what kind of input // is allowed through and what isn't. And for the input that *IS* allowed through, the UI should // react. - get => explictlyIgnoreFocus || InputRuntime.s_Instance.runInBackground; + get => explictlyIgnoreFocus || InputRuntime.s_Instance.runInBackground +#if UNITY_EDITOR + || InputSystem.s_Manager.isReplayActive +#endif + ; } /// From d0f16d84e9b9358568f8001bf99de34174e4bc12 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Wed, 15 Apr 2026 15:05:26 +0100 Subject: [PATCH 04/14] Update Events.md --- Packages/com.unity.inputsystem/Documentation~/Events.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Packages/com.unity.inputsystem/Documentation~/Events.md b/Packages/com.unity.inputsystem/Documentation~/Events.md index f93ce87ba1..c9f29e6b80 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Events.md +++ b/Packages/com.unity.inputsystem/Documentation~/Events.md @@ -223,6 +223,9 @@ trace.Dispose(); Dispose event traces after use, so that they do not leak memory on the unmanaged (C++) memory heap. +> [!NOTE] +> **Keyboard text input is not replayed to UI text fields.** Keyboard state (key presses) is captured and replayed correctly and remains accessible via `Keyboard.current`. However there is a known limitation with character delivery to UI Framework components (uGUI `InputField` or UIToolkit `TextField`). These components receive text through a separate native pipeline that is not fed by event replay. As a result, text typed into UI text fields during recording will not appear during playback. + You can also write event traces out to files/streams, load them back in, and replay recorded streams. ```CSharp From c2921495a949273cdf3222ec430e004ca424e606 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Wed, 15 Apr 2026 15:05:52 +0100 Subject: [PATCH 05/14] Update CHANGELOG.md --- Packages/com.unity.inputsystem/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index b242446471..b2f58b895e 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed +- Fixed `InputRecorder` playback not starting when the Game View is not focused in the Editor [ISXB-1319](https://jira.unity3d.com/browse/ISXB-1319) - Fixed a `NullReferenceException` thrown when removing all action maps [UUM-137116](https://jira.unity3d.com/browse/UUM-137116) - Simplified default setting messaging by consolidating repetitive messages into a single HelpBox. - Fixed a `NullPointerReferenceException` thrown in `InputManagerStateMonitors.FireStateChangeNotifications` logging by adding validation [UUM-136095]. From 41691cb30ae0b69fbe01a5bb3d1e31565e4d8a18 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Wed, 15 Apr 2026 15:07:18 +0100 Subject: [PATCH 06/14] format --- .../Runtime/Plugins/UI/InputSystemUIInputModule.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs index 0395f15fcc..22df32fae8 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs @@ -999,9 +999,9 @@ private bool shouldIgnoreFocus // react. get => explictlyIgnoreFocus || InputRuntime.s_Instance.runInBackground #if UNITY_EDITOR - || InputSystem.s_Manager.isReplayActive + || InputSystem.s_Manager.isReplayActive #endif - ; + ; } /// From 89ffe5f86e455c04f07c7814557a0525bb596406 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Wed, 15 Apr 2026 15:17:58 +0100 Subject: [PATCH 07/14] Adds missing UNITY_EDITOR define WinPlayer build made to ensure compilation --- .../Runtime/Events/InputEventTrace.cs | 16 ++++++++++++++-- .../InputSystem/Runtime/InputManager.cs | 5 ++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index 16e4056481..4ae6b0a5a4 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1072,8 +1072,10 @@ public class ReplayController : IDisposable private double m_StartTimeAsPerRuntime; private int m_AllEventsByTimeIndex = 0; private List m_AllEventsByTime; +#if UNITY_EDITOR private bool m_ReplayBypassActive; private Action m_ClearReplayBypassCallback; +#endif internal ReplayController(InputEventTrace trace) { @@ -1090,13 +1092,15 @@ public void Dispose() { InputSystem.onBeforeUpdate -= OnBeginFrame; finished = true; +#if UNITY_EDITOR EndReplayBypass(); - +#endif foreach (var device in m_CreatedDevices) InputSystem.RemoveDevice(device); m_CreatedDevices = default; } +#if UNITY_EDITOR // Signals InputManager to treat events as if game view has focus, bypassing // editor focus routing that would otherwise defer pointer/keyboard events to // editor updates where they reach the editor UI instead of the game. @@ -1135,7 +1139,7 @@ private void EndReplayBypass() --InputSystem.s_Manager.m_ActiveReplayCount; } } - +#endif /// /// Replay events recorded from on device . /// @@ -1291,7 +1295,9 @@ public ReplayController Rewind() public ReplayController PlayAllFramesOneByOne() { finished = false; +#if UNITY_EDITOR BeginReplayBypass(); +#endif InputSystem.onBeforeUpdate += OnBeginFrame; return this; } @@ -1310,7 +1316,9 @@ public ReplayController PlayAllFramesOneByOne() public ReplayController PlayAllEvents() { finished = false; +#if UNITY_EDITOR BeginReplayBypass(); +#endif try { while (MoveNext(true, out var eventPtr)) @@ -1355,7 +1363,9 @@ public ReplayController PlayAllEventsAccordingToTimestamps() // Start playback. finished = false; +#if UNITY_EDITOR BeginReplayBypass(); +#endif m_StartTimeAsPerFirstEvent = -1; m_AllEventsByTimeIndex = -1; InputSystem.onBeforeUpdate += OnBeginFrame; @@ -1426,9 +1436,11 @@ private void Finished() { finished = true; InputSystem.onBeforeUpdate -= OnBeginFrame; +#if UNITY_EDITOR // Schedule bypass removal for after the next OnUpdate, so any events already // queued into the native buffer this frame are still processed with the bypass active. ScheduleEndReplayBypass(); +#endif m_OnFinished?.Invoke(); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs index 9dbac13300..c1290cb3db 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs @@ -3384,7 +3384,10 @@ private unsafe void ProcessEventBuffer(InputUpdateType updateType, ref InputEven // Removal and configuration change events should always be processed. // During replay, allow events through for devices disabled due to background // focus loss — the replay intentionally re-injects events for those devices. - if (device != null && !device.enabled && !isReplayActive && + if (device != null && !device.enabled && +#if UNITY_EDITOR + !isReplayActive && +#endif currentEventType != DeviceRemoveEvent.Type && currentEventType != DeviceConfigurationEvent.Type && (device.m_DeviceFlags & (InputDevice.DeviceFlags.DisabledInRuntime | From fa519b62949af8fddb16802dc9c4130737a8a7d9 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Wed, 15 Apr 2026 17:11:05 +0100 Subject: [PATCH 08/14] Address PR feedback --- .../InputSystem/Runtime/Events/InputEventTrace.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index 4ae6b0a5a4..7a6523a04d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1106,6 +1106,11 @@ public void Dispose() // editor updates where they reach the editor UI instead of the game. private void BeginReplayBypass() { + if (m_ClearReplayBypassCallback != null) + { + InputSystem.onAfterUpdate -= m_ClearReplayBypassCallback; + } + if (!m_ReplayBypassActive) { m_ReplayBypassActive = true; @@ -1121,8 +1126,12 @@ private void ScheduleEndReplayBypass() if (!m_ReplayBypassActive) return; - if (m_ClearReplayBypassCallback == null) - m_ClearReplayBypassCallback = EndReplayBypass; + if (m_ClearReplayBypassCallback != null) + { + return; + } + + m_ClearReplayBypassCallback = EndReplayBypass; InputSystem.onAfterUpdate += m_ClearReplayBypassCallback; } From 082be337794bce3de5032cf15343f5b94311d8b7 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Wed, 15 Apr 2026 17:29:05 +0100 Subject: [PATCH 09/14] format --- .../InputSystem/Runtime/Events/InputEventTrace.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index 7a6523a04d..03c81baa50 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1148,6 +1148,7 @@ private void EndReplayBypass() --InputSystem.s_Manager.m_ActiveReplayCount; } } + #endif /// /// Replay events recorded from on device . From f5bbd8b69f17d40042f8d9231379700f6fb1ad72 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau <122548697+MorganHoarau@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:57:43 +0100 Subject: [PATCH 10/14] Apply suggestion on Doc --- Packages/com.unity.inputsystem/Documentation~/Events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/Documentation~/Events.md b/Packages/com.unity.inputsystem/Documentation~/Events.md index c9f29e6b80..a5fb7ecbc5 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Events.md +++ b/Packages/com.unity.inputsystem/Documentation~/Events.md @@ -224,7 +224,7 @@ trace.Dispose(); Dispose event traces after use, so that they do not leak memory on the unmanaged (C++) memory heap. > [!NOTE] -> **Keyboard text input is not replayed to UI text fields.** Keyboard state (key presses) is captured and replayed correctly and remains accessible via `Keyboard.current`. However there is a known limitation with character delivery to UI Framework components (uGUI `InputField` or UIToolkit `TextField`). These components receive text through a separate native pipeline that is not fed by event replay. As a result, text typed into UI text fields during recording will not appear during playback. +> **Keyboard text input is not replayed to UI text fields.** Keyboard state (key presses) is captured and replayed correctly and remains accessible via `Keyboard.current`. However, there is a known limitation with character delivery to UI Framework components (uGUI `InputField` or UI Toolkit `TextField`). These components receive text through a separate native pipeline that is not fed by event replay. As a result, text typed into UI text fields during recording will not appear during playback. You can also write event traces out to files/streams, load them back in, and replay recorded streams. From a6394d759e2d996b5cbb0c5a0ed6d5a848aac36b Mon Sep 17 00:00:00 2001 From: Morgan Hoarau <122548697+MorganHoarau@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:07:28 +0100 Subject: [PATCH 11/14] Adds null check in ReplayController disposal Co-authored-by: u-pr[bot] <205906871+u-pr[bot]@users.noreply.github.com> --- .../InputSystem/Runtime/Events/InputEventTrace.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index 03c81baa50..108e811122 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1145,7 +1145,8 @@ private void EndReplayBypass() if (m_ReplayBypassActive) { m_ReplayBypassActive = false; - --InputSystem.s_Manager.m_ActiveReplayCount; + if (InputSystem.s_Manager != null) + --InputSystem.s_Manager.m_ActiveReplayCount; } } From a4fb82a33d1d49beda0c6d636e921754d046089a Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Fri, 1 May 2026 19:57:08 +0100 Subject: [PATCH 12/14] Rework impl design Renamed concepts and created static InputManager API similar to AssetDatabase.StartAssetEditing. keeps it internal and automatic --- .../Runtime/Events/InputEventTrace.cs | 75 ++++++++----------- .../InputSystem/Runtime/InputManager.cs | 42 +++++++++-- .../Plugins/UI/InputSystemUIInputModule.cs | 2 +- 3 files changed, 66 insertions(+), 53 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index 108e811122..b5151b16ff 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1073,8 +1073,7 @@ public class ReplayController : IDisposable private int m_AllEventsByTimeIndex = 0; private List m_AllEventsByTime; #if UNITY_EDITOR - private bool m_ReplayBypassActive; - private Action m_ClearReplayBypassCallback; + private bool m_EditorEventPassthroughActive; #endif internal ReplayController(InputEventTrace trace) @@ -1093,7 +1092,7 @@ public void Dispose() InputSystem.onBeforeUpdate -= OnBeginFrame; finished = true; #if UNITY_EDITOR - EndReplayBypass(); + StopEditorEventPassthrough(); #endif foreach (var device in m_CreatedDevices) InputSystem.RemoveDevice(device); @@ -1101,55 +1100,41 @@ public void Dispose() } #if UNITY_EDITOR - // Signals InputManager to treat events as if game view has focus, bypassing - // editor focus routing that would otherwise defer pointer/keyboard events to - // editor updates where they reach the editor UI instead of the game. - private void BeginReplayBypass() + private void StartEditorEventPassthrough() { - if (m_ClearReplayBypassCallback != null) + if (!m_EditorEventPassthroughActive) { - InputSystem.onAfterUpdate -= m_ClearReplayBypassCallback; - } - - if (!m_ReplayBypassActive) - { - m_ReplayBypassActive = true; - ++InputSystem.s_Manager.m_ActiveReplayCount; + m_EditorEventPassthroughActive = true; + InputManager.StartEditorEventPassthrough(); } } - // Schedules the bypass to be cleared after the current OnUpdate finishes processing - // events (via onAfterUpdate). This ensures events already queued in the native buffer - // are still processed with the bypass active before it is removed. - private void ScheduleEndReplayBypass() + private void StopEditorEventPassthrough() { - if (!m_ReplayBypassActive) - return; + // Clean up any pending deferred stop. + InputSystem.onAfterUpdate -= DeferredStopEditorEventPassthrough; - if (m_ClearReplayBypassCallback != null) + if (m_EditorEventPassthroughActive) { - return; + m_EditorEventPassthroughActive = false; + InputManager.StopEditorEventPassthrough(); } - - m_ClearReplayBypassCallback = EndReplayBypass; - InputSystem.onAfterUpdate += m_ClearReplayBypassCallback; } - private void EndReplayBypass() + // Defers passthrough stop to after the current update so events already queued + // in the native buffer are still processed with passthrough active. + private void ScheduleStopEditorEventPassthrough() { - if (m_ClearReplayBypassCallback != null) - { - InputSystem.onAfterUpdate -= m_ClearReplayBypassCallback; - m_ClearReplayBypassCallback = null; - } - if (m_ReplayBypassActive) - { - m_ReplayBypassActive = false; - if (InputSystem.s_Manager != null) - --InputSystem.s_Manager.m_ActiveReplayCount; - } + if (!m_EditorEventPassthroughActive) + return; + InputSystem.onAfterUpdate += DeferredStopEditorEventPassthrough; } + private void DeferredStopEditorEventPassthrough() + { + InputSystem.onAfterUpdate -= DeferredStopEditorEventPassthrough; + StopEditorEventPassthrough(); + } #endif /// /// Replay events recorded from on device . @@ -1307,8 +1292,9 @@ public ReplayController PlayAllFramesOneByOne() { finished = false; #if UNITY_EDITOR - BeginReplayBypass(); + StartEditorEventPassthrough(); #endif + m_OnReplayStart?.Invoke(); InputSystem.onBeforeUpdate += OnBeginFrame; return this; } @@ -1328,8 +1314,9 @@ public ReplayController PlayAllEvents() { finished = false; #if UNITY_EDITOR - BeginReplayBypass(); + StartEditorEventPassthrough(); #endif + m_OnReplayStart?.Invoke(); try { while (MoveNext(true, out var eventPtr)) @@ -1375,7 +1362,7 @@ public ReplayController PlayAllEventsAccordingToTimestamps() // Start playback. finished = false; #if UNITY_EDITOR - BeginReplayBypass(); + StartEditorEventPassthrough(); #endif m_StartTimeAsPerFirstEvent = -1; m_AllEventsByTimeIndex = -1; @@ -1448,9 +1435,9 @@ private void Finished() finished = true; InputSystem.onBeforeUpdate -= OnBeginFrame; #if UNITY_EDITOR - // Schedule bypass removal for after the next OnUpdate, so any events already - // queued into the native buffer this frame are still processed with the bypass active. - ScheduleEndReplayBypass(); + // Defer passthrough stop so events already queued in the native buffer + // this frame are still processed with passthrough active. + ScheduleStopEditorEventPassthrough(); #endif m_OnFinished?.Invoke(); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs index c1290cb3db..7033478b7e 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs @@ -493,14 +493,40 @@ public bool runPlayerUpdatesInEditMode } /// - /// Number of active instances currently replaying. - /// When greater than zero, focus-based gating is bypassed so that replayed events reach the game - /// regardless of Game View focus. This affects event routing (A), disabled-device discard (B), - /// and UI module processing (C). See ISXB-1319. + /// Ref-counted flag that bypasses Game View focus gating for event processing. + /// When greater than zero, events are processed as if the Game View has focus, + /// regardless of actual focus state. This affects event routing, disabled-device + /// discard, and UI module processing. /// - internal int m_ActiveReplayCount; + /// + /// Use / + /// to manage this counter. Follows the same pattern as + /// AssetDatabase.StartAssetEditing/StopAssetEditing. + /// + /// + /// + private int m_EditorEventPassthroughCount; + + internal bool isEditorEventPassthroughActive => m_EditorEventPassthroughCount > 0; + + /// + /// Signals that events should bypass Game View focus gating. Ref-counted: + /// each call must be balanced by a corresponding . + /// + internal static void StartEditorEventPassthrough() + { + ++s_Manager.m_EditorEventPassthroughCount; + } - internal bool isReplayActive => m_ActiveReplayCount > 0; + /// + /// Signals that the caller no longer needs events to bypass Game View focus gating. + /// Decrements the ref count started by . + /// + internal static void StopEditorEventPassthrough() + { + if (s_Manager != null && s_Manager.m_EditorEventPassthroughCount > 0) + --s_Manager.m_EditorEventPassthroughCount; + } #endif // UNITY_EDITOR @@ -514,7 +540,7 @@ public bool runPlayerUpdatesInEditMode private bool gameHasFocus => #if UNITY_EDITOR - m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus || isReplayActive; + m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus || isEditorEventPassthroughActive; #else applicationHasFocus || gameShouldGetInputRegardlessOfFocus; #endif @@ -3386,7 +3412,7 @@ private unsafe void ProcessEventBuffer(InputUpdateType updateType, ref InputEven // focus loss — the replay intentionally re-injects events for those devices. if (device != null && !device.enabled && #if UNITY_EDITOR - !isReplayActive && + !isEditorEventPassthroughActive && #endif currentEventType != DeviceRemoveEvent.Type && currentEventType != DeviceConfigurationEvent.Type && diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs index 22df32fae8..bcd42b9332 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs @@ -999,7 +999,7 @@ private bool shouldIgnoreFocus // react. get => explictlyIgnoreFocus || InputRuntime.s_Instance.runInBackground #if UNITY_EDITOR - || InputSystem.s_Manager.isReplayActive + || InputSystem.s_Manager.isEditorEventPassthroughActive #endif ; } From 06747613185360e037d383dee82c3e205dda29fe Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Fri, 1 May 2026 20:03:34 +0100 Subject: [PATCH 13/14] Commit missing local changes --- .../InputSystem/Runtime/Events/InputEventTrace.cs | 2 -- .../InputSystem/Runtime/InputManager.cs | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index b5151b16ff..e8b21afcd2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1294,7 +1294,6 @@ public ReplayController PlayAllFramesOneByOne() #if UNITY_EDITOR StartEditorEventPassthrough(); #endif - m_OnReplayStart?.Invoke(); InputSystem.onBeforeUpdate += OnBeginFrame; return this; } @@ -1316,7 +1315,6 @@ public ReplayController PlayAllEvents() #if UNITY_EDITOR StartEditorEventPassthrough(); #endif - m_OnReplayStart?.Invoke(); try { while (MoveNext(true, out var eventPtr)) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs index 7033478b7e..9b3c10e00f 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs @@ -515,7 +515,7 @@ public bool runPlayerUpdatesInEditMode /// internal static void StartEditorEventPassthrough() { - ++s_Manager.m_EditorEventPassthroughCount; + ++InputSystem.s_Manager.m_EditorEventPassthroughCount; } /// @@ -524,8 +524,8 @@ internal static void StartEditorEventPassthrough() /// internal static void StopEditorEventPassthrough() { - if (s_Manager != null && s_Manager.m_EditorEventPassthroughCount > 0) - --s_Manager.m_EditorEventPassthroughCount; + if (InputSystem.s_Manager != null && InputSystem.s_Manager.m_EditorEventPassthroughCount > 0) + --InputSystem.s_Manager.m_EditorEventPassthroughCount; } #endif // UNITY_EDITOR From 820619d01b9c19204305c9b97f141a6eef2abe62 Mon Sep 17 00:00:00 2001 From: Morgan Hoarau Date: Fri, 1 May 2026 20:05:50 +0100 Subject: [PATCH 14/14] formatter --- .../InputSystem/Runtime/Events/InputEventTrace.cs | 1 + .../com.unity.inputsystem/InputSystem/Runtime/InputManager.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index e8b21afcd2..8d7c1dd688 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1135,6 +1135,7 @@ private void DeferredStopEditorEventPassthrough() InputSystem.onAfterUpdate -= DeferredStopEditorEventPassthrough; StopEditorEventPassthrough(); } + #endif /// /// Replay events recorded from on device . diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs index 9b3c10e00f..431479d431 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs @@ -527,6 +527,7 @@ internal static void StopEditorEventPassthrough() if (InputSystem.s_Manager != null && InputSystem.s_Manager.m_EditorEventPassthroughCount > 0) --InputSystem.s_Manager.m_EditorEventPassthroughCount; } + #endif // UNITY_EDITOR