| /* |
| * Copyright 2026 Rive |
| * |
| * A canvas sampling another canvas replays after its writer regardless of |
| * record order, so the reversed recording must match the in-order one. The |
| * cycle GM pins the demoted back edge to previous-frame sampling. |
| */ |
| |
| #include "gm.hpp" |
| #include "gmutils.hpp" |
| |
| #if defined(WITH_RIVE_SCRIPTING) && defined(RIVE_CANVAS) |
| |
| #include "rive/renderer/render_canvas.hpp" |
| #include "rive/renderer/rive_renderer.hpp" |
| #include "rive/renderer/cmd/deferred_replayer.hpp" |
| #include "rive/renderer/cmd/deferred_session.hpp" |
| |
| using namespace rivegm; |
| using namespace rive; |
| using namespace rive::gpu; |
| |
| namespace |
| { |
| // DeferredFrameSink over the GM harness. The harness frame is already open, |
| // so the first sink action flushes it and later frames resume with preserve. |
| class DagGMSink : public rive::cmd::DeferredFrameSink |
| { |
| public: |
| DagGMSink(RenderContext* rc, |
| const RenderContext::FrameDescriptor& mainDesc) : |
| m_rc(rc), m_mainDesc(mainDesc) |
| {} |
| |
| Factory* factory() override { return TestingWindow::Get()->factory(); } |
| |
| // The GM harness owns one main render target. |
| Renderer* beginScreenFrame(uint64_t target) override |
| { |
| assert(target == 0); |
| flushOpenFrame(); |
| auto d = m_mainDesc; |
| d.loadAction = LoadAction::preserveRenderTarget; |
| m_rc->beginFrame(std::move(d)); |
| m_frameOpen = true; |
| m_screen = std::make_unique<RiveRenderer>(m_rc); |
| return m_screen.get(); |
| } |
| |
| Renderer* beginCanvasContent(RenderCanvas* canvas, |
| uint32_t clearColor) override |
| { |
| flushOpenFrame(); |
| m_activeCanvas = canvas; |
| auto d = m_mainDesc; |
| d.renderTargetWidth = canvas->width(); |
| d.renderTargetHeight = canvas->height(); |
| d.loadAction = LoadAction::clear; |
| d.clearColor = clearColor; |
| m_rc->beginFrame(std::move(d)); |
| m_frameOpen = true; |
| m_canvasRenderer = std::make_unique<RiveRenderer>(m_rc); |
| return m_canvasRenderer.get(); |
| } |
| |
| void endCanvasContent() override |
| { |
| if (m_activeCanvas == nullptr) |
| { |
| return; |
| } |
| TestingWindow::Get()->flushPLSContext(m_activeCanvas->renderTarget()); |
| m_frameOpen = false; |
| m_canvasRenderer = nullptr; |
| m_activeCanvas = nullptr; |
| } |
| |
| private: |
| // The harness (or the previous replay) leaves the main frame open. |
| void flushOpenFrame() |
| { |
| if (!m_flushedHarnessFrame || m_frameOpen) |
| { |
| TestingWindow::Get()->flushPLSContext(); |
| m_flushedHarnessFrame = true; |
| m_frameOpen = false; |
| } |
| } |
| |
| RenderContext* m_rc; |
| RenderContext::FrameDescriptor m_mainDesc; |
| bool m_flushedHarnessFrame = false; |
| bool m_frameOpen = false; |
| std::unique_ptr<RiveRenderer> m_screen; |
| std::unique_ptr<RiveRenderer> m_canvasRenderer; |
| RenderCanvas* m_activeCanvas = nullptr; |
| }; |
| |
| rcp<RenderPath> ovalPath(rive::cmd::DeferredSession& session, AABB bounds) |
| { |
| RawPath raw; |
| raw.addOval(bounds); |
| return session.makeRenderPath(raw, FillRule::nonZero); |
| } |
| |
| rcp<RenderPaint> solidPaint(rive::cmd::DeferredSession& session, ColorInt color) |
| { |
| auto paint = session.makeRenderPaint(); |
| paint->color(color); |
| return paint; |
| } |
| |
| void drawCanvasImage(Renderer* r, |
| RenderCanvas* canvas, |
| float x, |
| float y, |
| bool flip) |
| { |
| r->save(); |
| r->translate(x, y); |
| if (flip) |
| { |
| r->translate(0, static_cast<float>(canvas->height())); |
| r->scale(1, -1); |
| } |
| r->drawImage(canvas->renderImage(), |
| {.filter = ImageFilter::nearest}, |
| BlendMode::srcOver, |
| 1.0f); |
| r->restore(); |
| } |
| |
| void replayFrameThroughGM(rive::cmd::DeferredSession& session, |
| rive::cmd::DeferredReplayer& replayer, |
| RenderContext* rc, |
| const RenderContext::FrameDescriptor& mainDesc) |
| { |
| rive::cmd::DeferredFrame frame = rive::cmd::snapshotFrame(session); |
| session.resetFrame(); |
| DagGMSink sink(rc, mainDesc); |
| replayer.replayFrame(frame, sink); |
| } |
| } // namespace |
| |
| // Canvas B samples canvas A; reversed records B's bracket first. Both GMs |
| // must produce identical pixels: the schedule, not record order, decides. |
| class CanvasDagChainGM : public GM |
| { |
| public: |
| CanvasDagChainGM(bool reversed) : GM(256, 256), m_reversed(reversed) {} |
| |
| ColorInt clearColor() const override { return 0xff202028; } |
| |
| void onDraw(Renderer*) override |
| { |
| auto rc = TestingWindow::Get()->renderContext(); |
| if (!rc) |
| { |
| return; |
| } |
| auto canvasA = rc->makeRenderCanvas(128, 128); |
| auto canvasB = rc->makeRenderCanvas(128, 128); |
| if (!canvasA || !canvasB) |
| { |
| return; |
| } |
| auto mainDesc = rc->frameDescriptor(); |
| bool flip = rc->platformFeatures().framebufferBottomUp; |
| |
| rive::cmd::DeferredSession session(nullptr); |
| rive::cmd::DeferredReplayer replayer; |
| auto green = solidPaint(session, 0xff30c060); |
| auto orange = solidPaint(session, 0xffe08830); |
| auto circle = ovalPath(session, {24, 24, 104, 104}); |
| auto dot = ovalPath(session, {8, 8, 40, 40}); |
| |
| auto recordA = [&]() { |
| Renderer* a = session.beginCanvasContent(canvasA.get(), 0xff103050); |
| a->drawPath(circle.get(), green.get()); |
| session.endCanvasContent(canvasA.get()); |
| }; |
| auto recordB = [&]() { |
| // B composites A, then draws its own dot on top. |
| Renderer* b = session.beginCanvasContent(canvasB.get(), 0xff501030); |
| drawCanvasImage(b, canvasA.get(), 0, 0, flip); |
| b->drawPath(dot.get(), orange.get()); |
| session.endCanvasContent(canvasB.get()); |
| }; |
| if (m_reversed) |
| { |
| recordB(); |
| recordA(); |
| } |
| else |
| { |
| recordA(); |
| recordB(); |
| } |
| auto screen = session.makeScreenRenderer(); |
| drawCanvasImage(screen.get(), canvasA.get(), 0, 64, flip); |
| drawCanvasImage(screen.get(), canvasB.get(), 128, 64, flip); |
| |
| replayFrameThroughGM(session, replayer, rc, mainDesc); |
| } |
| |
| private: |
| bool m_reversed; |
| }; |
| |
| GMREGISTER(canvas_dag_chain, return new CanvasDagChainGM(false)) |
| GMREGISTER(canvas_dag_chain_reversed, return new CanvasDagChainGM(true)) |
| |
| // A samples B while B samples A. The demoted back edge samples the previous |
| // frame by contract: frame two must show frame one's content crossed over, |
| // deterministic because frame one seeded both canvases. |
| class CanvasDagCycleGM : public GM |
| { |
| public: |
| CanvasDagCycleGM() : GM(256, 256) {} |
| |
| ColorInt clearColor() const override { return 0xff202028; } |
| |
| void onDraw(Renderer*) override |
| { |
| auto rc = TestingWindow::Get()->renderContext(); |
| if (!rc) |
| { |
| return; |
| } |
| auto canvasA = rc->makeRenderCanvas(128, 128); |
| auto canvasB = rc->makeRenderCanvas(128, 128); |
| if (!canvasA || !canvasB) |
| { |
| return; |
| } |
| auto mainDesc = rc->frameDescriptor(); |
| bool flip = rc->platformFeatures().framebufferBottomUp; |
| |
| rive::cmd::DeferredSession session(nullptr); |
| rive::cmd::DeferredReplayer replayer; |
| |
| // Frame one: seed A green, B orange, no cross sampling. |
| { |
| auto green = solidPaint(session, 0xff30c060); |
| auto orange = solidPaint(session, 0xffe08830); |
| auto circle = ovalPath(session, {24, 24, 104, 104}); |
| Renderer* a = session.beginCanvasContent(canvasA.get(), 0xff103050); |
| a->drawPath(circle.get(), green.get()); |
| session.endCanvasContent(canvasA.get()); |
| Renderer* b = session.beginCanvasContent(canvasB.get(), 0xff501030); |
| b->drawPath(circle.get(), orange.get()); |
| session.endCanvasContent(canvasB.get()); |
| replayFrameThroughGM(session, replayer, rc, mainDesc); |
| } |
| |
| // Frame two: each canvas samples the other shrunken, then the screen |
| // shows both. The back edge sees frame one's pixels. |
| { |
| auto white = solidPaint(session, 0xffffffff); |
| auto dot = ovalPath(session, {4, 4, 24, 24}); |
| Renderer* a = session.beginCanvasContent(canvasA.get(), 0xff103050); |
| a->save(); |
| a->scale(0.5f, 0.5f); |
| drawCanvasImage(a, canvasB.get(), 0, 0, flip); |
| a->restore(); |
| a->drawPath(dot.get(), white.get()); |
| session.endCanvasContent(canvasA.get()); |
| |
| Renderer* b = session.beginCanvasContent(canvasB.get(), 0xff501030); |
| b->save(); |
| b->scale(0.5f, 0.5f); |
| drawCanvasImage(b, canvasA.get(), 0, 0, flip); |
| b->restore(); |
| b->drawPath(dot.get(), white.get()); |
| session.endCanvasContent(canvasB.get()); |
| |
| auto screen = session.makeScreenRenderer(); |
| drawCanvasImage(screen.get(), canvasA.get(), 0, 64, flip); |
| drawCanvasImage(screen.get(), canvasB.get(), 128, 64, flip); |
| replayFrameThroughGM(session, replayer, rc, mainDesc); |
| } |
| } |
| }; |
| |
| GMREGISTER(canvas_dag_cycle, return new CanvasDagCycleGM()) |
| |
| #else |
| |
| // Canvas or scripting disabled: nothing to register. |
| |
| #endif |