From d0832ca558c66358092a97083a082c1eb0302151 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 25 Jul 2023 13:48:07 -0700 Subject: [PATCH] Fix support for headless vulkan context --- include/renderer_vk/renderer_vk.hpp | 14 +- src/core/renderer_vk/renderer_vk.cpp | 337 +++++++++++++++------------ 2 files changed, 192 insertions(+), 159 deletions(-) diff --git a/include/renderer_vk/renderer_vk.hpp b/include/renderer_vk/renderer_vk.hpp index 59d8cdae..6ebbcb7e 100644 --- a/include/renderer_vk/renderer_vk.hpp +++ b/include/renderer_vk/renderer_vk.hpp @@ -32,9 +32,14 @@ class RendererVK final : public Renderer { std::vector swapchainImages = {}; std::vector swapchainImageViews = {}; - // Per-swapchain-image data - // Each vector is `swapchainImageCount` in size - std::vector presentCommandBuffers = {}; + // This value is the degree of parallelism to allow multiple frames to be in-flight + // aka: "double-buffer"/"triple-buffering" + // Todo: make this a configuration option + static constexpr usize frameBufferingCount = 3; + + // Frame-buffering data + // Each vector is `frameBufferingCount` in size + std::vector frameCommandBuffers = {}; std::vector swapImageFreeSemaphore = {}; std::vector renderFinishedSemaphore = {}; std::vector frameFinishedFences = {}; @@ -42,7 +47,8 @@ class RendererVK final : public Renderer { // Recreate the swapchain, possibly re-using the old one in the case of a resize vk::Result recreateSwapchain(vk::SurfaceKHR surface, vk::Extent2D swapchainExtent); - u64 currentFrame = 0; + u64 frameBufferingIndex = 0; + public: RendererVK(GPU& gpu, const std::array& internalRegs, const std::array& externalRegs); ~RendererVK() override; diff --git a/src/core/renderer_vk/renderer_vk.cpp b/src/core/renderer_vk/renderer_vk.cpp index 92ce12e4..4b6956d5 100644 --- a/src/core/renderer_vk/renderer_vk.cpp +++ b/src/core/renderer_vk/renderer_vk.cpp @@ -155,48 +155,6 @@ vk::Result RendererVK::recreateSwapchain(vk::SurfaceKHR surface, vk::Extent2D sw Helpers::panic("Error creating acquiring swapchain images: %s\n", vk::to_string(getResult.result).c_str()); } - // Swapchain Command buffer(s) - vk::CommandBufferAllocateInfo commandBuffersInfo = {}; - commandBuffersInfo.commandPool = commandPool.get(); - commandBuffersInfo.level = vk::CommandBufferLevel::ePrimary; - commandBuffersInfo.commandBufferCount = swapchainImageCount; - - if (auto allocateResult = device->allocateCommandBuffersUnique(commandBuffersInfo); allocateResult.result == vk::Result::eSuccess) { - presentCommandBuffers = std::move(allocateResult.value); - } else { - Helpers::panic("Error allocating command buffer: %s\n", vk::to_string(allocateResult.result).c_str()); - } - - // Swapchain synchronization primitives - vk::FenceCreateInfo fenceInfo = {}; - fenceInfo.flags = vk::FenceCreateFlagBits::eSignaled; - - vk::SemaphoreCreateInfo semaphoreInfo = {}; - - swapImageFreeSemaphore.resize(swapchainImageCount); - renderFinishedSemaphore.resize(swapchainImageCount); - frameFinishedFences.resize(swapchainImageCount); - - for (usize i = 0; i < swapchainImageCount; i++) { - if (auto createResult = device->createSemaphoreUnique(semaphoreInfo); createResult.result == vk::Result::eSuccess) { - swapImageFreeSemaphore[i] = std::move(createResult.value); - } else { - Helpers::panic("Error creating 'present-ready' semaphore: %s\n", vk::to_string(createResult.result).c_str()); - } - - if (auto createResult = device->createSemaphoreUnique(semaphoreInfo); createResult.result == vk::Result::eSuccess) { - renderFinishedSemaphore[i] = std::move(createResult.value); - } else { - Helpers::panic("Error creating 'post-render' semaphore: %s\n", vk::to_string(createResult.result).c_str()); - } - - if (auto createResult = device->createFenceUnique(fenceInfo); createResult.result == vk::Result::eSuccess) { - frameFinishedFences[i] = std::move(createResult.value); - } else { - Helpers::panic("Error creating 'present-ready' semaphore: %s\n", vk::to_string(createResult.result).c_str()); - } - } - return vk::Result::eSuccess; } @@ -208,133 +166,143 @@ RendererVK::~RendererVK() {} void RendererVK::reset() {} void RendererVK::display() { - // Block, on the CPU, to ensure that this swapchain-frame is ready for more work - if (auto waitResult = device->waitForFences({frameFinishedFences[currentFrame].get()}, true, std::numeric_limits::max()); + // Block, on the CPU, to ensure that this frame-buffering-frame is ready for more work + if (auto waitResult = device->waitForFences({frameFinishedFences[frameBufferingIndex].get()}, true, std::numeric_limits::max()); waitResult != vk::Result::eSuccess) { Helpers::panic("Error waiting on swapchain fence: %s\n", vk::to_string(waitResult).c_str()); } - u32 swapchainImageIndex = std::numeric_limits::max(); - if (const auto acquireResult = - device->acquireNextImageKHR(swapchain.get(), std::numeric_limits::max(), swapImageFreeSemaphore[currentFrame].get(), {}); - acquireResult.result == vk::Result::eSuccess) { - swapchainImageIndex = acquireResult.value; - } else { - switch (acquireResult.result) { - case vk::Result::eSuboptimalKHR: - case vk::Result::eErrorOutOfDateKHR: { - // Surface resized - vk::Extent2D swapchainExtent; - { - int windowWidth, windowHeight; - // Block until we have a valid surface-area to present to - // Usually this is because the window has been minimized - // Todo: We should still be rendering even without a valid swapchain - do { - SDL_Vulkan_GetDrawableSize(targetWindow, &windowWidth, &windowHeight); - } while (!windowWidth || !windowHeight); - swapchainExtent.width = windowWidth; - swapchainExtent.height = windowHeight; + // Get the next available swapchain image, and signal the semaphore when it's ready + static constexpr u32 swapchainImageInvalid = std::numeric_limits::max(); + u32 swapchainImageIndex = swapchainImageInvalid; + if (swapchain) { + if (const auto acquireResult = + device->acquireNextImageKHR(swapchain.get(), std::numeric_limits::max(), swapImageFreeSemaphore[frameBufferingIndex].get(), {}); + acquireResult.result == vk::Result::eSuccess) { + swapchainImageIndex = acquireResult.value; + } else { + switch (acquireResult.result) { + case vk::Result::eSuboptimalKHR: + case vk::Result::eErrorOutOfDateKHR: { + // Surface resized + vk::Extent2D swapchainExtent; + { + int windowWidth, windowHeight; + // Block until we have a valid surface-area to present to + // Usually this is because the window has been minimized + // Todo: We should still be rendering even without a valid swapchain + do { + SDL_Vulkan_GetDrawableSize(targetWindow, &windowWidth, &windowHeight); + } while (!windowWidth || !windowHeight); + swapchainExtent.width = windowWidth; + swapchainExtent.height = windowHeight; + } + recreateSwapchain(surface.get(), swapchainExtent); + break; + } + default: { + Helpers::panic("Error acquiring next swapchain image: %s\n", vk::to_string(acquireResult.result).c_str()); } - recreateSwapchain(surface.get(), swapchainExtent); - break; - } - default: { - Helpers::panic("Error acquiring next swapchain image: %s\n", vk::to_string(acquireResult.result).c_str()); } } } - vk::UniqueCommandBuffer& presentCommandBuffer = presentCommandBuffers.at(currentFrame); + vk::UniqueCommandBuffer& frameCommandBuffer = frameCommandBuffers.at(frameBufferingIndex); vk::CommandBufferBeginInfo beginInfo = {}; beginInfo.flags = vk::CommandBufferUsageFlagBits::eSimultaneousUse; - if (const vk::Result beginResult = presentCommandBuffer->begin(beginInfo); beginResult != vk::Result::eSuccess) { + if (const vk::Result beginResult = frameCommandBuffer->begin(beginInfo); beginResult != vk::Result::eSuccess) { Helpers::panic("Error beginning command buffer recording: %s\n", vk::to_string(beginResult).c_str()); } { - static const std::array presentScopeColor = {{1.0f, 0.0f, 1.0f, 1.0f}}; + static const std::array frameScopeColor = {{1.0f, 0.0f, 1.0f, 1.0f}}; - Vulkan::DebugLabelScope debugScope(presentCommandBuffer.get(), presentScopeColor, "Present"); + Vulkan::DebugLabelScope debugScope(frameCommandBuffer.get(), frameScopeColor, "Frame"); // Prepare for color-clear - presentCommandBuffer->pipelineBarrier( - vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags(), {}, {}, - {vk::ImageMemoryBarrier( - vk::AccessFlagBits::eMemoryRead, vk::AccessFlagBits::eTransferWrite, vk::ImageLayout::eUndefined, - vk::ImageLayout::eTransferDstOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, swapchainImages[swapchainImageIndex], - vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) - )} - ); + if (swapchainImageIndex != swapchainImageInvalid) { + frameCommandBuffer->pipelineBarrier( + vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags(), {}, {}, + {vk::ImageMemoryBarrier( + vk::AccessFlagBits::eMemoryRead, vk::AccessFlagBits::eTransferWrite, vk::ImageLayout::eUndefined, + vk::ImageLayout::eTransferDstOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, swapchainImages[swapchainImageIndex], + vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) + )} + ); - presentCommandBuffer->clearColorImage( - swapchainImages[swapchainImageIndex], vk::ImageLayout::eTransferDstOptimal, presentScopeColor, - vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) - ); - - // Prepare for present - presentCommandBuffer->pipelineBarrier( - vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::DependencyFlags(), {}, {}, - {vk::ImageMemoryBarrier( - vk::AccessFlagBits::eNone, vk::AccessFlagBits::eColorAttachmentWrite, vk::ImageLayout::eTransferDstOptimal, - vk::ImageLayout::ePresentSrcKHR, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, swapchainImages[swapchainImageIndex], + static const std::array clearColor = {{0.0f, 0.0f, 0.0f, 1.0f}}; + frameCommandBuffer->clearColorImage( + swapchainImages[swapchainImageIndex], vk::ImageLayout::eTransferDstOptimal, clearColor, vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) - )} - ); + ); + + // Prepare for present + frameCommandBuffer->pipelineBarrier( + vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::DependencyFlags(), {}, {}, + {vk::ImageMemoryBarrier( + vk::AccessFlagBits::eNone, vk::AccessFlagBits::eColorAttachmentWrite, vk::ImageLayout::eTransferDstOptimal, + vk::ImageLayout::ePresentSrcKHR, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, swapchainImages[swapchainImageIndex], + vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) + )} + ); + } } - if (const vk::Result endResult = presentCommandBuffer->end(); endResult != vk::Result::eSuccess) { + if (const vk::Result endResult = frameCommandBuffer->end(); endResult != vk::Result::eSuccess) { Helpers::panic("Error ending command buffer recording: %s\n", vk::to_string(endResult).c_str()); } vk::SubmitInfo submitInfo = {}; // Wait for any previous uses of the image image to finish presenting - submitInfo.setWaitSemaphores(swapImageFreeSemaphore[currentFrame].get()); + if (swapchainImageIndex != swapchainImageInvalid) { + submitInfo.setWaitSemaphores(swapImageFreeSemaphore[frameBufferingIndex].get()); + static const vk::PipelineStageFlags waitStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + submitInfo.setWaitDstStageMask(waitStageMask); + } // Signal when finished - submitInfo.setSignalSemaphores(renderFinishedSemaphore[currentFrame].get()); + submitInfo.setSignalSemaphores(renderFinishedSemaphore[frameBufferingIndex].get()); - static const vk::PipelineStageFlags waitStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; - submitInfo.setWaitDstStageMask(waitStageMask); + submitInfo.setCommandBuffers(frameCommandBuffer.get()); - submitInfo.setCommandBuffers(presentCommandBuffer.get()); + device->resetFences({frameFinishedFences[frameBufferingIndex].get()}); - device->resetFences({frameFinishedFences[currentFrame].get()}); - - if (const vk::Result submitResult = graphicsQueue.submit({submitInfo}, frameFinishedFences[currentFrame].get()); + if (const vk::Result submitResult = graphicsQueue.submit({submitInfo}, frameFinishedFences[frameBufferingIndex].get()); submitResult != vk::Result::eSuccess) { Helpers::panic("Error submitting to graphics queue: %s\n", vk::to_string(submitResult).c_str()); } - vk::PresentInfoKHR presentInfo = {}; - presentInfo.setWaitSemaphores(renderFinishedSemaphore[currentFrame].get()); - presentInfo.setSwapchains(swapchain.get()); - presentInfo.setImageIndices(swapchainImageIndex); + if (swapchainImageIndex != swapchainImageInvalid) { + vk::PresentInfoKHR presentInfo = {}; + presentInfo.setWaitSemaphores(renderFinishedSemaphore[frameBufferingIndex].get()); + presentInfo.setSwapchains(swapchain.get()); + presentInfo.setImageIndices(swapchainImageIndex); - if (const auto presentResult = presentQueue.presentKHR(presentInfo); presentResult == vk::Result::eSuccess) { - } else { - switch (presentResult) { - case vk::Result::eSuboptimalKHR: - case vk::Result::eErrorOutOfDateKHR: { - // Surface resized - vk::Extent2D swapchainExtent; - { - int windowWidth, windowHeight; - SDL_Vulkan_GetDrawableSize(targetWindow, &windowWidth, &windowHeight); - swapchainExtent.width = windowWidth; - swapchainExtent.height = windowHeight; + if (const auto presentResult = presentQueue.presentKHR(presentInfo); presentResult == vk::Result::eSuccess) { + } else { + switch (presentResult) { + case vk::Result::eSuboptimalKHR: + case vk::Result::eErrorOutOfDateKHR: { + // Surface resized + vk::Extent2D swapchainExtent; + { + int windowWidth, windowHeight; + SDL_Vulkan_GetDrawableSize(targetWindow, &windowWidth, &windowHeight); + swapchainExtent.width = windowWidth; + swapchainExtent.height = windowHeight; + } + recreateSwapchain(surface.get(), swapchainExtent); + break; + } + default: { + Helpers::panic("Error presenting swapchain image: %s\n", vk::to_string(presentResult).c_str()); } - recreateSwapchain(surface.get(), swapchainExtent); - break; - } - default: { - Helpers::panic("Error presenting swapchain image: %s\n", vk::to_string(presentResult).c_str()); } } } - currentFrame = ((currentFrame + 1) % swapchainImageCount); + frameBufferingIndex = ((frameBufferingIndex + 1) % frameBufferingCount); } void RendererVK::initGraphicsContext(SDL_Window* window) { @@ -365,11 +333,11 @@ void RendererVK::initGraphicsContext(SDL_Window* window) { }; // Get any additional extensions that SDL wants as well - { + if (targetWindow) { unsigned int extensionCount = 0; - SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr); + SDL_Vulkan_GetInstanceExtensions(targetWindow, &extensionCount, nullptr); std::vector sdlInstanceExtensions(extensionCount); - SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, sdlInstanceExtensions.data()); + SDL_Vulkan_GetInstanceExtensions(targetWindow, &extensionCount, sdlInstanceExtensions.data()); instanceExtensions.insert(instanceExtensions.end(), sdlInstanceExtensions.begin(), sdlInstanceExtensions.end()); } @@ -411,10 +379,12 @@ void RendererVK::initGraphicsContext(SDL_Window* window) { } // Create surface - if (VkSurfaceKHR newSurface; SDL_Vulkan_CreateSurface(window, instance.get(), &newSurface)) { - surface.reset(newSurface); - } else { - Helpers::warn("Error creating Vulkan surface"); + if (window) { + if (VkSurfaceKHR newSurface; SDL_Vulkan_CreateSurface(window, instance.get(), &newSurface)) { + surface.reset(newSurface); + } else { + Helpers::warn("Error creating Vulkan surface"); + } } // Pick physical device @@ -423,18 +393,20 @@ void RendererVK::initGraphicsContext(SDL_Window* window) { std::vector::iterator partitionEnd = physicalDevices.end(); // Prefer GPUs that can access the surface - const auto surfaceSupport = [this](const vk::PhysicalDevice& physicalDevice) -> bool { - const usize queueCount = physicalDevice.getQueueFamilyProperties().size(); - for (usize queueIndex = 0; queueIndex < queueCount; ++queueIndex) { - if (auto supportResult = physicalDevice.getSurfaceSupportKHR(queueIndex, surface.get()); - supportResult.result == vk::Result::eSuccess) { - return supportResult.value; + if (surface) { + const auto surfaceSupport = [this](const vk::PhysicalDevice& physicalDevice) -> bool { + const usize queueCount = physicalDevice.getQueueFamilyProperties().size(); + for (usize queueIndex = 0; queueIndex < queueCount; ++queueIndex) { + if (auto supportResult = physicalDevice.getSurfaceSupportKHR(queueIndex, surface.get()); + supportResult.result == vk::Result::eSuccess) { + return supportResult.value; + } } - } - return false; - }; + return false; + }; - partitionEnd = std::stable_partition(physicalDevices.begin(), partitionEnd, surfaceSupport); + partitionEnd = std::stable_partition(physicalDevices.begin(), partitionEnd, surfaceSupport); + } // Prefer Discrete GPUs const auto isDiscrete = [](const vk::PhysicalDevice& physicalDevice) -> bool { @@ -454,26 +426,32 @@ void RendererVK::initGraphicsContext(SDL_Window* window) { std::vector deviceQueueInfos; { const std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); - + std::unordered_set queueFamilyRequests; // Get present queue family - for (usize queueFamilyIndex = 0; queueFamilyIndex < queueFamilyProperties.size(); ++queueFamilyIndex) { - if (auto supportResult = physicalDevice.getSurfaceSupportKHR(queueFamilyIndex, surface.get()); - supportResult.result == vk::Result::eSuccess) { - if (supportResult.value) { - presentQueueFamily = queueFamilyIndex; - break; + if (surface) { + for (usize queueFamilyIndex = 0; queueFamilyIndex < queueFamilyProperties.size(); ++queueFamilyIndex) { + if (auto supportResult = physicalDevice.getSurfaceSupportKHR(queueFamilyIndex, surface.get()); + supportResult.result == vk::Result::eSuccess) { + if (supportResult.value) { + presentQueueFamily = queueFamilyIndex; + break; + } } } + queueFamilyRequests.emplace(presentQueueFamily); } static const float queuePriority = 1.0f; graphicsQueueFamily = findQueueFamily(queueFamilyProperties, vk::QueueFlagBits::eGraphics); + queueFamilyRequests.emplace(graphicsQueueFamily); computeQueueFamily = findQueueFamily(queueFamilyProperties, vk::QueueFlagBits::eCompute); + queueFamilyRequests.emplace(computeQueueFamily); transferQueueFamily = findQueueFamily(queueFamilyProperties, vk::QueueFlagBits::eTransfer); + queueFamilyRequests.emplace(transferQueueFamily); // Requests a singular queue for each unique queue-family - const std::unordered_set queueFamilyRequests = {presentQueueFamily, graphicsQueueFamily, computeQueueFamily, transferQueueFamily}; + for (const u32 queueFamilyIndex : queueFamilyRequests) { deviceQueueInfos.emplace_back(vk::DeviceQueueCreateInfo({}, queueFamilyIndex, 1, &queuePriority)); } @@ -482,15 +460,31 @@ void RendererVK::initGraphicsContext(SDL_Window* window) { // Create Device vk::DeviceCreateInfo deviceInfo = {}; - static const char* deviceExtensions[] = { - VK_KHR_SWAPCHAIN_EXTENSION_NAME, + // Device extensions + std::vector deviceExtensions = { #if defined(__APPLE__) "VK_KHR_portability_subset", #endif // VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME }; - deviceInfo.ppEnabledExtensionNames = deviceExtensions; - deviceInfo.enabledExtensionCount = std::size(deviceExtensions); + + std::unordered_set physicalDeviceExtensions; + if (const auto enumerateResult = physicalDevice.enumerateDeviceExtensionProperties(); enumerateResult.result == vk::Result::eSuccess) { + for (const auto& extension : enumerateResult.value) { + physicalDeviceExtensions.insert(extension.extensionName); + } + } else { + Helpers::panic("Error enumerating physical devices extensions: %s\n", vk::to_string(enumerateResult.result).c_str()); + } + + // Opertional extensions + + // Optionally enable the swapchain, to support "headless" rendering + if (physicalDeviceExtensions.contains(VK_KHR_SWAPCHAIN_EXTENSION_NAME)) { + deviceExtensions.emplace_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + } + + deviceInfo.setPEnabledExtensionNames(deviceExtensions); vk::StructureChain deviceFeatureChain = {}; @@ -528,6 +522,39 @@ void RendererVK::initGraphicsContext(SDL_Window* window) { } // Create swapchain + if (targetWindow && surface) { + vk::Extent2D swapchainExtent; + { + int windowWidth, windowHeight; + SDL_Vulkan_GetDrawableSize(window, &windowWidth, &windowHeight); + swapchainExtent.width = windowWidth; + swapchainExtent.height = windowHeight; + } + recreateSwapchain(surface.get(), swapchainExtent); + } + + // Create frame-buffering data + // Frame-buffering Command buffer(s) + vk::CommandBufferAllocateInfo commandBuffersInfo = {}; + commandBuffersInfo.commandPool = commandPool.get(); + commandBuffersInfo.level = vk::CommandBufferLevel::ePrimary; + commandBuffersInfo.commandBufferCount = frameBufferingCount; + + if (auto allocateResult = device->allocateCommandBuffersUnique(commandBuffersInfo); allocateResult.result == vk::Result::eSuccess) { + frameCommandBuffers = std::move(allocateResult.value); + } else { + Helpers::panic("Error allocating command buffer: %s\n", vk::to_string(allocateResult.result).c_str()); + } + + // Frame-buffering synchronization primitives + vk::FenceCreateInfo fenceInfo = {}; + fenceInfo.flags = vk::FenceCreateFlagBits::eSignaled; + + vk::SemaphoreCreateInfo semaphoreInfo = {}; + + swapImageFreeSemaphore.resize(frameBufferingCount); + renderFinishedSemaphore.resize(frameBufferingCount); + frameFinishedFences.resize(frameBufferingCount); vk::Extent2D swapchainExtent; { int windowWidth, windowHeight;