diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 4ef3b09d..6967e81f 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -331,8 +331,6 @@ namespace IPA.Injector pluginAsyncLoadTask.Wait(); permissionFixTask.Wait(); - UnityGame.EnsureRuntimeGameVersion(); - log.Debug("Plugins loaded"); log.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP())); PluginComponent.Create(); diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index 0b37ea80..d67fc8e4 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -140,6 +140,7 @@ + diff --git a/IPA.Loader/Loader/PluginComponent.cs b/IPA.Loader/Loader/PluginComponent.cs index 76eccfd2..3bfaa349 100644 --- a/IPA.Loader/Loader/PluginComponent.cs +++ b/IPA.Loader/Loader/PluginComponent.cs @@ -1,6 +1,8 @@ using IPA.Config; using IPA.Loader.Composite; -using System.Diagnostics.CodeAnalysis; +using IPA.Utilities; +using IPA.Utilities.Async; +using System.Diagnostics.CodeAnalysis; using UnityEngine; using UnityEngine.SceneManagement; // ReSharper disable UnusedMember.Local @@ -26,6 +28,9 @@ namespace IPA.Loader if (!initialized) { + UnityGame.SetMainThread(); + UnityGame.EnsureRuntimeGameVersion(); + PluginManager.Load(); bsPlugins = new CompositeBSPlugin(PluginManager.BSMetas); @@ -42,7 +47,11 @@ namespace IPA.Loader SceneManager.activeSceneChanged += OnActiveSceneChanged; SceneManager.sceneLoaded += OnSceneLoaded; - SceneManager.sceneUnloaded += OnSceneUnloaded; + SceneManager.sceneUnloaded += OnSceneUnloaded; + + var unitySched = UnityMainThreadTaskScheduler.Default as UnityMainThreadTaskScheduler; + if (!unitySched.IsRunning) + StartCoroutine(unitySched.Coroutine()); initialized = true; } @@ -52,6 +61,10 @@ namespace IPA.Loader { bsPlugins.OnUpdate(); ipaPlugins.OnUpdate(); + + var unitySched = UnityMainThreadTaskScheduler.Default as UnityMainThreadTaskScheduler; + if (!unitySched.IsRunning) + StartCoroutine(unitySched.Coroutine()); } void LateUpdate() diff --git a/IPA.Loader/Loader/StateTransitionTransaction.cs b/IPA.Loader/Loader/StateTransitionTransaction.cs index 5452cfcd..948bcd4e 100644 --- a/IPA.Loader/Loader/StateTransitionTransaction.cs +++ b/IPA.Loader/Loader/StateTransitionTransaction.cs @@ -203,11 +203,10 @@ namespace IPA.Loader /// /// /// After this completes, this transaction will be disposed. - /// It is illegal to call this anywhere but the main Unity thread. /// /// The that is returned will error if any of the mods being disabled /// error. It is up to the caller to handle these in a sane way, like logging them. If nothing else, do something like this: - /// + /// /// // get your transaction... /// var complete = transaction.Commit(); /// complete.ContinueWith(t => { diff --git a/IPA.Loader/Utilities/Async/SingleThreadTaskScheduler.cs b/IPA.Loader/Utilities/Async/SingleThreadTaskScheduler.cs index 85cced15..f092ac8a 100644 --- a/IPA.Loader/Utilities/Async/SingleThreadTaskScheduler.cs +++ b/IPA.Loader/Utilities/Async/SingleThreadTaskScheduler.cs @@ -20,7 +20,7 @@ namespace IPA.Utilities.Async /// /// Gets whether or not the underlying thread has been started. /// - /// Thrown if this object has already been disposed. + /// Thrown if this object has already been disposed. public bool IsRunning { get @@ -33,7 +33,7 @@ namespace IPA.Utilities.Async /// /// Starts the thread that executes tasks scheduled with this /// - /// Thrown if this object has already been disposed. + /// Thrown if this object has already been disposed. public void Start() { ThrowIfDisposed(); @@ -48,7 +48,7 @@ namespace IPA.Utilities.Async /// After this method returns, this object has been disposed and is no longer in a valid state. /// /// an of s that did not execute - /// Thrown if this object has already been disposed. + /// Thrown if this object has already been disposed. public IEnumerable Exit() { ThrowIfDisposed(); @@ -69,7 +69,7 @@ namespace IPA.Utilities.Async /// /// After this method returns, this object has been disposed and is no longer in a valid state. /// - /// Thrown if this object has already been disposed. + /// Thrown if this object has already been disposed. public void Join() { ThrowIfDisposed(); @@ -94,7 +94,7 @@ namespace IPA.Utilities.Async /// scheduled for this by the runtime. /// /// the to queue - /// Thrown if this object has already been disposed. + /// Thrown if this object has already been disposed. protected override void QueueTask(Task task) { ThrowIfDisposed(); @@ -112,7 +112,7 @@ namespace IPA.Utilities.Async /// the task to attempt to execute /// whether the task was previously queued to this scheduler /// - /// Thrown if this object has already been disposed. + /// Thrown if this object has already been disposed. protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { ThrowIfDisposed(); @@ -123,7 +123,7 @@ namespace IPA.Utilities.Async private void ThrowIfDisposed() { if (disposedValue) - throw new InvalidOperationException("Object already disposed"); + throw new ObjectDisposedException(nameof(SingleThreadTaskScheduler)); } private void ExecuteTasks() diff --git a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs new file mode 100644 index 00000000..f0b97184 --- /dev/null +++ b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace IPA.Utilities.Async +{ + public class UnityMainThreadTaskScheduler : TaskScheduler, IDisposable + { + public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler(); + + private readonly ConcurrentDictionary tasks = new ConcurrentDictionary(); + private int queueEndPosition = 0; + private int queuePosition = 0; + + private struct QueueItem : IEquatable, IEquatable, IEquatable + { + public int Index; + public Task Task; + + public QueueItem(int index, Task task) : this() + { + Index = index; + Task = task; + } + + public bool Equals(int other) => Index.Equals(other); + public bool Equals(Task other) => Task.Equals(other); + public bool Equals(QueueItem other) => other.Index == Index || other.Task == Task; + } + + public bool IsRunning { get; private set; } = false; + + public int YieldAfterTasks { get; set; } = 4; + + public IEnumerator Coroutine() + { + ThrowIfDisposed(); + + IsRunning = true; + yield return null; // yield immediately + + try + { + while (true) + { + if (queuePosition < queueEndPosition) + { + var yieldAfter = YieldAfterTasks; + for (int i = 0; i < yieldAfter && queuePosition < queueEndPosition; i++) + { + if (tasks.TryRemove(new QueueItem { Index = Interlocked.Increment(ref queuePosition) }, out var task)) + TryExecuteTask(task); // we succesfully removed the task + else + i++; // we didn't + } + } + yield return null; + } + } + finally + { + IsRunning = false; + } + } + + /// + /// Throws a . + /// + /// nothing + /// Always. + protected override IEnumerable GetScheduledTasks() + { + // this is only for debuggers which we can't use sooooo + throw new NotSupportedException(); + } + + protected override void QueueTask(Task task) + { + ThrowIfDisposed(); + + tasks.TryAdd(new QueueItem(Interlocked.Increment(ref queueEndPosition), task), task); + } + + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + ThrowIfDisposed(); + + if (!UnityGame.OnMainThread) return false; + + if (taskWasPreviouslyQueued) + if (!tasks.TryRemove(new QueueItem { Task = task }, out var _)) + return false; // if we couldn't remove it, its not in our queue, so it already ran + + return TryExecuteTask(task); + } + + private void ThrowIfDisposed() + { + if (disposedValue) + throw new ObjectDisposedException(nameof(SingleThreadTaskScheduler)); + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + } +} diff --git a/IPA.Loader/Utilities/UnityGame.cs b/IPA.Loader/Utilities/UnityGame.cs index cdd26f31..4b1409d3 100644 --- a/IPA.Loader/Utilities/UnityGame.cs +++ b/IPA.Loader/Utilities/UnityGame.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Reflection; +using System.Threading; using UnityEngine; #if NET3 using Path = Net3_Proxy.Path; @@ -59,6 +60,16 @@ namespace IPA.Utilities SelfConfig.Instance.LastGameVersion = gameVer.ToString(); } + 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; + + internal static void SetMainThread() + => mainThread = Thread.CurrentThread; + /// /// The different types of releases of the game. ///