mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 21:21:17 +01:00
fix(renderer): Gracefully handle compilation failures (#9755) c09b771645
Ensure that rendering still succeeds when compilations fail (e.g., by falling back on an uber shader or at least not crashing). Valid compilations may fail in the real world if the device is pressed for resources or in a bad state. Co-authored-by: Chris Dalton <chris@rive.app> Co-authored-by: Jonathon Copeland <jcopela4@gmail.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
6582d5bf02f1751b9968f3e2156f209224336df8
|
||||
c09b771645eb07550b1c4cf276dffe128670769e
|
||||
|
||||
@@ -128,6 +128,11 @@ template <class T, class... Args> std::unique_ptr<T> make_unique(Args&&... args)
|
||||
{
|
||||
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
template <typename T> static std::unique_ptr<T> adopt_unique(T* window)
|
||||
{
|
||||
return std::unique_ptr<T>(window);
|
||||
}
|
||||
} // namespace rivestd
|
||||
|
||||
#endif // rive_types
|
||||
|
||||
@@ -1098,6 +1098,13 @@ struct FlushDescriptor
|
||||
bool clockwiseFillOverride = false;
|
||||
bool hasTriangleVertices = false;
|
||||
bool wireframe = false;
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
// Synthesize compilation failures to make sure the device handles them
|
||||
// gracefully. (e.g., by falling back on an uber shader or at least not
|
||||
// crashing.) Valid compilations may fail in the real world if the device is
|
||||
// pressed for resources or in a bad state.
|
||||
bool synthesizeCompilationFailures = false;
|
||||
#endif
|
||||
|
||||
// Command buffer that rendering commands will be added to.
|
||||
// - VkCommandBuffer on Vulkan.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "rive/renderer/render_context_helper_impl.hpp"
|
||||
#include "rive/shapes/paint/image_sampler.hpp"
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
|
||||
#ifndef RIVE_OBJC_NOP
|
||||
@@ -194,7 +194,7 @@ private:
|
||||
// the given features.
|
||||
const DrawPipeline* findCompatibleDrawPipeline(gpu::DrawType,
|
||||
gpu::ShaderFeatures,
|
||||
gpu::InterlockMode,
|
||||
const gpu::FlushDescriptor&,
|
||||
gpu::ShaderMiscFlags);
|
||||
|
||||
void flush(const FlushDescriptor&) override;
|
||||
@@ -231,7 +231,7 @@ private:
|
||||
|
||||
id<MTLSamplerState> m_imageSamplers[ImageSampler::MAX_SAMPLER_PERMUTATIONS];
|
||||
|
||||
std::map<uint32_t, std::unique_ptr<DrawPipeline>> m_drawPipelines;
|
||||
std::unordered_map<uint32_t, std::unique_ptr<DrawPipeline>> m_drawPipelines;
|
||||
|
||||
// Vertex/index buffers for drawing path patches.
|
||||
id<MTLBuffer> m_pathPatchVertexBuffer;
|
||||
|
||||
@@ -102,6 +102,13 @@ public:
|
||||
// Override all paths' fill rules (winding or even/odd) to emulate
|
||||
// clockwiseAtomic mode.
|
||||
bool clockwiseFillOverride = false;
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
// Synthesize compilation failures to make sure the device handles them
|
||||
// gracefully. (e.g., by falling back on an uber shader or at least not
|
||||
// crashing.) Valid compilations may fail in the real world if the
|
||||
// device is pressed for resources or in a bad state.
|
||||
bool synthesizeCompilationFailures = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
// Called at the beginning of a frame and establishes where and how it will
|
||||
|
||||
@@ -20,6 +20,7 @@ struct FiddleContextOptions
|
||||
// execution of goldens & gms significantly on Vulkan/Windows.)
|
||||
bool allowHeadlessRendering = false;
|
||||
bool enableVulkanValidationLayers = false;
|
||||
bool disableDebugCallbacks = false;
|
||||
const char* gpuNameFilter = nullptr; // Substring of GPU name to use.
|
||||
};
|
||||
|
||||
|
||||
@@ -39,19 +39,22 @@ public:
|
||||
const char** glfwExtensions;
|
||||
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
|
||||
|
||||
m_instance = VKB_CHECK(
|
||||
vkb::InstanceBuilder()
|
||||
.set_app_name("path_fiddle")
|
||||
.set_engine_name("Rive Renderer")
|
||||
vkb::InstanceBuilder instanceBuilder;
|
||||
instanceBuilder.set_app_name("path_fiddle")
|
||||
.set_engine_name("Rive Renderer")
|
||||
.enable_extensions(glfwExtensionCount, glfwExtensions)
|
||||
.require_api_version(1, options.coreFeaturesOnly ? 0 : 3, 0)
|
||||
.set_minimum_instance_version(1, 0, 0);
|
||||
m_instance = VKB_CHECK(instanceBuilder.build());
|
||||
#ifdef DEBUG
|
||||
.set_debug_callback(rive_vkb::default_debug_callback)
|
||||
.enable_validation_layers(
|
||||
m_options.enableVulkanValidationLayers)
|
||||
instanceBuilder.enable_validation_layers(
|
||||
m_options.enableVulkanValidationLayers);
|
||||
if (!m_options.disableDebugCallbacks)
|
||||
{
|
||||
instanceBuilder.set_debug_callback(
|
||||
rive_vkb::default_debug_callback);
|
||||
}
|
||||
#endif
|
||||
.enable_extensions(glfwExtensionCount, glfwExtensions)
|
||||
.require_api_version(1, options.coreFeaturesOnly ? 0 : 3, 0)
|
||||
.set_minimum_instance_version(1, 0, 0)
|
||||
.build());
|
||||
m_instanceDispatchTable = m_instance.make_table();
|
||||
|
||||
VulkanFeatures vulkanFeatures;
|
||||
|
||||
@@ -22,7 +22,10 @@ struct BackgroundCompileJob
|
||||
gpu::ShaderFeatures shaderFeatures;
|
||||
gpu::InterlockMode interlockMode;
|
||||
gpu::ShaderMiscFlags shaderMiscFlags;
|
||||
id<MTLLibrary> compiledLibrary;
|
||||
id<MTLLibrary> compiledLibrary = nil;
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
bool synthesizeCompilationFailure = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
// Compiles "draw" shaders in a background thread. A "draw" shaders is either
|
||||
|
||||
@@ -258,25 +258,52 @@ void BackgroundShaderCompiler::threadMain()
|
||||
compileOptions.preserveInvariance = YES;
|
||||
}
|
||||
compileOptions.preprocessorMacros = defines;
|
||||
job.compiledLibrary = [m_gpu newLibraryWithSource:source
|
||||
options:compileOptions
|
||||
error:&err];
|
||||
if (job.compiledLibrary == nil)
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
if (job.synthesizeCompilationFailure)
|
||||
{
|
||||
int lineNumber = 1;
|
||||
std::stringstream stream(source.UTF8String);
|
||||
std::string lineStr;
|
||||
while (std::getline(stream, lineStr, '\n'))
|
||||
{
|
||||
fprintf(stderr, "%4i| %s\n", lineNumber++, lineStr.c_str());
|
||||
}
|
||||
fprintf(stderr, "%s\n", err.localizedDescription.UTF8String);
|
||||
fprintf(stderr, "Failed to compile shader.\n\n");
|
||||
abort();
|
||||
assert(job.compiledLibrary == nil);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
job.compiledLibrary = [m_gpu newLibraryWithSource:source
|
||||
options:compileOptions
|
||||
error:&err];
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
|
||||
if (job.compiledLibrary == nil)
|
||||
{
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
if (job.synthesizeCompilationFailure)
|
||||
{
|
||||
fprintf(stderr, "Synthesizing shader compilation failure...\n");
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
// The compile job failed, most likely to external environmental
|
||||
// factors. Give up on this shader and let the render context
|
||||
// fall back on an uber shader instead.
|
||||
int lineNumber = 1;
|
||||
std::stringstream stream(source.UTF8String);
|
||||
std::string lineStr;
|
||||
while (std::getline(stream, lineStr, '\n'))
|
||||
{
|
||||
fprintf(stderr, "%4i| %s\n", lineNumber++, lineStr.c_str());
|
||||
}
|
||||
fprintf(stderr, "%s\n", err.localizedDescription.UTF8String);
|
||||
}
|
||||
|
||||
fprintf(stderr, "Failed to compile shader.\n");
|
||||
assert(false
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
|| job.synthesizeCompilationFailure
|
||||
#endif
|
||||
);
|
||||
}
|
||||
|
||||
m_finishedJobs.push_back(std::move(job));
|
||||
m_workFinishedCondition.notify_all();
|
||||
}
|
||||
|
||||
@@ -263,6 +263,13 @@ public:
|
||||
gpu::ShaderFeatures shaderFeatures,
|
||||
gpu::ShaderMiscFlags shaderMiscFlags)
|
||||
{
|
||||
if (library == nil)
|
||||
{
|
||||
// This pipeline is being built from a shader that failed to
|
||||
// compile. Leave everything nil and let draws fail.
|
||||
return;
|
||||
}
|
||||
|
||||
auto makePipelineState = [=](id<MTLFunction> vertexMain,
|
||||
id<MTLFunction> fragmentMain,
|
||||
MTLPixelFormat pixelFormat) {
|
||||
@@ -341,8 +348,15 @@ public:
|
||||
vertexMain, fragmentMain, MTLPixelFormatBGRA8Unorm);
|
||||
}
|
||||
|
||||
bool valid() const
|
||||
{
|
||||
assert((m_pipelineStateRGBA8 != nil) == (m_pipelineStateBGRA8 != nil));
|
||||
return m_pipelineStateRGBA8 != nil;
|
||||
}
|
||||
|
||||
id<MTLRenderPipelineState> pipelineState(MTLPixelFormat pixelFormat) const
|
||||
{
|
||||
assert(valid());
|
||||
assert(pixelFormat == MTLPixelFormatRGBA8Unorm ||
|
||||
pixelFormat == MTLPixelFormatRGBA16Float ||
|
||||
pixelFormat == MTLPixelFormatRGBA8Unorm_sRGB ||
|
||||
@@ -361,8 +375,8 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
id<MTLRenderPipelineState> m_pipelineStateRGBA8;
|
||||
id<MTLRenderPipelineState> m_pipelineStateBGRA8;
|
||||
id<MTLRenderPipelineState> m_pipelineStateRGBA8 = nil;
|
||||
id<MTLRenderPipelineState> m_pipelineStateBGRA8 = nil;
|
||||
};
|
||||
|
||||
#if defined(RIVE_IOS) || defined(RIVE_XROS) || defined(RIVE_APPLETVOS)
|
||||
@@ -925,37 +939,14 @@ void RenderContextMetalImpl::resizeAtlasTexture(uint32_t width, uint32_t height)
|
||||
const RenderContextMetalImpl::DrawPipeline* RenderContextMetalImpl::
|
||||
findCompatibleDrawPipeline(gpu::DrawType drawType,
|
||||
gpu::ShaderFeatures shaderFeatures,
|
||||
gpu::InterlockMode interlockMode,
|
||||
const gpu::FlushDescriptor& desc,
|
||||
gpu::ShaderMiscFlags shaderMiscFlags)
|
||||
{
|
||||
uint32_t pipelineKey = gpu::ShaderUniqueKey(
|
||||
drawType, shaderFeatures, interlockMode, shaderMiscFlags);
|
||||
auto pipelineIter = m_drawPipelines.find(pipelineKey);
|
||||
if (pipelineIter == m_drawPipelines.end())
|
||||
{
|
||||
// The shader for this pipeline hasn't been scheduled for compiling yet.
|
||||
// Schedule it to compile in the background.
|
||||
m_backgroundShaderCompiler->pushJob({
|
||||
.drawType = drawType,
|
||||
.shaderFeatures = shaderFeatures,
|
||||
.interlockMode = interlockMode,
|
||||
.shaderMiscFlags = shaderMiscFlags,
|
||||
});
|
||||
pipelineIter = m_drawPipelines.insert({pipelineKey, nullptr}).first;
|
||||
}
|
||||
|
||||
if (pipelineIter->second != nullptr)
|
||||
{
|
||||
// The pipeline is fully compiled and loaded.
|
||||
return pipelineIter->second.get();
|
||||
}
|
||||
|
||||
// The shader for this pipeline hasn't finished compiling yet. Start by
|
||||
// finding a fully-featured superset of features whose pipeline we can fall
|
||||
// Find a fully-featured superset of features whose pipeline we can fall
|
||||
// back on while waiting for it to compile.
|
||||
ShaderFeatures fullyFeaturedPipelineFeatures =
|
||||
gpu::ShaderFeaturesMaskFor(drawType, interlockMode);
|
||||
if (interlockMode == gpu::InterlockMode::atomics)
|
||||
gpu::ShaderFeaturesMaskFor(drawType, desc.interlockMode);
|
||||
if (desc.interlockMode == gpu::InterlockMode::atomics)
|
||||
{
|
||||
// Never add ENABLE_ADVANCED_BLEND to an atomic pipeline that doesn't
|
||||
// use advanced blend, since in atomic mode, the shaders behave
|
||||
@@ -971,49 +962,76 @@ const RenderContextMetalImpl::DrawPipeline* RenderContextMetalImpl::
|
||||
}
|
||||
shaderFeatures &= fullyFeaturedPipelineFeatures;
|
||||
|
||||
// Fully-featured "rasterOrdering" pipelines should have already been
|
||||
// pre-loaded from the static library.
|
||||
assert(shaderFeatures != fullyFeaturedPipelineFeatures ||
|
||||
interlockMode != gpu::InterlockMode::rasterOrdering);
|
||||
|
||||
// Poll to see if the shader is actually done compiling, but only wait if
|
||||
// it's a fully-feature pipeline. Otherwise, we can fall back on the
|
||||
// fully-featured pipeline while we wait for compilation.
|
||||
BackgroundCompileJob job;
|
||||
bool shouldWaitForBackgroundCompilation =
|
||||
shaderFeatures == fullyFeaturedPipelineFeatures ||
|
||||
m_contextOptions.synchronousShaderCompilations;
|
||||
while (m_backgroundShaderCompiler->popFinishedJob(
|
||||
&job, shouldWaitForBackgroundCompilation))
|
||||
uint32_t pipelineKey = gpu::ShaderUniqueKey(
|
||||
drawType, shaderFeatures, desc.interlockMode, shaderMiscFlags);
|
||||
auto pipelineIter = m_drawPipelines.find(pipelineKey);
|
||||
if (pipelineIter == m_drawPipelines.end())
|
||||
{
|
||||
uint32_t jobKey = gpu::ShaderUniqueKey(job.drawType,
|
||||
job.shaderFeatures,
|
||||
job.interlockMode,
|
||||
job.shaderMiscFlags);
|
||||
m_drawPipelines[jobKey] =
|
||||
std::make_unique<DrawPipeline>(m_gpu,
|
||||
job.compiledLibrary,
|
||||
@GLSL_drawVertexMain,
|
||||
@GLSL_drawFragmentMain,
|
||||
job.drawType,
|
||||
job.interlockMode,
|
||||
job.shaderFeatures,
|
||||
job.shaderMiscFlags);
|
||||
if (jobKey == pipelineKey)
|
||||
// The shader for this pipeline hasn't been scheduled for compiling yet.
|
||||
// Schedule it to compile in the background.
|
||||
m_backgroundShaderCompiler->pushJob({
|
||||
.drawType = drawType,
|
||||
.shaderFeatures = shaderFeatures,
|
||||
.interlockMode = desc.interlockMode,
|
||||
.shaderMiscFlags = shaderMiscFlags,
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
.synthesizeCompilationFailure = desc.synthesizeCompilationFailures,
|
||||
#endif
|
||||
});
|
||||
pipelineIter = m_drawPipelines.insert({pipelineKey, nullptr}).first;
|
||||
}
|
||||
|
||||
if (pipelineIter->second == nullptr)
|
||||
{
|
||||
// The shader for this pipeline hasn't finished compiling yet.
|
||||
// Fully-featured "rasterOrdering" pipelines should have already been
|
||||
// pre-loaded from the static library.
|
||||
assert(shaderFeatures != fullyFeaturedPipelineFeatures ||
|
||||
desc.interlockMode != gpu::InterlockMode::rasterOrdering);
|
||||
|
||||
// Poll to see if the shader is actually done compiling, but only wait
|
||||
// if it's a fully-feature pipeline. Otherwise, we can fall back on the
|
||||
// fully-featured pipeline while we wait for compilation.
|
||||
BackgroundCompileJob job;
|
||||
bool shouldWaitForBackgroundCompilation =
|
||||
shaderFeatures == fullyFeaturedPipelineFeatures ||
|
||||
m_contextOptions.synchronousShaderCompilations;
|
||||
while (m_backgroundShaderCompiler->popFinishedJob(
|
||||
&job, shouldWaitForBackgroundCompilation))
|
||||
{
|
||||
// The shader we wanted was actually done compiling and pending
|
||||
// being built into a pipeline.
|
||||
return pipelineIter->second.get();
|
||||
uint32_t jobKey = gpu::ShaderUniqueKey(job.drawType,
|
||||
job.shaderFeatures,
|
||||
job.interlockMode,
|
||||
job.shaderMiscFlags);
|
||||
m_drawPipelines[jobKey] =
|
||||
std::make_unique<DrawPipeline>(m_gpu,
|
||||
job.compiledLibrary,
|
||||
@GLSL_drawVertexMain,
|
||||
@GLSL_drawFragmentMain,
|
||||
job.drawType,
|
||||
job.interlockMode,
|
||||
job.shaderFeatures,
|
||||
job.shaderMiscFlags);
|
||||
if (jobKey == pipelineKey)
|
||||
{
|
||||
// The shader we wanted was actually done compiling and pending
|
||||
// being built into a pipeline.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The shader for this feature set hasn't finished compiling. Use the
|
||||
// pipeline that has all features enabled while we wait for it to finish.
|
||||
assert(shaderFeatures != fullyFeaturedPipelineFeatures);
|
||||
return findCompatibleDrawPipeline(drawType,
|
||||
fullyFeaturedPipelineFeatures,
|
||||
interlockMode,
|
||||
shaderMiscFlags);
|
||||
if ((pipelineIter->second == nullptr || !pipelineIter->second->valid()) &&
|
||||
shaderFeatures != fullyFeaturedPipelineFeatures)
|
||||
{
|
||||
// The shader for this feature set hasn't finished compiling (or it
|
||||
// failed to compile). Use the uber-shader pipeline that has all
|
||||
// features enabled while we wait for it to finish.
|
||||
return findCompatibleDrawPipeline(
|
||||
drawType, fullyFeaturedPipelineFeatures, desc, shaderMiscFlags);
|
||||
}
|
||||
|
||||
return pipelineIter->second.get();
|
||||
}
|
||||
|
||||
void RenderContextMetalImpl::prepareToFlush(uint64_t nextFrameNumber,
|
||||
@@ -1512,12 +1530,18 @@ void RenderContextMetalImpl::flush(const FlushDescriptor& desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
const DrawPipeline* drawPipeline = findCompatibleDrawPipeline(
|
||||
batch.drawType, shaderFeatures, desc, batchMiscFlags);
|
||||
if (drawPipeline == nullptr || !drawPipeline->valid())
|
||||
{
|
||||
// The shader for this draw AND the uber-shader both failed to
|
||||
// compile. This should virtually never happen, and can only happen
|
||||
// on non-Apple Silicon, where we don't use precompiled shaders.
|
||||
// Skip the draw.
|
||||
continue;
|
||||
}
|
||||
id<MTLRenderPipelineState> drawPipelineState =
|
||||
findCompatibleDrawPipeline(batch.drawType,
|
||||
shaderFeatures,
|
||||
desc.interlockMode,
|
||||
batchMiscFlags)
|
||||
->pipelineState(renderTarget->pixelFormat());
|
||||
drawPipeline->pipelineState(renderTarget->pixelFormat());
|
||||
|
||||
// Bind the appropriate image texture, if any.
|
||||
if (auto imageTextureMetal =
|
||||
|
||||
@@ -1019,6 +1019,10 @@ void RenderContext::LogicalFlush::layoutResources(
|
||||
m_flushDesc.tessDataHeight = tessDataHeight;
|
||||
m_flushDesc.clockwiseFillOverride = frameDescriptor.clockwiseFillOverride;
|
||||
m_flushDesc.wireframe = frameDescriptor.wireframe;
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
m_flushDesc.synthesizeCompilationFailures =
|
||||
frameDescriptor.synthesizeCompilationFailures;
|
||||
#endif
|
||||
|
||||
m_flushDesc.externalCommandBuffer = flushResources.externalCommandBuffer;
|
||||
|
||||
|
||||
@@ -200,16 +200,11 @@
|
||||
// polyfilled as a 2D texture, the "array index" needs to be a 0..1 normalized
|
||||
// y coordinate instead of the literal array index.
|
||||
#define TEXTURE_R16F_1D_ARRAY(SET, IDX, NAME) TEXTURE_R16F(SET, IDX, NAME)
|
||||
#define TEXTURE_SAMPLE_LOD_1D_ARRAY(NAME, \
|
||||
SAMPLER_NAME, \
|
||||
X, \
|
||||
ARRAY_INDEX, \
|
||||
ARRAY_INDEX_NORMALIZED, \
|
||||
LOD) \
|
||||
TEXTURE_SAMPLE_LOD(NAME, \
|
||||
SAMPLER_NAME, \
|
||||
float2(X, ARRAY_INDEX_NORMALIZED), \
|
||||
LOD)
|
||||
// clang-format off
|
||||
// Clang formatting on this line trips up the Qualcomm compiler.
|
||||
#define TEXTURE_SAMPLE_LOD_1D_ARRAY(NAME, SAMPLER_NAME, X, ARRAY_INDEX, ARRAY_INDEX_NORMALIZED, LOD) \
|
||||
TEXTURE_SAMPLE_LOD(NAME, SAMPLER_NAME, float2(X, ARRAY_INDEX_NORMALIZED), LOD)
|
||||
// clang-format on
|
||||
|
||||
#define TEXTURE_RG32UI(SET, IDX, NAME) TEXTURE_RGBA32UI(SET, IDX, NAME)
|
||||
|
||||
|
||||
@@ -97,12 +97,16 @@ std::unique_ptr<TestingGLRenderer> TestingGLRenderer::MakePLS(
|
||||
: 0,
|
||||
.disableRasterOrdering =
|
||||
(m_rendererFlags &
|
||||
TestingWindow::RendererFlags::disableRasterOrdering),
|
||||
TestingWindow::RendererFlags::disableRasterOrdering) ||
|
||||
options.disableRasterOrdering,
|
||||
.wireframe = options.wireframe,
|
||||
.clockwiseFillOverride =
|
||||
(m_rendererFlags &
|
||||
TestingWindow::RendererFlags::clockwiseFillOverride) ||
|
||||
options.clockwiseFillOverride};
|
||||
options.clockwiseFillOverride,
|
||||
.synthesizeCompilationFailures =
|
||||
options.synthesizeCompilationFailures,
|
||||
};
|
||||
m_renderContext->beginFrame(frameDescriptor);
|
||||
}
|
||||
|
||||
|
||||
@@ -441,8 +441,10 @@ public:
|
||||
{
|
||||
uint32_t clearColor;
|
||||
bool doClear = true;
|
||||
bool disableRasterOrdering = false;
|
||||
bool wireframe = false;
|
||||
bool clockwiseFillOverride = false;
|
||||
bool synthesizeCompilationFailures = false;
|
||||
};
|
||||
virtual std::unique_ptr<rive::Renderer> beginFrame(const FrameOptions&) = 0;
|
||||
virtual void endFrame(std::vector<uint8_t>* pixelData = nullptr) = 0;
|
||||
@@ -472,23 +474,18 @@ public:
|
||||
|
||||
virtual ~TestingWindow() {}
|
||||
|
||||
protected:
|
||||
uint32_t m_width = 0;
|
||||
uint32_t m_height = 0;
|
||||
|
||||
struct BackendParams
|
||||
{
|
||||
bool coreFeaturesOnly = false;
|
||||
bool srgb = false;
|
||||
bool clockwiseFill = false;
|
||||
bool disableValidationLayers = false;
|
||||
bool disableDebugCallbacks = false;
|
||||
std::string gpuNameFilter;
|
||||
};
|
||||
|
||||
static TestingWindow* MakeGLFW(Backend, Visibility);
|
||||
static TestingWindow* MakeEGL(Backend, void* platformWindow);
|
||||
#ifdef _WIN32
|
||||
static TestingWindow* MakeD3D(Visibility);
|
||||
#endif
|
||||
#if defined(__APPLE__) && !defined(RIVE_UNREAL)
|
||||
static TestingWindow* MakeMetalTexture();
|
||||
#endif
|
||||
@@ -501,6 +498,10 @@ protected:
|
||||
static TestingWindow* MakeAndroidVulkan(const BackendParams&,
|
||||
void* platformWindow);
|
||||
static TestingWindow* MakeSkia();
|
||||
|
||||
protected:
|
||||
uint32_t m_width = 0;
|
||||
uint32_t m_height = 0;
|
||||
};
|
||||
|
||||
RIVE_MAKE_ENUM_BITSET(TestingWindow::RendererFlags);
|
||||
|
||||
@@ -36,20 +36,22 @@ public:
|
||||
m_height = ANativeWindow_getHeight(window);
|
||||
rive_vkb::load_vulkan();
|
||||
|
||||
m_instance = VKB_CHECK(
|
||||
vkb::InstanceBuilder()
|
||||
.set_app_name("path_fiddle")
|
||||
.set_engine_name("Rive Renderer")
|
||||
vkb::InstanceBuilder instanceBuilder;
|
||||
instanceBuilder.set_app_name("path_fiddle")
|
||||
.set_engine_name("Rive Renderer")
|
||||
.enable_extension(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME)
|
||||
.require_api_version(1, m_backendParams.coreFeaturesOnly ? 0 : 3, 0)
|
||||
.set_minimum_instance_version(1, 0, 0);
|
||||
#ifdef DEBUG
|
||||
.set_debug_callback(rive_vkb::default_debug_callback)
|
||||
.enable_validation_layers(true)
|
||||
instanceBuilder.enable_validation_layers(
|
||||
!backendParams.disableValidationLayers);
|
||||
if (!backendParams.disableDebugCallbacks)
|
||||
{
|
||||
instanceBuilder.set_debug_callback(
|
||||
rive_vkb::default_debug_callback);
|
||||
}
|
||||
#endif
|
||||
.enable_extension(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME)
|
||||
.require_api_version(1,
|
||||
m_backendParams.coreFeaturesOnly ? 0 : 3,
|
||||
0)
|
||||
.set_minimum_instance_version(1, 0, 0)
|
||||
.build());
|
||||
m_instance = VKB_CHECK(instanceBuilder.build());
|
||||
m_instanceDispatchTable = m_instance.make_table();
|
||||
|
||||
VkAndroidSurfaceCreateInfoKHR androidSurfaceCreateInfo = {
|
||||
@@ -177,9 +179,12 @@ public:
|
||||
? gpu::LoadAction::clear
|
||||
: gpu::LoadAction::preserveRenderTarget,
|
||||
.clearColor = options.clearColor,
|
||||
.disableRasterOrdering = options.disableRasterOrdering,
|
||||
.wireframe = options.wireframe,
|
||||
.clockwiseFillOverride =
|
||||
m_backendParams.clockwiseFill || options.clockwiseFillOverride,
|
||||
.synthesizeCompilationFailures =
|
||||
options.synthesizeCompilationFailures,
|
||||
});
|
||||
|
||||
return std::make_unique<RiveRenderer>(m_renderContext.get());
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "testing_window.hpp"
|
||||
|
||||
#if defined(TESTING) || defined(RIVE_TOOLS_NO_GLFW)
|
||||
#if defined(RIVE_TOOLS_NO_GLFW)
|
||||
TestingWindow* TestingWindow::MakeFiddleContext(Backend,
|
||||
Visibility,
|
||||
const BackendParams&,
|
||||
@@ -240,7 +240,9 @@ public:
|
||||
.coreFeaturesOnly = m_backendParams.coreFeaturesOnly,
|
||||
.srgb = m_backendParams.srgb,
|
||||
.allowHeadlessRendering = visibility == Visibility::headless,
|
||||
.enableVulkanValidationLayers = true,
|
||||
.enableVulkanValidationLayers =
|
||||
!backendParams.disableValidationLayers,
|
||||
.disableDebugCallbacks = backendParams.disableDebugCallbacks,
|
||||
.gpuNameFilter = backendParams.gpuNameFilter.c_str(),
|
||||
};
|
||||
|
||||
@@ -288,10 +290,7 @@ public:
|
||||
|
||||
if (m_fiddleContext == nullptr)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"error: unable to create FiddleContext for %s.\n",
|
||||
BackendName(backend));
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
// On Mac we need to call glfwSetWindowSize even though we created the
|
||||
// window with these same dimensions.
|
||||
@@ -314,6 +313,8 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
bool valid() const { return m_fiddleContext != nullptr; }
|
||||
|
||||
rive::Factory* factory() override { return m_fiddleContext->factory(); }
|
||||
|
||||
void resize(int width, int height) override
|
||||
@@ -340,9 +341,14 @@ public:
|
||||
: rive::gpu::LoadAction::preserveRenderTarget,
|
||||
.clearColor = options.clearColor,
|
||||
.msaaSampleCount = m_msaaSampleCount,
|
||||
.disableRasterOrdering = options.disableRasterOrdering,
|
||||
.wireframe = options.wireframe,
|
||||
.clockwiseFillOverride =
|
||||
m_backendParams.clockwiseFill || options.clockwiseFillOverride,
|
||||
#ifdef WITH_RIVE_TOOLS
|
||||
.synthesizeCompilationFailures =
|
||||
options.synthesizeCompilationFailures,
|
||||
#endif
|
||||
};
|
||||
m_fiddleContext->begin(std::move(frameDescriptor));
|
||||
return m_fiddleContext->makeRenderer(m_width, m_height);
|
||||
@@ -414,10 +420,13 @@ TestingWindow* TestingWindow::MakeFiddleContext(
|
||||
const BackendParams& backendParams,
|
||||
void* platformWindow)
|
||||
{
|
||||
return new TestingWindowFiddleContext(backend,
|
||||
visibility,
|
||||
backendParams,
|
||||
platformWindow);
|
||||
auto window = std::make_unique<TestingWindowFiddleContext>(backend,
|
||||
visibility,
|
||||
backendParams,
|
||||
platformWindow);
|
||||
if (!window->valid())
|
||||
window = nullptr;
|
||||
return window.release();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "testing_window.hpp"
|
||||
|
||||
#if defined(__APPLE__) && !defined(TESTING) && !defined(RIVE_UNREAL)
|
||||
#if defined(__APPLE__) && !defined(RIVE_UNREAL)
|
||||
|
||||
#include "rive/renderer/metal/render_context_metal_impl.h"
|
||||
#include "rive/renderer/rive_renderer.hpp"
|
||||
@@ -34,9 +34,6 @@ public:
|
||||
return m_renderContext.get();
|
||||
}
|
||||
|
||||
// rive::gpu::RenderTarget* renderTarget() const override { return
|
||||
// m_renderTarget.get(); }
|
||||
|
||||
std::unique_ptr<rive::Renderer> beginFrame(
|
||||
const FrameOptions& options) override
|
||||
{
|
||||
@@ -47,8 +44,11 @@ public:
|
||||
? rive::gpu::LoadAction::clear
|
||||
: rive::gpu::LoadAction::preserveRenderTarget,
|
||||
.clearColor = options.clearColor,
|
||||
.disableRasterOrdering = options.disableRasterOrdering,
|
||||
.wireframe = options.wireframe,
|
||||
.clockwiseFillOverride = options.clockwiseFillOverride,
|
||||
.synthesizeCompilationFailures =
|
||||
options.synthesizeCompilationFailures,
|
||||
};
|
||||
m_renderContext->beginFrame(frameDescriptor);
|
||||
m_flushCommandBuffer = [m_queue commandBuffer];
|
||||
|
||||
@@ -28,20 +28,22 @@ public:
|
||||
{
|
||||
rive_vkb::load_vulkan();
|
||||
|
||||
m_instance = VKB_CHECK(
|
||||
vkb::InstanceBuilder()
|
||||
.set_app_name("rive_tools")
|
||||
.set_engine_name("Rive Renderer")
|
||||
.set_headless(true)
|
||||
vkb::InstanceBuilder instanceBuilder;
|
||||
instanceBuilder.set_app_name("rive_tools")
|
||||
.set_engine_name("Rive Renderer")
|
||||
.set_headless(true)
|
||||
.require_api_version(1, m_backendParams.coreFeaturesOnly ? 0 : 3, 0)
|
||||
.set_minimum_instance_version(1, 0, 0);
|
||||
#ifdef DEBUG
|
||||
.enable_validation_layers()
|
||||
.set_debug_callback(rive_vkb::default_debug_callback)
|
||||
instanceBuilder.enable_validation_layers(
|
||||
!backendParams.disableValidationLayers);
|
||||
if (!backendParams.disableDebugCallbacks)
|
||||
{
|
||||
instanceBuilder.set_debug_callback(
|
||||
rive_vkb::default_debug_callback);
|
||||
}
|
||||
#endif
|
||||
.require_api_version(1,
|
||||
m_backendParams.coreFeaturesOnly ? 0 : 3,
|
||||
0)
|
||||
.set_minimum_instance_version(1, 0, 0)
|
||||
.build());
|
||||
m_instance = VKB_CHECK(instanceBuilder.build());
|
||||
|
||||
VulkanFeatures vulkanFeatures;
|
||||
std::tie(m_device, vulkanFeatures) =
|
||||
@@ -116,9 +118,12 @@ public:
|
||||
? rive::gpu::LoadAction::clear
|
||||
: rive::gpu::LoadAction::preserveRenderTarget,
|
||||
.clearColor = options.clearColor,
|
||||
.disableRasterOrdering = options.disableRasterOrdering,
|
||||
.wireframe = options.wireframe,
|
||||
.clockwiseFillOverride =
|
||||
m_backendParams.clockwiseFill || options.clockwiseFillOverride,
|
||||
.synthesizeCompilationFailures =
|
||||
options.synthesizeCompilationFailures,
|
||||
};
|
||||
m_renderContext->beginFrame(frameDescriptor);
|
||||
return std::make_unique<RiveRenderer>(m_renderContext.get());
|
||||
|
||||
@@ -9,7 +9,7 @@ end
|
||||
|
||||
rive_tools_project('gms', 'RiveTool')
|
||||
do
|
||||
files({ 'gm/*.cpp', RIVE_PLS_DIR .. '/shader_hotload/**.cpp' })
|
||||
files({ 'gm/*.cpp' })
|
||||
filter({ 'options:for_unreal' })
|
||||
do
|
||||
defines({ 'RIVE_UNREAL' })
|
||||
@@ -19,7 +19,7 @@ end
|
||||
rive_tools_project('goldens', 'RiveTool')
|
||||
do
|
||||
exceptionhandling('On')
|
||||
files({ 'goldens/goldens.cpp', RIVE_PLS_DIR .. '/shader_hotload/**.cpp' })
|
||||
files({ 'goldens/goldens.cpp' })
|
||||
filter({ 'options:for_unreal' })
|
||||
do
|
||||
defines({ 'RIVE_UNREAL' })
|
||||
@@ -28,13 +28,12 @@ end
|
||||
|
||||
rive_tools_project('player', 'RiveTool')
|
||||
do
|
||||
files({ 'player/player.cpp', RIVE_PLS_DIR .. '/shader_hotload/**.cpp' })
|
||||
files({ 'player/player.cpp' })
|
||||
end
|
||||
|
||||
rive_tools_project('command_buffer_example', 'RiveTool')
|
||||
do
|
||||
files({
|
||||
'command_buffer_example/command_buffer_example.cpp',
|
||||
RIVE_PLS_DIR .. '/shader_hotload/**.cpp',
|
||||
})
|
||||
end
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
dofile('rive_build_config.lua')
|
||||
defines({ 'WITH_RIVE_TOOLS' })
|
||||
|
||||
RIVE_RUNTIME_DIR = path.getabsolute('..')
|
||||
RIVE_PLS_DIR = path.getabsolute('../renderer')
|
||||
@@ -299,6 +300,7 @@ do
|
||||
RIVE_PLS_DIR .. '/path_fiddle/fiddle_context_d3d12.cpp',
|
||||
RIVE_PLS_DIR .. '/path_fiddle/fiddle_context_vulkan.cpp',
|
||||
RIVE_PLS_DIR .. '/path_fiddle/fiddle_context_dawn.cpp',
|
||||
RIVE_PLS_DIR .. '/shader_hotload/**.cpp',
|
||||
})
|
||||
|
||||
if _TARGET_OS == 'windows' then
|
||||
|
||||
117
tests/unit_tests/renderer/rendering_tests.cpp
Normal file
117
tests/unit_tests/renderer/rendering_tests.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 Rive
|
||||
*/
|
||||
|
||||
#include "common/testing_window.hpp"
|
||||
#include "rive/renderer.hpp"
|
||||
#include "rive/factory.hpp"
|
||||
#include <catch.hpp>
|
||||
|
||||
namespace rive::gpu
|
||||
{
|
||||
// Factories to manually instantiate real rendering contexts, for unit testing
|
||||
// the full pipeline.
|
||||
static std::function<std::unique_ptr<TestingWindow>()>
|
||||
testingWindowFactories[] = {
|
||||
[]() {
|
||||
return rivestd::adopt_unique(TestingWindow::MakeVulkanTexture({
|
||||
#ifdef RIVE_ANDROID
|
||||
// Android doesn't support validation layers for command line
|
||||
// apps like the unit_tests.
|
||||
.disableValidationLayers = true,
|
||||
// The OnePlus7 doesn't support debug callbacks either for
|
||||
// command line apps.
|
||||
.disableDebugCallbacks = true,
|
||||
#endif
|
||||
}));
|
||||
},
|
||||
#if defined(__APPLE__)
|
||||
[]() {
|
||||
return rivestd::adopt_unique(TestingWindow::MakeMetalTexture());
|
||||
},
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
// TODO: d3d12 currently fails with:
|
||||
// Assertion failed: m_copyFence->GetCompletedValue() == 0, file
|
||||
// C:\...\fiddle_context_d3d12.cpp, line 179
|
||||
// []() {
|
||||
// return rivestd::adopt_unique(TestingWindow::MakeFiddleContext(
|
||||
// TestingWindow::Backend::d3d12,
|
||||
// TestingWindow::Visibility::headless,
|
||||
// {},
|
||||
// nullptr));
|
||||
// },
|
||||
[]() {
|
||||
return rivestd::adopt_unique(TestingWindow::MakeFiddleContext(
|
||||
TestingWindow::Backend::d3d,
|
||||
TestingWindow::Visibility::headless,
|
||||
{},
|
||||
nullptr));
|
||||
},
|
||||
#endif
|
||||
#ifdef RIVE_ANDROID
|
||||
[]() {
|
||||
return rivestd::adopt_unique(
|
||||
TestingWindow::MakeEGL(TestingWindow::Backend::gl, nullptr));
|
||||
},
|
||||
#endif
|
||||
};
|
||||
|
||||
// Ensure that rendering still succeeds when compilations fail (e.g., by falling
|
||||
// back on an uber shader or at least not crashing). Valid compilations may fail
|
||||
// in the real world if the device is pressed for resources or in a bad state.
|
||||
TEST_CASE("synthesizeCompilationFailure", "[rendering]")
|
||||
{
|
||||
for (auto testingWindowFactory : testingWindowFactories)
|
||||
{
|
||||
std::unique_ptr<TestingWindow> window = testingWindowFactory();
|
||||
if (window == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Factory* factory = window->factory();
|
||||
|
||||
window->resize(32, 32);
|
||||
|
||||
// Expected colors after we draw a cyan rectangle.
|
||||
std::vector<uint8_t> drawColors;
|
||||
drawColors.reserve(32 * 32 * 4);
|
||||
for (size_t i = 0; i < 32 * 32; ++i)
|
||||
drawColors.insert(drawColors.end(), {0x00, 0xff, 0xff, 0xff});
|
||||
|
||||
// Expected colors when only the clear happens (because even the uber
|
||||
// shader failed to compile).
|
||||
std::vector<uint8_t> clearColors;
|
||||
clearColors.reserve(32 * 32 * 4);
|
||||
for (size_t i = 0; i < 32 * 32; ++i)
|
||||
clearColors.insert(clearColors.end(), {0xff, 0x00, 0x00, 0xff});
|
||||
|
||||
for (bool disableRasterOrdering : {false, true})
|
||||
{
|
||||
auto renderer = window->beginFrame({
|
||||
.clearColor = 0xffff0000,
|
||||
.doClear = true,
|
||||
.disableRasterOrdering = disableRasterOrdering,
|
||||
.synthesizeCompilationFailures = true,
|
||||
});
|
||||
|
||||
rcp<RenderPath> path = factory->makeRenderPath(AABB{0, 0, 32, 32});
|
||||
rcp<RenderPaint> paint = factory->makeRenderPaint();
|
||||
paint->color(0xff00ffff);
|
||||
renderer->drawPath(path.get(), paint.get());
|
||||
|
||||
std::vector<uint8_t> pixels;
|
||||
window->endFrame(&pixels);
|
||||
|
||||
// There are two acceptable results to this test:
|
||||
//
|
||||
// 1) The draw happens anyway because we fell back on a precompiled
|
||||
// uber shader.
|
||||
//
|
||||
// 2) The uber shader also synthesizes a compilation faiulre, so
|
||||
// only the clear color makes it through.
|
||||
CHECK((pixels == drawColors || pixels == clearColors));
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace rive::gpu
|
||||
Reference in New Issue
Block a user