Developer Diary: XNA Game Loop and Threading

Posted 04/10/2007 @ 06:30:35 AM by Joseph Molnar
Filed under: Developer Diary , Programming , Xbox 360 , XNA

In my previous Developer Diary I talk about the tools and tutorials I used to get some background on XNA. After playing with the tutorials I decided to start digging into the mechanics of what makes an XNA game. The big defining elements are the two main Game methods, Draw and Update. In the process of looking at these method I thought I would give you some insight into .NET threading.

Note: Most of my sample code will be implemented as DrawableGameComponents instead of actual Games. I found this the most convenient way to bring code into an existing project.

Main Game Loops and Threading

Draw, which is intended for drawing frames, and Update, which is intended to run game logic, are called automatically by the XNA framework. When I saw these methods I had assumed they were called via different threads. After all, game developers typically try to achieve 60 frames per second (fps) and therefore try to remove any potential contention.

I was wrong. In .NET you typically get the current thread id using Thread.CurrentThread.GetManagedThreadId. For example, in the following code I retrieve the current thread id in both the Draw and Update methods.

/// <summary>
/// Sample class for checking thread id.
/// </summary>
public class CheckThreadIdClass : DrawableGameComponent {
    /// <summary>
    /// Constructor required for the game component.
    /// </summary>
    /// <param name="theGame">The Game this component will be used by.</param>
    public CheckThreadIdClass( Game theGame )
        : base( theGame ) {
    }

    /// <summary>
    /// The method used to draw frames.
    /// </summary>
    /// <param name="gameTime">GameTime since last call.</param>
    public override void Draw( GameTime gameTime ) {
        base.Draw( gameTime );
        // get the thread that is being called by this thread.
        int currentThreadId = Thread.CurrentThread.ManagedThreadId;
    }

    /// <summary>
    /// The method used to update game location.
    /// </summary>
    /// <param name="gameTime">GameTime since last call.</param>
    public override void Update( GameTime gameTime ) {
        base.Update( gameTime );
        // get the thread that is being called by this thread.
        int currentThreadId = Thread.CurrentThread.ManagedThreadId;
    }
}

Under Windows you will get the same thread id value in both Draw and Update (probably 1). Under the 360 both methods also report the same value, but it is a large negative value, -117440492 (not sure what this value means). From here I added some code to create a new thread to see what its thread id is.

/// <summary>
/// Sample class for creating a thread.
/// </summary>
public class CreateThreadClass : DrawableGameComponent {
    private Thread extraThread = null;
    private volatile bool isStopping = false;

    /// <summary>
    /// Constructor required for the game component.
    /// </summary>
    /// <param name="theGame">The Game this component will be used by.</param>
    public CreateThreadClass( Game theGame )
        : base( theGame ) {
    }

    public override void Initialize( ) {
        base.Initialize( );
        // create and start thread
        extraThread = new Thread( ThreadMethod );
        extraThread.Start( );
    }

    /// <summary>
    /// The method used to draw frames.
    /// </summary>
    /// <param name="gameTime">GameTime since last call.</param>
    public override void Draw( GameTime gameTime ) {
        base.Draw( gameTime );
        // get the thread that is being called by this thread.
        int currentThreadId = Thread.CurrentThread.ManagedThreadId;
    }

    /// <summary>
    /// The method used to update game location.
    /// </summary>
    /// <param name="gameTime">GameTime since last call.</param>
    public override void Update( GameTime gameTime ) {
        base.Update( gameTime );
        if( GamePad.GetState( PlayerIndex.One ).Buttons.Back == ButtonState.
Pressed ) {
            ShutDown( );
        } else {
            // get the thread that is being called by this thread.
            int currentThreadId = Thread.CurrentThread.ManagedThreadId;
        }
    }

    /// <summary>
    /// Method called when we are to shutdown the game.
    /// </summary>
    private void ShutDown( ) {
        isStopping = true;
        // wait for the thread to die
        if( this.extraThread != null ) {
            this.extraThread.Join( );
            this.extraThread = null;
        }
        // now exit
        this.Game.Exit( );
    }

    /// <summary>
    /// Sample thread method.
    /// </summary>
    private void ThreadMethod( ) {
        // get the thread that is being called by this thread.
        int currentThreadId = Thread.CurrentThread.ManagedThreadId;

        while( !isStopping ) {
        }
    }
}

On the Xbox 360 the new Thread's thread id returned a similar large negative value, -117440472. At this point I recalled that Microsoft added a special method, Thread.SetProcessorAffinity, for the 360 that allows you to associate a software thread to hardware thread (one of the six the 360 has). So I changed the ThreadMethod method to use a particular hardware thread.

Note: Thread.SetProcessorAffinity must be called within the thread that wishes to run on particular hardware threads.

/// <summary>
/// Sample thread method.
/// </summary>
private void ThreadMethod( ) {
    // get the thread that is being called by this thread.
    int currentThreadId = Thread.CurrentThread.ManagedThreadId;
#if XBOX360
    // set the processor threads to run on
    Thread.CurrentThread.SetProcessorAffinity( new int[ ] { 3 } );
    // re-get the thread id
    currentThreadId = Thread.CurrentThread.ManagedThreadId;
#endif
    while( !isStopping ) {
    }
}

The Xbox 360 returned the same negative value, -117440472. Needless to say, it is a safe assumption that Draw and Update are in the same thread.

The reason for this is simple. Having Draw and Update in the same thread is easier for novices. Writing threaded code when sharing data between threads, as would be the case if Draw and Update were in separate threads, is trickier than it may seem.

Some Threading Notes of Interest

The MSDN Library description for Thread.SetProcessorAffinity has a description of the 360's threading (see the remarks section). Of interest is how many threads are actually listed as either being reserved or partially used by the 360's system software and Dashboard. It seems there is more overhead than I recall from blogs and podcasts on this subject. Perhaps this breakdown is specific to XNA on the 360?

From a tool standpoint, there is one other item of interest. It appears that Visual C# Express does not support the thread debug window, which allows you to view, switch and freeze threads. While some may consider this a mild annoyance there are certain circumstances, like deadlock debugging, that will be difficult to debug without this feature; I would love for Microsoft to allow XNA to be installed into non-Express editions of Visual Studio 2005. Microsoft has stated this may come in a future update to the XNA tools, though most likely not in the update coming this month.

Follow-on Post

I'll continue my discussion on threading, delving into implementation options and difficulties over the next few post:

Recent Developer Diary Articles

Comments

Sometimes it's even better when you have control over your own game loop. I've written an article about various game loops and their pros and cons at http://dewitters.koonsolo.com/gameloop.html

I'm not sure how XNA handles this, but I assume a constant framerate?

Post a comment