blob: be408a147b976bf4981c71d91d17eb90edd2a5b5 [file]
/*
* Copyright 2024 Rive
*/
#include "testing_window.hpp"
// Stub out when rive_vk_bootstrap is unavailable; those builds supply
// their own TestingWindow.
#if !defined(RIVE_VULKAN) || defined(RIVE_NO_VK_BOOTSTRAP)
TestingWindow* TestingWindow::MakeVulkanTexture(const BackendParams&)
{
return nullptr;
}
#else
#include "common/offscreen_render_target.hpp"
#include "rive_vk_bootstrap/vulkan_device.hpp"
#include "rive_vk_bootstrap/vulkan_instance.hpp"
#include "rive_vk_bootstrap/vulkan_headless_frame_synchronizer.hpp"
#include "rive/renderer/rive_renderer.hpp"
#include "rive/renderer/vulkan/render_context_vulkan_impl.hpp"
#include "rive/renderer/vulkan/render_target_vulkan.hpp"
namespace rive::gpu
{
class TestingWindowVulkanTexture : public TestingWindow
{
public:
TestingWindowVulkanTexture(const BackendParams& backendParams) :
m_backendParams(backendParams)
{
using namespace rive_vkb;
m_instance = VulkanInstance::Create(VulkanInstance::Options{
.appName = "Rive Unit Tests",
.idealAPIVersion =
m_backendParams.core ? VK_API_VERSION_1_1 : VK_API_VERSION_1_3,
#ifndef NDEBUG
.desiredValidationType =
m_backendParams.disableValidationLayers
? VulkanValidationType::none
: (m_backendParams.wantVulkanSynchronizationValidation
? VulkanValidationType::synchronization
: VulkanValidationType::core),
.wantDebugCallbacks = !m_backendParams.disableDebugCallbacks,
#endif
});
assert(m_instance != nullptr);
m_device = VulkanDevice::Create(
*m_instance,
VulkanDevice::Options{
.coreFeaturesOnly = m_backendParams.core,
.gpuNameFilter = m_backendParams.gpuNameFilter.c_str(),
.headless = true,
});
assert(m_device != nullptr);
m_renderContext = RenderContextVulkanImpl::MakeContext(
m_instance->vkInstance(),
m_device->vkPhysicalDevice(),
m_device->vkDevice(),
m_device->vulkanFeatures(),
m_instance->getVkGetInstanceProcAddrPtr(),
{
.forceAtomicMode = backendParams.atomic,
.shaderCompilationMode = backendParams.shaderCompilationMode,
});
// Enable canvas pre-pass support
// (makeCommandBuffer/commitCommandBuffer) by providing a command queue
// for canvas rendering.
VkQueue graphicsQueue;
auto impl =
m_renderContext->static_impl_cast<RenderContextVulkanImpl>();
auto vkGetDeviceQueue = reinterpret_cast<PFN_vkGetDeviceQueue>(
impl->vulkanContext()->GetDeviceProcAddr(m_device->vkDevice(),
"vkGetDeviceQueue"));
vkGetDeviceQueue(m_device->vkDevice(),
m_device->graphicsQueueFamilyIndex(),
0,
&graphicsQueue);
impl->setCanvasQueue(graphicsQueue,
m_device->graphicsQueueFamilyIndex());
}
~TestingWindowVulkanTexture()
{
// Destroy the offscreen frame syncrhonizer first because it
// synchronizes for in-flight command buffers.
m_frameSynchronizer = nullptr;
m_renderContext.reset();
m_renderTarget.reset();
}
rive::Factory* factory() override { return m_renderContext.get(); }
rive::gpu::RenderContext* renderContext() const override
{
return m_renderContext.get();
}
rcp<rive_tests::OffscreenRenderTarget> makeOffscreenRenderTarget(
uint32_t width,
uint32_t height,
bool riveRenderable) const override
{
return rive_tests::OffscreenRenderTarget::MakeVulkan(vk(),
width,
height,
riveRenderable);
}
std::unique_ptr<rive::Renderer> beginFrame(
const FrameOptions& options) override
{
if (m_frameSynchronizer == nullptr ||
m_frameSynchronizer->width() != m_width ||
m_frameSynchronizer->height() != m_height)
{
VkFormat imageFormat =
m_backendParams.srgb ? VK_FORMAT_R8G8B8A8_SRGB
: m_backendParams.core ? VK_FORMAT_R8G8B8A8_UNORM
: VK_FORMAT_B8G8R8A8_UNORM;
// Don't use VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT or
// VK_IMAGE_USAGE_STORAGE_BIT so we can test our (more complex)
// codepaths that make us work without it.
// The GMs that use offscreen "riveRenderable" render targets ensure
// we still cover the simpler direct rendering cases.
VkImageUsageFlags usageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT;
uint64_t currentFrameNumber =
m_frameSynchronizer != nullptr
? m_frameSynchronizer->currentFrameNumber()
: 0;
m_frameSynchronizer =
rive_vkb::VulkanHeadlessFrameSynchronizer::Create(
*m_instance,
*m_device,
ref_rcp(vk()),
rive_vkb::VulkanHeadlessFrameSynchronizer::Options{
.width = m_width,
.height = m_height,
.imageFormat = imageFormat,
.imageUsageFlags = usageFlags,
.initialFrameNumber = currentFrameNumber,
});
assert(m_frameSynchronizer != nullptr);
m_renderTarget = impl()->makeRenderTarget(
m_width,
m_height,
m_frameSynchronizer->imageFormat(),
m_frameSynchronizer->imageUsageFlags());
}
rive::gpu::RenderContext::FrameDescriptor frameDescriptor = {
.renderTargetWidth = m_width,
.renderTargetHeight = m_height,
.loadAction = options.doClear
? rive::gpu::LoadAction::clear
: rive::gpu::LoadAction::preserveRenderTarget,
.clearColor = options.clearColor,
.msaaSampleCount = m_backendParams.msaa ? 4u : 0u,
.disableRasterOrdering = options.disableRasterOrdering,
.wireframe = options.wireframe,
.fillsDisabled = options.fillsDisabled,
.strokesDisabled = options.strokesDisabled,
.clockwiseFillOverride =
m_backendParams.clockwise || options.clockwiseFillOverride,
.synthesizedFailureType = options.synthesizedFailureType,
};
m_renderContext->beginFrame(frameDescriptor);
return std::make_unique<RiveRenderer>(m_renderContext.get());
}
void flushPLSContext(RenderTarget* offscreenRenderTarget) final
{
if (!m_frameSynchronizer->isFrameStarted())
{
VK_CHECK(m_frameSynchronizer->beginFrame());
m_renderTarget->setTargetImageView(
m_frameSynchronizer->vkImageView(),
m_frameSynchronizer->vkImage(),
m_frameSynchronizer->lastAccess());
}
m_renderContext->flush({
.renderTarget = offscreenRenderTarget != nullptr
? offscreenRenderTarget
: m_renderTarget.get(),
.externalCommandBuffer =
m_frameSynchronizer->currentCommandBuffer(),
.currentFrameNumber = m_frameSynchronizer->currentFrameNumber(),
.safeFrameNumber = m_frameSynchronizer->safeFrameNumber(),
});
}
void endFrame(std::vector<uint8_t>* pixelData) override
{
flushPLSContext(nullptr);
auto lastAccess = m_renderTarget->targetLastAccess();
if (pixelData != nullptr)
{
m_frameSynchronizer->queueImageCopy(&lastAccess);
}
VK_CHECK(m_frameSynchronizer->endFrame(lastAccess));
if (pixelData != nullptr)
{
VK_CHECK(
m_frameSynchronizer->getPixelsFromLastImageCopy(pixelData));
}
}
private:
RenderContextVulkanImpl* impl() const
{
return m_renderContext->static_impl_cast<RenderContextVulkanImpl>();
}
VulkanContext* vk() const { return impl()->vulkanContext(); }
void* vulkanGraphicsQueue() const override
{
if (!vk() || !m_device)
return VK_NULL_HANDLE;
// graphicsQueue() is protected on VulkanFrameSynchronizer, so retrieve
// the queue via vkGetDeviceQueue using the known family index.
auto pfnGetDeviceQueue = reinterpret_cast<PFN_vkGetDeviceQueue>(
vk()->GetDeviceProcAddr(vk()->device, "vkGetDeviceQueue"));
if (!pfnGetDeviceQueue)
return VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
pfnGetDeviceQueue(vk()->device,
m_device->graphicsQueueFamilyIndex(),
0,
&queue);
return queue;
}
uint32_t vulkanGraphicsQueueFamilyIndex() const override
{
return m_device ? m_device->graphicsQueueFamilyIndex() : 0;
}
void* vulkanGetInstanceProcAddr() const override
{
return m_instance ? reinterpret_cast<void*>(
m_instance->getVkGetInstanceProcAddrPtr())
: nullptr;
}
void* getCurrentCommandBuffer() const override
{
// Per the base-class contract: return nullptr when no CB is open.
// The handle exposed by VulkanFrameSynchronizer is allocated up front
// but is only in the recording state between
// waitForFenceAndBeginFrame() and endFrame(). Handing it out before
// that causes Ore to record into an unbegun CB
// (VUID-vkCmdBeginRenderPass-commandBuffer-recording on validation, GPU
// ring timeout on RADV, heap corruption on nvidia). Matches
// testing_window_android_vulkan.cpp.
if (m_frameSynchronizer == nullptr ||
!m_frameSynchronizer->isFrameStarted())
return nullptr;
return m_frameSynchronizer->currentCommandBuffer();
}
void* getOreContext() const override
{
return m_renderContext->getOreContext();
}
void beginOreFrame() override
{
auto oreContext =
static_cast<rive::ore::Context*>(m_renderContext->getOreContext());
if (!m_frameSynchronizer->isFrameStarted())
{
VK_CHECK(m_frameSynchronizer->beginFrame());
m_renderTarget->setTargetImageView(
m_frameSynchronizer->vkImageView(),
m_frameSynchronizer->vkImage(),
m_frameSynchronizer->lastAccess());
}
oreContext->beginFrame(
{.externalCommandBuffer =
m_frameSynchronizer->currentCommandBuffer(),
.safeFrameNumber = m_frameSynchronizer->safeFrameNumber(),
.currentFrameNumber = m_frameSynchronizer->currentFrameNumber()});
}
void endOreFrame() override
{
auto oreContext =
static_cast<rive::ore::Context*>(m_renderContext->getOreContext());
oreContext->endFrame();
auto lastAccess = m_renderTarget->targetLastAccess();
VK_CHECK(m_frameSynchronizer->endFrame(lastAccess));
}
const BackendParams m_backendParams;
std::unique_ptr<rive_vkb::VulkanInstance> m_instance;
std::unique_ptr<rive_vkb::VulkanDevice> m_device;
std::unique_ptr<RenderContext> m_renderContext;
std::unique_ptr<rive_vkb::VulkanHeadlessFrameSynchronizer>
m_frameSynchronizer;
rcp<RenderTargetVulkanImpl> m_renderTarget;
};
}; // namespace rive::gpu
TestingWindow* TestingWindow::MakeVulkanTexture(
const BackendParams& backendParams)
{
return new rive::gpu::TestingWindowVulkanTexture(backendParams);
}
#endif