Изыскания путиПрежде, чем пилить графическую субсистему (окончательный вариант, где опыт - сын ошибок трудных) - решил придумать ей название
https://gamedev.ru/flame/forum/?id=270983&m=5598127#m12Какан(果敢 [kakan] ~na кн. решительный, смелый, отважный)
https://vkguide.dev/docs/extra-chapter/multithreading/ говорит:
The first and most classic way of multithreading a game engine is to make multiple threads, and have each of them perform their own task.
For example, you have a Game Thread, which runs all of the gameplay logic and AI. Then you have a Render Thread that handles all the code that deals with rendering, preparing objects to draw and executing graphics commands.
An example we can see of this is Unreal Engine 2, 3, and 4.
Подобная архитектура у меня уже была: отдельный тред для логики, рендер в основном треде и воркер-треды для задач типа загрузки из файла и дешифровки png.
И даже работало, хоть и подглюкивало.
Но теперь я ея вынужден сделать заново, потому что она была полным гумном.
Дано:
1. СУБД слишком удобная, чтобы что-то делать без неё - но, архитектурно, абсолютно однотредовая. Ширше не будет - я и так почти год убил, пытаясь сидеть на трёх стульях (вышло бы как с огнелисом - который, сцуко, плодится и размножается, пока вся память не окажется зажрана дюжиной процессов firefox.exe). Хватит. Фтопку.
2. Изпользуемый гапи - OpenGL / GL ES, что налагает ограничение: все команды на рендер - только в основном потоке.
Из этого - физика:
1. Физика - свободноинтервальная с частотой кадров 1000. То есть, каждый объект тикает с той частотой, которой пожелает, просчитывая себя *вперёд*. Например, 500 мс для прямолинейно летящих снарядов. Остальные объекты, когда просчитывают свою реакцию с ним - используют интерполяцию между его прошлой и будущей точками. В случае столкновения - будят его, приводят его текущее состояние к текущему тику и лишь после этого сталкиваются.
Для более сложных объектов (монстр по буеракам) просчёт вперёд может содержать массив из позиций капсулы.
Как следствие - очень дороги массовые столкновения "куча мала", но былинно дёшевы отдельные, не взаимодействующие объекты. Представьте себе десять тысяч летящих фаерболов, каждый из которых пересчитывается два раза в секунду, а всё остальное время - спит.
Следствие следствия - приемлемо дешёвой становится лагокомпенсация всего мира.
2. Лагокомпенсация всего мира на основе расслоённой реальности (объект нижнего слоя при взаимодействии с ним в вышележащих слоях клонируется по принципу copy-on-write, а при взаимодействии в своём слое - аналогично клонируется в вышележащий слой)
Из такой физики - следствие: физику можно продвинуть вперёд на нужное число милисекунд, подстраиваясь под кадры рендера. Т.е (упрощённо, сингл):
1. Основной тред: вангует момент следующего кадра.
2. Основной тред: собрал инпуты и отправил событие побудки логическому треду
3. Логический тред: проснулся, применил инпуты, продвинул физику до текущей милисекунды.
4. Логический тред: сочиняет задание (job) какану, на основе списка вижуалов, создавая новые по необходимости
5. Логический тред: отправляет какану задание.
6. Логический тред: продвигает физику до <навангованный момент следущего кадра минус две милисекунды>
7. Логический тред: баиньки.
3. Основной тред: какан окучивает отложимые вижуалы с прошлого кадра (дорисовывает лайтмапы, обновляет скины игроков и прочая) выделяя на это столько милисекунд, сколько логический поток в среднем думал над физикой и заданием в прошлые N кадров.
4. Основной тред: если делать нечего - какан баиньки до получения задания, таймаут - конец кадра из расчёта 30 фпс
5. Основной тред: если тайм-аут (логический поток задумался) - какан берёт старое задание с предыдущего кадра, если ответа нет дольше полсекунды - сам составляет себе задание на синий экран.
6. Основной тред: какан приступает к обработке задания. Первый этап: распараллеливаемое окучивание специфических вижуалов. Выковыривает из задания все анимации и генерации мешей по воксельным чанкам, будит вспомогательные треды и вместе с ними окучивает.
7. Основной тред: когда буфер распараллеливемых задач исчерпан - какан приступает к выполнению задания, что сводится к переводу из древовидной стрктуры объектов в последовательность команд OpenGL.
8. Основной тред: закончив отправку команд - какан делает SwapBuffers, сопровождаемые принудительной синхронизацией цпу и гпу (например, glReadPixels из фронт буфера). Причём, динамически, часть кадров с включённы Vsync, часть - без.
9. Основной тред: балансировщик нагрузки анализирует тайминги на последние надцать кадров: сколько занял SwapBuffers без Vsync'а, на какие моменты приходился конец операции с VSync'ом и какой частоте монитора это соответствует - чтобы на основании этого ванговать момент следующего кадра и подбирать управляющий коэффициент DRR.
10. Основной тред: если, паче чаяния, до следующего кадра ещё дофига времени - баиньки.
11. Основной тред: goto 1
====================================================================================
Какан, воплощениеВстал вопрос, где имплементировать:
А) На стороне эмо: удобство составления задания (может быть деревом из объектов), не нужен пространный АПИ, крайне неудобно работать с вижуалами.
Б) На стороне матки: удобство работы с вижуалами, удобство сопряжения с ГАПИ (убирается необходимость транслировать ГАПИ в отладочную dll), крайне неудобно сочинять задания
В конечном итоге принял Б). Потребный новый АПИ будет минимизирован за счёт использования универсальных записей:
- Код: Выделить всё
type
TVisLink = {$ifdef devmodedll} pointer {$else} TVisual {$endif};
PKakaJob = ^TKakaJob;
PKakaState = ^TKakaState;
PKakaTarget = ^TKakaTarget;
TKakaTarget = packed record
Buffer: TVisLink; // texture. NIL means the back buffer.
// The order of targets is auto-determined based on which states use this same texture as texture
// Target with no jobs in all its states would be ignored
PState: PKakaState;
end;
TKakaJobFlagsEnum = (kjf_Skippable)
TKakaJobFlagsSet = set of TKakaJobFlagsEnum;
TKakaJob = record
Visual: TVisLink;
Flags: TKakaJobFlagsSet; //
Priority, // if skippable, sorted by.
QFDropOnSkip // if skipped, load balancer drops Quality Factor by this much
: float;
end;
TKakaState = record
Jobs: array of TKakaJob;
// the same job can be shared between several states, possibly of different targets
// state with no jobs would not execute but merge with the next one instead
// all below: if NIL, inherit from previous
Shader: TVisLink;
Components: TRenderComponentSet; // uniforms & attributes used -- see the shader visual
Matrix: PMatrix4f;
Blend: PKakaBlend;
Depth: PKakaDepth;
Scissor: PKakaScissor;
Texture: PKakaTexture;
// ...
Next: PKakaState;
end;
function NewKakaTarget: PKakaTarget; // adds one default state to the targrt
procedure KakaTarget(ptarget: PKakaTarget); // switches current target
function NewKakaJob: PKakaJob;
function NewKakState: PKakaState;
function GetCurrentKakaState: PKakaState;
function PushKakaState: PKakaState; // pushes existing one down stack, creates new one and returns it
function PopKakaState: PKakaState; // creates a CLONE of the state it removes from the stack
Технические решения:
1. Анимация - это *отдельный* объект, связанный с объектом моба, но тикающий на своей частоте (10..60 кадров в секунду, зависит от частоты ключевых кадров и наличия симуляции ткани). Вижуал у них - один на двоих, анимируемая модель, но задание для неё составляют на пару.
Главный профит - анимация становится подвержена лагокомпенсации.
2. Симуляция ткани (и волос) *не* честная. В физике просчитываются специальные «тряпочные кости», столкновимые с очень ограниченным набором примитивов, потом анимированная этими костями ткань дополнительно сталкивается, мнётся и апплятся прочие констрейнты алгоритмами, «родства не помнящими» (т. е. не зависящими только от текущего состояния).
3. Механизм «пропускабельных» заданий с сортировкой по приоритету (например, анимаций) их можно дропать, если не хватило времени, но это понижает автобалансируемое качество и наполняет в основном вижуале «чашу терпения». То есть, если модель не анимировали несколько кадров и чаша терпения переполнилась - следующее задание на анимацию объект выставит обязательным к исполнению.