Parallel Game Programming
Earlier lectures (see the lecture notes page here) discussed threaded programming in general. Now we will look at the specific situation of using threads inside games.
Brief Disclaimer
There are mutiple ways to add parallelism to game programming, we are only going to examine a few of them. The techniques that we discuess are not necessarily the very best techniques for adding parallelism to games, but they are fairly simple, and allow us to get started without adding too much complexity.
Parallel Update of Game Components
If you have separated your code into a number of components that operate independently, then parallelizing the update calls is fairly easy. Recall that if you have a number of game components comp1, comp2, and comp3, the main program update does something like the following:
Update(GameTime g) { comp1.Update(g); comp2.Update(g); comp3.Update(g); base.Update(g); }
(of course, this might be happening implicitly, if you've added comp1, comp2, and comp3 to the Components property of the main game, but that is what the base.Update(g) call would do)
How can we get these three updates to run in parallel? We could create a new thread for each game component (which contains as an instance variable the time since the last frame and a pointer to the component that we want to run), and then start each thread up. A join at the end of the updates will ensure that we wait for all updates to occur before we go on to the drawing code.
Component Thread (Simple)
class ComponentThread { public ComponentThread(GameComponent component, GameTime time) { mGameComponent = component; mGameTime = time; } GameComponent mGameComponent; GameTime mGameTime;
// XBOX only -- need to place threads on processors #if XBOX public int mProcessorAffinity = 3; #endif
public void Go() {
// XBOX only -- need to place threads on processors #if XBOX Thread.CurrentThread.SetProcessorAffinity(new int[] {mProcessorAffinity}); #endif mGameComponent.Update(mGameTime); } }
Starting a Thread for Every Component, Every Frame
protected override void Update(GameTime gameTime)
{
// Create a wrapper for each thread (to store the component that
// the thread will be running, and the gametime to use on the
// update call
ComponentThread compThread1 = new ComponentThread(comp1, gameTime);
ComponentThread compThread2 = new ComponentThread(comp2, gameTime);
ComponentThread compThread3 = new ComponentThread(comp3, gameTime);
// XBOX only -- need to place threads on processors
#if XBOX
compThread1.mProcessorAffinity = 3;
compThread2.mProcessorAffinity = 4;
compThread3.mProcessorAffinity = 5;
#endif
// Create a thread for each game component
Thread t1 = new Thread(comp1.Go);
Thread t2 = new Thread(comp2.Go);
Thread t3 = new Thread(comp3.Go);
// Kick off all the threads
t1.Start();
t2.Start();
t3.Start();
// Any other updates we want to do will run on the main thread
base.Update(gameTime);
// Wait for everyone to finish
t1.Join();
t2.Join();
t3.Join();
}
The astute may notice that GameTime is a class, not a struct, so we are passing the same pointer to all three component wrappers. The game components themselves have no idea that they are running in parallel, so they will not lock acces to the gameTime class that they all share. Is this a problem? No -- because instances of the GameTime class are all completely read-only. No one can change the value of the GameTime class out from under us, so it is not a problem
Another thing to note: On the xbox, we need to place threads on processors (while on windows, we are not allowed to place threads on processors, the OS does it for us). Of the 6 porcessors, 0 and 2 are reserved by XNA. The main thread runs on 1. So, worker threads should probably be confined to 3, 4, and 5
This approach will work, and will give us a modest increase in framerate (assuming that we are limited in our framerate by the update calls, and not the drawing or the montior refresh rate, and we are not using a fixed frame rate), but the gains are nominal. My tests on a 2-component game got the framerate increased from a solid 15fps to somehting that fluctuated between 17FPS and 21FPS) The problem is that we are spending way too much time starting up and shutting down the threads each frame. We need to avoid starting and stopping threads
Parallel Component Manager
Instead of starting and stopping threads, we will create one thread for each component at the beginning of the program, and then use AutoResetEvents to syncronize between frames. We'd like to abstract as much of the parallel code as possible to a single class, so we will create a ParallelComponent class to do all of our parallel management. We will make the following adjustments to our main game class:
- In the constructor
- Create a ParallelComponentManager
- Add all game components that we want to run in parallel to this manager
- In the update loop:
- At the start of the update loop, let the parallel component manager know that the update loop is starting (Call a standard Update method on the parallel component manager)
- At the end of the update loop, wait for all of the threads to finish, by calling a syncronize method on the parallel component mangager.
Code to add to the game constructor
mParallel = new ParallelComponentManager(this); mParallel.AddParallelComponent(mWorld1); mParallel.AddParallelComponent(mWorld2);
New Update Loop
protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) { mParallel.Shutdown(); this.Exit(); } else { mParallel.Update(gameTime); // Do any other updating that we want to happen on the main thread base.Update(gameTime); mParallel.Syncronize(); } }
Note that since our threads are running continually, we want to be good citizens and shut everything down when we are done. So, on exit we will call a shutdown method on our parallel manager that will kill all of the threads
All that remains is to create our Parallel Manager:
Parallel Component Manager
public class ParallelComponentManager : Microsoft.Xna.Framework.GameComponent { public ParallelComponentManager(Game game) : base(game) { mGameThreads = new List(); mGameComponents = new List (); mThreadFinished = new List (); mThreadReadyToGo = new List (); }
public void AddParallelComponent(GameComponent g) { EventWaitHandle ready = new AutoResetEvent(false); mThreadReadyToGo.Add(ready); EventWaitHandle finished = new AutoResetEvent(false); mThreadFinished.Add(finished); Worker w = new Worker(this, g, finished, ready); Thread workerThread = new Thread(w.Go); mGameComponents.Add(g); mGameThreads.Add(workerThread);
// XBOX only -- need to place threads on processors #if XBOX w.mProcesserAffinity = (mGameThreads.Count - 1) % 3 + 3; #endif
workerThread.Start(); } public GameTime mGameTime; ListmGameComponents; List mGameThreads; List mThreadReadyToGo; List mThreadFinished; public override void Update(GameTime time) { mGameTime = time; foreach (EventWaitHandle h in mThreadReadyToGo) { h.Set(); } } public void Shutdown() { foreach (Thread t in mGameThreads) { t.Abort(); } mThreadReadyToGo.Clear(); mThreadFinished.Clear(); } public void Syncronize() { foreach (EventWaitHandle h in mThreadFinished) { h.WaitOne(); } } } class Worker { GameComponent mGameComponent; ParallelComponentManager mParallelManager; EventWaitHandle mThreadFinished; EventWaitHandle mThreadReadyToGo;
// XBOX only -- need to place threads on processors #if XBOX public int mProcesserAffinity = 3; #endif
public Worker(ParallelComponentManager parent, GameComponent component, EventWaitHandle finished, EventWaitHandle ready) { mGameComponent = component; mParallelManager = parent; mThreadFinished = finished; mThreadReadyToGo = ready; } public void Go() { // XBOX only -- need to place threads on processors #if XBOX Thread.CurrentThread.SetProcessorAffinity(new int[] {mProcesserAffinity}); #endif while (true) { mThreadReadyToGo.WaitOne(); GameTime time = mParallelManager.mGameTime; // If we were really paranoid, we would acquire a lock and // then clone mGameTime. However, since GameTime fields // are read-only, that should not be necessary. mGameComponent.Update(time); mThreadFinished.Set(); } } }
Is that all there is?
So, it should be easy to parallelize your code -- each game component does not even need to know it is running in parallel, right?
... not exactly. For one, we have assumed that each game component is complete independent of all other game components, which is not always a safe asuumption.
What if our different game components share data? What can we do?
- Use data that may be inconsistent, and hope nothing goes wrong. While this looks pretty bad on its face (and in general it is a pretty bad idea to just hope everything works out), if you can convince yourself that most memory contention will be mutilple reads, this might be OK
- Protect all shared data with locks. This could cause some slowdown if there is lots of contention for some data, and there still might be some inconsistent results
- Before each component starts, have it copy all the shared data that it needs to a buffer, and use the buffered data
- Also do some form of double-buffering for this
- Reconfigure your components to share little/no data
- Some combination of the above
Examples
Here are the examples we went over in lecture
- ParallelGame.zip The colliding squares example. Note the cheating with the vertical split in the screen
- ParallelGameExample.zip Orbiting smilely faces example