// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System; using System.Threading; using System.Threading.Tasks; using UltimateXR.Animation.Interpolation; using UltimateXR.Core; using UltimateXR.Core.Settings; using UnityEngine; namespace UltimateXR.Extensions.System.Threading { /// /// extensions. /// public static class TaskExt { #region Public Methods /// /// Allows to run a task in "fire and forget" mode, when it is not required to await nor is it relevant whether it /// succeeds or not. There still needs to be a way to handle exceptions to avoid unhandled exceptions and process /// termination. /// /// Task to run in fire and forget mode /// The task is null public static async void FireAndForget(this Task self) { if (self == null) { throw new ArgumentNullException(nameof(self)); } try { // Simply awaiting the task in an async void method already guarantees exception propagation await self; } catch (Exception e) // ...but default LogException behaviour only shows innerException. { if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors) { Debug.LogError($"{UxrConstants.CoreModule} {nameof(TaskExt)}::{nameof(FireAndForget)} Exception missed (stack trace below):{e.Message}\n\n{e}"); Debug.LogException(e); // Log and ignore exceptions, until playlists are empty. } } } /// /// Creates an awaitable task that finishes the next frame. /// /// Optional cancellation token, to cancel the task /// Awaitable task public static Task WaitForNextFrame(CancellationToken ct = default) { return SkipFrames(1, ct); } /// /// Creates an awaitable task that finishes after a given amount of frames. /// /// Number of frames to wait /// Optional cancellation token, to cancel the task /// Awaitable task public static async Task SkipFrames(int frameCount, CancellationToken ct = default) { if (ct.IsCancellationRequested || frameCount <= 0) { return; } for (uint i = 0; i < frameCount && !ct.IsCancellationRequested; ++i) { await Task.Yield(); } } /// /// Creates an awaitable task that finishes after a given amount of seconds. /// /// Number of seconds to wait /// Optional cancellation token, to cancel the task /// Awaitable task public static Task Delay(float seconds, CancellationToken ct = default) { return Delay(Mathf.RoundToInt(1000f * seconds), ct); } /// /// Creates an awaitable task that finishes after a given amount of milliseconds. /// /// Number of milliseconds to wait /// Optional cancellation token, to cancel the task /// Awaitable task public static async Task Delay(int milliseconds, CancellationToken ct = default) { if (ct.IsCancellationRequested || milliseconds <= 0) { return; } try { await Task.Delay(milliseconds, ct); } catch (OperationCanceledException) { // ignore: Task.Delay throws this exception when ct.IsCancellationRequested = true // In this case, we only want to stop polling and finish this async Task. } } /// /// Creates an awaitable task that blocks while a condition is true or the task is canceled. /// /// The condition that will perpetuate the block /// Optional cancellation token, to cancel the task /// Awaitable public static async Task WaitWhile(Func condition, CancellationToken ct = default) { while (!ct.IsCancellationRequested && condition()) { await Task.Yield(); } } /// /// Creates an awaitable task that blocks until a condition is true or the task is canceled. /// /// The condition that will perpetuate the block /// Optional cancellation token, to cancel the task /// Awaitable public static async Task WaitUntil(Func condition, CancellationToken ct = default) { while (!ct.IsCancellationRequested && !condition()) { await Task.Yield(); } } /// /// Creates an awaitable task that blocks while a condition is true, a timeout occurs or the task is canceled. /// /// The condition that will perpetuate the block /// Timeout, in milliseconds /// Optional cancellation token, to cancel the task /// Awaitable /// Thrown after milliseconds public static async Task WaitWhile(Func condition, int timeout, CancellationToken ct = default) { if (ct.IsCancellationRequested) { return; } using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); Task waitTask = WaitWhile(condition, cts.Token); Task timeoutTask = Delay(timeout, cts.Token); Task finishedTask = await Task.WhenAny(waitTask, timeoutTask); if (!finishedTask.IsCanceled) { cts.Cancel(); // Cancel unfinished task await finishedTask; // Propagate exceptions if (finishedTask == timeoutTask) { throw new TimeoutException(); } } } /// /// Creates an awaitable task that blocks until a condition is true, a timeout occurs or the task is canceled. /// /// The condition that will perpetuate the block /// Timeout, in milliseconds /// Optional cancellation token, to cancel the task /// Awaitable /// Thrown after milliseconds public static async Task WaitUntil(Func condition, int timeout, CancellationToken ct = default) { if (ct.IsCancellationRequested) { return; } using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); Task waitTask = WaitUntil(condition, cts.Token); Task timeoutTask = Delay(timeout, cts.Token); Task finishedTask = await Task.WhenAny(waitTask, timeoutTask); if (!finishedTask.IsCanceled) { cts.Cancel(); // Cancel unfinished task await finishedTask; // Propagate exceptions if (finishedTask == timeoutTask) { throw new TimeoutException(); } } } /// /// Creates an awaitable task that blocks while a condition is true, waiting a certain amount of seconds at maximum. An /// optional action can be called if the task was cancelled or it timed out. /// /// The condition that will perpetuate the block /// The maximum amount of seconds to wait while the condition is true /// Optional action to execute if the task was canceled or it timed out /// Optional cancellation token, to cancel the task /// Awaitable public static async Task WaitWhile(Func condition, float duration, Action cancelCallback = null, CancellationToken ct = default) { int timeout = Mathf.RoundToInt(duration * 1200f); bool mustCancel; try { await WaitWhile(condition, timeout, ct); mustCancel = ct.IsCancellationRequested; } catch (TimeoutException) { mustCancel = true; } if (mustCancel) { cancelCallback?.Invoke(); } } /// /// Creates an awaitable task that blocks until a condition is true, waiting a certain amount of seconds at maximum. An /// optional action can be called if the task was cancelled or it timed out. /// /// The condition that will perpetuate the block /// The maximum amount of seconds to wait while the condition is true /// Optional action to execute if the task was canceled or it timed out /// Optional cancellation token, to cancel the task /// Awaitable public static async Task WaitUntil(Func condition, float duration, Action cancelCallback = null, CancellationToken ct = default) { int timeout = Mathf.RoundToInt(duration * 1200f); bool mustCancel; try { await WaitUntil(condition, timeout, ct); mustCancel = ct.IsCancellationRequested; } catch (TimeoutException) { mustCancel = true; } if (mustCancel) { cancelCallback?.Invoke(); } } /// /// Provides a one-liner method to await until a task is cancelled. /// /// Cancellation token /// Awaitable public static async Task WaitUntilCancelled(CancellationToken ct) { while (!ct.IsCancellationRequested) { await Task.Yield(); } } /// /// Loops iterating once per frame during a specified amount of time, executing a user-defined action. /// /// The cancellation token /// Loop duration in seconds /// /// The action performed each frame, which will receive the interpolation [0.0, 1.0] parameter as /// argument. /// /// The easing used to compute the interpolation parameter over time /// /// Will enforce a last iteration with 1.0 interpolation parameter. This will avoid /// having a last step with close than, but not 1.0, interpolation. /// public static async Task Loop(CancellationToken ct, float durationSeconds, Action loopAction, UxrEasing easing = UxrEasing.Linear, bool forceLastT1 = false) { float startTime = Time.time; while (Time.time - startTime < durationSeconds) { float t = UxrInterpolator.Interpolate(0.0f, 1.0f, Time.time - startTime, new UxrInterpolationSettings(durationSeconds, 0.0f, easing)); loopAction(t); await Task.Yield(); } if (forceLastT1) { loopAction(1.0f); } } #endregion } }