feat(renderer): Add scissor support to more backends (#12778) 424d2323f9 This adds support for using scissor rects during clips to OpenGL, D3D 11 and 12, and Metal. WebGPU and the external platforms are not yet done This also adds a few new gms, and updates the rebaseline script to work with a new directory (which does not have a subdirectory) Co-authored-by: Josh Jersild <joshua@rive.app>
diff --git a/.rive_head b/.rive_head index aeb8937..4d85c79 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -b2438309fa61b3d52d4d1a0e6e97f3ef5ce71bd6 +424d2323f9ff926290455cfc2da0d8cdcaba430f
diff --git a/include/rive/math/aabb.hpp b/include/rive/math/aabb.hpp index f4b6b05..7cd08ae 100644 --- a/include/rive/math/aabb.hpp +++ b/include/rive/math/aabb.hpp
@@ -41,6 +41,14 @@ std::min(bottom, math::clamp_cast<T>(b.bottom))}; } + // Do an intersection, but return a consistent, fully-zero rectangle if the + // intersection ends up empty or negative. + template <typename U> TAABB intersectOrEmpty(TAABB<U> b) const + { + auto r = intersect(b); + return r.empty() ? TAABB{} : r; + } + template <typename U> TAABB<U> lossless_numeric_cast() const { return {
diff --git a/renderer/include/rive/renderer/gl/gl_state.hpp b/renderer/include/rive/renderer/gl/gl_state.hpp index d083fa2..484f29d 100644 --- a/renderer/include/rive/renderer/gl/gl_state.hpp +++ b/renderer/include/rive/renderer/gl/gl_state.hpp
@@ -9,8 +9,16 @@ #include "rive/refcnt.hpp" #include "rive/renderer/gpu.hpp" +#include <optional> + namespace rive::gpu { +enum class ScissorAction : bool +{ + disable, // Disable scissor rect for this pipeline + ignore, // Do not set or clear the scissor rect +}; + // Lightweight wrapper around common GL state. class GLState : public RefCnt<GLState> { @@ -28,6 +36,7 @@ // box before passing it to GL, which is why this function needs // renderTargetHeight.) void setScissor(IAABB, uint32_t renderTargetHeight); + void setScissor(AABBu16, uint32_t renderTargetHeight); // Set the scissor with the raw values that will be passed to glScissor(). void setScissorRaw(uint32_t left, uint32_t top, @@ -41,7 +50,8 @@ uint8_t stencilWriteMask); void setBlendEquation(gpu::BlendEquation); void disableBlending() { setBlendEquation(gpu::BlendEquation::none); } - void setPipelineState(const gpu::PipelineState&); + void setPipelineState(const gpu::PipelineState&, + ScissorAction = ScissorAction::disable); void bindProgram(GLuint); void bindVAO(GLuint);
diff --git a/renderer/src/d3d11/render_context_d3d_impl.cpp b/renderer/src/d3d11/render_context_d3d_impl.cpp index 879c90c..69143b7 100644 --- a/renderer/src/d3d11/render_context_d3d_impl.cpp +++ b/renderer/src/d3d11/render_context_d3d_impl.cpp
@@ -518,6 +518,9 @@ d3dCapabilities.supportsRasterizerOrderedViews; m_platformFeatures.supportsAtomicMode = true; m_platformFeatures.maxTextureSize = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION; + + m_platformFeatures.supportsClipScissor = true; + // BC1–BC7 are required at D3D feature level 11.0+. m_platformFeatures.supportsTextureCompressionBC = true; @@ -537,7 +540,8 @@ // Details on default state here: // https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_rasterizer_desc - rasterDesc.ScissorEnable = FALSE; + // Always enable scissor - we'll set it to the full display when it's unused + rasterDesc.ScissorEnable = TRUE; rasterDesc.MultisampleEnable = FALSE; rasterDesc.AntialiasedLineEnable = FALSE; VERIFY_OK(m_gpu->CreateRasterizerState( @@ -546,7 +550,6 @@ // ...And with scissor and no culling for the atlas fill. rasterDesc.CullMode = D3D11_CULL_NONE; - rasterDesc.ScissorEnable = TRUE; VERIFY_OK(m_gpu->CreateRasterizerState( &rasterDesc, m_atlasFillRasterState.ReleaseAndGetAddressOf())); @@ -558,7 +561,6 @@ m_atlasStrokeRasterState.ReleaseAndGetAddressOf())); // ...And with wireframe for debugging. - rasterDesc.ScissorEnable = FALSE; rasterDesc.FillMode = D3D11_FILL_WIREFRAME; VERIFY_OK(m_gpu->CreateRasterizerState( &rasterDesc, @@ -2032,6 +2034,13 @@ !desc.fixedFunctionColorOutput && !renderTarget->targetTextureSupportsUAV(); + const auto fullUpdateScissorRect = + desc.renderTargetUpdateBounds.lossless_numeric_cast<uint16_t>(); + + // Start the current scissor rect out inside-out to guarantee that the first + // rectangle we get doesn't match it. + auto currentScissorRect = AABBu16{0xffff, 0xffff, 0, 0}; + for (const DrawBatch& batch : *desc.drawList) { DrawType drawType = batch.drawType; @@ -2064,6 +2073,18 @@ continue; } + auto desiredScissorRect = batch.scissorRect.has_value() + ? fullUpdateScissorRect.intersectOrEmpty( + batch.scissorRect.value()) + : fullUpdateScissorRect; + + if (desiredScissorRect != currentScissorRect) + { + currentScissorRect = desiredScissorRect; + D3D11_RECT scissor = make_scissor(currentScissorRect); + m_gpuContext->RSSetScissorRects(1, &scissor); + } + if (auto imageTextureD3D = static_cast<const TextureD3DImpl*>(batch.imageTexture)) {
diff --git a/renderer/src/d3d12/render_context_d3d12_impl.cpp b/renderer/src/d3d12/render_context_d3d12_impl.cpp index 8698b31..5b158b6 100644 --- a/renderer/src/d3d12/render_context_d3d12_impl.cpp +++ b/renderer/src/d3d12/render_context_d3d12_impl.cpp
@@ -661,6 +661,9 @@ m_capabilities.supportsRasterizerOrderedViews; m_platformFeatures.supportsAtomicMode = true; m_platformFeatures.maxTextureSize = D3D12_REQ_TEXTURE2D_U_OR_V_DIMENSION; + + m_platformFeatures.supportsClipScissor = true; + // BC1–BC7 are required at D3D feature level 11.0+. m_platformFeatures.supportsTextureCompressionBC = true; @@ -1570,12 +1573,7 @@ 0.0f, static_cast<float>(width), static_cast<float>(height)); - CD3DX12_RECT scissorRect(desc.renderTargetUpdateBounds.left, - desc.renderTargetUpdateBounds.top, - desc.renderTargetUpdateBounds.right, - desc.renderTargetUpdateBounds.bottom); cmdList->RSSetViewports(1, &viewport); - cmdList->RSSetScissorRects(1, &scissorRect); // Setup and clear the PLS textures. { @@ -1720,6 +1718,13 @@ m_heapDescriptorOffset += imageDescriptorOffset; + const auto fullUpdateScissorRect = + desc.renderTargetUpdateBounds.lossless_numeric_cast<uint16_t>(); + + // Start the current scissor rect out inside-out to guarantee that the first + // rectangle we get doesn't match it. + auto currentScissorRect = AABBu16{0xffff, 0xffff, 0, 0}; + RIVE_PROF_GPUNAME_L(1, "DrawList"); for (const DrawBatch& batch : *desc.drawList) { @@ -1782,6 +1787,24 @@ continue; } + { + auto desiredScissorRect = + batch.scissorRect.has_value() + ? fullUpdateScissorRect.intersectOrEmpty( + batch.scissorRect.value()) + : fullUpdateScissorRect; + + if (desiredScissorRect != currentScissorRect) + { + currentScissorRect = desiredScissorRect; + CD3DX12_RECT scissorRect{currentScissorRect.left, + currentScissorRect.top, + currentScissorRect.right, + currentScissorRect.bottom}; + cmdList->RSSetScissorRects(1, &scissorRect); + } + } + cmdList->SetPipelineState(pipeline->m_d3dPipelineState.Get()); if (auto imageTextureD3D12 =
diff --git a/renderer/src/gl/gl_state.cpp b/renderer/src/gl/gl_state.cpp index 54ce655..f3fb877 100644 --- a/renderer/src/gl/gl_state.cpp +++ b/renderer/src/gl/gl_state.cpp
@@ -93,6 +93,16 @@ scissor.height()); } +void GLState::setScissor(AABBu16 scissor, uint32_t renderTargetHeight) +{ + assert(scissor.right >= scissor.left); + assert(scissor.bottom >= scissor.top); + setScissorRaw(scissor.left, + renderTargetHeight - scissor.bottom, + scissor.width(), + scissor.height()); +} + void GLState::setScissorRaw(uint32_t left, uint32_t top, uint32_t width, @@ -345,9 +355,19 @@ } } -void GLState::setPipelineState(const gpu::PipelineState& pipelineState) +void GLState::setPipelineState(const gpu::PipelineState& pipelineState, + ScissorAction scissorAction) { - disableScissor(); // Scissor isn't currently used in the pipeline state. + switch (scissorAction) + { + case ScissorAction::disable: + disableScissor(); + break; + + case ScissorAction::ignore: + break; + } + setDepthStencilEnabled(pipelineState.depthTestEnabled, pipelineState.stencilTestEnabled); if (pipelineState.stencilTestEnabled)
diff --git a/renderer/src/gl/pls_impl_rw_texture.cpp b/renderer/src/gl/pls_impl_rw_texture.cpp index d215351..7548e52 100644 --- a/renderer/src/gl/pls_impl_rw_texture.cpp +++ b/renderer/src/gl/pls_impl_rw_texture.cpp
@@ -144,7 +144,8 @@ // Bind and initialize the PLS backing textures. renderContextImpl->state()->setPipelineState( - gpu::COLOR_ONLY_PIPELINE_STATE); + gpu::COLOR_ONLY_PIPELINE_STATE, + ScissorAction::ignore); renderContextImpl->state()->setScissor(desc.renderTargetUpdateBounds, renderTarget->height());
diff --git a/renderer/src/gl/render_context_gl_impl.cpp b/renderer/src/gl/render_context_gl_impl.cpp index a47b3e8..8ef7fb0 100644 --- a/renderer/src/gl/render_context_gl_impl.cpp +++ b/renderer/src/gl/render_context_gl_impl.cpp
@@ -208,6 +208,16 @@ glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize); m_platformFeatures.maxTextureSize = maxTextureSize; + if (!capabilities.isAdreno || capabilities.adrenoSeries < 600 || + capabilities.adrenoSeries >= 700) + { + // Currently there's what appears to be a driver bug where setting a + // scissor rect on the atlasBlit step (even if the rect is the full + // render target) causes some display corruption. Until we can find a + // workaround, just disable clip scissor on Adreno 6xx models. + m_platformFeatures.supportsClipScissor = true; + } + m_platformFeatures.supportsTextureCompressionBC = m_capabilities.EXT_texture_compression_s3tc && m_capabilities.EXT_texture_compression_bptc; @@ -2385,7 +2395,8 @@ if ((desc.atlasFillBatchCount | desc.atlasStrokeBatchCount) != 0) { // Finish setting up the atlas render pass and clear the atlas. - m_state->setPipelineState(gpu::COLOR_ONLY_PIPELINE_STATE); + m_state->setPipelineState(gpu::COLOR_ONLY_PIPELINE_STATE, + ScissorAction::ignore); glBindFramebuffer(GL_FRAMEBUFFER, m_atlasRenderFBO); glViewport(0, 0, desc.atlasContentWidth, desc.atlasContentHeight); @@ -2468,7 +2479,8 @@ // Draw the atlas fills. if (desc.atlasFillBatchCount != 0) { - m_state->setPipelineState(m_atlasFillPipelineState); + m_state->setPipelineState(m_atlasFillPipelineState, + ScissorAction::ignore); m_state->bindProgram(m_atlasFillProgram); for (size_t i = 0; i < desc.atlasFillBatchCount; ++i) { @@ -2491,7 +2503,8 @@ // Draw the atlas strokes. if (desc.atlasStrokeBatchCount != 0) { - m_state->setPipelineState(m_atlasStrokePipelineState); + m_state->setPipelineState(m_atlasStrokePipelineState, + ScissorAction::ignore); m_state->bindProgram(m_atlasStrokeProgram); for (size_t i = 0; i < desc.atlasStrokeBatchCount; ++i) { @@ -2721,6 +2734,9 @@ bool clipPlanesEnabled = false; + const auto fullUpdateScissorRect = + desc.renderTargetUpdateBounds.lossless_numeric_cast<uint16_t>(); + // Execute the DrawList. for (const DrawBatch& batch : *desc.drawList) { @@ -2793,7 +2809,18 @@ clipPlanesEnabled = needsClipPlanes; } } - m_state->setPipelineState(pipelineState); + + if (batch.scissorRect.has_value()) + { + auto scissorRect = fullUpdateScissorRect.intersectOrEmpty( + batch.scissorRect.value()); + m_state->setPipelineState(pipelineState, ScissorAction::ignore); + m_state->setScissor(scissorRect, renderTarget->height()); + } + else + { + m_state->setPipelineState(pipelineState); + } if (enums::any_flag_set(batch.barriers, BarrierFlags::plsAtomic | @@ -3114,7 +3141,8 @@ glutils::Uniform1iByName(m_blitAsDrawProgram, GLSL_sourceTexture, 0); } - m_state->setPipelineState(gpu::COLOR_ONLY_PIPELINE_STATE); + m_state->setPipelineState(gpu::COLOR_ONLY_PIPELINE_STATE, + ScissorAction::ignore); m_state->setScissor(bounds, renderTargetHeight); m_state->bindProgram(m_blitAsDrawProgram); m_state->bindVAO(m_emptyVAO);
diff --git a/renderer/src/metal/render_context_metal_impl.mm b/renderer/src/metal/render_context_metal_impl.mm index 2a44671..18131db 100644 --- a/renderer/src/metal/render_context_metal_impl.mm +++ b/renderer/src/metal/render_context_metal_impl.mm
@@ -500,6 +500,8 @@ #endif m_platformFeatures.atomicPLSInitNeedsDraw = true; + m_platformFeatures.supportsClipScissor = true; + // Texture compression support varies by Apple platform family. #if defined(RIVE_IOS) || defined(RIVE_XROS) || defined(RIVE_APPLETVOS) || \ defined(RIVE_IOS_SIMULATOR) || defined(RIVE_XROS_SIMULATOR) || \ @@ -1280,7 +1282,7 @@ }; } -static MTLScissorRect make_scissor(const TAABB<uint16_t>& scissor) +static MTLScissorRect make_scissor(const AABBu16& scissor) { return { static_cast<NSUInteger>(scissor.left), @@ -1736,6 +1738,13 @@ } // Execute the DrawList. + + // Start the current scissor rect inside out to guarantee that the first + // rectangle we get doesn't match it. + const auto fullRenderTargetScissorRect = + desc.renderTargetUpdateBounds.lossless_numeric_cast<uint16_t>(); + auto currentScissorRect = AABBu16{0xffff, 0xffff, 0, 0}; + id<MTLRenderCommandEncoder> encoder = makeRenderPassForDraws( desc, pass, commandBuffer, baselineShaderMiscFlags); for (const DrawBatch& batch : *desc.drawList) @@ -1798,6 +1807,21 @@ // Skip the draw. continue; } + + { + auto desiredScissorRect = + batch.scissorRect.has_value() + ? fullRenderTargetScissorRect.intersectOrEmpty( + batch.scissorRect.value()) + : fullRenderTargetScissorRect; + + if (desiredScissorRect != currentScissorRect) + { + currentScissorRect = desiredScissorRect; + [encoder setScissorRect:make_scissor(currentScissorRect)]; + } + } + id<MTLRenderPipelineState> drawPipelineState = drawPipeline->pipelineState(renderTarget->pixelFormat());
diff --git a/renderer/src/render_context.cpp b/renderer/src/render_context.cpp index 5fdeb58..6ab905e 100644 --- a/renderer/src/render_context.cpp +++ b/renderer/src/render_context.cpp
@@ -1539,7 +1539,8 @@ // Write out all the data for our high level draws, and build up a low-level // draw list. - if (m_ctx->frameInterlockMode() == gpu::InterlockMode::rasterOrdering) + if (!platformFeatures.supportsClipScissor && + m_ctx->frameInterlockMode() == gpu::InterlockMode::rasterOrdering) { for (const DrawUniquePtr& draw : m_draws) { @@ -1911,10 +1912,6 @@ // bitmask, we insert a barrier between them. switch (m_flushDesc.interlockMode) { - case gpu::InterlockMode::rasterOrdering: - // rasterOrdering and clockwise modes don't reorder draws. - RIVE_UNREACHABLE(); - case gpu::InterlockMode::atomics: { // In atomic mode, we need barriers any time draws overlap. @@ -1929,9 +1926,10 @@ break; } + case gpu::InterlockMode::rasterOrdering: case gpu::InterlockMode::clockwise: - // clockwise mode doesn't need barriers, but we still reorder in - // order to improve batching. + // clockwise and rasterOrdering modes don't need barriers, but + // we still reorder in order to improve batching. break; case gpu::InterlockMode::clockwiseAtomic:
diff --git a/renderer/src/vulkan/render_context_vulkan_impl.cpp b/renderer/src/vulkan/render_context_vulkan_impl.cpp index 2212fab..4b14e6a 100644 --- a/renderer/src/vulkan/render_context_vulkan_impl.cpp +++ b/renderer/src/vulkan/render_context_vulkan_impl.cpp
@@ -3596,15 +3596,11 @@ default: // Default behavior is to use the batch's scissor rect, // intersected with the render pass' rect. - desiredScissorRect = batch.scissorRect.has_value() - ? renderPassScissorRect.intersect( - batch.scissorRect.value()) - : renderPassScissorRect; - } - - if (desiredScissorRect.empty()) - { - desiredScissorRect = {0, 0, 0, 0}; + desiredScissorRect = + batch.scissorRect.has_value() + ? renderPassScissorRect.intersectOrEmpty( + batch.scissorRect.value()) + : renderPassScissorRect; } if (desiredScissorRect != currentScissorRect)
diff --git a/tests/gm/clipshapes.cpp b/tests/gm/clipshapes.cpp new file mode 100644 index 0000000..dbbe11e --- /dev/null +++ b/tests/gm/clipshapes.cpp
@@ -0,0 +1,158 @@ +/* + * Copyright 2026 Rive + */ + +#include "gm.hpp" +#include "gmutils.hpp" +#include "rive/math/math_types.hpp" +#include "rive/renderer.hpp" +#include "common/rand.hpp" + +using namespace rivegm; +using namespace rive; + +namespace +{ +enum class UseFeather : bool +{ + No, + Yes, +}; + +enum class ShapeSize +{ + Small, + Large, + Stretch, +}; + +template <UseFeather Feather, + ShapeSize InnerSize, + BlendMode Blend = BlendMode::srcOver> +class ClipShapesGM : public GM +{ +public: + static constexpr int32_t SectionSize = 400; + static constexpr int32_t Spacing = 50; + static constexpr int32_t RoundRadius = 50; + static constexpr int32_t SectionCount = 3; + static constexpr int32_t ImageDim = SectionCount * SectionSize; + + ClipShapesGM() : GM(ImageDim, ImageDim) {} + + void onDraw(rive::Renderer* renderer) override + { + { + Paint p; + p->color(0xff000000); + renderer->drawPath( + PathBuilder::Rect( + {0.0f, 0.0f, float(ImageDim), float(ImageDim)}), + p); + } + { + Paint p; + p->color(0xff2c1642); + p->feather(10.0f); + auto yStep = 40.0f; + for (auto y = 20.0f; y < float(SectionCount * SectionSize); y++) + { + auto path = PathBuilder::Rect({ + -100.0f, + y, + float(ImageDim) + 100.0f, + y + yStep * 0.3f, + }); + path->fillRule(FillRule::clockwise); + renderer->drawPath(path, p); + y += yStep; + yStep *= 1.12f; + } + } + constexpr uint32_t Colors[SectionCount][SectionCount] = { + { + 0xff881177, + 0xffeedd00, + 0xff00bbcc, + }, + { + 0xffaa3355, + 0xff99dd55, + 0xffee9944, + }, + { + 0xffcc6666, + 0xff3366bb, + 0xff22ccbb, + }, + }; + for (auto yIndex = 0; yIndex < 3; yIndex++) + { + for (auto xIndex = 0; xIndex < 3; xIndex++) + { + renderer->save(); + { + const auto box = AABB{ + float(SectionSize * xIndex + Spacing / 2), + float(SectionSize * yIndex + Spacing / 2), + float(SectionSize * (xIndex + 1) - Spacing / 2), + float(SectionSize * (yIndex + 1) - Spacing / 2), + }; + auto clipper = PathBuilder::RRect(box, + float(RoundRadius), + float(RoundRadius)); + renderer->clipPath(clipper); + + Paint p; + p->color(Colors[xIndex][yIndex]); + p->blendMode(Blend); + if constexpr (Feather == UseFeather::Yes) + { + p->feather(float(Spacing)); + } + + if constexpr (InnerSize == ShapeSize::Large) + { + const auto dim = + float(SectionCount * SectionSize) * 0.5f; + auto path = PathBuilder::Circle(dim, dim, dim * 0.8f); + path->fillRule(FillRule::clockwise); + renderer->drawPath(path, p); + } + else + { + auto x = lerp(box.right(), + box.left(), + float(xIndex) / float(SectionCount - 1)); + auto y = lerp(box.bottom(), + box.top(), + float(yIndex) / float(SectionCount - 1)); + auto path = + PathBuilder::Circle(x, y, SectionSize * 0.24f); + path->fillRule(FillRule::clockwise); + renderer->drawPath(path, p); + } + } + renderer->restore(); + } + } + } +}; +} // namespace + +GMREGISTER(clip_shapes_large_corners, + return (new ClipShapesGM<UseFeather::No, ShapeSize::Large>())) +GMREGISTER(clip_shapes_large_corners_feathered, + return (new ClipShapesGM<UseFeather::Yes, ShapeSize::Large>())) +GMREGISTER(clip_shapes_small_corners, + return (new ClipShapesGM<UseFeather::No, ShapeSize::Small>())) +GMREGISTER(clip_shapes_small_corners_feathered, + return (new ClipShapesGM<UseFeather::Yes, ShapeSize::Small>())) +GMREGISTER(clip_shapes_large_corners_feathered_blend, + return (new ClipShapesGM<UseFeather::Yes, + ShapeSize::Large, + BlendMode::screen>())) +GMREGISTER(clip_shapes_small_corners_feathered_blend, + return (new ClipShapesGM<UseFeather::Yes, + ShapeSize::Small, + BlendMode::screen>()))
diff --git a/tests/gm/gmmain.cpp b/tests/gm/gmmain.cpp index c0c34cf..36dc0ce 100644 --- a/tests/gm/gmmain.cpp +++ b/tests/gm/gmmain.cpp
@@ -56,6 +56,12 @@ MAKE_GM(bug615686) MAKE_GM(cliprectintersections) MAKE_GM(cliprects) + MAKE_GM(clip_shapes_large_corners) + MAKE_GM(clip_shapes_large_corners_feathered) + MAKE_GM(clip_shapes_large_corners_feathered_blend) + MAKE_GM(clip_shapes_small_corners) + MAKE_GM(clip_shapes_small_corners_feathered) + MAKE_GM(clip_shapes_small_corners_feathered_blend) MAKE_GM(concavepaths) MAKE_GM(convexpaths) MAKE_GM(convex_lineonly_ths)