Frontend и рендеринг
Экспериментальный декодер Ogg/Theora, порядок полноэкранной отрисовки,
встроенное представление через RenderIface, загрузка текстур, поведение
соотношения сторон и граница визуальной приёмки описаны в разделе
Видео. Эта страница отвечает за общую
инфраструктуру frontend и renderer, но не за проектную политику синематиков.
Документация движка. Страница описывает переиспользуемые абстракции приложения, ввода, звука, окна и рендеринга из
Source/Frontend/, а также клиентский мост render targets изSource/Client/.
Статус контракта
Это основанное на исходном коде объяснение текущей архитектуры frontend и
рендеринга. Настройки проекта, сгенерированные script API, форматы ресурсов и
сериализованные metadata эффектов сохраняют уровни стабильности, заданные их
владеющими справочниками. Native-классы Application, Renderer, контексты
backend, распределители atlas и клиентские draw managers считаются деталями
реализации, если индекс публичных контрактов не говорит обратного.
Встраиваемый проект отвечает за выбранные backends, authored effects, визуальный уровень качества, целевые разрешения, бюджеты производительности и приёмку платформ. Движок отвечает за семантику выбора backend, контракты render resources, соглашения о матрицах и depth и описанные здесь переиспользуемые маршруты проверки.
Назначение
Frontend-слой образует границу между платформой и runtime движка. Он владеет окнами, границами кадров, очередями ввода, преобразованием touch/gamepad, доступом к аудиоустройству, выбором renderer и низкоуровневыми объектами render backend. Клиентский runtime использует этот слой через устойчивые интерфейсы и не обращается напрямую к SDL, OpenGL, Direct3D или Web API.
Читайте эту страницу вместе со следующими документами:
- Клиентский runtime описывает, как эти сервисы используют
ClientEngine,SpriteManager,MapViewи runtime managers. - Форматы изображений и спрайтов описывают загрузку baked sprites, размещение в atlas, filter borders, hit masks и identity cache изображений.
- Форматы шрифтов и layout текста описывают bitmap fonts, размещение glyphs в atlas, масштаб при bind, layout текста и rendering flags.
- Build Workflow и BuildTools Pipeline описывают configure/build composition.
- Сборка, упаковка и отладка в браузере и сборка, упаковка и отладка на Android описывают package/debug flows платформ.
- Нативная отладка и отладка AngelScript описывает native debugging и stack traces.
Проверенные пути исходного кода
Source/Frontend/Application.hSource/Frontend/Application.cppSource/Frontend/ApplicationInit.cppSource/Frontend/ApplicationHeadless.cppSource/Frontend/ApplicationStub.cppSource/Frontend/Rendering.hSource/Frontend/Rendering.cppSource/Frontend/Rendering-OpenGL.cppSource/Frontend/Rendering-Direct3D.cppSource/Frontend/Rendering-Vulkan.cppSource/Frontend/Rendering-SDLGpu.cppSource/Frontend/Rendering-Null.cppSource/Common/Settings.incSource/Client/RenderTarget.hSource/Client/RenderTarget.cppSource/Client/SpriteManager.hSource/Client/SpriteManager.cppSource/Client/DefaultSprites.hSource/Client/DefaultSprites.cppSource/Client/TextureAtlas.hSource/Client/TextureAtlas.cppSource/Client/ModelSprites.hSource/Client/ModelSprites.cppSource/Client/ModelSpriteLayout.hSource/Client/ModelSpriteLayout.cppSource/Common/AnimationInfo.hSource/Common/AnimationInfo.cppSource/Common/ModelBounds.hSource/Common/ModelBounds.cppSource/Common/Geometry.hSource/Common/Geometry.cppSource/Client/EffectManager.hBuildTools/cmake/stages/Packages.cmakeSource/Tests/Test_Rendering.cppSource/Tests/Test_Geometry.cppSource/Tests/Test_ModelBaker.cppSource/Tests/Test_ImageBaker.cppSource/Tests/Test_TextureAtlas.cppSource/Tools/ImageBaker.cppSource/Tools/SpriteMeshing.cppSource/Tools/BakingReport.cppBuildTools/cmake/stages/Init.cmake
Карта слоёв
Frontend/rendering разделён на три слоя:
- Application layer (
Application,AppWindow,AppInput,AppAudio,AppRender) владеет платформенными сервисами и границами кадров. - Renderer layer (
Rendererи его backends) владеет GPU/null resources: текстурами, draw buffers, эффектами, матрицами, scissor state, presentation и обработкой resize. - Client drawing layer (
SpriteManager,RenderTargetManager,EffectManager,MapView) строит операции движка и игры поверх renderer abstraction.
Благодаря этому основная часть клиентского кода не зависит от renderer. Клиент запрашивает sprites, effects, draw buffers, render targets и input events, а выбранный backend решает, как они реализуются.
Декодирование исходных изображений принадлежит baker, а не frontend. Во время
выполнения DefaultSpriteFactory превращает baked RGBA container в sprites в
atlas, TextureAtlasManager выделяет место по AtlasType, а renderer получает
только загрузки регионов текстуры и draw data. Полный путь описан в
форматах изображений и спрайтов, а
движение отдельных кадров через offsets - в Sprite Root Motion.
Дескрипторы bitmap fonts являются raw runtime resources; связанные PNG/TGA всё
равно проходят обычный image baking. FontManager разбирает descriptor,
загружает обычные и bordered glyph regions в font atlas и отправляет текст через
SpriteManager. Backend renderers не интерпретируют FOFNT/BMFont syntax или
wrapping flags. Этой границей и точным поведением измерения/отрисовки владеет
справочник шрифтов.
Соглашение о матрицах
В render math движка действует одно соглашение:
mat44— тип матрицы GLM изSource/Common/Common.h.- Хранение column-major. Прямой индекс имеет вид
matrix[column][row], translation находится вmatrix[3].xyz, аglm::value_ptr(matrix)можно копировать в uniform buffers без transpose. - Алгебра использует column vectors:
clip = Proj * View * Model * vec4(position, 1.0). - Shader code следует тому же правилу через
ProjMatrix * vec4(...). Различия backend должны находиться в constructors матриц и shader cross-compilation, а не в случайных transpose на call sites.
Имена RowMajor/ColumnMajor допустимы только на явной границе преобразования
с внешним форматом. Внутренние renderer, model, particle и geometry paths
называют матрицы по роли: ProjMatrix, ViewMatrix, ViewProjMatrix,
WorldMatrix.
Инициализация приложения
InitApp() и LoadAppSettings() в Source/Frontend/ApplicationInit.cpp
готовят global settings и application services до создания engine object
клиентом, сервером или инструментом.
Основные обязанности:
- разобрать command line и локальную конфигурацию;
- загрузить app settings из config/cache sources;
- один раз инициализировать frontend globals;
- при необходимости вызвать поддержку baking через
FO_BakeResources; - подготовить app-level services для клиентов, инструментов и headless/test modes.
Эта инициализация намеренно общая не только для графического клиента. Server, Mapper, Editor, tests и package flows используют разные flags и window modes, но там, где это применимо, проходят через общий frontend setup.
После инициализации клиент однократно записывает в журнал версию ОС, число ядер CPU, объём памяти, видеодрайвер SDL, режим дисплея и параметры окна; браузерная сборка также записывает user agent. Каждый renderer называет выбранное устройство и драйвер, а ухудшение возможностей помечает предупреждением. Эти сведения описывают окружение, но не заменяют приёмку GPU или платформы для релиза.
Сервисы приложения
Публичная frontend-поверхность определена в Source/Frontend/Application.h.
Application
Application владеет frontend-состоянием уровня процесса:
- главным и дочерними окнами;
- выбором активного окна;
- границами кадра
BeginFrame()/EndFrame(); - границами рендеринга окна
BeginWindowRender()/EndWindowRender(); - открытием ссылок и пользовательскими message/progress/choice dialogs;
- регистрацией callback главного цикла;
- запросом выхода и ожиданием завершения;
- распознаванием touch gestures и обновлением состояния gamepad.
AppRender / IAppRender
AppRender — принадлежащий приложению facade над выбранным Renderer.
Клиентский и инструментальный код использует его вместо downcast к конкретному
GPU backend. Контракт включает:
- создание ресурсов через
CreateTexture(),CreateDrawBuffer()иCreateEffect(); - доступ к projection через
CreateOrthoMatrix()иGetProjMatrix(); - выбор target/depth range через
SetRenderTarget()иSetOrthoDepthRange(); - очистку target через
ClearRenderTarget(); - clipping через
EnableScissor()иDisableScissor(); - ориентацию render target через
IsRenderTargetFlipped().
AppWindow::GetRender() возвращает facade, принадлежащий конкретному real или
virtual window. Операции должны оставаться внутри владеющего window/engine
instance: render resources и settings встроенного клиента не являются globals
хоста.
AppWindow / IAppWindow
Обязанности окна:
- size, screen size, position, display rect, focus и fullscreen state;
- minimize, blink, always-on-top, title, input grabbing и destruction;
- различение реальных OS windows и virtual windows (
IsVirtual()), которые multi-client host композитит сам и рендерит в собственном virtual size; - получение native
WindowInternalHandleдля render backends; - использование
HeadlessWindowStubв headless/stub contexts.
AppInput / IAppInput
Обязанности ввода:
- polling очереди
InputEvent, очистка и добавление events; - управление положением mouse;
- включение screen keyboard;
- clipboard text;
- gamepad state;
- нормализация mouse, keyboard, wheel, touch tap/double-tap/scroll/zoom.
При преобразовании platform mouse button в script-facing MouseButton
сохраняется конкретный id (Left, Right, Middle, Ext0/Ext1, …).
Неизвестные native buttons игнорируются и не превращаются в primary click.
Низкоуровневые события переводятся в script events в
ClientEngine::ProcessInputEvent().
Главный источник InputEvent::MouseMoveEvent — SDL mouse-motion. На backends,
где SDL даёт global mouse coordinates (Windows, macOS, X11 и разрешённые OS/2
drivers), Application::BeginFrame() также опрашивает global state, пока
приложение в focus. Если SDL motion event в кадре не было, но global position
изменилась, frontend синтезирует mouse-move. Поэтому cursor и edge scroll не
замирают, когда OS pointer выходит за окно. Тот же перевод host-to-active-window
преобразует координаты embedded virtual clients через display rect и
aspect-fit mapping.
AppAudio / IAppAudio
Обязанности audio:
- сообщать, включён ли звук;
- устанавливать audio stream callback;
- преобразовывать audio formats;
- смешивать audio;
- lock/unlock устройства вокруг critical sections.
Поддерживаемые WAV/Ogg authoring profiles, AudioBaker delivery, поиск effects/music, playback handles/spatial updates, repeat, volume и audible validation описаны в Audio и сгенерированном audio reference. Здесь описана platform abstraction, а не правила authoring звука конкретной игры.
Headless и stub modes
Для tools, tests, CI и platform staging важны два неграфических режима:
Source/Frontend/ApplicationHeadless.cppподдерживает работу без видимого client window.Source/Frontend/ApplicationStub.cppпредоставляет stub implementations render/input/audio/window interfaces.
Stub layer не является полноценным renderer. Он позволяет tests и non-graphical flows проверять engine logic без GPU/window/audio device. Тест, которому нужен видимый рендеринг, должен заявлять это явно и не полагаться на stub behavior.
Ограничение частоты кадров
Циклы desktop-клиента, Mapper и viewers используют FrameBalancer (Source/Common/Common.h). При Render.VSync = true собственное ожидание выключено: представление кадра уже ждёт дисплей. Иначе положительный Render.FixedFPS ограничивает частоту через precise_sleep независимо от Render.Sleep; превышенный бюджет компенсируется более короткими ожиданиями следующих кадров, с долгом не более секунды. Render.Sleep применяется только при FixedFPS = 0: ноль уступает поток, положительное значение использует coarse_sleep, -1 оставляет цикл без ограничения. Ветви проверены в Source/Tests/Test_Common.cpp.
Абстракция рендеринга
Source/Frontend/Rendering.h определяет renderer-facing types:
RenderType— семейство backend.EffectUsage— категория эффекта при load/compile.RenderPrimitiveType— topology draw buffer.BlendFuncTypeиBlendEquationType— blend state из effect config.DepthVariantTypeиEFFECT_DEPTH_VARIANTS— slot варианта depth state отдельного draw.Vertex2DиVertex3D— layouts вершин sprite/model paths.RenderTexture— texture/render-target resource backend с блокирующим чтением и запросом области.RenderTextureReadback— ожидаемый CPU snapshot пикселей;ImmediateTextureReadbackхранит уже доступные на CPU pixels.RenderDrawBuffer— vertex/index storage, загружаемый в backend.RenderEffect— shader/effect со standard uniform и script-value buffers.Renderer— интерфейс, реализованный конкретными backends.
Для primitive batches из SpriteManager::DrawPoints поля PosX/PosY —
локальные pixel coordinates относительно draw_area с вычтенным scroll,
а TexU/TexV содержат PrimitivePoint::TexUV + draw_area.xy. Для устойчивых
к миру fragment effects один и тот же constant TexUV задаётся всем вершинам
batch: это абсолютная, привязанная к map origin pixel position anchor-hex
_screenRawHex. Shader восстанавливает абсолютный world pixel как
gl_FragCoord.xy + InTexCoord. Константный varying не даёт barycentric rounding
двигать noise; пространственное изменение предоставляет gl_FragCoord. Схема
устойчива к camera scroll/zoom, deformation fan triangles и parity offset-row
hex grid. MapView::LightFanToPrimitves вычисляет значение через
GeometryHelper::GetHexOffset(mpos(0, 0), _screenRawHex), а
Primitive_Light.fofx использует его для world-stable jitter края света.
Нормализованная radial distance хранится в PrimitivePoint::PointPosZ и через
InPosition.z позволяет smoothstep плавно поднять brightness от нуля на rim.
Source/Frontend/Rendering.cpp владеет backend-independent helpers, включая
проверки allocation draw buffers и parsing effect configuration. Он читает
sections Effect/EffectInfo, pass count, blend settings и script-visible
buffers до передачи shader files конкретному backend.
Чтение текстуры на CPU
RenderTexture::GetTextureRegion(pos, size) — блокирующее чтение. Оно подходит для инициализации, screenshot/dump и других операций, которым pixels нужны немедленно, но не для повторного picking внутри frame path. Оба пути требуют положительного размера прямоугольника внутри границ текстуры.
RequestTextureRegion(pos, size) записывает copy в точке запроса, учитывая предшествующие ему draws и clears. RenderTextureReadback::TakePixels() проверяет готовность без ожидания: до завершения возвращает std::nullopt, затем отдаёт pixels ровно один раз. Повторное получение после успеха бросает исключение. Layout pixels и порядок строк совпадают с GetTextureRegion. Запрос и его backend resources должны оставаться внутри lifetime владеющего renderer/context.
Сам запрос не на всех платформах неблокирующий; следующие fallback-пути входят в текущий контракт:
| Backend | Запрос и завершение |
|---|---|
| Null | CPU copy в ImmediateTextureReadback; результат готов сразу. |
| Direct3D 11 | CopySubresourceRegion в собственную staging texture; Map с D3D11_MAP_FLAG_DO_NOT_WAIT проверяет завершение. |
| OpenGL | Pixel-pack buffer и glFenceSync; glClientWaitSync с нулевым timeout проверяет завершение. Нужны sync, pixel-buffer и map-range capabilities; на Web и без этих capabilities запрос выполняет блокирующее чтение сразу. |
| Vulkan | Host-visible copy в записываемом frame command buffer между render passes; completed-frame tracking либо frame fence подтверждает готовность без submit/wait в запросе. Вне записи frame запрос использует блокирующий fallback. |
| SDL_GPU | Download transfer buffer в текущем command buffer; общий SDL_QueryGPUFence отслеживает завершение. Submission fence получают только buffers с readbacks; до submission результат не готов. |
ModelSprite::IsHitTest использует CPU vector<bool> alpha mask изображения модели в atlas вместо чтения GPU pixel для каждого запроса. DrawToAtlas помечает mask устаревшей; следующий hit test обновляет её, держа не более одного readback in flight и сохраняя последнюю готовую mask до получения новой. До готовности первой mask hit testing возвращает false. Силуэт движущейся или анимированной модели может отставать от rendered pose, обычно на frame или два, но фиксированный срок завершения не гарантируется. Это клиентское представление/picking, не server-authoritative проверка попадания или боя. Обычный AtlasSprite строит mask из source pixels при загрузке. RenderTargetManager больше не содержит last-pixel-pick cache и API его инвалидации.
Геометрия atlas спрайтов и моделей
EffectUsage::QuadSprite — историческое имя effect slot, а не ограничение на
четыре вершины. Sprite draw buffer является indexed triangle list; исходный
AtlasSprite может отправлять baked silhouette vertices/indices вместо
неявного rectangle из 4 vertices/6 indices. Все backends потребляют один и тот
же buffer, отдельного backend-specific polygon path нет.
Local mesh coordinates отсчитываются от точного bounding box выбранной
геометрии. Baked frame хранит исходный logical bitmap size и origin crop внутри
него, а individual sprite offset сохраняет исходный logical root. Screen
position и atlas UV являются affine mappings одной cropped coordinate, поэтому
scale, rotation, map projection, depth standing sprite и egg flags продолжают
работать для каждой вершины без пустых texture rows/columns. Map lighting
сохраняет плоскость полного bitmap quad: DrawSprites передаёт left/right
colors, а mesh vertex получает
lerp(left, right, clamp((localX + sourceOffsetX) / sourceWidth, 0, 1)).
Это положение по горизонтали в исходном bitmap, а не в crop или opaque contour;
из-за RGBA8 округление промежуточных mesh colors допускает отличие не более
одной единицы channel.
Baked polygon mesh используется только обычной отрисовкой полного изображения.
У каждой allocation остаётся корректный rectangle GetAtlasRect(), который
может быть меньше исходного logical image и смещён внутри него. Region crops,
tiled patterns, padded custom effects/outlines и mapper previews проецируют
physical rectangle обратно в logical coordinates через SourceOffset.
Region UV нормализованы к исходному image; transparent cropped margins
вырезаются из destination. Поэтому polygon crop не сдвигает и не растягивает
GUI 9-slice, repeated pattern, preview или source-region composition. Эффект,
создающий pixels за пределами silhouette, обязан использовать padded/quad path.
Runtime model sprite тоже может использовать cropped quad, но его logical
layout вычисляется автоматически. .fo3d больше не принимает DrawSize и
ViewSize; default render settings также не задают fallback dimensions.
ModelAnimationInfo.foinfo bounds schema v2 содержит aggregate root-space model
AABB, отдельный idle-priority view AABB и AABB каждой animation. В сборках
FO_ENABLE_3D общий loader EngineMetadata один раз разбирает и проверяет весь
companion; renderer получает immutable model records из registry вместо второго
client config parser/cache bounds.
Клиент проецирует enabled animation bounds через active base transform и
вычисляет extrema для любого continuous facing angle. Body и projected shadow
задают animation-wide DrawRect и dimensions active logical scratch frame.
View bound выбирает Unarmed + Idle, затем любую Idle, затем deterministic
animation/static fallback; проекция по всем направлениям даёт устойчивый
ViewRect. Logical frame — tight extent текущей animation, выровненный вверх к
sprite frame scale, но не к power of two. Ground root находится в точном
projected pixel (-DrawRect.x, -DrawRect.y), доступном через
ModelInstance::GetFramePivot(), а не в фиксированной доле frame. Поэтому
low/center-origin creature не резервирует пустую высоту. ViewRect исключает
shadow и не зависит от меняющегося atlas crop, чтобы name, coarse picking,
transparent eggs и flying text не дёргались при turn/animation.
View rectangle начинается с baked idle-priority bound. Live weighted pose не
участвует: накопление per-frame vertices растянуло бы name rectangle по всему
root motion clip. Selected child models учитываются через baked link envelopes.
SelectModelViewBounds заменяет idle view полным текущим
animation-plus-link envelope только тогда, когда его projected top ниже, поэтому
prone/corpse configuration опускает name, а поднятое оружие не поднимает его.
Presentation policy проекта по-прежнему может задаваться authored NameOffset.
SelectModelViewBounds позволяет view целиком следовать вниз за active
animation: если после тех же base transform/projection baked box clip имеет
более низкий top, берётся весь box clip. Сравнение выполняется в projected
space, что важно для imports с RotX = +/-90. Corpse/prone body иначе оставил бы
name на standing height; при этом более широкий lying pose требует полного box.
Повышенный weapon/overhead swing игнорируется, поэтому name не поднимается во
время удара. Оба input baked per clip, и результат не дрейфует внутри animation.
Автоматический логический кадр использует переиспользуемый scratch render target 2x. Фабрика спрайтов моделей хранит совпадающие размеры в LRU-кэше с мягким бюджетом 8 x 1024 x 1024 цветовых пикселей (32 MiB RGBA плюс память глубины конкретного backend). Кадр, превышающий этот бюджет, занимает кэш один. Очистка sprite cache освобождает все scratch targets и сбрасывает заимствованную текстуру blit effect до удаления её владельца; atlas allocations живых спрайтов и общие материалы моделей остаются валидными. Бюджет ограничивает удерживаемые промежуточные цветовые пиксели, а не всю память рендерера и не фиксированное число targets. Клиент объединяет baked active-animation bounds с per-animation AABB выбранных direct non-particle child links, parent clips которых активны. Если matching clip не активен либо link вложен под другой child rig, используется baked aggregate link envelope. Затем восемь выбранных corners проецируются по facing sweep. Это удерживает layer/equipment geometry в facing-independent fixed frame, не резервируя полный all-animation travel rigid attachment и не выполняя per-frame weighted-vertex walk. Envelope расширяют только emitting particle systems; dormant effect ничего не резервирует и попадает в bounded expansion pass, если начинает emission.
Dynamic draw envelope задаётся ModelSpriteBounds::Rect: baked geometry,
projected shadow, live particles и full frame, принудительно выбранный effect.
Это внутренний renderer contract, который определяет frame и atlas crop.
Game.GetDrawCritter3dBounds предоставляет два стабильных layout rectangles.
DrawRect является conservative envelope baked active clip и selected links
вместе с projected shadow и предназначен для diagnostics renderer layout.
ViewRect является semantic interface bound: обычно он использует
idle-priority envelope, а при необходимости выбирает полный более низкий
prone/active clip. GUI portraits и world overlays используют его вместо более
крупного draw envelope. Отдельный pose rectangle удалён, поскольку дублировал
тот же contract active model occupancy, что и DrawRect, без собственного
поведения.
Уже emitted world-space particles не масштабируются вместе с model. Fit по
внутреннему Rect создал бы feedback loop, уменьшающий model при неизменном
размере particles. Effects входят во frame, но не в interface fit.
Если exact envelope больше, frame расширяется и перерисовывается. Последовательные frame placements объединяются как root-relative intervals, поэтому соседние округлённые pivots или живая world-space particle не заставляют стабильный frame бесконечно чередоваться. Interval anchor знаковый: tight frame может целиком находиться по одну сторону от model root, оставляя pivot за пределами frame; bounded retry loop всё равно отклоняет действительно unbounded layout.
Layout helpers без live GPU используют
AppRender::MIN_ATLAS_SIZE / FRAME_SCALE
(MODEL_SPRITE_MAX_LOGICAL_FRAME_DIMENSION) как portable logical limit; atlas
limit bake host не является контрактом игрового устройства. Scratch texture
model sprite ограничивается Render.ModelSpriteMaxTextureWidth / Height и
atlas текущей машины. Envelope сверх limit рисуется с crop, а bounded retry loop
по-прежнему отклоняет layout, который не сходится внутри limit.
ModelInstance::SetupFrame также отклоняет logical
draw_size * FRAME_SCALE выше AppRender::MAX_ATLAS_WIDTH /
MAX_ATLAS_HEIGHT текущей машины, называя model и оба размера до анонимного
device texture-allocation failure. Meshes, отключённые собственным default link
модели в .fo3d, не входят ни в drawn, ни в pose bounds; runtime учитывает
default DisableMesh так же, как layer/attachment, удерживая geometry внутри
baked layout budget.
В atlas выделяется и копируется только selected region, а crop origin отражается в sprite offset, сохраняя root, hit test и map position.
Active layer/child tree расширяет idle-priority base view и aggregate lighting
bounds. Animation switch может обновить DrawRect/scratch frame, но ViewRect
использует накопленный view envelope model+layers и не заменяется временно
меньшим root-model idle view. Left/right map-light colors выбираются в
root-relative crop endpoints configuration envelope, поэтому изменение scratch
size не меняет light mix, а wide gear не clamp-ится к base-model endpoints.
Внутри одного active animation/combined-mesh envelope следующие pose changes только расширяют slot. Identity envelope меняется после стабилизации enabled body/movement tracks, изменения generated mesh composition или shadow coverage; это разрешает одно shrink к новому stable envelope вместо накопления всех animation за lifetime. Direction в identity не входит. Новая allocation резервируется и копируется до публикации frame/crop; неудача оставляет старую allocation live и планирует retry.
Model-attached particles после первого update используют live SPARK AABB, до
него — advertised canvas, и выбирают весь current frame. При изменении scratch
frame уже emitted atlas-space particles rebased до rerender. Non-default model
effects отключают tight crop. Это не bound для shader displacement: эффект,
сдвигающий vertices за обычную geometry, требует отдельного conservative
контракта. При Render.ModelDirectDraw atlas preview/hit-test сохраняются, а
видимая geometry рисуется напрямую в scene.
Game.DumpAtlases() и команда Mapper Dump atlases аннотируют read-back PNG,
не меняя runtime texture: magenta — triangle edges, cyan — mesh vertices,
yellow rectangle — implicit quad, red X — explicitly empty baked frame.
AtlasSprite владеет mesh metadata, а live allocation хранит nullable
non-owning observer и очищает его при release, поэтому reused slot не показывает
stale geometry.
AtlasSprite отдельно хранит authored logical size/offset и cropped allocation.
Mesh vertices размещаются через SourceOffset в logical canvas, UV локальны к
crop. Поэтому GetSize(), GetOffset(), scale и hit testing сохраняют исходный
контракт даже после удаления transparent borders: экономия texture memory не
видна GUI layout, anchors или input routing.
Runtime allocation остаётся per image, но TextureAtlasLayout использует
dynamic MaxRects вместо order-sensitive guillotine tree. Он хранит overlapping
maximal free rectangles, выбирает best short-side fit, затем long-side fit и
wasted area без rotation. Manager сравнивает fit во всех существующих atlases
данного type и только затем создаёт page; при equal score выбирается старый
atlas. Packed rectangle уже включает one-pixel texture border, поэтому padding,
sprite pixels и UV не меняются.
Font sheets, model material textures, particle maps и Spine attachment textures
— rectangular consumers: authored glyph/normalized UV адресуют весь source
bitmap, а consumer получает только atlas rect без SourceOffset. Они загружаются
через SpriteManager::LoadSpriteAsQuad, который использует baked mesh metadata
только для восстановления исходного logical canvas перед upload. Обычный
AtlasSprite раскрыл бы crop dimensions и сдвинул UV. Runtime-generated model и
particle sprites уже занимают rectangular allocations и восстановления не
требуют.
Каждый live sprite владеет engine unique_del_* handle на encapsulated,
stable-address TextureAtlasLayout::Allocation. Release очищает mesh observer
и за constant time возвращает rectangle прямо в free list. Список не coalesce-ится
сразу: после placement miss DefragmentFreeRectangles перестраивает точный
maximal set из live allocations до создания новой page. Prune содержащихся
rectangles запускается только после роста значительно выше результата прошлого
prune и индексирует keepers по coarse atlas cells, не выполняя full scan на hot
path. Surviving sprites, pixels и UV не двигаются; settings и serialization не
меняются.
TextureAtlasManager::CleanupAtlases() удаляет пустые pages вместе с принадлежащими manager render targets, включая OneImage. SpriteManager вызывает очистку после вытеснения sprite cache; она выполняется и перед созданием новой page. Page с любой live allocation сохраняется: pixels не перемещаются, UV не меняются. Границу проверяют ExpiredOneImageAtlasReleasesRenderTarget и AtlasCleanupReleasesOnlyEmptyPages.
Render.DrawWireframe включает backend-independent geometry overlay.
SpriteManager копирует реально отправленные triangle edges после position,
scale, rotation, map projection и standing depth, затем рисует opaque magenta
line list поверх normal pass. Видны и два triangles обычного quad, поэтому
toggle не зависит от SpriteMesh.Enabled. По умолчанию он выключен и не меняет
draw buffer, atlas или baked resource.
Render backends
Компилируемый набор RenderType и способы выбора:
RenderType |
Выбор | Текущий контракт |
|---|---|---|
Null |
Render.NullRenderer или headless/stub path |
Реализованный CPU-only backend проверки без видимого GPU output. |
OpenGL |
Render.ForceOpenGL или последний automatic GPU choice |
Реализованный native OpenGL/OpenGL ES/WebGL; render targets перевёрнуты. |
Direct3D |
Render.ForceDirect3D или первый automatic Windows choice |
Реализованный Direct3D 11; render targets не перевёрнуты. |
Metal |
Render.ForceMetal |
Прямой Metal — placeholder: enum/platform flag существуют, но force приводит к AppInitException, класса Metal_Renderer нет. |
Vulkan |
Render.ForceVulkan или automatic choice перед OpenGL, если более ранний backend не создан |
Реализованный dynamically loaded Vulkan; render targets не перевёрнуты. |
SDLGpu |
Render.ForceSDLGpu, при необходимости Render.SDLGpuDriver |
Реализованный явный SDL_GPU поверх Vulkan/Metal/D3D12; render targets не перевёрнуты. |
Application сначала учитывает force selectors, затем compiled automatic order.
Это не health-probing fallback chain: после создания backend ошибка init
возвращается наружу, и другой backend не пробуется. Force selectors должны быть
взаимоисключающими. Render.ForceSDLGpu не становится automatic default.
Прямой Metal недоступен; реализованный Apple Metal route проходит через
Render.ForceSDLGpu = True и Render.SDLGpuDriver = metal.
Vulkan и SDL_GPU включаются по умолчанию в non-headless/non-Web builds и
отключаются FO_DISABLE_VULKAN / FO_DISABLE_SDL_GPU. Сборка использует
vendored SDL headers/drivers, а не внешний Vulkan SDK. OpenGL, Direct3D и
platform flags задаёт platform branch в BuildTools/cmake/stages/Init.cmake.
Null renderer
Source/Frontend/Rendering-Null.cpp реализует Null_Renderer, Null_Texture,
Null_DrawBuffer и Null_Effect. Backend нужен tests/headless flows без GPU,
но всё равно проверяет dimensions, buffer counts, render-target state и доступ
к texture regions, поэтому ловит многие нарушения API.
OpenGL renderer
Source/Frontend/Rendering-OpenGL.cpp реализует OpenGL/WebGL path:
- создаёт SDL/OpenGL или WebGL context;
- загружает и проверяет GL entry points/extensions;
- ограничивает atlas через
AppRender::MAX_ATLAS_SIZEи backend limits; - создаёт textures, draw buffers и effects;
- загружает vertex/fragment shader через effect loader;
- сообщает
IsRenderTargetFlipped() == true; - укладывает требуемые shader uniform blocks в один bump-allocated UBO, zero-initializes отсутствующие blocks, делает один
glBufferSubDataи связывает ranges черезglBindBufferRange; - пропускает redundant
SetRenderTargetдля уже применённого target; cache invalidates при resize/destruction, а texture creation восстанавливает текущий framebuffer.
OpenGL — основной путь WebAssembly/WebGL; изменения проверяются по сборкой, упаковкой и отладкой в браузере.
Direct3D renderer
Source/Frontend/Rendering-Direct3D.cpp реализует Direct3D 11:
Renderer загружает запечённый -dxbc bytecode: runtime-компиляции HLSL и
зависимости от d3dcompiler_47.dll нет. Обычный минимум устройства — feature
level 10.0. Сборка без 3D может принять level 9.3, если эффекты запечены с
Baking.Direct3DLevel9Shaders; сборка с 3D никогда не создаёт 9.3 device.
Для 9.3 размер atlas ограничен 4096 пикселями, point-list draws выполняются
без index buffer. Levels 9.1 и 9.2 не поддерживаются.
При создании устройства сначала запрашивается feature level 11.1. Если старый runtime Direct3D 11.0 отвергает весь список с E_INVALIDARG (в частности, Windows 7 без platform update), движок повторяет тот же запрос hardware или WARP без 11.1. Остальные ошибки остаются обычными ошибками создания устройства. Журнал содержит выбранный feature level и, если доступно, DXGI adapter, идентификаторы vendor/device, версию пользовательского драйвера и объём видеопамяти.
- создаёт D3D device, swap chain и render-target resources;
- не фиксирует refresh rate windowed swap chain, оставляя выбор desktop compositor;
- создаёт textures/staging textures, draw buffers, constant buffers и effects;
- загружает vertex/pixel shaders через effect loader;
- при resize пересоздаёт backbuffer/depth resources;
- сообщает
IsRenderTargetFlipped() == false.
Изменения Direct3D проверяются в Windows build/debug flow встраиваемого проекта.
Отсутствие GPU не заминается, а отвергается. Когда аппаратного устройства нет, DXGI охотно
возвращает программный растеризатор WARP, который рисует каждый кадр на CPU: клиент после этого
запускается, работает и играть в него нельзя. Теперь такая подмена — явный выбор:
Render.AllowSoftwareRenderer (по умолчанию выключен) разрешает её для диагностики или машины без
дисплея, а лог называет устройство Warp, чтобы медленная сессия не оставалась загадкой. С
выключенной настройкой создание устройства падает громко, а не выдаёт клиент, который лишь выглядит
играбельным.
Placeholder прямого Metal
FO_HAVE_METAL, RenderType::Metal, Render.ForceMetal и SDL Metal window flag
присутствуют, но direct Metal_Renderer не реализован. Поэтому
Render.ForceMetal = True немедленно вызывает AppInitException; такую
конфигурацию нельзя рекламировать или считать release evidence.
Metal driver SDL_GPU — другой, реализованный backend. Используйте
Render.ForceSDLGpu = True и Render.SDLGpuDriver = metal и применяйте те же
visible/shader/interaction/performance gates, что к Vulkan и D3D12 drivers.
Vulkan renderer
Source/Frontend/Rendering-Vulkan.cpp реализует Vulkan. Он включён по умолчанию
(кроме FO_DISABLE_VULKAN, headless-only и Web) и не требует внешнего Vulkan
SDK: headers приходят из vendored SDL3
(ThirdParty/SDL/src/video/khronos), loader разрешается динамически.
Render.ForceVulkan выбирает его явно; иначе он может стать automatic choice,
если более ранний backend не настроен.
vulkan-1.lib не линкуется. Файл компилируется с VK_NO_PROTOTYPES, а
SDL_Vulkan_LoadLibrary и SDL_Vulkan_GetVkGetInstanceProcAddr загружают
entry points через X-macro table. Поэтому binary не имеет load-time import
vulkan-1.dll и запускается без Vulkan runtime, пока backend не выбран; при
выборе отсутствие runtime даёт ошибку SDL_Vulkan_LoadLibrary.
Основные свойства:
- Одна queue, два frames in flight.
VULKAN_FRAMES_IN_FLIGHT = 2; каждый slot содержит command buffer, fence, acquire semaphore, descriptor pool, mapped uniform bump buffer, staging ring и deferred-destroy queue.BeginFrame()ждёт fence своего slot, очищает deferred destroys, resets pools, acquires swapchain image, clears и начинает render pass.EndFrame()submits и presents. Render-complete semaphores принадлежат swapchain images, acquire semaphores — slots. Fence гарантирует завершение всех более ранних submissions этой queue. - Deferred destroy per slot.
Destroy*Safe(...)добавляют handles в current slot; queue очищается после ожидания его fence. Разные helper names нужны из-за integer handles на 32-bit. Всегда используетсяVK_NULL_HANDLE. Swapchain recreation делаетvkDeviceWaitIdle, очищает все queues и перестраивает synchronization. - Uploads и requested readbacks записываются в frame command buffer; блокирующее чтение flush-ит его.
UpdateTextureRegionвременно завершает render pass и пишет barrier/copy/barrier в frame buffer через staging ring, сохраняя порядок предшествующих clears/uploads.RequestTextureRegionтакже записывает copy между render passes без mid-frame submit/wait;TakePixelsпроверяет completed-frame index либо submission fence.GetTextureRegionпо-прежнему вызываетFlushFrameCommandBufferMidFrame()(submit записанного prefix, wait idle, возобновление recording), затем immediate staging copy. Uploads и readback requests вне записи frame используют immediate path. Новые immediate-queue операции должны сохранять эти границы порядка и готовности. - Dynamic geometry использует per-draw-buffer/per-slot ring pools. Каждый
DrawBuffer::Uploadберёт следующий persistently mapped HOST_VISIBLE buffer текущего slot. Ring resets при первом acquire нового frame после fence. Capacity grows только при необходимости; steady state — memcpy без create/allocate/free на каждый draw. Static buffers используют one-off staging в device-local memory. - Shaders baked с
highp.mediumpпревращается в SPIR-VRelaxedPrecision, который Vulkan drivers могут выполнить как FP16; большие time/world values переполняются и дают black output. Effect baker поэтому выдаётprecision highp float. - Backbuffer metrics обновляются при resize без обязательного
SetRenderTarget.ApplySwapchainTargetMetrics()вызывается при выборе backbuffer,OnResizeWindow()и deferred recreation. Это важно для host ImGui, который рисует прямо в swapchain. - Два fixed descriptor set layouts. Set 0 — uniform buffers, set 1 — combined image samplers. На draw backend выделяет sets из per-frame pool и пишет uniforms в один host-visible bump buffer; binding берётся из reflected
EffectInfo. - Контракт
.fofx. Uniform blocks объявляются какlayout(set = 0, binding = N, std140), samplers —layout(set = 1, binding = N). Пропущенныйsetпомещает всё в set 0, создаёт descriptor mismatch/never-updated validation errors и может привести к device loss. OpenGL/Direct3D имеют отдельные namespaces и такой дефект там не проявляют. - Полнота uniforms. Descriptor set обязан записать каждый используемый block, поэтому required-but-unset standard buffers zero-initialized перед upload.
- Surface format. Swapchain использует
VK_FORMAT_B8G8R8A8_UNORM/SRGB_NONLINEAR, проверенный через surface formats. Texture render targets используют тот же color format для совместимости render pass; CPU upload/readback меняет R/B. - Present mode следует
Render.VSync. VSync использует FIFO; при выключенном VSync предпочтительны IMMEDIATE, затем MAILBOX. Выбор логируется при creation/recreation. - Orientation.
IsRenderTargetFlipped() == false, ortho использует Y-up как другие backends; Vulkan Y-down компенсируется negative-height viewport Vulkan 1.1. Матрицы остаются одинаковыми, что важно для CPU-side model camera math и front-face winding. - Point primitives. Из-за ограничений SPIRV-Cross
POINT_LISTотображается вTRIANGLE_LIST; текущий content point rendering не использует. - Physical device. Предпочитается discrete GPU с graphics+present queue family и swapchain extension.
- Validation. При
Render.RenderDebugили debug build доступныйVK_LAYER_KHRONOS_validationпишет сообщения[VkLayer/...]черезVK_EXT_debug_utils.
Проверяйте Vulkan на машине с рабочим runtime и, для validation output, Khronos
validation layer. Запускайте visible client с
Render.ForceVulkan=True Render.RenderDebug=True; в log должно быть ноль
[VkLayer/...] errors. Vulkan SDK не является build prerequisite движка.
SDL_GPU renderer
Source/Frontend/Rendering-SDLGpu.cpp реализует opt-in backend SDL3 SDL_GPU,
который достигает Vulkan/Metal/D3D12 через vendored drivers. Он включён по
умолчанию (FO_HAVE_SDL_GPU), кроме headless-only/Web и
FO_DISABLE_SDL_GPU, но выбирается только Render.ForceSDLGpu.
Render.SDLGpuDriver может зафиксировать vulkan, metal или direct3d12, а
Render.RenderDebug включает debug mode.
Основные свойства:
- Immediate-mode поверх explicit passes.
Contextдержит не более одного open render/copy pass; passes открываются lazy, clear хранится до load-op, uploads идут через cycled transfer buffers. БлокирующееGetTextureRegionsubmits и ждёт fence.RequestTextureRegionматериализует pending clears и записывает download в текущий command buffer без такого ожидания;Present()получает общий submission fence только для buffers с readbacks.TakePixels()проверяет fence, а блокирующий flush отмечает readbacks завершённого buffer готовыми. - Backbuffer proxy. Рендеринг идёт в RGBA8 proxy, затем
Present()blit-ит его в swapchain. Это сохраняет uniform color formats и безопасные mid-frame flushes. - Pipeline cache per effect. Immutable pipelines keyed по pass, topology, наличию depth,
DisableBlending,DisableCulling. - Отдельные SDL shader flavors. SDL_GPU требует per-stage descriptor sets (vertex sampler/UBO sets 0/1, fragment sets 2/3), поэтому baker выдаёт
-spv_sdl, remapped-msl_*и[EffectInfoSdl]. Native Vulkan-spvне меняется. - Push uniforms.
SDL_PushGPU{Vertex,Fragment}UniformDataотправляет до четырёх slots per stage на каждый draw; baker проверяет limit. Public optionals effect сохраняют persistent semantics других backends. ProjBuf/MainTexBufcaller-owned при наличии. Backend auto-fills их только при_needX && !X.has_value(), затем resets лишь эти два. Это критично для 3D:ModelInstanceпередаёт model projection; без проверки skinned mesh оказался бы вне atlas. Остальные externally fed buffers также не перезаписываются и сохраняются между draws.- Общие black-map fixes.
highpSPIR-V и wrapped shader time исключают FP16 overflow иsin(large time)NaN. - Topology/orientation/depth.
POINT_LISTстановитсяTRIANGLE_LIST,IsRenderTargetFlipped() == false, ortho использует depth[0,1], depth texture —D24_UNORMилиD32_FLOAT. Max atlas size 4096 из-за отсутствия query в SDL_GPU.
Запускайте client scene с Render.ForceSDLGpu=True Render.RenderDebug=True,
проверяя видимые map и GUI без validation errors рядом с default backend. Для
driver-specific claim явно задавайте Render.SDLGpuDriver.
Render targets и клиентский мост
Source/Client/RenderTarget.h/.cpp связывают high-level drawing code с backend
textures. RenderTargetManager:
- создаёт render targets с optional depth и linear filtering;
- выделяет
RenderTextureчерезIAppRender::CreateTexture(); - сохраняет и восстанавливает предыдущий backend target при allocation/clear;
- поддерживает stack
PushRenderTarget()/PopRenderTarget(); - очищает current target;
- изменяет размеры targets;
- удаляет targets, очищает stack и умеет dump textures для диагностики.
MapView, SpriteManager, ModelSpriteFactory и ParticleSpriteFactory
используют targets для map layers, light buffers, model/particle atlas render,
hit testing и offscreen composition.
Manager владеет каждым созданным target; MapView заимствует targets карты, освещения и indoor mask и освобождает все три в OnDestroySelf(). Сначала queued sprite draws выполняются через flush, пока их текстуры живы. Перед удалением indoor mask EffectManager::ClearIndoorMaskTexture() очищает только совпадающие borrows IndoorMaskTex во всех cached effects, включая эффекты, уже не выбранные для map flush. Mask другой живой карты и окружающий target stack остаются целыми. GetRenderTargetCount() считает targets во владении manager для lifecycle diagnostics, а не backend memory или driver residency.
Source/Tests/Test_ClientEntityLifetime.cpp закрепляет повторное освобождение при default, disabled-mask и direct-draw settings (MapViewRenderTargetsAreReleasedOnDestroy), а также queued draws, cached effects, другую живую карту и outer target (MapViewDestroyClearsOnlyItsCachedIndoorMaskReferences). Hardware memory и видимая приёмка переходов между картами проверяются отдельно.
MapView::DestroyRenderTargets() используется и при выгрузке, и в construction-failure guard: удерживаемые скриптовые handles не задерживают удаление targets. Границы проверяют ClientMapUnloadReleasesRenderTargetsWithRetainedHandles и ClientMapConstructionFailureReleasesRenderTargets. Немедленное освобождение native buffers/pools описано в жизненном цикле клиента.
При загрузке локальной карты View.MapRenderTargetScale фиксирует размеры
render targets карты, освещения и indoor mask как логический размер экрана,
умноженный на этот коэффициент. Движок ограничивает размер пределом текстуры
renderer; вид, превышающий получившийся target, разбивается на несколько chunks.
Light target накладывается только на нарисованную область текущего chunk, расширенную на padding render target и запас в одну восьмую chunk для тряски и refractive sampling. MapView::GetMapCompositeRect задаёт эту границу и для обычного FlushLight, и для custom fog-slot path. При приближении карты это избавляет от композиции всего увеличенного light target, сохраняя краевые pixels, которые могут читать эффекты.
Gui::CheckHit кеширует boolean result для текущих Game.FrameTime и query
position, поскольку cursor drawing, zoom и movement могут проверять одну точку
несколько раз за frame, тогда как FindHit обходит все screen trees. Изменения
GUI activation, geometry, ordering, scroll, crop, изображения для transparent
hit и hittability инвалидируют cache. Refresh resolution и language проходит
через _RefreshPositionRecursive; setters с неизменившимся значением не
инвалидируют cache. Внутренний _Move намеренно не инвалидирует сам по себе,
поскольку Draw использует временную пару _Move каждый frame, поэтому direct
callers, меняющие persistent layout, инвалидируют явно.
Model-attached SPARK systems сохраняют уже spawned particles в simulation space, пока emitter следует attachment point модели. Non-identity root transform particle resource выбирает position+facing вместо полного bone matrix, чтобы старые particles оставались world-stable, а новые появлялись в текущей точке. Model movement offset вычитается в particle model space до camera rotation/projection; setup-time positive и draw-time negative offsets сокращаются для newly emitted particles.
Размер экрана, разрешение и letterboxing
Рендеринг использует два разных размера:
- Logical screen size —
Settings.View.ScreenWidth/ScreenHeight. В этой системе координат игра рисует_rtMain, projection и GUI/ImGui. - Backbuffer size — реальный output surface: pixels OS window, monitor fullscreen или virtual render texture embedded client.
Игра всегда рисует _rtMain в logical size. Финальный blit при
Renderer::SetRenderTarget(nullptr) растягивает его в backbuffer с сохранением
aspect ratio, центрируя и добавляя bars только при разных aspect. При равных
размерах это 1:1. _rtMain изменяется по GetScreenSize() на
screen-size-changed event. Events семантически разделены:
OnScreenSizeChanged — logical size, OnWindowSizeChanged — physical/host size.
Script offscreen surfaces (Game.ActivateOffscreenSurface /
Game.PresentOffscreenSurface) также работают в logical coordinates, потому
что scripts рисуют их при active _rtMain. Pooled targets создаются по
SpriteManager::GetScreenSize() и resize-ятся до reuse; иначе GUI effects могут
обрезать content после увеличения resolution. Active scissor stack применяется
при flush и к offscreen surfaces, поэтому cropped GUI subtree сохраняет viewport
boundary внутри эффекта.
Windowed
Window pixels и logical size поддерживаются равными. Событие
SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED вне fullscreen записывает
Settings.ScreenWidth/Height, вызывает OnWindowSizeChanged, а
OnScreenSizeChanged — только при реальном изменении settings.
Game.SetResolution(w, h) сначала меняет logical size, затем OS window, если
клиент не fullscreen и не virtual; последующее OS event не вызывает повторный
GUI/map refresh того же logical resolution.
Fullscreen (borderless desktop)
SDL_SetWindowFullscreenMode(window, nullptr) держит framebuffer равным monitor
size. Fullscreen resolution означает logical render size:
Game.SetResolution меняет logical size, а blit aspect-fits его в monitor.
Startup/toggle/pixel-size events fullscreen обновляют renderer/backbuffer, но не
перезаписывают Settings.ScreenWidth/Height и не вызывают
OnScreenSizeChanged, иначе выбранное logical resolution схлопнулось бы к
monitor size. AppWindow::ToggleFullscreen() помечает transition до SDL call,
потому что queued event может прийти при старых flags. Bars ожидаемы только при
разном aspect.
SDL_SetWindowSize не действует в fullscreen/maximized. Поэтому native client
сохраняет выбранный size как pending windowed size. При выходе из fullscreen
SpriteManager::ToggleFullscreen() применяет его и заново центрирует окно с
учётом accumulated resolution delta.
Embedded clients в multi-client host (virtual windows)
ServerApp может размещать несколько embedded clients (Single/Tile/
Cascade, Spawn Client). Каждый имеет собственный engine instance,
GlobalSettings и virtual AppWindow:
- physical virtual size (
_virtualSize,GetSize()) отделён от logical resolution (_virtualScreenSize,GetScreenSize()); - game render идёт в
_rtMainlogical size, затем aspect-fit в_virtualRenderTexphysical size; - host aspect-fit/center-ит virtual texture в display rect и обратным тем же mapping переводит input, поэтому black bars не искажают local mouse coordinates.
Resolution change обновляет settings владеющего engine, не host. Virtual
SetScreenSize хранит logical value в _virtualScreenSize, а
SpriteManager::SetScreenSize отражает его в собственные
Settings.ScreenWidth/Height до handlers. SetResolution не вызывает
SetWindowSize для virtual window и не меняет _virtualSize; host layout и
render texture не resize-ятся. В standalone один engine, поэтому settings
engine и App->Settings совпадают.
GUI re-center выполняется через OnScreenSizeChanged →
Gui::Callback_OnResolutionChanged(), заново layout-ящий screens по
Settings.View.ScreenWidth/Height; Anchor: None центрируется по parent/screen.
Таким образом, _rtMain задаёт pixels, а settings — layout coordinates.
Local-map viewport при фактическом logical size change сразу recenter-ится на
chosen critter. MapView берёт logical client size, а не physical backbuffer;
fullscreen scaling остаётся финальному blit.
Effects и shader data
Syntax .fofx, pass/render state, vertex inputs, resources, descriptors, baked
artifacts, cache identity, lifetime script values и authoring validation
принадлежат Effect Format и
сгенерированному reference. Эта
страница описывает участие effects в frontend/render pipeline.
RenderEffect владеет standard buffers:
- projection/main texture;
- transparent egg;
- sprite border;
- time/random/script values;
- camera/model/model-texture/model-animation.
EffectManager загружает minimal/default effects, выбирает script effects,
пишет script-value buffers и обновляет их per frame. Scripts задают один float
через Game.SetEffectScriptValue(...) или range через
Game.SetEffectScriptValues(effectType, effectSubtype, valueStartIndex, values,
valuesOffset = 0, valuesCount = -1). Оба API проверяют effect, наличие
ScriptValueBuf и range EFFECT_SCRIPT_VALUES.
Shader time session-relative и wrapped. TimeBuf (FrameTime.x /
GameTime.x) rebased к первому rendered frame и wrapped по 8192 s в
EffectManager::PerFrameEffectUpdate. Это animation phase, не absolute clock.
Большой steady-clock/session time ухудшает fp32 fract/hash/sin, вызывает
stepping и затем теряет frame delta. Exact wrap сохраняет granularity менее 1 ms
для любой сессии ценой phase pop примерно раз в 2,3 часа; используйте его для
noise/ambient math. Hash lattice дополнительно wrap-ится локально; script-side
accumulators через ScriptValueBuf требуют того же подхода.
При изменении effect определите владельца:
- config parsing —
Rendering.cpp; - shader loading/draw — соответствующий OpenGL, Direct3D, Vulkan или SDL_GPU backend;
- selection/update —
EffectManager; - map/client sequencing —
MapViewилиSpriteManager.
Базовые эффекты minimal profile
Движок поставляет fixed base effects в Resources/Core/Effects/ и bootstrap
effects в Resources/Embedded/Effects/. Header каждого .fofx фиксирует
назначение, slot и принцип работы.
Base shaders рассчитаны на опциональный Direct3D feature level 9.3: без
gl_FragCoord/position semantic, screen derivatives, texture-size queries и
dynamic array/vector indexing. Baker выдаёт HLSL SM4, GLSL 330, GLSL ES 300 и
Metal, затем компилирует HLSL в -dxbc; opt-in bake добавляет Aon9 только
для non-model effects. Стандартный Direct3D path требует level 10.0 или выше.
Строка Profile: minimal в header фиксирует это ограничение.
Default mapping: Font/Iface/Generic/Critter/Rain → 2D_Default;
Roof/Tile/Flat → 2D_NoDepth; Primitive → Primitive_Default;
Light → Primitive_Light; Fog → Primitive_Fog;
FlushPrimitive/FlushMap/FlushLight/FlushFog/FlushRenderTarget →
соответствующие Flush_*; SkinnedModel → 3D_Skinned; ImGui → setting
ImGuiDefaultEffect. Остальные 2D_WithoutEgg, 3D_NormalMapping,
Flush_Map_BlackWhite, Font_Default, Interface_Default, Particles_*
выбираются per draw/mesh/particle system.
FlushMap соединяет intermediate map target с готовым viewport layer. Его RGB
уже alpha-composited, поэтому Flush_Map и Flush_Map_BlackWhite пишут output
alpha 1.0: нельзя снова умножать RGB на промежуточный alpha или переносить
coverage. Project override этого slot сохраняет правило. Generic
FlushRenderTarget сохраняет RGBA для model/particle/GUI/offscreen surfaces.
Проект с richer hardware размещает собственные advanced-profile copies в pack,
baked после Core/Embedded, под тем же resource name. Проектная копия shadows
engine fallback и может использовать derivatives/per-fragment lighting, но
minimal base движка не меняется.
Depth state эффекта и общий depth buffer карты
Каждый pass .fofx задаёт:
DepthWrite(defaultTrue) →_depthWrite[pass].DepthFunc(defaultAlways) →_depthFunc[pass]:Always,Never,Less,LessEqual,Equal,GreaterEqual,Greater,NotEqual. Backends учитывают различие NDC-Z OpenGL[-1,1]и D3D/Vulkan/SDL_GPU[0,1], поэтому depth effects проверяются на каждом заявленном backend.DepthVariants(defaultFalse) →_depthVariants. Один draw может установитьRenderEffect::DepthVariant:FromEffect,TestWrite,TestNoWrite,NoTestWrite,NoTestNoWrite;Testиспользует effectDepthFunc,NoTestзаменяет его наAlways. Это позволяет particle node хранить test/write intent без отдельного effect file на каждую комбинацию.
Resolved state адресуется slot через ResolveDepthVariantSlot и
EFFECT_DEPTH_VARIANTS = 4. Direct3D заранее строит depth-stencil object per
slot, Vulkan — pipeline per (pass, primitive, blend, slot), SDL_GPU включает
slot в cache key, OpenGL применяет state напрямую. Resolver отклоняет slot,
который не был built, чтобы backend не отключил depth молча и не переиспользовал
stale pipeline. Slot кодирует resolved state, поэтому эквивалентный requested
variant допустим даже для effect без alternatives. RenderEffect::CanBatch
сравнивает DepthVariant.
Snapshot фона сцены
Refractive content не может читать target, в который пишет. Поэтому
SpriteManager::AcquireSceneBackground() копирует current target в отдельный
target-sized render target. Copy lazy и не чаще одного раза на direct-draw
replay: DrawSprites invalidates snapshot перед replay, а copy выполняется
только при запросе. Blit opaque — нужны colors позади, а не повторное blending.
Effect читает copy через RenderEffect::BackgroundTex рядом с MainTex и
IndoorMaskTex; shader объявляет BackgroundTex. Все четыре реализованных GPU
backends связывают его одинаково. Snapshot сохраняет orientation source target,
поэтому shader сам flip-ит screen lookup для flipped texture.
Particle runtime получает provider через
ParticleRuntimeServices::SceneBackgroundProvider непосредственно в draw.
Unavailable означает отсутствие scene (например atlas offscreen) и fail-close.
Deferred используется для auxiliary preview системы, которую позже рисуют
directly: distortion node пропускается без retirement, а scene draw запрашивает
provider снова.
Model-attached runtime получает provider только внутри
ModelInstance::DrawInScene. Direct model всё равно обновляет auxiliary atlas
для preview/hit test; при Render.ModelDirectDraw этот refresh возвращает
Deferred, и attachment доживает до subsequent scene replay. Обычная
atlas-rendered model возвращает Unavailable.
Face culling отдельного draw
Culling — свойство draw, не effect usage. Caller задаёт
RenderEffect::CullMode: None, Back, Front; default None.
RenderEffect::CanBatch сравнивает mode. 3D models задают Back либо None,
particle runtime переводит culling emitter node (Front, Back, Double).
Backends с baked rasterizer state создают варианты только при
CullVariants = True: Direct3D — rasterizer object per mode, Vulkan — pipeline
per (pass, primitive, blend, depth slot, cull mode), SDL_GPU — cache key,
OpenGL — immediate glCullFace. ResolveCullMode() throws при unbuilt mode,
исключая silent drop/fallback. Front face везде counter-clockwise.
MapView::_rtMap создаётся with_depth, поэтому world использует общий depth
buffer. EffectUsage::QuadSprite и Model участвуют в нём; на targets без
depth attachment (UI, light, final screen flush) state является no-op.
- Screen-space quads (GUI, fonts, render-target blits, non-map effects) начинают с
Vertex2D::PosZ = 0. Для map spriteSpriteManagerперезаписывает Z перед flush в_rtMap. - Standing map sprites (
Item,Critter) пишут depth, но не test-ят его (Always+ write в2D_Default/2D_WithoutEgg). Depth нужен direct particles/models; sprite-vs-sprite решает painter order. Их vertical planes имеют общий gradientProjectMapYToVerticalDepthи не пересекаются, поэтому order по anchor depth точно совпадает с per-pixelLessEqual, не создавая z-fighting coincident rows. MapSpriteList::MakeDrawOrderPosсортирует standing sprites поGeometryHelper::GetHexScreenRow(GetHexPos().y), то есть по классу одинаковой ground depth (+2X/-1Y), а не по raw hex row. Внутри строки ключ имеет порядок[group 8][row 24][layer 8][sub-layer 8][hex X 16]: персонаж рисуется перед любым стоящим предметом своей экранной строки, а среди предметов стены с низким sub-layer рисуются раньше. Ближние строки по-прежнему побеждают; при равном ключе действует порядок добавления.Item.DrawOrderSubLayerизменяем и публично синхронизируется: получив новое значение, клиент пересоздаёт спрайты предмета с новым ключом без перезагрузки карты. Контракт закреплёнTest_Geometry.cppиTest_MapSprite.cpp.- Depth/sort anchor — logical root, не bitmap bottom-center. Item proto
Offsetодновременно позиционирует visual bitmap и хранится как_rootOffset; depth proxy вычитает его вGetMapRootOffset()иscene_pos_y, чтобы tree anchors на trunk. Critter root offset равен нулю. - Только standing
Item/Critterработают с depth. Floor tiles, roofs и flat overlays painter-only/depth-inert.MapSpriteучитываетElevation;HexOffsetи runtime/tweak offsets проецируются по ground plane и меняют screen/depth непрерывно. Viewport-onlyfield.Offsetв world depth не входит; intrinsicSprite::Offsetопределяет logical ground root. - Floor/flat layers сохраняют atlas XY/UV и используют no-depth effects. Tiles/roofs выбирают
Effects.Tile/Effects.Roof, flat items —Effects.FlatпоGetDrawFlatten(). Для scriptMapSpriteHolderdefault effect назначаетMapViewпо draw-order segment: tile/pre-light, flat/after-light, roof или generic. - Item draw order определяется
GetDrawFlatten(), неIsScenery/IsWall: upright →Item, static flat →FlatItemPreLight, dynamic flat →FlatItemAfterLight. Старые Scenery/Item пары слоёв объединены; одинаковый hex tie-break-ится add order (_globalPos). Dead critters сохраняютEffects.Critterи depth write, но standing sprites их не test-ят и покрывают по draw order. Плоский труп остаётся ниже стоящих предметов и персонажей даже при различии экранных рядов. Створка двери и рама в одном слое и ряду упорядочиваются сначала по sub-layer, затем по X; оба случая закреплены вTest_MapSprite.cppи всё ещё требуют визуальной проверки сцены. - Flat/background layers рисуются до standing sprites с
DepthWrite=False,DepthFunc=Always; им не нужны ground projection или layer bias. Standing sprites получают per-vertexPosZчерезProjectMapYToVerticalDepth, без draw-order bias. - Единственный оставшийся layer-bias применяется direct-draw particles/models в конце sprite pass: один шаг из half-pixel budget
MAP_LAYER_DEPTH_BIAS / (DrawOrderType::Last + 1), ниже threshold subpixel snapping. CoreиEmbedded2D_Default.fofxобязаны проецироватьInPosition.xyz; flatten Z уничтожит scene depth.2D_Defaultи2D_WithoutEggdiscard final alpha<= 1/255, чтобы transparent texels не писали invisible depth.- Roofs — floor tiles с positive
Elevation = Geometry.MapRoofElevation; XY anchor задаётBaseTile.Offset. Roof rangeRoof..Lastрисуется отдельным trailing pass после всех lower layers и их direct replay.Effects.Roofне test/write depth, поэтому крыша всегда рисуется сверху и не clips subsequent content. - Particles используют
LessEqual, обычноDepthWrite=False; color variants включаютDepthVariants. Atlas variants discard alpha<= 1/255, принимаютParticleSamplingBufдля point sampling через texel-center snap и вручную clamp/repeat внутри atlas sub-rectangle. SPARK non-atlas variants передают готовые atlas coordinates и neutral sampling buffer. - Models (
3D_*) используютLessEqual+DepthWrite=True, поэтому direct mesh пишет реальную surface depth, а не плоскость atlas quad. OnRenderMap_AfterSpritesAndFogвызывается после sprite/fog pass до flush map target. Script debug markers вычисляют position изMap.GetHexMapPos,entity.GetSpriteOffset,entity.Elevationи draw-area origin, не обращаясь к transient sprite instances.- Contours принадлежат scripts проекта. Native contour pass отсутствует. Проект может cache-ить entities с ненулевым
Contourи наOnRenderMap_AfterSpritesвызыватьMap.DrawEntitySprite(entity, effectSubtype, colour, padding). Mapper selection использует тот же property-based путь.
Спрайты direct-to-scene
Sprite может переопределить IsDirectDraw() и рисовать geometry прямо в
current scene target с общим depth вместо atlas quad batch. Чтобы не разрывать
batch вокруг каждого такого sprite, SpriteManager::DrawSprites собирает их и
после общего Flush() выполняет Sprite::DrawInScene(scene_pos, depth).
Opaque sprites пишут depth (Always, write), direct transparents test-ят его
(LessEqual, no write). Anchor использует projected
hex + HexOffset + SpriteOffset/TweakOffset + Elevation, исключает viewport
field.Offset и получает только один anchor-bias step.
ParticleSprite поддерживает два типа из .spark attribute draw in scene:
- Atlas (default false):
Update()двигает simulation и по cadence обновляет offscreen atlas черезDrawParticleToAtlas; sprite рисуется batched quad,IsDirectDraw() == false. - Scene (true):
IsDirectDraw() == true; simulation продолжается вне visibility, аDrawInSceneбез advancement обновляет transform и рисует прямо в_rtMapчерез map view-proj. Particles сохраняют lifetime offscreen и depth-sort against scene.
ParticleSprite::Play() respawn-ит backend-neutral ParticleSystem до start.
Facade делегирует через ParticleRuntimeSystem, поэтому renderer-facing code не
содержит SPARK/Effekseer branches. One-shot SPARK можно повторно запускать после
Game.PlaySprite(...) и cache reuse.
Live bounds и rebasing emitted particles также являются capabilities facade.
Оба backend сообщают bounds baked extent: SPARK через .spk bounds, Effekseer
через .efk trailer. Rebasing нужен только SPARK world-space; Effekseer
композитит instance transforms с root matrix и использует no-op.
Seeded respawn детерминирован per system instance. Effekseer передаёт seed
manager handle. Каждый SparkParticleRuntimeBackend владеет SPKContext с IO
registry, default zone и ambient generator; loaded graph связывается с context.
Каждая system хранит собственный generator state и временно bind-ит его при
clone/prewarm/update, поэтому interleaved effects и разные engine instances не
влияют на sequence.
SparkExtension.h раскрывает только facade, forward declarations и plain
renderer data helpers. SPARK headers, SparkQuadRenderer и adapter скрыты в
.cpp; Mapper/Baker читают properties через helpers.
ParticleSystem::SetScale() обновляет neutral setup, применяет zero-delta
transform refresh и заставляет atlas redraw без respawn/reset elapsed time.
Контракт одинаков для atlas/scene и обоих runtimes.
Core Effekseer runtime также проходит через эти sprite/direct-scene paths.
Effekseer renderer interfaces используются как callbacks evaluated data:
FOnline копирует значения, строит RenderDrawBuffer, выбирает RenderEffect и
отправляет через обычный renderer. Никакой stock Effekseer graphics backend не
нужен.
Callback collectors fail closed на malformed topology. Они проверяют hard limit
instances и exact count из BeginRendering/BeginRenderingGroup; strip требует
chain order. Ring packets копируют outer/center/inner shape/colors, воспроизводят
8-vertex/12-index segment topology, angular fades и Z-sort, разделяя geometry
при 64 000 vertices для 16-bit indices. Для Z-sort сортируется lightweight index
permutation, затем materialize snapshots: это сохраняет deterministic stable
order и избегает invalid std::stable_sort over-aligned SIMD elements.
Distortion nodes refract scene: RG channels texture задают displacement в
plane particle, alpha маскирует форму, background берётся из snapshot. Plane per
vertex передаётся через model vertex layout tangent/bitangent; intensity и
orientation background — в reserved ParticleSamplingBuf. Поддержана только
sprite family; остальные material families отклоняются при load.
Model nodes рисуют mesh per instance. .efkmodel сначала полностью
валидируется перед vendored parser: максимум 64 MiB, 4096 frames, 64000 vertices
и 21333 faces per frame, с проверкой indices/counts. Instance transform
применяется к vertices, сохраняя общий draw-buffer/atlas/batching path. Node
culling переходит в CullMode, animated mesh выбирает frame по instance time.
Ribbon и Track используют общий strip builder. Он превращает chain
cross-sections left/center/right в два quad strips с общей center line,
растягивает texture по chain, применяет colors/atlas flags и chunks по vertex
budget. Ribbon трансформирует authored edge offsets или ориентирует их вокруг
emitter up axis для viewpoint mode. Track строит width поперёк travel direction,
усредняет interior joints и интерполирует width/color head-tail-middle.
Неподдержанные spline smoothing, tiled UV, trail smoothing, view offset,
left-handed strips, non-default Track materials и Z-sort отклоняются до build.
Positions[2]/[3] ribbon не проверяются вне spline path, потому что приходят
uninitialized.
Test_EffekseerParticleRuntime.cpp содержит self-contained cooked fixtures
реального callback-to-FOnline path: legacy determinism/fixed-step/quad/UV;
project-authored 1.80.5 sprite Z-sort; modern SKFE/1810 Ring topology, radii,
UV, all Z-sort modes и 64k chunking. Strip fixtures компилируются в test, чтобы
получить одновременно несколько instances, и закрепляют topology, shared
center, continuity, UV, width и viewpoint orientation.
Adapter принимает одну Default-material color texture либо renderer-owned white
pixel. Поддержаны Clamp и Repeat внутри atlas rectangle; Mirror rejected.
Nearest на linearly filtered atlas реализован snap к texel center. Dormant
non-zero distortion intensity при disabled material игнорируется; active
distortion всё равно проходит capability gate.
Effekseer sprites всегда scene type. Direct-scene prewarm ждёт первого
DrawInScene, чтобы получить current map transform. До него Update() не
advance-ит system; затем Effekseer проходит ровно одну секунду и reset-ит
wall-clock origin, не считая offscreen wait второй раз. RefreshRenderTransform
делает только zero-delta refresh, не forced first tick.
Flag проходит SparkQuadRenderer::GetDrawInScene() →
ParticleSystem::GetDrawInScene() → ParticleSpriteFactory::LoadSprite.
Model-bone particles (ModelInstance::RunParticle) идут другим путём и attribute
не учитывают.
ModelSprite становится direct scene при Render.ModelDirectDraw. Default
false оставляет cached atlas path. При true DrawInScene строит общий map
view-proj, включает logical root (scene_pos + raw scene depth) и вызывает
ModelInstance::DrawInScene. Skinning/animation переиспользуются, atlas-only
camera tilt пропускается, а DrawToAtlas остаётся для preview/hit test и берёт
весь automatic logical frame. Attached SPARK/Effekseer используют active direct
proj; distortion получает snapshot on demand. Старый shadow pass выключен,
поскольку его math atlas-space.
Cached model-sprite frames используют вычисленный logical limit: minimum из
Render.ModelSpriteMaxTextureWidth / Height и
AppRender::MAX_ATLAS_WIDTH / HEIGHT текущей машины, делённый на
FRAME_SCALE, потому что model рендерится в physical scratch texture с
масштабом 2x. Dynamic model-bone particle bounds выше этого budget считаются
недоступными: существующий model frame остаётся валидным, а runaway outlying
geometry обрезается. Это не позволяет malformed или long-lived particle motion
запросить unbounded CPU/GPU allocation в headless и rendered paths.
World scale. Render.ModelProjFactor задаёт screen pixels per 3D world unit
одновременно для models и in-scene particles. Engine default 40.0; проект
может согласованно переопределить. Authoring metric остаётся
1 world unit = 1 hex = 1 m, но projection factor не обязан равняться
MAP_HEX_WIDTH. Radius N units занимает N hexes и совпадает с масштабом direct
3D models. Полный neutral route описан в
Particle Format.
Практики встраиваемого проекта
Текущие проекты показывают полезные способы composition, но конкретные settings, effect names, scripts и acceptance evidence остаются project-owned:
- Оставляйте все selectors
Render.Force*выключенными в обычном shipping profile. Для диагностики и cross-backend acceptance создавайте узкие sub-configs/launch recipes ровно с одним force selector. - Не копируйте render block старого проекта. Начинайте с settings для точно pinned Engine revision и отдельно проверяйте каждый override. Backend selectors и model/layout settings менялись; старый
.fomain— migration evidence, не template. - Размещайте richer shaders проекта в resource pack после Engine
Core/Embedded. Shadow того же name допустим только как осознанная замена minimal base effect с сохранением alpha/depth/descriptor/uniform contract. Это advanced-profile overrides, а не изменение fallback движка. - Считайте
Render.ModelProjFactorединым authoring decision models и in-scene particles. Его изменение — visible content-scale migration; вместе проверяйте creatures, attachments, effects, hit areas и map occlusion. - Logical resolution policy, options settings, project effect slots, offscreen composition, contours и визуальная/accessibility приёмка принадлежат проектным docs/tests. Движок даёт mechanics, но не сертифицирует UI/art direction игры.
- Квалифицируйте каждый renderer/driver, заявленный release. Clean scene на default backend не доказывает Vulkan, SDL_GPU, WebGL или конкретный SDL driver. Фиксируйте selector, driver, platform, scene, logs и screenshot/interaction evidence.
Platform packages и связь с BuildTools
BuildTools/cmake/stages/Packages.cmake участвует в generation package targets.
Package workflow решает, какие app/runtime artifacts поставлять; доступность
backend определяется configured source, compile definitions, third-party deps и
toolchains.
Сохраняйте границы:
- frontend source определяет возможности движка;
- CMake/BuildTools определяют built apps/backends/platform packages;
- project presets выбирают конфигурации;
- platform docs объясняют debug resulting package.
Generated target names одного проекта не являются универсальными Engine names.
Тесты frontend/rendering
Минимальная engine-local проверка без GPU —
Source/Tests/Test_Rendering.cpp. NullRenderer проверяет texture
read/write/clear, upload draw buffer и effect draw, а также rejection unbuilt
depth variant. Atlas packing/dump geometry принадлежит
Test_TextureAtlas.cpp, matrix/depth projection — Test_Geometry.cpp, а
model/image/particle suites — соответствующим runtime paths.
Requested-region sections NullRenderer проверяют layout строк, snapshot в точке запроса, bounds и однократную передачу результата. В Test_ClientEngine.cpp есть ModelSpriteHitTestReadsItsMaskFromTheAtlas для повторного использования CPU-mask и её обновления после redraw. Fixture компилируется только с FO_ANGELSCRIPT_SCRIPTING; Managed-only build его не выполняет. Оба fixtures не доказывают fence timing и видимый picking на GPU backend.
Native tests доказывают backend-neutral invariants, но не реализацию GPU. Добавляйте visible target-specific route для каждого затронутого backend: Null/headless, OpenGL/WebGL, Direct3D, Vulkan, SDL_GPU. У direct Metal нет реализованного route и acceptance evidence.
Чек-лист проверки
При изменении frontend/rendering убедитесь, что:
Applicationinit работает для graphical, headless и test/tool flows;- input changes сохраняют
InputEventinvariants и mapping client script events; - touch/gamepad changes platform-neutral либо явно guarded;
- изменение проверено на affected backend: Null/headless, OpenGL/WebGL, Direct3D, Vulkan или SDL_GPU; direct Metal placeholder не считается coverage;
- render-target stack push/pop и restoration previous target сохранены;
- orientation учитывает
IsRenderTargetFlipped(): OpenGL flipped, Direct3D/Vulkan/SDL_GPU not flipped; - effect changes описывают parsing config, shader files и script-value buffers; Vulkan resources соблюдают set-0-UBO/set-1-sampler;
- Web changes связаны со сборкой, упаковкой и отладкой в браузере, Android — со сборкой, упаковкой и отладкой на Android, native/script attach/debug — с нативной, AngelScript и Managed отладкой.