D3D Resource Management

Here I'll try to describe how I do Direct3D resource management. I find this way quite convenient, however, don't take it too seriously. I don't think I'm the first using the technique described below; and of course there can be better ways.

The problems

Several problems arise in D3D resource management:

  • Resources (textures, buffers, etc.) depend on some particular D3D device. If you want to change the device, you have to recreate the resources.
  • Some types of resources can be "lost" (those living in default pool) in some situations (eg. fulscreen-windowed switch). These need to be recreated in these situations.
  • You may want to be able to reload some resources "on the fly" (alt-tab, edit a shader, alt-tab, reload - and you see immediate feedback).
It would be cool if all those problems could be solved in some easy and transparent to the application way (you know, "seamlessly integrates" :)).

The solution

In short: Proxy objects!

More detailed: a proxy object in this case is an object that just holds a pointer to the actual D3D resource object. Pseudocode:

struct CD3DTexture {
    IDirect3DTexture9* object;
};
Of course, you'll probably use templates/macros to define all those proxy classes (rough list: texture, cube texture, volume texture, surface, index buffer, vertex buffer, pixel shader, vertex shader, query, state block, vertex declaration; probably some others that proxy D3DX objects, like effect, mesh). Some of the proxies can contain convenience methods, etc.

Now, the main idea: the application (and most parts of engine/framework) don't mess with D3D objects directly - all they know is the proxy objects. Any proxy object is never recreated or changed. The application can just store a pointer to such CD3DTexture, and it will be valid at anytime, no matter if D3D device is changed, lost or some other bad things happen. The texture may even be reloaded - the proxy doesn't change. Of course, the objects that the proxies refer to may change.

Simple, isn't it?

The details (and the devil)

For whole this proxy-thing to work, some code needs to deal will all the details. For D3D, basically we have four situations:

  1. D3D device is created. Here, all the proxy objects that released their resources in 2nd situation must re-create them.
  2. D3D device is destroyed. All proxy objects must be released by now, either here or in 3rd situation.
  3. D3D device is lost.
  4. D3D device is reset after 3rd situation.
The first two situations usually affect D3D resources living not in the default pool; and the last two situations - default pool resources. Some types of resources, for example ID3DXEffect proxies, must deal with all 4 situations (create in 1, release in 2, and call corresponding methods in 3 and 4).

For this, I create an abstract interface like this (the details like abstract virtual destructors are omitted):

struct IDeviceResource {
    virtual void createResource() = 0;    // 1
    virtual void activateResource() = 0;  // 4
    virtual void passivateResource() = 0; // 3
    virtual void deleteResource() = 0;    // 2
};

Then, all resource management is centralized in some "resource managers" that do loading/creating resources on demand and implement this IDeviceResource interface. The resource managers are registered into some single "notifier" that calls IDeviceResource methods in response to the above four situations.

A simple resource manager can perform an operation like "here's the resource ID, give me the resource". Usually it contains a (resource ID->proxy) map, so it can return the same physical resource for the same ID (so only the first query actually loads it, the rest just look it up in the map). Resource ID usually somehow indicates the file name where the resource is.

A basic resource manager, for example, a texture manager (static textures loaded from storage, in managed pool), deals with this like:

// internal method: load and create basic, non-proxied texture
IDirect3DTexture9* CTextureBundle::loadTexture( resourceID ) { /* ... */ }

// public method: return the texture proxy given texture ID CD3DTexture* CTextureBundle::getResourceById( resourceID ) { if id->proxy map contains proxy for given resourceID { return it; } else { create new proxy P; P.object = loadTexture( resourceID ); insert (resourceID->P) into id->proxy map; } }

// IDeviceResource: re-load all textures into existing proxies void CTextureBundle::createResource() { for each element P in id->proxy map { P.proxy = loadTexture( P.resourceID ); } } // IDeviceResource: release all textures void CTextureBundle::deleteResource() { for each element P in id->proxy map { P.proxy.object->Release(); // release the D3D texture P.proxy.object = NULL; } } // IDeviceResource: nothing to do in these void CTextureBundle::activateResource() { } void CTextureBundle::passivateResource() { }

Making it more complex

Simple loaded resources are easy, you say - but what about resources that are created procedurally or are modified versions of the loaded resources?

Let's take textures created by the application (for rendertargets, procedurally computed textures and the like) for example. While in above case only simple "resource ID" is enough to create the resource at any time - this is more complex. A created texture must have some unique ID and all the information that enabled you to create it. I do it like this - have an abstract interface "texture creator":

struct ITextureCreator {
  virtual IDirect3DTexture9* createTexture() = 0;
};
Some realization of this interface, that creates rendertargets proportional to backbuffer size, might look like this:
class CScreenBasedTextureCreator : public ITextureCreator {
public:
  CScreenBasedTextureCreator( float widthFactor, float heightFactor,
      int levels, DWORD usage, D3DFORMAT format, D3DPOOL pool );
  virtual IDirect3DTexture9* createTexture();
private:
  // members
};

A resource manager for such textures stores a map that maps resource ID to the proxy and this ITextureCreator. The manager also has a method like "here's the ID and the creator object, please register this resource". So, whole texture manager might look like this:

void CSharedTextureBundle::registerTexture( resourceID, ITextureCreator* creator ) {
  create new proxy P;
  P.object = creator->createTexture();
  insert ( resourceID -> pair(P,creator) ) into the map;
}

// public method: return the texture proxy given texture ID CD3DTexture* CSharedTextureBundle::getResourceById( resourceID ) { // the resource map must contain the ID - else it’s error return proxy from the resourceID->(proxy,creator) map; }

The rest is pretty similar, but this time some textures may be in default pool (those are dealt with in activateResource/passivateResource).

Using this technique, application registers the rendertargets/procedural textures at start of day (or at some other time), and then can look them up or just store the pointers to the proxies (remember, the proxies never change :)).

The caveats

All above assumes that either the application uses proxies only at "right" time moments (eg. between the device loss and reset the application doesn't do anything, just the needed resources are released and recreated), or it manually checks for NULL objects in proxies and knows how to handle that.

Some resources may depend on the other resources. For example, meshes might want to store their vertex declarations; procedural meshes may want to be initialized from stored meshes, etc. So it's important to order the notifications to the resource managers carefully.

Some alternatives

The "resource ID" isn't necessarily a string. For example, my current vertex declaration manager takes a "vertex/stream format" as a resource ID, and just constructs D3D vertex declarations from that.

The managers for both loaded and created resources can be unified - afterall, loading from files is just another type of ITextureCreator in textures case. Some thought is needed on how to avoid "register resource" for loaded resources...

The hidden part

If you look closely, my Shaderey demo sourcecode contains quite much of whole this stuff (not the most recent version though). A really up-to-date version can be found at my demo/game engine dingus website.

If I've written complete nonsense here (or described your own patented technique :)), feel free to drop me a mail.