| /* |
| * 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" |
| |
| #ifdef RIVE_CANVAS |
| |
| #include "common/testing_window_deferred_sink.hpp" |
| #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 |
| { |
| 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) |
| { |
| r->save(); |
| r->translate(x, y); |
| 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(); |
| rive_tests::TestingWindowFrameSink sink(TestingWindow::Get(), 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(); |
| |
| rive::cmd::DeferredSession session(rive::ore::ReplayCaps{}); |
| 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); |
| 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); |
| drawCanvasImage(screen.get(), canvasB.get(), 128, 64); |
| |
| 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(); |
| |
| rive::cmd::DeferredSession session(rive::ore::ReplayCaps{}); |
| 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); |
| 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); |
| b->restore(); |
| b->drawPath(dot.get(), white.get()); |
| session.endCanvasContent(canvasB.get()); |
| |
| auto screen = session.makeScreenRenderer(); |
| drawCanvasImage(screen.get(), canvasA.get(), 0, 64); |
| drawCanvasImage(screen.get(), canvasB.get(), 128, 64); |
| replayFrameThroughGM(session, replayer, rc, mainDesc); |
| } |
| } |
| }; |
| |
| GMREGISTER(canvas_dag_cycle, return new CanvasDagCycleGM()) |
| |
| #else |
| |
| // Canvas disabled: nothing to register. |
| |
| #endif |