RAII для ресурсов, которые могут быть признаны недействительными

Я C++ и DirectX программист-любитель, поэтому большая часть знаний, которые у меня есть, взяты из старых книг по разработке игр, в которых дизайн кода предназначен только для того, чтобы что-то запустить и запустить в качестве демонстрации, оставляя мне множество соображений по дизайну даже для самая простая из программ. Во время разработки такой программы я недавно узнал о RAII, поэтому я решил попробовать этот шаблон проектирования, потому что, как я понял, объект должен быть пригодным для использования и действительным при создании, и это значительно упрощает способ использования объектов. программа. Раньше я использовал шаблон create() и destroy() в некоторых своих объектах, что приводило к большому количеству проверок в большинстве функций-членов.

В архитектуре программы у меня очень мало графических объектов, являющихся обертками вокруг DirectX ресурсов, один из которых является Texture объектом. Например, если я хочу визуализировать тайловую карту, у меня может быть Tile объектов, созданных с помощью указателей на AnimatedImage объектов, которые созданы с указателями на 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. Добавьте уровень косвенности, используя реестр всех графических объектов, который будет возвращать индекс для указателя Texture или указатель для указателя Texture, чтобы существующие объекты, которые зависят от графики, не нужно было уничтожать. Преимущества и недостатки такие же, как и выше, с дополнительным недостатком дополнительных накладных расходов из-за косвенности.

  3. Сохраните текущее состояние программы и отмотайте назад до тех пор, пока графические объекты не будут повторно получены, затем перестройте программу в том состоянии, в котором она была. Никакого реального преимущества я не могу придумать, но кажется наиболее RAII подходящим. Недостатком является сложность реализации этого для сценария, который не слишком распространен.

  4. Полностью отделите все визуальные представления объектов от их физических представлений. Преимущество состоит в том, что на самом деле создаются только те объекты, которые необходимо воссоздать, что может оставить остальную часть программы в допустимом состоянии. Недостатком является то, что физическим и визуальным объектам по-прежнему необходимо каким-то образом знать друг о друге, что может привести к некоторому раздуванию кода управления объектами.

  5. Прервать выполнение программы. Преимущество в том, что это настолько просто, насколько это возможно, и очень мало работы тратится на то, что не часто случается. Недостатком является то, что это будет неприятно для любого, кто использует программу.


person TheBIGbadJackal    schedule 08.03.2015    source источник
comment
Возможно, это связано с тем, что я не знаком с DirectX API, но я не вижу дилеммы. Можно ли перефразировать ваш вопрос с помощью кода? Не обязательно компилировать, конечно. Было бы достаточно чего-то, указывающего на ресурсы, их приобретение/освобождение/аннулирование/повторное получение и обработку исключений, концептуально требуемых.   -  person Pradhan    schedule 08.03.2015
comment
@Pradhan Я добавил код, который показывает основную идею. Мои графические объекты в программе являются обертками DirectX, оставляя минималистичный интерфейс рендеринга спрайтов: GraphicsDevice, Texture и SpriteBuffer. Исключения выбрасываются, если объект не удается построить.   -  person TheBIGbadJackal    schedule 08.03.2015


Ответы (1)


Первое решение

Хороший вопрос для конца 2000-х (по крайней мере, для десктопной графики). В 2015 году вам лучше забыть о DirectX 9 с потерянным устройством и использовать DirectX 11 (или даже грядущий DirectX 12).

Второе решение

Если вы все еще хотите придерживаться устаревшего API (или если вы в то же время используете что-то вроде OpenGL ES на мобильных устройствах, где потеря контекста является обычным явлением), есть способ, который работает очень хорошо (среди прочего). В общем, это твоя смесь.

Оснастите графические объекты методом recreate().

а также

Добавьте уровень косвенности, используя реестр всех графических объектов.

Вот:

  • рефакторинг вашего кода с помощью шаблона Factory: заставить пользователя выделять новые ресурсы с помощью функций (обтекание new, std::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;
    }
    
  • заставьте ресурсы помнить свои параметры (при необходимости их можно изменить во время выполнения, но их также следует сохранять. Для больших или дорогостоящих объектов параметров используйте Шаблон прокси)

    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
    }
    

Вы можете построить более сложную и интеллектуальную систему поверх этого.

Связанные примечания:

  • #P12#
    #P13#
  • #P14#
    #P15#
  • #P16#
    #P17#
  • Не переусердствовать. Большинство игр вообще не надоедает. Они обрабатывают потерянное устройство так же, как если бы пользователь сделал «Сохранить игру» -> «Выход» -> «Загрузить игру», соответствующим образом разрабатывая средства запуска.

  • Другой способ использования отдельно или в сочетании с фабрикой — ленивая инициализация: заставьте свой ресурс проверять оба если он действителен сам и устройство потеряно.

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

    Он добавляет некоторые накладные расходы при каждом доступе к ресурсу, но это безопасный и очень простой в реализации способ убедиться, что ваш ресурс работает, когда это необходимо.

person Ivan Aksamentov - Drop    schedule 08.03.2015
comment
Большое спасибо за эти предложения! Я рассмотрю некоторые из этих шаблонов. Тем не менее, я должен был быть более конкретным. Я использую DirectX 11, который, к счастью, не теряет устройство при переключении из полноэкранного и оконного режимов, как в предыдущих версиях, но, похоже, задокументировано, что в редких случаях устройство все еще может быть потеряно. Об этом будет сигнализировать код возврата DXGI_ERROR_DEVICE_REMOVED из функции Present(). - person TheBIGbadJackal; 08.03.2015
comment
Взгляните на обработку сценариев удаления устройства в Direct3D 11. В этой статье перечислены 4 случая, когда устройство D3D11 может выйти из строя (например, переустановить драйвер или физически удалить видеокарту). Я думаю, что все они крайне патологические и встречаются очень редко. Вам не следует беспокоиться, если пользователь удалит видеокарту во время игры (потому что это безумие). Просто выдает ошибку и просит перезапустить игру (и/или сменить пользователя) ;) - person Ivan Aksamentov - Drop; 08.03.2015
comment
Лучший способ проверить сценарий «устройство удалено» (что чаще всего происходит, когда видеодрайвер обновляется во время работы приложения) — использовать команду dxcap -forcetdr из командной строки Visual Studio с правами администратора во время работы приложения. - person Chuck Walbourn; 26.07.2016