/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; internal static class OVRTask { internal static OVRTask FromGuid(Guid id) => Create(id); internal static OVRTask FromRequest(ulong id) => Create(GetId(id)); internal static OVRTask FromResult(TResult result) { var task = Create(Guid.NewGuid()); task.SetResult(result); return task; } internal static OVRTask GetExisting(Guid id) => Get(id); internal static OVRTask GetExisting(ulong id) => Get(GetId(id)); internal static void SetResult(Guid id, TResult result) => GetExisting(id).SetResult(result); internal static void SetResult(ulong id, TResult result) => GetExisting(id).SetResult(result); private static OVRTask Get(Guid id) { return new OVRTask(id); } private static OVRTask Create(Guid id) { var task = Get(id); task.AddToPending(); return task; } internal static unsafe Guid GetId(ulong value) { const ulong hashModifier1 = 0x319642b2d24d8ec3; const ulong hashModifier2 = 0x96de1b173f119089; var guid = default(Guid); *(ulong*)&guid = unchecked(value + hashModifier1); *((ulong*)&guid + 1) = hashModifier2; return guid; } } /// /// Represents an awaitable task. /// /// /// This is a task-like object which supports the await pattern. Typically, you do not need to /// create or use this object directly. Instead, you can either : /// - await a method which returns an object of type , /// which will eventually return a /// - poll the property and then call /// - pass a delegate by calling . Note that an additional state object can get passed in and added as a parameter of the callback, see /// Requires the main thread to complete the await contract - blocking can result in an infinite loop. /// /// The type of result being awaited. public readonly struct OVRTask : IEquatable>, IDisposable { #region static private static readonly HashSet Pending = new HashSet(); private static readonly Dictionary Results = new Dictionary(); private static readonly Dictionary Continuations = new Dictionary(); private delegate void CallbackInvoker(Guid guid, TResult result); private delegate bool CallbackRemover(Guid guid); private static readonly Dictionary CallbackInvokers = new Dictionary(); private static readonly Dictionary CallbackRemovers = new Dictionary(); private static readonly HashSet CallbackClearers = new HashSet(); private delegate bool InternalDataRemover(Guid guid); private static readonly Dictionary InternalDataRemovers = new Dictionary(); private static readonly HashSet InternalDataClearers = new HashSet(); private static readonly Dictionary> SubscriberRemovers = new Dictionary>(); private static readonly HashSet SubscriberClearers = new HashSet(); #endregion private readonly Guid _id; internal OVRTask(Guid id) { _id = id; } internal void AddToPending() => Pending.Add(_id); internal bool IsPending => Pending.Contains(_id); internal void SetInternalData(T data) => InternalData.Set(_id, data); internal bool TryGetInternalData(out T data) => InternalData.TryGet(_id, out data); internal void SetResult(TResult result) { // Means no one was awaiting this result. if (!Pending.Remove(_id)) return; if (InternalDataRemovers.TryGetValue(_id, out var internalDataRemover)) { InternalDataRemovers.Remove(_id); internalDataRemover(_id); } if (SubscriberRemovers.TryGetValue(_id, out var subscriberRemover)) { SubscriberRemovers.Remove(_id); subscriberRemover(_id); } if (CallbackInvokers.TryGetValue(_id, out var invoker)) { CallbackInvokers.Remove(_id); invoker(_id, result); } else { // Add to the results so that GetResult can retrieve it later. Results.Add(_id, result); if (Continuations.TryGetValue(_id, out var continuation)) { Continuations.Remove(_id); continuation(); } } } private static class InternalData { private static readonly Dictionary Data = new Dictionary(); public static bool TryGet(Guid taskId, out T data) { return Data.TryGetValue(taskId, out data); } public static void Set(Guid taskId, T data) { Data[taskId] = data; InternalDataRemovers.Add(taskId, Remover); InternalDataClearers.Add(Clearer); } private static readonly InternalDataRemover Remover = Remove; private static readonly Action Clearer = Clear; private static bool Remove(Guid taskId) => Data.Remove(taskId); private static void Clear() => Data.Clear(); } static class IncrementalResultSubscriber { static readonly Dictionary> Subscribers = new Dictionary>(); public static void Set(Guid taskId, Action subscriber) { Subscribers[taskId] = subscriber; SubscriberRemovers[taskId] = Remover; SubscriberClearers.Add(Clearer); } public static void Notify(Guid taskId, T result) { if (Subscribers.TryGetValue(taskId, out var subscriber)) { subscriber(result); } } static readonly Action Remover = Remove; static void Remove(Guid id) => Subscribers.Remove(id); static readonly Action Clearer = Clear; static void Clear() => Subscribers.Clear(); } /// /// Sets the delegate to be invoked when an incremental result is available before the task is complete. /// /// /// Some tasks may provide incremental results before the task is complete. In this case, you can use /// to receive those results as they become available. /// /// For example, the task may provide a list of results over some period of time and may be able to provide /// partial results as they become available, before the task completes. /// /// Invoked whenever /// is called. /// The type of the incremental result. This is typically different than the /// . /// Thrown when is `null`. internal void SetIncrementalResultCallback( Action onIncrementalResultAvailable) { if (onIncrementalResultAvailable == null) throw new ArgumentNullException(nameof(onIncrementalResultAvailable)); IncrementalResultSubscriber.Set(_id, onIncrementalResultAvailable); } /// /// Notifies a subscriber of an incremental result associated with an ongoing task. /// /// /// Use this to provide partial results that may be available before the task fully completes. /// /// The type of the result, usually different from . internal void NotifyIncrementalResult(TIncrementalResult incrementalResult) => IncrementalResultSubscriber.Notify(_id, incrementalResult); #region Polling Implementation /// /// Indicates whether the task has completed. /// /// /// Choose only one pattern out of the three proposed way of awaiting for the task completion: /// Polling,async/await or /// as all three patterns will end up calling the which can only be called once. /// /// True if the task has completed. can be called. public bool IsCompleted => !IsPending; /// /// Gets the result of the Task. /// /// /// This method should only be called once is true. /// Calling it multiple times leads to undefined behavior. /// Do not use in conjunction with any other methods (await or using ). /// /// Returns the result of type . /// Thrown when the task doesn't have any available result. This could /// happen if the method is called before is true, after the task has been disposed of /// or if this method has already been called once. public TResult GetResult() { if (!Results.TryGetValue(_id, out var value)) { throw new InvalidOperationException($"Task {_id} doesn't have any available result."); } Results.Remove(_id); return value; } #endregion #region Awaiter Contract Implementation /// /// Definition of an awaiter that satisfies the await contract. /// /// /// This allows an to be awaited using the await keyword. /// Typically, you should not use this struct; instead, it is used by the compiler by /// automatically calling the method when using the await keyword. /// public readonly struct Awaiter : INotifyCompletion { private readonly OVRTask _task; internal Awaiter(OVRTask task) { _task = task; } public bool IsCompleted => _task.IsCompleted; public void OnCompleted(Action continuation) => _task.WithContinuation(continuation); public TResult GetResult() => _task.GetResult(); } /// /// Gets an awaiter that satisfies the await contract. /// /// /// This allows an to be awaited using the await keyword. /// Typically, you should not call this directly; instead, it is invoked by the compiler, e.g., /// /// /// var task = GetResultAsync(); /// /// // compiler uses GetAwaiter here /// var result = await task; /// ]]> /// Or, more commonly: /// /// /// Requires the main thread to complete the await contract - blocking can result in an infinite loop. /// /// Returns an Awaiter-like object that satisfies the await pattern. public Awaiter GetAwaiter() => new Awaiter(this); private void WithContinuation(Action continuation) { ValidateDelegateAndThrow(continuation, nameof(continuation)); Continuations[_id] = continuation; } #endregion #region Delegate Implementation readonly struct Callback { private static readonly Dictionary Callbacks = new Dictionary(); readonly Action _delegate; static void Invoke(Guid taskId, TResult result) { if (Callbacks.TryGetValue(taskId, out var callback)) { Callbacks.Remove(taskId); callback.Invoke(result); } } static bool Remove(Guid taskId) => Callbacks.Remove(taskId); static void Clear() => Callbacks.Clear(); void Invoke(TResult result) => _delegate(result); Callback(Action @delegate) => _delegate = @delegate; public static readonly CallbackInvoker Invoker = Invoke; public static readonly CallbackRemover Remover = Remove; public static readonly Action Clearer = Clear; public static void Add(Guid taskId, Action @delegate) { Callbacks.Add(taskId, new Callback(@delegate)); CallbackInvokers.Add(taskId, Invoker); CallbackRemovers.Add(taskId, Remover); CallbackClearers.Add(Clearer); } } readonly struct CallbackWithState { private static readonly Dictionary> Callbacks = new Dictionary>(); readonly T _data; readonly Action _delegate; static void Invoke(Guid taskId, TResult result) { if (Callbacks.TryGetValue(taskId, out var callback)) { Callbacks.Remove(taskId); callback.Invoke(result); } } CallbackWithState(T data, Action @delegate) { _data = data; _delegate = @delegate; } private static readonly CallbackInvoker Invoker = Invoke; private static readonly CallbackRemover Remover = Remove; private static readonly Action Clearer = Clear; private static void Clear() => Callbacks.Clear(); private static bool Remove(Guid taskId) => Callbacks.Remove(taskId); private void Invoke(TResult result) => _delegate(result, _data); public static void Add(Guid taskId, T data, Action callback) { Callbacks.Add(taskId, new CallbackWithState(data, callback)); CallbackInvokers.Add(taskId, Invoker); CallbackRemovers.Add(taskId, Remover); CallbackClearers.Add(Clearer); } } /// /// Registers a delegate that will get called on completion of the task. /// /// /// The delegate will be invoked with the result as parameter. /// Do not use in conjunction with any other methods (await or calling ). /// /// A delegate to be invoked when this task completes. If the task is already complete, /// is invoked immediately. /// /// Thrown if is null. /// Thrown if there already is a delegate or a continuation registered to this task. public void ContinueWith(Action onCompleted) { ValidateDelegateAndThrow(onCompleted, nameof(onCompleted)); if (IsCompleted) { onCompleted.Invoke(GetResult()); } else { Callback.Add(_id, onCompleted); } } /// /// Registers a delegate that will get called on completion of the task. /// /// /// The delegate will be invoked with and the result as /// parameters. /// Do not use in conjunction with any other methods (await or calling ). /// /// A delegate to be invoked when this task completes. If the task is already complete, /// is invoked immediately. /// An object to store and pass to . /// /// Thrown if is null. /// Thrown if there already is a delegate or a continuation registered to this task. public void ContinueWith(Action onCompleted, T state) { ValidateDelegateAndThrow(onCompleted, nameof(onCompleted)); if (IsCompleted) { onCompleted.Invoke(GetResult(), state); } else { CallbackWithState.Add(_id, state, onCompleted); } } void ValidateDelegateAndThrow(object @delegate, string paramName) { if (@delegate == null) throw new ArgumentNullException(paramName); if (Continuations.ContainsKey(_id)) throw new InvalidOperationException($"Task {_id} is already being used by an await call."); if (CallbackInvokers.ContainsKey(_id)) throw new InvalidOperationException($"Task {_id} is already being used with ContinueWith."); } #endregion #region IDisposable Implementation /// /// Disposes of the task. /// /// /// Invalidate this object but does not cancel the task. /// In the case where the result will not actually be consumed, it must be called to prevent a memory leak. /// You can not call nor use await on a disposed task. /// public void Dispose() { Results.Remove(_id); Continuations.Remove(_id); Pending.Remove(_id); CallbackInvokers.Remove(_id); if (CallbackRemovers.TryGetValue(_id, out var remover)) { CallbackRemovers.Remove(_id); remover(_id); } if (InternalDataRemovers.TryGetValue(_id, out var internalDataRemover)) { InternalDataRemovers.Remove(_id); internalDataRemover(_id); } if (SubscriberRemovers.TryGetValue(_id, out var subscriberRemover)) { SubscriberRemovers.Remove(_id); subscriberRemover(_id); } } #endregion #region IEquatable Implementation public bool Equals(OVRTask other) => _id == other._id; public override bool Equals(object obj) => obj is OVRTask other && Equals(other); public static bool operator ==(OVRTask lhs, OVRTask rhs) => lhs.Equals(rhs); public static bool operator !=(OVRTask lhs, OVRTask rhs) => !lhs.Equals(rhs); public override int GetHashCode() => _id.GetHashCode(); public override string ToString() => _id.ToString(); #endregion }