SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
VulkanSSAO.cpp
Go to the documentation of this file.
2
3#include <random>
4#include <cmath>
5#include <array>
6#include <cstring>
7#include <vector>
8#include "Core/Logger.hpp"
9
10namespace Sleak {
11 namespace RenderEngine {
12
13// ==================================================================
14// ==================== SSAO (HBAO-quality) =========================
15// ==================================================================
16// Half-resolution hemisphere AO with a 32-sample cosine-weighted kernel,
17// 4x4 random rotation tile, and depth-aware bilateral blur.
18
19/// Creates the full-res raw and blurred SSAO color images, views, and samplers.
20bool VulkanRenderer::CreateSSAOImages() {
21 // Full-resolution SSAO — half-res caused a visible seam at the center texel boundary.
22 m_ssaoExtent.width = scExtent.width;
23 m_ssaoExtent.height = scExtent.height;
24
25 auto createR8 = [&](VkImage& image, VkDeviceMemory& mem, VkImageView& view) -> bool {
26 VkImageCreateInfo info{};
27 info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
28 info.imageType = VK_IMAGE_TYPE_2D;
29 info.extent.width = m_ssaoExtent.width;
30 info.extent.height = m_ssaoExtent.height;
31 info.extent.depth = 1;
32 info.mipLevels = 1;
33 info.arrayLayers = 1;
34 info.format = m_ssaoFormat;
35 info.tiling = VK_IMAGE_TILING_OPTIMAL;
36 info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
37 // TRANSFER_DST enables vkCmdClearColorImage when SSAO is disabled
38 // (the lighting pass always samples the blur image regardless).
39 info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT
40 | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
41 info.samples = VK_SAMPLE_COUNT_1_BIT;
42 info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
43 if (vkCreateImage(device, &info, nullptr, &image) != VK_SUCCESS) return false;
44
45 VkMemoryRequirements req;
46 vkGetImageMemoryRequirements(device, image, &req);
47 VkMemoryAllocateInfo alloc{};
48 alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
49 alloc.allocationSize = req.size;
50 alloc.memoryTypeIndex = FindMemoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
51 if (vkAllocateMemory(device, &alloc, nullptr, &mem) != VK_SUCCESS) return false;
52 vkBindImageMemory(device, image, mem, 0);
53
54 VkImageViewCreateInfo vinfo{};
55 vinfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
56 vinfo.image = image;
57 vinfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
58 vinfo.format = m_ssaoFormat;
59 vinfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
60 return vkCreateImageView(device, &vinfo, nullptr, &view) == VK_SUCCESS;
61 };
62
63 if (!createR8(m_ssaoRawImage, m_ssaoRawMemory, m_ssaoRawView)) return false;
64 if (!createR8(m_ssaoBlurImage, m_ssaoBlurMemory, m_ssaoBlurView)) return false;
65
66 // Linear clamp sampler used by all SSAO consumers (the lighting pass
67 // samples at full res — linear reconstructs the half-res buffer smoothly).
68 VkSamplerCreateInfo ls{};
69 ls.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
70 ls.magFilter = VK_FILTER_LINEAR;
71 ls.minFilter = VK_FILTER_LINEAR;
72 ls.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
73 ls.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
74 ls.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
75 ls.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
76 ls.minLod = 0.0f;
77 ls.maxLod = 0.0f;
78 if (vkCreateSampler(device, &ls, nullptr, &m_ssaoSampler) != VK_SUCCESS) return false;
79
80 // Point sampler for depth input (we want nearest to avoid bilinear
81 // bleed across silhouettes when reading the depth buffer).
82 VkSamplerCreateInfo ps{};
83 ps.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
84 ps.magFilter = VK_FILTER_NEAREST;
85 ps.minFilter = VK_FILTER_NEAREST;
86 ps.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
87 ps.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
88 ps.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
89 ps.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
90 ps.minLod = 0.0f;
91 ps.maxLod = 0.0f;
92 if (vkCreateSampler(device, &ps, nullptr, &m_ssaoPointSampler) != VK_SUCCESS) return false;
93
94 return true;
95}
96
97/// Creates the shared SSAO render pass (R8 color, DONT_CARE load, shader-read-only output).
98bool VulkanRenderer::CreateSSAORenderPass() {
99 // Single-attachment render pass — R8 color, DONT_CARE load, STORE out,
100 // finalLayout SHADER_READ_ONLY so the next pass can sample directly.
101 VkAttachmentDescription colorAtt{};
102 colorAtt.format = m_ssaoFormat;
103 colorAtt.samples = VK_SAMPLE_COUNT_1_BIT;
104 colorAtt.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
105 colorAtt.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
106 colorAtt.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
107 colorAtt.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
108 colorAtt.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
109 colorAtt.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
110
111 VkAttachmentReference colorRef{};
112 colorRef.attachment = 0;
113 colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
114
115 VkSubpassDescription subpass{};
116 subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
117 subpass.colorAttachmentCount = 1;
118 subpass.pColorAttachments = &colorRef;
119
120 std::array<VkSubpassDependency, 2> deps{};
121 deps[0].srcSubpass = VK_SUBPASS_EXTERNAL;
122 deps[0].dstSubpass = 0;
123 deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
124 deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
125 deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
126 deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
127 deps[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
128
129 deps[1].srcSubpass = 0;
130 deps[1].dstSubpass = VK_SUBPASS_EXTERNAL;
131 deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
132 deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
133 deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
134 deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
135 deps[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
136
137 VkRenderPassCreateInfo rp{};
138 rp.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
139 rp.attachmentCount = 1;
140 rp.pAttachments = &colorAtt;
141 rp.subpassCount = 1;
142 rp.pSubpasses = &subpass;
143 rp.dependencyCount = static_cast<uint32_t>(deps.size());
144 rp.pDependencies = deps.data();
145
146 return vkCreateRenderPass(device, &rp, nullptr, &m_ssaoRenderPass) == VK_SUCCESS;
147}
148
149/// Creates the raw and blur SSAO framebuffers.
150bool VulkanRenderer::CreateSSAOFramebuffers() {
151 auto makeFB = [&](VkImageView v, VkFramebuffer& out) -> bool {
152 VkFramebufferCreateInfo fb{};
153 fb.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
154 fb.renderPass = m_ssaoRenderPass;
155 fb.attachmentCount = 1;
156 fb.pAttachments = &v;
157 fb.width = m_ssaoExtent.width;
158 fb.height = m_ssaoExtent.height;
159 fb.layers = 1;
160 return vkCreateFramebuffer(device, &fb, nullptr, &out) == VK_SUCCESS;
161 };
162 if (!makeFB(m_ssaoRawView, m_ssaoRawFramebuffer)) return false;
163 if (!makeFB(m_ssaoBlurView, m_ssaoBlurFramebuffer)) return false;
164 return true;
165}
166
167/// Creates the SSAO input/UBO/blur descriptor layouts, pool, sets, and UBO buffers.
168bool VulkanRenderer::CreateSSAODescriptorResources() {
169 // Set 0 for SSAO: bindings 0..2 (gNormalRough, gDepth, noise).
170 // World position is reconstructed from gDepth + InvViewProj (set 1 UBO).
171 {
172 std::array<VkDescriptorSetLayoutBinding, 3> binds{};
173 for (uint32_t i = 0; i < 3; ++i) {
174 binds[i].binding = i;
175 binds[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
176 binds[i].descriptorCount = 1;
177 binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
178 }
179 VkDescriptorSetLayoutCreateInfo info{};
180 info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
181 info.bindingCount = static_cast<uint32_t>(binds.size());
182 info.pBindings = binds.data();
183 if (vkCreateDescriptorSetLayout(device, &info, nullptr, &m_ssaoInputDSL) != VK_SUCCESS) return false;
184 }
185 // Set 1 for SSAO: UBO (kernel, matrices, params).
186 {
187 VkDescriptorSetLayoutBinding b{};
188 b.binding = 0;
189 b.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
190 b.descriptorCount = 1;
191 b.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
192 VkDescriptorSetLayoutCreateInfo info{};
193 info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
194 info.bindingCount = 1;
195 info.pBindings = &b;
196 if (vkCreateDescriptorSetLayout(device, &info, nullptr, &m_ssaoUboDSL) != VK_SUCCESS) return false;
197 }
198 // Set 0 for SSAO blur: bindings 0 (raw SSAO), 1 (depth).
199 {
200 std::array<VkDescriptorSetLayoutBinding, 2> binds{};
201 for (uint32_t i = 0; i < 2; ++i) {
202 binds[i].binding = i;
203 binds[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
204 binds[i].descriptorCount = 1;
205 binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
206 }
207 VkDescriptorSetLayoutCreateInfo info{};
208 info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
209 info.bindingCount = static_cast<uint32_t>(binds.size());
210 info.pBindings = binds.data();
211 if (vkCreateDescriptorSetLayout(device, &info, nullptr, &m_ssaoBlurDSL) != VK_SUCCESS) return false;
212 }
213
214 // Pool: (4 samplers + 2 samplers) * 2 sets per frame + 1 UBO per frame.
215 std::array<VkDescriptorPoolSize, 2> sizes{};
216 sizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
217 sizes[0].descriptorCount = (4 + 2) * MAX_FRAMES_IN_FLIGHT;
218 sizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
219 sizes[1].descriptorCount = 1 * MAX_FRAMES_IN_FLIGHT;
220
221 VkDescriptorPoolCreateInfo pool{};
222 pool.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
223 pool.poolSizeCount = static_cast<uint32_t>(sizes.size());
224 pool.pPoolSizes = sizes.data();
225 pool.maxSets = 3 * MAX_FRAMES_IN_FLIGHT; // input + ubo + blur
226 if (vkCreateDescriptorPool(device, &pool, nullptr, &m_ssaoDescriptorPool) != VK_SUCCESS) return false;
227
228 // Allocate input (set 0) + UBO (set 1) + blur (set 0) for each frame slot.
229 auto allocSets = [&](VkDescriptorSetLayout dsl, std::array<VkDescriptorSet, MAX_FRAMES_IN_FLIGHT>& out) -> bool {
230 std::array<VkDescriptorSetLayout, MAX_FRAMES_IN_FLIGHT> layouts;
231 layouts.fill(dsl);
232 VkDescriptorSetAllocateInfo a{};
233 a.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
234 a.descriptorPool = m_ssaoDescriptorPool;
235 a.descriptorSetCount = MAX_FRAMES_IN_FLIGHT;
236 a.pSetLayouts = layouts.data();
237 return vkAllocateDescriptorSets(device, &a, out.data()) == VK_SUCCESS;
238 };
239 if (!allocSets(m_ssaoInputDSL, m_ssaoInputSets)) return false;
240 if (!allocSets(m_ssaoUboDSL, m_ssaoUboSets)) return false;
241 if (!allocSets(m_ssaoBlurDSL, m_ssaoBlurSets)) return false;
242
243 // Create SSAO UBO buffers (per frame).
244 static constexpr VkDeviceSize uboSize = sizeof(SSAOParams);
245 for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
246 VkBufferCreateInfo bi{};
247 bi.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
248 bi.size = uboSize;
249 bi.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
250 bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
251 if (vkCreateBuffer(device, &bi, nullptr, &m_ssaoUboBuffers[i]) != VK_SUCCESS) return false;
252
253 VkMemoryRequirements req;
254 vkGetBufferMemoryRequirements(device, m_ssaoUboBuffers[i], &req);
255 VkMemoryAllocateInfo alloc{};
256 alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
257 alloc.allocationSize = req.size;
258 alloc.memoryTypeIndex = FindMemoryType(req.memoryTypeBits,
259 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
260 if (vkAllocateMemory(device, &alloc, nullptr, &m_ssaoUboMemory[i]) != VK_SUCCESS) return false;
261 vkBindBufferMemory(device, m_ssaoUboBuffers[i], m_ssaoUboMemory[i], 0);
262 if (vkMapMemory(device, m_ssaoUboMemory[i], 0, uboSize, 0, &m_ssaoUboMapped[i]) != VK_SUCCESS) return false;
263
264 // Bind UBO to set 1 descriptor.
265 VkDescriptorBufferInfo bufInfo{};
266 bufInfo.buffer = m_ssaoUboBuffers[i];
267 bufInfo.offset = 0;
268 bufInfo.range = uboSize;
269
270 VkWriteDescriptorSet w{};
271 w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
272 w.dstSet = m_ssaoUboSets[i];
273 w.dstBinding = 0;
274 w.dstArrayElement = 0;
275 w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
276 w.descriptorCount = 1;
277 w.pBufferInfo = &bufInfo;
278 vkUpdateDescriptorSets(device, 1, &w, 0, nullptr);
279 }
280
281 return true;
282}
283
284/// Generates and uploads the 4x4 tangent-plane rotation noise texture.
285bool VulkanRenderer::CreateSSAONoiseTexture() {
286 // 4x4 RGBA8 noise — random tangent-plane rotation vectors with Z=0
287 // (they live in the tangent plane of the surface).
288 const uint32_t noiseCount = SSAO_NOISE_SIZE * SSAO_NOISE_SIZE;
289 std::array<uint8_t, noiseCount * 4> pixels{};
290
291 std::mt19937 rng(12345u);
292 std::uniform_real_distribution<float> dist(0.0f, 1.0f);
293 for (uint32_t i = 0; i < noiseCount; ++i) {
294 float x = dist(rng) * 2.0f - 1.0f;
295 float y = dist(rng) * 2.0f - 1.0f;
296 // Remap [-1,1] → [0,255] via (v * 0.5 + 0.5) * 255.
297 pixels[i * 4 + 0] = static_cast<uint8_t>((x * 0.5f + 0.5f) * 255.0f);
298 pixels[i * 4 + 1] = static_cast<uint8_t>((y * 0.5f + 0.5f) * 255.0f);
299 pixels[i * 4 + 2] = 128; // Z = 0
300 pixels[i * 4 + 3] = 255;
301 }
302
303 // Create image.
304 VkImageCreateInfo info{};
305 info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
306 info.imageType = VK_IMAGE_TYPE_2D;
307 info.extent.width = SSAO_NOISE_SIZE;
308 info.extent.height = SSAO_NOISE_SIZE;
309 info.extent.depth = 1;
310 info.mipLevels = 1;
311 info.arrayLayers = 1;
312 info.format = VK_FORMAT_R8G8B8A8_UNORM;
313 info.tiling = VK_IMAGE_TILING_OPTIMAL;
314 info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
315 info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
316 info.samples = VK_SAMPLE_COUNT_1_BIT;
317 info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
318 if (vkCreateImage(device, &info, nullptr, &m_ssaoNoiseImage) != VK_SUCCESS) return false;
319
320 VkMemoryRequirements req;
321 vkGetImageMemoryRequirements(device, m_ssaoNoiseImage, &req);
322 VkMemoryAllocateInfo alloc{};
323 alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
324 alloc.allocationSize = req.size;
325 alloc.memoryTypeIndex = FindMemoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
326 if (vkAllocateMemory(device, &alloc, nullptr, &m_ssaoNoiseMemory) != VK_SUCCESS) return false;
327 vkBindImageMemory(device, m_ssaoNoiseImage, m_ssaoNoiseMemory, 0);
328
329 VkImageViewCreateInfo vinfo{};
330 vinfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
331 vinfo.image = m_ssaoNoiseImage;
332 vinfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
333 vinfo.format = VK_FORMAT_R8G8B8A8_UNORM;
334 vinfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
335 if (vkCreateImageView(device, &vinfo, nullptr, &m_ssaoNoiseView) != VK_SUCCESS) return false;
336
337 // Staging + upload.
338 VkBuffer staging = VK_NULL_HANDLE;
339 VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
340 const VkDeviceSize uploadSize = pixels.size();
341
342 VkBufferCreateInfo bi{};
343 bi.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
344 bi.size = uploadSize;
345 bi.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
346 bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
347 if (vkCreateBuffer(device, &bi, nullptr, &staging) != VK_SUCCESS) return false;
348
349 VkMemoryRequirements sreq;
350 vkGetBufferMemoryRequirements(device, staging, &sreq);
351 VkMemoryAllocateInfo salloc{};
352 salloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
353 salloc.allocationSize = sreq.size;
354 salloc.memoryTypeIndex = FindMemoryType(sreq.memoryTypeBits,
355 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
356 if (vkAllocateMemory(device, &salloc, nullptr, &stagingMemory) != VK_SUCCESS) {
357 vkDestroyBuffer(device, staging, nullptr);
358 return false;
359 }
360 vkBindBufferMemory(device, staging, stagingMemory, 0);
361
362 void* mapped = nullptr;
363 vkMapMemory(device, stagingMemory, 0, uploadSize, 0, &mapped);
364 memcpy(mapped, pixels.data(), pixels.size());
365 vkUnmapMemory(device, stagingMemory);
366
367 // Single-shot command buffer for upload.
368 VkCommandBufferAllocateInfo cbAlloc{};
369 cbAlloc.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
370 cbAlloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
371 cbAlloc.commandPool = commands;
372 cbAlloc.commandBufferCount = 1;
373 VkCommandBuffer cmd;
374 vkAllocateCommandBuffers(device, &cbAlloc, &cmd);
375
376 VkCommandBufferBeginInfo begin{};
377 begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
378 begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
379 vkBeginCommandBuffer(cmd, &begin);
380
381 VkImageMemoryBarrier b0{};
382 b0.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
383 b0.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
384 b0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
385 b0.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
386 b0.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
387 b0.image = m_ssaoNoiseImage;
388 b0.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
389 b0.srcAccessMask = 0;
390 b0.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
391 vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
392 0, 0, nullptr, 0, nullptr, 1, &b0);
393
394 VkBufferImageCopy region{};
395 region.bufferOffset = 0;
396 region.bufferRowLength = 0;
397 region.bufferImageHeight = 0;
398 region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
399 region.imageOffset = {0, 0, 0};
400 region.imageExtent = {SSAO_NOISE_SIZE, SSAO_NOISE_SIZE, 1};
401 vkCmdCopyBufferToImage(cmd, staging, m_ssaoNoiseImage,
402 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
403
404 VkImageMemoryBarrier b1 = b0;
405 b1.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
406 b1.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
407 b1.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
408 b1.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
409 vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
410 0, 0, nullptr, 0, nullptr, 1, &b1);
411
412 vkEndCommandBuffer(cmd);
413
414 VkSubmitInfo submit{};
415 submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
416 submit.commandBufferCount = 1;
417 submit.pCommandBuffers = &cmd;
418 vkQueueSubmit(graphicsQueue, 1, &submit, VK_NULL_HANDLE);
419 vkQueueWaitIdle(graphicsQueue);
420
421 vkFreeCommandBuffers(device, commands, 1, &cmd);
422 vkDestroyBuffer(device, staging, nullptr);
423 vkFreeMemory(device, stagingMemory, nullptr);
424
425 // Noise sampler — repeat (we want the 4x4 tile to wrap across the screen).
426 VkSamplerCreateInfo ns{};
427 ns.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
428 ns.magFilter = VK_FILTER_NEAREST;
429 ns.minFilter = VK_FILTER_NEAREST;
430 ns.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
431 ns.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
432 ns.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
433 ns.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
434 ns.minLod = 0.0f;
435 ns.maxLod = 0.0f;
436 if (vkCreateSampler(device, &ns, nullptr, &m_ssaoNoiseSampler) != VK_SUCCESS) return false;
437
438 return true;
439}
440
441/// Compiles the SSAO and SSAO-blur shaders and creates their pipelines.
442bool VulkanRenderer::CreateSSAOPipelines() {
443 // ---- SSAO main pipeline ----
444 m_ssaoShader = new VulkanShader(device);
445 if (!m_ssaoShader->compile("assets/shaders/ssao.vert.spv",
446 "assets/shaders/ssao.frag.spv")) {
447 SLEAK_ERROR("SSAO: failed to compile ssao shaders");
448 return false;
449 }
450
451 std::array<VkDescriptorSetLayout, 2> ssaoLayouts = { m_ssaoInputDSL, m_ssaoUboDSL };
452 VkPipelineLayoutCreateInfo pli{};
453 pli.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
454 pli.setLayoutCount = static_cast<uint32_t>(ssaoLayouts.size());
455 pli.pSetLayouts = ssaoLayouts.data();
456 if (vkCreatePipelineLayout(device, &pli, nullptr, &m_ssaoPipelineLayout) != VK_SUCCESS) {
457 SLEAK_ERROR("SSAO: failed to create ssao pipeline layout");
458 return false;
459 }
460
461 // Common pipeline state for all full-screen post-process shaders.
462 VkPipelineShaderStageCreateInfo ssaoStages[] = {
463 m_ssaoShader->GetVertexInfo(),
464 m_ssaoShader->GetFragInfo()
465 };
466 VkPipelineVertexInputStateCreateInfo vin{};
467 vin.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
468
469 VkPipelineInputAssemblyStateCreateInfo ia{};
470 ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
471 ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
472
473 std::vector<VkDynamicState> dynStates = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
474 VkPipelineDynamicStateCreateInfo ds{};
475 ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
476 ds.dynamicStateCount = static_cast<uint32_t>(dynStates.size());
477 ds.pDynamicStates = dynStates.data();
478
479 VkPipelineViewportStateCreateInfo vps{};
480 vps.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
481 vps.viewportCount = 1;
482 vps.scissorCount = 1;
483
484 VkPipelineRasterizationStateCreateInfo rs{};
485 rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
486 rs.polygonMode = VK_POLYGON_MODE_FILL;
487 rs.cullMode = VK_CULL_MODE_NONE;
488 rs.lineWidth = 1.0f;
489
490 VkPipelineMultisampleStateCreateInfo ms{};
491 ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
492 ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
493
494 VkPipelineDepthStencilStateCreateInfo dss{};
495 dss.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
496
497 VkPipelineColorBlendAttachmentState blendAtt{};
498 blendAtt.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
499 VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
500
501 VkPipelineColorBlendStateCreateInfo cb{};
502 cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
503 cb.attachmentCount = 1;
504 cb.pAttachments = &blendAtt;
505
506 VkGraphicsPipelineCreateInfo gpi{};
507 gpi.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
508 gpi.stageCount = 2;
509 gpi.pStages = ssaoStages;
510 gpi.pVertexInputState = &vin;
511 gpi.pInputAssemblyState = &ia;
512 gpi.pViewportState = &vps;
513 gpi.pRasterizationState = &rs;
514 gpi.pMultisampleState = &ms;
515 gpi.pDepthStencilState = &dss;
516 gpi.pColorBlendState = &cb;
517 gpi.pDynamicState = &ds;
518 gpi.layout = m_ssaoPipelineLayout;
519 gpi.renderPass = m_ssaoRenderPass;
520 gpi.subpass = 0;
521
522 if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_ssaoPipeline) != VK_SUCCESS) {
523 SLEAK_ERROR("SSAO: failed to create ssao pipeline");
524 return false;
525 }
526
527 // ---- SSAO blur pipeline ----
528 m_ssaoBlurShader = new VulkanShader(device);
529 if (!m_ssaoBlurShader->compile("assets/shaders/ssao_blur.vert.spv",
530 "assets/shaders/ssao_blur.frag.spv")) {
531 SLEAK_ERROR("SSAO: failed to compile ssao_blur shaders");
532 return false;
533 }
534
535 VkPipelineLayoutCreateInfo pliBlur{};
536 pliBlur.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
537 pliBlur.setLayoutCount = 1;
538 pliBlur.pSetLayouts = &m_ssaoBlurDSL;
539 if (vkCreatePipelineLayout(device, &pliBlur, nullptr, &m_ssaoBlurPipelineLayout) != VK_SUCCESS) {
540 SLEAK_ERROR("SSAO: failed to create ssao blur pipeline layout");
541 return false;
542 }
543
544 VkPipelineShaderStageCreateInfo blurStages[] = {
545 m_ssaoBlurShader->GetVertexInfo(),
546 m_ssaoBlurShader->GetFragInfo()
547 };
548 gpi.pStages = blurStages;
549 gpi.layout = m_ssaoBlurPipelineLayout;
550 if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_ssaoBlurPipeline) != VK_SUCCESS) {
551 SLEAK_ERROR("SSAO: failed to create ssao blur pipeline");
552 return false;
553 }
554
555 return true;
556}
557
558/// Creates all SSAO images, render pass, framebuffers, descriptors, and pipelines.
559bool VulkanRenderer::CreateSSAOResources() {
560 if (m_ssaoResourcesCreated) return true;
561
562 if (!CreateSSAOImages()) { SLEAK_ERROR("SSAO: images failed"); return false; }
563 if (!CreateSSAONoiseTexture()) { SLEAK_ERROR("SSAO: noise failed"); return false; }
564 if (!CreateSSAORenderPass()) { SLEAK_ERROR("SSAO: render pass failed"); return false; }
565 if (!CreateSSAOFramebuffers()) { SLEAK_ERROR("SSAO: framebuffers failed"); return false; }
566 if (!CreateSSAODescriptorResources()) { SLEAK_ERROR("SSAO: descriptors failed"); return false; }
567 if (!CreateSSAOPipelines()) { SLEAK_ERROR("SSAO: pipelines failed"); return false; }
568
569 m_ssaoResourcesCreated = true;
570 SLEAK_INFO("SSAO resources created ({}x{})", m_ssaoExtent.width, m_ssaoExtent.height);
571 return true;
572}
573
574/// Destroys all SSAO pipelines, framebuffers, descriptors, images, and samplers.
575void VulkanRenderer::CleanupSSAOResources() {
576 if (!m_ssaoResourcesCreated) return;
577
578 if (m_ssaoPipeline) { vkDestroyPipeline(device, m_ssaoPipeline, nullptr); m_ssaoPipeline = VK_NULL_HANDLE; }
579 if (m_ssaoBlurPipeline) { vkDestroyPipeline(device, m_ssaoBlurPipeline, nullptr); m_ssaoBlurPipeline = VK_NULL_HANDLE; }
580 if (m_ssaoPipelineLayout) { vkDestroyPipelineLayout(device, m_ssaoPipelineLayout, nullptr); m_ssaoPipelineLayout = VK_NULL_HANDLE; }
581 if (m_ssaoBlurPipelineLayout) { vkDestroyPipelineLayout(device, m_ssaoBlurPipelineLayout, nullptr); m_ssaoBlurPipelineLayout = VK_NULL_HANDLE; }
582 delete m_ssaoShader; m_ssaoShader = nullptr;
583 delete m_ssaoBlurShader; m_ssaoBlurShader = nullptr;
584
585 if (m_ssaoRawFramebuffer) { vkDestroyFramebuffer(device, m_ssaoRawFramebuffer, nullptr); m_ssaoRawFramebuffer = VK_NULL_HANDLE; }
586 if (m_ssaoBlurFramebuffer) { vkDestroyFramebuffer(device, m_ssaoBlurFramebuffer, nullptr); m_ssaoBlurFramebuffer = VK_NULL_HANDLE; }
587 if (m_ssaoRenderPass) { vkDestroyRenderPass(device, m_ssaoRenderPass, nullptr); m_ssaoRenderPass = VK_NULL_HANDLE; }
588
589 if (m_ssaoDescriptorPool) { vkDestroyDescriptorPool(device, m_ssaoDescriptorPool, nullptr); m_ssaoDescriptorPool = VK_NULL_HANDLE; }
590 if (m_ssaoInputDSL) { vkDestroyDescriptorSetLayout(device, m_ssaoInputDSL, nullptr); m_ssaoInputDSL = VK_NULL_HANDLE; }
591 if (m_ssaoUboDSL) { vkDestroyDescriptorSetLayout(device, m_ssaoUboDSL, nullptr); m_ssaoUboDSL = VK_NULL_HANDLE; }
592 if (m_ssaoBlurDSL) { vkDestroyDescriptorSetLayout(device, m_ssaoBlurDSL, nullptr); m_ssaoBlurDSL = VK_NULL_HANDLE; }
593
594 for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
595 if (m_ssaoUboMapped[i]) { vkUnmapMemory(device, m_ssaoUboMemory[i]); m_ssaoUboMapped[i] = nullptr; }
596 if (m_ssaoUboBuffers[i]) { vkDestroyBuffer(device, m_ssaoUboBuffers[i], nullptr); m_ssaoUboBuffers[i] = VK_NULL_HANDLE; }
597 if (m_ssaoUboMemory[i]) { vkFreeMemory(device, m_ssaoUboMemory[i], nullptr); m_ssaoUboMemory[i] = VK_NULL_HANDLE; }
598 }
599
600 if (m_ssaoRawView) { vkDestroyImageView(device, m_ssaoRawView, nullptr); m_ssaoRawView = VK_NULL_HANDLE; }
601 if (m_ssaoBlurView) { vkDestroyImageView(device, m_ssaoBlurView, nullptr); m_ssaoBlurView = VK_NULL_HANDLE; }
602 if (m_ssaoRawImage) { vkDestroyImage(device, m_ssaoRawImage, nullptr); m_ssaoRawImage = VK_NULL_HANDLE; }
603 if (m_ssaoBlurImage) { vkDestroyImage(device, m_ssaoBlurImage, nullptr); m_ssaoBlurImage = VK_NULL_HANDLE; }
604 if (m_ssaoRawMemory) { vkFreeMemory(device, m_ssaoRawMemory, nullptr); m_ssaoRawMemory = VK_NULL_HANDLE; }
605 if (m_ssaoBlurMemory) { vkFreeMemory(device, m_ssaoBlurMemory, nullptr); m_ssaoBlurMemory = VK_NULL_HANDLE; }
606
607 if (m_ssaoNoiseView) { vkDestroyImageView(device, m_ssaoNoiseView, nullptr); m_ssaoNoiseView = VK_NULL_HANDLE; }
608 if (m_ssaoNoiseImage) { vkDestroyImage(device, m_ssaoNoiseImage, nullptr); m_ssaoNoiseImage = VK_NULL_HANDLE; }
609 if (m_ssaoNoiseMemory) { vkFreeMemory(device, m_ssaoNoiseMemory, nullptr); m_ssaoNoiseMemory = VK_NULL_HANDLE; }
610 if (m_ssaoNoiseSampler) { vkDestroySampler(device, m_ssaoNoiseSampler, nullptr); m_ssaoNoiseSampler = VK_NULL_HANDLE; }
611
612 if (m_ssaoSampler) { vkDestroySampler(device, m_ssaoSampler, nullptr); m_ssaoSampler = VK_NULL_HANDLE; }
613 if (m_ssaoPointSampler) { vkDestroySampler(device, m_ssaoPointSampler, nullptr); m_ssaoPointSampler = VK_NULL_HANDLE; }
614
615 m_ssaoResourcesCreated = false;
616}
617
618/// Writes the GBuffer, depth, and noise samplers into the SSAO input and blur descriptor sets.
619void VulkanRenderer::UpdateSSAODescriptors() {
620 if (!m_ssaoResourcesCreated) return;
621
622 // Write per-frame descriptors. Do all frame slots now — called during
623 // init before any frames are recorded, so no concurrent GPU reads.
624 for (uint32_t f = 0; f < MAX_FRAMES_IN_FLIGHT; ++f) {
625 // Set 0 (input samplers): gNormalRough, gDepth, noise.
626 std::array<VkDescriptorImageInfo, 3> inputInfos{};
627 // gNormalRough = gbuffer[1]; world position reconstructed from depth.
628 inputInfos[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
629 inputInfos[0].imageView = m_gbufferViews[1];
630 inputInfos[0].sampler = m_ssaoPointSampler;
631
632 inputInfos[1].imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
633 inputInfos[1].imageView = depthImageView;
634 inputInfos[1].sampler = m_ssaoPointSampler;
635
636 inputInfos[2].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
637 inputInfos[2].imageView = m_ssaoNoiseView;
638 inputInfos[2].sampler = m_ssaoNoiseSampler;
639
640 std::array<VkWriteDescriptorSet, 3> inputWrites{};
641 for (uint32_t i = 0; i < 3; ++i) {
642 inputWrites[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
643 inputWrites[i].dstSet = m_ssaoInputSets[f];
644 inputWrites[i].dstBinding = i;
645 inputWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
646 inputWrites[i].descriptorCount = 1;
647 inputWrites[i].pImageInfo = &inputInfos[i];
648 }
649 vkUpdateDescriptorSets(device, static_cast<uint32_t>(inputWrites.size()),
650 inputWrites.data(), 0, nullptr);
651
652 // Blur set 0: raw SSAO + depth.
653 std::array<VkDescriptorImageInfo, 2> blurInfos{};
654 blurInfos[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
655 blurInfos[0].imageView = m_ssaoRawView;
656 blurInfos[0].sampler = m_ssaoSampler;
657
658 blurInfos[1].imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
659 blurInfos[1].imageView = depthImageView;
660 blurInfos[1].sampler = m_ssaoPointSampler;
661
662 std::array<VkWriteDescriptorSet, 2> blurWrites{};
663 for (uint32_t i = 0; i < 2; ++i) {
664 blurWrites[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
665 blurWrites[i].dstSet = m_ssaoBlurSets[f];
666 blurWrites[i].dstBinding = i;
667 blurWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
668 blurWrites[i].descriptorCount = 1;
669 blurWrites[i].pImageInfo = &blurInfos[i];
670 }
671 vkUpdateDescriptorSets(device, static_cast<uint32_t>(blurWrites.size()),
672 blurWrites.data(), 0, nullptr);
673 }
674}
675
676/// Fills the SSAO UBO with the cached camera matrices and a cosine-weighted hemisphere kernel.
677void VulkanRenderer::UpdateSSAOUBO() {
678 if (!m_ssaoResourcesCreated || !m_ssaoUboMapped[currentFrame]) return;
679
680 SSAOParams p{};
681 memcpy(p.View, m_cachedView, sizeof(p.View));
682 memcpy(p.Projection, m_cachedProjection, sizeof(p.Projection));
683 memcpy(p.InvViewProj, m_cachedInvViewProj, sizeof(p.InvViewProj));
684
685 // Generate cosine-weighted hemisphere kernel — we do this once in a
686 // session (use a fixed RNG seed). The kernel vectors are in the tangent
687 // space of the surface: Z points along the normal.
688 static bool s_kernelInit = false;
689 static float s_kernel[SSAO_KERNEL_SIZE][4];
690 if (!s_kernelInit) {
691 std::mt19937 rng(20240520u);
692 std::uniform_real_distribution<float> d(0.0f, 1.0f);
693 for (uint32_t i = 0; i < SSAO_KERNEL_SIZE; ++i) {
694 float x = d(rng) * 2.0f - 1.0f;
695 float y = d(rng) * 2.0f - 1.0f;
696 float z = d(rng); // positive Z — hemisphere
697 float len = std::sqrt(x*x + y*y + z*z);
698 if (len < 1e-6f) { x = 0.0f; y = 0.0f; z = 1.0f; len = 1.0f; }
699 x /= len; y /= len; z /= len;
700 float scale = float(i) / float(SSAO_KERNEL_SIZE);
701 // Bias samples closer to the origin (quadratic distance falloff).
702 scale = 0.1f + 0.9f * scale * scale;
703 s_kernel[i][0] = x * scale;
704 s_kernel[i][1] = y * scale;
705 s_kernel[i][2] = z * scale;
706 s_kernel[i][3] = 0.0f;
707 }
708 s_kernelInit = true;
709 }
710 memcpy(p.Kernel, s_kernel, sizeof(p.Kernel));
711
712 p.ScreenW = static_cast<float>(scExtent.width);
713 p.ScreenH = static_cast<float>(scExtent.height);
714 // Noise tile scale: screen pixels / noise texture size so the 4x4 noise tiles naturally.
715 p.NoiseScaleX = static_cast<float>(scExtent.width) / float(SSAO_NOISE_SIZE);
716 p.NoiseScaleY = static_cast<float>(scExtent.height) / float(SSAO_NOISE_SIZE);
717
718 p.Radius = 0.5f; // 0.5m world-space hemisphere
719 p.Bias = 0.012f;
720 p.Power = 2.5f;
721 p.Intensity = 1.3f;
722 p.KernelSize = 16; // sample first 16 of the 32-vec kernel (perf; no res change → no seam)
723
724 memcpy(m_ssaoUboMapped[currentFrame], &p, sizeof(p));
725}
726
727/// Primes the disabled-effect fallback images (ssaoBlur=white, ssr=black, bloom
728/// mip0=black) once after (re)creation, leaving them SHADER_READ_ONLY. Per-frame
729/// disabled paths then skip the redundant clear since the content is static.
730void VulkanRenderer::InitDisabledEffectFallbacks() {
731 if (m_ssaoBlurImage == VK_NULL_HANDLE ||
732 m_ssrImage == VK_NULL_HANDLE ||
733 m_bloomImage == VK_NULL_HANDLE)
734 return;
735
736 VkCommandBufferAllocateInfo ca{};
737 ca.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
738 ca.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
739 ca.commandPool = commands;
740 ca.commandBufferCount = 1;
741 VkCommandBuffer initCmd;
742 if (vkAllocateCommandBuffers(device, &ca, &initCmd) != VK_SUCCESS) return;
743
744 VkCommandBufferBeginInfo bi{};
745 bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
746 bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
747 vkBeginCommandBuffer(initCmd, &bi);
748
749 const VkImageSubresourceRange sr = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
750
751 struct Prime { VkImage img; VkClearColorValue clr; };
752 VkClearColorValue white{}; white.float32[0] = 1.0f; white.float32[1] = 1.0f;
753 white.float32[2] = 1.0f; white.float32[3] = 1.0f;
754 VkClearColorValue black{};
755 Prime items[3] = {
756 { m_ssaoBlurImage, white }, // white = no occlusion
757 { m_ssrImage, black }, // black = no reflection
758 { m_bloomImage, black }, // black = no bloom (mip 0)
759 };
760
761 for (auto& it : items) {
762 VkImageMemoryBarrier bar{};
763 bar.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
764 bar.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
765 bar.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
766 bar.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
767 bar.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
768 bar.srcAccessMask = 0;
769 bar.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
770 bar.image = it.img;
771 bar.subresourceRange = sr;
772 vkCmdPipelineBarrier(initCmd,
773 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
774 0, 0, nullptr, 0, nullptr, 1, &bar);
775
776 vkCmdClearColorImage(initCmd, it.img,
777 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &it.clr, 1, &sr);
778
779 bar.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
780 bar.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
781 bar.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
782 bar.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
783 vkCmdPipelineBarrier(initCmd,
784 VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
785 0, 0, nullptr, 0, nullptr, 1, &bar);
786 }
787
788 vkEndCommandBuffer(initCmd);
789
790 VkSubmitInfo sub{};
791 sub.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
792 sub.commandBufferCount = 1;
793 sub.pCommandBuffers = &initCmd;
794 vkQueueSubmit(graphicsQueue, 1, &sub, VK_NULL_HANDLE);
795 vkQueueWaitIdle(graphicsQueue);
796 vkFreeCommandBuffers(device, commands, 1, &initCmd);
797
798 m_ssaoFallbackPrimed = true;
799 m_ssrFallbackPrimed = true;
800 m_bloomFallbackPrimed = true;
801}
802
803/// Runs the raw SSAO and bilateral blur passes, or clears the blur target when SSAO is disabled.
804void VulkanRenderer::RenderSSAOPasses() {
805 if (!m_ssaoResourcesCreated) return;
806
807 // SSAO disabled: clear the blur target (sampled by the lighting pass at
808 // binding 7) to white = no occlusion, and leave it SHADER_READ_ONLY so the
809 // lighting pass never samples an UNDEFINED image. Mirrors the SSR fallback.
810 if (!m_ssaoEnabled) {
811 // Already primed to white SHADER_READ_ONLY — the content is static, so
812 // re-clearing every frame is wasted work. Re-prime only if the enabled
813 // path dirtied the image since (runtime toggle).
814 if (m_ssaoFallbackPrimed) return;
815 VkImageMemoryBarrier toClear{};
816 toClear.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
817 toClear.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
818 toClear.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
819 toClear.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
820 toClear.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
821 toClear.srcAccessMask = 0;
822 toClear.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
823 toClear.image = m_ssaoBlurImage;
824 toClear.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
825 vkCmdPipelineBarrier(command,
826 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
827 0, 0, nullptr, 0, nullptr, 1, &toClear);
828
829 VkClearColorValue white{};
830 white.float32[0] = 1.0f; white.float32[1] = 1.0f;
831 white.float32[2] = 1.0f; white.float32[3] = 1.0f;
832 VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
833 vkCmdClearColorImage(command, m_ssaoBlurImage,
834 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &white, 1, &range);
835
836 VkImageMemoryBarrier toRead = toClear;
837 toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
838 toRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
839 toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
840 toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
841 vkCmdPipelineBarrier(command,
842 VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
843 0, 0, nullptr, 0, nullptr, 1, &toRead);
844 m_ssaoFallbackPrimed = true;
845 return;
846 }
847
848 // Enabled path dirties the blur image; force a re-prime if SSAO is later
849 // disabled so the lighting pass doesn't sample stale occlusion.
850 m_ssaoFallbackPrimed = false;
851
852 UpdateSSAOUBO();
853
854 // ---- Pass 1: raw SSAO ----
855 VkRenderPassBeginInfo rp{};
856 rp.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
857 rp.renderPass = m_ssaoRenderPass;
858 rp.framebuffer = m_ssaoRawFramebuffer;
859 rp.renderArea.offset = {0, 0};
860 rp.renderArea.extent = m_ssaoExtent;
861 rp.clearValueCount = 0;
862
863 vkCmdBeginRenderPass(command, &rp, VK_SUBPASS_CONTENTS_INLINE);
864 FillFullscreenViewportScissor(command, m_ssaoExtent);
865 vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_ssaoPipeline);
866 VkDescriptorSet ssaoSets[2] = { m_ssaoInputSets[currentFrame], m_ssaoUboSets[currentFrame] };
867 vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
868 m_ssaoPipelineLayout, 0, 2, ssaoSets, 0, nullptr);
869 vkCmdDraw(command, 3, 1, 0, 0);
870 vkCmdEndRenderPass(command);
871
872 // ---- Pass 2: bilateral blur ----
873 rp.framebuffer = m_ssaoBlurFramebuffer;
874 vkCmdBeginRenderPass(command, &rp, VK_SUBPASS_CONTENTS_INLINE);
875 FillFullscreenViewportScissor(command, m_ssaoExtent);
876 vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_ssaoBlurPipeline);
877 vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
878 m_ssaoBlurPipelineLayout, 0, 1,
879 &m_ssaoBlurSets[currentFrame], 0, nullptr);
880 vkCmdDraw(command, 3, 1, 0, 0);
881 vkCmdEndRenderPass(command);
882}
883
884} // namespace RenderEngine
885} // namespace Sleak
#define SLEAK_ERROR(...)
Definition Logger.hpp:22
#define SLEAK_INFO(...)
Definition Logger.hpp:20
Backend-facing rendering layer shared by the four graphics backends.
Root namespace for everything the engine exposes.
Definition Camera.hpp:10