RAII for resources that can be invalidated

2019-07-20 07:11发布

I'm a hobbyist C++ and DirectX programmer, so most of the knowledge I have is from old game development books in which the code designs are just to get something up and running as a demonstration, leaving me with a lot of design considerations for even the simplest of programs. During development of such a program, I recently learned of RAII, so I decided to give this design pattern a shot because from what I understood, an object should be usable and valid upon construction and this greatly simplifies the way objects can be used by the program. Previously, I had been using a create() & destroy() pattern in some of my objects which lead to a lot of validation checking in most member functions.

In the program's architecture, I have very few graphics objects that are wrappers around DirectX resources, one of them being a Texture object. If I want to render tile map for example, I could have Tile objects that are constructed with pointers to AnimatedImage objects which are constructed with pointers to Texture objects.

The problem with using DirectX is that there are times in which the graphics device becomes lost, such as a driver update for the video card during program execution. When these events happen, the existing graphics resources must be released and reacquired to continue rendering normally, including the destruction and reconstruction of the Texture objects. This makes the use of an RAII design pattern seem like it may not be the best choice. I would need recreate the Texture objects, recreate the AnimatedImage objects, and then recreate the Tile objects. It seems like an extreme hassle because some objects that are recreated will contain more than just image data.

So if we start out with some example code (not exact, but it serves its purpose):

// Construct graphics objects into some structure we will pass around.
graphics.pDevice = new GraphicsDevice(windowHandle, screenWitdth, screenHeight);
graphics.pTexture1 = new Texture(graphics.pDevice, width1, height1, pPixelData1);
graphics.pTexture2 = new Texture(graphics.pDevice, width2, height2, pPixelData2);
graphics.pSpriteBuffer = new SpriteBuffer(graphics.pDevice, maxSprites);

Elsewhere in the program that builds objects for a tile map:

// Construct some in-game animations.
images.pGrass = new AnimatedImage(graphics.pTexture1, coordinates1[4], duration1);
images.pWater = new AnimatedImage(graphics.pTexture2, coordinates2[4], duration2);

// Construct objects to display the animation and contain physical attributes.
thisMap.pMeadowTile = new Tile(images.pGrass, TILE_ATTRIBUTE_SOFT);
thisMap.pPondTile = new Tile(images.pWater, TILE_ATTRIBUTE_SWIMMABLE);

Then during the rendering routine:

while (gameState.isRunning())
{
    graphics.pSpriteBuffer->clear();
    thisMap.bufferSprites(graphics.pSpriteBuffer);
    graphics.pSpriteBuffer->draw();
    if (graphics.pDevice->present() == RESULT_COULD_NOT_COMPLETE)
    {
         // Uh oh! The device has been lost!
         // We need to release and recreate all graphics objects or we cannot render.
         // Let's destruct the sprite buffer, textures, and graphics device.
         // But wait, our animations were constructed with textures, the pointers are
         //   no longer valid and must be destructed.
         // Come to think of it, the tiles were created with animations, so they too 
         //   must be destructed, which is a hassle since their physical attributes
         //   really are unrelated to the graphics.
         // Oh no, what other objects have graphical dependencies must we consider?
    }
}

Is there some design concept I am missing here or is this one of those cases in which RAII works, but at a unnecessarily large cost if there is a lot of object to object dependency? Are there any known design patterns that are specifically suited for this scenario?

Here were some of the ways I have thought of approaching this:

  1. Equip the graphics objects with a recreate() method. The advantage is that any object that points to a Texture can retain that pointer without being destroyed. The disadvantage is that if the reacquisition fails, I would be left with a zombie object which is no better than the create() & destroy() pattern.

  2. Add a level of indirection using a registry of all graphics objects that will return an index to a Texture pointer or a pointer to a Texture pointer so that existing objects that rely on graphics don't need to be destroyed. Advantages and disadvantages are the same as above with the added disadvantage of additional overhead from the indirection.

  3. Store the current state of the program and unwind back until the graphics objects have been reacquired, then rebuild the program in the state it was in. No real advantage I can think of, but seems the most RAII appropriate. The disadvantage is the complexity in implementing this for a scenario that is not too common.

  4. Completely segregate all visual representations of objects from their physical representations. The advantage is that only the necessary objects to be recreated actually are, which can leave the rest of the program in a valid state. The disadvantage is that the physical and visual objects still need to know about each other in some way, which may lead to some bloated object management code.

  5. Abort program execution. The advantage is that this is as easy as it gets and very little work is spent for something that does not often happen. The disadvantage is that it would be frustrating to anyone using the program.

1条回答
走好不送
2楼-- · 2019-07-20 07:30

First solution

That's a good question for the late 2000ths (at least for desktop graphics). In 2015 you'd better forget DirectX 9 with it's device lost and do DirectX 11 (or even upcoming DirectX 12).

Second solution

If you still want to stick with deprecated API (or if you are using same time something like OpenGL ES on mobile devices, where context loss is a common event), there is a way that works pretty well (among others). Basically it is a mix of yours

Equip the graphics objects with a recreate() method

and

Add a level of indirection using a registry of all graphics objects

Here it is:

  • refactor your code with a Factory pattern: force user to allocate new resources with functions (wrap new, std::make_shared or whatever you use inside them)

    auto resource = device->createResource(param0, param1);

  • make factory remember resources somehow

    std::vector<IResourcePtr> resources;
    
    ResourcePtr Device::createResource(T param0, U param1)
    {
        auto resource = std::make_shared<Resource>(this, param0, param1);
        resources.push_back(resource);
        return resource;
    }
    
  • make resources remember it's parameters (they could be changed if necessary during runtime, but should be saved as well. For big or costly parameter objects use Proxy pattern)

    Resource::Resource(IDevice* device, T param0, U param1)
       : m_device(device)
       , m_param0(param0)
       , m_param1(param1)
    {
        create(); // private
    }
    
  • on device lost event, release all objects, than recreate them

    while (rendering)
    {
        device->fixIfLost();
        ...
    }
    
    void Device::fixIfLost()
    {
        if(isLost())
        {
            for(auto&& resource: resources)
                resource->reload();
        }
    }
    
    void Resource::reload()
    {
        release(); // private
        create();  // private
    }
    

You can build more complicated and intelligent system on top of that.

Related notes:

  • The disadvantage is that if the reacquisition fails, I would be left with a zombie object

    It is not specific to device lost event. Handle resource fails immediately, before giving up control to the user, same way that you do when you create resource first time and it fails (by throwing an exception (so user could handle it), or by using placeholder resource or by shutting down the application, or anything else -- you to decide)

  • Completely segregate all visual representations of objects from their physical representations.

    Must have. Not even a question to discuss, unless you are building a tetris. Use MVC or modern stuff like ECS. Never store your Mesh inside Player, and your ParticleEmitter inside Fireball. Never even make them know each other.

  • Store the current state of the program and unwind back until the graphics objects have been reacquired, then rebuild the program in the state it was in

    It's very useful. What you've described is a "save game" / "load game" mechanics. It could also be used to implement "replay" functionality and game-engine movies. Note (as to add to point 2), your save data will never include visual representations (Unless you want multi-gigabyte save files).

  • Don't overengineer. Most games don't bother at all. They handle device lost in the same way as if user did "Save game" -> "Exit" -> "Load game", designing startup facilities appropriately.

  • Another way to use by itself or in combination with factory is Lazy initialization: make your resource to verify both if it's valid itself and device lost.

    void Resource::apply()
    {
        if((!isValid()) || (!device->isValid()))
        {
    
        }
       // apply resource here
    }
    

    It adds some overhead each time resource is accessed, but it's a safe and very simple to implement way to ensure your resource is up when it's needed

查看更多
登录 后发表回答