blob: 4c4d2a52b2c4281c542ddf8c85a3f186b72c5406 [file]
/*
* 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)