New Fun Blog – Scott Bilas

Take what you want, and leave the rest (just like your salad bar).

Gem: A Generic Handle-Based Resource Manager

with 6 comments

From Game Programming Gems
Hardcover, Charles River Media, August 2000
Edited by Mark DeLoura

Introduction

All computer applications are databases. They spend most of their time juggling data resources – creating, destroying, caching, modifying, querying, saving, and restoring objects of various types. Games typically contain multiple types of databases, each of which is generally hard coded for each different case to keep things speedy. Some examples of game databases are file systems, texture managers, font managers, game actor managers. On top of those, there will also be a wide variety of domain-specific databases that completely depend on the game’s genre and content.

A resource database that’s built into all C++ games is the basic object memory manager. A programmer calls new to construct a new object and passes its pointer around for other objects to pass it messages. When the object is no longer needed, somebody deletes it and its resources are returned to the system. This method works great in general, but it breaks down when we have to worry about shared resources. This is where we need a more specialized database.

Let’s use a font object for our example. The font would at minimum consist of a bitmap and a set of specifications, such as the X,Y (or U,V) locations of its character cells, so the graphics system can render it to the screen. Such an object is fairly heavy duty in terms of memory usage and creation time. Different systems in the game, such as the development console and a text control within the GUI, will want to use font objects, but we can’t have each system creating its own local copy of the font object. Obviously, this would be slow and use up a lot of memory. To solve this problem we’ll come up with a way to share font objects. Chances are the solution will be called the FontMgr and will feature methods that get pointers to fonts, loading them in on the fly and caching them until they are no longer needed. The FontMgr will be made available from a global location (possibly as a Singleton) and will be responsible for all the font objects in the system.

What we’re really talking about here is a specialized database. The FontMgr will be responsible for juggling font resources and, now that it’s considered an API, will suddenly take on additional responsibilities as the central clearing house for fonts. What if someone tells it to delete a font to free up resources, but some systems in the game still have pointers to it? How do we guarantee safety of the system without sacrificing performance? Will we be copy-pasting this code again (with slight tweaks) when it comes time to build the MousePointerMgr? This gem presents a simple, safe, generic, and efficient way to manage controlled resource objects.

Method

The job of a resource manager is to create resources on demand, hand them out to anyone who asks, and then eventually delete them. Handing out those resources as simple pointers is certainly easy and convenient, but it’s not a very safe way to do it. Pointers can “dangle” – one part of the system may tell the resource manager to delete a resource, which then immediately invalidates all other outstanding pointers. There’s no good way to prevent the dangling pointer problem from happening, and the only way we would find out that someone was attempting to dereference a deleted object is when the access violation dialog box comes up and the game crashes. The problem is that, with pointers, there’s no way to know how many references are outstanding, given that clients can copy the pointers as many times as they like without telling the manager about it.

Another problem is that the underlying data organization can’t change with pointers. Any reallocation of buffers would immediately invalidate all outstanding pointers. This becomes especially important with saving the game to disk. Pointers can’t be saved to disk because the next time the game is loaded, system memory will probably be configured differently or we may even be on a completely different machine. The pointers must be converted into a form that can be restored, which will probably be an offset or a unique identifier of some sort. Working around this problem isn’t exactly trivial and can require a lot of work to support in client code.

So it’s plainly not a good idea for a safe and flexible resource manager to be handing out pointers. Rather than using pointers, or attempting to write some kind of super-intelligent over-complicated “smart pointer”, we can add one layer of abstraction and use handles instead, and put the burden on the manager class. Handles are an ancient programming concept that API’s have been using with great success for decades. An example of this is the HANDLE type returned by the CreateFile() call in Win32’s file system. A file handle, representing an open file system object, is created through the CreateFile() call, passed to other functions such as ReadFile() and SetFilePointer() for manipulation, and then finally closed off with CloseHandle(). Attempting to call those functions with an invalid or closed handle will not cause a crash, but will instead return an error code. This method is efficient, safe, and easy to understand.

Handles almost always fit into a single CPU register for efficient storage in collections and passing as parameters to functions. They can be easily checked for validity and provide a level of indirection that allows the underlying data organization to change without invalidating any outstanding handles. This has significant advantages over passing around pointers. Handles can also be easily saved to disk, because the data structures they refer to can be reconstructed in the same order on a game restore. This allows the handles to be stored directly with no conversions necessary, because they are already natively in unique identifier form.

The Handle Class

A fast and safe way to represent handles is to use an unsigned integer composed of two bitfield components (this class appears in Listing 1). The first component (m_Index) is a unique identifier for fast dereferencing into the handle manager’s database. The handle manager can use this number however it likes, but perhaps the most efficient is as a simple index into an std::vector. The second component (m_Magic) is a “magic number” that can be used to validate the handle. Upon dereferencing, the handle manager can check to make sure that the magic number component of the handle matches up with its corresponding entry in the database.

The Handle class is very simple and really doesn’t do much except manage the magic number. Upon calling Init(), the handle will be given the next magic number, which auto increments and will wrap around if necessary. Note that the magic number is not intended to be a GUID. Its purpose is to serve as a very simple and fast validity check and is relying on the high improbability of a condition arising where one object happens to have the same index and magic number (via wrapping) as another. The magic number of zero is reserved for the “null handle” – where the handle’s data is zero. The default Handle constructor sets itself to null, a state that will return true on an IsNull() query. This is convenient to use for an error condition – a function that creates an object and returns a handle to it can just return a null handle to indicate an error occurred.

In most ways the Handle class will act as a read-only unsigned integer. It’s not intended to be modified after being created, though it can safely be assigned back to null to reset it. You’ll notice that it’s a parameterized class, taking a TAG type in order to fully define it. The template parameter TAG doesn’t do anything except differentiate among types of handles – an object of type TAG is never used anywhere in the system. The motivation here is type safety. Without parameterizing Handle, a handle meant for one type of resource could be passed to a function expecting a handle to a different type of resource without a complaint from the compiler. So to keep things safe, we create a new handle type, taking any unique symbol and using it for the parameter. The TAG type can really be anything so long as it’s unique across Handle types, but it’s convenient to define an empty struct and use that in the typedef for a handle, like this texture handle example:

[code lang="cpp"]
struct tagTexture  {  };
typedef Handle <tagTexture> HTexture;
[/code]

Now we need a handle manager that is responsible for acquiring, dereferencing, and releasing objects (via handles) for a higher-level owner.

The HandleMgr Class

The HandleMgr class is a parameterized type composed of three main elements: a data store, a magic number store, and a free list (this class appears in Listing 2). The data store is simply a vector (or any other randomly accessible collection) of objects of type DATA. The DATA type, the first type parameter for HandleMgr, should be a very simple class that contains context information about the resource that it controls. For example, in a HandleMgr that manages files, the DATA type would probably just have the file handle and the name of the file:

[code lang="cpp"]
struct FileEntry
{
std::string m_FileName;
HANDLE m_FileHandle; // OS file handle
};

struct tagFile { };
typedef Handle <tagFile> HFile;
typedef HandleMgr <FileEntry, HFile> FileHandleMgr;
[/code]

This simple handle manager will maintain a set of context objects that correspond to all the open files that it knows about. The FileHandleMgr class will probably not be used directly by clients, but will instead be owned by another class (call it FileMgr) that handles the abstraction and knows about the problem domain (that is, what DATA is supposed to represent). This class might look something like this:

[code lang="cpp"]
class FileMgr
{
FileHandleMgr m_Mgr;

public:
HFile OpenFile ( const char* name );
bool ReadFile ( HFile file, void* out, size_t bytes );
bool CloseFile( HFile file );

// ...
};
[/code]

Upon calling any of these methods, FileMgr will ask its m_Mgr to dereference the handle to get at the actual FileEntry object. After verifying that the dereference succeeded (it will fail on an invalid handle), it will then perform the operation.

For our HandleMgr class, each handle will reference exactly one element within the object store, plus its corresponding element in the magic number store. Dereferencing the handles to get at the actual FileEntry object is as simple as using the m_Index component of the handle as an index into the object store (a very fast operation).

When dereferencing the handle, the code will also check the m_Magic component against the same index in the magic number store to make sure the handle is valid. As handles are freed and reacquired, corresponding entries in the magic number store are updated with the new handle magic numbers. This nearly guarantees that “dangling” handles on released objects won’t refer to unexpected objects when the slots are filled by a later handle acquisition, but will instead simply fail to work and return an error code. Obviously, the magic number store will always have the same number of elements as the object store.

As objects are released, the handle manager will add the indexes of the slots they occupy to the free list. This saves it the trouble of needing to search through the object store to find an open slot, which results in a tasty O(n) complexity for new handle acquisition. It’s important to note that the DATA type is not your typical C++ class. It shouldn’t have constructors and destructors that do anything important, such as acquire and release local resources. Objects contained within the object store are constructed, destroyed, and copied as the vector class sees fit. Note that the std::string used in the sample FileEntry is “simple” enough for our needs – it’s reference-counted, which minimizes the impact of its constructors and destructors and makes it nearly free for vector to copy.

When asked to acquire an object from the store, we’ll likely end up reusing an object that has already been constructed but is no longer in use, as indicated by its entry in the free list. It will need its members reinitialized before it can be used, because it won’t have had the constructor call to set it up. When an object is freed from the store, it will not get destroyed, but will instead have its index added to the free list, and as such will need its resources manually freed. These minor limitations arise from the fact that we’re embedding our DATA type directly in the vector, rather than using pointers and creating and destroying the objects with new and delete for each handle acquisition and release. The major advantage here is speed, in that the objects don’t have to be completely brought up and shut down each time. To make things more convenient, the initialize/shutdown code can be moved into member functions for easy callback by the HandleMgr owner.

The amount of handle validation necessary may depend on the application, and could even be chosen through an additional template parameter for HandleMgr. For example, the test for an invalid handle may be found to be unnecessary and could be removed (though the debug assertion should always remain). For a more robust system where error handling is important, the code could, upon detecting an invalid handle, set an error condition, and then abort the function call.

Sample Usage

In Listing 3 I provide a sample texture manager class. This class allows clients to ask it for textures by name and will construct them on demand. It automatically unloads the textures on deletion, and provides a set of query functions to use the textures. The textures are indexed by name for speedy lookup to make sure that the same texture is not added to the store twice. It would be a simple exercise to add reference counting to this example to make it safer, replacing DeleteTexture() with ReleaseTexture().

For another (larger) sample of file handle usage, see the sample code for my GDC 2000 talk, It’s Still Loading? Designing an Efficient File System.

Notes

The HandleMgr class is very simple and is meant to illustrate some basic concepts, but it can be expanded in a number of ways, either with the existing HandleMgr or separate classes:

  • Create a HandleMgr that will work better with larger DATA objects, holding them indirectly through pointers. It would also allow hiding of the data structure to clients.
  • Add automatic reference counting as standard functionality, rather than leaving it to be the responsibility of the owner of the HandleMgr.
  • Add support for constant-time iteration over the potentially sparse object store by embedding a linked list within its elements. Use STL style iterator naming and operation for consistency.
  • Many databases, such as a font manager or texture manager, will likely require indexes to access objects by name to retrieve handles. Build this in as a standard feature or as a separate (derivative) class.
  • The HandleMgr system is especially effective when combined with the Singleton pattern (see “An Automatic Singleton Utility” elsewhere in this book). Many of a game’s databases are naturally singletons.
  • Take the Singleton pattern a little further, and make the TAG type of Handle actually be the type that it corresponds to within the HandleMgr. Then the Handle could have an operator -> that would dereference itself into a TAG by directly accessing the Singleton that manages it.
  • Save game functionality should be fairly easy to add, but it is necessarily specific to your game’s architecture. The handles can be saved out directly – just make sure that the HandleMgr stores the indexes for its objects along with the object data, and on restore, all handles will remain valid.

Listing 1 – Handle

[code lang="cpp"]
#include <cassert>

template <typename TAG>
class Handle
{
union
{
enum
{
// sizes to use for bit fields
MAX_BITS_INDEX = 16,
MAX_BITS_MAGIC = 16,

// sizes to compare against for asserting dereferences
MAX_INDEX = ( 1 << MAX_BITS_INDEX) - 1,
MAX_MAGIC = ( 1 << MAX_BITS_MAGIC) - 1,
};

struct
{
unsigned m_Index : MAX_BITS_INDEX; // index into resource array
unsigned m_Magic : MAX_BITS_MAGIC; // magic number to check
};
unsigned int m_Handle;
};

public:

// Lifetime.

Handle( void ) : m_Handle( 0 ) { }

void Init( unsigned int index );

// Query.

unsigned int GetIndex ( void ) const { return ( m_Index ); }
unsigned int GetMagic ( void ) const { return ( m_Magic ); }
unsigned int GetHandle( void ) const { return ( m_Handle ); }
bool IsNull ( void ) const { return ( !m_Handle ); }

operator unsigned int ( void ) const { return ( m_Handle ); }
};

template <typename TAG>
void Handle <TAG> :: Init( unsigned int index )
{
assert( IsNull() ); // don't allow reassignment
assert( index <= MAX_INDEX ); // verify range

static unsigned int s_AutoMagic = 0;
if ( ++s_AutoMagic > MAX_MAGIC )
{
s_AutoMagic = 1; // 0 is used for "null handle"
}

m_Index = index;
m_Magic = s_AutoMagic;
}

template <typename TAG>
inline bool operator != ( Handle <TAG> l, Handle <TAG> r )
{ return ( l.GetHandle() != r.GetHandle() ); }

template <typename TAG>
inline bool operator == ( Handle <TAG> l, Handle <TAG> r )
{ return ( l.GetHandle() == r.GetHandle() ); }
[/code]

Listing 2 – HandleMgr

[code lang="cpp"]
#include <vector>
#include <cassert>

template <typename DATA, typename HANDLE>
class HandleMgr
{
private:
// private types
typedef std::vector <DATA> UserVec;
typedef std::vector <unsigned int> MagicVec;
typedef std::vector <unsigned int> FreeVec;

// private data
UserVec m_UserData; // data we're going to get to
MagicVec m_MagicNumbers; // corresponding magic numbers
FreeVec m_FreeSlots; // keeps track of free slots in the db

public:

// Lifetime.

HandleMgr( void ) { }
~HandleMgr( void ) { }

// Handle methods.

// acquisition
DATA* Acquire( HANDLE& handle );
void Release( HANDLE handle );

// dereferencing
DATA* Dereference( HANDLE handle );
const DATA* Dereference( HANDLE handle ) const;

// other query
unsigned int GetUsedHandleCount( void ) const
{ return ( m_MagicNumbers.size() - m_FreeSlots.size() ); }
bool HasUsedHandles( void ) const
{ return ( !!GetUsedHandleCount() ); }
};

template <typename DATA, typename HANDLE>
DATA* HandleMgr <DATA, HANDLE> :: Acquire( HANDLE& handle )
{
// if free list is empty, add a new one otherwise use first one found

unsigned int index;
if ( m_FreeSlots.empty() )
{
index = m_MagicNumbers.size();
handle.Init( index );
m_UserData.push_back( DATA() );
m_MagicNumbers.push_back( handle.GetMagic() );
}
else
{
index = m_FreeSlots.back();
handle.Init( index );
m_FreeSlots.pop_back();
m_MagicNumbers[ index ] = handle.GetMagic();
}
return ( m_UserData.begin() + index );
}

template <typename DATA, typename HANDLE>
void HandleMgr <DATA, HANDLE> :: Release( HANDLE handle )
{
// which one?
unsigned int index = handle.GetIndex();

// make sure it's valid
assert( index < m_UserData.size() );
assert( m_MagicNumbers[ index ] == handle.GetMagic() );

// ok remove it - tag as unused and add to free list
m_MagicNumbers[ index ] = 0;
m_FreeSlots.push_back( index );
}

template <typename DATA, typename HANDLE>
inline DATA* HandleMgr <DATA, HANDLE>
:: Dereference( HANDLE handle )
{
if ( handle.IsNull() ) return ( 0 );

// check handle validity - $ this check can be removed for speed
// if you can assume all handle references are always valid.
unsigned int index = handle.GetIndex();
if ( ( index >= m_UserData.size() )
|| ( m_MagicNumbers[ index ] != handle.GetMagic() ) )
{
// no good! invalid handle == client programming error
assert( 0 );
return ( 0 );
}

return ( m_UserData.begin() + index );
}

template <typename DATA, typename HANDLE>
inline const DATA* HandleMgr <DATA, HANDLE>
:: Dereference( HANDLE handle ) const
{
// this lazy cast is ok - non-const version does not modify anything
typedef HandleMgr <DATA, HANDLE> ThisType;
return ( const_cast <ThisType*> ( this )->Dereference( handle ) );
}
[/code]

Listing 3 – Sample Managed Class

[code lang="cpp"]
#include <vector>
#include <map>
#include <cassert>

// ... [ platform-specific surface handle type here ]
typedef LPDIRECTDRAWSURFACE7 OsHandle;

struct tagTexture { };
typedef Handle <tagTexture> HTexture;

class TextureMgr
{

// Texture object data and db.

struct Texture
{
typedef std::vector <OsHandle> HandleVec;

std::string m_Name; // for reconstruction
unsigned int m_Width; // mip 0 width
unsigned int m_Height; // mip 1 width
HandleVec m_Handles; // handles to mip surfaces

OsHandle GetOsHandle( unsigned int mip ) const
{
assert( mip < m_Handles.size() );
return ( m_Handles[ mip ] );
}

bool Load ( const std::string& name );
void Unload( void );
};

typedef HandleMgr <Texture, HTexture> HTextureMgr;

// Index by name into db.

// case-insensitive string comparison predicate
struct istring_less
{
bool operator () ( const std::string& l, const std::string& r ) const
{ return ( ::stricmp( l.c_str(), r.c_str() ) < 0 ); }
};

typedef std::map <std::string, HTexture, istring_less > NameIndex;
typedef std::pair <NameIndex::iterator, bool> NameIndexInsertRc;

// Private data.

HTextureMgr m_Textures;
NameIndex m_NameIndex;

public:

// Lifetime.

TextureMgr( void ) { /* ... */ }
~TextureMgr( void );

// Texture management.

HTexture GetTexture ( const char* name );
void DeleteTexture( HTexture htex );

// Texture query.

const std::string& GetName( HTexture htex ) const
{ return ( m_Textures.Dereference( htex )->m_Name ); }
int GetWidth( HTexture htex ) const
{ return ( m_Textures.Dereference( htex )->m_Width ); }
int GetHeight( HTexture htex ) const
{ return ( m_Textures.Dereference( htex )->m_Height ); }
OsHandle GetTexture( HTexture htex, unsigned int mip = 0 ) const
{ return ( m_Textures.Dereference( htex )->GetOsHandle( mip ) ); }
};

TextureMgr :: ~TextureMgr( void )
{
// release all our remaining textures before we go
NameIndex::iterator i, begin = m_NameIndex.begin(), end = m_NameIndex.end();
for ( i = begin ; i != end ; ++i )
{
m_Textures.Dereference( i->second )->Unload();
}
}

HTexture TextureMgr :: GetTexture( const char* name )
{
// insert/find
NameIndexInsertRc rc =
m_NameIndex.insert( std::make_pair( name, HTexture() ) );
if ( rc.second )
{
// this is a new insertion
Texture* tex = m_Textures.Acquire( rc.first->second );
if ( !tex->Load( rc.first->first ) )
{
DeleteTexture( rc.first->second );
rc.first->second = HTexture();
}
}
return ( rc.first->second );
}

void TextureMgr :: DeleteTexture( HTexture htex )
{
Texture* tex = m_Textures.Dereference( htex );
if ( tex != 0 )
{
// delete from index
m_NameIndex.erase( m_NameIndex.find( tex->m_Name ) );

// delete from db
tex->Unload();
m_Textures.Release( htex );
}
}

bool TextureMgr::Texture :: Load( const std::string& name )
{
m_Name = name;
// ... [ load texture from file system, return false on failure ]
return ( true /* or false on error */ );
}

void TextureMgr::Texture :: Unload( void )
{
m_Name.erase();
// ... [ free up mip surfaces ]
m_Handles.clear();
}
[/code]

January 25th, 2010 at 11:31 pm

Posted in

6 Responses to 'Gem: A Generic Handle-Based Resource Manager'

Subscribe to comments with RSS or TrackBack to 'Gem: A Generic Handle-Based Resource Manager'.

  1. Scott,
    I am working on my first game, finally nailed the idea for the game play. I am working on the subsystems now and came across your article in Game Programming Gems 1 titled “A Generic Handle-Base Resource Manager”. I was very intrigued by the article and copied the 3 files given into my project. I went through the code and while I understand most of it, I am still a bit foggy on templates and typenames. I added my own FileMgr based on the handle and HandleMgr headers and the Texture Manager implementation you provided. I am getting a compile error as it seems the HandleMgr can’t return from Vector (std::_Vector_iterator) to TextureMgr::Texture *. The code for the HandleMgr is given below. Please could you take a look and see how I can resolve this issue.

    template
    class HandleMgr
    {
    private:
    // private types
    typedef std::vector UserVec;
    typedef std::vector MagicVec;
    typedef std::vector FreeVec;

    // private data
    UserVec m_UserData; // data we’re going to get to
    MagicVec m_MagicNumbers; // corresponding magic numbers
    FreeVec m_FreeSlots; // keeps track of free slots in the db

    public:
    // Lifetime.
    HandleMgr( void ) { }
    ~HandleMgr( void ) { }

    // Handle methods.
    // acquisition
    DATA* Acquire( HANDLE& handle );
    void Release( HANDLE handle );

    // dereferencing
    DATA* Dereference( HANDLE handle );
    const DATA* Dereference( HANDLE handle ) const;

    // other query
    unsigned int GetUsedHandleCount( void ) const
    { return ( m_MagicNumbers.size() – m_FreeSlots.size() ); }
    bool HasUsedHandles( void ) const
    { return ( !!GetUsedHandleCount() ); }
    };

    template
    DATA* HandleMgr :: Acquire( HANDLE& handle )
    {
    // if free list is empty, add a new one otherwise use first one found

    unsigned int index;
    if ( m_FreeSlots.empty() )
    {
    index = m_MagicNumbers.size();
    handle.Init( index );
    m_UserData.push_back( DATA() );
    m_MagicNumbers.push_back( handle.GetMagic() );
    }
    else
    {
    index = m_FreeSlots.back();
    handle.Init( index );
    m_FreeSlots.pop_back();
    m_MagicNumbers[ index ] = handle.GetMagic();
    }
    return ( m_UserData.begin() + index ); // error here (see below)
    }

    Compile ERROR Message:
    (error C2440: ‘return’ : cannot convert from ’std::_Vector_iterator’ to ‘TextureMgr::Texture *’) –> No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called while compiling class template member function ‘TextureMgr::Texture *HandleMgr::Acquire(HANDLE &)’

    Any help is greatly appreciated and please let me know if you need anything else.

    Chris Clark

    10 Apr 11 at 8:25 pm

  2. This article was written under a version of Visual C++ that used an STL where iterators on vectors were simply pointers. In more modern versions of the STL, like the one you have, iterators are usually wrapped up into classes. The error you’re getting is because the iterator does not auto convert to the pointer again.

    To fix, just take the address of the dereferenced iterator. Something like this:

    [code]
    return &*(m_UserData.begin() + index);
    [/code]

    Scott

    12 Apr 11 at 8:48 pm

  3. Thank you, I will give it a try and let you know how it goes.

    Chris Clark

    12 Apr 11 at 8:53 pm

  4. It works! Thank you very much for your help!

    Chris Clark

    12 Apr 11 at 8:58 pm

  5. Scott,

    Just wanted to say thanks for an excellent set of articles on your blog here. It is one thing to try and learn these things on your own, but nothing can replace learning by reading great code.

    It is refreshing to see such a wealth of source code associated with an article! Was quite a fun exercise to refactor your design (including the full package builder/manager) into my current engine using boost::paths and memory mapped files!

    Thanks again mate,
    John

    John

    26 Sep 11 at 1:02 am

  6. I’ve read your articles about component-based entity system, very powerful and flexible approach. This article is great, too. Thank you.

    Topright

    9 Jan 13 at 5:32 am

Leave a Reply