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!
This commit is contained in:
Wunkolo 2023-07-25 22:00:37 -07:00
parent d0832ca558
commit e3699fe8f8
5 changed files with 354 additions and 15 deletions

View file

@ -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})

View file

@ -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<vk::UniqueCommandBuffer> frameCommandBuffers = {};
@ -44,6 +46,9 @@ class RendererVK final : public Renderer {
std::vector<vk::UniqueSemaphore> renderFinishedSemaphore = {};
std::vector<vk::UniqueFence> frameFinishedFences = {};
std::vector<vk::UniqueImage> topScreenImages = {};
std::vector<vk::UniqueImage> 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);

View file

@ -0,0 +1,36 @@
#pragma once
#include <span>
#include <type_traits>
#include <utility>
#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<vk::Result, vk::UniqueDeviceMemory> commitImageHeap(
vk::Device device, vk::PhysicalDevice physicalDevice, const std::span<const vk::Image> images,
vk::MemoryPropertyFlags memoryProperties = vk::MemoryPropertyFlagBits::eDeviceLocal,
vk::MemoryPropertyFlags memoryExcludeProperties = vk::MemoryPropertyFlagBits::eProtected
);
std::tuple<vk::Result, vk::UniqueDeviceMemory> commitBufferHeap(
vk::Device device, vk::PhysicalDevice physicalDevice, const std::span<const vk::Buffer> buffers,
vk::MemoryPropertyFlags memoryProperties = vk::MemoryPropertyFlagBits::eDeviceLocal,
vk::MemoryPropertyFlags memoryExcludeProperties = vk::MemoryPropertyFlagBits::eProtected
);
} // namespace Vulkan

View file

@ -1,12 +1,14 @@
#include "renderer_vk/renderer_vk.hpp"
#include <limits>
#include <ranges>
#include <span>
#include <unordered_set>
#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<float, 4> 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<float, 4> topClearColor = {{1.0f, 0.0f, 0.0f, 1.0f}};
static const std::array<float, 4> 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<vk::Image> 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) {}

View file

@ -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<vk::DeviceSize>(value % size);
value -= mod;
return static_cast<vk::DeviceSize>(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<u32>(i);
}
}
return -1;
}
std::tuple<vk::Result, vk::UniqueDeviceMemory> commitImageHeap(
vk::Device device, vk::PhysicalDevice physicalDevice, const std::span<const vk::Image> images, vk::MemoryPropertyFlags memoryProperties,
vk::MemoryPropertyFlags memoryExcludeProperties
) {
vk::MemoryAllocateInfo imageHeapAllocInfo = {};
u32 imageHeapMemoryTypeBits = 0xFFFFFFFF;
std::vector<vk::BindImageMemoryInfo> 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<vk::Result, vk::UniqueDeviceMemory> commitBufferHeap(
vk::Device device, vk::PhysicalDevice physicalDevice, const std::span<const vk::Buffer> buffers, vk::MemoryPropertyFlags memoryProperties,
vk::MemoryPropertyFlags memoryExcludeProperties
) {
vk::MemoryAllocateInfo bufferHeapAllocInfo = {};
u32 bufferHeapMemoryTypeBits = 0xFFFFFFFF;
std::vector<vk::BindBufferMemoryInfo> 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