Files
fparkan/docs/tomes/02-architecture.md
Valentin Popov 78fc5f1deb
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Test / Lint (push) Failing after 1m7s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
docs: rewrite MkDocs documentation
2026-06-22 01:58:51 +04:00

26 KiB
Raw Permalink Blame History

II. Запуск, архитектура и игровой цикл

Этот том описывает путь от запуска iron_3d.exe до устойчивого кадра: загрузку iron3d.dll, создание shell/game objects, поднятие платформенных сервисов, запуск World3D, расчёт simulation step, безопасное удаление объектов, рендер и завершение программы.

Главная особенность Iron3D -- это не один монолитный engine object, а связка небольшого Win32 bootstrap и набора DLL, которые обмениваются фабриками, singleton-интерфейсами и C++ vtable. Совместимая реализация может изменить физическое деление на библиотеки, но не может произвольно менять порядок инициализации, object identity, правила владения, fallback ресурсов и порядок событий.

iron_3d.exe
  -> iron3d.dll
     -> services.dll
     -> World3D.dll
     -> Terrain.dll
     -> Ngi32.dll
     -> AniMesh.dll / ArealMap.dll / Effect.dll
     -> ai.dll / Behavior.dll / Wizard.dll
     -> Control.dll / MisLoad.dll / Net.dll / Joystick.dll

Карта модулей

Во внешней архитектуре обнаружено пятнадцать DLL. Экспортов сравнительно мало: они обычно создают объект, возвращают singleton или дают доступ к уже поднятой подсистеме. Основная работа выполняется через C++-интерфейсы, поэтому порядок виртуальных слотов является частью ABI, особенно для compatibility shim эпохи MSVC6.

iron_3d.exe
    |
    v
iron3d.dll  -- композиция игры, shell и главный цикл
    |
    +-- services.dll -- доступ к display, GUI, ресурсам, звуку, таймеру и сети
    +-- World3D.dll  -- объекты, очередь, время, камера и кадр
    +-- Terrain.dll  -- ландшафт, свет, атмосфера и визуальный слой мира
    +-- ai.dll / Behavior.dll / Wizard.dll
    +-- Control.dll / Effect.dll / MisLoad.dll
    +-- Net.dll / Joystick.dll
    +-- Ngi32.dll    -- ресурсы, графика, звук, математика и CPU dispatch

Циклы импортов между DLL ожидаемы. Terrain создаёт визуальные объекты и обращается к World3D, а World3D получает world-interface из Terrain. Это не значит, что обе библиотеки совместно владеют всем состоянием. Реальные границы задаются интерфейсами, refcount, очередью объектов и порядком shutdown.

Практичная новая структура может быть внутренним набором модулей platform, resources, world, mission, terrain, render, animation, effects, behavior, physics, audio и network. Важно сохранить не DLL-границы, а контракты: имена ресурсов, порядок поиска, fallback-ветки, object ID, момент создания mirror objects, численное поведение и последовательность событий.

Роли модулей

iron3d.dll создаёт shell и game objects, читает iron_3d.ini, поднимает display, sound, CD-audio, network и настройки World3D, загружает миссионные и UI-конфигурации, содержит message pump и вызывает расчёт/рендер игры.

services.dll работает как service locator. Через него запрашиваются display, GUI, network manager, resource manager, sound server и timer. Этот слой отделяет высокоуровневую игру от деталей создания устройств.

World3D.dll -- центральный runtime: очередь объектов, идентификаторы, события, отложенное удаление, game time, pause, manual input, камера, material/texture/lightmap managers, сетевые mirrors, расчёт и 3D-проход.

Terrain.dll отвечает не только за землю. В его область входят ландшафт, здания, визуальный слой мира, камера, shade/state layer, primitive buffers, сортировочные слои, источники света, тени, microtextures, атмосфера, дождь, молнии, солнце и flares.

Ngi32.dll содержит низкоуровневые сервисы: DirectDraw/Direct3D-era renderer, DirectSound, readers NRes/RsLi, память, часы, математику, пересечения, определение CPU и таблицу быстрых процедур g_FastProc.

Предметные DLL закрывают отдельные области. AniMesh.dll загружает модели и агентов. ArealMap.dll строит spatial graph и маршруты. Behavior.dll реализует поведение юнитов. ai.dll содержит стратегический AI и миссионные сценарии. Wizard.dll корректирует локальное движение. Control.dll обслуживает физическую модель и столкновения. Effect.dll создаёт runtime-FX. MisLoad.dll читает миссионные данные. Net.dll инкапсулирует DirectPlay. Joystick.dll работает через DirectInput.

Поток данных

Миссия не создаёт готовый кадр напрямую. Данные проходят через несколько уровней: описание объекта, прототипы, ресурсы, runtime-object, контроллеры, simulation state, render items и только затем платформенный renderer.

mission data
  -> object identity and properties
  -> prototype registry
  -> model/material/texture/effect resources
  -> World3D object + domain controllers
  -> simulation state
  -> visible render items
  -> Ngi32 render interface
  -> DirectX-era device

Этот поток объясняет, почему нельзя объединять физический архив, metadata entry, декодированный payload и готовый runtime-кэш. У каждого уровня свой срок жизни, собственный refcount и собственные ошибки. Детали ресурсного конвейера описаны в Томе III, а сборка мира из миссии -- в Томе IV.

Bootstrap

iron_3d.exe -- небольшой PE32/x86 bootstrap размером 36 864 байта. Основная игровая логика находится в iron3d.dll. Исполняемый файл создаёт Win32-процесс, подготавливает окружение, загружает библиотеку и получает восемь публичных точек входа:

createShell       deleteShell
createGame        deleteGame
createSubsystems  deleteSubsystems
getIGame          getIShell

Эти функции образуют внешнюю границу игры. createShell создаёт оболочку интерфейса и меню, createGame -- объект игровой логики, createSubsystems -- аппаратные и runtime-сервисы. Getter-функции возвращают уже созданные объекты.

Запуск удобно читать как конечный автомат:

PROCESS_CREATED
  -> LIBRARY_READY
  -> ENTRYPOINTS_READY
  -> SHELL_CREATED
  -> GAME_CREATED
  -> SUBSYSTEMS_READY
  -> MAIN_LOOP
  -> SUBSYSTEMS_CLOSED
  -> GAME_DELETED
  -> SHELL_DELETED

Каждый переход имеет обратное действие. Если display, sound или другой обязательный сервис не создан, главный цикл не начинается, но уже созданные объекты освобождаются в обратном порядке. Новая оболочка запуска должна работать из каталога оригинальной установки, сохранять смысл относительных путей, создавать окно до графической подсистемы и закрывать частично поднятые сервисы без предположения, что init дошёл до конца.

Bootstrap обеих полных частей побайтно одинаков, хотя файл второй части может иметь другое имя:

size       36 864
entry RVA  0x147E
SHA-256    f476af85c034a4b4f34f49d0806e4dff397b5da0ee26d382a7674231144979f7

Следовательно, различия полных частей начинаются после передачи управления DLL и игровым данным. Адреса executable демоверсии относятся к другой binary profile и не должны переноситься на полные версии без проверки hash.

Инициализация подсистем

Iron3D разделяет создание высокоуровневых объектов и создание подсистем. createShell конструирует оболочку пользовательского интерфейса, createGame создаёт объект игры, а createSubsystems связывает их с display, sound, network и World3D.

Высокоуровневая последовательность выглядит так:

прочитать iron_3d.ini
  -> получить display service
  -> создать окно и графическое устройство
  -> проверить доступность 3D-драйвера
  -> выбрать CURRENT_D3DCARD
  -> получить sound service и настроить громкость
  -> создать network instance и передать application GUID
  -> создать World3D game settings

Ошибка отсутствующего 3D-устройства обрабатывается отдельно от ошибок ресурсов: это разные стадии запуска. Конфигурация влияет не только на разрешение. В runtime попадают графическая карта, громкость эффектов, CD-audio, режим CD-sound, сетевое приложение и World3D settings. Application GUID сетевой подсистемы:

{3C1D1F01-A870-11D1-8400-000021B14415}

Один и тот же GUID передаётся сетевому объекту и service layer. Если он разойдётся, экземпляры игры станут логически разными приложениями, даже при исправном транспорте.

stdInitGame

После платформенных сервисов World3D создаёт внутренний runtime:

  1. Создаёт глобальную очередь объектов.
  2. Сохраняет window handle и режим игры.
  3. При нужном режиме ограничивает курсор областью окна.
  4. Получает или создаёт 3D sound object.
  5. Загружает реестр адресов компонентов из Comp.ini.
  6. Получает или создаёт 3D renderer.
  7. Читает профиль возможностей renderer.
  8. Загружает component type 6.
  9. Для multiplayer создаёт NetWatcher.
  10. Получает world-interface из Terrain.
  11. Устанавливает исходные параметры света и тумана.

Порядок важен. World objects не должны появляться до queue, ресурсы рендера -- до renderer, сетевые mirror objects -- до NetWatcher. В новой реализации у каждого этапа должен быть явный признак успешного создания, чтобы shutdown мог безопасно разобрать неполный init.

Завершение

Shutdown идёт в обратном направлении: прекращаются игровые расчёты и сетевые наблюдатели, разбираются отложенные операции, освобождаются world objects и менеджеры, затем renderer и sound, затем game settings и platform services. Ограничение курсора снимается, глобальные ссылки очищаются.

Полезный протокол завершения:

  1. Запретить новые события и новые объекты.
  2. Дождаться выхода из calculation/render traversal.
  3. Разобрать очередь deferred operations.
  4. Отсоединить объекты от очереди, контроллеров и менеджеров.
  5. Освободить managers и singletons после их consumers.
  6. Закрыть устройства и платформенные сервисы.

Такой порядок защищает от dangling-ссылок между World3D, Terrain, renderer, sound и сетевым слоем.

Главный цикл

Главный цикл -- не одна функция update_and_render, а расписание, связывающее Win32 messages, input, игровые события, таймеры, сеть и renderer. Системная очередь сообщает об активации окна, вводе, изменении состояния процесса и выходе. Очередь World3D рассчитывает игровые объекты. У этих очередей разные правила времени и владения, поэтому их нельзя смешивать в один контейнер.

Подтверждённые точки вызова в одном из профилей:

stdCalculateGame       RVA 0x5FA94, 0x604C1, 0x6086B
ClearManualEventsList  RVA 0x6052F
stdRenderGame          RVA 0x60B2F
UpdateManualEventsList в обработчике сообщений около RVA 0xA3759

Смысловой skeleton:

while (running) {
    stdCalculateGame();
    clear_keyboard_snapshot();
    update_shell_and_mode();
    ClearManualEventsList();
    process_window_messages();
    update_timers_ui_gameplay_network();
    if (mode_requires_extra_step) stdCalculateGame();
    if (render_enabled) {
        stdSetCurrentCamera(camera);
        stdRenderGame(camera);
    } else {
        sleep_briefly();
    }
    update_post_render_state();
}

Ввод из window messages накапливается между расчётными шагами. Если читать клавиатуру только внутри рендера, события будут теряться при пропущенных кадрах или отключённом выводе.

stdCalculateGame

Calculation pass сначала очищает или подготавливает список manual events, увеличивает внутренний depth/counter и опрашивает input device. Если устройство временно потеряно, выполняется повторное получение доступа и чтение повторяется. Затем при незамороженной игре выставляется признак in_calculation и вызывается основной traversal очереди объектов.

prepare input/events
  -> enter calculation
  -> dispatch queue events
  -> objects update behavior and transforms
  -> leave calculation
  -> apply deferred operations
  -> occasional cache maintenance

После traversal разбирается deferred-delete list. Объект может запросить собственное удаление во время события, но память освобождается только после завершения обхода. Периодически также очищаются давно неиспользуемые ресурсы и объекты по порогам часов порядка 20 и 60 секунд.

Совместимый runtime должен иметь явный traversal depth или флаг in_calculation. Нельзя полагаться на то, что контейнер выдержит удаление текущего элемента из обработчика события.

Жизненный цикл кадра

Рендер читает состояние, подготовленное расчётом. Кадр начинается до renderer-а: message pump уже накопил ввод, World3D уже обновил объекты, отложенные операции и анимации, после чего выбирается камера и обновляется listener звука.

system messages and input
  -> simulation calculation
  -> deferred object operations
  -> animation and transforms
  -> camera and sound listener
  -> visibility and render queues
  -> materials and draw passes
  -> renderer completion
  -> end-of-render callbacks and UI

В World3D::stdRenderGame виден крупный каркас: установка camera/viewport, renderer frame boundaries, traversal мира, завершение world/shade path, renderer completion, снятие in_render, восстановление viewport и рассылка end-of-render callbacks. Эти callbacks позволяют объектам безопасно обновить временные ресурсы после того, как draw-команды больше их не используют.

Один calculation step не обязан соответствовать одному изображению. Главный цикл допускает дополнительный вызов stdCalculateGame и режим, в котором расчёт продолжается без вывода кадра. Поэтому нужно хранить отдельно:

  1. монотонные платформенные часы;
  2. игровое время с pause и масштабированием;
  3. длительность текущего calculation step;
  4. локальное время анимации и FX;
  5. реальные часы обслуживания кэшей.

Игровую логику нельзя выводить из render delta: изменение частоты кадров тогда изменит движение, камеру и сценарные таймеры. Подробности render item и рисков кадровой совместимости вынесены в справочник Render frame.

World3D

World3D связывает игровые объекты, события, время, ввод, камеру, сетевые отражения и визуальное представление. Он не содержит всю предметную логику: движение делегируется Behavior/Wizard, физика -- Control, мир -- Terrain. Его задача -- общая идентичность, порядок вызовов и безопасный жизненный цикл.

CreateQueue создаёт singleton-объект размером 20 байт, а GetQueue возвращает его. Очередь служит центральным маршрутизатором событий и операций над объектами.

Публичный слой предоставляет отдельные функции для локальных и сетевых объектов:

CreateObject
AddObjectToGame
AddNewObjectToGame
CreateMirrorObject
AddMirrorObjectToGame
AddNewMirrorToGame

Разделение "создать" и "добавить в игру" означает два этапа: сначала выделить и настроить instance, затем зарегистрировать его в общей системе. Это позволяет loader-у заполнить свойства до появления объекта в расчётной очереди.

Идентичность объектов

Object ID кодирует не только порядковый номер. Проверки диапазонов показывают разбиение на номер игрока, класс и индекс. Mirror object представляет объект, владельцем которого является другой участник. Локальный runtime хранит его видимое состояние, но источник авторитетных изменений находится удалённо.

struct ObjectId {
    uint32_t raw;
    uint16_t owner_player;
    uint16_t class_and_index;
};

Точное битовое разбиение нужно брать из сетевых функций. На уровне API уже сейчас полезно разделить логические свойства: is_local, is_mirror, owner, class и index.

Минимальный runtime-object должен хранить identifier, type, owner, transform, active state, ordered property bag, ссылки на controllers, участие в расчёте и рендере, сетевой статус и флаг отложенного удаления. Специализированные DLL могут быть представлены компонентами, но порядок их вызовов задаёт World3D.

Отложенное удаление

DeleteGameObject проверяет, идёт ли calculation pass. Если обход активен и удаление не принудительное, объект помещается в deferred list. KillGameObject отправляет запрос через очередь, а не освобождает память напрямую.

void request_delete(Object* o) {
    if (world.in_calculation) {
        world.deferred_delete.push_back(o);
        o->pending_delete = true;
    } else {
        world.detach_and_release(o);
    }
}

Это защищает итераторы, связи и текущий стек вызовов. Любая новая подсистема, способная удалить объект из обработчика события, обязана пользоваться тем же механизмом.

Регистрация в очереди и владение памятью -- разные понятия. Удаление из мира не всегда означает немедленное освобождение instance: часть объектов и managers использует intrusive reference count, а renderer, sound и resource managers могут возвращать уже существующий singleton с увеличенным счётчиком. Поэтому global manager закрывается после всех объектов, которые на него ссылаются.

Детерминизм

Даже при одинаковых формулах результат зависит от порядка. Стабильный runtime сохраняет последовательность queue traversal, момент формирования input snapshot, порядок сетевых сообщений, обработку deferred operations и порядок обращений к RNG. Оптимизация и многопоточность допустимы только при детерминированном объединении результатов.

Для переносимой реализации полезно разделить scheduler phases и immutable render snapshot. Это архитектурная рекомендация для новой реализации, а не утверждение о точном layout исходных C++ classes.

Стабильность между сборками

Внешняя архитектура полных Частей 1 и 2 сохраняет те же пятнадцать DLL, 313 exports, имена, ordinals и import sets. Побайтно идентичны:

ai.dll, Behavior.dll, Joystick.dll, MisLoad.dll, Net.dll,
Ngi32.dll, Terrain.dll, Wizard.dll, World3D.dll

Пересобраны:

AniMesh.dll, ArealMap.dll, Control.dll, Effect.dll,
iron3d.dll, services.dll

Это разделяет переносимость выводов. World3D lifecycle, Terrain, NRes/RsLi readers, mission loader, AI/Behavior/Wizard, DirectPlay wrapper и joystick adapter подтверждаются одной машинной реализацией. Model/agent runtime, collision, effects, shell/composition и service layer требуют отдельного сравнения поведения Частей 1 и 2.

Для World3D.dll Частей 1 и 2 применим общий hash:

World3D.dll SHA-256
17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e

RVA внутри iron3d.dll нельзя считать общими без проверки конкретного файла: эта DLL пересобрана между частями, а демоверсия имеет отдельный binary profile. Смысловая последовательность цикла переносится как контракт scheduler-а, но адреса остаются build-specific.