blob: d7046641b20fe63a20c982e6514145e148365c19 [file]
/*
* 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()));
}