// Copyright HTC Corporation All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using VIVE.OpenXR.Feature; using static VIVE.OpenXR.Feature.FutureWrapper; namespace VIVE.OpenXR.Toolkits { /// /// FutureTask is not a C# Task. It is a wrapper for OpenXR Future. /// Each OpenXR Future may have its own FutureTask type because the result and the /// complete function are different. /// However the poll and complete are common. This class use c# Task to poll the future. /// You can if you do not want to wait for the result. /// However Cancel should be called before the Complete. /// When is true, call to complete the future and get the result. /// /// You can customize the type depending on the complete's result. public class FutureTask : IDisposable { private Task<(XrResult, XrFutureStateEXT)> pollTask; private Task autoCompleteTask; Func completeFunc; private CancellationTokenSource cts; private IntPtr future; bool autoComplete = false; private int pollIntervalMS = 10; /// /// Set poll inverval in milliseconds. The value will be clamped between 1 and 2000. /// public int PollIntervalMS { get => pollIntervalMS; set => pollIntervalMS = Math.Clamp(value, 1, 2000); } public bool IsAutoComplete => autoComplete; bool isCompleted = false; public bool IsCompleted => isCompleted; public bool Debug { get; set; } = false; /// /// The FutureTask is used to poll and complete the future. /// Once the FutureTask is create, poll will start running in the period of pollIntervalMS. /// if auto complete is set, the future will be do completed once user check IsPollCompleted and IsCompleted. /// I prefered to use non-autoComplete. /// If no auto complete, you can cancel the task and no need to free resouce. /// Once it completed, you need handle the result to avoid leakage. /// /// /// /// Set poll inverval in milliseconds. The value will be clamped between 1 and 2000. /// If true, do Complete when check IsPollCompleted and IsCompleted public FutureTask(IntPtr future, Func completeFunc, int pollIntervalMS = 10, bool autoComplete = false) { cts = new CancellationTokenSource(); this.completeFunc = completeFunc; this.future = future; this.pollIntervalMS = Math.Clamp(pollIntervalMS, 1, 2000); // User may get PollTask and run. So, we need to make sure the pollTask is created. pollTask = MakePollTask(this, cts.Token); // will set autoComplete true in AutoComplete. this.autoComplete = false; if (autoComplete) AutoComplete(); } /// /// AutoComplete will complete the future once the poll task is ready and success. /// If you want to handle error, you should not use AutoComplete. /// public void AutoComplete() { if (autoComplete) return; autoComplete = true; autoCompleteTask = pollTask.ContinueWith(task => { // If the task is cancelled or faulted, we do not need to complete the future. if (task.IsCanceled || task.IsFaulted) { isCompleted = true; return; } var result = task.Result; // Make sure call Complete only if poll task is ready and success. if (result.Item1 == XrResult.XR_SUCCESS) { if (result.Item2 == XrFutureStateEXT.Ready) { Complete(); } } isCompleted = true; }); } /// /// Used for create FromResult if you need return the result immediately. /// /// /// FutureTask(Task<(XrResult, XrFutureStateEXT)> pollTask, Func completeFunc) { this.pollTask = pollTask; this.completeFunc = completeFunc; this.future = IntPtr.Zero; } public Task<(XrResult, XrFutureStateEXT)> PollTask => pollTask; /// /// If AutoComplete is set, the task will be created. Otherwise, it will be null. /// public Task AutoCompleteTask => autoCompleteTask; public bool IsPollCompleted => pollTask.IsCompleted; public XrResult PollResult => pollTask.Result.Item1; public IntPtr Future => future; /// /// Cancel the future. If the future is not completed yet, it will be cancelled. Otherwise, nothing will happen. /// public void Cancel() { if (!isCompleted) { cts?.Cancel(); FutureWrapper.Instance?.CancelFuture(future); } future = IntPtr.Zero; } /// /// Make sure do Complete after IsPollCompleted. If the future is not poll completed yet, throw exception. /// /// The result of the completeFunc. /// Thrown when the pollTask is not completed yet. public TResult Complete() { if (isCompleted) return result; if (pollTask.IsCompleted) { if (this.Debug) UnityEngine.Debug.Log("FutureTask is completed."); isCompleted = true; if (pollTask.Result.Item1 == XrResult.XR_SUCCESS) { result = completeFunc(future); isCompleted = true; return result; } if (this.Debug) UnityEngine.Debug.Log("FutureTask is completed with error. Check if pollTask result error."); return default; } else { throw new Exception("FutureTask is not completed yet."); } } /// /// Wait until poll task is completed. If the task is not completed, it will block the thread. /// If AutoComplete is set, wait until the complete task is completed. /// public void Wait() { pollTask.Wait(); if (autoComplete) autoCompleteTask.Wait(); } TResult result; private bool disposedValue; /// /// This Result did not block the thread. If not completed, it will return undefined value. Make sure you call it when Complete is done. /// public TResult Result => result; public static FutureTask FromResult(TResult result) { return new FutureTask(Task.FromResult((XrResult.XR_SUCCESS, XrFutureStateEXT.Ready)), (future) => result); } /// /// Poll until the future is ready. Caceled if the cts is cancelled. But the future will not be cancelled. /// /// /// /// /// static async Task<(XrResult, XrFutureStateEXT)> MakePollTask(FutureTask futureTask, CancellationToken ct) { XrFuturePollInfoEXT pollInfo = new XrFuturePollInfoEXT() { type = XrStructureType.XR_TYPE_FUTURE_POLL_INFO_EXT, next = IntPtr.Zero, future = futureTask.Future }; do { ct.ThrowIfCancellationRequested(); XrResult ret = FutureWrapper.Instance.PollFuture(ref pollInfo, out FutureWrapper.XrFuturePollResultEXT pollResult); if (ret == XrResult.XR_SUCCESS) { if (pollResult.state == XrFutureStateEXT.Ready) { if (futureTask.Debug) UnityEngine.Debug.Log("Future is ready."); return (XrResult.XR_SUCCESS, pollResult.state); } else if (pollResult.state == XrFutureStateEXT.Pending) { if (futureTask.Debug) UnityEngine.Debug.Log("Wait for future."); await Task.Delay(futureTask.pollIntervalMS); continue; } } else { return (ret, XrFutureStateEXT.None); } } while (true); } protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { pollTask?.Dispose(); pollTask = null; autoCompleteTask?.Dispose(); autoCompleteTask = null; cts?.Dispose(); cts = null; } if (future != IntPtr.Zero && !isCompleted) FutureWrapper.Instance?.CancelFuture(future); future = IntPtr.Zero; disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } } /// /// Help to manage the future task. Tasks are name less. In order to manager tasks, /// additonal information are required. And this class is used to store those information. /// Helps to retrive the task by the identity, or retrive the identity by the task. /// /// What the task work for. How to identify this task. /// The task's output type, for example, XrResult or Tuple. public class FutureTaskManager : IDisposable { readonly List<(Identity, FutureTask)> tasks = new List<(Identity, FutureTask)>(); private bool disposedValue; public FutureTaskManager() { } public FutureTask GetTask(Identity identity) { return tasks.FirstOrDefault(x => x.Item1.Equals(identity)).Item2; } /// /// Add a task to the manager. /// /// /// public void AddTask(Identity identity, FutureTask task) { tasks.Add((identity, task)); } /// /// Remove keeped task and cancel it If task is not completed. /// /// public void RemoveTask(Identity identity) { var task = tasks.FirstOrDefault(x => x.Item1.Equals(identity)); if (task.Item2 != null) { task.Item2.Cancel(); task.Item2.Dispose(); } tasks.Remove(task); } /// /// Remove keeped task and cancel it If task is not completed. /// /// public void RemoveTask(FutureTask task) { var t = tasks.FirstOrDefault(x => x.Item2 == task); if (t.Item2 != null) { t.Item2.Cancel(); t.Item2.Dispose(); } tasks.Remove(t); } /// /// Get all tasks's list. /// /// public List<(Identity, FutureTask)> GetTasks() { return tasks; } /// /// Check if has any task. /// /// public bool IsEmpty() { return tasks.Count == 0; } /// /// Clear all tasks and cancel them. If tasks are auto completed, make sure their results handled. /// Otherwise, the resource will be leaked. /// /// public void Clear(bool cancelTask = true) { if (cancelTask) { foreach (var task in tasks) { if (task.Item2 != null) { task.Item2.Cancel(); task.Item2.Dispose(); } } } tasks.Clear(); } protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { Clear(); } disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } } }