Element 61

Monday, August 08, 2005

Resource Management System -- Part 1

This is the first in a series of posts to build a reference counted "resource" management system for your game/graphics engine. First of all, what defines a resource? For our purposes, there are several conditions that determine what can be used as a resource:

  • It can be used by multiple clients, and needs to be reference counted.
  • Most of the clients of these objects do not need raw pointers most of the time.
  • There should be a master list of all of these objects somewhere.
  • Even when this object is not used, we may want to defer destruction of it for any reason.
The more up-to-date of you with regards to C++ might be wondering why we can't simply dole out boost::shared_ptr objects everywhere, and maybe keep a list of them somewhere. This isn't a terrible approach, but it doesn't really provide the most coherent system ever. The only good way to defer destruction is to keep an extra shared_ptr within the manager system. And it doesn't make the reference count inherent to the object. (I am aware of the existence of intrusive pointers in boost, but not familiar with their usage.) Lastly, boost is huge. Just plain massive. Overkill, really. All things considered, we'll do it ourselves for now.

Here's a quick rundown of the system we're going to build. Each type of resource will be managed completely seperately; we make no attempt to fuse all the resources together via base classes and using RTTI to cast them. It's just not worth the effort. We will have handles to resources, which are the main currency handed around when working with resources. Finally there will be the manager, who maintains the list of resources, deals with resource creation and destruction, and allows access to raw pointers if desired. To start with, we'll write up a base class that allows an object to be reference counted.
class RefCountedBase
{
private:
    unsigned int        m_RefCount;

public:
    RefCountedBase() : m_RefCount( 0 )
    { }

    unsigned int RefCount() const { return m_RefCount; }

    unsigned int AddRef()
    {
        return ++m_RefCount;
    }

    unsigned int Release()
    {
        assert( m_RefCount > 0 );
        return --m_RefCount;
    }
};
There's really nothing to explain here; any class that inherits from this will have a reference count associated with it that can be increased, decreased, and accessed. If you're familiar with DirectX or COM, you'll no doubt notice the similarity to IUnknown. Indeed, usage is identical, and this base class is totally independent; you can use it on pretty much anything that needs a reference count. Of course, the main problem here is that we need to explicitly manage the reference count, which is a pain. We need to address this, and the best way is an object that behaves similarly to a shared_ptr. This object will be our resource handle. Let's summarize the basic behavior of these handles:
  • Any user constructed handle (that is, a new handle not generated from inside a manager) is always invalid.
  • Invalid handles don't affect any reference counts, since they have no object to affect.
  • The handle should decrement the reference count of its resource when destroyed.
  • The copy constructor should increment the reference count of the resource.
  • Assignment should decrement the current reference count, assign, and increment the reference count of the new resource.
  • A handle does not include a direct pointer to its resource. This helps discourage access to the resource, unless you really need it.
  • Handles can be compared with == and !=.
  • Handles can be used for any reference counted type.
Since we are managing every type of resource independently, we do not require an inheritance hierarchy for handles to follow. Instead, our handle will be a templated class. Amongst other things, this means that you can never mix handles; a handle to an image can't be used as a handle to a vertex buffer. The next question is, what member variables does a handle have? Before answering that question, let's examine what a resource manager needs to do.

A resource manager has one really important responsibility -- it's the only one which has actual pointers to the resources in question. There is no other way to get access to these pointers. If you're not part of the subsystem which contains the manager, you can never access those pointers. As a result, all managers have three main responsibilities:
  • Maintain the list of resources.
  • Provide a way to flush unused resources out of memory.
  • Receive the requests from handles to alter reference counts.
There's a lot of ways we could keep a list of objects, but ideally we want something very efficient. At first, you might be tempted to use an STL container such as a std::set, and store iterators in handles. Unfortunately, if we do that, there are all sorts of nasty catches with iterators becoming invalidated. Instead, what we'll do is to store the resource list in a std::vector, and keep an array index in the handles. When a resource is flushed out of memory, we delete its pointer, set it to NULL, but leave that NULL as a blank slot in the resource list. Additionally, for efficiency's sake we add that blank slot to another queue. When we load a new resource, we can take a slot off this list, or if the list is empty, we can push_back onto the resource list. With this design, all of the operations relating to resources are constant time.

That really just about covers it for design. For part 2, we'll cover the implementation of handles and managers, and tie everything together

1 Comments:

  • Patiently awaiting part III! (well, maybe not so patiently.)

    By Anonymous Sam, at 8/25/2005 2:50 PM  

Post a Comment

<< Home