133 lines
40 KiB
Markdown
133 lines
40 KiB
Markdown
# 15. Первый GPU-driven renderer Faset: данные, порядок проходов и проверка корректности
|
||
|
||
Исследование от 17 сентября 2026; проектные решения обновлены 18 сентября 2026. Это продолжение [обзора UE](07-unreal-graphics-source-study.md): здесь разобран контракт одного будущего GPU-driven прототипа. **Принято, реализация запланирована:** собственные Vulkan 1.3 backend и Render Graph, Slang → SPIR-V, SDL3, Linux/Windows desktop 2D/3D, базовый путь без обязательного RT. Поддерживаемые модели GPU и проверенная матрица драйверов ещё не определены. Актуальные границы — в [архитектуре](../ARCHITECTURE.md) и [плане до/после MVP](../../PLAN.md).
|
||
|
||
**Место в плане:** 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](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. [Создание шаблона](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/InstanceCulling/InstanceCullingContext.cpp#L278), [upload и clear](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/InstanceCulling/InstanceCullingContext.cpp#L817). Shader `InstanceCullBuildInstanceIdBufferCS` для видимого instance атомарно увеличивает второй элемент соответствующей команды и записывает ID в выделенный этому draw диапазон. [Atomic и запись](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/InstanceCulling/BuildInstanceDrawCommands.usf#L346), [clear kernel](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/InstanceCulling/BuildInstanceDrawCommands.usf#L372).
|
||
|
||
При этом `SubmitDrawCommands()` всё ещё обходит draw batches на CPU, выбирает indirect buffer/offset и вызывает submission. Это GPU-driven выбор экземпляров, но не обещание полного исчезновения CPU draw work. [Цикл submission](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/InstanceCulling/InstanceCullingContext.cpp#L1764).
|
||
|
||
**Предложение 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](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdDrawIndexedIndirect.html), [VkPhysicalDeviceFeatures](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFeatures.html). Позже 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](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/InstanceCulling/BuildInstanceDrawCommands.usf#L197). Это связывает геометрию прошлого кадра с его 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-сцены.
|
||
|
||
1. **Import/Upload.** Подключить previous HZB и связанный с ним `HistoryMetadata`. Загрузить изменившиеся instance records и текущие view constants. Сохранить ресурсы старых submitted frames до окончания их GPU-использования.
|
||
2. **Init.** Записать templates, очистить main/post instance counts, deferred count и debug counters. Пустая сцена тоже должна иметь определённые выходы. UE отдельно очищает ID buffer, если compute был пропущен из-за отсутствия instances. [Обработка нуля](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/InstanceCulling/InstanceCullingContext.cpp#L956).
|
||
3. **MainCull.** Сначала current frustum. Кандидат с недействительной историей сразу попадает в Main. Остальные проверяются previous bounds/view против previous HZB: предположительно закрытые попадают в Deferred, остальные — в Main. Frustum reject, Main и Deferred образуют непересекающееся разбиение входов.
|
||
4. **MainRaster.** Hardware indexed indirect рисует Main в очищенные current depth и color/ID attachments. Если объект с прошлого кадра переехал, здесь он уже находится в текущем положении.
|
||
5. **BuildCurrentHZB.** Построить furthest-depth pyramid из **текущего результата MainRaster**. Это подмножество текущих opaque occluders, а не копия предыдущей глубины.
|
||
6. **PostCull.** Только Deferred проверяется уже current bounds/view против этого current HZB. Прошедшие instances записываются в Post. Dispatch count сначала можно оставить фиксированным верхним пределом, с проверкой индекса относительно GPU deferred count; indirect dispatch — следующая оптимизация.
|
||
7. **PostRaster.** Нарисовать Post, сохраняя depth/color Main через load, с теми же depth convention и geometry shader-параметрами. Нельзя очистить depth между main и post.
|
||
8. **Export/Finish.** Сохранить HZB и его metadata для будущего кадра; отрисовать остальные разрешённые категории, UI, diagnostic overlays; отправить timestamps/counters в отложенный readback.
|
||
|
||
**Факт UE.** Nanite добавляет main cull/raster, затем `BuildHZBFurthest`, подставляет полученную текстуру в culling parameters и добавляет post cull/raster. [Main](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/Nanite/NaniteCullRaster.cpp#L7003), [построение HZB и Post](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/Nanite/NaniteCullRaster.cpp#L7021). Main shader берёт предыдущие матрицы; post permutation проверяет current rectangle. [FBoxCull::HZB](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/Nanite/NaniteCullingCommon.ush#L623). Очередь occluded instances хранит одновременно view и instance IDs. [WriteOccludedInstance](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/Nanite/NaniteInstanceCulling.usf#L169).
|
||
|
||
**Почему это безопаснее 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](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp#L582).
|
||
|
||
## 4. HZB: меньше оптимизаций, больше инвариантов
|
||
|
||
**Факт UE.** `BuildHZB()` вычисляет power-of-two размеры, обычно начиная с половинного разрешения; выделяет отдельные closest/furthest textures, создаёт UAV каждого output mip и читает нужный parent mip. [Размеры и описание](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/SceneTextureReductions.cpp#L130), [mip UAV](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/SceneTextureReductions.cpp#L201), [parent SRV](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/SceneTextureReductions.cpp#L331). В обычной shader-ветке furthest HZB получает minimum depth по четырём входам; для reversed Z visible-test использует `Rect.Depth >= MinDepth`. [Reduction](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/HZB.usf#L247), [сравнение](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/Nanite/NaniteHZBCull.ush#L195).
|
||
|
||
**Предложение.** Начать с 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](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/HZB.cpp#L13). Применять эти коэффициенты от текущего окна к старой пирамиде нельзя.
|
||
|
||
У Nanite оптимизированная footprint-формула прямо предполагает один центральный sample; авторы отмечают необходимость изменения для MSAA/conservative raster. [GetScreenRect](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/Nanite/NaniteHZBCull.ush#L80). Более того, отдельная ветка смешивания depth с иным VisBuffer sampling stride помечена как неконсервативная. [HZB.usf, VIS_BUFFER_FORMAT 4](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Shaders/Private/HZB.usf#L235). Это причина не переносить все 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](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/InstanceCulling/InstanceCullingContext.cpp#L582), [FInstanceCullingDrawParams](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Public/InstanceCulling/InstanceCullingContext.h#L33). RDG объединяет совместимые состояния по subresources, затем отдельно создаёт texture/buffer transitions. [CompilePassBarriers](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/RenderCore/Private/RenderGraphBuilder.cpp#L3783), [CollectPassBarriers](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/RenderCore/Private/RenderGraphBuilder.cpp#L3885).
|
||
|
||
**Предложение.** Первому 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. `GENERAL` layout не отменяет memory dependency.
|
||
- PostCull writes → indirect и vertex reads; depth после HZB sampling возвращается в attachment use, color/depth Main сохраняются для Post.
|
||
|
||
Официальные [Vulkan synchronization examples](https://docs.vulkan.org/guide/latest/synchronization_examples.html) отдельно показывают 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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/servers/rendering/rendering_device.cpp#L6288), [команда и tracking](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/servers/rendering/rendering_device.cpp#L6370). Для нашего 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](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/RenderCore/Public/RenderGraphBuilder.inl#L447), [reference](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/RenderCore/Private/RenderGraphBuilder.cpp#L1373), [результат extraction](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/RenderCore/Private/RenderGraphBuilder.cpp#L2202). Следующий graph импортирует external texture и дедуплицирует её по underlying RHI texture. [RegisterExternalTexture](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/RenderCore/Private/RenderGraphBuilder.cpp#L1097).
|
||
|
||
Так становится виден важный контракт: если 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. [Создание](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/SceneVisibility.cpp#L5472), [условия](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/SceneVisibility.cpp#L5487), [выбор history](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/SceneVisibility.cpp#L5543). Это больше, чем `bIgnoreExistingQueries`, который обрабатывается отдельной веткой. [Occlusion query reset](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/SceneVisibility.cpp#L5572). Nanite отключает two-pass, если previous HZB отсутствует. [Проверка](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/Renderer/Private/Nanite/NaniteCullRaster.cpp#L4088).
|
||
|
||
**Предложение 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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp#L155), [cache](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp#L328), [clear_context](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.cpp#L496). Наш вариант должен дополнительно проверять 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](https://github.com/shader-slang/slang/blob/master/docs/user-guide/a2-01-spirv-target-specific.md) и [Khronos: pipeline cache](https://docs.vulkan.org/guide/latest/pipeline_cache.html).
|
||
|
||
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](https://learn.microsoft.com/en-us/windows/win32/direct3d12/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.
|
||
|
||
1. **Пусто / один объект / capacity boundary.** Нулевые args, без чтения мусора; заполнение каждого bin до предела; сброс counts каждый кадр. При overflow запрещено лишь ограничить запись, оставив indirect count больше capacity. Initial design резервирует worst-case диапазоны, debug проверяет counts и canaries.
|
||
2. **Стена и дверь.** За стеной плотная сетка meshes; дверь резко открывается, удаляется и телепортируется. Открытые объекты должны появиться в том же итоговом кадре через Post.
|
||
3. **Камера.** Поворот, teleport/cut, переключение perspective/orthographic, near-plane crossing и камера внутри bounds. Проверять первый кадр без истории отдельно.
|
||
4. **Размеры и представления.** Нечётные width/height, смещённый viewport, resize/minimize/restore, два viewport разных размеров. Отсутствие чужой или устаревшей history — критерий корректности, не только производительности.
|
||
5. **Жизненный цикл.** Массовые spawn/despawn, reuse GPU slot, повторный импорт mesh, несколько frames in flight. Проверять generation, delayed deletion и предыдущие transforms.
|
||
6. **Контрольная открытая сцена.** Почти все 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-поведение пока не проверялись.
|