A game framework written with osu! in mind.
at master 243 lines 8.1 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 5// See the LICENCE file in the repository root for full licence text. 6 7using System; 8using System.Collections.Generic; 9using System.Diagnostics; 10using System.Globalization; 11using System.Linq; 12using System.Threading; 13using osu.Framework.Development; 14using osu.Framework.Extensions.IEnumerableExtensions; 15using osu.Framework.Logging; 16using osu.Framework.Threading; 17 18namespace osu.Framework.Platform 19{ 20 /// <summary> 21 /// Runs a game host in a specific threading mode. 22 /// </summary> 23 public class ThreadRunner 24 { 25 private readonly InputThread mainThread; 26 27 private readonly List<GameThread> threads = new List<GameThread>(); 28 29 public IReadOnlyCollection<GameThread> Threads 30 { 31 get 32 { 33 lock (threads) 34 return threads.ToArray(); 35 } 36 } 37 38 private double maximumUpdateHz = GameThread.DEFAULT_ACTIVE_HZ; 39 40 public double MaximumUpdateHz 41 { 42 set 43 { 44 maximumUpdateHz = value; 45 updateMainThreadRates(); 46 } 47 } 48 49 private double maximumInactiveHz = GameThread.DEFAULT_INACTIVE_HZ; 50 51 public double MaximumInactiveHz 52 { 53 set 54 { 55 maximumInactiveHz = value; 56 updateMainThreadRates(); 57 } 58 } 59 60 private readonly object startStopLock = new object(); 61 62 /// <summary> 63 /// Construct a new ThreadRunner instance. 64 /// </summary> 65 /// <param name="mainThread">The main window thread. Used for input in multi-threaded execution; all game logic in single-threaded execution.</param> 66 /// <exception cref="NotImplementedException"></exception> 67 public ThreadRunner(InputThread mainThread) 68 { 69 this.mainThread = mainThread; 70 AddThread(mainThread); 71 } 72 73 /// <summary> 74 /// Add a new non-main thread. In single-threaded execution, threads will be executed in the order they are added. 75 /// </summary> 76 public void AddThread(GameThread thread) 77 { 78 lock (threads) 79 { 80 if (!threads.Contains(thread)) 81 threads.Add(thread); 82 } 83 } 84 85 /// <summary> 86 /// Remove a non-main thread. 87 /// </summary> 88 public void RemoveThread(GameThread thread) 89 { 90 lock (threads) 91 threads.Remove(thread); 92 } 93 94 private ExecutionMode? activeExecutionMode; 95 96 public ExecutionMode ExecutionMode { private get; set; } = ExecutionMode.MultiThreaded; 97 98 public virtual void RunMainLoop() 99 { 100 // propagate any requested change in execution mode at a safe point in frame execution 101 ensureCorrectExecutionMode(); 102 103 Debug.Assert(activeExecutionMode != null); 104 105 switch (activeExecutionMode.Value) 106 { 107 case ExecutionMode.SingleThread: 108 { 109 lock (threads) 110 { 111 foreach (var t in threads) 112 t.RunSingleFrame(); 113 } 114 115 break; 116 } 117 118 case ExecutionMode.MultiThreaded: 119 // still need to run the main/input thread on the window loop 120 mainThread.RunSingleFrame(); 121 break; 122 } 123 } 124 125 public void Start() => ensureCorrectExecutionMode(); 126 127 public void Suspend() 128 { 129 lock (startStopLock) 130 { 131 pauseAllThreads(); 132 activeExecutionMode = null; 133 } 134 } 135 136 public void Stop() 137 { 138 const int thread_join_timeout = 30000; 139 140 Threads.ForEach(t => t.Exit()); 141 Threads.Where(t => t.Running).ForEach(t => 142 { 143 var thread = t.Thread; 144 145 if (thread == null) 146 { 147 // has already been cleaned up (or never started) 148 return; 149 } 150 151 if (!thread.Join(thread_join_timeout)) 152 Logger.Log($"Thread {t.Name} failed to exit in allocated time ({thread_join_timeout}ms).", LoggingTarget.Runtime, LogLevel.Important); 153 }); 154 155 // 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. 156 mainThread.WaitForState(GameThreadState.Exited); 157 158 ThreadSafety.ResetAllForCurrentThread(); 159 } 160 161 private void ensureCorrectExecutionMode() 162 { 163 // locking is required as this method may be called from two different threads. 164 lock (startStopLock) 165 { 166 if (ExecutionMode == activeExecutionMode) 167 return; 168 169 activeExecutionMode = ThreadSafety.ExecutionMode = ExecutionMode; 170 Logger.Log($"Execution mode changed to {activeExecutionMode}"); 171 } 172 173 pauseAllThreads(); 174 175 switch (ExecutionMode) 176 { 177 case ExecutionMode.MultiThreaded: 178 { 179 // switch to multi-threaded 180 foreach (var t in Threads) 181 t.Start(); 182 183 break; 184 } 185 186 case ExecutionMode.SingleThread: 187 { 188 // switch to single-threaded. 189 foreach (var t in Threads) 190 { 191 // only throttle for the main thread 192 t.Initialize(withThrottling: t == mainThread); 193 } 194 195 // this is usually done in the execution loop, but required here for the initial game startup, 196 // which would otherwise leave values in an incorrect state. 197 ThreadSafety.ResetAllForCurrentThread(); 198 break; 199 } 200 } 201 202 updateMainThreadRates(); 203 } 204 205 private void pauseAllThreads() 206 { 207 // shut down threads in reverse to ensure audio stops last (other threads may be waiting on a queued event otherwise) 208 foreach (var t in Threads.Reverse()) 209 t.Pause(); 210 } 211 212 private void updateMainThreadRates() 213 { 214 if (activeExecutionMode == ExecutionMode.SingleThread) 215 { 216 mainThread.ActiveHz = maximumUpdateHz; 217 mainThread.InactiveHz = maximumInactiveHz; 218 } 219 else 220 { 221 mainThread.ActiveHz = GameThread.DEFAULT_ACTIVE_HZ; 222 mainThread.InactiveHz = GameThread.DEFAULT_INACTIVE_HZ; 223 } 224 } 225 226 /// <summary> 227 /// Sets the current culture of all threads to the supplied <paramref name="culture"/>. 228 /// </summary> 229 public void SetCulture(CultureInfo culture) 230 { 231 // 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. 232 Thread.CurrentThread.CurrentCulture = culture; 233 234 // for multi-threaded mode, schedule the culture change on all threads. 235 // 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. 236 // in that case, the stored value will be set on the actual threads after the next Start() call. 237 foreach (var t in Threads) 238 { 239 t.Scheduler.Add(() => t.CurrentCulture = culture); 240 } 241 } 242 } 243}