SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
VulkanRenderer.cpp
Go to the documentation of this file.
8
9#include <SDL3/SDL_vulkan.h>
10#include <Core/Window.hpp>
11#include <algorithm>
12#include <cstddef>
13#include <cstdint>
14#include <cstdlib>
15#include <cstring>
16#include <format>
17#include <fstream>
18#include <limits>
19#include <optional>
20#include <set>
21#include <stdexcept>
22#include <string>
23#include <vector>
26#include "Core/Logger.hpp"
27#include "Core/CommandLine.hpp"
28#include "Camera/Camera.hpp"
29#include "Math/Matrix.hpp"
30#include <random>
31#include "SDL3/SDL_error.h"
32#include "SDL3/SDL_video.h"
33#ifdef PLATFORM_LINUX
34 #include "vulkan/vulkan_wayland.h"
35#elif defined(PLATFORM_WIN)
36 #include <vulkan/vulkan_win32.h>
37#endif
38
39namespace Sleak {
40 namespace RenderEngine {
41
42/// Constructs the renderer, sets the clear color, and registers
43/// ResourceManager factory callbacks.
45 : sdlWindow(window) {
47 clearColor = {{0.3f, 0.4f, 1.0f, 1.0f}};
48
59
61 [this](const void* data, uint32_t w, uint32_t h, TextureFormat fmt, uint32_t maxMip) -> ::Sleak::Texture* {
62 auto* tex = new VulkanTexture(device, physicalDevice, commands, graphicsQueue);
63 tex->SetMaxMipLevels(maxMip);
64 if (tex->LoadFromMemory(data, w, h, fmt)) {
65 WriteTextureDescriptors(tex);
66 return tex;
67 }
68 delete tex;
69 return nullptr;
70 });
71}
72
73/// Calls Cleanup() to tear down all Vulkan resources.
77
78/// Runs the full Vulkan bring-up sequence: instance, device, swapchain,
79/// pipelines, and sync objects.
81 if (!InitVulkan())
82 SLEAK_RETURN_ERR("Failed to initialize Vulkan Instance!");
83
84 if (!SetupDebugMessenger()) {
85 SLEAK_WARN("Failed to setup validation layer of vulkan instance")
86 }
87
88 if (!CreateSurface())
89 SLEAK_RETURN_ERR("Failed to create render surface!")
90
91 if (!CreateDevice())
92 SLEAK_RETURN_ERR("Failed to initialize devices!");
93
94 if (!CreateSwapChain())
95 SLEAK_RETURN_ERR("Failed to create swap chain!");
96
97 if (!CreateImageViews())
98 SLEAK_RETURN_ERR("Failed to create image views for renderer!");
99
100 if (!CreateDepthResources())
101 SLEAK_RETURN_ERR("Failed to create depth resources!");
102
103 if (!CreateMSAAColorResources())
104 SLEAK_RETURN_ERR("Failed to create MSAA color resources!");
105
106 if (!CreateRenderPass())
107 SLEAK_RETURN_ERR("Failed to create a render pass for the renderer!");
108
109 if (!CreateDescriptorSetLayout())
110 SLEAK_RETURN_ERR("Failed to create descriptor set layout!");
111
112 if (!CreateDescriptorPool())
113 SLEAK_RETURN_ERR("Failed to create descriptor pool!");
114
115 if (!AllocateDescriptorSets())
116 SLEAK_RETURN_ERR("Failed to allocate descriptor sets!");
117
118 if (!CreateCommandPool())
119 SLEAK_RETURN_ERR("Failed to create command pool for renderer!");
120
121 if (!CreateCommandBuffer())
122 SLEAK_RETURN_ERR("Failed to create command buffers for renderer!");
123
124 if (!CreateDefaultTexture())
125 SLEAK_WARN("Failed to create default white texture for Vulkan");
126
127 if (!CreateGraphicsPipeline())
128 SLEAK_RETURN_ERR("Failed to create graphics pipeline!");
129
130 if (!CreateShadowLightUBOResources())
131 SLEAK_WARN("Failed to create light UBO resources — dynamic lighting disabled");
132
133 if (!CreateShadowResources())
134 SLEAK_WARN("Failed to create shadow mapping resources — shadows disabled");
135
136 if (!CreateFrameBuffer())
137 SLEAK_RETURN_ERR("Failed to create framebuffer of renderer!");
138
139 // Deferred GBuffer — initialized after swapchain framebuffers are ready
140 if (m_deferredEnabled) {
141 if (!CreateGBufferResources())
142 SLEAK_WARN("Failed to create GBuffer resources — deferred rendering disabled");
143 }
144
145 // Eagerly create bone UBO resources so set 1 is always bound at pass start.
146 // Must happen after CreateDescriptorSetLayout() (boneDescriptorSetLayout is ready)
147 // and after GBuffer init (m_deferredEnabled is known).
148 if (!CreateBoneUBOResources())
149 SLEAK_WARN("Failed to pre-create bone UBO resources — skinned meshes may malfunction on first frame");
150
151 if (!CreateSyncObjects())
152 SLEAK_RETURN_ERR("Failed to synchronization objects of renderer!");
153
155
156 SLEAK_INFO("Vulkan renderer has been initialized successfully!");
157
158 return true;
159}
160
161/// Prepares the command buffer and begins the shadow/GBuffer/forward render
162/// pass. Does not end the command buffer; the RenderCommandQueue records
163/// draw commands via the RenderContext interface after this returns.
165 bFrameStarted = false;
166 m_inGeometryPass = false;
167 m_inForwardTransparentPass = false;
168 m_forwardPassOpen = false;
169 m_activeCustomFormat = 0;
170 if (!bRender)
171 return;
172
173 // Commit staged lightVP. Do this BEFORE the shadow pass so the shadow
174 // map and the main pass both read the same m_lightVP this frame.
175 if (m_hasPendingLightVP) {
176 memcpy(m_lightVP, m_pendingLightVP, sizeof(m_lightVP));
177 }
178
179 // Apply pending changes between frames
186
187 VkResult result;
188
189 if (device && !inFlightFences.empty()) {
190 vkWaitForFences(device, 1, &inFlightFences[currentFrame],
191 VK_TRUE, UINT64_MAX);
192 }
193
194 // Clean up staging buffers from the previous use of this frame slot.
195 // The fence wait above guarantees the GPU finished both the transfer
196 // (waited on by the render submit) and the render itself.
197 auto& flush = m_asyncFlush[currentFrame];
198 if (flush.submitted) {
199 // Free command buffer FIRST to release references to staging buffers
200 if (flush.commandBuffer != VK_NULL_HANDLE) {
201 vkFreeCommandBuffers(flush.device, flush.commandPool, 1,
202 &flush.commandBuffer);
203 }
204 for (auto& pending : flush.stagingBuffers) {
205 vmaDestroyBuffer(VulkanBuffer::GetAllocator(), pending.buffer,
206 pending.memory);
207 VulkanBuffer::UntrackAllocation(pending.allocSize,
208 pending.memoryTypeIndex);
209 }
210 flush = {};
211 }
212
213 VulkanBuffer::ProcessDeferredDeletions(MAX_FRAMES_IN_FLIGHT);
215
216 // Enable batched buffer uploads for this frame (async, zero CPU blocking).
217 // This is disabled during init/scene transitions where buffers may be
218 // created and destroyed before a flush.
220
221 // Acquire the next image from the swapchain
222 result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX,
223 imageAvailableSemaphores[m_semaphoreIndex],
224 VK_NULL_HANDLE, &CurrentFrameIndex);
225 if (result == VK_ERROR_OUT_OF_DATE_KHR) {
226 RecreateSwapChain();
227 return;
228 } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
229 SLEAK_ERROR("Failed to acquire swapchain image!");
230 return;
231 }
232
233 // Wait if this swapchain image is still in use by a DIFFERENT frame slot
234 if (CurrentFrameIndex < imagesInFlight.size() &&
235 imagesInFlight[CurrentFrameIndex] != VK_NULL_HANDLE &&
236 imagesInFlight[CurrentFrameIndex] != inFlightFences[currentFrame]) {
237 vkWaitForFences(device, 1, &imagesInFlight[CurrentFrameIndex],
238 VK_TRUE, UINT64_MAX);
239 }
240 imagesInFlight[CurrentFrameIndex] = inFlightFences[currentFrame];
241
242 // Reset the fence only after all waits are done
243 vkResetFences(device, 1, &inFlightFences[currentFrame]);
244
245 // Select the command buffer for this frame-in-flight
246 command = commandBuffers[currentFrame];
247
248 // Reset and begin the command buffer
249 vkResetCommandBuffer(command, 0);
250
251 VkCommandBufferBeginInfo beginInfo{};
252 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
253 beginInfo.flags = 0;
254
255 if (vkBeginCommandBuffer(command, &beginInfo) != VK_SUCCESS) {
256 SLEAK_ERROR("Failed to begin command buffer!");
257 return;
258 }
259
260 bFrameStarted = true;
261 m_pbrMaterialSlot[currentFrame] = 0; // reset PBR material ring for this frame
262
263 // Skip shadow pass if no cached draws — preserve previous frame's shadow map
264 auto* shadowQueue = RenderCommandQueue::GetInstance();
265 bool hasShadowDraws = shadowQueue && shadowQueue->HasCachedShadowDraws();
266
267 if (m_shadowResourcesCreated && m_shadowPassEnabled && hasShadowDraws) {
268 VkClearValue shadowClear{};
269 shadowClear.depthStencil = {1.0f, 0};
270
271 VkRenderPassBeginInfo shadowPassInfo{};
272 shadowPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
273 shadowPassInfo.renderPass = m_shadowRenderPass;
274 shadowPassInfo.framebuffer = m_shadowFramebuffer;
275 shadowPassInfo.renderArea.offset = {0, 0};
276 shadowPassInfo.renderArea.extent = {m_shadowMapResolution, m_shadowMapResolution};
277 shadowPassInfo.clearValueCount = 1;
278 shadowPassInfo.pClearValues = &shadowClear;
279
280 vkCmdBeginRenderPass(command, &shadowPassInfo, VK_SUBPASS_CONTENTS_INLINE);
281 vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_shadowPipeline);
282
283 // shadow_depth.vert statically declares `layout(set = 1, binding = 0) uniform BoneUBO`
284 // (skinning conditioned on boneWeights). The shader must have set 1 bound even for
285 // non-skinned casters, otherwise vkCmdDrawIndexed fires VUID-vkCmdDrawIndexed-None-08600.
286 // Bind the bone UBO once at pass start so all shadow draws (skinned or static) are legal.
287 if (m_boneUBOCreated) {
288 vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
289 pipelineLay, 1, 1,
290 &boneDescriptorSets[currentFrame], 0, nullptr);
291 }
292
293 VkViewport shadowViewport{};
294 shadowViewport.x = 0.0f;
295 shadowViewport.y = 0.0f;
296 shadowViewport.width = static_cast<float>(m_shadowMapResolution);
297 shadowViewport.height = static_cast<float>(m_shadowMapResolution);
298 shadowViewport.minDepth = 0.0f;
299 shadowViewport.maxDepth = 1.0f;
300 vkCmdSetViewport(command, 0, 1, &shadowViewport);
301
302 VkRect2D shadowScissor{};
303 shadowScissor.offset = {0, 0};
304 shadowScissor.extent = {m_shadowMapResolution, m_shadowMapResolution};
305 vkCmdSetScissor(command, 0, 1, &shadowScissor);
306
307 m_shadowPassActive = true;
308 m_shadowPCCacheValid = false;
309 auto* queue = RenderCommandQueue::GetInstance();
310 if (queue) {
311 queue->ExecuteShadowPass(this);
312 }
313 m_shadowPassActive = false;
314
315 vkCmdEndRenderPass(command);
316 }
317
318 // ---- Deferred path: begin GBuffer render pass ----
319 if (m_gbufferResourcesCreated && m_deferredEnabled) {
320 // 4 clear values: RT0, RT1, RT2, depth
321 VkClearValue gbufferClears[GBUFFER_COUNT + 1];
322 for (uint32_t i = 0; i < GBUFFER_COUNT; ++i) {
323 gbufferClears[i].color = {0.0f, 0.0f, 0.0f, 0.0f};
324 }
325 gbufferClears[GBUFFER_COUNT].depthStencil = {1.0f, 0};
326
327 VkRenderPassBeginInfo gbufferPassInfo{};
328 gbufferPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
329 gbufferPassInfo.renderPass = m_gbufferRenderPass;
330 gbufferPassInfo.framebuffer = m_gbufferFramebuffer;
331 gbufferPassInfo.renderArea.offset = {0, 0};
332 gbufferPassInfo.renderArea.extent = scExtent;
333 gbufferPassInfo.clearValueCount = GBUFFER_COUNT + 1;
334 gbufferPassInfo.pClearValues = gbufferClears;
335
336 vkCmdBeginRenderPass(command, &gbufferPassInfo, VK_SUBPASS_CONTENTS_INLINE);
337 vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferPipeline);
338
339 // Set viewport and scissor
340 VkViewport viewport{};
341 viewport.x = 0.0f;
342 viewport.y = 0.0f;
343 viewport.width = static_cast<float>(scExtent.width);
344 viewport.height = static_cast<float>(scExtent.height);
345 viewport.minDepth = 0.0f;
346 viewport.maxDepth = 1.0f;
347 vkCmdSetViewport(command, 0, 1, &viewport);
348
349 VkRect2D scissor{};
350 scissor.offset = {0, 0};
351 scissor.extent = scExtent;
352 vkCmdSetScissor(command, 0, 1, &scissor);
353
354 // Bind descriptor sets for GBuffer geometry pass.
355 // Set 0 (PBR material) is bound per-material by BindPBRMaterial().
356 // Set 1 (bone matrices) is frame-constant: bound here so m_skinnedGbufferPipeline
357 // can always find a valid set 1, even for frames where no skinned draw fires.
358 // Sets 2-3 are frame-constant: light/shadow UBO and shadow samplers.
359 if (m_gbufferGeomLayout != VK_NULL_HANDLE && m_lightUBOCreated) {
360 if (m_boneUBOCreated) {
361 vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
362 m_gbufferGeomLayout, 1, 1,
363 &boneDescriptorSets[currentFrame], 0, nullptr);
364 }
365 vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
366 m_gbufferGeomLayout, 2, 1,
367 &m_lightUBODescriptorSets[currentFrame], 0, nullptr);
368 vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
369 m_gbufferGeomLayout, 3, 1,
370 &m_shadowSamplerDescriptorSets[currentFrame], 0, nullptr);
371 }
372
373 m_inGeometryPass = true;
374
375 // ImGui new frame (same as forward path below)
376 bImFrameActive = false;
377 if (bImInitialized) {
378 ImGui_ImplVulkan_NewFrame();
379 ImGui_ImplSDL3_NewFrame();
380 auto& io = ImGui::GetIO();
381 if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
382 ImGui::NewFrame();
383 bImFrameActive = true;
384 }
385 }
386 return;
387 }
388
389 // ---- Forward path (non-deferred): begin main render pass ----
390 // When MSAA: 3 attachments (color, depth, resolve); otherwise 2
391 // Use stack array to avoid per-frame heap allocation
392 VkClearValue clearValues[3];
393 uint32_t clearValueCount = 2;
394 clearValues[0] = clearColor;
395 clearValues[1].depthStencil = {1.0f, 0};
396 if (m_msaaSamples != VK_SAMPLE_COUNT_1_BIT) {
397 clearValues[2].color = clearColor.color;
398 clearValueCount = 3;
399 }
400
401 VkRenderPassBeginInfo passInfo{};
402 passInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
403 passInfo.renderPass = renderPass;
404 passInfo.framebuffer = swapChainFramebuffers[CurrentFrameIndex];
405 passInfo.renderArea.offset = {0, 0};
406 passInfo.renderArea.extent = scExtent;
407 passInfo.clearValueCount = clearValueCount;
408 passInfo.pClearValues = clearValues;
409
410 vkCmdBeginRenderPass(command, &passInfo, VK_SUBPASS_CONTENTS_INLINE);
411
412 vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
413
414 // Bind texture descriptor set if available
415 if (m_textureDescriptorsWritten &&
416 CurrentFrameIndex < descriptorSets.size()) {
417 vkCmdBindDescriptorSets(
418 command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 0, 1,
419 &descriptorSets[CurrentFrameIndex], 0, nullptr);
420 }
421
422 // Set dynamic viewport and scissor
423 VkViewport viewport{};
424 viewport.x = 0.0f;
425 viewport.y = 0.0f;
426 viewport.width = static_cast<float>(scExtent.width);
427 viewport.height = static_cast<float>(scExtent.height);
428 viewport.minDepth = 0.0f;
429 viewport.maxDepth = 1.0f;
430 vkCmdSetViewport(command, 0, 1, &viewport);
431
432 VkRect2D scissor{};
433 scissor.offset = {0, 0};
434 scissor.extent = scExtent;
435 vkCmdSetScissor(command, 0, 1, &scissor);
436
437 // Bind light UBO at set 2 and shadow sampler at set 3
438 if (m_lightUBOCreated) {
439 vkCmdBindDescriptorSets(
440 command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 2, 1,
441 &m_lightUBODescriptorSets[currentFrame], 0, nullptr);
442 vkCmdBindDescriptorSets(
443 command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 3, 1,
444 &m_shadowSamplerDescriptorSets[currentFrame], 0, nullptr);
445 }
446
447 // RenderCommandQueue will now call Draw/DrawIndexed/Bind* methods
448 // via the RenderContext interface on this object
449
450 bImFrameActive = false;
451 if (bImInitialized) {
452 ImGui_ImplVulkan_NewFrame();
453 ImGui_ImplSDL3_NewFrame();
454 auto& io = ImGui::GetIO();
455 if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
456 ImGui::NewFrame();
457 bImFrameActive = true;
458 }
459 }
460}
461
462/// Ends the active render pass, submits the command buffer, and presents.
464 if (!bRender || !bFrameStarted)
465 return;
466
467 const bool deferredPath =
468 m_gbufferResourcesCreated && m_deferredEnabled && m_bloomResourcesCreated;
469
470 // Safety: if geometry pass is still open (ExecuteDeferredLightingPass not called),
471 // end it now so we don't have a dangling render pass.
472 if (m_gbufferResourcesCreated && m_deferredEnabled && m_inGeometryPass) {
473 vkCmdEndRenderPass(command);
474 m_inGeometryPass = false;
475 }
476
477 // In deferred mode, open a forward render pass so the HDR scene image ends
478 // in SHADER_READ_ONLY_OPTIMAL regardless of whether any transparent pass
479 // ran. BeginForwardTransparentPass already opens this RP; if the game
480 // didn't call it (no transparent objects), open/close a trivial one here.
481 if (deferredPath && !m_forwardPassOpen && !m_inGeometryPass) {
482 VkRenderPassBeginInfo rpBegin{};
483 rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
484 rpBegin.renderPass = m_forwardRenderPass;
485 rpBegin.framebuffer = m_forwardFramebuffers[CurrentFrameIndex];
486 rpBegin.renderArea.offset = {0, 0};
487 rpBegin.renderArea.extent = scExtent;
488 rpBegin.clearValueCount = 0;
489 vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE);
490 m_forwardPassOpen = true;
491 }
492
493 // Close the forward pass (HDR scene → SHADER_READ_ONLY_OPTIMAL via finalLayout).
494 if (m_forwardPassOpen) {
495 vkCmdEndRenderPass(command);
496 m_forwardPassOpen = false;
497 }
498
499 if (deferredPath) {
500 // TAA: accumulate current HDR frame with history, write resolved result
501 // back into hdrScene. Also handles the depth barrier (ATTACHMENT → READ_ONLY)
502 // so SSR can skip its own barrier when TAA is enabled.
503 RenderTAAPass();
504 // Screen-space reflections — reads TAA-resolved hdrScene + GBuffer.
505 RenderSSRPass();
506 // Bloom pyramid generates the bloom result from the HDR scene.
507 RenderBloomPass();
508 // Composite pass reads HDR scene + bloom + SSR, applies ACES + gamma,
509 // writes to the swapchain. ImGui is drawn inside this pass.
510 RenderBloomCompositePass();
511 } else {
512 // Forward (non-deferred) path — ImGui inside main render pass.
513 if (bImFrameActive) {
514 ImGui::Render();
515 ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command);
516 }
517 vkCmdEndRenderPass(command);
518 }
519
520 if (vkEndCommandBuffer(command) != VK_SUCCESS) {
521 SLEAK_ERROR("Failed to end command buffer!");
522 return;
523 }
524
525 // Submit
526 VkSubmitInfo submitInfo{};
527 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
528
529 // Wait on image-available; also wait on transfer semaphore if uploads happened
530 VkSemaphore waitSemaphores[2];
531 VkPipelineStageFlags waitStages[2];
532 uint32_t waitCount = 0;
533
534 waitSemaphores[waitCount] = imageAvailableSemaphores[m_semaphoreIndex];
535 waitStages[waitCount] = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
536 waitCount++;
537
538 if (m_asyncFlush[currentFrame].submitted) {
539 waitSemaphores[waitCount] = m_transferSemaphores[currentFrame];
540 waitStages[waitCount] = VK_PIPELINE_STAGE_VERTEX_INPUT_BIT;
541 waitCount++;
542 }
543
544 submitInfo.waitSemaphoreCount = waitCount;
545 submitInfo.pWaitSemaphores = waitSemaphores;
546 submitInfo.pWaitDstStageMask = waitStages;
547
548 submitInfo.commandBufferCount = 1;
549 submitInfo.pCommandBuffers = &command;
550
551 // Index renderFinished semaphore by acquired image index: when image N
552 // is re-acquired, the previous present of image N is guaranteed complete,
553 // so renderFinishedSemaphores[N] is safe to reuse.
554 VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[CurrentFrameIndex]};
555 submitInfo.signalSemaphoreCount = 1;
556 submitInfo.pSignalSemaphores = signalSemaphores;
557
558 if (vkQueueSubmit(graphicsQueue, 1, &submitInfo,
559 inFlightFences[currentFrame]) != VK_SUCCESS) {
560 SLEAK_ERROR("Failed to submit draw command buffer!");
561 }
562
563 // Present
564 VkPresentInfoKHR presentInfo{};
565 presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
566 presentInfo.waitSemaphoreCount = 1;
567 presentInfo.pWaitSemaphores = signalSemaphores;
568
569 VkSwapchainKHR swapChains[] = {swapChain};
570 presentInfo.swapchainCount = 1;
571 presentInfo.pSwapchains = swapChains;
572 presentInfo.pImageIndices = &CurrentFrameIndex;
573
574 VkResult presentResult = vkQueuePresentKHR(presentQueue, &presentInfo);
575
576 if (presentResult == VK_ERROR_OUT_OF_DATE_KHR ||
577 presentResult == VK_SUBOPTIMAL_KHR) {
578 RecreateSwapChain();
579 } else if (presentResult != VK_SUCCESS) {
580 SLEAK_ERROR("Failed to present render!");
581 }
582
583 // Reset per-frame deferred state flags
584 m_forwardPassOpen = false;
585 m_inForwardTransparentPass = false;
586 m_inGeometryPass = false;
587 bFrameStarted = false; // command buffer submitted; recording is complete
588
589 currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
590 m_semaphoreIndex = (m_semaphoreIndex + 1) %
591 static_cast<uint32_t>(imageAvailableSemaphores.size());
592
594}
595
596/// True while the bound vertex buffer's format has no pipeline for this pass.
597bool VulkanRenderer::CustomFormatDrawsSuppressed() const {
598 return m_activeCustomFormat != 0 && m_customFormatUnbound;
599}
600
601/// Issues a non-indexed draw call and updates the vertex/triangle counters.
602void VulkanRenderer::Draw(uint32_t vertexCount) {
603 if (!bFrameStarted) return;
604 if (CustomFormatDrawsSuppressed()) return;
605 vkCmdDraw(command, vertexCount, 1, 0, 0);
606 if (!m_shadowPassActive) {
607 DrawnVertices += vertexCount;
608 DrawnTriangles += vertexCount / 3;
609 }
610}
611
612/// Issues an indexed draw call and updates the vertex/triangle counters.
613void VulkanRenderer::DrawIndexed(uint32_t indexCount) {
614 if (!bFrameStarted) return;
615 if (CustomFormatDrawsSuppressed()) return;
616 vkCmdDrawIndexed(command, indexCount, 1, 0, 0, 0);
617 if (!m_shadowPassActive) {
618 DrawnVertices += indexCount;
619 DrawnTriangles += indexCount / 3;
620 }
621}
622
623/// Issues an instanced, non-indexed draw call.
624void VulkanRenderer::DrawInstance(uint32_t instanceCount,
625 uint32_t vertexPerInstance) {
626 if (!bFrameStarted) return;
627 if (CustomFormatDrawsSuppressed()) return;
628 vkCmdDraw(command, vertexPerInstance, instanceCount, 0, 0);
629}
630
631/// Issues an instanced, indexed draw call.
632void VulkanRenderer::DrawIndexedInstance(uint32_t instanceCount,
633 uint32_t indexPerInstance) {
634 if (!bFrameStarted) return;
635 if (CustomFormatDrawsSuppressed()) return;
636 vkCmdDrawIndexed(command, indexPerInstance, instanceCount, 0, 0, 0);
637}
638
639/// Stores the cull face for the next pipeline rebuild (Vulkan state is baked).
641 // Vulkan pipeline state is baked, needs pipeline recreation.
642 // Store for next pipeline rebuild.
643 Face = face;
644}
645
646/// Stores the polygon mode for the next pipeline rebuild (Vulkan state is baked).
648 // Vulkan pipeline state is baked, needs pipeline recreation.
649 Mode = mode;
650}
651
652/// Sets the dynamic viewport on the active command buffer.
653void VulkanRenderer::SetViewport(float x, float y, float width,
654 float height, float minDepth,
655 float maxDepth) {
656 if (!bFrameStarted) return;
657 VkViewport viewport{};
658 viewport.x = x;
659 viewport.y = y;
660 viewport.width = width;
661 viewport.height = height;
662 viewport.minDepth = minDepth;
663 viewport.maxDepth = maxDepth;
664 vkCmdSetViewport(command, 0, 1, &viewport);
665}
666
667/// Stores the clear color used by the next BeginRender.
668void VulkanRenderer::ClearRenderTarget(float r, float g, float b,
669 float a) {
670 clearColor = {{r, g, b, a}};
671}
672
673/// No-op; depth/stencil clears are driven by the render pass clear values.
674void VulkanRenderer::ClearDepthStencil(bool clearDepth, bool clearStencil,
675 float depth, uint8_t stencil) {
676 // Handled by render pass clear values
677}
678
679/// Binds a vertex buffer slot, switching to the pipeline that matches the
680/// buffer's registered vertex format.
682 uint32_t slot) {
683 if (!bFrameStarted) return;
684 auto* vkBuf = static_cast<VulkanBuffer*>(buffer.get());
685 if (!vkBuf) return;
686
687 VertexFormatHandle wantFormat = buffer->GetVertexFormat();
688 if (wantFormat != 0) {
689 BeginCustomFormatPass(wantFormat);
690 } else if (m_activeCustomFormat != 0) {
692 }
693
694 VkBuffer buffers[] = {vkBuf->GetVkBuffer()};
695 VkDeviceSize offsets[] = {0};
696 vkCmdBindVertexBuffers(command, slot, 1, buffers, offsets);
697}
698
699/// Binds a 32-bit index buffer.
701 uint32_t slot) {
702 if (!bFrameStarted) return;
703 auto* vkBuf = static_cast<VulkanBuffer*>(buffer.get());
704 if (!vkBuf) return;
705 vkCmdBindIndexBuffer(command, vkBuf->GetVkBuffer(), 0,
706 VK_INDEX_TYPE_UINT32);
707}
708
709/// Pushes constant-buffer data via push constants, applying TAA jitter or
710/// the shadow push-constant cache as needed.
712 uint32_t slot) {
713 if (!bFrameStarted) return;
714 auto* vkBuf = static_cast<VulkanBuffer*>(buffer.get());
715 if (!vkBuf) return;
716
717 // Use push constants — recorded into the command buffer per draw call
718 void* data = vkBuf->GetData();
719 if (!data) return;
720
721 uint32_t size = static_cast<uint32_t>(vkBuf->GetSize());
722 if (size > 128) size = 128; // Vulkan guarantees at least 128 bytes
723
724 // Choose the pipeline layout that owns the currently bound pipeline.
725 // GBuffer geometry pass uses m_gbufferGeomLayout; all other passes use pipelineLay.
726 VkPipelineLayout activeLayout = (m_inGeometryPass && m_gbufferGeomLayout != VK_NULL_HANDLE)
727 ? m_gbufferGeomLayout : pipelineLay;
728
729 // In the geometry pass (not shadow), apply TAA sub-pixel jitter to WVP.
730 // Sub-pixel jitter: add jx*col3 to col0 and jy*col3 to col1.
731 // Y is negated because the geometry shader flips gl_Position.y.
732 if (m_inGeometryPass && !m_shadowPassActive && m_taaEnabled &&
733 (m_taaJitter[0] != 0.0f || m_taaJitter[1] != 0.0f) && size >= 64) {
734 float jdata[32];
735 memcpy(jdata, data, size);
736 const float jx = m_taaJitter[0] * 2.0f; // UV → NDC
737 const float jy = -m_taaJitter[1] * 2.0f; // negate for Y-flip
738 // GLSL computes WVP_cpu^T * v, so clip.x is dot(col0, v).
739 // Adding jx*col3 to col0 adds jx*clip.w to clip.x → uniform NDC shift.
740 for (int r = 0; r < 4; ++r) {
741 jdata[r * 4 + 0] += jx * jdata[r * 4 + 3]; // col0 += jx * col3
742 jdata[r * 4 + 1] += jy * jdata[r * 4 + 3]; // col1 += jy * col3
743 }
744 vkCmdPushConstants(command, activeLayout,
745 VK_SHADER_STAGE_VERTEX_BIT, 0, size, jdata);
746 return;
747 }
748
749 if (m_shadowPassActive && slot == 0 && size >= 128) {
750 // Shadow mode: push [LightVP*World (64)][World (64)].
751 // Buffer layout: [WVP (64 bytes)][World (64 bytes)].
752 // LightVP is frame-constant, so memoize on World and reuse the result
753 // across the many draws that share the identity transform.
754 const float* srcWorld = reinterpret_cast<const float*>(
755 static_cast<const char*>(data) + 64);
756
757 if (!m_shadowPCCacheValid ||
758 memcmp(srcWorld, m_shadowWorldCache, 64) != 0) {
759 // shadowWVP = World * LightVP (row-major)
760 for (int r = 0; r < 4; ++r) {
761 for (int c = 0; c < 4; ++c) {
762 float sum = 0.0f;
763 for (int k = 0; k < 4; ++k) {
764 sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c];
765 }
766 m_shadowPCCache[r * 4 + c] = sum;
767 }
768 }
769 memcpy(&m_shadowPCCache[16], srcWorld, 64);
770 memcpy(m_shadowWorldCache, srcWorld, 64);
771 m_shadowPCCacheValid = true;
772 }
773
774 vkCmdPushConstants(command, activeLayout,
775 VK_SHADER_STAGE_VERTEX_BIT, 0, 128, m_shadowPCCache);
776 } else {
777 vkCmdPushConstants(command, activeLayout,
778 VK_SHADER_STAGE_VERTEX_BIT, 0, size, data);
779 }
780}
781
782/// Allocates and initializes a VulkanBuffer.
784 void* data) {
785 auto* buffer = new VulkanBuffer(device, physicalDevice, size, type,
786 commands, graphicsQueue);
787 if (!buffer->Initialize(data)) {
788 delete buffer;
789 return nullptr;
790 }
791 return buffer;
792}
793
794/// Compiles a VulkanShader from source.
795Shader* VulkanRenderer::CreateShader(const std::string& shaderSource) {
796 auto* shader = new VulkanShader(device);
797 if (shader->compile(shaderSource)) {
798 return shader;
799 }
800 delete shader;
801 return nullptr;
802}
803
804/// Loads a texture from disk and writes its descriptor sets.
805::Sleak::Texture* VulkanRenderer::CreateTexture(const std::string& TexturePath) {
806 auto* texture = new VulkanTexture(device, physicalDevice, commands,
807 graphicsQueue);
808 if (texture->LoadFromFile(TexturePath)) {
809 WriteTextureDescriptors(texture);
810 return texture;
811 }
812 delete texture;
813 return nullptr;
814}
815
816/// Loads a texture from an in-memory RGBA8 buffer.
818 uint32_t height,
819 void* data) {
820 auto* texture = new VulkanTexture(device, physicalDevice, commands,
821 graphicsQueue);
822 if (texture->LoadFromMemory(data, width, height, TextureFormat::RGBA8)) {
823 return texture;
824 }
825 delete texture;
826 return nullptr;
827}
828
829/// Loads a cubemap from six face images and writes it into the skybox
830/// descriptor sets.
832 const std::array<std::string, 6>& facePaths) {
833 auto* texture = new VulkanCubemapTexture(device, physicalDevice,
834 commands, graphicsQueue);
835 if (texture->LoadCubemap(facePaths)) {
836 // Create skybox pipeline if not already created
837 if (skyboxPipeline == VK_NULL_HANDLE) {
838 if (!CreateSkyboxPipeline()) {
839 SLEAK_ERROR("VulkanRenderer: Failed to create skybox pipeline");
840 delete texture;
841 return nullptr;
842 }
843 }
844
845 // Write cubemap to skybox descriptor sets
846 m_skyboxCubemapView = texture->GetImageView();
847 m_skyboxCubemapSampler = texture->GetSampler();
848 for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) {
849 VkDescriptorImageInfo imageInfo{};
850 imageInfo.imageLayout =
851 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
852 imageInfo.imageView = m_skyboxCubemapView;
853 imageInfo.sampler = m_skyboxCubemapSampler;
854
855 VkWriteDescriptorSet descriptorWrite{};
856 descriptorWrite.sType =
857 VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
858 descriptorWrite.dstSet = skyboxDescriptorSets[i];
859 descriptorWrite.dstBinding = 0;
860 descriptorWrite.dstArrayElement = 0;
861 descriptorWrite.descriptorType =
862 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
863 descriptorWrite.descriptorCount = 1;
864 descriptorWrite.pImageInfo = &imageInfo;
865
866 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0,
867 nullptr);
868 }
869 m_skyboxDescriptorsWritten = true;
870
871 return texture;
872 }
873 delete texture;
874 return nullptr;
875}
876
877/// Loads an equirectangular panorama as a cubemap and writes it into the
878/// skybox descriptor sets.
880 const std::string& panoramaPath) {
881 auto* texture = new VulkanCubemapTexture(device, physicalDevice,
882 commands, graphicsQueue);
883 if (texture->LoadEquirectangular(panoramaPath)) {
884 // Create skybox pipeline if not already created
885 if (skyboxPipeline == VK_NULL_HANDLE) {
886 if (!CreateSkyboxPipeline()) {
887 SLEAK_ERROR("VulkanRenderer: Failed to create skybox pipeline");
888 delete texture;
889 return nullptr;
890 }
891 }
892
893 // Write cubemap to skybox descriptor sets
894 m_skyboxCubemapView = texture->GetImageView();
895 m_skyboxCubemapSampler = texture->GetSampler();
896 for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) {
897 VkDescriptorImageInfo imageInfo{};
898 imageInfo.imageLayout =
899 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
900 imageInfo.imageView = m_skyboxCubemapView;
901 imageInfo.sampler = m_skyboxCubemapSampler;
902
903 VkWriteDescriptorSet descriptorWrite{};
904 descriptorWrite.sType =
905 VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
906 descriptorWrite.dstSet = skyboxDescriptorSets[i];
907 descriptorWrite.dstBinding = 0;
908 descriptorWrite.dstArrayElement = 0;
909 descriptorWrite.descriptorType =
910 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
911 descriptorWrite.descriptorCount = 1;
912 descriptorWrite.pImageInfo = &imageInfo;
913
914 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0,
915 nullptr);
916 }
917 m_skyboxDescriptorsWritten = true;
918
919 // Trigger IBL precompute now that we have an environment cubemap
920 return texture;
921 }
922 delete texture;
923 return nullptr;
924}
925
926/// Binds a texture's descriptor set at slot 0, skipping cubemaps and the
927/// GBuffer geometry pass.
929 uint32_t slot) {
930 if (!bFrameStarted) return;
931 if (!texture.IsValid() || slot != 0)
932 return;
933
934 // Cubemap textures are bound via skybox pass, skip here
935 if (texture->GetType() == TextureType::TextureCube)
936 return;
937
938 auto* vkTex = static_cast<VulkanTexture*>(texture.get());
939 if (!vkTex || !vkTex->HasDescriptorSets())
940 return;
941
942 const auto& sets = vkTex->GetDescriptorSets();
943 if (CurrentFrameIndex < sets.size()) {
944 // In the GBuffer geometry pass set 0 is the PBR material descriptor set
945 // (m_pbrMaterialDSL, bound by BindPBRMaterial via m_gbufferGeomLayout).
946 // Binding a forward single-sampler descriptor set here with the wrong
947 // layout would corrupt set 0 and trigger VK_ERROR_DEVICE_LOST.
948 // BindPBRMaterial owns set 0 during the geometry pass — skip here.
949 if (m_inGeometryPass)
950 return;
951 vkCmdBindDescriptorSets(
952 command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 0, 1,
953 &sets[CurrentFrameIndex], 0, nullptr);
954 }
955}
956
957/// Raw-pointer variant of BindTexture.
958void VulkanRenderer::BindTextureRaw(Sleak::Texture* texture, uint32_t slot) {
959 if (!bFrameStarted) return;
960 if (!texture || slot != 0)
961 return;
962
963 // Cubemap textures are bound via skybox pass, skip here
964 if (texture->GetType() == TextureType::TextureCube)
965 return;
966
967 auto* vkTex = static_cast<VulkanTexture*>(texture);
968 if (!vkTex || !vkTex->HasDescriptorSets())
969 return;
970
971 const auto& sets = vkTex->GetDescriptorSets();
972 if (CurrentFrameIndex < sets.size()) {
973 // In the GBuffer geometry pass set 0 is the PBR material descriptor set
974 // (m_pbrMaterialDSL, bound by BindPBRMaterial via m_gbufferGeomLayout).
975 // Binding a forward single-sampler descriptor set here with the wrong
976 // layout would corrupt set 0 and trigger VK_ERROR_DEVICE_LOST.
977 // BindPBRMaterial owns set 0 during the geometry pass — skip here.
978 if (m_inGeometryPass)
979 return;
980 vkCmdBindDescriptorSets(
981 command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 0, 1,
982 &sets[CurrentFrameIndex], 0, nullptr);
983 }
984}
985
986/// Allocates one primary command buffer per frame in flight.
987bool VulkanRenderer::CreateCommandBuffer() {
988 commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
989
990 VkCommandBufferAllocateInfo allocInfo{};
991 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
992 allocInfo.commandPool = commands;
993 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
994 allocInfo.commandBufferCount = MAX_FRAMES_IN_FLIGHT;
995
996 if (vkAllocateCommandBuffers(device, &allocInfo,
997 commandBuffers.data()) != VK_SUCCESS)
998 SLEAK_RETURN_ERR("Failed to allocate command buffers!");
999
1000 return true;
1001}
1002
1003/// Blocks until the device finishes all submitted GPU work.
1005 if (device) vkDeviceWaitIdle(device);
1006}
1007
1008/// Kicks off the current frame's async buffer upload batch.
1010 m_asyncFlush[currentFrame] = VulkanBuffer::FlushPendingCopiesAsync(
1011 m_transferSemaphores[currentFrame]);
1012}
1013
1014/// Returns total bytes currently allocated by VulkanBuffer.
1016 return static_cast<size_t>(VulkanBuffer::GetTotalAllocatedBytes());
1017}
1018
1019/// Returns the device-local heap size reported by the allocator.
1021 return static_cast<size_t>(VulkanBuffer::GetDeviceLocalHeapSize());
1022}
1023
1024/// Tears down every Vulkan resource in reverse dependency order.
1026 SLEAK_INFO("Cleaning Vulkan...");
1027
1028 bRender = false;
1029
1030 // Wait for the device to finish all work
1031 if (device) {
1032 vkDeviceWaitIdle(device);
1033 }
1034
1036
1037 // Flush all deferred buffer deletions now that GPU is idle
1039
1040 // Shutdown ImGUI before destroying Vulkan resources
1041 if (bImInitialized) {
1042 ImGui_ImplVulkan_Shutdown();
1043 ImGui_ImplSDL3_Shutdown();
1044 ImGui::DestroyContext();
1045 bImInitialized = false;
1046 }
1047 if (imguiDescriptorPool) {
1048 vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr);
1049 imguiDescriptorPool = VK_NULL_HANDLE;
1050 }
1051
1052 // Destroy descriptor pool (frees descriptor sets too)
1053 if (descriptorPool) {
1054 vkDestroyDescriptorPool(device, descriptorPool, nullptr);
1055 descriptorPool = VK_NULL_HANDLE;
1056 }
1057 descriptorSets.clear();
1058
1059 // Destroy skybox resources
1060 if (skyboxPipeline) {
1061 vkDestroyPipeline(device, skyboxPipeline, nullptr);
1062 skyboxPipeline = VK_NULL_HANDLE;
1063 }
1064 if (skyboxDescriptorPool) {
1065 vkDestroyDescriptorPool(device, skyboxDescriptorPool, nullptr);
1066 skyboxDescriptorPool = VK_NULL_HANDLE;
1067 }
1068 skyboxDescriptorSets.clear();
1069 delete skyboxShader;
1070 skyboxShader = nullptr;
1071
1072 // Destroy skinned pipeline resources
1073 if (skinnedPipeline) {
1074 vkDestroyPipeline(device, skinnedPipeline, nullptr);
1075 skinnedPipeline = VK_NULL_HANDLE;
1076 }
1077 delete skinnedShader;
1078 skinnedShader = nullptr;
1079
1080 // Destroy debug line pipeline resources
1081 if (debugLinePipeline) {
1082 vkDestroyPipeline(device, debugLinePipeline, nullptr);
1083 debugLinePipeline = VK_NULL_HANDLE;
1084 }
1085 delete debugLineShader;
1086 debugLineShader = nullptr;
1087
1088 // Destroy custom vertex format pipelines
1089 DestroyCustomFormatPipelines();
1090
1091 // Destroy MSAA color resources
1092 CleanupMSAAColorResources();
1093
1094 // Destroy deferred GBuffer resources
1095 CleanupGBufferResources();
1096
1097 // Destroy shadow mapping resources
1098 CleanupShadowResources();
1099
1100 // Backstop: free any shader modules whose resource-guarded cleanup was
1101 // skipped (guard false while shader non-null). Cleanups null after delete,
1102 // so these are no-ops when already freed — delete(nullptr) is safe.
1103 delete m_gbufferShader; m_gbufferShader = nullptr;
1104 delete m_lightingShader; m_lightingShader = nullptr;
1105 delete m_ssaoShader; m_ssaoShader = nullptr;
1106 delete m_ssaoBlurShader; m_ssaoBlurShader = nullptr;
1107 delete m_ssrShader; m_ssrShader = nullptr;
1108 delete m_taaShader; m_taaShader = nullptr;
1109 delete m_bloomThresholdShader; m_bloomThresholdShader = nullptr;
1110 delete m_bloomDownsampleShader; m_bloomDownsampleShader = nullptr;
1111 delete m_bloomUpsampleShader; m_bloomUpsampleShader = nullptr;
1112 delete m_bloomCompositeShader; m_bloomCompositeShader = nullptr;
1113
1114 // Destroy bone UBO resources
1115 CleanupBoneUBOResources();
1116
1117 // Destroy descriptor set layouts
1118 if (m_shadowSamplerDescriptorSetLayout) {
1119 vkDestroyDescriptorSetLayout(device, m_shadowSamplerDescriptorSetLayout, nullptr);
1120 m_shadowSamplerDescriptorSetLayout = VK_NULL_HANDLE;
1121 }
1122 if (m_lightUBODescriptorSetLayout) {
1123 vkDestroyDescriptorSetLayout(device, m_lightUBODescriptorSetLayout, nullptr);
1124 m_lightUBODescriptorSetLayout = VK_NULL_HANDLE;
1125 }
1126 if (boneDescriptorSetLayout) {
1127 vkDestroyDescriptorSetLayout(device, boneDescriptorSetLayout, nullptr);
1128 boneDescriptorSetLayout = VK_NULL_HANDLE;
1129 }
1130 if (descriptorSetLayout) {
1131 vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
1132 descriptorSetLayout = VK_NULL_HANDLE;
1133 }
1134
1135 // Clean up depth resources
1136 CleanupDepthResources();
1137
1138 // Destroy swapchain
1139 if (swapChain && device) {
1140 vkDestroySwapchainKHR(device, swapChain, nullptr);
1141 swapChain = VK_NULL_HANDLE;
1142 }
1143
1144 // Destroy framebuffers
1145 for (auto& buffer : swapChainFramebuffers) {
1146 if (buffer) {
1147 vkDestroyFramebuffer(device, buffer, nullptr);
1148 buffer = VK_NULL_HANDLE;
1149 }
1150 }
1151 swapChainFramebuffers.clear();
1152
1153 // Destroy image views
1154 for (auto& imgView : swapChainImageViews) {
1155 if (imgView) {
1156 vkDestroyImageView(device, imgView, nullptr);
1157 imgView = VK_NULL_HANDLE;
1158 }
1159 }
1160 swapChainImageViews.clear();
1161
1162 // Destroy sync objects
1163 for (auto& sem : imageAvailableSemaphores) {
1164 if (sem) vkDestroySemaphore(device, sem, nullptr);
1165 }
1166 imageAvailableSemaphores.clear();
1167
1168 for (auto& sem : renderFinishedSemaphores) {
1169 if (sem) vkDestroySemaphore(device, sem, nullptr);
1170 }
1171 renderFinishedSemaphores.clear();
1172
1173 for (auto& fence : inFlightFences) {
1174 if (fence) vkDestroyFence(device, fence, nullptr);
1175 }
1176 inFlightFences.clear();
1177
1178 // Destroy transfer semaphores and free any pending staging buffers
1179 for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
1180 if (m_transferSemaphores[i]) {
1181 vkDestroySemaphore(device, m_transferSemaphores[i], nullptr);
1182 m_transferSemaphores[i] = VK_NULL_HANDLE;
1183 }
1184 auto& af = m_asyncFlush[i];
1185 if (af.commandBuffer != VK_NULL_HANDLE) {
1186 vkFreeCommandBuffers(af.device, af.commandPool, 1,
1187 &af.commandBuffer);
1188 }
1189 for (auto& pending : af.stagingBuffers) {
1190 vmaDestroyBuffer(VulkanBuffer::GetAllocator(), pending.buffer,
1191 pending.memory);
1192 VulkanBuffer::UntrackAllocation(pending.allocSize);
1193 }
1194 af = {};
1195 }
1196
1197 // Destroy shader
1198 if (simpleShader) {
1199 delete simpleShader;
1200 simpleShader = nullptr;
1201 }
1202
1203 // Destroy pipeline
1204 if (pipeline) {
1205 vkDestroyPipeline(device, pipeline, nullptr);
1206 pipeline = VK_NULL_HANDLE;
1207 }
1208
1209 // Destroy pipeline layout
1210 if (pipelineLay) {
1211 vkDestroyPipelineLayout(device, pipelineLay, nullptr);
1212 pipelineLay = VK_NULL_HANDLE;
1213 }
1214
1215 // Destroy render pass
1216 if (renderPass) {
1217 vkDestroyRenderPass(device, renderPass, nullptr);
1218 renderPass = VK_NULL_HANDLE;
1219 }
1220
1221 // Destroy default texture
1222 if (m_defaultTexture) {
1223 delete m_defaultTexture;
1224 m_defaultTexture = nullptr;
1225 }
1226
1227 // Destroy command pool (this will also free command buffers)
1228 if (commands) {
1229 vkDestroyCommandPool(device, commands, nullptr);
1230 commands = VK_NULL_HANDLE;
1231 }
1232
1233 // Destroy surface after swapchain is gone
1234 if (surface && instance) {
1235 vkDestroySurfaceKHR(instance, surface, nullptr);
1236 surface = VK_NULL_HANDLE;
1237 }
1238
1239 // Destroy debug messenger
1240 if (debugMessenger && vkDestroyDebugUtilsMessengerEXT) {
1241 vkDestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
1242 debugMessenger = VK_NULL_HANDLE;
1243 }
1244
1245 // Drain any buffers freed during teardown, then destroy the VMA allocator
1246 // (it must outlive every vmaDestroyBuffer, and both precede vkDestroyDevice).
1249
1250 // Destroy logical device
1251 if (device) {
1252 vkDestroyDevice(device, nullptr);
1253 device = VK_NULL_HANDLE;
1254 }
1255
1256 // Destroy Vulkan instance
1257 if (instance) {
1258 vkDestroyInstance(instance, nullptr);
1259 instance = VK_NULL_HANDLE;
1260 }
1261}
1262
1263/// Recreates the swapchain for the new window dimensions.
1264void VulkanRenderer::Resize(uint32_t width, uint32_t height) {
1265 if (device) {
1266 RecreateSwapChain();
1267 }
1268}
1269
1270/// Rebuilds the swapchain and its dependents (image views, depth, MSAA,
1271/// framebuffers, GBuffer) after a resize or resolution change.
1272bool VulkanRenderer::RecreateSwapChain() {
1273 vkDeviceWaitIdle(device);
1274
1275 // Cleanup GBuffer BEFORE CleanupSwapChain (which destroys depth image)
1276 // to avoid dangling image view references in GBuffer framebuffer
1277 bool hadGBuffer = m_gbufferResourcesCreated;
1278 CleanupGBufferResources();
1279
1280 CleanupMSAAColorResources();
1281 CleanupSwapChain();
1282
1283 if (!CreateSwapChain()) {
1284 SLEAK_ERROR("Failed to recreate swap chain!");
1285 return false;
1286 }
1287 if (!CreateImageViews()) {
1288 SLEAK_ERROR("Failed to recreate image views!");
1289 return false;
1290 }
1291 if (!CreateDepthResources()) {
1292 SLEAK_ERROR("Failed to recreate depth resources!");
1293 return false;
1294 }
1295 if (!CreateMSAAColorResources()) {
1296 SLEAK_ERROR("Failed to recreate MSAA color resources!");
1297 return false;
1298 }
1299 if (!CreateFrameBuffer()) {
1300 SLEAK_ERROR("Failed to recreate framebuffers!");
1301 return false;
1302 }
1303
1304 // Resize imagesInFlight in case swapchain image count changed
1305 imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE);
1306
1307 // Recreate GBuffer resources if they were previously created
1308 if (hadGBuffer && m_deferredEnabled) {
1309 if (!CreateGBufferResources())
1310 SLEAK_WARN("RecreateSwapChain: Failed to recreate GBuffer resources!");
1311 }
1312
1313 return true;
1314}
1315
1316/// Rebuilds the swapchain-dependent pipelines and render pass for a queued
1317/// MSAA sample count change.
1320 return;
1321 m_msaaChangeRequested = false;
1322
1323 uint32_t newCount = m_pendingMsaaSampleCount;
1324 m_msaaSampleCount = newCount;
1325
1326 // Convert to Vulkan enum
1327 switch (newCount) {
1328 case 1: m_msaaSamples = VK_SAMPLE_COUNT_1_BIT; break;
1329 case 2: m_msaaSamples = VK_SAMPLE_COUNT_2_BIT; break;
1330 case 4: m_msaaSamples = VK_SAMPLE_COUNT_4_BIT; break;
1331 case 8: m_msaaSamples = VK_SAMPLE_COUNT_8_BIT; break;
1332 default: m_msaaSamples = VK_SAMPLE_COUNT_1_BIT; break;
1333 }
1334
1335 SLEAK_INFO("Applying MSAA change: {}x", newCount);
1336
1337 vkDeviceWaitIdle(device);
1338
1339 // Destroy render pass
1340 if (renderPass) {
1341 vkDestroyRenderPass(device, renderPass, nullptr);
1342 renderPass = VK_NULL_HANDLE;
1343 }
1344
1345 // Destroy main-pass pipelines (NOT shadow pipeline)
1346 if (pipeline) {
1347 vkDestroyPipeline(device, pipeline, nullptr);
1348 pipeline = VK_NULL_HANDLE;
1349 }
1350 if (skyboxPipeline) {
1351 vkDestroyPipeline(device, skyboxPipeline, nullptr);
1352 skyboxPipeline = VK_NULL_HANDLE;
1353 }
1354 if (skinnedPipeline) {
1355 vkDestroyPipeline(device, skinnedPipeline, nullptr);
1356 skinnedPipeline = VK_NULL_HANDLE;
1357 }
1358 if (debugLinePipeline) {
1359 vkDestroyPipeline(device, debugLinePipeline, nullptr);
1360 debugLinePipeline = VK_NULL_HANDLE;
1361 }
1362 // Custom-format variants are built against the render passes torn down
1363 // here; drop them so the next draw rebuilds against the new ones.
1364 DestroyCustomFormatPipelines();
1365
1366 // Cleanup GBuffer BEFORE swapchain/depth (avoids dangling image view refs)
1367 CleanupGBufferResources();
1368
1369 // Shutdown ImGUI
1370 if (bImInitialized) {
1371 ImGui_ImplVulkan_Shutdown();
1372 ImGui_ImplSDL3_Shutdown();
1373 ImGui::DestroyContext();
1374 bImInitialized = false;
1375 }
1376
1377 // Cleanup swapchain-related resources
1378 CleanupMSAAColorResources();
1379 CleanupSwapChain();
1380
1381 // Recreate everything
1382 CreateSwapChain();
1383 CreateImageViews();
1384 CreateDepthResources();
1385 CreateMSAAColorResources();
1386 CreateRenderPass();
1387 CreateFrameBuffer();
1388 CreateGraphicsPipeline();
1389 CreateSkyboxPipeline();
1390 CreateSkinnedPipeline();
1391 CreateDebugLinePipeline();
1392 if (m_deferredEnabled) CreateGBufferResources();
1393 CreateImGUI();
1394
1395 // Re-bind skybox cubemap texture to the new descriptor sets
1396 if (m_skyboxCubemapView != VK_NULL_HANDLE && m_skyboxCubemapSampler != VK_NULL_HANDLE) {
1397 for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) {
1398 VkDescriptorImageInfo imageInfo{};
1399 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1400 imageInfo.imageView = m_skyboxCubemapView;
1401 imageInfo.sampler = m_skyboxCubemapSampler;
1402
1403 VkWriteDescriptorSet descriptorWrite{};
1404 descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1405 descriptorWrite.dstSet = skyboxDescriptorSets[i];
1406 descriptorWrite.dstBinding = 0;
1407 descriptorWrite.dstArrayElement = 0;
1408 descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1409 descriptorWrite.descriptorCount = 1;
1410 descriptorWrite.pImageInfo = &imageInfo;
1411
1412 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
1413 }
1414 m_skyboxDescriptorsWritten = true;
1415 }
1416
1417 SLEAK_INFO("MSAA change applied successfully");
1418}
1419
1420/// Recreates the swapchain to apply a queued VSync toggle.
1423 return;
1424 m_vsyncChangeRequested = false;
1425 RecreateSwapChain();
1426 SLEAK_INFO("VSync {}", m_vsync ? "enabled" : "disabled");
1427}
1428
1429/// No-op; Vulkan polygon mode changes require pipeline recreation.
1431 // Pipeline recreation needed for Vulkan polygon mode changes
1432}
1433
1434/// No-op; Vulkan cull mode changes require pipeline recreation.
1436 // Pipeline recreation needed for Vulkan cull mode changes
1437}
1438
1439/// Creates the graphics command pool.
1440bool VulkanRenderer::CreateCommandPool() {
1441 VkCommandPoolCreateInfo poolInfo{};
1442 poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
1443 poolInfo.queueFamilyIndex = QueueIDs.GraphicsIndex;
1444 poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
1445
1446 if (vkCreateCommandPool(device, &poolInfo, nullptr, &commands) !=
1447 VK_SUCCESS)
1448 SLEAK_RETURN_ERR("Failed to create command pool!");
1449
1450 return true;
1451}
1452
1453/// Creates the per-swapchain-image semaphores and per-frame fences and
1454/// transfer semaphores.
1455bool VulkanRenderer::CreateSyncObjects() {
1456 uint32_t imageCount = static_cast<uint32_t>(swapChainImages.size());
1457
1458 // Semaphores sized to swapchain image count to prevent reuse
1459 // while the presentation engine still holds a reference.
1460 imageAvailableSemaphores.resize(imageCount);
1461 renderFinishedSemaphores.resize(imageCount);
1462 inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
1463 imagesInFlight.resize(imageCount, VK_NULL_HANDLE);
1464
1465 VkSemaphoreCreateInfo semaphoreInfo{};
1466 semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
1467
1468 VkFenceCreateInfo fenceInfo{};
1469 fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
1470 fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
1471
1472 // Create per-swapchain-image semaphores
1473 for (uint32_t i = 0; i < imageCount; i++) {
1474 if (vkCreateSemaphore(device, &semaphoreInfo, nullptr,
1475 &imageAvailableSemaphores[i]) != VK_SUCCESS ||
1476 vkCreateSemaphore(device, &semaphoreInfo, nullptr,
1477 &renderFinishedSemaphores[i]) != VK_SUCCESS) {
1478 SLEAK_RETURN_ERR("Failed to create synchronization objects!");
1479 }
1480 }
1481
1482 // Create per-frame-in-flight fences and transfer semaphores
1483 for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
1484 if (vkCreateSemaphore(device, &semaphoreInfo, nullptr,
1485 &m_transferSemaphores[i]) != VK_SUCCESS ||
1486 vkCreateFence(device, &fenceInfo, nullptr,
1487 &inFlightFences[i]) != VK_SUCCESS) {
1488 SLEAK_RETURN_ERR("Failed to create synchronization objects!");
1489 }
1490 m_asyncFlush[i] = {};
1491 }
1492
1493 m_semaphoreIndex = 0;
1494 return true;
1495}
1496
1497/// Recreates the extent-dependent shadow map objects (image, view,
1498/// framebuffer) at the queued resolution. Keep in sync with
1499/// CreateShadowResources; samplers, render pass, and pipeline are extent-independent.
1501 if (!m_shadowResChangeRequested) return;
1503 if (!m_shadowResourcesCreated) return;
1505
1506 vkDeviceWaitIdle(device);
1507
1508 vkDestroyFramebuffer(device, m_shadowFramebuffer, nullptr);
1509 m_shadowFramebuffer = VK_NULL_HANDLE;
1510 vkDestroyImageView(device, m_shadowImageView, nullptr);
1511 m_shadowImageView = VK_NULL_HANDLE;
1512 vkDestroyImage(device, m_shadowImage, nullptr);
1513 m_shadowImage = VK_NULL_HANDLE;
1514 vkFreeMemory(device, m_shadowImageMemory, nullptr);
1515 m_shadowImageMemory = VK_NULL_HANDLE;
1516
1518
1519 VkImageCreateInfo imageInfo{};
1520 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1521 imageInfo.imageType = VK_IMAGE_TYPE_2D;
1522 imageInfo.extent = {m_shadowMapResolution, m_shadowMapResolution, 1};
1523 imageInfo.mipLevels = 1;
1524 imageInfo.arrayLayers = 1;
1525 imageInfo.format = VK_FORMAT_D32_SFLOAT;
1526 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1527 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1528 imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT |
1529 VK_IMAGE_USAGE_SAMPLED_BIT;
1530 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1531 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1532
1533 if (vkCreateImage(device, &imageInfo, nullptr, &m_shadowImage) !=
1534 VK_SUCCESS) {
1535 SLEAK_ERROR("Shadow resolution change: image creation failed");
1537 return;
1538 }
1539
1540 VkMemoryRequirements memReqs;
1541 vkGetImageMemoryRequirements(device, m_shadowImage, &memReqs);
1542
1543 VkMemoryAllocateInfo allocInfo{};
1544 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1545 allocInfo.allocationSize = memReqs.size;
1546 allocInfo.memoryTypeIndex = FindMemoryType(
1547 memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
1548
1549 if (vkAllocateMemory(device, &allocInfo, nullptr, &m_shadowImageMemory) !=
1550 VK_SUCCESS) {
1551 SLEAK_ERROR("Shadow resolution change: memory allocation failed");
1553 return;
1554 }
1555 vkBindImageMemory(device, m_shadowImage, m_shadowImageMemory, 0);
1556
1557 VkImageViewCreateInfo viewInfo{};
1558 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1559 viewInfo.image = m_shadowImage;
1560 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1561 viewInfo.format = VK_FORMAT_D32_SFLOAT;
1562 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
1563 viewInfo.subresourceRange.baseMipLevel = 0;
1564 viewInfo.subresourceRange.levelCount = 1;
1565 viewInfo.subresourceRange.baseArrayLayer = 0;
1566 viewInfo.subresourceRange.layerCount = 1;
1567
1568 if (vkCreateImageView(device, &viewInfo, nullptr, &m_shadowImageView) !=
1569 VK_SUCCESS) {
1570 SLEAK_ERROR("Shadow resolution change: image view creation failed");
1572 return;
1573 }
1574
1575 VkFramebufferCreateInfo fbInfo{};
1576 fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
1577 fbInfo.renderPass = m_shadowRenderPass;
1578 fbInfo.attachmentCount = 1;
1579 fbInfo.pAttachments = &m_shadowImageView;
1580 fbInfo.width = m_shadowMapResolution;
1581 fbInfo.height = m_shadowMapResolution;
1582 fbInfo.layers = 1;
1583
1584 if (vkCreateFramebuffer(device, &fbInfo, nullptr, &m_shadowFramebuffer) !=
1585 VK_SUCCESS) {
1586 SLEAK_ERROR("Shadow resolution change: framebuffer creation failed");
1588 return;
1589 }
1590
1591 // Initial layout transition — descriptor must be valid pre-first-pass
1592 {
1593 VkCommandBufferAllocateInfo cmdAllocInfo{};
1594 cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
1595 cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
1596 cmdAllocInfo.commandPool = commands;
1597 cmdAllocInfo.commandBufferCount = 1;
1598
1599 VkCommandBuffer cmdBuf;
1600 vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmdBuf);
1601
1602 VkCommandBufferBeginInfo cmdBeginInfo{};
1603 cmdBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
1604 cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
1605 vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo);
1606
1607 VkImageMemoryBarrier barrier{};
1608 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
1609 barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1610 barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
1611 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1612 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1613 barrier.image = m_shadowImage;
1614 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
1615 barrier.subresourceRange.baseMipLevel = 0;
1616 barrier.subresourceRange.levelCount = 1;
1617 barrier.subresourceRange.baseArrayLayer = 0;
1618 barrier.subresourceRange.layerCount = 1;
1619 barrier.srcAccessMask = 0;
1620 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
1621
1622 vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
1623 VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0,
1624 nullptr, 0, nullptr, 1, &barrier);
1625
1626 vkEndCommandBuffer(cmdBuf);
1627
1628 VkSubmitInfo layoutSubmit{};
1629 layoutSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
1630 layoutSubmit.commandBufferCount = 1;
1631 layoutSubmit.pCommandBuffers = &cmdBuf;
1632
1633 VkFenceCreateInfo layoutFenceInfo{};
1634 layoutFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
1635 VkFence layoutFence;
1636 vkCreateFence(device, &layoutFenceInfo, nullptr, &layoutFence);
1637 vkQueueSubmit(graphicsQueue, 1, &layoutSubmit, layoutFence);
1638 vkWaitForFences(device, 1, &layoutFence, VK_TRUE, UINT64_MAX);
1639 vkDestroyFence(device, layoutFence, nullptr);
1640 vkFreeCommandBuffers(device, commands, 1, &cmdBuf);
1641 }
1642
1643 // Point set-3 descriptors at the new image view
1644 if (m_lightUBOCreated && m_shadowImageView && m_shadowSampler &&
1645 m_shadowRawSampler) {
1646 for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
1647 VkDescriptorImageInfo compareInfo{};
1648 compareInfo.imageLayout =
1649 VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
1650 compareInfo.imageView = m_shadowImageView;
1651 compareInfo.sampler = m_shadowSampler;
1652
1653 VkDescriptorImageInfo rawInfo{};
1654 rawInfo.imageLayout =
1655 VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
1656 rawInfo.imageView = m_shadowImageView;
1657 rawInfo.sampler = m_shadowRawSampler;
1658
1659 std::array<VkWriteDescriptorSet, 2> writes{};
1660 writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1661 writes[0].dstSet = m_shadowSamplerDescriptorSets[i];
1662 writes[0].dstBinding = 0;
1663 writes[0].dstArrayElement = 0;
1664 writes[0].descriptorType =
1665 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1666 writes[0].descriptorCount = 1;
1667 writes[0].pImageInfo = &compareInfo;
1668
1669 writes[1] = writes[0];
1670 writes[1].dstBinding = 1;
1671 writes[1].pImageInfo = &rawInfo;
1672
1673 vkUpdateDescriptorSets(device,
1674 static_cast<uint32_t>(writes.size()),
1675 writes.data(), 0, nullptr);
1676 }
1677 }
1678
1679 SLEAK_INFO("VulkanRenderer: Shadow map resized to {}x{}",
1681}
1682
1683/// Top-level orchestrator for deferred rendering: creates the GBuffer color
1684/// images, then the render passes, descriptors, and pipelines that read
1685/// them. Shares the depth image from CreateDepthResources() (already SAMPLED_BIT).
1686bool VulkanRenderer::CreateGBufferResources() {
1687 if (m_gbufferResourcesCreated) return true;
1688
1689 // Create GBuffer color attachment images
1690 for (uint32_t i = 0; i < GBUFFER_COUNT; ++i) {
1691 VkImageCreateInfo imageInfo{};
1692 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1693 imageInfo.imageType = VK_IMAGE_TYPE_2D;
1694 imageInfo.extent.width = scExtent.width;
1695 imageInfo.extent.height = scExtent.height;
1696 imageInfo.extent.depth = 1;
1697 imageInfo.mipLevels = 1;
1698 imageInfo.arrayLayers = 1;
1699 imageInfo.format = m_gbufferFormats[i];
1700 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1701 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1702 imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
1703 | VK_IMAGE_USAGE_SAMPLED_BIT;
1704 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1705 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1706
1707 if (vkCreateImage(device, &imageInfo, nullptr, &m_gbufferImages[i]) != VK_SUCCESS) {
1708 SLEAK_ERROR("GBuffer: Failed to create GBuffer image {}!", i);
1709 return false;
1710 }
1711
1712 VkMemoryRequirements memReqs;
1713 vkGetImageMemoryRequirements(device, m_gbufferImages[i], &memReqs);
1714
1715 VkMemoryAllocateInfo allocInfo{};
1716 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1717 allocInfo.allocationSize = memReqs.size;
1718 allocInfo.memoryTypeIndex = FindMemoryType(memReqs.memoryTypeBits,
1719 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
1720
1721 if (vkAllocateMemory(device, &allocInfo, nullptr, &m_gbufferMemory[i]) != VK_SUCCESS) {
1722 SLEAK_ERROR("GBuffer: Failed to allocate GBuffer memory {}!", i);
1723 return false;
1724 }
1725 vkBindImageMemory(device, m_gbufferImages[i], m_gbufferMemory[i], 0);
1726
1727 VkImageViewCreateInfo viewInfo{};
1728 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1729 viewInfo.image = m_gbufferImages[i];
1730 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1731 viewInfo.format = m_gbufferFormats[i];
1732 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
1733 viewInfo.subresourceRange.baseMipLevel = 0;
1734 viewInfo.subresourceRange.levelCount = 1;
1735 viewInfo.subresourceRange.baseArrayLayer = 0;
1736 viewInfo.subresourceRange.layerCount = 1;
1737
1738 if (vkCreateImageView(device, &viewInfo, nullptr, &m_gbufferViews[i]) != VK_SUCCESS) {
1739 SLEAK_ERROR("GBuffer: Failed to create GBuffer image view {}!", i);
1740 return false;
1741 }
1742 }
1743
1744 if (!CreateGBufferRenderPass()) { SLEAK_ERROR("GBuffer: render pass failed!"); return false; }
1745 if (!CreateGBufferFramebuffer()) { SLEAK_ERROR("GBuffer: framebuffer failed!"); return false; }
1746 // SSAO + bloom must be created BEFORE the lighting/forward render passes
1747 // and BEFORE CreateGBufferDescriptorSets — the lighting pass framebuffers
1748 // reference m_hdrSceneView (created by CreateBloomResources) and the
1749 // GBuffer sampler set 0 binding 7 samples the SSAO blur result.
1750 if (!CreateSSAOResources()) { SLEAK_ERROR("GBuffer: SSAO resources failed!"); return false; }
1751 if (!CreateBloomResources()) { SLEAK_ERROR("GBuffer: bloom/HDR resources failed!"); return false; }
1752 // SSR needs the HDR scene view (created by CreateBloomResources), so it
1753 // must come after that. The bloom composite pass later samples SSR.
1754 if (!CreateSSRResources()) { SLEAK_ERROR("GBuffer: SSR resources failed!"); return false; }
1755 // Prime the disabled-effect fallback images once so the per-frame disabled
1756 // paths can skip their redundant clears (ssao/ssr/bloom).
1757 m_ssaoFallbackPrimed = false;
1758 m_ssrFallbackPrimed = false;
1759 m_bloomFallbackPrimed = false;
1760 InitDisabledEffectFallbacks();
1761 if (!CreateGBufferDescriptorSets()) { SLEAK_ERROR("GBuffer: descriptor sets failed!"); return false; }
1762 if (!CreateDeferredCBResources()) { SLEAK_ERROR("GBuffer: deferred CB failed!"); return false; }
1763 if (!CreatePBRMaterialResources()) { SLEAK_ERROR("GBuffer: PBR material resources failed!"); return false; }
1764 if (!CreateIBLResources()) { SLEAK_ERROR("GBuffer: IBL resources failed!"); return false; }
1765 if (!CreateLightingRenderPass()) { SLEAK_ERROR("GBuffer: lighting RP failed!"); return false; }
1766 if (!CreateLightingFramebuffers()) { SLEAK_ERROR("GBuffer: lighting FBs failed!"); return false; }
1767 if (!CreateLightingPipeline()) { SLEAK_ERROR("GBuffer: lighting pipeline failed!"); return false; }
1768 if (!CreateForwardRenderPass()) { SLEAK_ERROR("GBuffer: forward RP failed!"); return false; }
1769 if (!CreateForwardFramebuffers()) { SLEAK_ERROR("GBuffer: forward FBs failed!"); return false; }
1770 if (!CreateGBufferPipeline()) { SLEAK_ERROR("GBuffer: gbuffer pipeline failed!"); return false; }
1771 if (!CreateSkinnedGbufferPipeline()) { SLEAK_ERROR("GBuffer: skinned gbuffer pipeline failed!"); return false; }
1772
1773 // Now that SSAO inputs (gNormalRough, gDepth) and SSAO blur
1774 // descriptor set 0 target are available, write SSAO descriptors.
1775 UpdateSSAODescriptors();
1776 // SSR input descriptors depend on m_gbufferViews + m_hdrSceneView, both
1777 // created above — safe to write now.
1778 UpdateSSRDescriptors();
1779
1780 // Recreate forward-pass pipelines so they use m_forwardRenderPass instead
1781 // of the main renderPass (which may have different attachments when MSAA
1782 // is active, or an incompatible finalLayout).
1783 if (m_forwardRenderPass != VK_NULL_HANDLE) {
1784 if (pipeline) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; }
1785 if (skyboxPipeline) { vkDestroyPipeline(device, skyboxPipeline, nullptr); skyboxPipeline = VK_NULL_HANDLE; }
1786 if (debugLinePipeline) { vkDestroyPipeline(device, debugLinePipeline, nullptr); debugLinePipeline = VK_NULL_HANDLE; }
1787 if (skinnedPipeline) { vkDestroyPipeline(device, skinnedPipeline, nullptr); skinnedPipeline = VK_NULL_HANDLE; }
1788 if (m_skinnedGbufferPipeline) { vkDestroyPipeline(device, m_skinnedGbufferPipeline, nullptr); m_skinnedGbufferPipeline = VK_NULL_HANDLE; }
1789 DestroyCustomFormatPipelines();
1790 // Destroy old skybox descriptor pool (CreateSkyboxPipeline allocates new ones)
1791 if (skyboxDescriptorPool) {
1792 vkDestroyDescriptorPool(device, skyboxDescriptorPool, nullptr);
1793 skyboxDescriptorPool = VK_NULL_HANDLE;
1794 }
1795 skyboxDescriptorSets.clear();
1796 delete skyboxShader;
1797 skyboxShader = nullptr;
1798
1799 CreateGraphicsPipeline();
1800 CreateSkyboxPipeline();
1801 CreateDebugLinePipeline();
1802 CreateSkinnedPipeline();
1803 CreateSkinnedGbufferPipeline();
1804
1805 m_gbufferResourcesCreated = true;
1806
1807 // Re-bind skybox cubemap to the newly allocated descriptor sets
1808 if (m_skyboxCubemapView != VK_NULL_HANDLE && m_skyboxCubemapSampler != VK_NULL_HANDLE) {
1809 for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) {
1810 VkDescriptorImageInfo imageInfo{};
1811 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1812 imageInfo.imageView = m_skyboxCubemapView;
1813 imageInfo.sampler = m_skyboxCubemapSampler;
1814
1815 VkWriteDescriptorSet descriptorWrite{};
1816 descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1817 descriptorWrite.dstSet = skyboxDescriptorSets[i];
1818 descriptorWrite.dstBinding = 0;
1819 descriptorWrite.dstArrayElement = 0;
1820 descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1821 descriptorWrite.descriptorCount = 1;
1822 descriptorWrite.pImageInfo = &imageInfo;
1823
1824 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
1825 }
1826 m_skyboxDescriptorsWritten = true;
1827 }
1828 }
1829
1830 SLEAK_INFO("VulkanRenderer: Deferred GBuffer resources created ({}x{})",
1831 scExtent.width, scExtent.height);
1832 return true;
1833}
1834
1835}
1836}
int width
int height
#define SLEAK_ERROR(...)
Definition Logger.hpp:22
#define SLEAK_RETURN_ERR(...)
Definition Logger.hpp:25
#define SLEAK_INFO(...)
Definition Logger.hpp:20
#define SLEAK_WARN(...)
Definition Logger.hpp:21
T * get() const
Definition RefPtr.hpp:170
Backend-agnostic GPU buffer: vertex, index, constant, or resource view target.
static RenderCommandQueue * GetInstance()
Lazily creates and returns the process-wide singleton instance.
virtual void ConfigureRenderMode()=0
Applies the current RenderMode (fill/wireframe/points) to backend state.
virtual void ConfigureRenderFace()=0
Applies the current RenderFace (cull mode) to backend state.
void UpdateFrameMetrics()
Rolls up frame count into rate/time/triangle stats once per MetricUpdateInterval.
Definition Renderer.hpp:241
void SetPerformanceCounter(bool value)
Definition Renderer.hpp:127
static void RegisterCreateCubemapTextureFromPanorama(T *instance, Texture *(T::*method)(const std::string &))
static void RegisterCreateCubemapTexture(T *instance, Texture *(T::*method)(const std::array< std::string, 6 > &))
static void RegisterCreateBuffer(T *instance, BufferBase *(T::*method)(BufferType, uint32_t, void *))
static void RegisterCreateShader(T *instance, Shader *(T::*method)(const std::string &))
static void RegisterCreateTextureFromMemory(std::function< Texture *(const void *, uint32_t, uint32_t, TextureFormat, uint32_t)> func)
static void RegisterCreateTexture(T *instance, Texture *(T::*method)(const std::string &))
Backend-agnostic compiled shader program.
Definition Shader.hpp:11
VMA-backed Vulkan buffer with staging uploads, batched copies, and a size-bucketed recycling pool.
static void ProcessDeferredDeletions(uint32_t maxFramesInFlight)
static VkDeviceSize GetTotalAllocatedBytes()
static void UntrackAllocation(VkDeviceSize size)
static AsyncFlushResult FlushPendingCopiesAsync(VkSemaphore signalSemaphore)
static VmaAllocator GetAllocator()
static VkDeviceSize GetDeviceLocalHeapSize()
static void SetBatchingEnabled(bool enabled)
Vulkan cubemap texture, loadable from six face images or a single equirectangular panorama.
virtual size_t GetGPUMemoryBudget() const override
Returns the device-local heap size reported by the allocator.
virtual bool CreateImGUI() override
Initializes ImGui and its Vulkan backend against the active render pass.
virtual size_t GetGPUMemoryUsed() const override
Returns total bytes currently allocated by VulkanBuffer.
virtual void Draw(uint32_t vertexCount) override
Issues a non-indexed draw call and updates the vertex/triangle counters.
void ApplyShadowResolutionChange() override
Recreates the extent-dependent shadow map objects at the queued resolution.
virtual void SetViewport(float x, float y, float width, float height, float minDepth=0.0f, float maxDepth=1.0f) override
Sets the dynamic viewport on the active command buffer.
virtual void BindTexture(RefPtr< Sleak::Texture > texture, uint32_t slot=0) override
Binds a texture's descriptor set at slot 0, skipping cubemaps and the GBuffer geometry pass.
virtual void BindIndexBuffer(RefPtr< BufferBase > buffer, uint32_t slot=0) override
Binds a 32-bit index buffer.
virtual void ClearDepthStencil(bool clearDepth, bool clearStencil, float depth, uint8_t stencil) override
No-op; depth/stencil clears are driven by the render pass clear values.
virtual void DrawInstance(uint32_t instanceCount, uint32_t vertexPerInstance) override
Issues an instanced, non-indexed draw call.
virtual void FlushPendingTransfers() override
Kicks off the current frame's async buffer upload batch.
virtual void WaitIdle() override
Blocks until the device finishes all submitted GPU work.
virtual void SetRenderMode(RenderMode mode) override
Stores the polygon mode for the next pipeline rebuild (Vulkan state is baked).
virtual void BindTextureRaw(Sleak::Texture *texture, uint32_t slot=0) override
Raw-pointer variant of BindTexture.
virtual Shader * CreateShader(const std::string &shaderSource) override
Compiles a VulkanShader from source.
virtual void Cleanup() override
Tears down every Vulkan resource in reverse dependency order.
virtual void DrawIndexed(uint32_t indexCount) override
Issues an indexed draw call and updates the vertex/triangle counters.
virtual BufferBase * CreateBuffer(BufferType Type, uint32_t size, void *data) override
Allocates and initializes a VulkanBuffer.
~VulkanRenderer()
Calls Cleanup() to tear down all Vulkan resources.
void ApplyVSyncChange() override
Recreates the swapchain to apply a queued VSync toggle.
virtual void EndCustomFormatPass() override
Restores the previous pipeline and descriptor set after custom-format draws.
virtual Texture * CreateTextureFromData(uint32_t width, uint32_t height, void *data) override
Loads a texture from an in-memory RGBA8 buffer.
virtual void BindConstantBuffer(RefPtr< BufferBase > buffer, uint32_t slot=0) override
virtual void EndRender() override
Ends the active render pass, submits the command buffer, and presents.
virtual Texture * CreateTexture(const std::string &TexturePath) override
Loads a texture from disk and writes its descriptor sets.
virtual void SetRenderFace(RenderFace face) override
Stores the cull face for the next pipeline rebuild (Vulkan state is baked).
Texture * CreateCubemapTexture(const std::array< std::string, 6 > &facePaths)
Loads a cubemap from six face images and writes it into the skybox descriptor sets.
virtual void DrawIndexedInstance(uint32_t instanceCount, uint32_t indexPerInstance) override
Issues an instanced, indexed draw call.
Texture * CreateCubemapTextureFromPanorama(const std::string &panoramaPath)
Loads an equirectangular panorama as a cubemap and writes it into the skybox descriptor sets.
virtual void ClearRenderTarget(float r, float g, float b, float a) override
Stores the clear color used by the next BeginRender.
virtual void Resize(uint32_t width, uint32_t height) override
Recreates the swapchain for the new window dimensions.
virtual void BindVertexBuffer(RefPtr< BufferBase > buffer, uint32_t slot=0) override
virtual void BeginCustomFormatPass(VertexFormatHandle format) override
Binds the custom-format pipeline matching the currently active render pass.
Vulkan vertex+fragment shader pair, loaded from precompiled SPIR-V modules.
Vulkan 2D texture: image + view + sampler, with per-swapchain-image descriptor sets.
const std::vector< VkDescriptorSet > & GetDescriptorSets() const
virtual bool IsValid() const
True if the pointer is non-null.
virtual TextureType GetType() const =0
TextureFormat
Definition Texture.hpp:10
Backend-facing rendering layer shared by the four graphics backends.
RenderMode
Rasterizer fill style for a draw.
BufferType
GPU buffer usage kind, drives backend binding flags and layout.
RenderFace
Which triangle winding gets culled.
Root namespace for everything the engine exposes.
Definition Camera.hpp:10
uint32_t VertexFormatHandle