可以失效的资源的 RAII

RAII for resources that can be invalidated

我是一个业余爱好者 C++DirectX 程序员,所以我拥有的大部分知识都来自旧的游戏开发书籍,其中的代码设计只是为了得到一些东西 运行 作为演示,即使是最简单的程序也给我留下了很多设计考虑因素。在开发这样一个程序的过程中,我最近了解到 RAII,所以我决定尝试一下这个设计模式,因为据我了解,一个对象在构造时应该是可用和有效的,这大大简化了对象可以被程序使用。以前,我一直在我的一些对象中使​​用 create() & destroy() 模式,这导致在大多数成员函数中进行大量验证检查。

在程序的体系结构中,我有很少的图形对象是 DirectX 资源的包装器,其中之一是 Texture 对象。例如,如果我想渲染瓦片地图,我可以使用指向 AnimatedImage 对象的指针构建 Tile 个对象,这些对象使用指向 Texture 个对象的指针构建。

使用DirectX的问题是图形设备有时会丢失,例如在程序执行期间更新视频卡的驱动程序。当这些事件发生时,必须释放并重新获取现有的图形资源才能继续正常渲染,包括 Texture 对象的销毁和重建。这使得 RAII 设计模式的使用看起来可能不是最佳选择。我需要重新创建 Texture 个对象,重新创建 AnimatedImage 个对象,然后重新创建 Tile 个对象。这似乎非常麻烦,因为重新创建的某些对象将包含的不仅仅是图像数据。

因此,如果我们从一些示例代码开始(不准确,但可以达到目的):

// 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);

在为瓦片地图构建对象的程序中的其他地方:

// 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);

然后在渲染过程中:

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?
    }
}

我是否在这里遗漏了一些设计概念,或者这是 RAII 工作的情况之一,但如果存在大量对象到对象的依赖性,则成本过高?是否有任何已知的设计模式专门适用于这种情况?

以下是我想到的一些方法:

  1. 为图形对象配备recreate()方法。优点是任何指向纹理的对象都可以保留该指针而不会被销毁。缺点是如果重新获取失败,我会留下一个僵尸对象,它并不比 create() & destroy() 模式好。

  2. 使用所有图形对象的注册表添加间接级别,这些对象将 return 指向 Texture 指针或指向 Texture 指针的索引这样就不需要销毁依赖图形的现有对象。优点和缺点与上面相同,但增加了间接开销的缺点。

  3. 存储程序的当前状态并回退直到图形对象被重新获取,然后以它所在的状态重建程序。我想不出真正的优势,但似乎最 RAII 合适。缺点是在不太常见的情况下实现它的复杂性。

  4. 将对象的所有视觉表示与其物理表示完全分离。优点是实际上只有需要重新创建的对象是有效的,这可以使程序的其余部分保持有效状态。缺点是物理和视觉对象仍然需要以某种方式相互了解,这可能会导致一些臃肿的对象管理代码。

  5. 中止程序执行。优点是这很容易,并且很少为不经常发生的事情花费很少的工作。缺点是使用该程序的任何人都会感到沮丧。

第一个解决方案

对于 2000 年代后期(至少对于桌面图形而言),这是一个很好的问题。在 2015 年,你最好忘记 DirectX 9,因为它的设备丢失了,转而使用 DirectX 11(甚至即将推出的 DirectX 12)。

第二种解决方案

如果您仍然想坚持使用已弃用的 API(或者如果您同时在移动设备上使用类似 OpenGL ES 的东西,其中上下文丢失是常见事件),有一种方法非常有效好吧(等等)。基本上都是你的混搭

Equip the graphics objects with a recreate() method

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

这里是:

  • 使用 Factory pattern 重构您的代码:强制用户使用函数分配新资源(包装 newstd::make_shared 或您在其中使用的任何内容)

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

  • 让工厂以某种方式记住资源

    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;
    }
    
  • 让资源记住它的参数(如果需要,可以在运行时更改它们,但也应该保存。对于大的或昂贵的参数对象,使用 Proxy pattern

    Resource::Resource(IDevice* device, T param0, U param1)
       : m_device(device)
       , m_param0(param0)
       , m_param1(param1)
    {
        create(); // private
    }
    
  • 在设备丢失事件中,释放所有对象,然后重新创建它们

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

您可以在此基础上构建更复杂、更智能的系统。

相关说明:

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

    它不是特定于设备丢失事件。在放弃对用户的控制权之前立即处理资源失败,这与您第一次创建资源并失败时所做的方式相同(通过抛出异常(以便用户可以处理它),或者通过使用占位符资源或通过关闭应用程序,或其他任何东西——你来决定)

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

    必须有。甚至不是要讨论的问题,除非你正在构建俄罗斯方块。使用 MVC or modern stuff like ECS。切勿将 Mesh 存储在 Player 中,将 ParticleEmitter 存储在 Fireball 中。永远不要让他们互相认识。

  • 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

    非常有用。您所描述的是 "save game" / "load game" 机制。它还可以用于实现 "replay" 功能和游戏引擎影片。请注意(添加到第 2 点),您的保存数据将永远不会包含视觉表示(除非您想要数 GB 的保存文件)。

  • 不要overengineer。大多数游戏根本不会打扰。他们处理设备丢失的方式与用户处理设备丢失的方式相同 "Save game" -> "Exit" -> "Load game",适当设计启动设施。

  • 另一种单独使用或与工厂结合使用的方法是Lazy initialization:让你的资源验证它本身是否有效以及设备是否丢失。

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

    每次访问资源时都会增加一些开销,但它是一种安全且实施起来非常简单的方法,可确保您的资源在需要时启动