Про ресурсы

После сопливого поста про сейвгеймы в Program Files просто обязан сделать что-то полезное (тем более, что занял им козырный нумер “100″). Например, написать рассказ про то, что должен делать resource manager.

Я уже неоднократно высказывал мысль о том, что как только вы в ресурс-менеджере пишите что-то вида enum { eTexture, eMesh, eSprite, … }; то ваш идеальный ресурс-менеджер идет лесом. Потому что он превращается в ресурс менеджер, к которому (достаточно искуственно) притянуты за уши куски других подсистем. Например, если мы корректируем формат меша, нам придется редактировать и подсистему управления геометрией, и ресурс-менеджер, что недопустимо.

Если мы выбросим enum из ресурс менеджера, то что останется в нем? Если кто-то думает, что исключительно fopen()/fread(), то он глубоко заблуждается.
Сначала вылезем на нужный уровень абстракции. В самом что ни на есть универсальном виде некий произвольный ресурс можно представить как следующий блок данных: {resource_type_id, resource_size, resource_data}. Здесь resource_type_id похож на наш пресловутый enum { eTexture, …}, но мы не забываем о том, что принципиально не хотим определять его. Соответственно, каждая подсистема должна определить себе этот resource_type_id (неважно, что это будет – строка, guid или что-то другое), задача ресурс менеджера заключается только в том, чтобы не допустить пересечения идентификаторов.

Второе размышление о менеджере. Ресурсы могут быть взаимосвязаны. Например, меш или спрайт ссылается на текстуры. В самом простейшем варианте можно возложить заботу о поиске и связывании на конкретные подсистемы. Например, все текстуры имеют имена и хранятся в текстурном словаре. Меш также хранит имена текстур и при своей загрузке ищет/грузит текстуры по именам. Но это “не наш метод” (С). Поэтому добавим в наш блок данных еще одно поле: {resource_type_id, resource_unique_id, resource_size, resource_data}. Поле resource_unique_id (возможно, в связке с resource_type_id) позволит ресурс менеджеру отыскивать нужные ресурсы в общем котле.

Далее. В случае “обычной загрузки” мы, как правило, прямо в процессе загрузки меша ищем и подгружаем нужные нам текстуры. С учетом абстракции data_source подобное может быть сложно реализуемо, поэтому есть два варианта решения:

1. Воспользовашись Бориными yield(), можно написать следующее:
[code lang="cpp"]MeshSystem::load(...)
{
...
ResourceSystem::RequestToLoadResource(resource_unique_id);
while (!ResourceSystem::IsResourceReady(resource_unique_id)) {
Yield();
}
...
}[/code]
2. Дадим эту информацию ресурс менеджеру еще до начала загрузки всего в память (на этапе подготовки ресурсов) и пусть он решает задачу: если ресурс {resource_unique_id1} требует себе {resource_unique_id2}, то к тому моменту, когда первый ресурс начинает загружаться в память, второй ресурс уже загружен и готов к использованию. Оба варианта приводят к организации ацикличного орграфа (меши могут ссылаться на одну и ту же текстуру, но кольцевые связи вгонят систему в ступор); второй вариант предпочтительнее тем, что ресурс-менеджер, заранее зная о взаимосвязях объектов, может проводить разные оптимизации и укладки данных.И еще одно размышление о менеджере. Изначально абстрагируемся от того, что ресурсы хранятся в виде файлов. Представим себе абстрактный источник данных, некоторое data_source, которое может быть как индивидуальными файлами, длинным файлом, zip-архивом или чем-то еще. Более того, data_source может иметь и не файловую структуру, например, поступать в игру по TCP/IP или через pipe.

Теперь со всем этим попробуем взлететь.

Первое. Заводим некоторое описание уровня, в котором находится список всех ресурсов (пространство, модельки, эффекты, навигационная карта AI и все прочее). По данной карте уровня мы можем построить список всех ресурсов, которые требуются для загрузки игрового уровня.
Второе. Список ресурсов, очевидно, может расширяться и укрупняться прямо в процессе экспорта (например, моделька потянула за собой текстуры).
Третье. В компонентах пишется специальный код (“Экспортер”), который подготовит данные в виде {resource_size, resource_data} или даже просто в виде записанного на диск файла.
Четвертое. В игре запускаем ресурс-менеджер на загрузку уровня. Менеджер поднимает данные из data_source и начинает загружать ресурсы в соответствии с глобальной картой. В варианте №1 загрузку можно вести с самого “тяжелого” ресурса, в варианте №2 – наоборот, сначала загружаются “легкие” ресурсы, а только потом те, кто на них завязан. По resource_type_id определдяется владелец ресурса и получает управление на загрузку своей информации.

“Экспортеры” и “Импортеры” конкретных ресурсов регистрируются в ресурс-менеджере через callback или наследуются от соответствующих интерфейсов.

Какие еще вкусности можно приклеить к системе:

1. Вместо простого {resource_size, resource_data} стоит разделить блок на нескольких кусков и типов, например: FIXED (грузиться в режиме memory mappped и остается в памяти до самого конца), DISCARD (грузится в память, после чего удаляется; такое может быть полезно, например, для PC текстур), SAVED (блок памяти грузится в память из сейвгейма и сохраняется в сейвгейм же). На первой загрузке уровня в качестве SAVED либо не используется ничего, либо используется “нулевой сейвгейм”.
2. Блоки в бинарном файле могут раскладываться по разным правилам (если ресурс менеджер знает про орграф). Например, провести топологическую сортировку или оптимизировать систему, разрезав длинный файл на несколько частей. В этом случае уже загруженные части поступают в обработку в “импортер”, а параллельно ресурс-менеджер догружает оставшиеся части (и если надо, раззиповывает их).
3. Разрезка может применяться и для организации стриминга. Засунем некоторые блоки в отдельный файл и будем поднимать его в память и удалять, а на его место грузить другой.
4. Ресурсы с одинаковым содержимым, к которым ведется доступ по readonly, могут быть прошарены вместе (автоматически!). При этом resource_unique_id остаются, но они ссылаются на один и тот же блок данных.

Тут еще можно много чего придумывать и рассказывать. Например, организацию компиляции ресурсов с добавлением новых ссылок (это примерно так же, как если бы из скомпилированного cpp получался бы не только obj, но и еще один cpp файл). Runtime подмену FIXED и SAVED данных даже незаметно для их владельцев. Способы организации unique id. Но я думаю, для затравки хватит. Главное, чтобы не произошло того, что ресурс менеджер узнал, с чем реально ему приходится работать. На правильном уровне абстракции и с соответствующими разумными ограничениями можно найти хорошее решение.

А еще оно все идеально ложится в борины елды, вот.

  • nsf

    Интересные заметки, недавно тоже работал над threaded ресурс менеджером (я новичок, для меня это в первый раз). Некоторые идеи пересекаются. Но у меня всё далеко не на профессиональном уровне :) Что там в 420 строк уместится.
    Например у меня тип представлен простой структурой:
    struct ResourceType {
    const char* name;
    ResourceData* (*loadDataBackground)(const ResourceLoadInfo*); // например: грузим текстуру с диска/архива/инета
    ResourceData* (*loadDataForeground)(const ResourceLoadInfo*); // например: грузим текстуру в API
    };
    Для текстуры будет примерно так: static const ResourceType TextureResourceType {“texture”, loadTextureBg, loadTextureFg};
    Конечно если можно обойтись без foreground процесса, можно его не использовать. В целом загрузка может осуществлятся в двух потоках и в две стадии.
    Основная идея в том, что ресурс менеджеру не обязательно знать, что за ресурсы он хранит и грузит. Вторая основная идея в том, что ресурс это не просто данные, а инфа о том, где их взять, т.е. список ресурсов загружается один раз, если проект простой или например может загружаться с каждым уровнем.
    Впрочем на этом весь ресурс менеджер у меня заканчивается. Всё хранится в хэшах, ресурсы поделены на группы, можно запрашивать загрузку или выгрузку групп ресурсов или загрузку/выгрузку отдельных ресурсов. Как мне кажется работает неплохо %) По крайней мере можно в небольшом проекте реализовать простенький loading screen. Ладно, в один пост не опишу всё что хотел, да и надо ли это кому? :)) Если вдруг надо, я на #gamedev @ IRCNET.RU

    Screw Yann L, screw Carmack, dDIMA! :) Пешы есчо

  • http://www.codygain.com neteraser

    Ах, Дима, не могу прекратить любить этот Vintage ! :)

  • Bandures

    Помойму ты старательно пытаешься убить главный бонус исходного варианта – простоту и элегантность.

  • dDIMA

    2 Bandures
    Все вкусности не отменяют главного – независимо от реализации различных вариантов склейки/разрезки/упаковки/… интерфейсы конкретных подсистем остаются на уровне событий Export, Load/Unload с фиксированным интерфейсом.

  • http://aruslan.livejournal.com/ aruslan

    > А еще оно все идеально ложится в борины елды, вот.

    Чисто чтобы люди не бегали по граблям уточню.
    СreateFile который инициирует лаги на DVD на yields не ложится.
    Впрочем, можно сильно заранее открыть все файлы или же иметь один файл на всё.

  • dDIMA

    > Чисто чтобы люди не бегали по граблям уточню.
    > СreateFile который инициирует лаги на DVD на yields не ложится.
    ReadFile(30 Mb) в yield тоже не попадает, ессно :).
    С учетом того, что накопитель как правило является внешним ресурсом с эксклюзивным доступом, такие вызовы должны ложиться запросами в отдельный тред, который будет разруливать чтения файлов, исключая параллельные запросы и минимизируя seek time. А еще можно FILE_FLAG_NO_BUFFERING.

  • http://aruslan.livejournal.com/ aruslan

    Именно так, дядя Дима.
    Я бы еще и сортировку запросов приделал на PC.
    Или даже и на консолях – благо есть функции всякие.
    Впрочем на консолях лучше ассерт приделать!

    Я же не педантизма для но счастья ради!

  • dDIMA

    > Я бы еще и сортировку запросов приделал на PC.
    Это вроде Петя хвасталсо, что такое в героях прикрутили. Точных цифр перформанса не помню, но помню, что впечатлился.

  • look4awhile

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

  • IronPeter

    Я не прикручивал в героях. В игре. Я анализировал лог файловых запросов и над ним изголялся теоретически. А в новом проекте Нивала таки прикрутил.