Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions Assets/Tests/InputSystem/CoreTests_Actions_Interactions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,114 @@ public void Actions_HoldInteraction_CanBePerformedWhenInvolvingMoreThanOneContro
Assert.That(canceledCount, Is.EqualTo(1));
}

[Test]
[Category("Actions")]
[TestCase(InputSettings.UpdateMode.ProcessEventsManually, InputUpdateType.Manual)]
[TestCase(InputSettings.UpdateMode.ProcessEventsInDynamicUpdate, InputUpdateType.Dynamic)]
[TestCase(InputSettings.UpdateMode.ProcessEventsInFixedUpdate, InputUpdateType.Fixed)]
public void Actions_HoldInteraction_DurationTimeoutDoesNotPerformDuringBeforeRender(InputSettings.UpdateMode updateMode, InputUpdateType updateType)
{
// This test uses the Hold interaction as a representative sample of an action timeout
// to test that it does not occur during BeforeRender even if the duration threshold
// is met beginning with that period.

var updateModeToRestore = InputSystem.settings.updateMode;
InputSystem.settings.updateMode = updateMode;

// We need one device that has before-render updates enabled for the update to enable
// at all.
const string kBeforeRenderDevice = @"
{
""name"" : ""BeforeRenderGamepad"",
""extend"" : ""Gamepad"",
""beforeRender"" : ""Update""
}
";
InputSystem.RegisterLayout(kBeforeRenderDevice);
InputSystem.AddDevice("BeforeRenderGamepad");

Assert.That(InputSystem.manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo(InputUpdateType.BeforeRender));

var keyboard = InputSystem.AddDevice<Keyboard>();

// Add both bindings just to ensure that there is the potential for the Input Action
// to be driven by an input device that enables the before-render update.

var action = new InputAction(binding: "<Keyboard>/space", interactions: "hold(duration=0.4)");
action.AddBinding("<BeforeRenderGamepad>/buttonSouth");
action.Enable();

using (var trace = new InputActionTrace(action))
{
// Since the InputManager uses timeslicing when doing fixed updated, we need to adjust the
// current times to ensure the event buffer with the press event is actually processed.
runtime.currentTimeForFixedUpdate = 10.1 + runtime.currentTimeOffsetToRealtimeSinceStartup;
currentTime = 10.1;

// Press and hold.
// queueEventOnly: true because otherwise it would invoke InputSystem.Update() instead of the update type we are testing.
Press(keyboard.spaceKey, time: 10.0, queueEventOnly: true);
InputSystem.Update(updateType);

Assert.That(trace, Started<HoldInteraction>(action, keyboard.spaceKey, time: 10.0, value: 1.0));
Assert.That(action.ReadValue<float>(), Is.EqualTo(1));
Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started));
Assert.That(action.WasPressedThisFrame(), Is.True);
Assert.That(action.WasPerformedThisFrame(), Is.False);
Assert.That(action.WasReleasedThisFrame(), Is.False);
Assert.That(action.WasCompletedThisFrame(), Is.False);

trace.Clear();

// Exceed hold time during BeforeRender phase. Make sure action does not perform yet.
currentTime = 10.5;
InputSystem.Update(InputUpdateType.BeforeRender);

Assert.That(trace, Is.Empty);
Assert.That(action.ReadValue<float>(), Is.EqualTo(1));
Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started));
Assert.That(action.WasPressedThisFrame(), Is.True);
Assert.That(action.WasPerformedThisFrame(), Is.False);
Assert.That(action.WasReleasedThisFrame(), Is.False);
Assert.That(action.WasCompletedThisFrame(), Is.False);

trace.Clear();

// Make sure action performs and *stays* performed after the next update.
runtime.currentTimeForFixedUpdate = 10.6;
currentTime = 10.6;
InputSystem.Update(updateType);

Assert.That(trace,
Performed<HoldInteraction>(action, keyboard.spaceKey, time: 10.6, duration: 0.6, value: 1.0));
Assert.That(action.ReadValue<float>(), Is.EqualTo(1));
Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed));
Assert.That(action.WasPressedThisFrame(), Is.False);
Assert.That(action.WasPerformedThisFrame(), Is.True);
Assert.That(action.WasReleasedThisFrame(), Is.False);
Assert.That(action.WasCompletedThisFrame(), Is.False);

trace.Clear();

runtime.currentTimeForFixedUpdate = 10.8 + runtime.currentTimeOffsetToRealtimeSinceStartup;
currentTime = 10.8;

// Release.
Release(keyboard.spaceKey, time: 10.7, queueEventOnly: true);
InputSystem.Update(updateType);

Assert.That(trace, Canceled<HoldInteraction>(action, keyboard.spaceKey, duration: 0.7, time: 10.7, value: 0.0));
Assert.That(action.ReadValue<float>(), Is.Zero);
Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting));
Assert.That(action.WasPressedThisFrame(), Is.False);
Assert.That(action.WasPerformedThisFrame(), Is.False);
Assert.That(action.WasReleasedThisFrame(), Is.True);
Assert.That(action.WasCompletedThisFrame(), Is.True);
}

InputSystem.settings.updateMode = updateModeToRestore;
}

[Test]
[Category("Actions")]
public void Actions_ReleasedHoldInteractionIsCancelled_WithMultipleBindings()
Expand Down
1 change: 1 addition & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixed the Action Maps list in the Input Actions editor losing keyboard focus after deleting a map, so that Delete, Duplicate, and arrow-key navigation kept working on the auto-selected replacement map without requiring an extra click [UUM-147152](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-147152)
- Fixed the Device Simulator plugin keeping the real mouse and pen disabled while working in other Editor windows. Conflicting native `Mouse`/`Pen` devices are now only disabled while the Simulator window is focused and re-enabled as soon as focus moves elsewhere [UUM-145509](https://jira.unity3d.com/browse/UUM-145509).
- Fixed the Input Actions editor in Project Settings losing the selected action or binding when opening the control picker from the Path field [UUM-151771](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-151771).
- Fixed polling for interaction phases (such as `WasPerformedThisFrame`) being missed during `Update` due to interaction timeouts sometimes occurring during the later before-render period when a before-render device (such as an XR HMD) is added. This means that timeouts and interactions like Hold may now wait to perform until the next frame instead of the earliest chance during before-render. [UUM-147719](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-147719)

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ private bool ShouldExitEarlyBasedOnBackgroundBehavior(InputUpdateType updateType
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe bool LegacyEarlyOutFromEventProcessing(InputUpdateType updateType, ref InputEventBuffer eventBuffer, ref bool dropStatusEvents)
{
var shouldProcessActionTimeouts = updateType.IsPlayerUpdate() && gameIsPlaying;
var shouldProcessTimeouts = ShouldProcessTimeouts(updateType);
// 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();
Expand All @@ -232,7 +232,7 @@ private unsafe bool LegacyEarlyOutFromEventProcessing(InputUpdateType updateType
{
// Normally, we process action timeouts after first processing all events. If we have no
// events, we still need to check timeouts.
if (shouldProcessActionTimeouts)
if (shouldProcessTimeouts)
m_StateMonitors.ProcessTimeouts();

if (shouldFlushEventBuffer)
Expand Down
29 changes: 26 additions & 3 deletions Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3296,7 +3296,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev
// Ensure optimized controls are in valid state
CheckAllDevicesOptimizedControlsHaveValidState();

var shouldProcessActionTimeouts = updateType.IsPlayerUpdate() && gameIsPlaying;
var shouldProcessTimeouts = ShouldProcessTimeouts(updateType);

// 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
Expand All @@ -3316,7 +3316,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev
{
// Normally, we process action timeouts after first processing all events. If we have no
// events, we still need to check timeouts.
if (shouldProcessActionTimeouts)
if (shouldProcessTimeouts)
m_StateMonitors.ProcessTimeouts();

InvokeAfterUpdateCallback(updateType);
Expand All @@ -3334,7 +3334,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev

ProcessEventBuffer(updateType, ref eventBuffer, currentTime, timesliceEvents, dropStatusEvents);

if (shouldProcessActionTimeouts)
if (shouldProcessTimeouts)
m_StateMonitors.ProcessTimeouts();

FinalizeUpdate(updateType);
Expand Down Expand Up @@ -4147,6 +4147,29 @@ private bool ShouldDiscardEditModeTransitionEvent(FourCC eventType, double event

#endif

/// <summary>
/// Determines whether state monitor and action timeouts should be processed for the given update type.
/// </summary>
/// <param name="updateType">The current update type</param>
/// <returns>True if timeouts should be processed, false otherwise.</returns>
/// <remarks>
/// Timeouts are skipped during <see cref="InputUpdateType.BeforeRender"/> to minimize work during the
/// render preparation phase and ensure consistent timeout timing regardless of whether before-render
/// devices are present.
/// </remarks>
/// <seealso cref="InputManagerStateMonitors.ProcessTimeouts"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
bool ShouldProcessTimeouts(InputUpdateType updateType)
{
// Skip when InputUpdateType.BeforeRender to avoid altering the typical update period when timeouts are processed
// just because a device that has before-render updates enabled is added to the system.
// Otherwise, users who poll in Update() would potentially miss action phase changes if the timeout
// occurs during the later BeforeRender period since methods like InputAction.WasPerformedThisFrame would be
// reset by the next Update().
// (ISX-2898) (ISX-2897): This exception may not be necessary once those issues are addressed.
return updateType.IsPlayerUpdate() && updateType != InputUpdateType.BeforeRender && gameIsPlaying;
}

bool AreMaximumEventBytesPerUpdateExceeded(uint totalEventBytesProcessed)
{
if (m_Settings.maxEventBytesPerUpdate > 0 &&
Expand Down
Loading