40 KiB
15. Первый GPU-driven renderer Faset: данные, порядок проходов и проверка корректности
Исследование от 17 сентября 2026; проектные решения обновлены 18 сентября 2026. Это продолжение обзора UE: здесь разобран контракт одного будущего GPU-driven прототипа. Принято, реализация запланирована: собственные Vulkan 1.3 backend и Render Graph, Slang → SPIR-V, SDL3, Linux/Windows desktop 2D/3D, базовый путь без обязательного RT. Поддерживаемые модели GPU и проверенная матрица драйверов ещё не определены. Актуальные границы — в архитектуре и плане до/после MVP.
Место в плане: M2 (базовая графика MVP) использует direct renderer с CPU frustum culling, простым PBR/тенями и отдельным упорядоченным 2D-путём; обе демки должны собираться в самостоятельный Player. GPU culling, indirect draws, HZB и LOD из этого исследования начинаются после MVP, в P2, с сохранением direct reference. Сначала fixed indirect bins и frustum, затем two-pass HZB, затем обычный mesh LOD с hysteresis. Кластерный LOD и streaming — дальнейшее исследование. Минимальный Render Graph с одной очередью и корректными barriers/lifetime нужен уже MVP; pass culling, aliasing и async compute не обязательны.
Граница инструментов: MCP работает только в редакторе, включая headless authoring/import/build, Play/Stop и логи редактора. Player — отдельный процесс с отдельным окном, без MCP; чтение/изменение runtime worlds и игровых сессий через MCP исключено. Диагностика renderer и ручной GPU capture не превращаются в канал MCP-доступа к Player.
Источники привязаны к source-manifest.json: UE 5.8.2, commit 16d75d84714512edfb744e1fd0a59e9c74d57873; Godot 4.8-dev, commit 9c776068d6ed23acd0c78bfe534272d1d2a3a619. Прочитаны выбранные тела C++ и shader-функций, затем официальные Vulkan/D3D12 документы. Репозитории не изменялись. Движки и GPU-прототип не запускались; ускорения, совместимость конкретных карт и качество culling измерениями не подтверждены. «Факт» ниже относится к просмотренному коду/документу; «предложение» — к нашему проекту; гипотезы отмечены отдельно.
1. Полезный результат не требует полного GPU command processor
Факт UE. В обычном FInstanceCullingContext шаблон indexed indirect command получает index count, first index, base vertex и нулевое число instances. В GPU-буфер загружается массив этих шаблонов, затем отдельно очищается instance count. Создание шаблона, upload и clear. Shader InstanceCullBuildInstanceIdBufferCS для видимого instance атомарно увеличивает второй элемент соответствующей команды и записывает ID в выделенный этому draw диапазон. Atomic и запись, clear kernel.
При этом SubmitDrawCommands() всё ещё обходит draw batches на CPU, выбирает indirect buffer/offset и вызывает submission. Это GPU-driven выбор экземпляров, но не обещание полного исчезновения CPU draw work. Цикл submission.
Предложение post-MVP реализации. В первом GPU-driven режиме Faset CPU формирует небольшое число bins по совместимому mesh/material/pipeline; GPU решает, какие instances входят в каждый bin. На bin заранее резервируется диапазон VisibleIds, вместимость которого равна числу его кандидатов. Compute меняет только count и содержимое этого диапазона; CPU отправляет фиксированный набор indirect draws. Нулевой instanceCount означает отсутствие геометрии. Это устраняет зависимость видимости от CPU readback и не требует сначала строить scan/compact массива draw-команд.
Можно начать с drawCount=1 на batch и firstInstance=0, передавая base offset списка ID через push constant. Vulkan требует отдельную feature multiDrawIndirect для drawCount>1; ненулевой firstInstance также зависит от drawIndirectFirstInstance. vkCmdDrawIndexedIndirect, VkPhysicalDeviceFeatures. Позже multi-draw и GPU count buffer сокращают CPU submission, если профиль покажет именно это ограничение. Нельзя назвать фиксированный CPU draw loop «нулевыми draw calls».
2. Контракт данных до написания culling shader
Предложение Faset. Render extraction выдаёт immutable snapshot для конкретного renderFrameId, независимо от частоты physics/gameplay updates. В snapshot нужны:
InstanceTable: стабильный ID с generation, current/previous transform, conservative local bounds, mesh/bin ID, flags и признак корректности предыдущего состояния.MeshTableи геометрия: index ranges, vertex offsets, layout, bounds. На первом этапе все ресурсы резидентны, без streaming и LOD-переходов.ViewData: current/previous world-to-clip, projection type, viewport rectangle, near-plane convention, размеры depth/HZB и validity epoch.DrawTemplates: совместимые pipeline/material bins, неизменяемые геометрические поля и base offsets.- Рабочие буферы кадра:
MainIds/MainArgs,DeferredIds/DeferredCount,PostIds/PostArgs, debug counters. - Изображения: current depth, color/debug instance-ID target, current HZB и imported previous HZB.
Идентичность GPU instance не должна зависеть от позиции entity в уплотняемом ECS storage. При удалении/повторном использовании slot меняется generation; вновь созданный, деформированный неизвестным способом или изменивший mesh instance получает previousValid=false. Для движения хранится предыдущий отрисованный transform, а не transform последнего physics substep.
Факт UE. Обычный instance culling использует current frustum, но тест previous HZB строит из PrevLocalToTranslatedWorld и предыдущих view/projection, пропуская near-plane crossing. Он дополнительно округляет сравниваемую глубину вверх для защиты от self-occlusion из-за точности. IsInstanceVisible. Это связывает геометрию прошлого кадра с его HZB; подстановка current transform в этот тест меняет смысл алгоритма.
Новое уточнение к 07. Отдельный velocity render target и TAA не являются предпосылкой этого HZB-прототипа: просмотренный тест использует матрицы и bounds. Motion vectors понадобятся для последующих temporal effects. Сначала полезнее зафиксировать отсутствие jitter и проверить culling; затем отдельно добавить согласованное jittered/unjittered преобразование.
3. Один кадр: конкретный порядок и смысл ресурсов
Следующая последовательность — предложение, адаптирующее Nanite two-pass к обычным meshes и hardware raster. Начальный материал — opaque, без alpha test/WPO, один sample, одно view. 2D overlay и прозрачность рисуются отдельным упорядоченным путём после opaque-сцены.
- Import/Upload. Подключить previous HZB и связанный с ним
HistoryMetadata. Загрузить изменившиеся instance records и текущие view constants. Сохранить ресурсы старых submitted frames до окончания их GPU-использования. - Init. Записать templates, очистить main/post instance counts, deferred count и debug counters. Пустая сцена тоже должна иметь определённые выходы. UE отдельно очищает ID buffer, если compute был пропущен из-за отсутствия instances. Обработка нуля.
- MainCull. Сначала current frustum. Кандидат с недействительной историей сразу попадает в Main. Остальные проверяются previous bounds/view против previous HZB: предположительно закрытые попадают в Deferred, остальные — в Main. Frustum reject, Main и Deferred образуют непересекающееся разбиение входов.
- MainRaster. Hardware indexed indirect рисует Main в очищенные current depth и color/ID attachments. Если объект с прошлого кадра переехал, здесь он уже находится в текущем положении.
- BuildCurrentHZB. Построить furthest-depth pyramid из текущего результата MainRaster. Это подмножество текущих opaque occluders, а не копия предыдущей глубины.
- PostCull. Только Deferred проверяется уже current bounds/view против этого current HZB. Прошедшие instances записываются в Post. Dispatch count сначала можно оставить фиксированным верхним пределом, с проверкой индекса относительно GPU deferred count; indirect dispatch — следующая оптимизация.
- PostRaster. Нарисовать Post, сохраняя depth/color Main через load, с теми же depth convention и geometry shader-параметрами. Нельзя очистить depth между main и post.
- Export/Finish. Сохранить HZB и его metadata для будущего кадра; отрисовать остальные разрешённые категории, UI, diagnostic overlays; отправить timestamps/counters в отложенный readback.
Факт UE. Nanite добавляет main cull/raster, затем BuildHZBFurthest, подставляет полученную текстуру в culling parameters и добавляет post cull/raster. Main, построение HZB и Post. Main shader берёт предыдущие матрицы; post permutation проверяет current rectangle. FBoxCull::HZB. Очередь occluded instances хранит одновременно view и instance IDs. WriteOccludedInstance.
Почему это безопаснее previous-HZB-only. Main может избыточно нарисовать скрытый объект; depth test это разрешает. Ошибочное предположение «закрыт» не окончательно: объект получает current-frame post test. Main HZB содержит только реальные текущие occluders, поэтому неполнота этой пирамиды уменьшает отсечение, а не создаёт дополнительные заслоняющие поверхности. Это аргумент корректности при консервативных bounds/depth tests, не доказательство конкретной ещё не написанной реализации.
Что можно упростить. Для первоначальной истории допустимо экспортировать HZB после Main, не перестраивая его после Post. Он будет неполным представлением предыдущего кадра; следующий post test сохраняет путь исправления. Полный final HZB потенциально улучшит следующий main culling и пригодится другим эффектам, но это отдельный проход и гипотеза о выгоде. Измерять оба варианта. Не приписывать этот выбор конкретному UE path: UE также сохраняет scene HZB отдельным этапом. BuildHZB и extraction.
4. HZB: меньше оптимизаций, больше инвариантов
Факт UE. BuildHZB() вычисляет power-of-two размеры, обычно начиная с половинного разрешения; выделяет отдельные closest/furthest textures, создаёт UAV каждого output mip и читает нужный parent mip. Размеры и описание, mip UAV, parent SRV. В обычной shader-ветке furthest HZB получает minimum depth по четырём входам; для reversed Z visible-test использует Rect.Depth >= MinDepth. Reduction, сравнение.
Предложение. Начать с R32-float pyramid, manual min reduction и одной dispatch на mip. При reversed Z пустой depth равен far=0; включение пустого sample в minimum должно делать тест более разрешающим. Для обычного Z меняется и reduction, и сравнение — нельзя переключить только depth compare pipeline. В первой реализации использовать полное conservative покрытие projected bounds; ошибаться в сторону «видим» при near-plane crossing, неизвестной проекции или некорректной арифметике.
Выбранный mip и количество sampled texels должны покрывать весь screen rectangle. Четыре произвольных угла крупного footprint не заменяют это условие. Нечётные размеры окна, viewport с ненулевым offset и padding до power-of-two входят в математический контракт. У UE есть отдельные viewport→HZB scale/bias и размеры view/texture. InitHZBCommonParameter. Применять эти коэффициенты от текущего окна к старой пирамиде нельзя.
У Nanite оптимизированная footprint-формула прямо предполагает один центральный sample; авторы отмечают необходимость изменения для MSAA/conservative raster. GetScreenRect. Более того, отдельная ветка смешивания depth с иным VisBuffer sampling stride помечена как неконсервативная. HZB.usf, VIS_BUFFER_FORMAT 4. Это причина не переносить все permutations в стартовый shader и не объявлять любой UE helper универсальным доказательством conservative occlusion.
FP16 pyramid, gather tricks, subgroup reductions и несколько mips за dispatch отложить. Их ввод должен сохранять conservative inequality, в том числе при равных глубинах; сравнение с R32 reference станет отдельным тестом.
5. Минимальный render graph должен видеть реальные способы чтения
Факт UE. Producer описывает indirect arguments как UAV, consumer — как ERHIAccess::IndirectArgs, а instance offset stream отдельно как vertex/index access. Compute parameters, FInstanceCullingDrawParams. RDG объединяет совместимые состояния по subresources, затем отдельно создаёт texture/buffer transitions. CompilePassBarriers, CollectPassBarriers.
Предложение. Первому Faset graph достаточно фиксированного порядка на одной graphics-capable queue, declarations read/write с диапазоном mip/layer, проверки read-before-write, initial/final state imported ресурсов и генерации барьеров. Pass culling, transient aliasing и async compute не обязательны. Имена и полный журнал transitions обязательны для отладки.
Для указанной схемы выделяются разные зависимости:
- Upload/clear writes → compute reads/writes scene tables и counters.
- Compute writes
Args→ draw-indirect reads. Если compute записалVisibleIds, его потребитель — vertex shader storage read, а не indirect stage. Одна декларация «GPU buffer» этих двух назначений не выражает. - Main depth attachment writes → compute sampled reads для HZB. Source stages — early/late fragment tests, не fragment shader.
- HZB mip write → sampled read этого mip следующей dispatch; затем HZB writes → PostCull reads.
GENERALlayout не отменяет memory dependency. - PostCull writes → indirect и vertex reads; depth после HZB sampling возвращается в attachment use, color/depth Main сохраняются для Post.
Официальные Vulkan synchronization examples отдельно показывают compute→indirect и depth attachment→compute зависимости. Это подтверждает различие потребителей; полный набор access/layout нужно выводить из нашего фактического usage. Первый backend может применять консервативные барьеры, затем сужать их под validation и capture. Не копировать SkipBarrier из специализированных UE allocations без восстановления причин его безопасности.
Полезная проверка в Godot. draw_list_draw_indirect() проверяет usage flag, pipeline и индексный буфер, передаёт команду драйверу и отдельно регистрирует RESOURCE_USAGE_INDIRECT_BUFFER_READ в draw graph. Validation, команда и tracking. Для нашего API полезно также проверять весь адресуемый диапазон commands с учётом count/stride, а diagnostics должны называть pass и ресурс.
6. История — ресурс с владельцем и версией, а не texture pointer
Факт UE. QueueTextureExtraction() очищает output pointer, помечает texture extracted, регистрирует extraction и делает её culling root; без явного разрешения отключает transient extraction. При compile extracted resource получает дополнительную reference, а результат возвращается в конце graph execution. QueueTextureExtraction, reference, результат extraction. Следующий graph импортирует external texture и дедуплицирует её по underlying RHI texture. RegisterExternalTexture.
Так становится виден важный контракт: если HZB нужен только следующему кадру, он всё равно необходимый output текущего graph. Его producer нельзя удалить как «никто в этом кадре не читает». Graph completion на CPU, наличие pooled handle и фактическое завершение использования GPU — разные события. Наше правило: history slot и staging memory переиспользуются лишь после установленного порядка GPU-доступов; CPU descriptors/allocations освобождаются после соответствующего completion fence. На первом этапе никакого aliasing history с transient attachments.
Факт camera cut. UE создаёт новый FPreviousViewInfo из текущих view rect/matrices. При first frame/time reset, camera cut, большом движении либо forced visibility reset текущий view получает этот новый набор и bPrevTransformsReset; обычный кадр получает прошлый набор из ViewState. Создание, условия, выбор history. Это больше, чем bIgnoreExistingQueries, который обрабатывается отдельной веткой. Occlusion query reset. Nanite отключает two-pass, если previous HZB отсутствует. Проверка.
Предложение Faset. ViewHistory хранит {viewId, frameId, epoch, extent, viewport, projection/depth convention, matrices, HZB, completion token}. Camera cut, новая игровая сессия, несовместимые projection/size или смена view сбрасывают validity. В invalid-frame весь current-frustum набор идёт в Main; после него создаётся новая история. Resize сначала делает новые attachments, прежние остаются жить до завершения старых submissions. Первый корректный кадр важнее попытки немедленно восстановить максимальную эффективность culling.
Не делить history между editor viewport, Game view и thumbnail camera. При временном отсутствии рендера нельзя считать frame counter симуляции возрастом GPU history. ID reuse и импорт нового mesh инвалидируют историю соответствующего instance даже при прежней камере.
Godot даёт небольшой полезный образец владения: configure() обновляет параметры и очищает старые render buffers; texture cache адресуется context/name, а clear_context() освобождает ресурсы выбранного эффекта. Configure, cache, clear_context. Наш вариант должен дополнительно проверять descriptor/epoch при lookup: совпадение имени само по себе не подтверждает актуальность размеров.
7. Возможности GPU и отложенные системы
Принятый API и будущая проверка профиля. Минимальная версия API — Vulkan 1.3; собственный backend использует synchronization2/dynamic rendering. Для данного post-MVP прототипа проверять graphics+compute queue, storage buffers/32-bit atomics, indirect-buffer usage, vertex shader чтение instance data, sampled depth format и R32 storage/sampled image, limits buffers/dispatch и presentation на целевой ОС. API version — лишь часть проверки, а не обещание поддержки любой карты с Vulkan 1.3. Более старый backend/fallback одновременно не разрабатывается.
Shader toolchain — принято, ещё не интегрировано. Один закреплённый Slang compiler в editor/cook выдаёт SPIR-V и метаданные Faset для bindings/layout; совместимый HLSL допускается в пределах проверенного подмножества. Player получает готовые .spv и metadata, без shader compiler. Создание GPU pipelines остаётся работой драйвера; готовый SPIR-V не устраняет эту стоимость. Согласованные matrix/buffer layouts, отражение параметров и совместимость интерфейса при замене shader проверяет наш движок. Hot reload shader — отдельная реализация Faset, не автоматическое свойство выбранного языка. См. Slang: SPIR-V target и Khronos: pipeline cache.
Mesh shaders, RT, 64-bit image atomics, sparse residency, bindless descriptor indexing и async compute не нужны предложенному fixed-material hardware prototype. Их следует запрашивать только для конкретного следующего механизма. Material bins сначала могут bind-иться CPU; GPU-generated commands не отменяют pipeline compatibility.
D3D12 подтверждает переносимость самой идеи, но не бинарного backend-контракта: ExecuteIndirect читает аргументы согласно заранее созданной command signature, часть состояния наследуется от command list, а затронутые root/vertex/index bindings после выполнения имеют документированные reset rules. Microsoft: Indirect Drawing. Поэтому не закладывать в общий API возможность «любые state changes из GPU» только потому, что один backend имеет соответствующую command signature. D3D12 здесь источник сравнения, не второй обещанный renderer.
Полный visibility-buffer material resolve откладывается. Для проверки occlusion достаточно hardware depth плюс целочисленный instance-ID debug attachment и простое shading. Debug ID не равен Nanite visbuffer: нет восстановления triangle barycentrics, derivatives, material bins или общего SW/HW atomic winner. Также откладываются streaming, clusters/LOD, masked materials, skinning/WPO, MSAA, temporal upscaling и transparent compaction. Последнее особенно важно для 2D: unordered append не сохраняет авторский порядок слоёв.
8. Проверочные сцены и критерий завершения
Это план измерений, не результаты. Baseline — та же geometry/material/raster pipeline, но без HZB; фиксируются scene snapshot, camera path, seed, разрешение, GPU, driver и flags. Сравнивать финальную depth/instance-ID картинку после Post; у coplanar ties допустим другой победивший ID, но не дырка или неверная глубина. Culling debug показывает причины: outside frustum, history-invalid, main-visible, deferred, post-visible, post-occluded.
- Пусто / один объект / capacity boundary. Нулевые args, без чтения мусора; заполнение каждого bin до предела; сброс counts каждый кадр. При overflow запрещено лишь ограничить запись, оставив indirect count больше capacity. Initial design резервирует worst-case диапазоны, debug проверяет counts и canaries.
- Стена и дверь. За стеной плотная сетка meshes; дверь резко открывается, удаляется и телепортируется. Открытые объекты должны появиться в том же итоговом кадре через Post.
- Камера. Поворот, teleport/cut, переключение perspective/orthographic, near-plane crossing и камера внутри bounds. Проверять первый кадр без истории отдельно.
- Размеры и представления. Нечётные width/height, смещённый viewport, resize/minimize/restore, два viewport разных размеров. Отсутствие чужой или устаревшей history — критерий корректности, не только производительности.
- Жизненный цикл. Массовые spawn/despawn, reuse GPU slot, повторный импорт mesh, несколько frames in flight. Проверять generation, delayed deletion и предыдущие transforms.
- Контрольная открытая сцена. Почти все instances видны. Здесь дополнительный HZB/Post overhead может сделать renderer медленнее baseline; этот результат должен оставаться видимым, а не исключаться из отчёта.
Собирать CPU extraction/upload/submission, GPU времена каждого прохода, количество Main/Deferred/Post, counts по bins, peak memory и invalidation reasons. Readback выполнять асинхронно; profiler не должен каждый кадр ждать GPU и тем самым менять исследуемую нагрузку. Разработчик сравнивает capture bundle с настройками, изображениями, counters и diagnostics средствами отладки renderer; это не MCP endpoint к игровому процессу. MCP получает только предусмотренные операции и логи редактора, без runtime session/world inspection. Профиль без ошибок validation — необходимая, но не достаточная проверка консервативности HZB.
Последовательность прототипа: direct reference → GPU frustum и fixed indirect bins → current HZB визуализация → two-pass с history reset → adversarial scenes → профиль. Только затем выбирать следующую затрату: multi-draw compaction, final HZB, FP16/mip batching или geometry LOD. Гипотеза ускорения на закрытой массовой сцене остаётся открытой до сравнения полного кадра.
Новые находки и фактический охват
Главное дополнение к 07: фиксированные CPU draw templates совместимы с GPU instance visibility; velocity target не требуется для первого HZB; history extraction — output/lifetime контракт; camera reset заменяет весь previous-view набор; indirect и vertex reads требуют разных зависимостей; неполный Main HZB может быть начальным вариантом истории с измеряемой ценой в эффективности.
Прочитаны тела UE FInstanceCullingContext создания buffers/submission, InstanceCullBuildInstanceIdBufferCS, ClearIndirectArgInstanceCountCS, Nanite FBoxCull::HZB, WriteOccludedInstance и main/HZB/post orchestration; BuildHZB, HZBBuildCS и HZB parameter helpers; previous-view setup/reset в SceneVisibility; RDG import/extraction, barrier compile/collection и resource reference handling. В Godot — indirect draw validation/tracking и named render-buffer creation/configuration/cleanup. В интернете — официальные Vulkan indirect/features/synchronization и Microsoft D3D12 indirect signatures. В этом исследовании не проверялись формат/driver capability matrix и runtime-поведение. Позднейшие проверки базового Faset renderer публикуются отдельно в docs/validation; они не подтверждают GPU-driven техники этого раздела.