blob: 164beedfb7b0c1b2af5b47ce86f4d49ed9554fc6 [file] [log] [blame]
/*
* Copyright 2024 Rive
*/
#include "testing_window.hpp"
#if !defined(RIVE_ANDROID) || !defined(RIVE_VULKAN)
TestingWindow* TestingWindow::MakeAndroidVulkan(const BackendParams&,
void* platformWindow)
{
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_swapchain.hpp"
#include "rive/renderer/rive_renderer.hpp"
#include "rive/renderer/vulkan/render_context_vulkan_impl.hpp"
#include "rive/renderer/vulkan/render_target_vulkan.hpp"
#include <vulkan/vulkan_android.h>
#include <vk_mem_alloc.h>
#include <android/native_app_glue/android_native_app_glue.h>
#include <android/log.h>
using namespace rive;
using namespace rive::gpu;
// Send errors to stderr and the Android log, just for redundancy in case one or
// the other gets dropped.
#define LOG_ERROR_LINE(FORMAT, ...) \
[](auto&&... args) { \
fprintf(stderr, FORMAT "\n", std::forward<decltype(args)>(args)...); \
__android_log_print(ANDROID_LOG_ERROR, \
"rive_android_tests", \
FORMAT, \
std::forward<decltype(args)>(args)...); \
}(__VA_ARGS__)
class TestingWindowAndroidVulkan : public TestingWindow
{
public:
TestingWindowAndroidVulkan(const BackendParams& backendParams,
ANativeWindow* window) :
m_backendParams(backendParams)
{
using namespace rive_vkb;
// Request Vulkan 1.3, except if we're in core mode where we want 1.0.
int minorVersionRequested = m_backendParams.core ? 0 : 3;
std::vector<const char*> extensionNames;
extensionNames.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
extensionNames.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
m_instance = std::make_unique<VulkanInstance>(VulkanInstance::Options{
.appName = "Rive Android Test",
.idealAPIVersion =
m_backendParams.core ? VK_API_VERSION_1_0 : VK_API_VERSION_1_3,
.requiredExtensions =
make_span(extensionNames.data(), extensionNames.size()),
#ifndef NDEBUG
.wantValidationLayers = !m_backendParams.disableDebugCallbacks,
.wantDebugCallbacks = !m_backendParams.disableValidationLayers,
#endif
});
m_vkDestroySurfaceKHR =
m_instance->loadInstanceFunc<PFN_vkDestroySurfaceKHR>(
"vkDestroySurfaceKHR");
assert(m_vkDestroySurfaceKHR != nullptr);
VkAndroidSurfaceCreateInfoKHR androidSurfaceCreateInfo = {
.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
.window = window,
};
auto pfnvkCreateAndroidSurfaceKHR =
m_instance->loadInstanceFunc<PFN_vkCreateAndroidSurfaceKHR>(
"vkCreateAndroidSurfaceKHR");
assert(pfnvkCreateAndroidSurfaceKHR != nullptr);
VK_CHECK(pfnvkCreateAndroidSurfaceKHR(m_instance->vkInstance(),
&androidSurfaceCreateInfo,
nullptr,
&m_windowSurface));
m_device = std::make_unique<VulkanDevice>(
*m_instance,
VulkanDevice::Options{
.coreFeaturesOnly = m_backendParams.core,
});
m_renderContext = RenderContextVulkanImpl::MakeContext(
m_instance->vkInstance(),
m_device->vkPhysicalDevice(),
m_device->vkDevice(),
m_device->vulkanFeatures(),
m_instance->getVkGetInstanceProcAddrPtr(),
{.forceAtomicMode = backendParams.atomic});
auto windowCapabilities =
m_device->getSurfaceCapabilities(m_windowSurface);
auto swapOpts = VulkanSwapchain::Options{
.formatPreferences =
{
{
.format = m_backendParams.srgb
? VK_FORMAT_R8G8B8A8_SRGB
: VK_FORMAT_R8G8B8A8_UNORM,
.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR,
},
// Fall back to either ordering of ARGB
{
.format = VK_FORMAT_R8G8B8A8_UNORM,
.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR,
},
{
.format = VK_FORMAT_B8G8R8A8_UNORM,
.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR,
},
},
.presentModePreferences =
{
VK_PRESENT_MODE_IMMEDIATE_KHR,
VK_PRESENT_MODE_FIFO_KHR,
},
.imageUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT,
};
if ((windowCapabilities.supportedUsageFlags &
VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT) != 0)
{
swapOpts.imageUsageFlags |= VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT;
}
m_swapchain =
std::make_unique<rive_vkb::VulkanSwapchain>(*m_instance,
*m_device,
ref_rcp(vk()),
m_windowSurface,
swapOpts);
m_androidWindowWidth = m_swapchain->width();
m_androidWindowHeight = m_swapchain->height();
m_renderTarget =
impl()->makeRenderTarget(m_width,
m_height,
m_swapchain->imageFormat(),
m_swapchain->imageUsageFlags());
}
~TestingWindowAndroidVulkan()
{
// Destroy the swapchain first because it synchronizes for in-flight
// command buffers.
m_swapchain = nullptr;
m_renderContext.reset();
m_renderTarget.reset();
m_overflowTexture.reset();
if (m_windowSurface != VK_NULL_HANDLE)
{
m_vkDestroySurfaceKHR(m_instance->vkInstance(),
m_windowSurface,
nullptr);
}
}
Factory* factory() override { return m_renderContext.get(); }
rive::gpu::RenderContext* renderContext() const override
{
return m_renderContext.get();
}
rive::gpu::RenderTarget* renderTarget() const override
{
return m_renderTarget.get();
}
void resize(int width, int height) override
{
TestingWindow::resize(width, height);
m_renderTarget =
impl()->makeRenderTarget(m_width,
m_height,
m_swapchain->imageFormat(),
m_swapchain->imageUsageFlags());
if (m_width > m_androidWindowWidth || m_height > m_androidWindowHeight)
{
VkImageUsageFlags overflowTextureUsageFlags =
m_swapchain->imageUsageFlags();
// Some ARM Mali GPUs experience a device loss when rendering to the
// overflow texture as an input attachment. The current assumption
// is that it has to do with some combination of input attachment
// usage and pixel readbacks. For now, just don't enable the input
// attachment flag on the overflow texture.
overflowTextureUsageFlags &= ~VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT;
m_overflowTexture = vk()->makeTexture2D({
.format = m_swapchain->imageFormat(),
.extent = {static_cast<uint32_t>(m_width),
static_cast<uint32_t>(m_height)},
.usage = m_swapchain->imageUsageFlags(),
});
}
else
{
m_overflowTexture.reset();
}
}
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
{
m_renderContext->beginFrame(RenderContext::FrameDescriptor{
.renderTargetWidth = m_width,
.renderTargetHeight = m_height,
.loadAction = options.doClear
? gpu::LoadAction::clear
: gpu::LoadAction::preserveRenderTarget,
.clearColor = options.clearColor,
.msaaSampleCount = m_backendParams.msaa ? 4 : 0,
.disableRasterOrdering = options.disableRasterOrdering,
.wireframe = options.wireframe,
.clockwiseFillOverride =
m_backendParams.clockwise || options.clockwiseFillOverride,
.synthesizedFailureType = options.synthesizedFailureType,
});
return std::make_unique<RiveRenderer>(m_renderContext.get());
}
void flushPLSContext(RenderTarget* offscreenRenderTarget) override
{
if (!m_swapchain->isFrameStarted())
{
m_swapchain->beginFrame();
if (m_overflowTexture != nullptr)
{
m_renderTarget->setTargetImageView(
m_overflowTexture->vkImageView(),
m_overflowTexture->vkImage(),
m_overflowTexture->lastAccess());
}
else
{
m_renderTarget->setTargetImageView(
m_swapchain->currentVkImageView(),
m_swapchain->currentVkImage(),
m_swapchain->currentLastAccess());
}
}
m_renderContext->flush({
.renderTarget = offscreenRenderTarget != nullptr
? offscreenRenderTarget
: m_renderTarget.get(),
.externalCommandBuffer = m_swapchain->currentCommandBuffer(),
.currentFrameNumber = m_swapchain->currentFrameNumber(),
.safeFrameNumber = m_swapchain->safeFrameNumber(),
});
}
void endFrame(std::vector<uint8_t>* pixelData) override
{
flushPLSContext(nullptr);
vkutil::ImageAccess swapchainLastAccess;
if (m_overflowTexture == nullptr)
{
// We rendered directly to the window. Submit and read back
// normally.
swapchainLastAccess = m_renderTarget->targetLastAccess();
if (pixelData != nullptr)
{
m_swapchain->queueImageCopy(&swapchainLastAccess,
IAABB::MakeWH(m_width, m_height));
}
}
else
{
// Blit the overflow texture onto the screen in order to give some
// visual feedback.
vkutil::ImageAccess swapchainLastAccess =
vk()->simpleImageMemoryBarrier(
m_swapchain->currentCommandBuffer(),
m_swapchain->currentLastAccess(),
{
.pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
.accessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
},
m_swapchain->currentVkImage());
vk()->blitSubRect(
m_swapchain->currentCommandBuffer(),
m_renderTarget->accessTargetImage(
m_swapchain->currentCommandBuffer(),
{
.pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
.accessMask = VK_ACCESS_TRANSFER_READ_BIT,
.layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
}),
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
m_swapchain->currentVkImage(),
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
IAABB{0,
0,
std::min<int>(m_width, m_androidWindowWidth),
std::min<int>(m_height, m_androidWindowHeight)});
m_overflowTexture->lastAccess() =
m_renderTarget->targetLastAccess();
if (pixelData != nullptr)
{
m_swapchain->queueImageCopy(m_overflowTexture->vkImage(),
m_swapchain->imageFormat(),
&m_overflowTexture->lastAccess(),
IAABB::MakeWH(m_width, m_height));
}
}
m_swapchain->endFrame(swapchainLastAccess);
if (pixelData != nullptr)
{
m_swapchain->getPixelsFromLastImageCopy(pixelData);
}
}
private:
RenderContextVulkanImpl* impl() const
{
return m_renderContext->static_impl_cast<RenderContextVulkanImpl>();
}
VulkanContext* vk() const { return impl()->vulkanContext(); }
BackendParams m_backendParams;
uint32_t m_androidWindowWidth;
uint32_t m_androidWindowHeight;
std::unique_ptr<rive_vkb::VulkanInstance> m_instance;
std::unique_ptr<rive_vkb::VulkanDevice> m_device;
VkSurfaceKHR m_windowSurface = VK_NULL_HANDLE;
std::unique_ptr<rive_vkb::VulkanSwapchain> m_swapchain;
std::unique_ptr<RenderContext> m_renderContext;
rcp<RenderTargetVulkanImpl> m_renderTarget;
rcp<vkutil::Texture2D> m_overflowTexture; // Used when the desired render
// size doesn't fit in the window.
PFN_vkDestroySurfaceKHR m_vkDestroySurfaceKHR = nullptr;
};
TestingWindow* TestingWindow::MakeAndroidVulkan(
const BackendParams& backendParams,
void* platformWindow)
{
return new TestingWindowAndroidVulkan(
backendParams,
reinterpret_cast<ANativeWindow*>(platformWindow));
}
#endif