| /* |
| * 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) |