Browse Source

Implement UnityGame.SwitchToMainThreadAsync

pull/86/head
Anairkoen Schno 1 year 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>
/// <returns><see langword="true"/> if they are equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="object.Equals(object)"/>
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
return obj is AlmostVersion version &&
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)
{
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);
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<IEnumerator> enumerators = new Stack<IEnumerator>(2);
private readonly Stack<IEnumerator> 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;
}
}


+ 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.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.
/// </summary>
/// <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>
/// Gets a factory for creating tasks on <see cref="Default"/>.
/// </summary>
/// <value>a factory for creating tasks on the default scheduler</value>
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>
{
@ -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
/// <returns>nothing</returns>
/// <exception cref="NotSupportedException">Always.</exception>
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>
/// 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);
}
internal void QueueAction(Action action)
{
ThrowIfDisposed();
tasks.Enqueue(new(action));
}
/// <summary>
/// Runs the task inline if the current thread is the Unity main thread.
/// </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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
@ -17,12 +20,12 @@ namespace IPA.Utilities
/// </summary>
public static class UnityGame
{
private static AlmostVersion _gameVersion;
private static AlmostVersion? _gameVersion;
/// <summary>
/// Provides the current game version.
/// </summary>
/// <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)
{
@ -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;
/// <summary>
/// Checks if the currently running code is running on the Unity main thread.
/// </summary>
/// <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()
=> mainThread = Thread.CurrentThread;
@ -120,9 +129,9 @@ namespace IPA.Utilities
/// This only gives a
/// </remarks>
/// <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>
/// Gets the path to the game's install directory.
/// </summary>
@ -165,4 +174,59 @@ namespace IPA.Utilities
&& 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