Kelsidavis-WoWee/src/rendering/vk_context.cpp

1415 lines
59 KiB
C++

#define VMA_IMPLEMENTATION
#include "rendering/vk_context.hpp"
#include "core/logger.hpp"
#include <VkBootstrap.h>
#include <SDL2/SDL_vulkan.h>
#include <imgui_impl_vulkan.h>
#include <algorithm>
#include <cstring>
namespace wowee {
namespace rendering {
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT severity,
[[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT type,
const VkDebugUtilsMessengerCallbackDataEXT* callbackData,
[[maybe_unused]] void* userData)
{
if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
LOG_ERROR("Vulkan: ", callbackData->pMessage);
} else if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
LOG_WARNING("Vulkan: ", callbackData->pMessage);
}
return VK_FALSE;
}
VkContext::~VkContext() {
shutdown();
}
bool VkContext::initialize(SDL_Window* window) {
LOG_INFO("Initializing Vulkan context");
if (!createInstance(window)) return false;
if (!createSurface(window)) return false;
if (!selectPhysicalDevice()) return false;
if (!createLogicalDevice()) return false;
if (!createAllocator()) return false;
int w, h;
SDL_Vulkan_GetDrawableSize(window, &w, &h);
if (!createSwapchain(w, h)) return false;
if (!createCommandPools()) return false;
if (!createSyncObjects()) return false;
if (!createImGuiResources()) return false;
LOG_INFO("Vulkan context initialized successfully");
return true;
}
void VkContext::shutdown() {
if (device) {
vkDeviceWaitIdle(device);
}
destroyImGuiResources();
// Destroy sync objects
for (auto& frame : frames) {
if (frame.inFlightFence) vkDestroyFence(device, frame.inFlightFence, nullptr);
if (frame.renderFinishedSemaphore) vkDestroySemaphore(device, frame.renderFinishedSemaphore, nullptr);
if (frame.imageAvailableSemaphore) vkDestroySemaphore(device, frame.imageAvailableSemaphore, nullptr);
if (frame.commandPool) vkDestroyCommandPool(device, frame.commandPool, nullptr);
frame = {};
}
if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; }
if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; }
destroySwapchain();
if (allocator) { vmaDestroyAllocator(allocator); allocator = VK_NULL_HANDLE; }
if (device) { vkDestroyDevice(device, nullptr); device = VK_NULL_HANDLE; }
if (surface) { vkDestroySurfaceKHR(instance, surface, nullptr); surface = VK_NULL_HANDLE; }
if (debugMessenger) {
auto func = reinterpret_cast<PFN_vkDestroyDebugUtilsMessengerEXT>(
vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"));
if (func) func(instance, debugMessenger, nullptr);
debugMessenger = VK_NULL_HANDLE;
}
if (instance) { vkDestroyInstance(instance, nullptr); instance = VK_NULL_HANDLE; }
LOG_INFO("Vulkan context shutdown");
}
bool VkContext::createInstance(SDL_Window* window) {
// Get required SDL extensions
unsigned int sdlExtCount = 0;
SDL_Vulkan_GetInstanceExtensions(window, &sdlExtCount, nullptr);
std::vector<const char*> sdlExts(sdlExtCount);
SDL_Vulkan_GetInstanceExtensions(window, &sdlExtCount, sdlExts.data());
vkb::InstanceBuilder builder;
builder.set_app_name("Wowee")
.set_app_version(VK_MAKE_VERSION(1, 0, 0))
.require_api_version(1, 1, 0);
for (auto ext : sdlExts) {
builder.enable_extension(ext);
}
if (enableValidation) {
builder.request_validation_layers(true)
.set_debug_callback(debugCallback);
}
auto instRet = builder.build();
if (!instRet) {
LOG_ERROR("Failed to create Vulkan instance: ", instRet.error().message());
return false;
}
vkbInstance_ = instRet.value();
instance = vkbInstance_.instance;
debugMessenger = vkbInstance_.debug_messenger;
LOG_INFO("Vulkan instance created");
return true;
}
bool VkContext::createSurface(SDL_Window* window) {
if (!SDL_Vulkan_CreateSurface(window, instance, &surface)) {
LOG_ERROR("Failed to create Vulkan surface: ", SDL_GetError());
return false;
}
return true;
}
bool VkContext::selectPhysicalDevice() {
vkb::PhysicalDeviceSelector selector{vkbInstance_};
selector.set_surface(surface)
.set_minimum_version(1, 1)
.prefer_gpu_device_type(vkb::PreferredDeviceType::discrete);
auto physRet = selector.select();
if (!physRet) {
LOG_ERROR("Failed to select Vulkan physical device: ", physRet.error().message());
return false;
}
vkbPhysicalDevice_ = physRet.value();
physicalDevice = vkbPhysicalDevice_.physical_device;
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(physicalDevice, &props);
uint32_t apiVersion = props.apiVersion;
VkPhysicalDeviceDepthStencilResolveProperties dsResolveProps{};
dsResolveProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_STENCIL_RESOLVE_PROPERTIES;
VkPhysicalDeviceProperties2 props2{};
props2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
props2.pNext = &dsResolveProps;
vkGetPhysicalDeviceProperties2(physicalDevice, &props2);
if (apiVersion >= VK_API_VERSION_1_2) {
VkResolveModeFlags modes = dsResolveProps.supportedDepthResolveModes;
if (modes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT) {
depthResolveMode_ = VK_RESOLVE_MODE_SAMPLE_ZERO_BIT;
depthResolveSupported_ = true;
} else if (modes & VK_RESOLVE_MODE_MIN_BIT) {
depthResolveMode_ = VK_RESOLVE_MODE_MIN_BIT;
depthResolveSupported_ = true;
} else if (modes & VK_RESOLVE_MODE_MAX_BIT) {
depthResolveMode_ = VK_RESOLVE_MODE_MAX_BIT;
depthResolveSupported_ = true;
} else if (modes & VK_RESOLVE_MODE_AVERAGE_BIT) {
depthResolveMode_ = VK_RESOLVE_MODE_AVERAGE_BIT;
depthResolveSupported_ = true;
}
} else {
depthResolveSupported_ = false;
depthResolveMode_ = VK_RESOLVE_MODE_NONE;
}
LOG_INFO("Vulkan device: ", props.deviceName);
LOG_INFO("Vulkan API version: ", VK_VERSION_MAJOR(props.apiVersion), ".",
VK_VERSION_MINOR(props.apiVersion), ".", VK_VERSION_PATCH(props.apiVersion));
LOG_INFO("Depth resolve support: ", depthResolveSupported_ ? "YES" : "NO");
return true;
}
bool VkContext::createLogicalDevice() {
vkb::DeviceBuilder deviceBuilder{vkbPhysicalDevice_};
auto devRet = deviceBuilder.build();
if (!devRet) {
LOG_ERROR("Failed to create Vulkan logical device: ", devRet.error().message());
return false;
}
auto vkbDevice = devRet.value();
device = vkbDevice.device;
auto gqRet = vkbDevice.get_queue(vkb::QueueType::graphics);
if (!gqRet) {
LOG_ERROR("Failed to get graphics queue");
return false;
}
graphicsQueue = gqRet.value();
graphicsQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::graphics).value();
auto pqRet = vkbDevice.get_queue(vkb::QueueType::present);
if (!pqRet) {
// Fall back to graphics queue for presentation
presentQueue = graphicsQueue;
presentQueueFamily = graphicsQueueFamily;
} else {
presentQueue = pqRet.value();
presentQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::present).value();
}
LOG_INFO("Vulkan logical device created");
return true;
}
bool VkContext::createAllocator() {
VmaAllocatorCreateInfo allocInfo{};
allocInfo.instance = instance;
allocInfo.physicalDevice = physicalDevice;
allocInfo.device = device;
allocInfo.vulkanApiVersion = VK_API_VERSION_1_1;
if (vmaCreateAllocator(&allocInfo, &allocator) != VK_SUCCESS) {
LOG_ERROR("Failed to create VMA allocator");
return false;
}
LOG_INFO("VMA allocator created");
return true;
}
bool VkContext::createSwapchain(int width, int height) {
vkb::SwapchainBuilder swapchainBuilder{physicalDevice, device, surface};
auto swapRet = swapchainBuilder
.set_desired_format({VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR})
.set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR) // VSync
.set_desired_extent(static_cast<uint32_t>(width), static_cast<uint32_t>(height))
.set_image_usage_flags(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)
.set_desired_min_image_count(2)
.set_old_swapchain(swapchain) // For recreation
.build();
if (!swapRet) {
LOG_ERROR("Failed to create Vulkan swapchain: ", swapRet.error().message());
return false;
}
// Destroy old swapchain if recreating
if (swapchain != VK_NULL_HANDLE) {
destroySwapchain();
}
auto vkbSwap = swapRet.value();
swapchain = vkbSwap.swapchain;
swapchainFormat = vkbSwap.image_format;
swapchainExtent = vkbSwap.extent;
swapchainImages = vkbSwap.get_images().value();
swapchainImageViews = vkbSwap.get_image_views().value();
// Create framebuffers for ImGui render pass (created after ImGui resources)
// Will be created in createImGuiResources or recreateSwapchain
LOG_INFO("Vulkan swapchain created: ", swapchainExtent.width, "x", swapchainExtent.height,
" (", swapchainImages.size(), " images)");
swapchainDirty = false;
return true;
}
void VkContext::destroySwapchain() {
for (auto fb : swapchainFramebuffers) {
if (fb) vkDestroyFramebuffer(device, fb, nullptr);
}
swapchainFramebuffers.clear();
for (auto iv : swapchainImageViews) {
if (iv) vkDestroyImageView(device, iv, nullptr);
}
swapchainImageViews.clear();
swapchainImages.clear();
if (swapchain) {
vkDestroySwapchainKHR(device, swapchain, nullptr);
swapchain = VK_NULL_HANDLE;
}
}
bool VkContext::createCommandPools() {
// Per-frame command pools (resettable)
for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = graphicsQueueFamily;
if (vkCreateCommandPool(device, &poolInfo, nullptr, &frames[i].commandPool) != VK_SUCCESS) {
LOG_ERROR("Failed to create command pool for frame ", i);
return false;
}
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = frames[i].commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
if (vkAllocateCommandBuffers(device, &allocInfo, &frames[i].commandBuffer) != VK_SUCCESS) {
LOG_ERROR("Failed to allocate command buffer for frame ", i);
return false;
}
}
// Immediate submit pool
VkCommandPoolCreateInfo immPoolInfo{};
immPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
immPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
immPoolInfo.queueFamilyIndex = graphicsQueueFamily;
if (vkCreateCommandPool(device, &immPoolInfo, nullptr, &immCommandPool) != VK_SUCCESS) {
LOG_ERROR("Failed to create immediate command pool");
return false;
}
return true;
}
bool VkContext::createSyncObjects() {
VkSemaphoreCreateInfo semInfo{};
semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; // Start signaled so first frame doesn't block
for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (vkCreateSemaphore(device, &semInfo, nullptr, &frames[i].imageAvailableSemaphore) != VK_SUCCESS ||
vkCreateSemaphore(device, &semInfo, nullptr, &frames[i].renderFinishedSemaphore) != VK_SUCCESS ||
vkCreateFence(device, &fenceInfo, nullptr, &frames[i].inFlightFence) != VK_SUCCESS) {
LOG_ERROR("Failed to create sync objects for frame ", i);
return false;
}
}
// Immediate submit fence (not signaled initially)
VkFenceCreateInfo immFenceInfo{};
immFenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
if (vkCreateFence(device, &immFenceInfo, nullptr, &immFence) != VK_SUCCESS) {
LOG_ERROR("Failed to create immediate submit fence");
return false;
}
return true;
}
bool VkContext::createDepthBuffer() {
VkImageCreateInfo imgInfo{};
imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imgInfo.imageType = VK_IMAGE_TYPE_2D;
imgInfo.format = depthFormat;
imgInfo.extent = {swapchainExtent.width, swapchainExtent.height, 1};
imgInfo.mipLevels = 1;
imgInfo.arrayLayers = 1;
imgInfo.samples = msaaSamples_;
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imgInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
VmaAllocationCreateInfo allocInfo{};
allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
if (vmaCreateImage(allocator, &imgInfo, &allocInfo, &depthImage, &depthAllocation, nullptr) != VK_SUCCESS) {
LOG_ERROR("Failed to create depth image");
return false;
}
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = depthImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = depthFormat;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(device, &viewInfo, nullptr, &depthImageView) != VK_SUCCESS) {
LOG_ERROR("Failed to create depth image view");
return false;
}
return true;
}
void VkContext::destroyDepthBuffer() {
if (depthImageView) { vkDestroyImageView(device, depthImageView, nullptr); depthImageView = VK_NULL_HANDLE; }
if (depthImage) { vmaDestroyImage(allocator, depthImage, depthAllocation); depthImage = VK_NULL_HANDLE; depthAllocation = VK_NULL_HANDLE; }
}
bool VkContext::createMsaaColorImage() {
if (msaaSamples_ == VK_SAMPLE_COUNT_1_BIT) return true; // No MSAA image needed
VkImageCreateInfo imgInfo{};
imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imgInfo.imageType = VK_IMAGE_TYPE_2D;
imgInfo.format = swapchainFormat;
imgInfo.extent = {swapchainExtent.width, swapchainExtent.height, 1};
imgInfo.mipLevels = 1;
imgInfo.arrayLayers = 1;
imgInfo.samples = msaaSamples_;
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imgInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
VmaAllocationCreateInfo allocInfo{};
allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
allocInfo.preferredFlags = VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT;
if (vmaCreateImage(allocator, &imgInfo, &allocInfo, &msaaColorImage_, &msaaColorAllocation_, nullptr) != VK_SUCCESS) {
// Retry without TRANSIENT (some drivers reject it at high sample counts)
imgInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
allocInfo.preferredFlags = 0;
if (vmaCreateImage(allocator, &imgInfo, &allocInfo, &msaaColorImage_, &msaaColorAllocation_, nullptr) != VK_SUCCESS) {
LOG_ERROR("Failed to create MSAA color image");
return false;
}
}
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = msaaColorImage_;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = swapchainFormat;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(device, &viewInfo, nullptr, &msaaColorView_) != VK_SUCCESS) {
LOG_ERROR("Failed to create MSAA color image view");
return false;
}
return true;
}
void VkContext::destroyMsaaColorImage() {
if (msaaColorView_) { vkDestroyImageView(device, msaaColorView_, nullptr); msaaColorView_ = VK_NULL_HANDLE; }
if (msaaColorImage_) { vmaDestroyImage(allocator, msaaColorImage_, msaaColorAllocation_); msaaColorImage_ = VK_NULL_HANDLE; msaaColorAllocation_ = VK_NULL_HANDLE; }
}
bool VkContext::createDepthResolveImage() {
if (msaaSamples_ == VK_SAMPLE_COUNT_1_BIT || !depthResolveSupported_) return true;
VkImageCreateInfo imgInfo{};
imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imgInfo.imageType = VK_IMAGE_TYPE_2D;
imgInfo.format = depthFormat;
imgInfo.extent = {swapchainExtent.width, swapchainExtent.height, 1};
imgInfo.mipLevels = 1;
imgInfo.arrayLayers = 1;
imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imgInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
VmaAllocationCreateInfo allocInfo{};
allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
if (vmaCreateImage(allocator, &imgInfo, &allocInfo, &depthResolveImage, &depthResolveAllocation, nullptr) != VK_SUCCESS) {
LOG_ERROR("Failed to create depth resolve image");
return false;
}
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = depthResolveImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = depthFormat;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(device, &viewInfo, nullptr, &depthResolveImageView) != VK_SUCCESS) {
LOG_ERROR("Failed to create depth resolve image view");
return false;
}
return true;
}
void VkContext::destroyDepthResolveImage() {
if (depthResolveImageView) {
vkDestroyImageView(device, depthResolveImageView, nullptr);
depthResolveImageView = VK_NULL_HANDLE;
}
if (depthResolveImage) {
vmaDestroyImage(allocator, depthResolveImage, depthResolveAllocation);
depthResolveImage = VK_NULL_HANDLE;
depthResolveAllocation = VK_NULL_HANDLE;
}
}
VkSampleCountFlagBits VkContext::getMaxUsableSampleCount() const {
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(physicalDevice, &props);
VkSampleCountFlags counts = props.limits.framebufferColorSampleCounts
& props.limits.framebufferDepthSampleCounts;
if (counts & VK_SAMPLE_COUNT_8_BIT) return VK_SAMPLE_COUNT_8_BIT;
if (counts & VK_SAMPLE_COUNT_4_BIT) return VK_SAMPLE_COUNT_4_BIT;
if (counts & VK_SAMPLE_COUNT_2_BIT) return VK_SAMPLE_COUNT_2_BIT;
return VK_SAMPLE_COUNT_1_BIT;
}
void VkContext::setMsaaSamples(VkSampleCountFlagBits samples) {
// Clamp to max supported
VkSampleCountFlagBits maxSamples = getMaxUsableSampleCount();
if (samples > maxSamples) samples = maxSamples;
msaaSamples_ = samples;
swapchainDirty = true;
}
bool VkContext::createImGuiResources() {
// Create depth buffer first
if (!createDepthBuffer()) return false;
// Create MSAA color image if needed
if (!createMsaaColorImage()) return false;
// Create single-sample depth resolve image for MSAA path (if supported)
if (!createDepthResolveImage()) return false;
bool useMsaa = (msaaSamples_ > VK_SAMPLE_COUNT_1_BIT);
if (useMsaa) {
const bool useDepthResolve = (depthResolveImageView != VK_NULL_HANDLE);
// MSAA render pass: 3 or 4 attachments
VkAttachmentDescription attachments[4] = {};
// Attachment 0: MSAA color target
attachments[0].format = swapchainFormat;
attachments[0].samples = msaaSamples_;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
// Attachment 1: Depth (multisampled)
attachments[1].format = depthFormat;
attachments[1].samples = msaaSamples_;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
// Attachment 2: Resolve target (swapchain image)
attachments[2].format = swapchainFormat;
attachments[2].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[2].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[2].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[2].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[2].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[2].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[2].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
if (useDepthResolve) {
attachments[3].format = depthFormat;
attachments[3].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[3].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[3].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[3].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[3].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[3].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[3].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
}
if (useDepthResolve) {
VkAttachmentDescription2 attachments2[4]{};
for (int i = 0; i < 4; ++i) {
attachments2[i].sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2;
attachments2[i].format = attachments[i].format;
attachments2[i].samples = attachments[i].samples;
attachments2[i].loadOp = attachments[i].loadOp;
attachments2[i].storeOp = attachments[i].storeOp;
attachments2[i].stencilLoadOp = attachments[i].stencilLoadOp;
attachments2[i].stencilStoreOp = attachments[i].stencilStoreOp;
attachments2[i].initialLayout = attachments[i].initialLayout;
attachments2[i].finalLayout = attachments[i].finalLayout;
}
VkAttachmentReference2 colorRef2{};
colorRef2.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2;
colorRef2.attachment = 0;
colorRef2.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference2 depthRef2{};
depthRef2.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2;
depthRef2.attachment = 1;
depthRef2.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference2 resolveRef2{};
resolveRef2.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2;
resolveRef2.attachment = 2;
resolveRef2.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference2 depthResolveRef2{};
depthResolveRef2.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2;
depthResolveRef2.attachment = 3;
depthResolveRef2.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkSubpassDescriptionDepthStencilResolve dsResolve{};
dsResolve.sType = VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_DEPTH_STENCIL_RESOLVE;
dsResolve.depthResolveMode = depthResolveMode_;
dsResolve.stencilResolveMode = VK_RESOLVE_MODE_NONE;
dsResolve.pDepthStencilResolveAttachment = &depthResolveRef2;
VkSubpassDescription2 subpass2{};
subpass2.sType = VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2;
subpass2.pNext = &dsResolve;
subpass2.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass2.colorAttachmentCount = 1;
subpass2.pColorAttachments = &colorRef2;
subpass2.pDepthStencilAttachment = &depthRef2;
subpass2.pResolveAttachments = &resolveRef2;
VkSubpassDependency2 dep2{};
dep2.sType = VK_STRUCTURE_TYPE_SUBPASS_DEPENDENCY_2;
dep2.srcSubpass = VK_SUBPASS_EXTERNAL;
dep2.dstSubpass = 0;
dep2.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dep2.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dep2.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo2 rpInfo2{};
rpInfo2.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2;
rpInfo2.attachmentCount = 4;
rpInfo2.pAttachments = attachments2;
rpInfo2.subpassCount = 1;
rpInfo2.pSubpasses = &subpass2;
rpInfo2.dependencyCount = 1;
rpInfo2.pDependencies = &dep2;
if (vkCreateRenderPass2(device, &rpInfo2, nullptr, &imguiRenderPass) != VK_SUCCESS) {
LOG_ERROR("Failed to create MSAA render pass (depth resolve)");
return false;
}
} else {
VkAttachmentReference colorRef{};
colorRef.attachment = 0;
colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference depthRef{};
depthRef.attachment = 1;
depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference resolveRef{};
resolveRef.attachment = 2;
resolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
subpass.pDepthStencilAttachment = &depthRef;
subpass.pResolveAttachments = &resolveRef;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rpInfo{};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpInfo.attachmentCount = 3;
rpInfo.pAttachments = attachments;
rpInfo.subpassCount = 1;
rpInfo.pSubpasses = &subpass;
rpInfo.dependencyCount = 1;
rpInfo.pDependencies = &dependency;
if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) {
LOG_ERROR("Failed to create MSAA render pass");
return false;
}
}
// Framebuffers: [msaaColorView, depthView, swapchainView, depthResolveView?]
swapchainFramebuffers.resize(swapchainImageViews.size());
for (size_t i = 0; i < swapchainImageViews.size(); i++) {
VkImageView fbAttachments[4] = {msaaColorView_, depthImageView, swapchainImageViews[i], depthResolveImageView};
VkFramebufferCreateInfo fbInfo{};
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbInfo.renderPass = imguiRenderPass;
fbInfo.attachmentCount = useDepthResolve ? 4 : 3;
fbInfo.pAttachments = fbAttachments;
fbInfo.width = swapchainExtent.width;
fbInfo.height = swapchainExtent.height;
fbInfo.layers = 1;
if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) {
LOG_ERROR("Failed to create MSAA swapchain framebuffer ", i);
return false;
}
}
} else {
// Non-MSAA render pass: 2 attachments (color + depth) — original path
VkAttachmentDescription attachments[2] = {};
// Color attachment (swapchain image)
attachments[0].format = swapchainFormat;
attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
// Depth attachment
attachments[1].format = depthFormat;
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference colorRef{};
colorRef.attachment = 0;
colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference depthRef{};
depthRef.attachment = 1;
depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
subpass.pDepthStencilAttachment = &depthRef;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rpInfo{};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpInfo.attachmentCount = 2;
rpInfo.pAttachments = attachments;
rpInfo.subpassCount = 1;
rpInfo.pSubpasses = &subpass;
rpInfo.dependencyCount = 1;
rpInfo.pDependencies = &dependency;
if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) {
LOG_ERROR("Failed to create render pass");
return false;
}
// Framebuffers: [swapchainView, depthView]
swapchainFramebuffers.resize(swapchainImageViews.size());
for (size_t i = 0; i < swapchainImageViews.size(); i++) {
VkImageView fbAttachments[2] = {swapchainImageViews[i], depthImageView};
VkFramebufferCreateInfo fbInfo{};
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbInfo.renderPass = imguiRenderPass;
fbInfo.attachmentCount = 2;
fbInfo.pAttachments = fbAttachments;
fbInfo.width = swapchainExtent.width;
fbInfo.height = swapchainExtent.height;
fbInfo.layers = 1;
if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) {
LOG_ERROR("Failed to create swapchain framebuffer ", i);
return false;
}
}
}
// Create descriptor pool for ImGui
VkDescriptorPoolSize poolSizes[] = {
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 100},
};
VkDescriptorPoolCreateInfo dpInfo{};
dpInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
dpInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
dpInfo.maxSets = 100;
dpInfo.poolSizeCount = 1;
dpInfo.pPoolSizes = poolSizes;
if (vkCreateDescriptorPool(device, &dpInfo, nullptr, &imguiDescriptorPool) != VK_SUCCESS) {
LOG_ERROR("Failed to create ImGui descriptor pool");
return false;
}
return true;
}
void VkContext::destroyImGuiResources() {
// Destroy uploaded UI textures
for (auto& tex : uiTextures_) {
if (tex.view) vkDestroyImageView(device, tex.view, nullptr);
if (tex.image) vkDestroyImage(device, tex.image, nullptr);
if (tex.memory) vkFreeMemory(device, tex.memory, nullptr);
}
uiTextures_.clear();
if (uiTextureSampler_) {
vkDestroySampler(device, uiTextureSampler_, nullptr);
uiTextureSampler_ = VK_NULL_HANDLE;
}
if (imguiDescriptorPool) {
vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr);
imguiDescriptorPool = VK_NULL_HANDLE;
}
destroyMsaaColorImage();
destroyDepthResolveImage();
destroyDepthBuffer();
// Framebuffers are destroyed in destroySwapchain()
if (imguiRenderPass) {
vkDestroyRenderPass(device, imguiRenderPass, nullptr);
imguiRenderPass = VK_NULL_HANDLE;
}
}
static uint32_t findMemType(VkPhysicalDevice physDev, uint32_t typeFilter, VkMemoryPropertyFlags props) {
VkPhysicalDeviceMemoryProperties memProps;
vkGetPhysicalDeviceMemoryProperties(physDev, &memProps);
for (uint32_t i = 0; i < memProps.memoryTypeCount; i++) {
if ((typeFilter & (1 << i)) && (memProps.memoryTypes[i].propertyFlags & props) == props)
return i;
}
return 0;
}
VkDescriptorSet VkContext::uploadImGuiTexture(const uint8_t* rgba, int width, int height) {
if (!device || !physicalDevice || width <= 0 || height <= 0 || !rgba)
return VK_NULL_HANDLE;
VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * height * 4;
// Create shared sampler on first call
if (!uiTextureSampler_) {
VkSamplerCreateInfo si{};
si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
si.magFilter = VK_FILTER_LINEAR;
si.minFilter = VK_FILTER_LINEAR;
si.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
si.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
if (vkCreateSampler(device, &si, nullptr, &uiTextureSampler_) != VK_SUCCESS) {
LOG_ERROR("Failed to create UI texture sampler");
return VK_NULL_HANDLE;
}
}
// Staging buffer
VkBuffer stagingBuffer;
VkDeviceMemory stagingMemory;
{
VkBufferCreateInfo bufInfo{};
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufInfo.size = imageSize;
bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device, &bufInfo, nullptr, &stagingBuffer) != VK_SUCCESS)
return VK_NULL_HANDLE;
VkMemoryRequirements memReqs;
vkGetBufferMemoryRequirements(device, stagingBuffer, &memReqs);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memReqs.size;
allocInfo.memoryTypeIndex = findMemType(physicalDevice, memReqs.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory) != VK_SUCCESS) {
vkDestroyBuffer(device, stagingBuffer, nullptr);
return VK_NULL_HANDLE;
}
vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0);
void* mapped;
vkMapMemory(device, stagingMemory, 0, imageSize, 0, &mapped);
memcpy(mapped, rgba, imageSize);
vkUnmapMemory(device, stagingMemory);
}
// Create image
VkImage image;
VkDeviceMemory imageMemory;
{
VkImageCreateInfo imgInfo{};
imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imgInfo.imageType = VK_IMAGE_TYPE_2D;
imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imgInfo.extent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
imgInfo.mipLevels = 1;
imgInfo.arrayLayers = 1;
imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imgInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vkCreateImage(device, &imgInfo, nullptr, &image) != VK_SUCCESS) {
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingMemory, nullptr);
return VK_NULL_HANDLE;
}
VkMemoryRequirements memReqs;
vkGetImageMemoryRequirements(device, image, &memReqs);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memReqs.size;
allocInfo.memoryTypeIndex = findMemType(physicalDevice, memReqs.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) {
vkDestroyImage(device, image, nullptr);
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingMemory, nullptr);
return VK_NULL_HANDLE;
}
vkBindImageMemory(device, image, imageMemory, 0);
}
// Upload via immediate submit
immediateSubmit([&](VkCommandBuffer cmd) {
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
VkBufferImageCopy region{};
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.imageExtent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
vkCmdCopyBufferToImage(cmd, stagingBuffer, image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
});
// Cleanup staging
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingMemory, nullptr);
// Create image view
VkImageView imageView;
{
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
vkDestroyImage(device, image, nullptr);
vkFreeMemory(device, imageMemory, nullptr);
return VK_NULL_HANDLE;
}
}
// Register with ImGui
VkDescriptorSet ds = ImGui_ImplVulkan_AddTexture(uiTextureSampler_, imageView,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
// Track for cleanup
uiTextures_.push_back({image, imageMemory, imageView});
return ds;
}
bool VkContext::recreateSwapchain(int width, int height) {
vkDeviceWaitIdle(device);
// Destroy old framebuffers
for (auto fb : swapchainFramebuffers) {
if (fb) vkDestroyFramebuffer(device, fb, nullptr);
}
swapchainFramebuffers.clear();
// Destroy old image views
for (auto iv : swapchainImageViews) {
if (iv) vkDestroyImageView(device, iv, nullptr);
}
swapchainImageViews.clear();
VkSwapchainKHR oldSwapchain = swapchain;
vkb::SwapchainBuilder swapchainBuilder{physicalDevice, device, surface};
auto swapRet = swapchainBuilder
.set_desired_format({VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR})
.set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR)
.set_desired_extent(static_cast<uint32_t>(width), static_cast<uint32_t>(height))
.set_image_usage_flags(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)
.set_desired_min_image_count(2)
.set_old_swapchain(oldSwapchain)
.build();
if (oldSwapchain) {
vkDestroySwapchainKHR(device, oldSwapchain, nullptr);
}
if (!swapRet) {
LOG_ERROR("Failed to recreate swapchain: ", swapRet.error().message());
swapchain = VK_NULL_HANDLE;
return false;
}
auto vkbSwap = swapRet.value();
swapchain = vkbSwap.swapchain;
swapchainFormat = vkbSwap.image_format;
swapchainExtent = vkbSwap.extent;
swapchainImages = vkbSwap.get_images().value();
swapchainImageViews = vkbSwap.get_image_views().value();
// Recreate depth buffer + MSAA color image + depth resolve image
destroyMsaaColorImage();
destroyDepthResolveImage();
destroyDepthBuffer();
// Destroy old render pass (needs recreation if MSAA changed)
if (imguiRenderPass) {
vkDestroyRenderPass(device, imguiRenderPass, nullptr);
imguiRenderPass = VK_NULL_HANDLE;
}
if (!createDepthBuffer()) return false;
if (!createMsaaColorImage()) return false;
if (!createDepthResolveImage()) return false;
bool useMsaa = (msaaSamples_ > VK_SAMPLE_COUNT_1_BIT);
if (useMsaa) {
const bool useDepthResolve = (depthResolveImageView != VK_NULL_HANDLE);
// MSAA render pass: 3 or 4 attachments
VkAttachmentDescription attachments[4] = {};
attachments[0].format = swapchainFormat;
attachments[0].samples = msaaSamples_;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachments[1].format = depthFormat;
attachments[1].samples = msaaSamples_;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
attachments[2].format = swapchainFormat;
attachments[2].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[2].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[2].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[2].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[2].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[2].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[2].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
if (useDepthResolve) {
attachments[3].format = depthFormat;
attachments[3].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[3].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[3].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[3].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[3].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[3].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[3].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
}
if (useDepthResolve) {
VkAttachmentDescription2 attachments2[4]{};
for (int i = 0; i < 4; ++i) {
attachments2[i].sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2;
attachments2[i].format = attachments[i].format;
attachments2[i].samples = attachments[i].samples;
attachments2[i].loadOp = attachments[i].loadOp;
attachments2[i].storeOp = attachments[i].storeOp;
attachments2[i].stencilLoadOp = attachments[i].stencilLoadOp;
attachments2[i].stencilStoreOp = attachments[i].stencilStoreOp;
attachments2[i].initialLayout = attachments[i].initialLayout;
attachments2[i].finalLayout = attachments[i].finalLayout;
}
VkAttachmentReference2 colorRef2{};
colorRef2.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2;
colorRef2.attachment = 0;
colorRef2.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference2 depthRef2{};
depthRef2.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2;
depthRef2.attachment = 1;
depthRef2.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference2 resolveRef2{};
resolveRef2.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2;
resolveRef2.attachment = 2;
resolveRef2.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference2 depthResolveRef2{};
depthResolveRef2.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2;
depthResolveRef2.attachment = 3;
depthResolveRef2.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkSubpassDescriptionDepthStencilResolve dsResolve{};
dsResolve.sType = VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_DEPTH_STENCIL_RESOLVE;
dsResolve.depthResolveMode = depthResolveMode_;
dsResolve.stencilResolveMode = VK_RESOLVE_MODE_NONE;
dsResolve.pDepthStencilResolveAttachment = &depthResolveRef2;
VkSubpassDescription2 subpass2{};
subpass2.sType = VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2;
subpass2.pNext = &dsResolve;
subpass2.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass2.colorAttachmentCount = 1;
subpass2.pColorAttachments = &colorRef2;
subpass2.pDepthStencilAttachment = &depthRef2;
subpass2.pResolveAttachments = &resolveRef2;
VkSubpassDependency2 dep2{};
dep2.sType = VK_STRUCTURE_TYPE_SUBPASS_DEPENDENCY_2;
dep2.srcSubpass = VK_SUBPASS_EXTERNAL;
dep2.dstSubpass = 0;
dep2.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dep2.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dep2.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo2 rpInfo2{};
rpInfo2.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2;
rpInfo2.attachmentCount = 4;
rpInfo2.pAttachments = attachments2;
rpInfo2.subpassCount = 1;
rpInfo2.pSubpasses = &subpass2;
rpInfo2.dependencyCount = 1;
rpInfo2.pDependencies = &dep2;
if (vkCreateRenderPass2(device, &rpInfo2, nullptr, &imguiRenderPass) != VK_SUCCESS) {
LOG_ERROR("Failed to recreate MSAA render pass (depth resolve)");
return false;
}
} else {
VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkAttachmentReference resolveRef{2, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
subpass.pDepthStencilAttachment = &depthRef;
subpass.pResolveAttachments = &resolveRef;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rpInfo{};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpInfo.attachmentCount = 3;
rpInfo.pAttachments = attachments;
rpInfo.subpassCount = 1;
rpInfo.pSubpasses = &subpass;
rpInfo.dependencyCount = 1;
rpInfo.pDependencies = &dependency;
if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) {
LOG_ERROR("Failed to recreate MSAA render pass");
return false;
}
}
swapchainFramebuffers.resize(swapchainImageViews.size());
for (size_t i = 0; i < swapchainImageViews.size(); i++) {
VkImageView fbAttachments[4] = {msaaColorView_, depthImageView, swapchainImageViews[i], depthResolveImageView};
VkFramebufferCreateInfo fbInfo{};
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbInfo.renderPass = imguiRenderPass;
fbInfo.attachmentCount = useDepthResolve ? 4 : 3;
fbInfo.pAttachments = fbAttachments;
fbInfo.width = swapchainExtent.width;
fbInfo.height = swapchainExtent.height;
fbInfo.layers = 1;
if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) {
LOG_ERROR("Failed to recreate MSAA swapchain framebuffer ", i);
return false;
}
}
} else {
// Non-MSAA render pass: 2 attachments
VkAttachmentDescription attachments[2] = {};
attachments[0].format = swapchainFormat;
attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
attachments[1].format = depthFormat;
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
subpass.pDepthStencilAttachment = &depthRef;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rpInfo{};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpInfo.attachmentCount = 2;
rpInfo.pAttachments = attachments;
rpInfo.subpassCount = 1;
rpInfo.pSubpasses = &subpass;
rpInfo.dependencyCount = 1;
rpInfo.pDependencies = &dependency;
if (vkCreateRenderPass(device, &rpInfo, nullptr, &imguiRenderPass) != VK_SUCCESS) {
LOG_ERROR("Failed to recreate render pass");
return false;
}
swapchainFramebuffers.resize(swapchainImageViews.size());
for (size_t i = 0; i < swapchainImageViews.size(); i++) {
VkImageView fbAttachments[2] = {swapchainImageViews[i], depthImageView};
VkFramebufferCreateInfo fbInfo{};
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbInfo.renderPass = imguiRenderPass;
fbInfo.attachmentCount = 2;
fbInfo.pAttachments = fbAttachments;
fbInfo.width = swapchainExtent.width;
fbInfo.height = swapchainExtent.height;
fbInfo.layers = 1;
if (vkCreateFramebuffer(device, &fbInfo, nullptr, &swapchainFramebuffers[i]) != VK_SUCCESS) {
LOG_ERROR("Failed to recreate swapchain framebuffer ", i);
return false;
}
}
}
swapchainDirty = false;
LOG_INFO("Swapchain recreated: ", swapchainExtent.width, "x", swapchainExtent.height);
return true;
}
VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) {
auto& frame = frames[currentFrame];
// Wait for this frame's fence (with timeout to detect GPU hangs)
static int beginFrameCounter = 0;
beginFrameCounter++;
VkResult fenceResult = vkWaitForFences(device, 1, &frame.inFlightFence, VK_TRUE, 5000000000ULL); // 5 second timeout
if (fenceResult == VK_TIMEOUT) {
LOG_ERROR("beginFrame[", beginFrameCounter, "] FENCE TIMEOUT (5s) on frame slot ", currentFrame, " — GPU hang detected!");
return VK_NULL_HANDLE;
}
if (fenceResult != VK_SUCCESS) {
LOG_ERROR("beginFrame[", beginFrameCounter, "] fence wait failed: ", (int)fenceResult);
return VK_NULL_HANDLE;
}
// Acquire next swapchain image
VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX,
frame.imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
swapchainDirty = true;
return VK_NULL_HANDLE;
}
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
LOG_ERROR("Failed to acquire swapchain image");
return VK_NULL_HANDLE;
}
vkResetFences(device, 1, &frame.inFlightFence);
vkResetCommandBuffer(frame.commandBuffer, 0);
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(frame.commandBuffer, &beginInfo);
return frame.commandBuffer;
}
void VkContext::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) {
static int endFrameCounter = 0;
endFrameCounter++;
VkResult endResult = vkEndCommandBuffer(cmd);
if (endResult != VK_SUCCESS) {
LOG_ERROR("endFrame[", endFrameCounter, "] vkEndCommandBuffer FAILED: ", (int)endResult);
}
auto& frame = frames[currentFrame];
VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &frame.imageAvailableSemaphore;
submitInfo.pWaitDstStageMask = &waitStage;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &cmd;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &frame.renderFinishedSemaphore;
VkResult submitResult = vkQueueSubmit(graphicsQueue, 1, &submitInfo, frame.inFlightFence);
if (submitResult != VK_SUCCESS) {
LOG_ERROR("endFrame[", endFrameCounter, "] vkQueueSubmit FAILED: ", (int)submitResult);
}
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = &frame.renderFinishedSemaphore;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapchain;
presentInfo.pImageIndices = &imageIndex;
VkResult result = vkQueuePresentKHR(presentQueue, &presentInfo);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
swapchainDirty = true;
}
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}
VkCommandBuffer VkContext::beginSingleTimeCommands() {
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = immCommandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer cmd;
vkAllocateCommandBuffers(device, &allocInfo, &cmd);
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(cmd, &beginInfo);
return cmd;
}
void VkContext::endSingleTimeCommands(VkCommandBuffer cmd) {
vkEndCommandBuffer(cmd);
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &cmd;
vkQueueSubmit(graphicsQueue, 1, &submitInfo, immFence);
vkWaitForFences(device, 1, &immFence, VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &immFence);
vkFreeCommandBuffers(device, immCommandPool, 1, &cmd);
}
void VkContext::immediateSubmit(std::function<void(VkCommandBuffer cmd)>&& function) {
VkCommandBuffer cmd = beginSingleTimeCommands();
function(cmd);
endSingleTimeCommands(cmd);
}
} // namespace rendering
} // namespace wowee