Graphics state management

Originally posted to Shawn Hargreaves Blog on MSDN, Thursday, October 25, 2007

Once upon a time there lived a young graphics coder named Johnny. One day Johnny wrote a program that displayed a 3D model. As is frequently the case with people his age, Johnny was somewhat careless and did not bother to set all the renderstates which an older or wiser programmer would undoubtedly have taken care of, but luckily for him these states had sensible defaults, so his omission did not cause any problems. The model was shiny. All was well with the world.

The next morning Johnny decided to add a framerate counter, so he inserted a simple call to SpriteBatch.DrawString. Woe! The framerate showed up ok, but the model no longer rendered correctly. Disaster!

Johnny has been bitten by the dreaded state management monster.

Keeping track of which states are set when, and by who, is not particularly glamorous, but this is an important part of creating a robust 3D program. If you get it wrong, your graphics will display incorrectly. If you get it right but change states too often, your program will run slowly. If you get it right but fail to make each drawing method sufficiently self contained, your program will be fragile and you can find one piece of code affecting the behavior of other unrelated modules.

There are many pieces of rendering state associated with the graphics device:

All must be set correctly for drawing to behave as expected. And yet most programs do not actually bother to set every state! This works because states have sensible default values, so for instance if your program never modifies the ScissorRectangle, you can assume it will be set to cover the entire screen, thus no scissoring will be performed.

But here's the thing. What if your program contains more than one module, perhaps even written by different people? Class A does not use the ScissorRectangle, and assumes it will always be set to a good default, but class B changes the ScissorRectangle. Although A and B both work perfectly in isolation, when you combine them in a single program the actions of B will cause A to render incorrectly.

The solution is to standardize how graphics device state is managed, and do this consistently across your entire program. There are several possible approaches:

 

Approach 1: Every Man For Himself

An obvious solution is to say every piece of drawing code must always set every possible state to whatever values it needs. This is simple and robust, producing self contained drawing methods that make no assumptions about how things are set up before they are called. This is particularly suitable for reusable framework code, for instance the XNA Framework SpriteBatch class works in this way.

In psuedocode:

    void Foo.Draw()
    {
        SetEveryPossibleStateToTheValuesIWant();

        DrawMyStuff();
    }

The downside is that the graphics device has a lot of state, so setting everything can be expensive.

You might think you can optimize this by checking what states are currently set, and only setting your new values if they are different to the current ones. This is not a good idea, because reading states back from the graphics device can actually be much slower (especially on Xbox) than just setting them regardless.

 

Approach 2: Leave No Trace

People are often tempted by logic along the lines of "in order to avoid class B messing up the rendering of class A, I will just have B put back whatever states were previously defined". The SaveStateMode.SaveState parameter to SpriteBatch.Begin and ModelMesh.Draw implements this behavior.

Trouble is, this is not a smart way to do things.

For one thing, it is slow. Really slow.

For another, it leads to a logical mess. Who says that B must restore state in order to avoid messing up A? Why isn't it A that should avoid messing up B? What about if you introduce a third class C? It quickly gets confusing trying to keep track of who does what when.

 

Approach 3: Consistency Is Next To Godliness

To avoid having to set the same state values over and over again, it can be useful to define some standardized values. For instance you might decide the scissor rectangle will always be set to the full screen, clip planes will be off, stencil will be disabled, and z-test will be enabled using a less-than compare mode.

As long as you make sure these standard values are always set, drawing methods only need to change whatever things they want different to the defaults. Then when they have finished drawing, they must set these few things back to the defaults. In psuedocode:

    void Foo.Draw()
    {
        ChangeAnyStatesIWantDifferentToTheDefault();

        DrawMyStuff();

        SetStatesThatIChangedBackToTheDefault();
    }

This can be faster than approach #1, because even though it has the extra overhead of cleaning up after itself, the number of states needing to be modified by any given call is typically very small compared to the total number of available states.

The downside is that some states just don't have sensible default values. What would you pick as the default vertex buffer, or default texture? Every drawing method is going to want different ones, so trying to set these things back to standardized defaults doesn't really make a lot of sense.

 

Approach 4: Not All Renderstates Are Created Equal

It can be useful to divide renderstates into two categories.

Transient states, which are likely to be different for every drawing method, might include:

Persistent states, which are likely to be the same for most drawing methods, could include:

Once you have decided which states are which, you can use approach #1 for the transient states and approach #3 for the persistent states. In psuedocode:

    void Foo.Draw()
    {
        ChangeAnyPersistentStatesIWantDifferentToTheDefault();
        SetAllTransientStatesToTheValuesIWant();

        DrawMyStuff();

        SetPersistentStatesThatIChangedBackToTheDefault();
    }

This gives the best of both worlds. You don't waste any time setting persistent states which are likely to already have been set by the previous guy, and you also don't waste any time restoring transient states where the next guy in line is likely to want something else in any case.

The trick is to decide up front which states are which, then make sure you follow the rules consistently across your entire codebase.

Blog index   -   Back to my homepage