Draft: Feature/add vulkan directx - #25
Draft
8bitniksis wants to merge 45 commits into
Draft
Conversation
- docs/: index, INSTALL, PROJECT_STRUCTURE, SYSTEM_DESIGN, DEV_RULES, USER_RULES, SKILLS, IMPLEMENTATION_PLAN - .claude/skills/cpp-style: скилл по CodingConvention.md со спецификой SGCore - зафиксированы расхождения README с фактическими CMake-пресетами и состояние тестов/CI
- IMPLEMENTATION_PLAN: 5 этапов — ревизия RHI и шейдеров, Vulkan, DX12, C-ABI граница ядро/редактор, WPF-редактор - зафиксировано фактическое состояние Graphics/API (GL-образный RHI, Vulkan-скелет 2023 г. без зависимости vulkan, DirectX отсутствует) - открытые вопросы: DX12 vs DX11, судьба GL-бэкендов и редакторов v1/v2, редактор под Linux, первоисточник шейдеров - SYSTEM_DESIGN: разделы «Рендер» и «Система плагинов» синхронизированы с целевым направлением
- этап 5 переработан: слой биндингов SungearEngine.NET как отдельная сборка, вьюпорт через NativeControlHost + Vulkan, Avalonia/WPF — вопрос №3 - новый этап 6: хостинг CoreCLR, скрипт как ECS-компонент, hot reload, сериализация полей, интероп-дисциплина - новый открытый вопрос: GPL-3.0 и лицензия скриптингового API - риски: GC/болтливый интероп; DX12 выведен из критического пути редактора - SYSTEM_DESIGN синхронизирован
- LICENSE заменён на Apache License 2.0, правообладатель — Pixelfield and Sungear Engine contributors - NOTICE: копирайт и фиксация, что релизы ≤ 0.14.0.8 остаются под GPL-3.0 - README: раздел License обновлён - IMPLEMENTATION_PLAN: вопрос №6 закрыт (решение 2026-08-16), требуется явное согласие всех четырёх правообладателей до мержа
- RHI_AUDIT.md: изоляция GL полная (244 вызова внутри бэкенда), 8 GL-измов по стоимости, охват ~16 подсистем рендера - SGSL-транслятор — точка добавления SPIR-V-таргета - выводы для проектирования RHI: command lists, PSO-кеш, дескрипторные наборы, фасад совместимости - задача 1.1 закрыта в IMPLEMENTATION_PLAN, документ добавлен в index
- RHI_DESIGN.md: IDevice, ICommandList, ISwapchain, PipelineStateDesc поверх существующих RenderState/BlendingState/MeshRenderState - модель биндинга: 4 набора по частоте + push constants, раскладка из рефлексии SGSL - шейдерный пайплайн: SGSL → GLSL → SPIR-V (+ DXIL для DX12) - фасад совместимости и порядок миграции проходов - открытые вопросы: dynamic_rendering, ARB_gl_spirv, минимальные версии API - задача 1.2 → «на утверждении»; ссылки из SYSTEM_DESIGN и index
- IMPLEMENTATION_PLAN: вопрос №2 решён — GL46/GLES остаются навсегда, по модели Unigine/Unity; смоук-сцена гоняется на GL при каждом изменении RHI - RHI_DESIGN: ограничение «в интерфейс не попадает то, что GL не исполнит»; capability-флаги в DeviceProperties; рантайм-выбор бэкенда с откатом и принудительным GL; фасад временный, GL-бэкенд постоянный - вопрос дизайна №5: один GL46 на ПК, GL4 удаляется при миграции
- GAPIType: SG_API_TYPE_DX12, хелперы isOpenGLAPI/isExplicitAPI/to-from string - GAPISelector: платформенный список предпочтения, пропуск недоступных бэкендов, принудительный выбор через SG_GAPI, откат с логом - CoreMain: хардкод GL4Renderer заменён на GAPISelector::selectRenderer() - Window/ImGuiLayer/GPUDeviceInfo: сравнения диапазонов enum заменены хелперами, добавлен DX12 в switch'и - INSTALL: Java как зависимость сборки (ANTLR4), раздел про SG_GAPI - IMPLEMENTATION_PLAN: 1.3 → код готов, 1.3a — рантайм-откат с пересозданием окна (в 2.2)
- Tests/Smoke → SGSmokeTest: PBR-тела, CSM-тени, атмосфера, прозрачность, SSAO; захват кадра в PNG, сравнение с эталоном, --gapi/--reference/--frames - IFrameBuffer::readAttachmentPixels() — полный readback attachment'а (GL) - RHI_DESIGN: copyTextureToBuffer как обязательная операция ICommandList - INSTALL §9, PROJECT_STRUCTURE: описание таргета и кодов выхода
|
|
8bitniksis
marked this pull request as ready for review
August 16, 2026 19:22
…or проверен - Tests/Smoke → SGSmokeTest: PBR-тела, атмосфера, прозрачность, SSAO; захват кадра в PNG, сравнение с эталоном, --gapi/--reference/--frames - IFrameBuffer::readAttachmentPixels(): полный readback в родном формате attachment'а (AttachmentReadback), реализация GL4; конвертация в RGBA8 на стороне теста - эталон Tests/Smoke/references/smoke_gl4_1920x1080.png (GL4 детерминирован) - находки: GL46Renderer сломан (чёрный кадр), тени только от Batch, Controllable3D перезаписывает поворот камеры — в плане 1.6a/1.6b - RHI_DESIGN: copyTextureToBuffer в родном формате; вопрос №5 уточнён - INSTALL: java нужен при обычной сборке; эталон привязан к разрешению
- форматы attachment'ов и readback (не предполагать RGBA8), SGG*-enum'ы глобальные - GAPISelector/SG_GAPI, состояние бэкендов (GL4 рабочий, GL46 сломан, Vulkan скелет) - Controllable3D перезаписывает поворот камеры, фреймбуфер = монитор, тени только от Batch - корпус SGSL и его Vulkan-несовместимости; смоук-тест; грабли IMaterial/AssetManager - каталог SKILLS, DEV_RULES, index обновлены
…корпуса - SGSLEVulkanizer: свободные юниформы → std140-блок SGLegacyUniforms (имена сохраняются, тела шейдеров не меняются), set/binding для сэмплеров/UBO/TBO с сохранением явных, #if-условия членов, замена gl_FragColor/gl_VertexID; биндинги едины для всех стадий программы - Tests/Shaders → SGShadersTest: юнит-снипеты + прогон корпуса с дампом (24 файла, 529 юниформов, 0 ошибок) - находка: instancing.sgshader инклудит несуществующий файл (план 1.4c) - RHI_DESIGN, INSTALL, IMPLEMENTATION_PLAN, скилл sgcore-graphics обновлены
…rv-reflect - vcpkg.json: glslang[tools] ≥ 15.1.0, spirv-reflect ≥ 1.4.309.0; подключены в корневом CMake, SGCore и SungearEngineInclude.cmake - Graphics/SPIRV: SPIRVCompiler (glslang, Vulkan 1.3, все стадии в одном TProgram с mapIO) и ShaderReflection (SPIRV-Reflect: биндинги, члены блоков, вершинные входы, push constants; объединение по стадиям) - SGSLEVulkanizer: пер-стадийные legacy-блоки с именем экземпляра и переписыванием обращений; блок не раньше struct-типов членов; предупреждение о макро-размерах массивов - Tests/Shaders: юнит-тест компиляции+рефлексии, режим --spirv для корпуса — 23/23 программ движка компилируются в SPIR-V - RHI_DESIGN, INSTALL, PROJECT_STRUCTURE, IMPLEMENTATION_PLAN, скилл обновлены
- GL46Renderer::createShader добавляет SG_GLSL4 (без него стадии пустые → чёрный кадр); версия "460 core" - GL46Texture2D (заглушка 2023 г., GL_INVALID_ENUM) удалён — наследуется рабочий GL4Texture2D - GAPISelector: GL46 перед GL4 в списке предпочтения - смоук gl46 = gl4 попиксельно; эталон переименован в smoke_gl_1920x1080.png - IMPLEMENTATION_PLAN 1.6b, RHI_DESIGN вопрос №5, INSTALL, скилл обновлены
- GL46Renderer::createShader добавляет SG_GLSL4 (без него стадии пустые → чёрный кадр); версия "460 core" - GL46Texture2D (заглушка 2023 г., GL_INVALID_ENUM) удалён — наследуется рабочий GL4Texture2D - GAPISelector: GL46 перед GL4 в списке предпочтения - смоук gl46 = gl4 попиксельно; эталон переименован в smoke_gl_1920x1080.png - IMPLEMENTATION_PLAN 1.6b, RHI_DESIGN вопрос №5, INSTALL, скилл обновлены
- Graphics/RHI: IDevice, ICommandList, ISwapchain, IGPUBuffer, IShaderProgram, IPipelineState/PipelineStateDesc (хеш для кеша), IDescriptorSet, RHITypes - GL/GL46/RHI: GL46Device, GL46CommandList (immediate), GL46PipelineState (VAO из VertexInputDesc, DSA), GL46GPUBuffer, GL46ShaderProgram (рефлексия через program interface query), GL46DescriptorSet, GL46Swapchain - IRenderer::getDevice(); GL46Renderer создаёт GL46Device в init() - SGSLEVulkanizer: Target::OPENGL — единый шейдерный путь GL46/Vulkan - Tests/RHI → SGRHITest: треугольник через RHI, UBO по рефлексии, readback — точные пиксели, PASS на GL 4.6 - RHI_DESIGN («Состояние реализации»), INSTALL, PROJECT_STRUCTURE, план, скилл
- RHIShaderLoader: .sgshader → SGSL → дефайны бэкенда → вулканизатор под диалект устройства → IShaderProgram (+SPIR-V для explicit API) - ScreenBlit: квад, PSO, UBO по рефлексии, дескрипторы; GL46Renderer:: renderTextureOnScreen идёт через него с откатом на legacy - IRenderer::readScreenPixels(); AttachmentReadback → отдельный заголовок - SGRHITest: экран = текстура байт в байт; смоук gl46 — 0 % расхождений - IMPLEMENTATION_PLAN 1.5c (шаг 1), RHI_DESIGN, скилл обновлены
…LSL, юниформы через UBO
- GL46Shader (m_useRHIUniforms, включает GL46Renderer): стадии проходят
SGSLEVulkanizer (OPENGL, биндинги с 8), после линковки — рефлексия
программы, UBO на каждый SGLegacyUniforms_<stage>; useX("имя") →
glNamedBufferSubData по offset/stride, name[i] поддержан, запись во все
блоки стадий; сэмплеры по-прежнему через glUniform
- SGSLEVulkanizer: Report::m_defaults — инициализаторы свободных юниформов;
GL46Shader вычисляет их (литералы, vecN(...)) как значения по умолчанию
- GL46ShaderProgram::reflectProgram — статическая рефлексия GL-программы,
stride элемента массива в BlockMember::m_paddedSize
- смоук gl46 — 0 % расхождений, gl4 — PASS, SGRHITest — PASS
- IMPLEMENTATION_PLAN 1.5c шаг 2, RHI_DESIGN, скилл обновлены
…, ICommandList - IVertexBuffer: addAttribute записывает AttributeDesc, бэкенды переопределяют addAttributeImpl; getAttributes() — источник VertexInputDesc для RHI - IMeshData::m_rhi — ленивое RHI-зеркало (вершины/цвета/индексы, VertexInputDesc), сбрасывается в prepare()/destroy(); геттеры legacy-буферов - GL46ProgramBase + GL46LegacyProgram: программа legacy-шейдера как IShaderProgram для PSO; GL46Shader::getRHIProgram(), bind() регистрирует текущий шейдер - GL46Renderer::renderMeshData: PSO (программа + кешированные состояния + раскладка) и draw через ICommandList; откат на legacy VAO - смоук gl46 — 0 %, gl4 — PASS, SGRHITest — PASS - INSTALL/скилл: ABI-грабля «Run-Time Check Failure Pixelfield-ru#2» — пересобирать все exe - IMPLEMENTATION_PLAN 1.5c шаг 3, RHI_DESIGN обновлены
…на ICommandList - IVertexBuffer/IIndexBuffer::getNativeHandle(); GL46GPUBuffer — не владеющий режим (adopted handle) для legacy-буферов - GL46Renderer::renderArray/renderArrayInstanced: PSO из записанных атрибутов буферов IVertexArray (слот на буфер, perInstance по divisor), обёртки кешируются по хендлу; откат на legacy при отсутствии шейдера - смоук gl46 — 0 %, gl4 — PASS, SGRHITest — PASS - IMPLEMENTATION_PLAN 1.5c шаг 4, RHI_DESIGN, скилл обновлены
- IFrameBuffer::getNativeHandle(); ICommandList::clearColorAttachment / clearDepthStencil (mid-pass clear); GL46CommandList::beginRenderPass биндит FBO по хендлу, ставит draw buffers, viewport по правилам legacy; endRenderPass восстанавливает viewport окна - GL46FrameBuffer: legacy bind/bindAttachmentsToDrawIn/clear/unbind → beginRenderPass/clear*/endRenderPass; смена draw buffers = re-begin (LOAD) - GL46Renderer::createFrameBuffer → GL46FrameBuffer; общий командный список - смоук gl46 — 0 %, gl4 — PASS, SGRHITest — PASS - IMPLEMENTATION_PLAN 1.5c шаг 5, RHI_DESIGN, скилл обновлены
…ulkan - RHI_DESIGN: решение по текстурам (таблица юнитов → IDescriptorSet по рефлексии в explicit-бэкенде, проходы не меняются); итог 1.5c на GL46 - IMPLEMENTATION_PLAN: 1.5c шаг 6, задача 2.3 расширена фасадами - скилл: паттерны юнит-модели, запрет смешивания с биндингом по рефлексии
…мбуферов Этап 2, задачи 2.1/2.2 закрыты, 2.3 частично. Зависимости (2.1): vulkan-headers, vulkan-loader (не Android), vulkan-memory-allocator в vcpkg.json; подключены в корневом CMake, SGCore/CMakeLists.txt и SungearEngineInclude.cmake. volk не нужен — loader линкуется напрямую (Vulkan::Loader). Инициализация (2.2): VulkanContext — instance 1.3, VK_LAYER_KHRONOS_validation в Debug, surface через GLFW, выбор GPU с graphics+present, device с dynamicRendering/synchronization2/VK_EXT_depth_clip_control, VMA. VulkanSwapchain — FIFO, ленивый acquire, present через Window::swapBuffers, 2 кадра в полёте. RHI на Vulkan (2.3): VulkanDevice, VulkanGPUBuffer, VulkanTexture, VulkanShaderProgram (SPIR-V + layout'ы по рефлексии), VulkanPipelineState (варианты по форматам прохода), VulkanDescriptorSet (транзиентные наборы), VulkanCommandList (dynamic rendering, отдельный transfer-буфер для аплоадов внутри прохода). Фасады VkTexture2D и VkFrameBuffer. Остальные Vk* — заглушки. Порядок завершения процесса (падало abort() уже после PASS теста, в статической деструкции): IRenderer::shutdown() в конце CoreMain::startCycle, реестр живых GPU-ресурсов в VulkanContext, VkRenderer::getLiveDevice() для фасадов вместо getInstance(), AudioDevice::shutdown(), FontsManager чистит шрифты до deinitializeFreetype, Logger::log проверяет spdlog-логгер до использования. Offscreen-проходы идут без флипа viewport'а (память как на GL), флип и инверсия front face — только в проходах в окно. Vulkan держится вне дефолтного списка GAPISelector до готовности legacy-фасадов (доступ через SG_GAPI=vulkan / --gapi vulkan). Проверено: SGRHITest --gapi vulkan — PASS, exit 0, ноль ошибок validation, пиксели совпадают с GL46 байт в байт; SGRHITest --gapi gl46 — PASS; SGSmokeTest --gapi gl46 — 0.000 %; SGShadersTest — PASS. Документация: RHI_DESIGN (раздел «Vulkan-бэкенд»), INSTALL (Vulkan SDK, RenderDoc, запуск тестов), IMPLEMENTATION_PLAN (статусы 2.1–2.3, 2.5), скилл sgcore-graphics.
Задача 2.3, первый пункт. VkShader: SGSL → вулканизатор (Target::VULKAN) → SPIR-V → VulkanShaderProgram; UBO на каждый SGLegacyUniforms_<stage> (host-visible, запись по offset/stride из рефлексии, name[i], дефолты из инициализаторов); useX() из проходов работает без их изменения. Все шейдеры движка компилируются на Vulkan. Таблица юнитов (RHI/VulkanTextureUnits): useTextureBlock(name, U) даёт половину «сэмплер → юнит», VkTexture2D::bind(U) — «юнит → текстура»; buildDescriptorSet() соединяет их по рефлексии. VkFrameBuffer::bindAttachment делегирует в текстуру, как GL4FrameBuffer. VulkanDescriptorSet получил setVulkanTexture. Исправления: - ShaderReflection::m_paddedSize — страйд ОДНОГО элемента массива; SPIR-V брал padded_size всего члена, из-за чего name[i] писал за пределы блока (280 ошибок на смоуке). Смысл поля зафиксирован в заголовке. - Аплоад текстуры сайзился по CPU-каналам, а копия — по VkFormat: добавлен VulkanTypesCaster::formatTexelSize, несходящийся аплоад отклоняется с диагностикой вместо невалидного vkCmdCopyBufferToImage. - VulkanShaderProgram и VulkanPipelineState держали VulkanDevice& и падали в деструкторе после смерти устройства (шейдер — ассет, живёт дольше рендерера): переведены на shared_ptr<VulkanContext> + реестр ресурсов. - ICubemapTexture в IDescriptorSet.h объявлялся как class, а это struct — MSVC мангирует по-разному, ошибка линковки. Проверено: SGRHITest vulkan/gl46 — PASS, exit 0; SGSmokeTest gl46 — 0.000 %; SGShadersTest — PASS; SGSmokeTest vulkan — ноль ошибок validation, сцена доходит до рендер-проходов (дальше нужен VkUniformBuffer).
8bitniksis
marked this pull request as draft
August 17, 2026 09:45
Задача 2.3, продолжение. VkUniformBuffer над host-visible RHI-буфером + реестр RHI/VulkanSharedUniformBuffers: общие блоки движка (CameraData, ProgramDataBlock, ...) объявлены без явного layout(binding), поэтому на Vulkan их биндинг раздаёт вулканизатор и связывание идёт по имени блока, а не по setLayoutLocation. Создание общих UBO и prepareUniformBuffers перенесены из GL4Renderer в IRenderer — это данные движка, а не GL. VkVertexBuffer / VkIndexBuffer над VulkanGPUBuffer. VkRenderer::renderMeshData строит PSO из программы привязанного VkShader, кешированных состояний и IMeshData::m_rhi и пишет в командный список открытого прохода (на Vulkan проход = один командный буфер). use*State() запоминают состояние для PSO. Удалён VkMeshData: его заглушка переопределяла IMeshData::prepare() пустым телом, из-за чего у каждого меша не было буферов вершин и Vulkan давал чёрный кадр при нуле ошибок валидации. GL такого класса не имеет — createMeshData() возвращает базовый IMeshData. VkShader резолвит массивы сэмплеров поэлементно (проходы адресуют mat_diffuseSamplers[i], рефлексия даёт имя без индекса). Проверено: SGSmokeTest vulkan — exit 0, геометрия рисуется (кадр ещё некорректный: attachment'ы прохода, текстуры материалов, TBO — следующий шаг); SGSmokeTest gl46 — 0.000 %; SGRHITest vulkan/gl46 — exit 0.
…скриптор VkShader::isUniformExists не снимал индекс перед обращением к рефлексии, а IShader::bindMaterialTextures прерывает цикл на первом несуществующем "name[i]" — из-за этого отвязывались ВСЕ текстуры материала. VkRenderer::getDummyTexture() — 1x1 белая картинка для сэмплеров, которые проходы оставляют непривязанными: Vulkan запрещает draw с незаписанным дескриптором, тогда как на GL такой сэмплер просто читает юнит 0. Проверено: SGSmokeTest vulkan — exit 0, предупреждения про mat_diffuseSamplers ушли (кадр всё ещё некорректный, осталось: TBO, соответствие attachment'ов выходам шейдера, renderArray); SGSmokeTest gl46 — 0.000 %; SGRHITest vulkan/gl46 — exit 0.
…аботу VulkanCommandList::begin() на уже пишущем списке вызывал vkBeginCommandBuffer, который сбрасывает буфер: legacy-фасады делят один список, а проходы биндят фреймбуфер, не отвязав предыдущий, — вся записанная работа молча пропадала. Теперь begin() доводит и сабмитит незавершённую работу (flushRecordedWork). SGSmokeTest получил --geometry-pass и --attachment N: съёмка промежуточных буферов конвейера вместо только финального — инструмент для сравнения бэкендов по этапам. Кадр на Vulkan пока пустой. Установлено: draw'ы доходят до vkCmdDrawIndexed в проходе 1920x1080 с 8 attachment'ами и пишут в тот же VkImage, который читает readback, но даже vkCmdClearAttachments внутри прохода не виден — проход не исполняется. Culling, depth-тест и привязка UBO камеры исключены. Проверено: SGSmokeTest gl46 — 0.000 %; SGRHITest vulkan/gl46 — exit 0; SGSmokeTest vulkan — exit 0.
Срез делает bind → bindAttachmentsToDrawIn → clearAttachment → unbind → readAttachmentPixels и требует точный цвет. Проходит на gl46 и vulkan: сам фасад и сабмит его прохода исправны, поэтому пустую сцену на Vulkan надо искать в другом месте — вероятно, во взаимном влиянии множества фреймбуферов на одном общем командном списке за кадр. Проверено: SGRHITest vulkan/gl46 — PASS, exit 0; SGSmokeTest gl46 — 0.000 %.
runFacadeSequenceSlice повторяет форму настоящего кадра на legacy-фасадах: два фреймбуфера (8 и 4 attachment'а), очистка как в clearPostProcessFrameBuffers, перекрывающиеся bind'ы, повторный bind с draw-набором. Проходит на gl46 и vulkan — гипотеза о том, что пустая сцена на Vulkan вызвана взаимным влиянием фреймбуферов на общем командном списке, опровергнута; причину надо искать в самих draw'ах. Тест попутно зафиксировал контракт: очистка адресует draw-буферы прохода, а не слоты attachment'ов, поэтому bindAttachmentsToDrawIn обязан идти до clear() — без этого на GL46 очистки не происходит вовсе (на Vulkan пустой draw-набор означает «все attachment'ы»). Записано в скилл sgcore-graphics. Проверено: SGRHITest vulkan/gl46 — PASS, exit 0; SGSmokeTest gl46 — 0.000 %.
…ые имена Движок адресует uniform'ы так, как их отдаёт GL, — листовыми путями вида "objectTransform.modelMatrix" (GL перечисляет GL_UNIFORM-ресурсы, а это всегда листья). SPIRV-Reflect вместо этого отдаёт член верхнего уровня со вложенными members, поэтому на Vulkan любая запись во вложенный uniform молча промахивалась и матрица модели никогда не доходила до шейдера. Листья теперь разворачиваются рекурсивно (точечные пути), смещения берутся из absolute_offset — относительно блока, а не относительно родительской структуры. Массивы структур репортятся одним членом: в блоках движка их нет, гадать не нужно. SGRHITest получил шейдер со struct SGTestTransform: проверяются и наличие путей, и то, что смещения отсчитываются от блока. На Vulkan до этой правки тест падал. Проверено: SGRHITest vulkan/gl46 — PASS, exit 0; SGSmokeTest gl46 — 0.000 %; SGShadersTest — PASS. Кадр на Vulkan всё ещё пуст: причин несколько, остаются маска записи цвета, discard в фрагментных шейдерах и данные legacy-блоков на реальных шейдерах.
Причина пустого кадра: draw с объявленным, но незаписанным дескриптором молча ничего не рисует. Проходы движка оставляют часть дескрипторов непривязанными — на GL это безнаказанно (сэмплер читает юнит 0, TBO не используется), а драйвер отбрасывал такой draw целиком, не выдавая ошибок в самом месте отрисовки. Теперь VkShader::buildDescriptorSet заполняет все незанятые дескрипторы: - VkRenderer::getDummyTexture() — 1x1 белая картинка для сэмплеров; - VkRenderer::getDummyTexelBufferView() — одноэлементный uniform texel buffer для samplerBuffer (u_bonesMatricesUniformBuffer у неанимированных мешей). VulkanDescriptorSet получил setTexelBuffer, VulkanCommandList пишет pTexelBufferView, буферы со storage-usage получают бит UNIFORM_TEXEL_BUFFER. Попутно исправлено: VulkanCommandList::begin() на уже пишущем списке терял записанную работу (vkBeginCommandBuffer сбрасывает буфер) — теперь она доводится и сабмитится; SPIR-V-рефлексия разворачивает вложенные структуры в листовые имена (objectTransform.modelMatrix), иначе запись во вложенный uniform молча промахивалась; VkShader::isUniformExists снимает индекс массива, иначе bindMaterialTextures обрывал цикл и отвязывал все текстуры материала. SGRHITest вырос до четырёх срезов: треугольник через RHI, вложенные структуры в рефлексии, путь legacy-фасада фреймбуфера, последовательность фреймбуферов кадра. SGSmokeTest получил --geometry-pass и --attachment N для съёмки промежуточных буферов конвейера. Проверено: SGSmokeTest --gapi vulkan --geometry-pass — сцена рисуется (сфера, плоскость, перспектива); SGSmokeTest gl46 — 0.000 %; SGRHITest vulkan/gl46 — PASS, exit 0; SGShadersTest — PASS. Осталось: постобработка на Vulkan (финальный кадр — шум), сравнение с эталоном GL46 (цвета, ориентация), renderArray/VkVertexArray/VkCubemapTexture.
Юниформы, различающиеся по draw'ам, не снимались по draw'у: VkShader держал один UBO на стадию и писал в него на месте. На GL это верно, потому что glUniform* попадает в программу немедленно, а на Vulkan draw записывается и исполняется позже — все draw'ы прохода читали значения последнего, и сцена схлопывалась на трансформ и материал последнего объекта. Блок теперь живёт в CPU-копии (LegacyBlock::m_values), а buildDescriptorSet() — вызывается один раз на draw — копирует его в срез арены устройства и биндит этот offset. Арена: VulkanDevice::allocateTransientUniforms(), bump-аллокатор по чанкам host-visible памяти; регионы ротируются в VkRenderer::prepareFrame, поэтому регион переписывается только через несколько кадров. Привязки дескрипторов снимать отдельно не нужно — VulkanCommandList материализует transient VkDescriptorSet на записи команды по версии набора. Попутно исправлено обратное чтение attachment'ов: vkCmdCopyImageToBuffer копирует память как есть, а объявленный m_dataType расходится с форматом образа (вложения 4-6 LayeredFrameReceiver — SGG_RGB16_FLOAT + SGG_FLOAT, реально R16G16B16A16_SFLOAT). Размер копии считался по m_dataType, поэтому staging заполнялся наполовину и декодировался со сдвигом — RGBA16F-вложения читались мусором, и сравнения Vulkan/GL46 по ним давали ложные выводы. Добавлен VulkanTypesCaster::formatLayout(VkFormat) и покомпонентное декодирование (half->float и пр.) с сохранением memcpy при точном совпадении раскладок. Проверено: вложение нормалей на Vulkan структурно совпадает с GL46 (каждый объект со своим трансформом и материалом, тени и блик на месте), SGRHITest — PASS на gl46 и vulkan, SGSmokeTest --gapi gl46 --reference — 0.000 %. Осталось: атмосфера на Vulkan не рисуется, прозрачная сфера идёт во вложение 1 вместо стохастической прозрачности, постобработка.
…айбокса Атмосфера на Vulkan выводилась белой из-за двух дефектов, оба в общем коде. IShader::bindTextures вычислял имена юниформов в function-local static, так что имя, полученное на первом вызове, использовалось для всех последующих слотов, и счётчик одного слота попадал в юниформ другого. Кого именно затрёт, зависит от порядка первого вызова, а он у бэкендов разный — поэтому баг и проявлялся несимметрично. Шейдер скайбокса в ветке mat_skyboxSamplers_CURRENT_COUNT > 0 задавал только skyboxCol.rgb, оставляя alpha равной 0 от vec4(0.0), — такой фрагмент исчезал при смешивании. Обе ветки и так дают одинаковый цвет (сэмплирование кубмапы закомментировано), поэтому вторая теперь тоже задаёт alpha. Ветку выбирало расхождение isUniformExists: GL-линкер выбрасывает объявленный, но неиспользуемый mat_skyboxSamplers и отвечает «нет», а в SPIR-V ресурс с явным binding остаётся. Воспроизвести GL-семантику из SPIR-V не удалось: и SpvReflectDescriptorBinding::accessed, и SpvReflectEntryPoint::used_uniforms считают такой сэмплер используемым (SPIR-V 1.6 перечисляет все глобальные переменные в интерфейсе OpEntryPoint). Попытка откачена, результат записан в скилл, чтобы не повторять. Измерено: геометрический проход Vulkan против GL46 — 47.272 % -> 1.582 %, GL46 против эталона — 0.000 %, SGRHITest — PASS на gl46 и vulkan. Оставшееся расхождение — ровно прозрачная сфера: на Vulkan она пишется во вложение 1, тогда как GL уводит её в стохастическую прозрачность.
…ом пути calculateStochasticTransparencyComponents задавала outputSTColor и outputLayerColor не на всех путях, а вызывающие не делают discard: он у них закомментирован. Незаписанный выход фрагмента по спецификации не определён, и бэкенды разошлись именно на этом — GL оставлял attachment нетронутым, Vulkan писал туда цвет фрагмента. Прозрачные объекты протекали во вложение 1, а весь стохастический слой (вложение 3) заполнялся непрозрачной геометрией, из чего композит делал шум. Ни один бэкенд не был неправ, дефект был в шейдере. Теперь оба выхода задаются на каждом пути, а «не трогать attachment» выражается явной alpha 0 при SrcAlpha-блендинге прохода, а не отсутствием записи: цвет прозрачного фрагмента по-прежнему уходит только в стохастический слой, как и ожидает resolve. Геометрический проход Vulkan против GL46 после этого сходится: вложение 1 — 1.582 % -> 0.000 %, вложение 3 — 100 % -> 0.108 % (сам дизеринг на границах), вложения 0, 2, 4, 5, 6, 7 — 0.000 %, вложения 0 и 1 FX-фреймбуфера — 0.000 %. GL46 против эталона — 0.000 %, SGRHITest — PASS на gl46 и vulkan. Осталась финальная композиция (вложение 7 FX-фреймбуфера): на Vulkan это шум при совпадающих входах и без предупреждений о раскладке изображений.
Две разницы семантики текстур между бэкендами, обе найдены сравнением с GL4Texture2D. Фильтр: GL создаёт attachment'ы фреймбуфера с GL_NEAREST, а Vulkan создавал их с VK_FILTER_LINEAR. Это не косметика — проходы сэмплируют вложения тексель в тексель и сравнивают результат точным ==, как гейт слоя в layer_default_fx.glsl (texture(SGPP_LayersVolumes, uv).rgb == calculatePPLayerVolume(...)), а интерполированный сэмпл не равен хранимому текселю почти нигде. Начальное содержимое: свежий VkImage хранит неопределённые данные, тогда как GL-текстура, созданная без данных, читается нулями. Движок на это опирается: все проходы открываются с LoadOp::SGG_LOAD, а attachment'ы FX-фреймбуфера не очищаются вообще, поэтому неопределённое содержимое доживало до финального кадра. createAsFrameBufferAttachment теперь очищает образ (цвет — в нули, глубина — в 1.0, чтобы проход, забывший очистить глубину, ничего не отбраковал). Шум в финальной композиции ни та, ни другая правка не убирают: он порождается самим композитом. Причина сужена до того же гейта — на GL он проходит, на Vulkan нет, а при непрохождении fragColor не присваивается вовсе, то есть это снова неопределённый выход фрагмента. NaN ни с одной стороны нет. Регрессий нет: GL46 против эталона — 0.000 %, геометрический проход Vulkan против GL46 — 0.000 %, SGRHITest — PASS на gl46 и vulkan.
…сэмплеры через useInteger Три дефекта, спрятанные друг под другом; каждый следующий открывался после устранения предыдущего. Шум порождал OutlinePass: проход 3 всегда рисует полноэкранный квад во вложение 7 и там, где контура нет, не присваивал outColor. На GL вложение случайно сохраняло цвет сцены, на Vulkan туда писалось неопределённое значение. outline.glsl теперь присваивает каждый выход на каждом пути, а «оставить цвет сцены» выражено discard (смешивания и глубины у прохода нет). Под шумом был чёрный кадр: FX-дро на Vulkan не записывались. Квады PostProcessPass — 6 индексов и ноль вершин, вершинный шейдер FX синтезирует квад из gl_VertexID, а prepareMeshRHI отвергал меш без вершин. Меш только с индексами теперь готовится без вершинного буфера и с пустым vertex input; renderMeshData не рисует нулевое число примитивов. Заодно убран мёртвый счётчик старой диагностики. Под чёрным был белый: PostProcessPass задаёт сэмплеры вложений эффекта через useInteger(name, unit) — на GL сэмплер это int-юниформ и glUniform1i выбирает юнит, — а VkShader::useInteger знал только legacy-блок, поэтому SG_SSAO_* читали dummy-белую текстуру. useInteger для имени, которое рефлексия знает как сэмплер, идёт в таблицу юнитов (isSamplerBinding). Ни один из трёх не виден по валидации: пропущенное дро не валидируется, а неопределённый выход фрагмента законен. Измерено: финальный кадр Vulkan против эталона GL46 — 100 % -> 7.138 % (max diff 255 -> 77), Vulkan детерминирован от прогона к прогону (0.000 %), GL46 против эталона — 0.000 %, SGRHITest — PASS на gl46 и vulkan. Остаток — SSAO: шумовая текстура создаётся из float-данных как RGB16_FLOAT с m_dataType UNSIGNED_BYTE; GL заливает биты float как байты (это уже в эталоне), Vulkan аплоад отклоняет. Починка изменит GL-эталон — отдельное решение.
Четыре причины, каждая — реальный дефект. Шейдер скайбокса объявлял pickingColor и не присваивал его: в буфер пикинга шли неопределённые значения. Присваивается нулём — небо не пикается. Вершинный layout меша объявлял атрибуты, которых программа не читает (контур берёт позиции и кости, постпроцесс — ничего). Объявлять непотребляемый атрибут законно, но валидация сообщает об этом на каждый пайплайн, поэтому VulkanPipelineState пересекает раскладку меша с getReflection().m_vertexInputs; программа входит в ключ кеша PSO, так что фильтрация остаётся по-вариантно. Текстура с отклонённым аплоадом оставалась в UNDEFINED, хотя проходы её всё равно биндят и сэмплируют — ошибка раскладки поверх уже сообщённой проблемы аплоада. VkTexture2D::uploadRegion при отказе очищает образ и переводит его в SHADER_READ_ONLY. Attachment, привязанный как сэмплер, пока проход в него же рисует: проходы биндят все attachment'ы фреймбуфера заранее и лишь потом выбирают цель (PostProcessPass). На GL это UB, здесь — ошибка валидации, а оставить дескриптор незаписанным нельзя, иначе draw отбрасывается. materializeSet подменяет такой дескриптор dummy-текстурой (isPassAttachment); сам dummy переехал из VkRenderer в VulkanDevice, потому что нужен и командному списку. Кадр после подмены не изменился — ни один шейдер движка не читает то, что пишет. Попутно: renderArray пропускает пустой draw вместо vkCmdDrawIndexed(0, 1); в имя attachment'а добавлены адрес фреймбуфера и размер, иначе одинаковые слоты разных фреймбуферов неразличимы в захвате RenderDoc. Остаются 9 «write is unused»: шейдер контура мультиплексирует три прохода через одну программу, и в пайплайне прохода 3 привязан один attachment — статический анализ не смотрит на ветку по vs_pass, убирается только разделением шейдера. Проверено: GL46 против эталона — 0.000 %, Vulkan — 7.138 % (без изменений), SGRHITest — PASS на gl46 и vulkan.
…ботал glTexImage2D приводит CPU-буфер к внутреннему формату на лету, а vkCmdCopyBufferToImage копирует байты как есть. Из-за этого аплоад шумовой текстуры SSAO отклонялся (m_dataType говорит «байты», образ — RGB16_FLOAT), текстура оставалась пустой, и в ssao.glsl normalize(vec3(0)) давал NaN: NaN отравлял TBN, все сравнения выборок становились ложными, occlusion копился нулём и итог 1.0 - 0/count = 1.0 — затенение исчезало целиком. VkTexture2D::uploadRegion теперь строит staging в раскладке образа (VulkanTypesCaster::formatLayout), декодируя канал за каналом, и оставляет memcpy только при точном совпадении раскладок. Это аплоадный двойник того, что readAttachmentPixels уже делает при чтении. Проверено пробником: шум на Vulkan сэмплируется точь-в-точь как на GL, отказов аплоада ноль, окклюзия считается и структурно совпадает с GL. Попутно ssao.glsl: гейт слоя делал return, не присвоив fragColor, — четвёртый случай неопределённого выхода фрагмента в этой сцене. Заменено на discard. Метрика финального кадра при этом уходит с 7.138 % на 46.171 %: это не регресс качества, а обратное — раньше эффекта не было вовсе, и плоский результат случайно оказывался ближе к эталону. Остаток — расхождение самой математики прохода окклюзии; проверено, что все его входы (ядро целиком, счётчик, шум, G-буфер, камера) идентичны на обоих бэкендах. GL46 против эталона — 0.000 %, SGRHITest — PASS на gl46 и vulkan.
…задачи 3.1/3.2) Инфраструктура (3.1): ветка SG_TARGET_OS_WINDOWS в CMake. d3d12/dxgi/dxguid берутся из Windows SDK и отдельного пакета не требуют, поэтому Agility SDK на этом шаге не нужен; зависимости vcpkg (spirv-cross, directx-dxc) понадобятся только шейдерному пути задачи 3.4. DX12Common — двойник VulkanCommon: SG_DX_CHECK и расшифровка HRESULT через системную таблицу сообщений. Инициализация (3.2, половина): DX12Context повторяет форму VulkanContext — DXGI-фабрика, выбор адаптера, device, direct-очередь. Адаптер берётся через EnumAdapterByGpuPreference(HIGH_PERFORMANCE), программный WARP отбрасывается, требуется FL 12.0. Слой отладки включается ДО создания device, иначе device создаётся без него; переключатель SG_DX_VALIDATION=1/0 — двойник SG_VK_VALIDATION. Колбэка у слоя нет, в отличие от VK_EXT_debug_utils, поэтому его очередь сливается в лог вручную (drainDebugMessages). DX12 убран из дефолтного списка предпочтения GAPISelector: он стоял там ПЕРВЫМ и «пропускался» только пока createRenderer отдавал nullptr. Как только бэкенд начинает создаваться, незавершённый DX12 становится дефолтом на Windows и отдаёт пустой кадр. Доступ через SG_GAPI=dx12, как у Vulkan. CoreMain::init() теперь останавливается, если рендерер не поднялся: setShouldClose(true) влияет только на главный цикл, а инициализация шла дальше создавать ассеты и падала с abort() на первом nullptr из фабрики вместо того, чтобы назвать настоящую причину. Это лечит и тот же латентный путь у GL4. Проверено: SGRHITest --gapi dx12 — устройство поднимается (NVIDIA GeForce GTX 1660 Ti, FL 12.0, слой отладки on), выход 0 без abort. Регрессий нет: SGRHITest — PASS на gl46 и vulkan, SGSmokeTest --gapi gl46 — 0.000 %.
…адачи 3.3–3.5) Ресурсы и PSO поверх RHI (DX12Device/CommandList/Texture/GPUBuffer/DescriptorSet/ PipelineState/Swapchain), шейдерный путь SPIR-V → HLSL (spirv-cross) → DXIL (DXC, SM 6.0) и legacy-фасады (DX12Texture2D, DX12CubemapTexture, DX12FrameBuffer, DX12Shader). Общее для explicit-бэкендов вынесено в Graphics/RHI/: PixelConversion, TextureUnits, SharedUniformBuffers, LiveDevice, RHIVertexBuffer/RHIIndexBuffer/RHIVertexArray/ RHIUniformBuffer, RHILegacyShader, RHILegacyDraw. Vulkan переведён на них без изменения кадра. Правило флипа вьюпорта у D3D — зеркало правила Vulkan, а не копия: у Vulkan NDC y = -1 попадает в строку 0 как у GL, у D3D туда попадает y = +1. Флипается offscreen, окно — нет; winding инвертирован следом (знак замерен пробником gl_FrontFacing). Пересчёт прямоугольника вьюпорта из GL-низа в D3D-верх нужен всем проходам, включая окно. Смоук против GL46: DX12 46.166 % (было 54.303 %), Vulkan 46.171 %; друг от друга бэкенды расходятся на 0.370 % при строгом допуске. Геометрические вложения DX12 — 0.000 %. Остаток целиком общий для explicit-бэкендов (шумовая текстура SSAO). SGRHITest — PASS на gl46/vulkan/dx12, GL46 против эталона — 0.000 %.
… (задачи 3.6, 3.7) Батчинг переведён с «точка на треугольник + геометрическая стадия» на инстансный дро: renderArrayInstanced(vao, state, 3, 0, trianglesCount), per-triangle данные тем же буфером с divisor = 1, угол треугольника из gl_VertexID. Геометрических стадий в корпусе шейдеров больше нет — это снимало блокер DX12 (spirv-cross не транслирует geometry в HLSL) и заодно убирает медленную стадию на GL и Vulkan. SG_TEXTURE_BUFFER перестал быть GL-специфичным: VulkanGPUBuffer::createTexelView даёт VkBufferView, DX12GPUBuffer — типизированный buffer SRV; фасады текстур при этом типе создают буфер вместо образа. Формат texel-буфера НЕ расширяется RGB→RGBA, в отличие от образов: данные лежат плотно, и расширение читает тексель с шагом 16 байт вместо 12. UNIFORM_TEXEL_BUFFER добавлен в join-цикл таблицы юнитов — до этого любой samplerBuffer на explicit-бэкендах читал заглушку, включая матрицы костей. Подсистема батчинга до этого не рисовала ничего и нигде не вызывалась. Попутно починено: перепутанные ключи шейдеров у PBRRPBatchingPass и PBRRPTerrainsPass; abort() в Batch::bind на пустом атласе; безусловный discard для нетекстурированных мешей; знаковость индексного texel-буфера; тип и число атрибутов uvOffsets (11 вместо 12 при нечётном texture_types_count); getMaxTextureBufferSize() == 0 на Vulkan и DX12. Покрытие: SGSmokeTest --batching + эталон smoke_gl_batching_1920x1080.png. gl46/gl4 — 0.000 %, DX12 — 0.000 % (max diff 2), Vulkan — 0.095 % (max 52). Обычный смоук без регрессий, SGRHITest — PASS на всех трёх.
… контура (1.4c, 2.5)
…ефекта (3.9) Подсистема инстансинга была написана целиком (компонент Instancing, InstancingUpdater в Scene.cpp, PBRRPInstancingPass в пайплайне, AutoInstancing::addEntity, вариант шейдера через SG_INSTANCED_RENDERING), но addEntity не вызывался нигде и путь не запускался ни разу. Добавлено покрытие SGSmokeTest --instancing и эталон smoke_gl_instancing_1920x1080.png. Три дефекта, любого из которых хватало, чтобы инстансинг не работал никогда: 1. AutoInstancing::addEntity создавал сущность без EnableInstancingPass, а проход ходит по view<EntityBaseInfo, Instancing, EnableInstancingPass>. 2. Атрибуты накапливались на буфере: addAttribute только добавляет, а useAttributes() заливает в текущий VAO весь список. Буфер меша (локации 0..8 из prepare()) при повторной привязке в массив инстансинга (7..15) затирал локации 0..6 — сами per-instance трансформы. Добавлен IVertexBuffer::clearAttributes(). 3. Раскладка атрибутов — свойство пары «буфер + вершинный массив», а не буфера. У GL её хранит VAO, а explicit-бэкенды пересобирают VertexInputDesc на каждый дро, и второй раскладке храниться было негде. Добавлены IVertexArray::setBufferAttributes/getBufferAttributes; их спрашивают RHILegacyDraw и GL46Renderer::drawLegacyArrayThroughRHI раньше, чем сам буфер. Плюс IMeshData::bindBuffersToVertexArray не регистрировала буферы в чужом массиве — ни вершинные, ни индексный. GL это скрывал состоянием VAO, а explicit-путь получал массив без геометрии и молча выходил из дро, без единого сообщения валидации: VulkanPipelineState выбрасывает входы шейдера, которые никто не кормит. Состояние: gl46 и gl4 — 0.000 %. Vulkan 57.2 % (рисует, геометрия рваная), DX12 54.3 % (не рисует) — задача 3.9 остаётся открытой. Регрессий нет: обычный смоук и батчинг прежние на всех бэкендах.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(RHI): единый графический слой поверх GL46/Vulkan — Vulkan рисует смоук-сцену
Ветка:
feature/add_vulkan_directx→mainДиапазон:
ef7ead28..d2d0c579· 31 коммит · 16–18.08.2026 · 148 файлов, +14 962 / −544Summary
Вводит промежуточный RHI-слой (
Graphics/RHI/) между движком и графическим API, переносит на него GL46 без регрессий и поднимает на нём рабочий Vulkan-бэкенд.SGSmokeTest --gapi vulkanрисует эталонную сцену (PBR-тела, атмосфера, CSM-тени, стохастическая прозрачность, SSAO, постобработка) и расходится с GL46-эталоном на 7.138 %; на старте этапа 2 было 100 % (чёрный кадр). GL46 остаётся эталонным бэкендом — 0.000 % расхождения на каждом коммите серии.Первый шаг дорожной карты (утверждена владельцем проекта 16.08): уход от OpenGL как единственного бэкенда к Vulkan/DirectX, с сохранением GL46 постоянным fallback-бэкендом.
Карта коммитов
26d8e9adf7c2d408f710a6060c05de355e5872c667133d9e63d7d9dce15bb6a98b9e37ae0c542ca20b9f96861372c1d23bb3627a0b4b40f5cb42d7768e3d2a8794e9572c05e922fdf5bafdd9e6dcab138326de9c4b0612f0ece7bdbe04595880e5367eb4058f864e74b1e68623f29c9e5d8d0ad6f50918d6d2d0c579Разбор по фазам
Фаза 0 — дизайн, выбор API, эталонная сцена (
26d8e9ad…67133d9e)Прежде чем трогать код, зафиксированы решения и построен измерительный инструмент.
26d8e9ad—docs/RHI_DESIGN.md: целевой RHI (IDevice,ICommandList,ISwapchain,PipelineStateDescповерх существующихRenderState/BlendingState/MeshRenderState), модель биндинга (4 набора по частоте + push constants, раскладка из рефлексии SGSL), шейдерный пайплайн SGSL → GLSL → SPIR-V.f7c2d408— решение по вопросу №2: OpenGL остаётся постоянным fallback-бэкендом (модель Unigine/Unity), а не временным мостом. Следствие для интерфейса: в RHI не попадает то, что GL не исполнит; различия закрываются capability-флагами вDeviceProperties.f710a606—GAPISelector: платформенный список предпочтения, принудительный выбор черезSG_GAPI, откат с логом. ХардкодGL4RendererвCoreMainубран; вGAPITypeдобавленSG_API_TYPE_DX12и хелперыisOpenGLAPI/isExplicitAPIвместо сравнений диапазонов enum.0c05de35,5e5872c6—Tests/Smoke→SGSmokeTest: детерминированная сцена, захват кадра в PNG, побайтовое сравнение с эталоном (--reference, порог/доля),--gapi. ДобавленIFrameBuffer::readAttachmentPixels()— чтение в родном формате attachment'а. Это главный инструмент всей серии: каждое последующее расхождение локализовано именно им.67133d9e— скиллsgcore-graphics: накопитель проверенных фактов и граблей графического слоя, пополняется каждым последующим коммитом.Фаза 1 — единый шейдерный конвейер (
63d7d9dc,e15bb6a9)Корпус шейдеров движка был написан в GL-стиле: 140+ свободных юниформов вне блоков, 100 сэмплеров без
binding,gl_FragColor,gl_VertexID. Ничего из этого Vulkan не принимает.63d7d9dc—SGSLEVulkanizer: свободные юниформы стадии сворачиваются в std140-блок с сохранением имён (тела шейдеров не переписываются), раздаютсяset/binding, заменяются GL-специфичные встроенные переменные. Биндинги едины для всех стадий программы.SGShadersTestпрогоняет весь корпус.e15bb6a9—Graphics/SPIRV:SPIRVCompiler(glslang 15.1, Vulkan 1.3 / SPIR-V 1.6, все стадии в одномTProgramсmapIO) иShaderReflection(SPIRV-Reflect). 23/23 программ движка компилируются в SPIR-V. Пер-стадийные legacy-блоки с именованным экземпляром — иначе glslang не линкует стадии, где одно имя принадлежит разным анонимным блокам.Итог фазы: один и тот же исходник шейдера собирается и под GL46, и под Vulkan — без ручного дублирования.
Фаза 2 — починка GL46 перед миграцией (
8b9e37ae,0c542ca2)Смоук-тест из фазы 0 показал:
GL46Rendererдаёт чёрный кадр. Причины —createShader()не добавлял дефайнSG_GLSL4(весь код шейдеров под#if defined(SG_GLSL4)исчезал → пустые стадии), иGL46Texture2Dбыл заглушкой 2023 г. сGL_INVALID_ENUM. Заглушка удалена (наследуется рабочийGL4Texture2D),GL46поставлен первым в списке предпочтения. Смоук на gl46 и gl4 совпал попиксельно, эталон переименован вsmoke_gl_1920x1080.png.Фаза 3 — RHI-каркас и миграция GL46 (
0b9f9686…94e9572c)Каркас и его первая реализация. Правило миграции: проход считается перенесённым, только когда смоук на gl46 даёт 0 %; legacy-путь остаётся откатом.
0b9f9686— интерфейсыGraphics/RHI/+ GL46-реализация (GL46Device, immediate-modeGL46CommandList,GL46PipelineStateс VAO изVertexInputDescчерез DSA,GL46ShaderProgramс рефлексией через program interface query).Tests/RHI→SGRHITestпроверяет вертикальный срез до точных пикселей.1372c1d2— первый проход на RHI:ScreenBlit(вывод текстуры на экран) вместо legacy-квада.RHIShaderLoaderсобирает.sgshaderпод диалект устройства.3bb3627a—GL46Shaderпереходит на RHI-модель данных: стадии проходят вулканизатор, юниформы пишутся в UBO по offset/stride из рефлексии вместоglUniform*. Сэмплеры сознательно остаются наglUniform1i.0b4b40f5— меши:IMeshData::m_rhi(ленивое RHI-зеркало), PSO из раскладки меша, draw черезICommandList.cb42d776— legacy vertex arrays (renderArray/renderArrayInstanced): не владеющие обёртки над GL-буферами, PSO из записанных атрибутов. После этого все точки отрисовки GL46 идут черезICommandList.8e3d2a87— фреймбуферы:bind/clear/unbindфасада →beginRenderPass/clear*/endRenderPass.94e9572c— зафиксировано решение по текстурам: юнит-модель проходов остаётся, для explicit-бэкендов делается фасад с таблицей юнитов, проходы не переписываются.Фаза 4 — Vulkan-бэкенд (
05e922fd…058f864e)05e922fd(крупнейший коммит серии, +4628) — зависимости (vulkan-headers,vulkan-loader, VMA),VulkanContext(instance 1.3, слои валидации в Debug, выбор GPU, VMA),VulkanSwapchain(FIFO, ленивый acquire), RHI на Vulkan (VulkanDevice,VulkanGPUBuffer,VulkanTexture,VulkanShaderProgram,VulkanPipelineState,VulkanDescriptorSet,VulkanCommandListна dynamic rendering), фасадыVkTexture2D/VkFrameBuffer. Отдельно вычищен порядок завершения процесса —abort()в статической деструкции после успешного теста (реестр живых GPU-ресурсов,IRenderer::shutdown(),AudioDevice::shutdown(),FontsManagerдоdeinitializeFreetype). Зафиксирована конвенция координат: offscreen-проходы без флипа viewport'а (раскладка памяти совпадает с GL), флип и инверсия front face — только в проходах в окно.SGRHITest --gapi vulkan— PASS, пиксели совпадают с GL46 байт в байт.f5bafdd9—VkShader: все шейдеры движка компилируются на Vulkan; таблица юнитов (useTextureBlockдаёт «сэмплер → юнит»,bind(U)— «юнит → текстура», соединяются по рефлексии перед draw). Попутно:m_paddedSizeв рефлексии — страйд одного элемента массива (иначеname[i]пишет за пределы блока: 280 ошибок на смоуке); аплоад текстуры сайзится поVkFormat, а не по CPU-каналам.e6dcab13— геометрия:VkVertexBuffer/VkIndexBuffer,renderMeshDataпишет в командный список открытого прохода. УдалёнVkMeshData— его заглушка переопределялаIMeshData::prepare()пустым телом, из-за чего у каждого меша не было буферов вершин при нуле ошибок валидации. Общие UBO движка связываются по имени блока (они объявлены без явногоbinding), их создание перенесено изGL4RendererвIRenderer— это данные движка, а не GL.8326de9c,4b0612f0,e5367eb4— три независимых дефекта, каждый давал пустой/неверный кадр:isUniformExistsне снимал индекс массива →bindMaterialTexturesобрывал цикл и отвязывал все текстуры материала;VulkanCommandList::begin()на уже пишущем списке терял записанную работу (vkBeginCommandBufferсбрасывает буфер, а фасады делят один список);SPIR-V-рефлексия не разворачивала вложенные структуры в листовые пути → запись в
objectTransform.modelMatrixмолча промахивалась.ece7bdbe,04595880— два теста, написанные для опровержения гипотез: путь legacy-фасада фреймбуфера и последовательность фреймбуферов кадра исправны на обоих бэкендах, значит причину пустой сцены надо искать в самих draw'ах. Попутно зафиксирован контракт: очистка адресует draw-буферы прохода, поэтомуbindAttachmentsToDrawInобязан идти доclear().058f864e— сцена появилась. Причина пустого кадра: draw с объявленным, но незаписанным дескриптором молча ничего не рисует — на GL сэмплер безнаказанно читает юнит 0, а драйвер отбрасывал такой draw целиком, не выдавая ошибок в месте отрисовки. Все незанятые слоты заполняются заглушками (1×1 белая текстура, одноэлементный texel buffer).Фаза 5 — доводка кадра до эталона (
74b1e686…d2d0c579)Пять коммитов, каждый — расхождение семантики GL↔Vulkan, подтверждённое измерением.
74b1e686glUniform*немедленный), на Vulkan draw только записывается, а исполняется позже — все draw'ы прохода читали значения последнегоvkCmdCopyImageToBuffer— сырое копирование без конвертации формата (в отличие отglReadPixels), из-за чего RGBA16F-вложения читались мусором и сравнения бэкендов давали ложные выводы23f29c9eIShader::bindTexturesдержал имена юниформов в function-localstatic— имя первого вызова применялось ко всем слотам; вскрылось асимметрично из-за разного порядка вызовов. (2) Ветка шейдера скайбокса задавала только.rgb, оставляя alpha = 05d8d0ad6calculateStochasticTransparencyComponentsприсваивала выходы не на всех путях, а вызывающие не делаютdiscard. Незаписанныйoutфрагмента по спецификации не определён: GL оставлял attachment нетронутым, Vulkan писал туда цвет фрагментаf50918d6VK_FILTER_LINEARвместоGL_NEAREST, а движок сравнивает сэмплы attachment'ов точным==; свежийVkImageсодержит неопределённые данные, тогда как GL-текстура читается нулямиd2d0c579OutlinePassне присваивалoutColorтам, где контура нет → под шумом оказался чёрный кадр, потому что FX-квады (6 индексов, ноль вершин — вершины строятся изgl_VertexIndex) отвергалисьprepareMeshRHI→ под чёрным белый, потому чтоuseInteger(name, unit)на имя сэмплера не обрабатывался фасадомКак проверялось
SGRHITest --gapi gl46/--gapi vulkanSGSmokeTest --gapi gl46 --referenceSGSmokeTest --gapi vulkan --referenceSGSmokeTest --gapi vulkanдважды подрядSGShadersTestЧто осталось (вне этого MR)
RGB16_FLOAT, ноITexture2D::create<T>не выставляетm_dataType. GL молча заливает биты float как байты (детерминированный мусор, который уже сидит в текущем эталоне), Vulkan такой аплоад отклоняет. Починка меняет GL-эталон — нужно решение владельца, в одностороннем порядке не делал.GAPISelector(доступ черезSG_GAPI=vulkan/--gapi vulkan). DX12 не начат.renderArrayшлётvkCmdDrawIndexed(0, 1)при нулевом числе индексов; debug-имена attachment'ов неуникальны между фреймбуферами; 18 предупреждений валидации о несоответствии выходов шейдера color-attachment'ам; прогонSGSmokeTest --gapi vulkanизредка зависает при завершении.Риски и откат
IShader::bindTextures(23f29c9e), три шейдера сцены (23f29c9e,5d8d0ad6,d2d0c579), перенос общих UBO вIRenderer(e6dcab13). Все они прогнаны через GL46-эталон с результатом 0.000 %.Graphics/) — ревью удобнее вести по фазам выше, а не построчно; фазы 0–3 трогают GL и общий код, фазы 4–5 почти целиком изолированы вGraphics/API/Vulkan/.