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:

Nothing else needs to change (except for writing the Parallel Component Manager, of course)

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; List mGameComponents; 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?

Examples

Here are the examples we went over in lecture