Browse Source

Implement UnityGame.SwitchToMainThreadAsync

pull/86/head
Anairkoen Schno 2 years ago
parent
commit
583fc19e1c
No known key found for this signature in database GPG Key ID: 40F6F33603F1772D
4 changed files with 106 additions and 24 deletions
  1. +1
    -1
      IPA.Loader/Utilities/AlmostVersion.cs
  2. +4
    -4
      IPA.Loader/Utilities/Async/Coroutines.cs
  3. +28
    -10
      IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs
  4. +73
    -9
      IPA.Loader/Utilities/UnityGame.cs

+ 1
- 1
IPA.Loader/Utilities/AlmostVersion.cs View File

@ -190,7 +190,7 @@ namespace IPA.Utilities
/// <param name="obj">the object to compare to</param> /// <param name="obj">the object to compare to</param>
/// <returns><see langword="true"/> if they are equal, <see langword="false"/> otherwise</returns> /// <returns><see langword="true"/> if they are equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="object.Equals(object)"/> /// <seealso cref="object.Equals(object)"/>
public override bool Equals(object obj)
public override bool Equals(object? obj)
{ {
return obj is AlmostVersion version && return obj is AlmostVersion version &&
SemverValue == version.SemverValue && SemverValue == version.SemverValue &&


+ 4
- 4
IPA.Loader/Utilities/Async/Coroutines.cs View File

@ -54,10 +54,10 @@ namespace IPA.Utilities.Async
public static Task AsTask(IEnumerator coroutine) public static Task AsTask(IEnumerator coroutine)
{ {
if (!UnityGame.OnMainThread) 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<VoidStruct>(coroutine, AsTaskSourceOptions); var tcs = new TaskCompletionSource<VoidStruct>(coroutine, AsTaskSourceOptions);
PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs));
_ = PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs));
return tcs.Task; return tcs.Task;
} }
@ -85,7 +85,7 @@ namespace IPA.Utilities.Async
enumerators.Push(coroutine); enumerators.Push(coroutine);
} }
private readonly Stack<IEnumerator> enumerators = new Stack<IEnumerator>(2);
private readonly Stack<IEnumerator> enumerators = new(2);
public object Current => enumerators.FirstOrDefault()?.Current; // effectively a TryPeek public object Current => enumerators.FirstOrDefault()?.Current; // effectively a TryPeek
@ -116,7 +116,7 @@ namespace IPA.Utilities.Async
} }
else else
{ // this enumerator completed, so pop it and continue { // this enumerator completed, so pop it and continue
enumerators.Pop();
_ = enumerators.Pop();
continue; continue;
} }
} }


+ 28
- 10
IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs View File

@ -1,12 +1,11 @@
using System;
#nullable enable
using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace IPA.Utilities.Async namespace IPA.Utilities.Async
@ -20,15 +19,15 @@ namespace IPA.Utilities.Async
/// Gets the default main thread scheduler that is managed by BSIPA. /// Gets the default main thread scheduler that is managed by BSIPA.
/// </summary> /// </summary>
/// <value>a scheduler that is managed by BSIPA</value> /// <value>a scheduler that is managed by BSIPA</value>
public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
public static new UnityMainThreadTaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
/// <summary> /// <summary>
/// Gets a factory for creating tasks on <see cref="Default"/>. /// Gets a factory for creating tasks on <see cref="Default"/>.
/// </summary> /// </summary>
/// <value>a factory for creating tasks on the default scheduler</value> /// <value>a factory for creating tasks on the default scheduler</value>
public static TaskFactory Factory { get; } = new TaskFactory(Default); public static TaskFactory Factory { get; } = new TaskFactory(Default);
private readonly ConcurrentQueue<QueueItem> tasks = new ConcurrentQueue<QueueItem>();
private static readonly ConditionalWeakTable<Task, QueueItem> itemTable = new ConditionalWeakTable<Task, QueueItem>();
private readonly ConcurrentQueue<QueueItem> tasks = new();
private static readonly ConditionalWeakTable<Task, QueueItem> itemTable = new();
private class QueueItem : IEquatable<Task>, IEquatable<QueueItem> private class QueueItem : IEquatable<Task>, IEquatable<QueueItem>
{ {
@ -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) public QueueItem(Task task)
{ {
@ -51,7 +52,13 @@ namespace IPA.Utilities.Async
Task = task; 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); 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 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 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: exit:
sw.Reset(); sw.Reset();
@ -181,7 +192,7 @@ namespace IPA.Utilities.Async
/// <returns>nothing</returns> /// <returns>nothing</returns>
/// <exception cref="NotSupportedException">Always.</exception> /// <exception cref="NotSupportedException">Always.</exception>
protected override IEnumerable<Task> GetScheduledTasks() protected override IEnumerable<Task> GetScheduledTasks()
=> tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).ToArray();
=> tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).NonNull().ToArray();
/// <summary> /// <summary>
/// Queues a given <see cref="Task"/> to this scheduler. The <see cref="Task"/> <i>must</i> be /// Queues a given <see cref="Task"/> to this scheduler. The <see cref="Task"/> <i>must</i> be
@ -198,6 +209,13 @@ namespace IPA.Utilities.Async
tasks.Enqueue(item); tasks.Enqueue(item);
} }
internal void QueueAction(Action action)
{
ThrowIfDisposed();
tasks.Enqueue(new(action));
}
/// <summary> /// <summary>
/// Runs the task inline if the current thread is the Unity main thread. /// Runs the task inline if the current thread is the Unity main thread.
/// </summary> /// </summary>


+ 73
- 9
IPA.Loader/Utilities/UnityGame.cs View File

@ -1,6 +1,9 @@
using IPA.Config;
#nullable enable
using IPA.Config;
using IPA.Utilities.Async;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -17,12 +20,12 @@ namespace IPA.Utilities
/// </summary> /// </summary>
public static class UnityGame public static class UnityGame
{ {
private static AlmostVersion _gameVersion;
private static AlmostVersion? _gameVersion;
/// <summary> /// <summary>
/// Provides the current game version. /// Provides the current game version.
/// </summary> /// </summary>
/// <value>the SemVer version of the game</value> /// <value>the SemVer version of the game</value>
public static AlmostVersion GameVersion => _gameVersion ?? (_gameVersion = new AlmostVersion(ApplicationVersionProxy));
public static AlmostVersion GameVersion => _gameVersion ??= new AlmostVersion(ApplicationVersionProxy);
internal static void SetEarlyGameVersion(AlmostVersion ver) internal static void SetEarlyGameVersion(AlmostVersion ver)
{ {
@ -76,24 +79,30 @@ namespace IPA.Utilities
} }
internal static bool IsGameVersionBoundary { get; private set; } 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() internal static void CheckGameVersionBoundary()
{ {
var gameVer = GameVersion; var gameVer = GameVersion;
var lastVerS = SelfConfig.LastGameVersion_; var lastVerS = SelfConfig.LastGameVersion_;
OldVersion = lastVerS != null ? new AlmostVersion(lastVerS, gameVer) : null; 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(); SelfConfig.Instance.LastGameVersion = gameVer.ToString();
} }
private static Thread mainThread;
private static Thread? mainThread;
/// <summary> /// <summary>
/// Checks if the currently running code is running on the Unity main thread. /// Checks if the currently running code is running on the Unity main thread.
/// </summary> /// </summary>
/// <value><see langword="true"/> if the curent thread is the Unity main thread, <see langword="false"/> otherwise</value> /// <value><see langword="true"/> if the curent thread is the Unity main thread, <see langword="false"/> otherwise</value>
public static bool OnMainThread => Thread.CurrentThread.ManagedThreadId == mainThread?.ManagedThreadId;
public static bool OnMainThread => Environment.CurrentManagedThreadId == mainThread?.ManagedThreadId;
/// <summary>
/// Asynchronously switches the current execution context to the Unity main thread.
/// </summary>
/// <returns>An awaitable which causes any following code to execute on the main thread.</returns>
public static SwitchToUnityMainThreadAwaitable SwitchToMainThreadAsync() => default;
internal static void SetMainThread() internal static void SetMainThread()
=> mainThread = Thread.CurrentThread; => mainThread = Thread.CurrentThread;
@ -120,9 +129,9 @@ namespace IPA.Utilities
/// This only gives a /// This only gives a
/// </remarks> /// </remarks>
/// <value>the type of release this is</value> /// <value>the type of release this is</value>
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;
/// <summary> /// <summary>
/// Gets the path to the game's install directory. /// Gets the path to the game's install directory.
/// </summary> /// </summary>
@ -165,4 +174,59 @@ namespace IPA.Utilities
&& installDirInfo.Parent?.Parent?.Name == "steamapps"; && installDirInfo.Parent?.Parent?.Name == "steamapps";
} }
} }
/// <summary>
/// An awaitable which, when awaited, switches the current context to the Unity main thread.
/// </summary>
/// <seealso cref="UnityGame.SwitchToMainThreadAsync"/>
[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types",
Justification = "This type should never be compared.")]
public struct SwitchToUnityMainThreadAwaitable
{
/// <summary>
/// Gets the awaiter for this awaitable.
/// </summary>
/// <returns>The awaiter for this awaitable.</returns>
public SwitchToUnityMainThreadAwaiter GetAwaiter() => default;
}
/// <summary>
/// An awaiter which, when awaited, switches the current context to the Unity main thread.
/// </summary>
/// <seealso cref="UnityGame.SwitchToMainThreadAsync"/>
[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!)();
/// <summary>
/// Gets whether or not this awaiter is completed.
/// </summary>
public bool IsCompleted => UnityGame.OnMainThread;
/// <summary>
/// Gets the result of this awaiter.
/// </summary>
public void GetResult() { }
/// <summary>
/// Registers a continuation to be called when this awaiter finishes.
/// </summary>
/// <param name="continuation">The continuation.</param>
public void OnCompleted(Action continuation)
{
var ec = ExecutionContext.Capture();
UnityMainThreadTaskScheduler.Default.QueueAction(() => ExecutionContext.Run(ec, InvokeAction, continuation));
}
/// <summary>
/// Registers a continuation to be called when this awaiter finishes, without capturing the execution context.
/// </summary>
/// <param name="continuation">The continuation.</param>
public void UnsafeOnCompleted(Action continuation)
{
UnityMainThreadTaskScheduler.Default.QueueAction(continuation);
}
}
} }

Loading…
Cancel
Save