Skip to content

Draft: Feature/add vulkan directx - #25

Draft
8bitniksis wants to merge 45 commits into
Pixelfield-ru:mainfrom
8bitniksis:feature/add_vulkan_directx
Draft

Draft: Feature/add vulkan directx#25
8bitniksis wants to merge 45 commits into
Pixelfield-ru:mainfrom
8bitniksis:feature/add_vulkan_directx

Conversation

@8bitniksis

@8bitniksis 8bitniksis commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

feat(RHI): единый графический слой поверх GL46/Vulkan — Vulkan рисует смоук-сцену

Ветка: feature/add_vulkan_directxmain
Диапазон: ef7ead28..d2d0c579 · 31 коммит · 16–18.08.2026 · 148 файлов, +14 962 / −544


Summary

Вводит промежуточный 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-бэкендом.


Карта коммитов

# Хеш Дата Тема Объём
Фаза 0 — дизайн, выбор API, эталонная сцена
1 26d8e9ad 08-16 docs(rhi): дизайн целевого RHI под Vulkan/DX12/GL46 +362 / −2
2 f7c2d408 08-16 docs(rhi): OpenGL закреплён как постоянный fallback-бэкенд +30 / −5
3 f710a606 08-16 feat(Graphics): GAPISelector — выбор графического API и DX12 в GAPIType +311 / −34
4 0c05de35 08-16 test(Smoke): эталонная рендер-сцена для сравнения бэкендов +693 / −8
5 5e5872c6 08-17 test(Smoke): смоук-сцена рендера и readback attachment'ов +178 / −25
6 67133d9e 08-17 docs(skills): скилл sgcore-graphics — факты и грабли графического слоя +191 / −3
Фаза 1 — единый шейдерный конвейер
7 63d7d9dc 08-17 feat(SGSL): проход вулканизации SGSL → Vulkan-GLSL + тест корпуса +1245 / −1
8 e15bb6a9 08-17 feat(Graphics): SPIR-V компиляция и рефлексия; glslang + spirv-reflect +921 / −75
Фаза 2 — починка GL46 перед миграцией
9 8b9e37ae 08-17 fix(Graphics): GL46Renderer снова рабочий; GL46 — основной GL-бэкенд −103
10 0c542ca2 08-17 fix(Graphics): (то же сообщение — продолжение, см. нит ниже) +46 / −47
Фаза 3 — RHI-каркас и миграция GL46
11 0b9f9686 08-17 feat(RHI): каркас RHI и его GL46-реализация; вертикальный срез +2044 / −10
12 1372c1d2 08-17 feat(RHI): первый проход на RHI — вывод текстуры на экран (ScreenBlit) +476 / −17
13 3bb3627a 08-17 feat(Graphics): GL46Shader на RHI-модели — вулканизированный GLSL, UBO +347 / −80
14 0b4b40f5 08-17 feat(RHI): меши через RHI — IGPUBuffer, PSO из раскладки меша +299 / −19
15 cb42d776 08-17 feat(RHI): legacy vertex arrays через RHI +154 / −5
16 8e3d2a87 08-17 feat(RHI): фреймбуферы GL46 через RHI render pass +256 / −18
17 94e9572c 08-17 docs(rhi): шаг 6 — юнит-модель текстур остаётся, фасад для Vulkan +18 / −4
Фаза 4 — Vulkan-бэкенд
18 05e922fd 08-17 feat(RHI): Vulkan-бэкенд — устройство, свопчейн, фасады текстур/FB +4628 / −108
19 f5bafdd9 08-17 feat(RHI): VkShader — фасад шейдера с таблицей юнитов +716 / −32
20 e6dcab13 08-17 feat(RHI): геометрия на Vulkan — буферы, UBO, renderMeshData +594 / −183
21 8326de9c 08-17 fix(RHI): текстуры материалов — индексы массивов и dummy-дескриптор +74 / −9
22 4b0612f0 08-17 fix(RHI): общий командный список больше не теряет записанную работу +93 / −7
23 ece7bdbe 08-17 test(RHI): покрытие пути legacy-фасада фреймбуфера +40 / −1
24 04595880 08-17 test(RHI): срез последовательности фреймбуферов кадра + контракт очистки +109 / −1
25 e5367eb4 08-17 fix(RHI): SPIR-V-рефлексия разворачивает вложенные структуры +79 / −17
26 058f864e 08-17 feat(RHI): Vulkan рисует сцену — заглушки для незаписанных дескрипторов +189 / −6
Фаза 5 — доводка кадра до эталона
27 74b1e686 08-18 fix(RHI): юниформы на draw и конвертация readback +916 / −68
28 23f29c9e 08-18 fix(render): небо — замороженные имена юниформов и альфа скайбокса +36 / −10
29 5d8d0ad6 08-18 fix(render): стохастическая прозрачность — выходы на каждом пути +30 / −10
30 f50918d6 08-18 fix(RHI): вложения — NEAREST и очистка при создании +49 / −8
31 d2d0c579 08-18 fix(RHI): шум финального кадра — контур, квады без вершин, сэмплеры +148 / −85

Разбор по фазам

Фаза 0 — дизайн, выбор API, эталонная сцена (26d8e9ad67133d9e)

Прежде чем трогать код, зафиксированы решения и построен измерительный инструмент.

  • 26d8e9addocs/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.
  • f710a606GAPISelector: платформенный список предпочтения, принудительный выбор через SG_GAPI, откат с логом. Хардкод GL4Renderer в CoreMain убран; в GAPIType добавлен SG_API_TYPE_DX12 и хелперы isOpenGLAPI/isExplicitAPI вместо сравнений диапазонов enum.
  • 0c05de35, 5e5872c6Tests/SmokeSGSmokeTest: детерминированная сцена, захват кадра в PNG, побайтовое сравнение с эталоном (--reference, порог/доля), --gapi. Добавлен IFrameBuffer::readAttachmentPixels() — чтение в родном формате attachment'а. Это главный инструмент всей серии: каждое последующее расхождение локализовано именно им.
  • 67133d9e — скилл sgcore-graphics: накопитель проверенных фактов и граблей графического слоя, пополняется каждым последующим коммитом.

Побочные находки, заведённые в план как отдельные задачи: тени отбрасывают только Batch-сущности (1.6a), Controllable3D перезаписывает поворот камеры каждый кадр (мешает детерминизму сцены).

Фаза 1 — единый шейдерный конвейер (63d7d9dc, e15bb6a9)

Корпус шейдеров движка был написан в GL-стиле: 140+ свободных юниформов вне блоков, 100 сэмплеров без binding, gl_FragColor, gl_VertexID. Ничего из этого Vulkan не принимает.

  • 63d7d9dcSGSLEVulkanizer: свободные юниформы стадии сворачиваются в std140-блок с сохранением имён (тела шейдеров не переписываются), раздаются set/binding, заменяются GL-специфичные встроенные переменные. Биндинги едины для всех стадий программы. SGShadersTest прогоняет весь корпус.
  • e15bb6a9Graphics/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.

Нит для ревью: 8b9e37ae и 0c542ca2 имеют идентичные сообщения — это одна логическая правка, разъехавшаяся на два коммита (код + документация). При squash-merge неважно; при rebase-merge стоит сжать.

Фаза 3 — RHI-каркас и миграция GL46 (0b9f968694e9572c)

Каркас и его первая реализация. Правило миграции: проход считается перенесённым, только когда смоук на gl46 даёт 0 %; legacy-путь остаётся откатом.

  • 0b9f9686 — интерфейсы Graphics/RHI/ + GL46-реализация (GL46Device, immediate-mode GL46CommandList, GL46PipelineState с VAO из VertexInputDesc через DSA, GL46ShaderProgram с рефлексией через program interface query). Tests/RHISGRHITest проверяет вертикальный срез до точных пикселей.
  • 1372c1d2 — первый проход на RHI: ScreenBlit (вывод текстуры на экран) вместо legacy-квада. RHIShaderLoader собирает .sgshader под диалект устройства.
  • 3bb3627aGL46Shader переходит на 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-бэкенд (05e922fd058f864e)

  • 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 байт в байт.
  • f5bafdd9VkShader: все шейдеры движка компилируются на 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 — доводка кадра до эталона (74b1e686d2d0c579)

Пять коммитов, каждый — расхождение семантики GL↔Vulkan, подтверждённое измерением.

Коммит Что было Причина Результат
74b1e686 Сцена схлопывалась на трансформ и материал последнего объекта Юниформы, различающиеся по draw'ам, писались в один буфер на месте. На GL это верно (glUniform* немедленный), на Vulkan draw только записывается, а исполняется позже — все draw'ы прохода читали значения последнего Per-draw срез transient-арены устройства. Попутно: vkCmdCopyImageToBuffer — сырое копирование без конвертации формата (в отличие от glReadPixels), из-за чего RGBA16F-вложения читались мусором и сравнения бэкендов давали ложные выводы
23f29c9e Небо белое (1) IShader::bindTextures держал имена юниформов в function-local static — имя первого вызова применялось ко всем слотам; вскрылось асимметрично из-за разного порядка вызовов. (2) Ветка шейдера скайбокса задавала только .rgb, оставляя alpha = 0 47.272 % → 1.582 %
5d8d0ad6 Прозрачные объекты протекали в непрозрачный слой, стохастический слой — шум calculateStochasticTransparencyComponents присваивала выходы не на всех путях, а вызывающие не делают discard. Незаписанный out фрагмента по спецификации не определён: GL оставлял attachment нетронутым, Vulkan писал туда цвет фрагмента Геометрический проход: вложение 1 — 0.000 %, вложение 3 — 100 % → 0.108 % (сам дизеринг)
f50918d6 (подготовка) Attachment'ы создавались с VK_FILTER_LINEAR вместо GL_NEAREST, а движок сравнивает сэмплы attachment'ов точным ==; свежий VkImage содержит неопределённые данные, тогда как GL-текстура читается нулями Два реальных разрыва семантики закрыты; шум остался
d2d0c579 Финальный кадр — шум Три дефекта, спрятанные друг под другом: OutlinePass не присваивал outColor там, где контура нет → под шумом оказался чёрный кадр, потому что FX-квады (6 индексов, ноль вершин — вершины строятся из gl_VertexIndex) отвергались prepareMeshRHI → под чёрным белый, потому что useInteger(name, unit) на имя сэмплера не обрабатывался фасадом 100 % → 7.138 %, max diff 255 → 77

Ни один из дефектов фазы 5 не виден по ошибкам валидации: пропущенный draw не валидируется, а неопределённый выход фрагмента законен. Инструментом локализации был --geometry-pass/--attachment N смоук-теста и точечные пробники в шейдерах.


Как проверялось

Проверка Результат
SGRHITest --gapi gl46 / --gapi vulkan PASS, exit 0, ноль ошибок валидации — на каждом коммите серии
SGSmokeTest --gapi gl46 --reference 0.000 % — на каждом коммите (регрессий GL46 за всю серию не было)
SGSmokeTest --gapi vulkan --reference 7.138 % (max diff 77/255) на текущей голове
SGSmokeTest --gapi vulkan дважды подряд 0.000 % — кадр детерминирован от прогона к прогону
SGShadersTest PASS — весь корпус транслируется и в GLSL, и в SPIR-V

Что осталось (вне этого MR)

  • Остаток 7.138 % — SSAO: шумовая текстура создаётся из float-данных как RGB16_FLOAT, но ITexture2D::create<T> не выставляет m_dataType. GL молча заливает биты float как байты (детерминированный мусор, который уже сидит в текущем эталоне), Vulkan такой аплоад отклоняет. Починка меняет GL-эталон — нужно решение владельца, в одностороннем порядке не делал.
  • Задачи плана 2.4–2.7: PSO/дескрипторы под полный PBRRP-пайплайн, Android-свопчейн. Vulkan держится вне дефолтного списка GAPISelector (доступ через SG_GAPI=vulkan / --gapi vulkan). DX12 не начат.
  • Хвосты, записанные в план: renderArray шлёт vkCmdDrawIndexed(0, 1) при нулевом числе индексов; debug-имена attachment'ов неуникальны между фреймбуферами; 18 предупреждений валидации о несоответствии выходов шейдера color-attachment'ам; прогон SGSmokeTest --gapi vulkan изредка зависает при завершении.

Риски и откат

  • GL-путь не тронут по существу: пути исполнения GL46/GL4 остались прежними, добавлены RHI-обёртки поверх существующих вызовов. Побайтовая идентичность подтверждена смоук-эталоном на каждом коммите.
  • Vulkan не включается сам: вне дефолтного списка бэкендов, продакшен-путь не затрагивается при отсутствии Vulkan SDK или драйвера.
  • Изменения в общем коде (не только в Vulkan-бэкенде) — стоит смотреть внимательнее всего: IShader::bindTextures (23f29c9e), три шейдера сцены (23f29c9e, 5d8d0ad6, d2d0c579), перенос общих UBO в IRenderer (e6dcab13). Все они прогнаны через GL46-эталон с результатом 0.000 %.
  • Размер диффа (104 файла в Graphics/) — ревью удобнее вести по фазам выше, а не построчно; фазы 0–3 трогают GL и общий код, фазы 4–5 почти целиком изолированы в Graphics/API/Vulkan/.

- 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: описание таргета и кодов выхода
@cla-assistant

cla-assistant Bot commented Aug 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@cla-assistant

cla-assistant Bot commented Aug 16, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@8bitniksis
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
8bitniksis marked this pull request as draft August 17, 2026 09:45
@8bitniksis 8bitniksis changed the title Feature/add vulkan directx Draft: Feature/add vulkan directx Aug 17, 2026
Задача 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 на всех трёх.
…ефекта (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 остаётся открытой. Регрессий нет: обычный смоук и
батчинг прежние на всех бэкендах.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant