feat(Renderer): Allow GMs in testing to use Uber shader or not (#13626) f0c2d52b31
* Add files

* Add GM

* Add GMs

* Clang format

* Potential fix for pull request finding

* Update pr_platform_tests.yaml

* Address review comments

* clang format

* Update pr_platform_tests.yaml

* Update render_context_vulkan_impl.cpp

* Update render_context_d3d_impl.hpp

* Fix for vulkan crashing

* Add GM and exclude for bad platforms

* More Gms

* Update testing only func

* Update async_pipeline_manager.hpp

* Update async_pipeline_manager.hpp

* Update async_pipeline_manager.hpp

* Clang format

* Create uber_parity.png

* Temp code to see VK failures on CI

* Delete uber_parity.png

* Address comments

* Update async_pipeline_manager.hpp

* Update async_pipeline_manager.hpp

* Create uber_parity_srcover.png

* Create uber_parity_srcover.png

* Gms

* GMs

* Gms

* Update uber_parity.png

* Update fiddle_context.hpp

* Update fiddle_context.hpp

* Update uber_parity.cpp

* Clang format

* Update Gms

* Remove diagnostic code

* Update pr_platform_tests.yaml

* Update comment

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: John White <aliasbinman@gmail.com>
diff --git a/.rive_head b/.rive_head
index 19585ff..e54968d 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-bcdfe4fe5fcee41b74d3bf96489bd67cac7236a8
+f0c2d52b31981b709abcb03d156887638a58e5a7
diff --git a/renderer/include/rive/renderer/async_pipeline_manager.hpp b/renderer/include/rive/renderer/async_pipeline_manager.hpp
index 05b722a..5b016f3 100644
--- a/renderer/include/rive/renderer/async_pipeline_manager.hpp
+++ b/renderer/include/rive/renderer/async_pipeline_manager.hpp
@@ -76,6 +76,17 @@
         assert(!m_jobThread.joinable());
     }
 
+#ifdef WITH_RIVE_TOOLS
+    ShaderCompilationMode shaderCompilationMode() const { return m_mode; }
+    ShaderCompilationMode testingOnly_setShaderCompilationMode(
+        ShaderCompilationMode new_mode)
+    {
+        ShaderCompilationMode oldMode = m_mode;
+        m_mode = new_mode;
+        return oldMode;
+    }
+#endif
+
     const PipelineType* tryGetPipeline(const PipelineProps& propsIn,
                                        const PlatformFeatures& platformFeatures)
     {
@@ -104,8 +115,7 @@
 
             case ShaderCompilationMode::onlyUbershaders:
                 // For ubershader-only loading, we'll always use the full
-                // ubershader
-                //  feature flags and always load synchronously.
+                // ubershader feature flags and always load synchronously.
                 props.shaderFeatures = ubershaderFeatures;
                 [[fallthrough]];
 
@@ -674,7 +684,11 @@
 
     bool m_isDone = false;
     uint32_t m_activePipelineCreationCount = 0;
+#ifdef WITH_RIVE_TOOLS
+    ShaderCompilationMode m_mode = ShaderCompilationMode::standard;
+#else
     const ShaderCompilationMode m_mode = ShaderCompilationMode::standard;
+#endif
     std::thread m_jobThread;
     std::optional<PipelineKey> m_currentThreadPipelineKey;
     std::mutex m_mutex;
diff --git a/renderer/include/rive/renderer/d3d11/render_context_d3d_impl.hpp b/renderer/include/rive/renderer/d3d11/render_context_d3d_impl.hpp
index 7b1413c..f5a5450 100644
--- a/renderer/include/rive/renderer/d3d11/render_context_d3d_impl.hpp
+++ b/renderer/include/rive/renderer/d3d11/render_context_d3d_impl.hpp
@@ -250,6 +250,14 @@
 
     D3D11PipelineManager m_pipelineManager;
 
+#ifdef WITH_RIVE_TOOLS
+    ShaderCompilationMode testingOnly_setShaderCompilationMode(
+        ShaderCompilationMode mode) override
+    {
+        return m_pipelineManager.testingOnly_setShaderCompilationMode(mode);
+    }
+#endif
+
     const D3DCapabilities m_d3dCapabilities;
 
     ComPtr<ID3D11Device> m_gpu;
diff --git a/renderer/include/rive/renderer/d3d12/render_context_d3d12_impl.hpp b/renderer/include/rive/renderer/d3d12/render_context_d3d12_impl.hpp
index fc89576..c466483 100644
--- a/renderer/include/rive/renderer/d3d12/render_context_d3d12_impl.hpp
+++ b/renderer/include/rive/renderer/d3d12/render_context_d3d12_impl.hpp
@@ -237,6 +237,14 @@
     D3D12PipelineManager m_pipelineManager;
     rcp<D3D12ResourceManager> m_resourceManager;
 
+#ifdef WITH_RIVE_TOOLS
+    ShaderCompilationMode testingOnly_setShaderCompilationMode(
+        ShaderCompilationMode mode) override
+    {
+        return m_pipelineManager.testingOnly_setShaderCompilationMode(mode);
+    }
+#endif
+
     // Rive buffer pools. These don't need to be rcp<> because the destructor of
     // RenderContextVulkanImpl is already synchronized.
     D3D12VolatileBufferPool m_flushUniformBufferPool;
diff --git a/renderer/include/rive/renderer/gl/render_context_gl_impl.hpp b/renderer/include/rive/renderer/gl/render_context_gl_impl.hpp
index 7368771..01ad6a5 100644
--- a/renderer/include/rive/renderer/gl/render_context_gl_impl.hpp
+++ b/renderer/include/rive/renderer/gl/render_context_gl_impl.hpp
@@ -522,6 +522,14 @@
 
     GLPipelineManager m_pipelineManager;
 
+#ifdef WITH_RIVE_TOOLS
+    ShaderCompilationMode testingOnly_setShaderCompilationMode(
+        ShaderCompilationMode mode) override
+    {
+        return m_pipelineManager.testingOnly_setShaderCompilationMode(mode);
+    }
+#endif
+
     // Vertex/index buffers for drawing paths.
     glutils::VAO m_drawVAO;
     glutils::Buffer m_patchVerticesBuffer;
diff --git a/renderer/include/rive/renderer/render_context_impl.hpp b/renderer/include/rive/renderer/render_context_impl.hpp
index 932ddae..bb6c2e6 100644
--- a/renderer/include/rive/renderer/render_context_impl.hpp
+++ b/renderer/include/rive/renderer/render_context_impl.hpp
@@ -37,6 +37,19 @@
                                                RenderBufferFlags,
                                                size_t) = 0;
 
+#ifdef WITH_RIVE_TOOLS
+    // Changes how pipelines are selected and compiled for subsequent flushes,
+    // returning the previous mode. Backends that don't support runtime
+    // changes ignore the request (and echo the requested mode back). Testing
+    // only: lets the golden tests render individual frames through
+    // ubershaders.
+    virtual ShaderCompilationMode testingOnly_setShaderCompilationMode(
+        ShaderCompilationMode mode)
+    {
+        return mode;
+    }
+#endif
+
     // Use platform apis to decode the image bytes and creates a texture if
     // available. If not available leaving its default implementation will cause
     // rive decoders to be used instead
diff --git a/renderer/include/rive/renderer/vulkan/render_context_vulkan_impl.hpp b/renderer/include/rive/renderer/vulkan/render_context_vulkan_impl.hpp
index d42e8c7..487e92b 100644
--- a/renderer/include/rive/renderer/vulkan/render_context_vulkan_impl.hpp
+++ b/renderer/include/rive/renderer/vulkan/render_context_vulkan_impl.hpp
@@ -476,6 +476,11 @@
     rcp<DescriptorSetPoolPool> m_descriptorSetPoolPool;
 
     std::unique_ptr<PipelineManagerVulkan> m_pipelineManager;
+
+#ifdef WITH_RIVE_TOOLS
+    ShaderCompilationMode testingOnly_setShaderCompilationMode(
+        ShaderCompilationMode mode) override;
+#endif
 };
 } // namespace rive::gpu
 
diff --git a/renderer/src/vulkan/render_context_vulkan_impl.cpp b/renderer/src/vulkan/render_context_vulkan_impl.cpp
index 3f4ef16..e0dad19 100644
--- a/renderer/src/vulkan/render_context_vulkan_impl.cpp
+++ b/renderer/src/vulkan/render_context_vulkan_impl.cpp
@@ -4346,4 +4346,13 @@
 
     return std::make_unique<RenderContext>(std::move(impl));
 }
+
+#ifdef WITH_RIVE_TOOLS
+ShaderCompilationMode RenderContextVulkanImpl::
+    testingOnly_setShaderCompilationMode(ShaderCompilationMode mode)
+{
+    return m_pipelineManager->testingOnly_setShaderCompilationMode(mode);
+}
+#endif
+
 } // namespace rive::gpu
diff --git a/tests/common/testing_window.hpp b/tests/common/testing_window.hpp
index 5de6d65..029a82d 100644
--- a/tests/common/testing_window.hpp
+++ b/tests/common/testing_window.hpp
@@ -237,6 +237,8 @@
 #ifdef WITH_RIVE_TOOLS
         rive::gpu::SynthesizedFailureType synthesizedFailureType =
             rive::gpu::SynthesizedFailureType::none;
+        rive::gpu::ShaderCompilationMode shaderCompilationMode =
+            rive::gpu::ShaderCompilationMode::standard;
 #endif
     };
     virtual std::unique_ptr<rive::Renderer> beginFrame(const FrameOptions&) = 0;
diff --git a/tests/gm/gm.cpp b/tests/gm/gm.cpp
index b51714e..045ba18 100644
--- a/tests/gm/gm.cpp
+++ b/tests/gm/gm.cpp
@@ -5,6 +5,8 @@
 #include "gm.hpp"
 
 #include "common/testing_window.hpp"
+#include "rive/renderer/render_context.hpp"
+#include "rive/renderer/render_context_impl.hpp"
 
 using namespace rivegm;
 
@@ -16,9 +18,32 @@
         .triangulationThresholds = DeterministicTriangulationThresholds,
     };
     updateFrameOptions(&frameOptions);
+
+    rive::gpu::RenderContext* renderContext = nullptr;
+    auto previousMode = rive::gpu::ShaderCompilationMode::standard;
+    if (frameOptions.shaderCompilationMode !=
+        rive::gpu::ShaderCompilationMode::standard)
+    {
+        renderContext = TestingWindow::Get()->renderContext();
+        if (renderContext != nullptr)
+        {
+            previousMode =
+                renderContext->impl()->testingOnly_setShaderCompilationMode(
+                    rive::gpu::ShaderCompilationMode::onlyUbershaders);
+        }
+    }
+
     auto renderer = TestingWindow::Get()->beginFrame(frameOptions);
     draw(renderer.get());
     TestingWindow::Get()->endFrame(pixels);
+
+    if ((frameOptions.shaderCompilationMode !=
+         rive::gpu::ShaderCompilationMode::standard) &&
+        renderContext != nullptr)
+    {
+        renderContext->impl()->testingOnly_setShaderCompilationMode(
+            previousMode);
+    }
 }
 
 void GM::draw(rive::Renderer* renderer)
diff --git a/tests/gm/gmmain.cpp b/tests/gm/gmmain.cpp
index 3c2816d..39100cd 100644
--- a/tests/gm/gmmain.cpp
+++ b/tests/gm/gmmain.cpp
@@ -203,6 +203,8 @@
     MAKE_GM(negative_interior_triangles)
     MAKE_GM(negative_interior_triangles_as_clip)
     MAKE_GM(transparentclear_blendmode)
+    MAKE_GM(uber_parity)
+    MAKE_GM(uber_parity_srcover)
     MAKE_GM(emptystrokefeather)
     MAKE_GM(emptystroke)
     MAKE_GM(trickycubicstrokes)
diff --git a/tests/gm/uber_parity.cpp b/tests/gm/uber_parity.cpp
new file mode 100644
index 0000000..4c4d2a5
--- /dev/null
+++ b/tests/gm/uber_parity.cpp
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2026 Rive
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm.hpp"
+#include "gmutils.hpp"
+#include "common/testing_window.hpp"
+#include "rive/renderer.hpp"
+#include "rive/renderer/render_context.hpp"
+#include "rive/renderer/render_context_impl.hpp"
+
+#include "assets/nomoon.png.hpp"
+
+using namespace rivegm;
+using namespace rive;
+
+// Renders the same scene twice, side by side: the LEFT cell through
+// fully-featured ubershader pipelines and the RIGHT cell through specialized
+// pipelines (via a render pass break between the two). The cells must match:
+// a few LSB of rounding difference is expected on translucent content (the
+// premultiplied and unmultiplied paint paths round differently), but any
+// visible or structural divergence is an ubershader bug. The golden pins
+// both halves, so a regression in either flavor diffs immediately.
+//
+// The scene is chosen to stress where the two pipeline flavors are most
+// likely to diverge: ubershaders compile ENABLE_ADVANCED_BLEND, which flips
+// the entire paint path to unmultiplied colors. So the scene leans on
+// content whose premultiplied vs unmultiplied evaluation differs in rounding
+// and codepath:
+//   - translucent gradients fading to very low alpha (premultiply rounding),
+//   - an image drawn at low opacity (the unmultiplied path round-trips image
+//     texels through unmultiply/premultiply),
+//   - an advanced blend (multiply) overlapping srcOver content (forces the
+//     dst-read machinery alongside the plain path),
+//   - thin strokes over the gradient (AA edge rounding).
+//
+// On backends that can't switch shader compilation mode at runtime, both
+// cells render specialized and trivially match.
+constexpr float CELL = 300.f;
+
+static void draw_scene(Renderer* renderer, float ox)
+{
+    Factory* factory = TestingWindow::Get()->factory();
+    constexpr float stops[2] = {0.f, 1.f};
+
+    renderer->save();
+    renderer->translate(ox, 0);
+
+    // Opaque horizontal gradient base.
+    AABB base = {20.f, 20.f, 280.f, 280.f};
+    ColorInt baseColors[2] = {0xff2060c0, 0xffc06020};
+    Paint basePaint;
+    basePaint->shader(factory->makeLinearGradient(base.left(),
+                                                  0,
+                                                  base.right(),
+                                                  0,
+                                                  baseColors,
+                                                  stops,
+                                                  2));
+    renderer->drawPath(PathBuilder::Rect(base), basePaint);
+
+    // Translucent vertical gradient fading to near-zero alpha: the
+    // premultiplied path rounds rgb*a per fragment, the unmultiplied path
+    // premultiplies after coverage; low-alpha texels expose the difference.
+    AABB overlay = {50.f, 50.f, 250.f, 250.f};
+    ColorInt overlayColors[2] = {0xccff2020, 0x0dffff20};
+    Paint overlayPaint;
+    overlayPaint->shader(factory->makeLinearGradient(0,
+                                                     overlay.top(),
+                                                     0,
+                                                     overlay.bottom(),
+                                                     overlayColors,
+                                                     stops,
+                                                     2));
+    renderer->drawPath(PathBuilder::Rect(overlay), overlayPaint);
+
+    // Multiply square overlapping both gradients: advanced blend content in
+    // the same scene as plain srcOver draws.
+    Paint multiplyPaint;
+    multiplyPaint->color(0xff90b0d0);
+    multiplyPaint->blendMode(BlendMode::multiply);
+    renderer->drawPath(PathBuilder::Rect({140.f, 80.f, 260.f, 200.f}),
+                       multiplyPaint);
+
+    // Image at low opacity: in the unmultiplied path, image texels round-trip
+    // through unmultiply_rgb and a later premultiply.
+    auto img = LoadImage(assets::nomoon_png());
+    if (img != nullptr)
+    {
+        renderer->save();
+        renderer->translate(60.f, 120.f);
+        float scale = 140.f / std::max(img->width(), img->height());
+        renderer->scale(scale, scale);
+        renderer->drawImage(img.get(),
+                            ImageSampler::LinearClamp(),
+                            BlendMode::srcOver,
+                            .35f);
+        renderer->restore();
+    }
+
+    // Thin translucent strokes over everything: AA edge rounding.
+    for (int i = 0; i < 3; ++i)
+    {
+        Paint strokePaint;
+        strokePaint->style(RenderPaintStyle::stroke);
+        strokePaint->thickness(1.5f);
+        strokePaint->color(0x8020ff80);
+        renderer->drawPath(PathBuilder::Circle(150.f, 150.f, 60.f + 25.f * i),
+                           strokePaint);
+    }
+
+    renderer->restore();
+}
+
+class uber_parity_GM : public rivegm::GM
+{
+public:
+    uber_parity_GM() : GM(600, 300) {}
+    ColorInt clearColor() const override { return 0xff404040; }
+
+    void updateFrameOptions(TestingWindow::FrameOptions* options) const override
+    {
+        // Makes GM::run restore the process-wide mode after endFrame(); the
+        // per-half modes below override it in between.
+        options->shaderCompilationMode =
+            rive::gpu::ShaderCompilationMode::onlyUbershaders;
+    }
+
+    void onDraw(rive::Renderer* renderer) override
+    {
+        gpu::RenderContext* renderContext =
+            TestingWindow::Get()->renderContext();
+        if (renderContext == nullptr)
+        {
+            // No runtime mode switching on this backend; both cells render
+            // the same way and trivially match.
+            draw_scene(renderer, 0);
+            draw_scene(renderer, CELL);
+            return;
+        }
+
+        gpu::RenderContext::FrameDescriptor frameDescriptor =
+            renderContext->frameDescriptor();
+        frameDescriptor.loadAction = gpu::LoadAction::preserveRenderTarget;
+
+        for (int i = 0; i < 2; ++i)
+        {
+            // Flushes the previous half (and initially the clear) under the
+            // mode that was live when its draws were recorded.
+            TestingWindow::Get()->flushPLSContext();
+            renderContext->impl()->testingOnly_setShaderCompilationMode(
+                i == 0 ? gpu::ShaderCompilationMode::onlyUbershaders
+                       : gpu::ShaderCompilationMode::alwaysSynchronous);
+            renderContext->beginFrame(frameDescriptor);
+            draw_scene(renderer, i * CELL);
+        }
+        // endFrame() flushes the right half under alwaysSynchronous, then
+        // GM::run restores the process-wide mode.
+    }
+};
+GMREGISTER(uber_parity, return new uber_parity_GM)
diff --git a/tests/gm/uber_parity_srcover.cpp b/tests/gm/uber_parity_srcover.cpp
new file mode 100644
index 0000000..38dfea6
--- /dev/null
+++ b/tests/gm/uber_parity_srcover.cpp
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2026 Rive
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm.hpp"
+#include "gmutils.hpp"
+#include "common/testing_window.hpp"
+#include "rive/renderer.hpp"
+#include "rive/renderer/render_context.hpp"
+#include "rive/renderer/render_context_impl.hpp"
+
+#include "assets/nomoon.png.hpp"
+
+using namespace rivegm;
+using namespace rive;
+
+// Companion to uber_parity: the same left-uber / right-specialized layout,
+// but with a srcOver-only scene and dithering disabled. Where uber_parity
+// checks that the features an ubershader *uses* match specialized rendering,
+// this GM checks that the features an ubershader compiles but does NOT use
+// are true no-ops:
+//
+//   - ENABLE_DITHER: specialized pipelines omit the feature entirely; the
+//     ubershader still runs the dither math and must neutralize it via
+//     0-value uniforms (ditherMode == none zeroes ditherScale/ditherBias).
+//     This zero-uniform path has no other test coverage.
+//   - ENABLE_ADVANCED_BLEND: compiled in the ubershader (flipping the whole
+//     paint path to unmultiplied colors), but every draw here is srcOver.
+//   - ENABLE_EVEN_ODD, ENABLE_CLIP_RECT, ENABLE_CLIPPING: compiled in the
+//     ubershader; the scene contains no even-odd fills, no clipRect, and no
+//     clipping.
+//
+// The specialized half compiles none of the above. The halves must match
+// within a few LSB of premult/unmult rounding on translucent content; any
+// structural divergence means an enabled-but-unused ubershader feature is
+// not a no-op.
+//
+// On backends that can't switch shader compilation mode at runtime, both
+// cells render specialized and trivially match.
+constexpr float CELL = 300.f;
+
+static void draw_srcover_scene(Renderer* renderer, float ox)
+{
+    Factory* factory = TestingWindow::Get()->factory();
+    constexpr float stops[2] = {0.f, 1.f};
+
+    renderer->save();
+    renderer->translate(ox, 0);
+
+    // Opaque horizontal gradient base. Banding on this smooth ramp must be
+    // identical in both halves: neither half may dither (the ubershader half
+    // has the dither code compiled and must no-op it).
+    AABB base = {20.f, 20.f, 280.f, 280.f};
+    ColorInt baseColors[2] = {0xff2060c0, 0xffc06020};
+    Paint basePaint;
+    basePaint->shader(factory->makeLinearGradient(base.left(),
+                                                  0,
+                                                  base.right(),
+                                                  0,
+                                                  baseColors,
+                                                  stops,
+                                                  2));
+    renderer->drawPath(PathBuilder::Rect(base), basePaint);
+
+    // Translucent vertical gradient fading to near-zero alpha: the
+    // premultiplied path rounds rgb*a per fragment, the unmultiplied path
+    // premultiplies after coverage; low-alpha texels expose the difference.
+    AABB overlay = {50.f, 50.f, 250.f, 250.f};
+    ColorInt overlayColors[2] = {0xccff2020, 0x0dffff20};
+    Paint overlayPaint;
+    overlayPaint->shader(factory->makeLinearGradient(0,
+                                                     overlay.top(),
+                                                     0,
+                                                     overlay.bottom(),
+                                                     overlayColors,
+                                                     stops,
+                                                     2));
+    renderer->drawPath(PathBuilder::Rect(overlay), overlayPaint);
+
+    // Image at low opacity: in the unmultiplied path, image texels round-trip
+    // through unmultiply_rgb and a later premultiply.
+    auto img = LoadImage(assets::nomoon_png());
+    if (img != nullptr)
+    {
+        renderer->save();
+        renderer->translate(60.f, 120.f);
+        float scale = 140.f / std::max(img->width(), img->height());
+        renderer->scale(scale, scale);
+        renderer->drawImage(img.get(),
+                            ImageSampler::LinearClamp(),
+                            BlendMode::srcOver,
+                            .35f);
+        renderer->restore();
+    }
+
+    // Thin translucent strokes over everything: AA edge rounding.
+    for (int i = 0; i < 3; ++i)
+    {
+        Paint strokePaint;
+        strokePaint->style(RenderPaintStyle::stroke);
+        strokePaint->thickness(1.5f);
+        strokePaint->color(0x8020ff80);
+        renderer->drawPath(PathBuilder::Circle(150.f, 150.f, 60.f + 25.f * i),
+                           strokePaint);
+    }
+
+    renderer->restore();
+}
+
+class uber_parity_srcover_GM : public rivegm::GM
+{
+public:
+    uber_parity_srcover_GM() : GM(600, 300) {}
+    ColorInt clearColor() const override { return 0xff404040; }
+
+    void updateFrameOptions(TestingWindow::FrameOptions* options) const override
+    {
+        // Makes GM::run restore the process-wide mode after endFrame(); the
+        // per-half modes below override it in between.
+        options->shaderCompilationMode =
+            rive::gpu::ShaderCompilationMode::onlyUbershaders;
+    }
+
+    void onDraw(rive::Renderer* renderer) override
+    {
+        gpu::RenderContext* renderContext =
+            TestingWindow::Get()->renderContext();
+        if (renderContext == nullptr)
+        {
+            // No runtime mode switching on this backend; both cells render
+            // the same way and trivially match.
+            draw_srcover_scene(renderer, 0);
+            draw_srcover_scene(renderer, CELL);
+            return;
+        }
+
+        gpu::RenderContext::FrameDescriptor frameDescriptor =
+            renderContext->frameDescriptor();
+        frameDescriptor.loadAction = gpu::LoadAction::preserveRenderTarget;
+        // Dither off: the specialized half drops ENABLE_DITHER from its
+        // pipelines; the ubershader half keeps the feature compiled and must
+        // neutralize it through the zeroed dither uniforms.
+        frameDescriptor.ditherMode = gpu::DitherMode::none;
+
+        for (int i = 0; i < 2; ++i)
+        {
+            // Flushes the previous half (and initially the clear) under the
+            // mode that was live when its draws were recorded.
+            TestingWindow::Get()->flushPLSContext();
+            renderContext->impl()->testingOnly_setShaderCompilationMode(
+                i == 0 ? gpu::ShaderCompilationMode::onlyUbershaders
+                       : gpu::ShaderCompilationMode::alwaysSynchronous);
+            renderContext->beginFrame(frameDescriptor);
+            draw_srcover_scene(renderer, i * CELL);
+        }
+        // endFrame() flushes the right half under alwaysSynchronous, then
+        // GM::run restores the process-wide mode.
+    }
+};
+GMREGISTER(uber_parity_srcover, return new uber_parity_srcover_GM)