| /* |
| * Copyright 2026 Rive |
| */ |
| |
| // Replays a SerializingFactory stream into a second SerializingFactory and |
| // asserts the re-recorded stream is byte identical, proving every call is |
| // reproduced in order. GPU free, pixels are covered by the GMs. |
| |
| #include "utils/serializing_factory.hpp" |
| #include "utils/serialized_replay.hpp" |
| #include "rive/math/raw_path.hpp" |
| #include "rive/math/mat2d.hpp" |
| |
| #include <catch.hpp> |
| #include <cstring> |
| |
| using namespace rive; |
| |
| TEST_CASE("serialized 2D commands replay byte-identically", |
| "[serialize][replay]") |
| { |
| SerializingFactory a; |
| a.frameSize(256, 256); |
| a.addFrame(); |
| auto rendererA = a.makeRenderer(); |
| |
| // Exercises every paint mutation op. |
| auto paint = a.makeRenderPaint(); |
| paint->color(0xFF112233); |
| paint->style(RenderPaintStyle::stroke); |
| paint->thickness(3.5f); |
| paint->join(StrokeJoin::round); |
| paint->cap(StrokeCap::square); |
| paint->blendMode(BlendMode::multiply); |
| paint->feather(2.0f); |
| |
| RawPath rp; |
| rp.move({0, 0}); |
| rp.line({10, 0}); |
| rp.cubic({10, 5}, {5, 10}, {0, 10}); |
| rp.close(); |
| auto path = a.makeRenderPath(rp, FillRule::evenOdd); |
| |
| auto clip = a.makeEmptyRenderPath(); |
| RawPath cp; |
| cp.move({0, 0}); |
| cp.line({20, 0}); |
| cp.line({20, 20}); |
| cp.close(); |
| clip->addRawPath(cp); |
| |
| ColorInt cols[2] = {0xFFFF0000, 0xFF0000FF}; |
| float stops[2] = {0.0f, 1.0f}; |
| auto grad = a.makeLinearGradient(0, 0, 100, 100, cols, stops, 2); |
| auto paint2 = a.makeRenderPaint(); |
| paint2->shader(grad); |
| |
| rendererA->save(); |
| rendererA->transform(Mat2D(1, 0, 0, 1, 5, 7)); |
| rendererA->clipPath(clip.get()); |
| rendererA->modulateOpacity(0.5f); |
| rendererA->drawPath(path.get(), paint.get()); |
| rendererA->drawPath(path.get(), paint2.get()); |
| rendererA->restore(); |
| |
| SerializingFactory b; |
| auto rendererB = b.makeRenderer(); |
| SerializedReplayHooks hooks; |
| hooks.onFrame = [&]() { b.addFrame(); }; |
| hooks.onFrameSize = [&](uint32_t w, uint32_t h) { b.frameSize(w, h); }; |
| REQUIRE(replaySerializedCommands(a.bytes(), &b, rendererB.get(), hooks)); |
| |
| auto sa = a.bytes(); |
| auto sb = b.bytes(); |
| REQUIRE(sa.size() == sb.size()); |
| CHECK(std::memcmp(sa.data(), sb.data(), sa.size()) == 0); |
| } |
| |
| TEST_CASE("serialized replay rejects a bad header", "[serialize][replay]") |
| { |
| const uint8_t garbage[8] = {'X', 'X', 'X', 'X', 1, 0, 0, 0}; |
| SerializingFactory b; |
| auto r = b.makeRenderer(); |
| CHECK_FALSE( |
| replaySerializedCommands(Span<const uint8_t>(garbage, sizeof(garbage)), |
| &b, |
| r.get())); |
| } |