载入中...
搜索中...
未找到
GraphicsPipeline.cpp
浏览该文件的文档.
1// Vulkan backend implementation — swapchain and pipeline creation.
2//
3// Re-split from the merged dev single-TU Graphics.cpp (pure move;
4// dev changes preserved). Shared helpers live in GraphicsInternal.h.
5
8#include "graphics/Light.h"
12#include "graphics/Outline.h"
14
15#include <SDL2/SDL.h>
16#include <SDL2/SDL_vulkan.h>
17
18#include <algorithm>
19#include <array>
20#include <cmath>
21#include <cstdio>
22#include <cstdlib>
23#include <cstring>
24#include <functional>
25#include <stdexcept>
26#include <string>
27#include <vector>
28#if !defined(_WIN32)
29#include <unistd.h>
30#endif
31
32#include "common/Exception.h"
34#include "common/config.h"
36#include "image/Image.h"
37#include "image/ImageData.h"
38#include "zeroerr/assert.h"
39
40#include <memory>
41
42
43#include <assimp/mesh.h>
44#include <assimp/matrix3x3.h>
45#include <assimp/matrix4x4.h>
46#include <assimp/vector3.h>
47#include <glm/gtc/matrix_transform.hpp>
48
49#include "graphics/shaders/color_vert_spv.inc"
50#include "graphics/shaders/color_frag_spv.inc"
51#include "graphics/shaders/textured_vert_spv.inc"
52#include "graphics/shaders/textured_frag_spv.inc"
53#include "graphics/shaders/mesh3d_vert_spv.inc"
54#include "graphics/shaders/mesh3d_frag_spv.inc"
55#include "graphics/shaders/mesh3d_clustered_vert_spv.inc"
56#include "graphics/shaders/mesh3d_clustered_frag_spv.inc"
57#include "graphics/shaders/mesh3d_shadow_vert_spv.inc"
58#include "graphics/shaders/mesh3d_shadow_frag_spv.inc"
59#include "graphics/shaders/mesh3d_shadow_alpha_vert_spv.inc"
60#include "graphics/shaders/mesh3d_shadow_alpha_frag_spv.inc"
61#include "graphics/shaders/mesh3d_gbuffer_vert_spv.inc"
62#include "graphics/shaders/mesh3d_gbuffer_frag_spv.inc"
63#include "graphics/shaders/mesh3d_gbuffer_alpha_frag_spv.inc"
64#include "graphics/shaders/mesh3d_hair_vert_spv.inc"
65#include "graphics/shaders/mesh3d_hair_frag_spv.inc"
66#include "graphics/shaders/lit2d_vert_spv.inc"
67#include "graphics/shaders/lit2d_frag_spv.inc"
69
70namespace eve::graphics::vulkan {
71
72namespace {
73
74vk::Pipeline createSolidColorPipeline(vkb::Device &device, const vkb::BuiltRenderPass &renderPass,
75 vk::PipelineLayout layout,
77 if (mode == BlendMode::Additive) {
78 // cbs/attachments must outlive build(); the builder must stay a single
79 // expression — copying the builder into a named local leaves its
80 // shader-stage pName pointers dangling into the temporary's storage.
81 std::vector<vk::PipelineColorBlendAttachmentState> attachments(1,
82 makeBlendAttachment(mode));
83 vk::PipelineColorBlendStateCreateInfo cbs{};
84 cbs.logicOpEnable = false;
85 cbs.attachmentCount = 1;
86 cbs.pAttachments = attachments.data();
87 return device.createPipeline()
88 .useClassicPipeline(embeddedSpirv(color_vert_spv), embeddedSpirv(color_frag_spv))
89 .setPipelineLayout(layout)
90 .setVertexInputState(vkb::VertexInputStateBuilder()
91 .addInputBinding<ColorVertex>()
92 .addAttributeDescription<ColorVertex>())
93 .setDynamicStatesViewportScissor()
94 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
95 vk::CullModeFlagBits::eNone, vk::FrontFace::eCounterClockwise)
96 .setColorBlending(cbs)
97 .build(renderPass);
98 }
99 if (mode == BlendMode::Alpha) {
100 return device.createPipeline()
101 .useClassicPipeline(embeddedSpirv(color_vert_spv), embeddedSpirv(color_frag_spv))
102 .setPipelineLayout(layout)
103 .setVertexInputState(vkb::VertexInputStateBuilder()
104 .addInputBinding<ColorVertex>()
105 .addAttributeDescription<ColorVertex>())
106 .setDynamicStatesViewportScissor()
107 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
108 vk::CullModeFlagBits::eNone, vk::FrontFace::eCounterClockwise)
109 .setAlphaBlending(1)
110 .build(renderPass);
111 }
112 // Opaque: keep the original single-expression chain (no explicit blend state).
113 return device.createPipeline()
114 .useClassicPipeline(embeddedSpirv(color_vert_spv), embeddedSpirv(color_frag_spv))
115 .setPipelineLayout(layout)
116 .setVertexInputState(vkb::VertexInputStateBuilder()
117 .addInputBinding<ColorVertex>()
118 .addAttributeDescription<ColorVertex>())
119 .setDynamicStatesViewportScissor()
120 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f, vk::CullModeFlagBits::eNone,
121 vk::FrontFace::eCounterClockwise)
122 .build(renderPass);
123}
124} // namespace
125
126// --- Swapchain and graphics pipelines -----------------------------------------
127
128void Graphics::createSwapchainAndPipeline() {
129 // Framebuffers / command buffers alias the current swapchain images; tear
130 // them down before replacing the swapchain (destroy() waitIdles first).
131 presentRecording = {};
132 swapchainPass = {};
133 presentModel.destroy();
134 presentModel = vkb::Present{};
135
136 vkb::SwapchainBuilder swapchainBuilder = device.createSwapchain();
137 if (pixelWidth > 0 && pixelHeight > 0)
138 swapchainBuilder.set_desired_extent(uint32_t(pixelWidth), uint32_t(pixelHeight));
139 // Prefer UNORM so clear/draw Color floats match getPixel without sRGB encode.
140 swapchainBuilder.set_desired_format(
141 {vk::Format::eB8G8R8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear});
142 if (vsyncEnabled) {
143 swapchainBuilder.set_desired_present_mode(vk::PresentModeKHR::eMailbox);
144 swapchainBuilder.add_fallback_present_mode(vk::PresentModeKHR::eFifo);
145 } else {
146 swapchainBuilder.set_desired_present_mode(vk::PresentModeKHR::eImmediate);
147 swapchainBuilder.add_fallback_present_mode(vk::PresentModeKHR::eMailbox);
148 swapchainBuilder.add_fallback_present_mode(vk::PresentModeKHR::eFifo);
149 }
150 // Allow screen getPixel / newImageData readback after present.
151 swapchainBuilder.add_image_usage_flags(vk::ImageUsageFlagBits::eTransferSrc);
152 auto swapRet = swapchainBuilder.set_old_swapchain(swapchain).build();
153 swapchain.destroy();
154 swapchain = swapRet;
155
156 depthImage = vkb::DepthStencilImage{device, swapchain.extent.width, swapchain.extent.height, depthFormat};
157
158 if (!renderpass) {
159 vkb::RenderPassBuilder rpBuilder{device};
160 renderpass =
161 rpBuilder.addPresentAttachment(swapchain.image_format, vk::AttachmentLoadOp::eClear)
162 .addDepthAttachment(depthFormat, vk::AttachmentLoadOp::eClear,
163 vk::AttachmentStoreOp::eDontCare)
164 .addSubpass(vkb::SubpassBuilder()
165 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal)
166 .setDepthStencilAttachment(
167 1, vk::ImageLayout::eDepthStencilAttachmentOptimal))
168 .addDependency(VK_SUBPASS_EXTERNAL, 0,
169 vk::PipelineStageFlagBits::eColorAttachmentOutput |
170 vk::PipelineStageFlagBits::eEarlyFragmentTests,
171 vk::PipelineStageFlagBits::eColorAttachmentOutput |
172 vk::PipelineStageFlagBits::eEarlyFragmentTests,
173 {},
174 vk::AccessFlagBits::eColorAttachmentRead |
175 vk::AccessFlagBits::eColorAttachmentWrite |
176 vk::AccessFlagBits::eDepthStencilAttachmentWrite)
177 .build();
178 }
179
180 if (!pipeline) {
181 pipelineLayout = createPipelineLayout(device);
182 pipeline = createSolidColorPipeline(device, renderpass, pipelineLayout);
183 solidAlphaPipeline = createSolidColorPipeline(device, renderpass, pipelineLayout,
185 additiveSolidPipeline = createSolidColorPipeline(device, renderpass, pipelineLayout,
187 }
188
189 // Scene-pass pipelines are initially built for the swapchain render pass at
190 // 1x; ensureScenePassPipelines rebuilds them only when the active scene pass
191 // (handle or sample count) differs.
192 scenePassPipelineTarget = vk::RenderPass(renderpass);
193 scenePassPipelineSamples = vk::SampleCountFlagBits::e1;
194
195 presentModel = device.createPresent(swapchain).build(renderpass, depthImage.imageView());
196 // Multi-frame overlap: submit + present without waiting on this frame's
197 // fence, so the CPU can build frame N+1 while the GPU still renders frame N.
198 // Present caps frames_in_flight at 2 (independent of swapchain image count)
199 // so present-wait semaphores are not reused while WSI still holds them.
200 // Per-frame mutable GPU resources must be multi-buffered (see *FrameSlots).
201 presentModel.synchronous_frames = false;
202 ensurePresentCaptureHook();
203 swapchainDirty = false;
204}
205
206void Graphics::createTexturedPipeline() {
207 if (texPipeline) return;
208
209 vkb::DescriptorSetLayoutBuilder layoutBuilder;
210 texSetLayoutUnique = layoutBuilder
211 .image(0, vk::DescriptorType::eCombinedImageSampler,
212 vk::ShaderStageFlagBits::eFragment, 1)
213 .image(1, vk::DescriptorType::eCombinedImageSampler,
214 vk::ShaderStageFlagBits::eFragment, 1)
215 .createUnique(device.instance);
216 texSetLayout = *texSetLayoutUnique;
217
218 vk::DescriptorPoolSize poolSizes[] = {
219 {vk::DescriptorType::eCombinedImageSampler, 8192},
220 {vk::DescriptorType::eUniformBuffer, 2048},
221 // Dynamic-offset UBOs (per-draw mesh3d ring) count against their own
222 // pool size type; without this entry the first dynamic set allocation
223 // fails with VK_ERROR_OUT_OF_POOL_MEMORY.
224 {vk::DescriptorType::eUniformBufferDynamic, 4096},
225 {vk::DescriptorType::eStorageBuffer, 256},
226 };
227 vk::DescriptorPoolCreateInfo poolInfo{};
228 poolInfo.maxSets = 4096;
229 poolInfo.poolSizeCount = 4;
230 poolInfo.pPoolSizes = poolSizes;
231 descriptorPool = device->createDescriptorPool(poolInfo);
232
233 texPipelineLayout = createPipelineLayout(device, texSetLayout);
234 const auto pcr =
235 pushConstantRange(vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment,
237 shaderPipelineLayout = createPipelineLayout(device, texSetLayout, &pcr);
238
239 auto vert = embeddedSpirv(textured_vert_spv);
240 auto frag = embeddedSpirv(textured_frag_spv);
241 texPipeline = createTexturedStylePipeline(vert, frag, renderpass, texPipelineLayout);
242 additiveTexPipeline = createTexturedStylePipeline(vert, frag, renderpass, texPipelineLayout,
244 opaqueTexPipeline = createTexturedStylePipeline(vert, frag, renderpass, texPipelineLayout,
246
247 createLit2DPipeline();
248}
249
250vk::Pipeline Graphics::createTexturedStylePipeline(const std::vector<uint32_t> &vert,
251 const std::vector<uint32_t> &frag,
252 const vkb::BuiltRenderPass &rp,
253 vk::PipelineLayout layout, BlendMode mode) {
254 ShaderModulePair modules(device, vert, frag);
255 if (mode == BlendMode::Additive) {
256 // cbs/attachments must outlive build(); keep the builder as a single
257 // expression (see createSolidColorPipeline).
258 std::vector<vk::PipelineColorBlendAttachmentState> attachments(1,
259 makeBlendAttachment(mode));
260 vk::PipelineColorBlendStateCreateInfo cbs{};
261 cbs.logicOpEnable = false;
262 cbs.attachmentCount = 1;
263 cbs.pAttachments = attachments.data();
264 return device.createPipeline()
265 .useClassicPipeline(modules.vert, modules.frag)
266 .setPipelineLayout(layout)
267 .setVertexInputState(vkb::VertexInputStateBuilder()
268 .addInputBinding<TexturedVertex>()
269 .addAttributeDescription<TexturedVertex>())
270 .setDynamicStatesViewportScissor()
271 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
272 vk::CullModeFlagBits::eNone, vk::FrontFace::eCounterClockwise)
273 .setDepthStencil(false, false)
274 .setColorBlending(cbs)
275 .build(rp);
276 }
277 if (mode == BlendMode::Opaque) {
278 return device.createPipeline()
279 .useClassicPipeline(modules.vert, modules.frag)
280 .setPipelineLayout(layout)
281 .setVertexInputState(vkb::VertexInputStateBuilder()
282 .addInputBinding<TexturedVertex>()
283 .addAttributeDescription<TexturedVertex>())
284 .setDynamicStatesViewportScissor()
285 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
286 vk::CullModeFlagBits::eNone, vk::FrontFace::eCounterClockwise)
287 .setDepthStencil(false, false)
288 .setColorBlending()
289 .build(rp);
290 }
291 // Alpha (original behavior).
292 return device.createPipeline()
293 .useClassicPipeline(modules.vert, modules.frag)
294 .setPipelineLayout(layout)
295 .setVertexInputState(vkb::VertexInputStateBuilder()
296 .addInputBinding<TexturedVertex>()
297 .addAttributeDescription<TexturedVertex>())
298 .setDynamicStatesViewportScissor()
299 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f, vk::CullModeFlagBits::eNone,
300 vk::FrontFace::eCounterClockwise)
301 .setDepthStencil(false, false)
302 .setAlphaBlending(1)
303 .build(rp);
304}
305
306void Graphics::createLit2DPipeline() {
307 if (lit2dPipeline) return;
308
309 vkb::DescriptorSetLayoutBuilder layoutBuilder;
310 lit2dSetLayoutUnique =
311 layoutBuilder
312 .image(0, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
313 .image(1, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
314 .buffer(2, vk::DescriptorType::eUniformBuffer, vk::ShaderStageFlagBits::eFragment, 1)
315 .createUnique(device.instance);
316 lit2dSetLayout = *lit2dSetLayoutUnique;
317
318 lit2dPipelineLayout = createPipelineLayout(device, lit2dSetLayout);
319
320 // Offscreen (synchronous) lit-2D path owns a dedicated UBO. The swapchain
321 // path's per-frame UBOs are allocated lazily in currentLighting2dUbo().
322 offscreenLighting2dUbo.allocate(frameToken(), device, vk::BufferUsageFlagBits::eUniformBuffer,
323 sizeof(Lighting2DUBO),
324 vk::MemoryPropertyFlagBits::eHostVisible |
325 vk::MemoryPropertyFlagBits::eHostCoherent);
326
327 auto vert = embeddedSpirv(lit2d_vert_spv);
328 auto frag = embeddedSpirv(lit2d_frag_spv);
329 lit2dPipeline = createTexturedStylePipeline(vert, frag, renderpass, lit2dPipelineLayout);
330}
331
332void Graphics::createMesh3DPipeline() {
333 if (mesh3dPipeline) return;
334
335 // Right-handed view, Y-up world. Projection uses perspectiveVulkanRH_ZO (Y flip for
336 // Vulkan NDC), so frontFace is Clockwise while mesh winding stays CCW in object space.
337 vkb::DescriptorSetLayoutBuilder layoutBuilder;
338 mesh3dSetLayoutUnique =
339 layoutBuilder
340 .buffer(0, vk::DescriptorType::eUniformBufferDynamic,
341 vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 1)
342 .image(1, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
343 .image(2, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
344 .image(3, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
345 .buffer(4, vk::DescriptorType::eUniformBufferDynamic, vk::ShaderStageFlagBits::eFragment, 1)
346 .image(5, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
347 .image(6, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
348 .image(7, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
349 .createUnique(device.instance);
350 mesh3dSetLayout = *mesh3dSetLayoutUnique;
351
352 mesh3dPipelineLayout = createPipelineLayout(device, mesh3dSetLayout);
353 const auto pcr =
354 pushConstantRange(vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment,
356 mesh3dShaderPipelineLayout = createPipelineLayout(device, mesh3dSetLayout, &pcr);
357
358 // Per-draw UBOs live in a per-frame-slot ring; descriptor sets are cached
359 // per texture combination (see mesh3dSetFor / ensureMesh3dRing).
360 mesh3dFrameSlots.clear();
361
362 auto vert = embeddedSpirv(mesh3d_vert_spv);
363 auto frag = embeddedSpirv(mesh3d_frag_spv);
364 mesh3dPipeline =
365 createMesh3DStylePipeline(vert, frag, mesh3dPipelineLayout, renderpass,
366 vk::SampleCountFlagBits::e1);
367}
368
369void Graphics::createMesh3DClusteredPipeline() {
370 if (mesh3dClusteredPipeline) return;
371
372 vkb::DescriptorSetLayoutBuilder layoutBuilder;
373 mesh3dClusteredSetLayoutUnique =
374 layoutBuilder
375 .buffer(0, vk::DescriptorType::eUniformBufferDynamic,
376 vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 1)
377 .image(1, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
378 .image(2, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
379 .image(3, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
380 .buffer(4, vk::DescriptorType::eStorageBuffer, vk::ShaderStageFlagBits::eFragment, 1)
381 .buffer(5, vk::DescriptorType::eStorageBuffer, vk::ShaderStageFlagBits::eFragment, 1)
382 .buffer(6, vk::DescriptorType::eStorageBuffer, vk::ShaderStageFlagBits::eFragment, 1)
383 .buffer(7, vk::DescriptorType::eUniformBufferDynamic, vk::ShaderStageFlagBits::eFragment, 1)
384 .image(8, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
385 .image(9, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment, 1)
386 .createUnique(device.instance);
387 mesh3dClusteredSetLayout = *mesh3dClusteredSetLayoutUnique;
388
389 mesh3dClusteredPipelineLayout = createPipelineLayout(device, mesh3dClusteredSetLayout);
390
391 mesh3dClusteredFrameSlots.clear();
392
393 auto vert = embeddedSpirv(mesh3d_clustered_vert_spv);
394 auto frag = embeddedSpirv(mesh3d_clustered_frag_spv);
395 mesh3dClusteredPipeline =
396 createMesh3DStylePipeline(vert, frag, mesh3dClusteredPipelineLayout, renderpass,
397 vk::SampleCountFlagBits::e1);
398}
399
400void Graphics::destroyShadowResources() {
401 destroyPipeline(device, shadowPipeline);
402 destroyPipelineLayout(device, shadowPipelineLayout);
403 destroyPipeline(device, shadowAlphaPipeline);
404 destroyPipelineLayout(device, shadowAlphaPipelineLayout);
405 for (auto &slot : shadowMaps) {
406 for (int i = 0; i < ShadowConfig::kCascades; ++i) {
407 if (slot.framebuffers[i]) {
408 device->destroyFramebuffer(slot.framebuffers[i]);
409 slot.framebuffers[i] = vk::Framebuffer{};
410 }
411 }
412 }
413 shadowMaps.clear();
414 if (shadowSampler) {
415 device->destroySampler(shadowSampler);
416 shadowSampler = {};
417 }
418 if (shadowRenderPass) {
419 device->destroyRenderPass(shadowRenderPass);
420 shadowRenderPass = {};
421 }
422 shadowPassCascade = -1;
423 shadowPassDraws.clear();
424 shadowPendingMask = 0;
425 for (auto &d : shadowCascadeDraws) d.clear();
426}
427
428void Graphics::destroyGBufferResources() {
429 gbufferPassActive = false;
430 gbufferPending = false;
431 gbufferPassDraws.clear();
432 for (auto &slot : gbufferSlots) {
433 slot.normalTex.gpuHandle = nullptr;
434 slot.depthColorTex.gpuHandle = nullptr;
435 slot.albedoTex.gpuHandle = nullptr;
436 slot.depthTex.gpuHandle = nullptr;
437 if (slot.framebuffer) {
438 device->destroyFramebuffer(slot.framebuffer);
439 slot.framebuffer = vk::Framebuffer{};
440 }
441 destroySampler(device, slot.normalGpu.sampler);
442 destroySampler(device, slot.depthColorGpu.sampler);
443 destroySampler(device, slot.albedoGpu.sampler);
444 destroySampler(device, slot.depthGpu.sampler);
445 }
446 gbufferSlots.clear();
447 post2Sets.clear();
448 destroyPipeline(device, gbufferPipeline);
449 destroyPipeline(device, gbufferAlphaPipeline);
450 destroyPipelineLayout(device, gbufferPipelineLayout);
451 if (gbufferRenderPass) {
452 device->destroyRenderPass(gbufferRenderPass);
453 gbufferRenderPass = {};
454 }
455 gbufferWidth = 0;
456 gbufferHeight = 0;
457 if (renderControl_) renderControl_->getGBuffer()->clear();
458}
459
460void Graphics::destroySceneColorResources() {
461 sceneColorPassOpen = false;
462 if (device.instance) device->waitIdle();
463 for (auto &slot : sceneColorSlots) {
464 slot.colorTex.gpuHandle = nullptr;
465 if (slot.framebuffer) {
466 device->destroyFramebuffer(slot.framebuffer);
467 slot.framebuffer = vk::Framebuffer{};
468 }
469 destroySampler(device, slot.colorGpu.sampler);
470 }
471 sceneColorSlots.clear();
472 post2Sets.clear();
473 if (sceneColorRenderPass) {
474 device->destroyRenderPass(sceneColorRenderPass);
475 sceneColorRenderPass = {};
476 }
477 sceneColorWidth = 0;
478 sceneColorHeight = 0;
479 sceneColorFormat = vk::Format::eUndefined;
480 sceneColorSamples = vk::SampleCountFlagBits::e1;
481}
482
483void Graphics::createUiColorResources(int width, int height) {
484 if (width <= 0 || height <= 0) return;
485 const vk::Format colorFmt = swapchain.image_format;
486 if (colorFmt == vk::Format::eUndefined) return;
487
488 const vk::SampleCountFlags supported =
489 device.physical_device.properties.limits.framebufferColorSampleCounts;
490 const vk::SampleCountFlagBits samples =
491 (supported & vk::SampleCountFlagBits::e4) ? vk::SampleCountFlagBits::e4
492 : vk::SampleCountFlagBits::e1;
493
494 // The render pass is size-independent and must remain stable for ImGui's
495 // lifetime: ImGui_ImplVulkan_Init builds its pipeline against it. Create it
496 // once, before any early-returns below.
497 if (!uiRenderPass) {
498 if (samples != vk::SampleCountFlagBits::e1) {
499 uiRenderPass =
500 device.createRenderPass()
501 .addColorAttachment(colorFmt, vk::AttachmentLoadOp::eClear,
502 vk::AttachmentStoreOp::eDontCare, samples)
503 .addResolveColorAttachment(colorFmt, vk::AttachmentLoadOp::eDontCare)
504 .addSubpass(vkb::SubpassBuilder()
505 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal)
506 .addResolveAttachment(1, vk::ImageLayout::eColorAttachmentOptimal))
507 .addExternalShaderReadDependencies()
508 .build();
509 } else {
510 uiRenderPass =
511 device.createRenderPass()
512 .addSampledColorAttachment(colorFmt)
513 .addSubpass(vkb::SubpassBuilder()
514 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal))
515 .addExternalShaderReadDependencies()
516 .build();
517 }
518 }
519 // Keep sample count in sync with the render pass even if target creation
520 // returns early (ImGui builds its pipeline from getUiMsaaSamples()).
521 uiColorSamples = samples;
522
523 if (!texSetLayout || !descriptorPool) return;
524
525 if (!uiColorSlots.empty() && uiColorWidth == width && uiColorHeight == height &&
526 uiColorFormat == colorFmt && uiColorSamples == samples)
527 return;
528 destroyUiColorTargets();
529
530 uiColorWidth = width;
531 uiColorHeight = height;
532 uiColorFormat = colorFmt;
533 uiColorSamples = samples;
534 const uint32_t w = uint32_t(width);
535 const uint32_t h = uint32_t(height);
536 const bool msaa = samples != vk::SampleCountFlagBits::e1;
537
538 uiColorSlots.resize(kAsyncResourceCopies);
539 for (auto &slot : uiColorSlots) {
540 slot.color = device.createColorTarget(w, h, colorFmt);
541 if (msaa) slot.msaaColor = device.createColorTarget(w, h, colorFmt, samples);
542 }
543 auto uiPass = uiRenderPass;
544
545 for (auto &slot : uiColorSlots) {
546 if (msaa) {
547 slot.framebuffer = uiPass.createFramebuffer(
548 device, w, h, {slot.msaaColor.asAttachment(), slot.color.asAttachment()});
549 } else {
550 slot.framebuffer = uiPass.createFramebuffer(device, w, h, {slot.color.asAttachment()});
551 }
552 }
553
554 auto makeSampleTex = [&](GpuTexture &gpu, Texture &tex, vk::ImageView view) {
555 vkb::SamplerBuilder sb;
556 gpu.sampler = sb.magFilter(vk::Filter::eLinear)
557 .minFilter(vk::Filter::eLinear)
558 .addressModeU(vk::SamplerAddressMode::eClampToEdge)
559 .addressModeV(vk::SamplerAddressMode::eClampToEdge)
560 .build(device);
561 auto sets = vkb::DescriptorSetBuilder()
562 .layout(texSetLayout)
563 .build(device.instance, descriptorPool);
564 gpu.descriptorSet = vkb::BoundSet{sets[0]};
565 gpu.width = width;
566 gpu.height = height;
567 gpu.viewOverride = view;
568 writeCombinedImageDescriptor(&gpu);
569 tex.width = width;
570 tex.height = height;
571 tex.pixelWidth = width;
572 tex.pixelHeight = height;
573 tex.gpuHandle = &gpu;
574 };
575 for (auto &slot : uiColorSlots)
576 makeSampleTex(slot.colorGpu, slot.colorTex, slot.color.imageView());
577}
578
579void Graphics::destroyUiColorTargets() {
580 if (device.instance) device->waitIdle();
581 for (auto &slot : uiColorSlots) {
582 slot.colorTex.gpuHandle = nullptr;
583 if (slot.framebuffer) {
584 device->destroyFramebuffer(slot.framebuffer);
585 slot.framebuffer = vk::Framebuffer{};
586 }
587 destroySampler(device, slot.colorGpu.sampler);
588 }
589 uiColorSlots.clear();
590 uiColorWidth = 0;
591 uiColorHeight = 0;
592 uiColorFormat = vk::Format::eUndefined;
593 // Keep uiColorSamples matching uiRenderPass; ImGui's pipeline is bound to it.
594}
595
596void Graphics::destroyUiColorResources() {
597 destroyUiColorTargets();
598 if (uiRenderPass) {
599 device->destroyRenderPass(uiRenderPass);
600 uiRenderPass = {};
601 }
602}
603
604void Graphics::queueUiResolve() {
605 auto *slot = currentUiColorSlot();
606 if (!slot || !slot->colorTex.gpuHandle) return;
607 TexturedBatch resolve{&slot->colorTex, nullptr, nullptr, BlendMode::Alpha, Batcher{}};
608 resolve.batch.addTexturedRect(0.f, 0.f, float(uiColorWidth), float(uiColorHeight),
609 Color(1.f, 1.f, 1.f, 1.f), 0.f, 0.f, 1.f, 1.f);
610 pendingUiResolve = std::move(resolve);
611}
612
613bool Graphics::renderUiOverlayPass() {
614 if (!presentOverlayFn_) return false;
615 createUiColorResources(int(swapchain.extent.width), int(swapchain.extent.height));
616 auto *slot = currentUiColorSlot();
617 if (!slot || !uiRenderPass || !slot->framebuffer) return false;
618
619 auto &cb = currentPresentCb();
620 const bool msaa = uiColorSamples != vk::SampleCountFlagBits::e1;
621 std::array<vk::ClearValue, 2> clears{};
622 clears[0].color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
623 clears[1].color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
624 vk::RenderPassBeginInfo rpBegin{};
625 rpBegin.renderPass = uiRenderPass;
626 rpBegin.framebuffer = slot->framebuffer;
627 rpBegin.renderArea = vk::Rect2D{{0, 0}, {uint32_t(uiColorWidth), uint32_t(uiColorHeight)}};
628 rpBegin.clearValueCount = msaa ? 2 : 1;
629 rpBegin.pClearValues = clears.data();
630
631 slot->color.beginColorAttachment();
632 if (msaa) slot->msaaColor.beginColorAttachment();
633 cb.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
634
635 VkCommandBuffer raw = static_cast<VkCommandBuffer>(cb);
637
638 cb.endRenderPass();
639 slot->color.endSampledLayout();
640 queueUiResolve();
641 return true;
642}
643
644
645void Graphics::createGBufferResources(int width, int height) {
646 if (width <= 0 || height <= 0) return;
647 if (!gbufferSlots.empty() && gbufferWidth == width && gbufferHeight == height && gbufferPipeline)
648 return;
649 destroyGBufferResources();
650
651 gbufferWidth = width;
652 gbufferHeight = height;
653 const uint32_t w = uint32_t(width);
654 const uint32_t h = uint32_t(height);
655 const vk::Format colorFmt = pickGBufferColorFormat(device);
656 const vk::Format depthFmt = vk::Format::eD32Sfloat;
657
658 gbufferSlots.resize(kAsyncResourceCopies);
659 for (auto &slot : gbufferSlots) {
660 slot.normal = device.createColorTarget(w, h, colorFmt);
661 slot.depthColor = device.createColorTarget(w, h, colorFmt);
662 slot.albedo = device.createColorTarget(w, h, colorFmt);
663 slot.depth = device.createDepthTarget(w, h, depthFmt, true);
664 }
665
666 auto gbufferPass =
667 device.createRenderPass()
668 .addSampledColorAttachment(colorFmt)
669 .addSampledColorAttachment(colorFmt)
670 .addSampledColorAttachment(colorFmt)
671 .addSampledDepthAttachment(depthFmt)
672 .addSubpass(vkb::SubpassBuilder()
673 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal)
674 .addAttachmentRef(1, vk::ImageLayout::eColorAttachmentOptimal)
675 .addAttachmentRef(2, vk::ImageLayout::eColorAttachmentOptimal)
676 .setDepthStencilAttachment(
677 3, vk::ImageLayout::eDepthStencilAttachmentOptimal))
678 .addExternalShaderReadDependencies()
679 .build();
680 gbufferRenderPass = gbufferPass;
681
682 for (auto &slot : gbufferSlots) {
683 slot.framebuffer = gbufferPass.createFramebuffer(
684 device, w, h,
685 {slot.normal.asAttachment(), slot.depthColor.asAttachment(), slot.albedo.asAttachment(),
686 slot.depth.asAttachment()});
687 }
688
689 auto layoutBuilder = device.createPipelineLayout();
690 if (texSetLayout) layoutBuilder.set(texSetLayout);
691 gbufferPipelineLayout =
692 layoutBuilder
693 .push<GBufferPush>(vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment)
694 .build();
695
696 std::vector<uint32_t> vert(mesh3d_gbuffer_vert_spv,
697 mesh3d_gbuffer_vert_spv + mesh3d_gbuffer_vert_spv_count);
698 std::vector<uint32_t> frag(mesh3d_gbuffer_frag_spv,
699 mesh3d_gbuffer_frag_spv + mesh3d_gbuffer_frag_spv_count);
700 vk::ShaderModule vertModule = vkb::PipelineBuilder::createShaderModule(device.instance, vert);
701 vk::ShaderModule fragModule = vkb::PipelineBuilder::createShaderModule(device.instance, frag);
702 gbufferPipeline = device.createPipeline()
703 .useClassicPipeline(vertModule, fragModule)
704 .setPipelineLayout(gbufferPipelineLayout)
705 .setVertexInputState(vkb::VertexInputStateBuilder()
706 .addInputBinding<MeshVertex>()
707 .addAttributeDescription<MeshVertex>())
708 .setDynamicStatesViewportScissor()
709 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
710 vk::CullModeFlagBits::eNone, vk::FrontFace::eClockwise)
711 .build(gbufferPass);
712 device->destroyShaderModule(vertModule);
713 device->destroyShaderModule(fragModule);
714
715 // Alpha-cutout variant for billboard/card geometry (sprite-stack slices):
716 // same layout/push constants, fragment discards transparent texels.
717 vk::ShaderModule alphaVertModule =
718 vkb::PipelineBuilder::createShaderModule(device.instance, vert);
719 std::vector<uint32_t> alphaFrag(mesh3d_gbuffer_alpha_frag_spv,
720 mesh3d_gbuffer_alpha_frag_spv +
721 mesh3d_gbuffer_alpha_frag_spv_count);
722 vk::ShaderModule alphaFragModule =
723 vkb::PipelineBuilder::createShaderModule(device.instance, alphaFrag);
724 gbufferAlphaPipeline = device.createPipeline()
725 .useClassicPipeline(alphaVertModule, alphaFragModule)
726 .setPipelineLayout(gbufferPipelineLayout)
727 .setVertexInputState(vkb::VertexInputStateBuilder()
728 .addInputBinding<MeshVertex>()
729 .addAttributeDescription<MeshVertex>())
730 .setDynamicStatesViewportScissor()
731 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f,
732 vk::CullModeFlagBits::eNone,
733 vk::FrontFace::eClockwise)
734 .build(gbufferPass);
735 device->destroyShaderModule(alphaVertModule);
736 device->destroyShaderModule(alphaFragModule);
737
738 auto makeSampleTex = [&](GpuTexture &gpu, Texture &tex, vk::ImageView view) {
739 vkb::SamplerBuilder sb;
740 gpu.sampler = sb.nearestClamp().build(device);
741 auto sets = vkb::DescriptorSetBuilder()
742 .layout(texSetLayout)
743 .build(device.instance, descriptorPool);
744 gpu.descriptorSet = vkb::BoundSet{sets[0]};
745 gpu.width = width;
746 gpu.height = height;
747 gpu.viewOverride = view;
748 writeCombinedImageDescriptor(&gpu);
749 tex.width = width;
750 tex.height = height;
751 tex.pixelWidth = width;
752 tex.pixelHeight = height;
753 tex.gpuHandle = &gpu;
754 };
755 for (auto &slot : gbufferSlots) {
756 makeSampleTex(slot.normalGpu, slot.normalTex, slot.normal.imageView());
757 makeSampleTex(slot.depthColorGpu, slot.depthColorTex, slot.depthColor.imageView());
758 makeSampleTex(slot.albedoGpu, slot.albedoTex, slot.albedo.imageView());
759 makeSampleTex(slot.depthGpu, slot.depthTex, slot.depth.imageView());
760 }
761}
762
763void Graphics::createSceneColorResources(int width, int height) {
764 if (width <= 0 || height <= 0) return;
765 if (!texSetLayout || !descriptorPool) return;
766 const vk::Format colorFmt = swapchain.image_format;
767 if (colorFmt == vk::Format::eUndefined) return;
768
769 const bool featureMsaa = !renderControl_ || renderControl_->isEnabled("msaa");
770 const int desired = featureMsaa ? msaaSamples : 0;
771 if (desired != appliedMsaa) appliedMsaa = desired;
772 const vk::SampleCountFlagBits samples = sampleCountFlagFor(clampMsaaSamples(appliedMsaa));
773
774 if (!sceneColorSlots.empty() && sceneColorWidth == width && sceneColorHeight == height &&
775 sceneColorFormat == colorFmt && sceneColorSamples == samples && sceneColorRenderPass)
776 return;
777 destroySceneColorResources();
778
779 sceneColorWidth = width;
780 sceneColorHeight = height;
781 sceneColorFormat = colorFmt;
782 sceneColorSamples = samples;
783 const uint32_t w = uint32_t(width);
784 const uint32_t h = uint32_t(height);
785 const vk::Format depthFmt = depthFormat;
786 const bool msaa = samples != vk::SampleCountFlagBits::e1;
787
788 sceneColorSlots.resize(kAsyncResourceCopies);
789 for (auto &slot : sceneColorSlots) {
790 slot.color = device.createColorTarget(w, h, colorFmt);
791 if (msaa) {
792 slot.msaaColor = device.createColorTarget(w, h, colorFmt, samples);
793 slot.depth = device.createDepthTarget(w, h, depthFmt, false, samples);
794 } else {
795 slot.depth = device.createDepthTarget(w, h, depthFmt, false);
796 }
797 }
798
799 if (msaa) {
800 // MSAA color is transient (DONT_CARE store). Sampling the 1x resolve.
801 // addSampledColorAttachment on the MSAA target makes MoltenVK treat it
802 // as a shader-readable texture2d and segfaults on macOS.
803 sceneColorRenderPass =
804 device.createRenderPass()
805 .addColorAttachment(colorFmt, vk::AttachmentLoadOp::eClear,
806 vk::AttachmentStoreOp::eDontCare, samples)
807 .addResolveColorAttachment(colorFmt, vk::AttachmentLoadOp::eDontCare)
808 .addDepthAttachment(depthFmt, vk::AttachmentLoadOp::eClear,
809 vk::AttachmentStoreOp::eDontCare, samples)
810 .addSubpass(vkb::SubpassBuilder()
811 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal)
812 .addResolveAttachment(1, vk::ImageLayout::eColorAttachmentOptimal)
813 .setDepthStencilAttachment(
814 2, vk::ImageLayout::eDepthStencilAttachmentOptimal))
815 .addExternalShaderReadDependencies()
816 .build();
817 } else {
818 sceneColorRenderPass =
819 device.createRenderPass()
820 .addSampledColorAttachment(colorFmt)
821 .addDepthAttachment(depthFmt, vk::AttachmentLoadOp::eClear,
822 vk::AttachmentStoreOp::eDontCare)
823 .addSubpass(vkb::SubpassBuilder()
824 .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal)
825 .setDepthStencilAttachment(
826 1, vk::ImageLayout::eDepthStencilAttachmentOptimal))
827 .addExternalShaderReadDependencies()
828 .build();
829 }
830 auto scenePass = sceneColorRenderPass;
831
832 for (auto &slot : sceneColorSlots) {
833 if (msaa) {
834 slot.framebuffer = scenePass.createFramebuffer(
835 device, w, h,
836 {slot.msaaColor.asAttachment(), slot.color.asAttachment(), slot.depth.asAttachment()});
837 } else {
838 slot.framebuffer = scenePass.createFramebuffer(
839 device, w, h, {slot.color.asAttachment(), slot.depth.asAttachment()});
840 }
841 }
842
843 auto makeSampleTex = [&](GpuTexture &gpu, Texture &tex, vk::ImageView view) {
844 vkb::SamplerBuilder sb;
845 gpu.sampler = sb.magFilter(vk::Filter::eLinear)
846 .minFilter(vk::Filter::eLinear)
847 .addressModeU(vk::SamplerAddressMode::eClampToEdge)
848 .addressModeV(vk::SamplerAddressMode::eClampToEdge)
849 .build(device);
850 auto sets = vkb::DescriptorSetBuilder()
851 .layout(texSetLayout)
852 .build(device.instance, descriptorPool);
853 gpu.descriptorSet = vkb::BoundSet{sets[0]};
854 gpu.width = width;
855 gpu.height = height;
856 gpu.viewOverride = view;
857 writeCombinedImageDescriptor(&gpu);
858 tex.width = width;
859 tex.height = height;
860 tex.pixelWidth = width;
861 tex.pixelHeight = height;
862 tex.gpuHandle = &gpu;
863 };
864 for (auto &slot : sceneColorSlots) makeSampleTex(slot.colorGpu, slot.colorTex, slot.color.imageView());
865}
866
867bool Graphics::beginSceneColorRenderPass() {
868 auto *slot = currentSceneColorSlot();
869 if (!slot || !sceneColorRenderPass || !slot->framebuffer) return false;
870 auto &cb = currentPresentCb();
871 const bool msaa = sceneColorSamples != vk::SampleCountFlagBits::e1;
872 // A=1 marks sky / far plane so SSGI skips uncleared pixels.
873 if (msaa) {
874 std::array<vk::ClearValue, 3> clears{};
875 clears[0].color = vk::ClearColorValue(
876 std::array<float, 4>{clearColor.r, clearColor.g, clearColor.b, 1.f});
877 clears[1].color = vk::ClearColorValue(
878 std::array<float, 4>{clearColor.r, clearColor.g, clearColor.b, 1.f});
879 clears[2].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
880 vk::RenderPassBeginInfo rpBegin{};
881 rpBegin.renderPass = sceneColorRenderPass;
882 rpBegin.framebuffer = slot->framebuffer;
883 rpBegin.renderArea =
884 vk::Rect2D{{0, 0}, {uint32_t(sceneColorWidth), uint32_t(sceneColorHeight)}};
885 rpBegin.clearValueCount = uint32_t(clears.size());
886 rpBegin.pClearValues = clears.data();
887 slot->msaaColor.beginColorAttachment();
888 slot->color.beginColorAttachment();
889 slot->depth.beginDepthAttachment();
890 cb.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
891 } else {
892 std::array<vk::ClearValue, 2> clears{};
893 clears[0].color = vk::ClearColorValue(
894 std::array<float, 4>{clearColor.r, clearColor.g, clearColor.b, 1.f});
895 clears[1].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
896 vk::RenderPassBeginInfo rpBegin{};
897 rpBegin.renderPass = sceneColorRenderPass;
898 rpBegin.framebuffer = slot->framebuffer;
899 rpBegin.renderArea =
900 vk::Rect2D{{0, 0}, {uint32_t(sceneColorWidth), uint32_t(sceneColorHeight)}};
901 rpBegin.clearValueCount = uint32_t(clears.size());
902 rpBegin.pClearValues = clears.data();
903 slot->color.beginColorAttachment();
904 slot->depth.beginDepthAttachment();
905 cb.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
906 }
907 sceneColorPassOpen = true;
908 return true;
909}
910
911void Graphics::endSceneColorRenderPass() {
912 if (!sceneColorPassOpen) return;
913 auto &cb = currentPresentCb();
914 cb.endRenderPass();
915 sceneColorPassOpen = false;
916 if (auto *slot = currentSceneColorSlot())
917 slot->color.endSampledLayout();
918}
919
920int Graphics::clampMsaaSamples(int requested) const {
921 if (requested <= 1) return 0;
922 vk::SampleCountFlags supported =
923 device.physical_device.properties.limits.framebufferColorSampleCounts &
924 device.physical_device.properties.limits.framebufferDepthSampleCounts;
925 const int candidates[3] = {8, 4, 2};
926 for (int c : candidates) {
927 if (requested >= c && (supported & sampleCountFlagFor(c))) return c;
928 }
929 for (int c : candidates) {
930 if (supported & sampleCountFlagFor(c)) return c;
931 }
932 return 0;
933}
934
935void Graphics::ensureScenePassPipelines(const vkb::BuiltRenderPass &target,
936 vk::SampleCountFlagBits samples) {
937 const vk::RenderPass targetHandle = vk::RenderPass(target);
938 if (targetHandle == scenePassPipelineTarget && samples == scenePassPipelineSamples) return;
939 device->waitIdle();
940
941 destroyPipeline(device, mesh3dPipeline);
942 destroyPipeline(device, mesh3dClusteredPipeline);
943 destroyPipeline(device, voxelRectPipeline);
944
945 mesh3dPipeline = createMesh3DStylePipeline(embeddedSpirv(mesh3d_vert_spv),
946 embeddedSpirv(mesh3d_frag_spv),
947 mesh3dPipelineLayout, target, samples);
948 mesh3dClusteredPipeline =
949 createMesh3DStylePipeline(embeddedSpirv(mesh3d_clustered_vert_spv),
950 embeddedSpirv(mesh3d_clustered_frag_spv),
951 mesh3dClusteredPipelineLayout, target, samples);
952 voxelRectPipeline = buildVoxelRectPipeline(target, samples);
953
954 for (auto &g : ownedGpuShaders) {
955 if (!g->isMesh3D) continue;
956 destroyPipeline(device, g->mesh3dPipeline);
957 destroyPipeline(device, g->mesh3dXrayPipeline);
958 g->mesh3dXrayPipeline = nullptr;
959 if (!g->owner) continue;
960 if (g->isHair3D) {
961 g->mesh3dPipeline =
962 createMesh3DHairPipeline(g->owner->vertexSpirv(), g->owner->fragmentSpirv(),
963 g->pipelineLayout, target, samples);
964 } else {
965 g->mesh3dPipeline =
966 createMesh3DStylePipeline(g->owner->vertexSpirv(), g->owner->fragmentSpirv(),
967 g->pipelineLayout, target, samples);
968 g->mesh3dXrayPipeline =
969 createMesh3DXrayPipeline(g->owner->vertexSpirv(), g->owner->fragmentSpirv(),
970 g->pipelineLayout, target, samples);
971 }
972 }
973
974 scenePassPipelineTarget = targetHandle;
975 scenePassPipelineSamples = samples;
976}
977
978void Graphics::queueSceneColorResolve() {
979 Texture *src = getSceneColorTexture();
980 if (!src || !src->gpuHandle) return;
981 Shader *sh = prepareSceneColorResolveShader(src);
982 if (!sh) return;
983 if (sceneColorComposited) return;
984 TexturedBatch resolve{src, nullptr, sh, BlendMode::Alpha, Batcher{}};
985 resolve.batch.addTexturedRect(0.f, 0.f, float(width), float(height), Color(1.f, 1.f, 1.f, 1.f),
986 0.f, 0.f, 1.f, 1.f);
987 pendingSceneResolve = std::move(resolve);
988}
989
990void Graphics::createShadowResources() {
991 if (!shadowMaps.empty()) return;
992
993 const uint32_t size = uint32_t(ShadowConfig::kMapSize);
994 const uint32_t layers = uint32_t(ShadowConfig::kCascades);
995
996 shadowMaps.resize(kAsyncResourceCopies);
997 for (auto &slot : shadowMaps)
998 slot.image = device.createDepthArray(size, size, layers, vk::Format::eD32Sfloat);
999
1000 vkb::SamplerBuilder sb;
1001 // Hardware PCF: linear filtering + depth compare on the D32 shadow map.
1002 // The compare result of each filtered depth sample is blended by the driver,
1003 // giving a soft penumbra instead of the hard 1-texel boundary.
1004 shadowSampler = sb.linearClamp().compareEnable(VK_TRUE).compareOp(vk::CompareOp::eLess).buildDepthPcf(device);
1005
1006 auto shadowPass =
1007 device.createRenderPass()
1008 .addSampledDepthAttachment(vk::Format::eD32Sfloat)
1009 .addSubpass(vkb::SubpassBuilder().setDepthStencilAttachment(
1010 0, vk::ImageLayout::eDepthStencilAttachmentOptimal))
1011 .addExternalShaderReadDependencies()
1012 .build();
1013 shadowRenderPass = shadowPass;
1014
1015 for (auto &slot : shadowMaps) {
1016 for (uint32_t i = 0; i < layers; ++i) {
1017 slot.framebuffers[i] = shadowPass.createFramebuffer(
1018 device, size, size, {slot.image.layerAttachment(i)});
1019 }
1020 }
1021
1022 shadowPipelineLayout =
1023 device.createPipelineLayout()
1024 .push<glm::mat4>(vk::ShaderStageFlagBits::eVertex)
1025 .build();
1026
1027 std::vector<uint32_t> vert(mesh3d_shadow_vert_spv,
1028 mesh3d_shadow_vert_spv + mesh3d_shadow_vert_spv_count);
1029 std::vector<uint32_t> frag(mesh3d_shadow_frag_spv,
1030 mesh3d_shadow_frag_spv + mesh3d_shadow_frag_spv_count);
1031 vk::ShaderModule vertModule = vkb::PipelineBuilder::createShaderModule(device.instance, vert);
1032 vk::ShaderModule fragModule = vkb::PipelineBuilder::createShaderModule(device.instance, frag);
1033 shadowPipeline =
1034 device.createPipeline()
1035 .useClassicPipeline(vertModule, fragModule)
1036 .setPipelineLayout(shadowPipelineLayout)
1037 .setVertexInputState(vkb::VertexInputStateBuilder()
1038 .addInputBinding<MeshVertex>()
1039 .addAttributeDescription<MeshVertex>())
1040 .setDynamicStatesViewportScissor()
1041 // No cull: Cornell-style one-sided interiors keep writing when the
1042 // ceiling/walls are back-facing the sun. Closest depth still wins
1043 // on closed meshes, so floors are not punched through.
1044 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f, vk::CullModeFlagBits::eNone,
1045 vk::FrontFace::eClockwise)
1046 .setDepthBias(0.0f, 0.5f)
1047 .build(shadowPass);
1048 device->destroyShaderModule(vertModule);
1049 device->destroyShaderModule(fragModule);
1050
1051 // Alpha-cutout variant for billboard/card shadow casters (sprite-stack
1052 // slices): same transform push constant, fragment samples the albedo and
1053 // discards transparent texels so shadows follow the silhouette.
1054 shadowAlphaPipelineLayout = device.createPipelineLayout()
1055 .set(texSetLayout)
1056 .push<glm::mat4>(vk::ShaderStageFlagBits::eVertex)
1057 .build();
1058 std::vector<uint32_t> alphaVert(mesh3d_shadow_alpha_vert_spv,
1059 mesh3d_shadow_alpha_vert_spv +
1060 mesh3d_shadow_alpha_vert_spv_count);
1061 std::vector<uint32_t> alphaFrag(mesh3d_shadow_alpha_frag_spv,
1062 mesh3d_shadow_alpha_frag_spv +
1063 mesh3d_shadow_alpha_frag_spv_count);
1064 vk::ShaderModule alphaVertModule =
1065 vkb::PipelineBuilder::createShaderModule(device.instance, alphaVert);
1066 vk::ShaderModule alphaFragModule =
1067 vkb::PipelineBuilder::createShaderModule(device.instance, alphaFrag);
1068 shadowAlphaPipeline =
1069 device.createPipeline()
1070 .useClassicPipeline(alphaVertModule, alphaFragModule)
1071 .setPipelineLayout(shadowAlphaPipelineLayout)
1072 .setVertexInputState(vkb::VertexInputStateBuilder()
1073 .addInputBinding<MeshVertex>()
1074 .addAttributeDescription<MeshVertex>())
1075 .setDynamicStatesViewportScissor()
1076 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f, vk::CullModeFlagBits::eNone,
1077 vk::FrontFace::eClockwise)
1078 .setDepthBias(0.0f, 0.5f)
1079 .build(shadowPass);
1080 device->destroyShaderModule(alphaVertModule);
1081 device->destroyShaderModule(alphaFragModule);
1082
1083 // Clear every ping-pong copy so sampling before the first real shadow pass
1084 // sees SHADER_READ_ONLY rather than UNDEFINED.
1085 vkb::executeImmediately(device.instance, uploadPool, device.getQueue(vkb::QueueType::graphics),
1086 [&](vk::CommandBuffer cb) {
1087 vk::ClearValue clear{};
1088 clear.depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
1089 for (auto &slot : shadowMaps) {
1090 slot.image.beginDepthAttachment();
1091 for (int c = 0; c < ShadowConfig::kCascades; ++c) {
1092 vk::RenderPassBeginInfo rpBegin{};
1093 rpBegin.renderPass = shadowRenderPass;
1094 rpBegin.framebuffer = slot.framebuffers[c];
1095 rpBegin.renderArea = vk::Rect2D{{0, 0}, {size, size}};
1096 rpBegin.clearValueCount = 1;
1097 rpBegin.pClearValues = &clear;
1098 cb.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
1099 cb.endRenderPass();
1100 }
1101 slot.image.endSampledLayout();
1102 }
1103 });
1104}
1105
1106void Graphics::ensureClusteredBuffers(size_t lightsBytes, size_t tableBytes, size_t indicesBytes) {
1107 auto &st = currentClusteredStorage();
1108 auto ensure = [&](vkb::GenericBuffer &buf, size_t &cap, size_t need) {
1109 if (need == 0) need = 4;
1110 if (cap >= need && buf.buffer) return;
1111 buf.release();
1112 // Grow with some slack.
1113 size_t alloc = std::max(need, cap ? cap * 2 : need);
1114 buf.allocate(frameToken(), device, vk::BufferUsageFlagBits::eStorageBuffer, vk::DeviceSize(alloc),
1115 vk::MemoryPropertyFlagBits::eHostVisible |
1116 vk::MemoryPropertyFlagBits::eHostCoherent);
1117 cap = alloc;
1118 };
1119 ensure(st.lightsBuf, st.lightsCap, lightsBytes);
1120 ensure(st.tableBuf, st.tableCap, tableBytes);
1121 ensure(st.indicesBuf, st.indicesCap, indicesBytes);
1122 // Reallocated handles invalidate this frame's cached descriptor sets; drop
1123 // them so mesh3dClusteredSetFor rebinds against the new buffers.
1124 currentMesh3dClusteredFrameSlots().sets.clear();
1125}
1126
1127void Graphics::uploadClusteredLighting(const ClusteredLightingUpload &upload) {
1128 const size_t lightsBytes =
1129 std::max(size_t(1), upload.lights.size()) * sizeof(ClusteredLightGpu);
1130 const size_t tableBytes =
1131 std::max(size_t(1), upload.clusterTable.size()) * sizeof(ClusterTableEntry);
1132 const size_t indicesBytes =
1133 std::max(size_t(1), upload.lightIndices.size()) * sizeof(uint32_t);
1134 ensureClusteredBuffers(lightsBytes, tableBytes, indicesBytes);
1135
1136 auto &st = currentClusteredStorage();
1137 if (!upload.lights.empty())
1138 st.lightsBuf.updateLocal(frameToken(), upload.lights.data(),
1139 upload.lights.size() * sizeof(ClusteredLightGpu));
1140 else {
1141 ClusteredLightGpu zero{};
1142 st.lightsBuf.updateLocal(frameToken(), &zero, sizeof(zero));
1143 }
1144 st.tableBuf.updateLocal(frameToken(), upload.clusterTable.data(),
1145 upload.clusterTable.size() * sizeof(ClusterTableEntry));
1146 st.indicesBuf.updateLocal(frameToken(), upload.lightIndices.data(),
1147 upload.lightIndices.size() * sizeof(uint32_t));
1148}
1149
1151 mesh3dClusteredActive = upload.active;
1152 mesh3dClustered = upload;
1153 if (upload.active) uploadClusteredLighting(upload);
1154}
1155
1157 mesh3dClusteredActive = active;
1158}
1159
1160void Graphics::ensureMesh3dStrides() {
1161 if (mesh3dUboStride != 0) return;
1162 const uint32_t align = std::max(
1163 1u, uint32_t(device.physical_device.properties.limits.minUniformBufferOffsetAlignment));
1164 mesh3dUboStride = alignUpValue(uint32_t(sizeof(Mesh3DUBO)), align);
1165 shadowUboStride = alignUpValue(uint32_t(sizeof(ShadowUBO)), align);
1166 mesh3dClusteredUboStride = alignUpValue(uint32_t(sizeof(Mesh3DClusteredUBO)), align);
1167}
1168
1169void Graphics::ensureMesh3dRing(Mesh3dFrameSlots &fslots) {
1170 ensureMesh3dStrides();
1171 const size_t want = std::max<size_t>(2048, fslots.lastDrawCount + 512);
1172 if (fslots.uboRing.buffer && fslots.capacity >= want) return;
1173 const size_t cap = std::max(want, fslots.capacity * 2);
1174 // Only called at frame start (before any draw of this frame is recorded),
1175 // so releasing the old ring is safe: no in-flight reader of this slot.
1176 fslots.uboRing.release();
1177 fslots.uboRing.allocate(frameToken(), device, vk::BufferUsageFlagBits::eUniformBuffer,
1178 vk::DeviceSize(cap) * mesh3dUboStride, kHostVisibleCoherent);
1179 fslots.shadowRing.release();
1180 fslots.shadowRing.allocate(frameToken(), device, vk::BufferUsageFlagBits::eUniformBuffer,
1181 vk::DeviceSize(cap) * shadowUboStride, kHostVisibleCoherent);
1182 fslots.capacity = cap;
1183 fslots.sets.clear(); // cached sets reference the old rings
1184}
1185
1186void Graphics::ensureMesh3dClusteredRing(Mesh3dClusteredFrameSlots &fslots) {
1187 ensureMesh3dStrides();
1188 const size_t want = std::max<size_t>(2048, fslots.lastDrawCount + 512);
1189 if (fslots.uboRing.buffer && fslots.capacity >= want) return;
1190 const size_t cap = std::max(want, fslots.capacity * 2);
1191 fslots.uboRing.release();
1192 fslots.uboRing.allocate(frameToken(), device, vk::BufferUsageFlagBits::eUniformBuffer,
1193 vk::DeviceSize(cap) * mesh3dClusteredUboStride, kHostVisibleCoherent);
1194 fslots.shadowRing.release();
1195 fslots.shadowRing.allocate(frameToken(), device, vk::BufferUsageFlagBits::eUniformBuffer,
1196 vk::DeviceSize(cap) * shadowUboStride, kHostVisibleCoherent);
1197 fslots.capacity = cap;
1198 fslots.sets.clear();
1199}
1200
1201vkb::BoundSet Graphics::mesh3dClusteredSetFor(GpuTexture *gpuTex, GpuTexture *normalTex,
1202 GpuTexture *envTex, GpuTexture *heightTex,
1203 Mesh3dClusteredFrameSlots &fslots) {
1204 ASSERT(gpuTex != nullptr);
1205 ASSERT(normalTex != nullptr);
1206 ASSERT(envTex != nullptr);
1207 ASSERT(heightTex != nullptr);
1208 ASSERT(currentShadowArrayView());
1209 ASSERT(fslots.uboRing.buffer);
1210 ASSERT(fslots.shadowRing.buffer);
1211
1212 Mesh3dSetKey key{gpuTex, normalTex, envTex, heightTex, nullptr};
1213 auto it = fslots.sets.find(key);
1214 if (it != fslots.sets.end()) return it->second;
1215
1216 vk::DescriptorSetAllocateInfo alloc{};
1217 alloc.descriptorPool = descriptorPool;
1218 alloc.descriptorSetCount = 1;
1219 alloc.pSetLayouts = &mesh3dClusteredSetLayout;
1220 vkb::UnboundSet unbound{device->allocateDescriptorSets(alloc).front()};
1221
1222 auto &st = currentClusteredStorage();
1223 vkb::DescriptorSetUpdater updater(14, 14, 0);
1224 updater.beginDescriptorSet(unbound)
1225 .beginBuffers(0, 0, vk::DescriptorType::eUniformBufferDynamic)
1226 .buffer(fslots.uboRing.buffer, 0, fslots.uboRing.size)
1227 .beginImages(1, 0, vk::DescriptorType::eCombinedImageSampler)
1228 .image(vkb::SampledImage::forLaterSample(gpuTex->sampler, gpuTex->imageView()))
1229 .beginImages(2, 0, vk::DescriptorType::eCombinedImageSampler)
1230 .image(vkb::SampledImage::forLaterSample(normalTex->sampler, normalTex->imageView()))
1231 .beginImages(3, 0, vk::DescriptorType::eCombinedImageSampler)
1232 .image(vkb::SampledImage::forLaterSample(envTex->sampler, envTex->imageView()))
1233 .beginBuffers(4, 0, vk::DescriptorType::eStorageBuffer)
1234 .buffer(st.lightsBuf.buffer, 0, vk::DeviceSize(st.lightsCap))
1235 .beginBuffers(5, 0, vk::DescriptorType::eStorageBuffer)
1236 .buffer(st.tableBuf.buffer, 0, vk::DeviceSize(st.tableCap))
1237 .beginBuffers(6, 0, vk::DescriptorType::eStorageBuffer)
1238 .buffer(st.indicesBuf.buffer, 0, vk::DeviceSize(st.indicesCap))
1239 .beginBuffers(7, 0, vk::DescriptorType::eUniformBufferDynamic)
1240 .buffer(fslots.shadowRing.buffer, 0, fslots.shadowRing.size)
1241 .beginImages(8, 0, vk::DescriptorType::eCombinedImageSampler)
1242 .image(vkb::SampledImage::forLaterSample(shadowSampler, currentShadowArrayView()))
1243 .beginImages(9, 0, vk::DescriptorType::eCombinedImageSampler)
1244 .image(vkb::SampledImage::forLaterSample(heightTex->sampler, heightTex->imageView()))
1245 .update(device.instance);
1246
1247 vkb::BoundSet bound = std::move(unbound).publish();
1248 fslots.sets.emplace(key, bound);
1249 return bound;
1250}
1251
1252vk::Pipeline Graphics::createMesh3DStylePipeline(const std::vector<uint32_t> &vert,
1253 const std::vector<uint32_t> &frag,
1254 vk::PipelineLayout layout,
1255 const vkb::BuiltRenderPass &rp,
1256 vk::SampleCountFlagBits samples) {
1257 vk::ShaderModule vertModule = vkb::PipelineBuilder::createShaderModule(device.instance, vert);
1258 vk::ShaderModule fragModule = vkb::PipelineBuilder::createShaderModule(device.instance, frag);
1259 vk::Pipeline pipeline =
1260 device.createPipeline()
1261 .useClassicPipeline(vertModule, fragModule)
1262 .setPipelineLayout(layout)
1263 .setVertexInputState(vkb::VertexInputStateBuilder()
1264 .addInputBinding<MeshVertex>()
1265 .addAttributeDescription<MeshVertex>())
1266 .setDynamicStatesViewportScissor()
1267 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f, vk::CullModeFlagBits::eNone,
1268 vk::FrontFace::eClockwise)
1269 .setMultisampler(false, samples)
1270 .setDepthStencil(true, true, vk::CompareOp::eLess)
1271 .setColorAttachmentCount(1)
1272 .build(rp);
1273 device->destroyShaderModule(vertModule);
1274 device->destroyShaderModule(fragModule);
1275 return pipeline;
1276}
1277
1278vk::Pipeline Graphics::createMesh3DHairPipeline(const std::vector<uint32_t> &vert,
1279 const std::vector<uint32_t> &frag,
1280 vk::PipelineLayout layout,
1281 const vkb::BuiltRenderPass &rp,
1282 vk::SampleCountFlagBits samples) {
1283 vk::ShaderModule vertModule = vkb::PipelineBuilder::createShaderModule(device.instance, vert);
1284 vk::ShaderModule fragModule = vkb::PipelineBuilder::createShaderModule(device.instance, frag);
1285 vk::Pipeline pipeline =
1286 device.createPipeline()
1287 .useClassicPipeline(vertModule, fragModule)
1288 .setPipelineLayout(layout)
1289 .setVertexInputState(vkb::VertexInputStateBuilder()
1290 .addInputBinding<MeshVertex>()
1291 .addAttributeDescription<MeshVertex>())
1292 .setDynamicStatesViewportScissor()
1293 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f, vk::CullModeFlagBits::eNone,
1294 vk::FrontFace::eClockwise)
1295 .setDepthBias(-1.5f, -1.0f)
1296 .setMultisampler(false, samples)
1297 .setDepthStencil(true, false, vk::CompareOp::eLess)
1298 .setAlphaBlending(1)
1299 .build(rp);
1300 device->destroyShaderModule(vertModule);
1301 device->destroyShaderModule(fragModule);
1302 return pipeline;
1303}
1304
1305vk::Pipeline Graphics::createMesh3DXrayPipeline(const std::vector<uint32_t> &vert,
1306 const std::vector<uint32_t> &frag,
1307 vk::PipelineLayout layout,
1308 const vkb::BuiltRenderPass &rp,
1309 vk::SampleCountFlagBits samples) {
1310 vk::ShaderModule vertModule = vkb::PipelineBuilder::createShaderModule(device.instance, vert);
1311 vk::ShaderModule fragModule = vkb::PipelineBuilder::createShaderModule(device.instance, frag);
1312 vk::Pipeline pipeline =
1313 device.createPipeline()
1314 .useClassicPipeline(vertModule, fragModule)
1315 .setPipelineLayout(layout)
1316 .setVertexInputState(vkb::VertexInputStateBuilder()
1317 .addInputBinding<MeshVertex>()
1318 .addAttributeDescription<MeshVertex>())
1319 .setDynamicStatesViewportScissor()
1320 .setRasterizer(vk::PolygonMode::eFill, false, false, 1.0f, vk::CullModeFlagBits::eNone,
1321 vk::FrontFace::eClockwise)
1322 .setMultisampler(false, samples)
1323 // Depth test/write off: the shader discards visible fragments itself
1324 // (sampling G-buffer scene depth), so occluded ones can paint over walls.
1325 .setDepthStencil(false, false, vk::CompareOp::eLess)
1326 .setAlphaBlending(1)
1327 .setColorAttachmentCount(1)
1328 .build(rp);
1329 device->destroyShaderModule(vertModule);
1330 device->destroyShaderModule(fragModule);
1331 return pipeline;
1332}
1333
1334void Graphics::ensureOffscreenPipelines() {
1335 if (offscreenRenderPass) return;
1336
1337 vkb::RenderPassBuilder rpBuilder = device.createRenderPass();
1338 offscreenRenderPass =
1339 rpBuilder.addSampledColorAttachment(vk::Format::eR8G8B8A8Unorm)
1340 .addSubpass(vkb::SubpassBuilder().addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal))
1341 .addDependency(VK_SUBPASS_EXTERNAL, 0)
1342 .build();
1343
1344 offscreenSolidPipeline =
1345 createSolidColorPipeline(device, offscreenRenderPass, pipelineLayout);
1346 offscreenSolidAlphaPipeline =
1347 createSolidColorPipeline(device, offscreenRenderPass, pipelineLayout, BlendMode::Alpha);
1348 offscreenAdditiveSolidPipeline =
1349 createSolidColorPipeline(device, offscreenRenderPass, pipelineLayout, BlendMode::Additive);
1350
1351 // Textured offscreen pipeline mirrors createTexturedPipeline but with offscreen RP.
1352 auto tvert = embeddedSpirv(textured_vert_spv);
1353 auto tfrag = embeddedSpirv(textured_frag_spv);
1354 offscreenTexPipeline =
1355 createTexturedStylePipeline(tvert, tfrag, offscreenRenderPass, texPipelineLayout);
1356 offscreenAdditiveTexPipeline =
1357 createTexturedStylePipeline(tvert, tfrag, offscreenRenderPass, texPipelineLayout,
1359 offscreenOpaqueTexPipeline =
1360 createTexturedStylePipeline(tvert, tfrag, offscreenRenderPass, texPipelineLayout,
1362
1363 if (lit2dPipelineLayout) {
1364 auto lvert = embeddedSpirv(lit2d_vert_spv);
1365 auto lfrag = embeddedSpirv(lit2d_frag_spv);
1366 offscreenLitPipeline =
1367 createTexturedStylePipeline(lvert, lfrag, offscreenRenderPass, lit2dPipelineLayout);
1368 }
1369
1370 // Lazily create offscreen pipelines for any custom shaders already loaded.
1371 for (auto &sh : ownedShaders) ensureShaderOffscreenPipeline(sh.get());
1372}
1373
1374void Graphics::ensureShaderOffscreenPipeline(Shader *shader) {
1375 if (!shader || !shader->gpuHandle || !offscreenRenderPass) return;
1376 // Mesh3D SPIR-V / layout is incompatible with the 2D textured offscreen pass.
1377 if (shader->getKind() == Shader::Kind::eMesh3D) return;
1378 auto *gpu = static_cast<GpuShader *>(shader->gpuHandle);
1379 if (gpu->isMesh3D || gpu->offscreenPipeline) return;
1380 gpu->offscreenPipeline = createTexturedStylePipeline(shader->vertexSpirv(), shader->fragmentSpirv(),
1381 offscreenRenderPass, shaderPipelineLayout);
1382}
1383
1384Texture *Graphics::getTexture() { return nullptr; }
1385
1386
1387} // namespace eve::graphics::vulkan
bool active
Definition CardTypes.cpp:34
std::string layout
vkb::Device & device
vk::ShaderModule vert
vk::ShaderModule frag
float height
Definition Grass.cpp:235
int h
int w
uint32_t c
int width
Shader * shader
glm::mat4 view
int d
image::ImageData::Colorf color
std::string image
virtual Texture * getTexture()=0
Sampleable color buffer; screen Canvas returns nullptr.
virtual void setMesh3DClusteredActive(bool active)=0
Cheap per-draw toggle for the already-uploaded clustered light table. Unlike setMesh3DClusteredLighti...
Shader * prepareSceneColorResolveShader(Texture *scene)
FXAA resolve shader that writes opaque RGB (ignores scene-color depth alpha).
Definition Graphics.cpp:131
virtual void setMesh3DClusteredLighting(const ClusteredLightingUpload &upload)=0
Enable clustered forward path for subsequent default mesh draws (SSBO light lists)....
std::unique_ptr< RenderControl > renderControl_
Definition Graphics.h:990
PresentOverlayFn presentOverlayFn_
Definition Graphics.h:985
static constexpr uint32_t kPushConstantBytes
Definition Shader.h:33
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
Texture * getSceneColorTexture() override
Sampleable 3D color target for the current frame (RGB = lit, A = linear depth). Valid after begin3DFr...
Definition Graphics.cpp:410
void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth) override
Light3DGpu ClusteredLightGpu
glm::vec4 Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
BlendMode
2D quad blend mode (drawn in draw order within a layer).
Definition BlendMode.h:6
CPU-built clustered lighting upload for one frame/camera. Point lights are clustered; directional lig...
static constexpr int kMapSize
Definition Shadow.h:9
static constexpr int kCascades
Definition Shadow.h:8
Per-frame / per-draw CSM constants (std140). Binding separate from Mesh3D Frame UBO.
Definition Shadow.h:17