| /* |
| * 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); |
| } |