blob: 527e708cc58fe1607de24b735d865ccfe81b74c7 [file]
/*
* Copyright 2025 Rive
*/
#include "gm.hpp"
#include "gmutils.hpp"
#include "common/testing_window.hpp"
#include "rive/renderer/render_canvas.hpp"
#include "rive/renderer/render_context.hpp"
#include "rive/renderer/rive_renderer.hpp"
using namespace rivegm;
using namespace rive;
using namespace rive::gpu;
// Renders Rive 2D content into a RenderCanvas and composites the result into
// the main framebuffer. Verifies the basic render-to-texture lifecycle:
// makeRenderCanvas() -> renderTarget() -> beginFrame/flush -> renderImage() ->
// drawImage.
class RenderCanvasBasic : public GM
{
public:
RenderCanvasBasic() : GM(256, 256) {}
ColorInt clearColor() const override { return 0xffff0000; } // red
void onDraw(rive::Renderer* originalRenderer) override
{
auto renderContext = TestingWindow::Get()->renderContext();
if (!renderContext)
return;
auto canvas = renderContext->makeRenderCanvas(256, 256);
if (!canvas)
return;
// Intercept the current frame and flush.
auto originalFrameDescriptor = renderContext->frameDescriptor();
TestingWindow::Get()->flushPLSContext();
// Begin a new frame targeting the canvas. Declare the frame
// dimensions explicitly so Rive sizes its PLS buffers for the
// canvas render target we are about to flush into. Without this,
// backends whose main frame descriptor is larger than the canvas
// (e.g. wagyu where the surface exceeds the GM size) trip the
// flushResources.renderTarget->width() ==
// m_frameDescriptor.renderTargetWidth assertion in
// RenderContext::flush.
auto canvasFrameDescriptor = originalFrameDescriptor;
canvasFrameDescriptor.renderTargetWidth = canvas->width();
canvasFrameDescriptor.renderTargetHeight = canvas->height();
canvasFrameDescriptor.clearColor = 0xff0000ff; // blue
renderContext->beginFrame(std::move(canvasFrameDescriptor));
// Draw a green circle into the canvas.
RiveRenderer renderer(renderContext);
Paint green(0xff00ff00);
PathBuilder builder;
int nsegs = 40;
float r = 80;
float cx = 128, cy = 128;
for (int i = 0; i <= nsegs; ++i)
{
float theta = 2 * math::PI * i / nsegs;
float x = cx + r * cosf(theta);
float y = cy + r * sinf(theta);
if (i == 0)
builder.moveTo(x, y);
else
builder.lineTo(x, y);
}
renderer.drawPath(builder.detach(), green);
// Flush to the canvas's render target.
TestingWindow::Get()->flushPLSContext(canvas->renderTarget());
// Resume the main frame and composite the canvas.
auto mainFrameDescriptor = originalFrameDescriptor;
mainFrameDescriptor.loadAction = gpu::LoadAction::preserveRenderTarget;
renderContext->beginFrame(std::move(mainFrameDescriptor));
renderer.drawImage(canvas->renderImage(),
{.filter = ImageFilter::nearest},
BlendMode::srcOver,
1);
}
};
GMREGISTER(render_canvas_basic, return new RenderCanvasBasic())
// Composites a RenderCanvas through drawImageMesh, whose UVs come straight
// from the mesh buffers instead of an image matrix. The circle sits high so a
// vertically mirrored composite is visible.
class RenderCanvasMesh : public GM
{
public:
RenderCanvasMesh() : GM(256, 256) {}
ColorInt clearColor() const override { return 0xffff0000; } // red
void onDraw(rive::Renderer* originalRenderer) override
{
auto renderContext = TestingWindow::Get()->renderContext();
if (!renderContext)
return;
auto canvas = renderContext->makeRenderCanvas(256, 256);
if (!canvas)
return;
auto originalFrameDescriptor = renderContext->frameDescriptor();
TestingWindow::Get()->flushPLSContext();
auto canvasFrameDescriptor = originalFrameDescriptor;
canvasFrameDescriptor.renderTargetWidth = canvas->width();
canvasFrameDescriptor.renderTargetHeight = canvas->height();
canvasFrameDescriptor.clearColor = 0xff0000ff; // blue
renderContext->beginFrame(std::move(canvasFrameDescriptor));
RiveRenderer renderer(renderContext);
Paint green(0xff00ff00);
PathBuilder builder;
int nsegs = 40;
float r = 60;
float cx = 128, cy = 80;
for (int i = 0; i <= nsegs; ++i)
{
float theta = 2 * math::PI * i / nsegs;
float x = cx + r * cosf(theta);
float y = cy + r * sinf(theta);
if (i == 0)
builder.moveTo(x, y);
else
builder.lineTo(x, y);
}
renderer.drawPath(builder.detach(), green);
TestingWindow::Get()->flushPLSContext(canvas->renderTarget());
auto mainFrameDescriptor = originalFrameDescriptor;
mainFrameDescriptor.loadAction = gpu::LoadAction::preserveRenderTarget;
renderContext->beginFrame(std::move(mainFrameDescriptor));
Factory* factory = TestingWindow::Get()->factory();
auto pts = factory->makeRenderBuffer(
RenderBufferType::vertex,
RenderBufferFlags::mappedOnceAtInitialization,
4 * sizeof(Vec2D));
memcpy(pts->map(),
std::array<Vec2D, 4>{Vec2D{0, 0},
Vec2D{256, 0},
Vec2D{0, 256},
Vec2D{256, 256}}
.data(),
pts->sizeInBytes());
pts->unmap();
auto uvs = factory->makeRenderBuffer(
RenderBufferType::vertex,
RenderBufferFlags::mappedOnceAtInitialization,
4 * sizeof(Vec2D));
memcpy(uvs->map(),
std::array<Vec2D, 4>{Vec2D{0, 0},
Vec2D{1, 0},
Vec2D{0, 1},
Vec2D{1, 1}}
.data(),
uvs->sizeInBytes());
uvs->unmap();
auto indices = factory->makeRenderBuffer(
RenderBufferType::index,
RenderBufferFlags::mappedOnceAtInitialization,
6 * sizeof(uint16_t));
memcpy(indices->map(),
std::array<uint16_t, 6>{0, 1, 2, 1, 2, 3}.data(),
indices->sizeInBytes());
indices->unmap();
renderer.drawImageMesh(canvas->renderImage(),
{.filter = ImageFilter::nearest},
pts,
uvs,
indices,
4,
6,
BlendMode::srcOver,
1);
}
};
GMREGISTER(render_canvas_mesh, return new RenderCanvasMesh())
// Winding sensitive content inside a canvas: a clockwise fill rule keeps the
// clockwise square and drops the counter clockwise one, under a circular clip.
// A target whose rows run the other way from the window inverts winding, so
// the squares swap if the front face is not set per target.
class RenderCanvasWinding : public GM
{
public:
RenderCanvasWinding() : GM(256, 256) {}
ColorInt clearColor() const override { return 0xffff0000; } // red
void onDraw(rive::Renderer* originalRenderer) override
{
auto renderContext = TestingWindow::Get()->renderContext();
if (!renderContext)
return;
auto canvas = renderContext->makeRenderCanvas(256, 256);
if (!canvas)
return;
auto originalFrameDescriptor = renderContext->frameDescriptor();
TestingWindow::Get()->flushPLSContext();
auto canvasFrameDescriptor = originalFrameDescriptor;
canvasFrameDescriptor.renderTargetWidth = canvas->width();
canvasFrameDescriptor.renderTargetHeight = canvas->height();
canvasFrameDescriptor.clearColor = 0xff0000ff; // blue
renderContext->beginFrame(std::move(canvasFrameDescriptor));
RiveRenderer renderer(renderContext);
// The clip stack outlives the flush, so keep it out of the composite.
renderer.save();
Path clip = PathBuilder::Circle(128, 128, 110);
renderer.clipPath(clip.get());
Paint green(0xff00ff00);
renderer.drawPath(PathBuilder(FillRule::clockwise)
.moveTo(24, 24)
.lineTo(120, 24)
.lineTo(120, 120)
.lineTo(24, 120)
.close()
.detach(),
green);
Paint yellow(0xffffff00);
renderer.drawPath(PathBuilder(FillRule::clockwise)
.moveTo(136, 136)
.lineTo(136, 232)
.lineTo(232, 232)
.lineTo(232, 136)
.close()
.detach(),
yellow);
renderer.restore();
TestingWindow::Get()->flushPLSContext(canvas->renderTarget());
auto mainFrameDescriptor = originalFrameDescriptor;
mainFrameDescriptor.loadAction = gpu::LoadAction::preserveRenderTarget;
renderContext->beginFrame(std::move(mainFrameDescriptor));
renderer.drawImage(canvas->renderImage(),
{.filter = ImageFilter::nearest},
BlendMode::srcOver,
1);
}
};
GMREGISTER(render_canvas_winding, return new RenderCanvasWinding())
// Verifies that RenderCanvas content persists across frames. Renders a green
// circle into the canvas on the first frame, then on the second frame only
// composites the canvas (without re-rendering into it).
class RenderCanvasPersistence : public GM
{
public:
RenderCanvasPersistence() : GM(256, 256) {}
ColorInt clearColor() const override { return 0xffff0000; } // red
void onDraw(rive::Renderer* originalRenderer) override
{
auto renderContext = TestingWindow::Get()->renderContext();
if (!renderContext)
return;
if (!m_canvas)
{
m_canvas = renderContext->makeRenderCanvas(256, 256);
if (!m_canvas)
return;
// Render green circle into the canvas on first call.
auto originalFrameDescriptor = renderContext->frameDescriptor();
TestingWindow::Get()->flushPLSContext();
auto canvasFD = originalFrameDescriptor;
canvasFD.renderTargetWidth = m_canvas->width();
canvasFD.renderTargetHeight = m_canvas->height();
canvasFD.clearColor = 0xff0000ff; // blue
renderContext->beginFrame(std::move(canvasFD));
RiveRenderer renderer(renderContext);
Paint green(0xff00ff00);
PathBuilder builder;
int nsegs = 40;
float r = 80;
float cx = 128, cy = 128;
for (int i = 0; i <= nsegs; ++i)
{
float theta = 2 * math::PI * i / nsegs;
float x = cx + r * cosf(theta);
float y = cy + r * sinf(theta);
if (i == 0)
builder.moveTo(x, y);
else
builder.lineTo(x, y);
}
renderer.drawPath(builder.detach(), green);
TestingWindow::Get()->flushPLSContext(m_canvas->renderTarget());
// Resume main frame.
auto mainFD = originalFrameDescriptor;
mainFD.loadAction = gpu::LoadAction::preserveRenderTarget;
renderContext->beginFrame(std::move(mainFD));
}
// Composite the canvas (rendered on first frame, persisted on later
// frames).
RiveRenderer renderer(renderContext);
renderer.drawImage(m_canvas->renderImage(),
{.filter = ImageFilter::nearest},
BlendMode::srcOver,
1);
}
private:
rcp<RenderCanvas> m_canvas;
};
GMREGISTER(render_canvas_persistence, return new RenderCanvasPersistence())