Taking out the trash

Taking out the trash

  • Comments 19

Memory management. Object ownership. Dangling references. Leaks. It's enough to make grown men cry, and small boys cower under their duvets quivering with fear.

 

But that was then. The .NET garbage collector takes care of such things for us, neh?


Not true!

 

Garbage collection is great for making code simpler and easier to write, but it isn't so good at handling managed wrappers around native resources such as textures or vertex buffers. The garbage collector doesn't control the underlying native objects: all it sees are the tiny managed wrappers. It is easy to get into a situation where your graphics card is struggling under the weight of hundreds of megabytes of texture data, but the garbage collector is thinking "ho hum, everything fine here, they did allocate this array of a hundred Texture objects, but that's ok, each Texture is only a few bytes and I have nearly a megabyte free still, so there's no point bothering to collect anything just yet".

 

When dealing with graphics resources, you really need to have more control over making sure things are destroyed at exactly the right time. The .NET framework provides a standard interface, IDisposable, for doing exactly this:

Texture2D texture = new Texture2D(...);

 

try

{

    // do stuff using texture

}

finally

{

    texture.Dispose();

}

This pattern is so common that C# has a special keyword for writing it more concisely:

using (Texture2D texture = new Texture2D(...))

{

    // do stuff using texture

}

The problem with IDisposable is that it doesn't scale very well to groups of related objects. Consider the XNA Content Pipeline. This can load textures, effects, and models, as well as whatever custom types people may choose to add. Who is responsible for unloading this data, and how should that work?

 

Textures are IDisposable. So are effects. At first glance it seems like this is good enough, and whoever loaded each piece of content ought to dispose it whenever they are done using it.

 

The problem comes when you consider the Model type. A model is a regular managed object, not a native GPU resource. Should this be IDisposable? Not really. But the model holds references to vertex buffers, effects, and textures, all of which are IDisposable. If you loaded a model, and that model was not IDisposable, how then could you clean up the various bits and pieces contained within it? Model would have to implement IDisposable, and provide a Dispose method that chained to the Dispose of the various component parts. That turns out to be a bad idea for two reasons...

 

First off, consider this code:

ContentManager loader = new ContentManager(GameServices);

 

Model a = loader.Load<Model>("London");

Model b = loader.Load<Model>("Tokyo");

 

// London and Tokyo both happen to reference the same texture,

// BrickWall.tga, so the ContentManager automatically loads a

// single instance of this, and shares it between both models.

 

a.Dispose();

 

// What happens to the shared texture here?

 

b.Dispose();

 

// At this point the BrickWall texture should be disposed.

A model can't always dispose every resource it is using, because some other model might still be sharing them. But we do eventually need to dispose the texture in order to avoid leaks.

 

Back in the elder days of C++ and manual memory management, we would have used reference counting to solve this problem. But reference counting sucks for all sorts of reasons I can't be bothered to go into here. It is better than nothing, but falls short of the automatic, rapid development approach .NET developers have rightly come to expect.

 

The other problem with making Model implement IDisposable is that this decision would propagate all the way up the object hierarchy. What if you are adding a new content type for your game, for instance a Level class that contains a Sky model, a Landscape model, some collision skin data, and a NotAtAllClichedDestructableCrate model? In order to correctly dispose those nested models, your Level class would also have to be IDisposable! As would anything else that contained a Level, and so on, for ever and ever. It would be way too easy to for someone to get this wrong and accidentally create a memory leak.

 

Our solution? Make resource cleanup belong to the ContentManager, rather than to each individual object. Using the XNA Content Pipeline, assets are loaded and unloaded like this:

ContentManager loader = new ContentManager(GameServices);

 

Model a = loader.Load<Model>("London");

Model b = loader.Load<Model>("Tokyo");

 

// At this point the shared BrickWall.tga is also loaded.

 

loader.Unload();

 

// Now all three resources are gone.

No reference counts. No need for Model to be IDisposable. No possibility of leaks. Simple. Safe. Splendid.

 

You may be thinking it seems a bit drastic to always unload everything in one fell swoop. Simple, sure, but not exactly very flexible! Fear not. If you need more control, you can create more than one ContentManager. You could use one for global assets that need to stick around for the entire duration of your game, another that gets unloaded at the end of each level, or even one per room that gets loaded and unloaded as the player moves around the world.

  • I think its more logical to store assets grouped by content manager anyway. I dont see it being a problem this way.
  • Awesome article, I think that will really help people moving from a C++ to C# way of thinking.

    If you are up for a follow up, perhaps illustrating the point with a freebie profiler that the express + xna people can use?
  • I very much agree with this approach ... good stuff Shawn.  Riddle me this though:

    What if your game requires "London" and "Tokyo" to be loaded and unloaded at different times.  Will each content manager load a different instance of that shared texture?  Can we have a third content manager which could pre-load the texture, share it with the london and tokyo  managers, then unload the texture when it's good and ready to?
  • I should clarify ... when I say "loaded and unloaded at different times", I meant to add, "potentially at the same time".
  • At the moment the texture will only be shared within a single resource manager, so if two different managers both want to reference it, two copies will get loaded.

    We did consider your idea of a third manager that could handle these shared requests, and I think it is a good one, but this didn't fit into the V1 schedule.

    Although now I come to think about it, the Load method is virtual, so this functionality could be added fairly easily by subclassing the existing manager. I smell another blog post topic for sometime after we actually ship this stuff...
  • PingBack from http://joran.omark.org/?p=11
  • Maybe it's just because I'm a C# guy, but having the ContentManager load/unload and manage the resources seems like the most intuitive place to put it. Granted, I'm also not a professional game developer :P.

    Great post! So far, I really like the architecture you guys came up with in XNA, well designed.
  • It seems to me that, since multiple ContentManager instances wouldn't know about shared unmanaged resources (such as textures), would there be two copies of brickwall.tga if there were two ContentManager instances that loaded models using it?

    Or is that managed somewhere deeper in the library?
  • "It is easy to get into a situation where your graphics card is struggling under the weight of hundreds of megabytes of texture data, but the garbage collector is thinking "ho hum, everything fine here, they did allocate this array of a hundred Texture objects, but that's ok, each Texture is only a few bytes and I have nearly a megabyte free still, so there's no point bothering to collect anything just yet"."

    Shawn, the .NET framework API System.GC.AddMemoryPressure (http://msdn2.microsoft.com/en-us/library/system.gc.addmemorypressure.aspx) was designed to alleviate this problem by letting the GC know about behind-the-scenes resource allocations.
  • AddMemoryPressure is a useful API, and great for the situation where a small managed object wraps a large native memory allocation, but it still isn't perfect for native objects that are expensive in ways other than just consuming memory.

    For instance, how much memory pressure should a texture add? Say the texture is 10 megabytes. If you have 20 of them, that's getting pretty close to filling up your 256 megabyte graphics card. But the garbage collector is looking at your 1 gigabyte of main RAM, so it doesn't see 200 megabytes of memory pressure as being all that urgent.

    It gets even stranger for things like file handles or GPU query objects, which are tiny, but represent complex OS resources.

    Adding the pressure is better than nothing, but still can't be relied on to always do the right thing for non-memory resource types.
  • PingBack from http://sharky.bluecog.co.nz/?p=68
  • This is an extremely interesting topic :-)

    I was unaware of AddMemoryPressure (that is totally cool!)

    It seems like you could implement a trivial video-memory-aware garbage collection invoker. Would it be possible to look at the amount of free memory on the video card and invoke GC.Collect(n) if the object being allocated is smaller than the available space?

  • I meant LARGER than the available free space -- I need to proof read

  • In case it helps anyone else, I created a "SmartContentManager" that is able to share assets with other SmartContentManagers per your comment above Shawn:

    http://codecube.net/item.asp?cc_ItemID=343

Page 1 of 2 (19 items) 12
Leave a Comment
  • Please add 4 and 6 and type the answer here:
  • Post