blob: 5cc9adc48b0d461206babd40d4c291fdd7ff435a [file]
/*
* Copyright 2026 Rive
*/
// Screen segments are the 2D stream regions outside canvas brackets, each
// naming the render target its draws belong to. Recording only, no GPU.
#include "rive/renderer/cmd/deferred_render_factory.hpp"
#include "rive/renderer/cmd/deferred_session.hpp"
#include <catch.hpp>
using namespace rive;
using rive::cmd::DeferredSegment;
using Target = rive::cmd::DeferredSegment::Target;
namespace
{
// A canvas recorder like the one beginCanvasContent hands a script.
std::unique_ptr<cmd::DeferredRenderer> canvasRecorder(
cmd::DeferredSession& session,
uint64_t canvasId)
{
return std::make_unique<cmd::DeferredRenderer>(&session.commandBuffer(),
&session.canvases(),
&session,
canvasId);
}
} // namespace
TEST_CASE("a screen only frame is one screen segment", "[ore][cmd][segment]")
{
cmd::DeferredSession session(nullptr);
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
uint32_t afterCreates =
static_cast<uint32_t>(session.commandBuffer().commandBytes().size());
session.screenRenderer()->drawPath(path.get(), paint.get());
session.closeOpenRange();
auto all = session.schedulerSegments();
REQUIRE(all.size() == 1);
CHECK(all[0].target == Target::screen);
CHECK(all[0].targetId == 0u);
CHECK(all[0].begin == afterCreates);
CHECK(all[0].end == session.commandBuffer().commandBytes().size());
}
TEST_CASE("bytes recorded before any target draws claim no segment",
"[ore][cmd][segment]")
{
cmd::DeferredSession session(nullptr);
// Creates and drained destroys replay from the whole stream, so they need
// no segment; giving them one would open a target's frame in a frame
// where only other targets drew.
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
CHECK(session.commandBuffer().commandBytes().size() > 0);
session.closeOpenRange();
CHECK(session.schedulerSegments().empty());
}
TEST_CASE("a canvas bracket carves leading and trailing screen segments",
"[ore][cmd][segment]")
{
cmd::DeferredSession session(nullptr);
auto canvas = canvasRecorder(session, 1);
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
uint32_t afterCreates =
static_cast<uint32_t>(session.commandBuffer().commandBytes().size());
session.screenRenderer()->drawPath(path.get(), paint.get());
canvas->drawPath(path.get(), paint.get());
session.screenRenderer()->drawPath(path.get(), paint.get());
session.closeOpenRange();
auto all = session.schedulerSegments();
REQUIRE(all.size() == 3);
CHECK(all[0].target == Target::screen);
CHECK(all[0].begin == afterCreates);
CHECK(all[1].target == Target::canvas);
CHECK(all[1].targetId == 1u);
CHECK(all[1].begin == all[0].end);
CHECK(all[2].target == Target::screen);
CHECK(all[2].begin == all[1].end);
CHECK(all[2].end == session.commandBuffer().commandBytes().size());
}
TEST_CASE("a canvas bracket at offset 0 has no leading screen segment",
"[ore][cmd][segment]")
{
cmd::DeferredSession session(nullptr);
auto canvas = canvasRecorder(session, 1);
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
// Creates land in the stream before the first draw, so open the canvas
// range from the very start by drawing into it first.
session.closeOpenRange();
uint32_t start =
static_cast<uint32_t>(session.commandBuffer().commandBytes().size());
canvas->drawPath(path.get(), paint.get());
session.screenRenderer()->drawPath(path.get(), paint.get());
session.closeOpenRange();
std::vector<DeferredSegment> after;
for (const auto& s : session.schedulerSegments())
{
if (s.begin >= start)
{
after.push_back(s);
}
}
REQUIRE(after.size() == 2);
CHECK(after[0].target == Target::canvas);
CHECK(after[0].begin == start);
CHECK(after[1].target == Target::screen);
CHECK(after[1].begin == after[0].end);
}
TEST_CASE("each screen target gets its own segments", "[ore][cmd][segment]")
{
cmd::DeferredSession session(nullptr);
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
// Two widgets painting in one frame, interleaved as Flutter would.
session.screenRenderer(0)->drawPath(path.get(), paint.get());
session.screenRenderer(7)->drawPath(path.get(), paint.get());
session.screenRenderer(0)->drawPath(path.get(), paint.get());
session.closeOpenRange();
auto all = session.schedulerSegments();
REQUIRE(all.size() == 3);
CHECK(all[0].targetId == 0u);
CHECK(all[1].targetId == 7u);
CHECK(all[2].targetId == 0u);
for (const auto& s : all)
{
CHECK(s.target == Target::screen);
}
for (size_t i = 1; i < all.size(); i++)
{
CHECK(all[i].begin == all[i - 1].end);
}
}
TEST_CASE("a canvas hands the stream back to the screen it interrupted",
"[ore][cmd][segment]")
{
cmd::DeferredSession session(nullptr);
auto canvas = canvasRecorder(session, 1);
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
session.screenRenderer(7)->drawPath(path.get(), paint.get());
canvas->drawPath(path.get(), paint.get());
session.closeOpenRange(); // closes the canvas range, as a snapshot would
// Creates recorded after that belong to target 7, which was drawing, not
// to the default screen, which drew nothing this frame.
auto later = session.makeEmptyRenderPath();
session.closeOpenRange();
auto all = session.schedulerSegments();
REQUIRE(all.size() == 3);
CHECK(all[0].target == Target::screen);
CHECK(all[0].targetId == 7u);
CHECK(all[1].target == Target::canvas);
CHECK(all[2].target == Target::screen);
CHECK(all[2].targetId == 7u);
}
TEST_CASE("a session frame closes when the last target finishes",
"[ore][cmd][segment]")
{
cmd::DeferredSession session(nullptr);
uint64_t a = session.acquireScreenTarget();
uint64_t b = session.acquireScreenTarget();
CHECK(a == 0u);
CHECK(b == 1u);
// Sequential painting: each target opens and closes its own window.
session.beginTargetFrame(a);
CHECK(session.endTargetFrame(a));
session.beginTargetFrame(b);
CHECK(session.endTargetFrame(b));
// Nested painting: the inner finish must not end the session's frame,
// resetting the stream under a target still recording.
session.beginTargetFrame(a);
session.beginTargetFrame(b);
CHECK(!session.endTargetFrame(b));
CHECK(session.endTargetFrame(a));
// A released target's id and recorder are reclaimed.
session.releaseScreenTarget(a);
CHECK(session.acquireScreenTarget() == a);
}
TEST_CASE("the screen recorder for a target survives resetFrame",
"[ore][cmd][segment]")
{
cmd::DeferredSession session(nullptr);
// FFI hosts take this raw and keep drawing through it across frames.
Renderer* first = session.screenRenderer(3);
session.resetFrame();
CHECK(session.screenRenderer(3) == first);
}