// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Threading; using osu.Framework.Development; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Logging; using osu.Framework.Threading; namespace osu.Framework.Platform { /// /// Runs a game host in a specific threading mode. /// public class ThreadRunner { private readonly InputThread mainThread; private readonly List threads = new List(); public IReadOnlyCollection Threads { get { lock (threads) return threads.ToArray(); } } private double maximumUpdateHz = GameThread.DEFAULT_ACTIVE_HZ; public double MaximumUpdateHz { set { maximumUpdateHz = value; updateMainThreadRates(); } } private double maximumInactiveHz = GameThread.DEFAULT_INACTIVE_HZ; public double MaximumInactiveHz { set { maximumInactiveHz = value; updateMainThreadRates(); } } private readonly object startStopLock = new object(); /// /// Construct a new ThreadRunner instance. /// /// The main window thread. Used for input in multi-threaded execution; all game logic in single-threaded execution. /// public ThreadRunner(InputThread mainThread) { this.mainThread = mainThread; AddThread(mainThread); } /// /// Add a new non-main thread. In single-threaded execution, threads will be executed in the order they are added. /// public void AddThread(GameThread thread) { lock (threads) { if (!threads.Contains(thread)) threads.Add(thread); } } /// /// Remove a non-main thread. /// public void RemoveThread(GameThread thread) { lock (threads) threads.Remove(thread); } private ExecutionMode? activeExecutionMode; public ExecutionMode ExecutionMode { private get; set; } = ExecutionMode.MultiThreaded; public virtual void RunMainLoop() { // propagate any requested change in execution mode at a safe point in frame execution ensureCorrectExecutionMode(); Debug.Assert(activeExecutionMode != null); switch (activeExecutionMode.Value) { case ExecutionMode.SingleThread: { lock (threads) { foreach (var t in threads) t.RunSingleFrame(); } break; } case ExecutionMode.MultiThreaded: // still need to run the main/input thread on the window loop mainThread.RunSingleFrame(); break; } } public void Start() => ensureCorrectExecutionMode(); public void Suspend() { lock (startStopLock) { pauseAllThreads(); activeExecutionMode = null; } } public void Stop() { const int thread_join_timeout = 30000; Threads.ForEach(t => t.Exit()); Threads.Where(t => t.Running).ForEach(t => { var thread = t.Thread; if (thread == null) { // has already been cleaned up (or never started) return; } if (!thread.Join(thread_join_timeout)) Logger.Log($"Thread {t.Name} failed to exit in allocated time ({thread_join_timeout}ms).", LoggingTarget.Runtime, LogLevel.Important); }); // as the input thread isn't actually handled by a thread, the above join does not necessarily mean it has been completed to an exiting state. mainThread.WaitForState(GameThreadState.Exited); ThreadSafety.ResetAllForCurrentThread(); } private void ensureCorrectExecutionMode() { // locking is required as this method may be called from two different threads. lock (startStopLock) { if (ExecutionMode == activeExecutionMode) return; activeExecutionMode = ThreadSafety.ExecutionMode = ExecutionMode; Logger.Log($"Execution mode changed to {activeExecutionMode}"); } pauseAllThreads(); switch (ExecutionMode) { case ExecutionMode.MultiThreaded: { // switch to multi-threaded foreach (var t in Threads) t.Start(); break; } case ExecutionMode.SingleThread: { // switch to single-threaded. foreach (var t in Threads) { // only throttle for the main thread t.Initialize(withThrottling: t == mainThread); } // this is usually done in the execution loop, but required here for the initial game startup, // which would otherwise leave values in an incorrect state. ThreadSafety.ResetAllForCurrentThread(); break; } } updateMainThreadRates(); } private void pauseAllThreads() { // shut down threads in reverse to ensure audio stops last (other threads may be waiting on a queued event otherwise) foreach (var t in Threads.Reverse()) t.Pause(); } private void updateMainThreadRates() { if (activeExecutionMode == ExecutionMode.SingleThread) { mainThread.ActiveHz = maximumUpdateHz; mainThread.InactiveHz = maximumInactiveHz; } else { mainThread.ActiveHz = GameThread.DEFAULT_ACTIVE_HZ; mainThread.InactiveHz = GameThread.DEFAULT_INACTIVE_HZ; } } /// /// Sets the current culture of all threads to the supplied . /// public void SetCulture(CultureInfo culture) { // for single-threaded mode, switch the current (assumed to be main) thread's culture, since it's actually the one that's running the frames. Thread.CurrentThread.CurrentCulture = culture; // for multi-threaded mode, schedule the culture change on all threads. // note that if the threads haven't been created yet (e.g. if the game started single-threaded), this will only store the culture in GameThread.CurrentCulture. // in that case, the stored value will be set on the actual threads after the next Start() call. foreach (var t in Threads) { t.Scheduler.Add(() => t.CurrentCulture = culture); } } } }