From e3699fe8f8ac799e3cf4208224ee8c3e273e4c66 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 25 Jul 2023 22:00:37 -0700 Subject: [PATCH] Allocate and present separate top/bottom screen framebuffer images Instead of operating directly on the swapchain images, we have our own top/bottom framebuffer images that will be rendered to independent of having an available swapchain. The images are blitted into the swapchain images, allowing for resizing too! --- CMakeLists.txt | 4 +- include/renderer_vk/renderer_vk.hpp | 5 + include/renderer_vk/vk_memory.hpp | 36 ++++++ src/core/renderer_vk/renderer_vk.cpp | 150 +++++++++++++++++++++-- src/core/renderer_vk/vk_memory.cpp | 174 +++++++++++++++++++++++++++ 5 files changed, 354 insertions(+), 15 deletions(-) create mode 100644 include/renderer_vk/vk_memory.hpp create mode 100644 src/core/renderer_vk/vk_memory.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 14262e65..f915444c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -239,11 +239,11 @@ if(ENABLE_VULKAN) ) set(RENDERER_VK_INCLUDE_FILES include/renderer_vk/renderer_vk.hpp - include/renderer_vk/vulkan_api.hpp include/renderer_vk/vk_debug.hpp + include/renderer_vk/vulkan_api.hpp include/renderer_vk/vk_debug.hpp include/renderer_vk/vk_memory.hpp ) set(RENDERER_VK_SOURCE_FILES src/core/renderer_vk/renderer_vk.cpp - src/core/renderer_vk/vulkan_api.cpp src/core/renderer_vk/vk_debug.cpp + src/core/renderer_vk/vulkan_api.cpp src/core/renderer_vk/vk_debug.cpp src/core/renderer_vk/vk_memory.cpp ) set(HEADER_FILES ${HEADER_FILES} ${RENDERER_VK_INCLUDE_FILES}) diff --git a/include/renderer_vk/renderer_vk.hpp b/include/renderer_vk/renderer_vk.hpp index 6ebbcb7e..9545a4d9 100644 --- a/include/renderer_vk/renderer_vk.hpp +++ b/include/renderer_vk/renderer_vk.hpp @@ -37,6 +37,8 @@ class RendererVK final : public Renderer { // Todo: make this a configuration option static constexpr usize frameBufferingCount = 3; + vk::UniqueDeviceMemory framebufferMemory = {}; + // Frame-buffering data // Each vector is `frameBufferingCount` in size std::vector frameCommandBuffers = {}; @@ -44,6 +46,9 @@ class RendererVK final : public Renderer { std::vector renderFinishedSemaphore = {}; std::vector frameFinishedFences = {}; + std::vector topScreenImages = {}; + std::vector bottomScreenImages = {}; + // Recreate the swapchain, possibly re-using the old one in the case of a resize vk::Result recreateSwapchain(vk::SurfaceKHR surface, vk::Extent2D swapchainExtent); diff --git a/include/renderer_vk/vk_memory.hpp b/include/renderer_vk/vk_memory.hpp new file mode 100644 index 00000000..8bf3f25d --- /dev/null +++ b/include/renderer_vk/vk_memory.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#include "helpers.hpp" +#include "vulkan_api.hpp" + +namespace Vulkan { + + // Will try to find a memory type that is suitable for the given requirements. + // Returns -1 if no suitable memory type was found. + s32 findMemoryTypeIndex( + vk::PhysicalDevice physicalDevice, u32 memoryTypeMask, vk::MemoryPropertyFlags memoryProperties, + vk::MemoryPropertyFlags memoryExcludeProperties = vk::MemoryPropertyFlagBits::eProtected + ); + + // Given an array of valid Vulkan image-handles or buffer-handles, these + // functions will allocate a single block of device-memory for all of them + // and bind them consecutively. + // There may be a case that all the buffers or images cannot be allocated + // to the same device memory due to their required memory-type. + std::tuple commitImageHeap( + vk::Device device, vk::PhysicalDevice physicalDevice, const std::span images, + vk::MemoryPropertyFlags memoryProperties = vk::MemoryPropertyFlagBits::eDeviceLocal, + vk::MemoryPropertyFlags memoryExcludeProperties = vk::MemoryPropertyFlagBits::eProtected + ); + + std::tuple commitBufferHeap( + vk::Device device, vk::PhysicalDevice physicalDevice, const std::span buffers, + vk::MemoryPropertyFlags memoryProperties = vk::MemoryPropertyFlagBits::eDeviceLocal, + vk::MemoryPropertyFlags memoryExcludeProperties = vk::MemoryPropertyFlagBits::eProtected + ); + +} // namespace Vulkan \ No newline at end of file diff --git a/src/core/renderer_vk/renderer_vk.cpp b/src/core/renderer_vk/renderer_vk.cpp index 4b6956d5..310fdec3 100644 --- a/src/core/renderer_vk/renderer_vk.cpp +++ b/src/core/renderer_vk/renderer_vk.cpp @@ -1,12 +1,14 @@ #include "renderer_vk/renderer_vk.hpp" #include +#include #include #include #include "SDL_vulkan.h" #include "helpers.hpp" #include "renderer_vk/vk_debug.hpp" +#include "renderer_vk/vk_memory.hpp" // Finds the first queue family that satisfies `queueMask` and excludes `queueExcludeMask` bits // Returns -1 if not found @@ -221,15 +223,27 @@ void RendererVK::display() { Vulkan::DebugLabelScope debugScope(frameCommandBuffer.get(), frameScopeColor, "Frame"); - // Prepare for color-clear if (swapchainImageIndex != swapchainImageInvalid) { + // Prepare images for color-clear 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) - )} + { + 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) + ), + vk::ImageMemoryBarrier( + vk::AccessFlagBits::eMemoryRead, vk::AccessFlagBits::eTransferWrite, vk::ImageLayout::eUndefined, + vk::ImageLayout::eTransferDstOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, + topScreenImages[frameBufferingIndex].get(), vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) + ), + vk::ImageMemoryBarrier( + vk::AccessFlagBits::eMemoryRead, vk::AccessFlagBits::eTransferWrite, vk::ImageLayout::eUndefined, + vk::ImageLayout::eTransferDstOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, + bottomScreenImages[frameBufferingIndex].get(), vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) + ), + } ); static const std::array clearColor = {{0.0f, 0.0f, 0.0f, 1.0f}}; @@ -238,6 +252,56 @@ void RendererVK::display() { vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) ); + //// Simulated rendering work, just clear the screens and get them ready to blit(transfer-src layout) + { + static const std::array topClearColor = {{1.0f, 0.0f, 0.0f, 1.0f}}; + static const std::array bottomClearColor = {{0.0f, 1.0f, 0.0f, 1.0f}}; + frameCommandBuffer->clearColorImage( + topScreenImages[frameBufferingIndex].get(), vk::ImageLayout::eTransferDstOptimal, topClearColor, + vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) + ); + frameCommandBuffer->clearColorImage( + bottomScreenImages[frameBufferingIndex].get(), vk::ImageLayout::eTransferDstOptimal, bottomClearColor, + vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) + ); + frameCommandBuffer->pipelineBarrier( + vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags(), {}, {}, + { + vk::ImageMemoryBarrier( + vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eTransferRead, vk::ImageLayout::eTransferDstOptimal, + vk::ImageLayout::eTransferSrcOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, + topScreenImages[frameBufferingIndex].get(), vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) + ), + vk::ImageMemoryBarrier( + vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eTransferRead, vk::ImageLayout::eTransferDstOptimal, + vk::ImageLayout::eTransferSrcOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, + bottomScreenImages[frameBufferingIndex].get(), vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1) + ), + } + ); + } + + // Blip top/bottom screen onto swapchain image + { + static const vk::ImageBlit topScreenBlit( + vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1), {vk::Offset3D{}, vk::Offset3D{400, 240, 1}}, + vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1), {vk::Offset3D{}, vk::Offset3D{400, 240, 1}} + ); + static const vk::ImageBlit bottomScreenBlit( + vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1), {vk::Offset3D{}, vk::Offset3D{320, 240, 1}}, + vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor, 0, 0, 1), + {vk::Offset3D{(400 / 2) - (320 / 2), 240, 0}, vk::Offset3D{(400 / 2) + (320 / 2), 240 + 240, 1}} + ); + frameCommandBuffer->blitImage( + topScreenImages[frameBufferingIndex].get(), vk::ImageLayout::eTransferSrcOptimal, swapchainImages[swapchainImageIndex], + vk::ImageLayout::eTransferDstOptimal, {topScreenBlit}, vk::Filter::eNearest + ); + frameCommandBuffer->blitImage( + bottomScreenImages[frameBufferingIndex].get(), vk::ImageLayout::eTransferSrcOptimal, swapchainImages[swapchainImageIndex], + vk::ImageLayout::eTransferDstOptimal, {bottomScreenBlit}, vk::Filter::eNearest + ); + } + // Prepare for present frameCommandBuffer->pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::DependencyFlags(), {}, {}, @@ -555,14 +619,74 @@ void RendererVK::initGraphicsContext(SDL_Window* window) { swapImageFreeSemaphore.resize(frameBufferingCount); renderFinishedSemaphore.resize(frameBufferingCount); frameFinishedFences.resize(frameBufferingCount); - vk::Extent2D swapchainExtent; - { - int windowWidth, windowHeight; - SDL_Vulkan_GetDrawableSize(window, &windowWidth, &windowHeight); - swapchainExtent.width = windowWidth; - swapchainExtent.height = windowHeight; + + vk::ImageCreateInfo topScreenInfo = {}; + topScreenInfo.setImageType(vk::ImageType::e2D); + topScreenInfo.setFormat(vk::Format::eR8G8B8A8Unorm); + topScreenInfo.setExtent(vk::Extent3D(400, 240, 1)); + topScreenInfo.setMipLevels(1); + topScreenInfo.setArrayLayers(2); // Two image layers, for 3D mode + topScreenInfo.setSamples(vk::SampleCountFlagBits::e1); + topScreenInfo.setTiling(vk::ImageTiling::eOptimal); + topScreenInfo.setUsage( + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment | vk::ImageUsageFlagBits::eTransferSrc | + vk::ImageUsageFlagBits::eTransferDst + ); + topScreenInfo.setSharingMode(vk::SharingMode::eExclusive); + topScreenInfo.setInitialLayout(vk::ImageLayout::eUndefined); + + vk::ImageCreateInfo bottomScreenInfo = topScreenInfo; + bottomScreenInfo.setExtent(vk::Extent3D(320, 240, 1)); + bottomScreenInfo.setArrayLayers(1); + + topScreenImages.resize(frameBufferingCount); + bottomScreenImages.resize(frameBufferingCount); + + for (usize i = 0; i < frameBufferingCount; 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()); + } + + if (auto createResult = device->createImageUnique(topScreenInfo); createResult.result == vk::Result::eSuccess) { + topScreenImages[i] = std::move(createResult.value); + } else { + Helpers::panic("Error creating top-screen image: %s\n", vk::to_string(createResult.result).c_str()); + } + + if (auto createResult = device->createImageUnique(bottomScreenInfo); createResult.result == vk::Result::eSuccess) { + bottomScreenImages[i] = std::move(createResult.value); + } else { + Helpers::panic("Error creating bottom-screen image: %s\n", vk::to_string(createResult.result).c_str()); + } + } + + // Commit memory to all of our images + { + const auto getImage = [](const vk::UniqueImage& image) -> vk::Image { return image.get(); }; + std::vector images; + std::ranges::transform(topScreenImages, std::back_inserter(images), getImage); + std::ranges::transform(bottomScreenImages, std::back_inserter(images), getImage); + + if (auto [result, imageMemory] = Vulkan::commitImageHeap(device.get(), physicalDevice, images); result == vk::Result::eSuccess) { + framebufferMemory = std::move(imageMemory); + } else { + Helpers::panic("Error allocating framebuffer memory: %s\n", vk::to_string(result).c_str()); + } } - recreateSwapchain(surface.get(), swapchainExtent); } void RendererVK::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) {} diff --git a/src/core/renderer_vk/vk_memory.cpp b/src/core/renderer_vk/vk_memory.cpp new file mode 100644 index 00000000..c9087719 --- /dev/null +++ b/src/core/renderer_vk/vk_memory.cpp @@ -0,0 +1,174 @@ +#include "renderer_vk/vk_memory.hpp" + +namespace Vulkan { + + static constexpr vk::DeviceSize alignUp(vk::DeviceSize value, std::size_t size) { + const vk::DeviceSize mod = static_cast(value % size); + value -= mod; + return static_cast(mod == vk::DeviceSize{0} ? value : value + size); + } + + // Given a speculative heap-allocation, defined by its current size and + // memory-type bits, appends a memory requirements structure to it, updating + // both the size and the required memory-type-bits. Returns the offset within + // the heap for the current MemoryRequirements Todo: Sun Apr 23 13:28:25 PDT + // 2023 Rather than using a running-size of the heap, look at all of the memory + // requests and optimally create a packing for all of the offset and alignment + // requirements. Such as by satisfying all of the largest alignments first, and + // then the smallest, to reduce padding + static vk::DeviceSize commitMemoryRequestToHeap( + const vk::MemoryRequirements& curMemoryRequirements, vk::DeviceSize& curHeapEnd, u32& curMemoryTypeBits, vk::DeviceSize sizeAlignment + ) { + // Accumulate a mask of all the memory types that satisfies each of the + // handles + curMemoryTypeBits &= curMemoryRequirements.memoryTypeBits; + + // Pad up the memory sizes so they are not considered aliasing + const vk::DeviceSize curMemoryOffset = alignUp(curHeapEnd, curMemoryRequirements.alignment); + // Pad the size by the required size-alignment. + // Intended for BufferImageGranularity + const vk::DeviceSize curMemorySize = alignUp(curMemoryRequirements.size, sizeAlignment); + + curHeapEnd = (curMemoryOffset + curMemorySize); + return curMemoryOffset; + } + + s32 findMemoryTypeIndex( + vk::PhysicalDevice physicalDevice, u32 memoryTypeMask, vk::MemoryPropertyFlags memoryProperties, + vk::MemoryPropertyFlags memoryExcludeProperties + ) { + const vk::PhysicalDeviceMemoryProperties deviceMemoryProperties = physicalDevice.getMemoryProperties(); + // Iterate the physical device's memory types until we find a match + for (std::size_t i = 0; i < deviceMemoryProperties.memoryTypeCount; i++) { + if( + // Is within memory type mask + (((memoryTypeMask >> i) & 0b1) == 0b1) && + // Has property flags + (deviceMemoryProperties.memoryTypes[i].propertyFlags + & memoryProperties) + == memoryProperties + && + // None of the excluded properties are enabled + !(deviceMemoryProperties.memoryTypes[i].propertyFlags + & memoryExcludeProperties) ) + { + return static_cast(i); + } + } + + return -1; + } + + std::tuple commitImageHeap( + vk::Device device, vk::PhysicalDevice physicalDevice, const std::span images, vk::MemoryPropertyFlags memoryProperties, + vk::MemoryPropertyFlags memoryExcludeProperties + ) { + vk::MemoryAllocateInfo imageHeapAllocInfo = {}; + u32 imageHeapMemoryTypeBits = 0xFFFFFFFF; + std::vector imageHeapBinds; + + const vk::DeviceSize bufferImageGranularity = physicalDevice.getProperties().limits.bufferImageGranularity; + + for (const vk::Image& curImage : images) { + const vk::DeviceSize curBindOffset = commitMemoryRequestToHeap( + device.getImageMemoryRequirements(curImage), imageHeapAllocInfo.allocationSize, imageHeapMemoryTypeBits, bufferImageGranularity + ); + + if (imageHeapMemoryTypeBits == 0) { + // No possible memory heap for all of the images to share + return std::make_tuple(vk::Result::eErrorOutOfDeviceMemory, vk::UniqueDeviceMemory()); + } + + // Put nullptr for the device memory for now + imageHeapBinds.emplace_back(vk::BindImageMemoryInfo{curImage, nullptr, curBindOffset}); + } + + const s32 memoryTypeIndex = findMemoryTypeIndex(physicalDevice, imageHeapMemoryTypeBits, memoryProperties, memoryExcludeProperties); + + if (memoryTypeIndex < 0) { + // Unable to find a memory heap that satisfies all the images + return std::make_tuple(vk::Result::eErrorOutOfDeviceMemory, vk::UniqueDeviceMemory()); + } + + imageHeapAllocInfo.memoryTypeIndex = memoryTypeIndex; + + vk::UniqueDeviceMemory imageHeapMemory = {}; + + if (auto allocResult = device.allocateMemoryUnique(imageHeapAllocInfo); allocResult.result == vk::Result::eSuccess) { + imageHeapMemory = std::move(allocResult.value); + } else { + return std::make_tuple(allocResult.result, vk::UniqueDeviceMemory()); + } + + // Assign the device memory to the bindings + for (vk::BindImageMemoryInfo& curBind : imageHeapBinds) { + curBind.memory = imageHeapMemory.get(); + } + + // Now bind them all in one call + if (const vk::Result bindResult = device.bindImageMemory2(imageHeapBinds); bindResult == vk::Result::eSuccess) { + // Binding memory succeeded + } else { + return std::make_tuple(bindResult, vk::UniqueDeviceMemory()); + } + + return std::make_tuple(vk::Result::eSuccess, std::move(imageHeapMemory)); + } + + std::tuple commitBufferHeap( + vk::Device device, vk::PhysicalDevice physicalDevice, const std::span buffers, vk::MemoryPropertyFlags memoryProperties, + vk::MemoryPropertyFlags memoryExcludeProperties + ) { + vk::MemoryAllocateInfo bufferHeapAllocInfo = {}; + u32 bufferHeapMemoryTypeBits = 0xFFFFFFFF; + std::vector bufferHeapBinds; + + const vk::DeviceSize bufferImageGranularity = physicalDevice.getProperties().limits.bufferImageGranularity; + + for (const vk::Buffer& curBuffer : buffers) { + const vk::DeviceSize curBindOffset = commitMemoryRequestToHeap( + device.getBufferMemoryRequirements(curBuffer), bufferHeapAllocInfo.allocationSize, bufferHeapMemoryTypeBits, bufferImageGranularity + ); + + if (bufferHeapMemoryTypeBits == 0) { + // No possible memory heap for all of the buffers to share + return std::make_tuple(vk::Result::eErrorOutOfDeviceMemory, vk::UniqueDeviceMemory()); + } + + // Put nullptr for the device memory for now + bufferHeapBinds.emplace_back(vk::BindBufferMemoryInfo{curBuffer, nullptr, curBindOffset}); + } + + const s32 memoryTypeIndex = findMemoryTypeIndex(physicalDevice, bufferHeapMemoryTypeBits, memoryProperties, memoryExcludeProperties); + + if (memoryTypeIndex < 0) { + // Unable to find a memory heap that satisfies all the buffers + return std::make_tuple(vk::Result::eErrorOutOfDeviceMemory, vk::UniqueDeviceMemory()); + } + + bufferHeapAllocInfo.memoryTypeIndex = memoryTypeIndex; + + vk::UniqueDeviceMemory bufferHeapMemory = {}; + + if (auto allocResult = device.allocateMemoryUnique(bufferHeapAllocInfo); allocResult.result == vk::Result::eSuccess) { + bufferHeapMemory = std::move(allocResult.value); + } else { + return std::make_tuple(allocResult.result, vk::UniqueDeviceMemory()); + } + + // Assign the device memory to the bindings + for (vk::BindBufferMemoryInfo& curBind : bufferHeapBinds) { + curBind.memory = bufferHeapMemory.get(); + } + + // Now bind them all in one call + if (const vk::Result bindResult = device.bindBufferMemory2(bufferHeapBinds); bindResult == vk::Result::eSuccess) { + // Binding memory succeeded + } else { + return std::make_tuple(bindResult, vk::UniqueDeviceMemory()); + } + + return std::make_tuple(vk::Result::eSuccess, std::move(bufferHeapMemory)); + } + +} // namespace Vulkan \ No newline at end of file