blob: d4a8ca2a7916a407fdfdf30a192a8b95c00028b8 [file]
/*
* Copyright 2025 Rive
*/
#include "rive_vk_bootstrap/vulkan_device.hpp"
#include "rive_vk_bootstrap/vulkan_instance.hpp"
#include "rive_vk_bootstrap/vulkan_frame_synchronizer.hpp"
#include "logging.hpp"
#include "vulkan_error_handling.hpp"
#include "vulkan_library.hpp"
namespace rive_vkb
{
VulkanFrameSynchronizer::VulkanFrameSynchronizer(
VulkanInstance& instance,
VulkanDevice& device,
rive::rcp<rive::gpu::VulkanContext>&& vk,
const Options& opts,
bool* successOut) :
m_vk(std::move(vk)),
m_device(device.vkDevice()),
m_monotonicFrameNumber(opts.initialFrameNumber)
{
*successOut = false;
// In-flight frame count must be at least two (need multiple to flip
// between)
assert(opts.inFlightFrameCount > 1);
// Load all of the functions we care about
#define LOAD(name) LOAD_MEMBER_INSTANCE_FUNC_OR_RETURN(this, name, instance);
RIVE_VK_FRAME_SYNC_INSTANCE_COMMANDS(LOAD);
#undef LOAD
m_vkGetDeviceQueue(m_device,
device.graphicsQueueFamilyIndex(),
0,
&m_graphicsQueue);
// Create the command pool
VkCommandPoolCreateInfo commandPoolCreateInfo = {
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
.queueFamilyIndex = device.graphicsQueueFamilyIndex(),
};
VK_CONFIRM_OR_RETURN_MSG(m_vkCreateCommandPool(m_device,
&commandPoolCreateInfo,
nullptr,
&m_commandPool),
"Failed to create Vulkan command pool");
// Create the alternating-frame sync objects
m_inFlightFrames.resize(opts.inFlightFrameCount);
for (auto& sync : m_inFlightFrames)
{
static constexpr VkFenceCreateInfo fenceCreateInfo = {
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
};
VK_CONFIRM_OR_RETURN_MSG(
m_vkCreateFence(m_device, &fenceCreateInfo, nullptr, &sync.fence),
"Failed to create Vulkan fence");
VkCommandBufferAllocateInfo cbufferAllocateInfo = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = m_commandPool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
};
VK_CONFIRM_OR_RETURN_MSG(
m_vkAllocateCommandBuffers(m_device,
&cbufferAllocateInfo,
&sync.commandBuffer),
"Failed to allocate Vulkan command buffers");
VK_CONFIRM_OR_RETURN(createSemaphore(&sync.semaphore));
sync.safeFrameNumber = m_monotonicFrameNumber;
}
if (!opts.externalGPUSynchronization)
{
// Without external GPU synchronization we need the to explicitly signal
// the very last semaphore in the chain so that the first frame has
// something correct to wait on.
VkSubmitInfo submitInfo = {
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &prev().semaphore,
};
VK_CONFIRM_OR_RETURN_MSG(
m_vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE),
"Failed to submit Vulkan queues");
}
*successOut = true;
}
VulkanFrameSynchronizer::~VulkanFrameSynchronizer()
{
// The instance command function pointers are loaded first thing in the
// constructor, but if construction failed partway (e.g. an OOM creating a
// fence) this destructor still runs while some pointers are null. Guard
// every call so teardown of a partially-constructed object cannot segfault.
//
// Note that the derived class will have already waited for the device to be
// idle so we can safely destroy things here.
for (auto& frame : m_inFlightFrames)
{
if (m_vkDestroySemaphore != nullptr)
{
destroySemaphore(frame.semaphore);
}
if (m_vkFreeCommandBuffers != nullptr &&
m_commandPool != VK_NULL_HANDLE)
{
m_vkFreeCommandBuffers(m_device,
m_commandPool,
1,
&frame.commandBuffer);
}
if (m_vkDestroyFence != nullptr)
{
m_vkDestroyFence(m_device, frame.fence, nullptr);
}
}
if (m_commandPool != VK_NULL_HANDLE && m_vkDestroyCommandPool != nullptr)
{
m_vkDestroyCommandPool(m_device, m_commandPool, nullptr);
}
}
VkResult VulkanFrameSynchronizer::waitForFenceAndBeginFrame(
VkSemaphore* optionalOutSemaphore)
{
// Before we can use the command buffers/semaphores for the current frame,
// we need to wait on its fence to stall the CPU until it's ready.
static constexpr auto NO_TIMEOUT = std::numeric_limits<uint64_t>::max();
VK_RETURN_RESULT_ON_ERROR_MSG(
m_vkWaitForFences(m_device, 1, &current().fence, true, NO_TIMEOUT),
"Failed to wait for Vulkan fence for next frame");
// Now we need to reset the command buffer
VkCommandBufferBeginInfo beginInfo = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
};
VK_RETURN_RESULT_ON_ERROR_MSG(
m_vkResetCommandBuffer(current().commandBuffer, 0),
"Failed to reset Vulkan command buffer");
VK_RETURN_RESULT_ON_ERROR_MSG(
m_vkBeginCommandBuffer(current().commandBuffer, &beginInfo),
"Failed to begin Vulkan command buffer");
m_monotonicFrameNumber++;
// Return the semaphore that external GPU synchronization (i.e. a swapchain)
// will signal.
if (optionalOutSemaphore != nullptr)
{
*optionalOutSemaphore = current().semaphore;
}
return VK_SUCCESS;
}
VkResult VulkanFrameSynchronizer::endFrame(
std::optional<VkSemaphore> externalSignalSemaphore)
{
auto& frame = current();
// This frame is done - reset the fence so that the submit can signal it.
VK_RETURN_RESULT_ON_ERROR_MSG(m_vkResetFences(m_device, 1, &frame.fence),
"Failed to reset Vulkan fences");
// Next, the command buffer needs to be ended (so it can be submitted)
VK_RETURN_RESULT_ON_ERROR_MSG(m_vkEndCommandBuffer(frame.commandBuffer),
"Failed to end Vulkan command buffer");
VkPipelineStageFlags waitStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
// If we were provided an external semaphore to signal (i.e. from a
// swapchain), we'll wait on the current frame's semaphore and signal the
// external one, otherwise we'll wait for the previous frame's semaphore and
// signal the current one.
auto waitSemaphore = externalSignalSemaphore.has_value() ? frame.semaphore
: prev().semaphore;
auto signalSemaphore = externalSignalSemaphore.has_value()
? externalSignalSemaphore.value()
: frame.semaphore;
VkSubmitInfo submitInfo = {
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &waitSemaphore,
.pWaitDstStageMask = &waitStageMask,
.commandBufferCount = 1,
.pCommandBuffers = &frame.commandBuffer,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &signalSemaphore,
};
VK_RETURN_RESULT_ON_ERROR_MSG(
m_vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, frame.fence),
"Failed to submit Vulkan queue");
// It will be safe to destroy assets in use in the current in-flight frame
// when the current frame number finishes.
frame.safeFrameNumber = m_monotonicFrameNumber;
// Cycle the render index to the next.
m_renderFrameIndex = (m_renderFrameIndex + 1) % m_inFlightFrames.size();
if (m_pixelReadState == PixelReadState::Queued)
{
m_pixelReadState = PixelReadState::Ready;
}
return VK_SUCCESS;
}
void VulkanFrameSynchronizer::queueImageCopy(
VkImage image,
VkFormat format,
rive::gpu::vkutil::ImageAccess* inOutLastAccess,
rive::IAABB pixelReadBounds)
{
assert(m_pixelReadState == PixelReadState::None &&
"Pixel read was while another is active.");
VkDeviceSize requiredBufferSize =
pixelReadBounds.height() * pixelReadBounds.width() * 4;
// Ensure that we have a read buffer that can hold the amount of data we
// need it to.
if (m_pixelReadBuffer == nullptr ||
m_pixelReadBuffer->info().size < requiredBufferSize)
{
m_pixelReadBuffer = m_vk->makeBuffer(
{
.size = requiredBufferSize,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
},
rive::gpu::vkutil::Mappability::readWrite);
}
constexpr rive::gpu::vkutil::ImageAccess TRANSFER_SRC_ACCESS = {
.pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
.accessMask = VK_ACCESS_TRANSFER_READ_BIT,
.layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
};
// We need to move the image into a mode where it can be copied.
auto& frame = current();
*inOutLastAccess = m_vk->simpleImageMemoryBarrier(frame.commandBuffer,
*inOutLastAccess,
TRANSFER_SRC_ACCESS,
image);
// Queue the actual copy
VkBufferImageCopy copyDesc = {
.imageSubresource =
{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.mipLevel = 0,
.baseArrayLayer = 0,
.layerCount = 1,
},
.imageOffset = {pixelReadBounds.left, pixelReadBounds.top, 0},
.imageExtent = {uint32_t(pixelReadBounds.width()),
uint32_t(pixelReadBounds.height()),
1},
};
m_vkCmdCopyImageToBuffer(frame.commandBuffer,
image,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
*m_pixelReadBuffer,
1,
&copyDesc);
// Now transition the buffer's state so it will be readable by the CPU once
// the operation is done.
m_vk->bufferMemoryBarrier(frame.commandBuffer,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_HOST_BIT,
0,
{
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_HOST_READ_BIT,
.buffer = *m_pixelReadBuffer,
});
m_pixelReadWidth = uint32_t(pixelReadBounds.width());
m_pixelReadHeight = uint32_t(pixelReadBounds.height());
m_pixelReadFormat = format;
m_pixelReadState = PixelReadState::Queued;
}
VkResult VulkanFrameSynchronizer::getPixelsFromLastImageCopy(
std::vector<uint8_t>* outPixels)
{
MappedPixelRead read;
VK_RETURN_RESULT_ON_ERROR(waitForPixelRead(&read));
outPixels->resize(read.width * read.height * 4);
assert(m_pixelReadBuffer->info().size >= outPixels->size());
for (auto y = 0u; y < read.height; y++)
{
// Copy the given row (the destination is flipped vertically vs the
// source so read the source the other way around)
auto src = read.data + read.strideBytes * (read.height - 1 - y);
uint8_t* dst = &outPixels->at(y * read.strideBytes);
memcpy(dst, src, read.strideBytes);
if (read.format == VK_FORMAT_B8G8R8A8_UNORM)
{
// Need to swap BGRA -> RGBA
for (auto x = 0u; x < read.strideBytes; x += 4)
{
std::swap(dst[x], dst[x + 2]);
}
}
}
finishPixelRead();
return VK_SUCCESS;
}
VkResult VulkanFrameSynchronizer::waitForPixelRead(MappedPixelRead* outRead)
{
assert(outRead != nullptr);
assert(m_pixelReadState != PixelReadState::None &&
"Pixels from image copy requested without one submitted");
assert(m_pixelReadState != PixelReadState::Queued &&
"Pixels from image copy requested before endFrame was called");
// In order to read this back, we need to wait for the previously-finished
// frame to finish.
auto& sync = prev();
static constexpr auto NO_TIMEOUT = std::numeric_limits<uint64_t>::max();
VK_RETURN_RESULT_ON_ERROR(
m_vkWaitForFences(m_device, 1, &sync.fence, true, NO_TIMEOUT));
// Make the texture data available to the CPU
m_pixelReadBuffer->invalidateContents();
outRead->data = static_cast<const uint8_t*>(m_pixelReadBuffer->contents());
outRead->width = m_pixelReadWidth;
outRead->height = m_pixelReadHeight;
outRead->strideBytes = m_pixelReadWidth * 4;
outRead->format = m_pixelReadFormat;
return VK_SUCCESS;
}
void VulkanFrameSynchronizer::finishPixelRead()
{
assert(m_pixelReadState != PixelReadState::None &&
"Pixels from image copy requested without one submitted");
assert(m_pixelReadState != PixelReadState::Queued &&
"Pixels from image copy requested before endFrame was called");
m_pixelReadState = PixelReadState::None;
}
VkResult VulkanFrameSynchronizer::createSemaphore(VkSemaphore* outSemaphore)
{
assert(outSemaphore != nullptr);
static constexpr VkSemaphoreCreateInfo semaCreateInfo = {
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
};
return m_vkCreateSemaphore(m_device,
&semaCreateInfo,
nullptr,
outSemaphore);
}
void VulkanFrameSynchronizer::destroySemaphore(VkSemaphore semaphore)
{
m_vkDestroySemaphore(m_device, semaphore, nullptr);
}
} // namespace rive_vkb