SleakEngine 0.1.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) { Draw(vertexCount, 0); }
603
604void VulkanRenderer::Draw(uint32_t vertexCount, uint32_t firstVertex) {
605 if (!bFrameStarted) return;
606 if (CustomFormatDrawsSuppressed()) return;
607 vkCmdDraw(command, vertexCount, 1, firstVertex, 0);
608 if (!m_shadowPassActive) {
609 DrawnVertices += vertexCount;
610 DrawnTriangles += vertexCount / 3;
611 }
612}
613
614/// Issues an indexed draw call and updates the vertex/triangle counters.
615void VulkanRenderer::DrawIndexed(uint32_t indexCount) {
616 DrawIndexed(indexCount, 0, 0);
617}
618
619void VulkanRenderer::DrawIndexed(uint32_t indexCount, uint32_t firstIndex,
620 int32_t baseVertex) {
621 if (!bFrameStarted) return;
622 if (CustomFormatDrawsSuppressed()) return;
623 vkCmdDrawIndexed(command, indexCount, 1, firstIndex, baseVertex, 0);
624 if (!m_shadowPassActive) {
625 DrawnVertices += indexCount;
626 DrawnTriangles += indexCount / 3;
627 }
628}
629
630/// Issues an instanced, non-indexed draw call.
631void VulkanRenderer::DrawInstance(uint32_t instanceCount,
632 uint32_t vertexPerInstance) {
633 if (!bFrameStarted) return;
634 if (CustomFormatDrawsSuppressed()) return;
635 vkCmdDraw(command, vertexPerInstance, instanceCount, 0, 0);
636}
637
638/// Issues an instanced, indexed draw call.
639void VulkanRenderer::DrawIndexedInstance(uint32_t instanceCount,
640 uint32_t indexPerInstance) {
641 DrawIndexedInstance(instanceCount, indexPerInstance, 0, 0);
642}
643
644void VulkanRenderer::DrawIndexedInstance(uint32_t instanceCount,
645 uint32_t indexPerInstance,
646 uint32_t firstIndex,
647 int32_t baseVertex) {
648 if (!bFrameStarted) return;
649 if (CustomFormatDrawsSuppressed()) return;
650 vkCmdDrawIndexed(command, indexPerInstance, instanceCount, firstIndex,
651 baseVertex, 0);
652}
653
654/// Stores the cull face for the next pipeline rebuild (Vulkan state is baked).
656 // Vulkan pipeline state is baked, needs pipeline recreation.
657 // Store for next pipeline rebuild.
658 Face = face;
659}
660
661/// Stores the polygon mode for the next pipeline rebuild (Vulkan state is baked).
663 // Vulkan pipeline state is baked, needs pipeline recreation.
664 Mode = mode;
665}
666
667/// Sets the dynamic viewport on the active command buffer.
668void VulkanRenderer::SetViewport(float x, float y, float width,
669 float height, float minDepth,
670 float maxDepth) {
671 if (!bFrameStarted) return;
672 VkViewport viewport{};
673 viewport.x = x;
674 viewport.y = y;
675 viewport.width = width;
676 viewport.height = height;
677 viewport.minDepth = minDepth;
678 viewport.maxDepth = maxDepth;
679 vkCmdSetViewport(command, 0, 1, &viewport);
680}
681
682/// Stores the clear color used by the next BeginRender.
683void VulkanRenderer::ClearRenderTarget(float r, float g, float b,
684 float a) {
685 clearColor = {{r, g, b, a}};
686}
687
688/// No-op; depth/stencil clears are driven by the render pass clear values.
689void VulkanRenderer::ClearDepthStencil(bool clearDepth, bool clearStencil,
690 float depth, uint8_t stencil) {
691 // Handled by render pass clear values
692}
693
694/// Binds a vertex buffer slot, switching to the pipeline that matches the
695/// buffer's registered vertex format.
697 uint32_t slot) {
698 if (!bFrameStarted) return;
699 auto* vkBuf = static_cast<VulkanBuffer*>(buffer.get());
700 if (!vkBuf) return;
701
702 VertexFormatHandle wantFormat = buffer->GetVertexFormat();
703 if (wantFormat != 0) {
704 BeginCustomFormatPass(wantFormat);
705 } else if (m_activeCustomFormat != 0) {
707 }
708
709 VkBuffer buffers[] = {vkBuf->GetVkBuffer()};
710 VkDeviceSize offsets[] = {0};
711 vkCmdBindVertexBuffers(command, slot, 1, buffers, offsets);
712}
713
714/// Binds a 32-bit index buffer.
716 uint32_t slot) {
717 if (!bFrameStarted) return;
718 auto* vkBuf = static_cast<VulkanBuffer*>(buffer.get());
719 if (!vkBuf) return;
720 vkCmdBindIndexBuffer(command, vkBuf->GetVkBuffer(), 0,
721 VK_INDEX_TYPE_UINT32);
722}
723
724/// Pushes constant-buffer data via push constants, applying TAA jitter or
725/// the shadow push-constant cache as needed.
727 uint32_t slot) {
728 if (!bFrameStarted) return;
729 auto* vkBuf = static_cast<VulkanBuffer*>(buffer.get());
730 if (!vkBuf) return;
731
732 // Use push constants — recorded into the command buffer per draw call
733 void* data = vkBuf->GetData();
734 if (!data) return;
735
736 uint32_t size = static_cast<uint32_t>(vkBuf->GetSize());
737 if (size > 128) size = 128; // Vulkan guarantees at least 128 bytes
738
739 // Choose the pipeline layout that owns the currently bound pipeline.
740 // GBuffer geometry pass uses m_gbufferGeomLayout; all other passes use pipelineLay.
741 VkPipelineLayout activeLayout = (m_inGeometryPass && m_gbufferGeomLayout != VK_NULL_HANDLE)
742 ? m_gbufferGeomLayout : pipelineLay;
743
744 // In the geometry pass (not shadow), apply TAA sub-pixel jitter to WVP.
745 // Sub-pixel jitter: add jx*col3 to col0 and jy*col3 to col1.
746 // Y is negated because the geometry shader flips gl_Position.y.
747 if (m_inGeometryPass && !m_shadowPassActive && m_taaEnabled &&
748 (m_taaJitter[0] != 0.0f || m_taaJitter[1] != 0.0f) && size >= 64) {
749 float jdata[32];
750 memcpy(jdata, data, size);
751 const float jx = m_taaJitter[0] * 2.0f; // UV → NDC
752 const float jy = -m_taaJitter[1] * 2.0f; // negate for Y-flip
753 // GLSL computes WVP_cpu^T * v, so clip.x is dot(col0, v).
754 // Adding jx*col3 to col0 adds jx*clip.w to clip.x → uniform NDC shift.
755 for (int r = 0; r < 4; ++r) {
756 jdata[r * 4 + 0] += jx * jdata[r * 4 + 3]; // col0 += jx * col3
757 jdata[r * 4 + 1] += jy * jdata[r * 4 + 3]; // col1 += jy * col3
758 }
759 vkCmdPushConstants(command, activeLayout,
760 VK_SHADER_STAGE_VERTEX_BIT, 0, size, jdata);
761 return;
762 }
763
764 if (m_shadowPassActive && slot == 0 && size >= 128) {
765 // Shadow mode: push [LightVP*World (64)][World (64)].
766 // Buffer layout: [WVP (64 bytes)][World (64 bytes)].
767 // LightVP is frame-constant, so memoize on World and reuse the result
768 // across the many draws that share the identity transform.
769 const float* srcWorld = reinterpret_cast<const float*>(
770 static_cast<const char*>(data) + 64);
771
772 if (!m_shadowPCCacheValid ||
773 memcmp(srcWorld, m_shadowWorldCache, 64) != 0) {
774 // shadowWVP = World * LightVP (row-major)
775 for (int r = 0; r < 4; ++r) {
776 for (int c = 0; c < 4; ++c) {
777 float sum = 0.0f;
778 for (int k = 0; k < 4; ++k) {
779 sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c];
780 }
781 m_shadowPCCache[r * 4 + c] = sum;
782 }
783 }
784 memcpy(&m_shadowPCCache[16], srcWorld, 64);
785 memcpy(m_shadowWorldCache, srcWorld, 64);
786 m_shadowPCCacheValid = true;
787 }
788
789 vkCmdPushConstants(command, activeLayout,
790 VK_SHADER_STAGE_VERTEX_BIT, 0, 128, m_shadowPCCache);
791 } else {
792 vkCmdPushConstants(command, activeLayout,
793 VK_SHADER_STAGE_VERTEX_BIT, 0, size, data);
794 }
795}
796
797/// Allocates and initializes a VulkanBuffer.
799 void* data) {
800 auto* buffer = new VulkanBuffer(device, physicalDevice, size, type,
801 commands, graphicsQueue);
802 if (!buffer->Initialize(data)) {
803 delete buffer;
804 return nullptr;
805 }
806 return buffer;
807}
808
809/// Compiles a VulkanShader from source.
810Shader* VulkanRenderer::CreateShader(const std::string& shaderSource) {
811 auto* shader = new VulkanShader(device);
812 if (shader->compile(shaderSource)) {
813 return shader;
814 }
815 delete shader;
816 return nullptr;
817}
818
819/// Loads a texture from disk and writes its descriptor sets.
820::Sleak::Texture* VulkanRenderer::CreateTexture(const std::string& TexturePath) {
821 auto* texture = new VulkanTexture(device, physicalDevice, commands,
822 graphicsQueue);
823 if (texture->LoadFromFile(TexturePath)) {
824 WriteTextureDescriptors(texture);
825 return texture;
826 }
827 delete texture;
828 return nullptr;
829}
830
831/// Loads a texture from an in-memory RGBA8 buffer.
833 uint32_t height,
834 void* data) {
835 auto* texture = new VulkanTexture(device, physicalDevice, commands,
836 graphicsQueue);
837 if (texture->LoadFromMemory(data, width, height, TextureFormat::RGBA8)) {
838 return texture;
839 }
840 delete texture;
841 return nullptr;
842}
843
844/// Loads a cubemap from six face images and writes it into the skybox
845/// descriptor sets.
847 const std::array<std::string, 6>& facePaths) {
848 auto* texture = new VulkanCubemapTexture(device, physicalDevice,
849 commands, graphicsQueue);
850 if (texture->LoadCubemap(facePaths)) {
851 // Create skybox pipeline if not already created
852 if (skyboxPipeline == VK_NULL_HANDLE) {
853 if (!CreateSkyboxPipeline()) {
854 SLEAK_ERROR("VulkanRenderer: Failed to create skybox pipeline");
855 delete texture;
856 return nullptr;
857 }
858 }
859
860 // Write cubemap to skybox descriptor sets
861 m_skyboxCubemapView = texture->GetImageView();
862 m_skyboxCubemapSampler = texture->GetSampler();
863 for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) {
864 VkDescriptorImageInfo imageInfo{};
865 imageInfo.imageLayout =
866 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
867 imageInfo.imageView = m_skyboxCubemapView;
868 imageInfo.sampler = m_skyboxCubemapSampler;
869
870 VkWriteDescriptorSet descriptorWrite{};
871 descriptorWrite.sType =
872 VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
873 descriptorWrite.dstSet = skyboxDescriptorSets[i];
874 descriptorWrite.dstBinding = 0;
875 descriptorWrite.dstArrayElement = 0;
876 descriptorWrite.descriptorType =
877 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
878 descriptorWrite.descriptorCount = 1;
879 descriptorWrite.pImageInfo = &imageInfo;
880
881 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0,
882 nullptr);
883 }
884 m_skyboxDescriptorsWritten = true;
885
886 return texture;
887 }
888 delete texture;
889 return nullptr;
890}
891
892/// Loads an equirectangular panorama as a cubemap and writes it into the
893/// skybox descriptor sets.
895 const std::string& panoramaPath) {
896 auto* texture = new VulkanCubemapTexture(device, physicalDevice,
897 commands, graphicsQueue);
898 if (texture->LoadEquirectangular(panoramaPath)) {
899 // Create skybox pipeline if not already created
900 if (skyboxPipeline == VK_NULL_HANDLE) {
901 if (!CreateSkyboxPipeline()) {
902 SLEAK_ERROR("VulkanRenderer: Failed to create skybox pipeline");
903 delete texture;
904 return nullptr;
905 }
906 }
907
908 // Write cubemap to skybox descriptor sets
909 m_skyboxCubemapView = texture->GetImageView();
910 m_skyboxCubemapSampler = texture->GetSampler();
911 for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) {
912 VkDescriptorImageInfo imageInfo{};
913 imageInfo.imageLayout =
914 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
915 imageInfo.imageView = m_skyboxCubemapView;
916 imageInfo.sampler = m_skyboxCubemapSampler;
917
918 VkWriteDescriptorSet descriptorWrite{};
919 descriptorWrite.sType =
920 VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
921 descriptorWrite.dstSet = skyboxDescriptorSets[i];
922 descriptorWrite.dstBinding = 0;
923 descriptorWrite.dstArrayElement = 0;
924 descriptorWrite.descriptorType =
925 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
926 descriptorWrite.descriptorCount = 1;
927 descriptorWrite.pImageInfo = &imageInfo;
928
929 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0,
930 nullptr);
931 }
932 m_skyboxDescriptorsWritten = true;
933
934 // Trigger IBL precompute now that we have an environment cubemap
935 return texture;
936 }
937 delete texture;
938 return nullptr;
939}
940
941/// Binds a texture's descriptor set at slot 0, skipping cubemaps and the
942/// GBuffer geometry pass.
944 uint32_t slot) {
945 if (!bFrameStarted) return;
946 if (!texture.IsValid() || slot != 0)
947 return;
948
949 // Cubemap textures are bound via skybox pass, skip here
950 if (texture->GetType() == TextureType::TextureCube)
951 return;
952
953 auto* vkTex = static_cast<VulkanTexture*>(texture.get());
954 if (!vkTex || !vkTex->HasDescriptorSets())
955 return;
956
957 const auto& sets = vkTex->GetDescriptorSets();
958 if (CurrentFrameIndex < sets.size()) {
959 // In the GBuffer geometry pass set 0 is the PBR material descriptor set
960 // (m_pbrMaterialDSL, bound by BindPBRMaterial via m_gbufferGeomLayout).
961 // Binding a forward single-sampler descriptor set here with the wrong
962 // layout would corrupt set 0 and trigger VK_ERROR_DEVICE_LOST.
963 // BindPBRMaterial owns set 0 during the geometry pass — skip here.
964 if (m_inGeometryPass)
965 return;
966 vkCmdBindDescriptorSets(
967 command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 0, 1,
968 &sets[CurrentFrameIndex], 0, nullptr);
969 }
970}
971
972/// Raw-pointer variant of BindTexture.
973void VulkanRenderer::BindTextureRaw(Sleak::Texture* texture, uint32_t slot) {
974 if (!bFrameStarted) return;
975 if (!texture || slot != 0)
976 return;
977
978 // Cubemap textures are bound via skybox pass, skip here
979 if (texture->GetType() == TextureType::TextureCube)
980 return;
981
982 auto* vkTex = static_cast<VulkanTexture*>(texture);
983 if (!vkTex || !vkTex->HasDescriptorSets())
984 return;
985
986 const auto& sets = vkTex->GetDescriptorSets();
987 if (CurrentFrameIndex < sets.size()) {
988 // In the GBuffer geometry pass set 0 is the PBR material descriptor set
989 // (m_pbrMaterialDSL, bound by BindPBRMaterial via m_gbufferGeomLayout).
990 // Binding a forward single-sampler descriptor set here with the wrong
991 // layout would corrupt set 0 and trigger VK_ERROR_DEVICE_LOST.
992 // BindPBRMaterial owns set 0 during the geometry pass — skip here.
993 if (m_inGeometryPass)
994 return;
995 vkCmdBindDescriptorSets(
996 command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 0, 1,
997 &sets[CurrentFrameIndex], 0, nullptr);
998 }
999}
1000
1001/// Allocates one primary command buffer per frame in flight.
1002bool VulkanRenderer::CreateCommandBuffer() {
1003 commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
1004
1005 VkCommandBufferAllocateInfo allocInfo{};
1006 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
1007 allocInfo.commandPool = commands;
1008 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
1009 allocInfo.commandBufferCount = MAX_FRAMES_IN_FLIGHT;
1010
1011 if (vkAllocateCommandBuffers(device, &allocInfo,
1012 commandBuffers.data()) != VK_SUCCESS)
1013 SLEAK_RETURN_ERR("Failed to allocate command buffers!");
1014
1015 return true;
1016}
1017
1018/// Blocks until the device finishes all submitted GPU work.
1020 if (device) vkDeviceWaitIdle(device);
1021}
1022
1023/// Kicks off the current frame's async buffer upload batch.
1025 m_asyncFlush[currentFrame] = VulkanBuffer::FlushPendingCopiesAsync(
1026 m_transferSemaphores[currentFrame]);
1027}
1028
1029/// Returns total bytes currently allocated by VulkanBuffer.
1031 return static_cast<size_t>(VulkanBuffer::GetTotalAllocatedBytes());
1032}
1033
1034/// Returns the device-local heap size reported by the allocator.
1036 return static_cast<size_t>(VulkanBuffer::GetDeviceLocalHeapSize());
1037}
1038
1039/// Tears down every Vulkan resource in reverse dependency order.
1041 SLEAK_INFO("Cleaning Vulkan...");
1042
1043 bRender = false;
1044
1045 // Wait for the device to finish all work
1046 if (device) {
1047 vkDeviceWaitIdle(device);
1048 }
1049
1051
1052 // Flush all deferred buffer deletions now that GPU is idle
1054
1055 // Shutdown ImGUI before destroying Vulkan resources
1056 if (bImInitialized) {
1057 ImGui_ImplVulkan_Shutdown();
1058 ImGui_ImplSDL3_Shutdown();
1059 ImGui::DestroyContext();
1060 bImInitialized = false;
1061 }
1062 if (imguiDescriptorPool) {
1063 vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr);
1064 imguiDescriptorPool = VK_NULL_HANDLE;
1065 }
1066
1067 // Destroy descriptor pool (frees descriptor sets too)
1068 if (descriptorPool) {
1069 vkDestroyDescriptorPool(device, descriptorPool, nullptr);
1070 descriptorPool = VK_NULL_HANDLE;
1071 }
1072 descriptorSets.clear();
1073
1074 // Destroy skybox resources
1075 if (skyboxPipeline) {
1076 vkDestroyPipeline(device, skyboxPipeline, nullptr);
1077 skyboxPipeline = VK_NULL_HANDLE;
1078 }
1079 if (skyboxDescriptorPool) {
1080 vkDestroyDescriptorPool(device, skyboxDescriptorPool, nullptr);
1081 skyboxDescriptorPool = VK_NULL_HANDLE;
1082 }
1083 skyboxDescriptorSets.clear();
1084 delete skyboxShader;
1085 skyboxShader = nullptr;
1086
1087 // Destroy skinned pipeline resources
1088 if (skinnedPipeline) {
1089 vkDestroyPipeline(device, skinnedPipeline, nullptr);
1090 skinnedPipeline = VK_NULL_HANDLE;
1091 }
1092 delete skinnedShader;
1093 skinnedShader = nullptr;
1094
1095 // Destroy debug line pipeline resources
1096 if (debugLinePipeline) {
1097 vkDestroyPipeline(device, debugLinePipeline, nullptr);
1098 debugLinePipeline = VK_NULL_HANDLE;
1099 }
1100 delete debugLineShader;
1101 debugLineShader = nullptr;
1102
1103 // Destroy custom vertex format pipelines
1104 DestroyCustomFormatPipelines();
1105
1106 // Destroy MSAA color resources
1107 CleanupMSAAColorResources();
1108
1109 // Destroy deferred GBuffer resources
1110 CleanupGBufferResources();
1111
1112 // Destroy shadow mapping resources
1113 CleanupShadowResources();
1114
1115 // Backstop: free any shader modules whose resource-guarded cleanup was
1116 // skipped (guard false while shader non-null). Cleanups null after delete,
1117 // so these are no-ops when already freed — delete(nullptr) is safe.
1118 delete m_gbufferShader; m_gbufferShader = nullptr;
1119 delete m_lightingShader; m_lightingShader = nullptr;
1120 delete m_ssaoShader; m_ssaoShader = nullptr;
1121 delete m_ssaoBlurShader; m_ssaoBlurShader = nullptr;
1122 delete m_ssrShader; m_ssrShader = nullptr;
1123 delete m_taaShader; m_taaShader = nullptr;
1124 delete m_bloomThresholdShader; m_bloomThresholdShader = nullptr;
1125 delete m_bloomDownsampleShader; m_bloomDownsampleShader = nullptr;
1126 delete m_bloomUpsampleShader; m_bloomUpsampleShader = nullptr;
1127 delete m_bloomCompositeShader; m_bloomCompositeShader = nullptr;
1128
1129 // Destroy bone UBO resources
1130 CleanupBoneUBOResources();
1131
1132 // Destroy descriptor set layouts
1133 if (m_shadowSamplerDescriptorSetLayout) {
1134 vkDestroyDescriptorSetLayout(device, m_shadowSamplerDescriptorSetLayout, nullptr);
1135 m_shadowSamplerDescriptorSetLayout = VK_NULL_HANDLE;
1136 }
1137 if (m_lightUBODescriptorSetLayout) {
1138 vkDestroyDescriptorSetLayout(device, m_lightUBODescriptorSetLayout, nullptr);
1139 m_lightUBODescriptorSetLayout = VK_NULL_HANDLE;
1140 }
1141 if (boneDescriptorSetLayout) {
1142 vkDestroyDescriptorSetLayout(device, boneDescriptorSetLayout, nullptr);
1143 boneDescriptorSetLayout = VK_NULL_HANDLE;
1144 }
1145 if (descriptorSetLayout) {
1146 vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
1147 descriptorSetLayout = VK_NULL_HANDLE;
1148 }
1149
1150 // Clean up depth resources
1151 CleanupDepthResources();
1152
1153 // Destroy swapchain
1154 if (swapChain && device) {
1155 vkDestroySwapchainKHR(device, swapChain, nullptr);
1156 swapChain = VK_NULL_HANDLE;
1157 }
1158
1159 // Destroy framebuffers
1160 for (auto& buffer : swapChainFramebuffers) {
1161 if (buffer) {
1162 vkDestroyFramebuffer(device, buffer, nullptr);
1163 buffer = VK_NULL_HANDLE;
1164 }
1165 }
1166 swapChainFramebuffers.clear();
1167
1168 // Destroy image views
1169 for (auto& imgView : swapChainImageViews) {
1170 if (imgView) {
1171 vkDestroyImageView(device, imgView, nullptr);
1172 imgView = VK_NULL_HANDLE;
1173 }
1174 }
1175 swapChainImageViews.clear();
1176
1177 // Destroy sync objects
1178 for (auto& sem : imageAvailableSemaphores) {
1179 if (sem) vkDestroySemaphore(device, sem, nullptr);
1180 }
1181 imageAvailableSemaphores.clear();
1182
1183 for (auto& sem : renderFinishedSemaphores) {
1184 if (sem) vkDestroySemaphore(device, sem, nullptr);
1185 }
1186 renderFinishedSemaphores.clear();
1187
1188 for (auto& fence : inFlightFences) {
1189 if (fence) vkDestroyFence(device, fence, nullptr);
1190 }
1191 inFlightFences.clear();
1192
1193 // Destroy transfer semaphores and free any pending staging buffers
1194 for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
1195 if (m_transferSemaphores[i]) {
1196 vkDestroySemaphore(device, m_transferSemaphores[i], nullptr);
1197 m_transferSemaphores[i] = VK_NULL_HANDLE;
1198 }
1199 auto& af = m_asyncFlush[i];
1200 if (af.commandBuffer != VK_NULL_HANDLE) {
1201 vkFreeCommandBuffers(af.device, af.commandPool, 1,
1202 &af.commandBuffer);
1203 }
1204 for (auto& pending : af.stagingBuffers) {
1205 vmaDestroyBuffer(VulkanBuffer::GetAllocator(), pending.buffer,
1206 pending.memory);
1207 VulkanBuffer::UntrackAllocation(pending.allocSize);
1208 }
1209 af = {};
1210 }
1211
1212 // Destroy shader
1213 if (simpleShader) {
1214 delete simpleShader;
1215 simpleShader = nullptr;
1216 }
1217
1218 // Destroy pipeline
1219 if (pipeline) {
1220 vkDestroyPipeline(device, pipeline, nullptr);
1221 pipeline = VK_NULL_HANDLE;
1222 }
1223
1224 // Destroy pipeline layout
1225 if (pipelineLay) {
1226 vkDestroyPipelineLayout(device, pipelineLay, nullptr);
1227 pipelineLay = VK_NULL_HANDLE;
1228 }
1229
1230 // Destroy render pass
1231 if (renderPass) {
1232 vkDestroyRenderPass(device, renderPass, nullptr);
1233 renderPass = VK_NULL_HANDLE;
1234 }
1235
1236 // Destroy default texture
1237 if (m_defaultTexture) {
1238 delete m_defaultTexture;
1239 m_defaultTexture = nullptr;
1240 }
1241
1242 // Destroy command pool (this will also free command buffers)
1243 if (commands) {
1244 vkDestroyCommandPool(device, commands, nullptr);
1245 commands = VK_NULL_HANDLE;
1246 }
1247
1248 // Destroy surface after swapchain is gone
1249 if (surface && instance) {
1250 vkDestroySurfaceKHR(instance, surface, nullptr);
1251 surface = VK_NULL_HANDLE;
1252 }
1253
1254 // Destroy debug messenger
1255 if (debugMessenger && vkDestroyDebugUtilsMessengerEXT) {
1256 vkDestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
1257 debugMessenger = VK_NULL_HANDLE;
1258 }
1259
1260 // Drain any buffers freed during teardown, then destroy the VMA allocator
1261 // (it must outlive every vmaDestroyBuffer, and both precede vkDestroyDevice).
1264
1265 // Destroy logical device
1266 if (device) {
1267 vkDestroyDevice(device, nullptr);
1268 device = VK_NULL_HANDLE;
1269 }
1270
1271 // Destroy Vulkan instance
1272 if (instance) {
1273 vkDestroyInstance(instance, nullptr);
1274 instance = VK_NULL_HANDLE;
1275 }
1276}
1277
1278/// Recreates the swapchain for the new window dimensions.
1279void VulkanRenderer::Resize(uint32_t width, uint32_t height) {
1280 if (device) {
1281 RecreateSwapChain();
1282 }
1283}
1284
1285/// Rebuilds the swapchain and its dependents (image views, depth, MSAA,
1286/// framebuffers, GBuffer) after a resize or resolution change.
1287bool VulkanRenderer::RecreateSwapChain() {
1288 vkDeviceWaitIdle(device);
1289
1290 // Cleanup GBuffer BEFORE CleanupSwapChain (which destroys depth image)
1291 // to avoid dangling image view references in GBuffer framebuffer
1292 bool hadGBuffer = m_gbufferResourcesCreated;
1293 CleanupGBufferResources();
1294
1295 CleanupMSAAColorResources();
1296 CleanupSwapChain();
1297
1298 if (!CreateSwapChain()) {
1299 SLEAK_ERROR("Failed to recreate swap chain!");
1300 return false;
1301 }
1302 if (!CreateImageViews()) {
1303 SLEAK_ERROR("Failed to recreate image views!");
1304 return false;
1305 }
1306 if (!CreateDepthResources()) {
1307 SLEAK_ERROR("Failed to recreate depth resources!");
1308 return false;
1309 }
1310 if (!CreateMSAAColorResources()) {
1311 SLEAK_ERROR("Failed to recreate MSAA color resources!");
1312 return false;
1313 }
1314 if (!CreateFrameBuffer()) {
1315 SLEAK_ERROR("Failed to recreate framebuffers!");
1316 return false;
1317 }
1318
1319 // Resize imagesInFlight in case swapchain image count changed
1320 imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE);
1321
1322 // Recreate GBuffer resources if they were previously created
1323 if (hadGBuffer && m_deferredEnabled) {
1324 if (!CreateGBufferResources())
1325 SLEAK_WARN("RecreateSwapChain: Failed to recreate GBuffer resources!");
1326 }
1327
1328 return true;
1329}
1330
1331/// Rebuilds the swapchain-dependent pipelines and render pass for a queued
1332/// MSAA sample count change.
1335 return;
1336 m_msaaChangeRequested = false;
1337
1338 uint32_t newCount = m_pendingMsaaSampleCount;
1339 m_msaaSampleCount = newCount;
1340
1341 // Convert to Vulkan enum
1342 switch (newCount) {
1343 case 1: m_msaaSamples = VK_SAMPLE_COUNT_1_BIT; break;
1344 case 2: m_msaaSamples = VK_SAMPLE_COUNT_2_BIT; break;
1345 case 4: m_msaaSamples = VK_SAMPLE_COUNT_4_BIT; break;
1346 case 8: m_msaaSamples = VK_SAMPLE_COUNT_8_BIT; break;
1347 default: m_msaaSamples = VK_SAMPLE_COUNT_1_BIT; break;
1348 }
1349
1350 SLEAK_INFO("Applying MSAA change: {}x", newCount);
1351
1352 vkDeviceWaitIdle(device);
1353
1354 // Destroy render pass
1355 if (renderPass) {
1356 vkDestroyRenderPass(device, renderPass, nullptr);
1357 renderPass = VK_NULL_HANDLE;
1358 }
1359
1360 // Destroy main-pass pipelines (NOT shadow pipeline)
1361 if (pipeline) {
1362 vkDestroyPipeline(device, pipeline, nullptr);
1363 pipeline = VK_NULL_HANDLE;
1364 }
1365 if (skyboxPipeline) {
1366 vkDestroyPipeline(device, skyboxPipeline, nullptr);
1367 skyboxPipeline = VK_NULL_HANDLE;
1368 }
1369 if (skinnedPipeline) {
1370 vkDestroyPipeline(device, skinnedPipeline, nullptr);
1371 skinnedPipeline = VK_NULL_HANDLE;
1372 }
1373 if (debugLinePipeline) {
1374 vkDestroyPipeline(device, debugLinePipeline, nullptr);
1375 debugLinePipeline = VK_NULL_HANDLE;
1376 }
1377 // Custom-format variants are built against the render passes torn down
1378 // here; drop them so the next draw rebuilds against the new ones.
1379 DestroyCustomFormatPipelines();
1380
1381 // Cleanup GBuffer BEFORE swapchain/depth (avoids dangling image view refs)
1382 CleanupGBufferResources();
1383
1384 // Shutdown ImGUI
1385 if (bImInitialized) {
1386 ImGui_ImplVulkan_Shutdown();
1387 ImGui_ImplSDL3_Shutdown();
1388 ImGui::DestroyContext();
1389 bImInitialized = false;
1390 }
1391
1392 // Cleanup swapchain-related resources
1393 CleanupMSAAColorResources();
1394 CleanupSwapChain();
1395
1396 // Recreate everything
1397 CreateSwapChain();
1398 CreateImageViews();
1399 CreateDepthResources();
1400 CreateMSAAColorResources();
1401 CreateRenderPass();
1402 CreateFrameBuffer();
1403 CreateGraphicsPipeline();
1404 CreateSkyboxPipeline();
1405 CreateSkinnedPipeline();
1406 CreateDebugLinePipeline();
1407 if (m_deferredEnabled) CreateGBufferResources();
1408 CreateImGUI();
1409
1410 // Re-bind skybox cubemap texture to the new descriptor sets
1411 if (m_skyboxCubemapView != VK_NULL_HANDLE && m_skyboxCubemapSampler != VK_NULL_HANDLE) {
1412 for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) {
1413 VkDescriptorImageInfo imageInfo{};
1414 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1415 imageInfo.imageView = m_skyboxCubemapView;
1416 imageInfo.sampler = m_skyboxCubemapSampler;
1417
1418 VkWriteDescriptorSet descriptorWrite{};
1419 descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1420 descriptorWrite.dstSet = skyboxDescriptorSets[i];
1421 descriptorWrite.dstBinding = 0;
1422 descriptorWrite.dstArrayElement = 0;
1423 descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1424 descriptorWrite.descriptorCount = 1;
1425 descriptorWrite.pImageInfo = &imageInfo;
1426
1427 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
1428 }
1429 m_skyboxDescriptorsWritten = true;
1430 }
1431
1432 SLEAK_INFO("MSAA change applied successfully");
1433}
1434
1435/// Recreates the swapchain to apply a queued VSync toggle.
1438 return;
1439 m_vsyncChangeRequested = false;
1440 RecreateSwapChain();
1441 SLEAK_INFO("VSync {}", m_vsync ? "enabled" : "disabled");
1442}
1443
1444/// No-op; Vulkan polygon mode changes require pipeline recreation.
1446 // Pipeline recreation needed for Vulkan polygon mode changes
1447}
1448
1449/// No-op; Vulkan cull mode changes require pipeline recreation.
1451 // Pipeline recreation needed for Vulkan cull mode changes
1452}
1453
1454/// Creates the graphics command pool.
1455bool VulkanRenderer::CreateCommandPool() {
1456 VkCommandPoolCreateInfo poolInfo{};
1457 poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
1458 poolInfo.queueFamilyIndex = QueueIDs.GraphicsIndex;
1459 poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
1460
1461 if (vkCreateCommandPool(device, &poolInfo, nullptr, &commands) !=
1462 VK_SUCCESS)
1463 SLEAK_RETURN_ERR("Failed to create command pool!");
1464
1465 return true;
1466}
1467
1468/// Creates the per-swapchain-image semaphores and per-frame fences and
1469/// transfer semaphores.
1470bool VulkanRenderer::CreateSyncObjects() {
1471 uint32_t imageCount = static_cast<uint32_t>(swapChainImages.size());
1472
1473 // Semaphores sized to swapchain image count to prevent reuse
1474 // while the presentation engine still holds a reference.
1475 imageAvailableSemaphores.resize(imageCount);
1476 renderFinishedSemaphores.resize(imageCount);
1477 inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
1478 imagesInFlight.resize(imageCount, VK_NULL_HANDLE);
1479
1480 VkSemaphoreCreateInfo semaphoreInfo{};
1481 semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
1482
1483 VkFenceCreateInfo fenceInfo{};
1484 fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
1485 fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
1486
1487 // Create per-swapchain-image semaphores
1488 for (uint32_t i = 0; i < imageCount; i++) {
1489 if (vkCreateSemaphore(device, &semaphoreInfo, nullptr,
1490 &imageAvailableSemaphores[i]) != VK_SUCCESS ||
1491 vkCreateSemaphore(device, &semaphoreInfo, nullptr,
1492 &renderFinishedSemaphores[i]) != VK_SUCCESS) {
1493 SLEAK_RETURN_ERR("Failed to create synchronization objects!");
1494 }
1495 }
1496
1497 // Create per-frame-in-flight fences and transfer semaphores
1498 for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
1499 if (vkCreateSemaphore(device, &semaphoreInfo, nullptr,
1500 &m_transferSemaphores[i]) != VK_SUCCESS ||
1501 vkCreateFence(device, &fenceInfo, nullptr,
1502 &inFlightFences[i]) != VK_SUCCESS) {
1503 SLEAK_RETURN_ERR("Failed to create synchronization objects!");
1504 }
1505 m_asyncFlush[i] = {};
1506 }
1507
1508 m_semaphoreIndex = 0;
1509 return true;
1510}
1511
1512/// Recreates the extent-dependent shadow map objects (image, view,
1513/// framebuffer) at the queued resolution. Keep in sync with
1514/// CreateShadowResources; samplers, render pass, and pipeline are extent-independent.
1516 if (!m_shadowResChangeRequested) return;
1518 if (!m_shadowResourcesCreated) return;
1520
1521 vkDeviceWaitIdle(device);
1522
1523 vkDestroyFramebuffer(device, m_shadowFramebuffer, nullptr);
1524 m_shadowFramebuffer = VK_NULL_HANDLE;
1525 vkDestroyImageView(device, m_shadowImageView, nullptr);
1526 m_shadowImageView = VK_NULL_HANDLE;
1527 vkDestroyImage(device, m_shadowImage, nullptr);
1528 m_shadowImage = VK_NULL_HANDLE;
1529 vkFreeMemory(device, m_shadowImageMemory, nullptr);
1530 m_shadowImageMemory = VK_NULL_HANDLE;
1531
1533
1534 VkImageCreateInfo imageInfo{};
1535 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1536 imageInfo.imageType = VK_IMAGE_TYPE_2D;
1537 imageInfo.extent = {m_shadowMapResolution, m_shadowMapResolution, 1};
1538 imageInfo.mipLevels = 1;
1539 imageInfo.arrayLayers = 1;
1540 imageInfo.format = VK_FORMAT_D32_SFLOAT;
1541 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1542 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1543 imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT |
1544 VK_IMAGE_USAGE_SAMPLED_BIT;
1545 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1546 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1547
1548 if (vkCreateImage(device, &imageInfo, nullptr, &m_shadowImage) !=
1549 VK_SUCCESS) {
1550 SLEAK_ERROR("Shadow resolution change: image creation failed");
1552 return;
1553 }
1554
1555 VkMemoryRequirements memReqs;
1556 vkGetImageMemoryRequirements(device, m_shadowImage, &memReqs);
1557
1558 VkMemoryAllocateInfo allocInfo{};
1559 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1560 allocInfo.allocationSize = memReqs.size;
1561 allocInfo.memoryTypeIndex = FindMemoryType(
1562 memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
1563
1564 if (vkAllocateMemory(device, &allocInfo, nullptr, &m_shadowImageMemory) !=
1565 VK_SUCCESS) {
1566 SLEAK_ERROR("Shadow resolution change: memory allocation failed");
1568 return;
1569 }
1570 vkBindImageMemory(device, m_shadowImage, m_shadowImageMemory, 0);
1571
1572 VkImageViewCreateInfo viewInfo{};
1573 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1574 viewInfo.image = m_shadowImage;
1575 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1576 viewInfo.format = VK_FORMAT_D32_SFLOAT;
1577 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
1578 viewInfo.subresourceRange.baseMipLevel = 0;
1579 viewInfo.subresourceRange.levelCount = 1;
1580 viewInfo.subresourceRange.baseArrayLayer = 0;
1581 viewInfo.subresourceRange.layerCount = 1;
1582
1583 if (vkCreateImageView(device, &viewInfo, nullptr, &m_shadowImageView) !=
1584 VK_SUCCESS) {
1585 SLEAK_ERROR("Shadow resolution change: image view creation failed");
1587 return;
1588 }
1589
1590 VkFramebufferCreateInfo fbInfo{};
1591 fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
1592 fbInfo.renderPass = m_shadowRenderPass;
1593 fbInfo.attachmentCount = 1;
1594 fbInfo.pAttachments = &m_shadowImageView;
1595 fbInfo.width = m_shadowMapResolution;
1596 fbInfo.height = m_shadowMapResolution;
1597 fbInfo.layers = 1;
1598
1599 if (vkCreateFramebuffer(device, &fbInfo, nullptr, &m_shadowFramebuffer) !=
1600 VK_SUCCESS) {
1601 SLEAK_ERROR("Shadow resolution change: framebuffer creation failed");
1603 return;
1604 }
1605
1606 // Initial layout transition — descriptor must be valid pre-first-pass
1607 {
1608 VkCommandBufferAllocateInfo cmdAllocInfo{};
1609 cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
1610 cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
1611 cmdAllocInfo.commandPool = commands;
1612 cmdAllocInfo.commandBufferCount = 1;
1613
1614 VkCommandBuffer cmdBuf;
1615 vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmdBuf);
1616
1617 VkCommandBufferBeginInfo cmdBeginInfo{};
1618 cmdBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
1619 cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
1620 vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo);
1621
1622 VkImageMemoryBarrier barrier{};
1623 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
1624 barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1625 barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
1626 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1627 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
1628 barrier.image = m_shadowImage;
1629 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
1630 barrier.subresourceRange.baseMipLevel = 0;
1631 barrier.subresourceRange.levelCount = 1;
1632 barrier.subresourceRange.baseArrayLayer = 0;
1633 barrier.subresourceRange.layerCount = 1;
1634 barrier.srcAccessMask = 0;
1635 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
1636
1637 vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
1638 VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0,
1639 nullptr, 0, nullptr, 1, &barrier);
1640
1641 vkEndCommandBuffer(cmdBuf);
1642
1643 VkSubmitInfo layoutSubmit{};
1644 layoutSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
1645 layoutSubmit.commandBufferCount = 1;
1646 layoutSubmit.pCommandBuffers = &cmdBuf;
1647
1648 VkFenceCreateInfo layoutFenceInfo{};
1649 layoutFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
1650 VkFence layoutFence;
1651 vkCreateFence(device, &layoutFenceInfo, nullptr, &layoutFence);
1652 vkQueueSubmit(graphicsQueue, 1, &layoutSubmit, layoutFence);
1653 vkWaitForFences(device, 1, &layoutFence, VK_TRUE, UINT64_MAX);
1654 vkDestroyFence(device, layoutFence, nullptr);
1655 vkFreeCommandBuffers(device, commands, 1, &cmdBuf);
1656 }
1657
1658 // Point set-3 descriptors at the new image view
1659 if (m_lightUBOCreated && m_shadowImageView && m_shadowSampler &&
1660 m_shadowRawSampler) {
1661 for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
1662 VkDescriptorImageInfo compareInfo{};
1663 compareInfo.imageLayout =
1664 VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
1665 compareInfo.imageView = m_shadowImageView;
1666 compareInfo.sampler = m_shadowSampler;
1667
1668 VkDescriptorImageInfo rawInfo{};
1669 rawInfo.imageLayout =
1670 VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
1671 rawInfo.imageView = m_shadowImageView;
1672 rawInfo.sampler = m_shadowRawSampler;
1673
1674 std::array<VkWriteDescriptorSet, 2> writes{};
1675 writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1676 writes[0].dstSet = m_shadowSamplerDescriptorSets[i];
1677 writes[0].dstBinding = 0;
1678 writes[0].dstArrayElement = 0;
1679 writes[0].descriptorType =
1680 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1681 writes[0].descriptorCount = 1;
1682 writes[0].pImageInfo = &compareInfo;
1683
1684 writes[1] = writes[0];
1685 writes[1].dstBinding = 1;
1686 writes[1].pImageInfo = &rawInfo;
1687
1688 vkUpdateDescriptorSets(device,
1689 static_cast<uint32_t>(writes.size()),
1690 writes.data(), 0, nullptr);
1691 }
1692 }
1693
1694 SLEAK_INFO("VulkanRenderer: Shadow map resized to {}x{}",
1696}
1697
1698/// Top-level orchestrator for deferred rendering: creates the GBuffer color
1699/// images, then the render passes, descriptors, and pipelines that read
1700/// them. Shares the depth image from CreateDepthResources() (already SAMPLED_BIT).
1701bool VulkanRenderer::CreateGBufferResources() {
1702 if (m_gbufferResourcesCreated) return true;
1703
1704 // Create GBuffer color attachment images
1705 for (uint32_t i = 0; i < GBUFFER_COUNT; ++i) {
1706 VkImageCreateInfo imageInfo{};
1707 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
1708 imageInfo.imageType = VK_IMAGE_TYPE_2D;
1709 imageInfo.extent.width = scExtent.width;
1710 imageInfo.extent.height = scExtent.height;
1711 imageInfo.extent.depth = 1;
1712 imageInfo.mipLevels = 1;
1713 imageInfo.arrayLayers = 1;
1714 imageInfo.format = m_gbufferFormats[i];
1715 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
1716 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
1717 imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
1718 | VK_IMAGE_USAGE_SAMPLED_BIT;
1719 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
1720 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1721
1722 if (vkCreateImage(device, &imageInfo, nullptr, &m_gbufferImages[i]) != VK_SUCCESS) {
1723 SLEAK_ERROR("GBuffer: Failed to create GBuffer image {}!", i);
1724 return false;
1725 }
1726
1727 VkMemoryRequirements memReqs;
1728 vkGetImageMemoryRequirements(device, m_gbufferImages[i], &memReqs);
1729
1730 VkMemoryAllocateInfo allocInfo{};
1731 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1732 allocInfo.allocationSize = memReqs.size;
1733 allocInfo.memoryTypeIndex = FindMemoryType(memReqs.memoryTypeBits,
1734 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
1735
1736 if (vkAllocateMemory(device, &allocInfo, nullptr, &m_gbufferMemory[i]) != VK_SUCCESS) {
1737 SLEAK_ERROR("GBuffer: Failed to allocate GBuffer memory {}!", i);
1738 return false;
1739 }
1740 vkBindImageMemory(device, m_gbufferImages[i], m_gbufferMemory[i], 0);
1741
1742 VkImageViewCreateInfo viewInfo{};
1743 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
1744 viewInfo.image = m_gbufferImages[i];
1745 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
1746 viewInfo.format = m_gbufferFormats[i];
1747 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
1748 viewInfo.subresourceRange.baseMipLevel = 0;
1749 viewInfo.subresourceRange.levelCount = 1;
1750 viewInfo.subresourceRange.baseArrayLayer = 0;
1751 viewInfo.subresourceRange.layerCount = 1;
1752
1753 if (vkCreateImageView(device, &viewInfo, nullptr, &m_gbufferViews[i]) != VK_SUCCESS) {
1754 SLEAK_ERROR("GBuffer: Failed to create GBuffer image view {}!", i);
1755 return false;
1756 }
1757 }
1758
1759 if (!CreateGBufferRenderPass()) { SLEAK_ERROR("GBuffer: render pass failed!"); return false; }
1760 if (!CreateGBufferFramebuffer()) { SLEAK_ERROR("GBuffer: framebuffer failed!"); return false; }
1761 // SSAO + bloom must be created BEFORE the lighting/forward render passes
1762 // and BEFORE CreateGBufferDescriptorSets — the lighting pass framebuffers
1763 // reference m_hdrSceneView (created by CreateBloomResources) and the
1764 // GBuffer sampler set 0 binding 7 samples the SSAO blur result.
1765 if (!CreateSSAOResources()) { SLEAK_ERROR("GBuffer: SSAO resources failed!"); return false; }
1766 if (!CreateBloomResources()) { SLEAK_ERROR("GBuffer: bloom/HDR resources failed!"); return false; }
1767 // SSR needs the HDR scene view (created by CreateBloomResources), so it
1768 // must come after that. The bloom composite pass later samples SSR.
1769 if (!CreateSSRResources()) { SLEAK_ERROR("GBuffer: SSR resources failed!"); return false; }
1770 // Prime the disabled-effect fallback images once so the per-frame disabled
1771 // paths can skip their redundant clears (ssao/ssr/bloom).
1772 m_ssaoFallbackPrimed = false;
1773 m_ssrFallbackPrimed = false;
1774 m_bloomFallbackPrimed = false;
1775 InitDisabledEffectFallbacks();
1776 if (!CreateGBufferDescriptorSets()) { SLEAK_ERROR("GBuffer: descriptor sets failed!"); return false; }
1777 if (!CreateDeferredCBResources()) { SLEAK_ERROR("GBuffer: deferred CB failed!"); return false; }
1778 if (!CreatePBRMaterialResources()) { SLEAK_ERROR("GBuffer: PBR material resources failed!"); return false; }
1779 if (!CreateIBLResources()) { SLEAK_ERROR("GBuffer: IBL resources failed!"); return false; }
1780 if (!CreateLightingRenderPass()) { SLEAK_ERROR("GBuffer: lighting RP failed!"); return false; }
1781 if (!CreateLightingFramebuffers()) { SLEAK_ERROR("GBuffer: lighting FBs failed!"); return false; }
1782 if (!CreateLightingPipeline()) { SLEAK_ERROR("GBuffer: lighting pipeline failed!"); return false; }
1783 if (!CreateForwardRenderPass()) { SLEAK_ERROR("GBuffer: forward RP failed!"); return false; }
1784 if (!CreateForwardFramebuffers()) { SLEAK_ERROR("GBuffer: forward FBs failed!"); return false; }
1785 if (!CreateGBufferPipeline()) { SLEAK_ERROR("GBuffer: gbuffer pipeline failed!"); return false; }
1786 if (!CreateSkinnedGbufferPipeline()) { SLEAK_ERROR("GBuffer: skinned gbuffer pipeline failed!"); return false; }
1787
1788 // Now that SSAO inputs (gNormalRough, gDepth) and SSAO blur
1789 // descriptor set 0 target are available, write SSAO descriptors.
1790 UpdateSSAODescriptors();
1791 // SSR input descriptors depend on m_gbufferViews + m_hdrSceneView, both
1792 // created above — safe to write now.
1793 UpdateSSRDescriptors();
1794
1795 // Recreate forward-pass pipelines so they use m_forwardRenderPass instead
1796 // of the main renderPass (which may have different attachments when MSAA
1797 // is active, or an incompatible finalLayout).
1798 if (m_forwardRenderPass != VK_NULL_HANDLE) {
1799 if (pipeline) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; }
1800 if (skyboxPipeline) { vkDestroyPipeline(device, skyboxPipeline, nullptr); skyboxPipeline = VK_NULL_HANDLE; }
1801 if (debugLinePipeline) { vkDestroyPipeline(device, debugLinePipeline, nullptr); debugLinePipeline = VK_NULL_HANDLE; }
1802 if (skinnedPipeline) { vkDestroyPipeline(device, skinnedPipeline, nullptr); skinnedPipeline = VK_NULL_HANDLE; }
1803 if (m_skinnedGbufferPipeline) { vkDestroyPipeline(device, m_skinnedGbufferPipeline, nullptr); m_skinnedGbufferPipeline = VK_NULL_HANDLE; }
1804 DestroyCustomFormatPipelines();
1805 // Destroy old skybox descriptor pool (CreateSkyboxPipeline allocates new ones)
1806 if (skyboxDescriptorPool) {
1807 vkDestroyDescriptorPool(device, skyboxDescriptorPool, nullptr);
1808 skyboxDescriptorPool = VK_NULL_HANDLE;
1809 }
1810 skyboxDescriptorSets.clear();
1811 delete skyboxShader;
1812 skyboxShader = nullptr;
1813
1814 CreateGraphicsPipeline();
1815 CreateSkyboxPipeline();
1816 CreateDebugLinePipeline();
1817 CreateSkinnedPipeline();
1818 CreateSkinnedGbufferPipeline();
1819
1820 m_gbufferResourcesCreated = true;
1821
1822 // Re-bind skybox cubemap to the newly allocated descriptor sets
1823 if (m_skyboxCubemapView != VK_NULL_HANDLE && m_skyboxCubemapSampler != VK_NULL_HANDLE) {
1824 for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) {
1825 VkDescriptorImageInfo imageInfo{};
1826 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1827 imageInfo.imageView = m_skyboxCubemapView;
1828 imageInfo.sampler = m_skyboxCubemapSampler;
1829
1830 VkWriteDescriptorSet descriptorWrite{};
1831 descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1832 descriptorWrite.dstSet = skyboxDescriptorSets[i];
1833 descriptorWrite.dstBinding = 0;
1834 descriptorWrite.dstArrayElement = 0;
1835 descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1836 descriptorWrite.descriptorCount = 1;
1837 descriptorWrite.pImageInfo = &imageInfo;
1838
1839 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
1840 }
1841 m_skyboxDescriptorsWritten = true;
1842 }
1843 }
1844
1845 SLEAK_INFO("VulkanRenderer: Deferred GBuffer resources created ({}x{})",
1846 scExtent.width, scExtent.height);
1847 return true;
1848}
1849
1850}
1851}
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)
VulkanBuffer(VkDevice device, VkPhysicalDevice physicalDevice, uint32_t size, BufferType type, VkCommandPool commandPool, VkQueue graphicsQueue)
static AsyncFlushResult FlushPendingCopiesAsync(VkSemaphore signalSemaphore)
static VmaAllocator GetAllocator()
static VkDeviceSize GetDeviceLocalHeapSize()
static void SetBatchingEnabled(bool enabled)
VulkanCubemapTexture(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
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 2D texture: image + view + sampler, with per-swapchain-image descriptor sets.
const std::vector< VkDescriptorSet > & GetDescriptorSets() const
VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
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