Virtualizing the GraphicsDevice in XNA Game Studio 2.0

Originally posted to Shawn Hargreaves Blog on MSDN, Wednesday, December 12, 2007

In the 2.0 XNA Framework, we virtualized the graphics device.

That sounds pretty cool, huh? But what does it actually mean, and why should you care?

In summary, you no longer have to care about a bunch of stuff you used to have to care about. You can stop reading this post right now, and everything will "just work" (tm).

Still here? You're obviously the kind of person who likes to understand how things work under the covers. Let me explain...

 

How things work in 1.0

When your game starts up, we call LoadGraphicsContent(true), in response to which you should load all your content, create textures, vertex buffers, rendertargets, etc.

If your game switches between windowed and fullscreen mode, or the user alt+tabs away from a fullscreen game, or some other game runs in fullscreen mode, or the user locks their desktop, the graphics device becomes lost. You cannot use a lost device, so we suspend calling Draw while it is unavailable. When the graphics device returns from this lost state, it needs to be reset, and when it is reset, some of your graphics resources become invalid. Specifically:

When this happens, we call LoadGraphicsContent(false), which tells you that some of your content needs to be reloaded or recreated, even though other content is still valid.

This is confusing to people. Most programmers don't understand which content is which, so they don't know what to do with that "loadAllContent" boolean parameter. If they get it wrong, their game might crash, or they might waste time needlessly reloading things that didn't need to be reloaded.

It can also be a pain to support LoadGraphicsContent getting called at arbitrary times after your game starts up. If you passed references to your content objects around, sharing them between many different game objects, you must track down all the places that stored a reference and update them to use the reloaded content.

A worse problem occurs if the user drags a game from one screen to another on a dual monitor machine. In that case, we must destroy the graphics device entirely, then create a new device on the second monitor. When this happens, all graphics resources are destroyed, so we call LoadGraphicsContent(true). If you stored a reference to the old GraphicsDevice object, that will no longer be valid. To handle this situation correctly, games must look up the current device each time they want to use it. This can be done using the IGraphicsDeviceService interface, the DrawableGameComponent.GraphicsDevice property, or the GraphicsDeviceManager class. The point is that you cannot store permanent references to the device, because this could change out from under you at any point.

 

How things work in 2.0

In the 2.0 framework, the graphics device and its associated resources always remain valid. You can store references to any objects you like, in confidence that those objects will not go away. The LoadContent method (which used to be called LoadGraphicsContent) still exists, but now it is only called once when your program starts up.

It is obviously still possible that the underlying native device might need to be reset or recreated. But in 2.0, the framework takes care of this internally. If it needs to recreate any graphics objects, it creates a new native object, copies across any data such as the contents of textures and vertex buffers, destroys the old, invalid native object, then changes the internal state of the managed wrapper object so this will reference the new native object in place of the old one. Your game code can go on using the same managed object, and does not need to care that the underlying native object was recreated behind the scenes.

There is only one fly in this ointment. For some kinds of resource, it is not technically possible for us to read existing data out of the old native objects. This applies to resource types that are designed to be updated rapidly on the fly or changed by the GPU, which are allocated in special memory that cannot be read back by the CPU. Specifically, this problem occurs with the DynamicVertexBuffer, DynamicIndexBuffer, RenderTarget*, and ResolveTexture2D classes.

This sounds bad, but in practice you usually don't need to care. We still do the trickery to update your managed wrapper objects, replacing their old underlying native objects with recreated versions. The only problem is that the contents of these objects have been lost. Where a 1.0 rendertarget would need to be recreated from scratch, a 2.0 rendertarget always remains a valid rendertarget, and keeps the same size, format, etc: it just won't contain the same image as before the device was reset or recreated.

For most programs, this turns out not to be a problem. For instance if your game uses a rendertarget for drawing shadow maps, or for bloom postprocessing, it won't care if the contents of that rendertarget are lost when the device is reset, because it was going to overwrite the whole thing during the next frame in any case.

Losing the contents of dynamic resources is only a problem if you update those resources on a sporadic basis, using them as a cache to store data from one frame to the next. For instance this would cause trouble if you were using rendertargets to cache 2D imposter images. If you fall into this (fortunately rather small) category, you can use the IsContentLost property or ContentLost event to detect when your dynamic resources have lost their contents.

 

How things work on Xbox

Xbox is not a multitasking operating system, so it does not suffer from lost graphics devices. When only one game can run at a time, you do not need to worry about other programs temporarily taking away the graphics card.

On Xbox, LoadGraphicsContent (or LoadContent in 2.0) is called just once at startup, then never again. There was no need for us to virtualize the Xbox graphics device, because the problem did not exist in the first place.

Blog index   -   Back to my homepage