From 583fc19e1c9b85fb20f740e8b9017888f996e657 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Thu, 21 Jul 2022 02:47:11 -0500 Subject: [PATCH] Implement UnityGame.SwitchToMainThreadAsync --- IPA.Loader/Utilities/AlmostVersion.cs | 2 +- IPA.Loader/Utilities/Async/Coroutines.cs | 8 +- .../Async/UnityMainThreadTaskScheduler.cs | 38 ++++++--- IPA.Loader/Utilities/UnityGame.cs | 82 +++++++++++++++++-- 4 files changed, 106 insertions(+), 24 deletions(-) diff --git a/IPA.Loader/Utilities/AlmostVersion.cs b/IPA.Loader/Utilities/AlmostVersion.cs index 6b9c8111..a9527503 100644 --- a/IPA.Loader/Utilities/AlmostVersion.cs +++ b/IPA.Loader/Utilities/AlmostVersion.cs @@ -190,7 +190,7 @@ namespace IPA.Utilities /// the object to compare to /// if they are equal, otherwise /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is AlmostVersion version && SemverValue == version.SemverValue && diff --git a/IPA.Loader/Utilities/Async/Coroutines.cs b/IPA.Loader/Utilities/Async/Coroutines.cs index b474e566..2062da28 100644 --- a/IPA.Loader/Utilities/Async/Coroutines.cs +++ b/IPA.Loader/Utilities/Async/Coroutines.cs @@ -54,10 +54,10 @@ namespace IPA.Utilities.Async public static Task AsTask(IEnumerator coroutine) { if (!UnityGame.OnMainThread) - return UnityMainThreadTaskScheduler.Factory.StartNew(() => AsTask(coroutine)).Unwrap(); + return UnityMainThreadTaskScheduler.Factory.StartNew(() => AsTask(coroutine), default, default, UnityMainThreadTaskScheduler.Default).Unwrap(); var tcs = new TaskCompletionSource(coroutine, AsTaskSourceOptions); - PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs)); + _ = PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs)); return tcs.Task; } @@ -85,7 +85,7 @@ namespace IPA.Utilities.Async enumerators.Push(coroutine); } - private readonly Stack enumerators = new Stack(2); + private readonly Stack enumerators = new(2); public object Current => enumerators.FirstOrDefault()?.Current; // effectively a TryPeek @@ -116,7 +116,7 @@ namespace IPA.Utilities.Async } else { // this enumerator completed, so pop it and continue - enumerators.Pop(); + _ = enumerators.Pop(); continue; } } diff --git a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs index e3fb3f54..fa4aea3a 100644 --- a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs +++ b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs @@ -1,12 +1,11 @@ -using System; +#nullable enable +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace IPA.Utilities.Async @@ -20,15 +19,15 @@ namespace IPA.Utilities.Async /// Gets the default main thread scheduler that is managed by BSIPA. /// /// a scheduler that is managed by BSIPA - public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler(); + public static new UnityMainThreadTaskScheduler Default { get; } = new UnityMainThreadTaskScheduler(); /// /// Gets a factory for creating tasks on . /// /// a factory for creating tasks on the default scheduler public static TaskFactory Factory { get; } = new TaskFactory(Default); - private readonly ConcurrentQueue tasks = new ConcurrentQueue(); - private static readonly ConditionalWeakTable itemTable = new ConditionalWeakTable(); + private readonly ConcurrentQueue tasks = new(); + private static readonly ConditionalWeakTable itemTable = new(); private class QueueItem : IEquatable, IEquatable { @@ -43,7 +42,9 @@ namespace IPA.Utilities.Async } } - public Task Task { get; private set; } = null; + public Task? Task { get; private set; } + + public Action? Action { get; private set; } public QueueItem(Task task) { @@ -51,7 +52,13 @@ namespace IPA.Utilities.Async Task = task; } - public bool Equals(Task other) => HasTask && other.Equals(Task); + public QueueItem(Action action) + { + HasTask = true; + Action = action; + } + + public bool Equals(Task? other) => other is not null && HasTask && other.Equals(Task); public bool Equals(QueueItem other) => other.HasTask == HasTask && Equals(other.Task); } @@ -146,7 +153,11 @@ namespace IPA.Utilities.Async do if (!tasks.TryDequeue(out task)) goto exit; // try dequeue, if we can't exit while (!task.HasTask); // if the dequeued task is empty, try again - TryExecuteTask(task.Task); + if (task.Task is not null) + { + _ = TryExecuteTask(task.Task); + } + task.Action?.Invoke(); } exit: sw.Reset(); @@ -181,7 +192,7 @@ namespace IPA.Utilities.Async /// nothing /// Always. protected override IEnumerable GetScheduledTasks() - => tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).ToArray(); + => tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).NonNull().ToArray(); /// /// Queues a given to this scheduler. The must be @@ -198,6 +209,13 @@ namespace IPA.Utilities.Async tasks.Enqueue(item); } + internal void QueueAction(Action action) + { + ThrowIfDisposed(); + + tasks.Enqueue(new(action)); + } + /// /// Runs the task inline if the current thread is the Unity main thread. /// diff --git a/IPA.Loader/Utilities/UnityGame.cs b/IPA.Loader/Utilities/UnityGame.cs index 9765af75..e87c414d 100644 --- a/IPA.Loader/Utilities/UnityGame.cs +++ b/IPA.Loader/Utilities/UnityGame.cs @@ -1,6 +1,9 @@ -using IPA.Config; +#nullable enable +using IPA.Config; +using IPA.Utilities.Async; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; @@ -17,12 +20,12 @@ namespace IPA.Utilities /// public static class UnityGame { - private static AlmostVersion _gameVersion; + private static AlmostVersion? _gameVersion; /// /// Provides the current game version. /// /// the SemVer version of the game - public static AlmostVersion GameVersion => _gameVersion ?? (_gameVersion = new AlmostVersion(ApplicationVersionProxy)); + public static AlmostVersion GameVersion => _gameVersion ??= new AlmostVersion(ApplicationVersionProxy); internal static void SetEarlyGameVersion(AlmostVersion ver) { @@ -76,24 +79,30 @@ namespace IPA.Utilities } internal static bool IsGameVersionBoundary { get; private set; } - internal static AlmostVersion OldVersion { get; private set; } + internal static AlmostVersion? OldVersion { get; private set; } internal static void CheckGameVersionBoundary() { var gameVer = GameVersion; var lastVerS = SelfConfig.LastGameVersion_; OldVersion = lastVerS != null ? new AlmostVersion(lastVerS, gameVer) : null; - IsGameVersionBoundary = OldVersion != null && gameVer != OldVersion; + IsGameVersionBoundary = OldVersion is not null && gameVer != OldVersion; SelfConfig.Instance.LastGameVersion = gameVer.ToString(); } - private static Thread mainThread; + private static Thread? mainThread; /// /// Checks if the currently running code is running on the Unity main thread. /// /// if the curent thread is the Unity main thread, otherwise - public static bool OnMainThread => Thread.CurrentThread.ManagedThreadId == mainThread?.ManagedThreadId; + public static bool OnMainThread => Environment.CurrentManagedThreadId == mainThread?.ManagedThreadId; + + /// + /// Asynchronously switches the current execution context to the Unity main thread. + /// + /// An awaitable which causes any following code to execute on the main thread. + public static SwitchToUnityMainThreadAwaitable SwitchToMainThreadAsync() => default; internal static void SetMainThread() => mainThread = Thread.CurrentThread; @@ -120,9 +129,9 @@ namespace IPA.Utilities /// This only gives a /// /// the type of release this is - public static Release ReleaseType => (_releaseCache ?? (_releaseCache = CheckIsSteam() ? Release.Steam : Release.Other)).Value; + public static Release ReleaseType => _releaseCache ??= CheckIsSteam() ? Release.Steam : Release.Other; - private static string _installRoot; + private static string? _installRoot; /// /// Gets the path to the game's install directory. /// @@ -165,4 +174,59 @@ namespace IPA.Utilities && installDirInfo.Parent?.Parent?.Name == "steamapps"; } } + + /// + /// An awaitable which, when awaited, switches the current context to the Unity main thread. + /// + /// + [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "This type should never be compared.")] + public struct SwitchToUnityMainThreadAwaitable + { + /// + /// Gets the awaiter for this awaitable. + /// + /// The awaiter for this awaitable. + public SwitchToUnityMainThreadAwaiter GetAwaiter() => default; + } + + /// + /// An awaiter which, when awaited, switches the current context to the Unity main thread. + /// + /// + [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "This type should never be compared.")] + public struct SwitchToUnityMainThreadAwaiter : INotifyCompletion, ICriticalNotifyCompletion + { + private static readonly ContextCallback InvokeAction = static o => ((Action)o!)(); + + /// + /// Gets whether or not this awaiter is completed. + /// + public bool IsCompleted => UnityGame.OnMainThread; + + /// + /// Gets the result of this awaiter. + /// + public void GetResult() { } + + /// + /// Registers a continuation to be called when this awaiter finishes. + /// + /// The continuation. + public void OnCompleted(Action continuation) + { + var ec = ExecutionContext.Capture(); + UnityMainThreadTaskScheduler.Default.QueueAction(() => ExecutionContext.Run(ec, InvokeAction, continuation)); + } + + /// + /// Registers a continuation to be called when this awaiter finishes, without capturing the execution context. + /// + /// The continuation. + public void UnsafeOnCompleted(Action continuation) + { + UnityMainThreadTaskScheduler.Default.QueueAction(continuation); + } + } }