diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index a2ece4afba..2c2621b9c3 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixed an issue where `UIToolkit` `ClickEvent` could be fired on Android after device rotation due to inactive touch state being replayed during action initial state checks [UUM-100125](https://jira.unity3d.com/browse/UUM-100125). - Fixed InputSystem.onAnyButtonPress fails to trigger when the device receives a touch [UUM-137930](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-137930). - Fixed an incorrect ArraysHelper.HaveDuplicateReferences implementation that didn't use its arguments right [ISXB-1792] (https://github.com/Unity-Technologies/InputSystem/pull/2376) +- Fixed input state freezing in `FixedUpdate` (and other reads) after user code resets the player loop with `PlayerLoop.SetPlayerLoop(PlayerLoop.GetDefaultPlayerLoop())` [UUM-140343](https://jira.unity3d.com/browse/UUM-140343). The InputSystem now re-injects its `PlayerLoop.Initialization` hook on the next editor tick if a user-driven reset has wiped it. ### Changed - Removed 32-bit compilation check for HID on Windows players, which had no impact anymore. (ISX-2543) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/NativeInputRuntime.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/NativeInputRuntime.cs index be296bc800..ac2746bc2a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/NativeInputRuntime.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/NativeInputRuntime.cs @@ -148,33 +148,63 @@ public Action onPlayerLoopInitialization // TODO move it to a proper native callback instead if (value != null) { - // Inject ourselves directly to PlayerLoop.Initialization as first subsystem to run, - // Use InputSystemPlayerLoopRunnerInitializationSystem as system type - var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop(); - var initStepIndex = playerLoop.subSystemList.IndexOf(x => x.type == typeof(PlayerLoop.Initialization)); - if (initStepIndex >= 0) - { - var systems = playerLoop.subSystemList[initStepIndex].subSystemList; - - // Check if we're not already injected - if (!systems.Select(x => x.type) - .Contains(typeof(InputSystemPlayerLoopRunnerInitializationSystem))) - { - ArrayHelpers.InsertAt(ref systems, 0, new UnityEngine.LowLevel.PlayerLoopSystem - { - type = typeof(InputSystemPlayerLoopRunnerInitializationSystem), - updateDelegate = () => m_PlayerLoopInitialization?.Invoke() - }); - - playerLoop.subSystemList[initStepIndex].subSystemList = systems; - UnityEngine.LowLevel.PlayerLoop.SetPlayerLoop(playerLoop); - } - } + EnsurePlayerLoopInjection(); + + // Watchdog for UUM-140343: user code calling + // PlayerLoop.SetPlayerLoop(GetDefaultPlayerLoop()) wipes our injection, + // and Unity provides no callback for SetPlayerLoop, no engine API to add to + // the default loop, and no per-frame hook inside the player loop that we + // could anchor to (anything in the loop can be wiped the same way). + // EditorApplication.update is invoked by SceneTracker, not by the player loop + // (see Editor/Src/Selection/SceneInspector.cpp), so it survives a user reset + // and lets us re-inject on the next editor tick. The cost is one cheap + // PlayerLoopSystem-array scan per editor tick while playing -- effectively zero + // when nothing's wrong. Outside play mode the early-return below skips it. + UnityEditor.EditorApplication.update -= EnsurePlayerLoopInjectionIfPlaying; + UnityEditor.EditorApplication.update += EnsurePlayerLoopInjectionIfPlaying; + } + else + { + UnityEditor.EditorApplication.update -= EnsurePlayerLoopInjectionIfPlaying; } m_PlayerLoopInitialization = value; } } + + // Idempotent: inserts InputSystemPlayerLoopRunnerInitializationSystem at the top of + // PlayerLoop.Initialization if it's not already there. + private void EnsurePlayerLoopInjection() + { + var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop(); + var initStepIndex = playerLoop.subSystemList.IndexOf(x => x.type == typeof(PlayerLoop.Initialization)); + if (initStepIndex < 0) + return; + + var systems = playerLoop.subSystemList[initStepIndex].subSystemList; + if (systems.Select(x => x.type) + .Contains(typeof(InputSystemPlayerLoopRunnerInitializationSystem))) + return; // Already injected. + + ArrayHelpers.InsertAt(ref systems, 0, new UnityEngine.LowLevel.PlayerLoopSystem + { + type = typeof(InputSystemPlayerLoopRunnerInitializationSystem), + updateDelegate = () => m_PlayerLoopInitialization?.Invoke() + }); + + playerLoop.subSystemList[initStepIndex].subSystemList = systems; + UnityEngine.LowLevel.PlayerLoop.SetPlayerLoop(playerLoop); + } + + // Watchdog tick. Fires from EditorApplication.update (independent of the player loop) and + // re-injects only when actually needed. In edit mode we don't care; OnPlayerLoopInitialization + // is gated on gameIsPlaying anyway, so a wiped injection while not playing is harmless. + private void EnsurePlayerLoopInjectionIfPlaying() + { + if (!UnityEditor.EditorApplication.isPlaying) + return; + EnsurePlayerLoopInjection(); + } #endif public Action onDeviceDiscovered diff --git a/Packages/com.unity.inputsystem/Tests/IntegrationTests/IntegrationTests.cs b/Packages/com.unity.inputsystem/Tests/IntegrationTests/IntegrationTests.cs index 8d45a2e76e..115a67da74 100644 --- a/Packages/com.unity.inputsystem/Tests/IntegrationTests/IntegrationTests.cs +++ b/Packages/com.unity.inputsystem/Tests/IntegrationTests/IntegrationTests.cs @@ -3,6 +3,7 @@ using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.LowLevel; +using UnityEngine.LowLevel; using UnityEngine.Scripting; using UnityEngine.TestTools; #if UNITY_EDITOR @@ -103,6 +104,54 @@ public void Integration_CanSendAndReceiveEvents() } } + // Regression test for UUM-140343: resetting the player loop to the engine + // default (e.g. user code calling PlayerLoop.SetPlayerLoop(GetDefaultPlayerLoop())) + // wipes any subsystem the InputSystem injected into PlayerLoop.Initialization. + // Without re-injection / a fallback path, input state buffers stop being + // switched correctly between editor and player updates, and FixedUpdate sees + // stale input data. + [UnityTest] + [Category("Integration")] + public IEnumerator Integration_InputUpdatesContinue_AfterResettingPlayerLoopToDefault() + { + var originalPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + var addedMouse = false; + var mouse = InputSystem.GetDevice(); + if (mouse == null) + { + mouse = InputSystem.AddDevice(); + addedMouse = true; + } + + try + { + // Reproduce the user scenario: reset the player loop to default mid-play. + PlayerLoop.SetPlayerLoop(PlayerLoop.GetDefaultPlayerLoop()); + + // Let one frame go by so any re-injection / detection has a chance to run. + yield return null; + + // Queue a left-button-down event for the mouse. + InputSystem.QueueStateEvent(mouse, new MouseState().WithButton(MouseButton.Left)); + + // Wait for the next FixedUpdate phase. The InputSystem should have + // processed the queued event by the time FixedUpdate runs. + yield return new WaitForFixedUpdate(); + + Assert.That(mouse.leftButton.isPressed, Is.True, + "Expected LMB state to be observed in FixedUpdate after PlayerLoop was reset to default. " + + "If this fails with isPressed=False, the bug from UUM-140343 is reproduced."); + } + finally + { + // Restore the player loop so sibling tests are not contaminated. + PlayerLoop.SetPlayerLoop(originalPlayerLoop); + if (addedMouse) + InputSystem.RemoveDevice(mouse); + } + } + #if UNITY_EDITOR [Test]