blob: f31c7ff074464121f188336a5247c964fd7dc176 [file]
/*
* Copyright 2024 Rive
*/
// Don't compile this file as part of the "tests" project.
#ifndef TESTING
#include "player.hpp"
#include <sstream>
#include "common/test_harness.hpp"
#include "common/testing_window.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/artboard.hpp"
#include "rive/file.hpp"
#include "rive/renderer.hpp"
#include "rive/scene.hpp"
#include "rive/text/font_hb.hpp"
#include "rive/text/raw_text.hpp"
#ifdef WITH_RIVE_SCRIPTING
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/lua/scripting_vm.hpp"
#include "rive/renderer/render_context.hpp"
#endif
#ifdef RIVE_CANVAS
#include "common/testing_window_sink.hpp"
#include "rive/renderer/cmd/deferred_session.hpp"
#endif
#include "assets/roboto_flex.ttf.hpp"
#include <stdio.h>
#include <fstream>
#if defined(RIVE_ANDROID) && !defined(RIVE_UNREAL)
#include "common/rive_android_app.hpp"
#endif
#ifdef __EMSCRIPTEN__
#include "common/rive_wasm_app.hpp"
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#endif
// The FPS readout drawn over the .riv, and the resources it needs.
struct Player::FPSOverlay
{
rive::rcp<rive::Font> roboto;
rive::rcp<rive::RenderPaint> blackStroke;
rive::rcp<rive::RenderPaint> whiteFill;
std::unique_ptr<rive::RawText> text;
int frames = 0;
std::chrono::high_resolution_clock::time_point timeLastUpdate;
};
Player::Player() : m_fps(std::make_unique<FPSOverlay>()) {}
Player::~Player() = default;
void Player::shutdown()
{
#ifdef RIVE_CANVAS
if (m_session != nullptr)
{
// Session made resources unwind through the session and the resident
// tables against the live context, not at exit.
m_fps.reset();
m_scene = nullptr;
m_artboard = nullptr;
m_file = nullptr;
m_session = nullptr;
m_replayer = nullptr;
}
#endif
}
static void update_parameter(int& val, int multiplier, char key, bool seenBang)
{
if (seenBang)
val = multiplier;
else if (key >= 'a')
val += multiplier;
else
val -= multiplier;
}
void Player::keyPressed(char key)
{
if (key >= '0' && key <= '9')
{
m_keyMultiplier = m_keyMultiplier * 10 + (key - '0');
m_seenDigit = true;
return;
}
if (key == '!')
{
m_seenBang = true;
return;
}
if (!m_seenDigit)
{
m_keyMultiplier = m_seenBang ? 0 : 1;
}
switch (key)
{
case 'h':
case 'H':
update_parameter(m_copiesLeft, m_keyMultiplier, key, m_seenBang);
break;
case 'k':
case 'K':
update_parameter(m_copiesAbove, m_keyMultiplier, key, m_seenBang);
break;
case 'l':
case 'L':
update_parameter(m_copiesRight, m_keyMultiplier, key, m_seenBang);
break;
case 'j':
case 'J':
update_parameter(m_copiesBelow, m_keyMultiplier, key, m_seenBang);
break;
case 'x':
case 'X':
update_parameter(m_copiesLeft, m_keyMultiplier, key, m_seenBang);
update_parameter(m_copiesRight, m_keyMultiplier, key, m_seenBang);
break;
case 'y':
case 'Y':
update_parameter(m_copiesAbove, m_keyMultiplier, key, m_seenBang);
update_parameter(m_copiesBelow, m_keyMultiplier, key, m_seenBang);
break;
case 'r':
case 'R':
update_parameter(m_rotations90, m_keyMultiplier, key, m_seenBang);
break;
case 'z':
case 'Z':
update_parameter(m_zoomLevel, m_keyMultiplier, key, m_seenBang);
break;
case 's':
case 'S':
update_parameter(m_spacing, m_keyMultiplier, key, m_seenBang);
break;
case 'm':
m_monitorIdx += m_keyMultiplier;
break;
case 'p':
m_paintStyle = (m_paintStyle + m_keyMultiplier) % 3;
break;
case 'P':
m_paintStyle = (m_paintStyle + 3 - (m_keyMultiplier % 3)) % 3;
break;
case 'w':
m_wireframe = !m_wireframe;
break;
case 'u':
m_paused = !m_paused;
break;
case 'f':
m_forceFixedDeltaTime = !m_forceFixedDeltaTime;
break;
case 'q':
case '\x03': // ^C
m_quit = true;
break;
case '\x1b': // Esc
break;
case '`':
m_hotloadShaders = true;
break;
case '~':
if (m_inputMode == InputMode::chars)
{
m_inputMode = InputMode::consolecommands;
}
else
{
m_inputMode = InputMode::chars;
}
break;
default:
// fprintf(stderr, "invalid option: %c\n", key);
// abort();
break;
}
m_keyMultiplier = 0;
m_seenDigit = false;
m_seenBang = false;
}
bool Player::parseArgs(int argc,
const char* const argv[],
FrameRunner::LaunchOptions& options)
{
bool onlyUbershaders = false;
for (int i = 1; i < argc; ++i)
{
if (strcmp(argv[i], "--test_harness") == 0)
{
TestHarness::Instance().init(TCPClient::Connect(argv[++i]), 0);
if (!TestHarness::Instance().fetchRivFile(m_rivName,
m_pendingRivBytes))
{
fprintf(stderr, "failed to fetch a riv file.");
abort();
}
}
else if (strcmp(argv[i], "--backend") == 0 ||
strcmp(argv[i], "-b") == 0)
{
options.backend =
TestingWindow::ParseBackend(argv[++i], &options.backendParams);
}
else if (argv[i][0] == '-' &&
argv[i][1] == 'b') // "-bvk" without a space.
{
options.backend =
TestingWindow::ParseBackend(argv[i] + 2,
&options.backendParams);
}
else if (strcmp(argv[i], "--options") == 0 ||
strcmp(argv[i], "-k") == 0)
{
for (const char* k = argv[++i]; *k; ++k)
{
keyPressed(*k);
}
}
else if (strcmp(argv[i], "--only_ubershaders") == 0 ||
strcmp(argv[i], "-u") == 0)
{
onlyUbershaders = true;
}
else if (strcmp(argv[i], "--deferred") == 0)
{
m_useDeferred = true;
}
else if (argv[i][0] == '-' &&
argv[i][1] == 'k') // "-k1234asdf" without a space.
{
for (const char* k = argv[i] + 2; *k; ++k)
{
keyPressed(*k);
}
}
else if (strcmp(argv[i], "--window") == 0 || strcmp(argv[i], "-w") == 0)
{
options.visibility = TestingWindow::Visibility::window;
}
else
{
// No argument name defaults to the source riv.
if (strcmp(argv[i], "--src") == 0 || strcmp(argv[i], "-s") == 0)
{
++i;
}
m_rivName = argv[i];
std::ifstream rivStream(m_rivName, std::ios::binary);
m_pendingRivBytes =
std::vector<uint8_t>(std::istreambuf_iterator<char>(rivStream),
{});
}
}
if (onlyUbershaders)
{
options.backendParams.shaderCompilationMode =
rive::gpu::ShaderCompilationMode::onlyUbershaders;
}
return !m_pendingRivBytes.empty();
}
void Player::init()
{
init(std::move(m_rivName), std::move(m_pendingRivBytes));
}
void Player::init(std::string rivName, std::vector<uint8_t> rivBytes)
{
m_rivName = std::move(rivName);
m_factory = TestingWindow::Get()->factory();
#ifdef RIVE_CANVAS
// Importing through the DeferredSession makes the artboard's own 2D
// resources deferred objects with ids so drawInternal can record.
if (m_useDeferred)
{
if (auto* rc = TestingWindow::Get()->renderContext())
{
if (auto* ore = rc->getOreContext())
{
m_session = std::make_unique<rive::cmd::DeferredSession>(
rive::ore::ReplayCaps::from(*ore));
// Bound before import so registration scripts that reach for
// the device find it, like a real host.
m_session->bindRenderContext(rc);
m_replayer = std::make_unique<rive::cmd::DeferredReplayer>();
m_factory = m_session.get();
}
}
if (m_session == nullptr)
{
printf("--deferred unavailable on this backend, drawing "
"immediate\n");
}
}
#else
if (m_useDeferred)
{
printf("--deferred requires a RIVE_CANVAS build, drawing immediate\n");
}
#endif
m_file = rive::File::import(rivBytes, m_factory);
assert(m_file);
m_artboard = m_file->artboardDefault();
assert(m_artboard);
m_scene = m_artboard->defaultStateMachine();
if (!m_scene)
{
m_scene = m_artboard->animationAt(0);
}
assert(m_scene);
// Setup FPS.
m_fps->roboto = HBFont::Decode(assets::roboto_flex_ttf());
m_fps->blackStroke = m_factory->makeRenderPaint();
m_fps->blackStroke->color(0xff000000);
m_fps->blackStroke->style(rive::RenderPaintStyle::stroke);
m_fps->blackStroke->thickness(4);
m_fps->whiteFill = m_factory->makeRenderPaint();
m_fps->whiteFill->color(0xffffffff);
m_fps->timeLastUpdate = std::chrono::high_resolution_clock::now();
m_timestampPrevFrame = std::chrono::high_resolution_clock::now();
}
bool Player::doFrame()
{
if (m_quit || TestingWindow::Get()->shouldQuit()
#if defined(RIVE_ANDROID) && !defined(RIVE_UNREAL)
|| !rive_android_app_poll_once()
#endif
)
{
m_quit = true;
return false;
}
#ifdef __EMSCRIPTEN__
{
// Fit the canvas to the browser window size.
int windowWidth = EM_ASM_INT(return window["innerWidth"]);
int windowHeight = EM_ASM_INT(return window["innerHeight"]);
double devicePixelRatio = emscripten_get_device_pixel_ratio();
int canvasExpectedWidth = windowWidth * devicePixelRatio;
int canvasExpectedHeight = windowHeight * devicePixelRatio;
if (TestingWindow::Get()->width() != canvasExpectedWidth ||
TestingWindow::Get()->height() != canvasExpectedHeight)
{
printf("Resizing HTML canvas to %i x %i.\n",
canvasExpectedWidth,
canvasExpectedHeight);
TestingWindow::Get()->resize(canvasExpectedWidth,
canvasExpectedHeight);
emscripten_set_element_css_size("#canvas",
windowWidth,
windowHeight);
}
}
#endif
std::chrono::time_point timeNow = std::chrono::high_resolution_clock::now();
const double elapsedS =
std::chrono::duration_cast<std::chrono::nanoseconds>(
timeNow - m_timestampPrevFrame)
.count() /
1e9; // convert to s
m_timestampPrevFrame = timeNow;
float advanceDeltaTime = static_cast<float>(elapsedS);
if (m_forceFixedDeltaTime)
{
advanceDeltaTime = 1.0f / 120;
}
m_scene->advanceAndApply(m_paused ? 0 : advanceDeltaTime);
m_copiesLeft = std::max(m_copiesLeft, 0);
m_copiesAbove = std::max(m_copiesAbove, 0);
m_copiesRight = std::max(m_copiesRight, 0);
m_copiesBelow = std::max(m_copiesBelow, 0);
int copyCount = (m_copiesLeft + 1 + m_copiesRight) *
(m_copiesAbove + 1 + m_copiesBelow);
if (copyCount != m_lastReportedCopyCount ||
m_paused != m_lastReportedPauseState)
{
printf("Drawing %i copies of %s%s at %u x %u\n",
copyCount,
m_rivName.c_str(),
m_paused ? " (paused)" : "",
TestingWindow::Get()->width(),
TestingWindow::Get()->height());
m_lastReportedCopyCount = copyCount;
m_lastReportedPauseState = m_paused;
}
const TestingWindow::FrameOptions frameOptions = {
.clearColor = 0xff303030,
.doClear = true,
.wireframe = m_wireframe,
.fillsDisabled = m_paintStyle == 2,
.strokesDisabled = m_paintStyle == 1,
};
std::unique_ptr<rive::Renderer> renderer;
#ifdef RIVE_CANVAS
if (m_session != nullptr)
{
m_session->recordOreReplayMarker();
renderer = m_session->makeScreenRenderer();
}
else
#endif
{
renderer = TestingWindow::Get()->beginFrame(frameOptions);
}
if (m_hotloadShaders)
{
m_hotloadShaders = false;
#ifndef RIVE_NO_STD_SYSTEM
std::system("sh rebuild_shaders.sh /tmp/rive");
TestingWindow::Get()->hotloadShaders();
#endif
}
renderer->save();
uint32_t width = TestingWindow::Get()->width();
uint32_t height = TestingWindow::Get()->height();
for (int i = m_rotations90; (i & 3) != 0; --i)
{
renderer->transform(rive::Mat2D(0, 1, -1, 0, width, 0));
std::swap(height, width);
}
if (m_zoomLevel != 0)
{
float scale = powf(1.25f, m_zoomLevel);
renderer->translate(width / 2.f, height / 2.f);
renderer->scale(scale, scale);
renderer->translate(width / -2.f, height / -2.f);
}
// Draw the .riv.
renderer->save();
renderer->align(rive::Fit::contain,
rive::Alignment::center,
rive::AABB(0, 0, width, height),
m_artboard->bounds());
float spacingPx = m_spacing * 5 + 150;
renderer->translate(-spacingPx * m_copiesLeft, -spacingPx * m_copiesAbove);
for (int y = -m_copiesAbove; y <= m_copiesBelow; ++y)
{
renderer->save();
for (int x = -m_copiesLeft; x <= m_copiesRight; ++x)
{
m_artboard->drawInternal(renderer.get());
renderer->translate(spacingPx, 0);
}
renderer->restore();
renderer->translate(0, spacingPx);
}
renderer->restore();
if (m_fps->text != nullptr)
{
// Draw FPS.
renderer->save();
renderer->translate(0, 20);
m_fps->text->render(renderer.get(), m_fps->blackStroke);
m_fps->text->render(renderer.get(), m_fps->whiteFill);
renderer->restore();
}
renderer->restore();
#ifdef RIVE_CANVAS
if (m_session != nullptr)
{
// Snapshot replay is the same path a threaded consumer takes.
rive::cmd::DeferredFrame frame = rive::cmd::snapshotFrame(*m_session);
m_session->resetFrame();
TestingWindowFrameSink sink(frameOptions);
m_replayer->replayFrame(frame, sink);
uint32_t dropped = m_replayer->droppedDraws();
if (dropped != 0 && dropped != m_lastDroppedDraws)
{
printf("deferred replay dropped %u draws\n", dropped);
}
m_lastDroppedDraws = dropped;
}
#endif
TestingWindow::Get()->endFrame();
// Count FPS.
++m_fps->frames;
const double elapsedFPSUpdate =
std::chrono::duration_cast<std::chrono::nanoseconds>(
timeNow - m_fps->timeLastUpdate)
.count() /
1e9; // convert to s
if (elapsedFPSUpdate >= 2.0)
{
double fps = m_fps->frames / elapsedFPSUpdate;
if (m_inputMode == InputMode::chars)
{
printf("[%.3f FPS]\n", fps);
}
char fpsRawText[32];
snprintf(fpsRawText, sizeof(fpsRawText), " %.1f FPS ", fps);
m_fps->text = std::make_unique<rive::RawText>(m_factory);
m_fps->text->maxWidth(width);
#ifdef RIVE_ANDROID
m_fps->text->align(rive::TextAlign::center);
#else
m_fps->text->align(rive::TextAlign::right);
#endif
m_fps->text->sizing(rive::TextSizing::fixed);
m_fps->text->append(fpsRawText, nullptr, m_fps->roboto, 50.f);
m_fps->frames = 0;
m_fps->timeLastUpdate = timeNow;
}
const rive::Mat2D alignmentMat =
computeAlignment(rive::Fit::contain,
rive::Alignment::center,
rive::AABB(0, 0, width, height),
m_artboard->bounds());
// Consume all input events until none are left in the queue
TestingWindow::InputEventData inputEventData;
while (TestingWindow::Get()->consumeInputEvent(inputEventData))
{
const rive::Vec2D mousePosAligned =
alignmentMat.invertOrIdentity() *
rive::Vec2D(inputEventData.metadata.posX,
inputEventData.metadata.posY);
switch (inputEventData.eventType)
{
case TestingWindow::InputEvent::KeyPress:
keyPressed(inputEventData.metadata.key);
break;
case TestingWindow::InputEvent::MouseMove:
m_scene->pointerMove(mousePosAligned);
break;
case TestingWindow::InputEvent::MouseDown:
m_scene->pointerDown(mousePosAligned);
break;
case TestingWindow::InputEvent::MouseUp:
m_scene->pointerUp(mousePosAligned);
break;
}
}
std::string command;
char key;
if (m_inputMode == InputMode::consolecommands)
{
while (TestHarness::Instance().peekChar(key))
{
command += key;
}
std::istringstream iss(command);
std::string first, second;
iss >> first >> second;
if (first == "~")
{
m_inputMode = InputMode::chars;
}
else if (first == "fire")
{
if (!second.empty())
{
if (auto* trigger = m_scene->getTrigger(second.c_str()))
{
trigger->fire();
}
}
}
}
else
{
while (TestHarness::Instance().peekChar(key))
{
keyPressed(key);
}
}
return true;
}
// Unreal owns the main loop: it creates its own Player, parses the same
// arguments through Player::parseArgs(), and pumps doFrame() once per engine
// tick. Everything below (the global player, the shutdown, and main() itself)
// is only for the standalone tool.
#ifndef RIVE_UNREAL
static Player player;
static void player_shutdown()
{
printf("\nShutting down\n");
player.shutdown();
TestingWindow::Destroy(); // Exercise our PLS teardown process now
// that we're done.
TestHarness::Instance().shutdown();
#ifdef __EMSCRIPTEN__
emscripten_cancel_main_loop();
EM_ASM(if (window && window.close) window.close(););
#else
exit(0);
#endif
}
#if defined(RIVE_IOS) || defined(RIVE_IOS_SIMULATOR)
int player_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)
#elif defined(EXTERN_TOOLS)
int rive_main(int argc, const char* 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
FrameRunner::LaunchOptions options;
const bool haveRiv = player.parseArgs(argc, argv, options);
TestingWindow::Init(options.backend,
options.backendParams,
options.visibility,
#ifdef RIVE_ANDROID
rive_android_app_wait_for_window()
#else
reinterpret_cast<void*>(
static_cast<intptr_t>(player.monitorIdx()))
#endif
);
if (!haveRiv)
{
fprintf(stderr, "no .riv file specified");
abort();
}
player.init();
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(
[]() {
if (!player.doFrame())
{
player_shutdown();
}
},
0,
true);
#else
for (;;)
{
if (!player.doFrame())
{
player_shutdown();
}
}
#endif
return 0;
}
#endif // !RIVE_UNREAL
#endif