diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index fa24a4d915..529484cce7 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -631,7 +631,9 @@ public void Actions_DoNotGetTriggeredByEditorUpdates() using (var trace = new InputActionTrace(action)) { - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); + Set(gamepad.leftTrigger, 0.123f, queueEventOnly: true); InputSystem.Update(InputUpdateType.Editor); @@ -661,13 +663,13 @@ public void Actions_DoNotGetTriggeredByOutOfFocusEventInEditor(InputSettings.Bac // could just rely on order of event. Which means this test work for a fixed timestamp and it should // changed accordingly. currentTime += 1.0f; - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); currentTime += 1.0f; // Queuing an event like it would be in the editor when the GameView is out of focus. Set(mouse.position, new Vector2(0.234f, 0.345f) , queueEventOnly: true); currentTime += 1.0f; // Gaining focus like it would happen in the editor when the GameView regains focus. - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); currentTime += 1.0f; // This emulates a device sync that happens when the player regains focus through an IOCTL command. // That's why it also has it's time incremented. @@ -720,14 +722,15 @@ public void Actions_TimeoutsDoNotGetTriggeredInEditorUpdates() trace.Clear(); - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); currentTime = 10; InputSystem.Update(InputUpdateType.Editor); Assert.That(trace, Is.Empty); - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); InputSystem.Update(InputUpdateType.Dynamic); actions = trace.ToArray(); diff --git a/Assets/Tests/InputSystem/CoreTests_Devices.cs b/Assets/Tests/InputSystem/CoreTests_Devices.cs index d75cfeed1a..ed90efe605 100644 --- a/Assets/Tests/InputSystem/CoreTests_Devices.cs +++ b/Assets/Tests/InputSystem/CoreTests_Devices.cs @@ -19,6 +19,7 @@ using UnityEngine.Scripting; using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; +using UnityEngineInternal.Input; using Gyroscope = UnityEngine.InputSystem.Gyroscope; using UnityEngine.TestTools.Constraints; using Is = NUnit.Framework.Is; @@ -1522,7 +1523,7 @@ public void Devices_CanReconnectDevice_WhenDisconnectedWhileAppIsOutOfFocus() Assert.That(device, Is.Not.Null); // Loose focus. - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); InputSystem.Update(); // Disconnect. @@ -1534,7 +1535,7 @@ public void Devices_CanReconnectDevice_WhenDisconnectedWhileAppIsOutOfFocus() Assert.That(InputSystem.devices, Is.Empty); // Regain focus. - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); InputSystem.Update(); var newDeviceId = runtime.ReportNewInputDevice(deviceDesc); @@ -4604,7 +4605,13 @@ void DeviceChangeCallback(InputDevice device, InputDeviceChange change) InputSystem.onDeviceChange += DeviceChangeCallback; var eventCount = 0; - InputSystem.onEvent += (eventPtr, _) => ++ eventCount; + InputSystem.onEvent += (eventPtr, _) => + { + // Focus events will always be processed no matter the state + // Since the test relies on counting events based on state, dont count focus events + if (eventPtr.data->type != (FourCC)FocusConstants.kEventType) + ++eventCount; + }; Assert.That(trackedDevice.enabled, Is.True); Assert.That(mouse.enabled, Is.True); @@ -4647,7 +4654,8 @@ void DeviceChangeCallback(InputDevice device, InputDeviceChange change) } // Lose focus. - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); Assert.That(sensor.enabled, Is.False); Assert.That(disabledDevice.enabled, Is.False); @@ -5068,7 +5076,8 @@ void DeviceChangeCallback(InputDevice device, InputDeviceChange change) commands.Clear(); // Regain focus. - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); + InputSystem.Update(InputUpdateType.Dynamic); Assert.That(sensor.enabled, Is.False); Assert.That(disabledDevice.enabled, Is.False); @@ -5275,13 +5284,10 @@ void DeviceChangeCallback(InputDevice device, InputDeviceChange change) "Sync Gamepad", "Sync Joystick", "Sync TrackedDevice", "Sync TrackedDevice2", "Sync Mouse", "Sync Mouse2", "Sync Mouse3", - "Sync Keyboard", "Reset Joystick" - })); - // Enabled devices that don't support syncs get reset. - Assert.That(changes, Is.EquivalentTo(new[] - { - "SoftReset Mouse1", "SoftReset Mouse3", "HardReset Joystick", "SoftReset TrackedDevice2" + "Sync Keyboard" })); + // Enabled devices that don't support syncs dont get reset for Ignore Focus as we do not want to cancel any actions. + Assert.That(changes, Is.Empty); break; } } @@ -5318,7 +5324,13 @@ public void Devices_CanSkipProcessingEventsWhileInBackground() Assert.That(performedCount, Is.EqualTo(1)); // Lose focus - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + // in the new system, we have to process the focus event to update the state of the devices. + // In the old system, this wouldn't work and would make the test fal + InputSystem.Update(); +#endif + Assert.That(gamepad.enabled, Is.False); // Queue an event while in the background. We don't want to see this event to be processed once focus @@ -5329,7 +5341,7 @@ public void Devices_CanSkipProcessingEventsWhileInBackground() InputSystem.Update(); // Gain focus - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); // Run update to try process events accordingly once focus is gained InputSystem.Update(); diff --git a/Assets/Tests/InputSystem/CoreTests_Editor.cs b/Assets/Tests/InputSystem/CoreTests_Editor.cs index 8cd65dd586..3e77d68c20 100644 --- a/Assets/Tests/InputSystem/CoreTests_Editor.cs +++ b/Assets/Tests/InputSystem/CoreTests_Editor.cs @@ -2720,7 +2720,8 @@ public void Editor_CanForceKeyboardAndMouseInputToGameViewWithoutFocus() var keyboard = InputSystem.AddDevice(); var mouse = InputSystem.AddDevice(); - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); Assert.That(keyboard.enabled, Is.True); Assert.That(mouse.enabled, Is.True); @@ -3016,7 +3017,8 @@ public void Editor_LeavingPlayMode_ReenablesAllDevicesTemporarilyDisabledDueToFo Set(mouse.position, new Vector2(123, 234)); Press(gamepad.buttonSouth); - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); Assert.That(gamepad.enabled, Is.False); diff --git a/Assets/Tests/InputSystem/CoreTests_State.cs b/Assets/Tests/InputSystem/CoreTests_State.cs index 12c185d9ab..ed5d59d8aa 100644 --- a/Assets/Tests/InputSystem/CoreTests_State.cs +++ b/Assets/Tests/InputSystem/CoreTests_State.cs @@ -704,7 +704,9 @@ public void State_CanSetUpMonitorsForStateChanges_InEditor() InputState.AddChangeMonitor(gamepad.leftStick, (control, time, eventPtr, monitorIndex) => monitorFired = true); - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); + Set(gamepad.leftStick, new Vector2(0.123f, 0.234f), queueEventOnly: true); InputSystem.Update(InputUpdateType.Editor); @@ -1676,7 +1678,9 @@ public void State_RecordingHistory_ExcludesEditorInputByDefault() { history.StartRecording(); - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); + Set(gamepad.leftTrigger, 0.123f, queueEventOnly: true); InputSystem.Update(InputUpdateType.Editor); @@ -1696,7 +1700,9 @@ public void State_RecordingHistory_CanCaptureEditorInput() history.updateMask = InputUpdateType.Editor; history.StartRecording(); - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); + Set(gamepad.leftTrigger, 0.123f, queueEventOnly: true); InputSystem.Update(InputUpdateType.Editor); diff --git a/Assets/Tests/InputSystem/Plugins/EnhancedTouchTests.cs b/Assets/Tests/InputSystem/Plugins/EnhancedTouchTests.cs index 29a11eb22e..f7a58b3310 100644 --- a/Assets/Tests/InputSystem/Plugins/EnhancedTouchTests.cs +++ b/Assets/Tests/InputSystem/Plugins/EnhancedTouchTests.cs @@ -158,11 +158,10 @@ public void EnhancedTouch_SupportsEditorUpdates(InputSettings.UpdateMode updateM Assert.That(Touch.activeTouches, Has.Count.EqualTo(1)); // And make sure we're not seeing the data in the editor. - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); InputSystem.Update(InputUpdateType.Editor); Assert.That(Touch.activeTouches, Is.Empty); - // Feed some data into editor state. BeginTouch(2, new Vector2(0.234f, 0.345f), queueEventOnly: true); InputSystem.Update(InputUpdateType.Editor); @@ -171,8 +170,25 @@ public void EnhancedTouch_SupportsEditorUpdates(InputSettings.UpdateMode updateM Assert.That(Touch.activeTouches[0].touchId, Is.EqualTo(2)); // Switch back to player. - runtime.PlayerFocusGained(); - InputSystem.Update(); + ScheduleFocusChangedEvent(applicationHasFocus: true); + + // Explicitly schedule the player's configured update type rather than relying on the default. + // Without explicit scheduling, defaultUpdateType would be Editor (since focus has not yet been + // gained during update), causing the editor buffer to be used instead of the player buffer, + // which would retrieve the wrong active touch. A proper fix would require removing defaultUpdateType + // and splitting player/editor update loops into separate methods. + switch (updateMode) + { + case InputSettings.UpdateMode.ProcessEventsInDynamicUpdate: + InputSystem.Update(InputUpdateType.Dynamic); + break; + case InputSettings.UpdateMode.ProcessEventsInFixedUpdate: + InputSystem.Update(InputUpdateType.Fixed); + break; + case InputSettings.UpdateMode.ProcessEventsManually: + InputSystem.Update(InputUpdateType.Manual); + break; + } Assert.That(Touch.activeTouches, Has.Count.EqualTo(1)); Assert.That(Touch.activeTouches[0].touchId, Is.EqualTo(1)); @@ -1160,7 +1176,7 @@ public void EnhancedTouch_ActiveTouchesGetCanceledOnFocusLoss_WithRunInBackgroun Assert.That(Touch.activeTouches, Has.Count.EqualTo(1)); Assert.That(Touch.activeTouches[0].phase, Is.EqualTo(TouchPhase.Began)); - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); if (runInBackground) { @@ -1171,7 +1187,7 @@ public void EnhancedTouch_ActiveTouchesGetCanceledOnFocusLoss_WithRunInBackgroun else { // When not running in the background, the same thing happens but only on focus gain. - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); InputSystem.Update(); } diff --git a/Assets/Tests/InputSystem/Plugins/InputForUITests.cs b/Assets/Tests/InputSystem/Plugins/InputForUITests.cs index 21bbc89767..0cb49e7171 100644 --- a/Assets/Tests/InputSystem/Plugins/InputForUITests.cs +++ b/Assets/Tests/InputSystem/Plugins/InputForUITests.cs @@ -715,11 +715,11 @@ public void UIActions_DoNotGetTriggeredByOutOfFocusEventInEditor(InputSettings.B currentTime += 1.0f; Update(); currentTime += 1.0f; - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); currentTime += 1.0f; Set(mouse.position, outOfFocusPosition , queueEventOnly: true); currentTime += 1.0f; - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); currentTime += 1.0f; Set(mouse.position, focusPosition, queueEventOnly: true); currentTime += 1.0f; @@ -727,7 +727,7 @@ public void UIActions_DoNotGetTriggeredByOutOfFocusEventInEditor(InputSettings.B // We call specific updates to simulate editor behavior when regaining focus. InputSystem.Update(InputUpdateType.Editor); Assert.AreEqual(0, m_InputForUIEvents.Count); - InputSystem.Update(); + InputSystem.Update(InputUpdateType.Dynamic); // Calling the event provider update after we call InputSystem updates so that we trigger InputForUI events EventProvider.NotifyUpdate(); diff --git a/Assets/Tests/InputSystem/Plugins/UITests.cs b/Assets/Tests/InputSystem/Plugins/UITests.cs index 15f17299ea..5bcf2b72df 100644 --- a/Assets/Tests/InputSystem/Plugins/UITests.cs +++ b/Assets/Tests/InputSystem/Plugins/UITests.cs @@ -4128,7 +4128,9 @@ public IEnumerator UI_WhenAppLosesAndRegainsFocus_WhileUIButtonIsPressed_UIButto scene.leftChildReceiver.events.Clear(); - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); + if (canRunInBackground) Assert.That(clickCanceled, Is.EqualTo(0)); else @@ -4139,7 +4141,9 @@ public IEnumerator UI_WhenAppLosesAndRegainsFocus_WhileUIButtonIsPressed_UIButto Assert.That(scene.eventSystem.hasFocus, Is.False); Assert.That(clicked, Is.False); - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); + InputSystem.Update(InputUpdateType.Dynamic); + scene.eventSystem.SendMessage("OnApplicationFocus", true); yield return null; @@ -4168,11 +4172,13 @@ public IEnumerator UI_WhenAppLosesAndRegainsFocus_WhileUIButtonIsPressed_UIButto // Ensure that losing and regaining focus doesn't cause the next click to be ignored clicked = false; - runtime.PlayerFocusLost(); + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); scene.eventSystem.SendMessage("OnApplicationFocus", false); yield return null; - runtime.PlayerFocusGained(); + ScheduleFocusChangedEvent(applicationHasFocus: true); + InputSystem.Update(InputUpdateType.Dynamic); scene.eventSystem.SendMessage("OnApplicationFocus", true); yield return null; diff --git a/Assets/Tests/InputSystem/Plugins/UserTests.cs b/Assets/Tests/InputSystem/Plugins/UserTests.cs index 1a8e13e776..5bb3b830d5 100644 --- a/Assets/Tests/InputSystem/Plugins/UserTests.cs +++ b/Assets/Tests/InputSystem/Plugins/UserTests.cs @@ -1268,8 +1268,21 @@ public void Users_DoNotReactToEditorInput() ++InputUser.listenForUnpairedDeviceActivity; InputUser.onUnpairedDeviceUsed += (control, eventPtr) => Assert.Fail("Should not react!"); - runtime.PlayerFocusLost(); - + // Process the focus event before pressing the button to ensure correct update type selection. + // + // Issue: When Update() is called without an update type, it uses defaultUpdateType which checks + // focus state. However, scheduled focus events aren't processed until an update runs, so the + // focus check sees stale state and selects the wrong update type. + // + // Workaround: Run a dynamic update first to process the focus event, ensuring the subsequent + // button press correctly uses editor update type. + // + // Alternative: Queue the button press and explicitly call an editor update to process both events. + // + // Proper fix: Remove defaultUpdateType and split editor/player loops, or always specify the + // update type explicitly when calling Update(). + ScheduleFocusChangedEvent(applicationHasFocus: false); + InputSystem.Update(InputUpdateType.Dynamic); Press(gamepad.buttonSouth); Assert.That(gamepad.buttonSouth.isPressed, Is.True); diff --git a/Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef b/Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef index 6510b4106f..1c823a2c87 100644 --- a/Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef +++ b/Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef @@ -70,6 +70,11 @@ "name": "Unity", "expression": "6000.3.0a6", "define": "UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY" + }, + { + "name": "Unity", + "expression": "6000.5.0a8", + "define": "UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS" } ], "noEngineReferences": false diff --git a/Packages/com.unity.inputsystem/InputSystem/IInputRuntime.cs b/Packages/com.unity.inputsystem/InputSystem/IInputRuntime.cs index f7851f12f0..8b1fb7cd61 100644 --- a/Packages/com.unity.inputsystem/InputSystem/IInputRuntime.cs +++ b/Packages/com.unity.inputsystem/InputSystem/IInputRuntime.cs @@ -2,6 +2,7 @@ using Unity.Collections.LowLevel.Unsafe; using UnityEngine.Analytics; using UnityEngine.InputSystem.Layouts; +using UnityEngineInternal.Input; #if UNITY_EDITOR using UnityEditor; @@ -109,12 +110,15 @@ internal unsafe interface IInputRuntime /// Action onDeviceDiscovered { get; set; } +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS /// /// Set delegate to call when the application changes focus. /// /// Action onPlayerFocusChanged { get; set; } +#endif + FocusFlags focusState { get; set; } /// // Is true when the player or game view has focus. /// @@ -183,11 +187,11 @@ internal unsafe interface IInputRuntime // If analytics are enabled, the runtime receives analytics events from the input manager. // See InputAnalytics. - #if UNITY_ANALYTICS || UNITY_EDITOR +#if UNITY_ANALYTICS || UNITY_EDITOR void SendAnalytic(InputAnalytics.IInputAnalytic analytic); - #endif // UNITY_ANALYTICS || UNITY_EDITOR +#endif // UNITY_ANALYTICS || UNITY_EDITOR - #if UNITY_EDITOR +#if UNITY_EDITOR Action onPlayModeChanged { get; set; } Action onProjectChange { get; set; } bool isInPlayMode { get; } @@ -197,7 +201,7 @@ internal unsafe interface IInputRuntime Func onUnityRemoteMessage { set; } void SetUnityRemoteGyroEnabled(bool value); void SetUnityRemoteGyroUpdateInterval(float interval); - #endif +#endif } internal static class InputRuntime diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.LegacyFocusHandling.cs b/Packages/com.unity.inputsystem/InputSystem/InputManager.LegacyFocusHandling.cs new file mode 100644 index 0000000000..2d936a465f --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.LegacyFocusHandling.cs @@ -0,0 +1,269 @@ +using System.Runtime.CompilerServices; +using UnityEngine; +using UnityEngine.InputSystem.LowLevel; +using UnityEngine.InputSystem.Utilities; + +namespace UnityEngine.InputSystem +{ +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + // Prior to 6000.5.a8 Input System mixed application focus with deferred events causing + // incorrect reasoning regarding which events happened in-focus vs out-of-focus. + // When running on an older editor, we define the enum here instead to reduce redundancy. + /// + /// Flags indicating various focus states for the application and editor. + /// + internal enum FocusFlags : ushort + { + /// + /// No focus state is active. + /// + None = 0, + + /// + /// In editor this means the GameView has focus. In a built player this means the player has focus. + /// + ApplicationFocus = (1 << 0) + }; + + internal partial class InputManager + { + internal void OnFocusChanged(bool focus) + { +#if UNITY_EDITOR + SyncAllDevicesWhenEditorIsActivated(); + + if (!m_Runtime.isInPlayMode) + { + focusState = focus ? FocusFlags.ApplicationFocus : FocusFlags.None; + return; + } + + var gameViewFocus = m_Settings.editorInputBehaviorInPlayMode; +#endif + + var runInBackground = +#if UNITY_EDITOR + // In the editor, the player loop will always be run even if the Game View does not have focus. This + // amounts to runInBackground being always true in the editor, regardless of what the setting in + // the Player Settings window is. + // + // If, however, "Game View Focus" is set to "Exactly As In Player", we force code here down the same + // path as in the player. + gameViewFocus != InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView || m_Runtime.runInBackground; +#else + m_Runtime.runInBackground; +#endif + + var backgroundBehavior = m_Settings.backgroundBehavior; + if (backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus && runInBackground) + { + // If runInBackground is true, no device changes should happen, even when focus is gained. So early out. + // If runInBackground is false, we still want to sync devices when focus is gained. So we need to continue further. + focusState = focus ? FocusFlags.ApplicationFocus : FocusFlags.None; + return; + } + +#if UNITY_EDITOR + // Set the current update type while we process the focus changes to make sure we + // feed into the right buffer. No need to do this in the player as it doesn't have + // the editor/player confusion. + m_CurrentUpdate = m_UpdateMask.GetUpdateTypeForPlayer(); +#endif + + if (!focus) + { + // We only react to loss of focus when we will keep running in the background. If not, + // we'll do nothing and just wait for focus to come back (where we then try to sync all devices). + if (runInBackground) + { + for (var i = 0; i < m_DevicesCount; ++i) + { + // Determine whether to run this device in the background. + var device = m_Devices[i]; + if (!device.enabled || ShouldRunDeviceInBackground(device)) + continue; + + // Disable the device. This will also soft-reset it. + EnableOrDisableDevice(device, false, DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + + // In case we invoked a callback that messed with our device array, adjust our index. + var index = m_Devices.IndexOfReference(device, m_DevicesCount); + if (index == -1) + --i; + else + i = index; + } + } + } + else + { + m_DiscardOutOfFocusEvents = true; + m_FocusRegainedTime = m_Runtime.currentTime; + // On focus gain, reenable and sync devices. + for (var i = 0; i < m_DevicesCount; ++i) + { + var device = m_Devices[i]; + + // Re-enable the device if we disabled it on focus loss. This will also issue a sync. + if (device.disabledWhileInBackground) + EnableOrDisableDevice(device, true, DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + // Try to sync. If it fails and we didn't run in the background, perform + // a reset instead. This is to cope with backends that are unable to sync but + // may still retain state which now may be outdated because the input device may + // have changed state while we weren't running. So at least make the backend flush + // its state (if any). + else if (device.enabled && !runInBackground && !device.RequestSync() && m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.IgnoreFocus) + ResetDevice(device); + } + } + +#if UNITY_EDITOR + m_CurrentUpdate = InputUpdateType.None; +#endif + + // We set this *after* the block above as defaultUpdateType is influenced by the setting. + focusState = focus ? FocusFlags.ApplicationFocus : FocusFlags.None; + } + + /// + /// Determines if the event buffer should be flushed without processing events. + /// + /// True if the buffer should be flushed, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ShouldFlushEventBuffer() + { +#if UNITY_EDITOR + // If out of focus and runInBackground is off and ExactlyAsInPlayer is on, discard input. + if (!gameHasFocus && + m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView + && + (!m_Runtime.runInBackground || m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices)) + return true; +#else + // In player builds, flush if out of focus and not running in background + if (!gameHasFocus && !m_Runtime.runInBackground) + return true; +#endif + return false; + } + + /// + /// Determines if we should exit early from event processing without handling events. + /// + /// The current event buffer + /// Whether the buffer can be flushed + /// The current update type + /// True if we should exit early, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ShouldExitEarlyFromEventProcessing(InputUpdateType updateType) + { +#if UNITY_EDITOR + // Check various PlayMode specific early exit conditions + if (ShouldExitEarlyBasedOnBackgroundBehavior(updateType)) + return true; + + // When the game is playing and has focus, we never process input in editor updates. + // All we do is just switch to editor state buffers and then exit. + if ((gameIsPlaying && gameHasFocus && updateType == InputUpdateType.Editor)) + return true; +#endif + + return false; + } + +#if UNITY_EDITOR + /// + /// Checks background behavior conditions for early exit from event processing. + /// + /// The current update type + /// True if we should exit early, false otherwise. + /// + /// Whenever this method returns true, it usually means that events are left in the buffer and should be + /// processed in a next update call. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ShouldExitEarlyBasedOnBackgroundBehavior(InputUpdateType updateType) + { + // In Play Mode, if we're in the background and not supposed to process events in this update + if ((!gameHasFocus || gameShouldGetInputRegardlessOfFocus) && updateType != InputUpdateType.Editor) + { + if (m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices || + m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDevicesRespectGameViewFocus) + return true; + } + + // Special case for IgnoreFocus behavior with AllDeviceInputAlwaysGoesToGameView in editor updates + if ((!gameHasFocus || gameShouldGetInputRegardlessOfFocus) && + m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus && + m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView && + updateType == InputUpdateType.Editor) + return true; + + return false; + } + +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe bool LegacyEarlyOutFromEventProcessing(InputUpdateType updateType, ref InputEventBuffer eventBuffer, ref bool dropStatusEvents) + { + var shouldProcessActionTimeouts = updateType.IsPlayerUpdate() && gameIsPlaying; + // Determine if we should flush the event buffer which would imply we exit early and do not process + // any of those events, ever. + var shouldFlushEventBuffer = ShouldFlushEventBuffer(); + // When we exit early, we may or may not flush the event buffer. It depends if we want to process events + // later once this method is called. + var shouldExitEarly = eventBuffer.eventCount == 0 || shouldFlushEventBuffer || ShouldExitEarlyFromEventProcessing(updateType); + dropStatusEvents = ShouldDropStatusEvents(eventBuffer, ref shouldExitEarly); + + // we exit early as we have no events in the buffer + if (shouldExitEarly) + { + // Normally, we process action timeouts after first processing all events. If we have no + // events, we still need to check timeouts. + if (shouldProcessActionTimeouts) + ProcessStateChangeMonitorTimeouts(); + + if (shouldFlushEventBuffer) + eventBuffer.Reset(); + InvokeAfterUpdateCallback(updateType); + m_CurrentUpdate = InputUpdateType.None; + return true; + } + return false; + } + + /// + /// Determines if status events should be dropped and modifies early exit behavior accordingly. + /// + /// The current event buffer + /// Reference to the early exit flag that may be modified + /// True if status events should be dropped, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ShouldDropStatusEvents(InputEventBuffer eventBuffer, ref bool canEarlyOut) + { + // If the game is not playing but we're sending all input events to the game, + // the buffer can just grow unbounded. So, in that case, set a flag to say we'd + // like to drop status events, and do not early out. + if (!gameIsPlaying && gameShouldGetInputRegardlessOfFocus && (eventBuffer.sizeInBytes > (100 * 1024))) + { + canEarlyOut = false; + return true; + } + return false; + } + + /// + /// Checks if an event should be discarded because it occurred while out of focus, under specific settings. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ShouldDiscardOutOfFocusEvent(double eventTime) + { + // If we care about focus, check if the event occurred while out of focus based on its timestamp. + if (gameHasFocus && m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.IgnoreFocus) + return m_DiscardOutOfFocusEvents && eventTime < m_FocusRegainedTime; + return false; + } + } +#endif // !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS +} diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.LegacyFocusHandling.cs.meta b/Packages/com.unity.inputsystem/InputSystem/InputManager.LegacyFocusHandling.cs.meta new file mode 100644 index 0000000000..b9dbb0ca48 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.LegacyFocusHandling.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 37c57605ec1653942a94491298f0b3b7 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs index 90b6dce04e..1e53728640 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs @@ -34,10 +34,19 @@ namespace UnityEngine.InputSystem { using DeviceChangeListener = Action; using DeviceStateChangeListener = Action; - using LayoutChangeListener = Action; using EventListener = Action; + using LayoutChangeListener = Action; using UpdateListener = Action; + static class FocusConstants + { +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + public const int kEventType = (int)NativeInputEventType.Focus; +#else + public const int kEventType = 0x464f4355; // 'FOCU' +#endif + } + /// /// Hub of the input system. /// @@ -231,10 +240,13 @@ public InputUpdateType defaultUpdateType { get { - if (m_CurrentUpdate != default) + if (m_CurrentUpdate != InputUpdateType.None) return m_CurrentUpdate; - #if UNITY_EDITOR +#if UNITY_EDITOR + // We can no longer rely on checking the current focus state, due to this check being used pre-update + // to determine in which update type to process input, and focus being updated in Update. + // The solution here would be to make update calls explicitly specify the update type and no longer use this property. if (!m_RunPlayerUpdatesInEditMode && (!gameIsPlaying || !gameHasFocus)) return InputUpdateType.Editor; #endif @@ -260,15 +272,36 @@ public InputSettings.ScrollDeltaBehavior scrollDeltaBehavior } } + public FocusFlags focusState + { + get + { +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + return m_Runtime.focusState; +#else + return m_FocusState; +#endif + } + set + { +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + if (m_Runtime != null) + m_Runtime.focusState = value; +#else + m_FocusState = value; +#endif + } + } + public float pollingFrequency { get { - #if UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY +#if UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY return m_Runtime.pollingFrequency; - #else +#else return m_PollingFrequency; - #endif +#endif } set @@ -277,13 +310,13 @@ public float pollingFrequency if (value <= 0) throw new ArgumentException("Polling frequency must be greater than zero", "value"); - #if UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY +#if UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY m_Runtime.pollingFrequency = value; - #else +#else m_PollingFrequency = value; if (m_Runtime != null) m_Runtime.pollingFrequency = value; - #endif +#endif } } @@ -475,12 +508,13 @@ public bool runPlayerUpdatesInEditMode #else true; #endif + private bool applicationHasFocus => (focusState & FocusFlags.ApplicationFocus) != FocusFlags.None; private bool gameHasFocus => #if UNITY_EDITOR - m_RunPlayerUpdatesInEditMode || m_HasFocus || gameShouldGetInputRegardlessOfFocus; + m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus; #else - m_HasFocus || gameShouldGetInputRegardlessOfFocus; + applicationHasFocus || gameShouldGetInputRegardlessOfFocus; #endif private bool gameShouldGetInputRegardlessOfFocus => @@ -702,7 +736,7 @@ private void PerformLayoutPostRegistration(InternedString layoutName, InlinedArr // (the latter is important as in that case, we should go through the normal matching // process and not just rely on the name of the layout). If so, we try here to recreate // the device with the just registered layout. - #if UNITY_EDITOR +#if UNITY_EDITOR for (var i = 0; i < m_SavedDeviceStates.LengthSafe(); ++i) { ref var deviceState = ref m_SavedDeviceStates[i]; @@ -715,7 +749,7 @@ private void PerformLayoutPostRegistration(InternedString layoutName, InlinedArr --i; } } - #endif +#endif // Let listeners know. var change = isReplacement ? InputControlLayoutChange.Replaced : InputControlLayoutChange.Added; @@ -905,7 +939,7 @@ private void RecreateDevice(InputDevice oldDevice, InternedString newLayout) private void AddAvailableDevicesMatchingDescription(InputDeviceMatcher matcher, InternedString layout) { - #if UNITY_EDITOR +#if UNITY_EDITOR // If we still have some devices saved from the last domain reload, see // if they are matched by the given matcher. If so, turn them into devices. for (var i = 0; i < m_SavedDeviceStates.LengthSafe(); ++i) @@ -918,7 +952,7 @@ private void AddAvailableDevicesMatchingDescription(InputDeviceMatcher matcher, --i; } } - #endif +#endif // See if the new description to layout mapping allows us to make // sense of a device we couldn't make sense of so far. @@ -1015,9 +1049,8 @@ public InputControlLayout TryLoadControlLayout(InternedString name) public InternedString TryFindMatchingControlLayout(ref InputDeviceDescription deviceDescription, int deviceId = InputDevice.InvalidDeviceId) { InternedString layoutName = new InternedString(string.Empty); - try + using (k_InputTryFindMatchingControllerMarker.Auto()) { - k_InputTryFindMatchingControllerMarker.Begin(); ////TODO: this will want to take overrides into account // See if we can match by description. @@ -1078,12 +1111,8 @@ public InternedString TryFindMatchingControlLayout(ref InputDeviceDescription de } m_DeviceFindLayoutCallbacks.UnlockForChanges(); } + return layoutName; } - finally - { - k_InputTryFindMatchingControllerMarker.End(); - } - return layoutName; } private InternedString FindOrRegisterDeviceLayoutForType(Type type) @@ -1113,10 +1142,10 @@ private bool IsDeviceLayoutMarkedAsSupportedInSettings(InternedString layoutName // all available devices to be added regardless of what "Supported Devices" says. This // is useful to ensure that things like keyboard, mouse, and pen keep working in the editor // even if not supported as devices in the game. - #if UNITY_EDITOR +#if UNITY_EDITOR if (InputEditorUserSettings.addDevicesNotSupportedByProject) return true; - #endif +#endif var supportedDevices = m_Settings.supportedDevices; if (supportedDevices.Count == 0) @@ -1358,9 +1387,9 @@ public void AddDevice(InputDevice device) // If we're running in the background, find out whether the device can run in // the background. If not, disable it. var isPlaying = true; - #if UNITY_EDITOR +#if UNITY_EDITOR isPlaying = m_Runtime.isInPlayMode; - #endif +#endif if (isPlaying && !gameHasFocus && m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.IgnoreFocus && m_Runtime.runInBackground @@ -1431,50 +1460,43 @@ public InputDevice AddDevice(InputDeviceDescription description) public InputDevice AddDevice(InputDeviceDescription description, bool throwIfNoLayoutFound, string deviceName = null, int deviceId = InputDevice.InvalidDeviceId, InputDevice.DeviceFlags deviceFlags = 0) { - k_InputAddDeviceMarker.Begin(); - // Look for matching layout. - var layout = TryFindMatchingControlLayout(ref description, deviceId); - - // If no layout was found, bail out. - if (layout.IsEmpty()) + using (k_InputAddDeviceMarker.Auto()) { - if (throwIfNoLayoutFound) - { - k_InputAddDeviceMarker.End(); - throw new ArgumentException($"Cannot find layout matching device description '{description}'", nameof(description)); - } + // Look for matching layout. + var layout = TryFindMatchingControlLayout(ref description, deviceId); - // If it's a device coming from the runtime, disable it. - if (deviceId != InputDevice.InvalidDeviceId) + // If no layout was found, bail out. + if (layout.IsEmpty()) { - var command = DisableDeviceCommand.Create(); - m_Runtime.DeviceCommand(deviceId, ref command); + if (throwIfNoLayoutFound) + { + throw new ArgumentException($"Cannot find layout matching device description '{description}'", nameof(description)); + } + + // If it's a device coming from the runtime, disable it. + if (deviceId != InputDevice.InvalidDeviceId) + { + var command = DisableDeviceCommand.Create(); + m_Runtime.DeviceCommand(deviceId, ref command); + } + return null; } - k_InputAddDeviceMarker.End(); - return null; + var device = AddDevice(layout, deviceId, deviceName, description, deviceFlags); + device.m_Description = description; + return device; } - - var device = AddDevice(layout, deviceId, deviceName, description, deviceFlags); - device.m_Description = description; - k_InputAddDeviceMarker.End(); - return device; } public InputDevice AddDevice(InputDeviceDescription description, InternedString layout, string deviceName = null, int deviceId = InputDevice.InvalidDeviceId, InputDevice.DeviceFlags deviceFlags = 0) { - try + using (k_InputAddDeviceMarker.Auto()) { - k_InputAddDeviceMarker.Begin(); var device = AddDevice(layout, deviceId, deviceName, description, deviceFlags); device.m_Description = description; return device; } - finally - { - k_InputAddDeviceMarker.End(); - } } public void RemoveDevice(InputDevice device, bool keepOnListOfAvailableDevices = false) @@ -1654,7 +1676,6 @@ public unsafe void ResetDevice(InputDevice device, bool alsoResetDontResetContro (int)deviceStateBlockSize, (byte*)resetMaskPtr + stateBlock.byteOffset); } - UpdateState(device, defaultUpdateType, statePtr, 0, deviceStateBlockSize, currentTime, new InputEventPtr((InputEvent*)stateEventPtr)); } @@ -1674,10 +1695,10 @@ public unsafe void ResetDevice(InputDevice device, bool alsoResetDontResetContro var doIssueResetCommand = isHardReset; if (issueResetCommand != null) doIssueResetCommand = issueResetCommand.Value; - #if UNITY_EDITOR +#if UNITY_EDITOR else if (m_Settings.editorInputBehaviorInPlayMode != InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView) doIssueResetCommand = false; - #endif +#endif if (doIssueResetCommand) device.RequestReset(); @@ -1865,9 +1886,9 @@ public void EnableOrDisableDevice(InputDevice device, bool enable, DeviceDisable return; device.disabledWhileInBackground = true; ResetDevice(device, issueResetCommand: false); - #if UNITY_EDITOR +#if UNITY_EDITOR if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView) - #endif +#endif { device.ExecuteDisableCommand(); device.disabledInRuntime = true; @@ -1951,18 +1972,23 @@ internal void InitializeData() // we don't know which one the user is going to use. The user // can manually turn off one of them to optimize operation. m_UpdateMask = InputUpdateType.Dynamic | InputUpdateType.Fixed; - m_HasFocus = Application.isFocused; - #if UNITY_EDITOR +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + m_FocusState = Application.isFocused + ? m_FocusState | FocusFlags.ApplicationFocus + : m_FocusState & ~FocusFlags.ApplicationFocus; +#endif + +#if UNITY_EDITOR m_EditorIsActive = true; m_UpdateMask |= InputUpdateType.Editor; #endif m_ScrollDeltaBehavior = InputSettings.ScrollDeltaBehavior.UniformAcrossAllPlatforms; - #if !UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY +#if !UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY // Default polling frequency is 60 Hz. m_PollingFrequency = 60; - #endif +#endif // Default input event handled policy. m_InputEventHandledPolicy = InputEventHandledPolicy.SuppressStateUpdates; @@ -2032,9 +2058,9 @@ internal void InitializeData() processors.AddTypeRegistration("CompensateDirection", typeof(CompensateDirectionProcessor)); processors.AddTypeRegistration("CompensateRotation", typeof(CompensateRotationProcessor)); - #if UNITY_EDITOR +#if UNITY_EDITOR processors.AddTypeRegistration("AutoWindowSpace", typeof(EditorWindowSpaceProcessor)); - #endif +#endif // Register interactions. interactions.AddTypeRegistration("Hold", typeof(HoldInteraction)); @@ -2093,40 +2119,38 @@ internal bool RegisterCustomTypes() m_CustomTypesRegistered = true; - k_InputRegisterCustomTypesMarker.Begin(); - - var inputSystemAssembly = typeof(InputProcessor).Assembly; - var inputSystemName = inputSystemAssembly.GetName().Name; + using (k_InputRegisterCustomTypesMarker.Auto()) + { + var inputSystemAssembly = typeof(InputProcessor).Assembly; + var inputSystemName = inputSystemAssembly.GetName().Name; #if UNITY_6000_5_OR_NEWER - var assemblies = CurrentAssemblies.GetLoadedAssemblies(); + var assemblies = CurrentAssemblies.GetLoadedAssemblies(); #else - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); #endif - foreach (var assembly in assemblies) - { - try + foreach (var assembly in assemblies) { - // exclude InputSystem assembly which should be loaded first - if (assembly == inputSystemAssembly) continue; - - // Only register types from assemblies that reference InputSystem - foreach (var referencedAssembly in assembly.GetReferencedAssemblies()) + try { - if (referencedAssembly.Name == inputSystemName) + // exclude InputSystem assembly which should be loaded first + if (assembly == inputSystemAssembly) continue; + + // Only register types from assemblies that reference InputSystem + foreach (var referencedAssembly in assembly.GetReferencedAssemblies()) { - RegisterCustomTypes(assembly.GetTypes()); - break; + if (referencedAssembly.Name == inputSystemName) + { + RegisterCustomTypes(assembly.GetTypes()); + break; + } } } - } - catch (ReflectionTypeLoadException) - { - // Ignore exception + catch (ReflectionTypeLoadException) + { + // Ignore exception + } } } - - k_InputRegisterCustomTypesMarker.End(); - return true; // Signal that custom types were extracted and registered. } @@ -2178,23 +2202,30 @@ internal void InstallRuntime(IInputRuntime runtime) m_Runtime.onUpdate = null; m_Runtime.onBeforeUpdate = null; m_Runtime.onDeviceDiscovered = null; - m_Runtime.onPlayerFocusChanged = null; m_Runtime.onShouldRunUpdate = null; - #if UNITY_EDITOR +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + m_Runtime.onPlayerFocusChanged = null; +#endif +#if UNITY_EDITOR m_Runtime.onPlayerLoopInitialization = null; - #endif +#endif } m_Runtime = runtime; m_Runtime.onUpdate = OnUpdate; m_Runtime.onDeviceDiscovered = OnNativeDeviceDiscovered; +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS m_Runtime.onPlayerFocusChanged = OnFocusChanged; +#endif m_Runtime.onShouldRunUpdate = ShouldRunUpdate; - #if UNITY_EDITOR +#if UNITY_EDITOR m_Runtime.onPlayerLoopInitialization = OnPlayerLoopInitialization; - #endif +#endif m_Runtime.pollingFrequency = pollingFrequency; - m_HasFocus = m_Runtime.isPlayerFocused; + + focusState = m_Runtime.isPlayerFocused + ? focusState | FocusFlags.ApplicationFocus + : focusState & ~FocusFlags.ApplicationFocus; // We only hook NativeInputSystem.onBeforeUpdate if necessary. if (m_BeforeUpdateListeners.length > 0 || m_HaveDevicesWithStateCallbackReceivers) @@ -2203,10 +2234,10 @@ internal void InstallRuntime(IInputRuntime runtime) m_NativeBeforeUpdateHooked = true; } - #if UNITY_ANALYTICS || UNITY_EDITOR +#if UNITY_ANALYTICS || UNITY_EDITOR InputAnalytics.Initialize(this); m_Runtime.onShutdown = () => InputAnalytics.OnShutdown(this); - #endif +#endif } internal void InstallGlobals() @@ -2258,7 +2289,9 @@ internal void UninstallGlobals() m_Runtime.onUpdate = null; m_Runtime.onDeviceDiscovered = null; m_Runtime.onBeforeUpdate = null; +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS m_Runtime.onPlayerFocusChanged = null; +#endif m_Runtime.onShouldRunUpdate = null; if (ReferenceEquals(InputRuntime.s_Instance, m_Runtime)) @@ -2301,9 +2334,9 @@ internal struct AvailableDevice // Used by EditorInputControlLayoutCache to determine whether its state is outdated. internal int m_LayoutRegistrationVersion; - #if !UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY +#if !UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY private float m_PollingFrequency; - #endif +#endif private InputEventHandledPolicy m_InputEventHandledPolicy; internal InputControlLayout.Collection m_Layouts; @@ -2328,10 +2361,10 @@ internal struct AvailableDevice private InputSettings.ScrollDeltaBehavior m_ScrollDeltaBehavior; - #if UNITY_EDITOR +#if UNITY_EDITOR // remember time offset to correctly restore it after editor mode is done private double latestNonEditorTimeOffsetToRealtimeSinceStartup; - #endif +#endif // We don't use UnityEvents and thus don't persist the callbacks during domain reloads. // Restoration of UnityActions is unreliable and it's too easy to end up with double @@ -2348,30 +2381,32 @@ internal struct AvailableDevice private CallbackArray m_ActionsChangedListeners; private bool m_NativeBeforeUpdateHooked; private bool m_HaveDevicesWithStateCallbackReceivers; - private bool m_HasFocus; +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + private FocusFlags m_FocusState = FocusFlags.ApplicationFocus; private bool m_DiscardOutOfFocusEvents; private double m_FocusRegainedTime; +#endif private InputEventStream m_InputEventStream; // We want to sync devices when the editor comes back into focus. Unfortunately, there's no // callback for this so we have to poll this state. - #if UNITY_EDITOR +#if UNITY_EDITOR private bool m_EditorIsActive; - #endif +#endif // Allow external users to hook in validators and draw custom UI in the binding path editor - #if UNITY_EDITOR +#if UNITY_EDITOR private Utilities.CallbackArray m_customBindingPathValidators; - #endif +#endif // We allocate the 'executeDeviceCommand' closure passed to 'onFindLayoutForDevice' // only once to avoid creating garbage. private InputDeviceExecuteCommandDelegate m_DeviceFindExecuteCommandDelegate; private int m_DeviceFindExecuteCommandDeviceId; - #if UNITY_ANALYTICS || UNITY_EDITOR +#if UNITY_ANALYTICS || UNITY_EDITOR private bool m_HaveSentStartupAnalytics; - #endif +#endif private IInputRuntime m_Runtime; private InputMetrics m_Metrics; @@ -2405,9 +2440,9 @@ internal bool paranoidReadValueCachingChecksEnabled private InputActionAsset m_Actions; - #if UNITY_EDITOR +#if UNITY_EDITOR internal IInputDiagnostics m_Diagnostics; - #endif +#endif private DeferBindingResolutionContext m_DeferBindingResolutionContext; @@ -2529,13 +2564,13 @@ private unsafe void InitializeDefaultState(InputDevice device) stateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer); } - #if UNITY_EDITOR +#if UNITY_EDITOR if (m_StateBuffers.m_EditorStateBuffers.valid) { stateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer); stateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer); } - #endif +#endif } private unsafe void InitializeDeviceState(InputDevice device) @@ -2615,13 +2650,13 @@ private unsafe void InitializeDeviceState(InputDevice device) deviceStateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer); } - #if UNITY_EDITOR +#if UNITY_EDITOR if (m_StateBuffers.m_EditorStateBuffers.valid) { deviceStateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer); deviceStateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer); } - #endif +#endif } } @@ -2796,13 +2831,13 @@ private void InstallBeforeUpdateHookIfNecessary() private void RestoreDevicesAfterDomainReloadIfNecessary() { - #if UNITY_EDITOR && !ENABLE_CORECLR +#if UNITY_EDITOR && !ENABLE_CORECLR if (m_SavedDeviceStates != null) RestoreDevicesAfterDomainReload(); - #endif +#endif } - #if UNITY_EDITOR +#if UNITY_EDITOR private void SyncAllDevicesWhenEditorIsActivated() { var isActive = m_Runtime.isEditorActive; @@ -2835,14 +2870,14 @@ internal void SyncAllDevicesAfterEnteringPlayMode() SyncAllDevices(); } - #endif // UNITY_EDITOR +#endif // UNITY_EDITOR private void WarnAboutDevicesFailingToRecreateAfterDomainReload() { // If we still have any saved device states, we have devices that we couldn't figure // out how to recreate after a domain reload. Log a warning for each of them and // let go of them. - #if UNITY_EDITOR +#if UNITY_EDITOR if (m_SavedDeviceStates == null) return; @@ -2855,7 +2890,7 @@ private void WarnAboutDevicesFailingToRecreateAfterDomainReload() // At this point, we throw the device states away and forget about // what we had before the domain reload. m_SavedDeviceStates = null; - #endif // UNITY_EDITOR +#endif // UNITY_EDITOR } private void OnBeforeUpdate(InputUpdateType updateType) @@ -2926,12 +2961,12 @@ internal void ApplySettings() throw new NotSupportedException("Invalid input update mode: " + m_Settings.updateMode); } - #if UNITY_EDITOR +#if UNITY_EDITOR // In the editor, we force editor updates to be on even if InputEditorUserSettings.lockInputToGameView is // on as otherwise we'll end up accumulating events in edit mode without anyone flushing the // queue out regularly. newUpdateMask |= InputUpdateType.Editor; - #endif +#endif updateMask = newUpdateMask; scrollDeltaBehavior = m_Settings.scrollDeltaBehavior; @@ -2980,9 +3015,9 @@ internal void ApplySettings() // Apply feature flags. if (m_Settings.m_FeatureFlags != null) { - #if UNITY_EDITOR +#if UNITY_EDITOR runPlayerUpdatesInEditMode = m_Settings.IsFeatureEnabled(InputFeatureNames.kRunPlayerUpdatesInEditMode); - #endif +#endif // Extract feature flags into fields since used in hot-path m_ReadValueCachingFeatureEnabled = m_Settings.IsFeatureEnabled((InputFeatureNames.kUseReadValueCaching)); @@ -3072,105 +3107,7 @@ private bool ShouldRunDeviceInBackground(InputDevice device) device.canRunInBackground; } - internal void OnFocusChanged(bool focus) - { - #if UNITY_EDITOR - SyncAllDevicesWhenEditorIsActivated(); - - if (!m_Runtime.isInPlayMode) - { - m_HasFocus = focus; - return; - } - - var gameViewFocus = m_Settings.editorInputBehaviorInPlayMode; - #endif - - var runInBackground = - #if UNITY_EDITOR - // In the editor, the player loop will always be run even if the Game View does not have focus. This - // amounts to runInBackground being always true in the editor, regardless of what the setting in - // the Player Settings window is. - // - // If, however, "Game View Focus" is set to "Exactly As In Player", we force code here down the same - // path as in the player. - gameViewFocus != InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView || m_Runtime.runInBackground; - #else - m_Runtime.runInBackground; - #endif - - var backgroundBehavior = m_Settings.backgroundBehavior; - if (backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus && runInBackground) - { - // If runInBackground is true, no device changes should happen, even when focus is gained. So early out. - // If runInBackground is false, we still want to sync devices when focus is gained. So we need to continue further. - m_HasFocus = focus; - return; - } - - #if UNITY_EDITOR - // Set the current update type while we process the focus changes to make sure we - // feed into the right buffer. No need to do this in the player as it doesn't have - // the editor/player confusion. - m_CurrentUpdate = m_UpdateMask.GetUpdateTypeForPlayer(); - #endif - - if (!focus) - { - // We only react to loss of focus when we will keep running in the background. If not, - // we'll do nothing and just wait for focus to come back (where we then try to sync all devices). - if (runInBackground) - { - for (var i = 0; i < m_DevicesCount; ++i) - { - // Determine whether to run this device in the background. - var device = m_Devices[i]; - if (!device.enabled || ShouldRunDeviceInBackground(device)) - continue; - - // Disable the device. This will also soft-reset it. - EnableOrDisableDevice(device, false, DeviceDisableScope.TemporaryWhilePlayerIsInBackground); - - // In case we invoked a callback that messed with our device array, adjust our index. - var index = m_Devices.IndexOfReference(device, m_DevicesCount); - if (index == -1) - --i; - else - i = index; - } - } - } - else - { - m_DiscardOutOfFocusEvents = true; - m_FocusRegainedTime = m_Runtime.currentTime; - // On focus gain, reenable and sync devices. - for (var i = 0; i < m_DevicesCount; ++i) - { - var device = m_Devices[i]; - - // Re-enable the device if we disabled it on focus loss. This will also issue a sync. - if (device.disabledWhileInBackground) - EnableOrDisableDevice(device, true, DeviceDisableScope.TemporaryWhilePlayerIsInBackground); - // Try to sync. If it fails and we didn't run in the background, perform - // a reset instead. This is to cope with backends that are unable to sync but - // may still retain state which now may be outdated because the input device may - // have changed state while we weren't running. So at least make the backend flush - // its state (if any). - else if (device.enabled && !runInBackground && !device.RequestSync()) - ResetDevice(device); - } - } - - #if UNITY_EDITOR - m_CurrentUpdate = InputUpdateType.None; - #endif - - // We set this *after* the block above as defaultUpdateType is influenced by the setting. - m_HasFocus = focus; - } - - #if UNITY_EDITOR +#if UNITY_EDITOR internal void LeavePlayMode() { // Reenable all devices and reset their play mode state. @@ -3198,7 +3135,7 @@ private void OnPlayerLoopInitialization() InputStateBuffers.SwitchTo(m_StateBuffers, InputUpdate.s_LatestUpdateType); } - #endif // UNITY_EDITOR +#endif // UNITY_EDITOR internal bool ShouldRunUpdate(InputUpdateType updateType) { @@ -3209,7 +3146,7 @@ internal bool ShouldRunUpdate(InputUpdateType updateType) var mask = m_UpdateMask; - #if UNITY_EDITOR +#if UNITY_EDITOR // If the player isn't running, the only thing we run is editor updates, except if // explicitly overriden via `runUpdatesInEditMode`. // NOTE: This means that in edit mode (outside of play mode) we *never* switch to player @@ -3218,7 +3155,7 @@ internal bool ShouldRunUpdate(InputUpdateType updateType) // it will see gamepad inputs going to the editor and respond to them. if (!gameIsPlaying && updateType != InputUpdateType.Editor && !runPlayerUpdatesInEditMode) return false; - #endif // UNITY_EDITOR +#endif // UNITY_EDITOR return (updateType & mask) != 0; } @@ -3243,117 +3180,110 @@ internal bool ShouldRunUpdate(InputUpdateType updateType) [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")] private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer eventBuffer) { - // NOTE: This is *not* using try/finally as we've seen unreliability in the EndSample() - // execution (and we're not sure where it's coming from). - k_InputUpdateProfilerMarker.Begin(); - - if (m_InputEventStream.isOpen) + using (k_InputUpdateProfilerMarker.Auto()) { - k_InputUpdateProfilerMarker.End(); - throw new InvalidOperationException("Already have an event buffer set! Was OnUpdate() called recursively?"); - } - - // Restore devices before checking update mask. See InputSystem.RunInitialUpdate(). - RestoreDevicesAfterDomainReloadIfNecessary(); - - // In the editor, we issue a sync on all devices when the editor comes back to the foreground. - #if UNITY_EDITOR - SyncAllDevicesWhenEditorIsActivated(); - #endif + if (m_InputEventStream.isOpen) + throw new InvalidOperationException("Already have an event buffer set! Was OnUpdate() called recursively?"); - if ((updateType & m_UpdateMask) == 0) - { - k_InputUpdateProfilerMarker.End(); - return; - } + // Restore devices before checking update mask. See InputSystem.RunInitialUpdate(). + RestoreDevicesAfterDomainReloadIfNecessary(); - WarnAboutDevicesFailingToRecreateAfterDomainReload(); + // In the editor, we issue a sync on all devices when the editor comes back to the foreground. +#if UNITY_EDITOR + SyncAllDevicesWhenEditorIsActivated(); +#endif - // First update sends out startup analytics. - #if UNITY_ANALYTICS || UNITY_EDITOR - if (!m_HaveSentStartupAnalytics) - { - InputAnalytics.OnStartup(this); - m_HaveSentStartupAnalytics = true; - } - #endif + if ((updateType & m_UpdateMask) == 0) + return; - // Update metrics. - ++m_Metrics.totalUpdateCount; + WarnAboutDevicesFailingToRecreateAfterDomainReload(); - #if UNITY_EDITOR - // If current update is editor update and previous update was non-editor, - // store the time offset so we can restore it right after editor update is complete - if (((updateType & InputUpdateType.Editor) == InputUpdateType.Editor) && (m_CurrentUpdate & InputUpdateType.Editor) == 0) - latestNonEditorTimeOffsetToRealtimeSinceStartup = - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup; - #endif + // First update sends out startup analytics. +#if UNITY_ANALYTICS || UNITY_EDITOR + if (!m_HaveSentStartupAnalytics) + { + InputAnalytics.OnStartup(this); + m_HaveSentStartupAnalytics = true; + } +#endif + // Update metrics. + ++m_Metrics.totalUpdateCount; - // Store current time offset. - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = m_Runtime.currentTimeOffsetToRealtimeSinceStartup; +#if UNITY_EDITOR + // If current update is editor update and previous update was non-editor, + // store the time offset so we can restore it right after editor update is complete + if (((updateType & InputUpdateType.Editor) == InputUpdateType.Editor) && (m_CurrentUpdate & InputUpdateType.Editor) == 0) + latestNonEditorTimeOffsetToRealtimeSinceStartup = InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup; +#endif - InputStateBuffers.SwitchTo(m_StateBuffers, updateType); + // Store current time offset. + InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = m_Runtime.currentTimeOffsetToRealtimeSinceStartup; - m_CurrentUpdate = updateType; - InputUpdate.OnUpdate(updateType); + InputStateBuffers.SwitchTo(m_StateBuffers, updateType); - // Ensure optimized controls are in valid state - CheckAllDevicesOptimizedControlsHaveValidState(); + m_CurrentUpdate = updateType; + InputUpdate.OnUpdate(updateType); - var shouldProcessActionTimeouts = updateType.IsPlayerUpdate() && gameIsPlaying; + // Ensure optimized controls are in valid state + CheckAllDevicesOptimizedControlsHaveValidState(); - // See if we're supposed to only take events up to a certain time. - // NOTE: We do not require the events in the queue to be sorted. Instead, we will walk over - // all events in the buffer each time. Note that if there are multiple events for the same - // device, it depends on the producer of these events to queue them in correct order. - // Otherwise, once an event with a newer timestamp has been processed, events coming later - // in the buffer and having older timestamps will get rejected. + var shouldProcessActionTimeouts = updateType.IsPlayerUpdate() && gameIsPlaying; - var currentTime = updateType == InputUpdateType.Fixed ? m_Runtime.currentTimeForFixedUpdate : m_Runtime.currentTime; - var timesliceEvents = (updateType == InputUpdateType.Fixed || updateType == InputUpdateType.BeforeRender) && - InputSystem.settings.updateMode == InputSettings.UpdateMode.ProcessEventsInFixedUpdate; + // See if we're supposed to only take events up to a certain time. + // NOTE: We do not require the events in the queue to be sorted. Instead, we will walk over + // all events in the buffer each time. Note that if there are multiple events for the same + // device, it depends on the producer of these events to queue them in correct order. + // Otherwise, once an event with a newer timestamp has been processed, events coming later + // in the buffer and having older timestamps will get rejected. + var currentTime = updateType == InputUpdateType.Fixed ? m_Runtime.currentTimeForFixedUpdate : m_Runtime.currentTime; + var timesliceEvents = (updateType == InputUpdateType.Fixed || updateType == InputUpdateType.BeforeRender) && + InputSystem.settings.updateMode == InputSettings.UpdateMode.ProcessEventsInFixedUpdate; - // Determine if we should flush the event buffer which would imply we exit early and do not process - // any of those events, ever. - var shouldFlushEventBuffer = ShouldFlushEventBuffer(); - // When we exit early, we may or may not flush the event buffer. It depends if we want to process events - // later once this method is called. - var shouldExitEarly = - eventBuffer.eventCount == 0 || shouldFlushEventBuffer || ShouldExitEarlyFromEventProcessing(updateType); + var dropStatusEvents = false; +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + // we exit early as we have no events in the buffer + if (eventBuffer.eventCount == 0) + { + // Normally, we process action timeouts after first processing all events. If we have no + // events, we still need to check timeouts. + if (shouldProcessActionTimeouts) + ProcessStateChangeMonitorTimeouts(); + InvokeAfterUpdateCallback(updateType); + m_CurrentUpdate = InputUpdateType.None; + return; + } #if UNITY_EDITOR - var dropStatusEvents = ShouldDropStatusEvents(eventBuffer, ref shouldExitEarly); + dropStatusEvents = ShouldDropStatusEvents(eventBuffer); #endif - if (shouldExitEarly) - { - // Normally, we process action timeouts after first processing all events. If we have no - // events, we still need to check timeouts. +#else + if (LegacyEarlyOutFromEventProcessing(updateType, ref eventBuffer, ref dropStatusEvents)) + return; +#endif + + ProcessEventBuffer(updateType, ref eventBuffer, currentTime, timesliceEvents, dropStatusEvents); + if (shouldProcessActionTimeouts) ProcessStateChangeMonitorTimeouts(); - k_InputUpdateProfilerMarker.End(); - InvokeAfterUpdateCallback(updateType); - if (shouldFlushEventBuffer) - eventBuffer.Reset(); - m_CurrentUpdate = default; - return; - } + FinalizeUpdate(updateType); + } // k_InputUpdateProfilerMarker + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ProcessEventBuffer(InputUpdateType updateType, ref InputEventBuffer eventBuffer, double currentTime, bool timesliceEvents, bool dropStatusEvents) + { var processingStartTime = Stopwatch.GetTimestamp(); var totalEventLag = 0.0; - #if UNITY_EDITOR - var isPlaying = gameIsPlaying; - #endif - try { m_InputEventStream = new InputEventStream(ref eventBuffer, m_Settings.maxQueuedEventsPerUpdate); var totalEventBytesProcessed = 0U; - InputEvent* skipEventMergingFor = null; + var focusEventType = new FourCC(FocusConstants.kEventType); // Handle events. while (m_InputEventStream.remainingEventCount > 0) @@ -3366,29 +3296,27 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // In before render updates, we only take state events and only those for devices // that have before render updates enabled. if (updateType == InputUpdateType.BeforeRender) - { - while (m_InputEventStream.remainingEventCount > 0) - { - Debug.Assert(!currentEventReadPtr->handled, - "Iterated to event in buffer that is already marked as handled"); - - device = TryGetDeviceById(currentEventReadPtr->deviceId); - if (device != null && device.updateBeforeRender && - (currentEventReadPtr->type == StateEvent.Type || - currentEventReadPtr->type == DeltaStateEvent.Type)) - break; - - currentEventReadPtr = m_InputEventStream.Advance(leaveEventInBuffer: true); - } - } + ProcessBeforeRenderStateEvents(out device, out currentEventReadPtr); if (m_InputEventStream.remainingEventCount == 0) break; var currentEventTimeInternal = currentEventReadPtr->internalTime; var currentEventType = currentEventReadPtr->type; - - #if UNITY_EDITOR +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS +#if UNITY_EDITOR + if (SkipEventDueToEditorBehaviour(updateType, currentEventType, dropStatusEvents, currentEventTimeInternal)) + continue; +#else + // In player builds, drop events if out of focus and not running in background, unless it is a focus event. + if (!gameHasFocus && !m_Runtime.runInBackground && currentEventType != focusEventType) + { + m_InputEventStream.Advance(false); + continue; + } +#endif +#else +#if UNITY_EDITOR if (dropStatusEvents) { // If the type here is a status event, ask advance not to leave the event in the buffer. Otherwise, leave it there. @@ -3406,8 +3334,8 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev m_InputEventStream.Advance(false); continue; } - #endif - +#endif +#endif // If we're timeslicing, check if the event time is within limits. if (timesliceEvents && currentEventTimeInternal >= currentTime) { @@ -3418,148 +3346,62 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // If we can't find the device, ignore the event. if (device == null) device = TryGetDeviceById(currentEventReadPtr->deviceId); - if (device == null) + if (device == null && currentEventType != focusEventType) { - #if UNITY_EDITOR +#if UNITY_EDITOR ////TODO: see if this is a device we haven't created and if so, just ignore m_Diagnostics?.OnCannotFindDeviceForEvent(new InputEventPtr(currentEventReadPtr)); - #endif +#endif m_InputEventStream.Advance(false); continue; } - // In the editor, we may need to bump events from editor updates into player updates - // and vice versa. - #if UNITY_EDITOR - if (isPlaying && !gameHasFocus) - { - if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode - .PointersAndKeyboardsRespectGameViewFocus && - m_Settings.backgroundBehavior != - InputSettings.BackgroundBehavior.ResetAndDisableAllDevices) - { - var isPointerOrKeyboard = device is Pointer || device is Keyboard; - if (updateType != InputUpdateType.Editor) - { - // Let everything but pointer and keyboard input through. - // If the event is from a pointer or keyboard, leave it in the buffer so it can be dealt with - // in a subsequent editor update. Otherwise, take it out. - if (isPointerOrKeyboard) - { - m_InputEventStream.Advance(true); - continue; - } - } - else - { - // Let only pointer and keyboard input through. - if (!isPointerOrKeyboard) - { - m_InputEventStream.Advance(true); - continue; - } - } - } - } - #endif // UNITY_EDITOR +#if UNITY_EDITOR + // In the editor, route keyboard/pointer events between Editor/Player updates if required. + if (ShouldDeferEventBetweenEditorAndPlayerUpdates(updateType, currentEventType, device)) + continue; +#endif // UNITY_EDITOR // If device is disabled, we let the event through only in certain cases. // Removal and configuration change events should always be processed. - if (!device.enabled && + if (device != null && !device.enabled && currentEventType != DeviceRemoveEvent.Type && currentEventType != DeviceConfigurationEvent.Type && (device.m_DeviceFlags & (InputDevice.DeviceFlags.DisabledInRuntime | InputDevice.DeviceFlags.DisabledWhileInBackground)) != 0) { - #if UNITY_EDITOR +#if UNITY_EDITOR // If the device is disabled in the backend, getting events for them // is something that indicates a problem in the backend so diagnose. if ((device.m_DeviceFlags & InputDevice.DeviceFlags.DisabledInRuntime) != 0) m_Diagnostics?.OnEventForDisabledDevice(currentEventReadPtr, device); - #endif +#endif m_InputEventStream.Advance(false); continue; } // Check if the device wants to merge successive events. - if (!settings.disableRedundantEventsMerging && device.hasEventMerger && currentEventReadPtr != skipEventMergingFor) + if (device != null && !settings.disableRedundantEventsMerging && device.hasEventMerger && currentEventReadPtr != skipEventMergingFor) { - // NOTE: This relies on events in the buffer being consecutive for the same device. This is not - // necessarily the case for events coming in from the background event queue where parallel - // producers may create interleaved input sequences. This will be fixed once we have the - // new buffering scheme for input events working in the native runtime. - - var nextEvent = m_InputEventStream.Peek(); - // If there is next event after current one. - if ((nextEvent != null) - // And if next event is for the same device. - && (currentEventReadPtr->deviceId == nextEvent->deviceId) - // And if next event is in the same timeslicing slot. - && (timesliceEvents ? (nextEvent->internalTime < currentTime) : true) - ) - { - // Then try to merge current event into next event. - if (((IEventMerger)device).MergeForward(currentEventReadPtr, nextEvent)) - { - // And if succeeded, skip current event, as it was merged into next event. - m_InputEventStream.Advance(false); - continue; - } - - // If we can't merge current event with next one for any reason, we assume the next event - // carries crucial entropy (button changed state, phase changed, counter changed, etc). - // Hence semantic meaning for current event is "can't merge current with next because next is different". - // But semantic meaning for next event is "next event carries important information and should be preserved", - // from that point of view next event should not be merged with current nor with _next after next_ event. - // - // For example, given such stream of events: - // Mouse Mouse Mouse Mouse Mouse Mouse Mouse - // Event no1 Event no2 Event no3 Event no4 Event no5 Event no6 Event no7 - // Time 1 Time 2 Time 3 Time 4 Time 5 Time 6 Time 7 - // Pos(10,20) Pos(12,21) Pos(13,23) Pos(14,24) Pos(16,25) Pos(17,27) Pos(18,28) - // Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) - // BtnLeft(0) BtnLeft(0) BtnLeft(0) BtnLeft(1) BtnLeft(1) BtnLeft(1) BtnLeft(1) - // - // if we then merge without skipping next event here: - // Mouse Mouse - // Event no3 Event no7 - // Time 3 Time 7 - // Pos(13,23) Pos(18,28) - // Delta(4,4) Delta(5,5) - // BtnLeft(0) BtnLeft(1) - // - // As you can see, the event no4 containing mouse button press was lost, - // and with it we lose the important information of timestamp of mouse button press. - // - // With skipping merging next event we will get: - // Mouse Mouse Mouse - // Time 3 Time 4 Time 7 - // Event no3 Event no4 Event no7 - // Pos(13,23) Pos(14,24) Pos(18,28) - // Delta(3,3) Delta(1,1) Delta(4,4) - // BtnLeft(0) BtnLeft(1) BtnLeft(1) - // - // And no4 is preserved, with the exact timestamp of button press. - skipEventMergingFor = nextEvent; - } + if (MergeWithNextEvent(device, currentEventReadPtr, timesliceEvents, currentTime, ref skipEventMergingFor)) + continue; } // Give the device a chance to do something with data before we propagate it to event listeners. - if (device.hasEventPreProcessor) + if (device != null && device.hasEventPreProcessor) { - #if UNITY_EDITOR +#if UNITY_EDITOR var eventSizeBeforePreProcessor = currentEventReadPtr->sizeInBytes; - #endif +#endif var shouldProcess = ((IEventPreProcessor)device).PreProcessEvent(currentEventReadPtr); - #if UNITY_EDITOR +#if UNITY_EDITOR if (currentEventReadPtr->sizeInBytes > eventSizeBeforePreProcessor) { - k_InputUpdateProfilerMarker.End(); throw new AccessViolationException($"'{device}'.PreProcessEvent tries to grow an event from {eventSizeBeforePreProcessor} bytes to {currentEventReadPtr->sizeInBytes} bytes, this will potentially corrupt events after the current event and/or cause out-of-bounds memory access."); } - #endif +#endif if (!shouldProcess) { // Skip event if PreProcessEvent considers it to be irrelevant. @@ -3572,7 +3414,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // NOTE: We call listeners also for events where the device is disabled. This is crucial for code // such as TouchSimulation that disables the originating devices and then uses its events to // create simulated events from. - if (m_EventListeners.length > 0) + if (m_EventListeners.length > 0 && currentEventType != focusEventType) { DelegateHelpers.InvokeCallbacksSafe(ref m_EventListeners, new InputEventPtr(currentEventReadPtr), device, k_InputOnEventMarker, "InputSystem.onEvent"); @@ -3592,236 +3434,557 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev ++m_Metrics.totalEventCount; m_Metrics.totalEventBytes += (int)currentEventReadPtr->sizeInBytes; - // Process. - switch (currentEventType) - { - case StateEvent.Type: - case DeltaStateEvent.Type: - - var eventPtr = new InputEventPtr(currentEventReadPtr); - - // Ignore the event if the last state update we received for the device was - // newer than this state event is. We don't allow devices to go back in time. - // - // NOTE: We make an exception here for devices that implement IInputStateCallbackReceiver (such - // as Touchscreen). For devices that dynamically incorporate state it can be hard ensuring - // a global ordering of events as there may be multiple substreams (e.g. each individual touch) - // that are generated in the backend and would require considerable work to ensure monotonically - // increasing timestamps across all such streams. - var deviceIsStateCallbackReceiver = device.hasStateCallbacks; - if (currentEventTimeInternal < device.m_LastUpdateTimeInternal && - !(deviceIsStateCallbackReceiver && device.stateBlock.format != eventPtr.stateFormat)) - { - #if UNITY_EDITOR - m_Diagnostics?.OnEventTimestampOutdated(new InputEventPtr(currentEventReadPtr), device); -#elif UNITY_ANDROID - // Android keyboards can send events out of order: Holding down a key will send multiple - // presses after a short time, like on most platforms. Unfortunately, on Android, the - // last of these "presses" can be timestamped to be after the event of the key release. - // If that happens, we'd skip the keyUp here, and the device state will have the key - // "stuck" pressed. So, special case here to not skip keyboard events on Android. ISXB-475 - // N.B. Android seems to have similar issues with touch input (OnStateEvent, Touchscreen.cs) - if (!(device is Keyboard)) -#endif - break; - } + ProcessEvent(device, updateType, currentEventReadPtr, ref totalEventBytesProcessed, currentEventTimeInternal); - // Update the state of the device from the event. If the device is an IInputStateCallbackReceiver, - // let the device handle the event. If not, we do it ourselves. - var haveChangedStateOtherThanNoise = true; - if (deviceIsStateCallbackReceiver) - { - m_ShouldMakeCurrentlyUpdatingDeviceCurrent = true; - // NOTE: We leave it to the device to make sure the event has the right format. This allows the - // device to handle multiple different incoming formats. - ((IInputStateCallbackReceiver)device).OnStateEvent(eventPtr); + m_InputEventStream.Advance(leaveEventInBuffer: false); - haveChangedStateOtherThanNoise = m_ShouldMakeCurrentlyUpdatingDeviceCurrent; - } - else - { - // If the state format doesn't match, ignore the event. - if (device.stateBlock.format != eventPtr.stateFormat) - { - #if UNITY_EDITOR - m_Diagnostics?.OnEventFormatMismatch(currentEventReadPtr, device); - #endif - break; - } + // Discard events in case the maximum event bytes per update has been exceeded + if (AreMaximumEventBytesPerUpdateExceeded(totalEventBytesProcessed)) + break; + } - haveChangedStateOtherThanNoise = UpdateState(device, eventPtr, updateType); - } + m_Metrics.totalEventProcessingTime += + ((double)(Stopwatch.GetTimestamp() - processingStartTime)) / Stopwatch.Frequency; + m_Metrics.totalEventLagTime += totalEventLag; - totalEventBytesProcessed += eventPtr.sizeInBytes; + ResetCurrentProcessedEventBytesForDevices(); - device.m_CurrentProcessedEventBytesOnUpdate += eventPtr.sizeInBytes; + m_InputEventStream.Close(ref eventBuffer); + } + catch (Exception) + { + // We need to restore m_InputEventStream to a sound state + // to avoid failing recursive OnUpdate check next frame. + m_InputEventStream.CleanUpAfterException(); + throw; + } +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + m_DiscardOutOfFocusEvents = false; +#endif + } - // Update timestamp on device. - // NOTE: We do this here and not in UpdateState() so that InputState.Change() will *NOT* change timestamps. - // Only events should. If running play mode updates in editor, we want to defer to the play mode - // callbacks to set the last update time to avoid dropping events only processed by the editor state. - if (device.m_LastUpdateTimeInternal <= eventPtr.internalTime - #if UNITY_EDITOR - && !(updateType == InputUpdateType.Editor && runPlayerUpdatesInEditMode) - #endif - ) - device.m_LastUpdateTimeInternal = eventPtr.internalTime; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ProcessBeforeRenderStateEvents(out InputDevice device, out InputEvent* currentEventPtr) + { + device = null; + currentEventPtr = m_InputEventStream.currentEventPtr; - // Make device current. Again, only do this when receiving events. - if (haveChangedStateOtherThanNoise) - device.MakeCurrent(); + // Process before render state events + while (m_InputEventStream.remainingEventCount > 0) + { + Debug.Assert(!currentEventPtr->handled, + "Iterated to event in buffer that is already marked as handled"); - break; + device = TryGetDeviceById(currentEventPtr->deviceId); + if (device != null && device.updateBeforeRender && + (currentEventPtr->type == StateEvent.Type || + currentEventPtr->type == DeltaStateEvent.Type)) + break; - case TextEvent.Type: - { - var textEventPtr = (TextEvent*)currentEventReadPtr; - if (device is ITextInputReceiver textInputReceiver) - { - var utf32Char = textEventPtr->character; - if (utf32Char >= 0x10000) - { - // Send surrogate pair. - utf32Char -= 0x10000; - var highSurrogate = 0xD800 + ((utf32Char >> 10) & 0x3FF); - var lowSurrogate = 0xDC00 + (utf32Char & 0x3FF); + currentEventPtr = m_InputEventStream.Advance(leaveEventInBuffer: true); + } + } - textInputReceiver.OnTextInput((char)highSurrogate); - textInputReceiver.OnTextInput((char)lowSurrogate); - } - else - { - // Send single, plain character. - textInputReceiver.OnTextInput((char)utf32Char); - } - } +#if UNITY_EDITOR + // Handles editor-specific focus/background early-out behavior and advances the stream accordingly. + // Returns true if event should be skipped (stream advanced), false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe bool SkipEventDueToEditorBehaviour(InputUpdateType updateType, FourCC currentEventType, bool dropStatusEvents, double currentEventTimeInternal) + { + var focusEventType = new FourCC(FocusConstants.kEventType); + + // Check if the next event is a FocusEvent. If so, cancel any in-progress actions before + // the focus change to prevent half-processed action states (e.g., Started without Performed). + // This avoids the problem of dropping individual events (which can cause button stuck states) + // by instead explicitly resetting action state at the right time. + var possibleFocusEvent = m_InputEventStream.Peek(); + if (possibleFocusEvent != null && possibleFocusEvent->type == focusEventType && + !gameShouldGetInputRegardlessOfFocus) + { + // Cancel all in-progress actions for all devices to prevent actions getting stuck + // in Started phase when focus changes cause events to be routed differently. + // We use SoftReset which cancels actions but doesn't reset device state. + for (var i = 0; i < m_DevicesCount; ++i) + { + var device = m_Devices[i]; + if (device.enabled) + InputActionState.OnDeviceChange(device, InputDeviceChange.SoftReset); + } + } - break; - } + // When the game is playing and has focus, we never process input in editor updates. + // All we do is just switch to editor state buffers and then exit. + if (gameIsPlaying && gameHasFocus && updateType == InputUpdateType.Editor + && currentEventType != focusEventType) + { + m_InputEventStream.Advance(true); + return true; + } - case IMECompositionEvent.Type: - { - var imeEventPtr = (IMECompositionEvent*)currentEventReadPtr; - var textInputReceiver = device as ITextInputReceiver; - textInputReceiver?.OnIMECompositionChanged(imeEventPtr->compositionString); - break; - } + //if we dont have focus and the editor behaviour is all input goes to gameview, which is the same behaviour as in a player + // and we are not allowed to run in the background or the background behaviour is that we reset and disable all devices + // If out of focus and runInBackground is off and ExactlyAsInPlayer is on, discard input. + if (!gameHasFocus + && m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView + && (!m_Runtime.runInBackground || m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices) + && currentEventType != focusEventType) + { + m_InputEventStream.Advance(false); + return true; + } - case DeviceRemoveEvent.Type: - { - RemoveDevice(device, keepOnListOfAvailableDevices: false); + // Check various PlayMode specific early exit conditions + if (ShouldExitEarlyBasedOnBackgroundBehavior(currentEventType, updateType)) + { + m_InputEventStream.Advance(true); + return true; + } - // If it's a native device with a description, put it on the list of disconnected - // devices. - if (device.native && !device.description.empty) - { - ArrayHelpers.AppendWithCapacity(ref m_DisconnectedDevices, - ref m_DisconnectedDevicesCount, device); - DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, - device, InputDeviceChange.Disconnected, k_InputOnDeviceChangeMarker, "InputSystem.onDeviceChange"); - } + if (dropStatusEvents) + { + // If the type here is a status event, ask advance not to leave the event in the buffer. Otherwise, leave it there. + if (currentEventType == StateEvent.Type || currentEventType == DeltaStateEvent.Type || currentEventType == IMECompositionEvent.Type) + m_InputEventStream.Advance(false); + else + m_InputEventStream.Advance(true); - break; - } + return true; + } - case DeviceConfigurationEvent.Type: - device.NotifyConfigurationChanged(); - InputActionState.OnDeviceChange(device, InputDeviceChange.ConfigurationChanged); - DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, - device, InputDeviceChange.ConfigurationChanged, k_InputOnDeviceChangeMarker, "InputSystem.onDeviceChange"); - break; + // Decide to skip events based on timing + if (ShouldDiscardEventInEditor(currentEventType, currentEventTimeInternal, updateType)) + { + m_InputEventStream.Advance(false); + return true; + } + return false; + } - case DeviceResetEvent.Type: - ResetDevice(device, - alsoResetDontResetControls: ((DeviceResetEvent*)currentEventReadPtr)->hardReset); - break; - } +#endif - m_InputEventStream.Advance(leaveEventInBuffer: false); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe bool ShouldDeferEventBetweenEditorAndPlayerUpdates(InputUpdateType updateType, FourCC currentEventType, InputDevice device) + { + var focusEventType = new FourCC(FocusConstants.kEventType); - // Discard events in case the maximum event bytes per update has been exceeded - if (AreMaximumEventBytesPerUpdateExceeded(totalEventBytesProcessed)) - break; + // If the event is a focus event, we want to let it through so that we can properly update our internal state of whether we have focus or not. + // This is crucial for making sure that we don't end up in a state where we have focus but are still dropping events because we haven't processed the focus event yet. + if (!(gameIsPlaying && !gameHasFocus) || currentEventType == focusEventType) + return false; + + // In the editor, we may need to bump events from editor updates into player updates and vice versa. + if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.PointersAndKeyboardsRespectGameViewFocus + && m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.ResetAndDisableAllDevices) + { + var isPointerOrKeyboard = device is Pointer || device is Keyboard; + // In player update, defer pointer/keyboard events to editor update. + if (updateType != InputUpdateType.Editor) + { + // Let everything but pointer and keyboard input through. + // If the event is from a pointer or keyboard, leave it in the buffer so it can be dealt with + // in a subsequent editor update. Otherwise, take it out. + if (isPointerOrKeyboard) + { + m_InputEventStream.Advance(true); + return true; + } + } + else + { + // In editor update, defer non-pointer/keyboard events to player update + // and let only pointer and keyboard input through. + if (!isPointerOrKeyboard) + { + m_InputEventStream.Advance(true); + return true; + } } + } + return false; + } - m_Metrics.totalEventProcessingTime += - ((double)(Stopwatch.GetTimestamp() - processingStartTime)) / Stopwatch.Frequency; - m_Metrics.totalEventLagTime += totalEventLag; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe bool MergeWithNextEvent(InputDevice device, InputEvent* currentEventReadPtr, bool timesliceEvents, double currentTime, + ref InputEvent* skipEventMergingFor) + { + // NOTE: This relies on events in the buffer being consecutive for the same device. This is not + // necessarily the case for events coming in from the background event queue where parallel + // producers may create interleaved input sequences. This will be fixed once we have the + // new buffering scheme for input events working in the native runtime. - ResetCurrentProcessedEventBytesForDevices(); + var nextEvent = m_InputEventStream.Peek(); + // If there is no next event after current one, early out. + if (nextEvent == null) + return false; - m_InputEventStream.Close(ref eventBuffer); + // if next event is for a different device, we cannot merge, so early out. + if (currentEventReadPtr->deviceId != nextEvent->deviceId) + return false; + + // if next event is not in the same timeslicing slot, early out. + if (timesliceEvents && !(nextEvent->internalTime < currentTime)) + return false; + + // Then try to merge current event into next event. + if (((IEventMerger)device).MergeForward(currentEventReadPtr, nextEvent)) + { + // And if succeeded, skip current event, as it was merged into next event. + m_InputEventStream.Advance(false); + return true; } - catch (Exception) + + // If we can't merge current event with next one for any reason, we assume the next event + // carries crucial entropy (button changed state, phase changed, counter changed, etc). + // Hence semantic meaning for current event is "can't merge current with next because next is different". + // But semantic meaning for next event is "next event carries important information and should be preserved", + // from that point of view next event should not be merged with current nor with _next after next_ event. + // + // For example, given such stream of events: + // Mouse Mouse Mouse Mouse Mouse Mouse Mouse + // Event no1 Event no2 Event no3 Event no4 Event no5 Event no6 Event no7 + // Time 1 Time 2 Time 3 Time 4 Time 5 Time 6 Time 7 + // Pos(10,20) Pos(12,21) Pos(13,23) Pos(14,24) Pos(16,25) Pos(17,27) Pos(18,28) + // Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) + // BtnLeft(0) BtnLeft(0) BtnLeft(0) BtnLeft(1) BtnLeft(1) BtnLeft(1) BtnLeft(1) + // + // if we then merge without skipping next event here: + // Mouse Mouse + // Event no3 Event no7 + // Time 3 Time 7 + // Pos(13,23) Pos(18,28) + // Delta(4,4) Delta(5,5) + // BtnLeft(0) BtnLeft(1) + // + // As you can see, the event no4 containing mouse button press was lost, + // and with it we lose the important information of timestamp of mouse button press. + // + // With skipping merging next event we will get: + // Mouse Mouse Mouse + // Time 3 Time 4 Time 7 + // Event no3 Event no4 Event no7 + // Pos(13,23) Pos(14,24) Pos(18,28) + // Delta(3,3) Delta(1,1) Delta(4,4) + // BtnLeft(0) BtnLeft(1) BtnLeft(1) + // + // And no4 is preserved, with the exact timestamp of button press. + skipEventMergingFor = nextEvent; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ProcessEvent(InputDevice device, InputUpdateType updateType, InputEvent* currentEventReadPtr, ref uint totalEventBytesProcessed, double currentEventTimeInternal) + { + var currentEventType = currentEventReadPtr->type; + + // Process. + switch (currentEventType) { - // We need to restore m_InputEventStream to a sound state - // to avoid failing recursive OnUpdate check next frame. - k_InputUpdateProfilerMarker.End(); - m_InputEventStream.CleanUpAfterException(); - throw; + case StateEvent.Type: + case DeltaStateEvent.Type: + ProcessStateEvent(device, updateType, currentEventReadPtr, ref totalEventBytesProcessed, currentEventTimeInternal); + break; + + case TextEvent.Type: + ProcessTextEvent(device, currentEventReadPtr); + break; + + case IMECompositionEvent.Type: + ProcessIMECompositionEvent(device, currentEventReadPtr); + break; + + case DeviceRemoveEvent.Type: + ProcessDeviceRemoveEvent(device); + break; + + case DeviceConfigurationEvent.Type: + ProcessDeviceConfigurationEvent(device); + break; + + case DeviceResetEvent.Type: + ResetDevice(device, alsoResetDontResetControls: ((DeviceResetEvent*)currentEventReadPtr)->hardReset); + break; +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + case FocusConstants.kEventType: + ProcessFocusEvent(currentEventReadPtr); + break; +#endif } + } - m_DiscardOutOfFocusEvents = false; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ProcessStateEvent(InputDevice device, InputUpdateType updateType, InputEvent* currentEventReadPtr, ref uint totalEventBytesProcessed, double currentEventTimeInternal) + { + var eventPtr = new InputEventPtr(currentEventReadPtr); + + // Ignore the event if the last state update we received for the device was + // newer than this state event is. We don't allow devices to go back in time. + // + // NOTE: We make an exception here for devices that implement IInputStateCallbackReceiver (such + // as Touchscreen). For devices that dynamically incorporate state it can be hard ensuring + // a global ordering of events as there may be multiple substreams (e.g. each individual touch) + // that are generated in the backend and would require considerable work to ensure monotonically + // increasing timestamps across all such streams. + var deviceIsStateCallbackReceiver = device.hasStateCallbacks; + if (currentEventTimeInternal < device.m_LastUpdateTimeInternal && + !(deviceIsStateCallbackReceiver && device.stateBlock.format != eventPtr.stateFormat)) + { +#if UNITY_EDITOR + m_Diagnostics?.OnEventTimestampOutdated(new InputEventPtr(currentEventReadPtr), device); +#elif UNITY_ANDROID + // Android keyboards can send events out of order: Holding down a key will send multiple + // presses after a short time, like on most platforms. Unfortunately, on Android, the + // last of these "presses" can be timestamped to be after the event of the key release. + // If that happens, we'd skip the keyUp here, and the device state will have the key + // "stuck" pressed. So, special case here to not skip keyboard events on Android. ISXB-475 + // N.B. Android seems to have similar issues with touch input (OnStateEvent, Touchscreen.cs) + if (!(device is Keyboard)) +#endif + return; + } - if (shouldProcessActionTimeouts) - ProcessStateChangeMonitorTimeouts(); + // Update the state of the device from the event. If the device is an IInputStateCallbackReceiver, + // let the device handle the event. If not, we do it ourselves. + var haveChangedStateOtherThanNoise = true; + if (deviceIsStateCallbackReceiver) + { + m_ShouldMakeCurrentlyUpdatingDeviceCurrent = true; + // NOTE: We leave it to the device to make sure the event has the right format. This allows the + // device to handle multiple different incoming formats. + ((IInputStateCallbackReceiver)device).OnStateEvent(eventPtr); - k_InputUpdateProfilerMarker.End(); - ////FIXME: need to ensure that if someone calls QueueEvent() from an onAfterUpdate callback, we don't end up with a - //// mess in the event buffer - //// same goes for events that someone may queue from a change monitor callback - InvokeAfterUpdateCallback(updateType); - //send pointer data to backend for OnMouseEvents -#if UNITY_INPUTSYSTEM_SUPPORTS_MOUSE_SCRIPT_EVENTS - var pointer = Pointer.current; - if (pointer != null && pointer.added && gameIsPlaying) - NativeInputSystem.DoSendMouseEvents(pointer.press.isPressed, pointer.press.wasPressedThisFrame, pointer.position.x.value, pointer.position.y.value); + haveChangedStateOtherThanNoise = m_ShouldMakeCurrentlyUpdatingDeviceCurrent; + } + else + { + // If the state format doesn't match, ignore the event. + if (device.stateBlock.format != eventPtr.stateFormat) + { +#if UNITY_EDITOR + m_Diagnostics?.OnEventFormatMismatch(currentEventReadPtr, device); #endif - m_CurrentUpdate = default; + return; + } + + haveChangedStateOtherThanNoise = UpdateState(device, eventPtr, updateType); + } + + totalEventBytesProcessed += eventPtr.sizeInBytes; + device.m_CurrentProcessedEventBytesOnUpdate += eventPtr.sizeInBytes; + + // Update timestamp on device. + // NOTE: We do this here and not in UpdateState() so that InputState.Change() will *NOT* change timestamps. + // Only events should. If running play mode updates in editor, we want to defer to the play mode + // callbacks to set the last update time to avoid dropping events only processed by the editor state. + if (device.m_LastUpdateTimeInternal <= eventPtr.internalTime +#if UNITY_EDITOR + && !(updateType == InputUpdateType.Editor && runPlayerUpdatesInEditMode) +#endif + ) + device.m_LastUpdateTimeInternal = eventPtr.internalTime; + + // Make device current. Again, only do this when receiving events. + if (haveChangedStateOtherThanNoise) + device.MakeCurrent(); } - /// - /// Determines if the event buffer should be flushed without processing events. - /// - /// True if the buffer should be flushed, false otherwise. - private bool ShouldFlushEventBuffer() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ProcessTextEvent(InputDevice device, InputEvent* currentEventReadPtr) + { + var textEventPtr = (TextEvent*)currentEventReadPtr; + if (device is ITextInputReceiver textInputReceiver) + { + var utf32Char = textEventPtr->character; + if (utf32Char >= 0x10000) + { + // Send surrogate pair. + utf32Char -= 0x10000; + var highSurrogate = 0xD800 + ((utf32Char >> 10) & 0x3FF); + var lowSurrogate = 0xDC00 + (utf32Char & 0x3FF); + + textInputReceiver.OnTextInput((char)highSurrogate); + textInputReceiver.OnTextInput((char)lowSurrogate); + } + else + { + // Send single, plain character. + textInputReceiver.OnTextInput((char)utf32Char); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ProcessIMECompositionEvent(InputDevice device, InputEvent* currentEventReadPtr) + { + var imeEventPtr = (IMECompositionEvent*)currentEventReadPtr; + var textInputReceiver = device as ITextInputReceiver; + textInputReceiver?.OnIMECompositionChanged(imeEventPtr->compositionString); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessDeviceRemoveEvent(InputDevice device) + { + RemoveDevice(device, keepOnListOfAvailableDevices: false); + + // If it's a native device with a description, put it on the list of disconnected + // devices. + if (device.native && !device.description.empty) + { + ArrayHelpers.AppendWithCapacity(ref m_DisconnectedDevices, + ref m_DisconnectedDevicesCount, device); + DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, + device, InputDeviceChange.Disconnected, k_InputOnDeviceChangeMarker, "InputSystem.onDeviceChange"); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessDeviceConfigurationEvent(InputDevice device) + { + device.NotifyConfigurationChanged(); + InputActionState.OnDeviceChange(device, InputDeviceChange.ConfigurationChanged); + DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, + device, InputDeviceChange.ConfigurationChanged, k_InputOnDeviceChangeMarker, "InputSystem.onDeviceChange"); + } + +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ProcessFocusEvent(InputEvent* currentEventReadPtr) { + var focusEventPtr = (InputFocusEvent*)currentEventReadPtr; + FocusFlags state = focusEventPtr->focusFlags; + focusState = state; + #if UNITY_EDITOR - // If out of focus and runInBackground is off and ExactlyAsInPlayer is on, discard input. - if (!gameHasFocus && - m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView - && - (!m_Runtime.runInBackground || m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices)) - return true; + SyncAllDevicesWhenEditorIsActivated(); + + if (!m_Runtime.isInPlayMode) + return; +#endif + + bool runInBackground = +#if UNITY_EDITOR + // In the editor, the player loop will always be run even if the Game View does not have focus. This + // amounts to runInBackground being always true in the editor, regardless of what the setting in + // the Player Settings window is. + // + // If, however, "Game View Focus" is set to "Exactly As In Player", we force code here down the same + // path as in the player. + //if we are in editor, and the editor input behaviour is that everything that is pressed goes to the game view, or we are allowed to run in the background + m_Settings.editorInputBehaviorInPlayMode != InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView + || m_Runtime.runInBackground; #else - // In player builds, flush if out of focus and not running in background - if (!gameHasFocus && !m_Runtime.runInBackground) - return true; + m_Runtime.runInBackground; #endif - return false; + + // BackgroundBehavior.IgnoreFocus means we ignore any state changes to the device, so we can early out + // If runInBackground is true, no device changes should happen, even when focus is gained. So early out. + // If runInBackground is false, we still want to sync devices when focus is gained. So we need to continue further. + if (m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus && runInBackground) + return; + + // Cache original device count in case it changes while we are processing devices. + var deviceCount = m_DevicesCount; + for (var i = 0; i < m_DevicesCount; ++i) + { + var device = m_Devices[i]; + + if (focusEventPtr->hasApplicationFocus) + UpdateDeviceStateOnFocusGained(device, runInBackground); + else + UpdateDeviceStateOnFocusLost(device, runInBackground); + + if (m_DevicesCount != deviceCount) + { + // In case we invoked a callback that messed with our device array, adjust our index. + var index = m_Devices.IndexOfReference(device, m_DevicesCount); + if (index == -1) + { + --i; + deviceCount = m_DevicesCount; + } + else + { + i = index; + deviceCount = m_DevicesCount; + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateDeviceStateOnFocusGained(InputDevice device, bool runInBackground) + { + // Re-enable the device if we disabled it on focus loss. This will also issue a sync. + if (device.disabledWhileInBackground) + { + EnableOrDisableDevice(device, true, DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + } + else if (device.enabled && !runInBackground) + { + bool requestSync = device.RequestSync(); + // Try to sync. If it fails and we didn't run in the background, perform + // a reset instead. This is to cope with backends that are unable to sync but + // may still retain state which now may be outdated because the input device may + // have changed state while we weren't running. So at least make the backend flush + // its state (if any). + if (!requestSync && m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.IgnoreFocus) + ResetDevice(device); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateDeviceStateOnFocusLost(InputDevice device, bool runInBackground) + { + if (!device.enabled || !runInBackground) + return; + + switch (m_Settings.backgroundBehavior) + { + case InputSettings.BackgroundBehavior.ResetAndDisableAllDevices: + { + // Disable the device. This will also soft-reset it. + EnableOrDisableDevice(device, false, DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + } + break; + case InputSettings.BackgroundBehavior.ResetAndDisableNonBackgroundDevices: + { + // Disable the device. This will also soft-reset it. + if (!ShouldRunDeviceInBackground(device)) + EnableOrDisableDevice(device, false, DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + } + break; + } } /// - /// Determines if we should exit early from event processing without handling events. + /// Determines if status events should be dropped and modifies early exit behavior accordingly. /// /// The current event buffer - /// Whether the buffer can be flushed - /// The current update type - /// True if we should exit early, false otherwise. - private bool ShouldExitEarlyFromEventProcessing(InputUpdateType updateType) + /// True if status events should be dropped, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ShouldDropStatusEvents(InputEventBuffer eventBuffer) { -#if UNITY_EDITOR - // Check various PlayMode specific early exit conditions - if (ShouldExitEarlyBasedOnBackgroundBehavior(updateType)) - return true; + // If the game is not playing but we're sending all input events to the game, + // the buffer can just grow unbounded. So, in that case, set a flag to say we'd + // like to drop status events, and do not early out. + return (!gameIsPlaying && gameShouldGetInputRegardlessOfFocus && (eventBuffer.sizeInBytes > (100 * 1024))); + } - // When the game is playing and has focus, we never process input in editor updates. - // All we do is just switch to editor state buffers and then exit. - if ((gameIsPlaying && gameHasFocus && updateType == InputUpdateType.Editor)) - return true; -#endif +#endif // UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS - return false; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FinalizeUpdate(InputUpdateType updateType) + { + ////FIXME: need to ensure that if someone calls QueueEvent() from an onAfterUpdate callback, we don't end up with a + //// mess in the event buffer + //// same goes for events that someone may queue from a change monitor callback + InvokeAfterUpdateCallback(updateType); + //send pointer data to backend for OnMouseEvents +#if UNITY_INPUTSYSTEM_SUPPORTS_MOUSE_SCRIPT_EVENTS + var pointer = Pointer.current; + if (pointer != null && pointer.added && gameIsPlaying) + NativeInputSystem.DoSendMouseEvents(pointer.press.isPressed, pointer.press.wasPressedThisFrame, pointer.position.x.value, pointer.position.y.value); +#endif + m_CurrentUpdate = default; } #if UNITY_EDITOR @@ -3834,10 +3997,13 @@ private bool ShouldExitEarlyFromEventProcessing(InputUpdateType updateType) /// Whenever this method returns true, it usually means that events are left in the buffer and should be /// processed in a next update call. /// - private bool ShouldExitEarlyBasedOnBackgroundBehavior(InputUpdateType updateType) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ShouldExitEarlyBasedOnBackgroundBehavior(FourCC currentEventType, InputUpdateType updateType) { // In Play Mode, if we're in the background and not supposed to process events in this update - if ((!gameHasFocus || gameShouldGetInputRegardlessOfFocus) && updateType != InputUpdateType.Editor) + if ((!gameHasFocus || gameShouldGetInputRegardlessOfFocus) + && updateType != InputUpdateType.Editor + && currentEventType != new FourCC(FocusConstants.kEventType)) { if (m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices || m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDevicesRespectGameViewFocus) @@ -3854,25 +4020,6 @@ private bool ShouldExitEarlyBasedOnBackgroundBehavior(InputUpdateType updateType return false; } - /// - /// Determines if status events should be dropped and modifies early exit behavior accordingly. - /// - /// The current event buffer - /// Reference to the early exit flag that may be modified - /// True if status events should be dropped, false otherwise. - private bool ShouldDropStatusEvents(InputEventBuffer eventBuffer, ref bool canEarlyOut) - { - // If the game is not playing but we're sending all input events to the game, - // the buffer can just grow unbounded. So, in that case, set a flag to say we'd - // like to drop status events, and do not early out. - if (!gameIsPlaying && gameShouldGetInputRegardlessOfFocus && (eventBuffer.sizeInBytes > (100 * 1024))) - { - canEarlyOut = false; - return true; - } - return false; - } - /// /// Determines if an event should be discarded based on timing or focus state. /// @@ -3880,8 +4027,14 @@ private bool ShouldDropStatusEvents(InputEventBuffer eventBuffer, ref bool canEa /// The internal time of the current event /// The current update type /// True if the event should be discarded, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldDiscardEventInEditor(FourCC eventType, double eventTime, InputUpdateType updateType) { +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + // Check if this is an event that occurred during edit mode transition + return ShouldDiscardEditModeTransitionEvent(eventType, eventTime, updateType); +#else + // Check if this is an event that occurred during edit mode transition if (ShouldDiscardEditModeTransitionEvent(eventType, eventTime, updateType)) return true; @@ -3891,6 +4044,7 @@ private bool ShouldDiscardEventInEditor(FourCC eventType, double eventTime, Inpu return true; return false; +#endif } /// @@ -3904,6 +4058,7 @@ private bool ShouldDiscardEventInEditor(FourCC eventType, double eventTime, Inpu /// Could be that ultimately we need to issue a full reset of all devices at the beginning of /// play mode in the editor. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ShouldDiscardEditModeTransitionEvent(FourCC eventType, double eventTime, InputUpdateType updateType) { return (eventType == StateEvent.Type || eventType == DeltaStateEvent.Type) && @@ -3914,17 +4069,6 @@ private bool ShouldDiscardEditModeTransitionEvent(FourCC eventType, double event InputSystem.domainStateManager.enterPlayModeTime == 0); } - /// - /// Checks if an event should be discarded because it occurred while out of focus, under specific settings. - /// - private bool ShouldDiscardOutOfFocusEvent(double eventTime) - { - // If we care about focus, check if the event occurred while out of focus based on its timestamp. - if (gameHasFocus && m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.IgnoreFocus) - return m_DiscardOutOfFocusEvents && eventTime < m_FocusRegainedTime; - return false; - } - #endif bool AreMaximumEventBytesPerUpdateExceeded(uint totalEventBytesProcessed) @@ -4123,14 +4267,14 @@ internal unsafe bool UpdateState(InputDevice device, InputUpdateType updateType, var flipped = FlipBuffersForDeviceIfNecessary(device, updateType); // Now write the state. - #if UNITY_EDITOR +#if UNITY_EDITOR if (updateType == InputUpdateType.Editor) { WriteStateChange(m_StateBuffers.m_EditorStateBuffers, deviceIndex, ref stateBlockOfDevice, stateOffsetInDevice, statePtr, stateSize, flipped); } else - #endif +#endif { WriteStateChange(m_StateBuffers.m_PlayerStateBuffers, deviceIndex, ref stateBlockOfDevice, stateOffsetInDevice, statePtr, stateSize, flipped); @@ -4144,13 +4288,13 @@ internal unsafe bool UpdateState(InputDevice device, InputUpdateType updateType, { foreach (var button in device.m_UpdatedButtons) { - #if UNITY_EDITOR +#if UNITY_EDITOR if (updateType == InputUpdateType.Editor) { ((ButtonControl)device.allControls[button]).UpdateWasPressedEditor(); } else - #endif +#endif ((ButtonControl)device.allControls[button]).UpdateWasPressed(); } } @@ -4159,13 +4303,13 @@ internal unsafe bool UpdateState(InputDevice device, InputUpdateType updateType, int buttonCount = 0; foreach (var button in device.m_ButtonControlsCheckingPressState) { - #if UNITY_EDITOR +#if UNITY_EDITOR if (updateType == InputUpdateType.Editor) { button.UpdateWasPressedEditor(); } else - #endif +#endif button.UpdateWasPressed(); ++buttonCount; @@ -4247,7 +4391,7 @@ private bool FlipBuffersForDeviceIfNecessary(InputDevice device, InputUpdateType return false; } - #if UNITY_EDITOR +#if UNITY_EDITOR ////REVIEW: should this use the editor update ticks as quasi-frame-boundaries? // Updates go to the editor only if the game isn't playing or does not have focus. // Otherwise we fall through to the logic that flips for the *next* dynamic and @@ -4261,7 +4405,7 @@ private bool FlipBuffersForDeviceIfNecessary(InputDevice device, InputUpdateType m_StateBuffers.m_EditorStateBuffers.SwapBuffers(device.m_DeviceIndex); return true; } - #endif +#endif // Flip buffers if we haven't already for this frame. if (device.m_CurrentUpdateStepCount != InputUpdate.s_UpdateStepCount) @@ -4279,7 +4423,7 @@ private bool FlipBuffersForDeviceIfNecessary(InputDevice device, InputUpdateType // Stuff everything that we want to survive a domain reload into // a m_SerializedState. - #if UNITY_EDITOR || DEVELOPMENT_BUILD +#if UNITY_EDITOR || DEVELOPMENT_BUILD [Serializable] internal struct DeviceState { @@ -4337,9 +4481,9 @@ internal struct SerializedState public InputSettings settings; public InputActionAsset actions; - #if UNITY_ANALYTICS || UNITY_EDITOR +#if UNITY_ANALYTICS || UNITY_EDITOR public bool haveSentStartupAnalytics; - #endif +#endif } internal SerializedState SaveState() @@ -4371,9 +4515,9 @@ internal SerializedState SaveState() return new SerializedState { layoutRegistrationVersion = m_LayoutRegistrationVersion, - #if !UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY +#if !UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY pollingFrequency = m_PollingFrequency, - #endif +#endif inputEventHandledPolicy = m_InputEventHandledPolicy, devices = deviceArray, availableDevices = m_AvailableDevices?.Take(m_AvailableDeviceCount).ToArray(), @@ -4385,9 +4529,9 @@ internal SerializedState SaveState() settings = m_Settings, actions = m_Actions, - #if UNITY_ANALYTICS || UNITY_EDITOR +#if UNITY_ANALYTICS || UNITY_EDITOR haveSentStartupAnalytics = m_HaveSentStartupAnalytics, - #endif +#endif }; } @@ -4398,9 +4542,9 @@ internal void RestoreStateWithoutDevices(SerializedState state) updateMask = state.updateMask; scrollDeltaBehavior = state.scrollDeltaBehavior; m_Metrics = state.metrics; - #if !UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY +#if !UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY m_PollingFrequency = state.pollingFrequency; - #endif +#endif m_InputEventHandledPolicy = state.inputEventHandledPolicy; // Cached settings might be null if the ScriptableObject was destroyed; create new default instance in this case. @@ -4419,9 +4563,9 @@ internal void RestoreStateWithoutDevices(SerializedState state) // and hence ownership lies with ADB. m_Actions = state.actions; - #if UNITY_ANALYTICS || UNITY_EDITOR +#if UNITY_ANALYTICS || UNITY_EDITOR m_HaveSentStartupAnalytics = state.haveSentStartupAnalytics; - #endif +#endif ////REVIEW: instead of accessing globals here, we could move this to when we re-create devices @@ -4447,74 +4591,73 @@ internal void RestoreStateWithoutDevices(SerializedState state) /// internal void RestoreDevicesAfterDomainReload() { - k_InputRestoreDevicesAfterReloadMarker.Begin(); - - using (InputDeviceBuilder.Ref()) + using (k_InputRestoreDevicesAfterReloadMarker.Auto()) { - DeviceState[] retainedDeviceStates = null; - var deviceStates = m_SavedDeviceStates; - var deviceCount = m_SavedDeviceStates.LengthSafe(); - m_SavedDeviceStates = null; // Prevent layout matcher registering themselves on the fly from picking anything off this list. - for (var i = 0; i < deviceCount; ++i) + using (InputDeviceBuilder.Ref()) { - ref var deviceState = ref deviceStates[i]; - - var device = TryGetDeviceById(deviceState.deviceId); - if (device != null) - continue; - - var layout = TryFindMatchingControlLayout(ref deviceState.description, - deviceState.deviceId); - if (layout.IsEmpty()) + DeviceState[] retainedDeviceStates = null; + var deviceStates = m_SavedDeviceStates; + var deviceCount = m_SavedDeviceStates.LengthSafe(); + m_SavedDeviceStates = null; // Prevent layout matcher registering themselves on the fly from picking anything off this list. + for (var i = 0; i < deviceCount; ++i) { - var previousLayout = new InternedString(deviceState.layout); - if (m_Layouts.HasLayout(previousLayout)) - layout = previousLayout; - } - if (layout.IsEmpty() || !RestoreDeviceFromSavedState(ref deviceState, layout)) - ArrayHelpers.Append(ref retainedDeviceStates, deviceState); - } + ref var deviceState = ref deviceStates[i]; - // See if we can make sense of an available device now that we couldn't make sense of - // before. This can be the case if there's new layout information that wasn't available - // before. - if (m_SavedAvailableDevices != null) - { - m_AvailableDevices = m_SavedAvailableDevices; - m_AvailableDeviceCount = m_SavedAvailableDevices.LengthSafe(); - for (var i = 0; i < m_AvailableDeviceCount; ++i) - { - var device = TryGetDeviceById(m_AvailableDevices[i].deviceId); + var device = TryGetDeviceById(deviceState.deviceId); if (device != null) continue; - if (m_AvailableDevices[i].isRemoved) - continue; + var layout = TryFindMatchingControlLayout(ref deviceState.description, + deviceState.deviceId); + if (layout.IsEmpty()) + { + var previousLayout = new InternedString(deviceState.layout); + if (m_Layouts.HasLayout(previousLayout)) + layout = previousLayout; + } + if (layout.IsEmpty() || !RestoreDeviceFromSavedState(ref deviceState, layout)) + ArrayHelpers.Append(ref retainedDeviceStates, deviceState); + } - var layout = TryFindMatchingControlLayout(ref m_AvailableDevices[i].description, - m_AvailableDevices[i].deviceId); - if (!layout.IsEmpty()) + // See if we can make sense of an available device now that we couldn't make sense of + // before. This can be the case if there's new layout information that wasn't available + // before. + if (m_SavedAvailableDevices != null) + { + m_AvailableDevices = m_SavedAvailableDevices; + m_AvailableDeviceCount = m_SavedAvailableDevices.LengthSafe(); + for (var i = 0; i < m_AvailableDeviceCount; ++i) { - try - { - AddDevice(layout, m_AvailableDevices[i].deviceId, - deviceDescription: m_AvailableDevices[i].description, - deviceFlags: m_AvailableDevices[i].isNative ? InputDevice.DeviceFlags.Native : 0); - } - catch (Exception) + var device = TryGetDeviceById(m_AvailableDevices[i].deviceId); + if (device != null) + continue; + + if (m_AvailableDevices[i].isRemoved) + continue; + + var layout = TryFindMatchingControlLayout(ref m_AvailableDevices[i].description, + m_AvailableDevices[i].deviceId); + if (!layout.IsEmpty()) { - // Just ignore. Simply means we still can't really turn the device into something useful. + try + { + AddDevice(layout, m_AvailableDevices[i].deviceId, + deviceDescription: m_AvailableDevices[i].description, + deviceFlags: m_AvailableDevices[i].isNative ? InputDevice.DeviceFlags.Native : 0); + } + catch (Exception) + { + // Just ignore. Simply means we still can't really turn the device into something useful. + } } } } - } - // Done. Discard saved arrays. - m_SavedDeviceStates = retainedDeviceStates; - m_SavedAvailableDevices = null; + // Done. Discard saved arrays. + m_SavedDeviceStates = retainedDeviceStates; + m_SavedAvailableDevices = null; + } } - - k_InputRestoreDevicesAfterReloadMarker.End(); } /// diff --git a/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs b/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs index 13fc987173..681d52a795 100644 --- a/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs +++ b/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs @@ -213,6 +213,7 @@ public Action onShutdown } } +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS public Action onPlayerFocusChanged { get => m_FocusChangedMethod; @@ -225,8 +226,15 @@ public Action onPlayerFocusChanged m_FocusChangedMethod = value; } } +#endif - public bool isPlayerFocused => Application.isFocused; + private FocusFlags m_FocusState = FocusFlags.None; + public FocusFlags focusState + { + get => m_FocusState; + set => m_FocusState = value; + } + public bool isPlayerFocused => (m_FocusState & FocusFlags.ApplicationFocus) != FocusFlags.None; public float pollingFrequency { @@ -301,6 +309,7 @@ private bool OnWantsToShutdown() return true; } +#if !UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS private Action m_FocusChangedMethod; private void OnFocusChanged(bool focus) @@ -308,6 +317,8 @@ private void OnFocusChanged(bool focus) m_FocusChangedMethod(focus); } +#endif + public Vector2 screenSize => new Vector2(Screen.width, Screen.height); public ScreenOrientation screenOrientation => Screen.orientation; diff --git a/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef b/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef index d551736cd1..84e2b9faeb 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef +++ b/Packages/com.unity.inputsystem/InputSystem/Unity.InputSystem.asmdef @@ -92,11 +92,16 @@ "expression": "6000.3.0a6", "define": "UNITY_INPUT_SYSTEM_PLATFORM_POLLING_FREQUENCY" }, - { - "name": "Unity", + { + "name": "Unity", "expression": "6000.4.0a4", "define": "UNITY_INPUTSYSTEM_SUPPORTS_MOUSE_SCRIPT_EVENTS" - } + }, + { + "name": "Unity", + "expression": "6000.5.0a8", + "define": "UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS" + } ], "noEngineReferences": false -} +} \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Utilities/DelegateHelpers.cs b/Packages/com.unity.inputsystem/InputSystem/Utilities/DelegateHelpers.cs index 2dccc52d07..447a081afa 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Utilities/DelegateHelpers.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Utilities/DelegateHelpers.cs @@ -12,25 +12,26 @@ public static void InvokeCallbacksSafe(ref CallbackArray callbacks, Prof { if (callbacks.length == 0) return; - marker.Begin(); - callbacks.LockForChanges(); - for (var i = 0; i < callbacks.length; ++i) + using (marker.Auto()) { - try - { - callbacks[i](); - } - catch (Exception exception) + callbacks.LockForChanges(); + for (var i = 0; i < callbacks.length; ++i) { - Debug.LogException(exception); - if (context != null) - Debug.LogError($"{exception.GetType().Name} while executing '{callbackName}' callbacks of '{context}'"); - else - Debug.LogError($"{exception.GetType().Name} while executing '{callbackName}' callbacks"); + try + { + callbacks[i](); + } + catch (Exception exception) + { + Debug.LogException(exception); + if (context != null) + Debug.LogError($"{exception.GetType().Name} while executing '{callbackName}' callbacks of '{context}'"); + else + Debug.LogError($"{exception.GetType().Name} while executing '{callbackName}' callbacks"); + } } + callbacks.UnlockForChanges(); } - callbacks.UnlockForChanges(); - marker.End(); } public static void InvokeCallbacksSafe(ref CallbackArray> callbacks, TValue argument, string callbackName, object context = null) @@ -62,25 +63,26 @@ public static void InvokeCallbacksSafe(ref CallbackArray(ref CallbackArray> callbacks, diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs index d551102500..1fa4430418 100644 --- a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs @@ -12,6 +12,7 @@ using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; using UnityEngine.InputSystem.XR; +using UnityEngineInternal.Input; #if UNITY_6000_5_OR_NEWER using UnityEngine.Assemblies; #endif @@ -840,6 +841,23 @@ public void Trigger(InputAction action) throw new NotImplementedException(); } + /// + /// Utility function for manually scheduling an InputFocusEvent. + /// This is useful for testing how the system reacts to focus changes. + /// + /// The focus state to be scheduled. + public unsafe void ScheduleFocusChangedEvent(bool applicationHasFocus) + { +#if UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS + // For now we only set application focus. In the future we want to add support for other focus as well + FocusFlags state = applicationHasFocus ? FocusFlags.ApplicationFocus : FocusFlags.None; + var evt = InputFocusEvent.Create(state); + InputSystem.QueueEvent(new InputEventPtr((InputEvent*)&evt.baseEvent)); +#else + runtime.InvokePlayerFocusChanged(applicationHasFocus); +#endif + } + /// /// The input runtime used during testing. /// @@ -1035,7 +1053,7 @@ private static bool IsEditMode() return Application.isEditor && !Application.isPlaying; } - #if UNITY_EDITOR +#if UNITY_EDITOR /// /// Represents an analytics registration event captured by test harness. /// @@ -1130,6 +1148,6 @@ protected void CollectAnalytics() CollectAnalytics((_) => true); } - #endif +#endif } } diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestRuntime.cs b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestRuntime.cs index 0042954c86..0fe87ed59b 100644 --- a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestRuntime.cs +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestRuntime.cs @@ -8,6 +8,7 @@ using UnityEngine.Analytics; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.Utilities; +using UnityEngineInternal.Input; #if UNITY_EDITOR using UnityEditor; @@ -235,20 +236,12 @@ public unsafe long DeviceCommand(int deviceId, InputDeviceCommand* commandPtr) public void InvokePlayerFocusChanged(bool newFocusState) { - m_HasFocus = newFocusState; + m_FocusState = newFocusState + ? m_FocusState | FocusFlags.ApplicationFocus + : m_FocusState & ~FocusFlags.ApplicationFocus; onPlayerFocusChanged?.Invoke(newFocusState); } - public void PlayerFocusLost() - { - InvokePlayerFocusChanged(false); - } - - public void PlayerFocusGained() - { - InvokePlayerFocusChanged(true); - } - public int ReportNewInputDevice(string deviceDescriptor, int deviceId = InputDevice.InvalidDeviceId) { lock (m_Lock) @@ -358,7 +351,8 @@ public struct PairedUser public Action onDeviceDiscovered { get; set; } public Action onShutdown { get; set; } public Action onPlayerFocusChanged { get; set; } - public bool isPlayerFocused => m_HasFocus; + public FocusFlags focusState { get { return m_FocusState; } set { m_FocusState = value; } } + public bool isPlayerFocused => (m_FocusState & FocusFlags.ApplicationFocus) != 0; public float pollingFrequency { get; set; } = 60.0f; // At least 60 Hz by default public double currentTime { get; set; } public double currentTimeForFixedUpdate { get; set; } @@ -432,7 +426,7 @@ public void SetUnityRemoteGyroUpdateInterval(float interval) internal const int kDefaultEventBufferSize = 1024 * 512; - private bool m_HasFocus = true; + private FocusFlags m_FocusState = FocusFlags.ApplicationFocus; private int m_NextDeviceId = 1; private int m_NextEventId = 1; internal int m_EventCount; diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/Unity.InputSystem.TestFramework.asmdef b/Packages/com.unity.inputsystem/Tests/TestFixture/Unity.InputSystem.TestFramework.asmdef index 3da733991f..27a76aacac 100644 --- a/Packages/com.unity.inputsystem/Tests/TestFixture/Unity.InputSystem.TestFramework.asmdef +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/Unity.InputSystem.TestFramework.asmdef @@ -1,5 +1,6 @@ { "name": "Unity.InputSystem.TestFramework", + "rootNamespace": "", "references": [ "Unity.InputSystem", "UnityEngine.TestRunner", @@ -13,13 +14,20 @@ "nunit.framework.dll" ], "autoReferenced": false, - "defineConstraints": ["UNITY_TESTS_FRAMEWORK"], + "defineConstraints": [ + "UNITY_TESTS_FRAMEWORK" + ], "versionDefines": [ { "name": "com.unity.test-framework", "expression": "", "define": "UNITY_TESTS_FRAMEWORK" + }, + { + "name": "Unity", + "expression": "6000.5.0a8", + "define": "UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS" } - ], - "versionDefines": [] + ], + "noEngineReferences": false } \ No newline at end of file