Thursday, 23 February 2012

Update On Another Thread

For some time I have been considering putting the Update processing on a separate thread to the Draw processing.  I had assumed it was difficult to synchronise all the values from one thread to another so I had put it off. 

I have several sections of distinct code, including the Artificial Intelligence (AI) and the Storage which already run on their own threads.  On the Xbox this allows me to specify which processor core they run on and most importantly allows tasks to run in parallel without degrading the performance of the other threads.


#if XBOX
  Thread.CurrentThread.SetProcessorAffinity(
                                     new int[] { 4 });
#endif



The part of the game I am working on is adding non-line of sight weapons.  The trouble is this dramatically increased the number of collision calculations I needed.  Even with only a handful of projectiles on the screen at once the Xbox started to drop a couple of frames.

I considered multi-threading the projectile collision calculations but the design got complicated.  At the same time I have been peer reviewing End Of Days: Infected vs Mercs.  A fun first person shooter which has many elements in common with my game.  Kevin, the developer of that game, was happy to chat about the design.  He said the best performance boost was to have the Update on a separate thread to the Draw.  Thanks Kevin and for the triangle code.

That was enough to make me look further and I found several postings commenting that it was not too difficult and it definitely gave the desired increase in speed.

I had plenty of experience with my other multi-threaded code to know what I needed to look out for.  I had standard code I use to launch and control the thread so I got under way.



private void StartUpdateThread()
{
  // Avoid duplicate threads running.
  if (!isUpdateThreadActive)
  {
    isUpdateThreadActive = true;
    killThreads = false;
    Thread threadUpdate = 
                    new Thread(ProcessUpdateThread);
    // Set to background so that when the foreground 
    // thread dies the background one dies as well
    threadUpdate.IsBackground = true;
    threadUpdate.Start();
  }
}

// Runs on another hardware thread on the Xbox. 
private void ProcessUpdateThread()
{
#if XBOX
  Thread.CurrentThread.SetProcessorAffinity(
                                     new int[] { 4 });
#endif
  updateTime = new GameSelfTimer();
  updateRandom = new Random();
  // Wait a moment for everything to catch up
  Thread.Sleep(5);
  while (!killThreads)
  {
    if (readyToUpdate && !readyToDraw)
    {
      readyToUpdate = false;
      updateTime.Update();
      ProcessUpdate(updateTime);
      readyToDraw = true;
    }
  }
  isUpdateThreadActive = false;
}


The above is the code I use throughout my game for launching and running the threads.

GameTime

I have mentioned elsewhere about the trouble with getting the standard XNA GameTime class on another thread so the first job was to completely replace GameTime throughout the entire code.

I just use my own timer based on a StopWatch.  My timer class has the same properties as GameTime to minimise the code changes.  It took an hour to carefully replace every occurrence and then test while it was still a single thread.

Synchronisation and Buffers

I swapped over and tested without any value being buffered from one thread to the other just simple synchronisation so the update thread only runs when something worth updating has changed.

It worked and showed a dramatic speed improvement but as expected the models on the screen stuttered because they were using incomplete values which were being calculated out of sync by the Update thread.

What did surprise me was how few values are changed and then shared between the Update and Draw threads:


  • View matrix calculated from the player camera position
  • Projection matrix calculated occasionally when the weapon sight zooms in
  • World position of every model
  • Skin transform matrix array for animations

I simply buffer those and use a boolean variable to indicate when changes are ready to update the buffers.  There may be a couple of others I find in time but the above have smoothed the display.



// From the main thread
public virtual void UpdateThreadBuffers()
{
  lock (lockPosition)
  {
    WorldPositionDraw = worldPositionBuffer;
  }
}


I use a base class for all models which mean that I only had to add the Update buffers method in a very few places.

The tricky bit was making sure I called it in the correct place for all instances.  That did not take as long as I expected.  Especially as static models don't need any changes after they are positioned.



// From the main thread.
protected void UpdateThreadBuffers()
{
  if (readyToDraw)
  {
    gameManager.Shading.UpdateThreadBuffers();
    for (int i = 0; i < Controllers.Count; i++)
    {
      Controllers[i].UpdateThreadBuffers();
    }
    PortableItems.UpdateDroppedThreadBuffers();
    // Always the last thing so update will run again
    readyToDraw = false;
  }
}


I also use 3D moving and animated models on some menus and option screens so I had to make changes outside of the main game to keep those screens compatible.

Job done.  Loads more processing capacity for the extra collision calculations I need, good frame rate and it should make future changes much easier.

No comments: