blob: be86366abd3541c4687a25159db727bb125a5ea1 [file]
#include <rive/animation/state_machine_instance.hpp>
#include <rive/artboard.hpp>
#include <rive/file.hpp>
#include <rive/node.hpp>
#include <rive/watermark.hpp>
#include <utils/no_op_renderer.hpp>
#include "catch.hpp"
#include "rive_file_reader.hpp"
#include <algorithm>
#include <chrono>
#include <memory>
#include <thread>
#include <vector>
using namespace rive;
namespace
{
// The watermark refuses to advance faster than the wall clock unless the host
// owns the timeline. Tests that want to drive time themselves — every one below
// that counts frames — say so by taking this.
struct DeterministicTime
{
DeterministicTime() { File::deterministicMode = true; }
~DeterministicTime() { File::deterministicMode = false; }
};
// Records which RenderPaths reached the renderer. Every ArtboardInstance builds
// its own paths, so path identity is what tells the watermark's artboard apart
// from the host's.
class PathRecordingRenderer : public NoOpRenderer
{
public:
std::vector<RenderPath*> paths;
void drawPath(RenderPath* path, RenderPaint*) override
{
paths.push_back(path);
}
bool sawAnyOf(const std::vector<RenderPath*>& others) const
{
for (auto path : paths)
{
if (std::find(others.begin(), others.end(), path) != others.end())
{
return true;
}
}
return false;
}
};
std::vector<RenderPath*> recordDraw(Artboard* artboard)
{
PathRecordingRenderer renderer;
artboard->draw(&renderer);
return renderer.paths;
}
// drawInternal is never diverted, so this is how to see an artboard's own
// content even while a watermark is attached to it.
std::vector<RenderPath*> recordDrawInternal(Artboard* artboard)
{
PathRecordingRenderer renderer;
artboard->drawInternal(&renderer);
return renderer.paths;
}
// The fixture's state machine tweens a node's x, so the node positions are a
// direct read on whether the artboard advanced.
std::vector<float> nodePositions(Artboard* artboard)
{
std::vector<float> positions;
for (auto node : artboard->find<Node>())
{
positions.push_back(node->x());
positions.push_back(node->y());
}
return positions;
}
// Builds a watermark out of a second instance of the same artboard. The two
// instances render distinct paths, which is all the tests below need to tell
// which one drew.
std::unique_ptr<Watermark> makeWatermark(const File* file,
std::vector<RenderPath*>* paths)
{
auto artboard = file->artboardDefault();
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
if (paths != nullptr)
{
*paths = recordDraw(artboard.get());
}
return std::make_unique<Watermark>(std::move(artboard),
std::move(stateMachine));
}
} // namespace
TEST_CASE("a watermark holds back the artboard it is attached to",
"[watermark]")
{
DeterministicTime deterministic;
auto file = ReadRiveFile("assets/data_bind_keyframes_test.riv");
auto artboard = file->artboardDefault();
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
// Captured before the watermark is attached, while the host still draws
// itself.
auto hostPaths = recordDraw(artboard.get());
REQUIRE(!hostPaths.empty());
auto frozenPositions = nodePositions(artboard.get());
REQUIRE(!frozenPositions.empty());
std::vector<RenderPath*> watermarkPaths;
artboard->watermark(makeWatermark(file.get(), &watermarkPaths));
REQUIRE(artboard->watermark() != nullptr);
REQUIRE(!watermarkPaths.empty());
for (int i = 0; i < 10; i++)
{
// Never reports "settled" while the pre-roll runs: a false here would
// stop the host's ticker mid watermark.
REQUIRE(stateMachine->advanceAndApply(0.016f));
REQUIRE(artboard->watermark() != nullptr);
CHECK(artboard->watermark()->isPlaying());
PathRecordingRenderer renderer;
artboard->draw(&renderer);
CHECK(!renderer.paths.empty());
CHECK(!renderer.sawAnyOf(hostPaths));
CHECK(nodePositions(artboard.get()) == frozenPositions);
}
}
TEST_CASE("the artboard takes over once the watermark is done", "[watermark]")
{
DeterministicTime deterministic;
auto file = ReadRiveFile("assets/data_bind_keyframes_test.riv");
auto artboard = file->artboardDefault();
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
auto hostPaths = recordDraw(artboard.get());
auto frozenPositions = nodePositions(artboard.get());
artboard->watermark(makeWatermark(file.get(), nullptr));
// The same artboard played with no watermark at all, to compare against.
auto control = file->artboardDefault();
auto controlStateMachine = control->stateMachineAt(0);
REQUIRE(controlStateMachine != nullptr);
controlStateMachine->advanceAndApply(0.0f);
// Long enough to outrun the pre-roll's hard time cap even if the
// watermark's state machine never settles on its own.
for (int i = 0; i < 800 && artboard->watermark() != nullptr; i++)
{
stateMachine->advanceAndApply(0.016f);
}
// Released the moment it finishes, so the pre-roll can't play twice.
REQUIRE(artboard->watermark() == nullptr);
// Once the pre-roll is out of the way the artboard plays exactly what it
// would have played without one: same frames, just delayed. The frame that
// released the watermark is already the artboard's first real one.
int hostFrames = 1;
for (int i = 0; i < 60; i++)
{
stateMachine->advanceAndApply(0.016f);
hostFrames++;
}
for (int i = 0; i < hostFrames; i++)
{
controlStateMachine->advanceAndApply(0.016f);
}
auto controlPositions = nodePositions(control.get());
REQUIRE(controlPositions != frozenPositions); // the fixture does animate
CHECK(nodePositions(artboard.get()) == controlPositions);
PathRecordingRenderer renderer;
artboard->draw(&renderer);
CHECK(renderer.sawAnyOf(hostPaths));
}
TEST_CASE("a watermark that is never advanced does not hide the artboard",
"[watermark]")
{
auto file = ReadRiveFile("assets/data_bind_keyframes_test.riv");
auto artboard = file->artboardDefault();
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
auto hostPaths = recordDraw(artboard.get());
artboard->watermark(makeWatermark(file.get(), nullptr));
// Playback that never runs through a state machine never starts the
// pre-roll, and must keep drawing the artboard rather than freeze on a
// watermark that would never end.
PathRecordingRenderer renderer;
artboard->draw(&renderer);
CHECK(renderer.sawAnyOf(hostPaths));
}
TEST_CASE("drawInternal never consults the watermark", "[watermark]")
{
DeterministicTime deterministic;
auto file = ReadRiveFile("assets/data_bind_keyframes_test.riv");
auto artboard = file->artboardDefault();
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
auto hostPaths = recordDraw(artboard.get());
REQUIRE(!hostPaths.empty());
std::vector<RenderPath*> watermarkPaths;
artboard->watermark(makeWatermark(file.get(), &watermarkPaths));
REQUIRE(stateMachine->advanceAndApply(0.016f));
REQUIRE(artboard->watermark()->isPlaying());
// Nested artboards and component lists render through drawInternal, which
// is why the gate lives in draw(): drawing this way must still render the
// artboard's own content even mid pre-roll.
PathRecordingRenderer renderer;
artboard->drawInternal(&renderer);
CHECK(renderer.sawAnyOf(hostPaths));
CHECK(!renderer.sawAnyOf(watermarkPaths));
}
TEST_CASE("a watermarked file pre-rolls the artboard it hands out",
"[watermark]")
{
DeterministicTime deterministic;
// created by watermark_export_test.dart in rive_core.
auto file = ReadRiveFile("assets/watermark_playback_test.riv");
auto* manifest = file->manifest();
REQUIRE(manifest != nullptr);
REQUIRE(manifest->hasWatermark());
// The exporter inlines the watermark after the host's artboards, and drags
// in whatever that watermark itself nests, so the total count follows the
// bundled artwork rather than being fixed. What this test needs is that the
// host kept index 0 and the watermark landed right after it.
REQUIRE(file->artboardCount() > 1);
REQUIRE(manifest->watermarkArtboardIndex() == 1);
auto artboard = file->artboardDefault();
REQUIRE(artboard != nullptr);
REQUIRE(artboard->watermark() != nullptr);
// The watermark artboard itself is still vended plain, so tooling can look
// at it without triggering a pre-roll of itself.
CHECK(file->artboardAt(1)->watermark() == nullptr);
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
auto hostPaths = recordDrawInternal(artboard.get());
REQUIRE(!hostPaths.empty());
REQUIRE(stateMachine->advanceAndApply(0.016f));
REQUIRE(artboard->watermark()->isPlaying());
PathRecordingRenderer duringPreRoll;
artboard->draw(&duringPreRoll);
CHECK(!duringPreRoll.paths.empty());
CHECK(!duringPreRoll.sawAnyOf(hostPaths));
// The bundled watermark rests in a one-shot animation, so its state machine
// settles well inside the pre-roll's hard time cap.
for (int i = 0; i < 800 && artboard->watermark() != nullptr; i++)
{
stateMachine->advanceAndApply(0.016f);
}
REQUIRE(artboard->watermark() == nullptr);
PathRecordingRenderer afterPreRoll;
artboard->draw(&afterPreRoll);
CHECK(afterPreRoll.sawAnyOf(hostPaths));
}
TEST_CASE("the watermark cannot be advanced faster than real time",
"[watermark]")
{
// Deliberately not DeterministicTime: this is the real-clock behaviour.
REQUIRE(!File::deterministicMode);
auto file = ReadRiveFile("assets/data_bind_keyframes_test.riv");
auto artboard = file->artboardDefault();
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
artboard->watermark(makeWatermark(file.get(), nullptr));
// One enormous delta is the obvious way to seek past a pre-roll. It buys
// only the wall time that has actually passed, which here is ~nothing.
REQUIRE(stateMachine->advanceAndApply(1000.0f));
CHECK(artboard->watermark() != nullptr);
// Neither does spinning: this loop is worth minutes of animation at face
// value but runs in well under a frame of real time.
for (int i = 0; i < 500; i++)
{
stateMachine->advanceAndApply(0.5f);
}
CHECK(artboard->watermark() != nullptr);
CHECK(artboard->watermark()->isPlaying());
// Nor does spinning *underneath* the clock tolerance, which is the subtle
// way through: while each delta was compared against the wall clock on its
// own, a delta at or just below the tolerance was never clamped, and the
// tolerance could be re-spent every call. This loop is worth far more than
// the pre-roll's hard cap at face value and still costs no real time.
for (int i = 0; i < 8000; i++)
{
stateMachine->advanceAndApply(0.004f);
}
CHECK(artboard->watermark() != nullptr);
CHECK(artboard->watermark()->isPlaying());
// And the artboard is still being held back, not quietly playing behind it.
PathRecordingRenderer renderer;
artboard->draw(&renderer);
CHECK(!renderer.paths.empty());
CHECK(!renderer.sawAnyOf(recordDrawInternal(artboard.get())));
}
TEST_CASE("a file without a watermark section vends plain artboards",
"[watermark]")
{
// This file carries a manifest (it has a string table) but no watermark
// section, which is the shape of every file exported today.
auto file = ReadRiveFile("assets/data_bind_keyframes_test.riv");
auto* manifest = file->manifest();
REQUIRE((manifest == nullptr || !manifest->hasWatermark()));
auto artboard = file->artboardDefault();
REQUIRE(artboard != nullptr);
CHECK(artboard->watermark() == nullptr);
for (size_t i = 0; i < file->artboardCount(); i++)
{
CHECK(file->artboardAt(i)->watermark() == nullptr);
}
}
// Idle time used to be bankable. The whole wall-clock gap went into the
// budget, so an instance left alone -- a backgrounded tab, a file loaded and
// not yet advanced -- funded a burst of advances that could drain the pre-roll
// a frame at a time without ever showing it. The credit for one call is now
// capped at the same ceiling a single advance may spend.
TEST_CASE("idle time cannot be banked to drain the watermark", "[watermark]")
{
// Deliberately not DeterministicTime: this is the real-clock behaviour.
REQUIRE(!File::deterministicMode);
auto file = ReadRiveFile("assets/data_bind_keyframes_test.riv");
auto artboard = file->artboardDefault();
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
artboard->watermark(makeWatermark(file.get(), nullptr));
// Sit idle for well over one frame's worth of real time...
std::this_thread::sleep_for(std::chrono::milliseconds(400));
// ...then spend it as fast as possible. Every one of these is capped to a
// frame, so banking the full gap would have let the burst consume all of
// it rather than the one frame that has been earned.
for (int i = 0; i < 20; i++)
{
stateMachine->advanceAndApply(1000.0f);
}
REQUIRE(artboard->watermark() != nullptr);
CHECK(artboard->watermark()->isPlaying());
// One frame's credit plus the tolerance, not the whole idle gap. The bound
// is deliberately loose: what matters is that it is nowhere near 0.4.
CHECK(artboard->watermark()->elapsedSeconds() < 0.3f);
}