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:
csmartdalton
2025-05-30 04:52:57 +00:00
parent a8cb90f661
commit e89cda8542
21 changed files with 381 additions and 163 deletions

View File

@@ -1 +1 @@
6582d5bf02f1751b9968f3e2156f209224336df8
c09b771645eb07550b1c4cf276dffe128670769e

View File

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

View File

@@ -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.

View File

@@ -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;

View File

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

View File

@@ -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.
};

View File

@@ -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;

View File

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

View File

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

View File

@@ -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 =

View File

@@ -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;

View File

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

View File

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

View File

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

View File

@@ -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());

View File

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

View File

@@ -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];

View File

@@ -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());

View File

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

View File

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

View 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