blob: 1af0ecae2f9621f3f1241f26ef585b5d0e2e0601 [file]
/*
* Copyright 2022 Rive
*/
// Don't compile this file as part of the "tests" project.
#ifndef TESTING
#include "goldens_shared.hpp"
#include "common/tcp_client.hpp"
#include "common/rive_mgr.hpp"
#include "common/write_png_file.hpp"
#include <filesystem>
#include <fstream>
#include <iostream>
#ifdef RIVE_ANDROID
#include "common/rive_android_app.hpp"
#endif
#ifdef __EMSCRIPTEN__
#include "common/rive_wasm_app.hpp"
#include <emscripten/emscripten.h>
#endif
constexpr static int kWindowTargetSize = 1600;
GoldensArguments s_args;
// RIVE_GOLDENS_ADVANCE=N advances N frames at sixty fps before rendering.
static void advanceScene(rive::Scene* scene)
{
const char* a = goldens_getenv("RIVE_GOLDENS_ADVANCE");
int frames = a ? atoi(a) : 0;
if (frames <= 0)
{
scene->advanceAndApply(0);
return;
}
for (int i = 0; i < frames; ++i)
scene->advanceAndApply(1.0f / 60.0f);
}
void dumpPixelsAsPng(const char* rivName,
int windowWidth,
int windowHeight,
std::vector<uint8_t> pixels)
{
assert(pixels.size() ==
static_cast<size_t>(windowHeight) * windowWidth * 4);
std::ostringstream imageName;
imageName
<< std::filesystem::path(rivName).filename().stem().generic_string();
if (s_args.rows() != 1 || s_args.cols() != 1)
{
imageName << '.' << s_args.cols() << 'x' << s_args.rows() << '.';
}
TestHarness::Instance().savePNG({
.name = imageName.str(),
.width = static_cast<uint32_t>(windowWidth),
.height = static_cast<uint32_t>(windowHeight),
.pixels = std::move(pixels),
});
if (s_args.verbose())
{
printf("[goldens] Sent %s\n",
std::filesystem::path(imageName.str())
.replace_extension("png")
.generic_string()
.c_str());
}
}
static bool render_and_dump_png(
int cellSize,
const char* rivName,
rive::Scene* scene,
rive::Artboard* artboard = nullptr,
rive::cmd::DeferredSession* deferredSession = nullptr)
{
// onceAfterGM can tear down the window between rivs, size every run.
TestingWindow::Get()->resize(cellSize * s_args.cols(),
cellSize * s_args.rows());
if (s_args.verbose())
{
printf("[goldens] Running %s...\n", rivName);
}
try
{
const int frames = s_args.cols() * s_args.rows();
const double duration = scene->durationSeconds();
const double frameDuration = duration / frames;
const rive::AABB cellBounds = rive::AABB(0, 0, cellSize, cellSize);
#if defined(WITH_RIVE_SCRIPTING) && defined(RIVE_CANVAS)
// Deferred mode records the screen and Ore through the session, then
// replays synchronously per grid cell. The cadence mirrors the
// immediate path below so the output must be byte identical.
if (deferredSession != nullptr && artboard != nullptr)
{
advanceScene(scene);
rive::cmd::DeferredReplayer replayer;
for (int y = 0; y < s_args.rows(); ++y)
{
for (int x = 0; x < s_args.cols(); ++x)
{
bool first = (x | y) == 0;
if (!first)
{
scene->advanceAndApply(frameDuration);
}
deferredSession->recordOreReplayMarker();
auto screenRec = deferredSession->makeScreenRenderer();
screenRec->save();
screenRec->translate(x * cellSize, y * cellSize);
screenRec->align(rive::Fit::cover,
rive::Alignment::center,
cellBounds,
scene->bounds());
artboard->drawInternal(screenRec.get());
screenRec->restore();
// Snapshot replay is the same path a threaded consumer
// takes.
rive::cmd::DeferredFrame frame =
rive::cmd::snapshotFrame(*deferredSession);
deferredSession->resetFrame();
GoldensFrameSink sink(first);
replayer.replayFrame(frame, sink);
bool last =
y == s_args.rows() - 1 && x == s_args.cols() - 1;
if (!last)
{
TestingWindow::Get()->endFrame();
}
}
}
int windowWidth = s_args.cols() * cellSize;
int windowHeight = s_args.rows() * cellSize;
std::vector<uint8_t> pixels;
TestingWindow::Get()->endFrame(&pixels);
dumpPixelsAsPng(rivName,
windowWidth,
windowHeight,
std::move(pixels));
return true;
}
#endif
// Render the scene in a grid.
advanceScene(scene);
auto renderer =
TestingWindow::Get()->beginFrame({.clearColor = 0xffffffff});
renderer->save();
for (int y = 0; y < s_args.rows(); ++y)
{
for (int x = 0; x < s_args.cols(); ++x)
{
if ((x | y) != 0)
{
TestingWindow::Get()->endFrame();
scene->advanceAndApply(frameDuration);
TestingWindow::Get()->beginFrame({.doClear = false});
}
renderer->save();
renderer->translate(x * cellSize, y * cellSize);
renderer->align(rive::Fit::cover,
rive::Alignment::center,
cellBounds,
scene->bounds());
if (artboard != nullptr)
{
artboard->drawInternal(renderer.get());
}
else
{
scene->draw(renderer.get());
}
renderer->restore();
}
}
renderer->restore();
// Save the png.
int windowWidth = s_args.cols() * cellSize;
int windowHeight = s_args.rows() * cellSize;
std::vector<uint8_t> pixels;
TestingWindow::Get()->endFrame(&pixels);
dumpPixelsAsPng(rivName, windowWidth, windowHeight, std::move(pixels));
if (s_args.interactive())
{
// Wait for any key if in interactive mode.
TestingWindow::InputEventData inputEventData =
TestingWindow::Get()->waitForInputEvent();
// Anything that isn't a key press will not progress.
while (inputEventData.eventType !=
TestingWindow::InputEvent::KeyPress)
{
inputEventData = TestingWindow::Get()->waitForInputEvent();
}
}
#if defined(RIVE_ANDROID) && !defined(RIVE_UNREAL)
if (!rive_android_app_poll_once())
{
return false;
}
#endif
#ifdef __EMSCRIPTEN__
// Yield control back to the browser so it can process its event loop.
emscripten_sleep(1);
#endif
}
catch (const char* msg)
{
fprintf(stderr, "%s: error: %s\n", rivName, msg);
abort();
}
catch (const std::exception& e)
{
fprintf(stderr, "%s: error: %s\n", rivName, e.what());
abort();
}
catch (...)
{
fprintf(stderr, "error rendering %s\n", rivName);
abort();
}
return true;
}
static bool process_single_golden_file(const std::string file, int cellSize)
{
std::ifstream stream(file, std::ios::binary);
if (!stream.good())
{
throw "Bad file";
}
std::vector<uint8_t> bytes(std::istreambuf_iterator<char>(stream), {});
#if defined(WITH_RIVE_SCRIPTING) && defined(RIVE_CANVAS)
if (const char* n = goldens_getenv("RIVE_GOLDENS_BENCH"))
{
int iters = atoi(n);
if (iters <= 0)
iters = 200;
run_benchmark(bytes,
s_args.artboard().c_str(),
s_args.stateMachine().c_str(),
iters);
return true;
}
#endif
bool ok;
{
RIVLoader riv(bytes,
s_args.artboard().c_str(),
s_args.stateMachine().c_str());
ok = render_and_dump_png(cellSize,
file.c_str(),
riv.stateMachine(),
riv.artboard(),
riv.deferredSession());
}
// Between-GM cleanup can tear the device down, so the loader and its
// recorded resources must already be gone.
TestingWindow::Get()->onceAfterGM();
return ok;
}
static bool is_riv_file(const std::filesystem::path& file)
{
return strcmp(file.extension().string().c_str(), ".riv") == 0;
}
#if defined(RIVE_UNREAL) || defined(EXTERN_TOOLS)
int goldens_main(int argc, const char* argv[])
#elif defined(RIVE_IOS) || defined(RIVE_IOS_SIMULATOR)
int goldens_ios_main(int argc, const char* argv[])
#elif defined(RIVE_ANDROID)
int rive_android_main(int argc, const char* const* argv)
#elif defined(__EMSCRIPTEN__)
int rive_wasm_main(int argc, const char* const* argv)
#else
int main(int argc, const char* argv[])
#endif
{
#ifdef _WIN32
// Cause stdout and stderr to print immediately without buffering.
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
#endif
try
{
s_args.parse(argc, argv);
TestingWindow::BackendParams backendParams;
auto backend =
s_args.backend().empty()
? TestingWindow::Backend::gl
: TestingWindow::ParseBackend(s_args.backend().c_str(),
&backendParams);
// For determinism, default to always using synchronously-compiled
// shaders
backendParams.shaderCompilationMode =
s_args.onlyUbershaders()
? rive::gpu::ShaderCompilationMode::onlyUbershaders
: rive::gpu::ShaderCompilationMode::alwaysSynchronous;
auto visibility = s_args.headless()
? TestingWindow::Visibility::headless
: TestingWindow::Visibility::window;
void* platformWindow = nullptr;
#if defined(RIVE_ANDROID) && !defined(RIVE_UNREAL)
platformWindow = rive_android_app_wait_for_window();
if (platformWindow != nullptr)
{
visibility = TestingWindow::Visibility::fullscreen;
}
#endif
TestingWindow::Init(backend, backendParams, visibility, platformWindow);
if (!s_args.testHarness().empty())
{
TestHarness::Instance().init(
TCPClient::Connect(s_args.testHarness().c_str()),
s_args.pngThreads());
}
else
{
TestHarness::Instance().init(
std::filesystem::path(s_args.output().c_str()),
s_args.pngThreads());
}
TestHarness::Instance().setPNGCompression(
s_args.fastPNG() ? PNGCompression::fast_rle
: PNGCompression::compact);
int cellSize =
kWindowTargetSize / std::max(s_args.cols(), s_args.rows());
int windowWidth = cellSize * s_args.cols();
int windowHeight = cellSize * s_args.rows();
TestingWindow::Get()->resize(windowWidth, windowHeight);
// First check if the --src argument is a TCP server instead of a file.
if (TestHarness::Instance().hasTCPConnection())
{
// Loop until the server is done sending .rivs.
std::string rivName;
std::vector<uint8_t> rivBytes;
while (TestHarness::Instance().fetchRivFile(rivName, rivBytes))
{
{
RIVLoader riv(rivBytes,
nullptr /*default artboard*/,
nullptr /*default state machine*/);
if (!render_and_dump_png(cellSize,
rivName.c_str(),
riv.stateMachine(),
riv.artboard(),
riv.deferredSession()))
{
return 0;
}
}
// Between-GM cleanup can tear the device down, so the loader
// and its recorded resources must already be gone.
TestingWindow::Get()->onceAfterGM();
}
}
else
{
#ifndef RIVE_REMOTE_ONLY
const std::filesystem::path& srcPath =
std::filesystem::path(s_args.src().c_str());
if (is_riv_file(srcPath))
{
// Render a single .riv file.
if (!process_single_golden_file(s_args.src().c_str(), cellSize))
{
return 0;
}
}
else
{
// Try to process every riv in the src path dir
try
{
for (const std::filesystem::directory_entry& file :
std::filesystem::directory_iterator(s_args.src()))
{
const std::filesystem::path& filePath = file.path();
if (is_riv_file(filePath))
{
if (!process_single_golden_file(filePath.string(),
cellSize))
{
return 0;
}
}
}
}
catch (...)
{
// Not a directory
throw "Bad src path";
}
}
#else
throw "Remote only goldens require a connection.";
#endif
}
}
catch (const args::Completion&)
{
return 0;
}
catch (const args::Help&)
{
return 0;
}
catch (const args::ParseError&)
{
return 1;
}
catch (args::ValidationError)
{
return 1;
}
catch (const char* msg)
{
fprintf(stderr, "error: %s\n", msg);
return -1;
}
TestingWindow::Destroy(); // Exercise our PLS teardown process now that
// we're done.
TestHarness::Instance().shutdown();
#ifdef __EMSCRIPTEN__
EM_ASM(if (window && window.close) window.close(););
#endif
return 0;
}
#endif