blob: f55de7c89f962009ed44e54b2857a4e7df870f12 [file] [edit]
/*
* 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