SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
VulkanSwapchain.cpp
Go to the documentation of this file.
2
3#include <Core/Window.hpp>
4#include <algorithm>
5#include <cstdint>
6#include <limits>
7#include <optional>
8#include <vector>
9#include "Core/Logger.hpp"
10#include "SDL3/SDL_video.h"
11
12namespace Sleak {
13 namespace RenderEngine {
14
15/// Creates the swapchain from the queried surface capabilities.
16bool VulkanRenderer::CreateSwapChain() {
17 auto details = QuerySwapchain();
18 if (!details.has_value())
19 return false;
20
21 if (details->formats.empty() && details->presentModes.empty())
22 SLEAK_RETURN_ERR("Swapchain is not supported for this GPU!");
23
24 auto format = ChooseFormat(details->formats);
25 auto mode = ChoosePresentMode(details->presentModes);
26 auto extent = ChooseExtend(details.value());
27
28 uint32_t imageCount = details->caps.minImageCount + 1;
29 if (details->caps.maxImageCount > 0 &&
30 imageCount > details->caps.maxImageCount)
31 imageCount = details->caps.maxImageCount;
32
33 VkSwapchainCreateInfoKHR info{};
34 info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
35 info.minImageCount = imageCount;
36 info.imageColorSpace = format.colorSpace;
37 info.imageFormat = format.format;
38 info.imageExtent = extent;
39 info.imageArrayLayers = 1;
40 info.presentMode = mode;
41 info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
42
43 uint32_t indices[] = {QueueIDs.GraphicsIndex, QueueIDs.PresentIndex};
44
45 if (QueueIDs.GraphicsIndex != QueueIDs.PresentIndex) {
46 info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
47 info.queueFamilyIndexCount = 2;
48 info.pQueueFamilyIndices = indices;
49 } else {
50 info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
51 }
52
53 info.surface = surface;
54 info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
55 info.preTransform = details->caps.currentTransform;
56 info.clipped = VK_TRUE;
57 info.oldSwapchain = VK_NULL_HANDLE;
58
59 VkResult result =
60 vkCreateSwapchainKHR(device, &info, nullptr, &swapChain);
61 if (result != VK_SUCCESS)
62 SLEAK_RETURN_ERR("Failed to create swap chain for renderer!")
63
64 uint32_t scImageCount = 0;
65 vkGetSwapchainImagesKHR(device, swapChain, &scImageCount, nullptr);
66 swapChainImages.resize(scImageCount);
67 vkGetSwapchainImagesKHR(device, swapChain, &scImageCount,
68 swapChainImages.data());
69
70 scImageFormat = format.format;
71 scExtent = extent;
72
73 return true;
74}
75
76/// Destroys the swapchain, its image views, and framebuffers.
77void VulkanRenderer::CleanupSwapChain() {
78 CleanupDepthResources();
79 CleanupMSAAColorResources();
80
81 for (auto framebuffer : swapChainFramebuffers) {
82 vkDestroyFramebuffer(device, framebuffer, nullptr);
83 }
84 swapChainFramebuffers.clear();
85
86 for (auto imageView : swapChainImageViews) {
87 vkDestroyImageView(device, imageView, nullptr);
88 }
89 swapChainImageViews.clear();
90
91 if (swapChain) {
92 vkDestroySwapchainKHR(device, swapChain, nullptr);
93 swapChain = VK_NULL_HANDLE;
94 }
95}
96
97/// Creates the MSAA color image used as the multisampled render target.
98bool VulkanRenderer::CreateMSAAColorResources() {
99 if (m_msaaSamples == VK_SAMPLE_COUNT_1_BIT)
100 return true; // No MSAA image needed
101
102 VkImageCreateInfo imageInfo{};
103 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
104 imageInfo.imageType = VK_IMAGE_TYPE_2D;
105 imageInfo.extent.width = scExtent.width;
106 imageInfo.extent.height = scExtent.height;
107 imageInfo.extent.depth = 1;
108 imageInfo.mipLevels = 1;
109 imageInfo.arrayLayers = 1;
110 imageInfo.format = scImageFormat;
111 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
112 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
113 imageInfo.usage = VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
114 imageInfo.samples = m_msaaSamples;
115 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
116
117 if (vkCreateImage(device, &imageInfo, nullptr, &m_msaaColorImage) != VK_SUCCESS)
118 SLEAK_RETURN_ERR("Failed to create MSAA color image!");
119
120 VkMemoryRequirements memRequirements;
121 vkGetImageMemoryRequirements(device, m_msaaColorImage, &memRequirements);
122
123 VkMemoryAllocateInfo allocInfo{};
124 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
125 allocInfo.allocationSize = memRequirements.size;
126 allocInfo.memoryTypeIndex = FindMemoryType(
127 memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
128
129 if (vkAllocateMemory(device, &allocInfo, nullptr, &m_msaaColorImageMemory) != VK_SUCCESS)
130 SLEAK_RETURN_ERR("Failed to allocate MSAA color image memory!");
131
132 vkBindImageMemory(device, m_msaaColorImage, m_msaaColorImageMemory, 0);
133
134 VkImageViewCreateInfo viewInfo{};
135 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
136 viewInfo.image = m_msaaColorImage;
137 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
138 viewInfo.format = scImageFormat;
139 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
140 viewInfo.subresourceRange.baseMipLevel = 0;
141 viewInfo.subresourceRange.levelCount = 1;
142 viewInfo.subresourceRange.baseArrayLayer = 0;
143 viewInfo.subresourceRange.layerCount = 1;
144
145 if (vkCreateImageView(device, &viewInfo, nullptr, &m_msaaColorImageView) != VK_SUCCESS)
146 SLEAK_RETURN_ERR("Failed to create MSAA color image view!");
147
148 return true;
149}
150
151/// Destroys the MSAA color image, view, and memory.
152void VulkanRenderer::CleanupMSAAColorResources() {
153 if (m_msaaColorImageView) {
154 vkDestroyImageView(device, m_msaaColorImageView, nullptr);
155 m_msaaColorImageView = VK_NULL_HANDLE;
156 }
157 if (m_msaaColorImage) {
158 vkDestroyImage(device, m_msaaColorImage, nullptr);
159 m_msaaColorImage = VK_NULL_HANDLE;
160 }
161 if (m_msaaColorImageMemory) {
162 vkFreeMemory(device, m_msaaColorImageMemory, nullptr);
163 m_msaaColorImageMemory = VK_NULL_HANDLE;
164 }
165}
166
167/// Queries the highest MSAA sample count the GPU supports.
168VkSampleCountFlagBits VulkanRenderer::GetMaxUsableSampleCount() {
169 VkPhysicalDeviceProperties props;
170 vkGetPhysicalDeviceProperties(physicalDevice, &props);
171 VkSampleCountFlags counts = props.limits.framebufferColorSampleCounts
172 & props.limits.framebufferDepthSampleCounts;
173 if (counts & VK_SAMPLE_COUNT_8_BIT) return VK_SAMPLE_COUNT_8_BIT;
174 if (counts & VK_SAMPLE_COUNT_4_BIT) return VK_SAMPLE_COUNT_4_BIT;
175 if (counts & VK_SAMPLE_COUNT_2_BIT) return VK_SAMPLE_COUNT_2_BIT;
176 return VK_SAMPLE_COUNT_1_BIT;
177}
178
179/// Creates an image view for each swapchain image.
180bool VulkanRenderer::CreateImageViews() {
181 swapChainImageViews.resize(swapChainImages.size());
182
183 for (size_t i = 0; i < swapChainImageViews.size(); i++) {
184 VkImageViewCreateInfo info{};
185 info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
186 info.image = swapChainImages[i];
187 info.viewType = VK_IMAGE_VIEW_TYPE_2D;
188 info.format = scImageFormat;
189
190 info.components = {VK_COMPONENT_SWIZZLE_IDENTITY,
191 VK_COMPONENT_SWIZZLE_IDENTITY,
192 VK_COMPONENT_SWIZZLE_IDENTITY,
193 VK_COMPONENT_SWIZZLE_IDENTITY};
194
195 info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
196 info.subresourceRange.baseMipLevel = 0;
197 info.subresourceRange.levelCount = 1;
198 info.subresourceRange.baseArrayLayer = 0;
199 info.subresourceRange.layerCount = 1;
200
201 auto result = vkCreateImageView(device, &info, nullptr,
202 &swapChainImageViews[i]);
203 if (result != VK_SUCCESS)
205 "Failed to create image view of swapchain, index: {}", i);
206 }
207
208 return true;
209}
210
211/// Creates the depth image, memory, and image view.
212bool VulkanRenderer::CreateDepthResources() {
213 depthFormat = FindDepthFormat();
214
215 // Create depth image
216 VkImageCreateInfo imageInfo{};
217 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
218 imageInfo.imageType = VK_IMAGE_TYPE_2D;
219 imageInfo.extent.width = scExtent.width;
220 imageInfo.extent.height = scExtent.height;
221 imageInfo.extent.depth = 1;
222 imageInfo.mipLevels = 1;
223 imageInfo.arrayLayers = 1;
224 imageInfo.format = depthFormat;
225 imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
226 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
227 // Always add SAMPLED_BIT so deferred lighting pass can read depth.
228 // When deferred is enabled, force 1x samples (GBuffer is non-MSAA).
229 VkSampleCountFlagBits depthSamples = m_deferredEnabled
230 ? VK_SAMPLE_COUNT_1_BIT : m_msaaSamples;
231 imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
232 | VK_IMAGE_USAGE_SAMPLED_BIT;
233 imageInfo.samples = depthSamples;
234 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
235
236 if (vkCreateImage(device, &imageInfo, nullptr, &depthImage) !=
237 VK_SUCCESS)
238 SLEAK_RETURN_ERR("Failed to create depth image!");
239
240 // Allocate memory
241 VkMemoryRequirements memRequirements;
242 vkGetImageMemoryRequirements(device, depthImage, &memRequirements);
243
244 VkMemoryAllocateInfo allocInfo{};
245 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
246 allocInfo.allocationSize = memRequirements.size;
247 allocInfo.memoryTypeIndex = FindMemoryType(
248 memRequirements.memoryTypeBits,
249 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
250
251 if (vkAllocateMemory(device, &allocInfo, nullptr, &depthImageMemory) !=
252 VK_SUCCESS)
253 SLEAK_RETURN_ERR("Failed to allocate depth image memory!");
254
255 vkBindImageMemory(device, depthImage, depthImageMemory, 0);
256
257 // Create image view
258 VkImageViewCreateInfo viewInfo{};
259 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
260 viewInfo.image = depthImage;
261 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
262 viewInfo.format = depthFormat;
263 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
264 viewInfo.subresourceRange.baseMipLevel = 0;
265 viewInfo.subresourceRange.levelCount = 1;
266 viewInfo.subresourceRange.baseArrayLayer = 0;
267 viewInfo.subresourceRange.layerCount = 1;
268
269 if (vkCreateImageView(device, &viewInfo, nullptr, &depthImageView) !=
270 VK_SUCCESS)
271 SLEAK_RETURN_ERR("Failed to create depth image view!");
272
273 return true;
274}
275
276/// Destroys the depth image, memory, and image view.
277void VulkanRenderer::CleanupDepthResources() {
278 if (depthImageView) {
279 vkDestroyImageView(device, depthImageView, nullptr);
280 depthImageView = VK_NULL_HANDLE;
281 }
282 if (depthImage) {
283 vkDestroyImage(device, depthImage, nullptr);
284 depthImage = VK_NULL_HANDLE;
285 }
286 if (depthImageMemory) {
287 vkFreeMemory(device, depthImageMemory, nullptr);
288 depthImageMemory = VK_NULL_HANDLE;
289 }
290}
291
292/// Picks the first supported depth-stencil format from the candidate list.
293VkFormat VulkanRenderer::FindDepthFormat() {
294 std::vector<VkFormat> candidates = {VK_FORMAT_D32_SFLOAT,
295 VK_FORMAT_D32_SFLOAT_S8_UINT,
296 VK_FORMAT_D24_UNORM_S8_UINT};
297
298 for (VkFormat format : candidates) {
299 VkFormatProperties props;
300 vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);
301
302 if (props.optimalTilingFeatures &
303 VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
304 return format;
305 }
306 }
307
308 return VK_FORMAT_D32_SFLOAT; // fallback
309}
310
311/// Finds a memory type index matching the filter and property flags.
312uint32_t VulkanRenderer::FindMemoryType(
313 uint32_t typeFilter, VkMemoryPropertyFlags properties) {
314 VkPhysicalDeviceMemoryProperties memProperties;
315 vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
316
317 for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
318 if ((typeFilter & (1 << i)) &&
319 (memProperties.memoryTypes[i].propertyFlags & properties) ==
320 properties) {
321 return i;
322 }
323 }
324
325 SLEAK_ERROR("Failed to find suitable memory type!");
326 return 0;
327}
328
329/// Queries surface capabilities, formats, and present modes.
330std::optional<SwapchainDetails> VulkanRenderer::QuerySwapchain() {
331 SwapchainDetails details;
332
333 VkResult result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
334 physicalDevice, surface, &details.caps);
335 if (result != VK_SUCCESS) {
336 SLEAK_ERROR("Failed to retrieve surface information!");
337 return {};
338 }
339
340 // Fixed: use resize() not reserve()
341 uint32_t formatCount = 0;
342 result = vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface,
343 &formatCount, nullptr);
344 if (result != VK_SUCCESS || formatCount < 1) {
345 SLEAK_ERROR("Failed to retrieve supported surface formats");
346 return {};
347 }
348
349 details.formats.resize(formatCount);
350 vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface,
351 &formatCount,
352 details.formats.data());
353
354 // Get surface present modes
355 uint32_t modeCount = 0;
356 result = vkGetPhysicalDeviceSurfacePresentModesKHR(
357 physicalDevice, surface, &modeCount, nullptr);
358 if (result != VK_SUCCESS || modeCount < 1) {
359 SLEAK_ERROR("Failed to retrieve present modes!");
360 return {};
361 }
362
363 details.presentModes.resize(modeCount);
364 vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface,
365 &modeCount,
366 details.presentModes.data());
367
368 return details;
369}
370
371/// Picks a UNORM surface format to avoid double sRGB encoding.
372VkSurfaceFormatKHR VulkanRenderer::ChooseFormat(
373 const std::vector<VkSurfaceFormatKHR>& formats) {
374 // Prefer UNORM so the GPU does NOT apply automatic sRGB gamma encoding
375 // on output. The game renders in sRGB/gamma space already (no linear
376 // pipeline), so using _SRGB would gamma-encode everything twice —
377 // producing a washed-out, overbright image. _UNORM writes values as-is,
378 // matching the behaviour of DX11/DX12 DXGI_FORMAT_*_UNORM swap chains.
379 for (auto& format : formats)
380 if (format.format == VK_FORMAT_B8G8R8A8_UNORM &&
381 format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
382 return format;
383
384 // Second preference: R8G8B8A8_UNORM
385 for (auto& format : formats)
386 if (format.format == VK_FORMAT_R8G8B8A8_UNORM &&
387 format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
388 return format;
389
390 return formats[0];
391}
392
393/// Picks FIFO when VSync is on, otherwise MAILBOX or IMMEDIATE.
394VkPresentModeKHR VulkanRenderer::ChoosePresentMode(
395 const std::vector<VkPresentModeKHR>& modes) {
396 if (m_vsync) {
397 // VSync ON: FIFO is guaranteed and provides VSync
398 return VK_PRESENT_MODE_FIFO_KHR;
399 }
400
401 // VSync OFF: prefer MAILBOX (no tearing, uncapped), then IMMEDIATE
402 for (auto& mode : modes)
403 if (mode == VK_PRESENT_MODE_MAILBOX_KHR)
404 return mode;
405 for (auto& mode : modes)
406 if (mode == VK_PRESENT_MODE_IMMEDIATE_KHR)
407 return mode;
408
409 return VK_PRESENT_MODE_FIFO_KHR;
410}
411
412/// Clamps the window size to the surface's supported extent.
413// Fixed: clamp height using height, not width
414VkExtent2D VulkanRenderer::ChooseExtend(SwapchainDetails details) {
415 if (details.caps.currentExtent.width !=
416 std::numeric_limits<uint32_t>::max())
417 return details.caps.currentExtent;
418
419 int width, height;
420 SDL_GetWindowSizeInPixels(sdlWindow->GetSDLWindow(), &width, &height);
421
422 VkExtent2D actualExtent = {static_cast<uint32_t>(width),
423 static_cast<uint32_t>(height)};
424
425 actualExtent.width =
426 std::clamp(actualExtent.width,
427 details.caps.minImageExtent.width,
428 details.caps.maxImageExtent.width);
429
430 actualExtent.height =
431 std::clamp(actualExtent.height,
432 details.caps.minImageExtent.height,
433 details.caps.maxImageExtent.height);
434
435 return actualExtent;
436}
437
438}
439}
int width
int height
#define SLEAK_ERROR(...)
Definition Logger.hpp:22
#define SLEAK_RETURN_ERR(...)
Definition Logger.hpp:25
Backend-facing rendering layer shared by the four graphics backends.
Root namespace for everything the engine exposes.
Definition Camera.hpp:10