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