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)